diff --git a/404.html b/404.html new file mode 100644 index 00000000..074a9b8e --- /dev/null +++ b/404.html @@ -0,0 +1,19 @@ + + + + + + 404 | Sunny's blog + + + + + + + +
Skip to content

404

PAGE NOT FOUND

But if you don't change your direction, and if you keep looking, you may end up where you are heading.
+ + + + + \ No newline at end of file diff --git "a/algorithm/\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.html" "b/algorithm/\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.html" new file mode 100644 index 00000000..e82290a2 --- /dev/null +++ "b/algorithm/\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.html" @@ -0,0 +1,94 @@ + + + + + + 🔥刷题之探索最优解 WIP | Sunny's blog + + + + + + + + +
Skip to content
On this page

🔥刷题之探索最优解 WIP

DANGER

WIP

正在创作

绳子盖住的最多点 WIP

一、题目描述: 给定一个有序数组arr 从左到右依次表示X轴上从左往右点的位置,给定一个正整数k, 返回如果有一根长度为k的绳子,最多能盖住几个点,绳子的边缘点碰到X轴上的点,也算盖住。 示例: 示例 1: 输入:arr = [1, 5, 13, 14, 15, 32, 43], k = 2 输出:3 示例 2: 输入:arr = [1, 5, 13, 14, 15, 32, 43], k = 5 输出:3

js
// 方法一:
+function rope(arr, k) {
+  let res = 1
+  for (let i = 0; i < arr.length; i++) {
+    let near = Dichotomous(arr, i, arr[i] - k) // 求数组中大于等于arr[i]-k的最小数
+    res = Math.max(res, i - near + 1)
+  }
+  return res
+}
+
+function Dichotomous(arr, r, value) {
+  let index = r
+  let l = 0
+  while (l < r) {
+    let mid = Math.floor((l + r) / 2)
+    if (arr[mid] >= value) {
+      r = mid - 1
+      index = mid
+    } else {
+      l = mid + 1
+    }
+  }
+  return index
+}
+// 方法二:
+function rope(arr, k) {
+  let l = 0,
+    r = 0,
+    n = arr.length
+  max = 0
+  while (l < n) {
+    while (r < n && arr[r] - arr[l] <= k) r++
+    max = Math.max(max, r - l++)
+  }
+  return max
+}
+
+
// 方法一:
+function rope(arr, k) {
+  let res = 1
+  for (let i = 0; i < arr.length; i++) {
+    let near = Dichotomous(arr, i, arr[i] - k) // 求数组中大于等于arr[i]-k的最小数
+    res = Math.max(res, i - near + 1)
+  }
+  return res
+}
+
+function Dichotomous(arr, r, value) {
+  let index = r
+  let l = 0
+  while (l < r) {
+    let mid = Math.floor((l + r) / 2)
+    if (arr[mid] >= value) {
+      r = mid - 1
+      index = mid
+    } else {
+      l = mid + 1
+    }
+  }
+  return index
+}
+// 方法二:
+function rope(arr, k) {
+  let l = 0,
+    r = 0,
+    n = arr.length
+  max = 0
+  while (l < n) {
+    while (r < n && arr[r] - arr[l] <= k) r++
+    max = Math.max(max, r - l++)
+  }
+  return max
+}
+
+

总结:在解决问题的基础上,再进行优化。方法一使用二分复杂度为O(nlogn),使用窗口法就可以将复杂度将为O(n)。

+ + + + + \ No newline at end of file diff --git a/article/cms.html b/article/cms.html new file mode 100644 index 00000000..dbe32a9c --- /dev/null +++ b/article/cms.html @@ -0,0 +1,20 @@ + + + + + + 一站式-后台前端解决方案调研 | Sunny's blog + + + + + + + + +
Skip to content
On this page

一站式-后台前端解决方案调研

TIP

搜索方式:github 搜名称

Hooks-Admin

react-admin

vue-element-admin

Antd Pro Vue - 背靠阿里,代码过硬,大型项目首选

Vue vben admin - 宝藏后台管理 基于 Vue3 UI清新 功能扎实

Naive Ui admin 适合小项目

vue3-antd-admin

gin-vue-admin

vue-pure-admin

vue3-composition-admin

vue-admin-perfect

gin-vue-admin

vue-vben-admin

Geeker-Admin

soybean-admin

vue-admin-box

vue-next-admin

vue-admin-better

v3-admin-vite

vue-manage-system

vue3-admin-plus

https://github.com/RainManGO/vue3-composition-admin

https://github.com/jzfai/vue3-admin-plus

https://github.com/tobe-fe-dalao/fast-vue3

https://github.com/ibwei/vue3-ts-base

https://github.com/jzfai/vue3-admin-ts

https://github.com/zouzhibin/vue-admin-perfect

+ + + + + \ No newline at end of file diff --git a/assets/2023-01-06-12-39-32.0dc2e5ed.png b/assets/2023-01-06-12-39-32.0dc2e5ed.png new file mode 100644 index 00000000..159aa4d6 Binary files /dev/null and b/assets/2023-01-06-12-39-32.0dc2e5ed.png differ diff --git a/assets/2023-01-06-12-40-13.74f836b6.png b/assets/2023-01-06-12-40-13.74f836b6.png new file mode 100644 index 00000000..99834853 Binary files /dev/null and b/assets/2023-01-06-12-40-13.74f836b6.png differ diff --git a/assets/2023-01-06-12-41-12.a8997093.png b/assets/2023-01-06-12-41-12.a8997093.png new file mode 100644 index 00000000..ac924d9c Binary files /dev/null and b/assets/2023-01-06-12-41-12.a8997093.png differ diff --git a/assets/2023-01-06-14-09-28.600f4f2f.png b/assets/2023-01-06-14-09-28.600f4f2f.png new file mode 100644 index 00000000..466d668d Binary files /dev/null and b/assets/2023-01-06-14-09-28.600f4f2f.png differ diff --git a/assets/2023-01-06-14-09-39.55ff2a8d.png b/assets/2023-01-06-14-09-39.55ff2a8d.png new file mode 100644 index 00000000..c873e987 Binary files /dev/null and b/assets/2023-01-06-14-09-39.55ff2a8d.png differ diff --git a/assets/2023-01-06-14-09-53.922b64be.png b/assets/2023-01-06-14-09-53.922b64be.png new file mode 100644 index 00000000..a1534fb8 Binary files /dev/null and b/assets/2023-01-06-14-09-53.922b64be.png differ diff --git a/assets/2023-01-06-14-10-02.287bc62f.png b/assets/2023-01-06-14-10-02.287bc62f.png new file mode 100644 index 00000000..09425f53 Binary files /dev/null and b/assets/2023-01-06-14-10-02.287bc62f.png differ diff --git a/assets/2023-01-06-16-01-47.0a0026be.png b/assets/2023-01-06-16-01-47.0a0026be.png new file mode 100644 index 00000000..f7552083 Binary files /dev/null and b/assets/2023-01-06-16-01-47.0a0026be.png differ diff --git a/assets/2023-01-06-16-14-44.9c23195b.png b/assets/2023-01-06-16-14-44.9c23195b.png new file mode 100644 index 00000000..5e105ed1 Binary files /dev/null and b/assets/2023-01-06-16-14-44.9c23195b.png differ diff --git a/assets/2023-01-06-16-16-45.8cacfb52.png b/assets/2023-01-06-16-16-45.8cacfb52.png new file mode 100644 index 00000000..1edb0006 Binary files /dev/null and b/assets/2023-01-06-16-16-45.8cacfb52.png differ diff --git a/assets/2023-01-06-16-16-57.792b0ec0.png b/assets/2023-01-06-16-16-57.792b0ec0.png new file mode 100644 index 00000000..27609fb3 Binary files /dev/null and b/assets/2023-01-06-16-16-57.792b0ec0.png differ diff --git a/assets/2023-01-06-16-17-35.bce22d59.png b/assets/2023-01-06-16-17-35.bce22d59.png new file mode 100644 index 00000000..fca0ec7c Binary files /dev/null and b/assets/2023-01-06-16-17-35.bce22d59.png differ diff --git a/assets/2023-01-06-16-23-08.16c5b4ae.png b/assets/2023-01-06-16-23-08.16c5b4ae.png new file mode 100644 index 00000000..75a873ac Binary files /dev/null and b/assets/2023-01-06-16-23-08.16c5b4ae.png differ diff --git a/assets/2023-01-06-16-23-16.12a8f778.png b/assets/2023-01-06-16-23-16.12a8f778.png new file mode 100644 index 00000000..3b4c2ed7 Binary files /dev/null and b/assets/2023-01-06-16-23-16.12a8f778.png differ diff --git a/assets/2023-01-06-16-37-27.347ac1c6.png b/assets/2023-01-06-16-37-27.347ac1c6.png new file mode 100644 index 00000000..bd1b16cf Binary files /dev/null and b/assets/2023-01-06-16-37-27.347ac1c6.png differ diff --git a/assets/2023-01-06-16-37-43.6a6a7a3c.png b/assets/2023-01-06-16-37-43.6a6a7a3c.png new file mode 100644 index 00000000..5d31f174 Binary files /dev/null and b/assets/2023-01-06-16-37-43.6a6a7a3c.png differ diff --git a/assets/2023-01-06-16-37-56.802983a9.png b/assets/2023-01-06-16-37-56.802983a9.png new file mode 100644 index 00000000..5b005692 Binary files /dev/null and b/assets/2023-01-06-16-37-56.802983a9.png differ diff --git a/assets/2023-01-06-16-38-33.30911d0e.png b/assets/2023-01-06-16-38-33.30911d0e.png new file mode 100644 index 00000000..a518868c Binary files /dev/null and b/assets/2023-01-06-16-38-33.30911d0e.png differ diff --git a/assets/2023-01-06-16-46-05.c830893b.png b/assets/2023-01-06-16-46-05.c830893b.png new file mode 100644 index 00000000..0a6ea9ef Binary files /dev/null and b/assets/2023-01-06-16-46-05.c830893b.png differ diff --git a/assets/2023-01-06-16-46-13.ea302e87.png b/assets/2023-01-06-16-46-13.ea302e87.png new file mode 100644 index 00000000..9e8d957b Binary files /dev/null and b/assets/2023-01-06-16-46-13.ea302e87.png differ diff --git a/assets/2023-01-06-16-49-52.3f0e9aff.png b/assets/2023-01-06-16-49-52.3f0e9aff.png new file mode 100644 index 00000000..bcea6851 Binary files /dev/null and b/assets/2023-01-06-16-49-52.3f0e9aff.png differ diff --git a/assets/2023-01-06-16-50-09.1857bf9e.png b/assets/2023-01-06-16-50-09.1857bf9e.png new file mode 100644 index 00000000..b361fd95 Binary files /dev/null and b/assets/2023-01-06-16-50-09.1857bf9e.png differ diff --git a/assets/2023-01-06-16-57-09.02129cc3.png b/assets/2023-01-06-16-57-09.02129cc3.png new file mode 100644 index 00000000..d20e9d95 Binary files /dev/null and b/assets/2023-01-06-16-57-09.02129cc3.png differ diff --git a/assets/2023-01-06-16-58-00.54f4410f.png b/assets/2023-01-06-16-58-00.54f4410f.png new file mode 100644 index 00000000..6a59f1bc Binary files /dev/null and b/assets/2023-01-06-16-58-00.54f4410f.png differ diff --git a/assets/2023-01-06-18-09-43.38e75eb4.png b/assets/2023-01-06-18-09-43.38e75eb4.png new file mode 100644 index 00000000..41464d22 Binary files /dev/null and b/assets/2023-01-06-18-09-43.38e75eb4.png differ diff --git a/assets/2023-02-01-13-16-09.4f2b336d.png b/assets/2023-02-01-13-16-09.4f2b336d.png new file mode 100644 index 00000000..4e5aef02 Binary files /dev/null and b/assets/2023-02-01-13-16-09.4f2b336d.png differ diff --git a/assets/2023-02-01-13-18-13.395fc83e.png b/assets/2023-02-01-13-18-13.395fc83e.png new file mode 100644 index 00000000..b025700e Binary files /dev/null and b/assets/2023-02-01-13-18-13.395fc83e.png differ diff --git a/assets/2023-02-01-13-21-47.813c0eeb.png b/assets/2023-02-01-13-21-47.813c0eeb.png new file mode 100644 index 00000000..b5071d9c Binary files /dev/null and b/assets/2023-02-01-13-21-47.813c0eeb.png differ diff --git a/assets/2023-02-01-13-27-42.d1eeb527.png b/assets/2023-02-01-13-27-42.d1eeb527.png new file mode 100644 index 00000000..803c8bf8 Binary files /dev/null and b/assets/2023-02-01-13-27-42.d1eeb527.png differ diff --git a/assets/2023-02-01-13-46-04.6c717bd8.png b/assets/2023-02-01-13-46-04.6c717bd8.png new file mode 100644 index 00000000..d31de935 Binary files /dev/null and b/assets/2023-02-01-13-46-04.6c717bd8.png differ diff --git a/assets/2023-02-01-13-48-01.99bd7f6c.png b/assets/2023-02-01-13-48-01.99bd7f6c.png new file mode 100644 index 00000000..8137d513 Binary files /dev/null and b/assets/2023-02-01-13-48-01.99bd7f6c.png differ diff --git a/assets/2023-02-01-13-49-33.2c20338b.png b/assets/2023-02-01-13-49-33.2c20338b.png new file mode 100644 index 00000000..1873a55d Binary files /dev/null and b/assets/2023-02-01-13-49-33.2c20338b.png differ diff --git a/assets/2023-02-01-13-52-47.6a1c0330.png b/assets/2023-02-01-13-52-47.6a1c0330.png new file mode 100644 index 00000000..6603a700 Binary files /dev/null and b/assets/2023-02-01-13-52-47.6a1c0330.png differ diff --git a/assets/2023-02-01-13-55-15.078d7618.png b/assets/2023-02-01-13-55-15.078d7618.png new file mode 100644 index 00000000..5fd77c39 Binary files /dev/null and b/assets/2023-02-01-13-55-15.078d7618.png differ diff --git a/assets/2023-02-01-13-56-31.fb7de130.png b/assets/2023-02-01-13-56-31.fb7de130.png new file mode 100644 index 00000000..51e6805d Binary files /dev/null and b/assets/2023-02-01-13-56-31.fb7de130.png differ diff --git a/assets/2023-02-01-13-58-47.a4395511.png b/assets/2023-02-01-13-58-47.a4395511.png new file mode 100644 index 00000000..93af0274 Binary files /dev/null and b/assets/2023-02-01-13-58-47.a4395511.png differ diff --git a/assets/2023-02-01-14-07-17.1c3d137e.png b/assets/2023-02-01-14-07-17.1c3d137e.png new file mode 100644 index 00000000..910b25d9 Binary files /dev/null and b/assets/2023-02-01-14-07-17.1c3d137e.png differ diff --git a/assets/2023-02-01-14-11-21.6ba7da11.png b/assets/2023-02-01-14-11-21.6ba7da11.png new file mode 100644 index 00000000..86832efd Binary files /dev/null and b/assets/2023-02-01-14-11-21.6ba7da11.png differ diff --git a/assets/2023-02-01-14-11-32.f9856356.png b/assets/2023-02-01-14-11-32.f9856356.png new file mode 100644 index 00000000..1e3706b9 Binary files /dev/null and b/assets/2023-02-01-14-11-32.f9856356.png differ diff --git a/assets/2023-02-01-14-11-46.6b67dfa8.png b/assets/2023-02-01-14-11-46.6b67dfa8.png new file mode 100644 index 00000000..10336e41 Binary files /dev/null and b/assets/2023-02-01-14-11-46.6b67dfa8.png differ diff --git a/assets/2023-02-01-14-22-04.bc84f192.png b/assets/2023-02-01-14-22-04.bc84f192.png new file mode 100644 index 00000000..228de123 Binary files /dev/null and b/assets/2023-02-01-14-22-04.bc84f192.png differ diff --git a/assets/2023-02-01-14-26-02.5a9b1719.png b/assets/2023-02-01-14-26-02.5a9b1719.png new file mode 100644 index 00000000..779cec88 Binary files /dev/null and b/assets/2023-02-01-14-26-02.5a9b1719.png differ diff --git a/assets/2023-02-01-14-30-58.4d89673b.png b/assets/2023-02-01-14-30-58.4d89673b.png new file mode 100644 index 00000000..4c42d40b Binary files /dev/null and b/assets/2023-02-01-14-30-58.4d89673b.png differ diff --git a/assets/2023-02-01-15-24-12.aec7b73c.png b/assets/2023-02-01-15-24-12.aec7b73c.png new file mode 100644 index 00000000..9a02308e Binary files /dev/null and b/assets/2023-02-01-15-24-12.aec7b73c.png differ diff --git a/assets/2023-02-01-15-24-21.dd217b1c.png b/assets/2023-02-01-15-24-21.dd217b1c.png new file mode 100644 index 00000000..e206adaf Binary files /dev/null and b/assets/2023-02-01-15-24-21.dd217b1c.png differ diff --git a/assets/2023-02-01-15-24-49.74e4a9b8.png b/assets/2023-02-01-15-24-49.74e4a9b8.png new file mode 100644 index 00000000..c38a046f Binary files /dev/null and b/assets/2023-02-01-15-24-49.74e4a9b8.png differ diff --git a/assets/2023-02-01-15-25-03.96124e39.png b/assets/2023-02-01-15-25-03.96124e39.png new file mode 100644 index 00000000..67b5965d Binary files /dev/null and b/assets/2023-02-01-15-25-03.96124e39.png differ diff --git a/assets/2023-02-01-15-25-57.438f544b.png b/assets/2023-02-01-15-25-57.438f544b.png new file mode 100644 index 00000000..a42edb6c Binary files /dev/null and b/assets/2023-02-01-15-25-57.438f544b.png differ diff --git a/assets/2023-02-01-15-26-20.04aff82f.png b/assets/2023-02-01-15-26-20.04aff82f.png new file mode 100644 index 00000000..1f66243b Binary files /dev/null and b/assets/2023-02-01-15-26-20.04aff82f.png differ diff --git a/assets/2023-02-01-15-26-32.a4ca8c1d.png b/assets/2023-02-01-15-26-32.a4ca8c1d.png new file mode 100644 index 00000000..be3b8c08 Binary files /dev/null and b/assets/2023-02-01-15-26-32.a4ca8c1d.png differ diff --git a/assets/2023-02-01-15-26-38.bd90e636.png b/assets/2023-02-01-15-26-38.bd90e636.png new file mode 100644 index 00000000..c3471085 Binary files /dev/null and b/assets/2023-02-01-15-26-38.bd90e636.png differ diff --git a/assets/2023-02-01-15-26-45.869e5c75.png b/assets/2023-02-01-15-26-45.869e5c75.png new file mode 100644 index 00000000..4a61a21f Binary files /dev/null and b/assets/2023-02-01-15-26-45.869e5c75.png differ diff --git a/assets/2023-02-01-15-27-05.a95f9201.png b/assets/2023-02-01-15-27-05.a95f9201.png new file mode 100644 index 00000000..b71ec3ad Binary files /dev/null and b/assets/2023-02-01-15-27-05.a95f9201.png differ diff --git a/assets/2023-02-01-19-09-45.7bd1e85b.png b/assets/2023-02-01-19-09-45.7bd1e85b.png new file mode 100644 index 00000000..5343741f Binary files /dev/null and b/assets/2023-02-01-19-09-45.7bd1e85b.png differ diff --git a/assets/2023-02-01-19-09-56.61a61a1b.png b/assets/2023-02-01-19-09-56.61a61a1b.png new file mode 100644 index 00000000..90ece6cf Binary files /dev/null and b/assets/2023-02-01-19-09-56.61a61a1b.png differ diff --git a/assets/2023-02-01-19-21-07.2d817c9c.png b/assets/2023-02-01-19-21-07.2d817c9c.png new file mode 100644 index 00000000..c51f78a8 Binary files /dev/null and b/assets/2023-02-01-19-21-07.2d817c9c.png differ diff --git a/assets/2023-02-01-19-21-23.d2a9dd80.png b/assets/2023-02-01-19-21-23.d2a9dd80.png new file mode 100644 index 00000000..52cfb057 Binary files /dev/null and b/assets/2023-02-01-19-21-23.d2a9dd80.png differ diff --git a/assets/2023-02-01-19-21-38.5391ed6b.png b/assets/2023-02-01-19-21-38.5391ed6b.png new file mode 100644 index 00000000..f040ae74 Binary files /dev/null and b/assets/2023-02-01-19-21-38.5391ed6b.png differ diff --git a/assets/2023-02-01-19-21-46.6a3ccad0.png b/assets/2023-02-01-19-21-46.6a3ccad0.png new file mode 100644 index 00000000..532ca8c9 Binary files /dev/null and b/assets/2023-02-01-19-21-46.6a3ccad0.png differ diff --git a/assets/2023-02-01-19-22-45.7a3d267b.png b/assets/2023-02-01-19-22-45.7a3d267b.png new file mode 100644 index 00000000..80c1e236 Binary files /dev/null and b/assets/2023-02-01-19-22-45.7a3d267b.png differ diff --git a/assets/2023-02-01-19-24-55.d7bd1da3.png b/assets/2023-02-01-19-24-55.d7bd1da3.png new file mode 100644 index 00000000..4abb8615 Binary files /dev/null and b/assets/2023-02-01-19-24-55.d7bd1da3.png differ diff --git a/assets/2023-02-01-19-25-37.a7c41d3a.png b/assets/2023-02-01-19-25-37.a7c41d3a.png new file mode 100644 index 00000000..0fb70ee6 Binary files /dev/null and b/assets/2023-02-01-19-25-37.a7c41d3a.png differ diff --git a/assets/2023-02-01-19-25-56.cdab3337.png b/assets/2023-02-01-19-25-56.cdab3337.png new file mode 100644 index 00000000..f5df2ce2 Binary files /dev/null and b/assets/2023-02-01-19-25-56.cdab3337.png differ diff --git a/assets/2023-02-01-19-31-36.cc16c9b9.png b/assets/2023-02-01-19-31-36.cc16c9b9.png new file mode 100644 index 00000000..f0c18557 Binary files /dev/null and b/assets/2023-02-01-19-31-36.cc16c9b9.png differ diff --git a/assets/2023-02-01-19-31-43.f7becb4c.png b/assets/2023-02-01-19-31-43.f7becb4c.png new file mode 100644 index 00000000..3ad5eebb Binary files /dev/null and b/assets/2023-02-01-19-31-43.f7becb4c.png differ diff --git a/assets/2023-02-01-19-31-50.42c02105.png b/assets/2023-02-01-19-31-50.42c02105.png new file mode 100644 index 00000000..71a5ab13 Binary files /dev/null and b/assets/2023-02-01-19-31-50.42c02105.png differ diff --git a/assets/2023-02-01-19-32-00.0bd5762e.png b/assets/2023-02-01-19-32-00.0bd5762e.png new file mode 100644 index 00000000..17f44329 Binary files /dev/null and b/assets/2023-02-01-19-32-00.0bd5762e.png differ diff --git a/assets/2023-02-01-19-32-06.107c8299.png b/assets/2023-02-01-19-32-06.107c8299.png new file mode 100644 index 00000000..d8a8546f Binary files /dev/null and b/assets/2023-02-01-19-32-06.107c8299.png differ diff --git a/assets/2023-02-01-20-57-45.daee2fcd.png b/assets/2023-02-01-20-57-45.daee2fcd.png new file mode 100644 index 00000000..42b7e1f3 Binary files /dev/null and b/assets/2023-02-01-20-57-45.daee2fcd.png differ diff --git a/assets/2023-02-01-21-02-59.47222bd8.png b/assets/2023-02-01-21-02-59.47222bd8.png new file mode 100644 index 00000000..af4d1cbf Binary files /dev/null and b/assets/2023-02-01-21-02-59.47222bd8.png differ diff --git a/assets/2023-02-01-21-04-28.4121323f.png b/assets/2023-02-01-21-04-28.4121323f.png new file mode 100644 index 00000000..9bc180ad Binary files /dev/null and b/assets/2023-02-01-21-04-28.4121323f.png differ diff --git a/assets/2023-02-01-21-04-38.6dfd96aa.png b/assets/2023-02-01-21-04-38.6dfd96aa.png new file mode 100644 index 00000000..04ba1070 Binary files /dev/null and b/assets/2023-02-01-21-04-38.6dfd96aa.png differ diff --git a/assets/2023-02-01-21-38-25.04ce3663.png b/assets/2023-02-01-21-38-25.04ce3663.png new file mode 100644 index 00000000..e335d32f Binary files /dev/null and b/assets/2023-02-01-21-38-25.04ce3663.png differ diff --git a/assets/2023-02-01-21-59-20.1fa5b4ed.png b/assets/2023-02-01-21-59-20.1fa5b4ed.png new file mode 100644 index 00000000..7f9ed7d5 Binary files /dev/null and b/assets/2023-02-01-21-59-20.1fa5b4ed.png differ diff --git a/assets/2023-02-01-21-59-43.f3f27dad.png b/assets/2023-02-01-21-59-43.f3f27dad.png new file mode 100644 index 00000000..fa2a660f Binary files /dev/null and b/assets/2023-02-01-21-59-43.f3f27dad.png differ diff --git a/assets/2023-02-01-22-00-22.5f64024d.png b/assets/2023-02-01-22-00-22.5f64024d.png new file mode 100644 index 00000000..f8731f1d Binary files /dev/null and b/assets/2023-02-01-22-00-22.5f64024d.png differ diff --git a/assets/2023-02-01-22-00-43.9fbe4838.png b/assets/2023-02-01-22-00-43.9fbe4838.png new file mode 100644 index 00000000..81385dce Binary files /dev/null and b/assets/2023-02-01-22-00-43.9fbe4838.png differ diff --git a/assets/2023-02-01-22-01-01.8aaf55c5.png b/assets/2023-02-01-22-01-01.8aaf55c5.png new file mode 100644 index 00000000..3638af0c Binary files /dev/null and b/assets/2023-02-01-22-01-01.8aaf55c5.png differ diff --git a/assets/2023-02-01-22-01-19.d349110a.png b/assets/2023-02-01-22-01-19.d349110a.png new file mode 100644 index 00000000..67abd835 Binary files /dev/null and b/assets/2023-02-01-22-01-19.d349110a.png differ diff --git a/assets/2023-02-01-22-03-34.729a63f6.png b/assets/2023-02-01-22-03-34.729a63f6.png new file mode 100644 index 00000000..b86fc088 Binary files /dev/null and b/assets/2023-02-01-22-03-34.729a63f6.png differ diff --git a/assets/2023-02-01-22-03-43.d520fd1e.png b/assets/2023-02-01-22-03-43.d520fd1e.png new file mode 100644 index 00000000..699aebe2 Binary files /dev/null and b/assets/2023-02-01-22-03-43.d520fd1e.png differ diff --git a/assets/2023-02-01-22-03-59.a0646341.png b/assets/2023-02-01-22-03-59.a0646341.png new file mode 100644 index 00000000..1a9f9032 Binary files /dev/null and b/assets/2023-02-01-22-03-59.a0646341.png differ diff --git a/assets/2023-02-01-22-04-06.095f01dd.png b/assets/2023-02-01-22-04-06.095f01dd.png new file mode 100644 index 00000000..97a4aadc Binary files /dev/null and b/assets/2023-02-01-22-04-06.095f01dd.png differ diff --git a/assets/2023-02-01-22-05-00.cd9ee97d.png b/assets/2023-02-01-22-05-00.cd9ee97d.png new file mode 100644 index 00000000..b20867dd Binary files /dev/null and b/assets/2023-02-01-22-05-00.cd9ee97d.png differ diff --git a/assets/2023-02-07-11-12-59.627e3f2a.png b/assets/2023-02-07-11-12-59.627e3f2a.png new file mode 100644 index 00000000..2c77fc8b Binary files /dev/null and b/assets/2023-02-07-11-12-59.627e3f2a.png differ diff --git a/assets/2023-02-07-11-14-40.d5296b77.png b/assets/2023-02-07-11-14-40.d5296b77.png new file mode 100644 index 00000000..85014e02 Binary files /dev/null and b/assets/2023-02-07-11-14-40.d5296b77.png differ diff --git a/assets/2023-02-07-11-14-45.5ba96cb7.png b/assets/2023-02-07-11-14-45.5ba96cb7.png new file mode 100644 index 00000000..2f3faa02 Binary files /dev/null and b/assets/2023-02-07-11-14-45.5ba96cb7.png differ diff --git a/assets/2023-02-07-11-14-51.cdbc7b84.png b/assets/2023-02-07-11-14-51.cdbc7b84.png new file mode 100644 index 00000000..8b1e7f17 Binary files /dev/null and b/assets/2023-02-07-11-14-51.cdbc7b84.png differ diff --git a/assets/2023-02-07-11-15-52.a8de8640.png b/assets/2023-02-07-11-15-52.a8de8640.png new file mode 100644 index 00000000..f54acde6 Binary files /dev/null and b/assets/2023-02-07-11-15-52.a8de8640.png differ diff --git a/assets/2023-02-07-11-16-05.3dde8b6a.png b/assets/2023-02-07-11-16-05.3dde8b6a.png new file mode 100644 index 00000000..b597ae96 Binary files /dev/null and b/assets/2023-02-07-11-16-05.3dde8b6a.png differ diff --git a/assets/2023-02-07-11-16-13.41917cf5.png b/assets/2023-02-07-11-16-13.41917cf5.png new file mode 100644 index 00000000..011d42a6 Binary files /dev/null and b/assets/2023-02-07-11-16-13.41917cf5.png differ diff --git a/assets/2023-02-07-11-16-20.65378089.png b/assets/2023-02-07-11-16-20.65378089.png new file mode 100644 index 00000000..ba7de096 Binary files /dev/null and b/assets/2023-02-07-11-16-20.65378089.png differ diff --git a/assets/2023-02-07-11-17-25.05d811a7.png b/assets/2023-02-07-11-17-25.05d811a7.png new file mode 100644 index 00000000..9497cb54 Binary files /dev/null and b/assets/2023-02-07-11-17-25.05d811a7.png differ diff --git a/assets/2023-02-07-11-18-39.1ae1946c.png b/assets/2023-02-07-11-18-39.1ae1946c.png new file mode 100644 index 00000000..5244aefc Binary files /dev/null and b/assets/2023-02-07-11-18-39.1ae1946c.png differ diff --git a/assets/2023-02-07-11-20-12.e6d25932.png b/assets/2023-02-07-11-20-12.e6d25932.png new file mode 100644 index 00000000..c1388bb9 Binary files /dev/null and b/assets/2023-02-07-11-20-12.e6d25932.png differ diff --git a/assets/2023-02-07-11-20-27.0df215a5.png b/assets/2023-02-07-11-20-27.0df215a5.png new file mode 100644 index 00000000..c206019e Binary files /dev/null and b/assets/2023-02-07-11-20-27.0df215a5.png differ diff --git a/assets/2023-02-07-11-20-35.cbe349d2.png b/assets/2023-02-07-11-20-35.cbe349d2.png new file mode 100644 index 00000000..7c1ecec4 Binary files /dev/null and b/assets/2023-02-07-11-20-35.cbe349d2.png differ diff --git a/assets/2023-02-07-11-21-01.23a66ab1.png b/assets/2023-02-07-11-21-01.23a66ab1.png new file mode 100644 index 00000000..3f7f8ec4 Binary files /dev/null and b/assets/2023-02-07-11-21-01.23a66ab1.png differ diff --git a/assets/2023-02-07-11-21-11.5f2e52fb.png b/assets/2023-02-07-11-21-11.5f2e52fb.png new file mode 100644 index 00000000..84031a7e Binary files /dev/null and b/assets/2023-02-07-11-21-11.5f2e52fb.png differ diff --git a/assets/2024-01-27-11-37-14.86d37b7f.png b/assets/2024-01-27-11-37-14.86d37b7f.png new file mode 100644 index 00000000..b04b990b Binary files /dev/null and b/assets/2024-01-27-11-37-14.86d37b7f.png differ diff --git a/assets/2024-01-27-11-42-53.5bf914cc.png b/assets/2024-01-27-11-42-53.5bf914cc.png new file mode 100644 index 00000000..15262500 Binary files /dev/null and b/assets/2024-01-27-11-42-53.5bf914cc.png differ diff --git a/assets/2024-01-27-11-43-39.3f42a6fd.png b/assets/2024-01-27-11-43-39.3f42a6fd.png new file mode 100644 index 00000000..74943299 Binary files /dev/null and b/assets/2024-01-27-11-43-39.3f42a6fd.png differ diff --git a/assets/2024-01-27-11-43-48.dc786c83.png b/assets/2024-01-27-11-43-48.dc786c83.png new file mode 100644 index 00000000..db18322f Binary files /dev/null and b/assets/2024-01-27-11-43-48.dc786c83.png differ diff --git a/assets/2024-01-27-11-45-28.86477da9.png b/assets/2024-01-27-11-45-28.86477da9.png new file mode 100644 index 00000000..3d0ea180 Binary files /dev/null and b/assets/2024-01-27-11-45-28.86477da9.png differ diff --git a/assets/2024-01-28-16-38-33.db813e08.png b/assets/2024-01-28-16-38-33.db813e08.png new file mode 100644 index 00000000..89f9175a Binary files /dev/null and b/assets/2024-01-28-16-38-33.db813e08.png differ diff --git a/assets/2024-01-28-16-43-50.943cbfac.png b/assets/2024-01-28-16-43-50.943cbfac.png new file mode 100644 index 00000000..918339bb Binary files /dev/null and b/assets/2024-01-28-16-43-50.943cbfac.png differ diff --git a/assets/2024-01-28-16-45-11.6c1597c5.png b/assets/2024-01-28-16-45-11.6c1597c5.png new file mode 100644 index 00000000..38bfcf27 Binary files /dev/null and b/assets/2024-01-28-16-45-11.6c1597c5.png differ diff --git a/assets/2024-01-28-16-52-03.61de8fe4.png b/assets/2024-01-28-16-52-03.61de8fe4.png new file mode 100644 index 00000000..2547d6db Binary files /dev/null and b/assets/2024-01-28-16-52-03.61de8fe4.png differ diff --git a/assets/2024-01-28-16-52-40.1ee5db60.png b/assets/2024-01-28-16-52-40.1ee5db60.png new file mode 100644 index 00000000..899c7f20 Binary files /dev/null and b/assets/2024-01-28-16-52-40.1ee5db60.png differ diff --git a/assets/2024-01-28-17-09-16.389ef971.png b/assets/2024-01-28-17-09-16.389ef971.png new file mode 100644 index 00000000..315cfea0 Binary files /dev/null and b/assets/2024-01-28-17-09-16.389ef971.png differ diff --git a/assets/2024-01-28-17-09-22.8a8ef8fe.png b/assets/2024-01-28-17-09-22.8a8ef8fe.png new file mode 100644 index 00000000..ed16f560 Binary files /dev/null and b/assets/2024-01-28-17-09-22.8a8ef8fe.png differ diff --git a/assets/2024-01-28-17-18-12.4f54b024.png b/assets/2024-01-28-17-18-12.4f54b024.png new file mode 100644 index 00000000..3f05783d Binary files /dev/null and b/assets/2024-01-28-17-18-12.4f54b024.png differ diff --git a/assets/2024-01-28-17-28-28.709acd8d.png b/assets/2024-01-28-17-28-28.709acd8d.png new file mode 100644 index 00000000..c416a2d4 Binary files /dev/null and b/assets/2024-01-28-17-28-28.709acd8d.png differ diff --git a/assets/2024-01-28-17-29-13.fbcf0c26.png b/assets/2024-01-28-17-29-13.fbcf0c26.png new file mode 100644 index 00000000..99396040 Binary files /dev/null and b/assets/2024-01-28-17-29-13.fbcf0c26.png differ diff --git a/assets/2024-01-28-17-30-23.3db19902.png b/assets/2024-01-28-17-30-23.3db19902.png new file mode 100644 index 00000000..650b32c3 Binary files /dev/null and b/assets/2024-01-28-17-30-23.3db19902.png differ diff --git a/assets/2024-04-09-16-41-09.9e776372.png b/assets/2024-04-09-16-41-09.9e776372.png new file mode 100644 index 00000000..86795762 Binary files /dev/null and b/assets/2024-04-09-16-41-09.9e776372.png differ diff --git a/assets/2024-04-09-16-41-29.01db7dfd.png b/assets/2024-04-09-16-41-29.01db7dfd.png new file mode 100644 index 00000000..2a95b635 Binary files /dev/null and b/assets/2024-04-09-16-41-29.01db7dfd.png differ diff --git a/assets/2024-04-09-16-52-36.f7ed461a.png b/assets/2024-04-09-16-52-36.f7ed461a.png new file mode 100644 index 00000000..3a2b1c8e Binary files /dev/null and b/assets/2024-04-09-16-52-36.f7ed461a.png differ diff --git a/assets/2024-04-09-20-41-59.c3e21810.png b/assets/2024-04-09-20-41-59.c3e21810.png new file mode 100644 index 00000000..58ca8600 Binary files /dev/null and b/assets/2024-04-09-20-41-59.c3e21810.png differ diff --git a/assets/2024-04-15-11-10-48.0adbb1ac.png b/assets/2024-04-15-11-10-48.0adbb1ac.png new file mode 100644 index 00000000..a2277e7f Binary files /dev/null and b/assets/2024-04-15-11-10-48.0adbb1ac.png differ diff --git a/assets/2024-04-15-11-37-55.b8c61089.png b/assets/2024-04-15-11-37-55.b8c61089.png new file mode 100644 index 00000000..6b63990b Binary files /dev/null and b/assets/2024-04-15-11-37-55.b8c61089.png differ diff --git a/assets/2024-04-15-11-45-10.5b79fe3e.png b/assets/2024-04-15-11-45-10.5b79fe3e.png new file mode 100644 index 00000000..a486b281 Binary files /dev/null and b/assets/2024-04-15-11-45-10.5b79fe3e.png differ diff --git a/assets/2024-04-15-14-51-24.69678060.png b/assets/2024-04-15-14-51-24.69678060.png new file mode 100644 index 00000000..86a1fb2b Binary files /dev/null and b/assets/2024-04-15-14-51-24.69678060.png differ diff --git a/assets/2024-04-15-16-29-04.a6a9af91.png b/assets/2024-04-15-16-29-04.a6a9af91.png new file mode 100644 index 00000000..6f7dc864 Binary files /dev/null and b/assets/2024-04-15-16-29-04.a6a9af91.png differ diff --git a/assets/2024-04-15-16-29-22.7bb9f883.png b/assets/2024-04-15-16-29-22.7bb9f883.png new file mode 100644 index 00000000..4d8b7ad7 Binary files /dev/null and b/assets/2024-04-15-16-29-22.7bb9f883.png differ diff --git a/assets/2024-04-15-16-30-16.8834c182.png b/assets/2024-04-15-16-30-16.8834c182.png new file mode 100644 index 00000000..ff8b9fbd Binary files /dev/null and b/assets/2024-04-15-16-30-16.8834c182.png differ diff --git a/assets/2024-04-15-16-32-00.7b8a2c55.png b/assets/2024-04-15-16-32-00.7b8a2c55.png new file mode 100644 index 00000000..990f1893 Binary files /dev/null and b/assets/2024-04-15-16-32-00.7b8a2c55.png differ diff --git a/assets/2024-04-15-16-32-46.4ffe7e55.png b/assets/2024-04-15-16-32-46.4ffe7e55.png new file mode 100644 index 00000000..6abf64f4 Binary files /dev/null and b/assets/2024-04-15-16-32-46.4ffe7e55.png differ diff --git a/assets/2024-04-15-16-33-30.53b81e69.png b/assets/2024-04-15-16-33-30.53b81e69.png new file mode 100644 index 00000000..b4e73817 Binary files /dev/null and b/assets/2024-04-15-16-33-30.53b81e69.png differ diff --git a/assets/2024-04-15-16-34-19.29fd84eb.png b/assets/2024-04-15-16-34-19.29fd84eb.png new file mode 100644 index 00000000..71f2ffd5 Binary files /dev/null and b/assets/2024-04-15-16-34-19.29fd84eb.png differ diff --git a/assets/2024-04-15-16-37-04.e38b03b0.png b/assets/2024-04-15-16-37-04.e38b03b0.png new file mode 100644 index 00000000..a4c58cf7 Binary files /dev/null and b/assets/2024-04-15-16-37-04.e38b03b0.png differ diff --git a/assets/2024-04-15-16-38-36.b5c1eb05.png b/assets/2024-04-15-16-38-36.b5c1eb05.png new file mode 100644 index 00000000..73fece0d Binary files /dev/null and b/assets/2024-04-15-16-38-36.b5c1eb05.png differ diff --git a/assets/2024-04-15-16-38-54.d9fefeed.png b/assets/2024-04-15-16-38-54.d9fefeed.png new file mode 100644 index 00000000..c8e3aa3d Binary files /dev/null and b/assets/2024-04-15-16-38-54.d9fefeed.png differ diff --git a/assets/2024-04-15-16-40-03.8b67fcb7.png b/assets/2024-04-15-16-40-03.8b67fcb7.png new file mode 100644 index 00000000..46b75e05 Binary files /dev/null and b/assets/2024-04-15-16-40-03.8b67fcb7.png differ diff --git a/assets/2024-04-15-16-40-30.c5b0dc7e.png b/assets/2024-04-15-16-40-30.c5b0dc7e.png new file mode 100644 index 00000000..d5c7b38a Binary files /dev/null and b/assets/2024-04-15-16-40-30.c5b0dc7e.png differ diff --git a/assets/2024-04-15-16-40-38.28de0ed4.png b/assets/2024-04-15-16-40-38.28de0ed4.png new file mode 100644 index 00000000..9eaf07bd Binary files /dev/null and b/assets/2024-04-15-16-40-38.28de0ed4.png differ diff --git a/assets/2024-04-15-16-41-42.7d56b4af.png b/assets/2024-04-15-16-41-42.7d56b4af.png new file mode 100644 index 00000000..05684123 Binary files /dev/null and b/assets/2024-04-15-16-41-42.7d56b4af.png differ diff --git a/assets/2024-04-15-16-42-00.7e19cb0b.png b/assets/2024-04-15-16-42-00.7e19cb0b.png new file mode 100644 index 00000000..53e83e99 Binary files /dev/null and b/assets/2024-04-15-16-42-00.7e19cb0b.png differ diff --git a/assets/2024-04-15-19-41-41.cddf3090.png b/assets/2024-04-15-19-41-41.cddf3090.png new file mode 100644 index 00000000..34d0dab6 Binary files /dev/null and b/assets/2024-04-15-19-41-41.cddf3090.png differ diff --git a/assets/2024-04-16-14-40-53.4815a665.png b/assets/2024-04-16-14-40-53.4815a665.png new file mode 100644 index 00000000..615e54ab Binary files /dev/null and b/assets/2024-04-16-14-40-53.4815a665.png differ diff --git a/assets/2024-04-16-14-41-08.0de0710b.png b/assets/2024-04-16-14-41-08.0de0710b.png new file mode 100644 index 00000000..14e75fe6 Binary files /dev/null and b/assets/2024-04-16-14-41-08.0de0710b.png differ diff --git a/assets/2024-04-16-15-37-25.281ad292.png b/assets/2024-04-16-15-37-25.281ad292.png new file mode 100644 index 00000000..8a3431d2 Binary files /dev/null and b/assets/2024-04-16-15-37-25.281ad292.png differ diff --git a/assets/2024-04-16-16-01-56.0f2eeef2.png b/assets/2024-04-16-16-01-56.0f2eeef2.png new file mode 100644 index 00000000..a82e18ef Binary files /dev/null and b/assets/2024-04-16-16-01-56.0f2eeef2.png differ diff --git a/assets/2024-04-16-17-22-00.eef1000a.png b/assets/2024-04-16-17-22-00.eef1000a.png new file mode 100644 index 00000000..de6c3a5d Binary files /dev/null and b/assets/2024-04-16-17-22-00.eef1000a.png differ diff --git a/assets/2024-04-16-17-23-18.2bad2a94.png b/assets/2024-04-16-17-23-18.2bad2a94.png new file mode 100644 index 00000000..6d7d480b Binary files /dev/null and b/assets/2024-04-16-17-23-18.2bad2a94.png differ diff --git a/assets/2024-04-16-17-25-36.abbf51b0.png b/assets/2024-04-16-17-25-36.abbf51b0.png new file mode 100644 index 00000000..fad314a3 Binary files /dev/null and b/assets/2024-04-16-17-25-36.abbf51b0.png differ diff --git a/assets/2024-06-16-15-36-15.5742d5bf.png b/assets/2024-06-16-15-36-15.5742d5bf.png new file mode 100644 index 00000000..efb11cbc Binary files /dev/null and b/assets/2024-06-16-15-36-15.5742d5bf.png differ diff --git "a/assets/algorithm_\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.md.da461927.js" "b/assets/algorithm_\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.md.da461927.js" new file mode 100644 index 00000000..3dfd2182 --- /dev/null +++ "b/assets/algorithm_\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.md.da461927.js" @@ -0,0 +1,75 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const C=JSON.parse('{"title":"🔥刷题之探索最优解 WIP","description":"","frontmatter":{},"headers":[{"level":2,"title":"绳子盖住的最多点 WIP","slug":"绳子盖住的最多点-wip","link":"#绳子盖住的最多点-wip","children":[]}],"relativePath":"algorithm/🔥刷题之探索最优解.md","lastUpdated":1673072248000}'),p={name:"algorithm/🔥刷题之探索最优解.md"},o=l(`

🔥刷题之探索最优解 WIP

DANGER

WIP

正在创作

绳子盖住的最多点 WIP

一、题目描述: 给定一个有序数组arr 从左到右依次表示X轴上从左往右点的位置,给定一个正整数k, 返回如果有一根长度为k的绳子,最多能盖住几个点,绳子的边缘点碰到X轴上的点,也算盖住。 示例: 示例 1: 输入:arr = [1, 5, 13, 14, 15, 32, 43], k = 2 输出:3 示例 2: 输入:arr = [1, 5, 13, 14, 15, 32, 43], k = 5 输出:3

js
// 方法一:
+function rope(arr, k) {
+  let res = 1
+  for (let i = 0; i < arr.length; i++) {
+    let near = Dichotomous(arr, i, arr[i] - k) // 求数组中大于等于arr[i]-k的最小数
+    res = Math.max(res, i - near + 1)
+  }
+  return res
+}
+
+function Dichotomous(arr, r, value) {
+  let index = r
+  let l = 0
+  while (l < r) {
+    let mid = Math.floor((l + r) / 2)
+    if (arr[mid] >= value) {
+      r = mid - 1
+      index = mid
+    } else {
+      l = mid + 1
+    }
+  }
+  return index
+}
+// 方法二:
+function rope(arr, k) {
+  let l = 0,
+    r = 0,
+    n = arr.length
+  max = 0
+  while (l < n) {
+    while (r < n && arr[r] - arr[l] <= k) r++
+    max = Math.max(max, r - l++)
+  }
+  return max
+}
+
+
// 方法一:
+function rope(arr, k) {
+  let res = 1
+  for (let i = 0; i < arr.length; i++) {
+    let near = Dichotomous(arr, i, arr[i] - k) // 求数组中大于等于arr[i]-k的最小数
+    res = Math.max(res, i - near + 1)
+  }
+  return res
+}
+
+function Dichotomous(arr, r, value) {
+  let index = r
+  let l = 0
+  while (l < r) {
+    let mid = Math.floor((l + r) / 2)
+    if (arr[mid] >= value) {
+      r = mid - 1
+      index = mid
+    } else {
+      l = mid + 1
+    }
+  }
+  return index
+}
+// 方法二:
+function rope(arr, k) {
+  let l = 0,
+    r = 0,
+    n = arr.length
+  max = 0
+  while (l < n) {
+    while (r < n && arr[r] - arr[l] <= k) r++
+    max = Math.max(max, r - l++)
+  }
+  return max
+}
+
+

总结:在解决问题的基础上,再进行优化。方法一使用二分复杂度为O(nlogn),使用窗口法就可以将复杂度将为O(n)。

`,6),e=[o];function r(B,c,t,y,F,i){return n(),a("div",null,e)}const m=s(p,[["render",r]]);export{C as __pageData,m as default}; diff --git "a/assets/algorithm_\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.md.da461927.lean.js" "b/assets/algorithm_\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.md.da461927.lean.js" new file mode 100644 index 00000000..e5456749 --- /dev/null +++ "b/assets/algorithm_\360\237\224\245\345\210\267\351\242\230\344\271\213\346\216\242\347\264\242\346\234\200\344\274\230\350\247\243.md.da461927.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const C=JSON.parse('{"title":"🔥刷题之探索最优解 WIP","description":"","frontmatter":{},"headers":[{"level":2,"title":"绳子盖住的最多点 WIP","slug":"绳子盖住的最多点-wip","link":"#绳子盖住的最多点-wip","children":[]}],"relativePath":"algorithm/🔥刷题之探索最优解.md","lastUpdated":1673072248000}'),p={name:"algorithm/🔥刷题之探索最优解.md"},o=l("",6),e=[o];function r(B,c,t,y,F,i){return n(),a("div",null,e)}const m=s(p,[["render",r]]);export{C as __pageData,m as default}; diff --git a/assets/app.f983686f.js b/assets/app.f983686f.js new file mode 100644 index 00000000..d86832dc --- /dev/null +++ b/assets/app.f983686f.js @@ -0,0 +1,24 @@ +/** +* @vue/shared v3.4.21 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Hs(e,t){const n=new Set(e.split(","));return t?s=>n.has(s.toLowerCase()):s=>n.has(s)}const me={},Nt=[],Oe=()=>{},Ur=()=>!1,pn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Fs=e=>e.startsWith("onUpdate:"),ke=Object.assign,js=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},zr=Object.prototype.hasOwnProperty,ie=(e,t)=>zr.call(e,t),q=Array.isArray,Ot=e=>zn(e)==="[object Map]",ki=e=>zn(e)==="[object Set]",ee=e=>typeof e=="function",ge=e=>typeof e=="string",Wt=e=>typeof e=="symbol",_e=e=>e!==null&&typeof e=="object",xi=e=>(_e(e)||ee(e))&&ee(e.then)&&ee(e.catch),wi=Object.prototype.toString,zn=e=>wi.call(e),Kr=e=>zn(e).slice(8,-1),Si=e=>zn(e)==="[object Object]",Ds=e=>ge(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Rt=Hs(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Kn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},qr=/-(\w)/g,Ze=Kn(e=>e.replace(qr,(t,n)=>n?n.toUpperCase():"")),Gr=/\B([A-Z])/g,Jt=Kn(e=>e.replace(Gr,"-$1").toLowerCase()),qn=Kn(e=>e.charAt(0).toUpperCase()+e.slice(1)),ds=Kn(e=>e?`on${qn(e)}`:""),_t=(e,t)=>!Object.is(e,t),ps=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},Wr=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Jr=e=>{const t=ge(e)?Number(e):NaN;return isNaN(t)?e:t};let bo;const $i=()=>bo||(bo=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Gn(e){if(q(e)){const t={};for(let n=0;n{if(n){const s=n.split(Qr);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function pe(e){let t="";if(ge(e))t=e;else if(q(e))for(let n=0;nge(e)?e:e==null?"":q(e)||_e(e)&&(e.toString===wi||!ee(e.toString))?JSON.stringify(e,Ci,2):String(e),Ci=(e,t)=>t&&t.__v_isRef?Ci(e,t.value):Ot(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,o],i)=>(n[hs(s,i)+" =>"]=o,n),{})}:ki(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>hs(n))}:Wt(t)?hs(t):_e(t)&&!q(t)&&!Si(t)?String(t):t,hs=(e,t="")=>{var n;return Wt(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.4.21 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Ne;class nl{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Ne,!t&&Ne&&(this.index=(Ne.scopes||(Ne.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Ne;try{return Ne=this,t()}finally{Ne=n}}}on(){Ne=this}off(){Ne=this.parent}stop(t){if(this._active){let n,s;for(n=0,s=this.effects.length;n=4))break}this._dirtyLevel===1&&(this._dirtyLevel=0),Tt()}return this._dirtyLevel>=4}set dirty(t){this._dirtyLevel=t?4:0}run(){if(this._dirtyLevel=0,!this.active)return this.fn();let t=pt,n=Pt;try{return pt=!0,Pt=this,this._runnings++,ko(this),this.fn()}finally{xo(this),this._runnings--,Pt=n,pt=t}}stop(){var t;this.active&&(ko(this),xo(this),(t=this.onStop)==null||t.call(this),this.active=!1)}}function il(e){return e.value}function ko(e){e._trackId++,e._depsLength=0}function xo(e){if(e.deps.length>e._depsLength){for(let t=e._depsLength;t{const n=new Map;return n.cleanup=e,n.computed=t,n},Cs=new WeakMap,Ct=Symbol(""),Vs=Symbol("");function Me(e,t,n){if(pt&&Pt){let s=Cs.get(e);s||Cs.set(e,s=new Map);let o=s.get(n);o||s.set(n,o=Ai(()=>s.delete(n))),Ei(Pt,o)}}function st(e,t,n,s,o,i){const r=Cs.get(e);if(!r)return;let l=[];if(t==="clear")l=[...r.values()];else if(n==="length"&&q(e)){const a=Number(s);r.forEach((f,p)=>{(p==="length"||!Wt(p)&&p>=a)&&l.push(f)})}else switch(n!==void 0&&l.push(r.get(n)),t){case"add":q(e)?Ds(n)&&l.push(r.get("length")):(l.push(r.get(Ct)),Ot(e)&&l.push(r.get(Vs)));break;case"delete":q(e)||(l.push(r.get(Ct)),Ot(e)&&l.push(r.get(Vs)));break;case"set":Ot(e)&&l.push(r.get(Ct));break}zs();for(const a of l)a&&Mi(a,4);Ks()}const rl=Hs("__proto__,__v_isRef,__isVue"),Ii=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Wt)),wo=ll();function ll(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const s=ce(this);for(let i=0,r=this.length;i{e[t]=function(...n){Vt(),zs();const s=ce(this)[t].apply(this,n);return Ks(),Tt(),s}}),e}function al(e){const t=ce(this);return Me(t,"has",e),t.hasOwnProperty(e)}class Ni{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){const o=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!o;if(n==="__v_isReadonly")return o;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(o?i?kl:Hi:i?Bi:Ri).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const r=q(t);if(!o){if(r&&ie(wo,n))return Reflect.get(wo,n,s);if(n==="hasOwnProperty")return al}const l=Reflect.get(t,n,s);return(Wt(n)?Ii.has(n):rl(n))||(o||Me(t,"get",n),i)?l:Ae(l)?r&&Ds(n)?l:l.value:_e(l)?o?Ws(l):Jn(l):l}}class Oi extends Ni{constructor(t=!1){super(!1,t)}set(t,n,s,o){let i=t[n];if(!this._isShallow){const a=zt(i);if(!Mn(s)&&!zt(s)&&(i=ce(i),s=ce(s)),!q(t)&&Ae(i)&&!Ae(s))return a?!1:(i.value=s,!0)}const r=q(t)&&Ds(n)?Number(n)e,Wn=e=>Reflect.getPrototypeOf(e);function mn(e,t,n=!1,s=!1){e=e.__v_raw;const o=ce(e),i=ce(t);n||(_t(t,i)&&Me(o,"get",t),Me(o,"get",i));const{has:r}=Wn(o),l=s?qs:n?Ys:ln;if(r.call(o,t))return l(e.get(t));if(r.call(o,i))return l(e.get(i));e!==o&&e.get(t)}function gn(e,t=!1){const n=this.__v_raw,s=ce(n),o=ce(e);return t||(_t(e,o)&&Me(s,"has",e),Me(s,"has",o)),e===o?n.has(e):n.has(e)||n.has(o)}function yn(e,t=!1){return e=e.__v_raw,!t&&Me(ce(e),"iterate",Ct),Reflect.get(e,"size",e)}function So(e){e=ce(e);const t=ce(this);return Wn(t).has.call(t,e)||(t.add(e),st(t,"add",e,e)),this}function $o(e,t){t=ce(t);const n=ce(this),{has:s,get:o}=Wn(n);let i=s.call(n,e);i||(e=ce(e),i=s.call(n,e));const r=o.call(n,e);return n.set(e,t),i?_t(t,r)&&st(n,"set",e,t):st(n,"add",e,t),this}function Po(e){const t=ce(this),{has:n,get:s}=Wn(t);let o=n.call(t,e);o||(e=ce(e),o=n.call(t,e)),s&&s.call(t,e);const i=t.delete(e);return o&&st(t,"delete",e,void 0),i}function Co(){const e=ce(this),t=e.size!==0,n=e.clear();return t&&st(e,"clear",void 0,void 0),n}function bn(e,t){return function(s,o){const i=this,r=i.__v_raw,l=ce(r),a=t?qs:e?Ys:ln;return!e&&Me(l,"iterate",Ct),r.forEach((f,p)=>s.call(o,a(f),a(p),i))}}function kn(e,t,n){return function(...s){const o=this.__v_raw,i=ce(o),r=Ot(i),l=e==="entries"||e===Symbol.iterator&&r,a=e==="keys"&&r,f=o[e](...s),p=n?qs:t?Ys:ln;return!t&&Me(i,"iterate",a?Vs:Ct),{next(){const{value:h,done:b}=f.next();return b?{value:h,done:b}:{value:l?[p(h[0]),p(h[1])]:p(h),done:b}},[Symbol.iterator](){return this}}}}function it(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function pl(){const e={get(i){return mn(this,i)},get size(){return yn(this)},has:gn,add:So,set:$o,delete:Po,clear:Co,forEach:bn(!1,!1)},t={get(i){return mn(this,i,!1,!0)},get size(){return yn(this)},has:gn,add:So,set:$o,delete:Po,clear:Co,forEach:bn(!1,!0)},n={get(i){return mn(this,i,!0)},get size(){return yn(this,!0)},has(i){return gn.call(this,i,!0)},add:it("add"),set:it("set"),delete:it("delete"),clear:it("clear"),forEach:bn(!0,!1)},s={get(i){return mn(this,i,!0,!0)},get size(){return yn(this,!0)},has(i){return gn.call(this,i,!0)},add:it("add"),set:it("set"),delete:it("delete"),clear:it("clear"),forEach:bn(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(i=>{e[i]=kn(i,!1,!1),n[i]=kn(i,!0,!1),t[i]=kn(i,!1,!0),s[i]=kn(i,!0,!0)}),[e,n,t,s]}const[hl,_l,vl,ml]=pl();function Gs(e,t){const n=t?e?ml:vl:e?_l:hl;return(s,o,i)=>o==="__v_isReactive"?!e:o==="__v_isReadonly"?e:o==="__v_raw"?s:Reflect.get(ie(n,o)&&o in s?n:s,o,i)}const gl={get:Gs(!1,!1)},yl={get:Gs(!1,!0)},bl={get:Gs(!0,!1)},Ri=new WeakMap,Bi=new WeakMap,Hi=new WeakMap,kl=new WeakMap;function xl(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function wl(e){return e.__v_skip||!Object.isExtensible(e)?0:xl(Kr(e))}function Jn(e){return zt(e)?e:Js(e,!1,ul,gl,Ri)}function Sl(e){return Js(e,!1,dl,yl,Bi)}function Ws(e){return Js(e,!0,fl,bl,Hi)}function Js(e,t,n,s,o){if(!_e(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=o.get(e);if(i)return i;const r=wl(e);if(r===0)return e;const l=new Proxy(e,r===2?s:n);return o.set(e,l),l}function Bt(e){return zt(e)?Bt(e.__v_raw):!!(e&&e.__v_isReactive)}function zt(e){return!!(e&&e.__v_isReadonly)}function Mn(e){return!!(e&&e.__v_isShallow)}function Fi(e){return Bt(e)||zt(e)}function ce(e){const t=e&&e.__v_raw;return t?ce(t):e}function en(e){return Object.isExtensible(e)&&En(e,"__v_skip",!0),e}const ln=e=>_e(e)?Jn(e):e,Ys=e=>_e(e)?Ws(e):e;class ji{constructor(t,n,s,o){this.getter=t,this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this.effect=new Us(()=>t(this._value),()=>Cn(this,this.effect._dirtyLevel===2?2:3)),this.effect.computed=this,this.effect.active=this._cacheable=!o,this.__v_isReadonly=s}get value(){const t=ce(this);return(!t._cacheable||t.effect.dirty)&&_t(t._value,t._value=t.effect.run())&&Cn(t,4),Di(t),t.effect._dirtyLevel>=2&&Cn(t,2),t._value}set value(t){this._setter(t)}get _dirty(){return this.effect.dirty}set _dirty(t){this.effect.dirty=t}}function $l(e,t,n=!1){let s,o;const i=ee(e);return i?(s=e,o=Oe):(s=e.get,o=e.set),new ji(s,o,i||!o,n)}function Di(e){var t;pt&&Pt&&(e=ce(e),Ei(Pt,(t=e.dep)!=null?t:e.dep=Ai(()=>e.dep=void 0,e instanceof ji?e:void 0)))}function Cn(e,t=4,n){e=ce(e);const s=e.dep;s&&Mi(s,t)}function Ae(e){return!!(e&&e.__v_isRef===!0)}function he(e){return Ui(e,!1)}function Pl(e){return Ui(e,!0)}function Ui(e,t){return Ae(e)?e:new Cl(e,t)}class Cl{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:ce(t),this._value=n?t:ln(t)}get value(){return Di(this),this._value}set value(t){const n=this.__v_isShallow||Mn(t)||zt(t);t=n?t:ce(t),_t(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:ln(t),Cn(this,4))}}function y(e){return Ae(e)?e.value:e}const Vl={get:(e,t,n)=>y(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const o=e[t];return Ae(o)&&!Ae(n)?(o.value=n,!0):Reflect.set(e,t,n,s)}};function zi(e){return Bt(e)?e:new Proxy(e,Vl)}/** +* @vue/runtime-core v3.4.21 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function ht(e,t,n,s){try{return s?e(...s):e()}catch(o){Yn(o,t,n)}}function Fe(e,t,n,s){if(ee(e)){const i=ht(e,t,n,s);return i&&xi(i)&&i.catch(r=>{Yn(r,t,n)}),i}const o=[];for(let i=0;i>>1,o=$e[s],i=cn(o);iQe&&$e.splice(t,1)}function Ml(e){q(e)?Ht.push(...e):(!ct||!ct.includes(e,e.allowRecurse?wt+1:wt))&&Ht.push(e),qi()}function Vo(e,t,n=an?Qe+1:0){for(;n<$e.length;n++){const s=$e[n];if(s&&s.pre){if(e&&s.id!==e.uid)continue;$e.splice(n,1),n--,s()}}}function An(e){if(Ht.length){const t=[...new Set(Ht)].sort((n,s)=>cn(n)-cn(s));if(Ht.length=0,ct){ct.push(...t);return}for(ct=t,wt=0;wte.id==null?1/0:e.id,Al=(e,t)=>{const n=cn(e)-cn(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Gi(e){Ts=!1,an=!0,$e.sort(Al);const t=Oe;try{for(Qe=0;Qe<$e.length;Qe++){const n=$e[Qe];n&&n.active!==!1&&ht(n,null,14)}}finally{Qe=0,$e.length=0,An(),an=!1,Qs=null,($e.length||Ht.length)&&Gi()}}function Il(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||me;let o=n;const i=t.startsWith("update:"),r=i&&t.slice(7);if(r&&r in s){const p=`${r==="modelValue"?"model":r}Modifiers`,{number:h,trim:b}=s[p]||me;b&&(o=n.map(P=>ge(P)?P.trim():P)),h&&(o=n.map(Wr))}let l,a=s[l=ds(t)]||s[l=ds(Ze(t))];!a&&i&&(a=s[l=ds(Jt(t))]),a&&Fe(a,e,6,o);const f=s[l+"Once"];if(f){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Fe(f,e,6,o)}}function Wi(e,t,n=!1){const s=t.emitsCache,o=s.get(e);if(o!==void 0)return o;const i=e.emits;let r={},l=!1;if(!ee(e)){const a=f=>{const p=Wi(f,t,!0);p&&(l=!0,ke(r,p))};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}return!i&&!l?(_e(e)&&s.set(e,null),null):(q(i)?i.forEach(a=>r[a]=null):ke(r,i),_e(e)&&s.set(e,r),r)}function Qn(e,t){return!e||!pn(t)?!1:(t=t.slice(2).replace(/Once$/,""),ie(e,t[0].toLowerCase()+t.slice(1))||ie(e,Jt(t))||ie(e,t))}let Pe=null,Xn=null;function In(e){const t=Pe;return Pe=e,Xn=e&&e.type.__scopeId||null,t}function qe(e){Xn=e}function Ge(){Xn=null}function I(e,t=Pe,n){if(!t||e._n)return e;const s=(...o)=>{s._d&&Fo(-1);const i=In(t);let r;try{r=e(...o)}finally{In(i),s._d&&Fo(1)}return r};return s._n=!0,s._c=!0,s._d=!0,s}function _s(e){const{type:t,vnode:n,proxy:s,withProxy:o,props:i,propsOptions:[r],slots:l,attrs:a,emit:f,render:p,renderCache:h,data:b,setupState:P,ctx:Y,inheritAttrs:G}=e;let ne,le;const ye=In(e);try{if(n.shapeFlag&4){const S=o||s,F=S;ne=Ue(p.call(F,S,h,i,P,b,Y)),le=a}else{const S=t;ne=Ue(S.length>1?S(i,{attrs:a,slots:l,emit:f}):S(i,null)),le=t.props?a:Nl(a)}}catch(S){sn.length=0,Yn(S,e,1),ne=T(Re)}let v=ne;if(le&&G!==!1){const S=Object.keys(le),{shapeFlag:F}=v;S.length&&F&7&&(r&&S.some(Fs)&&(le=Ol(le,r)),v=vt(v,le))}return n.dirs&&(v=vt(v),v.dirs=v.dirs?v.dirs.concat(n.dirs):n.dirs),n.transition&&(v.transition=n.transition),ne=v,In(ye),ne}const Nl=e=>{let t;for(const n in e)(n==="class"||n==="style"||pn(n))&&((t||(t={}))[n]=e[n]);return t},Ol=(e,t)=>{const n={};for(const s in e)(!Fs(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function Rl(e,t,n){const{props:s,children:o,component:i}=e,{props:r,children:l,patchFlag:a}=t,f=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&a>=0){if(a&1024)return!0;if(a&16)return s?To(s,r,f):!!r;if(a&8){const p=t.dynamicProps;for(let h=0;he.__isSuspense;function Qi(e,t){t&&t.pendingBranch?q(e)?t.effects.push(...e):t.effects.push(e):Ml(e)}const Fl=Symbol.for("v-scx"),jl=()=>Ke(Fl);function Kt(e,t){return Zn(e,null,t)}function Xi(e,t){return Zn(e,null,{flush:"post"})}const xn={};function Xe(e,t,n){return Zn(e,t,n)}function Zn(e,t,{immediate:n,deep:s,flush:o,once:i,onTrack:r,onTrigger:l}=me){if(t&&i){const N=t;t=(...Z)=>{N(...Z),F()}}const a=Se,f=N=>s===!0?N:It(N,s===!1?1:void 0);let p,h=!1,b=!1;if(Ae(e)?(p=()=>e.value,h=Mn(e)):Bt(e)?(p=()=>f(e),h=!0):q(e)?(b=!0,h=e.some(N=>Bt(N)||Mn(N)),p=()=>e.map(N=>{if(Ae(N))return N.value;if(Bt(N))return f(N);if(ee(N))return ht(N,a,2)})):ee(e)?t?p=()=>ht(e,a,2):p=()=>(P&&P(),Fe(e,a,3,[Y])):p=Oe,t&&s){const N=p;p=()=>It(N())}let P,Y=N=>{P=v.onStop=()=>{ht(N,a,4),P=v.onStop=void 0}},G;if(os)if(Y=Oe,t?n&&Fe(t,a,3,[p(),b?[]:void 0,Y]):p(),o==="sync"){const N=jl();G=N.__watcherHandles||(N.__watcherHandles=[])}else return Oe;let ne=b?new Array(e.length).fill(xn):xn;const le=()=>{if(!(!v.active||!v.dirty))if(t){const N=v.run();(s||h||(b?N.some((Z,R)=>_t(Z,ne[R])):_t(N,ne)))&&(P&&P(),Fe(t,a,3,[N,ne===xn?void 0:b&&ne[0]===xn?[]:ne,Y]),ne=N)}else v.run()};le.allowRecurse=!!t;let ye;o==="sync"?ye=le:o==="post"?ye=()=>Ee(le,a&&a.suspense):(le.pre=!0,a&&(le.id=a.uid),ye=()=>Zs(le));const v=new Us(p,Oe,ye),S=Vi(),F=()=>{v.stop(),S&&js(S.effects,v)};return t?n?le():ne=v.run():o==="post"?Ee(v.run.bind(v),a&&a.suspense):v.run(),G&&G.push(F),F}function Dl(e,t,n){const s=this.proxy,o=ge(e)?e.includes(".")?Zi(s,e):()=>s[e]:e.bind(s,s);let i;ee(t)?i=t:(i=t.handler,n=t);const r=hn(this),l=Zn(o,i.bind(s),n);return r(),l}function Zi(e,t){const n=t.split(".");return()=>{let s=e;for(let o=0;o0){if(n>=t)return e;n++}if(s=s||new Set,s.has(e))return e;if(s.add(e),Ae(e))It(e.value,t,n,s);else if(q(e))for(let o=0;o{It(o,t,n,s)});else if(Si(e))for(const o in e)It(e[o],t,n,s);return e}function Ye(e,t,n,s){const o=e.dirs,i=t&&t.dirs;for(let r=0;r{e.isMounted=!0}),or(()=>{e.isUnmounting=!0}),e}const Be=[Function,Array],er={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Be,onEnter:Be,onAfterEnter:Be,onEnterCancelled:Be,onBeforeLeave:Be,onLeave:Be,onAfterLeave:Be,onLeaveCancelled:Be,onBeforeAppear:Be,onAppear:Be,onAfterAppear:Be,onAppearCancelled:Be},zl={name:"BaseTransition",props:er,setup(e,{slots:t}){const n=ro(),s=Ul();return()=>{const o=t.default&&nr(t.default(),!0);if(!o||!o.length)return;let i=o[0];if(o.length>1){for(const b of o)if(b.type!==Re){i=b;break}}const r=ce(e),{mode:l}=r;if(s.isLeaving)return vs(i);const a=Eo(i);if(!a)return vs(i);const f=Ls(a,r,s,n);Es(a,f);const p=n.subTree,h=p&&Eo(p);if(h&&h.type!==Re&&!St(a,h)){const b=Ls(h,r,s,n);if(Es(h,b),l==="out-in")return s.isLeaving=!0,b.afterLeave=()=>{s.isLeaving=!1,n.update.active!==!1&&(n.effect.dirty=!0,n.update())},vs(i);l==="in-out"&&a.type!==Re&&(b.delayLeave=(P,Y,G)=>{const ne=tr(s,h);ne[String(h.key)]=h,P[ut]=()=>{Y(),P[ut]=void 0,delete f.delayedLeave},f.delayedLeave=G})}return i}}},Kl=zl;function tr(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function Ls(e,t,n,s){const{appear:o,mode:i,persisted:r=!1,onBeforeEnter:l,onEnter:a,onAfterEnter:f,onEnterCancelled:p,onBeforeLeave:h,onLeave:b,onAfterLeave:P,onLeaveCancelled:Y,onBeforeAppear:G,onAppear:ne,onAfterAppear:le,onAppearCancelled:ye}=t,v=String(e.key),S=tr(n,e),F=(R,W)=>{R&&Fe(R,s,9,W)},N=(R,W)=>{const H=W[1];F(R,W),q(R)?R.every(ae=>ae.length<=1)&&H():R.length<=1&&H()},Z={mode:i,persisted:r,beforeEnter(R){let W=l;if(!n.isMounted)if(o)W=G||l;else return;R[ut]&&R[ut](!0);const H=S[v];H&&St(e,H)&&H.el[ut]&&H.el[ut](),F(W,[R])},enter(R){let W=a,H=f,ae=p;if(!n.isMounted)if(o)W=ne||a,H=le||f,ae=ye||p;else return;let M=!1;const te=R[wn]=be=>{M||(M=!0,be?F(ae,[R]):F(H,[R]),Z.delayedLeave&&Z.delayedLeave(),R[wn]=void 0)};W?N(W,[R,te]):te()},leave(R,W){const H=String(e.key);if(R[wn]&&R[wn](!0),n.isUnmounting)return W();F(h,[R]);let ae=!1;const M=R[ut]=te=>{ae||(ae=!0,W(),te?F(Y,[R]):F(P,[R]),R[ut]=void 0,S[H]===e&&delete S[H])};S[H]=e,b?N(b,[R,M]):M()},clone(R){return Ls(R,t,n,s)}};return Z}function vs(e){if(es(e))return e=vt(e),e.children=null,e}function Eo(e){return es(e)?e.children?e.children[0]:void 0:e}function Es(e,t){e.shapeFlag&6&&e.component?Es(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function nr(e,t=!1,n){let s=[],o=0;for(let i=0;i1)for(let i=0;ike({name:e.name},t,{setup:e}))():e}const Ft=e=>!!e.type.__asyncLoader,es=e=>e.type.__isKeepAlive;function ql(e,t){sr(e,"a",t)}function Gl(e,t){sr(e,"da",t)}function sr(e,t,n=Se){const s=e.__wdc||(e.__wdc=()=>{let o=n;for(;o;){if(o.isDeactivated)return;o=o.parent}return e()});if(ts(t,s,n),n){let o=n.parent;for(;o&&o.parent;)es(o.parent.vnode)&&Wl(s,t,n,o),o=o.parent}}function Wl(e,t,n,s){const o=ts(t,e,s,!0);mt(()=>{js(s[t],o)},n)}function ts(e,t,n=Se,s=!1){if(n){const o=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...r)=>{if(n.isUnmounted)return;Vt();const l=hn(n),a=Fe(t,n,e,r);return l(),Tt(),a});return s?o.unshift(i):o.push(i),i}}const ot=e=>(t,n=Se)=>(!os||e==="sp")&&ts(e,(...s)=>t(...s),n),Jl=ot("bm"),je=ot("m"),Yl=ot("bu"),no=ot("u"),or=ot("bum"),mt=ot("um"),Ql=ot("sp"),Xl=ot("rtg"),Zl=ot("rtc");function ea(e,t=Se){ts("ec",e,t)}function Ce(e,t,n,s){let o;const i=n&&n[s];if(q(e)||ge(e)){o=new Array(e.length);for(let r=0,l=e.length;rt(r,l,void 0,i&&i[l]));else{const r=Object.keys(e);o=new Array(r.length);for(let l=0,a=r.length;lRn(t)?!(t.type===Re||t.type===Q&&!ir(t.children)):!0)?e:null}const Ms=e=>e?gr(e)?lo(e)||e.proxy:Ms(e.parent):null,tn=ke(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Ms(e.parent),$root:e=>Ms(e.root),$emit:e=>e.emit,$options:e=>so(e),$forceUpdate:e=>e.f||(e.f=()=>{e.effect.dirty=!0,Zs(e.update)}),$nextTick:e=>e.n||(e.n=Xs.bind(e.proxy)),$watch:e=>Dl.bind(e)}),ms=(e,t)=>e!==me&&!e.__isScriptSetup&&ie(e,t),ta={get({_:e},t){const{ctx:n,setupState:s,data:o,props:i,accessCache:r,type:l,appContext:a}=e;let f;if(t[0]!=="$"){const P=r[t];if(P!==void 0)switch(P){case 1:return s[t];case 2:return o[t];case 4:return n[t];case 3:return i[t]}else{if(ms(s,t))return r[t]=1,s[t];if(o!==me&&ie(o,t))return r[t]=2,o[t];if((f=e.propsOptions[0])&&ie(f,t))return r[t]=3,i[t];if(n!==me&&ie(n,t))return r[t]=4,n[t];As&&(r[t]=0)}}const p=tn[t];let h,b;if(p)return t==="$attrs"&&Me(e,"get",t),p(e);if((h=l.__cssModules)&&(h=h[t]))return h;if(n!==me&&ie(n,t))return r[t]=4,n[t];if(b=a.config.globalProperties,ie(b,t))return b[t]},set({_:e},t,n){const{data:s,setupState:o,ctx:i}=e;return ms(o,t)?(o[t]=n,!0):s!==me&&ie(s,t)?(s[t]=n,!0):ie(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:o,propsOptions:i}},r){let l;return!!n[r]||e!==me&&ie(e,r)||ms(t,r)||(l=i[0])&&ie(l,r)||ie(s,r)||ie(tn,r)||ie(o.config.globalProperties,r)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:ie(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Mo(e){return q(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let As=!0;function na(e){const t=so(e),n=e.proxy,s=e.ctx;As=!1,t.beforeCreate&&Ao(t.beforeCreate,e,"bc");const{data:o,computed:i,methods:r,watch:l,provide:a,inject:f,created:p,beforeMount:h,mounted:b,beforeUpdate:P,updated:Y,activated:G,deactivated:ne,beforeDestroy:le,beforeUnmount:ye,destroyed:v,unmounted:S,render:F,renderTracked:N,renderTriggered:Z,errorCaptured:R,serverPrefetch:W,expose:H,inheritAttrs:ae,components:M,directives:te,filters:be}=t;if(f&&sa(f,s,null),r)for(const oe in r){const D=r[oe];ee(D)&&(s[oe]=D.bind(n))}if(o){const oe=o.call(n,n);_e(oe)&&(e.data=Jn(oe))}if(As=!0,i)for(const oe in i){const D=i[oe],tt=ee(D)?D.bind(n,n):ee(D.get)?D.get.bind(n,n):Oe,_n=!ee(D)&&ee(D.set)?D.set.bind(n):Oe,yt=re({get:tt,set:_n});Object.defineProperty(s,oe,{enumerable:!0,configurable:!0,get:()=>yt.value,set:We=>yt.value=We})}if(l)for(const oe in l)rr(l[oe],s,n,oe);if(a){const oe=ee(a)?a.call(n):a;Reflect.ownKeys(oe).forEach(D=>{ns(D,oe[D])})}p&&Ao(p,e,"c");function U(oe,D){q(D)?D.forEach(tt=>oe(tt.bind(n))):D&&oe(D.bind(n))}if(U(Jl,h),U(je,b),U(Yl,P),U(no,Y),U(ql,G),U(Gl,ne),U(ea,R),U(Zl,N),U(Xl,Z),U(or,ye),U(mt,S),U(Ql,W),q(H))if(H.length){const oe=e.exposed||(e.exposed={});H.forEach(D=>{Object.defineProperty(oe,D,{get:()=>n[D],set:tt=>n[D]=tt})})}else e.exposed||(e.exposed={});F&&e.render===Oe&&(e.render=F),ae!=null&&(e.inheritAttrs=ae),M&&(e.components=M),te&&(e.directives=te)}function sa(e,t,n=Oe){q(e)&&(e=Is(e));for(const s in e){const o=e[s];let i;_e(o)?"default"in o?i=Ke(o.from||s,o.default,!0):i=Ke(o.from||s):i=Ke(o),Ae(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:r=>i.value=r}):t[s]=i}}function Ao(e,t,n){Fe(q(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function rr(e,t,n,s){const o=s.includes(".")?Zi(n,s):()=>n[s];if(ge(e)){const i=t[e];ee(i)&&Xe(o,i)}else if(ee(e))Xe(o,e.bind(n));else if(_e(e))if(q(e))e.forEach(i=>rr(i,t,n,s));else{const i=ee(e.handler)?e.handler.bind(n):t[e.handler];ee(i)&&Xe(o,i,e)}}function so(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:o,optionsCache:i,config:{optionMergeStrategies:r}}=e.appContext,l=i.get(t);let a;return l?a=l:!o.length&&!n&&!s?a=t:(a={},o.length&&o.forEach(f=>Nn(a,f,r,!0)),Nn(a,t,r)),_e(t)&&i.set(t,a),a}function Nn(e,t,n,s=!1){const{mixins:o,extends:i}=t;i&&Nn(e,i,n,!0),o&&o.forEach(r=>Nn(e,r,n,!0));for(const r in t)if(!(s&&r==="expose")){const l=oa[r]||n&&n[r];e[r]=l?l(e[r],t[r]):t[r]}return e}const oa={data:Io,props:No,emits:No,methods:Zt,computed:Zt,beforeCreate:Ve,created:Ve,beforeMount:Ve,mounted:Ve,beforeUpdate:Ve,updated:Ve,beforeDestroy:Ve,beforeUnmount:Ve,destroyed:Ve,unmounted:Ve,activated:Ve,deactivated:Ve,errorCaptured:Ve,serverPrefetch:Ve,components:Zt,directives:Zt,watch:ra,provide:Io,inject:ia};function Io(e,t){return t?e?function(){return ke(ee(e)?e.call(this,this):e,ee(t)?t.call(this,this):t)}:t:e}function ia(e,t){return Zt(Is(e),Is(t))}function Is(e){if(q(e)){const t={};for(let n=0;n1)return n&&ee(t)?t.call(s&&s.proxy):t}}function ca(e,t,n,s=!1){const o={},i={};En(i,ss,1),e.propsDefaults=Object.create(null),ar(e,t,o,i);for(const r in e.propsOptions[0])r in o||(o[r]=void 0);n?e.props=s?o:Sl(o):e.type.props?e.props=o:e.props=i,e.attrs=i}function ua(e,t,n,s){const{props:o,attrs:i,vnode:{patchFlag:r}}=e,l=ce(o),[a]=e.propsOptions;let f=!1;if((s||r>0)&&!(r&16)){if(r&8){const p=e.vnode.dynamicProps;for(let h=0;h{a=!0;const[b,P]=cr(h,t,!0);ke(r,b),P&&l.push(...P)};!n&&t.mixins.length&&t.mixins.forEach(p),e.extends&&p(e.extends),e.mixins&&e.mixins.forEach(p)}if(!i&&!a)return _e(e)&&s.set(e,Nt),Nt;if(q(i))for(let p=0;p-1,P[1]=G<0||Y-1||ie(P,"default"))&&l.push(h)}}}const f=[r,l];return _e(e)&&s.set(e,f),f}function Oo(e){return e[0]!=="$"&&!Rt(e)}function Ro(e){return e===null?"null":typeof e=="function"?e.name||"":typeof e=="object"&&e.constructor&&e.constructor.name||""}function Bo(e,t){return Ro(e)===Ro(t)}function Ho(e,t){return q(t)?t.findIndex(n=>Bo(n,e)):ee(t)&&Bo(t,e)?0:-1}const ur=e=>e[0]==="_"||e==="$stable",oo=e=>q(e)?e.map(Ue):[Ue(e)],fa=(e,t,n)=>{if(t._n)return t;const s=I((...o)=>oo(t(...o)),n);return s._c=!1,s},fr=(e,t,n)=>{const s=e._ctx;for(const o in e){if(ur(o))continue;const i=e[o];if(ee(i))t[o]=fa(o,i,s);else if(i!=null){const r=oo(i);t[o]=()=>r}}},dr=(e,t)=>{const n=oo(t);e.slots.default=()=>n},da=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=ce(t),En(t,"_",n)):fr(t,e.slots={})}else e.slots={},t&&dr(e,t);En(e.slots,ss,1)},pa=(e,t,n)=>{const{vnode:s,slots:o}=e;let i=!0,r=me;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:(ke(o,t),!n&&l===1&&delete o._):(i=!t.$stable,fr(t,o)),r=t}else t&&(dr(e,t),r={default:1});if(i)for(const l in o)!ur(l)&&r[l]==null&&delete o[l]};function On(e,t,n,s,o=!1){if(q(e)){e.forEach((b,P)=>On(b,t&&(q(t)?t[P]:t),n,s,o));return}if(Ft(s)&&!o)return;const i=s.shapeFlag&4?lo(s.component)||s.component.proxy:s.el,r=o?null:i,{i:l,r:a}=e,f=t&&t.r,p=l.refs===me?l.refs={}:l.refs,h=l.setupState;if(f!=null&&f!==a&&(ge(f)?(p[f]=null,ie(h,f)&&(h[f]=null)):Ae(f)&&(f.value=null)),ee(a))ht(a,l,12,[r,p]);else{const b=ge(a),P=Ae(a);if(b||P){const Y=()=>{if(e.f){const G=b?ie(h,a)?h[a]:p[a]:a.value;o?q(G)&&js(G,i):q(G)?G.includes(i)||G.push(i):b?(p[a]=[i],ie(h,a)&&(h[a]=p[a])):(a.value=[i],e.k&&(p[e.k]=a.value))}else b?(p[a]=r,ie(h,a)&&(h[a]=r)):P&&(a.value=r,e.k&&(p[e.k]=r))};r?(Y.id=-1,Ee(Y,n)):Y()}}}let rt=!1;const ha=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",_a=e=>e.namespaceURI.includes("MathML"),Sn=e=>{if(ha(e))return"svg";if(_a(e))return"mathml"},$n=e=>e.nodeType===8;function va(e){const{mt:t,p:n,o:{patchProp:s,createText:o,nextSibling:i,parentNode:r,remove:l,insert:a,createComment:f}}=e,p=(v,S)=>{if(!S.hasChildNodes()){n(null,v,S),An(),S._vnode=v;return}rt=!1,h(S.firstChild,v,null,null,null),An(),S._vnode=v,rt&&console.error("Hydration completed but contains mismatches.")},h=(v,S,F,N,Z,R=!1)=>{const W=$n(v)&&v.data==="[",H=()=>G(v,S,F,N,Z,W),{type:ae,ref:M,shapeFlag:te,patchFlag:be}=S;let we=v.nodeType;S.el=v,be===-2&&(R=!1,S.dynamicChildren=null);let U=null;switch(ae){case qt:we!==3?S.children===""?(a(S.el=o(""),r(v),v),U=v):U=H():(v.data!==S.children&&(rt=!0,v.data=S.children),U=i(v));break;case Re:ye(v)?(U=i(v),le(S.el=v.content.firstChild,v,F)):we!==8||W?U=H():U=i(v);break;case jt:if(W&&(v=i(v),we=v.nodeType),we===1||we===3){U=v;const oe=!S.children.length;for(let D=0;D{R=R||!!S.dynamicChildren;const{type:W,props:H,patchFlag:ae,shapeFlag:M,dirs:te,transition:be}=S,we=W==="input"||W==="option";if(we||ae!==-1){te&&Ye(S,null,F,"created");let U=!1;if(ye(v)){U=pr(N,be)&&F&&F.vnode.props&&F.vnode.props.appear;const D=v.content.firstChild;U&&be.beforeEnter(D),le(D,v,F),S.el=v=D}if(M&16&&!(H&&(H.innerHTML||H.textContent))){let D=P(v.firstChild,S,v,F,N,Z,R);for(;D;){rt=!0;const tt=D;D=D.nextSibling,l(tt)}}else M&8&&v.textContent!==S.children&&(rt=!0,v.textContent=S.children);if(H)if(we||!R||ae&48)for(const D in H)(we&&(D.endsWith("value")||D==="indeterminate")||pn(D)&&!Rt(D)||D[0]===".")&&s(v,D,null,H[D],void 0,void 0,F);else H.onClick&&s(v,"onClick",null,H.onClick,void 0,void 0,F);let oe;(oe=H&&H.onVnodeBeforeMount)&&He(oe,F,S),te&&Ye(S,null,F,"beforeMount"),((oe=H&&H.onVnodeMounted)||te||U)&&Qi(()=>{oe&&He(oe,F,S),U&&be.enter(v),te&&Ye(S,null,F,"mounted")},N)}return v.nextSibling},P=(v,S,F,N,Z,R,W)=>{W=W||!!S.dynamicChildren;const H=S.children,ae=H.length;for(let M=0;M{const{slotScopeIds:W}=S;W&&(Z=Z?Z.concat(W):W);const H=r(v),ae=P(i(v),S,H,F,N,Z,R);return ae&&$n(ae)&&ae.data==="]"?i(S.anchor=ae):(rt=!0,a(S.anchor=f("]"),H,ae),ae)},G=(v,S,F,N,Z,R)=>{if(rt=!0,S.el=null,R){const ae=ne(v);for(;;){const M=i(v);if(M&&M!==ae)l(M);else break}}const W=i(v),H=r(v);return l(v),n(null,S,H,W,F,N,Sn(H),Z),W},ne=(v,S="[",F="]")=>{let N=0;for(;v;)if(v=i(v),v&&$n(v)&&(v.data===S&&N++,v.data===F)){if(N===0)return i(v);N--}return v},le=(v,S,F)=>{const N=S.parentNode;N&&N.replaceChild(v,S);let Z=F;for(;Z;)Z.vnode.el===S&&(Z.vnode.el=Z.subTree.el=v),Z=Z.parent},ye=v=>v.nodeType===1&&v.tagName.toLowerCase()==="template";return[p,h]}const Ee=Qi;function ma(e){return ga(e,va)}function ga(e,t){const n=$i();n.__VUE__=!0;const{insert:s,remove:o,patchProp:i,createElement:r,createText:l,createComment:a,setText:f,setElementText:p,parentNode:h,nextSibling:b,setScopeId:P=Oe,insertStaticContent:Y}=e,G=(c,u,_,k=null,x=null,C=null,E=void 0,$=null,V=!!u.dynamicChildren)=>{if(c===u)return;c&&!St(c,u)&&(k=vn(c),We(c,x,C,!0),c=null),u.patchFlag===-2&&(V=!1,u.dynamicChildren=null);const{type:w,ref:A,shapeFlag:z}=u;switch(w){case qt:ne(c,u,_,k);break;case Re:le(c,u,_,k);break;case jt:c==null&&ye(u,_,k,E);break;case Q:M(c,u,_,k,x,C,E,$,V);break;default:z&1?F(c,u,_,k,x,C,E,$,V):z&6?te(c,u,_,k,x,C,E,$,V):(z&64||z&128)&&w.process(c,u,_,k,x,C,E,$,V,Mt)}A!=null&&x&&On(A,c&&c.ref,C,u||c,!u)},ne=(c,u,_,k)=>{if(c==null)s(u.el=l(u.children),_,k);else{const x=u.el=c.el;u.children!==c.children&&f(x,u.children)}},le=(c,u,_,k)=>{c==null?s(u.el=a(u.children||""),_,k):u.el=c.el},ye=(c,u,_,k)=>{[c.el,c.anchor]=Y(c.children,u,_,k,c.el,c.anchor)},v=({el:c,anchor:u},_,k)=>{let x;for(;c&&c!==u;)x=b(c),s(c,_,k),c=x;s(u,_,k)},S=({el:c,anchor:u})=>{let _;for(;c&&c!==u;)_=b(c),o(c),c=_;o(u)},F=(c,u,_,k,x,C,E,$,V)=>{u.type==="svg"?E="svg":u.type==="math"&&(E="mathml"),c==null?N(u,_,k,x,C,E,$,V):W(c,u,x,C,E,$,V)},N=(c,u,_,k,x,C,E,$)=>{let V,w;const{props:A,shapeFlag:z,transition:j,dirs:J}=c;if(V=c.el=r(c.type,C,A&&A.is,A),z&8?p(V,c.children):z&16&&R(c.children,V,null,k,x,gs(c,C),E,$),J&&Ye(c,null,k,"created"),Z(V,c,c.scopeId,E,k),A){for(const de in A)de!=="value"&&!Rt(de)&&i(V,de,null,A[de],C,c.children,k,x,nt);"value"in A&&i(V,"value",null,A.value,C),(w=A.onVnodeBeforeMount)&&He(w,k,c)}J&&Ye(c,null,k,"beforeMount");const se=pr(x,j);se&&j.beforeEnter(V),s(V,u,_),((w=A&&A.onVnodeMounted)||se||J)&&Ee(()=>{w&&He(w,k,c),se&&j.enter(V),J&&Ye(c,null,k,"mounted")},x)},Z=(c,u,_,k,x)=>{if(_&&P(c,_),k)for(let C=0;C{for(let w=V;w{const $=u.el=c.el;let{patchFlag:V,dynamicChildren:w,dirs:A}=u;V|=c.patchFlag&16;const z=c.props||me,j=u.props||me;let J;if(_&&bt(_,!1),(J=j.onVnodeBeforeUpdate)&&He(J,_,u,c),A&&Ye(u,c,_,"beforeUpdate"),_&&bt(_,!0),w?H(c.dynamicChildren,w,$,_,k,gs(u,x),C):E||D(c,u,$,null,_,k,gs(u,x),C,!1),V>0){if(V&16)ae($,u,z,j,_,k,x);else if(V&2&&z.class!==j.class&&i($,"class",null,j.class,x),V&4&&i($,"style",z.style,j.style,x),V&8){const se=u.dynamicProps;for(let de=0;de{J&&He(J,_,u,c),A&&Ye(u,c,_,"updated")},k)},H=(c,u,_,k,x,C,E)=>{for(let $=0;${if(_!==k){if(_!==me)for(const $ in _)!Rt($)&&!($ in k)&&i(c,$,_[$],null,E,u.children,x,C,nt);for(const $ in k){if(Rt($))continue;const V=k[$],w=_[$];V!==w&&$!=="value"&&i(c,$,w,V,E,u.children,x,C,nt)}"value"in k&&i(c,"value",_.value,k.value,E)}},M=(c,u,_,k,x,C,E,$,V)=>{const w=u.el=c?c.el:l(""),A=u.anchor=c?c.anchor:l("");let{patchFlag:z,dynamicChildren:j,slotScopeIds:J}=u;J&&($=$?$.concat(J):J),c==null?(s(w,_,k),s(A,_,k),R(u.children||[],_,A,x,C,E,$,V)):z>0&&z&64&&j&&c.dynamicChildren?(H(c.dynamicChildren,j,_,x,C,E,$),(u.key!=null||x&&u===x.subTree)&&hr(c,u,!0)):D(c,u,_,A,x,C,E,$,V)},te=(c,u,_,k,x,C,E,$,V)=>{u.slotScopeIds=$,c==null?u.shapeFlag&512?x.ctx.activate(u,_,k,E,V):be(u,_,k,x,C,E,V):we(c,u,V)},be=(c,u,_,k,x,C,E)=>{const $=c.component=Ca(c,k,x);if(es(c)&&($.ctx.renderer=Mt),Va($),$.asyncDep){if(x&&x.registerDep($,U),!c.el){const V=$.subTree=T(Re);le(null,V,u,_)}}else U($,c,u,_,x,C,E)},we=(c,u,_)=>{const k=u.component=c.component;if(Rl(c,u,_))if(k.asyncDep&&!k.asyncResolved){oe(k,u,_);return}else k.next=u,El(k.update),k.effect.dirty=!0,k.update();else u.el=c.el,k.vnode=u},U=(c,u,_,k,x,C,E)=>{const $=()=>{if(c.isMounted){let{next:A,bu:z,u:j,parent:J,vnode:se}=c;{const At=_r(c);if(At){A&&(A.el=se.el,oe(c,A,E)),At.asyncDep.then(()=>{c.isUnmounted||$()});return}}let de=A,ve;bt(c,!1),A?(A.el=se.el,oe(c,A,E)):A=se,z&&ps(z),(ve=A.props&&A.props.onVnodeBeforeUpdate)&&He(ve,J,A,se),bt(c,!0);const xe=_s(c),De=c.subTree;c.subTree=xe,G(De,xe,h(De.el),vn(De),c,x,C),A.el=xe.el,de===null&&Bl(c,xe.el),j&&Ee(j,x),(ve=A.props&&A.props.onVnodeUpdated)&&Ee(()=>He(ve,J,A,se),x)}else{let A;const{el:z,props:j}=u,{bm:J,m:se,parent:de}=c,ve=Ft(u);if(bt(c,!1),J&&ps(J),!ve&&(A=j&&j.onVnodeBeforeMount)&&He(A,de,u),bt(c,!0),z&&fs){const xe=()=>{c.subTree=_s(c),fs(z,c.subTree,c,x,null)};ve?u.type.__asyncLoader().then(()=>!c.isUnmounted&&xe()):xe()}else{const xe=c.subTree=_s(c);G(null,xe,_,k,c,x,C),u.el=xe.el}if(se&&Ee(se,x),!ve&&(A=j&&j.onVnodeMounted)){const xe=u;Ee(()=>He(A,de,xe),x)}(u.shapeFlag&256||de&&Ft(de.vnode)&&de.vnode.shapeFlag&256)&&c.a&&Ee(c.a,x),c.isMounted=!0,u=_=k=null}},V=c.effect=new Us($,Oe,()=>Zs(w),c.scope),w=c.update=()=>{V.dirty&&V.run()};w.id=c.uid,bt(c,!0),w()},oe=(c,u,_)=>{u.component=c;const k=c.vnode.props;c.vnode=u,c.next=null,ua(c,u.props,k,_),pa(c,u.children,_),Vt(),Vo(c),Tt()},D=(c,u,_,k,x,C,E,$,V=!1)=>{const w=c&&c.children,A=c?c.shapeFlag:0,z=u.children,{patchFlag:j,shapeFlag:J}=u;if(j>0){if(j&128){_n(w,z,_,k,x,C,E,$,V);return}else if(j&256){tt(w,z,_,k,x,C,E,$,V);return}}J&8?(A&16&&nt(w,x,C),z!==w&&p(_,z)):A&16?J&16?_n(w,z,_,k,x,C,E,$,V):nt(w,x,C,!0):(A&8&&p(_,""),J&16&&R(z,_,k,x,C,E,$,V))},tt=(c,u,_,k,x,C,E,$,V)=>{c=c||Nt,u=u||Nt;const w=c.length,A=u.length,z=Math.min(w,A);let j;for(j=0;jA?nt(c,x,C,!0,!1,z):R(u,_,k,x,C,E,$,V,z)},_n=(c,u,_,k,x,C,E,$,V)=>{let w=0;const A=u.length;let z=c.length-1,j=A-1;for(;w<=z&&w<=j;){const J=c[w],se=u[w]=V?ft(u[w]):Ue(u[w]);if(St(J,se))G(J,se,_,null,x,C,E,$,V);else break;w++}for(;w<=z&&w<=j;){const J=c[z],se=u[j]=V?ft(u[j]):Ue(u[j]);if(St(J,se))G(J,se,_,null,x,C,E,$,V);else break;z--,j--}if(w>z){if(w<=j){const J=j+1,se=Jj)for(;w<=z;)We(c[w],x,C,!0),w++;else{const J=w,se=w,de=new Map;for(w=se;w<=j;w++){const Ie=u[w]=V?ft(u[w]):Ue(u[w]);Ie.key!=null&&de.set(Ie.key,w)}let ve,xe=0;const De=j-se+1;let At=!1,mo=0;const Qt=new Array(De);for(w=0;w=De){We(Ie,x,C,!0);continue}let Je;if(Ie.key!=null)Je=de.get(Ie.key);else for(ve=se;ve<=j;ve++)if(Qt[ve-se]===0&&St(Ie,u[ve])){Je=ve;break}Je===void 0?We(Ie,x,C,!0):(Qt[Je-se]=w+1,Je>=mo?mo=Je:At=!0,G(Ie,u[Je],_,null,x,C,E,$,V),xe++)}const go=At?ya(Qt):Nt;for(ve=go.length-1,w=De-1;w>=0;w--){const Ie=se+w,Je=u[Ie],yo=Ie+1{const{el:C,type:E,transition:$,children:V,shapeFlag:w}=c;if(w&6){yt(c.component.subTree,u,_,k);return}if(w&128){c.suspense.move(u,_,k);return}if(w&64){E.move(c,u,_,Mt);return}if(E===Q){s(C,u,_);for(let z=0;z$.enter(C),x);else{const{leave:z,delayLeave:j,afterLeave:J}=$,se=()=>s(C,u,_),de=()=>{z(C,()=>{se(),J&&J()})};j?j(C,se,de):de()}else s(C,u,_)},We=(c,u,_,k=!1,x=!1)=>{const{type:C,props:E,ref:$,children:V,dynamicChildren:w,shapeFlag:A,patchFlag:z,dirs:j}=c;if($!=null&&On($,null,_,c,!0),A&256){u.ctx.deactivate(c);return}const J=A&1&&j,se=!Ft(c);let de;if(se&&(de=E&&E.onVnodeBeforeUnmount)&&He(de,u,c),A&6)Dr(c.component,_,k);else{if(A&128){c.suspense.unmount(_,k);return}J&&Ye(c,null,u,"beforeUnmount"),A&64?c.type.remove(c,u,_,x,Mt,k):w&&(C!==Q||z>0&&z&64)?nt(w,u,_,!1,!0):(C===Q&&z&384||!x&&A&16)&&nt(V,u,_),k&&_o(c)}(se&&(de=E&&E.onVnodeUnmounted)||J)&&Ee(()=>{de&&He(de,u,c),J&&Ye(c,null,u,"unmounted")},_)},_o=c=>{const{type:u,el:_,anchor:k,transition:x}=c;if(u===Q){jr(_,k);return}if(u===jt){S(c);return}const C=()=>{o(_),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(c.shapeFlag&1&&x&&!x.persisted){const{leave:E,delayLeave:$}=x,V=()=>E(_,C);$?$(c.el,C,V):V()}else C()},jr=(c,u)=>{let _;for(;c!==u;)_=b(c),o(c),c=_;o(u)},Dr=(c,u,_)=>{const{bum:k,scope:x,update:C,subTree:E,um:$}=c;k&&ps(k),x.stop(),C&&(C.active=!1,We(E,c,u,_)),$&&Ee($,u),Ee(()=>{c.isUnmounted=!0},u),u&&u.pendingBranch&&!u.isUnmounted&&c.asyncDep&&!c.asyncResolved&&c.suspenseId===u.pendingId&&(u.deps--,u.deps===0&&u.resolve())},nt=(c,u,_,k=!1,x=!1,C=0)=>{for(let E=C;Ec.shapeFlag&6?vn(c.component.subTree):c.shapeFlag&128?c.suspense.next():b(c.anchor||c.el);let cs=!1;const vo=(c,u,_)=>{c==null?u._vnode&&We(u._vnode,null,null,!0):G(u._vnode||null,c,u,null,null,null,_),cs||(cs=!0,Vo(),An(),cs=!1),u._vnode=c},Mt={p:G,um:We,m:yt,r:_o,mt:be,mc:R,pc:D,pbc:H,n:vn,o:e};let us,fs;return t&&([us,fs]=t(Mt)),{render:vo,hydrate:us,createApp:aa(vo,us)}}function gs({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function bt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function pr(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function hr(e,t,n=!1){const s=e.children,o=t.children;if(q(s)&&q(o))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,r=n[i-1];i-- >0;)n[i]=r,r=t[r];return n}function _r(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:_r(t)}const ba=e=>e.__isTeleport,Q=Symbol.for("v-fgt"),qt=Symbol.for("v-txt"),Re=Symbol.for("v-cmt"),jt=Symbol.for("v-stc"),sn=[];let ze=null;function d(e=!1){sn.push(ze=e?null:[])}function ka(){sn.pop(),ze=sn[sn.length-1]||null}let un=1;function Fo(e){un+=e}function vr(e){return e.dynamicChildren=un>0?ze||Nt:null,ka(),un>0&&ze&&ze.push(e),e}function m(e,t,n,s,o,i){return vr(g(e,t,n,s,o,i,!0))}function X(e,t,n,s,o){return vr(T(e,t,n,s,o,!0))}function Rn(e){return e?e.__v_isVNode===!0:!1}function St(e,t){return e.type===t.type&&e.key===t.key}const ss="__vInternal",mr=({key:e})=>e??null,Vn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ge(e)||Ae(e)||ee(e)?{i:Pe,r:e,k:t,f:!!n}:e:null);function g(e,t=null,n=null,s=0,o=null,i=e===Q?0:1,r=!1,l=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&mr(t),ref:t&&Vn(t),scopeId:Xn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:Pe};return l?(io(a,n),i&128&&e.normalize(a)):n&&(a.shapeFlag|=ge(n)?8:16),un>0&&!r&&ze&&(a.patchFlag>0||i&6)&&a.patchFlag!==32&&ze.push(a),a}const T=xa;function xa(e,t=null,n=null,s=0,o=null,i=!1){if((!e||e===Ji)&&(e=Re),Rn(e)){const l=vt(e,t,!0);return n&&io(l,n),un>0&&!i&&ze&&(l.shapeFlag&6?ze[ze.indexOf(e)]=l:ze.push(l)),l.patchFlag|=-2,l}if(Aa(e)&&(e=e.__vccOpts),t){t=wa(t);let{class:l,style:a}=t;l&&!ge(l)&&(t.class=pe(l)),_e(a)&&(Fi(a)&&!q(a)&&(a=ke({},a)),t.style=Gn(a))}const r=ge(e)?1:Hl(e)?128:ba(e)?64:_e(e)?4:ee(e)?2:0;return g(e,t,n,s,o,r,i,!0)}function wa(e){return e?Fi(e)||ss in e?ke({},e):e:null}function vt(e,t,n=!1){const{props:s,ref:o,patchFlag:i,children:r}=e,l=t?Tn(s||{},t):s;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&mr(l),ref:t&&t.ref?n&&o?q(o)?o.concat(Vn(t)):[o,Vn(t)]:Vn(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:r,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Q?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&vt(e.ssContent),ssFallback:e.ssFallback&&vt(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Le(e=" ",t=0){return T(qt,null,e,t)}function Sa(e,t){const n=T(jt,null,e);return n.staticCount=t,n}function K(e="",t=!1){return t?(d(),X(Re,null,e)):T(Re,null,e)}function Ue(e){return e==null||typeof e=="boolean"?T(Re):q(e)?T(Q,null,e.slice()):typeof e=="object"?ft(e):T(qt,null,String(e))}function ft(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:vt(e)}function io(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(q(t))n=16;else if(typeof t=="object")if(s&65){const o=t.default;o&&(o._c&&(o._d=!1),io(e,o()),o._c&&(o._d=!0));return}else{n=32;const o=t._;!o&&!(ss in t)?t._ctx=Pe:o===3&&Pe&&(Pe.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else ee(t)?(t={default:t,_ctx:Pe},n=32):(t=String(t),s&64?(n=16,t=[Le(t)]):n=8);e.children=t,e.shapeFlag|=n}function Tn(...e){const t={};for(let n=0;nSe||Pe;let Bn,Os;{const e=$i(),t=(n,s)=>{let o;return(o=e[n])||(o=e[n]=[]),o.push(s),i=>{o.length>1?o.forEach(r=>r(i)):o[0](i)}};Bn=t("__VUE_INSTANCE_SETTERS__",n=>Se=n),Os=t("__VUE_SSR_SETTERS__",n=>os=n)}const hn=e=>{const t=Se;return Bn(e),e.scope.on(),()=>{e.scope.off(),Bn(t)}},jo=()=>{Se&&Se.scope.off(),Bn(null)};function gr(e){return e.vnode.shapeFlag&4}let os=!1;function Va(e,t=!1){t&&Os(t);const{props:n,children:s}=e.vnode,o=gr(e);ca(e,n,o,t),da(e,s);const i=o?Ta(e,t):void 0;return t&&Os(!1),i}function Ta(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=en(new Proxy(e.ctx,ta));const{setup:s}=n;if(s){const o=e.setupContext=s.length>1?Ea(e):null,i=hn(e);Vt();const r=ht(s,e,0,[e.props,o]);if(Tt(),i(),xi(r)){if(r.then(jo,jo),t)return r.then(l=>{Do(e,l,t)}).catch(l=>{Yn(l,e,0)});e.asyncDep=r}else Do(e,r,t)}else yr(e,t)}function Do(e,t,n){ee(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:_e(t)&&(e.setupState=zi(t)),yr(e,n)}let Uo;function yr(e,t,n){const s=e.type;if(!e.render){if(!t&&Uo&&!s.render){const o=s.template||so(e).template;if(o){const{isCustomElement:i,compilerOptions:r}=e.appContext.config,{delimiters:l,compilerOptions:a}=s,f=ke(ke({isCustomElement:i,delimiters:l},r),a);s.render=Uo(o,f)}}e.render=s.render||Oe}{const o=hn(e);Vt();try{na(e)}finally{Tt(),o()}}}function La(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return Me(e,"get","$attrs"),t[n]}}))}function Ea(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return La(e)},slots:e.slots,emit:e.emit,expose:t}}function lo(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(zi(en(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in tn)return tn[n](e)},has(t,n){return n in t||n in tn}}))}function Ma(e,t=!0){return ee(e)?e.displayName||e.name:e.name||t&&e.__name}function Aa(e){return ee(e)&&"__vccOpts"in e}const re=(e,t)=>$l(e,t,os);function Hn(e,t,n){const s=arguments.length;return s===2?_e(t)&&!q(t)?Rn(t)?T(e,null,[t]):T(e,t):T(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&Rn(n)&&(n=[n]),T(e,t,n))}const Ia="3.4.21";/** +* @vue/runtime-dom v3.4.21 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const Na="http://www.w3.org/2000/svg",Oa="http://www.w3.org/1998/Math/MathML",dt=typeof document<"u"?document:null,zo=dt&&dt.createElement("template"),Ra={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const o=t==="svg"?dt.createElementNS(Na,e):t==="mathml"?dt.createElementNS(Oa,e):dt.createElement(e,n?{is:n}:void 0);return e==="select"&&s&&s.multiple!=null&&o.setAttribute("multiple",s.multiple),o},createText:e=>dt.createTextNode(e),createComment:e=>dt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>dt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,o,i){const r=n?n.previousSibling:t.lastChild;if(o&&(o===i||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===i||!(o=o.nextSibling)););else{zo.innerHTML=s==="svg"?`${e}`:s==="mathml"?`${e}`:e;const l=zo.content;if(s==="svg"||s==="mathml"){const a=l.firstChild;for(;a.firstChild;)l.appendChild(a.firstChild);l.removeChild(a)}t.insertBefore(l,n)}return[r?r.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},lt="transition",Xt="animation",fn=Symbol("_vtc"),is=(e,{slots:t})=>Hn(Kl,Ba(e),t);is.displayName="Transition";const br={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};is.props=ke({},er,br);const kt=(e,t=[])=>{q(e)?e.forEach(n=>n(...t)):e&&e(...t)},Ko=e=>e?q(e)?e.some(t=>t.length>1):e.length>1:!1;function Ba(e){const t={};for(const M in e)M in br||(t[M]=e[M]);if(e.css===!1)return t;const{name:n="v",type:s,duration:o,enterFromClass:i=`${n}-enter-from`,enterActiveClass:r=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:a=i,appearActiveClass:f=r,appearToClass:p=l,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:b=`${n}-leave-active`,leaveToClass:P=`${n}-leave-to`}=e,Y=Ha(o),G=Y&&Y[0],ne=Y&&Y[1],{onBeforeEnter:le,onEnter:ye,onEnterCancelled:v,onLeave:S,onLeaveCancelled:F,onBeforeAppear:N=le,onAppear:Z=ye,onAppearCancelled:R=v}=t,W=(M,te,be)=>{xt(M,te?p:l),xt(M,te?f:r),be&&be()},H=(M,te)=>{M._isLeaving=!1,xt(M,h),xt(M,P),xt(M,b),te&&te()},ae=M=>(te,be)=>{const we=M?Z:ye,U=()=>W(te,M,be);kt(we,[te,U]),qo(()=>{xt(te,M?a:i),at(te,M?p:l),Ko(we)||Go(te,s,G,U)})};return ke(t,{onBeforeEnter(M){kt(le,[M]),at(M,i),at(M,r)},onBeforeAppear(M){kt(N,[M]),at(M,a),at(M,f)},onEnter:ae(!1),onAppear:ae(!0),onLeave(M,te){M._isLeaving=!0;const be=()=>H(M,te);at(M,h),Da(),at(M,b),qo(()=>{M._isLeaving&&(xt(M,h),at(M,P),Ko(S)||Go(M,s,ne,be))}),kt(S,[M,be])},onEnterCancelled(M){W(M,!1),kt(v,[M])},onAppearCancelled(M){W(M,!0),kt(R,[M])},onLeaveCancelled(M){H(M),kt(F,[M])}})}function Ha(e){if(e==null)return null;if(_e(e))return[ys(e.enter),ys(e.leave)];{const t=ys(e);return[t,t]}}function ys(e){return Jr(e)}function at(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[fn]||(e[fn]=new Set)).add(t)}function xt(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[fn];n&&(n.delete(t),n.size||(e[fn]=void 0))}function qo(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Fa=0;function Go(e,t,n,s){const o=e._endId=++Fa,i=()=>{o===e._endId&&s()};if(n)return setTimeout(i,n);const{type:r,timeout:l,propCount:a}=ja(e,t);if(!r)return s();const f=r+"end";let p=0;const h=()=>{e.removeEventListener(f,b),i()},b=P=>{P.target===e&&++p>=a&&h()};setTimeout(()=>{p(n[Y]||"").split(", "),o=s(`${lt}Delay`),i=s(`${lt}Duration`),r=Wo(o,i),l=s(`${Xt}Delay`),a=s(`${Xt}Duration`),f=Wo(l,a);let p=null,h=0,b=0;t===lt?r>0&&(p=lt,h=r,b=i.length):t===Xt?f>0&&(p=Xt,h=f,b=a.length):(h=Math.max(r,f),p=h>0?r>f?lt:Xt:null,b=p?p===lt?i.length:a.length:0);const P=p===lt&&/\b(transform|all)(,|$)/.test(s(`${lt}Property`).toString());return{type:p,timeout:h,propCount:b,hasTransform:P}}function Wo(e,t){for(;e.lengthJo(n)+Jo(e[s])))}function Jo(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Da(){return document.body.offsetHeight}function Ua(e,t,n){const s=e[fn];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Yo=Symbol("_vod"),za=Symbol("_vsh"),kr=Symbol("");function Ka(e){const t=ro();if(!t)return;const n=t.ut=(o=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(i=>Bs(i,o))},s=()=>{const o=e(t.proxy);Rs(t.subTree,o),n(o)};Xi(s),je(()=>{const o=new MutationObserver(s);o.observe(t.subTree.el.parentNode,{childList:!0}),mt(()=>o.disconnect())})}function Rs(e,t){if(e.shapeFlag&128){const n=e.suspense;e=n.activeBranch,n.pendingBranch&&!n.isHydrating&&n.effects.push(()=>{Rs(n.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)Bs(e.el,t);else if(e.type===Q)e.children.forEach(n=>Rs(n,t));else if(e.type===jt){let{el:n,anchor:s}=e;for(;n&&(Bs(n,t),n!==s);)n=n.nextSibling}}function Bs(e,t){if(e.nodeType===1){const n=e.style;let s="";for(const o in t)n.setProperty(`--${o}`,t[o]),s+=`--${o}: ${t[o]};`;n[kr]=s}}const qa=/(^|;)\s*display\s*:/;function Ga(e,t,n){const s=e.style,o=ge(n);let i=!1;if(n&&!o){if(t)if(ge(t))for(const r of t.split(";")){const l=r.slice(0,r.indexOf(":")).trim();n[l]==null&&Ln(s,l,"")}else for(const r in t)n[r]==null&&Ln(s,r,"");for(const r in n)r==="display"&&(i=!0),Ln(s,r,n[r])}else if(o){if(t!==n){const r=s[kr];r&&(n+=";"+r),s.cssText=n,i=qa.test(n)}}else t&&e.removeAttribute("style");Yo in e&&(e[Yo]=i?s.display:"",e[za]&&(s.display="none"))}const Qo=/\s*!important$/;function Ln(e,t,n){if(q(n))n.forEach(s=>Ln(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=Wa(e,t);Qo.test(n)?e.setProperty(Jt(s),n.replace(Qo,""),"important"):e[s]=n}}const Xo=["Webkit","Moz","ms"],bs={};function Wa(e,t){const n=bs[t];if(n)return n;let s=Ze(t);if(s!=="filter"&&s in e)return bs[t]=s;s=qn(s);for(let o=0;oks||(tc.then(()=>ks=0),ks=Date.now());function sc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Fe(oc(s,n.value),t,5,[s])};return n.value=e,n.attached=nc(),n}function oc(e,t){if(q(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>o=>!o._stopped&&s&&s(o))}else return t}const ni=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ic=(e,t,n,s,o,i,r,l,a)=>{const f=o==="svg";t==="class"?Ua(e,s,f):t==="style"?Ga(e,n,s):pn(t)?Fs(t)||Za(e,t,n,s,r):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):rc(e,t,s,f))?Ya(e,t,s,i,r,l,a):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Ja(e,t,s,f))};function rc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&ni(t)&&ee(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const o=e.tagName;if(o==="IMG"||o==="VIDEO"||o==="CANVAS"||o==="SOURCE")return!1}return ni(t)&&ge(n)?!1:t in e}const lc=["ctrl","shift","alt","meta"],ac={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>lc.some(n=>e[`${n}Key`]&&!t.includes(n))},cc=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(o,...i)=>{for(let r=0;r{const t=fc().createApp(...e),{mount:n}=t;return t.mount=s=>{const o=hc(s);if(o)return n(o,!0,pc(o))},t};function pc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function hc(e){return ge(e)?document.querySelector(e):e}const O=(e,t)=>{const n=e.__vccOpts||e;for(const[s,o]of t)n[s]=o;return n},_c="modulepreload",vc=function(e){return"/blog/"+e},oi={},mc=function(t,n,s){if(!n||n.length===0)return t();const o=document.getElementsByTagName("link");return Promise.all(n.map(i=>{if(i=vc(i),i in oi)return;oi[i]=!0;const r=i.endsWith(".css"),l=r?'[rel="stylesheet"]':"";if(!!s)for(let p=o.length-1;p>=0;p--){const h=o[p];if(h.href===i&&(!r||h.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${i}"]${l}`))return;const f=document.createElement("link");if(f.rel=r?"stylesheet":_c,r||(f.as="script",f.crossOrigin=""),f.href=i,document.head.appendChild(f),r)return new Promise((p,h)=>{f.addEventListener("load",p),f.addEventListener("error",()=>h(new Error(`Unable to preload CSS for ${i}`)))})})).then(()=>t()).catch(i=>{const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=i,window.dispatchEvent(r),!r.defaultPrevented)throw i})};const gc=B({__name:"VPBadge",props:{text:{},type:{}},setup(e){return(t,n)=>(d(),m("span",{class:pe(["VPBadge",t.type??"tip"])},[L(t.$slots,"default",{},()=>[Le(fe(t.text),1)],!0)],2))}});const yc=O(gc,[["__scopeId","data-v-a1867d5d"]]),bc=JSON.parse(`{"lang":"en-US","title":"Sunny's blog","description":"composition api form validator for vue","base":"/blog/","head":[],"appearance":true,"themeConfig":{"outline":[1,3],"sidebar":[{"text":"Introduction","collapsible":true,"items":[{"text":"Getting Started","link":"/getting-started"}]},{"text":"知识碎片(部分文章转载)","collapsible":true,"items":[{"text":"解决npm link本地代码后 react库存在多份实例的问题","link":"/fragment/react-duplicate"},{"text":"面试官:tree-shaking的原理是什么?","link":"/fragment/tree-shaking"},{"text":"禁止别人调试自己的前端页面代码","link":"/fragment/disable-debugger"},{"text":"如何给所有的async函数添加try/catch?","link":"/fragment/auto-try-catch"},{"text":"输入 npm run xxx 后发生了什么?","link":"/fragment/npm-scripts"},{"text":"vue nextTick实现原理,必拿下! ","link":"/fragment/nextTick"},{"text":"JS中数据类型检测四种方式的优缺点","link":"https://juejin.cn/post/6844904115097567239"},{"text":"如何在 ES5 环境下实现一个const ? ","link":"/fragment/const"},{"text":"思考为什么不要使用 return await","link":"/fragment/return-await"},{"text":"React Hooks实现倒计时及避坑","link":"/fragment/react-hooks-timer"},{"text":"使用 react 的 hook 实现一个 useRequest","link":"/fragment/useRequest"},{"text":"前端录制回放系统初体验","link":"/fragment/video"},{"text":"面试官问我JS中forEach能不能跳出循环","link":"/fragment/forEach"},{"text":"请设计一个不能操作DOM和调接口的环境","link":"/fragment/沙盒"},{"text":"前端接口防止重复请求实现方案","link":"/fragment/api-no-repeat"},{"text":"面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:?","link":"/fragment/promise-cancel"},{"text":"前端中 JS 发起的请求可以暂停吗?","link":"/fragment/fetch-pause"},{"text":"如何取消 Fetch 请求","link":"/fragment/fetch"},{"text":"react函数组件中使用useState改变值后立刻获取最新值 ","link":"/fragment/react-useState"},{"text":"一行代码,让网页变为黑白配色","link":"/fragment/黑白"},{"text":"前沿技术","link":"/fragment/前沿技术"},{"text":"手撕babel插件-消灭console!","link":"/fragment/babel-console"},{"text":"Monorepo","link":"/fragment/Monorepo"},{"text":"🔥 微内核架构在前端的实现及其应用","link":"/fragment/微内核架构"},{"text":"什么?后端要一次性返回我10万条数据!且看我这8种方案机智应对!","link":"https://juejin.cn/post/7205101745936416829?"},{"text":"被裁员了,记录下去年刚入职一个月时在组内关于前端基建的技术分享","link":"https://juejin.cn/post/7256393626682163237"},{"text":"2023 年了,你为什么还不用 SWR ?","link":"https://juejin.cn/post/7247028435339591740"},{"text":"如何实现准时的setTimeout","link":"/fragment/setTimeout"},{"text":"requestIdleCallback和requestAnimationFrame详解","link":"https://juejin.cn/post/6844903848981577735"},{"text":"如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?","link":"/fragment/var-array"},{"text":"尾调用优化","link":"https://www.ruanyifeng.com/blog/2015/04/tail-call.html"},{"text":"Git","link":"/fe-utils/git"},{"text":"前端 JavaScript 必会工具库合集","link":"/fe-utils/js工具库"},{"text":"一站式-后台前端解决方案","link":"/article/cms"},{"text":"涌现出来的新tools","link":"/fe-utils/tool"},{"text":"接口设计","link":"/fragment/接口设计"}]},{"text":"前端工程化","collapsible":true,"items":[{"text":"前端工程化 OnePage","link":"/front-end-engineering/engineering-onepage"},{"text":"Npm package:npm发包必知必会","link":"https://juejin.cn/post/7093873173128544270"},{"text":"中高级前端必须掌握的package.json最新最全指南","link":"https://juejin.cn/post/7240805459288522808"},{"text":"前端性能优化方法论","link":"/front-end-engineering/performance"},{"text":"webpack常用拓展","link":"/front-end-engineering/webpack常用拓展"},{"text":"CSS工程化","link":"/front-end-engineering/CSS工程化"},{"text":"CSS 预处理器之SCSS","link":"/front-end-engineering/CSS 预处理器之SCSS"},{"text":"CSS 后处理器之PostCSS","link":"/front-end-engineering/后处理器"},{"text":"JS 兼容性","link":"/front-end-engineering/jscompatibility"},{"text":"webpack5 模块联邦","link":"/front-end-engineering/webpack5-mf"},{"text":"pnpm原理","link":"/front-end-engineering/pnpm原理"},{"text":"模块化","link":"/front-end-engineering/modularization"},{"text":"包管理器","link":"/front-end-engineering/PackageManager"},{"text":"主题切换","link":"/front-end-engineering/theme"},{"text":"纯原生开发 Web Components 超详细分享","link":"https://juejin.cn/post/7158717654369304612"},{"text":"Node 组成原理","link":"/front-end-engineering/node"}]},{"text":"Vuejs 21篇","collapsible":true,"items":[{"text":"那些年,被问烂了的 Vuejs 面试题","link":"/vue/vue-interview"},{"text":"虚拟DOM详解","link":"/vue/vdom"},{"text":"Vuejs 组件通信概览","link":"/vue/component-communication"},{"text":"探索 v-model 原理","link":"/vue/v-model"},{"text":"Vuejs 数据响应原理","link":"/vue/reactive"},{"text":"Vue2 diff算法原理","link":"/vue/diff"},{"text":"Vue 生命周期","link":"/vue/lifecycle"},{"text":"computed","link":"/vue/computed"},{"text":"keep-alive 与 LRU 算法","link":"/vue/keep-alive-LRU"},{"text":"Vue3 新特性","link":"/vue/vue3-onepage"},{"text":"vue-cli 到底帮我们做了什么","link":"/vue/vue-cli"},{"text":"Vue 编译器为什么如此强大","link":"/vue/vue-compile"},{"text":"$nextTick 工作原理","link":"/vue/nextTick"},{"text":"Vue 和 React 的核心区别","link":"/vue/vs"},{"text":"一篇精通 slot","link":"/vue/slot"},{"text":"Vue SSR 服务端渲染","link":"/vue/SSR"},{"text":"Vue 指令篇","link":"/vue/directive"},{"text":"VueRouter 路由从使用到源码","link":"/vue/vue-router"},{"text":"Vuex 思想和源码","link":"/vue/vuex"},{"text":"今天我是面试官","link":"/vue/interviewer"},{"text":"vue 手写篇","link":"/vue/challages"}]},{"text":"React 16篇","collapsible":true,"items":[{"text":"React onePage","link":"/react/"},{"text":"那些年,被问烂了的 React 面试题","link":"/react/react-interview"},{"text":"React Hooks","link":"/react/hooks"},{"text":"React 组件通信","link":"/react/component-communication"},{"text":"React Context","link":"/react/context"},{"text":"React 生命周期","link":"/react/lifecycle"},{"text":"React 事件机制","link":"/react/event"},{"text":"React 渲染原理","link":"/react/render"},{"text":"React 动画","link":"/react/transition"},{"text":"React Router","link":"/react/ReactRouter"},{"text":"JS 应用的状态容器,提供可预测的状态管理 Redux","link":"/react/Redux"},{"text":"视图-仓库-路由","link":"/react/react-redux-router"},{"text":"dva 设计思想","link":"/react/dva"},{"text":"Umi 企业级前端框架","link":"/react/umi"},{"text":"React 调试工具","link":"/react/utils"},{"text":"Fiber","link":"/react/Fiber"}]},{"text":"TypeScript","items":[{"text":"TypeScript OnePage","link":"/ts/TypeScript-onePage"}]},{"text":"前端三件套","collapsible":true,"items":[{"text":"异步处理","link":"/js/异步处理"},{"text":"代理与反射","link":"/js/代理与反射"},{"text":"迭代器和生成器","link":"/js/迭代器和生成器"},{"text":"HTML_CSS 面试题","link":"/html-css/interview"},{"text":"HTML5 OnePage","link":"/html-css/HTML"},{"text":"CSS3 OnePage","link":"/html-css/CSS"},{"text":"Canvas 和 svg","link":"/html-css/canvas-svg"},{"text":"CSS3动画","link":"/html-css/animation"},{"text":"CSS3选择器","link":"/html-css/selector"},{"text":"显示器的成像原理","link":"/html-css/principle"},{"text":"拖拽 API","link":"/html-css/drag"},{"text":"一文搞懂flex:0,1,auto,none","link":"/html-css/flex"},{"text":"CSS 实现多行文本“展开收起”","link":"https://juejin.cn/post/6963904955262435336"},{"text":"钉钉官网首页的炫酷动效” 被我用css新特性轻松破解啦~","link":"https://juejin.cn/post/7274149231367798803"}]},{"text":"算法","collapsible":true,"items":[{"text":"如何在 10 亿数中找出前 1000 大的数","link":"https://juejin.cn/post/6844903880208171015"},{"text":"🔥刷题之探索最优解","link":"/algorithm/🔥刷题之探索最优解"}]},{"text":"面试系列","collapsible":true,"items":[{"text":"面试官:你还有问题要问我吗","link":"/interview/面试官:你还有问题要问我吗"},{"text":"算法笔试","link":"/interview/算法笔试"}]},{"text":"技术之外","collapsible":true,"items":[{"text":"关于写作,这是我目前能想到最全的","link":"https://mp.weixin.qq.com/s/EAU3u7iVnyK5Gi00mgewLw"}]}],"nav":[{"text":"知识碎片(部分文章转载)","collapsible":true,"items":[{"text":"解决npm link本地代码后 react库存在多份实例的问题","link":"/fragment/react-duplicate"},{"text":"面试官:tree-shaking的原理是什么?","link":"/fragment/tree-shaking"},{"text":"禁止别人调试自己的前端页面代码","link":"/fragment/disable-debugger"},{"text":"如何给所有的async函数添加try/catch?","link":"/fragment/auto-try-catch"},{"text":"输入 npm run xxx 后发生了什么?","link":"/fragment/npm-scripts"},{"text":"vue nextTick实现原理,必拿下! ","link":"/fragment/nextTick"},{"text":"JS中数据类型检测四种方式的优缺点","link":"https://juejin.cn/post/6844904115097567239"},{"text":"如何在 ES5 环境下实现一个const ? ","link":"/fragment/const"},{"text":"思考为什么不要使用 return await","link":"/fragment/return-await"},{"text":"React Hooks实现倒计时及避坑","link":"/fragment/react-hooks-timer"},{"text":"使用 react 的 hook 实现一个 useRequest","link":"/fragment/useRequest"},{"text":"前端录制回放系统初体验","link":"/fragment/video"},{"text":"面试官问我JS中forEach能不能跳出循环","link":"/fragment/forEach"},{"text":"请设计一个不能操作DOM和调接口的环境","link":"/fragment/沙盒"},{"text":"前端接口防止重复请求实现方案","link":"/fragment/api-no-repeat"},{"text":"面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:?","link":"/fragment/promise-cancel"},{"text":"前端中 JS 发起的请求可以暂停吗?","link":"/fragment/fetch-pause"},{"text":"如何取消 Fetch 请求","link":"/fragment/fetch"},{"text":"react函数组件中使用useState改变值后立刻获取最新值 ","link":"/fragment/react-useState"},{"text":"一行代码,让网页变为黑白配色","link":"/fragment/黑白"},{"text":"前沿技术","link":"/fragment/前沿技术"},{"text":"手撕babel插件-消灭console!","link":"/fragment/babel-console"},{"text":"Monorepo","link":"/fragment/Monorepo"},{"text":"🔥 微内核架构在前端的实现及其应用","link":"/fragment/微内核架构"},{"text":"什么?后端要一次性返回我10万条数据!且看我这8种方案机智应对!","link":"https://juejin.cn/post/7205101745936416829?"},{"text":"被裁员了,记录下去年刚入职一个月时在组内关于前端基建的技术分享","link":"https://juejin.cn/post/7256393626682163237"},{"text":"2023 年了,你为什么还不用 SWR ?","link":"https://juejin.cn/post/7247028435339591740"},{"text":"如何实现准时的setTimeout","link":"/fragment/setTimeout"},{"text":"requestIdleCallback和requestAnimationFrame详解","link":"https://juejin.cn/post/6844903848981577735"},{"text":"如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?","link":"/fragment/var-array"},{"text":"尾调用优化","link":"https://www.ruanyifeng.com/blog/2015/04/tail-call.html"},{"text":"Git","link":"/fe-utils/git"},{"text":"前端 JavaScript 必会工具库合集","link":"/fe-utils/js工具库"},{"text":"一站式-后台前端解决方案","link":"/article/cms"},{"text":"涌现出来的新tools","link":"/fe-utils/tool"},{"text":"接口设计","link":"/fragment/接口设计"}]},{"text":"前端工程化","collapsible":true,"items":[{"text":"前端工程化 OnePage","link":"/front-end-engineering/engineering-onepage"},{"text":"Npm package:npm发包必知必会","link":"https://juejin.cn/post/7093873173128544270"},{"text":"中高级前端必须掌握的package.json最新最全指南","link":"https://juejin.cn/post/7240805459288522808"},{"text":"前端性能优化方法论","link":"/front-end-engineering/performance"},{"text":"webpack常用拓展","link":"/front-end-engineering/webpack常用拓展"},{"text":"CSS工程化","link":"/front-end-engineering/CSS工程化"},{"text":"CSS 预处理器之SCSS","link":"/front-end-engineering/CSS 预处理器之SCSS"},{"text":"CSS 后处理器之PostCSS","link":"/front-end-engineering/后处理器"},{"text":"JS 兼容性","link":"/front-end-engineering/jscompatibility"},{"text":"webpack5 模块联邦","link":"/front-end-engineering/webpack5-mf"},{"text":"pnpm原理","link":"/front-end-engineering/pnpm原理"},{"text":"模块化","link":"/front-end-engineering/modularization"},{"text":"包管理器","link":"/front-end-engineering/PackageManager"},{"text":"主题切换","link":"/front-end-engineering/theme"},{"text":"纯原生开发 Web Components 超详细分享","link":"https://juejin.cn/post/7158717654369304612"},{"text":"Node 组成原理","link":"/front-end-engineering/node"}]},{"text":"Vuejs 21篇","collapsible":true,"items":[{"text":"那些年,被问烂了的 Vuejs 面试题","link":"/vue/vue-interview"},{"text":"虚拟DOM详解","link":"/vue/vdom"},{"text":"Vuejs 组件通信概览","link":"/vue/component-communication"},{"text":"探索 v-model 原理","link":"/vue/v-model"},{"text":"Vuejs 数据响应原理","link":"/vue/reactive"},{"text":"Vue2 diff算法原理","link":"/vue/diff"},{"text":"Vue 生命周期","link":"/vue/lifecycle"},{"text":"computed","link":"/vue/computed"},{"text":"keep-alive 与 LRU 算法","link":"/vue/keep-alive-LRU"},{"text":"Vue3 新特性","link":"/vue/vue3-onepage"},{"text":"vue-cli 到底帮我们做了什么","link":"/vue/vue-cli"},{"text":"Vue 编译器为什么如此强大","link":"/vue/vue-compile"},{"text":"$nextTick 工作原理","link":"/vue/nextTick"},{"text":"Vue 和 React 的核心区别","link":"/vue/vs"},{"text":"一篇精通 slot","link":"/vue/slot"},{"text":"Vue SSR 服务端渲染","link":"/vue/SSR"},{"text":"Vue 指令篇","link":"/vue/directive"},{"text":"VueRouter 路由从使用到源码","link":"/vue/vue-router"},{"text":"Vuex 思想和源码","link":"/vue/vuex"},{"text":"今天我是面试官","link":"/vue/interviewer"},{"text":"vue 手写篇","link":"/vue/challages"}]},{"text":"React 16篇","collapsible":true,"items":[{"text":"React onePage","link":"/react/"},{"text":"那些年,被问烂了的 React 面试题","link":"/react/react-interview"},{"text":"React Hooks","link":"/react/hooks"},{"text":"React 组件通信","link":"/react/component-communication"},{"text":"React Context","link":"/react/context"},{"text":"React 生命周期","link":"/react/lifecycle"},{"text":"React 事件机制","link":"/react/event"},{"text":"React 渲染原理","link":"/react/render"},{"text":"React 动画","link":"/react/transition"},{"text":"React Router","link":"/react/ReactRouter"},{"text":"JS 应用的状态容器,提供可预测的状态管理 Redux","link":"/react/Redux"},{"text":"视图-仓库-路由","link":"/react/react-redux-router"},{"text":"dva 设计思想","link":"/react/dva"},{"text":"Umi 企业级前端框架","link":"/react/umi"},{"text":"React 调试工具","link":"/react/utils"},{"text":"Fiber","link":"/react/Fiber"}]}],"socialLinks":[{"icon":"github","link":"https://github.com/Sunny-117/blog"}],"footer":{"copyright":"Copyright © 2022-present sunny-117"},"editLink":{"pattern":"https://github.com/Sunny-117/blog","text":"Edit this page on Gitlab"},"lastUpdatedText":"Last Updated"},"locales":{},"langs":{},"scrollOffset":90,"cleanUrls":"disabled"}`),rs=/^[a-z]+:/i,kc=/^pathname:\/\//,ii="vitepress-theme-appearance",Te=typeof window<"u",xr={relativePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0};function xc(e,t){t.sort((n,s)=>{const o=s.split("/").length-n.split("/").length;return o!==0?o:s.length-n.length});for(const n of t)if(e.startsWith(n))return n}function ri(e,t){const n=xc(t,Object.keys(e));return n?e[n]:void 0}function wc(e){const{locales:t}=e.themeConfig||{},n=e.locales;return t&&n?Object.keys(t).reduce((s,o)=>(s[o]={label:t[o].label,lang:n[o].lang},s),{}):{}}function Sc(e,t){t=Pc(e,t);const n=ri(e.locales||{},t),s=ri(e.themeConfig.locales||{},t);return Object.assign({},e,n,{themeConfig:Object.assign({},e.themeConfig,s,{locales:{}}),lang:(n||e).lang,locales:{},langs:wc(e)})}function wr(e,t){const n=t.title||e.title,s=t.titleTemplate??e.titleTemplate;if(typeof s=="string"&&s.includes(":title"))return s.replace(/:title/g,n);const o=$c(e.title,s);return`${n}${o}`}function $c(e,t){return t===!1?"":t===!0||t===void 0?` | ${e}`:e===t?"":` | ${t}`}function Pc(e,t){if(!Te)return t;const n=e.base,s=n.endsWith("/")?n.slice(0,-1):n;return t.slice(s.length)}function Cc(e,t){const[n,s]=t;if(n!=="meta")return!1;const o=Object.entries(s)[0];return o==null?!1:e.some(([i,r])=>i===n&&r[o[0]]===o[1])}function Vc(e,t){return[...e.filter(n=>!Cc(t,n)),...t]}const Tc=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,Lc=/^[a-z]:/i;function li(e){const t=Lc.exec(e),n=t?t[0]:"";return n+e.slice(n.length).replace(Tc,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}function Ec(e,t){return`${e}${t}`.replace(/\/+/g,"/")}function dn(e){return rs.test(e)?e:Ec(Gt.value.base,e)}function Sr(e){let t=e.replace(/\.html$/,"");if(t=decodeURIComponent(t),t.endsWith("/")&&(t+="index"),Te){const n="/blog/";t=li(t.slice(n.length).replace(/\//g,"_")||"index")+".md";const s=__VP_HASH_MAP__[t.toLowerCase()];t=`${n}assets/${t}.${s}.js`}else t=`./${li(t.slice(1).replace(/\//g,"_"))}.md.js`;return t}const $r=Symbol(),Gt=Pl(bc);function Mc(e){const t=re(()=>Sc(Gt.value,e.path));return{site:t,theme:re(()=>t.value.themeConfig),page:re(()=>e.data),frontmatter:re(()=>e.data.frontmatter),lang:re(()=>t.value.lang),localePath:re(()=>{const{langs:n,lang:s}=t.value,o=Object.keys(n).find(i=>n[i].lang===s);return dn(o||"/")}),title:re(()=>wr(t.value,e.data)),description:re(()=>e.data.description||t.value.description),isDark:he(!1)}}function ue(){const e=Ke($r);if(!e)throw new Error("vitepress data not properly injected in app");return e}const Pr=Symbol(),ai="http://a.com",Ac=()=>({path:"/",component:null,data:xr});function Ic(e,t){const n=Jn(Ac()),s={route:n,go:o};async function o(l=Te?location.href:"/"){var f,p;await((f=s.onBeforeRouteChange)==null?void 0:f.call(s,l));const a=new URL(l,ai);Gt.value.cleanUrls==="disabled"&&!a.pathname.endsWith("/")&&!a.pathname.endsWith(".html")&&(a.pathname+=".html",l=a.pathname+a.search+a.hash),Te&&(history.replaceState({scrollPosition:window.scrollY},document.title),history.pushState(null,"",l)),await r(l),await((p=s.onAfterRouteChanged)==null?void 0:p.call(s,l))}let i=null;async function r(l,a=0,f=!1){const p=new URL(l,ai),h=i=p.pathname;try{let b=await e(h);if(i===h){i=null;const{default:P,__pageData:Y}=b;if(!P)throw new Error(`Invalid route component: ${P}`);n.path=Te?h:dn(h),n.component=en(P),n.data=en(Y),Te&&Xs(()=>{if(p.hash&&!a){let G=null;try{G=document.querySelector(decodeURIComponent(p.hash))}catch(ne){console.warn(ne)}if(G){ci(G,p.hash);return}}window.scrollTo(0,a)})}}catch(b){if(!/fetch/.test(b.message)&&!/^\/404(\.html|\/)?$/.test(l)&&console.error(b),!f)try{const P=await fetch(Gt.value.base+"hashmap.json");window.__VP_HASH_MAP__=await P.json(),await r(l,a,!0);return}catch{}i===h&&(i=null,n.path=Te?h:dn(h),n.component=t?en(t):null,n.data=xr)}}return Te&&(window.addEventListener("click",l=>{if(l.target.closest("button"))return;const f=l.target.closest("a");if(f&&!f.closest(".vp-raw")&&!f.download){const{href:p,origin:h,pathname:b,hash:P,search:Y,target:G}=f,ne=window.location,le=b.match(/\.\w+$/);!l.ctrlKey&&!l.shiftKey&&!l.altKey&&!l.metaKey&&G!=="_blank"&&h===ne.origin&&!(le&&le[0]!==".html")&&(l.preventDefault(),b===ne.pathname&&Y===ne.search?P&&P!==ne.hash&&(history.pushState(null,"",P),window.dispatchEvent(new Event("hashchange")),ci(f,P,f.classList.contains("header-anchor"))):o(p))}},{capture:!0}),window.addEventListener("popstate",l=>{r(location.href,l.state&&l.state.scrollPosition||0)}),window.addEventListener("hashchange",l=>{l.preventDefault()})),s}function Nc(){const e=Ke(Pr);if(!e)throw new Error("useRouter() is called without provider.");return e}function gt(){return Nc().route}function ci(e,t,n=!1){let s=null;try{s=e.classList.contains("header-anchor")?e:document.querySelector(decodeURIComponent(t))}catch(o){console.warn(o)}if(s){let o=Gt.value.scrollOffset;typeof o=="string"&&(o=document.querySelector(o).getBoundingClientRect().bottom+24);const i=parseInt(window.getComputedStyle(s).paddingTop,10),r=window.scrollY+s.getBoundingClientRect().top-o+i;!n||Math.abs(r-window.scrollY)>window.innerHeight?window.scrollTo(0,r):window.scrollTo({left:0,top:r,behavior:"smooth"})}}const Oc=B({name:"VitePressContent",props:{onContentUpdated:Function},setup(e){const t=gt();return no(()=>{var n;(n=e.onContentUpdated)==null||n.call(e)}),()=>Hn("div",{style:{position:"relative"}},[t.component?Hn(t.component):null])}}),Cr=/#.*$/,Rc=/(index)?\.(md|html)$/,Bc=typeof window<"u",Hc=he(Bc?location.hash:"");function Fc(e){return rs.test(e)}function jc(e,t){let n,s=!1;return()=>{n&&clearTimeout(n),s?n=setTimeout(e,t):(e(),s=!0,setTimeout(()=>{s=!1},t))}}function Yt(e,t,n=!1){if(t===void 0)return!1;if(e=fi(`/${e}`),n)return new RegExp(t).test(e);if(fi(t)!==e)return!1;const s=t.match(Cr);return s?Hc.value===s[0]:!0}function ui(e){return/^\//.test(e)?e:`/${e}`}function fi(e){return decodeURI(e).replace(Cr,"").replace(Rc,"")}function Fn(e){if(Fc(e))return e.replace(kc,"");const{site:t}=ue(),{pathname:n,search:s,hash:o}=new URL(e,"http://example.com"),i=n.endsWith("/")||n.endsWith(".html")?e:`${n.replace(/(\.md)?$/,t.value.cleanUrls==="disabled"?".html":"")}${s}${o}`;return dn(i)}function Vr(e,t){if(Array.isArray(e))return e;if(e==null)return[];t=ui(t);const n=Object.keys(e).sort((s,o)=>o.split("/").length-s.split("/").length).find(s=>t.startsWith(ui(s)));return n?e[n]:[]}function Dc(e){const t=[];function n(s){for(const o of s)o.link&&t.push({...o,link:o.link}),"items"in o&&n(o.items)}for(const s of e)n(s.items);return t}function et(){const e=gt(),{theme:t,frontmatter:n}=ue(),s=he(!1),o=re(()=>{const p=t.value.sidebar,h=e.data.relativePath;return p?Vr(p,h):[]}),i=re(()=>n.value.sidebar!==!1&&o.value.length>0&&n.value.layout!=="home"),r=re(()=>n.value.layout!=="home"&&n.value.aside!==!1);function l(){s.value=!0}function a(){s.value=!1}function f(){s.value?a():l()}return{isOpen:s,sidebar:o,hasSidebar:i,hasAside:r,open:l,close:a,toggle:f}}function Uc(e,t){let n;Kt(()=>{n=e.value?document.activeElement:void 0}),je(()=>{window.addEventListener("keyup",s)}),mt(()=>{window.removeEventListener("keyup",s)});function s(o){o.key==="Escape"&&e.value&&(t(),n==null||n.focus())}}const zc=B({__name:"VPSkipLink",setup(e){const t=gt(),n=he();Xe(()=>t.path,()=>n.value.focus());function s({target:o}){const i=document.querySelector(o.hash);if(i){const r=()=>{i.removeAttribute("tabindex"),i.removeEventListener("blur",r)};i.setAttribute("tabindex","-1"),i.addEventListener("blur",r),i.focus(),window.scrollTo(0,0)}}return(o,i)=>(d(),m(Q,null,[g("span",{ref_key:"backToTop",ref:n,tabindex:"-1"},null,512),g("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:s}," Skip to content ")],64))}});const Kc=O(zc,[["__scopeId","data-v-87df81e5"]]),qc={key:0,class:"VPBackdrop"},Gc=B({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(e){return(t,n)=>(d(),X(is,{name:"fade"},{default:I(()=>[t.show?(d(),m("div",qc)):K("",!0)]),_:1}))}});const Wc=O(Gc,[["__scopeId","data-v-0b9fab8a"]]);function Jc(){const e=he(!1);function t(){e.value=!0,window.addEventListener("resize",o)}function n(){e.value=!1,window.removeEventListener("resize",o)}function s(){e.value?n():t()}function o(){window.outerWidth>=768&&n()}const i=gt();return Xe(()=>i.path,n),{isScreenOpen:e,openScreen:t,closeScreen:n,toggleScreen:s}}const Yc=["src","alt"],Qc={inheritAttrs:!1},Xc=B({...Qc,__name:"VPImage",props:{image:{},alt:{}},setup(e){return(t,n)=>{const s=Lt("VPImage",!0);return t.image?(d(),m(Q,{key:0},[typeof t.image=="string"||"src"in t.image?(d(),m("img",Tn({key:0,class:"VPImage"},typeof t.image=="string"?t.$attrs:{...t.image,...t.$attrs},{src:y(dn)(typeof t.image=="string"?t.image:t.image.src),alt:t.alt??(typeof t.image=="string"?"":t.image.alt||"")}),null,16,Yc)):(d(),m(Q,{key:1},[T(s,Tn({class:"dark",image:t.image.dark,alt:typeof t.image.dark=="string"?t.image.alt:t.image.dark.alt||t.image.alt},t.$attrs),null,16,["image","alt"]),T(s,Tn({class:"light",image:t.image.light,alt:typeof t.image.light=="string"?t.image.alt:t.image.light.alt||t.image.alt},t.$attrs),null,16,["image","alt"])],64))],64)):K("",!0)}}});const Tr=O(Xc,[["__scopeId","data-v-f3b4d64a"]]),Zc=["href"],eu=B({__name:"VPNavBarTitle",setup(e){const{site:t,theme:n}=ue(),{hasSidebar:s}=et();return(o,i)=>(d(),m("div",{class:pe(["VPNavBarTitle",{"has-sidebar":y(s)}])},[g("a",{class:"title",href:y(t).base},[L(o.$slots,"nav-bar-title-before",{},void 0,!0),T(Tr,{class:"logo",image:y(n).logo},null,8,["image"]),y(n).siteTitle?(d(),m(Q,{key:0},[Le(fe(y(n).siteTitle),1)],64)):y(n).siteTitle===void 0?(d(),m(Q,{key:1},[Le(fe(y(t).title),1)],64)):K("",!0),L(o.$slots,"nav-bar-title-after",{},void 0,!0)],8,Zc)],2))}});const tu=O(eu,[["__scopeId","data-v-eb7e926a"]]);const nu={key:0,class:"VPNavBarSearch"},su={type:"button",class:"DocSearch DocSearch-Button","aria-label":"Search"},ou={class:"DocSearch-Button-Container"},iu=g("svg",{class:"DocSearch-Search-Icon",width:"20",height:"20",viewBox:"0 0 20 20"},[g("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none","fill-rule":"evenodd","stroke-linecap":"round","stroke-linejoin":"round"})],-1),ru={class:"DocSearch-Button-Placeholder"},lu=g("span",{class:"DocSearch-Button-Keys"},[g("kbd",{class:"DocSearch-Button-Key"}),g("kbd",{class:"DocSearch-Button-Key"},"K")],-1),au=B({__name:"VPNavBarSearch",setup(e){Ka(r=>({"7cd2b7b1":o.value}));const t=()=>null,{theme:n}=ue(),s=he(!1),o=he("'Meta'");je(()=>{if(!n.value.algolia)return;o.value=/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?"'⌘'":"'Ctrl'";const r=a=>{a.key==="k"&&(a.ctrlKey||a.metaKey)&&(a.preventDefault(),i(),l())},l=()=>{window.removeEventListener("keydown",r)};window.addEventListener("keydown",r),mt(l)});function i(){s.value||(s.value=!0)}return(r,l)=>{var a;return y(n).algolia?(d(),m("div",nu,[s.value?(d(),X(y(t),{key:0})):(d(),m("div",{key:1,id:"docsearch",onClick:i},[g("button",su,[g("span",ou,[iu,g("span",ru,fe(((a=y(n).algolia)==null?void 0:a.buttonText)||"Search"),1)]),lu])]))])):K("",!0)}}});const cu={},uu={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",height:"24px",viewBox:"0 0 24 24",width:"24px"},fu=g("path",{d:"M0 0h24v24H0V0z",fill:"none"},null,-1),du=g("path",{d:"M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z"},null,-1),pu=[fu,du];function hu(e,t){return d(),m("svg",uu,pu)}const _u=O(cu,[["render",hu]]),vu=B({__name:"VPLink",props:{href:{},noIcon:{type:Boolean}},setup(e){const t=e,n=re(()=>t.href&&rs.test(t.href));return(s,o)=>(d(),X(to(s.href?"a":"span"),{class:pe(["VPLink",{link:s.href}]),href:s.href?y(Fn)(s.href):void 0,target:n.value?"_blank":void 0,rel:n.value?"noreferrer":void 0},{default:I(()=>[L(s.$slots,"default",{},void 0,!0),n.value&&!s.noIcon?(d(),X(_u,{key:0,class:"icon"})):K("",!0)]),_:3},8,["class","href","target","rel"]))}});const Et=O(vu,[["__scopeId","data-v-a424041d"]]),mu=B({__name:"VPNavBarMenuLink",props:{item:{}},setup(e){const{page:t}=ue();return(n,s)=>(d(),X(Et,{class:pe({VPNavBarMenuLink:!0,active:y(Yt)(y(t).relativePath,n.item.activeMatch||n.item.link,!!n.item.activeMatch)}),href:n.item.link,noIcon:!0},{default:I(()=>[Le(fe(n.item.text),1)]),_:1},8,["class","href"]))}});const gu=O(mu,[["__scopeId","data-v-cf94e6e3"]]),ao=he();let Lr=!1,ws=0;function yu(e){const t=he(!1);if(typeof window<"u"){!Lr&&bu(),ws++;const n=Xe(ao,s=>{var o,i,r;s===e.el.value||(o=e.el.value)!=null&&o.contains(s)?(t.value=!0,(i=e.onFocus)==null||i.call(e)):(t.value=!1,(r=e.onBlur)==null||r.call(e))});mt(()=>{n(),ws--,ws||ku()})}return Ws(t)}function bu(){document.addEventListener("focusin",Er),Lr=!0,ao.value=document.activeElement}function ku(){document.removeEventListener("focusin",Er)}function Er(){ao.value=document.activeElement}const xu={},wu={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Su=g("path",{d:"M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"},null,-1),$u=[Su];function Pu(e,t){return d(),m("svg",wu,$u)}const Mr=O(xu,[["render",Pu]]),Cu={},Vu={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Tu=g("circle",{cx:"12",cy:"12",r:"2"},null,-1),Lu=g("circle",{cx:"19",cy:"12",r:"2"},null,-1),Eu=g("circle",{cx:"5",cy:"12",r:"2"},null,-1),Mu=[Tu,Lu,Eu];function Au(e,t){return d(),m("svg",Vu,Mu)}const Iu=O(Cu,[["render",Au]]),Nu={class:"VPMenuLink"},Ou=B({__name:"VPMenuLink",props:{item:{}},setup(e){const{page:t}=ue();return(n,s)=>(d(),m("div",Nu,[T(Et,{class:pe({active:y(Yt)(y(t).relativePath,n.item.activeMatch||n.item.link)}),href:n.item.link},{default:I(()=>[Le(fe(n.item.text),1)]),_:1},8,["class","href"])]))}});const ls=O(Ou,[["__scopeId","data-v-10c604e9"]]),Ru={class:"VPMenuGroup"},Bu={key:0,class:"title"},Hu=B({__name:"VPMenuGroup",props:{text:{},items:{}},setup(e){return(t,n)=>(d(),m("div",Ru,[t.text?(d(),m("p",Bu,fe(t.text),1)):K("",!0),(d(!0),m(Q,null,Ce(t.items,s=>(d(),m(Q,null,["link"in s?(d(),X(ls,{key:0,item:s},null,8,["item"])):K("",!0)],64))),256))]))}});const Fu=O(Hu,[["__scopeId","data-v-ab2e3e0e"]]),ju={class:"VPMenu"},Du={key:0,class:"items"},Uu=B({__name:"VPMenu",props:{items:{}},setup(e){return(t,n)=>(d(),m("div",ju,[t.items?(d(),m("div",Du,[(d(!0),m(Q,null,Ce(t.items,s=>(d(),m(Q,{key:s.text},["link"in s?(d(),X(ls,{key:0,item:s},null,8,["item"])):(d(),X(Fu,{key:1,text:s.text,items:s.items},null,8,["text","items"]))],64))),128))])):K("",!0),L(t.$slots,"default",{},void 0,!0)]))}});const zu=O(Uu,[["__scopeId","data-v-1e097e78"]]),Ku=["aria-expanded","aria-label"],qu={key:0,class:"text"},Gu={class:"menu"},Wu=B({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(e){const t=he(!1),n=he();yu({el:n,onBlur:s});function s(){t.value=!1}return(o,i)=>(d(),m("div",{class:"VPFlyout",ref_key:"el",ref:n,onMouseenter:i[1]||(i[1]=r=>t.value=!0),onMouseleave:i[2]||(i[2]=r=>t.value=!1)},[g("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":t.value,"aria-label":o.label,onClick:i[0]||(i[0]=r=>t.value=!t.value)},[o.button||o.icon?(d(),m("span",qu,[o.icon?(d(),X(to(o.icon),{key:0,class:"option-icon"})):K("",!0),Le(" "+fe(o.button)+" ",1),T(Mr,{class:"text-icon"})])):(d(),X(Iu,{key:1,class:"icon"}))],8,Ku),g("div",Gu,[T(zu,{items:o.items},{default:I(()=>[L(o.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}});const co=O(Wu,[["__scopeId","data-v-9b25190a"]]),Ju=B({__name:"VPNavBarMenuGroup",props:{item:{}},setup(e){const{page:t}=ue();return(n,s)=>(d(),X(co,{class:pe({VPNavBarMenuGroup:!0,active:y(Yt)(y(t).relativePath,n.item.activeMatch,!!n.item.activeMatch)}),button:n.item.text,items:n.item.items},null,8,["class","button","items"]))}}),Yu=e=>(qe("data-v-13efb78a"),e=e(),Ge(),e),Qu={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},Xu=Yu(()=>g("span",{id:"main-nav-aria-label",class:"visually-hidden"},"Main Navigation",-1)),Zu=B({__name:"VPNavBarMenu",setup(e){const{theme:t}=ue();return(n,s)=>y(t).nav?(d(),m("nav",Qu,[Xu,(d(!0),m(Q,null,Ce(y(t).nav,o=>(d(),m(Q,{key:o.text},["link"in o?(d(),X(gu,{key:0,item:o},null,8,["item"])):(d(),X(Ju,{key:1,item:o},null,8,["item"]))],64))),128))])):K("",!0)}});const ef=O(Zu,[["__scopeId","data-v-13efb78a"]]),tf={},nf={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},sf=g("path",{d:"M0 0h24v24H0z",fill:"none"},null,-1),of=g("path",{d:" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z ",class:"css-c4d79v"},null,-1),rf=[sf,of];function lf(e,t){return d(),m("svg",nf,rf)}const Ar=O(tf,[["render",lf]]),af={class:"items"},cf={class:"title"},uf=B({__name:"VPNavBarTranslations",setup(e){const{theme:t}=ue();return(n,s)=>y(t).localeLinks?(d(),X(co,{key:0,class:"VPNavBarTranslations",icon:Ar},{default:I(()=>[g("div",af,[g("p",cf,fe(y(t).localeLinks.text),1),(d(!0),m(Q,null,Ce(y(t).localeLinks.items,o=>(d(),X(ls,{key:o.link,item:o},null,8,["item"]))),128))])]),_:1})):K("",!0)}});const ff=O(uf,[["__scopeId","data-v-daa8938c"]]);const df={},pf={class:"VPSwitch",type:"button",role:"switch"},hf={class:"check"},_f={key:0,class:"icon"};function vf(e,t){return d(),m("button",pf,[g("span",hf,[e.$slots.default?(d(),m("span",_f,[L(e.$slots,"default",{},void 0,!0)])):K("",!0)])])}const mf=O(df,[["render",vf],["__scopeId","data-v-b7a12636"]]),gf={},yf={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},bf=Sa('',9),kf=[bf];function xf(e,t){return d(),m("svg",yf,kf)}const wf=O(gf,[["render",xf]]),Sf={},$f={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},Pf=g("path",{d:"M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"},null,-1),Cf=[Pf];function Vf(e,t){return d(),m("svg",$f,Cf)}const Tf=O(Sf,[["render",Vf]]),Lf=B({__name:"VPSwitchAppearance",setup(e){const{site:t,isDark:n}=ue(),s=he(!1),o=typeof localStorage<"u"?i():()=>{};je(()=>{s.value=document.documentElement.classList.contains("dark")});function i(){const r=window.matchMedia("(prefers-color-scheme: dark)"),l=document.documentElement.classList;let a=localStorage.getItem(ii),f=t.value.appearance==="dark"&&a==null||(a==="auto"||a==null?r.matches:a==="dark");r.onchange=b=>{a==="auto"&&h(f=b.matches)};function p(){h(f=!f),a=f?r.matches?"auto":"dark":r.matches?"light":"auto",localStorage.setItem(ii,a)}function h(b){const P=document.createElement("style");P.type="text/css",P.appendChild(document.createTextNode(`:not(.VPSwitchAppearance):not(.VPSwitchAppearance *) { + -webkit-transition: none !important; + -moz-transition: none !important; + -o-transition: none !important; + -ms-transition: none !important; + transition: none !important; +}`)),document.head.appendChild(P),s.value=b,l[b?"add":"remove"]("dark"),window.getComputedStyle(P).opacity,document.head.removeChild(P)}return p}return Xe(s,r=>{n.value=r}),(r,l)=>(d(),X(mf,{class:"VPSwitchAppearance","aria-label":"toggle dark mode","aria-checked":s.value,onClick:y(o)},{default:I(()=>[T(wf,{class:"sun"}),T(Tf,{class:"moon"})]),_:1},8,["aria-checked","onClick"]))}});const uo=O(Lf,[["__scopeId","data-v-200b9c2a"]]),Ef={key:0,class:"VPNavBarAppearance"},Mf=B({__name:"VPNavBarAppearance",setup(e){const{site:t}=ue();return(n,s)=>y(t).appearance?(d(),m("div",Ef,[T(uo)])):K("",!0)}});const Af=O(Mf,[["__scopeId","data-v-54695850"]]),If={discord:'Discord',facebook:'Facebook',github:'GitHub',instagram:'Instagram',linkedin:'LinkedIn',slack:'Slack',twitter:'Twitter',youtube:'YouTube'},Nf=["href","innerHTML"],Of=B({__name:"VPSocialLink",props:{icon:{},link:{}},setup(e){const t=e,n=re(()=>typeof t.icon=="object"?t.icon.svg:If[t.icon]);return(s,o)=>(d(),m("a",{class:"VPSocialLink",href:s.link,target:"_blank",rel:"noopener",innerHTML:n.value},null,8,Nf))}});const Rf=O(Of,[["__scopeId","data-v-35fc4184"]]),Bf={class:"VPSocialLinks"},Hf=B({__name:"VPSocialLinks",props:{links:{}},setup(e){return(t,n)=>(d(),m("div",Bf,[(d(!0),m(Q,null,Ce(t.links,({link:s,icon:o})=>(d(),X(Rf,{key:s,icon:o,link:s},null,8,["icon","link"]))),128))]))}});const fo=O(Hf,[["__scopeId","data-v-6726bf19"]]),Ff=B({__name:"VPNavBarSocialLinks",setup(e){const{theme:t}=ue();return(n,s)=>y(t).socialLinks?(d(),X(fo,{key:0,class:"VPNavBarSocialLinks",links:y(t).socialLinks},null,8,["links"])):K("",!0)}});const jf=O(Ff,[["__scopeId","data-v-d4d4d140"]]),Df=e=>(qe("data-v-60f214ab"),e=e(),Ge(),e),Uf={key:0,class:"group"},zf={class:"trans-title"},Kf={key:1,class:"group"},qf={class:"item appearance"},Gf=Df(()=>g("p",{class:"label"},"Appearance",-1)),Wf={class:"appearance-action"},Jf={key:2,class:"group"},Yf={class:"item social-links"},Qf=B({__name:"VPNavBarExtra",setup(e){const{site:t,theme:n}=ue(),s=re(()=>n.value.localeLinks||t.value.appearance||n.value.socialLinks);return(o,i)=>s.value?(d(),X(co,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:I(()=>[y(n).localeLinks?(d(),m("div",Uf,[g("p",zf,fe(y(n).localeLinks.text),1),(d(!0),m(Q,null,Ce(y(n).localeLinks.items,r=>(d(),X(ls,{key:r.link,item:r},null,8,["item"]))),128))])):K("",!0),y(t).appearance?(d(),m("div",Kf,[g("div",qf,[Gf,g("div",Wf,[T(uo)])])])):K("",!0),y(n).socialLinks?(d(),m("div",Jf,[g("div",Yf,[T(fo,{class:"social-links-list",links:y(n).socialLinks},null,8,["links"])])])):K("",!0)]),_:1})):K("",!0)}});const Xf=O(Qf,[["__scopeId","data-v-60f214ab"]]),Zf=e=>(qe("data-v-9e7b0a49"),e=e(),Ge(),e),ed=["aria-expanded"],td=Zf(()=>g("span",{class:"container"},[g("span",{class:"top"}),g("span",{class:"middle"}),g("span",{class:"bottom"})],-1)),nd=[td],sd=B({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(e){return(t,n)=>(d(),m("button",{type:"button",class:pe(["VPNavBarHamburger",{active:t.active}]),"aria-label":"mobile navigation","aria-expanded":t.active,"aria-controls":"VPNavScreen",onClick:n[0]||(n[0]=s=>t.$emit("click"))},nd,10,ed))}});const od=O(sd,[["__scopeId","data-v-9e7b0a49"]]),id={class:"container"},rd={class:"content"},ld=B({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(e){const{hasSidebar:t}=et();return(n,s)=>(d(),m("div",{class:pe(["VPNavBar",{"has-sidebar":y(t)}])},[g("div",id,[T(tu,null,{"nav-bar-title-before":I(()=>[L(n.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":I(()=>[L(n.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3}),g("div",rd,[L(n.$slots,"nav-bar-content-before",{},void 0,!0),T(au,{class:"search"}),T(ef,{class:"menu"}),T(ff,{class:"translations"}),T(Af,{class:"appearance"}),T(jf,{class:"social-links"}),T(Xf,{class:"extra"}),L(n.$slots,"nav-bar-content-after",{},void 0,!0),T(od,{class:"hamburger",active:n.isScreenOpen,onClick:s[0]||(s[0]=o=>n.$emit("toggle-screen"))},null,8,["active"])])])],2))}});const ad=O(ld,[["__scopeId","data-v-122e577b"]]);function cd(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1),Dt=[],Dn=!1,ho=-1,on=void 0,$t=void 0,rn=void 0,Ir=function(t){return Dt.some(function(n){return!!(n.options.allowTouchMove&&n.options.allowTouchMove(t))})},Un=function(t){var n=t||window.event;return Ir(n.target)||n.touches.length>1?!0:(n.preventDefault&&n.preventDefault(),!1)},ud=function(t){if(rn===void 0){var n=!!t&&t.reserveScrollBarGap===!0,s=window.innerWidth-document.documentElement.clientWidth;if(n&&s>0){var o=parseInt(window.getComputedStyle(document.body).getPropertyValue("padding-right"),10);rn=document.body.style.paddingRight,document.body.style.paddingRight=o+s+"px"}}on===void 0&&(on=document.body.style.overflow,document.body.style.overflow="hidden")},fd=function(){rn!==void 0&&(document.body.style.paddingRight=rn,rn=void 0),on!==void 0&&(document.body.style.overflow=on,on=void 0)},dd=function(){return window.requestAnimationFrame(function(){if($t===void 0){$t={position:document.body.style.position,top:document.body.style.top,left:document.body.style.left};var t=window,n=t.scrollY,s=t.scrollX,o=t.innerHeight;document.body.style.position="fixed",document.body.style.top=-n,document.body.style.left=-s,setTimeout(function(){return window.requestAnimationFrame(function(){var i=o-window.innerHeight;i&&n>=o&&(document.body.style.top=-(n+i))})},300)}})},pd=function(){if($t!==void 0){var t=-parseInt(document.body.style.top,10),n=-parseInt(document.body.style.left,10);document.body.style.position=$t.position,document.body.style.top=$t.top,document.body.style.left=$t.left,window.scrollTo(n,t),$t=void 0}},hd=function(t){return t?t.scrollHeight-t.scrollTop<=t.clientHeight:!1},_d=function(t,n){var s=t.targetTouches[0].clientY-ho;return Ir(t.target)?!1:n&&n.scrollTop===0&&s>0||hd(n)&&s<0?Un(t):(t.stopPropagation(),!0)},Nr=function(t,n){if(!t){console.error("disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.");return}if(!Dt.some(function(o){return o.targetElement===t})){var s={targetElement:t,options:n||{}};Dt=[].concat(cd(Dt),[s]),jn?dd():ud(n),jn&&(t.ontouchstart=function(o){o.targetTouches.length===1&&(ho=o.targetTouches[0].clientY)},t.ontouchmove=function(o){o.targetTouches.length===1&&_d(o,t)},Dn||(document.addEventListener("touchmove",Un,po?{passive:!1}:void 0),Dn=!0))}},Or=function(){jn&&(Dt.forEach(function(t){t.targetElement.ontouchstart=null,t.targetElement.ontouchmove=null}),Dn&&(document.removeEventListener("touchmove",Un,po?{passive:!1}:void 0),Dn=!1),ho=-1),jn?pd():fd(),Dt=[]};const vd=B({__name:"VPNavScreenMenuLink",props:{text:{},link:{}},setup(e){const t=Ke("close-screen");return(n,s)=>(d(),X(Et,{class:"VPNavScreenMenuLink",href:n.link,onClick:y(t)},{default:I(()=>[Le(fe(n.text),1)]),_:1},8,["href","onClick"]))}});const md=O(vd,[["__scopeId","data-v-814ed9a7"]]),gd={},yd={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},bd=g("path",{d:"M18.9,10.9h-6v-6c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-6c-0.6,0-1,0.4-1,1s0.4,1,1,1h6v6c0,0.6,0.4,1,1,1s1-0.4,1-1v-6h6c0.6,0,1-0.4,1-1S19.5,10.9,18.9,10.9z"},null,-1),kd=[bd];function xd(e,t){return d(),m("svg",yd,kd)}const wd=O(gd,[["render",xd]]),Sd=B({__name:"VPNavScreenMenuGroupLink",props:{text:{},link:{}},setup(e){const t=Ke("close-screen");return(n,s)=>(d(),X(Et,{class:"VPNavScreenMenuGroupLink",href:n.link,onClick:y(t)},{default:I(()=>[Le(fe(n.text),1)]),_:1},8,["href","onClick"]))}});const Rr=O(Sd,[["__scopeId","data-v-e0a398be"]]),$d={class:"VPNavScreenMenuGroupSection"},Pd={key:0,class:"title"},Cd=B({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(e){return(t,n)=>(d(),m("div",$d,[t.text?(d(),m("p",Pd,fe(t.text),1)):K("",!0),(d(!0),m(Q,null,Ce(t.items,s=>(d(),X(Rr,{key:s.text,text:s.text,link:s.link},null,8,["text","link"]))),128))]))}});const Vd=O(Cd,[["__scopeId","data-v-5c6f67f7"]]),Td=["aria-controls","aria-expanded"],Ld={class:"button-text"},Ed=["id"],Md={key:1,class:"group"},Ad=B({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(e){const t=e,n=he(!1),s=re(()=>`NavScreenGroup-${t.text.replace(" ","-").toLowerCase()}`);function o(){n.value=!n.value}return(i,r)=>(d(),m("div",{class:pe(["VPNavScreenMenuGroup",{open:n.value}])},[g("button",{class:"button","aria-controls":s.value,"aria-expanded":n.value,onClick:o},[g("span",Ld,fe(i.text),1),T(wd,{class:"button-icon"})],8,Td),g("div",{id:s.value,class:"items"},[(d(!0),m(Q,null,Ce(i.items,l=>(d(),m(Q,{key:l.text},["link"in l?(d(),m("div",{key:l.text,class:"item"},[T(Rr,{text:l.text,link:l.link},null,8,["text","link"])])):(d(),m("div",Md,[T(Vd,{text:l.text,items:l.items},null,8,["text","items"])]))],64))),128))],8,Ed)],2))}});const Id=O(Ad,[["__scopeId","data-v-2782ed6d"]]),Nd={key:0,class:"VPNavScreenMenu"},Od=B({__name:"VPNavScreenMenu",setup(e){const{theme:t}=ue();return(n,s)=>y(t).nav?(d(),m("nav",Nd,[(d(!0),m(Q,null,Ce(y(t).nav,o=>(d(),m(Q,{key:o.text},["link"in o?(d(),X(md,{key:0,text:o.text,link:o.link},null,8,["text","link"])):(d(),X(Id,{key:1,text:o.text||"",items:o.items},null,8,["text","items"]))],64))),128))])):K("",!0)}}),Rd=e=>(qe("data-v-ee8ee021"),e=e(),Ge(),e),Bd={key:0,class:"VPNavScreenAppearance"},Hd=Rd(()=>g("p",{class:"text"},"Appearance",-1)),Fd=B({__name:"VPNavScreenAppearance",setup(e){const{site:t}=ue();return(n,s)=>y(t).appearance?(d(),m("div",Bd,[Hd,T(uo)])):K("",!0)}});const jd=O(Fd,[["__scopeId","data-v-ee8ee021"]]),Dd={class:"list"},Ud=["href"],zd=B({__name:"VPNavScreenTranslations",setup(e){const{theme:t}=ue(),n=he(!1);function s(){n.value=!n.value}return(o,i)=>y(t).localeLinks?(d(),m("div",{key:0,class:pe(["VPNavScreenTranslations",{open:n.value}])},[g("button",{class:"title",onClick:s},[T(Ar,{class:"icon lang"}),Le(" "+fe(y(t).localeLinks.text)+" ",1),T(Mr,{class:"icon chevron"})]),g("ul",Dd,[(d(!0),m(Q,null,Ce(y(t).localeLinks.items,r=>(d(),m("li",{key:r.link,class:"item"},[g("a",{class:"link",href:r.link},fe(r.text),9,Ud)]))),128))])],2)):K("",!0)}});const Kd=O(zd,[["__scopeId","data-v-4e6c0dfc"]]),qd=B({__name:"VPNavScreenSocialLinks",setup(e){const{theme:t}=ue();return(n,s)=>y(t).socialLinks?(d(),X(fo,{key:0,class:"VPNavScreenSocialLinks",links:y(t).socialLinks},null,8,["links"])):K("",!0)}}),Gd={class:"container"},Wd=B({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(e){const t=he(null);function n(){Nr(t.value,{reserveScrollBarGap:!0})}function s(){Or()}return(o,i)=>(d(),X(is,{name:"fade",onEnter:n,onAfterLeave:s},{default:I(()=>[o.open?(d(),m("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:t},[g("div",Gd,[L(o.$slots,"nav-screen-content-before",{},void 0,!0),T(Od,{class:"menu"}),T(Kd,{class:"translations"}),T(jd,{class:"appearance"}),T(qd,{class:"social-links"}),L(o.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):K("",!0)]),_:3}))}});const Jd=O(Wd,[["__scopeId","data-v-dc3ea62e"]]),Yd=B({__name:"VPNav",setup(e){const{isScreenOpen:t,closeScreen:n,toggleScreen:s}=Jc(),{hasSidebar:o}=et();return ns("close-screen",n),(i,r)=>(d(),m("header",{class:pe(["VPNav",{"no-sidebar":!y(o)}])},[T(ad,{"is-screen-open":y(t),onToggleScreen:y(s)},{"nav-bar-title-before":I(()=>[L(i.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":I(()=>[L(i.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":I(()=>[L(i.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":I(()=>[L(i.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),T(Jd,{open:y(t)},{"nav-screen-content-before":I(()=>[L(i.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":I(()=>[L(i.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])],2))}});const Qd=O(Yd,[["__scopeId","data-v-0b81f789"]]),Xd={},Zd={xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",viewBox:"0 0 24 24"},ep=g("path",{d:"M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z"},null,-1),tp=g("path",{d:"M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z"},null,-1),np=g("path",{d:"M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z"},null,-1),sp=g("path",{d:"M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z"},null,-1),op=[ep,tp,np,sp];function ip(e,t){return d(),m("svg",Zd,op)}const rp=O(Xd,[["render",ip]]),lp=e=>(qe("data-v-9b9d1bde"),e=e(),Ge(),e),ap={key:0,class:"VPLocalNav"},cp=["aria-expanded"],up=lp(()=>g("span",{class:"menu-text"},"Menu",-1)),fp=B({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(e){const{hasSidebar:t}=et();function n(){window.scrollTo({top:0,left:0,behavior:"smooth"})}return(s,o)=>y(t)?(d(),m("div",ap,[g("button",{class:"menu","aria-expanded":s.open,"aria-controls":"VPSidebarNav",onClick:o[0]||(o[0]=i=>s.$emit("open-menu"))},[T(rp,{class:"menu-icon"}),up],8,cp),g("a",{class:"top-link",href:"#",onClick:n}," Return to top ")])):K("",!0)}});const dp=O(fp,[["__scopeId","data-v-9b9d1bde"]]),pp={},hp={version:"1.1",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},_p=g("path",{d:"M19,2H5C3.3,2,2,3.3,2,5v14c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V5C22,3.3,20.7,2,19,2z M20,19c0,0.6-0.4,1-1,1H5c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h14c0.6,0,1,0.4,1,1V19z"},null,-1),vp=g("path",{d:"M16,11h-3V8c0-0.6-0.4-1-1-1s-1,0.4-1,1v3H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h3v3c0,0.6,0.4,1,1,1s1-0.4,1-1v-3h3c0.6,0,1-0.4,1-1S16.6,11,16,11z"},null,-1),mp=[_p,vp];function gp(e,t){return d(),m("svg",hp,mp)}const yp=O(pp,[["render",gp]]),bp={},kp={xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",viewBox:"0 0 24 24"},xp=g("path",{d:"M19,2H5C3.3,2,2,3.3,2,5v14c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V5C22,3.3,20.7,2,19,2zM20,19c0,0.6-0.4,1-1,1H5c-0.6,0-1-0.4-1-1V5c0-0.6,0.4-1,1-1h14c0.6,0,1,0.4,1,1V19z"},null,-1),wp=g("path",{d:"M16,11H8c-0.6,0-1,0.4-1,1s0.4,1,1,1h8c0.6,0,1-0.4,1-1S16.6,11,16,11z"},null,-1),Sp=[xp,wp];function $p(e,t){return d(),m("svg",kp,Sp)}const Pp=O(bp,[["render",$p]]),Cp=["innerHTML"],Vp=B({__name:"VPSidebarLink",props:{item:{},depth:{default:1}},setup(e){const{page:t,frontmatter:n}=ue(),s=re(()=>n.value.sidebarDepth||1/0),o=Ke("close-sidebar");return(i,r)=>{const l=Lt("VPSidebarLink",!0);return d(),m(Q,null,[T(Et,{class:pe(["link",{active:y(Yt)(y(t).relativePath,i.item.link)}]),style:Gn({paddingLeft:16*(i.depth-1)+"px"}),href:i.item.link,onClick:y(o)},{default:I(()=>[g("span",{innerHTML:i.item.text,class:pe(["link-text",{light:i.depth>1}])},null,10,Cp)]),_:1},8,["class","style","href","onClick"]),"items"in i.item&&i.depth(d(),X(l,{key:a.link,item:a,depth:i.depth+1},null,8,["item","depth"]))),128)):K("",!0)],64)}}});const Tp=O(Vp,[["__scopeId","data-v-2807181e"]]),Lp=["role"],Ep=["innerHTML"],Mp={class:"action"},Ap={class:"items"},Ip=B({__name:"VPSidebarGroup",props:{text:{},items:{},collapsible:{type:Boolean},collapsed:{type:Boolean}},setup(e){const t=e,n=he(!1);Kt(()=>{n.value=!!(t.collapsible&&t.collapsed)});const{page:s}=ue();Kt(()=>{t.items.some(i=>Yt(s.value.relativePath,i.link))&&(n.value=!1)});function o(){t.collapsible&&(n.value=!n.value)}return(i,r)=>(d(),m("section",{class:pe(["VPSidebarGroup",{collapsible:i.collapsible,collapsed:n.value}])},[i.text?(d(),m("div",{key:0,class:"title",role:i.collapsible?"button":void 0,onClick:o},[g("h2",{innerHTML:i.text,class:"title-text"},null,8,Ep),g("div",Mp,[T(Pp,{class:"icon minus"}),T(yp,{class:"icon plus"})])],8,Lp)):K("",!0),g("div",Ap,[(d(!0),m(Q,null,Ce(i.items,l=>(d(),X(Tp,{key:l.link,item:l},null,8,["item"]))),128))])],2))}});const Np=O(Ip,[["__scopeId","data-v-21a905e4"]]),Op=e=>(qe("data-v-d084bab6"),e=e(),Ge(),e),Rp={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},Bp=Op(()=>g("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),Hp=B({__name:"VPSidebar",props:{open:{type:Boolean}},setup(e){const{sidebar:t,hasSidebar:n}=et(),s=e;let o=he(null);function i(){Nr(o.value,{reserveScrollBarGap:!0})}function r(){Or()}return Xi(async()=>{var l;s.open?(i(),(l=o.value)==null||l.focus()):r()}),(l,a)=>y(n)?(d(),m("aside",{key:0,class:pe(["VPSidebar",{open:l.open}]),ref_key:"navEl",ref:o,onClick:a[0]||(a[0]=cc(()=>{},["stop"]))},[g("nav",Rp,[Bp,L(l.$slots,"sidebar-nav-before",{},void 0,!0),(d(!0),m(Q,null,Ce(y(t),f=>(d(),m("div",{key:f.text,class:"group"},[T(Np,{text:f.text,items:f.items,collapsible:f.collapsible,collapsed:f.collapsed},null,8,["text","items","collapsible","collapsed"])]))),128)),L(l.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):K("",!0)}});const Fp=O(Hp,[["__scopeId","data-v-d084bab6"]]),jp={},Dp={class:"VPPage"};function Up(e,t){const n=Lt("Content");return d(),m("div",Dp,[T(n)])}const zp=O(jp,[["render",Up]]),Kp=B({__name:"VPButton",props:{tag:{},size:{},theme:{},text:{},href:{}},setup(e){const t=e,n=re(()=>[t.size??"medium",t.theme??"brand"]),s=re(()=>t.href&&rs.test(t.href)),o=re(()=>t.tag?t.tag:t.href?"a":"button");return(i,r)=>(d(),X(to(o.value),{class:pe(["VPButton",n.value]),href:i.href?y(Fn)(i.href):void 0,target:s.value?"_blank":void 0,rel:s.value?"noreferrer":void 0},{default:I(()=>[Le(fe(i.text),1)]),_:1},8,["class","href","target","rel"]))}});const qp=O(Kp,[["__scopeId","data-v-38d91e85"]]),Gp=e=>(qe("data-v-5d2a201e"),e=e(),Ge(),e),Wp={class:"container"},Jp={class:"main"},Yp={key:0,class:"name"},Qp={class:"clip"},Xp={key:1,class:"text"},Zp={key:2,class:"tagline"},eh={key:3,class:"actions"},th={key:0,class:"image"},nh={class:"image-container"},sh=Gp(()=>g("div",{class:"image-bg"},null,-1)),oh=B({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(e){return(t,n)=>(d(),m("div",{class:pe(["VPHero",{"has-image":t.image}])},[g("div",Wp,[g("div",Jp,[t.name?(d(),m("h1",Yp,[g("span",Qp,fe(t.name),1)])):K("",!0),t.text?(d(),m("p",Xp,fe(t.text),1)):K("",!0),t.tagline?(d(),m("p",Zp,fe(t.tagline),1)):K("",!0),t.actions?(d(),m("div",eh,[(d(!0),m(Q,null,Ce(t.actions,s=>(d(),m("div",{key:s.link,class:"action"},[T(qp,{tag:"a",size:"medium",theme:s.theme,text:s.text,href:s.link},null,8,["theme","text","href"])]))),128))])):K("",!0)]),t.image?(d(),m("div",th,[g("div",nh,[sh,T(Tr,{class:"image-src",image:t.image},null,8,["image"])])])):K("",!0)])],2))}});const ih=O(oh,[["__scopeId","data-v-5d2a201e"]]),rh=B({__name:"VPHomeHero",setup(e){const{frontmatter:t}=ue();return(n,s)=>y(t).hero?(d(),X(ih,{key:0,class:"VPHomeHero",name:y(t).hero.name,text:y(t).hero.text,tagline:y(t).hero.tagline,image:y(t).hero.image,actions:y(t).hero.actions},null,8,["name","text","tagline","image","actions"])):K("",!0)}}),lh={},ah={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},ch=g("path",{d:"M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"},null,-1),uh=[ch];function fh(e,t){return d(),m("svg",ah,uh)}const dh=O(lh,[["render",fh]]),ph={class:"box"},hh={key:0,class:"icon"},_h={class:"title"},vh={class:"details"},mh={key:1,class:"link-text"},gh={class:"link-text-value"},yh=B({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{}},setup(e){return(t,n)=>(d(),X(Et,{class:"VPFeature",href:t.link,"no-icon":!0},{default:I(()=>[g("article",ph,[t.icon?(d(),m("div",hh,fe(t.icon),1)):K("",!0),g("h2",_h,fe(t.title),1),g("p",vh,fe(t.details),1),t.linkText?(d(),m("div",mh,[g("p",gh,[Le(fe(t.linkText)+" ",1),T(dh,{class:"link-text-icon"})])])):K("",!0)])]),_:1},8,["href"]))}});const bh=O(yh,[["__scopeId","data-v-eb26ee7e"]]),kh={key:0,class:"VPFeatures"},xh={class:"container"},wh={class:"items"},Sh=B({__name:"VPFeatures",props:{features:{}},setup(e){const t=e,n=re(()=>{const s=t.features.length;if(s){if(s===2)return"grid-2";if(s===3)return"grid-3";if(s%3===0)return"grid-6";if(s%2===0)return"grid-4"}else return});return(s,o)=>s.features?(d(),m("div",kh,[g("div",xh,[g("div",wh,[(d(!0),m(Q,null,Ce(s.features,i=>(d(),m("div",{key:i.title,class:pe(["item",[n.value]])},[T(bh,{icon:i.icon,title:i.title,details:i.details,link:i.link,"link-text":i.linkText},null,8,["icon","title","details","link","link-text"])],2))),128))])])])):K("",!0)}});const $h=O(Sh,[["__scopeId","data-v-0faf78b0"]]),Ph=B({__name:"VPHomeFeatures",setup(e){const{frontmatter:t}=ue();return(n,s)=>y(t).features?(d(),X($h,{key:0,class:"VPHomeFeatures",features:y(t).features},null,8,["features"])):K("",!0)}}),Ch={class:"VPHome"},Vh=B({__name:"VPHome",setup(e){return(t,n)=>{const s=Lt("Content");return d(),m("div",Ch,[L(t.$slots,"home-hero-before",{},void 0,!0),T(rh),L(t.$slots,"home-hero-after",{},void 0,!0),L(t.$slots,"home-features-before",{},void 0,!0),T(Ph),L(t.$slots,"home-features-after",{},void 0,!0),T(s)])}}});const Th=O(Vh,[["__scopeId","data-v-19f91060"]]);var pi;const Br=typeof window<"u";Br&&((pi=window==null?void 0:window.navigator)!=null&&pi.userAgent)&&/iP(ad|hone|od)/.test(window.navigator.userAgent);function Lh(e){return e}function Eh(e){return Vi()?(ol(e),!0):!1}function Mh(e){return typeof e=="function"?re(e):he(e)}function Ah(e,t=!0){ro()?je(e):t?e():Xs(e)}const Ih=Br?window:void 0;function Nh(e,t=!1){const n=he(),s=()=>n.value=!!e();return s(),Ah(s,t),n}function hi(e,t={}){const{window:n=Ih}=t,s=Nh(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const i=he(!1),r=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",l):o.removeListener(l))},l=()=>{s.value&&(r(),o=n.matchMedia(Mh(e).value),i.value=o.matches,"addEventListener"in o?o.addEventListener("change",l):o.addListener(l))};return Kt(l),Eh(()=>r()),i}const _i=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},vi="__vueuse_ssr_handlers__";_i[vi]=_i[vi]||{};var mi;(function(e){e.UP="UP",e.RIGHT="RIGHT",e.DOWN="DOWN",e.LEFT="LEFT",e.NONE="NONE"})(mi||(mi={}));var Oh=Object.defineProperty,gi=Object.getOwnPropertySymbols,Rh=Object.prototype.hasOwnProperty,Bh=Object.prototype.propertyIsEnumerable,yi=(e,t,n)=>t in e?Oh(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Hh=(e,t)=>{for(var n in t||(t={}))Rh.call(t,n)&&yi(e,n,t[n]);if(gi)for(var n of gi(t))Bh.call(t,n)&&yi(e,n,t[n]);return e};const Fh={easeInSine:[.12,0,.39,0],easeOutSine:[.61,1,.88,1],easeInOutSine:[.37,0,.63,1],easeInQuad:[.11,0,.5,0],easeOutQuad:[.5,1,.89,1],easeInOutQuad:[.45,0,.55,1],easeInCubic:[.32,0,.67,0],easeOutCubic:[.33,1,.68,1],easeInOutCubic:[.65,0,.35,1],easeInQuart:[.5,0,.75,0],easeOutQuart:[.25,1,.5,1],easeInOutQuart:[.76,0,.24,1],easeInQuint:[.64,0,.78,0],easeOutQuint:[.22,1,.36,1],easeInOutQuint:[.83,0,.17,1],easeInExpo:[.7,0,.84,0],easeOutExpo:[.16,1,.3,1],easeInOutExpo:[.87,0,.13,1],easeInCirc:[.55,0,1,.45],easeOutCirc:[0,.55,.45,1],easeInOutCirc:[.85,0,.15,1],easeInBack:[.36,0,.66,-.56],easeOutBack:[.34,1.56,.64,1],easeInOutBack:[.68,-.6,.32,1.6]};Hh({linear:Lh},Fh);function jh(){const{hasSidebar:e}=et(),t=hi("(min-width: 960px)"),n=hi("(min-width: 1280px)");return{isAsideEnabled:re(()=>!n.value&&!t.value?!1:e.value?n.value:t.value)}}const Dh=71;function Uh(e){if(e===!1)return[];let t=[];return document.querySelectorAll("h2, h3, h4, h5, h6").forEach(n=>{n.textContent&&n.id&&t.push({level:Number(n.tagName[1]),title:n.innerText.replace(/\s+#\s*$/,""),link:`#${n.id}`})}),zh(t,e)}function zh(e,t=2){return Kh(e,typeof t=="number"?[t,t]:t==="deep"?[2,6]:t)}function Kh(e,t){const n=[];return e=e.map(s=>({...s})),e.forEach((s,o)=>{s.level>=t[0]&&s.level<=t[1]&&qh(o,e,t)&&n.push(s)}),n}function qh(e,t,n){if(e===0)return!0;const s=t[e];for(let o=e-1;o>=0;o--){const i=t[o];if(i.level=n[0]&&i.level<=n[1])return i.children==null&&(i.children=[]),i.children.push(s),!1}return!0}function Gh(e,t){const{isAsideEnabled:n}=jh(),s=jc(i,100);let o=null;je(()=>{requestAnimationFrame(i),window.addEventListener("scroll",s)}),no(()=>{r(location.hash)}),mt(()=>{window.removeEventListener("scroll",s)});function i(){if(!n.value)return;const l=[].slice.call(e.value.querySelectorAll(".outline-link")),a=[].slice.call(document.querySelectorAll(".content .header-anchor")).filter(P=>l.some(Y=>Y.hash===P.hash&&P.offsetParent!==null)),f=window.scrollY,p=window.innerHeight,h=document.body.offsetHeight,b=Math.abs(f+p-h)<1;if(a.length&&b){r(a[a.length-1].hash);return}for(let P=0;P{const s=Lt("VPDocAsideOutlineItem",!0);return d(),m("ul",{class:pe(t.root?"root":"nested")},[(d(!0),m(Q,null,Ce(t.headers,({children:o,link:i,title:r})=>(d(),m("li",null,[g("a",{class:"outline-link",href:i,onClick:n[0]||(n[0]=(...l)=>t.onClick&&t.onClick(...l))},fe(r),9,Jh),o!=null&&o.length?(d(),X(s,{key:0,headers:o,onClick:t.onClick},null,8,["headers","onClick"])):K("",!0)]))),256))],2)}}});const Qh=O(Yh,[["__scopeId","data-v-45527559"]]),Xh=e=>(qe("data-v-31a9545f"),e=e(),Ge(),e),Zh={class:"content"},e_={class:"outline-title"},t_={"aria-labelledby":"doc-outline-aria-label"},n_=Xh(()=>g("span",{class:"visually-hidden",id:"doc-outline-aria-label"}," Table of Contents for current page ",-1)),s_=B({__name:"VPDocAsideOutline",setup(e){const{frontmatter:t,theme:n}=ue(),s=re(()=>t.value.outline??n.value.outline),o=Ke("onContentUpdated");o.value=()=>{i.value=Uh(s.value)};const i=he([]),r=re(()=>i.value.length>0),l=he(),a=he();Gh(l,a);function f({target:p}){const h="#"+p.href.split("#")[1],b=document.querySelector(decodeURIComponent(h));b==null||b.focus()}return(p,h)=>(d(),m("div",{class:pe(["VPDocAsideOutline",{"has-outline":r.value}]),ref_key:"container",ref:l},[g("div",Zh,[g("div",{class:"outline-marker",ref_key:"marker",ref:a},null,512),g("div",e_,fe(y(n).outlineTitle||"On this page"),1),g("nav",t_,[n_,T(Qh,{headers:i.value,root:!0,onClick:f},null,8,["headers"])])])],2))}});const o_=O(s_,[["__scopeId","data-v-31a9545f"]]),i_={class:"VPDocAsideCarbonAds"},r_=B({__name:"VPDocAsideCarbonAds",setup(e){const t=()=>null;return(n,s)=>(d(),m("div",i_,[T(y(t))]))}}),l_=e=>(qe("data-v-ac88e149"),e=e(),Ge(),e),a_={class:"VPDocAside"},c_=l_(()=>g("div",{class:"spacer"},null,-1)),u_=B({__name:"VPDocAside",setup(e){const{theme:t}=ue();return(n,s)=>(d(),m("div",a_,[L(n.$slots,"aside-top",{},void 0,!0),L(n.$slots,"aside-outline-before",{},void 0,!0),T(o_),L(n.$slots,"aside-outline-after",{},void 0,!0),c_,L(n.$slots,"aside-ads-before",{},void 0,!0),y(t).carbonAds?(d(),X(r_,{key:0})):K("",!0),L(n.$slots,"aside-ads-after",{},void 0,!0),L(n.$slots,"aside-bottom",{},void 0,!0)]))}});const f_=O(u_,[["__scopeId","data-v-ac88e149"]]);function d_(){const{theme:e,page:t}=ue();return re(()=>{const{text:n="Edit this page",pattern:s}=e.value.editLink||{},{relativePath:o}=t.value;return{url:s.replace(/:path/g,o),text:n}})}function p_(){const{page:e,theme:t,frontmatter:n}=ue();return re(()=>{const s=Vr(t.value.sidebar,e.value.relativePath),o=Dc(s),i=o.findIndex(r=>Yt(e.value.relativePath,r.link));return{prev:n.value.prev?{...o[i-1],text:n.value.prev}:o[i-1],next:n.value.next?{...o[i+1],text:n.value.next}:o[i+1]}})}const h_={},__={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},v_=g("path",{d:"M18,23H4c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h7c0.6,0,1,0.4,1,1s-0.4,1-1,1H4C3.4,5,3,5.4,3,6v14c0,0.6,0.4,1,1,1h14c0.6,0,1-0.4,1-1v-7c0-0.6,0.4-1,1-1s1,0.4,1,1v7C21,21.7,19.7,23,18,23z"},null,-1),m_=g("path",{d:"M8,17c-0.3,0-0.5-0.1-0.7-0.3C7,16.5,6.9,16.1,7,15.8l1-4c0-0.2,0.1-0.3,0.3-0.5l9.5-9.5c1.2-1.2,3.2-1.2,4.4,0c1.2,1.2,1.2,3.2,0,4.4l-9.5,9.5c-0.1,0.1-0.3,0.2-0.5,0.3l-4,1C8.2,17,8.1,17,8,17zM9.9,12.5l-0.5,2.1l2.1-0.5l9.3-9.3c0.4-0.4,0.4-1.1,0-1.6c-0.4-0.4-1.2-0.4-1.6,0l0,0L9.9,12.5z M18.5,2.5L18.5,2.5L18.5,2.5z"},null,-1),g_=[v_,m_];function y_(e,t){return d(),m("svg",__,g_)}const b_=O(h_,[["render",y_]]),k_={class:"VPLastUpdated"},x_=["datetime"],w_=B({__name:"VPDocFooterLastUpdated",setup(e){const{theme:t,page:n}=ue(),s=re(()=>new Date(n.value.lastUpdated)),o=re(()=>s.value.toISOString()),i=he("");return je(()=>{Kt(()=>{i.value=s.value.toLocaleString(window.navigator.language)})}),(r,l)=>(d(),m("p",k_,[Le(fe(y(t).lastUpdatedText??"Last updated")+": ",1),g("time",{datetime:o.value},fe(i.value),9,x_)]))}});const S_=O(w_,[["__scopeId","data-v-040668ce"]]),$_={key:0,class:"VPDocFooter"},P_={key:0,class:"edit-info"},C_={key:0,class:"edit-link"},V_={key:1,class:"last-updated"},T_={key:1,class:"prev-next"},L_={class:"pager"},E_=["href"],M_=["innerHTML"],A_=["innerHTML"],I_=["href"],N_=["innerHTML"],O_=["innerHTML"],R_=B({__name:"VPDocFooter",setup(e){const{theme:t,page:n,frontmatter:s}=ue(),o=d_(),i=p_(),r=re(()=>t.value.editLink&&s.value.editLink!==!1),l=re(()=>n.value.lastUpdated&&s.value.lastUpdated!==!1),a=re(()=>r.value||l.value||i.value.prev||i.value.next);return(f,p)=>{var h,b;return a.value?(d(),m("footer",$_,[r.value||l.value?(d(),m("div",P_,[r.value?(d(),m("div",C_,[T(Et,{class:"edit-link-button",href:y(o).url,"no-icon":!0},{default:I(()=>[T(b_,{class:"edit-link-icon"}),Le(" "+fe(y(o).text),1)]),_:1},8,["href"])])):K("",!0),l.value?(d(),m("div",V_,[T(S_)])):K("",!0)])):K("",!0),y(i).prev||y(i).next?(d(),m("div",T_,[g("div",L_,[y(i).prev?(d(),m("a",{key:0,class:"pager-link prev",href:y(Fn)(y(i).prev.link)},[g("span",{class:"desc",innerHTML:((h=y(t).docFooter)==null?void 0:h.prev)??"Previous page"},null,8,M_),g("span",{class:"title",innerHTML:y(i).prev.text},null,8,A_)],8,E_)):K("",!0)]),g("div",{class:pe(["pager",{"has-prev":y(i).prev}])},[y(i).next?(d(),m("a",{key:0,class:"pager-link next",href:y(Fn)(y(i).next.link)},[g("span",{class:"desc",innerHTML:((b=y(t).docFooter)==null?void 0:b.next)??"Next page"},null,8,N_),g("span",{class:"title",innerHTML:y(i).next.text},null,8,O_)],8,I_)):K("",!0)],2)])):K("",!0)])):K("",!0)}}});const B_=O(R_,[["__scopeId","data-v-a5aaf7cd"]]),H_=e=>(qe("data-v-c557394e"),e=e(),Ge(),e),F_={class:"container"},j_={key:0,class:"aside"},D_=H_(()=>g("div",{class:"aside-curtain"},null,-1)),U_={class:"aside-container"},z_={class:"aside-content"},K_={class:"content"},q_={class:"content-container"},G_={class:"main"},W_=B({__name:"VPDoc",setup(e){const t=gt(),{hasSidebar:n,hasAside:s}=et(),o=re(()=>t.path.replace(/[./]+/g,"_").replace(/_html$/,"")),i=he();return ns("onContentUpdated",i),(r,l)=>{const a=Lt("Content");return d(),m("div",{class:pe(["VPDoc",{"has-sidebar":y(n),"has-aside":y(s)}])},[g("div",F_,[y(s)?(d(),m("div",j_,[D_,g("div",U_,[g("div",z_,[T(f_,null,{"aside-top":I(()=>[L(r.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":I(()=>[L(r.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":I(()=>[L(r.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":I(()=>[L(r.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":I(()=>[L(r.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":I(()=>[L(r.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])])):K("",!0),g("div",K_,[g("div",q_,[L(r.$slots,"doc-before",{},void 0,!0),g("main",G_,[T(a,{class:pe(["vp-doc",o.value]),onContentUpdated:i.value},null,8,["class","onContentUpdated"])]),L(r.$slots,"doc-footer-before",{},void 0,!0),T(B_),L(r.$slots,"doc-after",{},void 0,!0)])])])],2)}}});const J_=O(W_,[["__scopeId","data-v-c557394e"]]),Y_=B({__name:"VPContent",setup(e){const t=gt(),{frontmatter:n}=ue(),{hasSidebar:s}=et(),o=Ke("NotFound");return(i,r)=>(d(),m("div",{class:pe(["VPContent",{"has-sidebar":y(s),"is-home":y(n).layout==="home"}]),id:"VPContent"},[y(t).component===y(o)?(d(),X(y(o),{key:0})):y(n).layout==="page"?(d(),X(zp,{key:1})):y(n).layout==="home"?(d(),X(Th,{key:2},{"home-hero-before":I(()=>[L(i.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-after":I(()=>[L(i.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":I(()=>[L(i.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":I(()=>[L(i.$slots,"home-features-after",{},void 0,!0)]),_:3})):(d(),X(J_,{key:3},{"doc-footer-before":I(()=>[L(i.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":I(()=>[L(i.$slots,"doc-before",{},void 0,!0)]),"doc-after":I(()=>[L(i.$slots,"doc-after",{},void 0,!0)]),"aside-top":I(()=>[L(i.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":I(()=>[L(i.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":I(()=>[L(i.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":I(()=>[L(i.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":I(()=>[L(i.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":I(()=>[L(i.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}});const Q_=O(Y_,[["__scopeId","data-v-447e44e8"]]),X_={class:"container"},Z_=["innerHTML"],e0=["innerHTML"],t0=B({__name:"VPFooter",setup(e){const{theme:t}=ue(),{hasSidebar:n}=et();return(s,o)=>y(t).footer?(d(),m("footer",{key:0,class:pe(["VPFooter",{"has-sidebar":y(n)}])},[g("div",X_,[y(t).footer.message?(d(),m("p",{key:0,class:"message",innerHTML:y(t).footer.message},null,8,Z_)):K("",!0),y(t).footer.copyright?(d(),m("p",{key:1,class:"copyright",innerHTML:y(t).footer.copyright},null,8,e0)):K("",!0)])],2)):K("",!0)}});const n0=O(t0,[["__scopeId","data-v-6891d08c"]]),s0={key:0,class:"Layout"},o0=B({__name:"Layout",setup(e){const{isOpen:t,open:n,close:s}=et(),o=gt();Xe(()=>o.path,s),Uc(t,s),ns("close-sidebar",s);const{frontmatter:i}=ue();return(r,l)=>{const a=Lt("Content");return y(i).layout!==!1?(d(),m("div",s0,[L(r.$slots,"layout-top",{},void 0,!0),T(Kc),T(Wc,{class:"backdrop",show:y(t),onClick:y(s)},null,8,["show","onClick"]),T(Qd,null,{"nav-bar-title-before":I(()=>[L(r.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":I(()=>[L(r.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":I(()=>[L(r.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":I(()=>[L(r.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":I(()=>[L(r.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":I(()=>[L(r.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3}),T(dp,{open:y(t),onOpenMenu:y(n)},null,8,["open","onOpenMenu"]),T(Fp,{open:y(t)},{"sidebar-nav-before":I(()=>[L(r.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":I(()=>[L(r.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),T(Q_,null,{"home-hero-before":I(()=>[L(r.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-after":I(()=>[L(r.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":I(()=>[L(r.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":I(()=>[L(r.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":I(()=>[L(r.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":I(()=>[L(r.$slots,"doc-before",{},void 0,!0)]),"doc-after":I(()=>[L(r.$slots,"doc-after",{},void 0,!0)]),"aside-top":I(()=>[L(r.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":I(()=>[L(r.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":I(()=>[L(r.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":I(()=>[L(r.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":I(()=>[L(r.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":I(()=>[L(r.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),T(n0),L(r.$slots,"layout-bottom",{},void 0,!0)])):(d(),X(a,{key:1}))}}});const i0=O(o0,[["__scopeId","data-v-b300eff0"]]),as=e=>(qe("data-v-b436a9f8"),e=e(),Ge(),e),r0={class:"NotFound"},l0=as(()=>g("p",{class:"code"},"404",-1)),a0=as(()=>g("h1",{class:"title"},"PAGE NOT FOUND",-1)),c0=as(()=>g("div",{class:"divider"},null,-1)),u0=as(()=>g("blockquote",{class:"quote"}," But if you don't change your direction, and if you keep looking, you may end up where you are heading. ",-1)),f0={class:"action"},d0=["href"],p0=B({__name:"NotFound",setup(e){const{site:t}=ue();return(n,s)=>(d(),m("div",r0,[l0,a0,c0,u0,g("div",f0,[g("a",{class:"link",href:y(t).base,"aria-label":"go to home"}," Take me home ",8,d0)])]))}});const h0=O(p0,[["__scopeId","data-v-b436a9f8"]]);const _0={Layout:i0,NotFound:h0,enhanceApp:({app:e})=>{e.component("Badge",yc)}};const Ut={..._0};function v0(e,t){let n=[],s=!0;const o=i=>{if(s){s=!1;return}n.forEach(r=>document.head.removeChild(r)),n=[],i.forEach(r=>{const l=m0(r);document.head.appendChild(l),n.push(l)})};Kt(()=>{const i=e.data,r=t.value,l=i&&i.description,a=i&&i.frontmatter.head||[];document.title=wr(r,i),document.querySelector("meta[name=description]").setAttribute("content",l||r.description),o(Vc(r.head,y0(a)))})}function m0([e,t,n]){const s=document.createElement(e);for(const o in t)s.setAttribute(o,t[o]);return n&&(s.innerHTML=n),s}function g0(e){return e[0]==="meta"&&e[1]&&e[1].name==="description"}function y0(e){return e.filter(t=>!g0(t))}const Ss=new Set,Hr=()=>document.createElement("link"),b0=e=>{const t=Hr();t.rel="prefetch",t.href=e,document.head.appendChild(t)},k0=e=>{const t=new XMLHttpRequest;t.open("GET",e,t.withCredentials=!0),t.send()};let Pn;const x0=Te&&(Pn=Hr())&&Pn.relList&&Pn.relList.supports&&Pn.relList.supports("prefetch")?b0:k0;function w0(){if(!Te||!window.IntersectionObserver)return;let e;if((e=navigator.connection)&&(e.saveData||/2g/.test(e.effectiveType)))return;const t=window.requestIdleCallback||setTimeout;let n=null;const s=()=>{n&&n.disconnect(),n=new IntersectionObserver(i=>{i.forEach(r=>{if(r.isIntersecting){const l=r.target;n.unobserve(l);const{pathname:a}=l;if(!Ss.has(a)){Ss.add(a);const f=Sr(a);x0(f)}}})}),t(()=>{document.querySelectorAll("#app a").forEach(i=>{const{target:r,hostname:l,pathname:a}=i,f=a.match(/\.\w+$/);f&&f[0]!==".html"||r!=="_blank"&&l===location.hostname&&(a!==location.pathname?n.observe(i):Ss.add(a))})})};je(s);const o=gt();Xe(()=>o.path,s),mt(()=>{n&&n.disconnect()})}const S0=B({setup(e,{slots:t}){const n=he(!1);return je(()=>{n.value=!0}),()=>n.value&&t.default?t.default():null}});function $0(){if(Te){const e=new Map;window.addEventListener("click",t=>{var s;const n=t.target;if(n.matches('div[class*="language-"] > button.copy')){const o=n.parentElement,i=(s=n.nextElementSibling)==null?void 0:s.nextElementSibling;if(!o||!i)return;const r=/language-(shellscript|shell|bash|sh|zsh)/.test(o.className);let l="";i.querySelectorAll("span.line:not(.diff.remove)").forEach(a=>l+=(a.textContent||"")+` +`),l=l.slice(0,-1),r&&(l=l.replace(/^ *(\$|>) /gm,"").trim()),P0(l).then(()=>{n.classList.add("copied"),clearTimeout(e.get(n));const a=setTimeout(()=>{n.classList.remove("copied"),n.blur(),e.delete(n)},2e3);e.set(n,a)})}})}}async function P0(e){try{return navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea"),n=document.activeElement;t.value=e,t.setAttribute("readonly",""),t.style.contain="strict",t.style.position="absolute",t.style.left="-9999px",t.style.fontSize="12pt";const s=document.getSelection(),o=s?s.rangeCount>0&&s.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand("copy"),document.body.removeChild(t),o&&(s.removeAllRanges(),s.addRange(o)),n&&n.focus()}}function C0(){Te&&window.addEventListener("click",e=>{var n,s;const t=e.target;if(t.matches(".vp-code-group input")){const o=(n=t.parentElement)==null?void 0:n.parentElement,i=Array.from((o==null?void 0:o.querySelectorAll("input"))||[]).indexOf(t),r=o==null?void 0:o.querySelector('div[class*="language-"].active'),l=(s=o==null?void 0:o.querySelectorAll('div[class*="language-"]'))==null?void 0:s[i];r&&l&&r!==l&&(r.classList.remove("active"),l.classList.add("active"))}})}const Fr=Ut.NotFound||(()=>"404 Not Found"),V0=B({name:"VitePressApp",setup(){const{site:e}=ue();return je(()=>{Xe(()=>e.value.lang,t=>{document.documentElement.lang=t},{immediate:!0})}),w0(),$0(),C0(),Ut.setup&&Ut.setup(),()=>Hn(Ut.Layout)}});function T0(){const e=E0(),t=L0();t.provide(Pr,e);const n=Mc(e.route);return t.provide($r,n),t.provide("NotFound",Fr),t.component("Content",Oc),t.component("ClientOnly",S0),Object.defineProperty(t.config.globalProperties,"$frontmatter",{get(){return n.frontmatter.value}}),Ut.enhanceApp&&Ut.enhanceApp({app:t,router:e,siteData:Gt}),{app:t,router:e,data:n}}function L0(){return dc(V0)}function E0(){let e=Te,t;return Ic(n=>{let s=Sr(n);return e&&(t=s),(e||t===s)&&(s=s.replace(/\.js$/,".lean.js")),Te&&(e=!1),mc(()=>import(s),[])},Fr)}if(Te){const{app:e,router:t,data:n}=T0();t.go().then(()=>{v0(t.route,n.site),e.mount("#app")})}export{O as _,Sa as a,g as b,m as c,T0 as createApp,Le as d,d as o,fe as t}; diff --git a/assets/article_cms.md.013c968f.js b/assets/article_cms.md.013c968f.js new file mode 100644 index 00000000..a988bb63 --- /dev/null +++ b/assets/article_cms.md.013c968f.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as a,a as p}from"./app.f983686f.js";const l=JSON.parse('{"title":"一站式-后台前端解决方案调研","description":"","frontmatter":{},"headers":[],"relativePath":"article/cms.md","lastUpdated":1710671959000}'),i={name:"article/cms.md"},n=p('

一站式-后台前端解决方案调研

TIP

搜索方式:github 搜名称

Hooks-Admin

react-admin

vue-element-admin

Antd Pro Vue - 背靠阿里,代码过硬,大型项目首选

Vue vben admin - 宝藏后台管理 基于 Vue3 UI清新 功能扎实

Naive Ui admin 适合小项目

vue3-antd-admin

gin-vue-admin

vue-pure-admin

vue3-composition-admin

vue-admin-perfect

gin-vue-admin

vue-vben-admin

Geeker-Admin

soybean-admin

vue-admin-box

vue-next-admin

vue-admin-better

v3-admin-vite

vue-manage-system

vue3-admin-plus

https://github.com/RainManGO/vue3-composition-admin

https://github.com/jzfai/vue3-admin-plus

https://github.com/tobe-fe-dalao/fast-vue3

https://github.com/ibwei/vue3-ts-base

https://github.com/jzfai/vue3-admin-ts

https://github.com/zouzhibin/vue-admin-perfect

',29),r=[n];function o(s,m,u,d,c,h){return t(),a("div",null,r)}const v=e(i,[["render",o]]);export{l as __pageData,v as default}; diff --git a/assets/article_cms.md.013c968f.lean.js b/assets/article_cms.md.013c968f.lean.js new file mode 100644 index 00000000..ed0832d5 --- /dev/null +++ b/assets/article_cms.md.013c968f.lean.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as a,a as p}from"./app.f983686f.js";const l=JSON.parse('{"title":"一站式-后台前端解决方案调研","description":"","frontmatter":{},"headers":[],"relativePath":"article/cms.md","lastUpdated":1710671959000}'),i={name:"article/cms.md"},n=p("",29),r=[n];function o(s,m,u,d,c,h){return t(),a("div",null,r)}const v=e(i,[["render",o]]);export{l as __pageData,v as default}; diff --git a/assets/fe-utils_git.md.6038ff7a.js b/assets/fe-utils_git.md.6038ff7a.js new file mode 100644 index 00000000..7d49d7d6 --- /dev/null +++ b/assets/fe-utils_git.md.6038ff7a.js @@ -0,0 +1,186 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"常用命令","description":"","frontmatter":{},"headers":[{"level":2,"title":"git merge 和 git rebase","slug":"git-merge-和-git-rebase","link":"#git-merge-和-git-rebase","children":[]},{"level":2,"title":"git pull 和 git fetch","slug":"git-pull-和-git-fetch","link":"#git-pull-和-git-fetch","children":[]},{"level":2,"title":"创建 SSH Key","slug":"创建-ssh-key","link":"#创建-ssh-key","children":[]},{"level":2,"title":"配置用户信息","slug":"配置用户信息","link":"#配置用户信息","children":[]},{"level":2,"title":"仓库","slug":"仓库","link":"#仓库","children":[]},{"level":2,"title":"增加/删除文件","slug":"增加-删除文件","link":"#增加-删除文件","children":[]},{"level":2,"title":"代码提交","slug":"代码提交","link":"#代码提交","children":[]},{"level":2,"title":"查看信息","slug":"查看信息","link":"#查看信息","children":[]},{"level":2,"title":"分支","slug":"分支","link":"#分支","children":[]},{"level":2,"title":"标签","slug":"标签","link":"#标签","children":[]},{"level":2,"title":"远程同步","slug":"远程同步","link":"#远程同步","children":[]},{"level":2,"title":"撤销","slug":"撤销","link":"#撤销","children":[]},{"level":2,"title":"忽略文件配置(.gitignore)","slug":"忽略文件配置-gitignore","link":"#忽略文件配置-gitignore","children":[]},{"level":2,"title":"参考资料","slug":"参考资料","link":"#参考资料","children":[]}],"relativePath":"fe-utils/git.md","lastUpdated":1707646177000}'),p={name:"fe-utils/git.md"},e=l(`

常用命令

git merge 和 git rebase

git pull 和 git fetch

创建 SSH Key

shell
$ ssh-keygen -t rsa -C "youremail@example.com"
+
$ ssh-keygen -t rsa -C "youremail@example.com"
+

测试 key 是否配置成功

shell
$ ssh -T git@gitee.com
+
$ ssh -T git@gitee.com
+

配置用户信息

shell
$ git config --global user.name "Your Name"
+$ git config --global user.email "email@example.com"
+
$ git config --global user.name "Your Name"
+$ git config --global user.email "email@example.com"
+

仓库

在当前目录新建一个 Git 代码库

shell
$ git init
+
$ git init
+

新建一个目录,将其初始化为 Git 代码库

shell
$ git init [project-name]
+
$ git init [project-name]
+

下载一个项目和它的整个代码历史

shell
$ git clone [url]
+
$ git clone [url]
+

增加/删除文件

添加指定文件到暂存区

shell
$ git add [file1] [file2] ...
+
$ git add [file1] [file2] ...
+

添加指定目录到暂存区,包括子目录

shell
$ git add [dir]
+
$ git add [dir]
+

添加当前目录的所有文件到暂存区

shell
$ git add .
+
$ git add .
+

添加每个变化前,都会要求确认 对于同一个文件的多处变化,可以实现分次提交

shell
$ git add -p
+
$ git add -p
+

删除工作区文件,并且将这次删除放入暂存区

shell
$ git rm [file1] [file2] ...
+
$ git rm [file1] [file2] ...
+

停止追踪指定文件,但该文件会保留在工作区

shell
$ git rm --cached [file]
+
$ git rm --cached [file]
+

改名文件,并且将这个改名放入暂存区

shell
$ git mv [file-original] [file-renamed]
+
$ git mv [file-original] [file-renamed]
+

代码提交

提交暂存区到仓库区

shell
$ git commit -m [message]
+
$ git commit -m [message]
+

提交工作区自上次 commit 之后的变化,直接到仓库区

shell
$ git commit -a
+
$ git commit -a
+

提交时显示所有 diff 信息

shell
$ git commit -v
+
$ git commit -v
+

使用一次新的 commit,替代上一次提交 如果代码没有任何新变化,则用来改写上一次 commit 的提交信息

shell
$ git commit --amend -m [message]
+
$ git commit --amend -m [message]
+

重做上一次 commit,并包括指定文件的新变化

shell
$ git commit --amend [file1] [file2] ...
+
$ git commit --amend [file1] [file2] ...
+

查看信息

显示有变更的文件

shell
$ git status
+
$ git status
+

显示当前分支的版本历史

shell
$ git log
+
$ git log
+

显示 commit 历史,以及每次 commit 发生变更的文件

shell
$ git log --stat
+
$ git log --stat
+

搜索提交历史,根据关键词

shell
$ git log -S [keyword]
+
$ git log -S [keyword]
+

显示某个 commit 之后的所有变动,每个 commit 占据一行

shell
$ git log [tag] HEAD --pretty=format:%s
+
$ git log [tag] HEAD --pretty=format:%s
+

显示某个 commit 之后的所有变动,其"提交说明"必须符合搜索条件

shell
$ git log [tag] HEAD --grep feature
+
$ git log [tag] HEAD --grep feature
+

显示某个文件的版本历史,包括文件改名

shell
$ git log --follow [file]
+
$ git log --follow [file]
+

显示指定文件相关的每一次 diff

shell
$ git log -p [file]
+
$ git log -p [file]
+

显示过去 5 次提交

shell
$ git log -5 --pretty --oneline
+
$ git log -5 --pretty --oneline
+

显示所有提交过的用户,按提交次数排序

shell
$ git shortlog -sn
+
$ git shortlog -sn
+

显示指定文件是什么人在什么时间修改过

shell
$ git blame [file]
+
$ git blame [file]
+

显示暂存区和工作区的差异

shell
$ git diff
+
$ git diff
+

显示暂存区和上一个 commit 的差异

shell
$ git diff --cached [file]
+
$ git diff --cached [file]
+

显示工作区与当前分支最新 commit 之间的差异

shell
$ git diff HEAD
+
$ git diff HEAD
+

显示两次提交之间的差异

shell
$ git diff [first-branch]...[second-branch]
+
$ git diff [first-branch]...[second-branch]
+

显示今天你写了多少行代码

shell
$ git diff --shortstat "@{0 day ago}"
+
$ git diff --shortstat "@{0 day ago}"
+

显示某次提交的元数据和内容变化

shell
$ git show [commit]
+
$ git show [commit]
+

显示某次提交发生变化的文件

shell
$ git show --name-only [commit]
+
$ git show --name-only [commit]
+

显示某次提交时,某个文件的内容

shell
$ git show [commit]:[filename]
+
$ git show [commit]:[filename]
+

显示当前分支的最近几次提交

shell
$ git reflog
+
$ git reflog
+

分支

列出所有本地分支

shell
$ git branch
+
$ git branch
+

列出所有远程分支

shell
$ git branch -r
+
$ git branch -r
+

列出所有本地分支和远程分支

shell
$ git branch -a
+
$ git branch -a
+

新建一个分支,但依然停留在当前分支

shell
$ git branch [branch-name]
+
$ git branch [branch-name]
+

新建一个分支,并切换到该分支

shell
$ git checkout -b [branch]
+
$ git checkout -b [branch]
+

新建一个分支,指向指定 commit

shell
$ git branch [branch] [commit]
+
$ git branch [branch] [commit]
+

新建一个分支,与指定的远程分支建立追踪关系

shell
$ git branch --track [branch] [remote-branch]
+
$ git branch --track [branch] [remote-branch]
+

切换到指定分支,并更新工作区

shell
$ git checkout [branch-name]
+
$ git checkout [branch-name]
+

切换到上一个分支

shell
$ git checkout -
+
$ git checkout -
+

建立追踪关系,在现有分支与指定的远程分支之间

shell
$ git branch --set-upstream [branch] [remote-branch]
+
$ git branch --set-upstream [branch] [remote-branch]
+

合并指定分支到当前分支

shell
$ git merge [branch]
+
$ git merge [branch]
+

选择一个 commit,合并进当前分支

shell
$ git cherry-pick [commit]
+
$ git cherry-pick [commit]
+

删除分支

shell
$ git branch -d [branch-name]
+
$ git branch -d [branch-name]
+

删除远程分支

shell
$ git push origin --delete [branch-name]
+
$ git push origin --delete [branch-name]
+

标签

列出所有 tag

shell
$ git tag
+
$ git tag
+

新建一个 tag 在当前 commit

shell
$ git tag [tag]
+
$ git tag [tag]
+

新建一个 tag 在指定 commit

shell
$ git tag [tag] [commit]
+
$ git tag [tag] [commit]
+

删除本地 tag

shell
$ git tag -d [tag]
+
$ git tag -d [tag]
+

删除远程 tag

shell
$ git push origin :refs/tags/[tagName]
+
$ git push origin :refs/tags/[tagName]
+

查看 tag 信息

shell
$ git show [tag]
+
$ git show [tag]
+

提交指定 tag

shell
$ git push [remote] [tag]
+
$ git push [remote] [tag]
+

提交所有 tag

shell
$ git push [remote] --tags
+
$ git push [remote] --tags
+

新建一个分支,指向某个 tag

shell
$ git checkout -b [branch] [tag]
+
$ git checkout -b [branch] [tag]
+

远程同步

下载远程仓库的所有变动

shell
$ git fetch [remote]
+
$ git fetch [remote]
+

显示所有远程仓库

shell
$ git remote -v
+
$ git remote -v
+

显示某个远程仓库的信息

shell
$ git remote show [remote]
+
$ git remote show [remote]
+

增加一个新的远程仓库,并命名

shell
$ git remote add [shortname] [url]
+
$ git remote add [shortname] [url]
+

取回远程仓库的变化,并与本地分支合并

shell
$ git pull [remote] [branch]
+
+$ git pull origin ding-dev
+
$ git pull [remote] [branch]
+
+$ git pull origin ding-dev
+

允许不相关历史提交,并强制合并

shell
$ git pull origin master --allow-unrelated-histories
+
$ git pull origin master --allow-unrelated-histories
+

上传本地指定分支到远程仓库

shell
$ git push [remote] [branch]
+
$ git push [remote] [branch]
+

强行推送当前分支到远程仓库,即使有冲突

shell
$ git push [remote] --force
+
$ git push [remote] --force
+

推送所有分支到远程仓库

shell
$ git push [remote] --all
+
$ git push [remote] --all
+

撤销

恢复暂存区的指定文件到工作区

shell
$ git checkout [file]
+
$ git checkout [file]
+

恢复某个 commit 的指定文件到暂存区和工作区

shell
$ git checkout [commit] [file]
+
$ git checkout [commit] [file]
+

恢复暂存区的所有文件到工作区

shell
$ git checkout .
+
$ git checkout .
+

重置暂存区的指定文件,与上一次 commit 保持一致,但工作区不变

shell
$ git reset [file]
+
$ git reset [file]
+

重置暂存区与工作区,与上一次 commit 保持一致

shell
$ git reset --hard
+
$ git reset --hard
+

重置当前分支的指针为指定 commit,同时重置暂存区,但工作区不变

shell
$ git reset [commit]
+
$ git reset [commit]
+

版本回退:重置当前分支的 HEAD 为指定 commit,同时重置暂存区和工作区,与指定 commit 一致

shell
$ git reset --hard [commit]
+
$ git reset --hard [commit]
+

重置当前 HEAD 为指定 commit,但保持暂存区和工作区不变

shell
$ git reset --keep [commit]
+
$ git reset --keep [commit]
+

新建一个 commit,用来撤销指定 commit > 后者的所有变化都将被前者抵消,并且应用到当前分支

shell
$ git revert [commit]
+
$ git revert [commit]
+

暂时将未提交的变化移除,稍后再移入

shell
$ git stash
+$ git stash pop
+
$ git stash
+$ git stash pop
+

reset:回到了三岁

revert:记住当前错误,继续往下走,对之前的进行重写

忽略文件配置(.gitignore)

1、配置语法:

以斜杠“/”开头表示目录;

以星号“*”通配多个字符;

以问号“?”通配单个字符

以方括号“[]”包含单个字符的匹配列表;

以叹号“!”表示不忽略(跟踪)匹配到的文件或目录;

此外,git 对于 .ignore 配置文件是按行从上到下进行规则匹配的,意味着如果前面的规则匹配的范围更大,则后面的规则将不会生效;

2、示例:

(1)规则:fd1/*        说明:忽略目录 fd1 下的全部内容;注意,不管是根目录下的 /fd1/ 目录,还是某个子目录 /child/fd1/ 目录,都会被忽略;

(2)规则:/fd1/*        说明:忽略根目录下的 /fd1/ 目录的全部内容;

(3)规则:

/* !.gitignore !/fw/bin/ !/fw/sf/

说明:忽略全部内容,但是不忽略 .gitignore 文件、根目录下的 /fw/bin/ 和 /fw/sf/ 目录;

github创建仓库
+
1. 桌面打开命令行,克隆git clone ...
+2. 打开克隆好的文件夹,在里面输入命令
+3. 切换分支 git checkout -b first
+4. 写代码
+5. git add .
+6. git commit -m '内容提示'
+7. git push origin first
+切回主分支:git checkout master
+
1. 桌面打开命令行,克隆git clone ...
+2. 打开克隆好的文件夹,在里面输入命令
+3. 切换分支 git checkout -b first
+4. 写代码
+5. git add .
+6. git commit -m '内容提示'
+7. git push origin first
+切回主分支:git checkout master
+

参考资料

local(每一个人的计算机上面也有一个项目仓库)

配置用户名

git config --global user.name 你的名字

配置邮箱

git config --global user.emial 你的邮箱

查看配置

  • git config --list

  • git config user.name

  • git config user.email

`,193),o=[e];function c(r,t,i,d,B,y){return a(),n("div",null,o)}const h=s(p,[["render",c]]);export{b as __pageData,h as default}; diff --git a/assets/fe-utils_git.md.6038ff7a.lean.js b/assets/fe-utils_git.md.6038ff7a.lean.js new file mode 100644 index 00000000..a76316e4 --- /dev/null +++ b/assets/fe-utils_git.md.6038ff7a.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"常用命令","description":"","frontmatter":{},"headers":[{"level":2,"title":"git merge 和 git rebase","slug":"git-merge-和-git-rebase","link":"#git-merge-和-git-rebase","children":[]},{"level":2,"title":"git pull 和 git fetch","slug":"git-pull-和-git-fetch","link":"#git-pull-和-git-fetch","children":[]},{"level":2,"title":"创建 SSH Key","slug":"创建-ssh-key","link":"#创建-ssh-key","children":[]},{"level":2,"title":"配置用户信息","slug":"配置用户信息","link":"#配置用户信息","children":[]},{"level":2,"title":"仓库","slug":"仓库","link":"#仓库","children":[]},{"level":2,"title":"增加/删除文件","slug":"增加-删除文件","link":"#增加-删除文件","children":[]},{"level":2,"title":"代码提交","slug":"代码提交","link":"#代码提交","children":[]},{"level":2,"title":"查看信息","slug":"查看信息","link":"#查看信息","children":[]},{"level":2,"title":"分支","slug":"分支","link":"#分支","children":[]},{"level":2,"title":"标签","slug":"标签","link":"#标签","children":[]},{"level":2,"title":"远程同步","slug":"远程同步","link":"#远程同步","children":[]},{"level":2,"title":"撤销","slug":"撤销","link":"#撤销","children":[]},{"level":2,"title":"忽略文件配置(.gitignore)","slug":"忽略文件配置-gitignore","link":"#忽略文件配置-gitignore","children":[]},{"level":2,"title":"参考资料","slug":"参考资料","link":"#参考资料","children":[]}],"relativePath":"fe-utils/git.md","lastUpdated":1707646177000}'),p={name:"fe-utils/git.md"},e=l("",193),o=[e];function c(r,t,i,d,B,y){return a(),n("div",null,o)}const h=s(p,[["render",c]]);export{b as __pageData,h as default}; diff --git "a/assets/fe-utils_js\345\267\245\345\205\267\345\272\223.md.361d6082.js" "b/assets/fe-utils_js\345\267\245\345\205\267\345\272\223.md.361d6082.js" new file mode 100644 index 00000000..70b25f61 --- /dev/null +++ "b/assets/fe-utils_js\345\267\245\345\205\267\345\272\223.md.361d6082.js" @@ -0,0 +1,279 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-18-09-43.38e75eb4.png",h=JSON.parse('{"title":"前端 JavaScript 必会工具库合集","description":"","frontmatter":{},"headers":[{"level":2,"title":"工具库集合","slug":"工具库集合","link":"#工具库集合","children":[{"level":3,"title":"jQuery: 让操作DOM变得更容易","slug":"jquery-让操作dom变得更容易","link":"#jquery-让操作dom变得更容易","children":[]},{"level":3,"title":"Lodash: 你能想到的工具函数它都帮你写了","slug":"lodash-你能想到的工具函数它都帮你写了","link":"#lodash-你能想到的工具函数它都帮你写了","children":[]},{"level":3,"title":"Animate.css: 常见的CSS动画效果都帮你写好了","slug":"animate-css-常见的css动画效果都帮你写好了","link":"#animate-css-常见的css动画效果都帮你写好了","children":[]},{"level":3,"title":"Axios:让网络请求变得更简单","slug":"axios-让网络请求变得更简单","link":"#axios-让网络请求变得更简单","children":[]},{"level":3,"title":"MockJS:ajax拦截和模拟数据生成","slug":"mockjs-ajax拦截和模拟数据生成","link":"#mockjs-ajax拦截和模拟数据生成","children":[]},{"level":3,"title":"Moment:让日期处理更容易","slug":"moment-让日期处理更容易","link":"#moment-让日期处理更容易","children":[]},{"level":3,"title":"ECharts:搞定所有你能想到的图表📈","slug":"echarts-搞定所有你能想到的图表📈","link":"#echarts-搞定所有你能想到的图表📈","children":[]},{"level":3,"title":"animejs:简单好用的JS动画库","slug":"animejs-简单好用的js动画库","link":"#animejs-简单好用的js动画库","children":[]},{"level":3,"title":"editormd:markdown编辑器","slug":"editormd-markdown编辑器","link":"#editormd-markdown编辑器","children":[]},{"level":3,"title":"validate:简单好用的JS对象验证库","slug":"validate-简单好用的js对象验证库","link":"#validate-简单好用的js对象验证库","children":[]},{"level":3,"title":"date-fns:功能和Moment几乎相同","slug":"date-fns-功能和moment几乎相同","link":"#date-fns-功能和moment几乎相同","children":[]},{"level":3,"title":"zepto:功能和jQuery几乎相同","slug":"zepto-功能和jquery几乎相同","link":"#zepto-功能和jquery几乎相同","children":[]},{"level":3,"title":"nprogress:简单好用的进度条插件YouTube就使用的是它","slug":"nprogress-简单好用的进度条插件youtube就使用的是它","link":"#nprogress-简单好用的进度条插件youtube就使用的是它","children":[]},{"level":3,"title":"qs:一个用于解析url的小工具","slug":"qs-一个用于解析url的小工具","link":"#qs-一个用于解析url的小工具","children":[]}]},{"level":2,"title":"使用方式","slug":"使用方式","link":"#使用方式","children":[{"level":3,"title":"对于第三方库,除了下载使用,还可以通过CDN在线使用","slug":"对于第三方库-除了下载使用-还可以通过cdn在线使用","link":"#对于第三方库-除了下载使用-还可以通过cdn在线使用","children":[]},{"level":3,"title":"JQuery","slug":"jquery","link":"#jquery","children":[]},{"level":3,"title":"Lodash","slug":"lodash","link":"#lodash","children":[]},{"level":3,"title":"Animate.css","slug":"animate-css","link":"#animate-css","children":[]},{"level":3,"title":"Axios","slug":"axios","link":"#axios","children":[]},{"level":3,"title":"MockJS","slug":"mockjs","link":"#mockjs","children":[]},{"level":3,"title":"Moment","slug":"moment","link":"#moment","children":[]}]}],"relativePath":"fe-utils/js工具库.md","lastUpdated":1673001921000}'),e={name:"fe-utils/js工具库.md"},o=l('

前端 JavaScript 必会工具库合集

工具库集合

jQuery: 让操作DOM变得更容易

官网:https://jquery.com/

中文网:https://jquery.cuishifeng.cn/

Lodash: 你能想到的工具函数它都帮你写了

官网:https://lodash.com/docs

中文网:https://www.lodashjs.com/

Animate.css: 常见的CSS动画效果都帮你写好了

官网:https://animate.style/

Axios:让网络请求变得更简单

官网:https://axios-http.com/zh/

MockJS:ajax拦截和模拟数据生成

官网:http://mockjs.com/

Moment:让日期处理更容易

官网:https://momentjs.com/

中文网:http://momentjs.cn/

ECharts:搞定所有你能想到的图表📈

官网:https://echarts.apache.org/zh

animejs:简单好用的JS动画库

官网:https://animejs.com/

editormd:markdown编辑器

官网:https://pandao.github.io/editor.md | |

validate:简单好用的JS对象验证库

官网:http://validatejs.org/

date-fns:功能和Moment几乎相同

官网:https://date-fns.org/

支持tree shaking

zepto:功能和jQuery几乎相同

官网:https://zeptojs.com/

对移动端支持更好,包体积更小

nprogress:简单好用的进度条插件YouTube就使用的是它

官网:https://github.com/rstacruz/nprogress

qs:一个用于解析url的小工具

官网:https://github.com/ljharb/qs

使用方式

对于第三方库,除了下载使用,还可以通过CDN在线使用

科普知识:CDN

CDN称之为内容分发网络(Content Delivery Network)。

简单来说,就是提供很多的服务器,用户访问时,自动就近选择服务器给用户提供资源

国内使用广泛的免费CDN站点:https://www.bootcdn.cn/

JQuery

官网:https://jquery.com/

中文网:https://jquery.cuishifeng.cn/

CDN:https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js

针对DOM的操作无非以下几种:

  • 获取它
  • 创建它
  • 监听它
  • 改变它

JQuery可以让上面整个过程更加轻松

jQuery函数

jQuery提供了一个函数,名称为jQuery,也可以写作$

该函数提供了强大的DOM控制能力

通过下面的示例,可以快速理解jQuery的核心功能

javascript
// 获取类样式为container的所有DOM
+const container = $(".container")
+
+// 获取container后面的兄弟元素,元素类样式必须包含list
+container.nextAll(".list");
+
+// 删除元素
+container.remove();
+
+// 找到所有类样式为list元素的后代li元素,给它们加上类样式item
+$(".list li").addClass("item");
+
+// 为所有p元素添加一些style
+$("p").css({ "color": "#ff0011", "background": "blue" });
+
+// 注册DOMContentLoaded事件
+$(function(){ 
+	// ...
+})
+
+// 给所有li元素注册点击事件
+$("li").click(function(){
+  // ...
+})
+
+// 创建一个a元素,设置其内容为link,然后将它作为子元素追加到类样式为.list的元素中
+$("<a>").text("link").appendTo(".list");
+
// 获取类样式为container的所有DOM
+const container = $(".container")
+
+// 获取container后面的兄弟元素,元素类样式必须包含list
+container.nextAll(".list");
+
+// 删除元素
+container.remove();
+
+// 找到所有类样式为list元素的后代li元素,给它们加上类样式item
+$(".list li").addClass("item");
+
+// 为所有p元素添加一些style
+$("p").css({ "color": "#ff0011", "background": "blue" });
+
+// 注册DOMContentLoaded事件
+$(function(){ 
+	// ...
+})
+
+// 给所有li元素注册点击事件
+$("li").click(function(){
+  // ...
+})
+
+// 创建一个a元素,设置其内容为link,然后将它作为子元素追加到类样式为.list的元素中
+$("<a>").text("link").appendTo(".list");
+

下面依次介绍jQuery中的核心概念,以便于文档查阅

jQuery对象和DOM对象

通过jQuery得到的元素是一个jQuery对象,而不是传统的DOM

jQuery对象是一个伪数组,它和DOM元素的关系如下

jQuery对象和DOM之间可以互相转换

javascript
// jQuery -> DOM
+jQuery对象[索引]
+jQuery对象.get(索引)
+
+// DOM -> jQuery
+$(DOM对象)
+
// jQuery -> DOM
+jQuery对象[索引]
+jQuery对象.get(索引)
+
+// DOM -> jQuery
+$(DOM对象)
+

官网文档中的目录

目录名内容
选择器选择器是一个字符串,用于描述要选中哪些元素
筛选在当前jQuery对象的基础上,进一步选中元素
文档处理更改HTML文档结构,例如删除元素、清空元素内容、改变元素之间的关系
属性控制元素属性,例如修改类样式、读取和设置文本框的value、读取和设置img的src
css控制元素style样式,例如改变字体颜色、设置背景、获取元素尺寸、获取和设置滚动位置
事件监听元素的事件,例如监听文档加载完成、监听元素被点击
ajaxjQuery封装了XHR,使ajax访问更加方便
这部分功能目前已被其他第三方库全面超越

Lodash

官网:https://lodash.com/docs

中文网:https://www.lodashjs.com/

CDN:https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js

Lodash是一个针对ES的古老工具库,它出现在ES5之前

Lodash提供了大量的API,弥补了ES中对象、函数、数组API不足的问题

你可以想到的大部分工具函数,都可以在Lodash中找到

如果你不编写框架或通用库,一般不会用到Lodash

Animate.css

官网:https://animate.style/

CDN:https://cdn.bootcdn.net/ajax/libs/animate.css/4.1.1/animate.min.css

Animate.css 库提供了大量的动画效果,开发者仅需使用它提供的类名即可

注意:animate.css中的动画对行盒无效

基本使用

类名格式为:

animate__animated animate__效果名
+
animate__animated animate__效果名
+

效果名分为以下几个大类,你可以从官网中找到对应的分类,每个分类下有多种效果名可供使用

分类含义
Attention seekers强调
Back entrances进入
Back exits退出
Bouncing entrances弹跳进入
Bouncing exits弹跳退出
Fading entrances淡入
Fading exits淡出
Flippers翻转
Lightspeed光速
Rotating entrances旋转进入
Rotating exits旋转退出
Specials特殊效果
Zooming entrances缩放进入
Zooming exits缩放退出
Sliding entrances滑动进入
Sliding exits滑动退出

工具类

Animate.css 还提供了多个工具类,可以控制动画的延时重复次数速度

  • 延时
css
/* 默认无延时 */
+animate__delay-1s  /* 延时1秒 */
+animate__delay-2s  /* 延时2秒 */
+animate__delay-3s  /* 延时3秒 */
+animate__delay-4s  /* 延时4秒 */
+animate__delay-5s  /* 延时5秒 */
+
/* 默认无延时 */
+animate__delay-1s  /* 延时1秒 */
+animate__delay-2s  /* 延时2秒 */
+animate__delay-3s  /* 延时3秒 */
+animate__delay-4s  /* 延时4秒 */
+animate__delay-5s  /* 延时5秒 */
+
  • 重复次数
css
/* 默认重复1次 */
+animate__repeat-2	/* 重复2次 */
+animate__repeat-3	/* 重复3次 */
+animate__infinite	/* 重复无限次 */
+
/* 默认重复1次 */
+animate__repeat-2	/* 重复2次 */
+animate__repeat-3	/* 重复3次 */
+animate__infinite	/* 重复无限次 */
+
  • 速度
css
/* 默认1秒内完成动画 */
+animate__slow /* 2秒内完成动画 */
+animate__slower	/* 3秒内完成动画 */
+animate__fast	/* 800毫秒内完成动画 */
+animate__faster	/* 500毫秒内完成动画 */
+
/* 默认1秒内完成动画 */
+animate__slow /* 2秒内完成动画 */
+animate__slower	/* 3秒内完成动画 */
+animate__fast	/* 800毫秒内完成动画 */
+animate__faster	/* 500毫秒内完成动画 */
+

示例:

html
<!-- 
+使用animate.css
+动画名:bounce
+速度:快
+重复:无限次
+延迟:1秒
+-->
+<h1
+    class="
+           animate__animated
+           animate__bounce
+           animate_fast
+           animate__infinite
+           animate__delay-1s
+           "
+    >
+  Hello Animate
+</h1>
+
<!-- 
+使用animate.css
+动画名:bounce
+速度:快
+重复:无限次
+延迟:1秒
+-->
+<h1
+    class="
+           animate__animated
+           animate__bounce
+           animate_fast
+           animate__infinite
+           animate__delay-1s
+           "
+    >
+  Hello Animate
+</h1>
+

Axios

官网:https://axios-http.com/zh/

CDN:https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js

axios是一个请求库,在浏览器环境中,它封装了XHR,提供更加便捷的API发送请求

基本使用

javascript
// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 get 请求到 https://study.duyiedu.com/api/user/exists?loginId=abc,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/user/exists", {
+  params: { // 这里可以配置 query,axios会自动将其进行Url编码
+    loginId: "abc"
+  },
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 post 请求到 https://study.duyiedu.com/api/user/reg
+// axios 会将第二个参数转换为JSON格式的请求体
+axios.post("https://study.duyiedu.com/api/user/reg", {
+  loginId: 'abc',
+  loginPwd: '123123',
+  nickname: '棒棒鸡'
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 get 请求到 https://study.duyiedu.com/api/user/exists?loginId=abc,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/user/exists", {
+  params: { // 这里可以配置 query,axios会自动将其进行Url编码
+    loginId: "abc"
+  },
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 post 请求到 https://study.duyiedu.com/api/user/reg
+// axios 会将第二个参数转换为JSON格式的请求体
+axios.post("https://study.duyiedu.com/api/user/reg", {
+  loginId: 'abc',
+  loginPwd: '123123',
+  nickname: '棒棒鸡'
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+

axios的基本用法为:

javascript
axios.get(url地址, [请求配置]);
+axios.post(url地址, [请求体对象], [请求配置]);
+
+// 或直接使用 axios 方法,在请求配置中填写请求方法
+axios(请求配置);
+
axios.get(url地址, [请求配置]);
+axios.post(url地址, [请求体对象], [请求配置]);
+
+// 或直接使用 axios 方法,在请求配置中填写请求方法
+axios(请求配置);
+

实例

axios允许开发者先创建一个实例,后续通过使用实例进行请求

这样做的好处是可以预先进行某些配置

示例:

javascript
// 创建实例
+const instance = axios.create({
+  baseURL: 'https://study.duyiedu.com/'
+});
+
+// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+instance.get("/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
// 创建实例
+const instance = axios.create({
+  baseURL: 'https://study.duyiedu.com/'
+});
+
+// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+instance.get("/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+

拦截器

有时,我们可能需要对所有的请求或响应做一些统一的处理

比如,在请求时,如果发现本地有token,需要附带到请求头

又比如,在拿到响应后,我们仅需要取响应体中的data属性

再比如,如果发生错误,我们需要做一个弹窗显示

这些统一的行为就非常适合使用拦截器

javascript
// 添加请求拦截器
+axios.interceptors.request.use(function (config) {
+  // config 为当前的请求配置
+  // 在发送请求之前做些什么
+  // 这里,我们添加一个请求头
+  const token = localStorage.getItem('token');
+  if(token){
+    config.headers.authorization = token;
+  }
+  return config; // 返回处理后的配置
+});
+
+// 添加响应拦截器
+axios.interceptors.response.use(function (resp) {
+  // 2xx 范围内的状态码都会触发该函数。
+  // 对响应数据做点什么
+  return resp.data.data; // 仅得到响应体中的data属性
+}, function (error) {
+  // 超出 2xx 范围的状态码都会触发该函数。
+  // 对响应错误做点什么
+  alert(error.message); // 弹出错误消息
+});
+
// 添加请求拦截器
+axios.interceptors.request.use(function (config) {
+  // config 为当前的请求配置
+  // 在发送请求之前做些什么
+  // 这里,我们添加一个请求头
+  const token = localStorage.getItem('token');
+  if(token){
+    config.headers.authorization = token;
+  }
+  return config; // 返回处理后的配置
+});
+
+// 添加响应拦截器
+axios.interceptors.response.use(function (resp) {
+  // 2xx 范围内的状态码都会触发该函数。
+  // 对响应数据做点什么
+  return resp.data.data; // 仅得到响应体中的data属性
+}, function (error) {
+  // 超出 2xx 范围的状态码都会触发该函数。
+  // 对响应错误做点什么
+  alert(error.message); // 弹出错误消息
+});
+

设置好拦截器后,后续的请求和响应都会触发对应的函数

拦截器可以针对axios实例进行设置

MockJS

官网:http://mockjs.com/

CDN:https://cdn.bootcdn.net/ajax/libs/Mock.js/1.0.0/mock-min.js

MockJS有两个作用:

  1. 产生模拟数据
  2. 拦截Ajax

下面两张图说明MockJS的作用

仅模拟数据

javascript
Mock.mock(数据模板)
+
Mock.mock(数据模板)
+

数据模板有其特有的书写规范,具体写法见官网

拦截+模拟数据

javascript
Mock.mock(要拦截的url, 要拦截的请求方法, 数据模板)
+
Mock.mock(要拦截的url, 要拦截的请求方法, 数据模板)
+

更多用法见官网

注意,MockJS拦截数据的原理是重写了XHR,因此它仅能拦截XHR的数据请求,而无法拦截使用fetch发出的请求

具体的,MockJS可以拦截:

  • 原生XmlHttpRequest
  • jQuery中的$.ajax
  • axios

MockJS可以模拟网络延时,用法为:

javascript
Mock.setup({
+  timeout: 400 // 网络延时400毫秒
+})
+
+Mock.setup({
+  timeout: '200-600' // 网络延时200-600毫秒
+})
+
Mock.setup({
+  timeout: 400 // 网络延时400毫秒
+})
+
+Mock.setup({
+  timeout: '200-600' // 网络延时200-600毫秒
+})
+

Moment

官网:https://momentjs.com/

中文网:http://momentjs.cn/

CDN:https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js

各种语言包:https://www.bootcdn.cn/moment.js/

Moment提供了强大的日期处理能力

时间基础知识

单位

单位名称换算
hour小时1 day = 24 hours
minute分钟1 hour = 60 minutes
second1 minute = 60 seconds
millisecond (ms)毫秒1 second = 1000 ms
nanosecond (ns)纳秒1 ms = 1000 ns

GMT和UTC

世界划分为24个时区,北京在东8区,格林威治在0时区。

GMT:Greenwish Mean Time 格林威治世界时。太阳时,精确到毫秒。

UTC:Universal Time Coodinated 世界协调时。以原子时间为计时标准,精确到纳秒。

国际标准中,已全面使用UTC时间,而不再使用GMT时间

GMT和UTC时间在文本表示格式上是一致的,均为星期缩写, 日期 月份 年份 时间 GMT,例如:

Thu, 27 Aug 2020 08:01:44 GMT
+
Thu, 27 Aug 2020 08:01:44 GMT
+

另外,ISO 8601标准规定,建议使用以下方式表示时间:

YYYY-MM-DDTHH:mm:ss.msZ
+例如:
+2020-08-27T08:01:44.000Z
+
YYYY-MM-DDTHH:mm:ss.msZ
+例如:
+2020-08-27T08:01:44.000Z
+

GMT、UTC、ISO 8601都表示的是零时区的时间

Unix 时间戳

Unix 时间戳(Unix Timestamp)是Unix系统最早提出的概念

它将UTC时间1970年1月1日凌晨作为起始时间,到指定时间经过的秒数(毫秒数)

程序中的时间处理

程序对时间的计算、存储务必使用UTC时间,或者时间戳

在和用户交互时,将UTC时间或时间戳转换为更加友好的文本

思考下面的问题:

  1. 用户的生日是本地时间还是UTC时间?
  2. 如果要比较两个日期的大小,是比较本地时间还是比较UTC时间?
  3. 如果要显示文章的发布日期,是显示本地时间还是显示UTC时间?
  4. 北京时间2020-8-28 10:00:00格林威治2020-8-28 02:00:00,两个时间哪个大,哪个小?
  5. 北京的时间戳为0格林威治的时间戳为0,它们的时间一样吗?
  6. 一个中国用户注册时填写的生日是1970-1-1,它出生的UTC时间是多少?时间戳是多少?

Moment的核心用法

Moment的使用分为两个部分:

  1. 获得Moment对象
  2. 针对Moment对象做各种操作
`,152),t=[o];function r(c,i,B,y,d,F){return a(),n("div",null,t)}const m=s(e,[["render",r]]);export{h as __pageData,m as default}; diff --git "a/assets/fe-utils_js\345\267\245\345\205\267\345\272\223.md.361d6082.lean.js" "b/assets/fe-utils_js\345\267\245\345\205\267\345\272\223.md.361d6082.lean.js" new file mode 100644 index 00000000..d4affdda --- /dev/null +++ "b/assets/fe-utils_js\345\267\245\345\205\267\345\272\223.md.361d6082.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-18-09-43.38e75eb4.png",h=JSON.parse('{"title":"前端 JavaScript 必会工具库合集","description":"","frontmatter":{},"headers":[{"level":2,"title":"工具库集合","slug":"工具库集合","link":"#工具库集合","children":[{"level":3,"title":"jQuery: 让操作DOM变得更容易","slug":"jquery-让操作dom变得更容易","link":"#jquery-让操作dom变得更容易","children":[]},{"level":3,"title":"Lodash: 你能想到的工具函数它都帮你写了","slug":"lodash-你能想到的工具函数它都帮你写了","link":"#lodash-你能想到的工具函数它都帮你写了","children":[]},{"level":3,"title":"Animate.css: 常见的CSS动画效果都帮你写好了","slug":"animate-css-常见的css动画效果都帮你写好了","link":"#animate-css-常见的css动画效果都帮你写好了","children":[]},{"level":3,"title":"Axios:让网络请求变得更简单","slug":"axios-让网络请求变得更简单","link":"#axios-让网络请求变得更简单","children":[]},{"level":3,"title":"MockJS:ajax拦截和模拟数据生成","slug":"mockjs-ajax拦截和模拟数据生成","link":"#mockjs-ajax拦截和模拟数据生成","children":[]},{"level":3,"title":"Moment:让日期处理更容易","slug":"moment-让日期处理更容易","link":"#moment-让日期处理更容易","children":[]},{"level":3,"title":"ECharts:搞定所有你能想到的图表📈","slug":"echarts-搞定所有你能想到的图表📈","link":"#echarts-搞定所有你能想到的图表📈","children":[]},{"level":3,"title":"animejs:简单好用的JS动画库","slug":"animejs-简单好用的js动画库","link":"#animejs-简单好用的js动画库","children":[]},{"level":3,"title":"editormd:markdown编辑器","slug":"editormd-markdown编辑器","link":"#editormd-markdown编辑器","children":[]},{"level":3,"title":"validate:简单好用的JS对象验证库","slug":"validate-简单好用的js对象验证库","link":"#validate-简单好用的js对象验证库","children":[]},{"level":3,"title":"date-fns:功能和Moment几乎相同","slug":"date-fns-功能和moment几乎相同","link":"#date-fns-功能和moment几乎相同","children":[]},{"level":3,"title":"zepto:功能和jQuery几乎相同","slug":"zepto-功能和jquery几乎相同","link":"#zepto-功能和jquery几乎相同","children":[]},{"level":3,"title":"nprogress:简单好用的进度条插件YouTube就使用的是它","slug":"nprogress-简单好用的进度条插件youtube就使用的是它","link":"#nprogress-简单好用的进度条插件youtube就使用的是它","children":[]},{"level":3,"title":"qs:一个用于解析url的小工具","slug":"qs-一个用于解析url的小工具","link":"#qs-一个用于解析url的小工具","children":[]}]},{"level":2,"title":"使用方式","slug":"使用方式","link":"#使用方式","children":[{"level":3,"title":"对于第三方库,除了下载使用,还可以通过CDN在线使用","slug":"对于第三方库-除了下载使用-还可以通过cdn在线使用","link":"#对于第三方库-除了下载使用-还可以通过cdn在线使用","children":[]},{"level":3,"title":"JQuery","slug":"jquery","link":"#jquery","children":[]},{"level":3,"title":"Lodash","slug":"lodash","link":"#lodash","children":[]},{"level":3,"title":"Animate.css","slug":"animate-css","link":"#animate-css","children":[]},{"level":3,"title":"Axios","slug":"axios","link":"#axios","children":[]},{"level":3,"title":"MockJS","slug":"mockjs","link":"#mockjs","children":[]},{"level":3,"title":"Moment","slug":"moment","link":"#moment","children":[]}]}],"relativePath":"fe-utils/js工具库.md","lastUpdated":1673001921000}'),e={name:"fe-utils/js工具库.md"},o=l("",152),t=[o];function r(c,i,B,y,d,F){return a(),n("div",null,t)}const m=s(e,[["render",r]]);export{h as __pageData,m as default}; diff --git a/assets/fe-utils_tool.md.1bfe9ae2.js b/assets/fe-utils_tool.md.1bfe9ae2.js new file mode 100644 index 00000000..b6cfd320 --- /dev/null +++ b/assets/fe-utils_tool.md.1bfe9ae2.js @@ -0,0 +1 @@ +import{_ as e,o,c as a,b as t,d as s}from"./app.f983686f.js";const M=JSON.parse('{"title":"涌现出来的新tools","description":"","frontmatter":{},"headers":[],"relativePath":"fe-utils/tool.md","lastUpdated":1710679304000}'),n={name:"fe-utils/tool.md"},i=t("h1",{id:"涌现出来的新tools",tabindex:"-1"},[s("涌现出来的新tools "),t("a",{class:"header-anchor",href:"#涌现出来的新tools","aria-hidden":"true"},"#")],-1),r=t("p",null,"apifox:一款国产的 API 管理神器",-1),c=t("p",null,"集成了 Postman + Swagger + Mock + JMeter 众多功能",-1),l=t("p",null,"可以让生成 api 文档、api调试、api Mock、api 自动化测试 变的十分高效,简单。",-1),d=t("p",null,"eolink | 国内第一个集Swagger+Postman+Mock+Jmeter单点工具于一身的 api 管理平台 | 以 api 为中心的前后端开发流程",-1),p=[i,r,c,l,d];function _(h,u,f,m,k,x){return o(),a("div",null,p)}const P=e(n,[["render",_]]);export{M as __pageData,P as default}; diff --git a/assets/fe-utils_tool.md.1bfe9ae2.lean.js b/assets/fe-utils_tool.md.1bfe9ae2.lean.js new file mode 100644 index 00000000..b6cfd320 --- /dev/null +++ b/assets/fe-utils_tool.md.1bfe9ae2.lean.js @@ -0,0 +1 @@ +import{_ as e,o,c as a,b as t,d as s}from"./app.f983686f.js";const M=JSON.parse('{"title":"涌现出来的新tools","description":"","frontmatter":{},"headers":[],"relativePath":"fe-utils/tool.md","lastUpdated":1710679304000}'),n={name:"fe-utils/tool.md"},i=t("h1",{id:"涌现出来的新tools",tabindex:"-1"},[s("涌现出来的新tools "),t("a",{class:"header-anchor",href:"#涌现出来的新tools","aria-hidden":"true"},"#")],-1),r=t("p",null,"apifox:一款国产的 API 管理神器",-1),c=t("p",null,"集成了 Postman + Swagger + Mock + JMeter 众多功能",-1),l=t("p",null,"可以让生成 api 文档、api调试、api Mock、api 自动化测试 变的十分高效,简单。",-1),d=t("p",null,"eolink | 国内第一个集Swagger+Postman+Mock+Jmeter单点工具于一身的 api 管理平台 | 以 api 为中心的前后端开发流程",-1),p=[i,r,c,l,d];function _(h,u,f,m,k,x){return o(),a("div",null,p)}const P=e(n,[["render",_]]);export{M as __pageData,P as default}; diff --git a/assets/fragment_Monorepo.md.3da7c407.js b/assets/fragment_Monorepo.md.3da7c407.js new file mode 100644 index 00000000..560ab6db --- /dev/null +++ b/assets/fragment_Monorepo.md.3da7c407.js @@ -0,0 +1 @@ +import{_ as o,o as e,c as a,a as n}from"./app.f983686f.js";const l="/blog/assets/2024-04-09-20-41-59.c3e21810.png",u=JSON.parse('{"title":"monorepo","description":"","frontmatter":{},"headers":[{"level":2,"title":"npm 的 install 流程","slug":"npm-的-install-流程","link":"#npm-的-install-流程","children":[]},{"level":2,"title":"package-lock.json","slug":"package-lock-json","link":"#package-lock-json","children":[]},{"level":2,"title":"npm 和 yarn","slug":"npm-和-yarn","link":"#npm-和-yarn","children":[]},{"level":2,"title":"monorepo 方案的优势","slug":"monorepo-方案的优势","link":"#monorepo-方案的优势","children":[]},{"level":2,"title":"monorepo 方案的劣势","slug":"monorepo-方案的劣势","link":"#monorepo-方案的劣势","children":[]},{"level":2,"title":"如何取舍?","slug":"如何取舍","link":"#如何取舍","children":[]}],"relativePath":"fragment/Monorepo.md","lastUpdated":1712922474000}'),r={name:"fragment/Monorepo.md"},p=n('

monorepo

npm 的 install 流程

package-lock.json

package-lock.json 的作用是进行锁版本号,保证整个开发团队的版本号统一,使用 monorepo 的项目有可能会提到一个最外层进行一个管理。

  • 为什么需要这个 package-lock.json package.json 的 semantic versioning(语意化版本控制),在不同的时间会安装不同的版本,如果没有 package-lock.json,不同的开发者可能就会得到不同版本的依赖,如果因为这个出现了一个 bug 的话,那排查起来也许会非常困难。
  • 更新规则 Npm v 5.4.2 以上:当 package.json 声明的版本依赖规范和 package-lock.json 安装版本兼容,则根据 package-lock json 安装依赖:如果两者不兼容,那么按照 package.json 安装依赖,并更新 package- lock.json 跟随 package.json 的语意化版本控制来进行更新,如果 package.json 中的依赖 a 的版本是^1.0.0,在这个时候如果 package-lock.json 中的版本锁定为 1.12.1 就是符合要求的不必重写,但如果是 0.12.11 即第一个数字变了,那就需要重写 package-lock.json 了。
  • 版本规则
    • ^: 只会执行不更改最左边非零数字的更新。 如果写入的是 ^0.13.0,则当运行 npm update 时,可以更新到 0.13.1、0.13.2 等,但不能更新到 0.14.0 或更高版本。 如果写入的是 ^1.13.0,则当运行 npm update 时,可以更新到 1.13.1、1.14.0 等,但不能更新到 2.0.0 或更高版本。
    • ~: 如果写入的是 〜0.13.0,则当运行 npm update 时,会更新到补丁版本:即 0.13.1 可以,但 0.14.0 不可以。
    • : 接受高于指定版本的任何版本。

    • =: 接受等于或高于指定版本的任何版本。

    • =: 接受确切的版本。
    • -: 接受一定范围的版本。例如:2.1.0 - 2.6.2。
    • ||: 组合集合。例如 < 2.1 || > 2.6。

npm 和 yarn

缺点。下面将两者进行比较

  1. 性能

每当 Yarn 或 npm 需要安装包时,它们都会执行一系列任务。在 npm 中,这些任务是按包顺序执行的,这意味着它会等待一个包完全安装,然后再继续下一个。相比之下,Yarn 并行执行这些任务,从而提高了性能。 虽然这两个管理器都提供缓存机制,但 Yarn 似乎做得更好一些。 尽管 Yarn 有一些优势,但 Yarn 和 npm 在它们的最新版本中的速度相当。所以我们不能评判孰优孰劣。

  1. 依赖版本

早期的时候 yarn 有 yarn.lock 来锁定版本,这一点上比 package.json 要强很多,而后面 npm 也推出了 package-lock.json,所以这一点上已经没太多差异了。

  1. 安全性

从版本 6 开始,npm 会在安装过程中审核软件包并告诉您是否发现了任何漏洞。我们可以通过 npm audit 针对已安装的软件包运行来手动执行此检查。如果发现任何漏洞,npm 会给我们安全建议。 Yarn 和 npm 都使用加密哈希算法来确保包的完整性。

  1. 工作区

工作区允许您拥有一个 monorepo 来管理跨多个项目的依赖项。这意味着您有一个单一的顶级根包,其中包含多个称为工作区的子包。

  1. 用哪个?

目前 2021 年,yarn 的安装速度还是比 npm 快,其他地方的差异并不大,基本上可以忽略,用哪个都行。

monorepo

monorepo 方案的优势

  1. 代码重用将变得非常容易:由于所有的项目代码都集中于一个代码仓库,我们将很容易抽离出各个项目共用的业务组件或工具,并通过 TypeScript,Lerna 或其他工具进行代码内引用;
  2. 依赖管理将变得非常简单:同理,由于项目之间的引用路径内化在同一个仓库之中,我们很容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用一些工具,我们将很容易地做到版本依赖管理和版本号自动升级;
  3. 代码重构将变得非常便捷:想想究竟是什么在阻止您进行代码重构,很多时候,原因来自于「不确定性」,您不确定对某个项目的修改是否对于其他项目而言是「致命的」,出于对未知的恐惧,您会倾向于不重构代码,这将导致整个项目代码的腐烂度会以惊人的速度增长。而在 monorepo 策略的指导下,您能够明确知道您的代码的影响范围,并且能够对被影响的项目可以进行统一的测试,这会鼓励您不断优化代码;
  4. 它倡导了一种开放,透明,共享的组织文化,这有利于开发者成长,代码质量的提升:在 monorepo 策略下,每个开发者都被鼓励去查看,修改他人的代码(只要有必要),同时,也会激起开发者维护代码,和编写单元测试的责任心(毕竟朋友来访之前,我们从不介意自己的房子究竟有多乱),这将会形成一种良性的技术氛围,从而保障整个组织的代码质量。

monorepo 方案的劣势

  1. 项目粒度的权限管理变得非常复杂:无论是 Git 还是其他 VCS 系统,在支持 monorepo 策略中项目粒度的权限管理上都没有令人满意的方案,这意味着 A 部门的 a 项目若是不想被 B 部门的开发者看到就很难了。(好在我们可以将 monorepo 策略实践在「项目级」这个层次上,这才是我们这篇文章的主题,我们后面会再次明确它);
  2. 新员工的学习成本变高:不同于一个项目一个代码仓库这种模式下,组织新人只要熟悉特定代码仓库下的代码逻辑,在 monorepo 策略下,新人可能不得不花更多精力来理清各个代码仓库之间的相互逻辑,当然这个成本可以通过新人文档的方式来解决,但维护文档的新鲜又需要消耗额外的人力;
  3. 对于公司级别的 monorepo 策略而言,需要专门的 VFS 系统,自动重构工具的支持:设想一下 Google 这样的企业是如何将十亿行的代码存储在一个仓库之中的?开发人员每次拉取代码需要等待多久?各个项目代码之间又如何实现权限管理,敏捷发布?任何简单的策略乘以足够的规模量级都会产生一个奇迹(不管是好是坏),对于中小企业而言,如果没有像 Google,Facebook 这样雄厚的人力资源,把所有项目代码放在同一个仓库里这个美好的愿望就只能是个空中楼阁。

如何取舍?

没错,软件开发领域从来没有「银弹」。monorepo 策略也并不完美,并且,我在实践中发现,要想完美在组织中运用 monorepo 策略,所需要的不仅是出色的编程技巧和耐心。团队日程,组织文化和个人影响力相互碰撞的最终结果才决定了想法最终是否能被实现。

但是请别灰心的太早,因为虽然让组织作出改变,统一施行 monorepo 策略困难重重,但这却并不意味着我们需要彻底跟 monorepo 策略说再见。我们还可以把 monorepo 策略实践在「项目」这个级别,即从逻辑上确定项目与项目之间的关联性,然后把相关联的项目整合在同一个仓库下,通常情况下,我们不会有太多相互关联的项目,这意味着我们能够免费得到 monorepo 策略的所有好处,并且可以拒绝支付大型 monorepo 架构的利息。

',26),i=[p];function t(c,s,m,d,h,k){return e(),a("div",null,i)}const _=o(r,[["render",t]]);export{u as __pageData,_ as default}; diff --git a/assets/fragment_Monorepo.md.3da7c407.lean.js b/assets/fragment_Monorepo.md.3da7c407.lean.js new file mode 100644 index 00000000..45bffa4a --- /dev/null +++ b/assets/fragment_Monorepo.md.3da7c407.lean.js @@ -0,0 +1 @@ +import{_ as o,o as e,c as a,a as n}from"./app.f983686f.js";const l="/blog/assets/2024-04-09-20-41-59.c3e21810.png",u=JSON.parse('{"title":"monorepo","description":"","frontmatter":{},"headers":[{"level":2,"title":"npm 的 install 流程","slug":"npm-的-install-流程","link":"#npm-的-install-流程","children":[]},{"level":2,"title":"package-lock.json","slug":"package-lock-json","link":"#package-lock-json","children":[]},{"level":2,"title":"npm 和 yarn","slug":"npm-和-yarn","link":"#npm-和-yarn","children":[]},{"level":2,"title":"monorepo 方案的优势","slug":"monorepo-方案的优势","link":"#monorepo-方案的优势","children":[]},{"level":2,"title":"monorepo 方案的劣势","slug":"monorepo-方案的劣势","link":"#monorepo-方案的劣势","children":[]},{"level":2,"title":"如何取舍?","slug":"如何取舍","link":"#如何取舍","children":[]}],"relativePath":"fragment/Monorepo.md","lastUpdated":1712922474000}'),r={name:"fragment/Monorepo.md"},p=n("",26),i=[p];function t(c,s,m,d,h,k){return e(),a("div",null,i)}const _=o(r,[["render",t]]);export{u as __pageData,_ as default}; diff --git a/assets/fragment_api-no-repeat.md.9e7ac149.js b/assets/fragment_api-no-repeat.md.9e7ac149.js new file mode 100644 index 00000000..c0d5e6f9 --- /dev/null +++ b/assets/fragment_api-no-repeat.md.9e7ac149.js @@ -0,0 +1,297 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-16-16-01-56.0f2eeef2.png",o="/blog/assets/2024-04-16-17-22-00.eef1000a.png",e="/blog/assets/2024-04-16-17-23-18.2bad2a94.png",r="/blog/assets/2024-04-16-17-25-36.abbf51b0.png",E=JSON.parse('{"title":"前端接口防止重复请求实现方案","description":"","frontmatter":{},"headers":[{"level":2,"title":"前言","slug":"前言","link":"#前言","children":[]},{"level":2,"title":"方案一:使用 axios 拦截器","slug":"方案一-使用-axios-拦截器","link":"#方案一-使用-axios-拦截器","children":[]},{"level":2,"title":"方案二:根据请求生成对应的 key","slug":"方案二-根据请求生成对应的-key","link":"#方案二-根据请求生成对应的-key","children":[]},{"level":2,"title":"方案三:相同的请求共享","slug":"方案三-相同的请求共享","link":"#方案三-相同的请求共享","children":[]},{"level":2,"title":"补充","slug":"补充","link":"#补充","children":[]}],"relativePath":"fragment/api-no-repeat.md","lastUpdated":1713259744000}'),c={name:"fragment/api-no-repeat.md"},B=l(`

前端接口防止重复请求实现方案

前言

前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加限制吗?前端加限制能够拦截的毕竟有限。可老板就是执意要前端搞一下子,行吧,搞就搞吧。 虽然大部分的接口处理我们都是加了 loading 的,但又不能确保真的是每个接口都加了的,可是如果要一个接口一个接口的排查,那这维护了四五年的系统,成百上千的接口肯定要耗费非常多的精力,根本就是不现实的,所以就只能去做 全局处理 。下面就来总结一下这次的防重复请求的实现方案:

方案一:使用 axios 拦截器

这个方案是最容易想到也是最 朴实无华 的一个方案:通过使用 axios 拦截器,在 请求拦截器 中开启全屏 Loading,然后在 响应拦截器 中将 Loading 关闭。

这个方案固然已经可以满足我们目前的需求,但不管三七二十一,直接搞个全屏 Loading 还是 不太美观 ,何况在目前项目的接口处理逻辑中还有一些 局部 Loading ,就有可能会出现 Loading 套 Loading 的情况,两个圈一起转,头皮发麻。

方案二:根据请求生成对应的 key

加 Loading 的方案不太友好,而对于同一个接口,如果传参都是一样的,一般来说都没有必要连续请求多次吧。那我们可不可以通过代码逻辑直接 把完全相同的请求给拦截掉 ,不让它到达服务端呢?这个思路不错,我们说干就干。 首先,我们要判断 什么样的请求属于是相同请求 :

一个请求包含的内容不外乎就是 请求方法 , 地址 , 参数 以及请求发出的 页面 hash 。那我们是不是就可以根据这几个数据把这个请求生成一个 key 来作为这个 请求的标识 呢?

js
// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+
// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+

有了请求的 key,我们就可以在请求拦截器中把每次发起的请求给 收集起来 ,后续如果有相同请求进来,那都去这个集合中去比对, 如果已经存在了,说明就是一个重复的请求 ,我们就给拦截掉。当请求完成响应后,再将这个请求从集合中移除。 合理,nice! 具体实现如下:

是不是觉得这种方案还不错,万事大吉?no,no,no! 这个方案虽然理论上是解决了接口防重复请求这个问题,但是它会引发更多的问题。比如,我有这样一个接口处理:

这里我连续点击了 4 次按钮,可以看到,的确是只有一个请求发送出去,可是因为在代码逻辑中,我们对错误进行了一些处理,所以就将报错消息提示了 3 次,这样是很不友好的,而且,如果在错误捕获中有做更多的逻辑处理,那么很有可能会导致整个程序的异常。

而且,这种方案还会有另外一个 比较严重的问题 :我们在上面在生成请求 key 的时候把 hash 考虑进去了( 如果是 history 路由,可以将 pathname 加入生成 key ),这是因为项目中会有一些数据字典型的接口,这些接口可能有不同页面都需要去调用,如果第一个页面请求的字典接口比较慢,第二个页面的接口就被拦截了,最后就会导致第二个页面逻辑错误。那么这么一看,我们生成 key 的时候加入了 hash ,讲道理就没问题了呀。

可是倘若我这 两个请求是来自同一个页面 呢?比如,一个页面同时加载两个组件,而这两个组件都需要调用某个接口时:

那么此时, 后调接口的组件就无法拿到正确数据了 。啊这,真是难顶!

方案三:相同的请求共享

延续我们方案二的前面思路,仍然是拦截相同请求,但这次我们可不可以 不直接把请求挂掉 ,而是 对于相同的请求我们先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求 。

思路我们已经明确了,但这里有几个需要注意的点:

  • 我们在拿到响应结果后,返回给之前我们 挂起的请求 时,我们要用到 发布订阅模式 (日常在面试题中看到,这次终于让我给用上了( ^▽^ ))
  • 对于挂起的请求,我们需要将它 拦截 ,不能让它执行正常的请求逻辑,所以一定要在 请求拦截器 中通过 return Promise.reject() 来直接中断请求,并做一些 特殊的标记 ,以便于 在响应拦截器中进行特殊处理 。
js
import axios from "axios";
+
+let instance = axios.create({
+  baseURL: "/api/",
+});
+
+// 发布订阅
+class EventEmitter {
+  constructor() {
+    this.event = {};
+  }
+  on(type, cbres, cbrej) {
+    if (!this.event[type]) {
+      this.event[type] = [[cbres, cbrej]];
+    } else {
+      this.event[type].push([cbres, cbrej]);
+    }
+  }
+
+  emit(type, res, ansType) {
+    if (!this.event[type]) return;
+    else {
+      this.event[type].forEach((cbArr) => {
+        if (ansType === "resolve") {
+          cbArr[0](res);
+        } else {
+          cbArr[1](res);
+        }
+      });
+    }
+  }
+}
+
+// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+
+// 存储已发送但未响应的请求
+const pendingRequest = new Set();
+// 发布订阅容器
+const ev = new EventEmitter();
+
+// 添加请求拦截器
+instance.interceptors.request.use(
+  async (config) => {
+    let hash = location.hash;
+    // 生成请求Key
+    let reqKey = generateReqKey(config, hash);
+
+    if (pendingRequest.has(reqKey)) {
+      // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
+      // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
+      let res = null;
+      try {
+        // 接口成功响应
+        res = await new Promise((resolve, reject) => {
+          ev.on(reqKey, resolve, reject);
+        });
+        return Promise.reject({
+          type: "limiteResSuccess",
+          val: res,
+        });
+      } catch (limitFunErr) {
+        // 接口报错
+        return Promise.reject({
+          type: "limiteResError",
+          val: limitFunErr,
+        });
+      }
+    } else {
+      // 将请求的key保存在config
+      config.pendKey = reqKey;
+      pendingRequest.add(reqKey);
+    }
+
+    return config;
+  },
+  function (error) {
+    return Promise.reject(error);
+  }
+);
+
+// 添加响应拦截器
+instance.interceptors.response.use(
+  function (response) {
+    // 将拿到的结果发布给其他相同的接口
+    handleSuccessResponse_limit(response);
+    return response;
+  },
+  function (error) {
+    return handleErrorResponse_limit(error);
+  }
+);
+
+// 接口响应成功
+function handleSuccessResponse_limit(response) {
+  const reqKey = response.config.pendKey;
+  if (pendingRequest.has(reqKey)) {
+    let x = null;
+    try {
+      x = JSON.parse(JSON.stringify(response));
+    } catch (e) {
+      x = response;
+    }
+    pendingRequest.delete(reqKey);
+    ev.emit(reqKey, x, "resolve");
+    delete ev.reqKey;
+  }
+}
+
+// 接口走失败响应
+function handleErrorResponse_limit(error) {
+  if (error.type && error.type === "limiteResSuccess") {
+    return Promise.resolve(error.val);
+  } else if (error.type && error.type === "limiteResError") {
+    return Promise.reject(error.val);
+  } else {
+    const reqKey = error.config.pendKey;
+    if (pendingRequest.has(reqKey)) {
+      let x = null;
+      try {
+        x = JSON.parse(JSON.stringify(error));
+      } catch (e) {
+        x = error;
+      }
+      pendingRequest.delete(reqKey);
+      ev.emit(reqKey, x, "reject");
+      delete ev.reqKey;
+    }
+  }
+  return Promise.reject(error);
+}
+
+export default instance;
+
import axios from "axios";
+
+let instance = axios.create({
+  baseURL: "/api/",
+});
+
+// 发布订阅
+class EventEmitter {
+  constructor() {
+    this.event = {};
+  }
+  on(type, cbres, cbrej) {
+    if (!this.event[type]) {
+      this.event[type] = [[cbres, cbrej]];
+    } else {
+      this.event[type].push([cbres, cbrej]);
+    }
+  }
+
+  emit(type, res, ansType) {
+    if (!this.event[type]) return;
+    else {
+      this.event[type].forEach((cbArr) => {
+        if (ansType === "resolve") {
+          cbArr[0](res);
+        } else {
+          cbArr[1](res);
+        }
+      });
+    }
+  }
+}
+
+// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+
+// 存储已发送但未响应的请求
+const pendingRequest = new Set();
+// 发布订阅容器
+const ev = new EventEmitter();
+
+// 添加请求拦截器
+instance.interceptors.request.use(
+  async (config) => {
+    let hash = location.hash;
+    // 生成请求Key
+    let reqKey = generateReqKey(config, hash);
+
+    if (pendingRequest.has(reqKey)) {
+      // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
+      // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
+      let res = null;
+      try {
+        // 接口成功响应
+        res = await new Promise((resolve, reject) => {
+          ev.on(reqKey, resolve, reject);
+        });
+        return Promise.reject({
+          type: "limiteResSuccess",
+          val: res,
+        });
+      } catch (limitFunErr) {
+        // 接口报错
+        return Promise.reject({
+          type: "limiteResError",
+          val: limitFunErr,
+        });
+      }
+    } else {
+      // 将请求的key保存在config
+      config.pendKey = reqKey;
+      pendingRequest.add(reqKey);
+    }
+
+    return config;
+  },
+  function (error) {
+    return Promise.reject(error);
+  }
+);
+
+// 添加响应拦截器
+instance.interceptors.response.use(
+  function (response) {
+    // 将拿到的结果发布给其他相同的接口
+    handleSuccessResponse_limit(response);
+    return response;
+  },
+  function (error) {
+    return handleErrorResponse_limit(error);
+  }
+);
+
+// 接口响应成功
+function handleSuccessResponse_limit(response) {
+  const reqKey = response.config.pendKey;
+  if (pendingRequest.has(reqKey)) {
+    let x = null;
+    try {
+      x = JSON.parse(JSON.stringify(response));
+    } catch (e) {
+      x = response;
+    }
+    pendingRequest.delete(reqKey);
+    ev.emit(reqKey, x, "resolve");
+    delete ev.reqKey;
+  }
+}
+
+// 接口走失败响应
+function handleErrorResponse_limit(error) {
+  if (error.type && error.type === "limiteResSuccess") {
+    return Promise.resolve(error.val);
+  } else if (error.type && error.type === "limiteResError") {
+    return Promise.reject(error.val);
+  } else {
+    const reqKey = error.config.pendKey;
+    if (pendingRequest.has(reqKey)) {
+      let x = null;
+      try {
+        x = JSON.parse(JSON.stringify(error));
+      } catch (e) {
+        x = error;
+      }
+      pendingRequest.delete(reqKey);
+      ev.emit(reqKey, x, "reject");
+      delete ev.reqKey;
+    }
+  }
+  return Promise.reject(error);
+}
+
+export default instance;
+

补充

到这里,这么一通操作下来上面的代码讲道理是万无一失了,但不得不说,线上的情况仍然是 复杂多样 的。而其中一个比较特殊的情况就是 文件上传 。

我在这里是上传了两个 不同的文件 的,但只调用了一次上传接口。按理说是两个不同的请求,可为什么会被我们前面写的逻辑给拦截掉一个呢?

可以看到, 请求体 data 中的数据是 FormData 类型 ,而我们在生成 请求 key 的时候,是通过 JSON.stringify 方法进行操作的,而对于 FormData 类型 的数据执行该函数得到的只有 {} 。所以,对于文件上传,尽管我们上传了不同的文件,但它们所发出的请求生成的 key 都是一样的,这么一来就触发了我们前面的拦截机制。 那么我们接下来我们只需要在我们原来的拦截逻辑中判断一下请求体的数据类型即可,如果含有 FormData 类型 的数据,我们就 直接放行 不再关注这个请求就是了。

js
function isFileUploadApi(config) {
+  return Object.prototype.toString.call(config.data) === "[object FormData]";
+}
+
function isFileUploadApi(config) {
+  return Object.prototype.toString.call(config.data) === "[object FormData]";
+}
+

demo

`,30),t=[B];function y(F,i,A,b,u,C){return n(),a("div",null,t)}const d=s(c,[["render",y]]);export{E as __pageData,d as default}; diff --git a/assets/fragment_api-no-repeat.md.9e7ac149.lean.js b/assets/fragment_api-no-repeat.md.9e7ac149.lean.js new file mode 100644 index 00000000..6282f878 --- /dev/null +++ b/assets/fragment_api-no-repeat.md.9e7ac149.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-16-16-01-56.0f2eeef2.png",o="/blog/assets/2024-04-16-17-22-00.eef1000a.png",e="/blog/assets/2024-04-16-17-23-18.2bad2a94.png",r="/blog/assets/2024-04-16-17-25-36.abbf51b0.png",E=JSON.parse('{"title":"前端接口防止重复请求实现方案","description":"","frontmatter":{},"headers":[{"level":2,"title":"前言","slug":"前言","link":"#前言","children":[]},{"level":2,"title":"方案一:使用 axios 拦截器","slug":"方案一-使用-axios-拦截器","link":"#方案一-使用-axios-拦截器","children":[]},{"level":2,"title":"方案二:根据请求生成对应的 key","slug":"方案二-根据请求生成对应的-key","link":"#方案二-根据请求生成对应的-key","children":[]},{"level":2,"title":"方案三:相同的请求共享","slug":"方案三-相同的请求共享","link":"#方案三-相同的请求共享","children":[]},{"level":2,"title":"补充","slug":"补充","link":"#补充","children":[]}],"relativePath":"fragment/api-no-repeat.md","lastUpdated":1713259744000}'),c={name:"fragment/api-no-repeat.md"},B=l("",30),t=[B];function y(F,i,A,b,u,C){return n(),a("div",null,t)}const d=s(c,[["render",y]]);export{E as __pageData,d as default}; diff --git a/assets/fragment_auto-try-catch.md.80aa30a2.js b/assets/fragment_auto-try-catch.md.80aa30a2.js new file mode 100644 index 00000000..7bac2414 --- /dev/null +++ b/assets/fragment_auto-try-catch.md.80aa30a2.js @@ -0,0 +1,879 @@ +import{_ as a,o as l,c as p,a as n,b as s}from"./app.f983686f.js";const d=JSON.parse('{"title":"如何给所有的 async 函数添加 try/catch?","description":"","frontmatter":{},"headers":[{"level":2,"title":"前言","slug":"前言","link":"#前言","children":[]},{"level":2,"title":"async 如果不加 try/catch 会发生什么事?","slug":"async-如果不加-try-catch-会发生什么事","link":"#async-如果不加-try-catch-会发生什么事","children":[]},{"level":2,"title":"babel 插件的最终效果","slug":"babel-插件的最终效果","link":"#babel-插件的最终效果","children":[]},{"level":2,"title":"babel 插件的实现思路","slug":"babel-插件的实现思路","link":"#babel-插件的实现思路","children":[]},{"level":2,"title":"babel 的核心:AST","slug":"babel-的核心-ast","link":"#babel-的核心-ast","children":[]},{"level":2,"title":"常用的 AST 节点类型对照表","slug":"常用的-ast-节点类型对照表","link":"#常用的-ast-节点类型对照表","children":[]},{"level":2,"title":"await 节点对应的 AST 结构","slug":"await-节点对应的-ast-结构","link":"#await-节点对应的-ast-结构","children":[]},{"level":2,"title":"babel 插件开发","slug":"babel-插件开发","link":"#babel-插件开发","children":[{"level":3,"title":"插件的基本格式示例","slug":"插件的基本格式示例","link":"#插件的基本格式示例","children":[]},{"level":3,"title":"寻找 await 节点","slug":"寻找-await-节点","link":"#寻找-await-节点","children":[]},{"level":3,"title":"向上查找 async 函数","slug":"向上查找-async-函数","link":"#向上查找-async-函数","children":[]},{"level":3,"title":"利用 babel-template 生成 try/catch 节点","slug":"利用-babel-template-生成-try-catch-节点","link":"#利用-babel-template-生成-try-catch-节点","children":[]},{"level":3,"title":"async 函数体替换成 try 语句","slug":"async-函数体替换成-try-语句","link":"#async-函数体替换成-try-语句","children":[]},{"level":3,"title":"若函数已存在 try/catch,则不处理","slug":"若函数已存在-try-catch-则不处理","link":"#若函数已存在-try-catch-则不处理","children":[]},{"level":3,"title":"添加报错信息","slug":"添加报错信息","link":"#添加报错信息","children":[]},{"level":3,"title":"添加用户选项","slug":"添加用户选项","link":"#添加用户选项","children":[]},{"level":3,"title":"最终代码","slug":"最终代码","link":"#最终代码","children":[]}]}],"relativePath":"fragment/auto-try-catch.md","lastUpdated":1713238328000}'),o={name:"fragment/auto-try-catch.md"},e=n(`

如何给所有的 async 函数添加 try/catch?

前言

阿里三面的时候被问到了这个问题,当时思路虽然正确,可惜表述的不够清晰

后来花了一些时间整理了下思路,那么如何实现给所有的 async 函数添加 try/catch 呢?

async 如果不加 try/catch 会发生什么事?

js
// 示例
+async function fn() {
+  let value = await new Promise((resolve, reject) => {
+    reject("failure");
+  });
+  console.log("do something...");
+}
+fn();
+
// 示例
+async function fn() {
+  let value = await new Promise((resolve, reject) => {
+    reject("failure");
+  });
+  console.log("do something...");
+}
+fn();
+

导致浏览器报错:一个未捕获的错误

在开发过程中,为了保证系统健壮性,或者是为了捕获异步的错误,需要频繁的在 async 函数中添加 try/catch,避免出现上述示例的情况

可是我很懒,不想一个个加,懒惰使我们进步😂

下面,通过手写一个 babel 插件,来给所有的 async 函数添加 try/catch

babel 插件的最终效果

原始代码:

js
async function fn() {
+  await new Promise((resolve, reject) => reject("报错"));
+  await new Promise((resolve) => resolve(1));
+  console.log("do something...");
+}
+fn();
+
async function fn() {
+  await new Promise((resolve, reject) => reject("报错"));
+  await new Promise((resolve) => resolve(1));
+  console.log("do something...");
+}
+fn();
+

使用插件转化后的代码:

js
async function fn() {
+  try {
+    await new Promise((resolve, reject) => reject("报错"));
+    await new Promise((resolve) => resolve(1));
+    console.log("do something...");
+  } catch (e) {
+    console.log("\\nfilePath: E:\\\\myapp\\\\src\\\\main.js\\nfuncName: fn\\nError:", e);
+  }
+}
+fn();
+
async function fn() {
+  try {
+    await new Promise((resolve, reject) => reject("报错"));
+    await new Promise((resolve) => resolve(1));
+    console.log("do something...");
+  } catch (e) {
+    console.log("\\nfilePath: E:\\\\myapp\\\\src\\\\main.js\\nfuncName: fn\\nError:", e);
+  }
+}
+fn();
+

通过详细的报错信息,帮助我们快速找到目标文件和具体的报错方法,方便去定位问题

babel 插件的实现思路

1)借助 AST 抽象语法树,遍历查找代码中的 await 关键字

2)找到 await 节点后,从父路径中查找声明的 async 函数,获取该函数的 body(函数中包含的代码)

3)创建 try/catch 语句,将原来 async 的 body 放入其中

4)最后将 async 的 body 替换成创建的 try/catch 语句

babel 的核心:AST

先聊聊 AST 这个帅小伙 🤠,不然后面的开发流程走不下去

AST 是代码的树形结构,生成 AST 分为两个阶段:词法分析和  语法分析

词法分析

词法分析阶段把字符串形式的代码转换为令牌(tokens) ,可以把 tokens 看作是一个扁平的语法片段数组,描述了代码片段在整个代码中的位置和记录当前值的一些信息

语法分析

语法分析阶段会把 token 转换成 AST 的形式,这个阶段会使用 token 中的信息把它们转换成一个 AST 的表述结构,使用 type 属性记录当前的类型

例如 let 代表着一个变量声明的关键字,所以它的 type 为 VariableDeclaration,而 a = 1 会作为 let 的声明描述,它的 type 为 VariableDeclarator

AST 在线查看工具:AST explorer

再举个 🌰,加深对 AST 的理解

js
function demo(n) {
+  return n * n;
+}
+
function demo(n) {
+  return n * n;
+}
+

转化成 AST 的结构

js
{
+  "type": "Program", // 整段代码的主体
+  "body": [
+    {
+      "type": "FunctionDeclaration", // function 的类型叫函数声明;
+      "id": { // id 为函数声明的 id
+        "type": "Identifier", // 标识符 类型
+        "name": "demo" // 标识符 具有名字
+      },
+      "expression": false,
+      "generator": false,
+      "async": false, // 代表是否 是 async function
+      "params": [ // 同级 函数的参数
+        {
+          "type": "Identifier",// 参数类型也是 Identifier
+          "name": "n"
+        }
+      ],
+      "body": { // 函数体内容 整个格式呈现一种树的格式
+        "type": "BlockStatement", // 整个函数体内容 为一个块状代码块类型
+        "body": [
+          {
+            "type": "ReturnStatement", // return 类型
+            "argument": {
+              "type": "BinaryExpression",// BinaryExpression 二进制表达式类型
+              "start": 30,
+              "end": 35,
+              "left": { // 分左 右 中 结构
+                "type": "Identifier",
+                "name": "n"
+              },
+              "operator": "*", // 属于操作符
+              "right": {
+                "type": "Identifier",
+                "name": "n"
+              }
+            }
+          }
+        ]
+      }
+    }
+  ],
+  "sourceType": "module"
+}
+
{
+  "type": "Program", // 整段代码的主体
+  "body": [
+    {
+      "type": "FunctionDeclaration", // function 的类型叫函数声明;
+      "id": { // id 为函数声明的 id
+        "type": "Identifier", // 标识符 类型
+        "name": "demo" // 标识符 具有名字
+      },
+      "expression": false,
+      "generator": false,
+      "async": false, // 代表是否 是 async function
+      "params": [ // 同级 函数的参数
+        {
+          "type": "Identifier",// 参数类型也是 Identifier
+          "name": "n"
+        }
+      ],
+      "body": { // 函数体内容 整个格式呈现一种树的格式
+        "type": "BlockStatement", // 整个函数体内容 为一个块状代码块类型
+        "body": [
+          {
+            "type": "ReturnStatement", // return 类型
+            "argument": {
+              "type": "BinaryExpression",// BinaryExpression 二进制表达式类型
+              "start": 30,
+              "end": 35,
+              "left": { // 分左 右 中 结构
+                "type": "Identifier",
+                "name": "n"
+              },
+              "operator": "*", // 属于操作符
+              "right": {
+                "type": "Identifier",
+                "name": "n"
+              }
+            }
+          }
+        ]
+      }
+    }
+  ],
+  "sourceType": "module"
+}
+

常用的 AST 节点类型对照表

`,35),c=s("table",null,[s("thead",null,[s("tr",null,[s("th",null,"类型原名称"),s("th",null,"中文名称"),s("th",null,"描述")])]),s("tbody",null,[s("tr",null,[s("td",null,"Program"),s("td",null,"程序主体"),s("td",null,"整段代码的主体")]),s("tr",null,[s("td",null,"VariableDeclaration"),s("td",null,"变量声明"),s("td",null,"声明一个变量,例如 var let const")]),s("tr",null,[s("td",null,[s("code",null,"FunctionDeclaration")]),s("td",null,"函数声明"),s("td",null,"声明一个函数,例如 function")]),s("tr",null,[s("td",null,"ExpressionStatement"),s("td",null,"表达式语句"),s("td",null,"通常是调用一个函数,例如 console.log()")]),s("tr",{var:"",a:""},[s("td",null,"BlockStatement"),s("td",null,"块语句"),s("td",null,"包裹在 {} 块内的代码,例如 if (condition)")]),s("tr",null,[s("td",null,"BreakStatement"),s("td",null,"中断语句"),s("td",null,"通常指 break")]),s("tr",null,[s("td",null,"ContinueStatement"),s("td",null,"持续语句"),s("td",null,"通常指 continue")]),s("tr",null,[s("td",null,"ReturnStatement"),s("td",null,"返回语句"),s("td",null,"通常指 return")]),s("tr",null,[s("td",null,"SwitchStatement"),s("td",null,"Switch 语句"),s("td",null,"通常指 Switch Case 语句中的 Switch")]),s("tr",null,[s("td",null,"IfStatement"),s("td",null,"If 控制流语句"),s("td",null,"控制流语句,通常指 if(condition){}else{}")]),s("tr",null,[s("td",null,"Identifier"),s("td",null,"标识符"),s("td",null,"标识,例如声明变量时 var identi = 5 中的 identi")]),s("tr",null,[s("td",null,"CallExpression"),s("td",null,"调用表达式"),s("td",null,"通常指调用一个函数,例如 console.log()")]),s("tr",null,[s("td",null,"BinaryExpression"),s("td",null,"二进制表达式"),s("td",null,"通常指运算,例如 1+2")]),s("tr",null,[s("td",null,"MemberExpression"),s("td",null,"成员表达式"),s("td",null,"通常指调用对象的成员,例如 console 对象的 log 成员")]),s("tr",null,[s("td",null,"ArrayExpression"),s("td",null,"数组表达式"),s("td",null,"通常指一个数组,例如 [1, 3, 5]")]),s("tr",null,[s("td",null,[s("code",null,"FunctionExpression")]),s("td",null,"函数表达式"),s("td",null,"例如 const func = function () {}")]),s("tr",null,[s("td",null,[s("code",null,"ArrowFunctionExpression")]),s("td",null,"箭头函数表达式"),s("td",null,"例如 const func = ()=> {}")]),s("tr",null,[s("td",null,[s("code",null,"AwaitExpression")]),s("td",null,"await 表达式"),s("td",null,"例如 let val = await f()")]),s("tr",null,[s("td",null,[s("code",null,"ObjectMethod")]),s("td",null,"对象中定义的方法"),s("td",null,"例如 let obj = { fn ()")]),s("tr",null,[s("td",null,"NewExpression"),s("td",null,"New 表达式"),s("td",null,"通常指使用 New 关键词")]),s("tr",null,[s("td",null,"AssignmentExpression"),s("td",null,"赋值表达式"),s("td",null,"通常指将函数的返回值赋值给变量")]),s("tr",null,[s("td",null,"UpdateExpression"),s("td",null,"更新表达式"),s("td",null,"通常指更新成员值,例如 i++")]),s("tr",null,[s("td",null,"Literal"),s("td",null,"字面量"),s("td",null,"字面量")]),s("tr",null,[s("td",null,"BooleanLiteral"),s("td",null,"布尔型字面量"),s("td",null,"布尔值,例如 true false")]),s("tr",null,[s("td",null,"NumericLiteral"),s("td",null,"数字型字面量"),s("td",null,"数字,例如 100")]),s("tr",null,[s("td",null,"StringLiteral"),s("td",null,"字符型字面量"),s("td",null,"字符串,例如 vansenb")]),s("tr",null,[s("td",null,"SwitchCase"),s("td",null,"Case 语句"),s("td",null,"通常指 Switch 语句中的 Case")])])],-1),t=n(`

await 节点对应的 AST 结构

1)原始代码

js
async function fn() {
+  await f();
+}
+
async function fn() {
+  await f();
+}
+

2)增加 try catch 后的代码

js
async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+
async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+

通过 AST 结构对比,插件的核心就是将原始函数的 body 放到 try 语句中

babel 插件开发

作者曾在《「历时 8 个月」10 万字前端知识体系总结(工程化篇)🔥》中聊过如何开发一个 babel 插件

这里简单回顾一下

插件的基本格式示例

js
module.exports = function (babel) {
+   let t = babel.type
+   return {
+     visitor: {
+       // 设置需要范围的节点类型
+       CallExression: (path, state) => {
+         do soming ……
+       }
+     }
+   }
+ }
+
module.exports = function (babel) {
+   let t = babel.type
+   return {
+     visitor: {
+       // 设置需要范围的节点类型
+       CallExression: (path, state) => {
+         do soming ……
+       }
+     }
+   }
+ }
+

1)通过 babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等

2)visitor:定义了一个访问者,可以设置需要访问的节点类型,当访问到目标节点后,做相应的处理来实现插件的功能

寻找 await 节点

回到业务需求,现在需要找到 await 节点,可以通过AwaitExpression表达式获取

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+      },
+    },
+  };
+};
+

向上查找 async 函数

通过findParent方法,在父节点中搜寻 async 节点

js
// async节点的属性为true
+const asyncPath = path.findParent((p) => p.node.async);
+
// async节点的属性为true
+const asyncPath = path.findParent((p) => p.node.async);
+

这里要注意,async 函数分为 4 种情况:函数声明 、箭头函数 、函数表达式 、函数为对象的方法

js
// 1️⃣:函数声明
+async function fn() {
+  await f();
+}
+
+// 2️⃣:函数表达式
+const fn = async function () {
+  await f();
+};
+
+// 3️⃣:箭头函数
+const fn = async () => {
+  await f();
+};
+
+// 4️⃣:async函数定义在对象中
+const obj = {
+  async fn() {
+    await f();
+  },
+};
+
// 1️⃣:函数声明
+async function fn() {
+  await f();
+}
+
+// 2️⃣:函数表达式
+const fn = async function () {
+  await f();
+};
+
+// 3️⃣:箭头函数
+const fn = async () => {
+  await f();
+};
+
+// 4️⃣:async函数定义在对象中
+const obj = {
+  async fn() {
+    await f();
+  },
+};
+

需要对这几种情况进行分别判断

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+        // 查找async函数的节点
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+        // 查找async函数的节点
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+      },
+    },
+  };
+};
+

利用 babel-template 生成 try/catch 节点

babel-template可以用以字符串形式的代码来构建 AST 树节点,快速优雅开发插件

js
// 引入babel-template
+const template = require("babel-template");
+
+// 定义try/catch语句模板
+let tryTemplate = \`
+try {
+} catch (e) {
+console.log(CatchError:e)
+}\`;
+
+// 创建模板
+const temp = template(tryTemplate);
+
+// 给模版增加key,添加console.log打印信息
+let tempArgumentObj = {
+  // 通过types.stringLiteral创建字符串字面量
+  CatchError: types.stringLiteral("Error"),
+};
+
+// 通过temp创建try语句的AST节点
+let tryNode = temp(tempArgumentObj);
+
// 引入babel-template
+const template = require("babel-template");
+
+// 定义try/catch语句模板
+let tryTemplate = \`
+try {
+} catch (e) {
+console.log(CatchError:e)
+}\`;
+
+// 创建模板
+const temp = template(tryTemplate);
+
+// 给模版增加key,添加console.log打印信息
+let tempArgumentObj = {
+  // 通过types.stringLiteral创建字符串字面量
+  CatchError: types.stringLiteral("Error"),
+};
+
+// 通过temp创建try语句的AST节点
+let tryNode = temp(tempArgumentObj);
+

async 函数体替换成 try 语句

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+
+        let tryNode = temp(tempArgumentObj);
+
+        // 获取父节点的函数体body
+        let info = asyncPath.node.body;
+
+        // 将函数体放到try语句的body中
+        tryNode.block.body.push(...info.body);
+
+        // 将父节点的body替换成新创建的try语句
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+
+        let tryNode = temp(tempArgumentObj);
+
+        // 获取父节点的函数体body
+        let info = asyncPath.node.body;
+
+        // 将函数体放到try语句的body中
+        tryNode.block.body.push(...info.body);
+
+        // 将父节点的body替换成新创建的try语句
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+

到这里,插件的基本结构已经成型,但还有点问题,如果函数已存在 try/catch,该怎么处理判断呢?

若函数已存在 try/catch,则不处理

js
// 示例代码,不再添加try/catch
+async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+
// 示例代码,不再添加try/catch
+async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+

通过isTryStatement判断是否已存在 try 语句

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        // 判断父路径中是否已存在try语句,若存在直接返回
+        if (path.findParent((p) => p.isTryStatement())) {
+          return false;
+        }
+
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+        let tryNode = temp(tempArgumentObj);
+        let info = asyncPath.node.body;
+        tryNode.block.body.push(...info.body);
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        // 判断父路径中是否已存在try语句,若存在直接返回
+        if (path.findParent((p) => p.isTryStatement())) {
+          return false;
+        }
+
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+        let tryNode = temp(tempArgumentObj);
+        let info = asyncPath.node.body;
+        tryNode.block.body.push(...info.body);
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+

添加报错信息

获取报错时的文件路径 filePath 和方法名称 funcName,方便快速定位问题

获取文件路径

js
// 获取编译目标文件的路径,如:E:\\myapp\\src\\App.vue
+const filePath = this.filename || this.file.opts.filename || "unknown";
+
// 获取编译目标文件的路径,如:E:\\myapp\\src\\App.vue
+const filePath = this.filename || this.file.opts.filename || "unknown";
+

获取报错的方法名称

js
// 定义方法名
+let asyncName = "";
+
+// 获取async节点的type类型
+let type = asyncPath.node.type;
+
+switch (type) {
+  // 1️⃣函数表达式
+  // 情况1:普通函数,如const func = async function () {}
+  // 情况2:箭头函数,如const func = async () => {}
+  case "FunctionExpression":
+  case "ArrowFunctionExpression":
+    // 使用path.getSibling(index)来获得同级的id路径
+    let identifier = asyncPath.getSibling("id");
+    // 获取func方法名
+    asyncName = identifier && identifier.node ? identifier.node.name : "";
+    break;
+
+  // 2️⃣函数声明,如async function fn2() {}
+  case "FunctionDeclaration":
+    asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+    break;
+
+  // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+  case "ObjectMethod":
+    asyncName = asyncPath.node.key.name || "";
+    break;
+}
+
+// 若asyncName不存在,通过argument.callee获取当前执行函数的name
+let funcName =
+  asyncName || (node.argument.callee && node.argument.callee.name) || "";
+
// 定义方法名
+let asyncName = "";
+
+// 获取async节点的type类型
+let type = asyncPath.node.type;
+
+switch (type) {
+  // 1️⃣函数表达式
+  // 情况1:普通函数,如const func = async function () {}
+  // 情况2:箭头函数,如const func = async () => {}
+  case "FunctionExpression":
+  case "ArrowFunctionExpression":
+    // 使用path.getSibling(index)来获得同级的id路径
+    let identifier = asyncPath.getSibling("id");
+    // 获取func方法名
+    asyncName = identifier && identifier.node ? identifier.node.name : "";
+    break;
+
+  // 2️⃣函数声明,如async function fn2() {}
+  case "FunctionDeclaration":
+    asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+    break;
+
+  // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+  case "ObjectMethod":
+    asyncName = asyncPath.node.key.name || "";
+    break;
+}
+
+// 若asyncName不存在,通过argument.callee获取当前执行函数的name
+let funcName =
+  asyncName || (node.argument.callee && node.argument.callee.name) || "";
+

添加用户选项

用户引入插件时,可以设置excludeincludecustomLog选项

exclude: 设置需要排除的文件,不对该文件进行处理

include: 设置需要处理的文件,只对该文件进行处理

customLog: 用户自定义的打印信息

最终代码

入口文件 index.js

js
// babel-template 用于将字符串形式的代码来构建AST树节点
+const template = require("babel-template");
+
+const {
+  tryTemplate,
+  catchConsole,
+  mergeOptions,
+  matchesFile,
+} = require("./util");
+
+module.exports = function (babel) {
+  // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
+  let types = babel.types;
+
+  // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
+  const visitor = {
+    AwaitExpression(path) {
+      // 通过this.opts 获取用户的配置
+      if (this.opts && !typeof this.opts === "object") {
+        return console.error(
+          "[babel-plugin-await-add-trycatch]: options need to be an object."
+        );
+      }
+
+      // 判断父路径中是否已存在try语句,若存在直接返回
+      if (path.findParent((p) => p.isTryStatement())) {
+        return false;
+      }
+
+      // 合并插件的选项
+      const options = mergeOptions(this.opts);
+
+      // 获取编译目标文件的路径,如:E:\\myapp\\src\\App.vue
+      const filePath = this.filename || this.file.opts.filename || "unknown";
+
+      // 在排除列表的文件不编译
+      if (matchesFile(options.exclude, filePath)) {
+        return;
+      }
+
+      // 如果设置了include,只编译include中的文件
+      if (options.include.length && !matchesFile(options.include, filePath)) {
+        return;
+      }
+
+      // 获取当前的await节点
+      let node = path.node;
+
+      // 在父路径节点中查找声明 async 函数的节点
+      // async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
+      const asyncPath = path.findParent(
+        (p) =>
+          p.node.async &&
+          (p.isFunctionDeclaration() ||
+            p.isArrowFunctionExpression() ||
+            p.isFunctionExpression() ||
+            p.isObjectMethod())
+      );
+
+      // 获取async的方法名
+      let asyncName = "";
+
+      let type = asyncPath.node.type;
+
+      switch (type) {
+        // 1️⃣函数表达式
+        // 情况1:普通函数,如const func = async function () {}
+        // 情况2:箭头函数,如const func = async () => {}
+        case "FunctionExpression":
+        case "ArrowFunctionExpression":
+          // 使用path.getSibling(index)来获得同级的id路径
+          let identifier = asyncPath.getSibling("id");
+          // 获取func方法名
+          asyncName = identifier && identifier.node ? identifier.node.name : "";
+          break;
+
+        // 2️⃣函数声明,如async function fn2() {}
+        case "FunctionDeclaration":
+          asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+          break;
+
+        // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+        case "ObjectMethod":
+          asyncName = asyncPath.node.key.name || "";
+          break;
+      }
+
+      // 若asyncName不存在,通过argument.callee获取当前执行函数的name
+      let funcName =
+        asyncName || (node.argument.callee && node.argument.callee.name) || "";
+
+      const temp = template(tryTemplate);
+
+      // 给模版增加key,添加console.log打印信息
+      let tempArgumentObj = {
+        // 通过types.stringLiteral创建字符串字面量
+        CatchError: types.stringLiteral(
+          catchConsole(filePath, funcName, options.customLog)
+        ),
+      };
+
+      // 通过temp创建try语句
+      let tryNode = temp(tempArgumentObj);
+
+      // 获取async节点(父节点)的函数体
+      let info = asyncPath.node.body;
+
+      // 将父节点原来的函数体放到try语句中
+      tryNode.block.body.push(...info.body);
+
+      // 将父节点的内容替换成新创建的try语句
+      info.body = [tryNode];
+    },
+  };
+  return {
+    name: "babel-plugin-await-add-trycatch",
+    visitor,
+  };
+};
+
// babel-template 用于将字符串形式的代码来构建AST树节点
+const template = require("babel-template");
+
+const {
+  tryTemplate,
+  catchConsole,
+  mergeOptions,
+  matchesFile,
+} = require("./util");
+
+module.exports = function (babel) {
+  // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
+  let types = babel.types;
+
+  // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
+  const visitor = {
+    AwaitExpression(path) {
+      // 通过this.opts 获取用户的配置
+      if (this.opts && !typeof this.opts === "object") {
+        return console.error(
+          "[babel-plugin-await-add-trycatch]: options need to be an object."
+        );
+      }
+
+      // 判断父路径中是否已存在try语句,若存在直接返回
+      if (path.findParent((p) => p.isTryStatement())) {
+        return false;
+      }
+
+      // 合并插件的选项
+      const options = mergeOptions(this.opts);
+
+      // 获取编译目标文件的路径,如:E:\\myapp\\src\\App.vue
+      const filePath = this.filename || this.file.opts.filename || "unknown";
+
+      // 在排除列表的文件不编译
+      if (matchesFile(options.exclude, filePath)) {
+        return;
+      }
+
+      // 如果设置了include,只编译include中的文件
+      if (options.include.length && !matchesFile(options.include, filePath)) {
+        return;
+      }
+
+      // 获取当前的await节点
+      let node = path.node;
+
+      // 在父路径节点中查找声明 async 函数的节点
+      // async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
+      const asyncPath = path.findParent(
+        (p) =>
+          p.node.async &&
+          (p.isFunctionDeclaration() ||
+            p.isArrowFunctionExpression() ||
+            p.isFunctionExpression() ||
+            p.isObjectMethod())
+      );
+
+      // 获取async的方法名
+      let asyncName = "";
+
+      let type = asyncPath.node.type;
+
+      switch (type) {
+        // 1️⃣函数表达式
+        // 情况1:普通函数,如const func = async function () {}
+        // 情况2:箭头函数,如const func = async () => {}
+        case "FunctionExpression":
+        case "ArrowFunctionExpression":
+          // 使用path.getSibling(index)来获得同级的id路径
+          let identifier = asyncPath.getSibling("id");
+          // 获取func方法名
+          asyncName = identifier && identifier.node ? identifier.node.name : "";
+          break;
+
+        // 2️⃣函数声明,如async function fn2() {}
+        case "FunctionDeclaration":
+          asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+          break;
+
+        // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+        case "ObjectMethod":
+          asyncName = asyncPath.node.key.name || "";
+          break;
+      }
+
+      // 若asyncName不存在,通过argument.callee获取当前执行函数的name
+      let funcName =
+        asyncName || (node.argument.callee && node.argument.callee.name) || "";
+
+      const temp = template(tryTemplate);
+
+      // 给模版增加key,添加console.log打印信息
+      let tempArgumentObj = {
+        // 通过types.stringLiteral创建字符串字面量
+        CatchError: types.stringLiteral(
+          catchConsole(filePath, funcName, options.customLog)
+        ),
+      };
+
+      // 通过temp创建try语句
+      let tryNode = temp(tempArgumentObj);
+
+      // 获取async节点(父节点)的函数体
+      let info = asyncPath.node.body;
+
+      // 将父节点原来的函数体放到try语句中
+      tryNode.block.body.push(...info.body);
+
+      // 将父节点的内容替换成新创建的try语句
+      info.body = [tryNode];
+    },
+  };
+  return {
+    name: "babel-plugin-await-add-trycatch",
+    visitor,
+  };
+};
+

util.js

js
const merge = require("deepmerge");
+
+// 定义try语句模板
+let tryTemplate = \`
+try {
+} catch (e) {
+console.log(CatchError,e)
+}\`;
+
+/*
+ * catch要打印的信息
+ * @param {string} filePath - 当前执行文件的路径
+ * @param {string} funcName - 当前执行方法的名称
+ * @param {string} customLog - 用户自定义的打印信息
+ */
+let catchConsole = (filePath, funcName, customLog) => \`
+filePath: \${filePath}
+funcName: \${funcName}
+\${customLog}:\`;
+
+// 默认配置
+const defaultOptions = {
+  customLog: "Error",
+  exclude: ["node_modules"],
+  include: [],
+};
+
+// 判断执行的file文件 是否在 exclude/include 选项内
+function matchesFile(list, filename) {
+  return list.find((name) => name && filename.includes(name));
+}
+
+// 合并选项
+function mergeOptions(options) {
+  let { exclude, include } = options;
+  if (exclude) options.exclude = toArray(exclude);
+  if (include) options.include = toArray(include);
+  // 使用merge进行合并
+  return merge.all([defaultOptions, options]);
+}
+
+function toArray(value) {
+  return Array.isArray(value) ? value : [value];
+}
+
+module.exports = {
+  tryTemplate,
+  catchConsole,
+  defaultOptions,
+  mergeOptions,
+  matchesFile,
+  toArray,
+};
+
const merge = require("deepmerge");
+
+// 定义try语句模板
+let tryTemplate = \`
+try {
+} catch (e) {
+console.log(CatchError,e)
+}\`;
+
+/*
+ * catch要打印的信息
+ * @param {string} filePath - 当前执行文件的路径
+ * @param {string} funcName - 当前执行方法的名称
+ * @param {string} customLog - 用户自定义的打印信息
+ */
+let catchConsole = (filePath, funcName, customLog) => \`
+filePath: \${filePath}
+funcName: \${funcName}
+\${customLog}:\`;
+
+// 默认配置
+const defaultOptions = {
+  customLog: "Error",
+  exclude: ["node_modules"],
+  include: [],
+};
+
+// 判断执行的file文件 是否在 exclude/include 选项内
+function matchesFile(list, filename) {
+  return list.find((name) => name && filename.includes(name));
+}
+
+// 合并选项
+function mergeOptions(options) {
+  let { exclude, include } = options;
+  if (exclude) options.exclude = toArray(exclude);
+  if (include) options.include = toArray(include);
+  // 使用merge进行合并
+  return merge.all([defaultOptions, options]);
+}
+
+function toArray(value) {
+  return Array.isArray(value) ? value : [value];
+}
+
+module.exports = {
+  tryTemplate,
+  catchConsole,
+  defaultOptions,
+  mergeOptions,
+  matchesFile,
+  toArray,
+};
+

本文章作者的 github 仓库

`,50),r=[e,c,t];function B(y,F,i,A,u,b){return l(),p("div",null,r)}const m=a(o,[["render",B]]);export{d as __pageData,m as default}; diff --git a/assets/fragment_auto-try-catch.md.80aa30a2.lean.js b/assets/fragment_auto-try-catch.md.80aa30a2.lean.js new file mode 100644 index 00000000..92dd82fe --- /dev/null +++ b/assets/fragment_auto-try-catch.md.80aa30a2.lean.js @@ -0,0 +1 @@ +import{_ as a,o as l,c as p,a as n,b as s}from"./app.f983686f.js";const d=JSON.parse('{"title":"如何给所有的 async 函数添加 try/catch?","description":"","frontmatter":{},"headers":[{"level":2,"title":"前言","slug":"前言","link":"#前言","children":[]},{"level":2,"title":"async 如果不加 try/catch 会发生什么事?","slug":"async-如果不加-try-catch-会发生什么事","link":"#async-如果不加-try-catch-会发生什么事","children":[]},{"level":2,"title":"babel 插件的最终效果","slug":"babel-插件的最终效果","link":"#babel-插件的最终效果","children":[]},{"level":2,"title":"babel 插件的实现思路","slug":"babel-插件的实现思路","link":"#babel-插件的实现思路","children":[]},{"level":2,"title":"babel 的核心:AST","slug":"babel-的核心-ast","link":"#babel-的核心-ast","children":[]},{"level":2,"title":"常用的 AST 节点类型对照表","slug":"常用的-ast-节点类型对照表","link":"#常用的-ast-节点类型对照表","children":[]},{"level":2,"title":"await 节点对应的 AST 结构","slug":"await-节点对应的-ast-结构","link":"#await-节点对应的-ast-结构","children":[]},{"level":2,"title":"babel 插件开发","slug":"babel-插件开发","link":"#babel-插件开发","children":[{"level":3,"title":"插件的基本格式示例","slug":"插件的基本格式示例","link":"#插件的基本格式示例","children":[]},{"level":3,"title":"寻找 await 节点","slug":"寻找-await-节点","link":"#寻找-await-节点","children":[]},{"level":3,"title":"向上查找 async 函数","slug":"向上查找-async-函数","link":"#向上查找-async-函数","children":[]},{"level":3,"title":"利用 babel-template 生成 try/catch 节点","slug":"利用-babel-template-生成-try-catch-节点","link":"#利用-babel-template-生成-try-catch-节点","children":[]},{"level":3,"title":"async 函数体替换成 try 语句","slug":"async-函数体替换成-try-语句","link":"#async-函数体替换成-try-语句","children":[]},{"level":3,"title":"若函数已存在 try/catch,则不处理","slug":"若函数已存在-try-catch-则不处理","link":"#若函数已存在-try-catch-则不处理","children":[]},{"level":3,"title":"添加报错信息","slug":"添加报错信息","link":"#添加报错信息","children":[]},{"level":3,"title":"添加用户选项","slug":"添加用户选项","link":"#添加用户选项","children":[]},{"level":3,"title":"最终代码","slug":"最终代码","link":"#最终代码","children":[]}]}],"relativePath":"fragment/auto-try-catch.md","lastUpdated":1713238328000}'),o={name:"fragment/auto-try-catch.md"},e=n("",35),c=s("table",null,[s("thead",null,[s("tr",null,[s("th",null,"类型原名称"),s("th",null,"中文名称"),s("th",null,"描述")])]),s("tbody",null,[s("tr",null,[s("td",null,"Program"),s("td",null,"程序主体"),s("td",null,"整段代码的主体")]),s("tr",null,[s("td",null,"VariableDeclaration"),s("td",null,"变量声明"),s("td",null,"声明一个变量,例如 var let const")]),s("tr",null,[s("td",null,[s("code",null,"FunctionDeclaration")]),s("td",null,"函数声明"),s("td",null,"声明一个函数,例如 function")]),s("tr",null,[s("td",null,"ExpressionStatement"),s("td",null,"表达式语句"),s("td",null,"通常是调用一个函数,例如 console.log()")]),s("tr",{var:"",a:""},[s("td",null,"BlockStatement"),s("td",null,"块语句"),s("td",null,"包裹在 {} 块内的代码,例如 if (condition)")]),s("tr",null,[s("td",null,"BreakStatement"),s("td",null,"中断语句"),s("td",null,"通常指 break")]),s("tr",null,[s("td",null,"ContinueStatement"),s("td",null,"持续语句"),s("td",null,"通常指 continue")]),s("tr",null,[s("td",null,"ReturnStatement"),s("td",null,"返回语句"),s("td",null,"通常指 return")]),s("tr",null,[s("td",null,"SwitchStatement"),s("td",null,"Switch 语句"),s("td",null,"通常指 Switch Case 语句中的 Switch")]),s("tr",null,[s("td",null,"IfStatement"),s("td",null,"If 控制流语句"),s("td",null,"控制流语句,通常指 if(condition){}else{}")]),s("tr",null,[s("td",null,"Identifier"),s("td",null,"标识符"),s("td",null,"标识,例如声明变量时 var identi = 5 中的 identi")]),s("tr",null,[s("td",null,"CallExpression"),s("td",null,"调用表达式"),s("td",null,"通常指调用一个函数,例如 console.log()")]),s("tr",null,[s("td",null,"BinaryExpression"),s("td",null,"二进制表达式"),s("td",null,"通常指运算,例如 1+2")]),s("tr",null,[s("td",null,"MemberExpression"),s("td",null,"成员表达式"),s("td",null,"通常指调用对象的成员,例如 console 对象的 log 成员")]),s("tr",null,[s("td",null,"ArrayExpression"),s("td",null,"数组表达式"),s("td",null,"通常指一个数组,例如 [1, 3, 5]")]),s("tr",null,[s("td",null,[s("code",null,"FunctionExpression")]),s("td",null,"函数表达式"),s("td",null,"例如 const func = function () {}")]),s("tr",null,[s("td",null,[s("code",null,"ArrowFunctionExpression")]),s("td",null,"箭头函数表达式"),s("td",null,"例如 const func = ()=> {}")]),s("tr",null,[s("td",null,[s("code",null,"AwaitExpression")]),s("td",null,"await 表达式"),s("td",null,"例如 let val = await f()")]),s("tr",null,[s("td",null,[s("code",null,"ObjectMethod")]),s("td",null,"对象中定义的方法"),s("td",null,"例如 let obj = { fn ()")]),s("tr",null,[s("td",null,"NewExpression"),s("td",null,"New 表达式"),s("td",null,"通常指使用 New 关键词")]),s("tr",null,[s("td",null,"AssignmentExpression"),s("td",null,"赋值表达式"),s("td",null,"通常指将函数的返回值赋值给变量")]),s("tr",null,[s("td",null,"UpdateExpression"),s("td",null,"更新表达式"),s("td",null,"通常指更新成员值,例如 i++")]),s("tr",null,[s("td",null,"Literal"),s("td",null,"字面量"),s("td",null,"字面量")]),s("tr",null,[s("td",null,"BooleanLiteral"),s("td",null,"布尔型字面量"),s("td",null,"布尔值,例如 true false")]),s("tr",null,[s("td",null,"NumericLiteral"),s("td",null,"数字型字面量"),s("td",null,"数字,例如 100")]),s("tr",null,[s("td",null,"StringLiteral"),s("td",null,"字符型字面量"),s("td",null,"字符串,例如 vansenb")]),s("tr",null,[s("td",null,"SwitchCase"),s("td",null,"Case 语句"),s("td",null,"通常指 Switch 语句中的 Case")])])],-1),t=n("",50),r=[e,c,t];function B(y,F,i,A,u,b){return l(),p("div",null,r)}const m=a(o,[["render",B]]);export{d as __pageData,m as default}; diff --git a/assets/fragment_babel-console.md.920be39c.js b/assets/fragment_babel-console.md.920be39c.js new file mode 100644 index 00000000..64853177 --- /dev/null +++ b/assets/fragment_babel-console.md.920be39c.js @@ -0,0 +1,531 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-15-11-10-48.0adbb1ac.png",o="/blog/assets/2024-04-15-11-37-55.b8c61089.png",e="/blog/assets/2024-04-15-11-45-10.5b79fe3e.png",u=JSON.parse('{"title":"🔥 手撕 babel 插件-消灭 console!","description":"","frontmatter":{},"headers":[{"level":2,"title":"初见 AST","slug":"初见-ast","link":"#初见-ast","children":[{"level":3,"title":"Program","slug":"program","link":"#program","children":[]},{"level":3,"title":"ExpressionStatement","slug":"expressionstatement","link":"#expressionstatement","children":[]},{"level":3,"title":"CallExpression","slug":"callexpression","link":"#callexpression","children":[]},{"level":3,"title":"MemberExpression","slug":"memberexpression","link":"#memberexpression","children":[]},{"level":3,"title":"Identifier","slug":"identifier","link":"#identifier","children":[]},{"level":3,"title":"StringLiteral","slug":"stringliteral","link":"#stringliteral","children":[]},{"level":3,"title":"公共属性","slug":"公共属性","link":"#公共属性","children":[]}]},{"level":2,"title":"如何写一个 babel 插件?","slug":"如何写一个-babel-插件","link":"#如何写一个-babel-插件","children":[]},{"level":2,"title":"构造 visitor 方法","slug":"构造-visitor-方法","link":"#构造-visitor-方法","children":[]},{"level":2,"title":"去除所有 console","slug":"去除所有-console","link":"#去除所有-console","children":[]},{"level":2,"title":"增加 env api","slug":"增加-env-api","link":"#增加-env-api","children":[]},{"level":2,"title":"增加 exclude api","slug":"增加-exclude-api","link":"#增加-exclude-api","children":[]},{"level":2,"title":"增加 commentWords api","slug":"增加-commentwords-api","link":"#增加-commentwords-api","children":[]},{"level":2,"title":"细节完善","slug":"细节完善","link":"#细节完善","children":[{"level":3,"title":"对于后缀注释","slug":"对于后缀注释","link":"#对于后缀注释","children":[]},{"level":3,"title":"对于前缀注释","slug":"对于前缀注释","link":"#对于前缀注释","children":[]}]}],"relativePath":"fragment/babel-console.md","lastUpdated":1713158613000}'),c={name:"fragment/babel-console.md"},r=l('

🔥 手撕 babel 插件-消灭 console!

写一个小插件来去除 生产环境 的 console.log

我们的目的是 去除 console.log ,我们首先需要通过 ast 查看语法树的结构。我们以下面的 console 为例: 注意 因为我们要写 babel 插件 所以我们选择 @babel/parser 库生成 ast,因为 babel 内部是使用这个库生成 ast 的

初见 AST

AST 是对源码的抽象,字面量、标识符、表达式、语句、模块语法、class 语法都有各自的 AST。

Program

program 是代表整个程序的节点,它有 body 属性代表程序体,存放 statement 数组,就是具体执行的语句的集合。 可以看到我们这里的 body 只有一个 ExpressionStatement 语句,即 console.log。

ExpressionStatement

statement 是语句,它是可以独立执行的单位,expression 是表达式,它俩唯一的区别是表达式执行完以后有返回值。所以 ExpressionStatement 表示这个表达式是被当作语句执行的。 ExpressionStatement 类型的 AST 有一个 expression 属性,代表当前的表达式。

CallExpression

expression 是表达式,CallExpression 表示调用表达式,console.log 就是一个调用表达式。 CallExpression 类型的 AST 有一个 callee 属性,指向被调用的函数。这里 console.log 就是 callee 的值。 CallExpression 类型的 AST 有一个 arguments 属性,指向参数。这里“我会被清除”就是 arguments 的值。

MemberExpression

Member Expression 通常是用于访问对象成员的。他有几种形式:

js
a.b
+a["b"]
+new.target
+super.b
+
a.b
+a["b"]
+new.target
+super.b
+

我们这里的 console.log 就是访问对象成员 log。

为什么 MemberExpression 外层有一个 CallExpression 呢?

实际上,我们可以理解为,MemberExpression 中的某一子结构具有函数调用,那么整个表达式就成为了一个 Call Expression。 MemberExpression 有一个属性 object 表示被访问的对象。这里 console 就是 object 的值。 MemberExpression 有一个属性 property 表示对象的属性。这里 log 就是 property 的值。 MemberExpression 有一个属性 computed 表示访问对象是何种方式。computed 为 true 表示[],false 表示. 。

Identifier

Identifer 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是 Identifer。

我们这里的 console 就是一个 identifier。Identifier 有一个属性 name 表示标识符的名字

StringLiteral

表示字符串字面量。我们这里的 log 就是一个字符串字面量。StringLiteral 有一个属性 value 表示字符串的值

公共属性

每种 AST 都有自己的属性,但是它们也有一些公共的属性:

  • type:AST 节点的类型
  • start、end、loc:start 和 end 代表该节点在源码中的开始和结束下标。而 loc 属性是一个对象,有 line 和 column 属性分别记录开始和结束的行列号
  • leadingComments、innerComments、trailingComments:表示开始的注释、中间的注释、结尾的注释,每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,想拿到某个 AST 的注释就通过这三个属性。

如何写一个 babel 插件?

babel 插件是作用在 transform 阶段。

transform 阶段有@babel/traverse,可以遍历 AST,并调用 visitor 函数修改 AST。 我们可以新建一个 js 文件,其中导出一个方法,返回一个对象,对象存在一个 visitor 属性,里面可以编写我们具体需要修改 AST 的逻辑。

js
export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+复制代码;
+
export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+复制代码;
+

构造 visitor 方法

path 是记录遍历路径的 api,它记录了父子节点的引用,还有很多增删改查 AST 的 api

js
const visitor = {
+  CallExpression(path, { opts }) {
+    //当traverse遍历到类型为CallExpression的AST时,会进入函数内部,我们需要在函数内部修改
+  },
+};
+
const visitor = {
+  CallExpression(path, { opts }) {
+    //当traverse遍历到类型为CallExpression的AST时,会进入函数内部,我们需要在函数内部修改
+  },
+};
+

我们需要遍历所有调用函数表达式 所以使用 CallExpression 。

去除所有 console

将所有的 console.log 去掉

path.get 表示获取某个属性的 path

path.matchesPattern 检查某个节点是否符合某种模式

path.remove 删除当前节点

js
CallExpression(path, { opts }) {
+  //获取callee的path
+  const calleePath = path.get("callee");
+  //检查callee中是否符合“console”这种模式
+  if (calleePath && calleePath.matchesPattern("console", true)) {
+    //如果符合 直接删除节点
+    path.remove();
+  }
+},
+
CallExpression(path, { opts }) {
+  //获取callee的path
+  const calleePath = path.get("callee");
+  //检查callee中是否符合“console”这种模式
+  if (calleePath && calleePath.matchesPattern("console", true)) {
+    //如果符合 直接删除节点
+    path.remove();
+  }
+},
+

增加 env api

一般去除 console.log 都是在生产环境执行 所以增加 env 参数

AST 的第二个参数 opt 中有插件传入的配置

js
const isProduction = process.env.NODE_ENV === "production";
+CallExpression(path, { opts }) {
+    const { env } = opts;
+    if (env === "production" || isProduction) {
+        path.remove();
+    }
+},
+
const isProduction = process.env.NODE_ENV === "production";
+CallExpression(path, { opts }) {
+    const { env } = opts;
+    if (env === "production" || isProduction) {
+        path.remove();
+    }
+},
+

增加 exclude api

我们上面去除了所有的 console,不管是 error、warning、table 都会清除,所以我们加一个 exclude api,传一个数组,可以去除想要去除的 console 类型

js
const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+
+const { env, exclude } = opts;
+if (env === "production" || isProduction) {
+  removeConsoleExpression(path, calleePath, exclude);
+}
+const removeConsoleExpression = (path, calleePath, exclude) => {
+  if (isArray(exclude)) {
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    //匹配上直接返回不进行操作
+    if (hasTarget) return;
+  }
+  path.remove();
+};
+
const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+
+const { env, exclude } = opts;
+if (env === "production" || isProduction) {
+  removeConsoleExpression(path, calleePath, exclude);
+}
+const removeConsoleExpression = (path, calleePath, exclude) => {
+  if (isArray(exclude)) {
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    //匹配上直接返回不进行操作
+    if (hasTarget) return;
+  }
+  path.remove();
+};
+

增加 commentWords api

某些时候 我们希望一些 console 不被删除 我们可以给他添加一些注释 比如

js
//no remove
+console.log("测试1");
+console.log("测试2"); //reserse
+//hhhhh
+console.log("测试3");
+
//no remove
+console.log("测试1");
+console.log("测试2"); //reserse
+//hhhhh
+console.log("测试3");
+

如上 我们希望带有 no remove 前缀注释的 console 和带有 reserse 后缀注释的 console 保留不被删除 之前我们提到 babel 给我们提供了 leadingComments(前缀注释)和 trailingComments(后缀注释)我们可以利用他们 由 AST 可知 她和 CallExpression 同级,所以我们需要获取他的父节点 然后获取父节点的属性

path.parentPath 获取父 path path.node 获取当前节点

js
const { exclude, commentWords, env } = opts;
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+// 判断是否有前缀注释
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+// 判断是否有后缀注释
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+//判断是否有关键字匹配 默认no remove || reserve 且如果commentWords和默认值是相斥的
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\\b)|(reserve\\b)/.test(node.value))
+  );
+};
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  //获取父path
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+  //标识是否有前缀注释
+  let leadingReserve = false;
+  //标识是否有后缀注释
+  let trailReserve = false;
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    parentNode.leadingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        leadingReserve = true;
+      }
+    });
+  }
+  if (hasTrailingComments(parentNode)) {
+    //traverse
+    parentNode.trailingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        trailReserve = true;
+      }
+    });
+  }
+  //如果没有前缀节点和后缀节点 直接删除节点
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+
const { exclude, commentWords, env } = opts;
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+// 判断是否有前缀注释
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+// 判断是否有后缀注释
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+//判断是否有关键字匹配 默认no remove || reserve 且如果commentWords和默认值是相斥的
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\\b)|(reserve\\b)/.test(node.value))
+  );
+};
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  //获取父path
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+  //标识是否有前缀注释
+  let leadingReserve = false;
+  //标识是否有后缀注释
+  let trailReserve = false;
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    parentNode.leadingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        leadingReserve = true;
+      }
+    });
+  }
+  if (hasTrailingComments(parentNode)) {
+    //traverse
+    parentNode.trailingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        trailReserve = true;
+      }
+    });
+  }
+  //如果没有前缀节点和后缀节点 直接删除节点
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+

细节完善

我们大致完成了插件 我们引进项目里面进行测试

js
console.log("测试 1");
+//no remove
+console.log("测试 2");
+console.log("测试 3"); //reserve
+console.log("测试 4");
+
console.log("测试 1");
+//no remove
+console.log("测试 2");
+console.log("测试 3"); //reserve
+console.log("测试 4");
+

新建.babelrc 引入插件

json
{
+  "plugins": [
+    [
+      "../dist/index.cjs",
+      {
+        "env": "production"
+      }
+    ]
+  ]
+}
+
{
+  "plugins": [
+    [
+      "../dist/index.cjs",
+      {
+        "env": "production"
+      }
+    ]
+  ]
+}
+

理论上应该移除测试 1、测试 4,但是我们惊讶的发现 竟然一个 console 没有删除!!经过排查 我们大致确定了问题所在。 因为测试 2 的前缀注释同时也被 AST 纳入了测试 1 的后缀注释中了,而测试 3 的后缀注释同时也被 AST 纳入了测试 4 的前缀注释中了 所以测试 1 存在后缀注释 测试 4 存在前缀注释 所以测试 1 和测试 4 没有被删除 那么我们怎么判断呢?

对于后缀注释

我们可以判断后缀注释是否与当前的调用表达式处于同一行,如果不是同一行,则不将其归纳为后缀注释

js
if (hasTrailingComments(parentNode)) {
+  const {
+    start: { line: currentLine },
+  } = parentNode.loc;
+  //traverse
+  // @ts-ignore
+  parentNode.trailingComments.forEach((comment) => {
+    const {
+      start: { line: currentCommentLine },
+    } = comment.loc;
+
+    if (currentLine === currentCommentLine) {
+      comment.belongCurrentLine = true;
+    }
+    //属于当前行才将其设置为后缀注释
+    if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) {
+      trailReserve = true;
+    }
+  });
+}
+
if (hasTrailingComments(parentNode)) {
+  const {
+    start: { line: currentLine },
+  } = parentNode.loc;
+  //traverse
+  // @ts-ignore
+  parentNode.trailingComments.forEach((comment) => {
+    const {
+      start: { line: currentCommentLine },
+    } = comment.loc;
+
+    if (currentLine === currentCommentLine) {
+      comment.belongCurrentLine = true;
+    }
+    //属于当前行才将其设置为后缀注释
+    if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) {
+      trailReserve = true;
+    }
+  });
+}
+

对于前缀注释

因为我们在后缀注释的节点中添加了一个变量 belongCurrentLine,表示该注释是否是和节点属于同一行。 那么对于前缀注释,我们只需要判断是否存在 belongCurrentLine,如果存在 belongCurrentLine,表示不能将其当作前缀注释。

js
if (hasLeadingComments(parentNode)) {
+  parentNode.leadingComments.forEach((comment) => {
+    if (isReserveComment(comment, commentWords) && !comment.belongCurrentLine) {
+      leadingReserve = true;
+    }
+  });
+}
+
if (hasLeadingComments(parentNode)) {
+  parentNode.leadingComments.forEach((comment) => {
+    if (isReserveComment(comment, commentWords) && !comment.belongCurrentLine) {
+      leadingReserve = true;
+    }
+  });
+}
+

code

js
// @ts-ignore
+const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+// @ts-ignore
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+
+const isProduction = process.env.NODE_ENV === "production";
+
+// @ts-ignore
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\\b)|(reserve\\b)/.test(node.value))
+  );
+};
+
+// @ts-ignore
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  // if has exclude key exclude this
+  if (isArray(exclude)) {
+    // @ts-ignore
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    if (hasTarget) return;
+  }
+
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+
+  let leadingReserve = false;
+  let trailReserve = false;
+
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    // @ts-ignore
+    parentNode.leadingComments.forEach((comment) => {
+      if (
+        isReserveComment(comment, commentWords) &&
+        !comment.belongCurrentLine
+      ) {
+        leadingReserve = true;
+      }
+    });
+  }
+
+  if (hasTrailingComments(parentNode)) {
+    const {
+      start: { line: currentLine },
+    } = parentNode.loc;
+    //traverse
+    // @ts-ignore
+    parentNode.trailingComments.forEach((comment) => {
+      const {
+        start: { line: currentCommentLine },
+      } = comment.loc;
+
+      if (currentLine === currentCommentLine) {
+        comment.belongCurrentLine = true;
+      }
+
+      if (
+        isReserveComment(comment, commentWords) &&
+        comment.belongCurrentLine
+      ) {
+        trailReserve = true;
+      }
+    });
+  }
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+
+// @ts-ignore
+// has leading comments
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+
+// @ts-ignore
+// has trailing comments
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+
+const visitor = {
+  // @ts-ignore
+  CallExpression(path, { opts }) {
+    const calleePath = path.get("callee");
+
+    const { exclude, commentWords, env } = opts;
+
+    if (calleePath && calleePath.matchesPattern("console", true)) {
+      if (env === "production" || isProduction) {
+        removeConsoleExpression(path, calleePath, exclude, commentWords);
+      }
+    }
+  },
+};
+
+export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+
// @ts-ignore
+const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+// @ts-ignore
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+
+const isProduction = process.env.NODE_ENV === "production";
+
+// @ts-ignore
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\\b)|(reserve\\b)/.test(node.value))
+  );
+};
+
+// @ts-ignore
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  // if has exclude key exclude this
+  if (isArray(exclude)) {
+    // @ts-ignore
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    if (hasTarget) return;
+  }
+
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+
+  let leadingReserve = false;
+  let trailReserve = false;
+
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    // @ts-ignore
+    parentNode.leadingComments.forEach((comment) => {
+      if (
+        isReserveComment(comment, commentWords) &&
+        !comment.belongCurrentLine
+      ) {
+        leadingReserve = true;
+      }
+    });
+  }
+
+  if (hasTrailingComments(parentNode)) {
+    const {
+      start: { line: currentLine },
+    } = parentNode.loc;
+    //traverse
+    // @ts-ignore
+    parentNode.trailingComments.forEach((comment) => {
+      const {
+        start: { line: currentCommentLine },
+      } = comment.loc;
+
+      if (currentLine === currentCommentLine) {
+        comment.belongCurrentLine = true;
+      }
+
+      if (
+        isReserveComment(comment, commentWords) &&
+        comment.belongCurrentLine
+      ) {
+        trailReserve = true;
+      }
+    });
+  }
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+
+// @ts-ignore
+// has leading comments
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+
+// @ts-ignore
+// has trailing comments
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+
+const visitor = {
+  // @ts-ignore
+  CallExpression(path, { opts }) {
+    const calleePath = path.get("callee");
+
+    const { exclude, commentWords, env } = opts;
+
+    if (calleePath && calleePath.matchesPattern("console", true)) {
+      if (env === "production" || isProduction) {
+        removeConsoleExpression(path, calleePath, exclude, commentWords);
+      }
+    }
+  },
+};
+
+export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+
`,68),t=[r];function B(y,F,i,A,m,C){return n(),a("div",null,t)}const d=s(c,[["render",B]]);export{u as __pageData,d as default}; diff --git a/assets/fragment_babel-console.md.920be39c.lean.js b/assets/fragment_babel-console.md.920be39c.lean.js new file mode 100644 index 00000000..80f2a414 --- /dev/null +++ b/assets/fragment_babel-console.md.920be39c.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-15-11-10-48.0adbb1ac.png",o="/blog/assets/2024-04-15-11-37-55.b8c61089.png",e="/blog/assets/2024-04-15-11-45-10.5b79fe3e.png",u=JSON.parse('{"title":"🔥 手撕 babel 插件-消灭 console!","description":"","frontmatter":{},"headers":[{"level":2,"title":"初见 AST","slug":"初见-ast","link":"#初见-ast","children":[{"level":3,"title":"Program","slug":"program","link":"#program","children":[]},{"level":3,"title":"ExpressionStatement","slug":"expressionstatement","link":"#expressionstatement","children":[]},{"level":3,"title":"CallExpression","slug":"callexpression","link":"#callexpression","children":[]},{"level":3,"title":"MemberExpression","slug":"memberexpression","link":"#memberexpression","children":[]},{"level":3,"title":"Identifier","slug":"identifier","link":"#identifier","children":[]},{"level":3,"title":"StringLiteral","slug":"stringliteral","link":"#stringliteral","children":[]},{"level":3,"title":"公共属性","slug":"公共属性","link":"#公共属性","children":[]}]},{"level":2,"title":"如何写一个 babel 插件?","slug":"如何写一个-babel-插件","link":"#如何写一个-babel-插件","children":[]},{"level":2,"title":"构造 visitor 方法","slug":"构造-visitor-方法","link":"#构造-visitor-方法","children":[]},{"level":2,"title":"去除所有 console","slug":"去除所有-console","link":"#去除所有-console","children":[]},{"level":2,"title":"增加 env api","slug":"增加-env-api","link":"#增加-env-api","children":[]},{"level":2,"title":"增加 exclude api","slug":"增加-exclude-api","link":"#增加-exclude-api","children":[]},{"level":2,"title":"增加 commentWords api","slug":"增加-commentwords-api","link":"#增加-commentwords-api","children":[]},{"level":2,"title":"细节完善","slug":"细节完善","link":"#细节完善","children":[{"level":3,"title":"对于后缀注释","slug":"对于后缀注释","link":"#对于后缀注释","children":[]},{"level":3,"title":"对于前缀注释","slug":"对于前缀注释","link":"#对于前缀注释","children":[]}]}],"relativePath":"fragment/babel-console.md","lastUpdated":1713158613000}'),c={name:"fragment/babel-console.md"},r=l("",68),t=[r];function B(y,F,i,A,m,C){return n(),a("div",null,t)}const d=s(c,[["render",B]]);export{u as __pageData,d as default}; diff --git a/assets/fragment_const.md.f419959d.js b/assets/fragment_const.md.f419959d.js new file mode 100644 index 00000000..32436710 --- /dev/null +++ b/assets/fragment_const.md.f419959d.js @@ -0,0 +1,63 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"如何在 ES5 环境下实现一个 const ?","description":"","frontmatter":{},"headers":[{"level":2,"title":"属性描述符","slug":"属性描述符","link":"#属性描述符","children":[]},{"level":2,"title":"const 实现原理","slug":"const-实现原理","link":"#const-实现原理","children":[]}],"relativePath":"fragment/const.md","lastUpdated":1713169443000}'),p={name:"fragment/const.md"},o=l(`

如何在 ES5 环境下实现一个 const ?

属性描述符

详解

const 实现原理

由于 ES5 环境没有 block 的概念,所以是无法百分百实现 const,只能是挂载到某个对象下,要么是全局的 window,要么就是自定义一个 object 来当容器

js
var __const = function __const(data, value) {
+  window.data = value; // 把要定义的data挂载到window下,并赋值value
+  Object.defineProperty(window, data, {
+    // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
+    enumerable: false,
+    configurable: false,
+    get: function () {
+      return value;
+    },
+    set: function (data) {
+      if (data !== value) {
+        // 当要对当前属性进行赋值时,则抛出错误!
+        throw new TypeError("Assignment to constant variable.");
+      } else {
+        return value;
+      }
+    },
+  });
+};
+__const("a", 10);
+console.log(a);
+delete a;
+console.log(a);
+for (let item in window) {
+  // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
+  if (item === "a") {
+    // 因为不可枚举,所以不执行
+    console.log(window[item]);
+  }
+}
+a = 20; // 报错
+
var __const = function __const(data, value) {
+  window.data = value; // 把要定义的data挂载到window下,并赋值value
+  Object.defineProperty(window, data, {
+    // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
+    enumerable: false,
+    configurable: false,
+    get: function () {
+      return value;
+    },
+    set: function (data) {
+      if (data !== value) {
+        // 当要对当前属性进行赋值时,则抛出错误!
+        throw new TypeError("Assignment to constant variable.");
+      } else {
+        return value;
+      }
+    },
+  });
+};
+__const("a", 10);
+console.log(a);
+delete a;
+console.log(a);
+for (let item in window) {
+  // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
+  if (item === "a") {
+    // 因为不可枚举,所以不执行
+    console.log(window[item]);
+  }
+}
+a = 20; // 报错
+
`,6),e=[o];function c(t,r,B,y,F,i){return n(),a("div",null,e)}const b=s(p,[["render",c]]);export{u as __pageData,b as default}; diff --git a/assets/fragment_const.md.f419959d.lean.js b/assets/fragment_const.md.f419959d.lean.js new file mode 100644 index 00000000..3332ea11 --- /dev/null +++ b/assets/fragment_const.md.f419959d.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"如何在 ES5 环境下实现一个 const ?","description":"","frontmatter":{},"headers":[{"level":2,"title":"属性描述符","slug":"属性描述符","link":"#属性描述符","children":[]},{"level":2,"title":"const 实现原理","slug":"const-实现原理","link":"#const-实现原理","children":[]}],"relativePath":"fragment/const.md","lastUpdated":1713169443000}'),p={name:"fragment/const.md"},o=l("",6),e=[o];function c(t,r,B,y,F,i){return n(),a("div",null,e)}const b=s(p,[["render",c]]);export{u as __pageData,b as default}; diff --git a/assets/fragment_disable-debugger.md.5f8672f6.js b/assets/fragment_disable-debugger.md.5f8672f6.js new file mode 100644 index 00000000..e5d91f40 --- /dev/null +++ b/assets/fragment_disable-debugger.md.5f8672f6.js @@ -0,0 +1,167 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"禁止别人调试自己的前端页面代码","description":"","frontmatter":{},"headers":[{"level":2,"title":"🎈 为啥要禁止?","slug":"🎈-为啥要禁止","link":"#🎈-为啥要禁止","children":[]},{"level":2,"title":"🎈 无限 debugger","slug":"🎈-无限-debugger","link":"#🎈-无限-debugger","children":[]},{"level":2,"title":"🎈 无限 debugger 的对策","slug":"🎈-无限-debugger-的对策","link":"#🎈-无限-debugger-的对策","children":[]},{"level":2,"title":"🎈 禁止断点的对策","slug":"🎈-禁止断点的对策","link":"#🎈-禁止断点的对策","children":[]},{"level":2,"title":"🎈 忽略执行的代码","slug":"🎈-忽略执行的代码","link":"#🎈-忽略执行的代码","children":[]},{"level":2,"title":"🎈 忽略执行代码的对策","slug":"🎈-忽略执行代码的对策","link":"#🎈-忽略执行代码的对策","children":[]},{"level":2,"title":"🎈 终极增强防调试代码","slug":"🎈-终极增强防调试代码","link":"#🎈-终极增强防调试代码","children":[]}],"relativePath":"fragment/disable-debugger.md","lastUpdated":1713241345000}'),p={name:"fragment/disable-debugger.md"},o=l(`

禁止别人调试自己的前端页面代码

🎈 为啥要禁止?

  • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析, 破解后获取数据
  • 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码

🎈 无限 debugger

  • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行
  • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的
  • 基础代码如下:
js
/**
+ * 基础禁止调试代码
+ */
+(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
/**
+ * 基础禁止调试代码
+ */
+(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+

🎈 无限 debugger 的对策

  • 如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大
  • 可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger
  • 这种方式虽然能去掉碍眼的 debugger ,但是无法通过左侧的行号添加 breakpoint

🎈 禁止断点的对策

  • 如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpoint 为 false 也无用
  • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的
js
(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+

🎈 忽略执行的代码

  • 通过添加 add script ignore list 需要忽略执行代码行或文件
  • 也可以达到禁止无限 debugger

🎈 忽略执行代码的对策

  • 那如何针对上面操作的恶意用户呢
  • 可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对
  • Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件
  • 当然使用的时候,为了更加的安全,最好使用加密后的脚本
js
// 加密前
+(() => {
+  function ban() {
+    setInterval(() => {
+      Function("debugger")();
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
+// 加密后
+eval(
+  (function (c, g, a, b, d, e) {
+    d = String;
+    if (!"".replace(/^/, String)) {
+      for (; a--; ) e[a] = b[a] || a;
+      b = [
+        function (f) {
+          return e[f];
+        },
+      ];
+      d = function () {
+        return "w+";
+      };
+      a = 1;
+    }
+    for (; a--; )
+      b[a] && (c = c.replace(new RegExp("\\b" + d(a) + "\\b", "g"), b[a]));
+    return c;
+  })(
+    '(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',
+    9,
+    9,
+    "block function setInterval Function debugger 50 try catch err".split(" "),
+    0,
+    {}
+  )
+);
+
// 加密前
+(() => {
+  function ban() {
+    setInterval(() => {
+      Function("debugger")();
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
+// 加密后
+eval(
+  (function (c, g, a, b, d, e) {
+    d = String;
+    if (!"".replace(/^/, String)) {
+      for (; a--; ) e[a] = b[a] || a;
+      b = [
+        function (f) {
+          return e[f];
+        },
+      ];
+      d = function () {
+        return "w+";
+      };
+      a = 1;
+    }
+    for (; a--; )
+      b[a] && (c = c.replace(new RegExp("\\b" + d(a) + "\\b", "g"), b[a]));
+    return c;
+  })(
+    '(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',
+    9,
+    9,
+    "block function setInterval Function debugger 50 try catch err".split(" "),
+    0,
+    {}
+  )
+);
+

🎈 终极增强防调试代码

  • 为了让自己写出来的代码更加的晦涩难懂,需要对上面的代码再优化一下
  • Function('debugger').call() 改成 (function(){return false;})['constructor']('debugger')['call']();
  • 并且添加条件,当窗口外部宽高和内部宽高的差值大于一定的值 ,我把 body 里的内容换成指定内容
  • 当然使用的时候,为了更加的安全,最好加密后再使用
js
(() => {
+  function block() {
+    if (
+      window.outerHeight - window.innerHeight > 200 ||
+      window.outerWidth - window.innerWidth > 200
+    ) {
+      document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
+    }
+    setInterval(() => {
+      (function () {
+        return false;
+      })
+        ["constructor"]("debugger")
+        ["call"]();
+    }, 50);
+  }
+  try {
+    block();
+  } catch (err) {}
+})();
+
(() => {
+  function block() {
+    if (
+      window.outerHeight - window.innerHeight > 200 ||
+      window.outerWidth - window.innerWidth > 200
+    ) {
+      document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
+    }
+    setInterval(() => {
+      (function () {
+        return false;
+      })
+        ["constructor"]("debugger")
+        ["call"]();
+    }, 50);
+  }
+  try {
+    block();
+  } catch (err) {}
+})();
+
`,19),e=[o];function c(r,B,t,y,F,i){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{b as __pageData,A as default}; diff --git a/assets/fragment_disable-debugger.md.5f8672f6.lean.js b/assets/fragment_disable-debugger.md.5f8672f6.lean.js new file mode 100644 index 00000000..614838a1 --- /dev/null +++ b/assets/fragment_disable-debugger.md.5f8672f6.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"禁止别人调试自己的前端页面代码","description":"","frontmatter":{},"headers":[{"level":2,"title":"🎈 为啥要禁止?","slug":"🎈-为啥要禁止","link":"#🎈-为啥要禁止","children":[]},{"level":2,"title":"🎈 无限 debugger","slug":"🎈-无限-debugger","link":"#🎈-无限-debugger","children":[]},{"level":2,"title":"🎈 无限 debugger 的对策","slug":"🎈-无限-debugger-的对策","link":"#🎈-无限-debugger-的对策","children":[]},{"level":2,"title":"🎈 禁止断点的对策","slug":"🎈-禁止断点的对策","link":"#🎈-禁止断点的对策","children":[]},{"level":2,"title":"🎈 忽略执行的代码","slug":"🎈-忽略执行的代码","link":"#🎈-忽略执行的代码","children":[]},{"level":2,"title":"🎈 忽略执行代码的对策","slug":"🎈-忽略执行代码的对策","link":"#🎈-忽略执行代码的对策","children":[]},{"level":2,"title":"🎈 终极增强防调试代码","slug":"🎈-终极增强防调试代码","link":"#🎈-终极增强防调试代码","children":[]}],"relativePath":"fragment/disable-debugger.md","lastUpdated":1713241345000}'),p={name:"fragment/disable-debugger.md"},o=l("",19),e=[o];function c(r,B,t,y,F,i){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{b as __pageData,A as default}; diff --git a/assets/fragment_fetch-pause.md.95306d7b.js b/assets/fragment_fetch-pause.md.95306d7b.js new file mode 100644 index 00000000..d8a2b2de --- /dev/null +++ b/assets/fragment_fetch-pause.md.95306d7b.js @@ -0,0 +1,135 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-16-14-40-53.4815a665.png",o="/blog/assets/2024-04-16-14-41-08.0de0710b.png",C=JSON.parse('{"title":"前端中 JS 发起的请求可以暂停吗?","description":"","frontmatter":{},"headers":[{"level":2,"title":"请求应该是什么?","slug":"请求应该是什么","link":"#请求应该是什么","children":[]},{"level":2,"title":"解答提问","slug":"解答提问","link":"#解答提问","children":[{"level":3,"title":"用 JS 实现 ”假暂停” 机制","slug":"用-js-实现-假暂停-机制","link":"#用-js-实现-假暂停-机制","children":[]},{"level":3,"title":"用法","slug":"用法","link":"#用法","children":[]},{"level":3,"title":"执行原理","slug":"执行原理","link":"#执行原理","children":[]}]},{"level":2,"title":"补充","slug":"补充","link":"#补充","children":[]}],"relativePath":"fragment/fetch-pause.md","lastUpdated":1713251442000}'),e={name:"fragment/fetch-pause.md"},r=l('

前端中 JS 发起的请求可以暂停吗?

JS 发起的请求可以暂停吗?这一句话当中有两个概念需要明确,一是什么样的状态才能称之为 暂停 ?二是 JS 发起的请求 是什么?

怎么样才算暂停?暂停 全称暂时停止,在已开始未结束的过程中临时停止可以称之为暂停,意味着这个过程可以在某个时间点截断然后在另一个时间点重新续上。

请求应该是什么?

这里得先介绍一下 TCP/IP 网络模型 , 网络模型自上而下分为 应用层、传输层、网络层和网络接口层。

上图表示的意思是,每次网络传输,应用数据在发送至目标前都需要通过网络模型一层一层的包装,就像寄快递一样,把要寄的物品先打包好登记一下大小,再装在盒子里登记一下目的地,然后再装到车上,最后送往目的地。 请求(Request) 这个概念就可以理解为客户端通过若干次数据网络传输,将单份数据完整发给服务端的行为,而针对某次请求服务端往客户端发送的答复数据则可以称之为 响应(Response) 。 理论上应用层的协议可以通过类似于标记数据包序列号等等一系列手段来实现暂停机制。但是 TCP 协议 并不支持 ,TCP 协议的数据传输是流式的,数据被视为一连串的字节流。客户端发送的数据会被拆分成多个 TCP 段(TCP segments),而这些段在网络中是独立传输的,无法直接控制每个 TCP 段的传输,因此也无法实现暂停请求或者暂停响应的功能。

解答提问

如果请求是指网络模型中的一次请求传输,那理所当然是不可能暂停的。 来看看提问者的使用场景 —— JS 发起的请求 ,那么可以认为问题当中的请求,应该是指在 JS 运行时中发起的 XMLHttpRequest 或者是 fetch 请求,而请求既然已经发起,那问的自然就是 响应是否能够被暂停 。 我们都知道像大文件分片上传、以及分片下载之类的功能本质上是将分片顺序定好之后按顺序请求,然后就可以通过中断顺序并记录中断点来实现暂停重传的机制,而单个请求并不具备这样的环境。

用 JS 实现 ”假暂停” 机制

虽然不能真正意义上实现暂停请求,但是我们其实可以模拟一个 假暂停 的功能,在前端的业务场景上,数据不是收到就可以直接打在客户脸上的(什么光速打击),前端开发者需要对这些数据进行处理之后渲染在界面上,如果我们能在请求发起之前增加一个控制器,在请求回来时,如果控制器为暂停状态则不处理数据,等待控制器恢复后再进行处理,是不是也能到达到目的?让我们试着实现一下。

假如我们使用 fetch 来请求。我们可以设计一个控制器 Promise 和请求放在一起用 Promise.all 包裹,当 fetch 完成时判断这个控制器的暂停状态,如果没有被暂停,则控制器也直接 resolve,同时整个 Promise.all 也 resolve 抛出。

ts
function _request() {
+  return new Promise<number>((res) =>
+    setTimeout(() => {
+      res(123);
+    }, 3000)
+  );
+}
+
+// 原本想使用 class extends Promise 来实现
+// 结果一直出现这个问题 https://github.com/nodejs/node/issues/13678
+function createPauseControllerPromise() {
+  const result = {
+    isPause: false,
+    resolveWhenResume: false,
+    resolve(value?: any) {},
+    pause() {
+      this.isPause = true;
+    },
+    resume() {
+      if (!this.isPause) return;
+      this.isPause = false;
+      if (this.resolveWhenResume) {
+        this.resolve();
+      }
+    },
+    promise: Promise.resolve(),
+  };
+
+  const promise = new Promise<void>((res) => {
+    result.resolve = res;
+  });
+
+  result.promise = promise;
+
+  return result;
+}
+
+function requestWithPauseControl<T extends () => Promise<any>>(request: T) {
+  const controller = createPauseControllerPromise();
+
+  const controlRequest = request().then((data) => {
+    if (!controller.isPause) controller.resolve();
+    controller.resolveWhenResume = controller.isPause;
+    return data;
+  });
+
+  const result = Promise.all([controlRequest, controller.promise]).then(
+    (data) => data[0]
+  );
+
+  result.finally(() => controller.resolve())(result as any).pause =
+    controller.pause.bind(controller);
+  (result as any).resume = controller.resume.bind(controller);
+
+  return result as ReturnType<T> & { pause: () => void; resume: () => void };
+}
+
function _request() {
+  return new Promise<number>((res) =>
+    setTimeout(() => {
+      res(123);
+    }, 3000)
+  );
+}
+
+// 原本想使用 class extends Promise 来实现
+// 结果一直出现这个问题 https://github.com/nodejs/node/issues/13678
+function createPauseControllerPromise() {
+  const result = {
+    isPause: false,
+    resolveWhenResume: false,
+    resolve(value?: any) {},
+    pause() {
+      this.isPause = true;
+    },
+    resume() {
+      if (!this.isPause) return;
+      this.isPause = false;
+      if (this.resolveWhenResume) {
+        this.resolve();
+      }
+    },
+    promise: Promise.resolve(),
+  };
+
+  const promise = new Promise<void>((res) => {
+    result.resolve = res;
+  });
+
+  result.promise = promise;
+
+  return result;
+}
+
+function requestWithPauseControl<T extends () => Promise<any>>(request: T) {
+  const controller = createPauseControllerPromise();
+
+  const controlRequest = request().then((data) => {
+    if (!controller.isPause) controller.resolve();
+    controller.resolveWhenResume = controller.isPause;
+    return data;
+  });
+
+  const result = Promise.all([controlRequest, controller.promise]).then(
+    (data) => data[0]
+  );
+
+  result.finally(() => controller.resolve())(result as any).pause =
+    controller.pause.bind(controller);
+  (result as any).resume = controller.resume.bind(controller);
+
+  return result as ReturnType<T> & { pause: () => void; resume: () => void };
+}
+

用法

我们可以通过调用 requestWithPauseControl(_request) 来替代调用 _request 使用,通过返回的 pause 和 resume 方法控制暂停和继续。

ts
const result = requestWithPauseControl(_request).then((data) => {
+  console.log(data);
+});
+
+if (Math.random() > 0.5) {
+  result.pause();
+}
+
+setTimeout(() => {
+  result.resume();
+}, 4000);
+
const result = requestWithPauseControl(_request).then((data) => {
+  console.log(data);
+});
+
+if (Math.random() > 0.5) {
+  result.pause();
+}
+
+setTimeout(() => {
+  result.resume();
+}, 4000);
+

执行原理

流程设计上是这样,设计一个控制器,发起请求并请求返回后,判断控制器的状态,若控制器不处于 “暂停” 状态时,正常返回数据;当控制器处于 “暂停状态” 时,控制器将置为 “一旦调用恢复方法就返回数据” 的状态。 代码中是利用了 Promise.all 捆绑一个控制器 Promise ,如果控制器处于暂停状态下,则不释放 Promise.all ,再将对应的 pause 方法和 resume 方法暴露出去给外界使用。

补充

有些同学错误的认为网络请求和响应是绝对不可以暂停的,我特意在文章前面提到了有关数据传输的内容,并且挂了一句“理论上应用层的协议可以通过类似于标记数据包序列号等等一系列手段来实现暂停机制”,这句话的意思是,如果你魔改 HTTP 或者自己设计实现一个应用层协议(例如像 socket、vmess 这些协议),只要双端支持该协议,是可以实现请求暂停或者响应暂停的,而且这不会影响到 TCP 连接,但是实现暂停机制需要对各种场景和 TCP 策略兜底才能有较好的可靠性。 例如,提供一类控制报文用于控制传输暂停,首先需要对所有数据包的序列号标记顺序,当需要暂停时,发送该序列号的暂停报文给接收端,接收端收到暂停报文就将已接收数据包的块标记返回给发送端等等(这和分片上传机制一样)。

`,21),c=[r];function t(B,y,F,i,A,u){return n(),a("div",null,c)}const m=s(e,[["render",t]]);export{C as __pageData,m as default}; diff --git a/assets/fragment_fetch-pause.md.95306d7b.lean.js b/assets/fragment_fetch-pause.md.95306d7b.lean.js new file mode 100644 index 00000000..363c7610 --- /dev/null +++ b/assets/fragment_fetch-pause.md.95306d7b.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-16-14-40-53.4815a665.png",o="/blog/assets/2024-04-16-14-41-08.0de0710b.png",C=JSON.parse('{"title":"前端中 JS 发起的请求可以暂停吗?","description":"","frontmatter":{},"headers":[{"level":2,"title":"请求应该是什么?","slug":"请求应该是什么","link":"#请求应该是什么","children":[]},{"level":2,"title":"解答提问","slug":"解答提问","link":"#解答提问","children":[{"level":3,"title":"用 JS 实现 ”假暂停” 机制","slug":"用-js-实现-假暂停-机制","link":"#用-js-实现-假暂停-机制","children":[]},{"level":3,"title":"用法","slug":"用法","link":"#用法","children":[]},{"level":3,"title":"执行原理","slug":"执行原理","link":"#执行原理","children":[]}]},{"level":2,"title":"补充","slug":"补充","link":"#补充","children":[]}],"relativePath":"fragment/fetch-pause.md","lastUpdated":1713251442000}'),e={name:"fragment/fetch-pause.md"},r=l("",21),c=[r];function t(B,y,F,i,A,u){return n(),a("div",null,c)}const m=s(e,[["render",t]]);export{C as __pageData,m as default}; diff --git a/assets/fragment_fetch.md.b657f3c7.js b/assets/fragment_fetch.md.b657f3c7.js new file mode 100644 index 00000000..edee8a6d --- /dev/null +++ b/assets/fragment_fetch.md.b657f3c7.js @@ -0,0 +1,97 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"如何取消 Fetch 请求","description":"","frontmatter":{},"headers":[{"level":2,"title":"[译] 如何取消你的 Promise?","slug":"译-如何取消你的-promise","link":"#译-如何取消你的-promise","children":[]}],"relativePath":"fragment/fetch.md","lastUpdated":1713172425000}'),p={name:"fragment/fetch.md"},o=l(`

如何取消 Fetch 请求

JavaScript 的 promise 一直是该语言的一大胜利——它们引发了异步编程的革命,极大地改善了 Web 性能。原生 promise 的一个缺点是,到目前为止,还没有可以取消 fetch 的真正方法。 JavaScript 规范中添加了新的 AbortController ,允许开发人员使用信号中止一个或多个 fetch 调用。 以下是取消 fetch 调用的工作流程:

  • 创建一个 AbortController 实例
  • 该实例具有 signal 属性
  • 将 signal 传递给 fetch option 的 signal
  • 调用 AbortController 的 abort 属性来取消所有使用该信号的 fetch。

以下是取消 Fetch 请求的基本步骤:

js
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(\`Request 1 is complete!\`);
+  })
+  .catch((e) => {
+    console.warn(\`Fetch 1 error: \${e.message}\`);
+  });
+
+// Abort request
+controller.abort();
+
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(\`Request 1 is complete!\`);
+  })
+  .catch((e) => {
+    console.warn(\`Fetch 1 error: \${e.message}\`);
+  });
+
+// Abort request
+controller.abort();
+

在 abort 调用时发生 AbortError ,因此你可以通过比较错误名称来侦听 catch 中的中止操作。

js
catch(e => {
+    if(e.name === "AbortError") {
+        // We know it's been canceled!
+    }
+});
+
catch(e => {
+    if(e.name === "AbortError") {
+        // We know it's been canceled!
+    }
+});
+

将相同的信号传递给多个 fetch 调用将会取消该信号的所有请求:

js
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(\`Request 1 is complete!\`);
+  })
+  .catch((e) => {
+    console.warn(\`Fetch 1 error: \${e.message}\`);
+  });
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(\`Request 2 is complete!\`);
+  })
+  .catch((e) => {
+    console.warn(\`Fetch 2 error: \${e.message}\`);
+  });
+
+// Wait 2 seconds to abort both requests
+setTimeout(() => controller.abort(), 2000);
+
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(\`Request 1 is complete!\`);
+  })
+  .catch((e) => {
+    console.warn(\`Fetch 1 error: \${e.message}\`);
+  });
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(\`Request 2 is complete!\`);
+  })
+  .catch((e) => {
+    console.warn(\`Fetch 2 error: \${e.message}\`);
+  });
+
+// Wait 2 seconds to abort both requests
+setTimeout(() => controller.abort(), 2000);
+

杰克·阿奇博尔德(Jack Archibald)在他的文章 Abortable fetch 中,详细介绍了一个很好的应用,它能够用于创建可中止的 Fetch,而无需所有样板

js
function abortableFetch(request, opts) {
+  const controller = new AbortController();
+  const signal = controller.signal;
+
+  return {
+    abort: () => controller.abort(),
+    ready: fetch(request, { ...opts, signal }),
+  };
+}
+
function abortableFetch(request, opts) {
+  const controller = new AbortController();
+  const signal = controller.signal;
+
+  return {
+    abort: () => controller.abort(),
+    ready: fetch(request, { ...opts, signal }),
+  };
+}
+

说实话,我对取消 Fetch 的方法并不感到兴奋。在理想的世界中,通过 Fetch 返回的 Promise 中的 .cancel() 会很酷,但是也会带来一些问题。无论如何,我为能够取消 Fetch 调用而感到高兴,你也应该如此!

[译] 如何取消你的 Promise?

`,13),e=[o];function c(r,t,B,y,F,i){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{b as __pageData,u as default}; diff --git a/assets/fragment_fetch.md.b657f3c7.lean.js b/assets/fragment_fetch.md.b657f3c7.lean.js new file mode 100644 index 00000000..44b5465c --- /dev/null +++ b/assets/fragment_fetch.md.b657f3c7.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"如何取消 Fetch 请求","description":"","frontmatter":{},"headers":[{"level":2,"title":"[译] 如何取消你的 Promise?","slug":"译-如何取消你的-promise","link":"#译-如何取消你的-promise","children":[]}],"relativePath":"fragment/fetch.md","lastUpdated":1713172425000}'),p={name:"fragment/fetch.md"},o=l("",13),e=[o];function c(r,t,B,y,F,i){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{b as __pageData,u as default}; diff --git a/assets/fragment_forEach.md.6ec35e76.js b/assets/fragment_forEach.md.6ec35e76.js new file mode 100644 index 00000000..59a09ab6 --- /dev/null +++ b/assets/fragment_forEach.md.6ec35e76.js @@ -0,0 +1,83 @@ +import{_ as s,o as n,c as a,a as p}from"./app.f983686f.js";const C=JSON.parse('{"title":"面试官问我 JS 中 forEach 能不能跳出循环","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/forEach.md","lastUpdated":1713163318000}'),l={name:"fragment/forEach.md"},o=p(`

面试官问我 JS 中 forEach 能不能跳出循环

forEach 是 不能使用任何手段跳出循环 的!为什么呢?我们知道 forEach 接收一个函数,它一般有两个参数,第一个是循环的当前元素,第二个是该元素对应的下标,手动实现一下伪代码:

js
Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    fn(this[i], i, this);
+  }
+};
+
Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    fn(this[i], i, this);
+  }
+};
+

forEach 是不是真的这么实现我无从考究,但是以上这个简单的伪代码确实满足 forEach 的特性,而且也很明显就是不能跳出循环,因为根本没有办法操作到真正的 for 循环体。

后来经过查阅文档,发现官方对 forEach 的定义根本不是我认为的语法糖,它的标准说法是 forEach 为每个数组元素执行一次你所提供的函数。官方文档也有这么一段话:

除抛出异常之外,没有其他方法可以停止或中断循环。如果您需要这种行为,则该 forEach()方法是错误的工具。

使用抛出异常来跳出 foreach 循环

js
let arr = [0, 1, "stop", 3, 4];
+try {
+  arr.forEach((element) => {
+    if (element === "stop") {
+      throw new Error("forEachBreak");
+    }
+    console.log(element); // 输出 0 1 后面不输出
+  });
+} catch (e) {
+  console.log(e.message); // forEachBreak
+}
+
let arr = [0, 1, "stop", 3, 4];
+try {
+  arr.forEach((element) => {
+    if (element === "stop") {
+      throw new Error("forEachBreak");
+    }
+    console.log(element); // 输出 0 1 后面不输出
+  });
+} catch (e) {
+  console.log(e.message); // forEachBreak
+}
+

那么可不可以认为,forEach 可以跳出循环,使用抛出异常就可以了?这点我认为仁者见仁智者见智吧,在 forEach 的设计中并没有中断循环的设计,而使用 try-catch 包裹时,当 循环体过大性能会随之下降 ,这是无法避免的,所以抛出异常可以作为一种中断 forEach 的手段,但并不是为解决 forEach 问题而存在的银弹。

再次回归到开头写的那段伪代码,对它进行一些优化,在真正的 for 循环中加入对传入函数的判断:

js
// 为避免争议此处不再覆写原有forEach函数
+Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    let ret = fn(this[i], i, this);
+    if (typeof ret !== "undefined" && (ret == null || ret == false)) break;
+  }
+};
+
// 为避免争议此处不再覆写原有forEach函数
+Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    let ret = fn(this[i], i, this);
+    if (typeof ret !== "undefined" && (ret == null || ret == false)) break;
+  }
+};
+

这样的话就能根据 return 值来进行循环跳出啦:

js
let arr = [0, 1, "stop", 3, 4];
+
+arr.myForEach((x) => {
+  if (x === "stop") return false;
+  console.log(x); // 输出 0 1 后面不输出
+});
+
+// return即为continue:
+arr.myForEach((x) => {
+  if (x === "stop") return;
+  console.log(x); // 0 1 3 4
+});
+
let arr = [0, 1, "stop", 3, 4];
+
+arr.myForEach((x) => {
+  if (x === "stop") return false;
+  console.log(x); // 输出 0 1 后面不输出
+});
+
+// return即为continue:
+arr.myForEach((x) => {
+  if (x === "stop") return;
+  console.log(x); // 0 1 3 4
+});
+

文档中还提到 forEach 需要一个同步函数 ,也就是说在使用异步函数或 Promise 作为回调时会发生预期以外的结果,所以 forEach 还是需要慎用。

当然,用简单的 for 循环去完成一切事情也不失为一种办法, 代码首先是写给人看的,附带能在机器上运行的作用 ,forEach 在很多时候用起来更加顺手,但也务必在理解 JS 如何设计这些工具函数的前提下来编写我们的业务代码。

我们可以在遍历数组时使用 for..of.. ,在遍历对象时使用 for..in.. ,而官方也在 forEach 文档下列举了其它一些工具函数,这里不做过多展开:

js
Array.prototype.find();
+Array.prototype.findIndex();
+Array.prototype.map();
+Array.prototype.filter();
+Array.prototype.every();
+Array.prototype.some();
+
Array.prototype.find();
+Array.prototype.findIndex();
+Array.prototype.map();
+Array.prototype.filter();
+Array.prototype.every();
+Array.prototype.some();
+

如何根据不同的业务场景,选择使用对应的工具函数来更有效地处理业务逻辑,才是我们真正应该思考的,或许这也是面试当中真正想考察的吧。

`,18),e=[o];function r(c,t,B,y,F,i){return n(),a("div",null,e)}const E=s(l,[["render",r]]);export{C as __pageData,E as default}; diff --git a/assets/fragment_forEach.md.6ec35e76.lean.js b/assets/fragment_forEach.md.6ec35e76.lean.js new file mode 100644 index 00000000..bf3c664d --- /dev/null +++ b/assets/fragment_forEach.md.6ec35e76.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as p}from"./app.f983686f.js";const C=JSON.parse('{"title":"面试官问我 JS 中 forEach 能不能跳出循环","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/forEach.md","lastUpdated":1713163318000}'),l={name:"fragment/forEach.md"},o=p("",18),e=[o];function r(c,t,B,y,F,i){return n(),a("div",null,e)}const E=s(l,[["render",r]]);export{C as __pageData,E as default}; diff --git a/assets/fragment_nextTick.md.f9431411.js b/assets/fragment_nextTick.md.f9431411.js new file mode 100644 index 00000000..3490875c --- /dev/null +++ b/assets/fragment_nextTick.md.f9431411.js @@ -0,0 +1,247 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"vue nextTick 实现原理,必拿下!","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么会有 nextTick 这个东西的存在?","slug":"为什么会有-nexttick-这个东西的存在","link":"#为什么会有-nexttick-这个东西的存在","children":[]},{"level":2,"title":"nextTick 的作用?","slug":"nexttick-的作用","link":"#nexttick-的作用","children":[]},{"level":2,"title":"nextTick 实现原理","slug":"nexttick-实现原理","link":"#nexttick-实现原理","children":[]},{"level":2,"title":"源码解读","slug":"源码解读","link":"#源码解读","children":[]}],"relativePath":"fragment/nextTick.md","lastUpdated":1713173154000}'),p={name:"fragment/nextTick.md"},o=l(`

vue nextTick 实现原理,必拿下!

为什么会有 nextTick 这个东西的存在?

因为 vue 采用的 异步更新策略 ,当监听到数据发生变化的时候不会立即去更新 DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更。

这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作 DOM 的次数,如果不采用这种方法,假设数据改变 100 次就要去更新 100 次 DOM,而频繁的 DOM 更新是很耗性能的。

nextTick 的作用?

nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到 DOM 更新后才执行;想要操作 基于最新数据生成的 DOM 时,就将这个操作放在 nextTick 的回调中;

nextTick 实现原理

将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务; nextTick 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0)

源码解读

源码位置 core/util/next-tick

源码并不复杂,三个函数,60 几行代码,沉下心去看!

ts
import { noop } from "shared/util";
+import { handleError } from "./error";
+import { isIE, isIOS, isNative } from "./env";
+
+//  noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
+//  handleError 错误处理函数
+//  isIE, isIOS, isNative 环境判断函数,
+//  isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false
+
+export let isUsingMicroTask = false; // 标记 nextTick 最终是否以微任务执行
+
+const callbacks = []; // 存放调用 nextTick 时传入的回调函数
+let pending = false; // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
+// 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
+
+// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
+// 回调的 this 自动绑定到调用它的实例上
+export function nextTick(cb?: Function, ctx?: Object) {
+  let _resolve;
+  // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
+  callbacks.push(() => {
+    if (cb) {
+      // 对传入的回调进行 try catch 错误捕获
+      try {
+        cb.call(ctx);
+      } catch (e) {
+        // 进行统一的错误处理
+        handleError(e, ctx, "nextTick");
+      }
+    } else if (_resolve) {
+      _resolve(ctx);
+    }
+  });
+
+  // 如果当前没有在 pending 的回调,
+  // 就执行 timeFunc 函数选择当前环境优先支持的异步方法
+  if (!pending) {
+    pending = true;
+    timerFunc();
+  }
+
+  // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
+  // 在返回的这个 promise.then 中 DOM 已经更新好了,
+  if (!cb && typeof Promise !== "undefined") {
+    return new Promise((resolve) => {
+      _resolve = resolve;
+    });
+  }
+}
+
+// 判断当前环境优先支持的异步方法,优先选择微任务
+// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
+// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
+// setImmediate 在 IE10 和 node 中支持
+
+// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次
+
+let timerFunc;
+// 判断当前环境是否原生支持 promise
+if (typeof Promise !== "undefined" && isNative(Promise)) {
+  // 支持 promise
+  const p = Promise.resolve();
+  timerFunc = () => {
+    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
+    p.then(flushCallbacks);
+    if (isIOS) setTimeout(noop);
+  };
+  // 标记当前 nextTick 使用的微任务
+  isUsingMicroTask = true;
+
+  // 如果不支持 promise,就判断是否支持 MutationObserver
+  // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
+} else if (
+  !isIE &&
+  typeof MutationObserver !== "undefined" &&
+  (isNative(MutationObserver) ||
+    MutationObserver.toString() === "[object MutationObserverConstructor]")
+) {
+  let counter = 1;
+  // new 一个 MutationObserver 类
+  const observer = new MutationObserver(flushCallbacks);
+  // 创建一个文本节点
+  const textNode = document.createTextNode(String(counter));
+  // 监听这个文本节点,当数据发生变化就执行 flushCallbacks
+  observer.observe(textNode, { characterData: true });
+  timerFunc = () => {
+    counter = (counter + 1) % 2;
+    textNode.data = String(counter); // 数据更新
+  };
+  isUsingMicroTask = true; // 标记当前 nextTick 使用的微任务
+
+  // 判断当前环境是否原生支持 setImmediate
+} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
+  timerFunc = () => {
+    setImmediate(flushCallbacks);
+  };
+} else {
+  // 以上三种都不支持就选择 setTimeout
+  timerFunc = () => {
+    setTimeout(flushCallbacks, 0);
+  };
+}
+
+// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
+// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
+function flushCallbacks() {
+  pending = false;
+  const copies = callbacks.slice(0); // 拷贝一份 callbacks
+  callbacks.length = 0; // 清空 callbacks
+  for (let i = 0; i < copies.length; i++) {
+    // 遍历执行传入的回调
+    copies[i]();
+  }
+}
+
+// 为什么要拷贝一份 callbacks
+
+// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
+// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
+// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
+// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
+// 否则就可能出现一直循环的情况,
+// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调
+
import { noop } from "shared/util";
+import { handleError } from "./error";
+import { isIE, isIOS, isNative } from "./env";
+
+//  noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
+//  handleError 错误处理函数
+//  isIE, isIOS, isNative 环境判断函数,
+//  isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false
+
+export let isUsingMicroTask = false; // 标记 nextTick 最终是否以微任务执行
+
+const callbacks = []; // 存放调用 nextTick 时传入的回调函数
+let pending = false; // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
+// 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
+
+// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
+// 回调的 this 自动绑定到调用它的实例上
+export function nextTick(cb?: Function, ctx?: Object) {
+  let _resolve;
+  // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
+  callbacks.push(() => {
+    if (cb) {
+      // 对传入的回调进行 try catch 错误捕获
+      try {
+        cb.call(ctx);
+      } catch (e) {
+        // 进行统一的错误处理
+        handleError(e, ctx, "nextTick");
+      }
+    } else if (_resolve) {
+      _resolve(ctx);
+    }
+  });
+
+  // 如果当前没有在 pending 的回调,
+  // 就执行 timeFunc 函数选择当前环境优先支持的异步方法
+  if (!pending) {
+    pending = true;
+    timerFunc();
+  }
+
+  // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
+  // 在返回的这个 promise.then 中 DOM 已经更新好了,
+  if (!cb && typeof Promise !== "undefined") {
+    return new Promise((resolve) => {
+      _resolve = resolve;
+    });
+  }
+}
+
+// 判断当前环境优先支持的异步方法,优先选择微任务
+// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
+// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
+// setImmediate 在 IE10 和 node 中支持
+
+// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次
+
+let timerFunc;
+// 判断当前环境是否原生支持 promise
+if (typeof Promise !== "undefined" && isNative(Promise)) {
+  // 支持 promise
+  const p = Promise.resolve();
+  timerFunc = () => {
+    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
+    p.then(flushCallbacks);
+    if (isIOS) setTimeout(noop);
+  };
+  // 标记当前 nextTick 使用的微任务
+  isUsingMicroTask = true;
+
+  // 如果不支持 promise,就判断是否支持 MutationObserver
+  // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
+} else if (
+  !isIE &&
+  typeof MutationObserver !== "undefined" &&
+  (isNative(MutationObserver) ||
+    MutationObserver.toString() === "[object MutationObserverConstructor]")
+) {
+  let counter = 1;
+  // new 一个 MutationObserver 类
+  const observer = new MutationObserver(flushCallbacks);
+  // 创建一个文本节点
+  const textNode = document.createTextNode(String(counter));
+  // 监听这个文本节点,当数据发生变化就执行 flushCallbacks
+  observer.observe(textNode, { characterData: true });
+  timerFunc = () => {
+    counter = (counter + 1) % 2;
+    textNode.data = String(counter); // 数据更新
+  };
+  isUsingMicroTask = true; // 标记当前 nextTick 使用的微任务
+
+  // 判断当前环境是否原生支持 setImmediate
+} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
+  timerFunc = () => {
+    setImmediate(flushCallbacks);
+  };
+} else {
+  // 以上三种都不支持就选择 setTimeout
+  timerFunc = () => {
+    setTimeout(flushCallbacks, 0);
+  };
+}
+
+// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
+// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
+function flushCallbacks() {
+  pending = false;
+  const copies = callbacks.slice(0); // 拷贝一份 callbacks
+  callbacks.length = 0; // 清空 callbacks
+  for (let i = 0; i < copies.length; i++) {
+    // 遍历执行传入的回调
+    copies[i]();
+  }
+}
+
+// 为什么要拷贝一份 callbacks
+
+// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
+// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
+// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
+// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
+// 否则就可能出现一直循环的情况,
+// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调
+
`,12),e=[o];function c(t,r,B,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{b as __pageData,u as default}; diff --git a/assets/fragment_nextTick.md.f9431411.lean.js b/assets/fragment_nextTick.md.f9431411.lean.js new file mode 100644 index 00000000..3e57cd78 --- /dev/null +++ b/assets/fragment_nextTick.md.f9431411.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"vue nextTick 实现原理,必拿下!","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么会有 nextTick 这个东西的存在?","slug":"为什么会有-nexttick-这个东西的存在","link":"#为什么会有-nexttick-这个东西的存在","children":[]},{"level":2,"title":"nextTick 的作用?","slug":"nexttick-的作用","link":"#nexttick-的作用","children":[]},{"level":2,"title":"nextTick 实现原理","slug":"nexttick-实现原理","link":"#nexttick-实现原理","children":[]},{"level":2,"title":"源码解读","slug":"源码解读","link":"#源码解读","children":[]}],"relativePath":"fragment/nextTick.md","lastUpdated":1713173154000}'),p={name:"fragment/nextTick.md"},o=l("",12),e=[o];function c(t,r,B,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{b as __pageData,u as default}; diff --git a/assets/fragment_npm-scripts.md.051d45f0.js b/assets/fragment_npm-scripts.md.051d45f0.js new file mode 100644 index 00000000..a0d02dbd --- /dev/null +++ b/assets/fragment_npm-scripts.md.051d45f0.js @@ -0,0 +1,19 @@ +import{_ as s,o as n,c as a,a as p}from"./app.f983686f.js";const e="/blog/assets/2024-04-15-19-41-41.cddf3090.png",b=JSON.parse('{"title":"输入 npm run xxx 后发生了什么?","description":"","frontmatter":{},"headers":[{"level":2,"title":"问题分析","slug":"问题分析","link":"#问题分析","children":[]},{"level":2,"title":"浅析 npm install 机制阅读","slug":"浅析-npm-install-机制阅读","link":"#浅析-npm-install-机制阅读","children":[]}],"relativePath":"fragment/npm-scripts.md","lastUpdated":1713238328000}'),l={name:"fragment/npm-scripts.md"},o=p(`

输入 npm run xxx 后发生了什么?

在前端开发中,我们经常使用 npm run dev 来启动本地开发环境。那么,当输入完类似 npm run xxx 的命令后,到底发生了什么呢?项目又是如何被启动的?

首先我们知道,node 可以把 .js 文件当做脚本来运行,例如启动后台项目时经常会输入:

shell
node app.js
+
node app.js
+

这个是直接使用全局安装的 node 命令来执行了 ./src 目录下的 app.js 文件。

而当我们使用 npm (或者 yarn)来管理项目时,会在根目录下生成一个 package.json 文件,其中的 scripts 属性,就是用于配置 npm run xxx 命令的,比如以下配置:

json
"scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+},
+
"scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+},
+

再看对应的 node_modules 目录下的.bin 目录,里面有一个 vite 文件

打开 vite 文件可以看到 #!/bin/sh,表示这是一个脚本(node 可以执行)。

当执行 npm run dev 时,首先会去 package.json 文件中找到 script 属性的"dev",然后再到 node_modules 目录下的.bin 目录中找到"dev"对应的"vite",执行 vite(由前面可知 vite 是一个脚本,类似于前面提过的 node app.js)。 再如:

json
{
+  "dev": "./node_modules/.bin/nodemon bin/www"
+}
+
{
+  "dev": "./node_modules/.bin/nodemon bin/www"
+}
+

当执行 npm run dev 时,相当于执行 "./node_modules/.bin/nodemon bin/www",其中 bin/www 作为参数传入。

总之:先去 package.json 的 scripts 属性中查找 xxx1,再去./node_modules/.bin 中查找 xxx1 对应的 sss1,执行 sss1 文件)

问题分析

TIP

问题 1: 为什么不直接执行 sss 而要执行 xxx 呢?

直接执行 sss 会报错,因为操作系统中不存在 sss 这一条指令。

TIP

问题 2: 为什么执行 npm run xxx(或者 yarn xxx)时就能成功呢?

因为我们在安装依赖的时候,是通过 npm i xxx (或者 yarn ...)来执行的,例如 npm i @vue/cli-service,npm 在 安装这个依赖的时候,就会 node_modules/.bin/ 目录中创建好名为 vue-cli-service 的几个可执行文件了。

.bin 目录下的文件,是一个个的软链接,打开文件可以看到文件顶部写着 #!/bin/sh ,表示这是一个脚本,可由 node 来执行。当使用 npm run xxx 执行 sss 时,虽然没有安装 sss 的全局命令,但是 npm 会到 ./node_modules/.bin 中找到 sss 文件作为脚本来执行,则相当于执行了 ./node_modules/.bin/sss。

即: 运行 npm run xxx 的时候,npm 会先在当前目录的 node_modules/.bin 查找要执行的程序,如果找到则运行;没有找到则从全局的 node_modules/.bin 中查找; 全局目录还是没找到,就会从 window 的 path 环境变量中查找有没有其他同名的可执行程序。

浅析 npm install 机制阅读

`,22),t=[o];function r(c,i,d,u,B,m){return n(),a("div",null,t)}const v=s(l,[["render",r]]);export{b as __pageData,v as default}; diff --git a/assets/fragment_npm-scripts.md.051d45f0.lean.js b/assets/fragment_npm-scripts.md.051d45f0.lean.js new file mode 100644 index 00000000..312d66ef --- /dev/null +++ b/assets/fragment_npm-scripts.md.051d45f0.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as p}from"./app.f983686f.js";const e="/blog/assets/2024-04-15-19-41-41.cddf3090.png",b=JSON.parse('{"title":"输入 npm run xxx 后发生了什么?","description":"","frontmatter":{},"headers":[{"level":2,"title":"问题分析","slug":"问题分析","link":"#问题分析","children":[]},{"level":2,"title":"浅析 npm install 机制阅读","slug":"浅析-npm-install-机制阅读","link":"#浅析-npm-install-机制阅读","children":[]}],"relativePath":"fragment/npm-scripts.md","lastUpdated":1713238328000}'),l={name:"fragment/npm-scripts.md"},o=p("",22),t=[o];function r(c,i,d,u,B,m){return n(),a("div",null,t)}const v=s(l,[["render",r]]);export{b as __pageData,v as default}; diff --git a/assets/fragment_promise-cancel.md.6a4fe1e8.js b/assets/fragment_promise-cancel.md.6a4fe1e8.js new file mode 100644 index 00000000..72dc388c --- /dev/null +++ b/assets/fragment_promise-cancel.md.6a4fe1e8.js @@ -0,0 +1,353 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-16-15-37-25.281ad292.png",b=JSON.parse('{"title":"面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:?","description":"","frontmatter":{},"headers":[{"level":2,"title":"取消功能","slug":"取消功能","link":"#取消功能","children":[]},{"level":2,"title":"进度通知功能","slug":"进度通知功能","link":"#进度通知功能","children":[]},{"level":2,"title":"End","slug":"end","link":"#end","children":[]}],"relativePath":"fragment/promise-cancel.md","lastUpdated":1713254070000}'),o={name:"fragment/promise-cancel.md"},e=l(`

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:?

问题:点击一个 button,触发一次这个函数,但是既没有走成功回调,也没有走失败回调。问下一次点击,类似取消上次 promise 的操作,但没有这种方法,怎么去做。我又说了提供变量控制,这个可以,还有就说不出来了。面试官说可以包装 promise,写一个类,提供一个 cancel 方法,类似这种,主要是不想拿到期望的 promise 值。

原面经链接: 美团暑期一面_牛客网 (nowcoder.com)

取消功能

我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它 永远停留至 pending 状态 。奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了 🤔

js
const p = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    // handler data, no resolve and reject
+  }, 1000);
+});
+console.log(p); // Promise {<pending>} 💡
+
const p = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    // handler data, no resolve and reject
+  }, 1000);
+});
+console.log(p); // Promise {<pending>} 💡
+

但注意我们的需求条件,是 在状态转换过程中 ,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。 其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:

js
const getData = () =>
+  new Promise((resolve) => {
+    setTimeout(() => {
+      console.log("发送网络请求获取数据"); // ❗
+      resolve("success get Data");
+    }, 2500);
+  });
+
+const timer = () =>
+  new Promise((_, reject) => {
+    setTimeout(() => {
+      reject("timeout");
+    }, 2000);
+  });
+
+const p = Promise.race([getData(), timer()])
+  .then((res) => {
+    console.log("获取数据:", res);
+  })
+  .catch((err) => {
+    console.log("超时: ", err);
+  });
+
const getData = () =>
+  new Promise((resolve) => {
+    setTimeout(() => {
+      console.log("发送网络请求获取数据"); // ❗
+      resolve("success get Data");
+    }, 2500);
+  });
+
+const timer = () =>
+  new Promise((_, reject) => {
+    setTimeout(() => {
+      reject("timeout");
+    }, 2000);
+  });
+
+const p = Promise.race([getData(), timer()])
+  .then((res) => {
+    console.log("获取数据:", res);
+  })
+  .catch((err) => {
+    console.log("超时: ", err);
+  });
+

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎 🤔,即使超时网络请求还会发出。

而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。比如让用户进行控制, 一个按钮用来表示发送请求,一个按钮表示取消 ,来中断 promise 的流程:当然这里我们不讨论关于请求的取消操作,重点在 Promise 上

其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生, clearTimeout 、 clearInterval 嘛。

OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <button id="send">Send</button>
+    <button id="cancel">Cancel</button>
+
+    <script>
+      class CancelToken {
+        constructor(cancelFn) {
+          this.promise = new Promise((resolve, reject) => {
+            cancelFn(() => {
+              console.log("delay cancelled");
+              resolve();
+            });
+          });
+        }
+      }
+      const sendButton = document.querySelector("#send");
+      const cancelButton = document.querySelector("#cancel");
+
+      function cancellableDelayedResolve(delay) {
+        console.log("prepare send request");
+        return new Promise((resolve, reject) => {
+          const id = setTimeout(() => {
+            console.log("ajax get data");
+            resolve();
+          }, delay);
+
+          const cancelToken = new CancelToken((cancelCallback) =>
+            cancelButton.addEventListener("click", cancelCallback)
+          );
+          cancelToken.promise.then(() => clearTimeout(id));
+        });
+      }
+      sendButton.addEventListener("click", () =>
+        cancellableDelayedResolve(1000)
+      );
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <button id="send">Send</button>
+    <button id="cancel">Cancel</button>
+
+    <script>
+      class CancelToken {
+        constructor(cancelFn) {
+          this.promise = new Promise((resolve, reject) => {
+            cancelFn(() => {
+              console.log("delay cancelled");
+              resolve();
+            });
+          });
+        }
+      }
+      const sendButton = document.querySelector("#send");
+      const cancelButton = document.querySelector("#cancel");
+
+      function cancellableDelayedResolve(delay) {
+        console.log("prepare send request");
+        return new Promise((resolve, reject) => {
+          const id = setTimeout(() => {
+            console.log("ajax get data");
+            resolve();
+          }, delay);
+
+          const cancelToken = new CancelToken((cancelCallback) =>
+            cancelButton.addEventListener("click", cancelCallback)
+          );
+          cancelToken.promise.then(() => clearTimeout(id));
+        });
+      }
+      sendButton.addEventListener("click", () =>
+        cancellableDelayedResolve(1000)
+      );
+    </script>
+  </body>
+</html>
+

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:

首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为 取消功能期限 ,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。

这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理 🤔?

介于自己没理解,我就按照自己的思路封装个不一样的 🤣:

js
const sendButton = document.querySelector("#send");
+const cancelButton = document.querySelector("#cancel");
+
+class CancelPromise {
+  // delay: 取消功能期限  request:获取数据请求(必须返回 promise)
+  constructor(delay, request) {
+    this.req = request;
+    this.delay = delay;
+    this.timer = null;
+  }
+
+  delayResolve() {
+    return new Promise((resolve, reject) => {
+      console.log("prepare request");
+      this.timer = setTimeout(() => {
+        console.log("send request");
+        this.timer = null;
+        this.req().then(
+          (res) => resolve(res),
+          (err) => reject(err)
+        );
+      }, this.delay);
+    });
+  }
+
+  cancelResolve() {
+    console.log("cancel promise");
+    this.timer && clearTimeout(this.timer);
+  }
+}
+
+// 模拟网络请求
+function getData() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve("this is data");
+    }, 2000);
+  });
+}
+
+const cp = new CancelPromise(1000, getData);
+
+sendButton.addEventListener("click", () =>
+  cp.delayResolve().then((res) => {
+    console.log("拿到数据:", res);
+  })
+);
+cancelButton.addEventListener("click", () => cp.cancelResolve());
+
const sendButton = document.querySelector("#send");
+const cancelButton = document.querySelector("#cancel");
+
+class CancelPromise {
+  // delay: 取消功能期限  request:获取数据请求(必须返回 promise)
+  constructor(delay, request) {
+    this.req = request;
+    this.delay = delay;
+    this.timer = null;
+  }
+
+  delayResolve() {
+    return new Promise((resolve, reject) => {
+      console.log("prepare request");
+      this.timer = setTimeout(() => {
+        console.log("send request");
+        this.timer = null;
+        this.req().then(
+          (res) => resolve(res),
+          (err) => reject(err)
+        );
+      }, this.delay);
+    });
+  }
+
+  cancelResolve() {
+    console.log("cancel promise");
+    this.timer && clearTimeout(this.timer);
+  }
+}
+
+// 模拟网络请求
+function getData() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve("this is data");
+    }, 2000);
+  });
+}
+
+const cp = new CancelPromise(1000, getData);
+
+sendButton.addEventListener("click", () =>
+  cp.delayResolve().then((res) => {
+    console.log("拿到数据:", res);
+  })
+);
+cancelButton.addEventListener("click", () => cp.cancelResolve());
+

进度通知功能

进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:

执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用

这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:

js
class TrackablePromise extends Promise {
+  constructor(executor) {
+    const notifyHandlers = [];
+    super((resolve, reject) => {
+      return executor(resolve, reject, (status) => {
+        notifyHandlers.map((handler) => handler(status));
+      });
+    });
+    this.notifyHandlers = notifyHandlers;
+  }
+  notify(notifyHandler) {
+    this.notifyHandlers.push(notifyHandler);
+    return this;
+  }
+}
+let p = new TrackablePromise((resolve, reject, notify) => {
+  function countdown(x) {
+    if (x > 0) {
+      notify(\`\${20 * x}% remaining\`);
+      setTimeout(() => countdown(x - 1), 1000);
+    } else {
+      resolve();
+    }
+  }
+  countdown(5);
+});
+
+p.notify((x) => setTimeout(console.log, 0, "progress:", x));
+p.then(() => setTimeout(console.log, 0, "completed"));
+
class TrackablePromise extends Promise {
+  constructor(executor) {
+    const notifyHandlers = [];
+    super((resolve, reject) => {
+      return executor(resolve, reject, (status) => {
+        notifyHandlers.map((handler) => handler(status));
+      });
+    });
+    this.notifyHandlers = notifyHandlers;
+  }
+  notify(notifyHandler) {
+    this.notifyHandlers.push(notifyHandler);
+    return this;
+  }
+}
+let p = new TrackablePromise((resolve, reject, notify) => {
+  function countdown(x) {
+    if (x > 0) {
+      notify(\`\${20 * x}% remaining\`);
+      setTimeout(() => countdown(x - 1), 1000);
+    } else {
+      resolve();
+    }
+  }
+  countdown(5);
+});
+
+p.notify((x) => setTimeout(console.log, 0, "progress:", x));
+p.then(() => setTimeout(console.log, 0, "completed"));
+

emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?

不好就自己再写一个 🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:

js
// 模拟数据请求
+function getData(timer, value) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(value);
+    }, timer);
+  });
+}
+
+let p = new TrackablePromise(async (resolve, reject, notify) => {
+  try {
+    const res1 = await getData1();
+    notify("已获取到一阶段数据");
+    const res2 = await getData2();
+    notify("已获取到二阶段数据");
+    const res3 = await getData3();
+    notify("已获取到三阶段数据");
+    resolve([res1, res2, res3]);
+  } catch (error) {
+    notify("出错!");
+    reject(error);
+  }
+});
+
+p.notify((x) => console.log(x));
+p.then((res) => console.log("Get All Data:", res));
+
// 模拟数据请求
+function getData(timer, value) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(value);
+    }, timer);
+  });
+}
+
+let p = new TrackablePromise(async (resolve, reject, notify) => {
+  try {
+    const res1 = await getData1();
+    notify("已获取到一阶段数据");
+    const res2 = await getData2();
+    notify("已获取到二阶段数据");
+    const res3 = await getData3();
+    notify("已获取到三阶段数据");
+    resolve([res1, res2, res3]);
+  } catch (error) {
+    notify("出错!");
+    reject(error);
+  }
+});
+
+p.notify((x) => console.log(x));
+p.then((res) => console.log("Get All Data:", res));
+

End

关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。

实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...

`,30),c=[e];function t(B,r,y,F,i,A){return n(),a("div",null,c)}const C=s(o,[["render",t]]);export{b as __pageData,C as default}; diff --git a/assets/fragment_promise-cancel.md.6a4fe1e8.lean.js b/assets/fragment_promise-cancel.md.6a4fe1e8.lean.js new file mode 100644 index 00000000..e758062d --- /dev/null +++ b/assets/fragment_promise-cancel.md.6a4fe1e8.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-16-15-37-25.281ad292.png",b=JSON.parse('{"title":"面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:?","description":"","frontmatter":{},"headers":[{"level":2,"title":"取消功能","slug":"取消功能","link":"#取消功能","children":[]},{"level":2,"title":"进度通知功能","slug":"进度通知功能","link":"#进度通知功能","children":[]},{"level":2,"title":"End","slug":"end","link":"#end","children":[]}],"relativePath":"fragment/promise-cancel.md","lastUpdated":1713254070000}'),o={name:"fragment/promise-cancel.md"},e=l("",30),c=[e];function t(B,r,y,F,i,A){return n(),a("div",null,c)}const C=s(o,[["render",t]]);export{b as __pageData,C as default}; diff --git a/assets/fragment_react-duplicate.md.37494912.js b/assets/fragment_react-duplicate.md.37494912.js new file mode 100644 index 00000000..5e4bb3a9 --- /dev/null +++ b/assets/fragment_react-duplicate.md.37494912.js @@ -0,0 +1,25 @@ +import{_ as s,o as a,c as n,a as e}from"./app.f983686f.js";const b=JSON.parse('{"title":"解决 npm link 本地代码后 react 库存在多份实例的问题","description":"","frontmatter":{},"headers":[{"level":2,"title":"非法 Hook 调用错误","slug":"非法-hook-调用错误","link":"#非法-hook-调用错误","children":[]},{"level":2,"title":"报错原因","slug":"报错原因","link":"#报错原因","children":[]},{"level":2,"title":"为什么 npm install 没有问题,npm link 有问题","slug":"为什么-npm-install-没有问题-npm-link-有问题","link":"#为什么-npm-install-没有问题-npm-link-有问题","children":[]},{"level":2,"title":"怎么解决这个问题","slug":"怎么解决这个问题","link":"#怎么解决这个问题","children":[{"level":3,"title":"使用 link 的方式将组件库的 react 依赖指向到主项目的 react 依赖","slug":"使用-link-的方式将组件库的-react-依赖指向到主项目的-react-依赖","link":"#使用-link-的方式将组件库的-react-依赖指向到主项目的-react-依赖","children":[]},{"level":3,"title":"使用 alias 为 Nodejs 指定 react 加载目录","slug":"使用-alias-为-nodejs-指定-react-加载目录","link":"#使用-alias-为-nodejs-指定-react-加载目录","children":[]}]}],"relativePath":"fragment/react-duplicate.md","lastUpdated":1713617788000}'),l={name:"fragment/react-duplicate.md"},o=e(`

解决 npm link 本地代码后 react 库存在多份实例的问题

非法 Hook 调用错误

在调试本地组件库代码时,link 到主项目时报错了如下错误

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
+1. You might have mismatching versions of React and the renderer (such as React DOM)
+2. You might be breaking the Rules of Hooks
+3. You might have more than one copy of React in the same app See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
+
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
+1. You might have mismatching versions of React and the renderer (such as React DOM)
+2. You might be breaking the Rules of Hooks
+3. You might have more than one copy of React in the same app See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
+

但是将代码发布到本地仓库后,install 下来是不会有问题的。

报错原因

React 官方文档上解释了这个原因。

为了使 Hook 正常工作,你应用代码中的 react 依赖以及 react-dom 的 package 内部使用的 react 依赖,必须解析为同一个模块。

也就是说,在你的应用里找到了两份 react 实例,并且 react-dom 中引用的 react 实例与直接 import 的 react 实例不是同一份。对应到我报错的场景里,就是 link 到本地组件库时,组件库中 import 了一份 react-dom, 而这一份 react-dom 引用的 react 实例,是本地组件库中安装的 react 实例。而非主应用中安装的那一份 react 实例。

npm install 在安装依赖的时候,会合并相同依赖,安装在最外层的 node_modules 中。所以只有一份依赖。而 npm link 是直接引用本地目录,本地目录的 node_modules 会和主项目的 node_modules 同时存在两份相同的引用。

怎么解决这个问题

要解决这个问题,就需要将组件库 react-dom 依赖的 react 指向主项目 node_modules 中安装的那一份 react 实例。我们可以通过两种方式来完成这个操作。

shell
cd [PACKAGE]
+npm link [PROJECT]/node_modules/react
+
cd [PACKAGE]
+npm link [PROJECT]/node_modules/react
+

使用 alias 为 Nodejs 指定 react 加载目录

js
// /build/wepack.base.config.js
+const config = {
+  alias: {
+    react: path.join(__dirname, "../node_modules/react"),
+  },
+};
+
// /build/wepack.base.config.js
+const config = {
+  alias: {
+    react: path.join(__dirname, "../node_modules/react"),
+  },
+};
+
`,17),p=[o];function c(r,t,i,d,h,B){return a(),n("div",null,p)}const u=s(l,[["render",c]]);export{b as __pageData,u as default}; diff --git a/assets/fragment_react-duplicate.md.37494912.lean.js b/assets/fragment_react-duplicate.md.37494912.lean.js new file mode 100644 index 00000000..f1214886 --- /dev/null +++ b/assets/fragment_react-duplicate.md.37494912.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,a as e}from"./app.f983686f.js";const b=JSON.parse('{"title":"解决 npm link 本地代码后 react 库存在多份实例的问题","description":"","frontmatter":{},"headers":[{"level":2,"title":"非法 Hook 调用错误","slug":"非法-hook-调用错误","link":"#非法-hook-调用错误","children":[]},{"level":2,"title":"报错原因","slug":"报错原因","link":"#报错原因","children":[]},{"level":2,"title":"为什么 npm install 没有问题,npm link 有问题","slug":"为什么-npm-install-没有问题-npm-link-有问题","link":"#为什么-npm-install-没有问题-npm-link-有问题","children":[]},{"level":2,"title":"怎么解决这个问题","slug":"怎么解决这个问题","link":"#怎么解决这个问题","children":[{"level":3,"title":"使用 link 的方式将组件库的 react 依赖指向到主项目的 react 依赖","slug":"使用-link-的方式将组件库的-react-依赖指向到主项目的-react-依赖","link":"#使用-link-的方式将组件库的-react-依赖指向到主项目的-react-依赖","children":[]},{"level":3,"title":"使用 alias 为 Nodejs 指定 react 加载目录","slug":"使用-alias-为-nodejs-指定-react-加载目录","link":"#使用-alias-为-nodejs-指定-react-加载目录","children":[]}]}],"relativePath":"fragment/react-duplicate.md","lastUpdated":1713617788000}'),l={name:"fragment/react-duplicate.md"},o=e("",17),p=[o];function c(r,t,i,d,h,B){return a(),n("div",null,p)}const u=s(l,[["render",c]]);export{b as __pageData,u as default}; diff --git a/assets/fragment_react-hooks-timer.md.7fb4e725.js b/assets/fragment_react-hooks-timer.md.7fb4e725.js new file mode 100644 index 00000000..20c58684 --- /dev/null +++ b/assets/fragment_react-hooks-timer.md.7fb4e725.js @@ -0,0 +1,211 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"React Hooks 实现倒计时及避坑","description":"","frontmatter":{},"headers":[{"level":2,"title":"正确示例 1","slug":"正确示例-1","link":"#正确示例-1","children":[]},{"level":2,"title":"正确示例 2","slug":"正确示例-2","link":"#正确示例-2","children":[]},{"level":2,"title":"错误示例","slug":"错误示例","link":"#错误示例","children":[]},{"level":2,"title":"错误示例会出现的问题","slug":"错误示例会出现的问题","link":"#错误示例会出现的问题","children":[]},{"level":2,"title":"原因","slug":"原因","link":"#原因","children":[]},{"level":2,"title":"解决方案","slug":"解决方案","link":"#解决方案","children":[]}],"relativePath":"fragment/react-hooks-timer.md","lastUpdated":1713167722000}'),p={name:"fragment/react-hooks-timer.md"},o=l(`

React Hooks 实现倒计时及避坑

正确示例 1

jsx
import { useState, useEffect } from "react";
+
+// 入参是一个时间段,比如6000s,不是具体的过期时间点,因为考虑到用户可能会修改电脑的时间,这样在获取当前时间的时候就会产生误差
+export const CountDown = ({ countDown = 4 }) => {
+  let cd = countDown;
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const handleData = () => {
+    if (cd <= 0) {
+      setTime("到时间啦");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(\`倒计时: \${d}\${h}\${m}\${s}秒\`);
+    cd--;
+    timer = setTimeout(() => {
+      handleData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    handleData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+export default CountDown;
+
import { useState, useEffect } from "react";
+
+// 入参是一个时间段,比如6000s,不是具体的过期时间点,因为考虑到用户可能会修改电脑的时间,这样在获取当前时间的时候就会产生误差
+export const CountDown = ({ countDown = 4 }) => {
+  let cd = countDown;
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const handleData = () => {
+    if (cd <= 0) {
+      setTime("到时间啦");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(\`倒计时: \${d}\${h}\${m}\${s}秒\`);
+    cd--;
+    timer = setTimeout(() => {
+      handleData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    handleData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+export default CountDown;
+

正确示例 2

jsx
import { useState, useEffect, useRef } from "react";
+
+const CountDown = ({ countDown }) => {
+  const cd = useRef(countDown);
+  const timer = useRef(null);
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd.current <= 0) {
+      setTime("");
+      return timer.current && clearTimeout(timer.current);
+    }
+    const d = parseInt(cd.current / (24 * 60 * 60) + "");
+    const h = parseInt(((cd.current / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd.current / 60) % 60) + "");
+    const s = parseInt((cd.current % 60) + "");
+    setTime(\`倒计时: \${d}\${h}\${m}\${s}秒\`);
+    cd.current--;
+    timer.current = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer.current && clearTimeout(timer.current);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+
import { useState, useEffect, useRef } from "react";
+
+const CountDown = ({ countDown }) => {
+  const cd = useRef(countDown);
+  const timer = useRef(null);
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd.current <= 0) {
+      setTime("");
+      return timer.current && clearTimeout(timer.current);
+    }
+    const d = parseInt(cd.current / (24 * 60 * 60) + "");
+    const h = parseInt(((cd.current / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd.current / 60) % 60) + "");
+    const s = parseInt((cd.current % 60) + "");
+    setTime(\`倒计时: \${d}\${h}\${m}\${s}秒\`);
+    cd.current--;
+    timer.current = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer.current && clearTimeout(timer.current);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+

错误示例

jsx
import { useState, useEffect } from "react";
+
+const CountDown = ({ countDown = 5 }) => {
+  let [cd, setCd] = useState(countDown);
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd <= 0) {
+      setTime("");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(\`倒计时: \${d}\${h}\${m}\${s}秒\`);
+    setCd(cd--); // 应该是setCd(cd - 1)
+    timer = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+
import { useState, useEffect } from "react";
+
+const CountDown = ({ countDown = 5 }) => {
+  let [cd, setCd] = useState(countDown);
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd <= 0) {
+      setTime("");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(\`倒计时: \${d}\${h}\${m}\${s}秒\`);
+    setCd(cd--); // 应该是setCd(cd - 1)
+    timer = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+

错误示例会出现的问题

  • cd 在 useEffect 中始终是初始值;
  • 在 DOM 元素上,会出现先是初始值,然后从初始值减 1 ,之后就不会在变化

原因

  • 因为 useEffect 的第二个参数是 [] ,所以 re-render (更新渲染)的时候不会重新执行 effect 函数,所以 cd 在 useEffect 中始终是初始值;
  • 因为 useEffect 第一次执行的时候即初始化的时候利用 setCd(cd--) 重新对 cd 赋值,所以会触发视图重新渲染一次;
  • 如果 useEffect 没有第二个参数 [] ,既没有依赖,那么就相当于 didMount 和 DidUpdate 两个生命周期的合集,所以更新的时候会重新执行 useEffect 【注意】: useEffect 的第一个参数是一个匿名函数,匿名函数执行完会立即被销毁,所以在组件重新更新的时候,会重新调用匿名函数,并获取更改后的 cd 值

解决方案

如上示例 1 , 2 ,因为方式 1 打破了 React 纯函数的规则,所以更加建议方式 2

TIP

第一段代码使用了普通的变量 cd 和 timer,它们被定义在组件函数的顶层。这意味着每次组件函数被调用时,都会重新创建新的 cd 和 timer,而不是保留它们的状态。这样做违反了 React 纯函数组件的规则,因为它引入了外部的状态管理。

第二段代码使用了 useRef 来创建了 cd 和 timer 这两个变量的引用。这意味着它们会被保存在组件的生命周期之外,并且在组件的多次渲染之间保持不变。因此,即使组件函数被重新调用,这些引用的值也会保持不变。这样做遵守了 React 纯函数组件的规则,因为它不引入外部状态管理。

因此,第二段代码符合 React 纯函数组件的规则,而第一段代码打破了这些规则。

【注意】

  • 在倒计时为 0 的时候一定要销毁倒计时
  • 在组件销毁的时候一定要手动销毁倒计时,否则即使跳转到其他页面,倒计时依旧在进行
`,16),e=[o];function t(c,r,B,y,F,i){return n(),a("div",null,e)}const C=s(p,[["render",t]]);export{u as __pageData,C as default}; diff --git a/assets/fragment_react-hooks-timer.md.7fb4e725.lean.js b/assets/fragment_react-hooks-timer.md.7fb4e725.lean.js new file mode 100644 index 00000000..acbcf993 --- /dev/null +++ b/assets/fragment_react-hooks-timer.md.7fb4e725.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"React Hooks 实现倒计时及避坑","description":"","frontmatter":{},"headers":[{"level":2,"title":"正确示例 1","slug":"正确示例-1","link":"#正确示例-1","children":[]},{"level":2,"title":"正确示例 2","slug":"正确示例-2","link":"#正确示例-2","children":[]},{"level":2,"title":"错误示例","slug":"错误示例","link":"#错误示例","children":[]},{"level":2,"title":"错误示例会出现的问题","slug":"错误示例会出现的问题","link":"#错误示例会出现的问题","children":[]},{"level":2,"title":"原因","slug":"原因","link":"#原因","children":[]},{"level":2,"title":"解决方案","slug":"解决方案","link":"#解决方案","children":[]}],"relativePath":"fragment/react-hooks-timer.md","lastUpdated":1713167722000}'),p={name:"fragment/react-hooks-timer.md"},o=l("",16),e=[o];function t(c,r,B,y,F,i){return n(),a("div",null,e)}const C=s(p,[["render",t]]);export{u as __pageData,C as default}; diff --git a/assets/fragment_react-useState.md.cf32534b.js b/assets/fragment_react-useState.md.cf32534b.js new file mode 100644 index 00000000..5c3b1a21 --- /dev/null +++ b/assets/fragment_react-useState.md.cf32534b.js @@ -0,0 +1,355 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"react 函数组件中使用 useState 改变值后立刻获取最新值","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么说 redux 和 react-hooks 有些渊源?","slug":"为什么说-redux-和-react-hooks-有些渊源","link":"#为什么说-redux-和-react-hooks-有些渊源","children":[]},{"level":2,"title":"如何实现 useState 改变值之后立刻获取最新的状态?","slug":"如何实现-usestate-改变值之后立刻获取最新的状态","link":"#如何实现-usestate-改变值之后立刻获取最新的状态","children":[]},{"level":2,"title":"如何实现 useSyncCallback?","slug":"如何实现-usesynccallback","link":"#如何实现-usesynccallback","children":[]}],"relativePath":"fragment/react-useState.md","lastUpdated":1713161059000}'),p={name:"fragment/react-useState.md"},o=l(`

react 函数组件中使用 useState 改变值后立刻获取最新值

为什么说 redux 和 react-hooks 有些渊源?

redux 是基于发布订阅模式,在内部实现一个状态,然后加一些约定和限制,外部不可以直接改变这个状态,只能通过 redux 提供的一个代理方法去触发这个状态的改变,使用者可以通过 redux 提供的订阅方法订阅事件,当状态改变的时候,redux 帮助我们发布这个事件,使得各个订阅者获得最新状态,是不是很简单,大道至简~

下面提供了个简版的 redux 代码

js
/**
+ *
+ * @param {*} reducer 处理状态方法
+ * @param {*} preloadState 初始状态
+ */
+function createStore(reducer, preloadState) {
+  /** 存储状态 */
+  let state = preloadState;
+
+  /** 存储事件 */
+  const listeners = [];
+
+  /** 获取状态方法,返回state */
+  const getState = () => {
+    return state;
+  };
+
+  /**
+   * 订阅事件,将事件存储到listeners中
+   * @param {*} listener 事件
+   */
+  const subscribe = (listener) => {
+    listeners.push(listener);
+  };
+
+  /**
+   * 派发action,发布事件
+   * @param {*} action 动作
+   */
+  const dispatch = (action) => {
+    state = reducer(state, action);
+    listeners.forEach((listener) => listener());
+  };
+
+  /** 状态机 */
+  const store = {
+    getState,
+    subscribe,
+    dispatch,
+  };
+
+  return store;
+}
+
+export default createStore;
+
/**
+ *
+ * @param {*} reducer 处理状态方法
+ * @param {*} preloadState 初始状态
+ */
+function createStore(reducer, preloadState) {
+  /** 存储状态 */
+  let state = preloadState;
+
+  /** 存储事件 */
+  const listeners = [];
+
+  /** 获取状态方法,返回state */
+  const getState = () => {
+    return state;
+  };
+
+  /**
+   * 订阅事件,将事件存储到listeners中
+   * @param {*} listener 事件
+   */
+  const subscribe = (listener) => {
+    listeners.push(listener);
+  };
+
+  /**
+   * 派发action,发布事件
+   * @param {*} action 动作
+   */
+  const dispatch = (action) => {
+    state = reducer(state, action);
+    listeners.forEach((listener) => listener());
+  };
+
+  /** 状态机 */
+  const store = {
+    getState,
+    subscribe,
+    dispatch,
+  };
+
+  return store;
+}
+
+export default createStore;
+

为什么说 react-hooks 跟 redux 有渊源,因为 Dan 总把 redux 的这种思想带到了 react-hooks 中,通过创建一个公共 hookStates 来存储所有的 hooks,通过添加一个索引 hookIndex 来顺序执行每一个 hook,所以 react-hooks 规定 hooks 不能使用在 if 语句和 for 语句中,来保持 hooks 按顺序执行,在 react-hooks 中有一个叫 useReducer 的 hooks,正是按 redux 的这个思想实现的,而我们常用的 useState 只是 useReducer 的一个语法糖,有人可能会说这个 hookStates 会不会很大,所有的 hooks 都存在里边,毕竟一个项目中有那么多组件,我说的只是简单的实现原理,我们知道 react 在 16 的几个版本之后就加入了 fiber 的概念,在 react 内部实现中,会把每个组件的 hookStates 存放到 fiber 节点上,来维护各个节点的状态,react 每次 render 之后都会把 hookIndex 置为 0,来保证下次 hooks 执行是从第一个开始执行,每个 hooks 执行完也会把 hookIndex++,下面是 useReducer 的简单实现:

js
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} reducer 状态处理函数
+ * @param {*} initialState 初始状态,可以传入函数实现懒加载
+ * @returns
+ */
+function useReducer(reducer, initialState) {
+  hookStates[hookIndex] =
+    hookStates[hookIndex] || typeof initialState === "function"
+      ? initialState()
+      : initialState;
+
+  /**
+   *
+   * dispatch
+   * @param {*} action 动作
+   * @returns
+   */
+  const dispatch = (action) => {
+    hookStates[hookIndex] = reducer
+      ? reducer(hookStates[hookIndex], action)
+      : action;
+    /** React内部方法实现重新render */
+    scheduleUpdate();
+  };
+  return [hookStates[hookIndex++], dispatch];
+}
+
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} reducer 状态处理函数
+ * @param {*} initialState 初始状态,可以传入函数实现懒加载
+ * @returns
+ */
+function useReducer(reducer, initialState) {
+  hookStates[hookIndex] =
+    hookStates[hookIndex] || typeof initialState === "function"
+      ? initialState()
+      : initialState;
+
+  /**
+   *
+   * dispatch
+   * @param {*} action 动作
+   * @returns
+   */
+  const dispatch = (action) => {
+    hookStates[hookIndex] = reducer
+      ? reducer(hookStates[hookIndex], action)
+      : action;
+    /** React内部方法实现重新render */
+    scheduleUpdate();
+  };
+  return [hookStates[hookIndex++], dispatch];
+}
+

前文说过 useState 是 useReducer 的语法糖,所以下面是 useState 的实现

js
/**
+ *
+ * useState 直接将reducer传为null,调用useReducer并返回
+ * @param {*} initialState 初始化状态
+ * @returns
+ */
+function useState(initialState) {
+  return useReducer(null, initialState);
+}
+
/**
+ *
+ * useState 直接将reducer传为null,调用useReducer并返回
+ * @param {*} initialState 初始化状态
+ * @returns
+ */
+function useState(initialState) {
+  return useReducer(null, initialState);
+}
+

如何实现 useState 改变值之后立刻获取最新的状态?

react-hooks 的思想其实是同步逻辑,但是在 react 的合成事件中状态更新是异步的,看一下下面这个场景:

jsx
function App() {
+  const [state, setstate] = useState(0);
+
+  const setT = () => {
+    setstate(2);
+    func();
+  };
+
+  const func = () => {
+    console.log(state);
+  };
+
+  return (
+    <div className="App">
+      <header className="App-header">
+        <button onClick={setT}>set 2</button>
+      </header>
+    </div>
+  );
+}
+
function App() {
+  const [state, setstate] = useState(0);
+
+  const setT = () => {
+    setstate(2);
+    func();
+  };
+
+  const func = () => {
+    console.log(state);
+  };
+
+  return (
+    <div className="App">
+      <header className="App-header">
+        <button onClick={setT}>set 2</button>
+      </header>
+    </div>
+  );
+}
+

当我们点击 set 2 按钮时,控制台打印的还是上一次的值 0,而不是最新的 2

因为在 react 合成事件中改变状态是异步的,出于减少 render 次数,react 会收集所有状态变更,然后比对优化,最后做一次变更,在代码中可以看出,func 的调用和 setstate 在同一个宏任务中,这是 react 还没有 render,所以直接使用 state 获取的肯定是上一次闭包里的值 0

有的人可能会说,直接将最新的值当作参数传递给 func 不就行了吗,对,这也是一种解决办法,但是有时不只是一个状态,可能要传递的参数很多,再有也是出于对 react-hooks 的深入研究,所以我选择通过自定义 hooks 实现在 useState 改变值之后立刻获取到最新的值,我们先看下实现的效果,代码变更如下:

如何实现 useSyncCallback?

js
// param: callback为回调函数
+// 用法 const newFunc = useSyncCallback(yourCallback)
+import { useEffect, useState, useCallback } from "react";
+
+const useSyncCallback = (callback) => {
+  const [proxyState, setProxyState] = useState({ current: false });
+
+  const Func = useCallback(() => {
+    setProxyState({ current: true });
+  }, [proxyState]);
+
+  useEffect(() => {
+    if (proxyState.current === true) setProxyState({ current: false });
+  }, [proxyState]);
+
+  useEffect(() => {
+    proxyState.current && callback();
+  });
+
+  return Func;
+};
+
+export default useSyncCallback;
+
// param: callback为回调函数
+// 用法 const newFunc = useSyncCallback(yourCallback)
+import { useEffect, useState, useCallback } from "react";
+
+const useSyncCallback = (callback) => {
+  const [proxyState, setProxyState] = useState({ current: false });
+
+  const Func = useCallback(() => {
+    setProxyState({ current: true });
+  }, [proxyState]);
+
+  useEffect(() => {
+    if (proxyState.current === true) setProxyState({ current: false });
+  }, [proxyState]);
+
+  useEffect(() => {
+    proxyState.current && callback();
+  });
+
+  return Func;
+};
+
+export default useSyncCallback;
+

在这里还需要介绍一个知识点 useEffect,useEffect 会在每次函数式组件 render 之后执行,可以通过传递第二个参数,配置依赖项,当依赖项变更 useEffect 才会更新,如果第二个参数传递一个空数组[],那么 useEffect 就会只执行一次,react-hooks 正是使用 useEffect 来模拟 componentDidMount 和 componentDidUpdate 两个生命周期,也可以通过在 useEffect 中 return 一个函数模拟 componentWillUnmount,那么 useEffect 是如何实现在 render 之后调用的呢,原理就是在 useEffect hooks 中会封装一个宏任务,然后把传进来的回调函数放到宏任务中去执行,下面实现了一个简版的 useEffect:

js
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} callback 副作用函数
+ * @param {*} dependencies 依赖项
+ * @returns
+ */
+function useEffect(callback, dependencies) {
+  /** 判断当前索引下hookStates是否存在值 */
+  if (hookStates[hookIndex]) {
+    /** 如果存在就取出,并结构出destroyFunc销毁函数,和上一次依赖项 */
+    const [destroyFunc, lastDep] = hookStates[hookIndex];
+    /** 判断当前依赖项中的值和上次依赖项中的值有没有变化 */
+    const isSame =
+      dependencies &&
+      dependencies.every((dep, index) => dep === lastDep[index]);
+    if (isSame) {
+      /** 如果没有变化就把索引加一,hooks向后遍历 */
+      hookIndex++;
+    } else {
+      /** 如果有变化,并且存在销毁函数,就先调用销毁函数 */
+      destroyFunc && destroyFunc();
+      /** 创建一个新的宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+      setTimeout(() => {
+        const destroyFunction = callback();
+        hookStates[hookIndex++] = [destroyFunction, dependencies];
+      });
+    }
+  } else {
+    /** 如果hookStates中不存在,证明是首次执行,直接创建一个宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+    setTimeout(() => {
+      const destroyFunction = callback();
+      hookStates[hookIndex++] = [destroyFunction, dependencies];
+    });
+  }
+}
+
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} callback 副作用函数
+ * @param {*} dependencies 依赖项
+ * @returns
+ */
+function useEffect(callback, dependencies) {
+  /** 判断当前索引下hookStates是否存在值 */
+  if (hookStates[hookIndex]) {
+    /** 如果存在就取出,并结构出destroyFunc销毁函数,和上一次依赖项 */
+    const [destroyFunc, lastDep] = hookStates[hookIndex];
+    /** 判断当前依赖项中的值和上次依赖项中的值有没有变化 */
+    const isSame =
+      dependencies &&
+      dependencies.every((dep, index) => dep === lastDep[index]);
+    if (isSame) {
+      /** 如果没有变化就把索引加一,hooks向后遍历 */
+      hookIndex++;
+    } else {
+      /** 如果有变化,并且存在销毁函数,就先调用销毁函数 */
+      destroyFunc && destroyFunc();
+      /** 创建一个新的宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+      setTimeout(() => {
+        const destroyFunction = callback();
+        hookStates[hookIndex++] = [destroyFunction, dependencies];
+      });
+    }
+  } else {
+    /** 如果hookStates中不存在,证明是首次执行,直接创建一个宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+    setTimeout(() => {
+      const destroyFunction = callback();
+      hookStates[hookIndex++] = [destroyFunction, dependencies];
+    });
+  }
+}
+

接下来我们就来使用 useEffect 实现我们想要的结果,因为 useEffect 是在 react 组件 render 之后才会执行,所以在 useEffect 获取的状态一定是最新的,所以利用这一点,把我们写的函数放到 useEffect 执行,函数里获取的状态就一定是最新的

js
useEffect(() => {
+  proxyState.current && callback();
+});
+
useEffect(() => {
+  proxyState.current && callback();
+});
+

首先,在 useSyncCallback 中创建一个标示 proxyState,初始的时候会把 proxyState 的 current 值赋成 false,在 callback 执行之前会先判断 current 是否为 true,如果为 true 就允许 callback 执行,若果为 false,就跳过不执行,因为 useEffect 在组件 render 之后,只要依赖项有变化就会执行,所以我们无法掌控我们的函数执行,在 useSyncCallback 中创建一个新的函数 Func,并返回,通过这个 Func 来模拟函数调用,

js
const [proxyState, setProxyState] = useState({ current: false });
+
const [proxyState, setProxyState] = useState({ current: false });
+

在这个函数中我们要做的就是变更 prxoyState 的 current 值为 true,来使得让 callback 被调用的条件成立,同时触发 react 组件 render 这样内部的 useEffect 就会执行,随后调用 callback 实现我们想要的效果。

`,24),e=[o];function t(c,r,B,y,i,F){return n(),a("div",null,e)}const A=s(p,[["render",t]]);export{b as __pageData,A as default}; diff --git a/assets/fragment_react-useState.md.cf32534b.lean.js b/assets/fragment_react-useState.md.cf32534b.lean.js new file mode 100644 index 00000000..98923f0e --- /dev/null +++ b/assets/fragment_react-useState.md.cf32534b.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const b=JSON.parse('{"title":"react 函数组件中使用 useState 改变值后立刻获取最新值","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么说 redux 和 react-hooks 有些渊源?","slug":"为什么说-redux-和-react-hooks-有些渊源","link":"#为什么说-redux-和-react-hooks-有些渊源","children":[]},{"level":2,"title":"如何实现 useState 改变值之后立刻获取最新的状态?","slug":"如何实现-usestate-改变值之后立刻获取最新的状态","link":"#如何实现-usestate-改变值之后立刻获取最新的状态","children":[]},{"level":2,"title":"如何实现 useSyncCallback?","slug":"如何实现-usesynccallback","link":"#如何实现-usesynccallback","children":[]}],"relativePath":"fragment/react-useState.md","lastUpdated":1713161059000}'),p={name:"fragment/react-useState.md"},o=l("",24),e=[o];function t(c,r,B,y,i,F){return n(),a("div",null,e)}const A=s(p,[["render",t]]);export{b as __pageData,A as default}; diff --git a/assets/fragment_return-await.md.21a919eb.js b/assets/fragment_return-await.md.21a919eb.js new file mode 100644 index 00000000..5b47f901 --- /dev/null +++ b/assets/fragment_return-await.md.21a919eb.js @@ -0,0 +1,85 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const d=JSON.parse('{"title":"思考为什么不要使用 return await","description":"","frontmatter":{},"headers":[{"level":2,"title":"Promise.resolve","slug":"promise-resolve","link":"#promise-resolve","children":[]},{"level":2,"title":"问题分析","slug":"问题分析","link":"#问题分析","children":[]},{"level":2,"title":"推荐写法","slug":"推荐写法","link":"#推荐写法","children":[]},{"level":2,"title":"return await promiseValue 与 return promiseValue 的比较","slug":"return-await-promisevalue-与-return-promisevalue-的比较","link":"#return-await-promisevalue-与-return-promisevalue-的比较","children":[]}],"relativePath":"fragment/return-await.md","lastUpdated":1713169443000}'),p={name:"fragment/return-await.md"},e=l(`

思考为什么不要使用 return await

一句话概括:因为 async 函数的返回值一定为 promise 包裹,所以即使你 return await promise,await 出来的值依旧要包裹一层 promise,所以 await 等于白 await 了

stackoverflow

在 ESLint 中有一条规则 no-return-await 。下面的代码会触发该条规则校验不通过:

js
async function foo() {
+  return await bar();
+}
+
async function foo() {
+  return await bar();
+}
+

这条规则的解释是, async function 的返回值总是封装在 Promise.resolve 中 , return await 实际上并没有做任何事情,只是在 Promise resolve 或 reject 之前增加了额外的时间。

Promise.resolve

Promise.resolve() 作用是将给定的参数转换为 Promise 对象

js
Promise.resolve("foo");
+// 等价于
+new Promise((resolve) => resolve("foo"));
+
Promise.resolve("foo");
+// 等价于
+new Promise((resolve) => resolve("foo"));
+

那么其实上面的问题所在就是 Promise.resolve() 的参数情况:《 ECMAScript 6 入门 》的 Promise.resolve()

问题分析

函数 foo() 是一个 async function ,会返回一个 Promise 对象,这个 Promise 对象其实是将函数内 return 语句后面的值使用 Promise.resolve 封装后返回。所以,执行 foo() 之后,我们明确知道得到的结果是一个 Promise 对象,并且在 foo() 的外部我们还会以某种方式比如 await 来继续使用这个结果。 另外,我们知道 await 命令后面如果是一个 Promise 对象,则返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。所以函数内 return 后的 await 的作用就是获取 bar() 的执行结果。 如此一来, async function 中使用 return await 相当于先获取结果然后又将结果变成 Promise 实例。 无论 return await 后面跟的是不是 Promise 对象,那么我们在函数外面都会得到一个 Promise 对象,所以函数内 return 后面的 await 就显得多余了。

虽然上面的写法不推荐使用,但是并不会产生运行错误,只是会造成性能上的损失。唯一影响,可能是其他开发者看到后,会笑话你基础不扎实吧。

推荐写法

首先,如果 async function 没有 return 语句的话,默认返回 undefined ,在 Devtool 中测试如下:

js
async function foo() {}
+foo();
+// Promise {<fulfilled>: undefined}
+//  [[PromiseState]]: "fulfilled"
+//  [[PromiseResult]]: undefined
+
async function foo() {}
+foo();
+// Promise {<fulfilled>: undefined}
+//  [[PromiseState]]: "fulfilled"
+//  [[PromiseResult]]: undefined
+

既然 return await 中的 await 是多余的,那么最直接的推荐写法就是去掉 await :

js
async function foo() {
+  return bar();
+}
+
async function foo() {
+  return bar();
+}
+

无论 bar() 执行结果是一个 Promise 还是一个普通值,我们将它交给 foo 函数外面的程序去处理。如果非要在 foo 函数中求得 bar() 的执行结果,那么也可以像下面这样写:

js
async function foo() {
+  const x = await bar();
+  return x;
+}
+
async function foo() {
+  const x = await bar();
+  return x;
+}
+

但是我觉得这完全只是为了规避 ESLint 的报错而写的一种障眼法,实际它 和 return await bar() 一样 没有任何好处。

return await promiseValue 与 return promiseValue 的比较

返回值隐式的传递给 Promise.resolve ,并不意味着 return await promiseValue; 和 return promiseValue; 在功能上相同。 return foo; 和 return await foo; ,有一些细微的差异:

  • return foo; 不管 foo 是 promise 还是 rejects 都将会直接返回 foo
  • 相反地,如果 foo 是一个 Promise , return await foo; 将等待 foo 执行(resolve)或拒绝(reject),如果是拒绝,将会在返回前抛出异常
js
function bar() {
+  throw new Error("报错了!");
+}
+
+async function foo() {
+  try {
+    return await bar();
+  } catch (error) {
+    console.log("知道了");
+  }
+}
+foo();
+// 知道了
+// Promise {<fulfilled>: undefined}
+
+async function foo2() {
+  try {
+    return bar();
+  } catch (error) {
+    console.log("我不知道");
+  }
+}
+foo2();
+// undefined
+
function bar() {
+  throw new Error("报错了!");
+}
+
+async function foo() {
+  try {
+    return await bar();
+  } catch (error) {
+    console.log("知道了");
+  }
+}
+foo();
+// 知道了
+// Promise {<fulfilled>: undefined}
+
+async function foo2() {
+  try {
+    return bar();
+  } catch (error) {
+    console.log("我不知道");
+  }
+}
+foo2();
+// undefined
+

如果想要在调试的堆栈中得到 bar() 抛出的错误信息,那么此时应该使用 return await 。

`,26),o=[e];function r(c,t,i,B,y,F){return n(),a("div",null,o)}const b=s(p,[["render",r]]);export{d as __pageData,b as default}; diff --git a/assets/fragment_return-await.md.21a919eb.lean.js b/assets/fragment_return-await.md.21a919eb.lean.js new file mode 100644 index 00000000..4659ebd6 --- /dev/null +++ b/assets/fragment_return-await.md.21a919eb.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const d=JSON.parse('{"title":"思考为什么不要使用 return await","description":"","frontmatter":{},"headers":[{"level":2,"title":"Promise.resolve","slug":"promise-resolve","link":"#promise-resolve","children":[]},{"level":2,"title":"问题分析","slug":"问题分析","link":"#问题分析","children":[]},{"level":2,"title":"推荐写法","slug":"推荐写法","link":"#推荐写法","children":[]},{"level":2,"title":"return await promiseValue 与 return promiseValue 的比较","slug":"return-await-promisevalue-与-return-promisevalue-的比较","link":"#return-await-promisevalue-与-return-promisevalue-的比较","children":[]}],"relativePath":"fragment/return-await.md","lastUpdated":1713169443000}'),p={name:"fragment/return-await.md"},e=l("",26),o=[e];function r(c,t,i,B,y,F){return n(),a("div",null,o)}const b=s(p,[["render",r]]);export{d as __pageData,b as default}; diff --git a/assets/fragment_setTimeout.md.dadee543.js b/assets/fragment_setTimeout.md.dadee543.js new file mode 100644 index 00000000..42618347 --- /dev/null +++ b/assets/fragment_setTimeout.md.dadee543.js @@ -0,0 +1,93 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-09-16-41-09.9e776372.png",o="/blog/assets/2024-04-09-16-41-29.01db7dfd.png",d=JSON.parse('{"title":"如何实现准时的 setTimeout","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么不准?","slug":"为什么不准","link":"#为什么不准","children":[]},{"level":2,"title":"如何实现准时的 “setTimeout”","slug":"如何实现准时的-settimeout-1","link":"#如何实现准时的-settimeout-1","children":[{"level":3,"title":"requestAnimationFrame","slug":"requestanimationframe","link":"#requestanimationframe","children":[]},{"level":3,"title":"while","slug":"while","link":"#while","children":[]},{"level":3,"title":"setTimeout 系统时间补偿","slug":"settimeout-系统时间补偿","link":"#settimeout-系统时间补偿","children":[]}]}],"relativePath":"fragment/setTimeout.md","lastUpdated":1712654359000}'),e={name:"fragment/setTimeout.md"},t=l(`

如何实现准时的 setTimeout

为什么不准?

因为 setTimeout 是一个宏任务,它的指定时间指的是: 进入主线程的时间。

js
setTimeout(callback, 进入主线程的时间);
+
setTimeout(callback, 进入主线程的时间);
+

所以什么时候可以执行 callback,需要看 主线程前面还有多少任务待执行 。

如何实现准时的 “setTimeout”

requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。

该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,回调函数执行次数通常是每秒 60 次,也就是每 16.7ms 执行一次,但是并不一定保证为 16.7 ms。

js
// 模拟代码
+function setTimeout(cb, delay) {
+  let startTime = Date.now();
+  loop();
+
+  function loop() {
+    const now = Date.now();
+    if (now - startTime >= delay) {
+      cb();
+      return;
+    }
+    requestAnimationFrame(loop);
+  }
+}
+
// 模拟代码
+function setTimeout(cb, delay) {
+  let startTime = Date.now();
+  loop();
+
+  function loop() {
+    const now = Date.now();
+    if (now - startTime >= delay) {
+      cb();
+      return;
+    }
+    requestAnimationFrame(loop);
+  }
+}
+

由于 16.7 ms 间隔执行,在使用间隔很小的定时器,很容易导致时间的不准确。因此这种方案仍然不是一种好的方案。

while

想得到准确的,我们第一反应就是如果我们能够主动去触发,获取到最开始的时间,以及不断去轮询当前时间,如果差值是预期的时间,那么这个定时器肯定是准确的,那么用 while 可以实现这个功能。

js
function timer(time) {
+  const startTime = Date.now();
+  while (true) {
+    const now = Date.now();
+    if (now - startTime >= time) {
+      console.log("误差", now - startTime - time);
+      return;
+    }
+  }
+}
+timer(5000);
+
function timer(time) {
+  const startTime = Date.now();
+  while (true) {
+    const now = Date.now();
+    if (now - startTime >= time) {
+      console.log("误差", now - startTime - time);
+      return;
+    }
+  }
+}
+timer(5000);
+

这样的方式很精确,但是我们知道 js 是单线程运行,使用这样的方式强行霸占线程会使得页面进入卡死状态,这样的结果显然是不合适的。

setTimeout 系统时间补偿

我们来看看此方案和原方案的区别

原方案:

setTimeout 系统时间补偿:

当每一次定时器执行时后,都去获取系统的时间来进行修正,虽然每次运行可能会有误差,但是通过系统时间对每次运行的修复,能够让后面每一次时间都得到一个补偿。

js
function timer() {
+  let speed = 500,
+    counter = 1,
+    start = new Date().getTime();
+
+  function instance() {
+    let real = counter * speed,
+      ideal = new Date().getTime() - start;
+
+    counter++;
+    let diff = ideal - real;
+    setTimeout(function () {
+      instance();
+    }, speed - diff); // 通过系统时间进行修复
+  }
+
+  setTimeout(function () {
+    instance();
+  }, speed);
+}
+
function timer() {
+  let speed = 500,
+    counter = 1,
+    start = new Date().getTime();
+
+  function instance() {
+    let real = counter * speed,
+      ideal = new Date().getTime() - start;
+
+    counter++;
+    let diff = ideal - real;
+    setTimeout(function () {
+      instance();
+    }, speed - diff); // 通过系统时间进行修复
+  }
+
+  setTimeout(function () {
+    instance();
+  }, speed);
+}
+
`,23),c=[t];function r(B,y,i,F,A,u){return n(),a("div",null,c)}const b=s(e,[["render",r]]);export{d as __pageData,b as default}; diff --git a/assets/fragment_setTimeout.md.dadee543.lean.js b/assets/fragment_setTimeout.md.dadee543.lean.js new file mode 100644 index 00000000..83c05954 --- /dev/null +++ b/assets/fragment_setTimeout.md.dadee543.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-09-16-41-09.9e776372.png",o="/blog/assets/2024-04-09-16-41-29.01db7dfd.png",d=JSON.parse('{"title":"如何实现准时的 setTimeout","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么不准?","slug":"为什么不准","link":"#为什么不准","children":[]},{"level":2,"title":"如何实现准时的 “setTimeout”","slug":"如何实现准时的-settimeout-1","link":"#如何实现准时的-settimeout-1","children":[{"level":3,"title":"requestAnimationFrame","slug":"requestanimationframe","link":"#requestanimationframe","children":[]},{"level":3,"title":"while","slug":"while","link":"#while","children":[]},{"level":3,"title":"setTimeout 系统时间补偿","slug":"settimeout-系统时间补偿","link":"#settimeout-系统时间补偿","children":[]}]}],"relativePath":"fragment/setTimeout.md","lastUpdated":1712654359000}'),e={name:"fragment/setTimeout.md"},t=l("",23),c=[t];function r(B,y,i,F,A,u){return n(),a("div",null,c)}const b=s(e,[["render",r]]);export{d as __pageData,b as default}; diff --git a/assets/fragment_tree-shaking.md.8743095c.js b/assets/fragment_tree-shaking.md.8743095c.js new file mode 100644 index 00000000..411f88e1 --- /dev/null +++ b/assets/fragment_tree-shaking.md.8743095c.js @@ -0,0 +1 @@ +import{_ as r,o as a,c as n,b as e,d as t}from"./app.f983686f.js";const k=JSON.parse('{"title":"面试官:tree-shaking 的原理是什么?","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/tree-shaking.md","lastUpdated":1713241045000}'),s={name:"fragment/tree-shaking.md"},o=e("h1",{id:"面试官-tree-shaking-的原理是什么",tabindex:"-1"},[t("面试官:tree-shaking 的原理是什么? "),e("a",{class:"header-anchor",href:"#面试官-tree-shaking-的原理是什么","aria-hidden":"true"},"#")],-1),c=e("p",null,[t("文章地址:"),e("a",{href:"https://juejin.cn/post/7265125368553685050",target:"_blank",rel:"noreferrer"},"https://juejin.cn/post/7265125368553685050")],-1),h=e("p",null,[t("代码实现:"),e("a",{href:"https://github.com/Sunny-117/cherry",target:"_blank",rel:"noreferrer"},"https://github.com/Sunny-117/cherry")],-1),i=[o,c,h];function d(_,p,l,f,g,u){return a(),n("div",null,i)}const b=r(s,[["render",d]]);export{k as __pageData,b as default}; diff --git a/assets/fragment_tree-shaking.md.8743095c.lean.js b/assets/fragment_tree-shaking.md.8743095c.lean.js new file mode 100644 index 00000000..411f88e1 --- /dev/null +++ b/assets/fragment_tree-shaking.md.8743095c.lean.js @@ -0,0 +1 @@ +import{_ as r,o as a,c as n,b as e,d as t}from"./app.f983686f.js";const k=JSON.parse('{"title":"面试官:tree-shaking 的原理是什么?","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/tree-shaking.md","lastUpdated":1713241045000}'),s={name:"fragment/tree-shaking.md"},o=e("h1",{id:"面试官-tree-shaking-的原理是什么",tabindex:"-1"},[t("面试官:tree-shaking 的原理是什么? "),e("a",{class:"header-anchor",href:"#面试官-tree-shaking-的原理是什么","aria-hidden":"true"},"#")],-1),c=e("p",null,[t("文章地址:"),e("a",{href:"https://juejin.cn/post/7265125368553685050",target:"_blank",rel:"noreferrer"},"https://juejin.cn/post/7265125368553685050")],-1),h=e("p",null,[t("代码实现:"),e("a",{href:"https://github.com/Sunny-117/cherry",target:"_blank",rel:"noreferrer"},"https://github.com/Sunny-117/cherry")],-1),i=[o,c,h];function d(_,p,l,f,g,u){return a(),n("div",null,i)}const b=r(s,[["render",d]]);export{k as __pageData,b as default}; diff --git a/assets/fragment_useRequest.md.2d50c7c0.js b/assets/fragment_useRequest.md.2d50c7c0.js new file mode 100644 index 00000000..57c0623f --- /dev/null +++ b/assets/fragment_useRequest.md.2d50c7c0.js @@ -0,0 +1,253 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"使用 react 的 hook 实现一个 useRequest","description":"","frontmatter":{},"headers":[{"level":2,"title":"基础结构","slug":"基础结构","link":"#基础结构","children":[]},{"level":2,"title":"具体的实现","slug":"具体的实现","link":"#具体的实现","children":[{"level":3,"title":"2.1 不带 config 配置的","slug":"_2-1-不带-config-配置的","link":"#_2-1-不带-config-配置的","children":[]},{"level":3,"title":"2.2 添加取消请求","slug":"_2-2-添加取消请求","link":"#_2-2-添加取消请求","children":[]},{"level":3,"title":"2.3 带有 config 配置的","slug":"_2-3-带有-config-配置的","link":"#_2-3-带有-config-配置的","children":[]}]}],"relativePath":"fragment/useRequest.md","lastUpdated":1713165048000}'),p={name:"fragment/useRequest.md"},o=l(`

使用 react 的 hook 实现一个 useRequest

我们在使用 react 开发前端应用时,不可避免地要进行数据请求,而在数据请求的前后都要执行很多的处理,例如

  1. 展示 loading 效果告诉用户后台正在处理请求;
  2. 若要写 async-await,每次都要 try-catch 进行包裹;
  3. 接口异常时要进行错误处理;

当请求比较多时,每次都要重复这样的操作。这里我们可以利用 react 提供的 hook,自己来封装一个 useRequest 。

基础结构

明确 useRequest 的输入和输出。

要输入的数据:

  • url:要请求的接口地址;
  • data:请求的数据(默认只有 get 方式);
  • config:其他一些配置,可选,如(manual?: boolean; // 是否需要手动触发);

要返回的数据:

  • loading:数据是否在请求中;
  • data:接口返回的数据;
  • error:接口异常时的错误;

具体的实现

2.1 不带 config 配置的

js
const useRequest = (url, data, config) => {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  const request = async () => {
+    setLoading(true);
+    try {
+      const result = await axios({
+        url,
+        params: data,
+        method: "get",
+      });
+      if (result && result.status >= 200 && result.status <= 304) {
+        setResult(result.data);
+      } else {
+        setError(new Error("get data error in useRequest"));
+      }
+    } catch (reason) {
+      setError(reason);
+    }
+    setLoading(false);
+  };
+  useEffect(() => {
+    request();
+  }, []);
+
+  return {
+    loading,
+    result,
+    error,
+  };
+};
+
const useRequest = (url, data, config) => {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  const request = async () => {
+    setLoading(true);
+    try {
+      const result = await axios({
+        url,
+        params: data,
+        method: "get",
+      });
+      if (result && result.status >= 200 && result.status <= 304) {
+        setResult(result.data);
+      } else {
+        setError(new Error("get data error in useRequest"));
+      }
+    } catch (reason) {
+      setError(reason);
+    }
+    setLoading(false);
+  };
+  useEffect(() => {
+    request();
+  }, []);
+
+  return {
+    loading,
+    result,
+    error,
+  };
+};
+

在我们不考虑第三个配置 config 的情况下,这个 useRequest 就已经可以使用了。

js
const App = () => {
+  const { loading, result, error } = useRequest(url);
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+    </div>
+  );
+};
+
const App = () => {
+  const { loading, result, error } = useRequest(url);
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+    </div>
+  );
+};
+

2.2 添加取消请求

我们在请求接口过程中,可能接口还没没有返回到数据,组件就已经被销毁了,因此我们还要添加上取消请求的操作,避免操作已经不存在的组件。关于 axios 如何取消请求,您可以查看之前写的一篇文章: axios 源码系列之如何取消请求

js
const request = useCallback(() => {
+  setLoading(true);
+  const CancelToken = axios.CancelToken;
+  const source = CancelToken.source();
+
+  axios({
+    url,
+    params: data,
+    method: "get",
+    cancelToken: source.token, // 将token注入到请求中
+  })
+    .then((result) => {
+      setResult(result.data);
+      setLoading(false);
+    })
+    .catch((thrown) => {
+      // 只有在非取消的请求时,才调用setError和setLoading
+      // 否则会报组件已被卸载还会调用方法的错误
+      if (!axios.isCancel(thrown)) {
+        setError(thrown);
+        setLoading(false);
+      }
+    });
+
+  return source;
+}, [url, data]);
+
+useEffect(() => {
+  const source = request();
+  return () => source.cancel("Operation canceled by the user.");
+}, [request]);
+
const request = useCallback(() => {
+  setLoading(true);
+  const CancelToken = axios.CancelToken;
+  const source = CancelToken.source();
+
+  axios({
+    url,
+    params: data,
+    method: "get",
+    cancelToken: source.token, // 将token注入到请求中
+  })
+    .then((result) => {
+      setResult(result.data);
+      setLoading(false);
+    })
+    .catch((thrown) => {
+      // 只有在非取消的请求时,才调用setError和setLoading
+      // 否则会报组件已被卸载还会调用方法的错误
+      if (!axios.isCancel(thrown)) {
+        setError(thrown);
+        setLoading(false);
+      }
+    });
+
+  return source;
+}, [url, data]);
+
+useEffect(() => {
+  const source = request();
+  return () => source.cancel("Operation canceled by the user.");
+}, [request]);
+

2.3 带有 config 配置的

然后,接着我们就要考虑把配置加上了,在上面的调用中,只要调用了 useRequest,就会马上触发。但若在符合某种情况下才触发怎办呢(例如用户点击后才产生接口请求)? 这里我们就需要再加上一个配置。

js
{
+  "manual": false, // 是否需要手动触发,若为false则立刻产生请求,若为true
+  "ready": false // 当manual为true时生效,为true时才产生请求
+}
+
{
+  "manual": false, // 是否需要手动触发,若为false则立刻产生请求,若为true
+  "ready": false // 当manual为true时生效,为true时才产生请求
+}
+

这里我们就要考虑 useRequest 中触发 reqeust 请求的条件了:

  • 没有 config 配置,或者有 config 且 manual 为 false;
  • 有 config 配置,且 manual 和 ready 均为 true;
js
const useRequest = () => {
+  // 其他代码保持不变
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && config.ready)) {
+      const source = request();
+      return () => source.cancel("Operation canceled by the user.");
+    }
+  }, [config]);
+};
+
const useRequest = () => {
+  // 其他代码保持不变
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && config.ready)) {
+      const source = request();
+      return () => source.cancel("Operation canceled by the user.");
+    }
+  }, [config]);
+};
+

使用方法

js
const App = () => {
+  const [ready, setReady] = useState(false);
+  const { loading, result, error } = useRequest(url, null, {
+    manual: true,
+    ready,
+  });
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+      <button onClick={() => setReady(true)}>产生请求</button>
+    </div>
+  );
+};
+
const App = () => {
+  const [ready, setReady] = useState(false);
+  const { loading, result, error } = useRequest(url, null, {
+    manual: true,
+    ready,
+  });
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+      <button onClick={() => setReady(true)}>产生请求</button>
+    </div>
+  );
+};
+

当然,实现手动触发的方式有很多,我在这里是用一个 config.ready 来触发,在 umi 框架中,是对外返回了一个 run 函数,然后执行 run 函数再来触发这个 useRequest 的执行。

js
const useRequest = () => {
+  // 其他代码保持不变,并暂时忽略
+  const [ready, setReady] = useState(false);
+
+  const run = (r: boolean) => {
+    setReady(r);
+  };
+
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && ready)) {
+      if (loading) {
+        return;
+      }
+      setLoading(true);
+      const source = request();
+
+      return () => {
+        setLoading(false);
+        setReady(false);
+        source.cancel("Operation canceled by the user.");
+      };
+    }
+  }, [config, ready]);
+};
+
const useRequest = () => {
+  // 其他代码保持不变,并暂时忽略
+  const [ready, setReady] = useState(false);
+
+  const run = (r: boolean) => {
+    setReady(r);
+  };
+
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && ready)) {
+      if (loading) {
+        return;
+      }
+      setLoading(true);
+      const source = request();
+
+      return () => {
+        setLoading(false);
+        setReady(false);
+        source.cancel("Operation canceled by the user.");
+      };
+    }
+  }, [config, ready]);
+};
+
`,28),e=[o];function c(r,B,t,y,F,i){return n(),a("div",null,e)}const b=s(p,[["render",c]]);export{u as __pageData,b as default}; diff --git a/assets/fragment_useRequest.md.2d50c7c0.lean.js b/assets/fragment_useRequest.md.2d50c7c0.lean.js new file mode 100644 index 00000000..d3d16a51 --- /dev/null +++ b/assets/fragment_useRequest.md.2d50c7c0.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"使用 react 的 hook 实现一个 useRequest","description":"","frontmatter":{},"headers":[{"level":2,"title":"基础结构","slug":"基础结构","link":"#基础结构","children":[]},{"level":2,"title":"具体的实现","slug":"具体的实现","link":"#具体的实现","children":[{"level":3,"title":"2.1 不带 config 配置的","slug":"_2-1-不带-config-配置的","link":"#_2-1-不带-config-配置的","children":[]},{"level":3,"title":"2.2 添加取消请求","slug":"_2-2-添加取消请求","link":"#_2-2-添加取消请求","children":[]},{"level":3,"title":"2.3 带有 config 配置的","slug":"_2-3-带有-config-配置的","link":"#_2-3-带有-config-配置的","children":[]}]}],"relativePath":"fragment/useRequest.md","lastUpdated":1713165048000}'),p={name:"fragment/useRequest.md"},o=l("",28),e=[o];function c(r,B,t,y,F,i){return n(),a("div",null,e)}const b=s(p,[["render",c]]);export{u as __pageData,b as default}; diff --git a/assets/fragment_var-array.md.e64e0b6d.js b/assets/fragment_var-array.md.e64e0b6d.js new file mode 100644 index 00000000..e0387483 --- /dev/null +++ b/assets/fragment_var-array.md.e64e0b6d.js @@ -0,0 +1,57 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const A=JSON.parse('{"title":"如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/var-array.md","lastUpdated":1715076847000}'),p={name:"fragment/var-array.md"},o=l(`

如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?

在 JavaScript 中,解构赋值语法的左侧是一个数组,而右侧则应该是一个具有迭代器接口的对象(如数组、Map、Set 等)。因此,将对象 {a: 1, b: 2} 解构赋值给 [a, b] 会导致语法错误

我们首先来看看报错是什么样的:

var [a, b] = {a: 1, b: 2} TypeError: {(intermediate value)(intermediate value)} is not iterable

这个错误是个类型错误,并且是对象有问题,因为对象是一个不具备迭代器属性的数据结构。所以我们可以知道,这个面试题就是考验我们对于迭代器属性的认识,我们再来个场景加深下理解。

js
let arr = [1, 2, 3];
+let obj = {
+  a: 1,
+  b: 2,
+  c: 3,
+};
+for (let item of arr) {
+  console.log(item);
+}
+for (let item of obj) {
+  console.log(item);
+}
+
let arr = [1, 2, 3];
+let obj = {
+  a: 1,
+  b: 2,
+  c: 3,
+};
+for (let item of arr) {
+  console.log(item);
+}
+for (let item of obj) {
+  console.log(item);
+}
+

我们知道 for of 只能遍历具有迭代器属性的,在遍历数组的时候会打印出 1 2 3 ,遍历对象时会报这样的一个错误 TypeError: obj is not iterable。 我们可以在最下面发现,数组原型上有 Symbol.iterator 这样一个属性,这个属性显然是从 Array 身上继承到的,并且这个属性的值是一个函数体,如果我们调用一下这个函数体会怎么样?我们打印来看看

js
console.log(arr.__proto__[Symbol.iterator]());
+// Object [Array Iterator] {}
+
console.log(arr.__proto__[Symbol.iterator]());
+// Object [Array Iterator] {}
+

最重要的点来了 🔥🔥🔥🔥

它返回的是一个对象类型,并且是一个迭代器对象!!!所以一个可迭代对象的基本结构是这样的:

js
interable
+{
+    [Symbol.iterator]: function () {
+        return 迭代器 (可通过next()就能读取到值)
+    }
+}
+
interable
+{
+    [Symbol.iterator]: function () {
+        return 迭代器 (可通过next()就能读取到值)
+    }
+}
+

我们可以得出只要一个数据结构身上,具有 [Symbol.iterator] 这样一个属性,且值是一个函数体,可以返回一个迭代器的话,我们就称这个数据结构是可迭代的。 这时候我们回到面试题之中,面试官要我们让 var [a, b] = {a: 1, b: 2} 这个等式成立,那么有了上面的铺垫,我们可以知道,我们接下来的操作就是:人为的为对象打造一个迭代器出来,也就是让对象的隐式原型可以继承到迭代器属性,我们可以先这样做:

js
Object.prototype[Symbol.iterator] = function () {};
+
+var [a, b] = { a: 1, b: 2 };
+console.log(a, b);
+
Object.prototype[Symbol.iterator] = function () {};
+
+var [a, b] = { a: 1, b: 2 };
+console.log(a, b);
+

这样的话,报错就改变了,变成:

TypeError: Result of the Symbol.iterator method is not an object

接下来,我们知道 var [a, b] = [1, 2] 这是肯定没有问题的,所以我们可以将对象身上的迭代器,打造成和数组身上的迭代器( arr[Symbol.iterator] )一样,代码如下:

js
Object.prototype[Symbol.iterator] = function () {
+  // 使用 Object.values(this) 方法获取对象的所有值,并返回这些值的迭代器对象
+  return Object.values(this)[Symbol.iterator]();
+};
+
Object.prototype[Symbol.iterator] = function () {
+  // 使用 Object.values(this) 方法获取对象的所有值,并返回这些值的迭代器对象
+  return Object.values(this)[Symbol.iterator]();
+};
+

这段代码是将 Object.prototype 上的 [Symbol.iterator] 方法重新定义为一个新的函数。新的函数通过调用 Object.values(this) 方法获取对象的所有值,并返回这些值的迭代器对象。 通过这个代码,我们可以使得任何 JavaScript 对象都具有了迭代能力。例如,对于一个对象 obj ,我们可以直接使用 for...of 循环或者 ... 操作符来遍历它的所有值。

`,18),e=[o];function r(c,t,B,y,F,i){return a(),n("div",null,e)}const d=s(p,[["render",r]]);export{A as __pageData,d as default}; diff --git a/assets/fragment_var-array.md.e64e0b6d.lean.js b/assets/fragment_var-array.md.e64e0b6d.lean.js new file mode 100644 index 00000000..ab785799 --- /dev/null +++ b/assets/fragment_var-array.md.e64e0b6d.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const A=JSON.parse('{"title":"如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/var-array.md","lastUpdated":1715076847000}'),p={name:"fragment/var-array.md"},o=l("",18),e=[o];function r(c,t,B,y,F,i){return a(),n("div",null,e)}const d=s(p,[["render",r]]);export{A as __pageData,d as default}; diff --git a/assets/fragment_video.md.a2bb9154.js b/assets/fragment_video.md.a2bb9154.js new file mode 100644 index 00000000..fab70997 --- /dev/null +++ b/assets/fragment_video.md.a2bb9154.js @@ -0,0 +1,515 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-15-14-51-24.69678060.png",b=JSON.parse('{"title":"前端录制回放系统初体验","description":"","frontmatter":{},"headers":[{"level":2,"title":"问题背景","slug":"问题背景","link":"#问题背景","children":[{"level":3,"title":"什么是前端录制回放?","slug":"什么是前端录制回放","link":"#什么是前端录制回放","children":[]},{"level":3,"title":"为什么需要?","slug":"为什么需要","link":"#为什么需要","children":[]},{"level":3,"title":"如何实现","slug":"如何实现","link":"#如何实现","children":[]}]},{"level":2,"title":"思路初现","slug":"思路初现","link":"#思路初现","children":[{"level":3,"title":"操作记录","slug":"操作记录","link":"#操作记录","children":[]},{"level":3,"title":"回放操作","slug":"回放操作","link":"#回放操作","children":[]},{"level":3,"title":"渲染环境","slug":"渲染环境","link":"#渲染环境","children":[]},{"level":3,"title":"数据还原","slug":"数据还原","link":"#数据还原","children":[]},{"level":3,"title":"定时器","slug":"定时器","link":"#定时器","children":[]}]},{"level":2,"title":"rrweb 框架","slug":"rrweb-框架","link":"#rrweb-框架","children":[]},{"level":2,"title":"rrweb 源码","slug":"rrweb-源码","link":"#rrweb-源码","children":[{"level":3,"title":"Record 录制","slug":"record-录制","link":"#record-录制","children":[]},{"level":3,"title":"Snapshot 快照","slug":"snapshot-快照","link":"#snapshot-快照","children":[]},{"level":3,"title":"Replay 回放","slug":"replay-回放","link":"#replay-回放","children":[]}]}],"relativePath":"fragment/video.md","lastUpdated":1713164447000}'),o={name:"fragment/video.md"},e=l(`

前端录制回放系统初体验

问题背景

什么是前端录制回放?

顾名思义,就是录制用户在网页中的各种操作,并且支持能随时回放操作。

为什么需要?

说到需要就不得不说一个经典的场景,一般前端做异常监控和错误上报,会采用自研或接入第三方 SDK 的形式,来收集和上报网站交互过程中 JavaScript 的报错信息和其它相关数据,也就是埋点。 在传统的埋点方案中,根据 SourceMap 能定位到具体报错代码文件和行列信息等。基本能定位大部分场景问题,但有一些情况下是很难复现错误,多是在测试扯皮的时候,程序员口头禅之一(我这里没有报错呀,是不是你电脑有问题)。 要是能把出错的操作过程录制下来就好了,这样就能方便我们复现场景了,且留存证据,好像是自己给自己挖了个坑。

如何实现

前端能实现录视频?我第一反应就是质疑,接着我就是一波 Google ,发现确实有可行方案。 在 Google 之前,我想到了通过设定定时器,对视图窗口进行截图,截图可用 canvas2html 的方式来实现,但这种方式无疑会造成性能问题,立马否决。 下面介绍我所「知道」的 Google 的方案

思路初现

网页本质上是一个 DOM 节点形式存在,通过浏览器渲染出来。我们是否可以把 DOM 以某种方式保存起来,并且在不同时间节点持续记录 DOM 数据状态。再将数据还原成 DOM 节点渲染出来完成回放呢?

操作记录

通过 document.documentElement.cloneNode() 克隆到 DOM 的数据对象,此时这个数据不能直接通过接口传输给后端,需要进行一些格式化预处理,处理成方便传输及存储的数据格式。最简单的方式就是进行序列化,也就是转换成 JSON 数据格式。

js
// 序列化后
+let docJSON = {
+  type: "Document",
+  childNodes: [
+    {
+      type: "Element",
+      tagName: "html",
+      attributes: {},
+      childNodes: [
+        {
+          type: "Element",
+          tagName: "head",
+          attributes: {},
+          childNodes: [],
+        },
+      ],
+    },
+  ],
+};
+
// 序列化后
+let docJSON = {
+  type: "Document",
+  childNodes: [
+    {
+      type: "Element",
+      tagName: "html",
+      attributes: {},
+      childNodes: [
+        {
+          type: "Element",
+          tagName: "head",
+          attributes: {},
+          childNodes: [],
+        },
+      ],
+    },
+  ],
+};
+

有完整的 DOM 数据之后,还需要在 DOM 变化时进行监听,记录每次变化的 DOM 节点信息。对数据进行监听可用 MutationObserver ,它是一个可以监听 DOM 变化的 API 。

js
const observer = new MutationObserver((mutationsList) => {
+  console.log(mutationsList); // 发生变化的数据
+});
+// 以上述配置开始观察目标节点
+observer.observe(document, {});
+
const observer = new MutationObserver((mutationsList) => {
+  console.log(mutationsList); // 发生变化的数据
+});
+// 以上述配置开始观察目标节点
+observer.observe(document, {});
+

除了对 DOM 变化进行监听以外,还有一个就是事件监听,用户与网页的交互多是通过鼠标,键盘等输入设备来进行。而这些交互的背后就是 JavaScript 的事件监听。事件监听可以通过绑定系统事件来完成,同样是需要记录下来,以鼠标移动为例:

js
// 鼠标移动
+document.addEventListener("mousemove", (e) => {
+  // 伪代码 获取鼠标移动的信息并记录下来
+  positions.push({
+    x: clientX,
+    y: clientY,
+    timeOffset: Date.now() - timeBaseline,
+  });
+});
+
// 鼠标移动
+document.addEventListener("mousemove", (e) => {
+  // 伪代码 获取鼠标移动的信息并记录下来
+  positions.push({
+    x: clientX,
+    y: clientY,
+    timeOffset: Date.now() - timeBaseline,
+  });
+});
+

回放操作

数据已经有了,接着就是回放,回放本质上是将 JSON 数据还原成 DOM 节点渲染出来。那就将快照数据还原就可以啊「嘴强王者」,数据还原并非那么容易啊!

渲染环境

首先为了确保回放过程代码隔离,需要沙箱环境, iframe 标签可以做到,并且 iframe 提供了 sandbox 属性可配置沙箱。沙箱环境的作用是确保代码安全并且不被干扰。

html
<iframe sandbox srcdoc></iframe>
+
<iframe sandbox srcdoc></iframe>
+

sanbox 属性可以做到沙箱作用, 点击查看文档 srcdoc 可以直接设置成一段 html 代码

数据还原

快照重组主要是 DOM 节点的重组,有点像虚拟 DOM 转成真实文档节点的过程,但是事件类型快照是不需要重组。

定时器

有了数据和环境,还需要定时器。通过定时器不停渲染 DOM ,实质上就是一个播放视频的效果, requestAnimationFrame 是最合适的。

requestAnimationFrame 执行机制在浏览器下一次 repaint(重绘)之前执行,执行频率取决于浏览器刷新频率,更适合制作动画效果

至此有一个大概的想法,距离落地还是有段距离。得益于开源,我们可上 Github 看看有没有合适的轮子可复制(借鉴),刚好有现成的一框架 「rrweb」 ,不妨一起看看。

rrweb 框架

rrweb 是一个前端录制和回放的框架。全称 record and replay the web ,顾名思义就是可以录制和回放 web 界面中的操作,其核心原理就是上面介绍的方案。

rrweb 包含三个部分:

  • rrweb-snapshot 主要处理 DOM 结构序列化和重组;
  • rrweb 主要功能是录制和回放;
  • rrweb-player 一个视频播放器 UI 空间

通过 rrweb.record 方法来录制页面, emit 回调可接受到录制的数据。

js
// 1.录制
+let events = []; // 记录快照
+
+rrweb.record({
+  emit(event) {
+    // 将 event 存入 events 数组中
+    events.push(event);
+  },
+});
+
// 1.录制
+let events = []; // 记录快照
+
+rrweb.record({
+  emit(event) {
+    // 将 event 存入 events 数组中
+    events.push(event);
+  },
+});
+

通过 rrweb.Replayer 可回放视频,需要传递录制好的数据。

js
// 2.回放
+const replayer = new rrweb.Replayer(events);
+replayer.play();
+
// 2.回放
+const replayer = new rrweb.Replayer(events);
+replayer.play();
+

rrweb 源码

按照以上所说的思路,接下来会解析其中一些关键代码,当然只是在我个人理解上做的一些分析,实际上 rrweb 源码远不止这些。

核心部分为三大块: record (录制)、 replay 回放、 snapshot 快照。

Record 录制

在 DOM 加载完成后, record 会做一次完整的 DOM 序列化,我们把它叫做全量快照,全量快照记录了整个 HTML 数据结构。 在 record.ts 中找到关键的入口函数的定义 init ,入口函数是会在 document 加载完成或(可交互,完成)时调用了 takeFullSnapshot 以及 observe(document) 函数。

js
if (
+    document.readyState === 'interactive' ||
+    document.readyState === 'complete'
+) {
+    init();
+} else {
+    on('load',() => { init(); },),
+}
+const init = () => {
+    takeFullSnapshot(); // 生成全量快照
+    handlers.push(observe(document)); //监听器
+};
+
if (
+    document.readyState === 'interactive' ||
+    document.readyState === 'complete'
+) {
+    init();
+} else {
+    on('load',() => { init(); },),
+}
+const init = () => {
+    takeFullSnapshot(); // 生成全量快照
+    handlers.push(observe(document)); //监听器
+};
+

document.readyState 包含三种状态:

  1. 可交互 interactive ;
  2. 正在加载中 loading ;
  3. 完成 complete

takeFullSnapshot 从字面意思能看出其作用是生成「完整」的快照,也就是会将 document 序列化出一个完整的数据,称之为 「全量快照」 。 所有序列化相关操作都是使用 snapshot 完成, snapshot 接受一个 dom 对象和一个配置对象传递 document 将整个页面序列化得到完成的快照数据。

js
// 生成全量快照
+takeFullSnapshot = (isCheckout = false) => {
+  const [node, idNodeMap] = snapshot(document, {
+    //...一些配置项
+  });
+};
+
// 生成全量快照
+takeFullSnapshot = (isCheckout = false) => {
+  const [node, idNodeMap] = snapshot(document, {
+    //...一些配置项
+  });
+};
+

idNodeMap 是一个 id 为 key , DOM 对象为 value 的 key-value 键值对对象

observe(document) 是一些监听器的初始化,同样是将整个 document 对象传过去进行监听,通过调用 initObservers 来初始化一些监听器。

js
const observe = (doc: Document) => {
+  return initObservers();
+};
+
const observe = (doc: Document) => {
+  return initObservers();
+};
+

在 observer.ts 文件中可以找到 initObservers 函数定义,该函数初始化了 11 个监听器,可以分为 DOM 类型 / Event 事件类型 / Media 媒体三大类:

js
export function initObservers(
+// dom
+const mutationObserver = initMutationObserver();
+const mousemoveHandler = initMoveObserver();
+const mouseInteractionHandler = initMouseInteractionObserver();
+const scrollHandler = initScrollObserver();
+const viewportResizeHandler = initViewportResizeObserver();
+// ...
+)
+
export function initObservers(
+// dom
+const mutationObserver = initMutationObserver();
+const mousemoveHandler = initMoveObserver();
+const mouseInteractionHandler = initMouseInteractionObserver();
+const scrollHandler = initScrollObserver();
+const viewportResizeHandler = initViewportResizeObserver();
+// ...
+)
+
  • DOM 变化监听器,主要有 DOM 变化(增删改), 样式变化,核心是通过 MutationObserver 来实现
js
let mutationObserverCtor = window.MutationObserver;
+
+const observer = new mutationObserverCtor(
+  // 处理变化的数据
+  mutationBuffer.processMutations.bind(mutationBuffer)
+);
+observer.observe(doc, {});
+return observer;
+
let mutationObserverCtor = window.MutationObserver;
+
+const observer = new mutationObserverCtor(
+  // 处理变化的数据
+  mutationBuffer.processMutations.bind(mutationBuffer)
+);
+observer.observe(doc, {});
+return observer;
+
  • 交互监听-以鼠标移动 initMoveObserver 为例
js
// 鼠标移动记录
+function initMoveObserver() {
+  const updatePosition =
+    (throttle < MouseEvent) |
+    (TouchEvent >
+      ((evt) => {
+        positions.push({
+          x: clientX,
+          y: clientY,
+        });
+      }));
+  const handlers = [
+    on("mousemove", updatePosition, doc),
+    on("touchmove", updatePosition, doc),
+  ];
+}
+
// 鼠标移动记录
+function initMoveObserver() {
+  const updatePosition =
+    (throttle < MouseEvent) |
+    (TouchEvent >
+      ((evt) => {
+        positions.push({
+          x: clientX,
+          y: clientY,
+        });
+      }));
+  const handlers = [
+    on("mousemove", updatePosition, doc),
+    on("touchmove", updatePosition, doc),
+  ];
+}
+
  • 媒体类型监听器,有 canvas / video / audio ,以 video 为例,本质上记录播放和暂停状态, mediaInteractionCb 将 play / pause 状态回调出来。
js
function initMediaInteractionObserver(): listenerHandler {
+    mediaInteractionCb({
+        type: type === 'play' ? MediaInteractions.Play : MediaInteractions.Pause,
+        id: mirror.getId(target as INode),
+    });
+}
+
+
function initMediaInteractionObserver(): listenerHandler {
+    mediaInteractionCb({
+        type: type === 'play' ? MediaInteractions.Play : MediaInteractions.Pause,
+        id: mirror.getId(target as INode),
+    });
+}
+
+

Snapshot 快照

snapshot 负责序列化和重组的功能,主要通过 serializeNodeWithId 处理 DOM 序列化和 rebuildWithSN 函数处理 DOM 重组。 serializeNodeWithId 函数负责序列化,主要做了三件事:

  • 调用 serializeNode 序列化 Node ;
  • 通过 genId() 生成唯一 ID 并绑定到 Node 中;
  • 递归实现序列化子节点,并最终返回一个带 ID 的对象
js
// 序列化一个带有ID的DOM
+export function serializeNodeWithId(n) {
+  // 1. 序列化 核心函数 serializeNode
+  const _serializedNode = serializeNode(n);
+  // 2. 生成唯一ID
+  let id = genId();
+  // 绑定ID
+  const serializedNode = Object.assign(_serializedNode, { id });
+
+  // 3.子节点序列化-递归
+  for (const childN of Array.from(n.childNodes)) {
+    const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
+    if (serializedChildNode) {
+      serializedNode.childNodes.push(serializedChildNode);
+    }
+  }
+}
+
// 序列化一个带有ID的DOM
+export function serializeNodeWithId(n) {
+  // 1. 序列化 核心函数 serializeNode
+  const _serializedNode = serializeNode(n);
+  // 2. 生成唯一ID
+  let id = genId();
+  // 绑定ID
+  const serializedNode = Object.assign(_serializedNode, { id });
+
+  // 3.子节点序列化-递归
+  for (const childN of Array.from(n.childNodes)) {
+    const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
+    if (serializedChildNode) {
+      serializedNode.childNodes.push(serializedChildNode);
+    }
+  }
+}
+

serializeNodeWithId 核心是通过 serializeNode 序列化 DOM ,针对不同的节点分别做了一些特殊处理。

节点属性的处理:

js
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
+    attributes[name] = transformAttribute(doc, tagName, name, value);
+}
+
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
+    attributes[name] = transformAttribute(doc, tagName, name, value);
+}
+

处理外联 css 样式,通过 getCssRulesString 获取到具体样式代码,并且储存到 attributes 中。

js
const cssText = getCssRulesString(stylesheet as CSSStyleSheet);
+if (cssText) {
+    attributes._cssText = absoluteToStylesheet(
+        cssText,
+        stylesheet!.href!,
+    );
+}
+
const cssText = getCssRulesString(stylesheet as CSSStyleSheet);
+if (cssText) {
+    attributes._cssText = absoluteToStylesheet(
+        cssText,
+        stylesheet!.href!,
+    );
+}
+

处理 form 表单,逻辑是保存选中状态,并且做了一些安全处理,例如密码框内容替换成 * 。

js
if (
+    attributes.type !== 'radio' &&
+    attributes.type !== 'checkbox' &&
+    // ...
+) {
+    attributes.value = maskInputOptions[tagName]
+        ? '*'.repeat(value.length)
+        : value;
+  } else if (n.checked) {
+    attributes.checked = n.checked;
+  }
+
if (
+    attributes.type !== 'radio' &&
+    attributes.type !== 'checkbox' &&
+    // ...
+) {
+    attributes.value = maskInputOptions[tagName]
+        ? '*'.repeat(value.length)
+        : value;
+  } else if (n.checked) {
+    attributes.checked = n.checked;
+  }
+

canvas 状态保存通过 toDataURL 保存 canvas 数据:

js
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
+
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
+

rebuild 负责重建 DOM :

  • 通过 buildNodeWithSN 函数重组 Node
  • 递归调用 重组子节点
js
export function buildNodeWithSN(n) {
+  // DOM 重组核心函数 buildNode
+  let node = buildNode(n, { doc, hackCss });
+  // 子节点重建并且appendChild
+  for (const childN of n.childNodes) {
+    const childNode = buildNodeWithSN(childN);
+    if (afterAppend) {
+      afterAppend(childNode);
+    }
+  }
+}
+
export function buildNodeWithSN(n) {
+  // DOM 重组核心函数 buildNode
+  let node = buildNode(n, { doc, hackCss });
+  // 子节点重建并且appendChild
+  for (const childN of n.childNodes) {
+    const childNode = buildNodeWithSN(childN);
+    if (afterAppend) {
+      afterAppend(childNode);
+    }
+  }
+}
+

Replay 回放

回放部分在 replay.ts 文件中,先创建沙箱环境,接着或进行重建 document 全量快照,在通过 requestAnimationFrame 模拟定时器的方式来播放增量快照。 replay 的构造函数接收两个参数,快照数据 events 和 配置项 config

js
export class Replayer {
+  constructor(events, config) {
+    // 1.创建沙箱环境
+    this.setupDom();
+    // 2.定时器
+    const timer = new Timer();
+    // 3.播放服务
+    this.service = new createPlayerService(events, timer);
+    this.service.start();
+  }
+}
+
export class Replayer {
+  constructor(events, config) {
+    // 1.创建沙箱环境
+    this.setupDom();
+    // 2.定时器
+    const timer = new Timer();
+    // 3.播放服务
+    this.service = new createPlayerService(events, timer);
+    this.service.start();
+  }
+}
+

构造函数中最核心三步,创建沙箱环境,定时器,和初始化播放器并且启动。播放器创建依赖 events 和 timer ,本质上还是使用 timer 来实现播放。

沙箱环境

首先,在 replay.ts 的构造函数中可以找打 this.setupDom 的调用, setupDom 核心是通过 iframe 来创建出一个沙箱环境。

js
private setupDom() {
+  // 创建iframe
+  this.iframe = document.createElement('iframe');
+  this.iframe.style.display = 'none';
+  this.iframe.setAttribute('sandbox', attributes.join(' '));
+}
+
private setupDom() {
+  // 创建iframe
+  this.iframe = document.createElement('iframe');
+  this.iframe.style.display = 'none';
+  this.iframe.setAttribute('sandbox', attributes.join(' '));
+}
+

播放服务

同样在 replay.ts 构造函数中,调用 createPlayerService 函数来创建播放器服务器,该函数在同级目录下的 machine.ts 中定义了,核心思路是通过给定时器 timer 加入需要执行的快照动作 actions , 在调用 timer.start() 开始回放快照。

js
export function createPlayerService() {
+    //...
+    play(ctx) {
+        // 获取每个 event 执行的 doAction 函数
+        for (const event of needEvents) {
+            //..
+            const castFn = getCastFn(event);
+            actions.push({
+                doAction: () => {
+                    castFn();
+                }
+            })
+            //..
+         }
+         // 添加到定时器队列中
+         timer.addActions(actions);
+         // 启动定时器播放 视频
+         timer.start();
+    },
+    //...
+}
+
export function createPlayerService() {
+    //...
+    play(ctx) {
+        // 获取每个 event 执行的 doAction 函数
+        for (const event of needEvents) {
+            //..
+            const castFn = getCastFn(event);
+            actions.push({
+                doAction: () => {
+                    castFn();
+                }
+            })
+            //..
+         }
+         // 添加到定时器队列中
+         timer.addActions(actions);
+         // 启动定时器播放 视频
+         timer.start();
+    },
+    //...
+}
+

播放服务使用到第三方库 @xstate/fsm 状态机来控制各种状态(播放,暂停,直播)

定时器 timer.ts 也是在同级目录下,核心是通过 requestAnimationFrame 实现了定时器功能, 并对快照回放,以队列的形式存储需要播放的快照 actions ,接着在 start 中递归调用 action.doAction 来实现对应时间节点的快照还原。

js
export class Timer {
+    // 添加队列
+    public addActions(actions: actionWithDelay[]) {
+        this.actions = this.actions.concat(actions);
+    }
+    // 播放队列
+    public start() {
+        function check() {
+            // ...
+            // 循环调用actions中的doAction 也就是 castFn 函数
+            while (actions.length) {
+                const action = actions[0];
+                actions.shift();
+                // doAction 会对快照进行回放动作,针对不同快照会执行不同动作
+                action.doAction();
+            }
+            if (actions.length > 0 || self.liveMode) {
+                self.raf = requestAnimationFrame(check);
+            }
+        }
+        this.raf = requestAnimationFrame(check);
+    }
+}
+
export class Timer {
+    // 添加队列
+    public addActions(actions: actionWithDelay[]) {
+        this.actions = this.actions.concat(actions);
+    }
+    // 播放队列
+    public start() {
+        function check() {
+            // ...
+            // 循环调用actions中的doAction 也就是 castFn 函数
+            while (actions.length) {
+                const action = actions[0];
+                actions.shift();
+                // doAction 会对快照进行回放动作,针对不同快照会执行不同动作
+                action.doAction();
+            }
+            if (actions.length > 0 || self.liveMode) {
+                self.raf = requestAnimationFrame(check);
+            }
+        }
+        this.raf = requestAnimationFrame(check);
+    }
+}
+

doAction 在不同类型快照会执行不同动作,在播放服务中 doAction 最终会调用 getCastFn 函数来做了一些 case :

js
private getCastFn(event: eventWithTime, isSync = false) {
+    switch (event.type) {
+        case EventType.DomContentLoaded: //dom 加载解析完成
+        case EventType.FullSnapshot: // 全量快照
+        case EventType.IncrementalSnapshot: //增量
+            castFn = () => {
+                this.applyIncremental(event, isSync);
+            }
+    }
+}
+
private getCastFn(event: eventWithTime, isSync = false) {
+    switch (event.type) {
+        case EventType.DomContentLoaded: //dom 加载解析完成
+        case EventType.FullSnapshot: // 全量快照
+        case EventType.IncrementalSnapshot: //增量
+            castFn = () => {
+                this.applyIncremental(event, isSync);
+            }
+    }
+}
+

applyIncremental 函数会增对不同的增量快照做不同处理,包含 DOM 增量, 鼠标交互,页面滚动等,以 DOM 增量快照的 case 为例,最终会走到 applyMutation 中:

js
private applyIncremental(){
+  switch (d.source) {
+      case IncrementalSource.Mutation: {
+        this.applyMutation(d, isSync); // DOM变化
+        break;
+      }
+      case IncrementalSource.MouseMove: //鼠标移动
+      case IncrementalSource.MouseInteraction: //鼠标点击事件
+
+  }
+}
+
private applyIncremental(){
+  switch (d.source) {
+      case IncrementalSource.Mutation: {
+        this.applyMutation(d, isSync); // DOM变化
+        break;
+      }
+      case IncrementalSource.MouseMove: //鼠标移动
+      case IncrementalSource.MouseInteraction: //鼠标点击事件
+
+  }
+}
+

applyMutation 才是最终执行 DOM 还原操作的地方,包含 DOM 的增删改步骤:

js
private applyMutation(d: mutationData, useVirtualParent: boolean) {
+    d.removes.forEach((mutation) => {
+        //.. 移除dom
+    });
+    const appendNode = (mutation: addedNodeMutation) => {
+        // 添加dom到具体节点下
+    };
+    d.adds.forEach((mutation) => {
+        // 添加
+        appendNode(mutation);
+    });
+    d.texts.forEach((mutation) => {
+        //...文本处理
+    });
+    d.attributes.forEach((mutation) => {
+        //...属性处理
+    });
+}
+
private applyMutation(d: mutationData, useVirtualParent: boolean) {
+    d.removes.forEach((mutation) => {
+        //.. 移除dom
+    });
+    const appendNode = (mutation: addedNodeMutation) => {
+        // 添加dom到具体节点下
+    };
+    d.adds.forEach((mutation) => {
+        // 添加
+        appendNode(mutation);
+    });
+    d.texts.forEach((mutation) => {
+        //...文本处理
+    });
+    d.attributes.forEach((mutation) => {
+        //...属性处理
+    });
+}
+

以上就是回放的关键流程实现代码, rrweb 中不仅仅是做了这些,还包含数据压缩,移动端处理,隐私问题等等细节处理,有兴趣可自行查看源码。

`,95),c=[e];function r(t,B,y,i,F,d){return n(),a("div",null,c)}const u=s(o,[["render",r]]);export{b as __pageData,u as default}; diff --git a/assets/fragment_video.md.a2bb9154.lean.js b/assets/fragment_video.md.a2bb9154.lean.js new file mode 100644 index 00000000..9e3911ac --- /dev/null +++ b/assets/fragment_video.md.a2bb9154.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-15-14-51-24.69678060.png",b=JSON.parse('{"title":"前端录制回放系统初体验","description":"","frontmatter":{},"headers":[{"level":2,"title":"问题背景","slug":"问题背景","link":"#问题背景","children":[{"level":3,"title":"什么是前端录制回放?","slug":"什么是前端录制回放","link":"#什么是前端录制回放","children":[]},{"level":3,"title":"为什么需要?","slug":"为什么需要","link":"#为什么需要","children":[]},{"level":3,"title":"如何实现","slug":"如何实现","link":"#如何实现","children":[]}]},{"level":2,"title":"思路初现","slug":"思路初现","link":"#思路初现","children":[{"level":3,"title":"操作记录","slug":"操作记录","link":"#操作记录","children":[]},{"level":3,"title":"回放操作","slug":"回放操作","link":"#回放操作","children":[]},{"level":3,"title":"渲染环境","slug":"渲染环境","link":"#渲染环境","children":[]},{"level":3,"title":"数据还原","slug":"数据还原","link":"#数据还原","children":[]},{"level":3,"title":"定时器","slug":"定时器","link":"#定时器","children":[]}]},{"level":2,"title":"rrweb 框架","slug":"rrweb-框架","link":"#rrweb-框架","children":[]},{"level":2,"title":"rrweb 源码","slug":"rrweb-源码","link":"#rrweb-源码","children":[{"level":3,"title":"Record 录制","slug":"record-录制","link":"#record-录制","children":[]},{"level":3,"title":"Snapshot 快照","slug":"snapshot-快照","link":"#snapshot-快照","children":[]},{"level":3,"title":"Replay 回放","slug":"replay-回放","link":"#replay-回放","children":[]}]}],"relativePath":"fragment/video.md","lastUpdated":1713164447000}'),o={name:"fragment/video.md"},e=l("",95),c=[e];function r(t,B,y,i,F,d){return n(),a("div",null,c)}const u=s(o,[["render",r]]);export{b as __pageData,u as default}; diff --git "a/assets/fragment_\345\211\215\346\262\277\346\212\200\346\234\257.md.c7fe3df8.js" "b/assets/fragment_\345\211\215\346\262\277\346\212\200\346\234\257.md.c7fe3df8.js" new file mode 100644 index 00000000..f012e5f8 --- /dev/null +++ "b/assets/fragment_\345\211\215\346\262\277\346\212\200\346\234\257.md.c7fe3df8.js" @@ -0,0 +1 @@ +import{_ as e,o as t,c as r,a}from"./app.f983686f.js";const j=JSON.parse('{"title":"前沿技术","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/前沿技术.md","lastUpdated":1718692967000}'),n={name:"fragment/前沿技术.md"},s=a('

前沿技术

https://juejin.cn/post/6844903827972292615

https://juejin.cn/post/7087924763275821063

https://juejin.cn/post/7130162638922711071

https://juejin.cn/post/7210970258369708092

https://supercodepower.com/cross-platform-tech

',6),o=[s];function p(c,_,h,i,f,l){return t(),r("div",null,o)}const u=e(n,[["render",p]]);export{j as __pageData,u as default}; diff --git "a/assets/fragment_\345\211\215\346\262\277\346\212\200\346\234\257.md.c7fe3df8.lean.js" "b/assets/fragment_\345\211\215\346\262\277\346\212\200\346\234\257.md.c7fe3df8.lean.js" new file mode 100644 index 00000000..7cb19cba --- /dev/null +++ "b/assets/fragment_\345\211\215\346\262\277\346\212\200\346\234\257.md.c7fe3df8.lean.js" @@ -0,0 +1 @@ +import{_ as e,o as t,c as r,a}from"./app.f983686f.js";const j=JSON.parse('{"title":"前沿技术","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/前沿技术.md","lastUpdated":1718692967000}'),n={name:"fragment/前沿技术.md"},s=a("",6),o=[s];function p(c,_,h,i,f,l){return t(),r("div",null,o)}const u=e(n,[["render",p]]);export{j as __pageData,u as default}; diff --git "a/assets/fragment_\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.md.3d9cf8a5.js" "b/assets/fragment_\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.md.3d9cf8a5.js" new file mode 100644 index 00000000..9f120d19 --- /dev/null +++ "b/assets/fragment_\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.md.3d9cf8a5.js" @@ -0,0 +1,273 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-09-16-52-36.f7ed461a.png",b=JSON.parse('{"title":"🔥 微内核架构在前端的实现及其应用","description":"","frontmatter":{},"headers":[{"level":2,"title":"前置知识","slug":"前置知识","link":"#前置知识","children":[]},{"level":2,"title":"如何实现","slug":"如何实现","link":"#如何实现","children":[{"level":3,"title":"如何调用","slug":"如何调用","link":"#如何调用","children":[]},{"level":3,"title":"Core 和 Plugin 的实现","slug":"core-和-plugin-的实现","link":"#core-和-plugin-的实现","children":[]},{"level":3,"title":"设计思想","slug":"设计思想","link":"#设计思想","children":[]}]},{"level":2,"title":"重点问题","slug":"重点问题","link":"#重点问题","children":[{"level":3,"title":"通讯问题","slug":"通讯问题","link":"#通讯问题","children":[]},{"level":3,"title":"插件的调度","slug":"插件的调度","link":"#插件的调度","children":[]},{"level":3,"title":"安全性和稳定性","slug":"安全性和稳定性","link":"#安全性和稳定性","children":[]},{"level":3,"title":"presets 预设","slug":"presets-预设","link":"#presets-预设","children":[]}]},{"level":2,"title":"应用场景","slug":"应用场景","link":"#应用场景","children":[{"level":3,"title":"webpack","slug":"webpack","link":"#webpack","children":[]},{"level":3,"title":"babel","slug":"babel","link":"#babel","children":[]},{"level":3,"title":"Vue","slug":"vue","link":"#vue","children":[]}]},{"level":2,"title":"小结","slug":"小结","link":"#小结","children":[]}],"relativePath":"fragment/微内核架构.md","lastUpdated":1712654359000}'),o={name:"fragment/微内核架构.md"},e=l('

🔥 微内核架构在前端的实现及其应用

前置知识

微内核架构,大白话讲就是插件系统,就像下面这张图:

从上图中可以看出,微内核架构主要由两部分组成:Core + plugin,也就是一个内核和多个插件。通常来说:

  • 内核主要负责一些基础功能、核心功能以及插件的管理;
  • 插件则是一个独立的功能模块,用来丰富和加强内核的能力。

内核和插件通常是解耦的,在不使用插件的情况下,内核也能够独立运行。看得出来这种架构拓展性极强,在前端主流框架中都能看到它的影子:

  • 你在用 vue 的时候,可以用 Vue.use()
  • webpack 用的一些 plugins
  • babel 用的一些 plugins
  • 浏览器插件
  • vscode 插件
  • koa 中间件(中间件也可以理解为插件)
  • 甚至是 jQuery 插件
  • ...

如何实现

话不多说,接下来就直接开撸,实现一个微内核系统。不过在做之前,我们要先明确要做什么东西,这里就以文档编辑器为例子吧,每篇文档由多个 Block 区块组成,每个 Block 又可以展示不同的内容(列表、图片、图表等),这种情况下,我们要想扩展文档没有的功能(比如脑图),就很适合用插件系统做啦!

如何调用

通常在开发之前,我们会先确定一下期望的调用方式,大抵应该是下面这个样子 👇🏻:

js
// 内核初始化
+const core = new Core();
+// 使用插件
+core.use(new Plugin1());
+core.use(new Plugin2());
+// 开始运行
+core.run();
+
// 内核初始化
+const core = new Core();
+// 使用插件
+core.use(new Plugin1());
+core.use(new Plugin2());
+// 开始运行
+core.run();
+

Core 和 Plugin 的实现

插件是围绕内核而生的,所以我们先来实现内核的部分吧,也就是 Core 类。通常内核有两个用途:一个是实现基础功能;一个是管理插件。基础功能要看具体使用场景,它是基于业务的,这里就略过了。我们的重点在插件,要想能够在内核中使用插件肯定是要在内核中开个口子的,最基本的问题就是如何注册、如何执行。一般来说插件的格式会长下面这个样子:

ts
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力,通常是个函数,也可以叫 apply、exec、handle */
+  fn: Function;
+}
+
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力,通常是个函数,也可以叫 apply、exec、handle */
+  fn: Function;
+}
+

再看看前面定义的调用方式, core.use() 就是向内核中注册插件了,而 core.run() 则是执行,也就是说 Core 中需要有 use 和 run 这两个方法,于是我们就能简单写出如下代码 👇🏻:

ts
/** 内核 Core :基础功能 + 插件调度器 */
+class Core {
+  pluginMap: Map<string, IPlugin> = new Map();
+  constructor() {
+    console.log("实现内核基础功能:文档初始化");
+  }
+  /** 插件注册,也可以叫 register,通常以注册表的形式实现,其实就是个对象映射 */
+  use(plugin: IPlugin): Core {
+    this.pluginMap.set(plugin.name, plugin);
+    return this; // 方便链式调用
+  }
+  /** 插件执行,也可以叫 start */
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn();
+    });
+  }
+}
+class Plugin1 implements IPlugin {
+  name = "Block1";
+  fn() {
+    console.log("扩展文档功能:Block1");
+  }
+}
+class Plugin2 implements IPlugin {
+  name = "Block2";
+  fn() {
+    console.log("扩展文档功能:Block2");
+  }
+}
+// 运行结果如下:
+// 实现内核基础功能:文档初始化
+// 扩展文档功能:Block1
+// 扩展文档功能:Block2
+
/** 内核 Core :基础功能 + 插件调度器 */
+class Core {
+  pluginMap: Map<string, IPlugin> = new Map();
+  constructor() {
+    console.log("实现内核基础功能:文档初始化");
+  }
+  /** 插件注册,也可以叫 register,通常以注册表的形式实现,其实就是个对象映射 */
+  use(plugin: IPlugin): Core {
+    this.pluginMap.set(plugin.name, plugin);
+    return this; // 方便链式调用
+  }
+  /** 插件执行,也可以叫 start */
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn();
+    });
+  }
+}
+class Plugin1 implements IPlugin {
+  name = "Block1";
+  fn() {
+    console.log("扩展文档功能:Block1");
+  }
+}
+class Plugin2 implements IPlugin {
+  name = "Block2";
+  fn() {
+    console.log("扩展文档功能:Block2");
+  }
+}
+// 运行结果如下:
+// 实现内核基础功能:文档初始化
+// 扩展文档功能:Block1
+// 扩展文档功能:Block2
+

设计思想

一个好的架构肯定是能够体现一些设计模式思想的:

  • 单一职责:每个插件相互独立,只负责其对应的功能模块,也方便进行单独测试。如果把功能点都写在内核里,就容易造成高耦合。
  • 开放封闭:也就是我们扩展某个功能,不会去修改老代码,每个插件的接入成本是一样的,我们也不用关心内核是怎么实现的,只要知道它暴露了什么 api,会用就行。
  • 策略模式:比如我们在渲染文档的某个 Block 区块时,需要根据不同 Block 类型(插件名、策略名)去执行不同的渲染方法(插件方法、策略),当然了,当前的代码体现的可能不是很明显。
  • 还有一个就是控制反转:这个暂时还没体现,但是下文会提到,简单来说就是通过依赖注入实现控制权的反转

重点问题

通讯问题

前面我们说道,内核和插件是解耦的,那如果我需要在插件中用到一些内核的功能,该怎么和内核通信呢?插件与插件之间又该怎么通信呢?别急,先来解决第一个问题,这个很简单,既然插件要用内核的东西,那我把内核暴露给插件不就好了,就像下面这样 👇🏻:

ts
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力:这个 ctx 就是内核 */
+  fn(ctx: Core): void;
+}
+class Core {
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this); // 注意这里,我们把 this 传递了进去
+    });
+  }
+}
+
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力:这个 ctx 就是内核 */
+  fn(ctx: Core): void;
+}
+class Core {
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this); // 注意这里,我们把 this 传递了进去
+    });
+  }
+}
+

这样一来我们就能在插件中获取 Core 实例了(也可以叫做上下文),相应的就能够用内核的一些东西了。比如内核中通常会有一些系统基础配置信息(isDev、platform、version 等),那我们在插件中就能根据不同环境、不同平台、不同版本进行进一步处理,甚至是更改内核的内容。 这个暴露内核的过程其实就是前文提到的控制反转,什么意思呢?比如通常情况下我们要在一个类中要使用另外一个类,你会怎么做呢?是不是会直接在这个类中直接实例化另外一个类,这个就是正常的控制权。现在呢,我们虽然需要在内核中使用插件,但是我们把内核实例暴露给了插件,变成了在插件中使用内核,注意,此时主动权已经在插件这里了,这就是通过依赖注入实现了控制反转,可以好仔体会一下 🤯。 这种方式虽然我们能够引用和修改到内核的一些信息,但是好像不能干预内核初始化和执行的过程,要是想在内核初始化和执行过程中做一些处理该怎么办呢?我们可以引入事件机制,也就是发布订阅模式,在内核执行过程中抛出一些事件,然后由插件去监听相应的事件,我们可以叫 events,也可以叫 hooks(webpack 里面就是 hooks),具体实现就像下面这样:

js
import { EventEmitter } from "events"; // webpack 中使用 tapable,很强大的一个发布订阅库
+
+class Core {
+  events: EventEmitter = new EventEmitter(); // 也可以叫 hooks,就是发布订阅
+  constructor() {
+    this.events.emit("beforeInit");
+    console.log("实现内核基础功能:文档初始化");
+    this.events.emit("afterInit");
+  }
+  run() {
+    this.events.emit("before all plugins");
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this);
+    });
+    this.events.emit("after all plugins");
+  }
+}
+// 插件中可以这样调用:this.events.on('xxxx');
+
import { EventEmitter } from "events"; // webpack 中使用 tapable,很强大的一个发布订阅库
+
+class Core {
+  events: EventEmitter = new EventEmitter(); // 也可以叫 hooks,就是发布订阅
+  constructor() {
+    this.events.emit("beforeInit");
+    console.log("实现内核基础功能:文档初始化");
+    this.events.emit("afterInit");
+  }
+  run() {
+    this.events.emit("before all plugins");
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this);
+    });
+    this.events.emit("after all plugins");
+  }
+}
+// 插件中可以这样调用:this.events.on('xxxx');
+

我们也可以改成生命周期的形式,比如:

ts
class Core {
+  constructor(opts?: IOption) {
+    opts?.beforeCreate();
+    console.log("实现内核基础功能:文档初始化");
+    opts?.afterCreate();
+  }
+}
+
class Core {
+  constructor(opts?: IOption) {
+    opts?.beforeCreate();
+    console.log("实现内核基础功能:文档初始化");
+    opts?.afterCreate();
+  }
+}
+

这就是生命周期钩子。除了内核本身的生命周期之外,插件也可以有,一般分为加载、运行和卸载三部分,道理类似。当然这里要注意上面两种通信方式的区别, hooks 侧重的是时机,它会在特定阶段触发,而 ctx 则侧重于共享信息的传递。 至于插件和插件的通信,通常来说在这个模式中使用相互影响的插件是不友好的,也不建议这么做,但是如果一定需要的话可以通过内核和一些 hooks 来做桥接。

插件的调度

我们思考一个问题,插件的加载顺序对内核有影响吗?多个插件之间应该如何运行,它们是怎样的关系?看看我们的代码是直接 forEach 遍历执行的,这样的对吗?此处可以停下来思考几秒种 🤔。

通常来说多个插件有以下几种运行方式:

具体采用哪种方式就要看我们的具体业务场景了,比如我们的业务是文档编辑器,插件主要用于扩展文档的 Block 功能,所以顺序不是很重要,相当于上面的第三种模式,主要就是加强功能。那管道式呢,管道式的特点就是上一步的输入是下一步的输出,也就是管道流,这种就是对顺序有要求的,改变插件的顺序,结果也是不一样的,比如我们的业务核心是处理数据,input 就是原始数据,中间的 plugin 就是各种数据处理步骤(比如采样、均化、归一等),output 就是最终处理后的结果;又比如构建工具 gulp 也是如此,有兴趣的可以自行了解下。最后是洋葱式,这个最典型的应用场景就是 koa 中间件了,每个路由都会经过层层中间件,又层层返回;还有 babel 遍历过程中访问器 vistor 的进出过程也是如此。了解上图的三种模式你就大概能知道何时何地加载、运行和销毁插件了。 那我们除了用 use 来使用插件外,还可以用什么方式来注册插件呢?可以用声明式注入,也就是通过配置文件来告诉系统应该去哪里去取什么插件,系统运行时会按照约定的配置去加载对应的插件。比如 babel 就可以通过在配置文件中填写插件名称,运行时就会去自行查找对应的插件并加载(可以想想我们平时开发时的各种 config.js 配置文件)。而编程式的就是系统提供某种注册 api,开发者通过将插件传入该 api 中来完成注册,也就是本文使用的方式。 另外我们再来说个小问题,就是插件的管理,也就是插件池,有时我们会用 map,有时我们会用数组,就像这样:

js
// map: {'pluginName1': plugin1, 'pluginName2': plugin2, ...}
+// 数组:[new Plugin1(), new Plugin2(), ...]
+
// map: {'pluginName1': plugin1, 'pluginName2': plugin2, ...}
+// 数组:[new Plugin1(), new Plugin2(), ...]
+

这两种用法在日常开发中也很常见,那它们有什么区别呢?这就要看你要求了,用 map 一般都是要具名的,目的是为了存取方便,尤其是取,就像缓存的感觉;而如果我们只需要单纯的遍历执行插件用数组就可以了,当然如果复杂度较高的情况下,可以两者一起使用,就像下面这样 👇🏻:

ts
class Core {
+  plugins: IPlugin[] = [];
+  pluginMap: Map<string, IPlugin> = new Map();
+  use(plugin: IPlugin): Core {
+    this.plugins.push(plugin);
+    this.pluginMap.set(plugin.name, plugin);
+    return this;
+  }
+}
+
class Core {
+  plugins: IPlugin[] = [];
+  pluginMap: Map<string, IPlugin> = new Map();
+  use(plugin: IPlugin): Core {
+    this.plugins.push(plugin);
+    this.pluginMap.set(plugin.name, plugin);
+    return this;
+  }
+}
+

安全性和稳定性

因为这种架构的扩展性相当强,并且插件也有能力去修改内核,所以安全性和稳定性的问题也是必须要考虑的问题 😬。 为了保障稳定性,在插件运行异常时,我们应当进行相应的容错处理,由内核来捕获并继续往下执行,不能让系统轻易崩掉,可以给个警告或错误的提示,或者通过系统级事件通知内核 重启 或 关闭 该插件。 关于安全性,我们可以只暴露必要的信息给插件,而不是全部(整个内核),这是什么意思呢,其实就是要搞个简单的沙箱,在前端这边具体点就是用 with+proxy+白名单 来处理,node 用 vm 模块来处理,当然在浏览器中要想完全隔绝是很难的,不过已经有个提案的 api 在路上了,可以期待一下。

presets 预设

presets 这个字眼在前端中应该还是挺常见的,它其实是什么意思呢,中文我们叫预设,本质就是一些插件的集合,亦即 Core + presets(几个插件) ,内核可以有不同的预设,presets1、presets2 等等,每个 presets 就是几个不同插件的组合,主要用途就是方便,不用我们一个一个去引入去设置,比如 babel 的预设、vue 脚手架中的预设。当然除了预设之外,我们也可以将一些必备的插件固化为内置插件,这个度由开发者自己去把握。

应用场景

webpack

js
module.exports = {
+  plugins: [
+    // 用配置的方式注册插件
+    new webpack.ProgressPlugin(),
+    new HtmlWebpackPlugin({ template: "./src/index.html" }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    // 用配置的方式注册插件
+    new webpack.ProgressPlugin(),
+    new HtmlWebpackPlugin({ template: "./src/index.html" }),
+  ],
+};
+
js
const pluginName = "ConsoleLogOnBuildWebpackPlugin";
+
+class ConsoleLogOnBuildWebpackPlugin {
+  apply(compiler) {
+    // apply就是插件的fn,compiler就是内核上下文
+    compiler.hooks.run.tap(pluginName, (compilation) => {
+      // hooks就是发布订阅
+      console.log("The webpack build process is starting!");
+    });
+  }
+}
+
+module.exports = ConsoleLogOnBuildWebpackPlugin;
+
const pluginName = "ConsoleLogOnBuildWebpackPlugin";
+
+class ConsoleLogOnBuildWebpackPlugin {
+  apply(compiler) {
+    // apply就是插件的fn,compiler就是内核上下文
+    compiler.hooks.run.tap(pluginName, (compilation) => {
+      // hooks就是发布订阅
+      console.log("The webpack build process is starting!");
+    });
+  }
+}
+
+module.exports = ConsoleLogOnBuildWebpackPlugin;
+

babel

配置文件方式注册插件

json
{
+  "plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]
+}
+
{
+  "plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]
+}
+

插件开发

js
export default function () {
+  return {
+    visitor: {
+      Identifier(path) {
+        // 插件的执行内容,看起来特殊一些,不过path可以理解为内核上下文
+        const name = path.node.name;
+        path.node.name = name.split("").reverse().join("");
+      },
+    },
+  };
+}
+
export default function () {
+  return {
+    visitor: {
+      Identifier(path) {
+        // 插件的执行内容,看起来特殊一些,不过path可以理解为内核上下文
+        const name = path.node.name;
+        path.node.name = name.split("").reverse().join("");
+      },
+    },
+  };
+}
+

Vue

js
const app = createApp();
+
+app.use(myPlugin, {});
+
const app = createApp();
+
+app.use(myPlugin, {});
+
js
const myPlugin = {
+  install(app, options) {},
+};
+
const myPlugin = {
+  install(app, options) {},
+};
+

类似 jQuery、window 从某种角度上来说也可以看做是内核,通过 $.fn() 或者在 prototype 中扩展方法也是一种插件的形式。

小结

最后我们再来巩固一下这篇文章的核心思想,微内核架构主要由两部分组成: Core + Plugin,其中: Core 需要具备的能力有:

  • 基础功能,视业务情况而定
  • 一些配置信息(环境变量、全局变量)
  • 插件管理:通信、调度、稳定性
  • 生命周期钩子 hooks:约束一些特定阶段,用较少的钩子尽可能覆盖大部分场景 plugin 应该具备的能力有:
  • 能被内核调用
  • 可以更改内核的一些东西
  • 相互独立
  • 插件一般先注册后干预执行,有需要再提供卸载功能 其实微内核架构的核心思想就是在系统内部预留一些入口,系统本身功能不变,但是通过这个入口可以集百家之长以丰富系统自身 🍺。

🔗 原文链接: https://juejin.cn/post/716307803160...

`,58),c=[e];function r(t,B,y,i,F,u){return n(),a("div",null,c)}const d=s(o,[["render",r]]);export{b as __pageData,d as default}; diff --git "a/assets/fragment_\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.md.3d9cf8a5.lean.js" "b/assets/fragment_\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.md.3d9cf8a5.lean.js" new file mode 100644 index 00000000..4ee861c8 --- /dev/null +++ "b/assets/fragment_\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.md.3d9cf8a5.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-09-16-52-36.f7ed461a.png",b=JSON.parse('{"title":"🔥 微内核架构在前端的实现及其应用","description":"","frontmatter":{},"headers":[{"level":2,"title":"前置知识","slug":"前置知识","link":"#前置知识","children":[]},{"level":2,"title":"如何实现","slug":"如何实现","link":"#如何实现","children":[{"level":3,"title":"如何调用","slug":"如何调用","link":"#如何调用","children":[]},{"level":3,"title":"Core 和 Plugin 的实现","slug":"core-和-plugin-的实现","link":"#core-和-plugin-的实现","children":[]},{"level":3,"title":"设计思想","slug":"设计思想","link":"#设计思想","children":[]}]},{"level":2,"title":"重点问题","slug":"重点问题","link":"#重点问题","children":[{"level":3,"title":"通讯问题","slug":"通讯问题","link":"#通讯问题","children":[]},{"level":3,"title":"插件的调度","slug":"插件的调度","link":"#插件的调度","children":[]},{"level":3,"title":"安全性和稳定性","slug":"安全性和稳定性","link":"#安全性和稳定性","children":[]},{"level":3,"title":"presets 预设","slug":"presets-预设","link":"#presets-预设","children":[]}]},{"level":2,"title":"应用场景","slug":"应用场景","link":"#应用场景","children":[{"level":3,"title":"webpack","slug":"webpack","link":"#webpack","children":[]},{"level":3,"title":"babel","slug":"babel","link":"#babel","children":[]},{"level":3,"title":"Vue","slug":"vue","link":"#vue","children":[]}]},{"level":2,"title":"小结","slug":"小结","link":"#小结","children":[]}],"relativePath":"fragment/微内核架构.md","lastUpdated":1712654359000}'),o={name:"fragment/微内核架构.md"},e=l("",58),c=[e];function r(t,B,y,i,F,u){return n(),a("div",null,c)}const d=s(o,[["render",r]]);export{b as __pageData,d as default}; diff --git "a/assets/fragment_\346\216\245\345\217\243\350\256\276\350\256\241.md.49cc865f.js" "b/assets/fragment_\346\216\245\345\217\243\350\256\276\350\256\241.md.49cc865f.js" new file mode 100644 index 00000000..03b366bb --- /dev/null +++ "b/assets/fragment_\346\216\245\345\217\243\350\256\276\350\256\241.md.49cc865f.js" @@ -0,0 +1 @@ +import{_ as t,o as a,c as o,b as e,d as s}from"./app.f983686f.js";const x=JSON.parse('{"title":"接口设计","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/接口设计.md","lastUpdated":1718523144000}'),r={name:"fragment/接口设计.md"},n=e("h1",{id:"接口设计",tabindex:"-1"},[s("接口设计 "),e("a",{class:"header-anchor",href:"#接口设计","aria-hidden":"true"},"#")],-1),c=e("p",null,"防止用户伪造请求:如果接口允许用户直接携带 ID,很容易被恶意用户利用,尝试修改其他用户的信息。使用 cookie(尤其是带有认证信息的 cookie)可以确保请求来自于正确的用户,并且认证信息通常在服务器端进行验证,增加了安全性。",-1),d=[n,c];function _(i,p,l,h,f,m){return a(),o("div",null,d)}const k=t(r,[["render",_]]);export{x as __pageData,k as default}; diff --git "a/assets/fragment_\346\216\245\345\217\243\350\256\276\350\256\241.md.49cc865f.lean.js" "b/assets/fragment_\346\216\245\345\217\243\350\256\276\350\256\241.md.49cc865f.lean.js" new file mode 100644 index 00000000..03b366bb --- /dev/null +++ "b/assets/fragment_\346\216\245\345\217\243\350\256\276\350\256\241.md.49cc865f.lean.js" @@ -0,0 +1 @@ +import{_ as t,o as a,c as o,b as e,d as s}from"./app.f983686f.js";const x=JSON.parse('{"title":"接口设计","description":"","frontmatter":{},"headers":[],"relativePath":"fragment/接口设计.md","lastUpdated":1718523144000}'),r={name:"fragment/接口设计.md"},n=e("h1",{id:"接口设计",tabindex:"-1"},[s("接口设计 "),e("a",{class:"header-anchor",href:"#接口设计","aria-hidden":"true"},"#")],-1),c=e("p",null,"防止用户伪造请求:如果接口允许用户直接携带 ID,很容易被恶意用户利用,尝试修改其他用户的信息。使用 cookie(尤其是带有认证信息的 cookie)可以确保请求来自于正确的用户,并且认证信息通常在服务器端进行验证,增加了安全性。",-1),d=[n,c];function _(i,p,l,h,f,m){return a(),o("div",null,d)}const k=t(r,[["render",_]]);export{x as __pageData,k as default}; diff --git "a/assets/fragment_\346\262\231\347\233\222.md.4cce94e9.js" "b/assets/fragment_\346\262\231\347\233\222.md.4cce94e9.js" new file mode 100644 index 00000000..5019483b --- /dev/null +++ "b/assets/fragment_\346\262\231\347\233\222.md.4cce94e9.js" @@ -0,0 +1,451 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"请设计一个不能操作 DOM 和调接口的环境","description":"","frontmatter":{},"headers":[{"level":2,"title":"实现思路","slug":"实现思路","link":"#实现思路","children":[]},{"level":2,"title":"如何禁止开发者操作 DOM ?","slug":"如何禁止开发者操作-dom","link":"#如何禁止开发者操作-dom","children":[]},{"level":2,"title":"如何禁止开发者调接口 ?","slug":"如何禁止开发者调接口","link":"#如何禁止开发者调接口","children":[]},{"level":2,"title":"最终方案:沙箱(Sandbox)","slug":"最终方案-沙箱-sandbox","link":"#最终方案-沙箱-sandbox","children":[]},{"level":2,"title":"沙箱的多种实现方式","slug":"沙箱的多种实现方式","link":"#沙箱的多种实现方式","children":[{"level":3,"title":"简陋的沙箱","slug":"简陋的沙箱","link":"#简陋的沙箱","children":[]},{"level":3,"title":"With + Proxy 实现沙箱","slug":"with-proxy-实现沙箱","link":"#with-proxy-实现沙箱","children":[]},{"level":3,"title":"天然的优质沙箱(iframe)","slug":"天然的优质沙箱-iframe","link":"#天然的优质沙箱-iframe","children":[]}]},{"level":2,"title":"需求实现","slug":"需求实现","link":"#需求实现","children":[]},{"level":2,"title":"持续优化","slug":"持续优化","link":"#持续优化","children":[]}],"relativePath":"fragment/沙盒.md","lastUpdated":1713239860000}'),p={name:"fragment/沙盒.md"},o=l(`

请设计一个不能操作 DOM 和调接口的环境

实现思路

1)利用 iframe 创建沙箱,取出其中的原生浏览器全局对象作为沙箱的全局对象

2)设置一个黑名单,若访问黑名单中的变量,则直接报错,实现阻止\\隔离的效果

3)在黑名单中添加 document 字段,来实现禁止开发者操作 DOM

4)在黑名单中添加 XMLHttpRequest、fetch、WebSocket 字段,实现禁用原生的方式调用接口

5)若访问当前全局对象中不存在的变量,则直接报错,实现禁用三方库调接口

6)最后还要拦截对 window 对象的访问,防止通过 window.document 来操作 DOM,避免沙箱逃逸

下面聊一聊,为何这样设计,以及中间会遇到什么问题

如何禁止开发者操作 DOM ?

在页面中,可以通过 document 对象来获取 HTML 元素,进行增删改查的 DOM 操作

如何禁止开发者操作 DOM,转化为如何阻止开发者获取 document 对象

1)传统思路

简单粗暴点,直接修改 window.document 的值,让开发者无法获取 document

js
// 将document设置为null
+window.document = null;
+
+// 设置无效,打印结果还是document
+console.log(window.document);
+
+// 删除document
+delete window.document;
+
+// 删除无效,打印结果还是document
+console.log(window.document);
+
// 将document设置为null
+window.document = null;
+
+// 设置无效,打印结果还是document
+console.log(window.document);
+
+// 删除document
+delete window.document;
+
+// 删除无效,打印结果还是document
+console.log(window.document);
+

好吧,document 修改不了也删除不了 🤔

使用 Object.getOwnPropertyDescriptor 查看,会发现 window.document 的 configurable 属性为 false(不可配置的)

js
Object.getOwnPropertyDescriptor(window, "document");
+// {get: ƒ, set: undefined, enumerable: true, configurable: false}
+
Object.getOwnPropertyDescriptor(window, "document");
+// {get: ƒ, set: undefined, enumerable: true, configurable: false}
+

configurable 决定了是否可以修改属性描述对象,也就是说,configurable 为 false 时,value、writable、enumerable 和 configurable 都不能被修改,以及无法被删除

2)有点高大上的思路

既然 document 对象修改不了,那如果环境中原本就没有 document 对象,是不是就可以实现该需求? 说到环境中没有 document 对象, Web Worker 直呼内行,作者曾在 《一文彻底了解 Web Worker,十万、百万条数据都是弟弟 🔥》 中聊过如何使用 Web Worker,和对应的特性

并且 Web Worker 更狠,不但没有 document 对象,连 window 对象也没有 😂

在 worker 线程中打印 window

js
onmessage = function (e) {
+  console.log(window); // 浏览器直接报错
+  postMessage();
+};
+
onmessage = function (e) {
+  console.log(window); // 浏览器直接报错
+  postMessage();
+};
+

在 Web Worker 线程的运行环境中无法访问 document 对象,这一条符合当前的需求,但是该环境中能获取 XMLHttpRequest 对象,可以发送 ajax 请求,不符合不能调接口的要求

如何禁止开发者调接口 ?

常规调接口方式有:

1)原生方式:XMLHttpRequest、fetch、WebSocket、jsonp、form 表单

2)三方实现:axios、jquery、request 等众多开源库

禁用原生方式调接口的思路:

1)XMLHttpRequest、fetch、WebSocket 这几种情况,可以禁止用户访问这些对象

2)jsonp、form 这两种方式,需要创建 script 或 form 标签,依然可以通过禁止开发者操作 DOM 的方式解决,不需要单独处理

如何禁用三方库调接口呢?三方库很多,没办法全部列出来,来进行逐一排除,禁止调接口的路好像也被封死了……😰

最终方案:沙箱(Sandbox)

通过上面的分析,传统的思路确实解决不了当前的需求

阻止开发者操作 DOM 和调接口,沙箱说:这个我熟啊,拦截隔离这类的活,我最拿手了 😀

沙箱(Sandbox) 是一种安全机制,为运行中的程序提供隔离环境,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行

前端沙箱的使用场景:

1)Chrome 浏览器打开的每个页面就是一个沙箱,保证彼此独立互不影响

2)执行 jsonp 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码

3)Vue 模板表达式的计算是运行在一个沙箱中,模板字符串中的表达式只能获取部分全局对象,详情见 源码

4)微前端框架 qiankun ,为了实现 js 隔离,在多种场景下均使用了沙箱

沙箱的多种实现方式

先聊下 js with 这个关键字:作用在于改变作用域,可以将某个对象添加到作用域链的顶部

with 对于沙箱的意义:可以实现所有变量均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值,相当于做了一层拦截,实现隔离的效果

简陋的沙箱

实现这样一个沙箱,要求程序中访问的所有变量,均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值

举个 🌰: ctx 作为执行上下文对象,待执行程序 code 可以访问到的变量,必须都来自 ctx 对象

js
// ctx 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 待执行程序
+const code = \`func(foo)\`;
+
// ctx 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 待执行程序
+const code = \`func(foo)\`;
+

沙箱示例:

js
// 定义全局变量foo
+var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 非常简陋的沙箱
+function veryPoorSandbox(code, ctx) {
+  // 使用with,将eval函数执行时的执行上下文指定为ctx
+  with (ctx) {
+    eval(code);
+  }
+}
+
+// 待执行程序
+const code = \`func(foo)\`;
+
+veryPoorSandbox(code, ctx);
+// 打印结果:"f1",不是最外层的全局变量"foo1"
+
// 定义全局变量foo
+var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 非常简陋的沙箱
+function veryPoorSandbox(code, ctx) {
+  // 使用with,将eval函数执行时的执行上下文指定为ctx
+  with (ctx) {
+    eval(code);
+  }
+}
+
+// 待执行程序
+const code = \`func(foo)\`;
+
+veryPoorSandbox(code, ctx);
+// 打印结果:"f1",不是最外层的全局变量"foo1"
+

这个沙箱有一个明显的问题,若提供的 ctx 上下文对象中,没有找到某个变量时,代码仍会沿着作用域链一层层向上查找 假如上文示例中的 ctx 对象没有设置 foo 属性,打印的结果还是外层作用域的 foo1

With + Proxy 实现沙箱

希望沙箱中的代码只在手动提供的上下文对象中查找变量,如果上下文对象中不存在该变量,则提示对应的错误

举个 🌰: ctx 作为执行上下文对象,待执行程序 code 可以访问到的变量,必须都来自 ctx 对象,如果 ctx 对象中不存在该变量,直接报错,不再通过作用域链向上查找

实现步骤:

1)使用 Proxy.has() 来拦截 with 代码块中的任意变量的访问

2)设置一个白名单,在白名单内的变量可以正常走作用域链的访问方式,不在白名单内的变量,会继续判断是否存 ctx 对象中,存在则正常访问,不存在则直接报错

3)使用 new Function 替代 eval,使用 new Function() 运行代码比 eval 更为好一些,函数的参数提供了清晰的接口来运行代码

new Function 与 eval 的区别

沙箱示例:

js
var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+};
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(shadow) {" + code + "}";
+  return new Function("shadow", code);
+}
+
+// 可访问全局作用域的白名单列表
+const access_white_list = ["func"];
+
+// 待执行程序
+const code = \`func(foo)\`;
+
+// 执行上下文对象的代理对象
+const ctxProxy = new Proxy(ctx, {
+  has: (target, prop) => {
+    // has 可以拦截 with 代码块中任意属性的访问
+    if (access_white_list.includes(prop)) {
+      // 在可访问的白名单内,可继续向上查找
+      return target.hasOwnProperty(prop);
+    }
+    if (!target.hasOwnProperty(prop)) {
+      throw new Error(\`Not found - \${prop}!\`);
+    }
+    return true;
+  },
+});
+
+// 没那么简陋的沙箱
+function littlePoorSandbox(code, ctx) {
+  // 将 this 指向手动构造的全局代理对象
+  withedYourCode(code).call(ctx, ctx);
+}
+littlePoorSandbox(code, ctxProxy);
+
+// 执行func(foo),报错: Uncaught Error: Not found - foo!
+
var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+};
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(shadow) {" + code + "}";
+  return new Function("shadow", code);
+}
+
+// 可访问全局作用域的白名单列表
+const access_white_list = ["func"];
+
+// 待执行程序
+const code = \`func(foo)\`;
+
+// 执行上下文对象的代理对象
+const ctxProxy = new Proxy(ctx, {
+  has: (target, prop) => {
+    // has 可以拦截 with 代码块中任意属性的访问
+    if (access_white_list.includes(prop)) {
+      // 在可访问的白名单内,可继续向上查找
+      return target.hasOwnProperty(prop);
+    }
+    if (!target.hasOwnProperty(prop)) {
+      throw new Error(\`Not found - \${prop}!\`);
+    }
+    return true;
+  },
+});
+
+// 没那么简陋的沙箱
+function littlePoorSandbox(code, ctx) {
+  // 将 this 指向手动构造的全局代理对象
+  withedYourCode(code).call(ctx, ctx);
+}
+littlePoorSandbox(code, ctxProxy);
+
+// 执行func(foo),报错: Uncaught Error: Not found - foo!
+

天然的优质沙箱(iframe)

iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离 利用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法,可以把 iframe.contentWindow 作为沙箱执行的全局 window 对象

沙箱示例:

js
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(sharedState) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // sandboxGlobal作为沙箱运行时的全局对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      has: (target, prop) => {
+        // has 可以拦截 with 代码块中任意属性的访问
+        if (sharedState.includes(prop)) {
+          // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
+          return false;
+        }
+
+        // 如果没有该属性,直接报错
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(\`Not find: \${prop}!\`);
+        }
+
+        // 属性存在,返回sandboxGlobal中的值
+        return true;
+      },
+    });
+  }
+}
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+function maybeAvailableSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 要执行的代码
+const code = \`
+  console.log(history == window.history) // 打印结果为 false
+  window.abc = 'sandbox'
+  Object.prototype.toString = () => {
+      console.log('Traped!')
+  }
+  console.log(window.abc) // sandbox
+\`;
+
+// sharedGlobal作为与外部执行环境共享的全局对象
+// code中获取的history为最外层作用域的history
+const sharedGlobal = ["history"];
+
+const globalProxy = new SandboxGlobalProxy(sharedGlobal);
+
+maybeAvailableSandbox(code, globalProxy);
+
+// 对外层的window对象没有影响
+console.log(window.abc); // undefined
+Object.prototype.toString(); // 并没有打印 Traped
+
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(sharedState) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // sandboxGlobal作为沙箱运行时的全局对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      has: (target, prop) => {
+        // has 可以拦截 with 代码块中任意属性的访问
+        if (sharedState.includes(prop)) {
+          // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
+          return false;
+        }
+
+        // 如果没有该属性,直接报错
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(\`Not find: \${prop}!\`);
+        }
+
+        // 属性存在,返回sandboxGlobal中的值
+        return true;
+      },
+    });
+  }
+}
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+function maybeAvailableSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 要执行的代码
+const code = \`
+  console.log(history == window.history) // 打印结果为 false
+  window.abc = 'sandbox'
+  Object.prototype.toString = () => {
+      console.log('Traped!')
+  }
+  console.log(window.abc) // sandbox
+\`;
+
+// sharedGlobal作为与外部执行环境共享的全局对象
+// code中获取的history为最外层作用域的history
+const sharedGlobal = ["history"];
+
+const globalProxy = new SandboxGlobalProxy(sharedGlobal);
+
+maybeAvailableSandbox(code, globalProxy);
+
+// 对外层的window对象没有影响
+console.log(window.abc); // undefined
+Object.prototype.toString(); // 并没有打印 Traped
+

可以看到,沙箱中对 window 的所有操作,都没有影响到外层的 window,实现了隔离的效果 😘

需求实现

继续使用上述的 iframe 标签来创建沙箱,代码主要修改点

1)设置 blacklist 黑名单,添加 document、XMLHttpRequest、fetch、WebSocket 来禁止开发者操作 DOM 和调接口

2)判断要访问的变量,是否在当前环境的 window 对象中,不在的直接报错,实现禁止通过三方库调接口

js
// 设置黑名单
+const blacklist = ["document", "XMLHttpRequest", "fetch", "WebSocket"];
+
+// 黑名单中的变量禁止访问
+if (blacklist.includes(prop)) {
+  throw new Error(\`Can't use: \${prop}!\`);
+}
+
// 设置黑名单
+const blacklist = ["document", "XMLHttpRequest", "fetch", "WebSocket"];
+
+// 黑名单中的变量禁止访问
+if (blacklist.includes(prop)) {
+  throw new Error(\`Can't use: \${prop}!\`);
+}
+

但有个很严重的漏洞,如果开发者通过 window.document 来获取 document 对象,依然是可以操作 DOM 的 😱 需要在黑名单中加入 window 字段,来解决这个沙箱逃逸的漏洞,虽然把 window 加入了黑名单,但 window 上的方法,如 open、close 等,依然是可以正常获取使用的

最终代码:

js
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(blacklist) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // 获取当前HTMLIFrameElement的Window对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      // has 可以拦截 with 代码块中任意属性的访问
+      has: (target, prop) => {
+        // 黑名单中的变量禁止访问
+        if (blacklist.includes(prop)) {
+          throw new Error(\`Can't use: \${prop}!\`);
+        }
+        // sandboxGlobal对象上不存在的属性,直接报错,实现禁用三方库调接口
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(\`Not find: \${prop}!\`);
+        }
+
+        // 返回true,获取当前提供上下文对象中的变量;如果返回false,会继续向上层作用域链中查找
+        return true;
+      },
+    });
+  }
+}
+
+// 使用with关键字,来改变作用域
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+
+// 将指定的上下文对象,添加到待执行代码作用域的顶部
+function makeSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 待执行的代码code,获取document对象
+const code = \`console.log(document)\`;
+
+// 设置黑名单
+// 经过小伙伴的指导,新添加Image字段,禁止使用new Image来调接口
+const blacklist = [
+  "window",
+  "document",
+  "XMLHttpRequest",
+  "fetch",
+  "WebSocket",
+  "Image",
+];
+
+// 将globalProxy对象,添加到新环境作用域链的顶部
+const globalProxy = new SandboxGlobalProxy(blacklist);
+
+makeSandbox(code, globalProxy);
+
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(blacklist) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // 获取当前HTMLIFrameElement的Window对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      // has 可以拦截 with 代码块中任意属性的访问
+      has: (target, prop) => {
+        // 黑名单中的变量禁止访问
+        if (blacklist.includes(prop)) {
+          throw new Error(\`Can't use: \${prop}!\`);
+        }
+        // sandboxGlobal对象上不存在的属性,直接报错,实现禁用三方库调接口
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(\`Not find: \${prop}!\`);
+        }
+
+        // 返回true,获取当前提供上下文对象中的变量;如果返回false,会继续向上层作用域链中查找
+        return true;
+      },
+    });
+  }
+}
+
+// 使用with关键字,来改变作用域
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+
+// 将指定的上下文对象,添加到待执行代码作用域的顶部
+function makeSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 待执行的代码code,获取document对象
+const code = \`console.log(document)\`;
+
+// 设置黑名单
+// 经过小伙伴的指导,新添加Image字段,禁止使用new Image来调接口
+const blacklist = [
+  "window",
+  "document",
+  "XMLHttpRequest",
+  "fetch",
+  "WebSocket",
+  "Image",
+];
+
+// 将globalProxy对象,添加到新环境作用域链的顶部
+const globalProxy = new SandboxGlobalProxy(blacklist);
+
+makeSandbox(code, globalProxy);
+

持续优化

经过与评论区小伙伴的交流,可以通过 new Image() 调接口,确实是个漏洞

js
// 不需要创建DOM 发送图片请求
+let img = new Image();
+img.src = "http://www.test.com/img.gif";
+
// 不需要创建DOM 发送图片请求
+let img = new Image();
+img.src = "http://www.test.com/img.gif";
+

黑名单中添加'Image'字段,堵上这个漏洞

`,79),e=[o];function c(r,t,B,y,F,i){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{u as __pageData,A as default}; diff --git "a/assets/fragment_\346\262\231\347\233\222.md.4cce94e9.lean.js" "b/assets/fragment_\346\262\231\347\233\222.md.4cce94e9.lean.js" new file mode 100644 index 00000000..deda27d7 --- /dev/null +++ "b/assets/fragment_\346\262\231\347\233\222.md.4cce94e9.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"请设计一个不能操作 DOM 和调接口的环境","description":"","frontmatter":{},"headers":[{"level":2,"title":"实现思路","slug":"实现思路","link":"#实现思路","children":[]},{"level":2,"title":"如何禁止开发者操作 DOM ?","slug":"如何禁止开发者操作-dom","link":"#如何禁止开发者操作-dom","children":[]},{"level":2,"title":"如何禁止开发者调接口 ?","slug":"如何禁止开发者调接口","link":"#如何禁止开发者调接口","children":[]},{"level":2,"title":"最终方案:沙箱(Sandbox)","slug":"最终方案-沙箱-sandbox","link":"#最终方案-沙箱-sandbox","children":[]},{"level":2,"title":"沙箱的多种实现方式","slug":"沙箱的多种实现方式","link":"#沙箱的多种实现方式","children":[{"level":3,"title":"简陋的沙箱","slug":"简陋的沙箱","link":"#简陋的沙箱","children":[]},{"level":3,"title":"With + Proxy 实现沙箱","slug":"with-proxy-实现沙箱","link":"#with-proxy-实现沙箱","children":[]},{"level":3,"title":"天然的优质沙箱(iframe)","slug":"天然的优质沙箱-iframe","link":"#天然的优质沙箱-iframe","children":[]}]},{"level":2,"title":"需求实现","slug":"需求实现","link":"#需求实现","children":[]},{"level":2,"title":"持续优化","slug":"持续优化","link":"#持续优化","children":[]}],"relativePath":"fragment/沙盒.md","lastUpdated":1713239860000}'),p={name:"fragment/沙盒.md"},o=l("",79),e=[o];function c(r,t,B,y,F,i){return n(),a("div",null,e)}const A=s(p,[["render",c]]);export{u as __pageData,A as default}; diff --git "a/assets/fragment_\351\273\221\347\231\275.md.e8f52bb8.js" "b/assets/fragment_\351\273\221\347\231\275.md.e8f52bb8.js" new file mode 100644 index 00000000..c1aaf1f6 --- /dev/null +++ "b/assets/fragment_\351\273\221\347\231\275.md.e8f52bb8.js" @@ -0,0 +1,37 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const h=JSON.parse('{"title":"一行代码,让网页变为黑白配色","description":"","frontmatter":{},"headers":[{"level":2,"title":"一行代码","slug":"一行代码","link":"#一行代码","children":[]},{"level":2,"title":"原理","slug":"原理","link":"#原理","children":[]},{"level":2,"title":"兼容性","slug":"兼容性","link":"#兼容性","children":[]},{"level":2,"title":"filter 样式加到 html 还是 body 上","slug":"filter-样式加到-html-还是-body-上","link":"#filter-样式加到-html-还是-body-上","children":[]}],"relativePath":"fragment/黑白.md","lastUpdated":1713159515000}'),e={name:"fragment/黑白.md"},p=l(`

一行代码,让网页变为黑白配色

让网页变为黑白配色,是个常见的诉求。而且往往是突如其来的诉求,是无法预知的。当发生这样的需求时,我们需要迅速完成变更发布。

一行代码

css
div {
+  filter: grayscale(1);
+}
+
div {
+  filter: grayscale(1);
+}
+

为了使整个网页生效,你可以把它放在 <html> 标签的样式里。直接写到 html 文件内,例如:

html
<style>
+  html {
+    filter: grayscale(1);
+  }
+</style>
+
<style>
+  html {
+    filter: grayscale(1);
+  }
+</style>
+

你也可以用内联样式,只要没用 important CSS 语法,内联样式优先级最高:

html
<html style="filter:grayscale(1)">
+  ...
+</html>
+
<html style="filter:grayscale(1)">
+  ...
+</html>
+

为了更好的兼容性,你可以补一个带 -webkit- 前缀的样式,放在 filter 后面:

html
<html style="filter:grayscale(1);-webkit-filter:grayscale(1)">
+  ...
+</html>
+
<html style="filter:grayscale(1);-webkit-filter:grayscale(1)">
+  ...
+</html>
+

原理

我们使用了 CSS 特性 filter,并用了 grayscale 对图片进行灰度转换,允许有一个参数,可以是数字(0 到 1)或百分比,0% 到 100% 之间的值会使灰度线性变化。

如果你不想完全灰掉。可以设置个相对小的数字。

兼容性

我们使用了 CSS 特性 filter,兼容性还不错

如果你想获得更好的兼容性,可以加一个前缀 -webkit- :

css
div {
+  filter: grayscale(0.95);
+  -webkit-filter: grayscale(0.95);
+}
+
div {
+  filter: grayscale(0.95);
+  -webkit-filter: grayscale(0.95);
+}
+

filter 样式加到 html 还是 body 上

把 filter 样式加到了 <body> 元素上。通常这没有问题。

但如果你的网页内有「绝对和固定定位」元素,一定要把 filter 样式加到 <html> 上。 原因见: drafts.fxtf.org/filter-effe…

引用:

A value other than none for the filter property results in the creation of a >containing block for absolute and fixed positioned descendants unless the element it > applies to is a document root element in the current browsing context.

翻译:

若 filter 属性的值不是 none,会给「绝对和固定定位的后代」创建一个 containing block

除非 filter 对应的元素是「当前浏览上下文中的文档根元素」(即 <html> )。

因此,兼容性最好的方法是把 filter 样式加到 <html> 上。这样不会影响「绝对和固定定位的后代」。 这里小程序有个坑,如果你的页面代码有「绝对和固定定位的后代」,就不能把 filter 样式 加到 <page> 上,而是要找个元素,这个元素没有「绝对和固定定位的后代」,你可以把 filter 样式加到这个元素上。

`,25),o=[p];function t(r,c,i,B,y,d){return a(),n("div",null,o)}const b=s(e,[["render",t]]);export{h as __pageData,b as default}; diff --git "a/assets/fragment_\351\273\221\347\231\275.md.e8f52bb8.lean.js" "b/assets/fragment_\351\273\221\347\231\275.md.e8f52bb8.lean.js" new file mode 100644 index 00000000..2bc8937b --- /dev/null +++ "b/assets/fragment_\351\273\221\347\231\275.md.e8f52bb8.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const h=JSON.parse('{"title":"一行代码,让网页变为黑白配色","description":"","frontmatter":{},"headers":[{"level":2,"title":"一行代码","slug":"一行代码","link":"#一行代码","children":[]},{"level":2,"title":"原理","slug":"原理","link":"#原理","children":[]},{"level":2,"title":"兼容性","slug":"兼容性","link":"#兼容性","children":[]},{"level":2,"title":"filter 样式加到 html 还是 body 上","slug":"filter-样式加到-html-还是-body-上","link":"#filter-样式加到-html-还是-body-上","children":[]}],"relativePath":"fragment/黑白.md","lastUpdated":1713159515000}'),e={name:"fragment/黑白.md"},p=l("",25),o=[p];function t(r,c,i,B,y,d){return a(),n("div",null,o)}const b=s(e,[["render",t]]);export{h as __pageData,b as default}; diff --git "a/assets/front-end-engineering_CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.md.8d9a6c7d.js" "b/assets/front-end-engineering_CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.md.8d9a6c7d.js" new file mode 100644 index 00000000..027e83f6 --- /dev/null +++ "b/assets/front-end-engineering_CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.md.8d9a6c7d.js" @@ -0,0 +1,2969 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const d=JSON.parse('{"title":"CSS 预处理器之 SCSS","description":"","frontmatter":{},"headers":[{"level":2,"title":"概述","slug":"概述","link":"#概述","children":[]},{"level":2,"title":"CSS 预处理器","slug":"css-预处理器","link":"#css-预处理器","children":[{"level":3,"title":"预处理器基本介绍","slug":"预处理器基本介绍","link":"#预处理器基本介绍","children":[]},{"level":3,"title":"Sass 快速入门","slug":"sass-快速入门","link":"#sass-快速入门","children":[]}]},{"level":2,"title":"Sass 基础语法","slug":"sass-基础语法","link":"#sass-基础语法","children":[{"level":3,"title":"注释","slug":"注释","link":"#注释","children":[]},{"level":3,"title":"变量","slug":"变量","link":"#变量","children":[]},{"level":3,"title":"数据类型","slug":"数据类型","link":"#数据类型","children":[]},{"level":3,"title":"嵌套语法","slug":"嵌套语法","link":"#嵌套语法","children":[]},{"level":3,"title":"插值语法","slug":"插值语法","link":"#插值语法","children":[]},{"level":3,"title":"运算","slug":"运算","link":"#运算","children":[]}]},{"level":2,"title":"Sass 控制指令","slug":"sass-控制指令","link":"#sass-控制指令","children":[{"level":3,"title":"三元运算符","slug":"三元运算符","link":"#三元运算符","children":[]},{"level":3,"title":"三元运算符","slug":"三元运算符-1","link":"#三元运算符-1","children":[]},{"level":3,"title":"@if 分支","slug":"if-分支","link":"#if-分支","children":[]},{"level":3,"title":"@for 循环","slug":"for-循环","link":"#for-循环","children":[]},{"level":3,"title":"@while 循环","slug":"while-循环","link":"#while-循环","children":[]},{"level":3,"title":"@each 循环","slug":"each-循环","link":"#each-循环","children":[]}]},{"level":2,"title":"Sass 混合指令","slug":"sass-混合指令","link":"#sass-混合指令","children":[{"level":3,"title":"混合指令基本的使用","slug":"混合指令基本的使用","link":"#混合指令基本的使用","children":[]},{"level":3,"title":"混合指令的参数","slug":"混合指令的参数","link":"#混合指令的参数","children":[]},{"level":3,"title":"@content","slug":"content","link":"#content","children":[]}]},{"level":2,"title":"Sass 函数指令","slug":"sass-函数指令","link":"#sass-函数指令","children":[{"level":3,"title":"自定义函数","slug":"自定义函数","link":"#自定义函数","children":[]},{"level":3,"title":"内置函数","slug":"内置函数","link":"#内置函数","children":[]}]},{"level":2,"title":"@规则","slug":"规则","link":"#规则","children":[{"level":3,"title":"@import","slug":"import","link":"#import","children":[]},{"level":3,"title":"@media","slug":"media","link":"#media","children":[]},{"level":3,"title":"@extend","slug":"extend","link":"#extend","children":[]},{"level":3,"title":"@at-root","slug":"at-root","link":"#at-root","children":[]},{"level":3,"title":"@debug、@warn、@error","slug":"debug、-warn、-error","link":"#debug、-warn、-error","children":[]}]},{"level":2,"title":"Sass 最佳实践与展望","slug":"sass-最佳实践与展望","link":"#sass-最佳实践与展望","children":[{"level":3,"title":"最佳实践","slug":"最佳实践","link":"#最佳实践","children":[]},{"level":3,"title":"Sass 未来发展","slug":"sass-未来发展","link":"#sass-未来发展","children":[]}]}],"relativePath":"front-end-engineering/CSS 预处理器之SCSS.md","lastUpdated":1715070178000}'),p={name:"front-end-engineering/CSS 预处理器之SCSS.md"},o=l(`

CSS 预处理器之 SCSS

概述

前端目前要做的事情是越来越多了,实际上有一部分工作是和业务逻辑无关的

  • 文件优化:压缩 JavaScriptCSSHTML 代码,压缩合并图片等。
  • 代码转换:将 TypeScript/ES 6 编译成 JavaScript、将 SCSS 编译成 CSS 等。
  • 代码优化:为 CSS 代码添加兼容性前缀等。

这些工作虽然和业务逻辑无关,但是你又不得不做。既然这些工作都要做,那么谁来做这些事情?

这里我们会通过一些工具来完成这些事情,但是这里又遇到一个问题,往往上面的这些事情需要好几个工作来完成。这里就会存在一种情况:需要先将项目拖入到工具 A 进行处理,拖入到工具 B 进行处理、C、D、E...

上面的步骤显得非常的麻烦,我们期望有那么一个工具能够帮助我们把上面的事情按照一定的顺序自动化的完成,这个工具就是我们前端构建工具。

总的来讲,“构建工具”里面“构建”二字是一个重点,这个工具究竟构建了一个啥?

将我们开发环境下的项目代码构建为能够部署上线的代码

通过构建工具,我们就可以省去繁杂的步骤,直接一条指令就能将开发环境的项目构建为生产环境的项目代码,之后要做的就是部署上线即可。

目前前端领域有非常的多的构建工具,整体来讲可以分为三代:

  • 第一代构建工具:以 grunt、gulp 为代表的构建工具
  • 第二代构建工具:以 webpack 为代表的构建工具
  • 第三代构建工具:以 Vite 为代表的构建工具
  1. 模块化

模块化是将 CSS 代码分解成独立的、可重用的模块,从而提高代码的可维护性和可读性。通常,每个模块都关注一个特定的功能或组件的样式。这有助于减少样式之间的依赖和冲突,也使得找到和修改相关样式变得更容易。模块化的实现可以通过原生的 CSS 文件拆分,或使用预处理器(如 Sass)的功能(例如 @import 和 @use)来实现。

  1. 命名规范

CSS 类名和选择器定义一致的命名规范有助于提高代码的可读性和可维护性。

以下是一些常见的命名规范:

BEMBlock, Element, Modifier):BEM 是一种命名规范,将类名分为三个部分:块(Block)、元素(Element)和修饰符(Modifier)。这种方法有助于表示组件之间的层级关系和状态变化。例如,navigation__link--active

OOCSS(面向对象的 CSS):OOCSS 旨在将可重用的样式划分为独立的“对象”,从而提高代码的可维护性和可扩展性。这种方法强调将结构(如布局)与皮肤(如颜色和字体样式)分离。这样可以让你更容易地复用和组合样式,创建更灵活的 UI 组件。

html
<button class="btn btn--primary">Primary Button</button>
+<button class="btn btn--secondary">Secondary Button</button>
+
<button class="btn btn--primary">Primary Button</button>
+<button class="btn btn--secondary">Secondary Button</button>
+
css
/* 结构样式 */
+.btn {
+  display: inline-block;
+  padding: 10px 20px;
+  border: none;
+  cursor: pointer;
+  font-size: 16px;
+}
+
+/* 皮肤样式 */
+.btn--primary {
+  background-color: blue;
+  color: white;
+}
+
+.btn--secondary {
+  background-color: gray;
+  color: white;
+}
+
/* 结构样式 */
+.btn {
+  display: inline-block;
+  padding: 10px 20px;
+  border: none;
+  cursor: pointer;
+  font-size: 16px;
+}
+
+/* 皮肤样式 */
+.btn--primary {
+  background-color: blue;
+  color: white;
+}
+
+.btn--secondary {
+  background-color: gray;
+  color: white;
+}
+

SMACSS(可扩展和模块化的 CSS 架构):是一种 CSS 编写方法,旨在提高 CSS 代码的可维护性、可扩展性和灵活性。SMACSS 将样式分为五个基本类别:Base、Layout、Module、StateTheme。这有助于组织 CSS 代码并使其易于理解和修改。

  • Base:包含全局样式和元素默认样式,例如:浏览器重置、全局字体设置等。
  • Layout:描述页面布局的大致结构,例如:页面分区、网格系统等。
  • Module:表示可重用的 UI 组件,例如:按钮、卡片、表单等。
  • State:表示 UI 组件的状态,例如:激活、禁用、隐藏等。通常,状态类会与其他类一起使用以修改其显示。
  • Theme:表示 UI 组件的视觉样式,例如:颜色、字体等。通常,主题类用于支持多个主题或在不同上下文中使用相同的组件。
  1. 预处理器

CSS 预处理器(如 Sass、LessStylus)是一种编程式的 CSS 语言,可以在开发过程中提供更高级的功能,如变量、嵌套、函数和混合等。预处理器将这些扩展语法编译为普通的 CSS 代码,以便浏览器能够解析。

  1. 后处理器

CSS 后处理器(如 PostCSS)可以在生成的 CSS 代码上执行各种操作,如添加浏览器前缀、优化规则和转换现代 CSS 功能以兼容旧浏览器等。它通常与构建工具(例如 Webpack)一起使用,以自动化这些任务。

  1. 代码优化

代码优化旨在减少 CSS 文件的大小、删除无用代码和提高性能。一些常见的优化工具包括:

  • CSSOCSSO 是一个 CSS 优化工具,可以压缩代码、删除冗余规则和合并相似的声明。

  • PurgeCSSPurgeCSS 是一个用于删除无用 CSS 规则的工具。它通过分析项目的 HTML、JS 和模板文件来检测实际使用的样式,并删除未使用的样式。

  • clean-cssclean-css 是一个高效的 CSS 压缩工具,可以删除空格、注释和重复的规则等,以减小文件大小。

  1. 构建工具和自动化

构建工具(如 WebpackGulpGrunt)可以帮助您自动化开发过程中的任务,如编译预处理器代码、合并和压缩 CSS 文件、优化图片等。这可以提高开发效率,确保项目的一致性,并简化部署流程。这些工具通常可以通过插件和配置来定制,以满足项目的特定需求。

  1. 响应式设计和移动优先

响应式设计是一种使网站在不同设备和屏幕尺寸上都能保持良好显示效果的方法。这通常通过使用媒体查询、弹性布局(如 FlexboxCSS Grid)和其他技术实现。移动优先策略是从最小屏幕尺寸(如手机)开始设计样式,然后逐步增强以适应更大的屏幕尺寸(如平板和桌面)。这种方法有助于保持代码的简洁性,并确保网站在移动设备上的性能优先。

  1. 设计系统和组件库

设计系统是一套规范,为开发人员和设计师提供统一的样式指南(如颜色、排版、间距等)、组件库和最佳实践。这有助于提高项目的一致性、可维护性和协作效率。组件库通常包含一系列预定义的可复用组件(如按钮、输入框、卡片等),可以快速集成到项目中。一些流行的组件库和 UI 框架包括 Bootstrap、FoundationMaterial-UI 等。

因此整个 CSS 都是逐渐在向工程化靠近的,上面所罗列的那么几点都是 CSS 在工程化方面的一些体现。

CSS 预处理器

预处理器基本介绍

平时在工作的时候,经常会面临这样的情况:需要书写很多的样式代码,特别是面对比较大的项目的时候,代码量会急剧提升,普通的 CSS 书写方式不方便维护以及扩展还有复用。

通过 CSS 预处理技术就可以解决上述的问题。基于预处理技术的语言,我们可以称之为 CSS 预处理语言,该语言会为你提供一套增强语法,我们在书写 CSS 的时候就使用增强语法来书写,书写完毕后通过预处理技术编译为普通的 CSS 语言即可。

CSS 预处理器的出现,解决了如下的问题:

  • 代码组织:通过嵌套、模块化和文件引入等功能,使 CSS 代码结构更加清晰,便于管理和维护。
  • 变量和函数:支持自定义变量和函数,增强代码的可重用性和一致性。
  • 代码简洁:通过简洁的语法结构和内置函数,减少代码冗余,提高开发效率。
  • 易于扩展:可以通过插件系统扩展预处理器的功能,方便地添加新特性。

目前前端领域常见的 CSS 预处理器有三个:

  • Sass
  • Less
  • Stylus

下面我们对这三个 CSS 预处理器做一个简单的介绍。

Sass

Sass 是最早出现的 CSS 预处理器,出现的时间为 2006 年,该预处理器有两种语法格式:

  • 缩进式语法(2006 年)
sass
$primary-color: #4CAF50
+
+.container
+	background-color: $primary-color
+	padding: 20px
+
+  .title
+    font-size: 24px
+    color: white
+
$primary-color: #4CAF50
+
+.container
+	background-color: $primary-color
+	padding: 20px
+
+  .title
+    font-size: 24px
+    color: white
+
  • 类 CSS 语法(2009 年)
scss
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+

Sass 提供了很多丰富的功能,例如有声明变量、嵌套、混合、继承、函数等,另外 Sass 还有强大的社区支持以及丰富的插件资源,因此 Sass 比较适合大型项目以及团队协作。

  • Less:Less 也是一个比较流行的 CSS 预处理器,该预处理器是在 2009 年出现的,该预处理器借鉴了 Sass 的长处,并在此基础上兼容 CSS 语法,让开发者开发起来更加的得心应手
less
@primary-color: #4caf50;
+
+.container {
+  background-color: @primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+
@primary-color: #4caf50;
+
+.container {
+  background-color: @primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+

早期的 Sass 一开始只有缩进式语法,所以 Less 的出现降低了开发者的学习成本,因为 Less 兼容原生 CSS 语法,相较于 Sass,Less 学习曲线平滑、语法简单,但是编程功能相比 Sass 要弱一些,并且插件和社区也没有 Sass 那么强大,Less 的出现反而让 Sass 出现了第二版语法(类 CSS 语法)

  • Stylus:Stylus 是一种灵活且强大的 CSS 预处理器,其语法非常简洁,具有很高的自定义性。Stylus 支持多种语法风格,包括缩进式和类 CSS 语法。Stylus 提供了丰富的功能,如变量、嵌套、混合、条件语句、循环等。相较于 SassLessStylus 的社区规模较小,但仍有不少开发者喜欢其简洁灵活的特点。
stylus
primary-color = #4CAF50
+
+.container
+  background-color primary-color
+  padding 20px
+
+  .title
+    font-size 24px
+    color white
+
primary-color = #4CAF50
+
+.container
+  background-color primary-color
+  padding 20px
+
+  .title
+    font-size 24px
+    color white
+

Sass 快速入门

Sass 最早是由 Hampton Catlin 于 2006 年开发的,并且于 2007 年首次发布。

在最初的时候, Sass 采用的是缩进敏感语法,文件的扩展名为 .sass,编写完毕之后,需要通过基于 ruby 的 ruby sass 的编译器编译为普通的 CSS。因此在最早期的时候,如果你想要使用 sass,你是需要安装 Ruby 环境。

为什么早期选择了采用 Ruby 呢?这和当时的 Web 开发大环境有关系,在 2006 - 2010 左右,当时 Web 服务器端开发就特别流行使用基于 Ruby 的 Web 框架 Ruby on Rails。像早期的 github、Twitter 一开始都是使用的 ruby。

到了 2009 年的时候, Less 的出现给 Sass 带来竞争压力,因为 Less 是基于原生 CSS 语法进行扩展,使用者没有什么学习压力,于是 Natalie WeizenbaumChris Eppstein 为 Sass 引入了新的类 CSS 语法,也是基于原生的 CSS 进行语法扩展,文件的后缀名为 scss。虽然文件的后缀以及语法更新了,但是功能上面仍然支持之前 sass 所支持的所有功能,加上 sass 本身插件以及社区就比 less 强大,因此 Sass 重新变为了主流。

接下来还需要说一下关于编译器。随着社区的发展和技术的进步,Sass 已经不在局限于 Ruby,目前已经有多种语言实现了 Sass 的编译器:

  • Dart Sass:由 Sass 官方团队维护,使用 Dart 语言编写。它是目前推荐的 Sass 编译器,因为它是最新的、功能最全的实现。

    GitHub 仓库:https://github.com/sass/dart-sass

  • LibSass:使用 C/C++ 编写的 Sass 编译器,它的性能优于 Ruby 版本。LibSass 有多个绑定,例如 Node SassNode.js 绑定)和 SassCC 绑定)。

    GitHub 仓库:https://github.com/sass/libsass

  • Ruby Sass:最早的 Ruby 实现,已被官方废弃,并建议迁移到 Dart Sass

官方推荐使用 Dart Sass 来作为 Sass 的编译器,并且在 npm 上面发布了 Dart Sass 的包,直接通过 npm 安装即可。

接下来我们来看一个 Sass 的快速上手示例,首先创建一个新的项目目录 sass-demo,使用 pnpm init 进行项目初始化,之后安装 sass 依赖:

bash
pnpm add sass -D
+
pnpm add sass -D
+

接下来在 src/index.scss 里面写入如下的 scss 代码:

scss
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+

接下来我们就会遇到第一个问题,编译。

这里我们就可以使用 sass 提供的 compile 方法进行编译。在 src 目录下面创建一个 index.js 文件,记入如下的代码:

js
// 读取 scss 文件
+// 调用 sass 依赖提供的 complie 进行文件的编译
+// 最终在 dist 目录下面新生成一个 index.css(编译后的文件)
+
+const sass = require("sass"); // 做 scss 文件编译的
+const path = require("path"); // 处理路径相关的
+const fs = require("fs"); // 处理文件读写相关的
+
+// 定义一下输入和输出的文件路径
+const scssPath = path.resolve("src", "index.scss");
+const cssDir = "dist"; // 之后编译的 index.css 要存储的目录名
+const cssPath = path.resolve(cssDir, "index.css");
+
+// 编译
+const result = sass.compile(scssPath);
+console.log(result.css);
+
+// 将编译后的字符串写入到文件里面
+if (!fs.existsSync(cssDir)) {
+  // 说明不存在,那就创建
+  fs.mkdirSync(cssDir);
+}
+
+fs.writeFileSync(cssPath, result.css);
+
// 读取 scss 文件
+// 调用 sass 依赖提供的 complie 进行文件的编译
+// 最终在 dist 目录下面新生成一个 index.css(编译后的文件)
+
+const sass = require("sass"); // 做 scss 文件编译的
+const path = require("path"); // 处理路径相关的
+const fs = require("fs"); // 处理文件读写相关的
+
+// 定义一下输入和输出的文件路径
+const scssPath = path.resolve("src", "index.scss");
+const cssDir = "dist"; // 之后编译的 index.css 要存储的目录名
+const cssPath = path.resolve(cssDir, "index.css");
+
+// 编译
+const result = sass.compile(scssPath);
+console.log(result.css);
+
+// 将编译后的字符串写入到文件里面
+if (!fs.existsSync(cssDir)) {
+  // 说明不存在,那就创建
+  fs.mkdirSync(cssDir);
+}
+
+fs.writeFileSync(cssPath, result.css);
+

上面的方式,每次需要手动的运行 index.js 来进行编译,而且还需要手动的指定要编译的文件是哪一个,比较麻烦。早期的时候出现了一个专门做 sass 编译的 GUI 工具,叫做考拉,对应的官网地址为:http://koala-app.com/

现在随着 Vscode 这个集大成的 IDE 的出现,可以直接安装 Sass 相关的插件来做编译操作,例如 scss-to-css

注意在一开始安装的时候,该插件进行 scss 转换时会压缩 CSS 代码,这个是可以进行配置的。

Sass 基础语法

注释

CSS 里面的注释 /* */ ,Sass 中支持 // 来进行注释,// 类型的注释再编译后是会消失

scss
/*
+  Hello
+*/
+
+// World
+
/*
+  Hello
+*/
+
+// World
+
css
/*
+  Hello
+*/
+
/*
+  Hello
+*/
+

如果是压缩输出模式,那么注释也会被去掉,这个时候可以在多行注释的第一个字符书写一个 ! ,此时即便是在压缩模式,这条注释也会被保留,通常用于添加版权信息

scss
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+
+.test {
+  width: 300px;
+}
+
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+
+.test {
+  width: 300px;
+}
+
css
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+.test {
+  width: 300px;
+}
+
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+.test {
+  width: 300px;
+}
+

变量

这是当初 Sass 推出时一个极大的亮点,支持变量的声明,声明方式很简单,通过 $ 开头来进行声明,赋值方法和 CSS 属性的写法是一致的。

scss
// 声明变量
+$width: 1600px;
+$pen-size: 3em;
+
+div {
+  width: $width;
+  font-size: $pen-size;
+}
+
// 声明变量
+$width: 1600px;
+$pen-size: 3em;
+
+div {
+  width: $width;
+  font-size: $pen-size;
+}
+
css
div {
+  width: 1600px;
+  font-size: 3em;
+}
+
div {
+  width: 1600px;
+  font-size: 3em;
+}
+

变量的声明时支持块级作用域的,如果是在一个嵌套规则内部定义的变量,那么就只能在嵌套规则内部使用(局部变量),如果不是在嵌套规则内定义的变量那就是全局变量。

scss
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red;
+
+  p.one {
+    width: $width; /* 800px */
+    color: $color; /* red */
+  }
+}
+
+p.two {
+  width: $width; /* 1600px */
+  color: $color; /* 报错,因为 $color 是一个局部变量 */
+}
+
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red;
+
+  p.one {
+    width: $width; /* 800px */
+    color: $color; /* red */
+  }
+}
+
+p.two {
+  width: $width; /* 1600px */
+  color: $color; /* 报错,因为 $color 是一个局部变量 */
+}
+

可以通过一个 !global 将一个局部变量转换为全局变量

scss
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red !global;
+
+  p.one {
+    width: $width;
+    color: $color;
+  }
+}
+
+p.two {
+  width: $width;
+  color: $color;
+}
+
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red !global;
+
+  p.one {
+    width: $width;
+    color: $color;
+  }
+}
+
+p.two {
+  width: $width;
+  color: $color;
+}
+
css
div p.one {
+  width: 800px;
+  color: red;
+}
+
+p.two {
+  width: 1600px;
+  color: red;
+}
+
div p.one {
+  width: 800px;
+  color: red;
+}
+
+p.two {
+  width: 1600px;
+  color: red;
+}
+

数据类型

因为 CSS 预处理器就是针对 CSS 这一块融入编程语言的特性进去,所以自然会有数据类型。

在 Sass 中支持 7 种数据类型:

  • 数值类型:1、2、13、10px
  • 字符串类型:有引号字符串和无引号字符串 "foo"、'bar'、baz
  • 布尔类型:true、false
  • 空值:null
  • 数组(list):用空格或者逗号来进行分隔,1px 10px 15px 5px、1px,10px,15px,5px
  • 字典(map):用一个小括号扩起来,里面是一对一对的键值对 (key1:value1, key2:value2)
  • 颜色类型:blue、#04a012、rgba(0,0,12,0.5)

数值类型

Sass 里面支持两种数值类型:带单位数值不带单位的数值,数字可以是正负数以及浮点数

scss
$my-age: 19;
+$your-age: 19.5;
+$height: 120px;
+
$my-age: 19;
+$your-age: 19.5;
+$height: 120px;
+

字符串类型

支持两种:有引号字符串无引号字符串

并且引号可以是单引号也可以是双引号

scss
$name: "Tom Bob";
+$container: "top bottom";
+$what: heart;
+
+div {
+  background-image: url($what + ".png");
+}
+
$name: "Tom Bob";
+$container: "top bottom";
+$what: heart;
+
+div {
+  background-image: url($what + ".png");
+}
+
css
div {
+  background-image: url(heart.png);
+}
+
div {
+  background-image: url(heart.png);
+}
+

布尔类型

该类型就两个值:true 和 false,可以进行逻辑运算,支持 and、or、not 来做逻辑运算

scss
$a: 1>0 and 0>5; // false
+$b: "a" == a; // true
+$c: false; // false
+$d: not $c; // true
+
$a: 1>0 and 0>5; // false
+$b: "a" == a; // true
+$c: false; // false
+$d: not $c; // true
+

空值类型

就一个值:null 表示为空的意思

scss
$value: null;
+
$value: null;
+

因为是空值,因此不能够使用它和其他类型进行算数运算

数组类型

数组有两种表示方式:通过空格来间隔 以及 通过逗号来间隔

例如:

scss
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+

关于数组,有如下的注意事项:

  1. 数组里面可以包含子数组,例如 1px 2px, 5px 6px 就是包含了两个数组,1px 2px 是一个数组,5px 6px 又是一个数组,如果内外数组的分隔方式相同,例如都是采用空格来分隔,这个时候可以使用一个小括号来分隔 (1px 2px) (5px 6px)
  2. 添加了小括号的内容最终被编译为 CSS 的时候,是会被去除掉小括号的,例如 (1px 2px) (5px 6px) ---> 1px 2px 5px 6px
scss
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+
+div {
+  padding: $list2;
+}
+
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+
+div {
+  padding: $list2;
+}
+
css
div {
+  padding: 1px 2px 5px 6px;
+}
+
div {
+  padding: 1px 2px 5px 6px;
+}
+
  1. 小括号如果为空,则表示是一个空数组,空数组是不可以直接编译为 CSS 的
scss
$list2: ();
+
+div {
+  padding: $list2; // 报错
+}
+
$list2: ();
+
+div {
+  padding: $list2; // 报错
+}
+

但是如果是数组里面包含空数组或者 null 空值,编译能够成功,空数组以及空值会被去除掉

scss
$list2: 1px 2px null 3px;
+$list3: 1px 2px () 3px;
+
+div {
+  padding: $list2;
+}
+
+.div2 {
+  padding: $list3;
+}
+
$list2: 1px 2px null 3px;
+$list3: 1px 2px () 3px;
+
+div {
+  padding: $list2;
+}
+
+.div2 {
+  padding: $list3;
+}
+
  1. 可以使用 nth 函数去访问数组里面的值,注意数组的下标是从 1 开始的。
scss
// 创建一个 List
+$font-sizes: 12px 14px 16px 18px 24px;
+
+// 通过索引访问 List 中的值
+$base-font-size: nth($font-sizes, 3);
+
+// 使用 List 中的值为元素设置样式
+body {
+  font-size: $base-font-size;
+}
+
// 创建一个 List
+$font-sizes: 12px 14px 16px 18px 24px;
+
+// 通过索引访问 List 中的值
+$base-font-size: nth($font-sizes, 3);
+
+// 使用 List 中的值为元素设置样式
+body {
+  font-size: $base-font-size;
+}
+
css
body {
+  font-size: 16px;
+}
+
body {
+  font-size: 16px;
+}
+

最后我们来看一个实际开发中用到数组的典型案例:

scss
$sizes: 40px 50px 60px;
+
+@each $s in $sizes {
+  .icon-#{$s} {
+    font-size: $s;
+    width: $s;
+    height: $s;
+  }
+}
+
$sizes: 40px 50px 60px;
+
+@each $s in $sizes {
+  .icon-#{$s} {
+    font-size: $s;
+    width: $s;
+    height: $s;
+  }
+}
+
css
.icon-40px {
+  font-size: 40px;
+  width: 40px;
+  height: 40px;
+}
+
+.icon-50px {
+  font-size: 50px;
+  width: 50px;
+  height: 50px;
+}
+
+.icon-60px {
+  font-size: 60px;
+  width: 60px;
+  height: 60px;
+}
+
.icon-40px {
+  font-size: 40px;
+  width: 40px;
+  height: 40px;
+}
+
+.icon-50px {
+  font-size: 50px;
+  width: 50px;
+  height: 50px;
+}
+
+.icon-60px {
+  font-size: 60px;
+  width: 60px;
+  height: 60px;
+}
+

字典类型

字典类型必须要使用小括号扩起来,小括号里面是一对一对的键值对

scss
$a: (
+  $key1: value1,
+  $key2: value2,
+);
+
$a: (
+  $key1: value1,
+  $key2: value2,
+);
+

可以通过 map-get 方法来获取字典值

scss
// 创建一个 Map
+$colors: (
+  "primary": #4caf50,
+  "secondary": #ff9800,
+  "accent": #2196f3,
+);
+
+$primary: map-get($colors, "primary");
+
+button {
+  background-color: $primary;
+}
+
// 创建一个 Map
+$colors: (
+  "primary": #4caf50,
+  "secondary": #ff9800,
+  "accent": #2196f3,
+);
+
+$primary: map-get($colors, "primary");
+
+button {
+  background-color: $primary;
+}
+
css
button {
+  background-color: #4caf50;
+}
+
button {
+  background-color: #4caf50;
+}
+

接下来还是看一个实际开发中的示例:

scss
$icons: (
+  "eye": "\\f112",
+  "start": "\\f12e",
+  "stop": "\\f12f",
+);
+
+@each $key, $value in $icons {
+  .icon-#{$key}:before {
+    display: inline-block;
+    font-family: "Open Sans";
+    content: $value;
+  }
+}
+
$icons: (
+  "eye": "\\f112",
+  "start": "\\f12e",
+  "stop": "\\f12f",
+);
+
+@each $key, $value in $icons {
+  .icon-#{$key}:before {
+    display: inline-block;
+    font-family: "Open Sans";
+    content: $value;
+  }
+}
+
css
.icon-eye:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\\f112";
+}
+
+.icon-start:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\\f12e";
+}
+
+.icon-stop:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\\f12f";
+}
+
.icon-eye:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\\f112";
+}
+
+.icon-start:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\\f12e";
+}
+
+.icon-stop:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\\f12f";
+}
+

颜色类型

支持原生 CSS 中各种颜色的表示方式,十六进制、RGB、RGBA、HSL、HSLA、颜色英语单词。

Sass 还提供了内置的 Colors 相关的各种函数,可以方便我们对颜色进行一个颜色值的调整和操作。

  • lighten 和 darken:调整颜色的亮度,lighten 是增加亮度、darken 是减少亮度
scss
$color: red;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: lighten($color, 10%); // 亮度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: darken($color, 10%); // 亮度减少10%
+}
+
$color: red;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: lighten($color, 10%); // 亮度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: darken($color, 10%); // 亮度减少10%
+}
+
css
.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: #ff3333;
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: #cc0000;
+}
+
.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: #ff3333;
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: #cc0000;
+}
+
  • saturate 和 desaturate:调整颜色的饱和度
scss
$color: #4caf50;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: saturate($color, 10%); // 饱和度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: desaturate($color, 10%); // 饱和度减少10%
+}
+
$color: #4caf50;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: saturate($color, 10%); // 饱和度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: desaturate($color, 10%); // 饱和度减少10%
+}
+
  • Adjust Hue:通过调整颜色的色相来创建新颜色。
scss
$color: #4caf50;
+$new-hue: adjust-hue($color, 30); // 色相增加 30 度
+
$color: #4caf50;
+$new-hue: adjust-hue($color, 30); // 色相增加 30 度
+
  • RGBA:为颜色添加透明度。
scss
$color: #4caf50;
+$transparent: rgba($color, 0.5); // 添加 50% 透明度
+
$color: #4caf50;
+$transparent: rgba($color, 0.5); // 添加 50% 透明度
+
  • Mix:混合两种颜色。
scss
$color1: #4caf50;
+$color2: #2196f3;
+$mixed: mix($color1, $color2, 50%); // 混合两种颜色,权重 50%
+
$color1: #4caf50;
+$color2: #2196f3;
+$mixed: mix($color1, $color2, 50%); // 混合两种颜色,权重 50%
+
  • Complementary:获取颜色的补充颜色。
scss
$color: #4caf50;
+$complementary: adjust-hue($color, 180); // 色相增加 180 度,获取补充颜色
+
$color: #4caf50;
+$complementary: adjust-hue($color, 180); // 色相增加 180 度,获取补充颜色
+

如果想要查阅具体有哪些颜色相关的函数,可以参阅官方文档:https://sass-lang.com/documentation/modules/color

嵌套语法

插值语法

运算

关于运算相关的一些函数:

  • calc
  • max 和 min
  • clamp

calc

该方法是 CSS3 提供的一个方法,用于做属性值的计算。

scss
.container {
+  width: 80%;
+  padding: 0 20px;
+
+  .element {
+    width: calc(100% - 40px);
+  }
+
+  .element2 {
+    width: calc(100px - 40px);
+  }
+}
+
.container {
+  width: 80%;
+  padding: 0 20px;
+
+  .element {
+    width: calc(100% - 40px);
+  }
+
+  .element2 {
+    width: calc(100px - 40px);
+  }
+}
+
css
.container {
+  width: 80%;
+  padding: 0 20px;
+}
+.container .element {
+  width: calc(100% - 40px);
+}
+.container .element2 {
+  width: 60px;
+}
+
.container {
+  width: 80%;
+  padding: 0 20px;
+}
+.container .element {
+  width: calc(100% - 40px);
+}
+.container .element2 {
+  width: 60px;
+}
+

注意,在上面的编译当中,如果单位相同,Sass 在做编译的时候会直接运行 calc 运算表达式,得到计算出来的最终值,但是如果单位不相同,会保留 calc 运算表达式。

min 和 max

min 是在一组数据里面找出最小值,max 就是在一组数据里面找到最大值。

scss
$width1: 500px;
+$width2: 600px;
+
+.element {
+  width: min($width1, $width2);
+}
+
$width1: 500px;
+$width2: 600px;
+
+.element {
+  width: min($width1, $width2);
+}
+
css
.element {
+  width: 500px;
+}
+
.element {
+  width: 500px;
+}
+

clamp

这个也是 CSS3 提供的函数,语法为:

css
clamp(min, value, max)
+
clamp(min, value, max)
+

min 代表下限,max 代表上限,value 是需要限制的值。clamp 的作用就是将 value 限制在 min 和 max 之间,如果 value 小于了 min 那么就取 min 作为值,如果 vlaue 大于了 max,那么就取 max 作为值。如果 value 在 min 和 max 之间,那么就返回 value 值本身。

scss
$min-font-size: 16px;
+$max-font-size: 24px;
+
+body {
+  font-size: clamp($min-font-size, 1.25vw + 1rem, $max-font-size);
+}
+
$min-font-size: 16px;
+$max-font-size: 24px;
+
+body {
+  font-size: clamp($min-font-size, 1.25vw + 1rem, $max-font-size);
+}
+
css
body {
+  font-size: clamp(16px, 1.25vw + 1rem, 24px);
+}
+
body {
+  font-size: clamp(16px, 1.25vw + 1rem, 24px);
+}
+

在上面的 CSS 代码中,我们希望通过视口宽度动态的调整 body 的字体大小。value 部分为 1.25vw + 1rem(这个计算会在浏览器环境中进行计算)。当视口宽度较小时,1.25vw + 1rem 计算结果可能是小于 16px,那么此时就取 16px。当视口宽度较大时,1.25vw + 1rem 计算结果可能大于 24px,那么此时就取 24px。如果 1.25vw + 1rem 计算值在 16px - 24px 之间,那么就取计算值结果。

Sass 控制指令

前面我们说了 CSS 预处理器最大的特点就是将编程语言的特性融入到了 CSS 里面,因此既然 CSS 预处理器里面都有变量、数据类型,自然而然也会有流程控制。

三元运算符

三元运算符

scss
if(expression, value1, value2)
+
if(expression, value1, value2)
+

示例如下:

scss
p {
+  color: if(1+1==2, green, yellow);
+}
+
+div {
+  color: if(1+1==3, green, yellow);
+}
+
p {
+  color: if(1+1==2, green, yellow);
+}
+
+div {
+  color: if(1+1==3, green, yellow);
+}
+
css
p {
+  color: green;
+}
+
+div {
+  color: yellow;
+}
+
p {
+  color: green;
+}
+
+div {
+  color: yellow;
+}
+

@if 分支

这个表示是分支。分支又分为三种:

  • 单分支
  • 双分支
  • 多分支

单分支

scss
p {
+  @if 1+1 == 2 {
+    color: red;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  }
+  margin: 10px;
+}
+
p {
+  @if 1+1 == 2 {
+    color: red;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  }
+  margin: 10px;
+}
+
css
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  margin: 10px;
+}
+
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  margin: 10px;
+}
+

双分支

仍然使用的是 @else

scss
p {
+  @if 1+1 == 2 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
p {
+  @if 1+1 == 2 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
css
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  color: blue;
+  margin: 10px;
+}
+
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  color: blue;
+  margin: 10px;
+}
+

多分支

使用 @else if 来写多分支

scss
$type: monster;
+p {
+  @if $type == ocean {
+    color: blue;
+  } @else if $type == matador {
+    color: red;
+  } @else if $type == monster {
+    color: green;
+  } @else {
+    color: black;
+  }
+}
+
$type: monster;
+p {
+  @if $type == ocean {
+    color: blue;
+  } @else if $type == matador {
+    color: red;
+  } @else if $type == monster {
+    color: green;
+  } @else {
+    color: black;
+  }
+}
+
css
p {
+  color: green;
+}
+
p {
+  color: green;
+}
+

@for 循环

语法如下:

scss
@for $var
+  from <start>
+  through <end>
+  #会包含
+  end 结束值
+  // 或者
+  @for
+  $var
+  from <start>
+  to
+  <end>
+  #to
+  不会包含
+  end 结束值
+;
+
@for $var
+  from <start>
+  through <end>
+  #会包含
+  end 结束值
+  // 或者
+  @for
+  $var
+  from <start>
+  to
+  <end>
+  #to
+  不会包含
+  end 结束值
+;
+
scss
@for $i from 1 to 3 {
+  .item-#{$i} {
+    width: $i * 2em;
+  }
+}
+
+@for $i from 1 through 3 {
+  .item2-#{$i} {
+    width: $i * 2em;
+  }
+}
+
@for $i from 1 to 3 {
+  .item-#{$i} {
+    width: $i * 2em;
+  }
+}
+
+@for $i from 1 through 3 {
+  .item2-#{$i} {
+    width: $i * 2em;
+  }
+}
+
css
.item-1 {
+  width: 2em;
+}
+
+.item-2 {
+  width: 4em;
+}
+
+.item2-1 {
+  width: 2em;
+}
+
+.item2-2 {
+  width: 4em;
+}
+
+.item2-3 {
+  width: 6em;
+}
+
.item-1 {
+  width: 2em;
+}
+
+.item-2 {
+  width: 4em;
+}
+
+.item2-1 {
+  width: 2em;
+}
+
+.item2-2 {
+  width: 4em;
+}
+
+.item2-3 {
+  width: 6em;
+}
+

@while 循环

语法如下:

scss
@while expression ;
+
@while expression ;
+
scss
$i: 6;
+@while $i > 0 {
+  .item-#{$i} {
+    width: 2em * $i;
+  }
+  $i: $i - 2;
+}
+
$i: 6;
+@while $i > 0 {
+  .item-#{$i} {
+    width: 2em * $i;
+  }
+  $i: $i - 2;
+}
+
css
.item-6 {
+  width: 12em;
+}
+
+.item-4 {
+  width: 8em;
+}
+
+.item-2 {
+  width: 4em;
+}
+
.item-6 {
+  width: 12em;
+}
+
+.item-4 {
+  width: 8em;
+}
+
+.item-2 {
+  width: 4em;
+}
+

注意,一定要要在 while 里面书写改变变量的表达式,否则就会形成一个死循环。

@each 循环

这个优有点类似于 JS 里面的 for...of 循环,会把数组或者字典类型的每一项值挨着挨着取出来

scss
@each $var in $vars ;
+
@each $var in $vars ;
+

$var 可以是任意的变量名,但是 $vars 只能是 list 或者 maps

下面是一个遍历列表(数组)

scss
$arr: puma, sea-slug, egret, salamander;
+
+@each $animal in $arr {
+  .#{$animal}-icon {
+    background-image: url("/images/#{$animal}.png");
+  }
+}
+
$arr: puma, sea-slug, egret, salamander;
+
+@each $animal in $arr {
+  .#{$animal}-icon {
+    background-image: url("/images/#{$animal}.png");
+  }
+}
+
css
.puma-icon {
+  background-image: url("/images/puma.png");
+}
+
+.sea-slug-icon {
+  background-image: url("/images/sea-slug.png");
+}
+
+.egret-icon {
+  background-image: url("/images/egret.png");
+}
+
+.salamander-icon {
+  background-image: url("/images/salamander.png");
+}
+
.puma-icon {
+  background-image: url("/images/puma.png");
+}
+
+.sea-slug-icon {
+  background-image: url("/images/sea-slug.png");
+}
+
+.egret-icon {
+  background-image: url("/images/egret.png");
+}
+
+.salamander-icon {
+  background-image: url("/images/salamander.png");
+}
+

下面是一个遍历字典类型的示例:

scss
$dict: (
+  h1: 2em,
+  h2: 1.5em,
+  h3: 1.2em,
+  h4: 1em,
+);
+
+@each $header, $size in $dict {
+  #{$header} {
+    font-size: $size;
+  }
+}
+
$dict: (
+  h1: 2em,
+  h2: 1.5em,
+  h3: 1.2em,
+  h4: 1em,
+);
+
+@each $header, $size in $dict {
+  #{$header} {
+    font-size: $size;
+  }
+}
+
css
h1 {
+  font-size: 2em;
+}
+
+h2 {
+  font-size: 1.5em;
+}
+
+h3 {
+  font-size: 1.2em;
+}
+
+h4 {
+  font-size: 1em;
+}
+
h1 {
+  font-size: 2em;
+}
+
+h2 {
+  font-size: 1.5em;
+}
+
+h3 {
+  font-size: 1.2em;
+}
+
+h4 {
+  font-size: 1em;
+}
+

Sass 混合指令

在 Sass 里面存在一种叫做混合指令的东西,它最大的特点就是允许我们创建可以重用的代码片段,从而避免了代码的重复,提供代码的可维护性。

混合指令基本的使用

首先要使用混合指令,我们需要先定义一个混合指令:

scss
@mixin name {
+  // 样式。。。。
+}
+
@mixin name {
+  // 样式。。。。
+}
+

例如:

scss
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+

接下来是如何使用指令,需要使用到 @include,后面跟上混合指令的名称即可。

scss
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+
+p {
+  @include large-text;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  @include large-text;
+}
+
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+
+p {
+  @include large-text;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  @include large-text;
+}
+
css
p {
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+}
+
p {
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+}
+

我们发现,混合指令编译之后,就是将混合指令内部的 CSS 样式放入到了 @include 的地方,因此我们可以很明显的感受到混合指令就是提取公共的样式出来,方便复用和维护。

在混合指令中,我们也可以引用其他的混合指令:

scss
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  @include background;
+  @include header-text;
+}
+
+p {
+  @include compound;
+}
+
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  @include background;
+  @include header-text;
+}
+
+p {
+  @include compound;
+}
+
css
p {
+  background-color: #fc0;
+  font-size: 20px;
+}
+
p {
+  background-color: #fc0;
+  font-size: 20px;
+}
+

混合指令是可以直接在最外层使用的,但是对混合指令本身有一些要求。要求混合指令的内部要有选择器。

scss
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  div {
+    @include background;
+    @include header-text;
+  }
+}
+
+@include compound;
+
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  div {
+    @include background;
+    @include header-text;
+  }
+}
+
+@include compound;
+
css
div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+
div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+

例如在上面的示例中,compound 混合指令里面不再是单纯的属性声明,而是有选择器在里面,这样的话就可以直接在最外层使用。

一般来讲,我们要在外部直接使用,我们一般会将其作为一个后代选择器。例如:

scss
.box {
+  @include compound;
+}
+
.box {
+  @include compound;
+}
+
css
.box div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+
.box div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+

混合指令的参数

混合指令能够设置参数,只需要在混合指令的后面添加一对圆括号即可,括号里面书写对应的形参。

例如:

scss
@mixin bg-color($color, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(red, 10px);
+}
+
+.box2 {
+  @include bg-color(blue, 20px);
+}
+
@mixin bg-color($color, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(red, 10px);
+}
+
+.box2 {
+  @include bg-color(blue, 20px);
+}
+
css
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: red;
+  border-radius: 10px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: red;
+  border-radius: 10px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+

既然在定义混合指令的时候,指定了参数,那么在使用的时候,传递的参数的个数一定要和形参一致,否则编译会出错。

在定义的时候,支持给形参添加默认值,例如:

scss
@mixin bg-color($color, $radius: 20px) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(blue);
+}
+
@mixin bg-color($color, $radius: 20px) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(blue);
+}
+

上面的示例是可以通过编译的,因为在定义 bg-color 的时候,我们为 $radius 设置了默认值。所以在使用的时候,即便没有传递第二个参数,也是 OK 的,因为会直接使用默认值。

在传递参数的时候,还支持关键词传参,就是指定哪一个参数是对应的哪一个形参,例如:

scss
@mixin bg-color($color: blue, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color($radius: 20px, $color: pink);
+}
+
+.box2 {
+  @include bg-color($radius: 20px);
+}
+
@mixin bg-color($color: blue, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color($radius: 20px, $color: pink);
+}
+
+.box2 {
+  @include bg-color($radius: 20px);
+}
+
css
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: pink;
+  border-radius: 20px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: pink;
+  border-radius: 20px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+

在定义混合指令的时候,如果不确定使用混合指令的地方会传入多少个参数,可以使用 ... 来声明(类似于 js 里面的不定参数),Sass 会把这些值当作一个列表来进行处理。

scss
@mixin box-shadow($shadow...) {
+  // ...
+  box-shadow: $shadow;
+}
+
+.box1 {
+  @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
+}
+
+.box2 {
+  @include box-shadow(
+    0 1px 2px rgba(0, 0, 0, 0.5),
+    0 2px 5px rgba(100, 0, 0, 0.5)
+  );
+}
+
@mixin box-shadow($shadow...) {
+  // ...
+  box-shadow: $shadow;
+}
+
+.box1 {
+  @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
+}
+
+.box2 {
+  @include box-shadow(
+    0 1px 2px rgba(0, 0, 0, 0.5),
+    0 2px 5px rgba(100, 0, 0, 0.5)
+  );
+}
+
css
.box1 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+.box2 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 2px 5px rgba(100, 0, 0, 0.5);
+}
+
.box1 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+.box2 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 2px 5px rgba(100, 0, 0, 0.5);
+}
+

在 Sass 中,... 有些时候也可以表示为参数展开的含义,例如:

scss
@mixin colors($text, $background, $border) {
+  color: $text;
+  background-color: $background;
+  border-color: $border;
+}
+
+$values: red, blue, pink;
+
+.box {
+  @include colors($values...);
+}
+
@mixin colors($text, $background, $border) {
+  color: $text;
+  background-color: $background;
+  border-color: $border;
+}
+
+$values: red, blue, pink;
+
+.box {
+  @include colors($values...);
+}
+

@content

@content 表示占位的意思,在使用混合指令的时候,会将指令大括号里面的内容放置到 @content 的位置,有点类似于插槽。

scss
@mixin test {
+  html {
+    @content;
+  }
+}
+
+@include test {
+  background-color: red;
+  .logo {
+    width: 600px;
+  }
+}
+
+@include test {
+  color: blue;
+  .box {
+    width: 200px;
+    height: 200px;
+  }
+}
+
@mixin test {
+  html {
+    @content;
+  }
+}
+
+@include test {
+  background-color: red;
+  .logo {
+    width: 600px;
+  }
+}
+
+@include test {
+  color: blue;
+  .box {
+    width: 200px;
+    height: 200px;
+  }
+}
+
css
html {
+  background-color: red;
+}
+html .logo {
+  width: 600px;
+}
+
+html {
+  color: blue;
+}
+html .box {
+  width: 200px;
+  height: 200px;
+}
+
html {
+  background-color: red;
+}
+html .logo {
+  width: 600px;
+}
+
+html {
+  color: blue;
+}
+html .box {
+  width: 200px;
+  height: 200px;
+}
+

下面是一个实际开发中的例子:

scss
@mixin button-theme($color) {
+  background-color: $color;
+  border: 1px solid darken($color, 15%);
+
+  &:hover {
+    background-color: lighten($color, 5%);
+    border-color: darken($color, 10%);
+  }
+
+  @content;
+}
+
+.button-primary {
+  @include button-theme(#007bff) {
+    width: 500px;
+    height: 400px;
+  }
+}
+
+.button-secondary {
+  @include button-theme(#6c757d) {
+    width: 300px;
+    height: 200px;
+  }
+}
+
@mixin button-theme($color) {
+  background-color: $color;
+  border: 1px solid darken($color, 15%);
+
+  &:hover {
+    background-color: lighten($color, 5%);
+    border-color: darken($color, 10%);
+  }
+
+  @content;
+}
+
+.button-primary {
+  @include button-theme(#007bff) {
+    width: 500px;
+    height: 400px;
+  }
+}
+
+.button-secondary {
+  @include button-theme(#6c757d) {
+    width: 300px;
+    height: 200px;
+  }
+}
+
css
.button-primary {
+  background-color: #007bff;
+  border: 1px solid #0056b3;
+  width: 500px;
+  height: 400px;
+}
+.button-primary:hover {
+  background-color: #1a88ff;
+  border-color: #0062cc;
+}
+
+.button-secondary {
+  background-color: #6c757d;
+  border: 1px solid #494f54;
+  width: 300px;
+  height: 200px;
+}
+.button-secondary:hover {
+  background-color: #78828a;
+  border-color: #545b62;
+}
+
.button-primary {
+  background-color: #007bff;
+  border: 1px solid #0056b3;
+  width: 500px;
+  height: 400px;
+}
+.button-primary:hover {
+  background-color: #1a88ff;
+  border-color: #0062cc;
+}
+
+.button-secondary {
+  background-color: #6c757d;
+  border: 1px solid #494f54;
+  width: 300px;
+  height: 200px;
+}
+.button-secondary:hover {
+  background-color: #78828a;
+  border-color: #545b62;
+}
+

最后我们需要说一先关于 @content 的作用域的问题。

在混合指令的局部作用域里面所定义的变量不会影响 @content 代码块中的变量,同样,在 @content 代码块中定义的变量不会影响到混合指令中的其他变量,两者之间的作用域是隔离的

scss
@mixin scope-test {
+  $test-variable: "mixin";
+
+  .mixin {
+    content: $test-variable;
+  }
+
+  @content;
+}
+
+.test {
+  $test-variable: "test";
+  @include scope-test {
+    .content {
+      content: $test-variable;
+    }
+  }
+}
+
@mixin scope-test {
+  $test-variable: "mixin";
+
+  .mixin {
+    content: $test-variable;
+  }
+
+  @content;
+}
+
+.test {
+  $test-variable: "test";
+  @include scope-test {
+    .content {
+      content: $test-variable;
+    }
+  }
+}
+
css
.test .mixin {
+  content: "mixin";
+}
+.test .content {
+  content: "test";
+}
+
.test .mixin {
+  content: "mixin";
+}
+.test .content {
+  content: "test";
+}
+

Sass 函数指令

上一小节所学习的混合指令虽然有点像函数,但是它并不是函数,因为混合指令所做的事情就是单纯的代码的替换工作,里面并不存在任何的计算。在 Sass 里面是支持函数指令。

自定义函数

在 Sass 里面自定义函数的语法如下:

scss
@function fn-name($params...) {
+  @return XXX;
+}
+
@function fn-name($params...) {
+  @return XXX;
+}
+

具体示例如下:

scss
@function divide($a, $b) {
+  @return $a / $b;
+}
+
+.container {
+  width: divide(100px, 2);
+}
+
@function divide($a, $b) {
+  @return $a / $b;
+}
+
+.container {
+  width: divide(100px, 2);
+}
+
css
.container {
+  width: 50px;
+}
+
.container {
+  width: 50px;
+}
+

函数可以接收多个参数,如果不确定会传递几个参数,那么可以使用前面介绍过的不定参数的形式。

scss
@function sum($nums...) {
+  $sum: 0;
+  @each $n in $nums {
+    $sum: $sum + $n;
+  }
+  @return $sum;
+}
+
+.box1 {
+  width: sum(1, 2, 3) + px;
+}
+
+.box2 {
+  width: sum(1, 2, 3, 4, 5, 6) + px;
+}
+
@function sum($nums...) {
+  $sum: 0;
+  @each $n in $nums {
+    $sum: $sum + $n;
+  }
+  @return $sum;
+}
+
+.box1 {
+  width: sum(1, 2, 3) + px;
+}
+
+.box2 {
+  width: sum(1, 2, 3, 4, 5, 6) + px;
+}
+
css
.box1 {
+  width: 6px;
+}
+
+.box2 {
+  width: 21px;
+}
+
.box1 {
+  width: 6px;
+}
+
+.box2 {
+  width: 21px;
+}
+

最后我们还是来看一个实际开发中的示例:

scss
// 根据传入的 $background-color 返回适当的文字颜色
+@function contrast-color($background-color) {
+  // 计算背景颜色的亮度
+  $brightness: red($background-color) * 0.299 + green($background-color) *
+    0.587 + blue($background-color) * 0.114;
+
+  // 根据亮度来返回黑色或者白色的文字颜色
+  @if $brightness > 128 {
+    @return #000;
+  } @else {
+    @return #fff;
+  }
+}
+
+.button {
+  $background-color: #007bff;
+  background-color: $background-color;
+  color: contrast-color($background-color);
+}
+
// 根据传入的 $background-color 返回适当的文字颜色
+@function contrast-color($background-color) {
+  // 计算背景颜色的亮度
+  $brightness: red($background-color) * 0.299 + green($background-color) *
+    0.587 + blue($background-color) * 0.114;
+
+  // 根据亮度来返回黑色或者白色的文字颜色
+  @if $brightness > 128 {
+    @return #000;
+  } @else {
+    @return #fff;
+  }
+}
+
+.button {
+  $background-color: #007bff;
+  background-color: $background-color;
+  color: contrast-color($background-color);
+}
+

在上面的代码示例中,我们首先定义了一个名为 contrast-color 的函数,该函数接收一个背景颜色参数,函数内部会根据这个背景颜色来决定文字应该是白色还是黑色。

css
.button {
+  background-color: #007bff;
+  color: #fff;
+}
+
.button {
+  background-color: #007bff;
+  color: #fff;
+}
+

内置函数

除了自定义函数,Sass 里面还提供了非常多的内置函数,你可以在官方文档:

https://sass-lang.com/documentation/modules

字符串相关内置函数

函数名和参数类型函数作用
quote($string)添加引号
unquote($string)除去引号
to-lower-case($string)变为小写
to-upper-case($string)变为大写
str-length($string)返回$string 的长度(汉字算一个)
str-index($string,$substring)返回$substring在$string 的位置
str-insert($string, $insert, $index)在$string的$index 处插入$insert
str-slice($string, $start-at, $end-at)截取$string的$start-at 和$end-at 之间的字符串

注意索引是从 1 开始的,如果书写 -1,那么就是倒着来的。两边都是闭区间

scss
$str: "Hello world!";
+
+.slice1 {
+  content: str-slice($str, 1, 5);
+}
+
+.slice2 {
+  content: str-slice($str, -1);
+}
+
$str: "Hello world!";
+
+.slice1 {
+  content: str-slice($str, 1, 5);
+}
+
+.slice2 {
+  content: str-slice($str, -1);
+}
+
css
.slice1 {
+  content: "Hello";
+}
+
+.slice2 {
+  content: "!";
+}
+
.slice1 {
+  content: "Hello";
+}
+
+.slice2 {
+  content: "!";
+}
+

数字相关内置函数

函数名和参数类型函数作用
percentage($number)转换为百分比形式
round($number)四舍五入为整数
ceil($number)数值向上取整
floor($number)数值向下取整
abs($number)获取绝对值
min($number...)获取最小值
max($number...)获取最大值
random($number?:number)不传入值:获得 0-1 的随机数;传入正整数 n:获得 0-n 的随机整数(左开右闭)
scss
.item {
+  width: percentage(2/5);
+  height: random(100) + px;
+  color: rgb(random(255), random(255), random(255));
+}
+
.item {
+  width: percentage(2/5);
+  height: random(100) + px;
+  color: rgb(random(255), random(255), random(255));
+}
+
css
.item {
+  width: 40%;
+  height: 83px;
+  color: rgb(31, 86, 159);
+}
+
.item {
+  width: 40%;
+  height: 83px;
+  color: rgb(31, 86, 159);
+}
+

数组相关内置函数

函数名和参数类型函数作用
length($list)获取数组长度
nth($list, n)获取指定下标的元素
set-nth($list, $n, $value)向$list的$n 处插入$value
join($list1, $list2, $separator)拼接$list1和list2;$separator 为新 list 的分隔符,默认为 auto,可选择 comma、space
append($list, $val, $separator)向$list的末尾添加$val;$separator 为新 list 的分隔符,默认为 auto,可选择 comma、space
index($list, $value)返回$value值在$list 中的索引值
zip($lists…)将几个列表结合成一个多维的列表;要求每个的列表个数值必须是相同的

下面是一个具体的示例:

scss
// 演示的是 join 方法
+$list1: 1px solid, 2px dotted;
+$list2: 3px dashed, 4px double;
+$combined-list: join($list1, $list2, comma);
+
+// 演示的是 append 方法
+$base-colors: red, green, blue;
+$extended-colors: append($base-colors, yellow, comma);
+
+// 演示 zip 方法
+$fonts: "Arial", "Helvetica", "Verdana";
+$weights: "normal", "bold", "italic";
+// $font-pair: ("Arial", "normal"),("Helvetica", "bold"),("Verdana","italic")
+$font-pair: zip($fonts, $weights);
+
+// 接下来我们来生成一下具体的样式
+@each $border-style in $combined-list {
+  .border-#{index($combined-list, $border-style)} {
+    border: $border-style;
+  }
+}
+
+@each $color in $extended-colors {
+  .bg-#{index($extended-colors, $color)} {
+    background-color: $color;
+  }
+}
+
+@each $pair in $font-pair {
+  $font: nth($pair, 1);
+  $weight: nth($pair, 2);
+  .text-#{index($font-pair, $pair)} {
+    font-family: $font;
+    font-weight: $weight;
+  }
+}
+
// 演示的是 join 方法
+$list1: 1px solid, 2px dotted;
+$list2: 3px dashed, 4px double;
+$combined-list: join($list1, $list2, comma);
+
+// 演示的是 append 方法
+$base-colors: red, green, blue;
+$extended-colors: append($base-colors, yellow, comma);
+
+// 演示 zip 方法
+$fonts: "Arial", "Helvetica", "Verdana";
+$weights: "normal", "bold", "italic";
+// $font-pair: ("Arial", "normal"),("Helvetica", "bold"),("Verdana","italic")
+$font-pair: zip($fonts, $weights);
+
+// 接下来我们来生成一下具体的样式
+@each $border-style in $combined-list {
+  .border-#{index($combined-list, $border-style)} {
+    border: $border-style;
+  }
+}
+
+@each $color in $extended-colors {
+  .bg-#{index($extended-colors, $color)} {
+    background-color: $color;
+  }
+}
+
+@each $pair in $font-pair {
+  $font: nth($pair, 1);
+  $weight: nth($pair, 2);
+  .text-#{index($font-pair, $pair)} {
+    font-family: $font;
+    font-weight: $weight;
+  }
+}
+
css
.border-1 {
+  border: 1px solid;
+}
+
+.border-2 {
+  border: 2px dotted;
+}
+
+.border-3 {
+  border: 3px dashed;
+}
+
+.border-4 {
+  border: 4px double;
+}
+
+.bg-1 {
+  background-color: red;
+}
+
+.bg-2 {
+  background-color: green;
+}
+
+.bg-3 {
+  background-color: blue;
+}
+
+.bg-4 {
+  background-color: yellow;
+}
+
+.text-1 {
+  font-family: "Arial";
+  font-weight: "normal";
+}
+
+.text-2 {
+  font-family: "Helvetica";
+  font-weight: "bold";
+}
+
+.text-3 {
+  font-family: "Verdana";
+  font-weight: "italic";
+}
+
.border-1 {
+  border: 1px solid;
+}
+
+.border-2 {
+  border: 2px dotted;
+}
+
+.border-3 {
+  border: 3px dashed;
+}
+
+.border-4 {
+  border: 4px double;
+}
+
+.bg-1 {
+  background-color: red;
+}
+
+.bg-2 {
+  background-color: green;
+}
+
+.bg-3 {
+  background-color: blue;
+}
+
+.bg-4 {
+  background-color: yellow;
+}
+
+.text-1 {
+  font-family: "Arial";
+  font-weight: "normal";
+}
+
+.text-2 {
+  font-family: "Helvetica";
+  font-weight: "bold";
+}
+
+.text-3 {
+  font-family: "Verdana";
+  font-weight: "italic";
+}
+

字典相关内置函数

函数名和参数类型函数作用
map-get($map, $key)获取$map中$key 对应的$value
map-merge($map1, $map2)合并$map1和$map2,返回一个新$map
map-remove($map, $key)从$map中删除$key,返回一个新$map
map-keys($map)返回$map所有的$key
map-values($map)返回$map所有的$value
map-has-key($map, $key)判断$map中是否存在$key,返回对应的布尔值
keywords($args)返回一个函数的参数,并可以动态修改其值

下面是一个使用了字典内置方法的相关示例:

scss
// 创建一个颜色映射表
+$colors: (
+  "primary": #007bff,
+  "secondary": #6c757d,
+  "success": #28a745,
+  "info": #17a2b8,
+  "warning": #ffc107,
+  "danger": #dc3545,
+);
+
+// 演示通过 map-get 获取对应的值
+@function btn-color($color-name) {
+  @return map-get($colors, $color-name);
+}
+
+// 演示通过 map-keys 获取映射表所有的 keys
+$color-keys: map-keys($colors);
+
+// 一个新的颜色映射表
+$more-colors: (
+  "light": #f8f9fa,
+  "dark": #343a40,
+);
+
+// 要将新的颜色映射表合并到 $colors 里面
+
+$all-colors: map-merge($colors, $more-colors);
+
+// 接下来我们来根据颜色映射表生成样式
+@each $color-key, $color-value in $all-colors {
+  .text-#{$color-key} {
+    color: $color-value;
+  }
+}
+
+button {
+  color: btn-color("primary");
+}
+
// 创建一个颜色映射表
+$colors: (
+  "primary": #007bff,
+  "secondary": #6c757d,
+  "success": #28a745,
+  "info": #17a2b8,
+  "warning": #ffc107,
+  "danger": #dc3545,
+);
+
+// 演示通过 map-get 获取对应的值
+@function btn-color($color-name) {
+  @return map-get($colors, $color-name);
+}
+
+// 演示通过 map-keys 获取映射表所有的 keys
+$color-keys: map-keys($colors);
+
+// 一个新的颜色映射表
+$more-colors: (
+  "light": #f8f9fa,
+  "dark": #343a40,
+);
+
+// 要将新的颜色映射表合并到 $colors 里面
+
+$all-colors: map-merge($colors, $more-colors);
+
+// 接下来我们来根据颜色映射表生成样式
+@each $color-key, $color-value in $all-colors {
+  .text-#{$color-key} {
+    color: $color-value;
+  }
+}
+
+button {
+  color: btn-color("primary");
+}
+
css
.text-primary {
+  color: #007bff;
+}
+
+.text-secondary {
+  color: #6c757d;
+}
+
+.text-success {
+  color: #28a745;
+}
+
+.text-info {
+  color: #17a2b8;
+}
+
+.text-warning {
+  color: #ffc107;
+}
+
+.text-danger {
+  color: #dc3545;
+}
+
+.text-light {
+  color: #f8f9fa;
+}
+
+.text-dark {
+  color: #343a40;
+}
+
+button {
+  color: #007bff;
+}
+
.text-primary {
+  color: #007bff;
+}
+
+.text-secondary {
+  color: #6c757d;
+}
+
+.text-success {
+  color: #28a745;
+}
+
+.text-info {
+  color: #17a2b8;
+}
+
+.text-warning {
+  color: #ffc107;
+}
+
+.text-danger {
+  color: #dc3545;
+}
+
+.text-light {
+  color: #f8f9fa;
+}
+
+.text-dark {
+  color: #343a40;
+}
+
+button {
+  color: #007bff;
+}
+

颜色相关内置函数

RGB 函数

函数名和参数类型函数作用
rgb($red, $green, $blue)返回一个 16 进制颜色值
rgba($red,$green,$blue,$alpha)返回一个 rgba;$red,$green 和$blue 可被当作一个整体以颜色单词、hsl、rgb 或 16 进制形式传入
red($color)从$color 中获取其中红色值
green($color)从$color 中获取其中绿色值
blue($color)从$color 中获取其中蓝色值
mix($color1,$color2,$weight?)按照$weight比例,将$color1 和$color2 混合为一个新颜色

HSL 函数

函数名和参数类型函数作用
hsl($hue,$saturation,$lightness)通过色相(hue)、饱和度(saturation)和亮度(lightness)的值创建一个颜色
hsla($hue,$saturation,$lightness,$alpha)通过色相(hue)、饱和度(saturation)、亮度(lightness)和透明(alpha)的值创建一个颜色
saturation($color)从一个颜色中获取饱和度(saturation)值
lightness($color)从一个颜色中获取亮度(lightness)值
adjust-hue($color,$degrees)通过改变一个颜色的色相值,创建一个新的颜色
lighten($color,$amount)通过改变颜色的亮度值,让颜色变亮,创建一个新的颜色
darken($color,$amount)通过改变颜色的亮度值,让颜色变暗,创建一个新的颜色
hue($color)从一个颜色中获取亮度色相(hue)值

Opacity 函数

函数名和参数类型函数作用
alpha($color)/opacity($color)获取颜色透明度值
rgba($color,$alpha)改变颜色的透明度
opacify($color, $amount) / fade-in($color, $amount)使颜色更不透明
transparentize($color, $amount) / fade-out($color, $amount)使颜色更加透明

其他内置函数

函数名和参数类型函数作用
type-of($value)返回$value 的类型
unit($number)返回$number 的单位
unitless($number)判断$number 是否没用带单位,返回对应的布尔值,没有带单位为 true
comparable($number1, $number2)判断$number1和$number2 是否可以做加、减和合并,返回对应的布尔值

示例如下:

scss
$value: 42;
+
+$value-type: type-of($value); // number
+
+$length: 10px;
+$length-unit: unit($length); // "px"
+
+$is-unitless: unitless(42); // true
+
+$can-compare: comparable(1px, 2em); // false
+$can-compare2: comparable(1px, 2px); // true
+
+// 根据 type-of 函数的结果生成样式
+.box {
+  content: "Value type: #{$value-type}";
+}
+
+// 根据 unit 函数的结果生成样式
+.length-label {
+  content: "Length unit: #{$length-unit}";
+}
+
+// 根据 unitless 函数的结果生成样式
+.unitless-label {
+  content: "Is unitless: #{$is-unitless}";
+}
+
+// 根据 comparable 函数的结果生成样式
+.comparable-label {
+  content: "Can compare: #{$can-compare}";
+}
+
+.comparable-label2 {
+  content: "Can compare: #{$can-compare2}";
+}
+
$value: 42;
+
+$value-type: type-of($value); // number
+
+$length: 10px;
+$length-unit: unit($length); // "px"
+
+$is-unitless: unitless(42); // true
+
+$can-compare: comparable(1px, 2em); // false
+$can-compare2: comparable(1px, 2px); // true
+
+// 根据 type-of 函数的结果生成样式
+.box {
+  content: "Value type: #{$value-type}";
+}
+
+// 根据 unit 函数的结果生成样式
+.length-label {
+  content: "Length unit: #{$length-unit}";
+}
+
+// 根据 unitless 函数的结果生成样式
+.unitless-label {
+  content: "Is unitless: #{$is-unitless}";
+}
+
+// 根据 comparable 函数的结果生成样式
+.comparable-label {
+  content: "Can compare: #{$can-compare}";
+}
+
+.comparable-label2 {
+  content: "Can compare: #{$can-compare2}";
+}
+
css
.box {
+  content: "Value type: number";
+}
+
+.length-label {
+  content: "Length unit: px";
+}
+
+.unitless-label {
+  content: "Is unitless: true";
+}
+
+.comparable-label {
+  content: "Can compare: false";
+}
+
+.comparable-label2 {
+  content: "Can compare: true";
+}
+
.box {
+  content: "Value type: number";
+}
+
+.length-label {
+  content: "Length unit: px";
+}
+
+.unitless-label {
+  content: "Is unitless: true";
+}
+
+.comparable-label {
+  content: "Can compare: false";
+}
+
+.comparable-label2 {
+  content: "Can compare: true";
+}
+

@规则

在原生 CSS 中,存在一些 @ 开头的规则,例如 @import、@media,Sass 对这些 @ 规则完全支持,不仅支持,还在原有的基础上做了一些扩展。

@import

在原生的 CSS 里面,@import 是导入其他的 CSS 文件,Sass 再此基础上做了一些增强:

  1. 编译时合并:Sass 在编译时将导入的文件内容合并到生成的 CSS 文件中,这意味着只会生成一个 CSS 文件,而不是像原生 CSS 那样需要额外的 HTTP 请求去加载导入的文件。

  2. 变量、函数和混合体共享:Sass 允许在导入的文件之间共享变量、函数和混合体,这有助于组织代码并避免重复。

  3. 部分文件:Sass 支持将文件名前缀为 _ 的文件称为部分文件(partials)。当使用 @import 指令导入部分文件时,Sass 不会生成一个单独的 CSS 文件,而是将其内容合并到主文件中。这有助于更好地组织项目。

  4. 文件扩展名可选:在 Sass 中,使用 @import 指令时可以省略文件扩展名(.scss 或 .sass),Sass 会自动识别并导入正确的文件。

  5. 嵌套导入:Sass 允许在一个文件中嵌套导入其他文件,但请注意,嵌套导入的文件将在父级上下文中编译,这可能会导致输出的 CSS 文件中的选择器层级不符合预期。

接下来,我们来看一个具体的例子:

src/
+  ├── _variable.scss
+  ├── _mixins.scss
+  ├── _header.scss
+  └── index.scss
+
src/
+  ├── _variable.scss
+  ├── _mixins.scss
+  ├── _header.scss
+  └── index.scss
+
scss
// _variable.scss
+$primary-color: #007bff;
+$secondary-color: #6c757d;
+
// _variable.scss
+$primary-color: #007bff;
+$secondary-color: #6c757d;
+
scss
// _mixins.scss
+@mixin reset-margin-padding {
+  margin: 0;
+  padding: 0;
+}
+
// _mixins.scss
+@mixin reset-margin-padding {
+  margin: 0;
+  padding: 0;
+}
+
scss
// _header.scss
+header {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+
// _header.scss
+header {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+

可以看出,在 _header.scss 里面使用了另外两个 scss 所定义的变量以及混合体,说明变量、函数和混合体是可以共享的。

之后我们在 index.scss 里面导入了这三个 scss

scss
@import "variable";
+@import "mixins";
+@import "header";
+
+body {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+
@import "variable";
+@import "mixins";
+@import "header";
+
+body {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+

最终生成的 css 如下:

css
header {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+
header {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+

最终只会生成一个 css。

通常情况下,我们在通过 @import 导入文件的时候,不给后缀名,会自动的寻找 sass 文件并将其导入。但是有一些情况下,会编译为普通的 CSS 语句,并不会导入任何文件:

  • 文件拓展名是 .css
  • 文件名以 http😕/ 开头;
  • 文件名是 url();
  • @import 包含 media queries

例如:

scss
@import "foo.css" @import "foo" screen;
+@import "http://foo.com/bar";
+@import url(foo);
+
@import "foo.css" @import "foo" screen;
+@import "http://foo.com/bar";
+@import url(foo);
+

@media

这个规则在原生 CSS 里面是做媒体查询,Sass 里面是完全支持的,并且做了一些增强操作。

  1. Sass 里面允许你讲 @media 嵌套在选择器内部
scss
.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: 768px) {
+    flex-direction: column;
+  }
+}
+
.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: 768px) {
+    flex-direction: column;
+  }
+}
+
css
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
  1. 允许使用变量
scss
$mobile-breakpoint: 768px;
+
+.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: $mobile-breakpoint) {
+    flex-direction: column;
+  }
+}
+
$mobile-breakpoint: 768px;
+
+.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: $mobile-breakpoint) {
+    flex-direction: column;
+  }
+}
+
css
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
  1. 可以使用混合体
scss
@mixin respond-to($breakpoint) {
+  @if $breakpoint == "mobile" {
+    @media (max-width: 768px) {
+      @content;
+    }
+  } @else if $breakpoint == "tablet" {
+    @media (min-width: 769px) and (max-width: 1024px) {
+      @content;
+    }
+  } @else if $breakpoint == "desktop" {
+    @media (min-width: 1025px) {
+      @content;
+    }
+  }
+}
+
+.container {
+  width: 80%;
+  @include respond-to("mobile") {
+    width: 100%;
+  }
+  @include respond-to("desktop") {
+    width: 70%;
+  }
+}
+
@mixin respond-to($breakpoint) {
+  @if $breakpoint == "mobile" {
+    @media (max-width: 768px) {
+      @content;
+    }
+  } @else if $breakpoint == "tablet" {
+    @media (min-width: 769px) and (max-width: 1024px) {
+      @content;
+    }
+  } @else if $breakpoint == "desktop" {
+    @media (min-width: 1025px) {
+      @content;
+    }
+  }
+}
+
+.container {
+  width: 80%;
+  @include respond-to("mobile") {
+    width: 100%;
+  }
+  @include respond-to("desktop") {
+    width: 70%;
+  }
+}
+
css
.container {
+  width: 80%;
+}
+@media (max-width: 768px) {
+  .container {
+    width: 100%;
+  }
+}
+@media (min-width: 1025px) {
+  .container {
+    width: 70%;
+  }
+}
+
.container {
+  width: 80%;
+}
+@media (max-width: 768px) {
+  .container {
+    width: 100%;
+  }
+}
+@media (min-width: 1025px) {
+  .container {
+    width: 70%;
+  }
+}
+

@extend

我们在书写 CSS 样式的时候,经常会遇到一种情况:一个元素使用的样式和另外一个元素基本相同,但是又增加了一些额外的样式。这个时候就可以使用继承。Sass 里面提供了@extend 规则来实现继承,让一个选择器能够继承另外一个选择器的样式规则。

scss
.button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend .button;
+  background-color: blue;
+}
+
.button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend .button;
+  background-color: blue;
+}
+
css
.button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+
.button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+

如果是刚接触的同学,可能会觉得 @extend 和 @mixin 比较相似,感觉都是把公共的样式提取出来了,但是两者其实是不同的。

  • 参数支持:@mixin 支持传递参数,使其更具灵活性;而 @extend 不支持参数传递。
  • 生成的 CSS:@extend 会将选择器合并,生成更紧凑的 CSS,并且所继承的样式在最终生成的 CSS 样式中也是真实存在的;而 @mixin 会在每个 @include 处生成完整的 CSS 代码,做的就是一个简单的 CSS 替换。
  • 使用场景:@extend 更适用于继承已有样式的情况,例如 UI 框架中的通用样式;而 @mixin 更适用于需要自定义参数的情况,例如为不同组件生成类似的样式。

接下来我们来看一个复杂的例子:

scss
.box {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  @extend .box;
+  border-width: 3px;
+}
+
+.box.a {
+  background-image: url("/image/abc.png");
+}
+
.box {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  @extend .box;
+  border-width: 3px;
+}
+
+.box.a {
+  background-image: url("/image/abc.png");
+}
+
css
.box,
+.container {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  border-width: 3px;
+}
+
+.box.a,
+.a.container {
+  background-image: url("/image/abc.png");
+}
+
.box,
+.container {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  border-width: 3px;
+}
+
+.box.a,
+.a.container {
+  background-image: url("/image/abc.png");
+}
+

在上面的代码中,container 是继承了 box 里面的所有样式,假设一个元素需要有 box 和 a 这两个类才能对应一段样式(abc),由于 box 类所对应的样式,如果是挂 container 这个类的话,这些样式也会有,所以一个元素如果挂了 container 和 a 这两个类,同样应该应用对应 abc 样式。

有些时候,我们需要定义一套用于继承的样式,不希望 Sass 单独编译输出,那么这种情况下就可以使用 % 作为占位符。

scss
%button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend %button;
+  background-color: blue;
+}
+
+.secondary-button {
+  @extend %button;
+  background-color: pink;
+}
+
%button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend %button;
+  background-color: blue;
+}
+
+.secondary-button {
+  @extend %button;
+  background-color: pink;
+}
+
css
.secondary-button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+
+.secondary-button {
+  background-color: pink;
+}
+
.secondary-button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+
+.secondary-button {
+  background-color: pink;
+}
+

@at-root

有些时候,我们可能会涉及到将嵌套规则移动到根级别(声明的时候并没有写在根级别)。这个时候就可以使用 @at-root

scss
.parent {
+  color: red;
+
+  @at-root .child {
+    color: blue;
+  }
+}
+
.parent {
+  color: red;
+
+  @at-root .child {
+    color: blue;
+  }
+}
+
css
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+

如果你想要移动的是一组规则,这个时候需要在 @at-root 后面添加一对大括号,将想要移动的这一组样式放入到大括号里面

scss
.parent {
+  color: red;
+
+  @at-root {
+    .child {
+      color: blue;
+    }
+    .test {
+      color: pink;
+    }
+    .test2 {
+      color: purple;
+    }
+  }
+}
+
.parent {
+  color: red;
+
+  @at-root {
+    .child {
+      color: blue;
+    }
+    .test {
+      color: pink;
+    }
+    .test2 {
+      color: purple;
+    }
+  }
+}
+
css
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+
+.test {
+  color: pink;
+}
+
+.test2 {
+  color: purple;
+}
+
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+
+.test {
+  color: pink;
+}
+
+.test2 {
+  color: purple;
+}
+

@debug、@warn、@error

这三个规则是和调试相关的,可以让我们在编译过程中输出一条信息,有助于调试和诊断代码中的问题。

Sass 最佳实践与展望

最佳实践

  1. 合理的组织你的文件和目录结构:可以将一个大的 Sass 文件拆分成一个一个小的模块,我们可以按照功能、页面、组件来划分我们的文件结构,这样的话更加利于我们的项目维护。
scss
sass/
+|
+|-- base/
+|   |-- _reset.scss  // 重置样式
+|   |-- _typography.scss  // 排版相关样式
+|   ...
+|
+|-- components/
+|   |-- _buttons.scss  // 按钮相关样式
+|   |-- _forms.scss  // 表单相关样式
+|   ...
+|
+|-- layout/
+|   |-- _header.scss  // 页眉相关样式
+|   |-- _footer.scss  // 页脚相关样式
+|   ...
+|
+|-- pages/
+|   |-- _home.scss  // 首页相关样式
+|   |-- _about.scss  // 关于页面相关样式
+|   ...
+|
+|-- utils/
+|   |-- _variables.scss  // 变量定义
+|   |-- _mixins.scss  // 混入定义
+|   |-- _functions.scss  // 函数定义
+|   ...
+|
+|-- main.scss // 主入口文件
+
sass/
+|
+|-- base/
+|   |-- _reset.scss  // 重置样式
+|   |-- _typography.scss  // 排版相关样式
+|   ...
+|
+|-- components/
+|   |-- _buttons.scss  // 按钮相关样式
+|   |-- _forms.scss  // 表单相关样式
+|   ...
+|
+|-- layout/
+|   |-- _header.scss  // 页眉相关样式
+|   |-- _footer.scss  // 页脚相关样式
+|   ...
+|
+|-- pages/
+|   |-- _home.scss  // 首页相关样式
+|   |-- _about.scss  // 关于页面相关样式
+|   ...
+|
+|-- utils/
+|   |-- _variables.scss  // 变量定义
+|   |-- _mixins.scss  // 混入定义
+|   |-- _functions.scss  // 函数定义
+|   ...
+|
+|-- main.scss // 主入口文件
+
  1. 多使用变量:在开发的时候经常会遇到一些会重复使用的值(颜色、字体、尺寸),我们就可以将这些值定义为变量,方便在项目中统一进行管理和修改。

  2. 关于嵌套:嵌套非常好用,但是要避免层数过多的嵌套,通常来讲不要超过 3 层

scss
.a {
+  .....
+  .b {
+    ....
+  }
+}
+
.a {
+  .....
+  .b {
+    ....
+  }
+}
+
css
.a {
+  ....;
+}
+.a .b {
+  ....;
+}
+
.a {
+  ....;
+}
+.a .b {
+  ....;
+}
+

如果嵌套层数过多:

scss
.a{
+  ...
+  .b{
+    ...
+    .c {
+      ...
+      .d {
+        ....
+        .e{
+          ....
+        }
+      }
+    }
+  }
+}
+
.a{
+  ...
+  .b{
+    ...
+    .c {
+      ...
+      .d {
+        ....
+        .e{
+          ....
+        }
+      }
+    }
+  }
+}
+
css
.a .b .c .d .e {
+  .....;
+}
+
.a .b .c .d .e {
+  .....;
+}
+
  1. 多使用混合指令:混合指令可以将公共的部分抽离出来,提高了代码的复用性。但是要清楚混合指令和 @extend 之间的区别,具体使用哪一个,取决于你写项目时的具体场景,不是说某一个就比另一个绝对的好。
  2. 使用函数:可以编写自定义函数来处理一些复杂的计算和操作。而且 Sass 还提供了很多非常好用的内置函数。
  3. 遵循常见的 Sass 编码规范:

Sass 未来发展

我们如果想要获取到 Sass 的最新动向,通常可以去 Sass 的社区看一下。

注意:一门成熟的技术,是一定会有对应社区的。理论上来讲,社区的形式是不限的,但是通常是以论坛的形式存在的,大家可以在论坛社区自由的讨论这门技术相关的话题。

社区往往包含了这门技术最新的动态,甚至有一些优秀的技术解决方案是先来自于社区,之后才慢慢成为正式的标准语法的。

目前市面上又很多 CSS 库都是基于 Sass 来进行构建了,例如:

  1. Compass - 老牌 Sass 框架,提供大量 Sass mixins 和函数,方便开发。
  2. Bourbon - 轻量级的 Sass mixin 库,提供常用的 mixins,简化 CSS 开发。
  3. Neat - 构建具有响应式网格布局的网站,基于 SassBourbon,容易上手。
  4. Materialize - 实现 Material Design 风格,基于 Sass 构建,提供丰富组件和元素。
  5. Bulma - 现代 CSS 框架,提供弹性网格和常见组件,可与 Sass 一起使用。
  6. Foundation - 老牌前端框架,基于 Sass,提供全面的组件和工具,适合构建复杂项目。
  7. Semantic UI - 设计美观的 UI 套件,基于 Sass 构建,提供丰富样式和交互。
  8. Spectre.css - 轻量级、响应式和现代的 CSS 框架,可以与 Sass 结合使用。

因此,基本上目前 Sass 已经成为了前端开发人员首选的 CSS 预处理器。因为 Sass 相比其他两个 CSS 预处理器,功能是最强大的,特性是最多的,社区也是最活跃的。

关于 Sass 官方团队,未来再对 Sass 进行更新的时候,基本上会往以下几个方面做出努力:

  • 性能优化
  • 持续的与现代 Web 技术的集成
  • 新功能的改进

本文所有源码均在 https://github.com/Sunny-117/blog/tree/main/code/sass-demo

`,394),e=[o];function c(r,t,B,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{d as __pageData,u as default}; diff --git "a/assets/front-end-engineering_CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.md.8d9a6c7d.lean.js" "b/assets/front-end-engineering_CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.md.8d9a6c7d.lean.js" new file mode 100644 index 00000000..b6f84745 --- /dev/null +++ "b/assets/front-end-engineering_CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.md.8d9a6c7d.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const d=JSON.parse('{"title":"CSS 预处理器之 SCSS","description":"","frontmatter":{},"headers":[{"level":2,"title":"概述","slug":"概述","link":"#概述","children":[]},{"level":2,"title":"CSS 预处理器","slug":"css-预处理器","link":"#css-预处理器","children":[{"level":3,"title":"预处理器基本介绍","slug":"预处理器基本介绍","link":"#预处理器基本介绍","children":[]},{"level":3,"title":"Sass 快速入门","slug":"sass-快速入门","link":"#sass-快速入门","children":[]}]},{"level":2,"title":"Sass 基础语法","slug":"sass-基础语法","link":"#sass-基础语法","children":[{"level":3,"title":"注释","slug":"注释","link":"#注释","children":[]},{"level":3,"title":"变量","slug":"变量","link":"#变量","children":[]},{"level":3,"title":"数据类型","slug":"数据类型","link":"#数据类型","children":[]},{"level":3,"title":"嵌套语法","slug":"嵌套语法","link":"#嵌套语法","children":[]},{"level":3,"title":"插值语法","slug":"插值语法","link":"#插值语法","children":[]},{"level":3,"title":"运算","slug":"运算","link":"#运算","children":[]}]},{"level":2,"title":"Sass 控制指令","slug":"sass-控制指令","link":"#sass-控制指令","children":[{"level":3,"title":"三元运算符","slug":"三元运算符","link":"#三元运算符","children":[]},{"level":3,"title":"三元运算符","slug":"三元运算符-1","link":"#三元运算符-1","children":[]},{"level":3,"title":"@if 分支","slug":"if-分支","link":"#if-分支","children":[]},{"level":3,"title":"@for 循环","slug":"for-循环","link":"#for-循环","children":[]},{"level":3,"title":"@while 循环","slug":"while-循环","link":"#while-循环","children":[]},{"level":3,"title":"@each 循环","slug":"each-循环","link":"#each-循环","children":[]}]},{"level":2,"title":"Sass 混合指令","slug":"sass-混合指令","link":"#sass-混合指令","children":[{"level":3,"title":"混合指令基本的使用","slug":"混合指令基本的使用","link":"#混合指令基本的使用","children":[]},{"level":3,"title":"混合指令的参数","slug":"混合指令的参数","link":"#混合指令的参数","children":[]},{"level":3,"title":"@content","slug":"content","link":"#content","children":[]}]},{"level":2,"title":"Sass 函数指令","slug":"sass-函数指令","link":"#sass-函数指令","children":[{"level":3,"title":"自定义函数","slug":"自定义函数","link":"#自定义函数","children":[]},{"level":3,"title":"内置函数","slug":"内置函数","link":"#内置函数","children":[]}]},{"level":2,"title":"@规则","slug":"规则","link":"#规则","children":[{"level":3,"title":"@import","slug":"import","link":"#import","children":[]},{"level":3,"title":"@media","slug":"media","link":"#media","children":[]},{"level":3,"title":"@extend","slug":"extend","link":"#extend","children":[]},{"level":3,"title":"@at-root","slug":"at-root","link":"#at-root","children":[]},{"level":3,"title":"@debug、@warn、@error","slug":"debug、-warn、-error","link":"#debug、-warn、-error","children":[]}]},{"level":2,"title":"Sass 最佳实践与展望","slug":"sass-最佳实践与展望","link":"#sass-最佳实践与展望","children":[{"level":3,"title":"最佳实践","slug":"最佳实践","link":"#最佳实践","children":[]},{"level":3,"title":"Sass 未来发展","slug":"sass-未来发展","link":"#sass-未来发展","children":[]}]}],"relativePath":"front-end-engineering/CSS 预处理器之SCSS.md","lastUpdated":1715070178000}'),p={name:"front-end-engineering/CSS 预处理器之SCSS.md"},o=l("",394),e=[o];function c(r,t,B,y,i,F){return n(),a("div",null,e)}const u=s(p,[["render",c]]);export{d as __pageData,u as default}; diff --git "a/assets/front-end-engineering_CSS\345\267\245\347\250\213\345\214\226.md.6acce3ef.js" "b/assets/front-end-engineering_CSS\345\267\245\347\250\213\345\214\226.md.6acce3ef.js" new file mode 100644 index 00000000..edf1b511 --- /dev/null +++ "b/assets/front-end-engineering_CSS\345\267\245\347\250\213\345\214\226.md.6acce3ef.js" @@ -0,0 +1,665 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-14-44.9c23195b.png",o="/blog/assets/2023-01-06-16-16-45.8cacfb52.png",e="/blog/assets/2023-01-06-16-16-57.792b0ec0.png",c="/blog/assets/2023-01-06-16-17-35.bce22d59.png",r="/blog/assets/2023-01-06-16-23-08.16c5b4ae.png",t="/blog/assets/2023-01-06-16-23-16.12a8f778.png",B="/blog/assets/2023-01-06-16-37-27.347ac1c6.png",i="/blog/assets/2023-01-06-16-37-43.6a6a7a3c.png",y="/blog/assets/2023-01-06-16-37-56.802983a9.png",F="/blog/assets/2023-01-06-16-38-33.30911d0e.png",E=JSON.parse('{"title":"CSS 工程化","description":"","frontmatter":{},"headers":[{"level":2,"title":"css 的问题","slug":"css-的问题","link":"#css-的问题","children":[{"level":3,"title":"类名冲突的问题","slug":"类名冲突的问题","link":"#类名冲突的问题","children":[]},{"level":3,"title":"重复样式","slug":"重复样式","link":"#重复样式","children":[]},{"level":3,"title":"css 文件细分问题","slug":"css-文件细分问题","link":"#css-文件细分问题","children":[]}]},{"level":2,"title":"如何解决","slug":"如何解决","link":"#如何解决","children":[{"level":3,"title":"解决类名冲突","slug":"解决类名冲突","link":"#解决类名冲突","children":[]},{"level":3,"title":"解决重复样式的问题","slug":"解决重复样式的问题","link":"#解决重复样式的问题","children":[]},{"level":3,"title":"解决 css 文件细分问题","slug":"解决-css-文件细分问题","link":"#解决-css-文件细分问题","children":[]}]},{"level":2,"title":"利用 webpack 拆分 css","slug":"利用-webpack-拆分-css","link":"#利用-webpack-拆分-css","children":[{"level":3,"title":"css-loader","slug":"css-loader","link":"#css-loader","children":[]},{"level":3,"title":"style-loader","slug":"style-loader","link":"#style-loader","children":[]}]},{"level":2,"title":"BEM","slug":"bem","link":"#bem","children":[]},{"level":2,"title":"css in js","slug":"css-in-js","link":"#css-in-js","children":[]},{"level":2,"title":"css module","slug":"css-module","link":"#css-module","children":[{"level":3,"title":"思路","slug":"思路","link":"#思路","children":[]},{"level":3,"title":"实现原理","slug":"实现原理","link":"#实现原理","children":[]},{"level":3,"title":"如何应用样式","slug":"如何应用样式","link":"#如何应用样式","children":[]},{"level":3,"title":"其他操作","slug":"其他操作","link":"#其他操作","children":[]},{"level":3,"title":"全局类名","slug":"全局类名","link":"#全局类名","children":[]},{"level":3,"title":"如何控制最终的类名","slug":"如何控制最终的类名","link":"#如何控制最终的类名","children":[]},{"level":3,"title":"其他注意事项","slug":"其他注意事项","link":"#其他注意事项","children":[]}]},{"level":2,"title":"CSS 预编译器","slug":"css-预编译器","link":"#css-预编译器","children":[{"level":3,"title":"基本原理","slug":"基本原理","link":"#基本原理","children":[]},{"level":3,"title":"LESS 的安装和使用","slug":"less-的安装和使用","link":"#less-的安装和使用","children":[]},{"level":3,"title":"LESS 的基本使用","slug":"less-的基本使用","link":"#less-的基本使用","children":[]},{"level":3,"title":"webpack 中使用 less","slug":"webpack-中使用-less","link":"#webpack-中使用-less","children":[]}]},{"level":2,"title":"PostCss","slug":"postcss","link":"#postcss","children":[{"level":3,"title":"什么是 PostCss","slug":"什么是-postcss","link":"#什么是-postcss","children":[]},{"level":3,"title":"安装","slug":"安装","link":"#安装","children":[]},{"level":3,"title":"配置文件","slug":"配置文件","link":"#配置文件","children":[]},{"level":3,"title":"插件","slug":"插件","link":"#插件","children":[]},{"level":3,"title":"postcss-preset-env","slug":"postcss-preset-env","link":"#postcss-preset-env","children":[]},{"level":3,"title":"自动的厂商前缀","slug":"自动的厂商前缀","link":"#自动的厂商前缀","children":[]},{"level":3,"title":"未来的 CSS 语法","slug":"未来的-css-语法","link":"#未来的-css-语法","children":[]},{"level":3,"title":"postcss-apply","slug":"postcss-apply","link":"#postcss-apply","children":[]},{"level":3,"title":"postcss-color-function","slug":"postcss-color-function","link":"#postcss-color-function","children":[]},{"level":3,"title":"postcss-import","slug":"postcss-import","link":"#postcss-import","children":[]}]},{"level":2,"title":"stylelint","slug":"stylelint","link":"#stylelint","children":[]},{"level":2,"title":"抽离 css 文件","slug":"抽离-css-文件","link":"#抽离-css-文件","children":[]},{"level":2,"title":"原子化 CSS(了解)","slug":"原子化-css-了解","link":"#原子化-css-了解","children":[]}],"relativePath":"front-end-engineering/CSS工程化.md","lastUpdated":1715076847000}'),d={name:"front-end-engineering/CSS工程化.md"},u=l(`

CSS 工程化

css 的问题

类名冲突的问题

当你写一个 css 类的时候,你是写全局的类呢,还是写多个层级选择后的类呢?

你会发现,怎么都不好

  • 过深的层级不利于编写、阅读、压缩、复用
  • 过浅的层级容易导致类名冲突

一旦样式多起来,这个问题就会变得越发严重,其实归根结底,就是类名冲突不好解决的问题

重复样式

这种问题就更普遍了,一些重复的样式值总是不断的出现在 css 代码中,维护起来极其困难

比如,一个网站的颜色一般就那么几种:

  • primary
  • info
  • warn
  • error
  • success

如果有更多的颜色,都是从这些色调中自然变化得来,可以想象,这些颜色会到处充斥到诸如背景、文字、边框中,一旦要做颜色调整,是一个非常大的工程

css 文件细分问题

在大型项目中,css 也需要更细的拆分,这样有利于 css 代码的维护。

比如,有一个做轮播图的模块,它不仅需要依赖 js 功能,还需要依赖 css 样式,既然依赖的 js 功能仅关心轮播图,那 css 样式也应该仅关心轮播图,由此类推,不同的功能依赖不同的 css 样式、公共样式可以单独抽离,这样就形成了不同于过去的 css 文件结构:文件更多、拆分的更细

而同时,在真实的运行环境下,我们却希望文件越少越好,这种情况和 JS 遇到的情况是一致的

因此,对于 css,也需要工程化管理

从另一个角度来说,css 的工程化会遇到更多的挑战,因为 css 不像 JS,它的语法本身经过这么多年并没有发生多少的变化(css3 也仅仅是多了一些属性而已),对于 css 语法本身的改变也是一个工程化的课题

如何解决

这么多年来,官方一直没有提出方案来解决上述问题

一些第三方机构针对不同的问题,提出了自己的解决方案

解决类名冲突

一些第三方机构提出了一些方案来解决该问题,常见的解决方案如下:

命名约定

即提供一种命名的标准,来解决冲突,常见的标准有:

  • BEM
  • OOCSS
  • AMCSS
  • SMACSS
  • 其他

css in js

这种方案非常大胆,它觉得,css 语言本身几乎无可救药了,干脆直接用 js 对象来表示样式,然后把样式直接应用到元素的 style 中

这样一来,css 变成了一个一个的对象,就可以完全利用到 js 语言的优势,你可以:

  • 通过一个函数返回一个样式对象
  • 把公共的样式提取到公共模块中返回
  • 应用 js 的各种特性操作对象,比如:混合、提取、拆分
  • 更多的花样

这种方案在手机端的 React Native 中大行其道

css module

非常有趣和好用的 css 模块化方案,编写简单,绝对不重名

具体的课程中详细介绍

解决重复样式的问题

css in js

这种方案虽然可以利用 js 语言解决重复样式值的问题,但由于太过激进,很多习惯写 css 的开发者编写起来并不是很适应

预编译器

有些第三方搞出一套 css 语言的进化版来解决这个问题,它支持变量、函数等高级语法,然后经过编译器将其编译成为正常的 css

这种方案特别像构建工具,不过它仅针对 css

常见的预编译器支持的语言有:

  • less
  • sass

解决 css 文件细分问题

这一部分,就要依靠构建工具,例如 webpack 来解决了

利用一些 loader 或 plugin 来打包、合并、压缩 css 文件

利用 webpack 拆分 css

要拆分 css,就必须把 css 当成像 js 那样的模块;要把 css 当成模块,就必须有一个构建工具(webpack),它具备合并代码的能力

而 webpack 本身只能读取 css 文件的内容、将其当作 JS 代码进行分析,因此,会导致错误

于是,就必须有一个 loader,能够将 css 代码转换为 js 代码

css-loader

css-loader 的作用,就是将 css 代码转换为 js 代码

它的处理原理极其简单:将 css 代码作为字符串导出

例如:

css
.red {
+  color: "#f40";
+}
+
.red {
+  color: "#f40";
+}
+

经过 css-loader 转换后变成 js 代码:

javascript
module.exports = \`.red{
+    color:"#f40";
+}\`;
+
module.exports = \`.red{
+    color:"#f40";
+}\`;
+

上面的 js 代码是经过我简化后的,不代表真实的 css-loader 的转换后代码,css-loader 转换后的代码会有些复杂,同时会导出更多的信息,但核心思想不变

再例如:

css
.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+
.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+

经过 css-loader 转换后变成 js 代码:

javascript
var import1 = require("./bg.png");
+module.exports = \`.red{
+    color:"#f40";
+    background:url("\${import1}")
+}\`;
+
var import1 = require("./bg.png");
+module.exports = \`.red{
+    color:"#f40";
+    background:url("\${import1}")
+}\`;
+

这样一来,经过 webpack 的后续处理,会把依赖./bg.png添加到模块列表,然后再将代码转换为

javascript
var import1 = __webpack_require__("./src/bg.png");
+module.exports = \`.red{
+    color:"#f40";
+    background:url("\${import1}")
+}\`;
+
var import1 = __webpack_require__("./src/bg.png");
+module.exports = \`.red{
+    color:"#f40";
+    background:url("\${import1}")
+}\`;
+

再例如:

css
@import "./reset.css";
+.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+
@import "./reset.css";
+.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+

会转换为:

javascript
var import1 = require("./reset.css");
+var import2 = require("./bg.png");
+module.exports = \`\${import1}
+.red{
+    color:"#f40";
+    background:url("\${import2}")
+}\`;
+
var import1 = require("./reset.css");
+var import2 = require("./bg.png");
+module.exports = \`\${import1}
+.red{
+    color:"#f40";
+    background:url("\${import2}")
+}\`;
+

总结,css-loader 干了什么:

  1. 将 css 文件的内容作为字符串导出
  2. 将 css 中的其他依赖作为 require 导入,以便 webpack 分析依赖

style-loader

由于 css-loader 仅提供了将 css 转换为字符串导出的能力,剩余的事情要交给其他 loader 或 plugin 来处理

style-loader 可以将 css-loader 转换后的代码进一步处理,将 css-loader 导出的字符串加入到页面的 style 元素中

例如:

css
.red {
+  color: "#f40";
+}
+
.red {
+  color: "#f40";
+}
+

经过 css-loader 转换后变成 js 代码:

javascript
module.exports = \`.red{
+    color:"#f40";
+}\`;
+
module.exports = \`.red{
+    color:"#f40";
+}\`;
+

经过 style-loader 转换后变成:

javascript
module.exports = \`.red{
+    color:"#f40";
+}\`;
+var style = module.exports;
+var styleElem = document.createElement("style");
+styleElem.innerHTML = style;
+document.head.appendChild(styleElem);
+module.exports = {};
+
module.exports = \`.red{
+    color:"#f40";
+}\`;
+var style = module.exports;
+var styleElem = document.createElement("style");
+styleElem.innerHTML = style;
+document.head.appendChild(styleElem);
+module.exports = {};
+

以上代码均为简化后的代码,并不代表真实的代码 style-loader 有能力避免同一个样式的重复导入

配置:先用 css-loader 在 style-loader

js
rules: [
+  {
+    test: /\\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+];
+
rules: [
+  {
+    test: /\\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+];
+

BEM

BEM 是一套针对 css 类样式的命名方法。

其他命名方法还有:OOCSS、AMCSS、SMACSS 等等

BEM 全称是:Block Element Modifier

一个完整的 BEM 类名:block**element_modifier,例如:banner**dot_selected,可以表示:轮播图中,处于选中状态的小圆点

一切类名都采用顶级的,不分级

css
banner__dot_selected {
+}
+banner_dot_unselected {
+}
+nav__dot_selected {
+}
+
banner__dot_selected {
+}
+banner_dot_unselected {
+}
+nav__dot_selected {
+}
+

三个部分的具体含义为:

  • Block:页面中的大区域,表示最顶级的划分,例如:轮播图(banner)、布局(layout)、文章(article)等等
  • element:区域中的组成部分,例如:轮播图中的横幅图片(banner__img)、轮播图中的容器(banner__container)、布局中的头部(layout__header)、文章中的标题(article_title)
  • modifier:可选。通常表示状态,例如:处于展开状态的布局左边栏(layout__left_expand)、处于选中状态的轮播图小圆点(banner__dot_selected)

在某些大型工程中,如果使用 BEM 命名法,还可能会增加一个前缀,来表示类名的用途,常见的前缀有:

  • l: layout,表示这个样式是用于布局的
  • c: component,表示这个样式是一个组件,即一个功能区域
  • u: util,表示这个样式是一个通用的、工具性质的样式
  • j: javascript,表示这个样式没有实际意义,是专门提供给 js 获取元素使用的

css in js

css in js 的核心思想是:用一个 JS 对象来描述样式,而不是 css 样式表

例如下面的对象就是一个用于描述样式的对象:

javascript
const styles = {
+  backgroundColor: "#f40",
+  color: "#fff",
+  width: "400px",
+  height: "500px",
+  margin: "0 auto",
+};
+
const styles = {
+  backgroundColor: "#f40",
+  color: "#fff",
+  width: "400px",
+  height: "500px",
+  margin: "0 auto",
+};
+

由于这种描述样式的方式根本就不存在类名,自然不会有类名冲突

至于如何把样式应用到界面上,不是它所关心的事情,你可以用任何技术、任何框架、任何方式将它应用到界面。

后续学习的 vue、react 都支持 css in js,可以非常轻松的应用到界面

css in js 的特点:

  • 绝无冲突的可能:由于它根本不存在类名,所以绝不可能出现类名冲突
  • 更加灵活:可以充分利用 JS 语言灵活的特点,用各种招式来处理样式
  • 应用面更广:只要支持 js 语言,就可以支持 css in js,因此,在一些用 JS 语言开发移动端应用的时候非常好用,因为移动端应用很有可能并不支持 css
  • 书写不便:书写样式,特别是公共样式的时候,处理起来不是很方便
  • 在页面中增加了大量冗余内容:在页面中处理 css in js 时,往往是将样式加入到元素的 style 属性中,会大量增加元素的内联样式,并且可能会有大量重复,不易阅读最终的页面代码

css module

通过命名规范来限制类名太过死板,而 css in js 虽然足够灵活,但是书写不便。 css module 开辟一种全新的思路来解决类名冲突的问题

思路

css module 遵循以下思路解决类名冲突问题:

  1. css 的类名冲突往往发生在大型项目中
  2. 大型项目往往会使用构建工具(webpack 等)搭建工程
  3. 构建工具允许将 css 样式切分为更加精细的模块
  4. 同 JS 的变量一样,每个 css 模块文件中难以出现冲突的类名,冲突的类名往往发生在不同的 css 模块文件中
  5. 只需要保证构建工具在合并样式代码后不会出现类名冲突即可

实现原理

在 webpack 中,作为处理 css 的 css-loader,它实现了 css module 的思想,要启用 css module,需要将 css-loader 的配置modules设置为true

配置方法 1:

js
module.exports = {
+  rules: [
+    {
+      test: /\\.css$/,
+      use: ["style-loader", "css-loader"],
+    },
+  ],
+};
+
module.exports = {
+  rules: [
+    {
+      test: /\\.css$/,
+      use: ["style-loader", "css-loader"],
+    },
+  ],
+};
+

配置方法 2:

js
module.exports = {
+  rules: [
+    {
+      test: /\\.css$/,
+      use: [
+        "style-loader",
+        {
+          loader: "css-loader",
+          options: {
+            module: true,
+          },
+        },
+      ],
+    },
+  ],
+};
+
module.exports = {
+  rules: [
+    {
+      test: /\\.css$/,
+      use: [
+        "style-loader",
+        {
+          loader: "css-loader",
+          options: {
+            module: true,
+          },
+        },
+      ],
+    },
+  ],
+};
+

css-loader 的实现方式如下:

原理极其简单,开启了 css module 后,css-loader 会将样式中的类名进行转换,转换为一个唯一的 hash 值。

由于 hash 值是根据模块路径和类名生成的,因此,不同的 css 模块,哪怕具有相同的类名,转换后的 hash 值也不一样。

如何应用样式

css module 带来了一个新的问题:源代码的类名和最终生成的类名是不一样的,而开发者只知道自己写的源代码中的类名,并不知道最终的类名是什么,那如何应用类名到元素上呢?

为了解决这个问题,css-loader 会导出原类名和最终类名的对应关系,该关系是通过一个对象描述的

这样一来,我们就可以在 js 代码中获取到 css 模块导出的结果,从而应用类名了

style-loader 为了我们更加方便的应用类名,会去除掉其他信息,仅暴露对应关系

javascript
rules: [
+  {
+    // test: /\\.css$/, use: ["style-loader", {
+    //     loader: "css-loader",
+    //     options: {
+    //         // modules: {
+    //         //     localIdentName: "[local]-[hash:5]"
+    //         // }
+    //         modules:true
+    //     }
+    // }]
+    // 配置
+    // 从右向左规则
+    test: /\\.css$/,
+    use: ["style-loader", "css-loader?modules"],
+  },
+];
+
rules: [
+  {
+    // test: /\\.css$/, use: ["style-loader", {
+    //     loader: "css-loader",
+    //     options: {
+    //         // modules: {
+    //         //     localIdentName: "[local]-[hash:5]"
+    //         // }
+    //         modules:true
+    //     }
+    // }]
+    // 配置
+    // 从右向左规则
+    test: /\\.css$/,
+    use: ["style-loader", "css-loader?modules"],
+  },
+];
+

其他操作

全局类名

某些类名是全局的、静态的,不需要进行转换,仅需要在类名位置使用一个特殊的语法即可:

css
:global(.main) {
+  ...;
+}
+
:global(.main) {
+  ...;
+}
+

使用了 global 的类名不会进行转换,相反的,没有使用 global 的类名,表示默认使用了 local

css
:local(.main) {
+  ...;
+}
+
:local(.main) {
+  ...;
+}
+

使用了 local 的类名表示局部类名,是可能会造成冲突的类名,会被 css module 进行转换

如何控制最终的类名

绝大部分情况下,我们都不需要控制最终的类名,因为控制它没有任何意义

如果一定要控制最终的类名,需要配置 css-loader 的localIdentName

其他注意事项

  • css module 往往配合构建工具使用
  • css module 仅处理顶级类名,尽量不要书写嵌套的类名,也没有这个必要
  • css module 仅处理类名,不处理其他选择器
  • css module 还会处理 id 选择器,不过任何时候都没有使用 id 选择器的理由
  • 使用了 css module 后,只要能做到让类名望文知意即可,不需要遵守其他任何的命名规范

CSS 预编译器

基本原理

编写 css 时,受限于 css 语言本身,常常难以处理一些问题:

  • 重复的样式值:例如常用颜色、常用尺寸
  • 重复的代码段:例如绝对定位居中、清除浮动
  • 重复的嵌套书写

由于官方迟迟不对 css 语言本身做出改进,一些第三方机构开始想办法来解决这些问题

其中一种方案,便是预编译器

预编译器的原理很简单,即使用一种更加优雅的方式来书写样式代码,通过一个编译器,将其转换为可被浏览器识别的传统 css 代码

目前,最流行的预编译器有LESSSASS,由于它们两者特别相似,因此仅学习一种即可

less 官网:http://lesscss.org/

less 中文文档 1(非官方):http://lesscss.cn/

less 中文文档 2(非官方):https://less.bootcss.com/

sass 官网:https://sass-lang.com/

sass 中文文档 1(非官方):https://www.sass.hk/

sass 中文文档 2(非官方):https://sass.bootcss.com/

LESS 的安装和使用

从原理可知,要使用 LESS,必须要安装 LESS 编译器

LESS 编译器是基于 node 开发的,可以通过 npm 下载安装

shell
npm i -D less
+
npm i -D less
+

安装好了 less 之后,它提供了一个 CLI 工具lessc,通过该工具即可完成编译

shell
lessc less代码文件 编译后的文件
+
lessc less代码文件 编译后的文件
+

试一试:

新建一个index.less文件,编写内容如下:

less
// less代码
+@red: #f40;
+
+.redcolor {
+  color: @red;
+}
+
// less代码
+@red: #f40;
+
+.redcolor {
+  color: @red;
+}
+

在 css 文件夹终端中运行命令:

shell
npx lessc index.less index.css
+
npx lessc index.less index.css
+

可以看到编译之后的代码:

css
.redcolor {
+  color: #f40;
+}
+
.redcolor {
+  color: #f40;
+}
+

LESS 的基本使用

具体的使用见文档:https://less.bootcss.com/

  • 变量
  • 混合
  • 嵌套
  • 运算
  • 函数
  • 作用域
  • 注释
  • 导入

webpack 中使用 less

转换过程: less --- 用 less-loader ---> css ---用 css loader ---> js -----用 style-loader ---> 放到 style 元素 只需要进行配置 webpack:less-loader

javascript
rules: [
+  {
+    test: /\\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+  {
+    test: /\\.less$/,
+    use: ["style-loader", "css-loader?modules", "less-loader"],
+  },
+];
+
rules: [
+  {
+    test: /\\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+  {
+    test: /\\.less$/,
+    use: ["style-loader", "css-loader?modules", "less-loader"],
+  },
+];
+

PostCss

什么是 PostCss

学习到现在,可以看出,CSS 工程化面临着诸多问题,而解决这些问题的方案多种多样。

如果把 CSS 单独拎出来看,光是样式本身,就有很多事情要处理。

既然有这么多事情要处理,何不把这些事情集中到一起统一处理呢?

PostCss 就是基于这样的理念出现的。

PostCss 类似于一个编译器,可以将样式源码编译成最终的 CSS 代码

看上去是不是和 LESS、SASS 一样呢?

但 PostCss 和 LESS、SASS 的思路不同,它其实只做一些代码分析之类的事情,将分析的结果交给插件,具体的代码转换操作是插件去完成的。

官方的一张图更能说明 postcss 的处理流程:

这一点有点像 webpack,webpack 本身仅做依赖分析、抽象语法树分析,其他的操作是靠插件和加载器完成的。

官网地址:https://postcss.org/

github 地址:https://github.com/postcss/postcss

安装

PostCss 是基于 node 编写的,因此可以使用 npm 安装

shell
npm i -D postcss
+
npm i -D postcss
+

postcss 库提供了对应的 js api 用于转换代码,如果你想使用 postcss 的一些高级功能,或者想开发 postcss 插件,就要 api 使用 postcss,api 的文档地址是:http://api.postcss.org/

后缀:.postcss .pcss .sss 安装插件:postcss-sugarss-language 可以识别拓展名

不过绝大部分时候,我们都是使用者,并不希望使用代码的方式来使用 PostCss

因此,我们可以再安装一个 postcss-cli,通过命令行来完成编译

shell
npm i -D postcss-cli
+
npm i -D postcss-cli
+

postcss-cli 提供一个命令,它调用 postcss 中的 api 来完成编译

命令的使用方式为:

shell
postcss 源码文件 -o 输出文件
+
postcss 源码文件 -o 输出文件
+

javascript
rules: [
+  {
+    test: /\\.pcss$/,
+    use: ["style-loader", "css-loader?modules", "postcss-loader"],
+  },
+];
+
rules: [
+  {
+    test: /\\.pcss$/,
+    use: ["style-loader", "css-loader?modules", "postcss-loader"],
+  },
+];
+

编译:npm start

配置文件

和 webpack 类似,postcss 有自己的配置文件,该配置文件会影响 postcss 的某些编译行为。

配置文件的默认名称是:postcss.config.js

例如:

javascript
module.exports = {
+  map: false, //关闭source-map
+};
+
module.exports = {
+  map: false, //关闭source-map
+};
+

插件

光使用 postcss 是没有多少意义的,要让它真正的发挥作用,需要插件

postcss 的插件市场:https://www.postcss.parts/

下面罗列一些 postcss 的常用插件

postcss-preset-env

过去使用 postcss 的时候,往往会使用大量的插件,它们各自解决一些问题

这样导致的结果是安装插件、配置插件都特别的繁琐

于是出现了这么一个插件postcss-preset-env,它称之为postcss预设环境,大意就是它整合了很多的常用插件到一起,并帮你完成了基本的配置,你只需要安装它一个插件,就相当于安装了很多插件了。

安装好该插件后,在 postcss 配置中加入下面的配置

javascript
module.exports = {
+  plugins: {
+    "postcss-preset-env": {}, // {} 中可以填写插件的配置
+  },
+};
+
module.exports = {
+  plugins: {
+    "postcss-preset-env": {}, // {} 中可以填写插件的配置
+  },
+};
+

该插件的功能很多,下面一一介绍

自动的厂商前缀

某些新的 css 样式需要在旧版本浏览器中使用厂商前缀方可实现

例如

css
::placeholder {
+  color: red;
+}
+
::placeholder {
+  color: red;
+}
+

该功能在不同的旧版本浏览器中需要书写为

css
::-webkit-input-placeholder {
+  color: red;
+}
+::-moz-placeholder {
+  color: red;
+}
+:-ms-input-placeholder {
+  color: red;
+}
+::-ms-input-placeholder {
+  color: red;
+}
+::placeholder {
+  color: red;
+}
+
::-webkit-input-placeholder {
+  color: red;
+}
+::-moz-placeholder {
+  color: red;
+}
+:-ms-input-placeholder {
+  color: red;
+}
+::-ms-input-placeholder {
+  color: red;
+}
+::placeholder {
+  color: red;
+}
+

要完成这件事情,需要使用autoprefixer库。

postcss-preset-env内部包含了该库,自动有了该功能。

如果需要调整兼容的浏览器范围,可以通过下面的方式进行配置

方式 1:在 postcss-preset-env 的配置中加入 browsers

javascript
module.exports = {
+  plugins: {
+    "postcss-preset-env": {
+      browsers: ["last 2 version", "> 1%"],
+    },
+  },
+};
+
module.exports = {
+  plugins: {
+    "postcss-preset-env": {
+      browsers: ["last 2 version", "> 1%"],
+    },
+  },
+};
+

方式 2【推荐】:添加 .browserslistrc 文件 --通用

创建文件.browserslistrc,填写配置内容

last 2 version
+> 1%
+
last 2 version
+> 1%
+

方式 3【推荐】:在 package.json 的配置中加入 browserslist

json
"browserslist": [
+    "last 2 version",
+    "> 1%"
+]
+
"browserslist": [
+    "last 2 version",
+    "> 1%"
+]
+

browserslist是一个多行的(数组形式的)标准字符串。

它的书写规范多而繁琐,详情见:https://github.com/browserslist/browserslist

一般情况下,大部分网站都使用下面的格式进行书写

last 2 version
+> 1% in CN
+not ie <= 8
+
last 2 version
+> 1% in CN
+not ie <= 8
+
  • last 2 version: 浏览器的兼容最近期的两个版本
  • > 1% in CN: 匹配中国大于 1%的人使用的浏览器, in CN可省略
  • not ie <= 8: 排除掉版本号小于等于 8 的 IE 浏览器

默认情况下,匹配的结果求的是并集

你可以通过网站:https://browserl.ist/ 对配置结果覆盖的浏览器进行查询,查询时,多行之间使用英文逗号分割

browserlist 的数据来自于CanIUse网站,由于数据并非实时的,所以不会特别准确

未来的 CSS 语法

CSS 的某些前沿语法正在制定过程中,没有形成真正的标准,如果希望使用这部分语法,为了浏览器兼容性,需要进行编译

过去,完成该语法编译的是cssnext库,不过有了postcss-preset-env后,它自动包含了该功能。

你可以通过postcss-preset-envstage配置,告知postcss-preset-env需要对哪个阶段的 css 语法进行兼容处理,它的默认值为 2

javascript
"postcss-preset-env": {
+    stage: 0
+}
+
"postcss-preset-env": {
+    stage: 0
+}
+

一共有 5 个阶段可配置:

  • Stage 0: Aspirational - 只是一个早期草案,极其不稳定
  • Stage 1: Experimental - 仍然极其不稳定,但是提议已被 W3C 公认
  • Stage 2: Allowable - 虽然还是不稳定,但已经可以使用了
  • Stage 3: Embraced - 比较稳定,可能将来会发生一些小的变化,它即将成为最终的标准
  • Stage 4: Standardized - 所有主流浏览器都应该支持的 W3C 标准

了解了以上知识后,接下来了解一下未来的 css 语法,尽管某些语法仍处于非常早期的阶段,但是有该插件存在,编译后仍然可以被浏览器识别

变量

未来的 css 语法是天然支持变量的

:root{}中定义常用变量,使用--前缀命名变量

css
:root {
+  --lightColor: #ddd;
+  --darkColor: #333;
+}
+
+a {
+  color: var(--lightColor);
+  background: var(--darkColor);
+}
+
:root {
+  --lightColor: #ddd;
+  --darkColor: #333;
+}
+
+a {
+  color: var(--lightColor);
+  background: var(--darkColor);
+}
+

编译后,仍然可以看到原语法,因为某些新语法的存在并不会影响浏览器的渲染,尽管浏览器可能不认识 如果不希望在结果中看到新语法,可以配置postcss-preset-envpreservefalse

自定义选择器

css
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
+@custom-selector :--enter :focus, :hover;
+
+a:--enter {
+  color: #f40;
+}
+
+:--heading {
+  font-weight: bold;
+}
+
+:--heading.active {
+  font-weight: bold;
+}
+
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
+@custom-selector :--enter :focus, :hover;
+
+a:--enter {
+  color: #f40;
+}
+
+:--heading {
+  font-weight: bold;
+}
+
+:--heading.active {
+  font-weight: bold;
+}
+

编译后

css
a:focus,
+a:hover {
+  color: #f40;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-weight: bold;
+}
+
+h1.active,
+h2.active,
+h3.active,
+h4.active,
+h5.active,
+h6.active {
+  font-weight: bold;
+}
+
a:focus,
+a:hover {
+  color: #f40;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-weight: bold;
+}
+
+h1.active,
+h2.active,
+h3.active,
+h4.active,
+h5.active,
+h6.active {
+  font-weight: bold;
+}
+

嵌套

与 LESS 相同,只不过嵌套的选择器前必须使用符号&

less
.a {
+  color: red;
+  & .b {
+    color: green;
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
.a {
+  color: red;
+  & .b {
+    color: green;
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+

编译后

css
.a {
+  color: red;
+}
+
+.a .b {
+  color: green;
+}
+
+.a > .b {
+  color: blue;
+}
+
+.a:hover {
+  color: #000;
+}
+
.a {
+  color: red;
+}
+
+.a .b {
+  color: green;
+}
+
+.a > .b {
+  color: blue;
+}
+
+.a:hover {
+  color: #000;
+}
+

postcss-apply

该插件可以支持在 css 中书写属性集

类似于 LESS 中的混入,可以利用 CSS 的新语法定义一个 CSS 代码片段,然后在需要的时候应用它

less
:root {
+  --center: {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.item {
+  @apply --center;
+}
+
:root {
+  --center: {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.item {
+  @apply --center;
+}
+

编译后

css
.item {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  -webkit-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
+}
+
.item {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  -webkit-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
+}
+

实际上,该功能也属于 cssnext,不知为何postcss-preset-env没有支持

postcss-color-function

该插件支持在源码中使用一些颜色函数

less
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: color(#aabbcc);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: color(#aabbcc a(90%));
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: color(#aabbcc red(90%));
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: color(#aabbcc tint(50%));
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: color(#aabbcc shade(50%));
+}
+
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: color(#aabbcc);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: color(#aabbcc a(90%));
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: color(#aabbcc red(90%));
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: color(#aabbcc tint(50%));
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: color(#aabbcc shade(50%));
+}
+

编译后

css
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: rgb(170, 187, 204);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: rgba(170, 187, 204, 0.9);
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: rgb(230, 187, 204);
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: rgb(213, 221, 230);
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: rgb(85, 94, 102);
+}
+
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: rgb(170, 187, 204);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: rgba(170, 187, 204, 0.9);
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: rgb(230, 187, 204);
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: rgb(213, 221, 230);
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: rgb(85, 94, 102);
+}
+

postcss-import

该插件可以让你在postcss文件中导入其他样式代码,通过该插件可以将它们合并

由于后续的知识点中,会将 postcss 加入到 webpack 中,而 webpack 本身具有依赖分析的功能,所以该插件的实际意义不大

stylelint

官网:https://stylelint.io/

在实际的开发中,我们可能会错误的或不规范的书写一些 css 代码,stylelint 插件会即时的发现错误

由于不同的公司可能使用不同的 CSS 书写规范,stylelint 为了保持灵活,它本身并没有提供具体的规则验证

你需要安装或自行编写规则验证方案

通常,我们会安装stylelint-config-standard库来提供标准的 CSS 规则判定

安装好后,我们需要告诉 stylelint 使用该库来进行规则验证

告知的方式有多种,比较常见的是使用文件.stylelintrc

json
//.styleintrc
+{
+  "extends": "stylelint-config-standard"
+}
+
//.styleintrc
+{
+  "extends": "stylelint-config-standard"
+}
+

此时,如果你的代码出现不规范的地方,编译时将会报出错误

css
body {
+  background: #f4;
+}
+
body {
+  background: #f4;
+}
+

发生了两处错误:

  1. 缩进应该只有两个空格
  2. 十六进制的颜色值不正确

如果某些规则并非你所期望的,可以在配置中进行设置

json
{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "indentation": null
+  }
+}
+
{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "indentation": null
+  }
+}
+

设置为null可以禁用该规则,或者设置为 4,表示一个缩进有 4 个空格。具体的设置需要参见 stylelint 文档:https://stylelint.io/

但是这种错误报告需要在编译时才会发生,如果我希望在编写代码时就自动在编辑器里报错呢?

既然想在编辑器里达到该功能,那么就要在编辑器里做文章

安装 vscode 的插件stylelint即可,它会读取你工程中的配置文件,按照配置进行实时报错

实际上,如果你拥有了stylelint插件,可以不需要在 postcss 中使用该插件了

抽离 css 文件

目前,css 代码被 css-loader 转换后,交给的是 style-loader 进行处理。

style-loader 使用的方式是用一段 js 代码,将样式加入到 style 元素中。

而实际的开发中,我们往往希望依赖的样式最终形成一个 css 文件

此时,就需要用到一个库:mini-css-extract-plugin

该库提供了 1 个 plugin 和 1 个 loader

  • plugin:负责生成 css 文件
  • loader:负责记录要生成的 css 文件的内容,同时导出开启 css-module 后的样式对象

使用方式:

javascript
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.css$/,
+        use: [MiniCssExtractPlugin.loader, "css-loader?modules"],
+      },
+    ],
+  },
+  plugins: [
+    new MiniCssExtractPlugin(), //负责生成css文件
+  ],
+};
+
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.css$/,
+        use: [MiniCssExtractPlugin.loader, "css-loader?modules"],
+      },
+    ],
+  },
+  plugins: [
+    new MiniCssExtractPlugin(), //负责生成css文件
+  ],
+};
+

配置生成的文件名

output.filename的含义一样,即根据 chunk 生成的样式文件名

配置生成的文件名,例如[name].[contenthash:5].css

默认情况下,每个 chunk 对应一个 css 文件

原子化 CSS(了解)

`,309),b=[u];function A(m,h,C,g,v,k){return n(),a("div",null,b)}const f=s(d,[["render",A]]);export{E as __pageData,f as default}; diff --git "a/assets/front-end-engineering_CSS\345\267\245\347\250\213\345\214\226.md.6acce3ef.lean.js" "b/assets/front-end-engineering_CSS\345\267\245\347\250\213\345\214\226.md.6acce3ef.lean.js" new file mode 100644 index 00000000..afa62cdf --- /dev/null +++ "b/assets/front-end-engineering_CSS\345\267\245\347\250\213\345\214\226.md.6acce3ef.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-14-44.9c23195b.png",o="/blog/assets/2023-01-06-16-16-45.8cacfb52.png",e="/blog/assets/2023-01-06-16-16-57.792b0ec0.png",c="/blog/assets/2023-01-06-16-17-35.bce22d59.png",r="/blog/assets/2023-01-06-16-23-08.16c5b4ae.png",t="/blog/assets/2023-01-06-16-23-16.12a8f778.png",B="/blog/assets/2023-01-06-16-37-27.347ac1c6.png",i="/blog/assets/2023-01-06-16-37-43.6a6a7a3c.png",y="/blog/assets/2023-01-06-16-37-56.802983a9.png",F="/blog/assets/2023-01-06-16-38-33.30911d0e.png",E=JSON.parse('{"title":"CSS 工程化","description":"","frontmatter":{},"headers":[{"level":2,"title":"css 的问题","slug":"css-的问题","link":"#css-的问题","children":[{"level":3,"title":"类名冲突的问题","slug":"类名冲突的问题","link":"#类名冲突的问题","children":[]},{"level":3,"title":"重复样式","slug":"重复样式","link":"#重复样式","children":[]},{"level":3,"title":"css 文件细分问题","slug":"css-文件细分问题","link":"#css-文件细分问题","children":[]}]},{"level":2,"title":"如何解决","slug":"如何解决","link":"#如何解决","children":[{"level":3,"title":"解决类名冲突","slug":"解决类名冲突","link":"#解决类名冲突","children":[]},{"level":3,"title":"解决重复样式的问题","slug":"解决重复样式的问题","link":"#解决重复样式的问题","children":[]},{"level":3,"title":"解决 css 文件细分问题","slug":"解决-css-文件细分问题","link":"#解决-css-文件细分问题","children":[]}]},{"level":2,"title":"利用 webpack 拆分 css","slug":"利用-webpack-拆分-css","link":"#利用-webpack-拆分-css","children":[{"level":3,"title":"css-loader","slug":"css-loader","link":"#css-loader","children":[]},{"level":3,"title":"style-loader","slug":"style-loader","link":"#style-loader","children":[]}]},{"level":2,"title":"BEM","slug":"bem","link":"#bem","children":[]},{"level":2,"title":"css in js","slug":"css-in-js","link":"#css-in-js","children":[]},{"level":2,"title":"css module","slug":"css-module","link":"#css-module","children":[{"level":3,"title":"思路","slug":"思路","link":"#思路","children":[]},{"level":3,"title":"实现原理","slug":"实现原理","link":"#实现原理","children":[]},{"level":3,"title":"如何应用样式","slug":"如何应用样式","link":"#如何应用样式","children":[]},{"level":3,"title":"其他操作","slug":"其他操作","link":"#其他操作","children":[]},{"level":3,"title":"全局类名","slug":"全局类名","link":"#全局类名","children":[]},{"level":3,"title":"如何控制最终的类名","slug":"如何控制最终的类名","link":"#如何控制最终的类名","children":[]},{"level":3,"title":"其他注意事项","slug":"其他注意事项","link":"#其他注意事项","children":[]}]},{"level":2,"title":"CSS 预编译器","slug":"css-预编译器","link":"#css-预编译器","children":[{"level":3,"title":"基本原理","slug":"基本原理","link":"#基本原理","children":[]},{"level":3,"title":"LESS 的安装和使用","slug":"less-的安装和使用","link":"#less-的安装和使用","children":[]},{"level":3,"title":"LESS 的基本使用","slug":"less-的基本使用","link":"#less-的基本使用","children":[]},{"level":3,"title":"webpack 中使用 less","slug":"webpack-中使用-less","link":"#webpack-中使用-less","children":[]}]},{"level":2,"title":"PostCss","slug":"postcss","link":"#postcss","children":[{"level":3,"title":"什么是 PostCss","slug":"什么是-postcss","link":"#什么是-postcss","children":[]},{"level":3,"title":"安装","slug":"安装","link":"#安装","children":[]},{"level":3,"title":"配置文件","slug":"配置文件","link":"#配置文件","children":[]},{"level":3,"title":"插件","slug":"插件","link":"#插件","children":[]},{"level":3,"title":"postcss-preset-env","slug":"postcss-preset-env","link":"#postcss-preset-env","children":[]},{"level":3,"title":"自动的厂商前缀","slug":"自动的厂商前缀","link":"#自动的厂商前缀","children":[]},{"level":3,"title":"未来的 CSS 语法","slug":"未来的-css-语法","link":"#未来的-css-语法","children":[]},{"level":3,"title":"postcss-apply","slug":"postcss-apply","link":"#postcss-apply","children":[]},{"level":3,"title":"postcss-color-function","slug":"postcss-color-function","link":"#postcss-color-function","children":[]},{"level":3,"title":"postcss-import","slug":"postcss-import","link":"#postcss-import","children":[]}]},{"level":2,"title":"stylelint","slug":"stylelint","link":"#stylelint","children":[]},{"level":2,"title":"抽离 css 文件","slug":"抽离-css-文件","link":"#抽离-css-文件","children":[]},{"level":2,"title":"原子化 CSS(了解)","slug":"原子化-css-了解","link":"#原子化-css-了解","children":[]}],"relativePath":"front-end-engineering/CSS工程化.md","lastUpdated":1715076847000}'),d={name:"front-end-engineering/CSS工程化.md"},u=l("",309),b=[u];function A(m,h,C,g,v,k){return n(),a("div",null,b)}const f=s(d,[["render",A]]);export{E as __pageData,f as default}; diff --git a/assets/front-end-engineering_PackageManager.md.d80a1c87.js b/assets/front-end-engineering_PackageManager.md.d80a1c87.js new file mode 100644 index 00000000..c1e57b39 --- /dev/null +++ b/assets/front-end-engineering_PackageManager.md.d80a1c87.js @@ -0,0 +1,235 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-22-03-34.729a63f6.png",e="/blog/assets/2023-02-01-22-03-43.d520fd1e.png",o="/blog/assets/2023-02-01-22-03-59.a0646341.png",r="/blog/assets/2023-02-01-22-04-06.095f01dd.png",c="/blog/assets/2023-02-01-22-05-00.cd9ee97d.png",C=JSON.parse('{"title":"包管理器","description":"","frontmatter":{},"headers":[{"level":2,"title":"包管理工具概述","slug":"包管理工具概述","link":"#包管理工具概述","children":[{"level":3,"title":"1.概念","slug":"_1-概念","link":"#_1-概念","children":[]},{"level":3,"title":"2.背景","slug":"_2-背景","link":"#_2-背景","children":[]},{"level":3,"title":"3.前端包管理器","slug":"_3-前端包管理器","link":"#_3-前端包管理器","children":[]}]},{"level":2,"title":"npm","slug":"npm","link":"#npm","children":[{"level":3,"title":"包的安装","slug":"包的安装","link":"#包的安装","children":[]},{"level":3,"title":"包配置","slug":"包配置","link":"#包配置","children":[]},{"level":3,"title":"包的使用","slug":"包的使用","link":"#包的使用","children":[]},{"level":3,"title":"简易数据爬虫","slug":"简易数据爬虫","link":"#简易数据爬虫","children":[]}]},{"level":2,"title":"语义版本","slug":"语义版本","link":"#语义版本","children":[{"level":3,"title":"1.避免还原的差异","slug":"_1-避免还原的差异","link":"#_1-避免还原的差异","children":[]},{"level":3,"title":"2.[扩展]npm 的差异版本处理","slug":"_2-扩展-npm-的差异版本处理","link":"#_2-扩展-npm-的差异版本处理","children":[]}]},{"level":2,"title":"npm 脚本 (npm scripts)","slug":"npm-脚本-npm-scripts","link":"#npm-脚本-npm-scripts","children":[]},{"level":2,"title":"运行环境配置","slug":"运行环境配置","link":"#运行环境配置","children":[{"level":3,"title":"在 node 中读取 package.json","slug":"在-node-中读取-package-json","link":"#在-node-中读取-package-json","children":[]}]},{"level":2,"title":"其他 npm 命令","slug":"其他-npm-命令","link":"#其他-npm-命令","children":[{"level":3,"title":"1.安装","slug":"_1-安装","link":"#_1-安装","children":[]},{"level":3,"title":"2.查询","slug":"_2-查询","link":"#_2-查询","children":[]},{"level":3,"title":"3.更新","slug":"_3-更新","link":"#_3-更新","children":[]},{"level":3,"title":"4.卸载包","slug":"_4-卸载包","link":"#_4-卸载包","children":[]},{"level":3,"title":"5.npm 配置","slug":"_5-npm-配置","link":"#_5-npm-配置","children":[]}]},{"level":2,"title":"发布包","slug":"发布包","link":"#发布包","children":[{"level":3,"title":"1.准备工作","slug":"_1-准备工作","link":"#_1-准备工作","children":[]},{"level":3,"title":"2.发布","slug":"_2-发布","link":"#_2-发布","children":[]},{"level":3,"title":"开源协议","slug":"开源协议","link":"#开源协议","children":[]}]},{"level":2,"title":"yarn 简介","slug":"yarn-简介","link":"#yarn-简介","children":[{"level":3,"title":"yarn 的核心命令","slug":"yarn-的核心命令","link":"#yarn-的核心命令","children":[]},{"level":3,"title":"yarn 的特别礼物","slug":"yarn-的特别礼物","link":"#yarn-的特别礼物","children":[]}]},{"level":2,"title":"其他包管理器","slug":"其他包管理器","link":"#其他包管理器","children":[{"level":3,"title":"cnpm","slug":"cnpm","link":"#cnpm","children":[]},{"level":3,"title":"nvm","slug":"nvm","link":"#nvm","children":[]},{"level":3,"title":"pnpm","slug":"pnpm","link":"#pnpm","children":[]},{"level":3,"title":"bower","slug":"bower","link":"#bower","children":[]}]}],"relativePath":"front-end-engineering/PackageManager.md","lastUpdated":1675736874000}'),t={name:"front-end-engineering/PackageManager.md"},i=l('

包管理器

包管理工具概述

1.概念

模块(module)

通常以单个文件形式存在的功能片段,入口文件通常称之为入口模块主模块

库(library,简称 lib)

以一个或多个模块组成的完整功能块,为开发中某一方面的问题提供完整的解决方案

包(package)

包含元数据的库,这些元数据包括:名称、描述、git 主页、许可证协议、作者、依赖等等

2.背景

CommonJS 的出现,使 node 环境下的 JS 代码可以用模块更加细粒度的划分。一个类、一个函数、一个对象、一个配置等等均可以作为模块,这种细粒度的划分,是开发大型应用的基石。 为了解决在开发过程中遇到的常见问题,比如加密、提供常见的工具方法、模拟数据等等,一时间,在前端社区涌现了大量的第三方库。这些库使用 CommonJS 标准书写而成,非常容易使用。 然而,在下载使用这些第三方库的时候,遇到难以处理的问题:

  • 下载过程繁琐
    • 进入官网或 github 主页
    • 找到并下载相应的版本
    • 拷贝到工程的目录中
    • 如果遇到有同名的库,需要更改名称
  • 如果该库需要依赖其他库,还需要按照要求先下载其他库
  • 开发环境中安装的大量的库如何在生产环境中还原,又如何区分
  • 更新一个库极度麻烦
  • 自己开发的库,如何在下一次开发使用

以上问题,就是包管理工具要解决的问题

3.前端包管理器

几乎可以这样认为,前端所有的包管理器都是基于 npm 的,目前,npm 即是一个包管理器,也是其他包管理的基石 npm 全称为 node package manager,即 node 包管理器,它运行在 node 环境中,让开发者可以用简单的方式完成包的查找、安装、更新、卸载、上传等操作

npm 之所以要运行在 node 环境,而不是浏览器环境,根本原因是因为浏览器环境无法提供下载、删除、读取本地文件的功能。而 node 属于服务器环境,没有浏览器的种种限制,理论上可以完全掌控运行 node 的计算机。

npm 的出现,弥补了 node 没有包管理器的缺陷,于是很快,node 在安装文件中内置了 npm,当开发者安装好 node 之后,就自动安装了 npm,不仅如此,node 环境还专门为 npm 提供了良好的支持,使用 npm 下载的包更加方便了。

npm 由三部分组成:

  • registry:入口
    • 可以把它想象成一个庞大的数据库
    • 第三方库的开发者,将自己的库按照 npm 的规范,打包上传到数据库中
    • 使用者通过统一的地址下载第三方包
  • 官网:https://www.npmjs.com/
    • 查询包
    • 注册、登录、管理个人信息
  • CLI:command-line interface 命令行接口
    • 这一部分是本门课讲解的重点
    • 安装好 npm 后,通过 CLI 来使用 npm 的各种功能
    • vscode 里面 Ctrl+j

cmd 常用命令 换到 e 盘:e: 查看目录:dir vscode 查看 npm 的当前版本 输入 npm -v 或者 npm --version 注意空格

node 和 npm 是互相成就的,node 的出现让 npm 火了,npm 的火爆带动了大量的第三方库的发展,很多优秀的第三方库打包上传到了 npm,这些第三方库又为 node 带来了大量的用户

npm

包的安装

安装(install)即下载包 由于 npm 的官方 registry 服务器位于国外,可能受网速影响导致下载缓慢或失败。因此,安装好 npm 之后,需要重新设置 registry 的地址为国内地址。目前,淘宝 https://registry.npm.taobao.org 提供了国内的 registry 地址,先设置到该地址。设置方式为npm config set registry [https://registry.npm.taobao.org](https://registry.npm.taobao.org)。设置好后,通过命令npm config get registry进行检查

npm 安装一个包,分为两种安装方式:

  1. 本地安装
  2. 全局安装

1.本地安装

使用命令npm install 包名npm i 包名即可完成本地安装 终端 clear 是清除命令 本地安装的包出现在当前目录下的node_modules目录中

随着开发的进展,node_modules目录会变得异常庞大,目录下的内容不适合直接传输到生产环境,因此通常使用.gitignore文件忽略该目录中的内容 本地安装适用于绝大部分的包,它会在当前目录及其子目录中发挥作用 通常在项目的根目录中使用本地安装 安装一个包的时候,npm 会自动管理依赖,它会下载该包的依赖包到node_modules目录中 例如:react 如果本地安装的包带有 CLI,npm 会将它的 CLI 脚本文件放置到node_modules/.bin下,使用命令npx 命令名即可调用(npx mocha) 例如:mocha

演示 安装 jQuery:命令 npm install jquery

2.全局安装

全局安装的包放置在一个特殊的全局目录,该目录可以通过命令npm config get prefix查看 使用命令npm install --global 包名npm i -g 包名 重要:全局安装的包并非所有工程可用,它仅提供全局的 CLI 工具 大部分情况下,都不需要全局安装包,除非:

  1. 包的版本非常稳定,很少有大的更新
  2. 提供的 CLI 工具在各个工程中使用的非常频繁
  3. CLI 工具仅为开发环境提供支持,而非部署环境

包配置

前节补充:同时下载两个包:命令 npm i jquery loadsh

目前遇到的问题:

  1. 拷贝工程后如何还原?
  2. 如何区分开发依赖和生产依赖?
  3. 如果自身的项目也是一个包,如何描述包的信息

以上这些问题都需要通过包的配置文件解决

1.配置文件

npm 将每个使用 npm 的工程本身都看作是一个包,包的信息需要通过一个名称固定的配置文件来描述 配置文件的名称固定为:package.json 可以手动创建该文件,而更多的时候,是通过命令npm init创建的 配置文件中可以描述大量的信息,包括:

  • name:包的名称,该名称必须是英文单词字符,支持连接符
  • version:版本
    • 版本规范:主版本号.次版本号.补丁版本号
    • 主版本号:仅当程序发生了重大变化时才会增长,如新增了重要功能、新增了大量的 API、技术架构发生了重大变化
    • 次版本号:仅当程序发生了一些小变化时才会增长,如新增了一些小功能、新增了一些辅助型的 API
    • 补丁版本号:仅当解决了一些 bug 或 进行了一些局部优化时更新,如修复了某个函数的 bug、提升了某个函数的运行效率
  • description:包的描述
  • homepage:官网地址
  • author:包的作者,必须是有效的 npm 账户名,书写规范是 account <mail>,例如:zhangsan <zhangsan@gmail.com>,不正确的账号和邮箱可能导致发布包时失败
  • repository:包的仓储地址,通常指 git 或 svn 的地址,它是一个对象
    • type:仓储类型,git 或 svn
    • url:地址

命令:get remote -v

  • main:包的入口文件,使用包的人默认从该入口文件导入包的内容
  • keywords: 搜索关键字,发布包后,可以通过该数组中的关键字搜索到包

    一步到位简化配置 json 文件

使用npm init --yesnpm init -y可以在生成配置文件时自动填充默认配置 操作:创建一个英文文件夹,右键终端打开,输入npm init --yesnpm init -y

2.保存依赖关系

大部分时候,我们仅仅是开发项目,并不会把它打包发布出去,尽管如此,我们仍然需要 package.json 文件 package.json 文件最重要的作用,是记录当前工程的依赖

  • dependencies:生产环境的依赖包
  • devDependencies:仅开发环境的依赖包
json
"dependencies": {
+    "jquery": "latest",//不推荐最新,防止开发冲突
+    "lodash": "4.17.15"
+},
+"devDependencies": {
+    "mocha": "6.2.2"
+}
+
"dependencies": {
+    "jquery": "latest",//不推荐最新,防止开发冲突
+    "lodash": "4.17.15"
+},
+"devDependencies": {
+    "mocha": "6.2.2"
+}
+

配置好依赖后,使用下面的命令即可安装依赖

shell
## 本地安装所有依赖 dependencies + devDependencies
+npm install
+npm i
+
+## 仅安装生产环境的依赖 dependencies
+npm install --production
+
## 本地安装所有依赖 dependencies + devDependencies
+npm install
+npm i
+
+## 仅安装生产环境的依赖 dependencies
+npm install --production
+

这样一来,代码移植就不是问题了,只需要移植源代码和 package.json 文件,不用移植 node_modules 目录,然后在移植之后通过命令即可重新恢复安装 为了更加方便的添加依赖,npm 支持在使用 install 命令时,加入一些额外的参数,用于将安装的依赖包保存到 package.json 文件中(自动地) 涉及的命令如下

shell
## 安装依赖到生产环境
+npm i 包名
+npm i --save 包名
+npm i -S 包名
+
+## 安装依赖到开发环境
+npm i --save-dev 包名
+npm i -D 包名
+
## 安装依赖到生产环境
+npm i 包名
+npm i --save 包名
+npm i -S 包名
+
+## 安装依赖到开发环境
+npm i --save-dev 包名
+npm i -D 包名
+

安装包之后 package.json 会自动生成:

json
 "dependencies": {
+     "jquery": "^3.4.1",
+     "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+     "mocha": "^6.2.2"
+ }
+
 "dependencies": {
+     "jquery": "^3.4.1",
+     "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+     "mocha": "^6.2.2"
+ }
+

自动保存的依赖版本,例如^15.1.3,这种书写方式叫做语义版本号(semver version),具体规则后续讲解

包的使用

引入模块:npm init---chapter2---回车----回车----npm i lodash 安装成功

nodejs 对 npm 支持非常良好 使用 nodejs 导入模块时传统方法:

javascript
var _ = require("./node_modules/lodash/index");
+// 习惯用下划线命名
+console.log(_);
+运行命令node index.js
+
var _ = require("./node_modules/lodash/index");
+// 习惯用下划线命名
+console.log(_);
+运行命令:node index.js
+

返回的里面有很多方法,例如:compact

javascript
var arr = _.compact([1, 3, 0, "", false, 5]);
+// compact会把数组里面判定为假的除去,返回新数组
+console.log(arr);
+
var arr = _.compact([1, 3, 0, "", false, 5]);
+// compact会把数组里面判定为假的除去,返回新数组
+console.log(arr);
+

当使用 nodejs 导入模块时,如果模块路径不是以 ./ 或 ../ 开头,则 node 会认为导入的模块来自于 node_modules 目录,例如:

javascript
var _ = require("lodash");
+
var _ = require("lodash");
+

它首先会从当前目录的以下位置寻找文件

shell
node_modules/lodash.js
+node_modules/lodash/入口文件
+
node_modules/lodash.js
+node_modules/lodash/入口文件
+

若当前目录没有这样的文件,则会回溯到上级目录按照同样的方式查找

运行命令:包管理器\\2. npm\\2-3. 包的使用> node .\\sub\\test.js

如果到顶级目录都无法找到文件,则抛出错误 Cannot find module 'jquery' 上面提到的入口文件按照以下规则确定

  1. 查看导入包的 package.json 文件,读取 main 字段作为入口文件
  2. 若不包含 main 字段,则使用 index.js 作为入口文件

    入口文件的规则同样适用于自己工程中的模块 自己工程中的模块为 a 作为举例:

html
// 首先,查看当前目录是否有 a.js //
+把a当作文件夹,并且,把该文件夹当作一个包,看该包中是否有package.json文件,读取main字段。。。。
+var a = require("./a"); console.log(a)
+
// 首先,查看当前目录是否有 a.js //
+把a当作文件夹,并且,把该文件夹当作一个包,看该包中是否有package.json文件,读取main字段。。。。
+var a = require("./a"); console.log(a)
+

在 node 中,还可以手动指定路径来导入相应的文件,这种情况比较少见

javascript
var test = require("lodash/fp/add");
+console.log(test);
+
var test = require("lodash/fp/add");
+console.log(test);
+

简易数据爬虫

补充: require 导入一个包,如果以./或../开头,就导入自己写的文件。如果没有写,就导入 node_modules 下面的模块 特殊情况:导入模块是一个特殊名字,使用 nodejs 自带的模块。(require("fs"))

初始化:命令 npm init 生成 package.js 文件 将豆瓣电影的电影数据抓取下来,保存到本地文件 movie.json 中 需要用到的包:用法通过 npm 网站查询

  1. axios:专门用于在各种环境中发送网络请求,并获取到服务器响应结果

    安装 npm i axios,   运行 node .\\index.js

  2. cheerio:jquery 的核心逻辑包,支持所有环境,可用于讲一个 html 字符串转换成为 jquery 对象,并通过 jquery 对象完成后续操作

  3. fs:node 核心模块,专门用于文件处理

    • fs.writeFile(文件名, 数据)

语义版本

思考:如果你编写了一个包 A,依赖另外一个包 B,你在编写代码时,包 B 的版本是 2.4.1,你是希望使用你包的人一定要安装包 B,并且是 2.4.1 版本,还是希望他可以安装更高的版本,如果你希望它安装更高的版本,高的什么程度呢 回顾:版本号规则 版本规范:主版本号.次版本号.补丁版本号

  • 主版本号:仅当程序发生了重大变化时才会增长,如新增了重要功能、新增了大量的 API、技术架构发生了重大变化
  • 次版本号:仅当程序发生了一些小变化时才会增长,如新增了一些小功能、新增了一些辅助型的 API
  • 补丁版本号:仅当解决了一些 bug 或 进行了一些局部优化时更新,如修复了某个函数的 bug、提升了某个函数的运行效率

有的时候,我们希望:安装我的依赖包的时候,次版本号和补丁版本号是可以有提升的,但是主版本号不能变化

有的时候,我们又希望:安装我的依赖包的时候,只有补丁版本号可以提升,其他都不能提升

甚至我们希望依赖包保持固定的版本,尽管这比较少见

这样一来,就需要在配置文件中描述清楚具体的依赖规则,而不是直接写上版本号那么简单。

这种规则的描述,即语义版本

语义版本的书写规则非常丰富,下面列出了一些常见的书写方式

符号描述示例示例描述
>大于某个版本>1.2.1大于 1.2.1 版本
>=大于等于某个版本>=1.2.1大于等于 1.2.1 版本
<小于某个版本<1.2.1小于 1.2.1 版本
<=小于等于某个版本<=1.2.1小于等于 1.2.1 版本
-介于两个版本之间1.2.1 - 1.4.5介于 1.2.1 和 1.4.5 之间
x不固定的版本号1.3.x只要保证主版本号是 1,次版本号是 3 即可
~补丁版本号可增~1.3.4保证主版本号是 1,次版本号是 3,补丁版本号大于等于 4
^此版本和补丁版本可增^1.3.4保证主版本号是 1,次版本号可以大于等于 3,补丁版本号可以大于等于 4
*最新版本*始终安装最新版本

1.避免还原的差异

版本依赖控制始终是一个两难的问题 如果允许版本增加,可以让依赖包的 bug 得以修复(补丁版本号),可以带来一些意外的惊喜(次版本号),但同样可能带来不确定的风险(新的 bug) 如果不允许版本增加,可以获得最好的稳定性,但失去了依赖包自我优化的能力 而有的时候情况更加复杂,如果依赖包升级后,依赖也发生了变化,会有更多不确定的情况出现 基于此,npm 在安装包的时候,会自动生成一个 package-lock.json 文件,该文件记录了安装包时的确切依赖关系 当移植工程时,如果移植了 package-lock.json 文件,恢复安装时,会按照 package-lock.json 文件中的确切依赖进行安装,最大限度的避免了差异

2.[扩展]npm 的差异版本处理

如果两个包依赖同一个包的不同版本,如下图

面对这种情况,在 node_modules 目录中,不会使用扁平的目录结构,而会形成嵌套的目录,如下图:

html
├── node_modules │ ├── a │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── a包的文件 │ ├── b │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── b包的文件
+
├── node_modules │ ├── a │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── a包的文件 │ ├── b │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── b包的文件
+

npm 脚本 (npm scripts)

在开发的过程中,我们可能会反复使用很多的 CLI 命令,例如:

  • 启动工程命令(node 或 一些第三方包提供的 CLI 命令)
  • 部署工程命令(一些第三方包提供的 CLI 命令)
  • 测试工程命令(一些第三方包提供的 CLI 命令)

这些命令纷繁复杂,根据第三方包的不同命令也会不一样,非常难以记忆 于是,npm 非常贴心的支持了脚本,只需要在 package.json 中配置 scripts 字段,即可配置各种脚本名称 之后,我们就可以运行简单的指令来完成各种操作了 运行方式是 npm run 脚本名称 不仅如此,npm 还对某些常用的脚本名称进行了简化,下面的脚本名称是不需要使用 run 的:

  • start 可以简化成 npm start
  • stop
  • test

一些细节:

  • 脚本中可以省略 npx
  • start 脚本有默认值:node server.js

演示

启动 node 方式一:

js 文件终端打开,初始化创建 package.json 文件,node .\\index.js

启动 node 方式二:

第三方工具 npm in nodemon 输入命令:npx nodemon .\\index.js 可以不断刷新运行,想停止:ctrl+c 以上两种方式要记着,太麻烦了 所以在 package.json 里面加上

json
{
+  "scripts": {
+    "start": "nodemon index.js",
+    "start": "npx nodemon index.js",
+    "trick": "chrome https://www.baidu.com",
+    "trick": "dir"
+  }
+}
+
{
+  "scripts": {
+    "start": "nodemon index.js",
+    "start": "npx nodemon index.js",
+    "trick": "chrome https://www.baidu.com",
+    "trick": "dir"
+  }
+}
+

向运行 node,这次就直接 npm run start/trick 这里可以配置各种命令

运行环境配置

我们书写的代码一般有三种运行环境:

  1. 开发环境
  2. 生产环境
  3. 测试环境

有的时候,我们可能需要在 node 代码中根据不同的环境做出不同的处理 如何优雅的让 node 知道处于什么环境,是极其重要的 通常我们使用如下的处理方式: node 中有一个全局变量 global (可以类比浏览器环境的 window),该变量是一个对象,对象中的所有属性均可以直接使用 global 有一个属性是 process,该属性是一个对象,包含了当前运行 node 程序的计算机的很多信息,其中有一个信息是 env,是一个对象,包含了计算机中所有的系统变量 设置环境变量: 名字:NODE_ENV 值:development 通常,我们通过系统变量 NODE_ENV 的值,来判定 node 程序处于何种环境

javascript
var a = "没有环境变量";
+// 此电脑属性高级系统设置进行配置
+// console.log(process.env.NODE_ENV)//development
+
+if (process.env.NODE_ENV === "development") {
+  a = "开发环境";
+} else if (process.env.NODE_ENV === "production") {
+  a = "生产环境";
+} else if (process.env.NODE_ENV === "test") {
+  a = "测试环境";
+}
+console.log(a);
+
var a = "没有环境变量";
+// 此电脑属性高级系统设置进行配置
+// console.log(process.env.NODE_ENV)//development
+
+if (process.env.NODE_ENV === "development") {
+  a = "开发环境";
+} else if (process.env.NODE_ENV === "production") {
+  a = "生产环境";
+} else if (process.env.NODE_ENV === "test") {
+  a = "测试环境";
+}
+console.log(a);
+

有两种方式设置 NODE_ENV 的值

  1. 永久设置
  2. 临时设置

我们一般使用临时设置

因此,我们可以配置 scripts 脚本,在设置好了 NODE_ENV 后启动程序

javascript
//开发环境
+"start": "set NODE_ENV=development&&node index.js",//这里不能有空格
+ //在package.json里面配置:相当于设置NODE_ENV为development并且执行index.js
+//生产环境
+"build": "set NODE_ENV=production&&node index.js
+//测试环境
+"build": "set NODE_ENV=test&&node index.js
+
//开发环境
+"start": "set NODE_ENV=development&&node index.js",//这里不能有空格
+ //在package.json里面配置:相当于设置NODE_ENV为development并且执行index.js
+//生产环境
+"build": "set NODE_ENV=production&&node index.js
+//测试环境
+"build": "set NODE_ENV=test&&node index.js
+

为了避免不同系统(macOS windows 不同)的设置方式的差异,可以使用第三方库 cross-env 对环境变量进行设置

安装 npm i --D cross-env 表示开发依赖

javascript
"start": "cross-env NODE_ENV=development node index.js",
+"build": "cross-env NODE_ENV=production node index.js",
+"test": "cross-env NODE_ENV=test node index.js"
+
+
+npm start;
+npm run build;
+npm test;
+
"start": "cross-env NODE_ENV=development node index.js",
+"build": "cross-env NODE_ENV=production node index.js",
+"test": "cross-env NODE_ENV=test node index.js"
+
+
+npm start;
+npm run build;
+npm test;
+

在 node 中读取 package.json

有的时候,我们可能在 package.json 中配置一些自定义的字段,这些字段需要在 node 中读取 在 node 中,可以直接导入一个 json 格式的文件,它会自动将其转换为 js 对象

javascript
//读取package.json文件中的版本号
+
+var config = require("./package.json");
+console.log(config.version);
+console.log(config.a);
+
//读取package.json文件中的版本号
+
+var config = require("./package.json");
+console.log(config.version);
+console.log(config.a);
+

其他 npm 命令

1.安装

  1. 精确安装最新版本
shell
npm install --save-exact 包名
+npm install -E 包名
+
npm install --save-exact 包名
+npm install -E 包名
+
  1. 安装指定版本
shell
npm install 包名@版本号
+
npm install 包名@版本号
+

2.查询

  1. 查询包安装路径
shell
npm root [-g]
+
npm root [-g]
+
  1. 查看包信息
shell
npm view 包名 [子信息]
+## view aliases:v info show
+
+依赖的包:npm v react dependencies
+版本:npm v react version
+
npm view 包名 [子信息]
+## view aliases:v info show
+
+依赖的包:npm v react dependencies
+版本:npm v react version
+
  1. 查询安装包
shell
npm list [-g] [--depth=依赖深度]//0开始
+## list aliases: ls  la  ll
+
npm list [-g] [--depth=依赖深度]//0开始
+## list aliases: ls  la  ll
+

3.更新

  1. 检查有哪些包需要更新
shell
npm outdated
+
npm outdated
+
  1. 更新包
shell
npm update [-g] [包名]
+## update 别名(aliases):up、upgrade
+
npm update [-g] [包名]
+## update 别名(aliases):up、upgrade
+

用 npm 安装最新版的 npm npm i -g npm

4.卸载包

shell
npm uninstall [-g] 包名
+## uninstall aliases: remove, rm, r, un, unlink
+
npm uninstall [-g] 包名
+## uninstall aliases: remove, rm, r, un, unlink
+

5.npm 配置

npm 的配置会对其他命令产生或多或少的影响 安装好 npm 之后,最终会产生两个配置文件,一个是用户配置,一个是系统配置,当两个文件的配置项有冲突的时候,用户配置会覆盖系统配置 通常,我们不关心具体的配置文件,而只关心最终生效的配置 通过下面的命令可以查询目前生效的各种配置

shell
npm config ls [-l] [--json]
+
npm config ls [-l] [--json]
+

另外,可以通过下面的命令操作配置

  1. 获取某个配置项
shell
npm config get 配置项
+
npm config get 配置项
+
  1. 设置某个配置项
shell
npm config set 配置项=值
+
npm config set 配置项=值
+
  1. 移除某个配置项
shell
npm config delete 配置项
+
npm config delete 配置项
+

发布包

1.准备工作

  1. 移除淘宝镜像源

命令:npm config delete registry 检查:npm config get registry

  1. 到 npm 官网注册一个账号,并完成邮箱认证
  2. 本地使用 npm cli 进行登录
    1. 使用命令npm login登录
    2. 使用命令npm whoami查看当前登录的账号
    3. 使用命令npm logout注销
  3. 创建工程根目录
  4. 使用 npm init 进行初始化(包名必须是小写) author:bnagbangji 1111@qq.com
  5. 创建 LICENSE 使用刚才使用的协议

2.发布

  1. 开发
  2. 确定版本
  3. 使用命令npm publish完成发布

开源协议

可以通过网站 http://choosealicense.online/appendix/ 选择协议,并复制协议内容

yarn

yarn 简介

yarn 官网:https://www.yarnpkg.com/zh-Hans/ yarn -v 是否安装成功

yarn 是由 Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具,它仍然使用 npm 的 registry,不过提供了全新 CLI 来对包进行管理 过去,yarn 的出现极大的抢夺了 npm 的市场,甚至有人戏言,npm 只剩下一个 registry 了。 之所以会出现这种情况,是因为在过去,npm 存在下面的问题:

  • 依赖目录嵌套层次深:过去,npm 的依赖是嵌套的,这在 windows 系统上是一个极大的问题,由于众所周知的原因,windows 系统无法支持太深的目录
  • 下载速度慢
    • 由于嵌套层次的问题,所以 npm 对包的下载只能是串行的,即前一个包下载完后才会下载下一个包,导致带宽资源没有完全利用
    • 多个相同版本的包被重复的下载
  • 控制台输出繁杂:过去,npm 安装包的时候,每安装一个依赖,就会输出依赖的详细信息,导致一次安装有大量的信息输出到控制台,遇到错误极难查看
  • 工程移植问题:由于 npm 的版本依赖可以是模糊的,可能会导致工程移植后,依赖的确切版本不一致。

针对上述问题,yarn 从诞生那天就已经解决,它用到了以下的手段:

  • 使用扁平的目录结构
  • 并行下载
  • 使用本地缓存
  • 控制台仅输出关键信息
  • 使用 yanr-lock 文件记录确切依赖

不仅如此,yarn 还优化了以下内容:

  • 增加了某些功能强大的命令
  • 让既有的命令更加语义化
  • 本地安装的 CLI 工具可以使用 yarn 直接启动
  • 将全局安装的目录当作一个普通的工程,生成 package.json 文件,便于全局安装移植

全局安装目录: npm root -g yarn 的出现给 npm 带来了巨大的压力,很快,npm 学习了 yarn 先进的理念,不断的对自身进行优化,到了目前的 npm6 版本,几乎完全解决了上面的问题:

  • 目录扁平化
  • 并行下载
  • 本地缓存 (清空缓存:npm chache clean)
  • 使用 package-lock 记录确切依赖
  • 增加了大量的命令别名
  • 内置了 npx,可以启动本地的 CLI 工具
  • 极大的简化了控制台输出

总结 npm6 之后,可以说 npm 已经和 yarn 非常接近,甚至没有差距了。很多新的项目,又重新从 yarn 转回到 npm。 这两个包管理器是目前的主流,都必须要学习。

yarn 的核心命令

  1. 初始化

初始化:yarn init [--yes/-y]

  1. 安装

添加指定包:yarn [global] add package-name [--dev/-D] [--exact/-E] 全局安装    

javascript
yarn global add nodemon
+
yarn global add nodemon
+

安装 package.json 中的所有依赖:yarn install [--production/--prod]

  1. 脚本和本地 CLI

运行脚本:yarn run 脚本名

start、stop、test 可以省略 run

运行本地安装的 CLI:yarn run CLI名

  1. 查询

查看 bin 目录:yarn [global] bin 查询包信息:yarn info 包名 [子字段]yarn info 包名 version 列举已安装的依赖:yarn [global] list [--depth=依赖深度]

yarn 的 list 命令和 npm 的 list 不同,yarn 输出的信息更加丰富,包括顶级目录结构、每个包的依赖版本号

  1. 更新

列举需要更新的包:yarn outdated 更新包:yarn [global] upgrade [包名]

  1. 卸载

卸载包:yarn remove 包名

yarn 的特别礼物

在终端命令上,yarn 不仅仅是对 npm 的命令做了一个改名,还增加了一些原本没有的命令,这些命令在某些时候使用起来非常方便

  1. yarn check

使用yarn check命令,可以验证 package.json 文件的依赖记录和 lock 文件是否一致

这对于防止篡改非常有用

  1. yarn audit

使用yarn audit命令,可以检查本地安装的包有哪些已知漏洞,以表格的形式列出,漏洞级别分为以下几种:

  • INFO:信息级别
javascript
module.exports = function (...args) {
+  // 求和,没有考虑到字符串输入会导致拼接
+  return args.reduce((s, item) => s + item, 0);
+};
+
module.exports = function (...args) {
+  // 求和,没有考虑到字符串输入会导致拼接
+  return args.reduce((s, item) => s + item, 0);
+};
+
  • LOW: 低级别
  • MODERATE:中级别
  • HIGH:高级别
  • CRITICAL:关键级别
  1. yarn why

使用yarn why 包名命令,可以在控制台打印出为什么安装了这个包,哪些包会用到它

  1. yarn create

非常有趣的命令

今后,我们会学习一些脚手架,所谓脚手架,就是使用一个命令来搭建一个工程结构

过去,我们都是使用如下的做法:

  1. 全局安装脚手架工具
  2. 使用全局命令搭建脚手架

由于大部分脚手架工具都是以create-xxx的方式命名的,比如 react 的官方脚手架名称为create-react-app

因此,可以使用yarn create命令来一步完成安装和搭建

例如:

shell
yarn create react-app my-app
+# 等同于下面的两条命令
+yarn global add create-react-app
+create-react-app my-app
+
yarn create react-app my-app
+# 等同于下面的两条命令
+yarn global add create-react-app
+create-react-app my-app
+

其他包管理器

cnpm

官网地址:https://npm.taobao.org/

为解决国内用户连接 npm registry 缓慢的问题,淘宝搭建了自己的 registry,即淘宝 npm 镜像源

过去,npm 没有提供修改 registry 的功能,因此,淘宝提供了一个 CLI 工具即 cnpm,它支持除了npm publish以外的所有命令,只不过连接的是淘宝镜像源

如今,npm 已经支持修改 registry 了,可能 cnpm 唯一的作用就是和 npm 共存,即如果要使用官方源,则使用 npm,如果使用淘宝源,则使用 cnpm

javascript
npm install -g cnpm --registry=https://registry.npm.taobao.org
+
npm install -g cnpm --registry=https://registry.npm.taobao.org
+

nvm

nvm 并非包管理器,它是用于管理多个 node 版本的工具

在实际的开发中,可能会出现多个项目分别使用的是不同的 node 版本,在这种场景下,管理不同的 node 版本就显得尤为重要

nvm 就是用于切换版本的一个工具

1.下载和安装

最新版下载地址:https://github.com/coreybutler/nvm-windows/releases

下载 nvm-setup.zip 后,直接安装 一路下一步,不要改动安装路径(开发类工具尽量不该动)

2.使用 nvm

nvm 提供了 CLI 工具,用于管理 node 版本

管理员运行输入 nvm,以查看各种可用命令 nvm arch:打印系统版本和默认 node 架构类型 nvm install 8.5.4:nvm 安装指定的 node 版本 nvm list :列出目前电脑上使用的以及已经安装过的那些 node 版本 npm list available:node 版本

为了加快下载速度,建议设置淘宝镜像 node 淘宝镜像:https://npm.taobao.org/mirrors/node/ npm 淘宝镜像:https://npm.taobao.org/mirrors/npm/

nvm node _mirror https://npm.taobao.org/mirrors/node/ nvm npm_mirror https://npm.taobao.org/mirrors/npm/ 查看全局安装包:npm -g list --depth=0 切换 node 版本:nvm use 10.18.0 看 node 版本:node -v 卸载:nvm uninstall 10.18.0

pnpm

pnpm 是一种新起的包管理器,从 npm 的下载量看,目前还没有超过 yarn,但它的实现方式值得主流包管理器学习,某些开发者极力推荐使用 pnpm

从结果上来看,它具有以下优势:

  1. 目前,安装效率高于 npm 和 yarn 的最新版
  2. 极其简洁的 node_modules 目录
  3. 避免了开发时使用间接依赖的问题(之前的包管理器可以使用间接依赖不报错)pnpm 没有把间接依赖直接放入 node_modules 里面
  4. 能极大的降低磁盘空间的占用
  5. 使用缓存

1.安装和使用

全局安装 pnpm

shell
npm install -g pnpm
+
npm install -g pnpm
+

之后在使用时,只需要把 npm 替换为 pnpm 即可

如果要执行安装在本地的 CLI,可以使用 pnpx,它和 npx 的功能完全一样,唯一不同的是,在使用 pnpx 执行一个需要安装的命令时,会使用 pnpm 进行安装

比如npx mocha执行本地的mocha命令时,如果mocha没有安装,则 npx 会自动的、临时的安装 mocha,安装好后,自动运行 mocha 命令 类似:npx create-react-app my-app 临时下载 create-react-app,然后自动运行命令

2.pnpm 原理

  1. 同 yarn 和 npm 一样,pnpm 仍然使用缓存来保存已经安装过的包,以及使用 pnpm-lock.yaml 来记录详细的依赖版本

缓存位于工程所在盘的根目录,所以位置不固定

  1. 不同于 yarn 和 npm, pnpm 使用符号链接和硬链接(可将它们想象成快捷方式)的做法来放置依赖,从而规避了从缓存中拷贝文件的时间,使得安装和卸载的速度更快
  2. 由于使用了符号链接和硬链接,pnpm 可以规避 windows 操作系统路径过长的问题,因此,它选择使用树形的依赖结果,有着几乎完美的依赖管理。也因为如此,项目中只能使用直接依赖,而不能使用间接依赖

3.注意事项

由于 pnpm 会改动 node_modules 目录结构,使得每个包只能使用直接依赖,而不能使用间接依赖,因此,如果使用 pnpm 安装的包中包含间接依赖,则会出现问题(现在不会了,除非使用了绝对路径)

由于 pnpm 超高的安装卸载效率,越来越多的包开始修正之前的间接依赖代码

bower

浏览器端

过时了

`,255),d=[i];function B(y,u,F,m,h,b){return n(),a("div",null,d)}const v=s(t,[["render",B]]);export{C as __pageData,v as default}; diff --git a/assets/front-end-engineering_PackageManager.md.d80a1c87.lean.js b/assets/front-end-engineering_PackageManager.md.d80a1c87.lean.js new file mode 100644 index 00000000..698d5503 --- /dev/null +++ b/assets/front-end-engineering_PackageManager.md.d80a1c87.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-22-03-34.729a63f6.png",e="/blog/assets/2023-02-01-22-03-43.d520fd1e.png",o="/blog/assets/2023-02-01-22-03-59.a0646341.png",r="/blog/assets/2023-02-01-22-04-06.095f01dd.png",c="/blog/assets/2023-02-01-22-05-00.cd9ee97d.png",C=JSON.parse('{"title":"包管理器","description":"","frontmatter":{},"headers":[{"level":2,"title":"包管理工具概述","slug":"包管理工具概述","link":"#包管理工具概述","children":[{"level":3,"title":"1.概念","slug":"_1-概念","link":"#_1-概念","children":[]},{"level":3,"title":"2.背景","slug":"_2-背景","link":"#_2-背景","children":[]},{"level":3,"title":"3.前端包管理器","slug":"_3-前端包管理器","link":"#_3-前端包管理器","children":[]}]},{"level":2,"title":"npm","slug":"npm","link":"#npm","children":[{"level":3,"title":"包的安装","slug":"包的安装","link":"#包的安装","children":[]},{"level":3,"title":"包配置","slug":"包配置","link":"#包配置","children":[]},{"level":3,"title":"包的使用","slug":"包的使用","link":"#包的使用","children":[]},{"level":3,"title":"简易数据爬虫","slug":"简易数据爬虫","link":"#简易数据爬虫","children":[]}]},{"level":2,"title":"语义版本","slug":"语义版本","link":"#语义版本","children":[{"level":3,"title":"1.避免还原的差异","slug":"_1-避免还原的差异","link":"#_1-避免还原的差异","children":[]},{"level":3,"title":"2.[扩展]npm 的差异版本处理","slug":"_2-扩展-npm-的差异版本处理","link":"#_2-扩展-npm-的差异版本处理","children":[]}]},{"level":2,"title":"npm 脚本 (npm scripts)","slug":"npm-脚本-npm-scripts","link":"#npm-脚本-npm-scripts","children":[]},{"level":2,"title":"运行环境配置","slug":"运行环境配置","link":"#运行环境配置","children":[{"level":3,"title":"在 node 中读取 package.json","slug":"在-node-中读取-package-json","link":"#在-node-中读取-package-json","children":[]}]},{"level":2,"title":"其他 npm 命令","slug":"其他-npm-命令","link":"#其他-npm-命令","children":[{"level":3,"title":"1.安装","slug":"_1-安装","link":"#_1-安装","children":[]},{"level":3,"title":"2.查询","slug":"_2-查询","link":"#_2-查询","children":[]},{"level":3,"title":"3.更新","slug":"_3-更新","link":"#_3-更新","children":[]},{"level":3,"title":"4.卸载包","slug":"_4-卸载包","link":"#_4-卸载包","children":[]},{"level":3,"title":"5.npm 配置","slug":"_5-npm-配置","link":"#_5-npm-配置","children":[]}]},{"level":2,"title":"发布包","slug":"发布包","link":"#发布包","children":[{"level":3,"title":"1.准备工作","slug":"_1-准备工作","link":"#_1-准备工作","children":[]},{"level":3,"title":"2.发布","slug":"_2-发布","link":"#_2-发布","children":[]},{"level":3,"title":"开源协议","slug":"开源协议","link":"#开源协议","children":[]}]},{"level":2,"title":"yarn 简介","slug":"yarn-简介","link":"#yarn-简介","children":[{"level":3,"title":"yarn 的核心命令","slug":"yarn-的核心命令","link":"#yarn-的核心命令","children":[]},{"level":3,"title":"yarn 的特别礼物","slug":"yarn-的特别礼物","link":"#yarn-的特别礼物","children":[]}]},{"level":2,"title":"其他包管理器","slug":"其他包管理器","link":"#其他包管理器","children":[{"level":3,"title":"cnpm","slug":"cnpm","link":"#cnpm","children":[]},{"level":3,"title":"nvm","slug":"nvm","link":"#nvm","children":[]},{"level":3,"title":"pnpm","slug":"pnpm","link":"#pnpm","children":[]},{"level":3,"title":"bower","slug":"bower","link":"#bower","children":[]}]}],"relativePath":"front-end-engineering/PackageManager.md","lastUpdated":1675736874000}'),t={name:"front-end-engineering/PackageManager.md"},i=l("",255),d=[i];function B(y,u,F,m,h,b){return n(),a("div",null,d)}const v=s(t,[["render",B]]);export{C as __pageData,v as default}; diff --git a/assets/front-end-engineering_engineering-onepage.md.c6fbbc99.js b/assets/front-end-engineering_engineering-onepage.md.c6fbbc99.js new file mode 100644 index 00000000..a920f2af --- /dev/null +++ b/assets/front-end-engineering_engineering-onepage.md.c6fbbc99.js @@ -0,0 +1,463 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"前端工程化 OnePage","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么会出现前端工程化","slug":"为什么会出现前端工程化","link":"#为什么会出现前端工程化","children":[]},{"level":2,"title":"nodejs对CommonJS的实现","slug":"nodejs对commonjs的实现","link":"#nodejs对commonjs的实现","children":[]},{"level":2,"title":"nodejs","slug":"nodejs","link":"#nodejs","children":[]},{"level":2,"title":"CommonJS标准和使用","slug":"commonjs标准和使用","link":"#commonjs标准和使用","children":[]},{"level":2,"title":"下面的代码执行结果是什么?","slug":"下面的代码执行结果是什么","link":"#下面的代码执行结果是什么","children":[]},{"level":2,"title":"ES6 module","slug":"es6-module","link":"#es6-module","children":[]},{"level":2,"title":"请对比一下CommonJS和ES Module","slug":"请对比一下commonjs和es-module","link":"#请对比一下commonjs和es-module","children":[]},{"level":2,"title":"对前端工程化,模块化,组件化的理解?","slug":"对前端工程化-模块化-组件化的理解","link":"#对前端工程化-模块化-组件化的理解","children":[]},{"level":2,"title":"webpack 中的 loader 属性和 plugins 属性的区别是什么?","slug":"webpack-中的-loader-属性和-plugins-属性的区别是什么","link":"#webpack-中的-loader-属性和-plugins-属性的区别是什么","children":[]},{"level":2,"title":"webpack 的核心概念都有哪些?","slug":"webpack-的核心概念都有哪些","link":"#webpack-的核心概念都有哪些","children":[]},{"level":2,"title":"ES6 中如何实现模块化的异步加载?","slug":"es6-中如何实现模块化的异步加载","link":"#es6-中如何实现模块化的异步加载","children":[]},{"level":2,"title":"说一下 webpack 中的几种 hash 的实现原理是什么?","slug":"说一下-webpack-中的几种-hash-的实现原理是什么","link":"#说一下-webpack-中的几种-hash-的实现原理是什么","children":[]},{"level":2,"title":"webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?","slug":"webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","link":"#webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","children":[]},{"level":2,"title":"webpack 中是如何处理图片的?","slug":"webpack-中是如何处理图片的","link":"#webpack-中是如何处理图片的","children":[]},{"level":2,"title":"webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?","slug":"webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","link":"#webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","children":[]},{"level":2,"title":"webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?","slug":"webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","link":"#webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","children":[]},{"level":2,"title":"介绍一下 webpack4 中的 tree-shaking 的工作流程?","slug":"介绍一下-webpack4-中的-tree-shaking-的工作流程","link":"#介绍一下-webpack4-中的-tree-shaking-的工作流程","children":[]},{"level":2,"title":"说一下 webpack loader 的作用是什么?","slug":"说一下-webpack-loader-的作用是什么","link":"#说一下-webpack-loader-的作用是什么","children":[]},{"level":2,"title":"在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?","slug":"在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","link":"#在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","children":[]},{"level":2,"title":"export 和 export default 的区别是什么?","slug":"export-和-export-default-的区别是什么","link":"#export-和-export-default-的区别是什么","children":[]},{"level":2,"title":"webpack 打包原理是什么?","slug":"webpack-打包原理是什么","link":"#webpack-打包原理是什么","children":[]},{"level":2,"title":"webpack 热更新原理是什么?","slug":"webpack-热更新原理是什么","link":"#webpack-热更新原理是什么","children":[]},{"level":2,"title":"如何优化 webpack 的打包速度?","slug":"如何优化-webpack-的打包速度","link":"#如何优化-webpack-的打包速度","children":[]},{"level":2,"title":"webpack 如何实现动态导入?","slug":"webpack-如何实现动态导入","link":"#webpack-如何实现动态导入","children":[]},{"level":2,"title":"说一下 webpack 有哪几种文件指纹","slug":"说一下-webpack-有哪几种文件指纹","link":"#说一下-webpack-有哪几种文件指纹","children":[]},{"level":2,"title":"常用的 webpack Loader 都有哪些?","slug":"常用的-webpack-loader-都有哪些","link":"#常用的-webpack-loader-都有哪些","children":[]},{"level":2,"title":"说一下 webpack 常用插件都有哪些?","slug":"说一下-webpack-常用插件都有哪些","link":"#说一下-webpack-常用插件都有哪些","children":[]},{"level":2,"title":"使用 babel-loader 会有哪些问题,可以怎样优化?","slug":"使用-babel-loader-会有哪些问题-可以怎样优化","link":"#使用-babel-loader-会有哪些问题-可以怎样优化","children":[]},{"level":2,"title":"babel 是如何对 class 进行编译的?","slug":"babel-是如何对-class-进行编译的","link":"#babel-是如何对-class-进行编译的","children":[]},{"level":2,"title":"释一下 babel-polyfill 的作用是什么?","slug":"释一下-babel-polyfill-的作用是什么","link":"#释一下-babel-polyfill-的作用是什么","children":[]},{"level":2,"title":"解释一下 less 的&的操作符是做什么用的?","slug":"解释一下-less-的-的操作符是做什么用的","link":"#解释一下-less-的-的操作符是做什么用的","children":[]},{"level":2,"title":"webpack proxy 工作原理,为什么能解决跨域?","slug":"webpack-proxy-工作原理-为什么能解决跨域","link":"#webpack-proxy-工作原理-为什么能解决跨域","children":[]},{"level":2,"title":"组件发布的是不是所有依赖这个组件库的项目都需要升级?","slug":"组件发布的是不是所有依赖这个组件库的项目都需要升级","link":"#组件发布的是不是所有依赖这个组件库的项目都需要升级","children":[]},{"level":2,"title":"具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)","slug":"具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","link":"#具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","children":[]},{"level":2,"title":"描述一下 webpack 的构建流程?(CVTE)","slug":"描述一下-webpack-的构建流程-cvte","link":"#描述一下-webpack-的构建流程-cvte","children":[]},{"level":2,"title":"解释一下 webpack 插件的实现原理?(CVTE)","slug":"解释一下-webpack-插件的实现原理-cvte","link":"#解释一下-webpack-插件的实现原理-cvte","children":[]},{"level":2,"title":"有用过哪些插件做项目的分析吗?(CVTE)","slug":"有用过哪些插件做项目的分析吗-cvte","link":"#有用过哪些插件做项目的分析吗-cvte","children":[]},{"level":2,"title":"什么是 babel,有什么作用?","slug":"什么是-babel-有什么作用","link":"#什么是-babel-有什么作用","children":[]},{"level":2,"title":"解释一下 npm 模块安装机制是什么?","slug":"解释一下-npm-模块安装机制是什么","link":"#解释一下-npm-模块安装机制是什么","children":[]},{"level":2,"title":"webpack与grunt、gulp的不同?","slug":"webpack与grunt、gulp的不同","link":"#webpack与grunt、gulp的不同","children":[]},{"level":2,"title":"2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?","slug":"_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","link":"#_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","children":[]},{"level":2,"title":"3.有哪些常见的Loader?他们是解决什么问题的?","slug":"_3-有哪些常见的loader-他们是解决什么问题的","link":"#_3-有哪些常见的loader-他们是解决什么问题的","children":[]},{"level":2,"title":"4.有哪些常见的Plugin?他们是解决什么问题的?","slug":"_4-有哪些常见的plugin-他们是解决什么问题的","link":"#_4-有哪些常见的plugin-他们是解决什么问题的","children":[]},{"level":2,"title":"5.Loader和Plugin的不同?","slug":"_5-loader和plugin的不同","link":"#_5-loader和plugin的不同","children":[]},{"level":2,"title":"6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全","slug":"_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","link":"#_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","children":[]},{"level":2,"title":"7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?","slug":"_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","link":"#_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","children":[]},{"level":2,"title":"11.怎么配置单页应用?怎么配置多页应用?","slug":"_11-怎么配置单页应用-怎么配置多页应用","link":"#_11-怎么配置单页应用-怎么配置多页应用","children":[]},{"level":2,"title":"12.npm打包时需要注意哪些?如何利用webpack来更好的构建?","slug":"_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","link":"#_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","children":[]},{"level":2,"title":"13.如何在vue项目中实现按需加载?","slug":"_13-如何在vue项目中实现按需加载","link":"#_13-如何在vue项目中实现按需加载","children":[]},{"level":2,"title":"能说说webpack的作用吗?","slug":"能说说webpack的作用吗","link":"#能说说webpack的作用吗","children":[]},{"level":2,"title":"为什么要打包呢?","slug":"为什么要打包呢","link":"#为什么要打包呢","children":[]},{"level":2,"title":"能说说模块化的好处吗?","slug":"能说说模块化的好处吗","link":"#能说说模块化的好处吗","children":[]},{"level":2,"title":"模块化打包方案知道啥,可以说说吗?","slug":"模块化打包方案知道啥-可以说说吗","link":"#模块化打包方案知道啥-可以说说吗","children":[]},{"level":2,"title":"那ES6 模块与 CommonJS 模块的差异有哪些呢?","slug":"那es6-模块与-commonjs-模块的差异有哪些呢","link":"#那es6-模块与-commonjs-模块的差异有哪些呢","children":[]},{"level":2,"title":"webpack的编译(打包)流程说说","slug":"webpack的编译-打包-流程说说","link":"#webpack的编译-打包-流程说说","children":[]},{"level":2,"title":"说一下 Webpack 的热更新原理吧?","slug":"说一下-webpack-的热更新原理吧","link":"#说一下-webpack-的热更新原理吧","children":[]},{"level":2,"title":"路由懒加载的原理","slug":"路由懒加载的原理","link":"#路由懒加载的原理","children":[]},{"level":2,"title":"路由懒加载?","slug":"路由懒加载","link":"#路由懒加载","children":[]},{"level":2,"title":"git 指令","slug":"git-指令","link":"#git-指令","children":[]},{"level":2,"title":"知道git的原理吗?说说他的原理吧","slug":"知道git的原理吗-说说他的原理吧","link":"#知道git的原理吗-说说他的原理吧","children":[]},{"level":2,"title":"package.json 字段","slug":"package-json-字段","link":"#package-json-字段","children":[]},{"level":2,"title":"npm安装和查找机制","slug":"npm安装和查找机制","link":"#npm安装和查找机制","children":[]}],"relativePath":"front-end-engineering/engineering-onepage.md","lastUpdated":1718508254000}'),p={name:"front-end-engineering/engineering-onepage.md"},e=l(`

前端工程化 OnePage

为什么会出现前端工程化

模块化:意味着 JS 总算可以开发大型项目(解决 JS 的复用问题)

组件化:解决 HTML、CSS、JS 的复用问题

工程化:前端项目越来越复杂、前端项目模块(组件)越来越多

为什么前端项目越来越复杂?

在书写前端项目的时候,可能会涉及到使用其他的语言,typescript、less/sass、coffeescript,涉及到编译

在部署的时候,还需要对代码进行丑化、压缩

代码优化:为 CSS 代码添加兼容性前缀

前端项目模块(组件)越来越多?

专门有一个 node_modules 来管理这些可以复用的代码。

前端工程化的出现,归根结底,其实就是要解决开发环境和生产环境不一致的问题。

nodejs对CommonJS的实现

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

nodejs中导入模块,使用相对路径,并且必须以./或../开头,浏览器可以省略./,nodejs不行

  1. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
javascript
 (function(){
+     //模块中的代码
+ })()
+
 (function(){
+     //模块中的代码
+ })()
+
  1. 为了保证顺利的导出模块内容,nodejs做了以下处理
    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
javascript
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+

面试

javascript
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+

经典面试题 util.js

javascript
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+

index.js

javascript
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+

经验:对exports赋值无意义,建议用module.exports

  1. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

过去,JS很难编写大型应用,因为有以下两个问题:

  1. 全局变量污染
  2. 难以管理的依赖关系

这些问题,都导致了JS无法进行精细的模块划分,因为精细的模块划分会导致更多的全局污染以及更加复杂的依赖关系 于是,先后出现了两大模块化标准,用于解决以上两个问题:

  • CommonJS
  • ES6 Module

注意:上面提到的两个均是模块化标准,具体的实现需要依托于JS的宿主环境

nodejs

node环境支持 CommonJS 模块化标准,所以,要使用 CommonJS,必须要先安装node

官网地址:https://nodejs.org/zh-cn/

nodejs直接运行某个js文件,该文件被称之为入口文件

nodejs遵循EcmaScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在nodejs中使用EcmaScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在nodejs中使用浏览器的 web api,例如:dom对象、window对象、document对象等

CommonJS标准和使用

node中的所有代码均在CommonJS规范下运行 具体规范如下:

  1. 一个JS文件即为一个模块,一个模块就是一个相对独立的功能,模块中的所有全局代码产生的变量、函数,均不会对全局造成任何污染,仅在模块内使用
  2. 如果一个模块需要暴露一些数据或功能供其他模块使用,需要使用代码module.exports = xxx,该过程称之为模块的导出
  3. 如果一个模块需要使用另一个模块导出的内容,需要使用代码require("模块路径")
    1. 路径必须以./../开头
    2. 如果模块文件后缀名为.js,可以省略后缀名
    3. require函数返回的是模块导出的内容
  4. 模块具有缓存,第一次导入模块时会缓存模块的导出,之后再导入同一个模块,直接使用之前缓存的结果。

同时也解决了JS的两个问题 浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module

原理: require中的伪代码

javascript
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
  1. 根据传递的模块路径,得到模块完整的绝对路径。因为绝对路径不会重复。

  2. require引入相同模块会有缓存,不会重复加载

  3. 如果没有缓存。真正运行模块代码的辅助函数

javascript
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
  1. 返回 module.exports

this === exports === module.exports === {}

下面的代码执行结果是什么?

javascript
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+

ES6 module

由于种种原因,CommonJS标准难以在浏览器中实现,因此一直在浏览器端一直没有合适的模块化标准,直到ES6标准出现 ES6规范了浏览器的模块化标准,一经发布,各大浏览器厂商纷纷在自己的浏览器中实现了该规范

模块的引入:浏览器使用以下方式引入一个ES6模块文件

html
<script src="JS文件" type="module">
+
<script src="JS文件" type="module">
+
  1. 模块的导出分为两种,基本导出(具名导出)默认导出基本导出导出的是该对象的某个属性,默认导出导出的是该对象的特殊属性default
javascript
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
  1. 基本导出可以有多个,默认导出只能有一个
  2. 基本导出必须要有名字,默认导出由于有特殊名字,所以可以不用写名字
javascript
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
  1. 模块的导入
javascript
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+

注意

  1. ES6 module 采用依赖预加载模式,所有模块导入代码均会提升到代码顶部
  2. 不能将导入代码放置到判断、循环中
  3. 导入的内容放置到常量中,不可更改
  4. ES6 module 使用了缓存,保证每个模块仅加载一次

请对比一下CommonJS和ES Module

  1. CMJ 是社区标准,ESM 是官方标准
  2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
  3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
  4. CMJ 是动态的依赖,ESM 既支持动态,也支持静态
  5. ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值

CommonJS是社区模块化标准,node环境支持该标准。它不产生新的语法,是使用API实现的,本质是把要加载的模块放到一个闭包中执行(这里可以详细的阐述)。因此,node环境中的所有JS文件在执行的时候,都是放到一个函数环境中执行的,这对使得模块内部可以访问函数的参数,如exports、module、__dirname等,而且对this的指向也会有影响。CommonJS是动态的模块化标准,这就意味着依赖关系是在运行过程中确定的,同时也意味着在导入导出模块时,并不限制书写的位置。 ES Module是官方模块化标准,目前node和浏览器均支持该标准,它引入了新的语法。ES Module是静态的模块化标准,在模块执行前,会递归确定所有依赖关系,然后加载所有文件,加载完成后再运行,这在浏览器环境下会产生多次请求。由于使用的是静态依赖,因此,它要求导入导出的代码必须放置到顶层,因为只有这样才能在代码运行前就确定依赖关系。ES Module导入模块时会将导入的结果绑定到标识符中,该标识符是一个常量,不可更改。 CommonJS和ES Module都使用了缓存,保证每个模块仅执行一次。

1.讲讲模块化规范

2.import和require的区别

3.require是如何解析路径的

1、ESM是编译时导出结果,CommonJS是运⾏时导出结果

2、ESM是导出引⽤,CommonJS是导出⼀个值

3、ESM⽀持异步,CommonJS只⽀持同步

下面的模块导出了什么结果?

javascript
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+

对前端工程化,模块化,组件化的理解?

这三者中,模块化是基础,没有模块化,就没有组件化和工程化

模块化的出现,解决了困扰前端的两大难题:全局污染问题和依赖混乱问题,从而让精细的拆分前端工程成为了可能。

工程化的出现,解决了前端开发环境和生产环境要求不一致的矛盾。在开发环境中,我们希望代码使用尽可能的细分,代码格式尽可能的统一和规范,而在生产环境中,我们希望代码尽可能的被压缩、混淆,尽可能的优化体积。工程化的出现,就是为了解决这一矛盾,它可以让我们舒服的在开发环境中书写代码,然后经过打包,生成最合适的生产环境代码,这样就解放了开发者的精力,让开发者把更多的注意力集中在开发环境上即可。

组件化开发是一些前端框架带来的概念,它把一个网页,或者一个站点,甚至一个完整的产品线,划分为多个小的组件,组件是一个可以复用的单元,它包含了一个某个区域的完整功能。这样一来,前端便具备了开发复杂应用的能力。

webpack 中的 loader 属性和 plugins 属性的区别是什么?

它们都是 webpack 功能的扩展点。

loader 是加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等

plugins 是插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等

webpack 的核心概念都有哪些?

参考答案:

  • loader 加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
  • plugin 插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
  • module 模块。webpack 将所有依赖均视为模块,无论是 js、css、html、图片,统统都是模块
  • entry 入口。打包过程中的概念,webpack 以一个或多个文件作为入口点,分析整个依赖关系。
  • chunk 打包过程中的概念,一个 chunk 是一个相对独立的打包过程,以一个或多个文件为入口,分析整个依赖关系,最终完成打包合并
  • bundle webpack 打包结果
  • tree shaking 树摇优化。在打包结果中,去掉没有用到的代码。
  • HMR 热更新。是指在运行期间,遇到代码更改后,无须重启整个项目,只更新变动的那一部分代码。
  • dev server 开发服务器。在开发环境中搭建的临时服务器,用于承载对打包结果的访问

ES6 中如何实现模块化的异步加载?

使用动态导入即可,导入后,得到的是一个 Promise,完成后,得到一个模块对象,其中包含了所有的导出结果。

说一下 webpack 中的几种 hash 的实现原理是什么?

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?

参考答案:

不会。它跟关联的内容是否有变化有关系,如果没有变化,hash 就不会变。具体来说,contenthash 和具体的打包文件内容有关,chunkhash 和某一 entry 为起点的打包过程中涉及的内容有关,hash 和整个工程所有模块内容有关。

webpack 中是如何处理图片的?

参考答案:

webpack 本身不处理图片,它会把图片内容仍然当做 JS 代码来解析,结果就是报错,打包失败。如果要处理图片,需要通过 loader 来处理。其中,url-loader 会把图片转换为 base64 编码,然后得到一个 dataurl,file-loader 则会将图片生成到打包目录中,然后得到一个资源路径。但无论是哪一种 loader,它们的核心功能,都是把图片内容转换成 JS 代码,因为只有转换成 JS 代码,webpack 才能识别。

webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?

说明:这道题的表述是有问题的,webpack 本身并不打包 html,相反,它如果遇到 html 代码会直接打包失败,因为 webpack 本身只能识别 JS。之所以能够打包出 html 文件,是因为插件或 loader 的作用,其中,比较常见的插件是 html-webpack-plugin。所以这道题的正确表述应该是:「html-webpack-plugin 打包出来的 html 为什么 style 放在头部 script 放在底部?」

webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?

要配置 CDN,有两个步骤:

  1. 在 html 模板中直接加入 cdn 引用
  2. 在 webpack 配置中,加入externals配置,告诉 webpack 不要打包其中的模块,转而使用全局变量

若要在开发环境中不使用 CDN,只需根据环境变量判断不同的环境,进行不同的打包处理即可。

  1. 在 html 模板中使用 ejs 模板语法进行判断,只有在生产环境中引入 CDN
  2. 在 webpack 配置中,可以根据process.env中的环境变量进行判断是否使用externals配置
  3. package.json脚本中设置不同的环境变量完成打包或开发启动。

介绍一下 webpack4 中的 tree-shaking 的工作流程?

推荐阅读:https://tsejx.github.io/webpack-guidebook/principle-analysis/operational-principle/tree-shaking

说一下 webpack loader 的作用是什么?

参考答案:

用于转换代码。有时是因为 webpack 无法识别某些内容,比如图片、css 等,需要由 loader 将其转换为 JS 代码。有时是因为某些代码需要被特殊处理,比如 JS 兼容性的处理,需要由 loader 将其进一步转换。不管是什么情况,loader 的作用只有一个,就是转换代码。

在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

export 和 export default 的区别是什么?

参考答案:

export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,比如变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出

export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出。

webpack 打包原理是什么?

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack 热更新原理是什么?

参考答案:

当开启热更新后,页面中会植入一段 websocket 脚本,同时,开发服务器也会和客户端建立 websocket 通信,当源码发生变动时,webpack 会进行以下处理:

  1. webpack 重新打包
  2. webpack-dev-server 检测到模块的变化,于是通过 webscoket 告知客户端变化已经发生
  3. 客户端收到消息后,通过 ajax 发送请求到开发服务器,以过去打包的 hash 值请求服务器的一个 json 文件
  4. 服务器告诉客户端哪些模块发生了变动,同时告诉客户端这次打包产生的新 hash 值
  5. 客户端再次用过去的 hash 值,以 JSONP 的方式请求变动的模块
  6. 服务器响应一个函数调用,用于更新模块的代码
  7. 此时,模块代码已经完成更新。客户端按照之前的监听配置,执行相应模块变动后的回调函数。

如何优化 webpack 的打包速度?

参考答案:

  1. noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  2. externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  3. 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  4. 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  5. 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  6. 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库

webpack 如何实现动态导入?

参考答案:

当遇到代码中包含动态导入语句时,webpack 会将导入的模块及其依赖分配到单独的一个 chunk 中进行打包,形成单独的打包结果。而动态导入的语句会被编译成一个普通的函数调用,该函数在执行时,会使用 JSONP 的方式动态的把分离出去的包加载到模块集合中。

说一下 webpack 有哪几种文件指纹

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

常用的 webpack Loader 都有哪些?

参考答案:

  • cache-loader:启用编译缓存
  • thread-loader:启用多线程编译
  • css-loader:编译 css 代码为 js
  • file-loader:保存文件到输出目录,将文件内容转换成文件路径
  • postcss-loader:将 css 代码使用 postcss 进行编译
  • url-loader:将文件内容转换成 dataurl
  • less-loader:将 less 代码转换成 css 代码
  • sass-loader:将 sass 代码转换成 css 代码
  • vue-loader:编译单文件组件
  • babel-loader:对 JS 代码进行降级处理

说一下 webpack 常用插件都有哪些?

参考答案:

  • clean-webpack-plugin:清除输出目录
  • copy-webpack-plugin:复制文件到输出目录
  • html-webpack-plugin:生成 HTML 文件
  • mini-css-extract-plugin:将 css 打包成单独文件的插件
  • HotModuleReplacementPlugin:热更新的插件
  • purifycss-webpack:去除无用的 css 代码
  • optimize-css-assets-webpack-plugin:优化 css 打包体积
  • uglify-js-plugin:对 JS 代码进行压缩、混淆
  • compression-webpack-plugin:gzip 压缩
  • webpack-bundle-analyzer:分析打包结果

使用 babel-loader 会有哪些问题,可以怎样优化?

参考答案:

  1. 如果不做特殊处理,babel-loader 会对所有匹配的模块进行降级,这对于那些已经处理好兼容性问题的第三方库显得多此一举,因此可以使用 exclude 配置排除掉这些第三方库
  2. 在旧版本的 babel-loader 中,默认开启了对 ESM 的转换,这样会导致 webpack 的 tree shaking 失效,因为 tree shaking 是需要保留 ESM 语法的,所以需要关闭 babel-loader 的 ESM 转换,在其新版本中已经默认关闭了。

babel 是如何对 class 进行编译的?

参考答案:

本质上就是把 class 语法转换成普通构造函数定义,并做了以下处理:

  1. 增加了对 this 指向的检测
  2. 将原型方法和静态方法变为不可枚举
  3. 将整个代码放到了立即执行函数中,运行后返回构造函数本身

释一下 babel-polyfill 的作用是什么?

说明:

babel-polyfill 已经是一个非常古老的项目了,babel 从 7.4 版本开始已不再支持它,转而使用更加强大的 core-js,此题也适用于问「core-js 的作用是什么」

解释一下 less 的&的操作符是做什么用的?

参考答案:

&符号后面的内容会和父级选择器合并书写,即中间不加入空格字符

webpack proxy 工作原理,为什么能解决跨域?

说明:

严格来说,webpack 只是一个打包工具,它并没有 proxy 的功能,甚至连服务器的功能都没有。之所以能够在 webpack 中使用 proxy 配置,是因为它的一个插件,即 webpack-dev-server 的能力。

所以,此题应该问做:「webpack-dev-server 工作原理,为什么能解决跨域?」

组件发布的是不是所有依赖这个组件库的项目都需要升级?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)

  1. 公共模块 比如某些多页应用会有多个入口,从而形成多个 chunk,而这些 chunk 中用到了一些公共模块,为了减少整体的包体积,可以使用 splitchunksplugin 将公共模块分离出来。 可以配置 minChunks 来指定被多少个 chunk 引用时进行分包
  2. 并行下载 由于 HTML5 支持 defer 和 async,因此可以同时下载多个 JS 文件以充分利用带宽。如果打包结果是一个很大的文件,就无法利用到这一点。 可以利用 splitchunks 插件将文件进行拆分,通过配置 maxSize 属性指定包体积达到多大时进行拆分

描述一下 webpack 的构建流程?(CVTE)

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

解释一下 webpack 插件的实现原理?(CVTE)

参考答案:

本质上,webpack 的插件是一个带有apply函数的对象。当 webpack 创建好 compiler 对象后,会执行注册插件的 apply 函数,同时将 compiler 对象作为参数传入。

在 apply 函数中,开发者可以通过 compiler 对象监听多个钩子函数的执行,不同的钩子函数对应 webpack 编译的不同阶段。当 webpack 进行到一定阶段后,会调用这些监听函数,同时将 compilation 对象传入。开发者可以使用 compilation 对象获取和改变 webpack 的各种信息,从而影响构建过程。

有用过哪些插件做项目的分析吗?(CVTE)

参考答案:

用过 webpack-bundle-analyzer 分析过打包结果,主要用于优化项目打包体积

什么是 babel,有什么作用?

参考答案:

babel 是一个 JS 编译器,主要用于将下一代的 JS 语言代码编译成兼容性更好的代码。

它其实本身做的事情并不多,它负责将 JS 代码编译成为 AST,然后依托其生态中的各种插件对 AST 中的语法和 API 进行处理

解释一下 npm 模块安装机制是什么?

参考答案:

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。 gruntgulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。 webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。 所以总结一下:

  • 从构建思路来说

gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

  • 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

3.有哪些常见的Loader?他们是解决什么问题的?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

4.有哪些常见的Plugin?他们是解决什么问题的?

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

5.Loader和Plugin的不同?

不同的作用

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。
  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。


7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。 编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。 相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

11.怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可,这里不再赘述 多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

12.npm打包时需要注意哪些?如何利用webpack来更好的构建?

Npm是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建。 NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

  1. CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
  2. 输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
  3. Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
  4. 不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
  5. 对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:
javascript
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+

13.如何在vue项目中实现按需加载?

Vue UI组件库的按需加载 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。 不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了。

javascript
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+

单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。 通过import()语句来控制加载时机,webpack内置了对于import()的解析,会将import()中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import()语句时,会去加载Chunk对应生成的文件。import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill

能说说webpack的作用吗?

  • 模块打包(静态资源拓展)。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容(翻译官loader)。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展(plugins)。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

为什么要打包呢?

逻辑多、文件多、项目的复杂度高了,所以要打包 例如: 让前端代码具有校验能力===>出现了ts css不好用===>出现了sass、less webpack可以解决这些问题

能说说模块化的好处吗?

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化打包方案知道啥,可以说说吗?

1、commonjs commonjs 是 Node 中的模块规范,通过 require 及 exports 进行导入导出 commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。 总结:commonjs是用在服务器端的,同步的,如nodejs2、AMD AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。 总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs3、CMD CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs4、esm esm 是 tc39 对于 ESMAScript 的模块话规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。 它使用 import/export 进行模块导入导出. 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。 export default命令,为模块指定默认输出

那ES6 模块与 CommonJS 模块的差异有哪些呢?

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

webpack的编译(打包)流程说说

  • 初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;
  • 开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
  • 确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
  • 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;
  • 输出完成:输出所有的chunk到文件系统;

说一下 Webpack 的热更新原理吧?

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为** HMR**。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS(无线路由)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

路由懒加载的原理

https://juejin.cn/post/6844904180285456398

路由懒加载?

当刚运行项目的时候,发现刚进入页面,就将所有的js文件和css文件加载了进来,这一进程十分的消耗时间。 如果打开哪个页面就对应的加载响应页面的js文件和css文件,那么页面加载速度会大大提升。

懒加载的好处是什么?

懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。

怎么使用的路由懒加载?

使用到的是es6的import语法,可以实现动态导入

路由懒加载的原理是什么

通过Webpack编译打包后,会把每个路由组件的代码分割成一一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。

作用就是webpack在打包的时候,对异步引入的库代码进行代码分割时(需要配置webpack的SplitChunkPlugin插件),为分割后的代码块取得名字 Vue中运用import的懒加载语句以及webpack的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。

npx

  1. 运行本地命令

使用npx 命令时,它会首先从本地工程的node_modules/.bin目录中寻找是否有对应的命令

例如:

shell
npx webpack
+
npx webpack
+

上面这条命令寻找本地工程的node_modules/.bin/webpack

如果将命令配置到package.jsonscripts中,可以省略npx

  1. 临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除

例如:

shell
npx prettyjson 1.json
+
npx prettyjson 1.json
+

npx会下载prettyjson包到临时目录,然后运行该命令

如果命令名称和需要下载的包名不一致时,可以手动指定报名

例如@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令

shell
npx -p @vue/cli vue create vue-app
+
npx -p @vue/cli vue create vue-app
+
  1. npm init

npm init通常用于初始化工程的package.json文件

除此之外,有时也可以充当npx的作用

shell
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+

git 指令

git fetch git fetch origin 分支 git rebase FETCH_HEAD 本地master更新到最新:git fetch origin master; git rebase FETCH_HEAD

git 和 svn 的区别 经常使用的 git 命令? git pull 和 git fetch 的区别 git rebase 和 git merge 的区别 git rebase和merge git分支管理、分支开发 git回退操作 git reset 和 git reverse区别?你还用过什么别的指令? git遇到冲突怎么进行解决

git-flow git reset --hard 版本号 git 如何合并分支; git 中,提交了 a, 然后又提交了 b, 如何撤回 b ? git merge、git rebase的区别 假设 master 分支切出了 test 分支,供所有人合代码测试, 然后我们 从稳定的 master 切出 一个 feature 分支进行开发, 现在 我们开发完了 feature 分支并将其merge 到 test 分支进行测试, 测试过程中出现一些问题,由于疏忽你;忘记切回 feature ,而是直接在 test 分支进行了开发 导致: test 分支优先于你的featuce 分支,此时你应该怎么特 test 分支的修改带入 feature

知道git的原理吗?说说他的原理吧

Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作 Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中那些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。 Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。 Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库

package.json 字段

name npm 包的名字,必须是一个小写的单词,可以包含连字符-和下划线_,发布时必填。

version npm 包的版本。需要遵循语义化版本格式x.x.x。

description npm 包的描述。

keywords npm 包的关键字。

homepage npm 包的主页地址。

bugs npm 包问题反馈的地址。

license 为 npm 包指定许可证

author npm 包的作者

files npm 包作为依赖安装时要包括的文件,格式是文件正则的数组。也可以使用 npmignore 来忽略个别文件。 以下文件总是被包含的,与配置无关 package.json README.md CHANGES / CHANGELOG / HISTORY LICENCE / LICENSE 以下文件总是被忽略的,与配置无关 .git .DS_Store node_modules .npmrc npm-debug.log package-lock.json ...

main 指定npm包的入口文件。

module 指定 ES 模块的入口文件。

typings 指定类型声明文件。

bin 开发可执行文件时,bin 字段可以帮助你设置链接,不需要手动设置 PATH。

repository npm 包托管的地方

scripts 可执行的命令。

dependencies npm包所依赖的其他npm包

devDependencies npm 包所依赖的构建和测试相关的 npm 包.

peerDependencies 指定 npm 包与主 npm 包的兼容性,当开发插件时是需要的.

engines 指定 npm 包可以使用的 Node 版本.

  • CSRF: 文件流steam(字节2020校招)
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
  • 安全相关:XSS,CSRF(字节2020校招)
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
  • 数据劫持(字节2020校招)
  • CSP,阻止iframe的嵌入(字节2020校招)
  • 进程和线程的区别知道吗?他们的通信方式有哪些?(字节2020校招)
  • js为什么是单线程(字节2020校招 + 京东2020校招)
  • section是用来做什么的(字节2020社招)
  • 了解SSO吗(字节2020社招)
  • 服务端渲染的方案有哪些?next.js和nuxt.js的区别(字节2020社招)
  • cdn的原理(字节2020社招)
  • JSBridge的原理(字节2020社招)
  • 了解过Serverless,什么是Serverless(字节2020社招)
无服务器~
+云计算?
+
无服务器~
+云计算?
+
  • 你还知道哪些新的前端技术(微前端、Low Code、可视化、BI、BFF)(字节2020社招)
  • 权限系统设计模型(DAC、MAC、RBAC、ABAC)(字节2020社招)
  • 什么是微前端(字节2020社招)
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
  • Node了解多少,有实际的项目经验吗(字节2020社招)
  • Linux了解多少,linux上查看日志的命令是什么(字节2020社招)
  • 强缓存和协商缓存(腾讯2020校招)
  • node可以做大型服务器吗,为什么(腾讯2020校招)
  • 用express还是koa(腾讯2020校招)
  • 用node写过哪些接口,登录验证是怎么做的(腾讯2020实习)
  • 清理node模块缓存的方法(腾讯2020校招)
  • 模仿一个xss的场景(从攻击到防御的流程)(腾讯2020校招)
  • Event loop知道吗?Node的eventloop的生命周期(京东2020校招)
  • JWT单点登录实现原理(小米2020校招)
  • 博客项目为什么使用Mysql?(小米2020校招)
  • 如何看待Node.js的中间件?它的好处是什么?(小米2020校招)
  • 为什么Node.js支持高并发请求?(小米2020校招)
  • 博客项目是SSR渲染还是客户端渲染?(小米2020校招)
  • web socket 和 socket io(广州长视科技2020校招)
  • 是否使⽤过webpack(不是通过vuecli的⽅式)(掌数科技2020社招)
  • 是否了解过express或者koa(掌数科技2020社招)
  • node如何实现热更新\u2029(橙智科技2020社招)
  • Node 知道哪些(express, sequelize,一面也问了)(微医云2020校招)
  • 用node做过什么(微医云2020校招)
  • Rest接口(微医云2020校招)
  • Linux指令
  • 测试工具什么什么
  • Linux指令
  • 测试工具什么什么
  • 浏览器和node环境的事件循环机制。(流利说2020校招)
  • https建立连接的过程。(流利说2020校招)
  • 讲述下http缓存机制,强缓存,协商缓存。(流利说2020校招)
  • jwt原理(北京蒸汽记忆2020校招)
  • 需求:实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后退时正确响应。给出你的技术实现方案?
  • 对Node的优点和缺点提出了自己的看法
  • nodejs的适用场景
  • (如果会用node)知道route, middleware, cluster, nodemon, pm2, server-side rendering么?
  • 解释一下 Backbone 的 MVC 实现方式?
  • 什么是“前端路由”?什么时候适合使用“前端路由”? “前端路由”有哪些优点和缺点?

操作系统:进程线程、死锁条件 Redis的底层数据结构 mongo的实务说一下, redis缓存机制 happypack 原理 【多进程打包 又讲了下进程和线程的区别】

  • koa 的原理 与express 的对比
  • 非js写的Node.js模块是如何使用Node.js调用的 【代码转换过程】 除了都会哪些后端语言 【一开始说自己用Node.js,面试官又问有没有其他的,楼主大学学过java,不过没怎么实践过】
  • Mysql的存储引擎 【这个直接不知道了 TAT】
  • 两个场景设计题感觉自己的设计方案还是有瑕疵的不是面试官想要的,并且问到mysql存储引擎直接无言以对了,感觉自己知识的广度还是有一定欠缺。最后问到性能优化正好自己之前优化过自己的博客网站做过相关的实践
  • koa了解吗

koa洋葱圈模型运行机制 express和koa区别 nginx原理 正向代理与反向代理 手写中间件测试请求时间

  • 进程和线程的区别

mongo的实务

node模块加载机制(require()原理)

路径转变,缓存

koa用的多吗

redis缓存机制是啥

8、 mysql如何创建索引 9、 mysql如何处理博客的点赞问题

完成prosimeFy babel 原理,class 是转换成什么 https://juejin.cn/post/6844904024928419848

javascript
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
javascript
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+

进程线程

  1. 页面缓存cache-control

  2. node写过东西吗

  3. node模块,path模块

  4. 对比一下commonjs模块化和es6 module

  5. 两个文件export的时候导出了相同的变量,避免重复--as

  6. exports和module.export的区别

  7. http和https

  8. 非对称加密和对称加密

  9. 打乱一个数组Math.random()的范围

  10. 问的特别全面,基础几乎全问

  11. 网站安全,CSRF,XSS,SQL注入攻击

  12. token是做什么的

node koa \\4. 由于简历写了了解Nodejs的底层原理。问了Node.js内存泄漏和如何定位内存泄漏 常见的内存场景:1. 闭包 2. http.globalAgent 开启keepAlive的时候,未清除事件监听 定位内存泄漏:1. heapdump 2. 用chromedev 生成三次内存快照 \\5. Node.js垃圾回收的原理 \\4. 客户端发请求给服务端发生了什么 \\6. 你知道MySQL和MongDB区别吗 \\7. 你平时怎么使用nodeJS的 \\8. restful风格规范是什么 新生代(from空间,to空间),老生代(标记清除,引用计数) NodeJs的EventLoop和V8的有什么区别 Node作为服务端语言,跟其他服务端语言比有什么特性?优势和劣势是什么? Mysql接触过?跨表查询外键索引(就知道个关键字所以没往下问了) 1、介绍一下项目的难点以及怎么解决的 5、移动端的业务有做过吗? 6、介绍一下你对中间件的理解7、怎么保证后端服务稳定性,怎么做容灾8、怎么让数据库查询更快9、数据库是用的什么?10、为什么用mysql1、如何用Node开启一个服务器 2、你的项目当中哪些地方用到了Node,为什么这个地方要用Node处理

  • 后端语言,涉及各种类型,主要都是TS,Java等,nodeJS作为动态语言,你怎么看?
  • 怎么理解后端,后端主要功能是做什么?
  • 校验器怎么做的?
  • 你对校验器做了什么封装?你用的这个库提供哪些功能?怎么实现两个关联参数校验?
  1. 进程与线程的概念
  2. 进程和线程的区别
  3. 浏览器渲染进程的线程有哪些
  4. 进程之前的通信方式
  5. 僵尸进程和孤儿进程是什么?
  6. 死锁产生的原因?如果解决死锁的问题?
  7. 如何实现浏览器内多个标签页之间的通信?
  8. 对Service Worker的理解

Mysql两种引擎

· 数据库表编码格式,UTF8和GBK区别

· 一个表里面姓名和学号两列,一行sql查询姓名重复的信息

· 防范XSS攻击

· 过滤或者编码哪些字符

  1. export和module.export区别
  2. Comonjs和es6 import区别

· Video标签可以播放的视频格式

  1. JWT
  2. 中间件有了解吗
  3. 进程和线程是什么
  4. nodejs的process的nexttick 为什么比微任务快
  5. 进程之间如何实现数据通信和数据同步
  6. node服务层如何封装接口
  7. 深挖底层原理 一直在拓展问

网易:谈谈同步10和异步10和它们的应用场景。https://zhuanlan.zhihu.com/p/115912936 队列解决:实现bfs返回树中值为value的节点 网易:

javascript
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+

你知道MySQL和MongDB区别吗

  1. · 如何防止sql注入

3 如何判断请求的资源已经全部返回。(从http角度,不要从api角度)

8 异步缓冲队列实现。例如需要发送十个请求,但最多只支持同时发送五个请求,需要等有请求完成后再进行剩余请求的发送。

2.用写websocket进度条以及使用场景

3.写回到上次浏览的位置(我说的记住位置存在sessionstorage/localstorage里),他问窗口大小变动怎么办,我说获取当前窗口大小等比例缩放scolltop。。。

6.正向代理,反向代理以及他们的应用场景

针对博客问了一下session,讲了cookie、session以及jwt,最后问如果不用cookie和localstorage,怎么做校验,比如匿名用户

你觉得你做过最牛逼的事情是什么

如果你有一天成了技术大牛,你最想做的事情是什么

介绍了下简历里各个项目的背景

react native ;node

博客技术文章怎么写的

monrepo技术栈,好处

node 中间件原理

发布订阅once加一个标识

是否能用于生产环境

父子组件执行顺序的原因

10w数据:虚拟滚动,bff(缓存,解决渲染问题)——新元

•如果你的技术方案和一起合作的同学不一致,你该怎么说服他? • 压力 •我觉得你说的这个方案不可行 • 稳定性 • 看到你家在深圳,计划长期在北京发展吗?

  • 个轮播组件的API • 案例问题 •假设我们现在面临着着会怎么排查?-个故障,部分用户反馈无法进入登录页面,你将 会怎么排查?

egg又不是不可替代的,一个bff而已,egg团队去开发artus了,Artus是更上层的框架,还是基于egg的

node里面for循环处理10亿数据,出现两个问题,js卡死,内存不够

为什么老是有题目处理101数据

我跟他讲:分开循环解决js卡死,把结果用node里面的fs.writefile写入本地文件

解決内存不够

bam就是浏览器上作为一个代理 但实际上mock数据还是在那个「API管理平台上的」

[]==![] 是true

[]== [] 是false

双等号 两边数据类型一样 如果是引用类型,就判断引用地址是否相同,两边数据类型不一样便会隐式转换。

有一点反直觉

如何在一个图片长列表页使用懒加载进行性能优化 ,然后说了图片替换和监听窗口滚动实现图片按需加载,那么如何实现监听呢?,然后扯到节流监听滚动条变化执行函数 输入框要求每次输入都要有联想功能,即随时向后台请求数据,但是频繁输入时,如何防止频繁多次请求 你对操作系统,编译原理的理解

  • 设计一个扫码登录的逻辑,为什么你扫码就可以登录你自己号,同时有这么多二维码
  • 讲清楚服务端,PC端,和手机端的逻辑
  • 怎么保存登陆态

项目权限管理(高低权限怎么区分,如果网络传输中途被人改包,怎么防止这一潜在危险) 在淘宝里面,商品数据量很大,前端怎么优化使加载速度更快、用户体验更好(severless + indexdb)  \\8. 在7的条件下,已知用户当前在商品列表页,并且下一步操作是点击某个商品进入详情页,怎么加快详情页的渲染速度

npm安装和查找机制

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装(不需要网络)
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

npm 缓存相关命令

shell
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
`,323),o=[e];function r(c,t,i,B,y,F){return n(),a("div",null,o)}const b=s(p,[["render",r]]);export{u as __pageData,b as default}; diff --git a/assets/front-end-engineering_engineering-onepage.md.c6fbbc99.lean.js b/assets/front-end-engineering_engineering-onepage.md.c6fbbc99.lean.js new file mode 100644 index 00000000..a920f2af --- /dev/null +++ b/assets/front-end-engineering_engineering-onepage.md.c6fbbc99.lean.js @@ -0,0 +1,463 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"前端工程化 OnePage","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么会出现前端工程化","slug":"为什么会出现前端工程化","link":"#为什么会出现前端工程化","children":[]},{"level":2,"title":"nodejs对CommonJS的实现","slug":"nodejs对commonjs的实现","link":"#nodejs对commonjs的实现","children":[]},{"level":2,"title":"nodejs","slug":"nodejs","link":"#nodejs","children":[]},{"level":2,"title":"CommonJS标准和使用","slug":"commonjs标准和使用","link":"#commonjs标准和使用","children":[]},{"level":2,"title":"下面的代码执行结果是什么?","slug":"下面的代码执行结果是什么","link":"#下面的代码执行结果是什么","children":[]},{"level":2,"title":"ES6 module","slug":"es6-module","link":"#es6-module","children":[]},{"level":2,"title":"请对比一下CommonJS和ES Module","slug":"请对比一下commonjs和es-module","link":"#请对比一下commonjs和es-module","children":[]},{"level":2,"title":"对前端工程化,模块化,组件化的理解?","slug":"对前端工程化-模块化-组件化的理解","link":"#对前端工程化-模块化-组件化的理解","children":[]},{"level":2,"title":"webpack 中的 loader 属性和 plugins 属性的区别是什么?","slug":"webpack-中的-loader-属性和-plugins-属性的区别是什么","link":"#webpack-中的-loader-属性和-plugins-属性的区别是什么","children":[]},{"level":2,"title":"webpack 的核心概念都有哪些?","slug":"webpack-的核心概念都有哪些","link":"#webpack-的核心概念都有哪些","children":[]},{"level":2,"title":"ES6 中如何实现模块化的异步加载?","slug":"es6-中如何实现模块化的异步加载","link":"#es6-中如何实现模块化的异步加载","children":[]},{"level":2,"title":"说一下 webpack 中的几种 hash 的实现原理是什么?","slug":"说一下-webpack-中的几种-hash-的实现原理是什么","link":"#说一下-webpack-中的几种-hash-的实现原理是什么","children":[]},{"level":2,"title":"webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?","slug":"webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","link":"#webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","children":[]},{"level":2,"title":"webpack 中是如何处理图片的?","slug":"webpack-中是如何处理图片的","link":"#webpack-中是如何处理图片的","children":[]},{"level":2,"title":"webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?","slug":"webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","link":"#webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","children":[]},{"level":2,"title":"webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?","slug":"webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","link":"#webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","children":[]},{"level":2,"title":"介绍一下 webpack4 中的 tree-shaking 的工作流程?","slug":"介绍一下-webpack4-中的-tree-shaking-的工作流程","link":"#介绍一下-webpack4-中的-tree-shaking-的工作流程","children":[]},{"level":2,"title":"说一下 webpack loader 的作用是什么?","slug":"说一下-webpack-loader-的作用是什么","link":"#说一下-webpack-loader-的作用是什么","children":[]},{"level":2,"title":"在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?","slug":"在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","link":"#在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","children":[]},{"level":2,"title":"export 和 export default 的区别是什么?","slug":"export-和-export-default-的区别是什么","link":"#export-和-export-default-的区别是什么","children":[]},{"level":2,"title":"webpack 打包原理是什么?","slug":"webpack-打包原理是什么","link":"#webpack-打包原理是什么","children":[]},{"level":2,"title":"webpack 热更新原理是什么?","slug":"webpack-热更新原理是什么","link":"#webpack-热更新原理是什么","children":[]},{"level":2,"title":"如何优化 webpack 的打包速度?","slug":"如何优化-webpack-的打包速度","link":"#如何优化-webpack-的打包速度","children":[]},{"level":2,"title":"webpack 如何实现动态导入?","slug":"webpack-如何实现动态导入","link":"#webpack-如何实现动态导入","children":[]},{"level":2,"title":"说一下 webpack 有哪几种文件指纹","slug":"说一下-webpack-有哪几种文件指纹","link":"#说一下-webpack-有哪几种文件指纹","children":[]},{"level":2,"title":"常用的 webpack Loader 都有哪些?","slug":"常用的-webpack-loader-都有哪些","link":"#常用的-webpack-loader-都有哪些","children":[]},{"level":2,"title":"说一下 webpack 常用插件都有哪些?","slug":"说一下-webpack-常用插件都有哪些","link":"#说一下-webpack-常用插件都有哪些","children":[]},{"level":2,"title":"使用 babel-loader 会有哪些问题,可以怎样优化?","slug":"使用-babel-loader-会有哪些问题-可以怎样优化","link":"#使用-babel-loader-会有哪些问题-可以怎样优化","children":[]},{"level":2,"title":"babel 是如何对 class 进行编译的?","slug":"babel-是如何对-class-进行编译的","link":"#babel-是如何对-class-进行编译的","children":[]},{"level":2,"title":"释一下 babel-polyfill 的作用是什么?","slug":"释一下-babel-polyfill-的作用是什么","link":"#释一下-babel-polyfill-的作用是什么","children":[]},{"level":2,"title":"解释一下 less 的&的操作符是做什么用的?","slug":"解释一下-less-的-的操作符是做什么用的","link":"#解释一下-less-的-的操作符是做什么用的","children":[]},{"level":2,"title":"webpack proxy 工作原理,为什么能解决跨域?","slug":"webpack-proxy-工作原理-为什么能解决跨域","link":"#webpack-proxy-工作原理-为什么能解决跨域","children":[]},{"level":2,"title":"组件发布的是不是所有依赖这个组件库的项目都需要升级?","slug":"组件发布的是不是所有依赖这个组件库的项目都需要升级","link":"#组件发布的是不是所有依赖这个组件库的项目都需要升级","children":[]},{"level":2,"title":"具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)","slug":"具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","link":"#具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","children":[]},{"level":2,"title":"描述一下 webpack 的构建流程?(CVTE)","slug":"描述一下-webpack-的构建流程-cvte","link":"#描述一下-webpack-的构建流程-cvte","children":[]},{"level":2,"title":"解释一下 webpack 插件的实现原理?(CVTE)","slug":"解释一下-webpack-插件的实现原理-cvte","link":"#解释一下-webpack-插件的实现原理-cvte","children":[]},{"level":2,"title":"有用过哪些插件做项目的分析吗?(CVTE)","slug":"有用过哪些插件做项目的分析吗-cvte","link":"#有用过哪些插件做项目的分析吗-cvte","children":[]},{"level":2,"title":"什么是 babel,有什么作用?","slug":"什么是-babel-有什么作用","link":"#什么是-babel-有什么作用","children":[]},{"level":2,"title":"解释一下 npm 模块安装机制是什么?","slug":"解释一下-npm-模块安装机制是什么","link":"#解释一下-npm-模块安装机制是什么","children":[]},{"level":2,"title":"webpack与grunt、gulp的不同?","slug":"webpack与grunt、gulp的不同","link":"#webpack与grunt、gulp的不同","children":[]},{"level":2,"title":"2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?","slug":"_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","link":"#_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","children":[]},{"level":2,"title":"3.有哪些常见的Loader?他们是解决什么问题的?","slug":"_3-有哪些常见的loader-他们是解决什么问题的","link":"#_3-有哪些常见的loader-他们是解决什么问题的","children":[]},{"level":2,"title":"4.有哪些常见的Plugin?他们是解决什么问题的?","slug":"_4-有哪些常见的plugin-他们是解决什么问题的","link":"#_4-有哪些常见的plugin-他们是解决什么问题的","children":[]},{"level":2,"title":"5.Loader和Plugin的不同?","slug":"_5-loader和plugin的不同","link":"#_5-loader和plugin的不同","children":[]},{"level":2,"title":"6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全","slug":"_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","link":"#_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","children":[]},{"level":2,"title":"7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?","slug":"_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","link":"#_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","children":[]},{"level":2,"title":"11.怎么配置单页应用?怎么配置多页应用?","slug":"_11-怎么配置单页应用-怎么配置多页应用","link":"#_11-怎么配置单页应用-怎么配置多页应用","children":[]},{"level":2,"title":"12.npm打包时需要注意哪些?如何利用webpack来更好的构建?","slug":"_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","link":"#_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","children":[]},{"level":2,"title":"13.如何在vue项目中实现按需加载?","slug":"_13-如何在vue项目中实现按需加载","link":"#_13-如何在vue项目中实现按需加载","children":[]},{"level":2,"title":"能说说webpack的作用吗?","slug":"能说说webpack的作用吗","link":"#能说说webpack的作用吗","children":[]},{"level":2,"title":"为什么要打包呢?","slug":"为什么要打包呢","link":"#为什么要打包呢","children":[]},{"level":2,"title":"能说说模块化的好处吗?","slug":"能说说模块化的好处吗","link":"#能说说模块化的好处吗","children":[]},{"level":2,"title":"模块化打包方案知道啥,可以说说吗?","slug":"模块化打包方案知道啥-可以说说吗","link":"#模块化打包方案知道啥-可以说说吗","children":[]},{"level":2,"title":"那ES6 模块与 CommonJS 模块的差异有哪些呢?","slug":"那es6-模块与-commonjs-模块的差异有哪些呢","link":"#那es6-模块与-commonjs-模块的差异有哪些呢","children":[]},{"level":2,"title":"webpack的编译(打包)流程说说","slug":"webpack的编译-打包-流程说说","link":"#webpack的编译-打包-流程说说","children":[]},{"level":2,"title":"说一下 Webpack 的热更新原理吧?","slug":"说一下-webpack-的热更新原理吧","link":"#说一下-webpack-的热更新原理吧","children":[]},{"level":2,"title":"路由懒加载的原理","slug":"路由懒加载的原理","link":"#路由懒加载的原理","children":[]},{"level":2,"title":"路由懒加载?","slug":"路由懒加载","link":"#路由懒加载","children":[]},{"level":2,"title":"git 指令","slug":"git-指令","link":"#git-指令","children":[]},{"level":2,"title":"知道git的原理吗?说说他的原理吧","slug":"知道git的原理吗-说说他的原理吧","link":"#知道git的原理吗-说说他的原理吧","children":[]},{"level":2,"title":"package.json 字段","slug":"package-json-字段","link":"#package-json-字段","children":[]},{"level":2,"title":"npm安装和查找机制","slug":"npm安装和查找机制","link":"#npm安装和查找机制","children":[]}],"relativePath":"front-end-engineering/engineering-onepage.md","lastUpdated":1718508254000}'),p={name:"front-end-engineering/engineering-onepage.md"},e=l(`

前端工程化 OnePage

为什么会出现前端工程化

模块化:意味着 JS 总算可以开发大型项目(解决 JS 的复用问题)

组件化:解决 HTML、CSS、JS 的复用问题

工程化:前端项目越来越复杂、前端项目模块(组件)越来越多

为什么前端项目越来越复杂?

在书写前端项目的时候,可能会涉及到使用其他的语言,typescript、less/sass、coffeescript,涉及到编译

在部署的时候,还需要对代码进行丑化、压缩

代码优化:为 CSS 代码添加兼容性前缀

前端项目模块(组件)越来越多?

专门有一个 node_modules 来管理这些可以复用的代码。

前端工程化的出现,归根结底,其实就是要解决开发环境和生产环境不一致的问题。

nodejs对CommonJS的实现

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

nodejs中导入模块,使用相对路径,并且必须以./或../开头,浏览器可以省略./,nodejs不行

  1. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
javascript
 (function(){
+     //模块中的代码
+ })()
+
 (function(){
+     //模块中的代码
+ })()
+
  1. 为了保证顺利的导出模块内容,nodejs做了以下处理
    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
javascript
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+

面试

javascript
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+

经典面试题 util.js

javascript
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+

index.js

javascript
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+

经验:对exports赋值无意义,建议用module.exports

  1. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

过去,JS很难编写大型应用,因为有以下两个问题:

  1. 全局变量污染
  2. 难以管理的依赖关系

这些问题,都导致了JS无法进行精细的模块划分,因为精细的模块划分会导致更多的全局污染以及更加复杂的依赖关系 于是,先后出现了两大模块化标准,用于解决以上两个问题:

  • CommonJS
  • ES6 Module

注意:上面提到的两个均是模块化标准,具体的实现需要依托于JS的宿主环境

nodejs

node环境支持 CommonJS 模块化标准,所以,要使用 CommonJS,必须要先安装node

官网地址:https://nodejs.org/zh-cn/

nodejs直接运行某个js文件,该文件被称之为入口文件

nodejs遵循EcmaScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在nodejs中使用EcmaScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在nodejs中使用浏览器的 web api,例如:dom对象、window对象、document对象等

CommonJS标准和使用

node中的所有代码均在CommonJS规范下运行 具体规范如下:

  1. 一个JS文件即为一个模块,一个模块就是一个相对独立的功能,模块中的所有全局代码产生的变量、函数,均不会对全局造成任何污染,仅在模块内使用
  2. 如果一个模块需要暴露一些数据或功能供其他模块使用,需要使用代码module.exports = xxx,该过程称之为模块的导出
  3. 如果一个模块需要使用另一个模块导出的内容,需要使用代码require("模块路径")
    1. 路径必须以./../开头
    2. 如果模块文件后缀名为.js,可以省略后缀名
    3. require函数返回的是模块导出的内容
  4. 模块具有缓存,第一次导入模块时会缓存模块的导出,之后再导入同一个模块,直接使用之前缓存的结果。

同时也解决了JS的两个问题 浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module

原理: require中的伪代码

javascript
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
  1. 根据传递的模块路径,得到模块完整的绝对路径。因为绝对路径不会重复。

  2. require引入相同模块会有缓存,不会重复加载

  3. 如果没有缓存。真正运行模块代码的辅助函数

javascript
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
  1. 返回 module.exports

this === exports === module.exports === {}

下面的代码执行结果是什么?

javascript
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+

ES6 module

由于种种原因,CommonJS标准难以在浏览器中实现,因此一直在浏览器端一直没有合适的模块化标准,直到ES6标准出现 ES6规范了浏览器的模块化标准,一经发布,各大浏览器厂商纷纷在自己的浏览器中实现了该规范

模块的引入:浏览器使用以下方式引入一个ES6模块文件

html
<script src="JS文件" type="module">
+
<script src="JS文件" type="module">
+
  1. 模块的导出分为两种,基本导出(具名导出)默认导出基本导出导出的是该对象的某个属性,默认导出导出的是该对象的特殊属性default
javascript
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
  1. 基本导出可以有多个,默认导出只能有一个
  2. 基本导出必须要有名字,默认导出由于有特殊名字,所以可以不用写名字
javascript
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
  1. 模块的导入
javascript
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+

注意

  1. ES6 module 采用依赖预加载模式,所有模块导入代码均会提升到代码顶部
  2. 不能将导入代码放置到判断、循环中
  3. 导入的内容放置到常量中,不可更改
  4. ES6 module 使用了缓存,保证每个模块仅加载一次

请对比一下CommonJS和ES Module

  1. CMJ 是社区标准,ESM 是官方标准
  2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
  3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
  4. CMJ 是动态的依赖,ESM 既支持动态,也支持静态
  5. ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值

CommonJS是社区模块化标准,node环境支持该标准。它不产生新的语法,是使用API实现的,本质是把要加载的模块放到一个闭包中执行(这里可以详细的阐述)。因此,node环境中的所有JS文件在执行的时候,都是放到一个函数环境中执行的,这对使得模块内部可以访问函数的参数,如exports、module、__dirname等,而且对this的指向也会有影响。CommonJS是动态的模块化标准,这就意味着依赖关系是在运行过程中确定的,同时也意味着在导入导出模块时,并不限制书写的位置。 ES Module是官方模块化标准,目前node和浏览器均支持该标准,它引入了新的语法。ES Module是静态的模块化标准,在模块执行前,会递归确定所有依赖关系,然后加载所有文件,加载完成后再运行,这在浏览器环境下会产生多次请求。由于使用的是静态依赖,因此,它要求导入导出的代码必须放置到顶层,因为只有这样才能在代码运行前就确定依赖关系。ES Module导入模块时会将导入的结果绑定到标识符中,该标识符是一个常量,不可更改。 CommonJS和ES Module都使用了缓存,保证每个模块仅执行一次。

1.讲讲模块化规范

2.import和require的区别

3.require是如何解析路径的

1、ESM是编译时导出结果,CommonJS是运⾏时导出结果

2、ESM是导出引⽤,CommonJS是导出⼀个值

3、ESM⽀持异步,CommonJS只⽀持同步

下面的模块导出了什么结果?

javascript
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+

对前端工程化,模块化,组件化的理解?

这三者中,模块化是基础,没有模块化,就没有组件化和工程化

模块化的出现,解决了困扰前端的两大难题:全局污染问题和依赖混乱问题,从而让精细的拆分前端工程成为了可能。

工程化的出现,解决了前端开发环境和生产环境要求不一致的矛盾。在开发环境中,我们希望代码使用尽可能的细分,代码格式尽可能的统一和规范,而在生产环境中,我们希望代码尽可能的被压缩、混淆,尽可能的优化体积。工程化的出现,就是为了解决这一矛盾,它可以让我们舒服的在开发环境中书写代码,然后经过打包,生成最合适的生产环境代码,这样就解放了开发者的精力,让开发者把更多的注意力集中在开发环境上即可。

组件化开发是一些前端框架带来的概念,它把一个网页,或者一个站点,甚至一个完整的产品线,划分为多个小的组件,组件是一个可以复用的单元,它包含了一个某个区域的完整功能。这样一来,前端便具备了开发复杂应用的能力。

webpack 中的 loader 属性和 plugins 属性的区别是什么?

它们都是 webpack 功能的扩展点。

loader 是加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等

plugins 是插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等

webpack 的核心概念都有哪些?

参考答案:

  • loader 加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
  • plugin 插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
  • module 模块。webpack 将所有依赖均视为模块,无论是 js、css、html、图片,统统都是模块
  • entry 入口。打包过程中的概念,webpack 以一个或多个文件作为入口点,分析整个依赖关系。
  • chunk 打包过程中的概念,一个 chunk 是一个相对独立的打包过程,以一个或多个文件为入口,分析整个依赖关系,最终完成打包合并
  • bundle webpack 打包结果
  • tree shaking 树摇优化。在打包结果中,去掉没有用到的代码。
  • HMR 热更新。是指在运行期间,遇到代码更改后,无须重启整个项目,只更新变动的那一部分代码。
  • dev server 开发服务器。在开发环境中搭建的临时服务器,用于承载对打包结果的访问

ES6 中如何实现模块化的异步加载?

使用动态导入即可,导入后,得到的是一个 Promise,完成后,得到一个模块对象,其中包含了所有的导出结果。

说一下 webpack 中的几种 hash 的实现原理是什么?

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?

参考答案:

不会。它跟关联的内容是否有变化有关系,如果没有变化,hash 就不会变。具体来说,contenthash 和具体的打包文件内容有关,chunkhash 和某一 entry 为起点的打包过程中涉及的内容有关,hash 和整个工程所有模块内容有关。

webpack 中是如何处理图片的?

参考答案:

webpack 本身不处理图片,它会把图片内容仍然当做 JS 代码来解析,结果就是报错,打包失败。如果要处理图片,需要通过 loader 来处理。其中,url-loader 会把图片转换为 base64 编码,然后得到一个 dataurl,file-loader 则会将图片生成到打包目录中,然后得到一个资源路径。但无论是哪一种 loader,它们的核心功能,都是把图片内容转换成 JS 代码,因为只有转换成 JS 代码,webpack 才能识别。

webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?

说明:这道题的表述是有问题的,webpack 本身并不打包 html,相反,它如果遇到 html 代码会直接打包失败,因为 webpack 本身只能识别 JS。之所以能够打包出 html 文件,是因为插件或 loader 的作用,其中,比较常见的插件是 html-webpack-plugin。所以这道题的正确表述应该是:「html-webpack-plugin 打包出来的 html 为什么 style 放在头部 script 放在底部?」

webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?

要配置 CDN,有两个步骤:

  1. 在 html 模板中直接加入 cdn 引用
  2. 在 webpack 配置中,加入externals配置,告诉 webpack 不要打包其中的模块,转而使用全局变量

若要在开发环境中不使用 CDN,只需根据环境变量判断不同的环境,进行不同的打包处理即可。

  1. 在 html 模板中使用 ejs 模板语法进行判断,只有在生产环境中引入 CDN
  2. 在 webpack 配置中,可以根据process.env中的环境变量进行判断是否使用externals配置
  3. package.json脚本中设置不同的环境变量完成打包或开发启动。

介绍一下 webpack4 中的 tree-shaking 的工作流程?

推荐阅读:https://tsejx.github.io/webpack-guidebook/principle-analysis/operational-principle/tree-shaking

说一下 webpack loader 的作用是什么?

参考答案:

用于转换代码。有时是因为 webpack 无法识别某些内容,比如图片、css 等,需要由 loader 将其转换为 JS 代码。有时是因为某些代码需要被特殊处理,比如 JS 兼容性的处理,需要由 loader 将其进一步转换。不管是什么情况,loader 的作用只有一个,就是转换代码。

在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

export 和 export default 的区别是什么?

参考答案:

export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,比如变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出

export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出。

webpack 打包原理是什么?

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack 热更新原理是什么?

参考答案:

当开启热更新后,页面中会植入一段 websocket 脚本,同时,开发服务器也会和客户端建立 websocket 通信,当源码发生变动时,webpack 会进行以下处理:

  1. webpack 重新打包
  2. webpack-dev-server 检测到模块的变化,于是通过 webscoket 告知客户端变化已经发生
  3. 客户端收到消息后,通过 ajax 发送请求到开发服务器,以过去打包的 hash 值请求服务器的一个 json 文件
  4. 服务器告诉客户端哪些模块发生了变动,同时告诉客户端这次打包产生的新 hash 值
  5. 客户端再次用过去的 hash 值,以 JSONP 的方式请求变动的模块
  6. 服务器响应一个函数调用,用于更新模块的代码
  7. 此时,模块代码已经完成更新。客户端按照之前的监听配置,执行相应模块变动后的回调函数。

如何优化 webpack 的打包速度?

参考答案:

  1. noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  2. externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  3. 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  4. 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  5. 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  6. 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库

webpack 如何实现动态导入?

参考答案:

当遇到代码中包含动态导入语句时,webpack 会将导入的模块及其依赖分配到单独的一个 chunk 中进行打包,形成单独的打包结果。而动态导入的语句会被编译成一个普通的函数调用,该函数在执行时,会使用 JSONP 的方式动态的把分离出去的包加载到模块集合中。

说一下 webpack 有哪几种文件指纹

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

常用的 webpack Loader 都有哪些?

参考答案:

  • cache-loader:启用编译缓存
  • thread-loader:启用多线程编译
  • css-loader:编译 css 代码为 js
  • file-loader:保存文件到输出目录,将文件内容转换成文件路径
  • postcss-loader:将 css 代码使用 postcss 进行编译
  • url-loader:将文件内容转换成 dataurl
  • less-loader:将 less 代码转换成 css 代码
  • sass-loader:将 sass 代码转换成 css 代码
  • vue-loader:编译单文件组件
  • babel-loader:对 JS 代码进行降级处理

说一下 webpack 常用插件都有哪些?

参考答案:

  • clean-webpack-plugin:清除输出目录
  • copy-webpack-plugin:复制文件到输出目录
  • html-webpack-plugin:生成 HTML 文件
  • mini-css-extract-plugin:将 css 打包成单独文件的插件
  • HotModuleReplacementPlugin:热更新的插件
  • purifycss-webpack:去除无用的 css 代码
  • optimize-css-assets-webpack-plugin:优化 css 打包体积
  • uglify-js-plugin:对 JS 代码进行压缩、混淆
  • compression-webpack-plugin:gzip 压缩
  • webpack-bundle-analyzer:分析打包结果

使用 babel-loader 会有哪些问题,可以怎样优化?

参考答案:

  1. 如果不做特殊处理,babel-loader 会对所有匹配的模块进行降级,这对于那些已经处理好兼容性问题的第三方库显得多此一举,因此可以使用 exclude 配置排除掉这些第三方库
  2. 在旧版本的 babel-loader 中,默认开启了对 ESM 的转换,这样会导致 webpack 的 tree shaking 失效,因为 tree shaking 是需要保留 ESM 语法的,所以需要关闭 babel-loader 的 ESM 转换,在其新版本中已经默认关闭了。

babel 是如何对 class 进行编译的?

参考答案:

本质上就是把 class 语法转换成普通构造函数定义,并做了以下处理:

  1. 增加了对 this 指向的检测
  2. 将原型方法和静态方法变为不可枚举
  3. 将整个代码放到了立即执行函数中,运行后返回构造函数本身

释一下 babel-polyfill 的作用是什么?

说明:

babel-polyfill 已经是一个非常古老的项目了,babel 从 7.4 版本开始已不再支持它,转而使用更加强大的 core-js,此题也适用于问「core-js 的作用是什么」

解释一下 less 的&的操作符是做什么用的?

参考答案:

&符号后面的内容会和父级选择器合并书写,即中间不加入空格字符

webpack proxy 工作原理,为什么能解决跨域?

说明:

严格来说,webpack 只是一个打包工具,它并没有 proxy 的功能,甚至连服务器的功能都没有。之所以能够在 webpack 中使用 proxy 配置,是因为它的一个插件,即 webpack-dev-server 的能力。

所以,此题应该问做:「webpack-dev-server 工作原理,为什么能解决跨域?」

组件发布的是不是所有依赖这个组件库的项目都需要升级?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)

  1. 公共模块 比如某些多页应用会有多个入口,从而形成多个 chunk,而这些 chunk 中用到了一些公共模块,为了减少整体的包体积,可以使用 splitchunksplugin 将公共模块分离出来。 可以配置 minChunks 来指定被多少个 chunk 引用时进行分包
  2. 并行下载 由于 HTML5 支持 defer 和 async,因此可以同时下载多个 JS 文件以充分利用带宽。如果打包结果是一个很大的文件,就无法利用到这一点。 可以利用 splitchunks 插件将文件进行拆分,通过配置 maxSize 属性指定包体积达到多大时进行拆分

描述一下 webpack 的构建流程?(CVTE)

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

解释一下 webpack 插件的实现原理?(CVTE)

参考答案:

本质上,webpack 的插件是一个带有apply函数的对象。当 webpack 创建好 compiler 对象后,会执行注册插件的 apply 函数,同时将 compiler 对象作为参数传入。

在 apply 函数中,开发者可以通过 compiler 对象监听多个钩子函数的执行,不同的钩子函数对应 webpack 编译的不同阶段。当 webpack 进行到一定阶段后,会调用这些监听函数,同时将 compilation 对象传入。开发者可以使用 compilation 对象获取和改变 webpack 的各种信息,从而影响构建过程。

有用过哪些插件做项目的分析吗?(CVTE)

参考答案:

用过 webpack-bundle-analyzer 分析过打包结果,主要用于优化项目打包体积

什么是 babel,有什么作用?

参考答案:

babel 是一个 JS 编译器,主要用于将下一代的 JS 语言代码编译成兼容性更好的代码。

它其实本身做的事情并不多,它负责将 JS 代码编译成为 AST,然后依托其生态中的各种插件对 AST 中的语法和 API 进行处理

解释一下 npm 模块安装机制是什么?

参考答案:

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。 gruntgulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。 webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。 所以总结一下:

  • 从构建思路来说

gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

  • 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

3.有哪些常见的Loader?他们是解决什么问题的?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

4.有哪些常见的Plugin?他们是解决什么问题的?

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

5.Loader和Plugin的不同?

不同的作用

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。
  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。


7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。 编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。 相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

11.怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可,这里不再赘述 多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

12.npm打包时需要注意哪些?如何利用webpack来更好的构建?

Npm是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建。 NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

  1. CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
  2. 输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
  3. Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
  4. 不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
  5. 对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:
javascript
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+

13.如何在vue项目中实现按需加载?

Vue UI组件库的按需加载 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。 不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了。

javascript
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+

单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。 通过import()语句来控制加载时机,webpack内置了对于import()的解析,会将import()中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import()语句时,会去加载Chunk对应生成的文件。import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill

能说说webpack的作用吗?

  • 模块打包(静态资源拓展)。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容(翻译官loader)。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展(plugins)。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

为什么要打包呢?

逻辑多、文件多、项目的复杂度高了,所以要打包 例如: 让前端代码具有校验能力===>出现了ts css不好用===>出现了sass、less webpack可以解决这些问题

能说说模块化的好处吗?

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化打包方案知道啥,可以说说吗?

1、commonjs commonjs 是 Node 中的模块规范,通过 require 及 exports 进行导入导出 commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。 总结:commonjs是用在服务器端的,同步的,如nodejs2、AMD AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。 总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs3、CMD CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs4、esm esm 是 tc39 对于 ESMAScript 的模块话规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。 它使用 import/export 进行模块导入导出. 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。 export default命令,为模块指定默认输出

那ES6 模块与 CommonJS 模块的差异有哪些呢?

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

webpack的编译(打包)流程说说

  • 初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;
  • 开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
  • 确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
  • 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;
  • 输出完成:输出所有的chunk到文件系统;

说一下 Webpack 的热更新原理吧?

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为** HMR**。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS(无线路由)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

路由懒加载的原理

https://juejin.cn/post/6844904180285456398

路由懒加载?

当刚运行项目的时候,发现刚进入页面,就将所有的js文件和css文件加载了进来,这一进程十分的消耗时间。 如果打开哪个页面就对应的加载响应页面的js文件和css文件,那么页面加载速度会大大提升。

懒加载的好处是什么?

懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。

怎么使用的路由懒加载?

使用到的是es6的import语法,可以实现动态导入

路由懒加载的原理是什么

通过Webpack编译打包后,会把每个路由组件的代码分割成一一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。

作用就是webpack在打包的时候,对异步引入的库代码进行代码分割时(需要配置webpack的SplitChunkPlugin插件),为分割后的代码块取得名字 Vue中运用import的懒加载语句以及webpack的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。

npx

  1. 运行本地命令

使用npx 命令时,它会首先从本地工程的node_modules/.bin目录中寻找是否有对应的命令

例如:

shell
npx webpack
+
npx webpack
+

上面这条命令寻找本地工程的node_modules/.bin/webpack

如果将命令配置到package.jsonscripts中,可以省略npx

  1. 临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除

例如:

shell
npx prettyjson 1.json
+
npx prettyjson 1.json
+

npx会下载prettyjson包到临时目录,然后运行该命令

如果命令名称和需要下载的包名不一致时,可以手动指定报名

例如@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令

shell
npx -p @vue/cli vue create vue-app
+
npx -p @vue/cli vue create vue-app
+
  1. npm init

npm init通常用于初始化工程的package.json文件

除此之外,有时也可以充当npx的作用

shell
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+

git 指令

git fetch git fetch origin 分支 git rebase FETCH_HEAD 本地master更新到最新:git fetch origin master; git rebase FETCH_HEAD

git 和 svn 的区别 经常使用的 git 命令? git pull 和 git fetch 的区别 git rebase 和 git merge 的区别 git rebase和merge git分支管理、分支开发 git回退操作 git reset 和 git reverse区别?你还用过什么别的指令? git遇到冲突怎么进行解决

git-flow git reset --hard 版本号 git 如何合并分支; git 中,提交了 a, 然后又提交了 b, 如何撤回 b ? git merge、git rebase的区别 假设 master 分支切出了 test 分支,供所有人合代码测试, 然后我们 从稳定的 master 切出 一个 feature 分支进行开发, 现在 我们开发完了 feature 分支并将其merge 到 test 分支进行测试, 测试过程中出现一些问题,由于疏忽你;忘记切回 feature ,而是直接在 test 分支进行了开发 导致: test 分支优先于你的featuce 分支,此时你应该怎么特 test 分支的修改带入 feature

知道git的原理吗?说说他的原理吧

Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作 Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中那些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。 Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。 Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库

package.json 字段

name npm 包的名字,必须是一个小写的单词,可以包含连字符-和下划线_,发布时必填。

version npm 包的版本。需要遵循语义化版本格式x.x.x。

description npm 包的描述。

keywords npm 包的关键字。

homepage npm 包的主页地址。

bugs npm 包问题反馈的地址。

license 为 npm 包指定许可证

author npm 包的作者

files npm 包作为依赖安装时要包括的文件,格式是文件正则的数组。也可以使用 npmignore 来忽略个别文件。 以下文件总是被包含的,与配置无关 package.json README.md CHANGES / CHANGELOG / HISTORY LICENCE / LICENSE 以下文件总是被忽略的,与配置无关 .git .DS_Store node_modules .npmrc npm-debug.log package-lock.json ...

main 指定npm包的入口文件。

module 指定 ES 模块的入口文件。

typings 指定类型声明文件。

bin 开发可执行文件时,bin 字段可以帮助你设置链接,不需要手动设置 PATH。

repository npm 包托管的地方

scripts 可执行的命令。

dependencies npm包所依赖的其他npm包

devDependencies npm 包所依赖的构建和测试相关的 npm 包.

peerDependencies 指定 npm 包与主 npm 包的兼容性,当开发插件时是需要的.

engines 指定 npm 包可以使用的 Node 版本.

  • CSRF: 文件流steam(字节2020校招)
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
  • 安全相关:XSS,CSRF(字节2020校招)
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
  • 数据劫持(字节2020校招)
  • CSP,阻止iframe的嵌入(字节2020校招)
  • 进程和线程的区别知道吗?他们的通信方式有哪些?(字节2020校招)
  • js为什么是单线程(字节2020校招 + 京东2020校招)
  • section是用来做什么的(字节2020社招)
  • 了解SSO吗(字节2020社招)
  • 服务端渲染的方案有哪些?next.js和nuxt.js的区别(字节2020社招)
  • cdn的原理(字节2020社招)
  • JSBridge的原理(字节2020社招)
  • 了解过Serverless,什么是Serverless(字节2020社招)
无服务器~
+云计算?
+
无服务器~
+云计算?
+
  • 你还知道哪些新的前端技术(微前端、Low Code、可视化、BI、BFF)(字节2020社招)
  • 权限系统设计模型(DAC、MAC、RBAC、ABAC)(字节2020社招)
  • 什么是微前端(字节2020社招)
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
  • Node了解多少,有实际的项目经验吗(字节2020社招)
  • Linux了解多少,linux上查看日志的命令是什么(字节2020社招)
  • 强缓存和协商缓存(腾讯2020校招)
  • node可以做大型服务器吗,为什么(腾讯2020校招)
  • 用express还是koa(腾讯2020校招)
  • 用node写过哪些接口,登录验证是怎么做的(腾讯2020实习)
  • 清理node模块缓存的方法(腾讯2020校招)
  • 模仿一个xss的场景(从攻击到防御的流程)(腾讯2020校招)
  • Event loop知道吗?Node的eventloop的生命周期(京东2020校招)
  • JWT单点登录实现原理(小米2020校招)
  • 博客项目为什么使用Mysql?(小米2020校招)
  • 如何看待Node.js的中间件?它的好处是什么?(小米2020校招)
  • 为什么Node.js支持高并发请求?(小米2020校招)
  • 博客项目是SSR渲染还是客户端渲染?(小米2020校招)
  • web socket 和 socket io(广州长视科技2020校招)
  • 是否使⽤过webpack(不是通过vuecli的⽅式)(掌数科技2020社招)
  • 是否了解过express或者koa(掌数科技2020社招)
  • node如何实现热更新\u2029(橙智科技2020社招)
  • Node 知道哪些(express, sequelize,一面也问了)(微医云2020校招)
  • 用node做过什么(微医云2020校招)
  • Rest接口(微医云2020校招)
  • Linux指令
  • 测试工具什么什么
  • Linux指令
  • 测试工具什么什么
  • 浏览器和node环境的事件循环机制。(流利说2020校招)
  • https建立连接的过程。(流利说2020校招)
  • 讲述下http缓存机制,强缓存,协商缓存。(流利说2020校招)
  • jwt原理(北京蒸汽记忆2020校招)
  • 需求:实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后退时正确响应。给出你的技术实现方案?
  • 对Node的优点和缺点提出了自己的看法
  • nodejs的适用场景
  • (如果会用node)知道route, middleware, cluster, nodemon, pm2, server-side rendering么?
  • 解释一下 Backbone 的 MVC 实现方式?
  • 什么是“前端路由”?什么时候适合使用“前端路由”? “前端路由”有哪些优点和缺点?

操作系统:进程线程、死锁条件 Redis的底层数据结构 mongo的实务说一下, redis缓存机制 happypack 原理 【多进程打包 又讲了下进程和线程的区别】

  • koa 的原理 与express 的对比
  • 非js写的Node.js模块是如何使用Node.js调用的 【代码转换过程】 除了都会哪些后端语言 【一开始说自己用Node.js,面试官又问有没有其他的,楼主大学学过java,不过没怎么实践过】
  • Mysql的存储引擎 【这个直接不知道了 TAT】
  • 两个场景设计题感觉自己的设计方案还是有瑕疵的不是面试官想要的,并且问到mysql存储引擎直接无言以对了,感觉自己知识的广度还是有一定欠缺。最后问到性能优化正好自己之前优化过自己的博客网站做过相关的实践
  • koa了解吗

koa洋葱圈模型运行机制 express和koa区别 nginx原理 正向代理与反向代理 手写中间件测试请求时间

  • 进程和线程的区别

mongo的实务

node模块加载机制(require()原理)

路径转变,缓存

koa用的多吗

redis缓存机制是啥

8、 mysql如何创建索引 9、 mysql如何处理博客的点赞问题

完成prosimeFy babel 原理,class 是转换成什么 https://juejin.cn/post/6844904024928419848

javascript
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
javascript
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+

进程线程

  1. 页面缓存cache-control

  2. node写过东西吗

  3. node模块,path模块

  4. 对比一下commonjs模块化和es6 module

  5. 两个文件export的时候导出了相同的变量,避免重复--as

  6. exports和module.export的区别

  7. http和https

  8. 非对称加密和对称加密

  9. 打乱一个数组Math.random()的范围

  10. 问的特别全面,基础几乎全问

  11. 网站安全,CSRF,XSS,SQL注入攻击

  12. token是做什么的

node koa \\4. 由于简历写了了解Nodejs的底层原理。问了Node.js内存泄漏和如何定位内存泄漏 常见的内存场景:1. 闭包 2. http.globalAgent 开启keepAlive的时候,未清除事件监听 定位内存泄漏:1. heapdump 2. 用chromedev 生成三次内存快照 \\5. Node.js垃圾回收的原理 \\4. 客户端发请求给服务端发生了什么 \\6. 你知道MySQL和MongDB区别吗 \\7. 你平时怎么使用nodeJS的 \\8. restful风格规范是什么 新生代(from空间,to空间),老生代(标记清除,引用计数) NodeJs的EventLoop和V8的有什么区别 Node作为服务端语言,跟其他服务端语言比有什么特性?优势和劣势是什么? Mysql接触过?跨表查询外键索引(就知道个关键字所以没往下问了) 1、介绍一下项目的难点以及怎么解决的 5、移动端的业务有做过吗? 6、介绍一下你对中间件的理解7、怎么保证后端服务稳定性,怎么做容灾8、怎么让数据库查询更快9、数据库是用的什么?10、为什么用mysql1、如何用Node开启一个服务器 2、你的项目当中哪些地方用到了Node,为什么这个地方要用Node处理

  • 后端语言,涉及各种类型,主要都是TS,Java等,nodeJS作为动态语言,你怎么看?
  • 怎么理解后端,后端主要功能是做什么?
  • 校验器怎么做的?
  • 你对校验器做了什么封装?你用的这个库提供哪些功能?怎么实现两个关联参数校验?
  1. 进程与线程的概念
  2. 进程和线程的区别
  3. 浏览器渲染进程的线程有哪些
  4. 进程之前的通信方式
  5. 僵尸进程和孤儿进程是什么?
  6. 死锁产生的原因?如果解决死锁的问题?
  7. 如何实现浏览器内多个标签页之间的通信?
  8. 对Service Worker的理解

Mysql两种引擎

· 数据库表编码格式,UTF8和GBK区别

· 一个表里面姓名和学号两列,一行sql查询姓名重复的信息

· 防范XSS攻击

· 过滤或者编码哪些字符

  1. export和module.export区别
  2. Comonjs和es6 import区别

· Video标签可以播放的视频格式

  1. JWT
  2. 中间件有了解吗
  3. 进程和线程是什么
  4. nodejs的process的nexttick 为什么比微任务快
  5. 进程之间如何实现数据通信和数据同步
  6. node服务层如何封装接口
  7. 深挖底层原理 一直在拓展问

网易:谈谈同步10和异步10和它们的应用场景。https://zhuanlan.zhihu.com/p/115912936 队列解决:实现bfs返回树中值为value的节点 网易:

javascript
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+

你知道MySQL和MongDB区别吗

  1. · 如何防止sql注入

3 如何判断请求的资源已经全部返回。(从http角度,不要从api角度)

8 异步缓冲队列实现。例如需要发送十个请求,但最多只支持同时发送五个请求,需要等有请求完成后再进行剩余请求的发送。

2.用写websocket进度条以及使用场景

3.写回到上次浏览的位置(我说的记住位置存在sessionstorage/localstorage里),他问窗口大小变动怎么办,我说获取当前窗口大小等比例缩放scolltop。。。

6.正向代理,反向代理以及他们的应用场景

针对博客问了一下session,讲了cookie、session以及jwt,最后问如果不用cookie和localstorage,怎么做校验,比如匿名用户

你觉得你做过最牛逼的事情是什么

如果你有一天成了技术大牛,你最想做的事情是什么

介绍了下简历里各个项目的背景

react native ;node

博客技术文章怎么写的

monrepo技术栈,好处

node 中间件原理

发布订阅once加一个标识

是否能用于生产环境

父子组件执行顺序的原因

10w数据:虚拟滚动,bff(缓存,解决渲染问题)——新元

•如果你的技术方案和一起合作的同学不一致,你该怎么说服他? • 压力 •我觉得你说的这个方案不可行 • 稳定性 • 看到你家在深圳,计划长期在北京发展吗?

  • 个轮播组件的API • 案例问题 •假设我们现在面临着着会怎么排查?-个故障,部分用户反馈无法进入登录页面,你将 会怎么排查?

egg又不是不可替代的,一个bff而已,egg团队去开发artus了,Artus是更上层的框架,还是基于egg的

node里面for循环处理10亿数据,出现两个问题,js卡死,内存不够

为什么老是有题目处理101数据

我跟他讲:分开循环解决js卡死,把结果用node里面的fs.writefile写入本地文件

解決内存不够

bam就是浏览器上作为一个代理 但实际上mock数据还是在那个「API管理平台上的」

[]==![] 是true

[]== [] 是false

双等号 两边数据类型一样 如果是引用类型,就判断引用地址是否相同,两边数据类型不一样便会隐式转换。

有一点反直觉

如何在一个图片长列表页使用懒加载进行性能优化 ,然后说了图片替换和监听窗口滚动实现图片按需加载,那么如何实现监听呢?,然后扯到节流监听滚动条变化执行函数 输入框要求每次输入都要有联想功能,即随时向后台请求数据,但是频繁输入时,如何防止频繁多次请求 你对操作系统,编译原理的理解

  • 设计一个扫码登录的逻辑,为什么你扫码就可以登录你自己号,同时有这么多二维码
  • 讲清楚服务端,PC端,和手机端的逻辑
  • 怎么保存登陆态

项目权限管理(高低权限怎么区分,如果网络传输中途被人改包,怎么防止这一潜在危险) 在淘宝里面,商品数据量很大,前端怎么优化使加载速度更快、用户体验更好(severless + indexdb)  \\8. 在7的条件下,已知用户当前在商品列表页,并且下一步操作是点击某个商品进入详情页,怎么加快详情页的渲染速度

npm安装和查找机制

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装(不需要网络)
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

npm 缓存相关命令

shell
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
`,323),o=[e];function r(c,t,i,B,y,F){return n(),a("div",null,o)}const b=s(p,[["render",r]]);export{u as __pageData,b as default}; diff --git a/assets/front-end-engineering_jscompatibility.md.73ef711a.js b/assets/front-end-engineering_jscompatibility.md.73ef711a.js new file mode 100644 index 00000000..ff808089 --- /dev/null +++ b/assets/front-end-engineering_jscompatibility.md.73ef711a.js @@ -0,0 +1,213 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-46-05.c830893b.png",e="/blog/assets/2023-01-06-16-46-13.ea302e87.png",o="/blog/assets/2023-01-06-16-49-52.3f0e9aff.png",r="/blog/assets/2023-01-06-16-50-09.1857bf9e.png",A=JSON.parse('{"title":"JS兼容性","description":"","frontmatter":{},"headers":[{"level":2,"title":"babel","slug":"babel","link":"#babel","children":[{"level":3,"title":"babel简介","slug":"babel简介","link":"#babel简介","children":[]},{"level":3,"title":"babel的安装","slug":"babel的安装","link":"#babel的安装","children":[]},{"level":3,"title":"babel的使用","slug":"babel的使用","link":"#babel的使用","children":[]},{"level":3,"title":"babel的配置","slug":"babel的配置","link":"#babel的配置","children":[]},{"level":3,"title":"babel预设","slug":"babel预设","link":"#babel预设","children":[]},{"level":3,"title":"babel插件","slug":"babel插件","link":"#babel插件","children":[]}]},{"level":2,"title":"ESLint","slug":"eslint","link":"#eslint","children":[{"level":3,"title":"使用","slug":"使用","link":"#使用","children":[]},{"level":3,"title":"配置","slug":"配置","link":"#配置","children":[]},{"level":3,"title":"env","slug":"env","link":"#env","children":[]},{"level":3,"title":"parserOptions","slug":"parseroptions","link":"#parseroptions","children":[]},{"level":3,"title":"parser","slug":"parser","link":"#parser","children":[]},{"level":3,"title":"globals","slug":"globals","link":"#globals","children":[]},{"level":3,"title":"extends","slug":"extends","link":"#extends","children":[]},{"level":3,"title":"ignoreFiles","slug":"ignorefiles","link":"#ignorefiles","children":[]},{"level":3,"title":"rules","slug":"rules","link":"#rules","children":[]},{"level":3,"title":"ESLint的由来","slug":"eslint的由来","link":"#eslint的由来","children":[]},{"level":3,"title":"如何验证","slug":"如何验证","link":"#如何验证","children":[]},{"level":3,"title":"配置规则","slug":"配置规则","link":"#配置规则","children":[]},{"level":3,"title":"在VSCode中及时发现问题","slug":"在vscode中及时发现问题","link":"#在vscode中及时发现问题","children":[]},{"level":3,"title":"使用继承","slug":"使用继承","link":"#使用继承","children":[]}]},{"level":2,"title":"企业开发的实际情况","slug":"企业开发的实际情况","link":"#企业开发的实际情况","children":[]}],"relativePath":"front-end-engineering/jscompatibility.md","lastUpdated":1672995138000}'),c={name:"front-end-engineering/jscompatibility.md"},t=l('

JS兼容性

babel

官网:https://babeljs.io/

民间中文网:https://www.babeljs.cn/

babel简介

babel一词来自于希伯来语,直译为巴别塔

巴别塔象征的统一的国度、统一的语言

而今天的JS世界缺少一座巴别塔,不同版本的浏览器能识别的ES标准并不相同,就导致了开发者面对不同版本的浏览器要使用不同的语言,和古巴比伦一样,前端开发也面临着这样的困境。

babel的出现,就是用于解决这样的问题,它是一个编译器,可以把不同标准书写的语言,编译为统一的、能被各种浏览器识别的语言

由于语言的转换工作灵活多样,babel的做法和postcss、webpack差不多,它本身仅提供一些分析功能,真正的转换需要依托于插件完成

babel的安装

babel可以和构建工具联合使用,也可以独立使用

如果要独立的使用babel,需要安装下面两个库:

  • @babel/core:babel核心库,提供了编译所需的所有api
  • @babel/cli:提供一个命令行工具,调用核心库的api完成编译
shell
npm i -D @babel/core @babel/cli
+
npm i -D @babel/core @babel/cli
+

babel的使用

@babel/cli的使用极其简单

它提供了一个命令babel

shell
# 按文件编译
+babel 要编译的文件 -o 编辑结果文件
+npx babel js/a.js -o js/b.js
+
+# 按目录编译
+babel 要编译的整个目录 -d 编译结果放置的目录
+npx babel js -d dist
+
# 按文件编译
+babel 要编译的文件 -o 编辑结果文件
+npx babel js/a.js -o js/b.js
+
+# 按目录编译
+babel 要编译的整个目录 -d 编译结果放置的目录
+npx babel js -d dist
+

babel的配置

可以看到,babel本身没有做任何事情,真正的编译要依托于babel插件babel预设来完成

babel预设和postcss预设含义一样,是多个插件的集合体,用于解决一系列常见的兼容问题

如何告诉babel要使用哪些插件或预设呢?需要通过一个配置文件.babelrc

json
{
+    "presets": [],
+    "plugins": []
+}
+
{
+    "presets": [],
+    "plugins": []
+}
+

babel预设

babel有多种预设,最常见的预设是@babel/preset-env

安装 @babel/preset-env可以让你使用最新的JS语法,而无需针对每种语法转换设置具体的插件

配置

json
{
+    "presets": [
+        "@babel/preset-env"
+    ]
+}
+
{
+    "presets": [
+        "@babel/preset-env"
+    ]
+}
+

兼容的浏览器

@babel/preset-env需要根据兼容的浏览器范围来确定如何编译,和postcss一样,可以使用文件.browserslistrc来描述浏览器的兼容范围

last 3 version
+> 1%
+not ie <= 8
+
last 3 version
+> 1%
+not ie <= 8
+

自身的配置

postcss-preset-env一样,@babel/preset-env自身也有一些配置

具体的配置见:https://www.babeljs.cn/docs/babel-preset-env#options

配置方式是:

json
{
+    "presets": [
+        ["@babel/preset-env", {
+            "配置项1": "配置值",
+            "配置项2": "配置值",
+            "配置项3": "配置值"
+        }]
+    ]
+}
+
{
+    "presets": [
+        ["@babel/preset-env", {
+            "配置项1": "配置值",
+            "配置项2": "配置值",
+            "配置项3": "配置值"
+        }]
+    ]
+}
+

其中一个比较常见的配置项是usebuiltins,该配置的默认值是false

它有什么用呢?由于该预设仅转换新的语法,并不对新的API进行任何处理

例如:

javascript
new Promise(resolve => {
+    resolve()
+})
+
new Promise(resolve => {
+    resolve()
+})
+

转换的结果为

javascript
new Promise(function (resolve) {
+  resolve();
+});
+
new Promise(function (resolve) {
+  resolve();
+});
+

如果遇到没有Promise构造函数的旧版本浏览器,该代码就会报错

而配置usebuiltins可以在编译结果中注入这些新的API,它的值默认为false,表示不注入任何新的API,可以将其设置为usage,表示根据API的使用情况,按需导入API

对于新的api npm i core-js 对于新的语法变成api实现 generator-runtime

json
{
+    "presets": [
+        ["@babel/preset-env", {
+            "useBuiltIns": "usage",
+            "corejs": 3//默认会使用2,我需要使用3版本
+        }]
+    ]
+}
+
{
+    "presets": [
+        ["@babel/preset-env", {
+            "useBuiltIns": "usage",
+            "corejs": 3//默认会使用2,我需要使用3版本
+        }]
+    ]
+}
+

babel插件

先安装在使用 npm i -D ...

注意:@babel/polyfill 已过时,目前被core-jsgenerator-runtime所取代

除了预设可以转换代码之外,插件也可以转换代码,它们的顺序是:

  • 插件在 Presets 前运行。
  • 插件顺序从前往后排列。
  • Preset 顺序是颠倒的(从后往前)。
js
{
+  "preset": ['a','b'],
+  "plugins":['c','d']
+}
+// cdba
+
{
+  "preset": ['a','b'],
+  "plugins":['c','d']
+}
+// cdba
+

通常情况下,@babel/preset-env只转换那些已经形成正式标准的语法,对于某些处于早期阶段、还没有确定的语法不做转换。

如果要转换这些语法,就要单独使用插件

下面随便列举一些插件

@babel/plugin-proposal-class-properties

该插件可以让你在类中书写初始化字段

javascript
class A {
+    a = 1;
+    constructor(){
+        this.b = 3;
+    }
+}
+
class A {
+    a = 1;
+    constructor(){
+        this.b = 3;
+    }
+}
+

@babel/plugin-proposal-function-bind

该插件可以让你轻松的为某个方法绑定this

javascript
function Print() {
+    console.log(this.loginId);
+}
+
+const obj = {
+    loginId: "abc"
+};
+
+obj::Print(); //相当于:Print.call(obj);
+
function Print() {
+    console.log(this.loginId);
+}
+
+const obj = {
+    loginId: "abc"
+};
+
+obj::Print(); //相当于:Print.call(obj);
+

遗憾的是,目前vscode无法识别该语法,会在代码中报错,虽然并不会有什么实际性的危害,但是影响观感

@babel/plugin-proposal-optional-chaining

javascript
const obj = {
+  foo: {
+    bar: {
+      baz: 42,
+    },
+  },
+};
+
+const baz = obj?.foo?.bar?.baz; // 42
+
+const safe = obj?.qux?.baz; // undefined
+
const obj = {
+  foo: {
+    bar: {
+      baz: 42,
+    },
+  },
+};
+
+const baz = obj?.foo?.bar?.baz; // 42
+
+const safe = obj?.qux?.baz; // undefined
+

babel-plugin-transform-remove-console

该插件会移除源码中的控制台输出语句

@babel/plugin-transform-runtime

用于提供一些公共的API,这些API会帮助代码转换 使用的时候发现依赖其他库,所以还要安装babel runtime

ESLint

ESLint是一个针对JS的代码风格检查工具,当不满足其要求的风格时,会给予警告或错误

官网:https://eslint.org/

民间中文网:https://eslint.bootcss.com/

使用

ESLint通常配合编辑器使用

  1. 在vscode中安装ESLint

该工具会自动检查工程中的JS文件

检查的工作交给eslint库,如果当前工程没有,则会去全局库中查找,如果都没有,则无法完成检查

另外,检查的依据是eslint的配置文件.eslintrc,如果找不到工程中的配置文件,也无法完成检查

  1. 安装eslint

npm i [-g] eslint

  1. 创建配置文件

可以通过eslint交互式命令创建配置文件

由于windows环境中git窗口对交互式命名支持不是很好,建议使用powershell

npx eslint --init

eslint会识别工程中的.eslintrc.*文件,也能够识别package.json中的eslintConfig字段

配置

env

配置代码的运行环境

  • browser:代码是否在浏览器环境中运行
  • es6:是否启用ES6的全局API,例如Promise

parserOptions

该配置指定eslint对哪些语法的支持

  • ecmaVersion: 支持的ES语法版本
  • sourceType
    • script:传统脚本
    • module:模块化脚本

parser

eslint的工作原理是先将代码进行解析,然后按照规则进行分析

eslint 默认使用Espree作为其解析器,你可以在配置文件中指定一个不同的解析器。

globals

配置可以使用的额外的全局变量

json
{
+  "globals": {
+    "var1": "readonly",
+    "var2": "writable"
+  }
+}
+
{
+  "globals": {
+    "var1": "readonly",
+    "var2": "writable"
+  }
+}
+

eslint支持注释形式的配置,在代码中使用下面的注释也可以完成配置

javascript
/* global var1, var2 */
+/* global var3:writable, var4:writable */
+
/* global var1, var2 */
+/* global var3:writable, var4:writable */
+

extends

该配置继承自哪里

它的值可以是字符串或者数组

比如:

json
{
+  "extends": "eslint:recommended"
+}
+
{
+  "extends": "eslint:recommended"
+}
+

表示,该配置缺失的位置,使用eslint推荐的规则

ignoreFiles

排除掉某些不需要验证的文件

.eslintignore 要放在根目录

dist/**/*.js
+node_modules// 自动忽略
+
dist/**/*.js
+node_modules// 自动忽略
+

rules

eslint规则集

每条规则影响某个方面的代码风格

每条规则都有下面几个取值:

  • off 或 0 或 false: 关闭该规则的检查
  • warn 或 1 或 true:警告,不会导致程序退出
  • error 或 2:错误,当被触发的时候,程序会退出

除了在配置文件中使用规则外,还可以在注释中使用:

javascript
/* eslint eqeqeq: "off", curly: "error" */
+
/* eslint eqeqeq: "off", curly: "error" */
+

https://eslint.bootcss.com/docs/rules/ 带有🔧的可以自动修复:npx eslint --fix src/index.js

ESLint官网:https://eslint.org/

ESLint民间中文网:https://eslint.bootcss.com/

ESLint的由来

JavaScript是一个过于灵活的语言,因此在企业开发中,往往会遇到下面两个问题:

  • 如何让所有员工书写高质量的代码? 比如使用===替代==
  • 如何让所有员工书写的代码风格保持统一? 比如字符串统一使用单引号

上面两个问题,一个代表着代码的质量,一个代表着代码的风格。

如果纯依靠人工进行检查,不仅费时费力,而且还容易出错。

ESLint由此诞生,它是一个工具,预先配置好各种规则,通过这些规则来自动化的验证代码,甚至自动修复

如何验证

shell
# 验证单个文件
+npx eslint 文件名
+# 验证全部文件
+npx eslint src/**
+
# 验证单个文件
+npx eslint 文件名
+# 验证全部文件
+npx eslint src/**
+

配置规则

eslint会自动寻找根目录中的配置文件,它支持三种配置文件:

  • .eslintrc JSON格式
  • .eslintrc.js JS格式
  • .eslintrc.yml YAML格式

这里以.eslintrc.js为例:

javascript
// ESLint 配置
+module.exports = {
+  // 配置规则
+  rules: {
+    规则名1: 级别,
+    规则名2: 级别,
+    ...
+  },
+};
+
// ESLint 配置
+module.exports = {
+  // 配置规则
+  rules: {
+    规则名1: 级别,
+    规则名2: 级别,
+    ...
+  },
+};
+

每条规则由名称和级别组成

规则名称决定了要检查什么

规则级别决定了检查没通过时的处理方式

所有的规则名称看这里:

所有级别如下:

  • 0 或 'off':关闭规则
  • 1 或 'warn':验证不通过提出警告
  • 2 或 'error':验证不通过报错,退出程序

在VSCode中及时发现问题

每次都要输入命令发现问题非常麻烦

可以安装VSCode插件ESLint,只要项目的node_modules中有eslint,它就会按照项目根目录下的规则自动检测

使用继承

ESLint的规则非常庞大,全部自定义过于麻烦

一般我们继承其他企业开源的方案来简化配置

这方面做的比较好的是一家叫Airbnb的公司,他们在开发前端项目的时候自定义了一套开源规则,受到全世界的认可

我们只需要安装它即可

shell
# 为了避免版本问题,不要直接安装eslint,直接安装下面的包,会自动安装相应版本的eslint
+npm i -D eslint-config-airbnb
+
# 为了避免版本问题,不要直接安装eslint,直接安装下面的包,会自动安装相应版本的eslint
+npm i -D eslint-config-airbnb
+

然后稍作配置

shell
module.exports = {
+	extends: 'airbnb' # 配置继承自 airbnb
+}
+
module.exports = {
+	extends: 'airbnb' # 配置继承自 airbnb
+}
+

企业开发的实际情况

我们要做什么?

  • 安装好VSCode的ESLint插件
  • 学会查看ESLint错误提示
',157),i=[t];function B(d,y,b,u,F,h){return n(),a("div",null,i)}const g=s(c,[["render",B]]);export{A as __pageData,g as default}; diff --git a/assets/front-end-engineering_jscompatibility.md.73ef711a.lean.js b/assets/front-end-engineering_jscompatibility.md.73ef711a.lean.js new file mode 100644 index 00000000..840d127d --- /dev/null +++ b/assets/front-end-engineering_jscompatibility.md.73ef711a.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-46-05.c830893b.png",e="/blog/assets/2023-01-06-16-46-13.ea302e87.png",o="/blog/assets/2023-01-06-16-49-52.3f0e9aff.png",r="/blog/assets/2023-01-06-16-50-09.1857bf9e.png",A=JSON.parse('{"title":"JS兼容性","description":"","frontmatter":{},"headers":[{"level":2,"title":"babel","slug":"babel","link":"#babel","children":[{"level":3,"title":"babel简介","slug":"babel简介","link":"#babel简介","children":[]},{"level":3,"title":"babel的安装","slug":"babel的安装","link":"#babel的安装","children":[]},{"level":3,"title":"babel的使用","slug":"babel的使用","link":"#babel的使用","children":[]},{"level":3,"title":"babel的配置","slug":"babel的配置","link":"#babel的配置","children":[]},{"level":3,"title":"babel预设","slug":"babel预设","link":"#babel预设","children":[]},{"level":3,"title":"babel插件","slug":"babel插件","link":"#babel插件","children":[]}]},{"level":2,"title":"ESLint","slug":"eslint","link":"#eslint","children":[{"level":3,"title":"使用","slug":"使用","link":"#使用","children":[]},{"level":3,"title":"配置","slug":"配置","link":"#配置","children":[]},{"level":3,"title":"env","slug":"env","link":"#env","children":[]},{"level":3,"title":"parserOptions","slug":"parseroptions","link":"#parseroptions","children":[]},{"level":3,"title":"parser","slug":"parser","link":"#parser","children":[]},{"level":3,"title":"globals","slug":"globals","link":"#globals","children":[]},{"level":3,"title":"extends","slug":"extends","link":"#extends","children":[]},{"level":3,"title":"ignoreFiles","slug":"ignorefiles","link":"#ignorefiles","children":[]},{"level":3,"title":"rules","slug":"rules","link":"#rules","children":[]},{"level":3,"title":"ESLint的由来","slug":"eslint的由来","link":"#eslint的由来","children":[]},{"level":3,"title":"如何验证","slug":"如何验证","link":"#如何验证","children":[]},{"level":3,"title":"配置规则","slug":"配置规则","link":"#配置规则","children":[]},{"level":3,"title":"在VSCode中及时发现问题","slug":"在vscode中及时发现问题","link":"#在vscode中及时发现问题","children":[]},{"level":3,"title":"使用继承","slug":"使用继承","link":"#使用继承","children":[]}]},{"level":2,"title":"企业开发的实际情况","slug":"企业开发的实际情况","link":"#企业开发的实际情况","children":[]}],"relativePath":"front-end-engineering/jscompatibility.md","lastUpdated":1672995138000}'),c={name:"front-end-engineering/jscompatibility.md"},t=l("",157),i=[t];function B(d,y,b,u,F,h){return n(),a("div",null,i)}const g=s(c,[["render",B]]);export{A as __pageData,g as default}; diff --git a/assets/front-end-engineering_modularization.md.14ea7e8d.js b/assets/front-end-engineering_modularization.md.14ea7e8d.js new file mode 100644 index 00000000..d3cfdf02 --- /dev/null +++ b/assets/front-end-engineering_modularization.md.14ea7e8d.js @@ -0,0 +1,233 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-21-59-20.1fa5b4ed.png",o="/blog/assets/2023-02-01-21-59-43.f3f27dad.png",e="/blog/assets/2023-02-01-22-00-22.5f64024d.png",r="/blog/assets/2023-02-01-22-00-43.9fbe4838.png",c="/blog/assets/2023-02-01-22-01-01.8aaf55c5.png",t="/blog/assets/2023-02-01-22-01-19.d349110a.png",v=JSON.parse('{"title":"模块化","description":"","frontmatter":{},"headers":[{"level":2,"title":"1.JavaScript 模块化发展史","slug":"_1-javascript-模块化发展史","link":"#_1-javascript-模块化发展史","children":[{"level":3,"title":"第一阶段","slug":"第一阶段","link":"#第一阶段","children":[]},{"level":3,"title":"第二阶段","slug":"第二阶段","link":"#第二阶段","children":[]},{"level":3,"title":"第三阶段","slug":"第三阶段","link":"#第三阶段","children":[]},{"level":3,"title":"第四阶段","slug":"第四阶段","link":"#第四阶段","children":[]}]},{"level":2,"title":"2. CommonJS","slug":"_2-commonjs","link":"#_2-commonjs","children":[{"level":3,"title":"2-2. CommonJS","slug":"_2-2-commonjs","link":"#_2-2-commonjs","children":[]},{"level":3,"title":"模块的导出","slug":"模块的导出","link":"#模块的导出","children":[]},{"level":3,"title":"模块的导入","slug":"模块的导入","link":"#模块的导入","children":[]},{"level":3,"title":"CommonJS 规范","slug":"commonjs-规范","link":"#commonjs-规范","children":[]}]},{"level":2,"title":"3.AMD 和 CMD","slug":"_3-amd-和-cmd","link":"#_3-amd-和-cmd","children":[{"level":3,"title":"3-1 浏览器端模块化的难题","slug":"_3-1-浏览器端模块化的难题","link":"#_3-1-浏览器端模块化的难题","children":[]},{"level":3,"title":"3-2AMD","slug":"_3-2amd","link":"#_3-2amd","children":[]},{"level":3,"title":"3-3CMD","slug":"_3-3cmd","link":"#_3-3cmd","children":[]}]},{"level":2,"title":"4.es6 模块化","slug":"_4-es6-模块化","link":"#_4-es6-模块化","children":[{"level":3,"title":"4-1.ES6 模块化简介","slug":"_4-1-es6-模块化简介","link":"#_4-1-es6-模块化简介","children":[]},{"level":3,"title":"4-2.基本导入导出","slug":"_4-2-基本导入导出","link":"#_4-2-基本导入导出","children":[]},{"level":3,"title":"模块的引入","slug":"模块的引入","link":"#模块的引入","children":[]},{"level":3,"title":"4-3. 默认导入导出","slug":"_4-3-默认导入导出","link":"#_4-3-默认导入导出","children":[]},{"level":3,"title":"4-4.ES6 模块化的其他细节","slug":"_4-4-es6-模块化的其他细节","link":"#_4-4-es6-模块化的其他细节","children":[]}]}],"relativePath":"front-end-engineering/modularization.md","lastUpdated":1675736874000}'),i={name:"front-end-engineering/modularization.md"},B=l(`

模块化

1.JavaScript 模块化发展史

第一阶段

在 JavaScript 语言刚刚诞生的时候,它仅仅用于实现页面中的一些小效果 那个时候,一个页面所用到的 JS 可能只有区区几百行的代码 在这种情况下,语言本身所存在的一些缺陷往往被大家有意的忽略,因为程序的规模实在太小,只要开发人员小心谨慎,往往不会造成什么问题 在这个阶段,也不存在专业的前端工程师,由于前端要做的事情实在太少,因此这一部分工作往往由后端工程师顺带完成 第一阶段发生的大事件:

  • 1996 年,NetScape 将 JavaScript 语言提交给欧洲的一个标准制定组织 ECMA(欧洲计算机制造商协会)
  • 1998 年,NetScape 在与微软浏览器 IE 的竞争中失利,宣布破产

第二阶段

ajax 的出现,逐渐改变了 JavaScript 在浏览器中扮演的角色。现在,它不仅可以实现小的效果,还可以和服务器之间进行交互,以更好的体验来改变数据 JS 代码的数量开始逐渐增长,从最初的几百行,到后来的几万行,前端程序逐渐变得复杂 后端开发者压力逐渐增加,致使一些公司开始招募专业的前端开发者 但此时,前端开发者的待遇远不及后端开发者,因为前端开发者承担的开发任务相对于后端开发来说,还是比较简单的,通过短短一个月的时间集训,就可以成为满足前端开发的需要 究其根本原因,是因为前端开发还有几个大的问题没有解决,这些问题都严重的制约了前端程序的规模进一步扩大:

  1. 浏览器解释执行 JS 的速度太慢
  2. 用户端的电脑配置不足
  3. 更多的代码带来了全局变量污染、依赖关系混乱等问题

上面三个问题,就像是阿喀琉斯之踵,成为前端开发挥之不去的阴影和原罪。 在这个阶段,前端开发处在一个非常尴尬的境地,它在传统的开发模式和前后端分离之间无助的徘徊 第二阶段的大事件:

  1. IE 浏览器制霸市场后,几乎不再更新
  2. ES4.0 流产,导致 JS 语言 10 年间几乎毫无变化
  3. 2008 年 ES5 发布,仅解决了一些 JS API 不足的糟糕局面

第三阶段

时间继续向前推移,到了 2008 年,谷歌的 V8 引擎发布(面试),将 JS 的执行速度推上了一个新的台阶,甚至可以和后端语言媲美。 摩尔定律持续发酵,个人电脑的配置开始飞跃 突然间,制约前端发展的两大问题得以解决,此时,只剩下最后一个问题还在负隅顽抗,即全局变量污染和依赖混乱的问题,解决了它,前端便可以突破一切障碍,未来无可限量。 于是,全世界的前端开发者在社区中激烈的讨论,想要为这个问题寻求解决之道...... 2008 年,有一个名叫 Ryan Dahl 小伙子正在为一件事焦头烂额,它需要在服务器端手写一个高性能的 web 服务,该服务对于性能要求之高,以至于目前市面上已有的 web 服务产品都满足不了需求。

服务器开发

新浪的服务器(电脑)收到请求 其中一个应用程序在做以下的事情 web 服务

  1. 监听 80 端口
  2. 将请求进行分析
  3. 将分析的结果交给相应的程序(php,Java)进行处理
  4. 把程序处理的结果返还给客户端

经过分析,它确定,如果要实现高性能,那么必须要尽可能的减少线程,而要减少线程,避免不了要实用异步的处理方案。 一开始,他打算自己实用 C/C++语言来编写,可是这一过程实在太痛苦。 就在他一筹莫展的时候,谷歌 V8 引擎的发布引起了他的注意,他突然发现,JS 不就是最好的实现 web 服务的语言吗?它天生就是单线程,并且是基于异步的!有了 V8 引擎的支撑,它的执行速度完全可以撑起一个服务器。而且 V8 是鼎鼎大名的谷歌公司发布的,谷歌一定会不断的优化 V8,有这种又省钱又省力的好事,我干嘛还要自己去写呢? 典型异步场景

javascript
setTimeout(function () {
+  console.log("a");
+}, 1000);
+//后面不会阻塞
+//JS单线程指的是执行线程,不代表浏览器单线程
+
setTimeout(function () {
+  console.log("a");
+}, 1000);
+//后面不会阻塞
+//JS单线程指的是执行线程,不代表浏览器单线程
+

于是,它基于开源的 V8 引擎,对源代码作了一些修改,便快速的完成了该项目。 2009 年,Ryan 推出了该 web 服务项目,命名为 nodejs。 从此,JS 第一次堂堂正正的入主后端,不再是必须附属于浏览器的“玩具”语言了。 也是从此刻开始,人们认识到,JS(ES)是一门真正的语言,它依附于运行环境(运行时)(宿主程序)而执行

nodejs 的诞生,便把 JS 中的最后一个问题放到了台前,即全局变量污染和依赖混乱问题 要直到,nodejs 是服务器端,如果不解决这个问题,分模块开发就无从实现,而模块化开发是所有后端程序必不可少的内容 经过社区的激烈讨论,最终,形成了一个模块化方案,即鼎鼎大名的 CommonJS,该方案,彻底解决了全局变量污染和依赖混乱的问题 该方案一出,立即被 nodejs 支持,于是,nodejs 成为了第一个为 JS 语言实现模块化的平台,为前端接下来的迅猛发展奠定了实践基础 该阶段发生的大事件:

  • 2008 年,V8 发布
  • IE 的市场逐步被 firefox 和 chrome 蚕食,现已无力回天
  • 2009 年,nodejs 发布,并附带 commonjs 模块化标准

第四阶段

CommonJS 的出现打开了前端开发者的思路

既然后端可以使用模块化的 JS,作为 JS 语言的老东家浏览器为什么不行呢?

于是,开始有人想办法把 CommonJS 运用到浏览器中

可是这里面存在诸多的困难(课程中详解)

办法总比困难多,有些开发者就想,既然 CommonJS 运用到浏览器困难,我们干嘛不自己重新定一个模块化的标准出来,难道就一定要用 CommonJS 标准吗?

于是很快,AMD 规范出炉,它解决的问题和 CommonJS 一样,但是可以更好的适应浏览器环境

相继的,CMD 规范出炉,它对 AMD 规范进行了改进

这些行为,都受到了 ECMA 官方的密切关注......

2015 年,ES6 发布,它提出了官方的模块化解决方案 —— ES6 模块化

从此以后,模块化成为了 JS 本身特有的性质,这门语言终于有了和其他语言较量的资本,成为了可以编写大型应用的正式语言

于此同时,很多开发者、技术厂商早已预见到 JS 的无穷潜力,于是有了下面的故事

  • 既然 JS 也能编写大型应用,那么自然也需要像其他语言那样有解决复杂问题的开发框架
    • Angular、React、Vue 等前端开发框架出现
    • Express、Koa 等后端开发框架出现
    • 各种后端数据库驱动出现
  • 要开发大型应用,自然少不了各种实用的第三方库的支持
    • npm 包管理器出现,实用第三方库变得极其方便
    • webpack 等构建工具出现,专门用于打包和部署
  • 既然 JS 可以放到服务器环境,为什么不能放到其他终端环境呢?
    • Electron 发布,可以使用 JS 语言开发桌面应用程序
    • RN 和 Vuex 等技术发布,可以使用 JS 语言编写移动端应用程序
    • 各种小程序出现,可以使用 JS 编写依附于其他应用的小程序
    • 目前还有很多厂商致力于将 JS 应用到各种其他的终端设备,最终形成大前端生态

可以看到,模块化的出现,是 JS 通向大型应用的基石,学习好模块化,变具备了编写大型应用的基本功。

2. CommonJS

nodejs 遵循 EcmaScript 标准,但由于脱离了浏览器环境,因此:

  1. 你可以在 nodejs 中使用 EcmaScript 标准的任何语法或 api,例如:循环、判断、数组、对象等
  2. 你不能在 nodejs 中使用浏览器的 web api,例如:dom 对象、window 对象、document 对象等

由于大部分开发者是从浏览器端开发转向 nodejs 开发的,为了降低开发者的学习成本,nodejs 中提供了一些和浏览器 web api 同样的对象或函数,例如:console、setTimeout、setInterval 等

2-2. CommonJS

在 nodejs 中,由于有且仅有一个入口文件(启动文件),而开发一个应用肯定会涉及到多个文件配合,因此,nodejs 对模块化的需求比浏览器端要大的多 由于 nodejs 刚刚发布的时候,前端没有统一的、官方的模块化规范,因此,它选择使用社区提供的 CommonJS 作为模块化规范 在学习 CommonJS 之前,首先认识两个重要的概念:模块的导出模块的导入

模块的导出

要理解模块的导出,首先要理解模块的含义 什么是模块? 模块就是一个 JS 文件,它实现了一部分功能,并隐藏自己的内部实现,同时提供了一些接口供其他模块使用 模块有两个核心要素:隐藏暴露demo

javascript
var count = 0; //需要隐藏的内部实现
+function getNumber() {
+  //要暴露的接口
+  count++;
+  return count;
+}
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+
var count = 0; //需要隐藏的内部实现
+function getNumber() {
+  //要暴露的接口
+  count++;
+  return count;
+}
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+

隐藏的,是自己内部的实现 暴露的,是希望外部使用的接口 任何一个正常的模块化标准,都应该默认隐藏模块中的所有实现,而通过一些语法或 api 调用来暴露接口 暴露接口的过程即模块的导出

模块的导入

当需要使用一个模块时,使用的是该模块暴露的部分(导出的部分),隐藏的部分是永远无法使用的。 当通过某种语法或 api 去使用一个模块时,这个过程叫做模块的导入

CommonJS 规范

CommonJS 使用exports导出模块,require导入模块 具体规范如下:

  1. 如果一个 JS 文件中存在exportsrequire,该 JS 文件是一个模块
  2. 模块内的所有代码均为隐藏代码,包括全局变量、全局函数,这些全局的内容均不应该对全局变量造成任何污染
  3. 如果一个模块需要暴露一些 API 提供给外部使用,需要通过exports导出,exports是一个空的对象,你可以为该对象添加任何需要导出的内容
  4. 如果一个模块需要导入其他模块,通过require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容

导出

javascript
// 原本相当于exports={}
+exports.getNumber = getNumber;
+相当于;
+// exports: {
+//     getNumber: getNumber
+// }
+exports.abc = 123;
+相当于;
+// exports: {
+//     getNumber: fn,
+//         abc: 123
+// }
+
// 原本相当于exports={}
+exports.getNumber = getNumber;
+相当于;
+// exports: {
+//     getNumber: getNumber
+// }
+exports.abc = 123;
+相当于;
+// exports: {
+//     getNumber: fn,
+//         abc: 123
+// }
+

导入

javascript
var a = require("./util.js");
+console.log(a);//a是对象
+count不能访问因为没有导出
+
var a = require("./util.js");
+console.log(a);//a是对象
+count不能访问,因为没有导出
+

3.AMD 和 CMD

3-1 浏览器端模块化的难题

本节为重点:AMD CMD 不常用了 CommonJS 的工作原理 当使用require(模块路径)导入一个模块时,node 会做以下两件事情(不考虑模块缓存):

  1. 通过模块路径找到本机文件,并读取文件内容
  2. 将文件中的代码放入到一个函数环境中执行,并将执行后 module.exports 的值作为 require 函数的返回结果

正是这两个步骤,使得 CommonJS 在 node 端可以良好的被支持 可以认为,CommonJS 是同步的,必须要等到加载完文件并执行完代码后才能继续向后执行 当浏览器遇到 CommonJS 当想要把 CommonJS 放到浏览器端时,就遇到了一些挑战

  1. 浏览器要加载 JS 文件,需要远程从服务器读取,而网络传输的效率远远低于 node 环境中读取本地文件的效率。由于 CommonJS 是同步的,这会极大的降低运行性能
  2. 如果需要读取 JS 文件内容并把它放入到一个环境中执行,需要浏览器厂商的支持,可是浏览器厂商不愿意提供支持,最大的原因是 CommonJS 属于社区标准,并非官方标准

新的规范 基于以上两点原因,浏览器无法支持模块化 可这并不代表模块化不能在浏览器中实现 要在浏览器中实现模块化,只要能解决上面的两个问题就行了 解决办法其实很简单:

  1. 远程加载 JS 浪费了时间?做成异步即可,加载完成后调用一个回调就行了
javascript
require("./a.js", function () {}); //回调
+
require("./a.js", function () {}); //回调
+
  1. 模块中的代码需要放置到函数中执行?编写模块时,直接放函数中就行了

基于这种简单有效的思路,出现了 AMD 和 CMD 规范,有效的解决了浏览器模块化的问题。

3-2AMD

全称是 Asynchronous Module Definition,即异步模块加载机制 require.js 实现了 AMD 规范

html
<script data-main="./js/index.js" src="./js/require.js"></script>
+<!-- index.js入口 , 必须引用require.js-->
+
<script data-main="./js/index.js" src="./js/require.js"></script>
+<!-- index.js入口 , 必须引用require.js-->
+

在 AMD 中,导入和导出模块的代码,都必须放置在 define 函数中

javascript
define([依赖的模块列表], function (模块名称列表) {
+  //模块内部的代码
+  return 导出的内容;
+});
+
define([依赖的模块列表], function (模块名称列表) {
+  //模块内部的代码
+  return 导出的内容;
+});
+

require.js 里面提供了个全局方法 define() 写法:

javascript
define(123); //导出123
+define({ a: 1, b: 2 }); //导出对象
+define(function () {
+  var a = 1; //不会污染
+  var b = 234;
+  return {
+    name: "b模块",
+    data: "b模块的数据",
+  };
+});
+
define(123); //导出123
+define({ a: 1, b: 2 }); //导出对象
+define(function () {
+  var a = 1; //不会污染
+  var b = 234;
+  return {
+    name: "b模块",
+    data: "b模块的数据",
+  };
+});
+

3-3CMD

全称是 Common Module Definition,公共模块定义规范 sea.js 实现了 CMD 规范

html
<script src="./js/sea.js"></script>
+<script>
+  seajs.use("./js/index");
+</script>
+
<script src="./js/sea.js"></script>
+<script>
+  seajs.use("./js/index");
+</script>
+

在 CMD 中,导入和导出模块的代码,都必须放置在 define 函数中

javascript
define(function (require, exports, module) {
+  //模块内部的代码
+});
+
define(function (require, exports, module) {
+  //模块内部的代码
+});
+

可以使用异步

javascript
define((require, exports, module) => {
+  require.async("a", function (a) {
+    console.log(a);
+  });
+  require.async("b", function (b) {
+    console.log(b);
+  });
+});
+
define((require, exports, module) => {
+  require.async("a", function (a) {
+    console.log(a);
+  });
+  require.async("b", function (b) {
+    console.log(b);
+  });
+});
+

4.es6 模块化

4-1.ES6 模块化简介

ECMA 组织参考了众多社区模块化标准,终于在 2015 年,随着 ES6 发布了官方的模块化标准,后成为 ES6 模块化 ES6 模块化具有以下的特点

  1. 使用依赖预声明的方式导入模块
    1. 依赖延迟声明(commonjs)
      1. 优点:某些时候可以提高效率
      2. 缺点:无法在一开始确定模块依赖关系(比较模糊)
    2. 依赖预声明(AMD)
      1. 优点:在一开始可以确定模块依赖关系
      2. 缺点:某些时候效率较低
  2. 灵活的多种导入导出方式(相对于 module.export 较简单)
  3. 规范的路径表示法:所有路径必须以./或../开头

4-2.基本导入导出

模块的引入

注意:这一部分非模块化标准 目前,浏览器使用以下方式引入一个 ES6 模块文件

html
<script src="入口文件" type="module">
+  //module作为模块运行
+  //当成了模块,就不会污染全局变量
+
<script src="入口文件" type="module">
+  //module作为模块运行
+  //当成了模块,就不会污染全局变量
+

模块的基本导出和导入

ES6 中的模块导入导出分为两种:

  1. 基本导入导出
  2. 默认导入导出

基本导出

类似于 exports.xxx = xxxx 基本导出可以有多个,每个必须有名称 基本导出的语法如下:

javascript
export 声明表达式  //必须是声明语句
+
export 声明表达式  //必须是声明语句
+

举例

javascript
export var a = 1; //导出a,值为1,类似于CommonJS中的exports.a = 1
+export function test() {
+  //导出test,值为一个函数,类似于CommonJS中的exports.test = function (){}
+}
+export class Person {}
+
+export const name = "abc";
+
export var a = 1; //导出a,值为1,类似于CommonJS中的exports.a = 1
+export function test() {
+  //导出test,值为一个函数,类似于CommonJS中的exports.test = function (){}
+}
+export class Person {}
+
+export const name = "abc";
+

javascript
export { 具名符号 }; // 大括号不是对象
+
export { 具名符号 }; // 大括号不是对象
+

举例

javascript
var age = 18;
+var sex = 1;
+
+export { age, sex }; //将age变量的名称作为导出的名称,age变量的值,作为导出的值
+
var age = 18;
+var sex = 1;
+
+export { age, sex }; //将age变量的名称作为导出的名称,age变量的值,作为导出的值
+

由于基本导出必须具有名称,所以要求导出内容必须跟上声明表达式具名符号

基本导入

由于使用的是依赖预加载,因此,导入任何其他模块,导入代码必须放置到所有代码之前 对于基本导出,如果要进行导入,使用下面的代码

javascript
import { 导入的符号列表 } from "模块路径";
+
import { 导入的符号列表 } from "模块路径";
+

举例

javascript
import { name, age } from "./a.js";
+
import { name, age } from "./a.js";
+

注意以下细节:

  • 导入时,可以通过关键字as对导入的符号进行重命名
javascript
import { name as name1, age as age1 } from "./a.js";
+var b = 3;
+console.log(b2);
+console.log(name1, age);
+console.log(b);
+
import { name as name1, age as age1 } from "./a.js";
+var b = 3;
+console.log(b2);
+console.log(name1, age);
+console.log(b);
+
  • 导入时使用的符号是常量,不可修改
  • 可以使用*号导入所有的基本导出,形成一个对象
javascript
import * as a from "./a.js";
+
import * as a from "./a.js";
+
javascript
import "./b.js"; //这条导入语句,仅会运行模块,不适用它内部的任何导出
+// 适用于初始化代码init
+
import "./b.js"; //这条导入语句,仅会运行模块,不适用它内部的任何导出
+// 适用于初始化代码init
+

4-3. 默认导入导出

默认导出

每个模块,除了允许有多个基本导出之外,还允许有一个默认导出 默认导出类似于 CommonJS 中的module.exports,由于只有一个,因此无需具名 具体的语法是

javascript
export default 默认导出的数据;
+
export default 默认导出的数据;
+

举例

javascript
export default 123;
+export default a;
+export default {
+  fn: function () { },
+  name: "adsfaf"
+}
+
export default 123;
+export default a;
+export default {
+  fn: function () { },
+  name: "adsfaf"
+}
+

javascript
export { 默认导出的数据 as default };
+
export { 默认导出的数据 as default };
+

举例

javascript
export { a as abc };
+
export { a as abc };
+

由于每个模块仅允许有一个默认导出,因此,每个模块不能出现多个默认导出语句

默认导入

需要想要导入一个模块的默认导出,需要使用下面的语法

javascript
import {导入的符号列表} from "模块路径
+
import {导入的符号列表} from "模块路径”
+

举例

javascript
import data from "./a.js"; //将a.js模块中的默认导出放置到常量data中
+
import data from "./a.js"; //将a.js模块中的默认导出放置到常量data中
+

类似于 CommonJS 中的

javascript
var 接受变量名 = require("模块路径	");
+
var 接受变量名 = require("模块路径	");
+

由于默认导入时变量名是自行定义的,因此没有别名一说 如果希望同时导入某个模块的默认导出和基本导出,可以使用下面的语法

javascript
import 接收默认导出的变量,{接收基本导出的变量} from "模块变量"
+
import 接收默认导出的变量,{接收基本导出的变量} from "模块变量"
+

注:如果使用*号,会将所有基本导出和默认导出聚合到一个对象中,默认导出会作为属性 default 存在

javascript
import data, { a, b } from "./a.js";
+// a里面默认导出放在data里面,基本导出放在a,b里面
+
import data, { a, b } from "./a.js";
+// a里面默认导出放在data里面,基本导出放在a,b里面
+

4-4.ES6 模块化的其他细节

  1. 尽量导出不可变值

当导出一个内容时,尽量保证该内容是不可变的(大部分情况都是如此) 因为,虽然导入后,无法更改导入内容,但是在导入的模块内部却有可能发生更改,这将导致一些无法预料的事情发生

javascript
export const name = "模块a"; //用const更加坐实了这点
+
export const name = "模块a"; //用const更加坐实了这点
+
  1. 可以使用无绑定的导入用于执行一些初始化代码

如果我们只是想执行模块中的一些代码,而不需要导入它的任何内容,可以使用无绑定的导入:

javascript
import "模块路径";
+
import "模块路径";
+

举例 arrayPatcher.js

javascript
Array.prototype.print = function () {
+  console.log(this);
+};
+//一些其他的代码
+
Array.prototype.print = function () {
+  console.log(this);
+};
+//一些其他的代码
+
javascript
import "./arrayPatcher.js"; //无绑定的导入
+var arr = [3, 4, 6, 6, 7];
+arr.print();
+
import "./arrayPatcher.js"; //无绑定的导入
+var arr = [3, 4, 6, 6, 7];
+arr.print();
+
  1. 可以使用绑定再导出,来重新导出来自另一个模块的内容

有的时候,我们可能需要用一个模块封装多个模块,然后有选择的将多个模块的内容分别导出,可以使用下面的语法轻松完成

javascript
export { 绑定的标识符 } from "模块路径";
+
export { 绑定的标识符 } from "模块路径";
+

举例

javascript
export { a, b } from "./m1.js";
+export { k, default, a as m2a } from "./m2.js";
+export const r = "m-r";
+
export { a, b } from "./m1.js";
+export { k, default, a as m2a } from "./m2.js";
+export const r = "m-r";
+
`,151),y=[B];function F(d,u,m,b,A,C){return a(),n("div",null,y)}const g=s(i,[["render",F]]);export{v as __pageData,g as default}; diff --git a/assets/front-end-engineering_modularization.md.14ea7e8d.lean.js b/assets/front-end-engineering_modularization.md.14ea7e8d.lean.js new file mode 100644 index 00000000..480a82d3 --- /dev/null +++ b/assets/front-end-engineering_modularization.md.14ea7e8d.lean.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-21-59-20.1fa5b4ed.png",o="/blog/assets/2023-02-01-21-59-43.f3f27dad.png",e="/blog/assets/2023-02-01-22-00-22.5f64024d.png",r="/blog/assets/2023-02-01-22-00-43.9fbe4838.png",c="/blog/assets/2023-02-01-22-01-01.8aaf55c5.png",t="/blog/assets/2023-02-01-22-01-19.d349110a.png",v=JSON.parse('{"title":"模块化","description":"","frontmatter":{},"headers":[{"level":2,"title":"1.JavaScript 模块化发展史","slug":"_1-javascript-模块化发展史","link":"#_1-javascript-模块化发展史","children":[{"level":3,"title":"第一阶段","slug":"第一阶段","link":"#第一阶段","children":[]},{"level":3,"title":"第二阶段","slug":"第二阶段","link":"#第二阶段","children":[]},{"level":3,"title":"第三阶段","slug":"第三阶段","link":"#第三阶段","children":[]},{"level":3,"title":"第四阶段","slug":"第四阶段","link":"#第四阶段","children":[]}]},{"level":2,"title":"2. CommonJS","slug":"_2-commonjs","link":"#_2-commonjs","children":[{"level":3,"title":"2-2. CommonJS","slug":"_2-2-commonjs","link":"#_2-2-commonjs","children":[]},{"level":3,"title":"模块的导出","slug":"模块的导出","link":"#模块的导出","children":[]},{"level":3,"title":"模块的导入","slug":"模块的导入","link":"#模块的导入","children":[]},{"level":3,"title":"CommonJS 规范","slug":"commonjs-规范","link":"#commonjs-规范","children":[]}]},{"level":2,"title":"3.AMD 和 CMD","slug":"_3-amd-和-cmd","link":"#_3-amd-和-cmd","children":[{"level":3,"title":"3-1 浏览器端模块化的难题","slug":"_3-1-浏览器端模块化的难题","link":"#_3-1-浏览器端模块化的难题","children":[]},{"level":3,"title":"3-2AMD","slug":"_3-2amd","link":"#_3-2amd","children":[]},{"level":3,"title":"3-3CMD","slug":"_3-3cmd","link":"#_3-3cmd","children":[]}]},{"level":2,"title":"4.es6 模块化","slug":"_4-es6-模块化","link":"#_4-es6-模块化","children":[{"level":3,"title":"4-1.ES6 模块化简介","slug":"_4-1-es6-模块化简介","link":"#_4-1-es6-模块化简介","children":[]},{"level":3,"title":"4-2.基本导入导出","slug":"_4-2-基本导入导出","link":"#_4-2-基本导入导出","children":[]},{"level":3,"title":"模块的引入","slug":"模块的引入","link":"#模块的引入","children":[]},{"level":3,"title":"4-3. 默认导入导出","slug":"_4-3-默认导入导出","link":"#_4-3-默认导入导出","children":[]},{"level":3,"title":"4-4.ES6 模块化的其他细节","slug":"_4-4-es6-模块化的其他细节","link":"#_4-4-es6-模块化的其他细节","children":[]}]}],"relativePath":"front-end-engineering/modularization.md","lastUpdated":1675736874000}'),i={name:"front-end-engineering/modularization.md"},B=l("",151),y=[B];function F(d,u,m,b,A,C){return a(),n("div",null,y)}const g=s(i,[["render",F]]);export{v as __pageData,g as default}; diff --git a/assets/front-end-engineering_node.md.78e6017c.js b/assets/front-end-engineering_node.md.78e6017c.js new file mode 100644 index 00000000..c9666e88 --- /dev/null +++ b/assets/front-end-engineering_node.md.78e6017c.js @@ -0,0 +1 @@ +import{_ as e,o,c as d,a}from"./app.f983686f.js";const i="/blog/assets/2024-06-16-15-36-15.5742d5bf.png",g=JSON.parse('{"title":"Node 组成原理","description":"","frontmatter":{},"headers":[{"level":2,"title":"Node.js 的特点包括:","slug":"node-js-的特点包括","link":"#node-js-的特点包括","children":[]},{"level":2,"title":"Node.js 组成","slug":"node-js-组成","link":"#node-js-组成","children":[]},{"level":2,"title":"其他资料","slug":"其他资料","link":"#其他资料","children":[]}],"relativePath":"front-end-engineering/node.md","lastUpdated":1718523698000}'),n={name:"front-end-engineering/node.md"},s=a('

Node 组成原理

Node.js 是一个开源的、跨平台的 JavaScript 运行环境,依赖于 Google V8 引擎,用于构建高性能的网络应用程序。Node.js 采用事件驱动、非阻塞 I/O 模型,使得它能够处理大量并发连接,适用于构建实时应用、高吞吐量的后端服务和网络代理等。

Node.js 广泛应用于 Web 开发、服务器端开发、实时通信、大数据处理等领域,被许多大型互联网公司和开发者使用和推崇。

Node.js 的特点包括:

  1. 单线程和事件驱动:Node.js 采用单线程的事件循环模型,通过异步 I/O 和事件驱动处理并发请求,避免了传统多线程模型中的线程切换和资源开销,提高了性能和可扩展性。
  2. 跨平台:Node.js 可运行于多个操作系统平台,包括 Windows、Linux 和 Mac OS 等。
  3. 高性能:由于基于 V8 引擎和非阻塞 I/O 模型,Node.js 具有快速的执行速度和高吞吐量,适用于处理大量并发请求的场景。
  4. 模块化和包管理:Node.js 支持模块化开发,可以通过 npm(Node Package Manager)进行包的管理和发布,方便了代码的组织和复用。
  5. 强大的社区支持:Node.js 拥有庞大的开发者社区,提供了丰富的第三方模块和工具,方便开发者进行开发和调试。

Node.js 组成

  1. 用户代码:JS 代码,开发者编写的
  2. 第三方库:大部分仍然是 JS 代码,由其他开发者编写
  3. 本地模块:Node.js 内置了一些核心模块,这些模块提供了基础的功能,如文件操作(fs 模块)、网络通信(http 模块)、加密(crypto 模块)、操作系统信息(os 模块)等。这些模块可以直接通过 require 函数进行引入使用。
  4. 内置模块:Node.js 有一个丰富的第三方模块生态系统,开发者可以通过 NPM 安装这些模块,并在自己的项目中引入使用。
  5. libuv:libuv 是一个跨平台的异步 I/O 库,它为 Node.js 提供了非阻塞的事件驱动的 I/O 操作。它可以处理文件系统操作、网络请求、定时器等等,在 Node.js 中用于处理事件循环。
  6. os api:将 Node.js 可运行于多个操作系统平台,包括 Windows、Linux 和 Mac OS 等。
  7. V8 引擎:Node.js 使用了 Google 开发的 V8 引擎作为其 JavaScript 执行引擎。V8 引擎可以将 JavaScript 代码直接转化为机器码,以提供高性能的执行效率。(c/c++代码,作用:把 JS 代码解释成为机器码。可以通过 v8 引擎的某种机制,扩展其功能。V8 引擎的扩展和对扩展的编译,是通过一个工具:gyp 工具。某些第三方库需要使用 node-gyp 工具进行构建,因此需要先安装 node-gyp)

其他资料

https://github.com/theanarkh/understand-nodejs

',10),t=[s];function l(r,c,h,p,_,j){return o(),d("div",null,t)}const u=e(n,[["render",l]]);export{g as __pageData,u as default}; diff --git a/assets/front-end-engineering_node.md.78e6017c.lean.js b/assets/front-end-engineering_node.md.78e6017c.lean.js new file mode 100644 index 00000000..1b089436 --- /dev/null +++ b/assets/front-end-engineering_node.md.78e6017c.lean.js @@ -0,0 +1 @@ +import{_ as e,o,c as d,a}from"./app.f983686f.js";const i="/blog/assets/2024-06-16-15-36-15.5742d5bf.png",g=JSON.parse('{"title":"Node 组成原理","description":"","frontmatter":{},"headers":[{"level":2,"title":"Node.js 的特点包括:","slug":"node-js-的特点包括","link":"#node-js-的特点包括","children":[]},{"level":2,"title":"Node.js 组成","slug":"node-js-组成","link":"#node-js-组成","children":[]},{"level":2,"title":"其他资料","slug":"其他资料","link":"#其他资料","children":[]}],"relativePath":"front-end-engineering/node.md","lastUpdated":1718523698000}'),n={name:"front-end-engineering/node.md"},s=a("",10),t=[s];function l(r,c,h,p,_,j){return o(),d("div",null,t)}const u=e(n,[["render",l]]);export{g as __pageData,u as default}; diff --git a/assets/front-end-engineering_performance.md.c74ee54e.js b/assets/front-end-engineering_performance.md.c74ee54e.js new file mode 100644 index 00000000..3014b379 --- /dev/null +++ b/assets/front-end-engineering_performance.md.c74ee54e.js @@ -0,0 +1,1351 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-01-27-11-37-14.86d37b7f.png",o="/blog/assets/2024-01-27-11-42-53.5bf914cc.png",e="/blog/assets/2024-01-27-11-43-39.3f42a6fd.png",c="/blog/assets/2024-01-27-11-43-48.dc786c83.png",r="/blog/assets/2024-01-27-11-45-28.86477da9.png",t="/blog/assets/2024-01-28-16-38-33.db813e08.png",B="/blog/assets/2024-01-28-16-43-50.943cbfac.png",y="/blog/assets/2024-01-28-16-45-11.6c1597c5.png",i="/blog/assets/2024-01-28-16-52-03.61de8fe4.png",F="/blog/assets/2024-01-28-16-52-40.1ee5db60.png",u="/blog/assets/2024-01-28-17-09-16.389ef971.png",d="/blog/assets/2024-01-28-17-09-22.8a8ef8fe.png",b="/blog/assets/2024-01-28-17-18-12.4f54b024.png",A="/blog/assets/2024-01-28-17-28-28.709acd8d.png",m="/blog/assets/2024-01-28-17-29-13.fbcf0c26.png",C="/blog/assets/2024-01-28-17-30-23.3db19902.png",_=JSON.parse('{"title":"前端性能优化方法论","description":"","frontmatter":{},"headers":[{"level":2,"title":"Webpack 优化","slug":"webpack-优化","link":"#webpack-优化","children":[{"level":3,"title":"1. 构建性能","slug":"_1-构建性能","link":"#_1-构建性能","children":[]},{"level":3,"title":"2. 传输性能","slug":"_2-传输性能","link":"#_2-传输性能","children":[]},{"level":3,"title":"3. 运行性能","slug":"_3-运行性能","link":"#_3-运行性能","children":[]},{"level":3,"title":"4. webpack5 内置优化","slug":"_4-webpack5-内置优化","link":"#_4-webpack5-内置优化","children":[]},{"level":3,"title":"字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化","slug":"字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","link":"#字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","children":[]}]},{"level":2,"title":"CSS","slug":"css","link":"#css","children":[]},{"level":2,"title":"网络层面","slug":"网络层面","link":"#网络层面","children":[{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"CDN","slug":"cdn","link":"#cdn","children":[]},{"level":3,"title":"增加带宽","slug":"增加带宽","link":"#增加带宽","children":[]},{"level":3,"title":"http 内置优化","slug":"http-内置优化","link":"#http-内置优化","children":[]},{"level":3,"title":"数据传输层面","slug":"数据传输层面","link":"#数据传输层面","children":[]}]},{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[{"level":3,"title":"Vue 开发优化","slug":"vue-开发优化","link":"#vue-开发优化","children":[]},{"level":3,"title":"Vue3 内置优化","slug":"vue3-内置优化","link":"#vue3-内置优化","children":[]},{"level":3,"title":"问题梳理","slug":"问题梳理","link":"#问题梳理","children":[]}]},{"level":2,"title":"React","slug":"react","link":"#react","children":[{"level":3,"title":"总结","slug":"总结-1","link":"#总结-1","children":[]}]},{"level":2,"title":"高性能 JavaScript","slug":"高性能-javascript","link":"#高性能-javascript","children":[{"level":3,"title":"开发注意","slug":"开发注意","link":"#开发注意","children":[]},{"level":3,"title":"懒加载","slug":"懒加载","link":"#懒加载","children":[]},{"level":3,"title":"回流与重绘","slug":"回流与重绘","link":"#回流与重绘","children":[]},{"level":3,"title":"如何对项目中的图片进行优化?","slug":"如何对项目中的图片进行优化","link":"#如何对项目中的图片进行优化","children":[]}]},{"level":2,"title":"优化首屏响应","slug":"优化首屏响应","link":"#优化首屏响应","children":[{"level":3,"title":"觉得快","slug":"觉得快","link":"#觉得快","children":[]},{"level":3,"title":"真实快","slug":"真实快","link":"#真实快","children":[]}]}],"relativePath":"front-end-engineering/performance.md","lastUpdated":1718532121000}'),h={name:"front-end-engineering/performance.md"},g=l('

前端性能优化方法论

https://juejin.cn/post/6993137683841155080

我们可以从两个方面来看性能优化的意义:

  1. 用户角度:网站优化能够让页面加载得更快,响应更加及时,极大提升用户体验。
  2. 服务商角度:优化会减少页面资源请求数,减小请求资源所占带宽大小,从而节省可观的带宽资源。

网站优化的目标就是减少网站加载时间,提高响应速度。 Google 和亚马逊的研究表明,Google 页面加载的时间从 0.4 秒提升到 0.9 秒导致丢失了 20% 流量和广告收入,对于亚马逊,页面加载时间每增加 100ms 就意味着 1% 的销售额损失。 可见,页面的加载速度对于用户有着至关重要的影响。

Webpack 优化

如何分析打包结果?webpack-bundle-analyzer

1. 构建性能

TIP

这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能

优化的目标,是降低从打包开始,到代码效果呈现所经过的时间

构建性能会影响开发效率。构建性能越高,开发过程中时间的浪费越少

1.1 减少模块解析

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

如果某个模块不做解析,该模块经过 loader 处理后的代码就是最终代码。

如果没有 loader 对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间,那么哪些模块不需要解析呢?

模块中无其他依赖:一些已经打包好的第三方库,比如 jquery,所以可以配置 module.noParse,它是一个正则,被正则匹配到的模块不会解析

js
module.exports = {
+  mode: "development",
+  module: {
+    noParse: /jquery/,
+  },
+};
+
module.exports = {
+  mode: "development",
+  module: {
+    noParse: /jquery/,
+  },
+};
+

1.2 优化 loader 性能

  1. 进一步限制 loader 的应用范围。对于某些库,不使用 loader

例如:babel-loader 可以转换 ES6 或更高版本的语法,可是有些库本身就是用 ES5 语法书写的,不需要转换,使用 babel-loader 反而会浪费构建时间 lodash 就是这样的一个库,lodash 是在 ES5 之前出现的库,使用的是 ES3 语法 通过 module.rule.exclude 或 module.rule.include,排除或仅包含需要应用 loader 的场景

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

如果暴力一点,甚至可以排除掉 node_modules 目录中的模块,或仅转换 src 目录的模块

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

这种做法是对 loader 的范围进行进一步的限制,和 noParse 不冲突

  1. 缓存 loader 的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的 loader 解析后,解析后的结果也不变 于是,可以将 loader 的解析结果保存下来,让后续的解析直接使用保存的结果 cache-loader 可以实现这样的功能:第一次打包会慢,因为有缓存的过程,以后就快了

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        use: ["cache-loader", ...loaders],
+      },
+    ],
+  },
+};
+
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\\.js$/,
+        use: ["cache-loader", ...loaders],
+      },
+    ],
+  },
+};
+

有趣的是,cache-loader 放到最前面,却能够决定后续的 loader 是否运行。实际上,loader 的运行过程中,还包含一个过程,即 pitch

cache-loader 还可以实现各自自定义的配置,具体方式见文档

  1. 为 loader 的运行开启多线程

thread-loader 会开启一个线程池,线程池中包含适量的线程

它会把后续的 loader 放到线程池的线程中运行,以提高构建效率。由于后续的 loader 会放到新的线程中,所以,后续的 loader 不能:

  • 使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定 thread-loader 放到什么位置

特别注意,开启和管理线程需要消耗时间,在小型项目中使用 thread-loader 反而会增加构建时间

HappyPack:受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。

HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

js
module: {
+  loaders: [
+    {
+      test: /\\.js$/,
+      include: [resolve('src')],
+      exclude: /node_modules/,
+      // id 后面的内容对应下面
+      loader: 'happypack/loader?id=happybabel'
+    }
+  ]
+},
+plugins: [
+  new HappyPack({
+    id: 'happybabel',
+    loaders: ['babel-loader?cacheDirectory'],
+    // 开启 4 个线程
+    threads: 4
+  })
+]
+
module: {
+  loaders: [
+    {
+      test: /\\.js$/,
+      include: [resolve('src')],
+      exclude: /node_modules/,
+      // id 后面的内容对应下面
+      loader: 'happypack/loader?id=happybabel'
+    }
+  ]
+},
+plugins: [
+  new HappyPack({
+    id: 'happybabel',
+    loaders: ['babel-loader?cacheDirectory'],
+    // 开启 4 个线程
+    threads: 4
+  })
+]
+

1.3 热替换 HMR

热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间 当使用 webpack-dev-server 时,考虑代码改动到效果呈现的过程

原理

  1. 更改配置
js
module.exports = {
+  devServer: {
+    hot: true, // 开启HMR
+  },
+  plugins: [
+    // 可选
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
+
module.exports = {
+  devServer: {
+    hot: true, // 开启HMR
+  },
+  plugins: [
+    // 可选
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
+
  1. 更改代码
js
// index.js
+
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
+}
+
// index.js
+
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
+}
+

首先,这段代码会参与最终运行!当开启了热更新后,webpack-dev-server会向打包结果中注入module.hot属性。默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

但如果运行了module.hot.accept(),将改变这一行为module.hot.accept()的作用是让webpack-dev-server通过 socket 管道,把服务器更新的内容发送到浏览器

然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行 插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行 所以,热替换发生在代码运行期

样式热替换

对于样式也是可以使用热替换的,但需要使用style-loader

因为热替换发生时,HotModuleReplacementPlugin只会简单的重新运行模块代码

因此style-loader的代码一运行,就会重新设置 style 元素中的样式

mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的

思考:webpack 的热更新是如何做到的?说明其原理?

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道 server 端和 client 端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

1.4 其他提升构建性能

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的 npm 包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shakingScope Hoisting 来剔除多余代码

思考:如何利用 webpack 来优化前端性能?(提高性能和体验)

用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS 文件, 利用 cssnano(css-loader?minimize)来压缩 css
  • 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动 webpack 时追加参数--optimize-minimize 来实现
  • 提取公共代码。

思考:如何提高 webpack 的构建速度?

  1. 多入口情况下,使用 CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常用库
  3. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使用 Happypack 实现多线程加速编译
  5. 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
  6. 使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码

2. 传输性能

TIP

传输性能是指,打包后的 JS 代码传输到浏览器经过的时间,在优化传输性能时要考虑到:

  1. 总传输量:所有需要传输的 JS 文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
  2. 文件数量:当访问页面时,需要传输的 JS 文件数量,文件数量越多,http 请求越多,响应速度越慢
  3. 浏览器缓存:JS 文件会被浏览器缓存,被缓存的文件不会再进行传输

2.1 手动分包(极大提升构建性能)

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的 js 代码

我们可以利用 webpack 对动态 import 的支持,从而达到把不同页面的代码打包到不同文件中

js
// routes
+export default [
+  {
+    name: "Home",
+    path: "/",
+    component: () => import(/* webpackChunkName: "home" */ "@/views/Home"),
+  },
+  {
+    name: "About",
+    path: "/about",
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
+];
+
// routes
+export default [
+  {
+    name: "Home",
+    path: "/",
+    component: () => import(/* webpackChunkName: "home" */ "@/views/Home"),
+  },
+  {
+    name: "About",
+    path: "/about",
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
+];
+

什么是分包:将一个整体的代码,分布到不同的打包文件中

什么时候要分包?

  • 多个 chunk 引入了公共模块
  • 公共模块体积较大或较少的变动

基本原理

手动分包的总体思路是:

  1. 先单独的打包公共模块

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构

js
//源码,入口文件index.js
+import $ from "jquery";
+import _ from "lodash";
+_.isArray($(".red"));
+
//源码,入口文件index.js
+import $ from "jquery";
+import _ from "lodash";
+_.isArray($(".red"));
+

由于资源清单中包含 jquery 和 lodash 两个模块,因此打包结果的大致格式是:

js
(function (modules) {
+  //...
+})({
+  // index.js文件的打包结果并没有变化
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
+    _.isArray($(".red"));
+  },
+  // 由于资源清单中存在,jquery的代码并不会出现在这里
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
+  },
+  // 由于资源清单中存在,lodash的代码并不会出现在这里
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = lodash;
+  },
+});
+
(function (modules) {
+  //...
+})({
+  // index.js文件的打包结果并没有变化
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
+    _.isArray($(".red"));
+  },
+  // 由于资源清单中存在,jquery的代码并不会出现在这里
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
+  },
+  // 由于资源清单中存在,lodash的代码并不会出现在这里
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = lodash;
+  },
+});
+
  1. 打包公共模块

打包公共模块是一个独立的打包过程

  1. 单独打包公共模块,暴露变量名 . npm run dll
js
// webpack.dll.config.js
+module.exports = {
+  mode: "production",
+  entry: {
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
+  },
+  output: {
+    filename: "dll/[name].js",
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
+};
+
// webpack.dll.config.js
+module.exports = {
+  mode: "production",
+  entry: {
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
+  },
+  output: {
+    filename: "dll/[name].js",
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
+};
+

利用DllPlugin生成资源清单

js
// webpack.dll.config.js
+module.exports = {
+  plugins: [
+    new webpack.DllPlugin({
+      path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
+};
+
// webpack.dll.config.js
+module.exports = {
+  plugins: [
+    new webpack.DllPlugin({
+      path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
+};
+

运行后,即可完成公共模块打包

使用公共模块

  1. 在页面中手动引入公共模块
html
<script src="./dll/jquery.js"></script>
+<script src="./dll/lodash.js"></script>
+
<script src="./dll/jquery.js"></script>
+<script src="./dll/lodash.js"></script>
+

重新设置clean-webpack-plugin。如果使用了插件clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置

js
new CleanWebpackPlugin({
+  // 要清除的文件或目录
+  // 排除掉dll目录本身和它里面的文件
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
+
new CleanWebpackPlugin({
+  // 要清除的文件或目录
+  // 排除掉dll目录本身和它里面的文件
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
+

目录和文件的匹配规则使用的是globbing patterns语法

使用 DllReferencePlugin 控制打包结果

js
module.exports = {
+  plugins: [
+    // 资源清单
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/jquery.manifest.json"),
+    }),
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    // 资源清单
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/jquery.manifest.json"),
+    }),
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
+

总结

手动打包的过程:

  1. 开启 output.library 暴露公共模块
  2. 用 DllPlugin 创建资源清单
  3. 用 DllReferencePlugin 使用资源清单

手动打包的注意事项:

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共 JS,以及避免被删除
  3. 不要对小型的公共 JS 库使用

优点:

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点:

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想

详解 dllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin 的使用方法如下:

js
// 单独配置在一个文件中
+// webpack.dll.conf.js
+const path = require("path");
+const webpack = require("webpack");
+module.exports = {
+  entry: {
+    // 想统一打包的类库
+    vendor: ["react"],
+  },
+  output: {
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
+  },
+  plugins: [
+    new webpack.DllPlugin({
+      // name 必须和 output.library 一致
+      name: "[name]-[hash]",
+      // 该属性需要与 DllReferencePlugin 中一致
+      context: __dirname,
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
+
// 单独配置在一个文件中
+// webpack.dll.conf.js
+const path = require("path");
+const webpack = require("webpack");
+module.exports = {
+  entry: {
+    // 想统一打包的类库
+    vendor: ["react"],
+  },
+  output: {
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
+  },
+  plugins: [
+    new webpack.DllPlugin({
+      // name 必须和 output.library 一致
+      name: "[name]-[hash]",
+      // 该属性需要与 DllReferencePlugin 中一致
+      context: __dirname,
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
+

然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中

js
// webpack.conf.js
+module.exports = {
+  // ...省略其他配置
+  plugins: [
+    new webpack.DllReferencePlugin({
+      context: __dirname,
+      // manifest 就是之前打包出来的 json 文件
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
+
// webpack.conf.js
+module.exports = {
+  // ...省略其他配置
+  plugins: [
+    new webpack.DllReferencePlugin({
+      context: __dirname,
+      // manifest 就是之前打包出来的 json 文件
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
+

可以通过一些小的优化点来加快打包速度

  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

2.2 自动分包(会降低构建效率,开发效率提升,新的模块不需要手动处理了)

  1. 基本原理

不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack 会自动的按照策略进行分包

实际上,webpack 在内部是使用 SplitChunksPlugin 进行分包的 过去有一个库 CommonsChunkPlugin 也可以实现分包,不过由于该库某些地方并不完善,到了 webpack4 之后,已被 SplitChunksPlugin 取代

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack 开启了一个新的 chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新 chunk 的产物
  1. 分包策略的基本配置

webpack 提供了 optimization 配置项,用于配置一些优化信息

其中 splitChunks 是分包策略的配置

js
module.exports = {
+  optimization: {
+    splitChunks: {
+      // 分包策略
+    },
+  },
+};
+
module.exports = {
+  optimization: {
+    splitChunks: {
+      // 分包策略
+    },
+  },
+};
+

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

chunks

该配置项用于配置需要应用分包策略的 chunk

我们知道,分包是从已有的 chunk 中分离出新的 chunk,那么哪些 chunk 需要分离呢

chunks 有三个取值,分别是:

  • all: 对于所有的 chunk 都要应用分包策略
  • async:【默认】仅针对异步 chunk 应用分包策略
  • initial:仅针对普通 chunk 应用分包策略

所以,你只需要配置 chunks 为 all 即可

maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则 webpack 会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和 tree shaking

  1. 分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新 chunk 名称的分隔符,默认值~

  • minChunks:一个模块被多少个 chunk 使用时,才会进行分包,默认值 1。如果我自己写一个文件,默认也不分包,因为自己写的那个太小,没达到拆分的条件,所以要配合 minSize 使用。

  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值 30000

  1. 缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack 按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack 提供了两个缓存组:

js
module.exports = {
+  optimization: {
+    splitChunks: {
+      //全局配置
+      cacheGroups: {
+        // 属性名是缓存组名称,会影响到分包的chunk名
+        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
+        vendors: {
+          test: /[\\\\/]node_modules[\\\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+        },
+        default: {
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
+          priority: -20, // 优先级
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
+
module.exports = {
+  optimization: {
+    splitChunks: {
+      //全局配置
+      cacheGroups: {
+        // 属性名是缓存组名称,会影响到分包的chunk名
+        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
+        vendors: {
+          test: /[\\\\/]node_modules[\\\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+        },
+        default: {
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
+          priority: -20, // 优先级
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
+

很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了

但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离

js
module.exports = {
+  optimization: {
+    splitChunks: {
+      chunks: "all",
+      cacheGroups: {
+        styles: {
+          // 样式抽离
+          test: /\\.css$/, // 匹配样式模块
+          minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
+  },
+  module: {
+    rules: [
+      { test: /\\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
+  },
+  plugins: [
+    new CleanWebpackPlugin(),
+    new HtmlWebpackPlugin({
+      template: "./public/index.html",
+      chunks: ["index"],
+    }),
+    new MiniCssExtractPlugin({
+      filename: "[name].[hash:5].css",
+      // chunkFilename是配置来自于分割chunk的文件名
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
+
module.exports = {
+  optimization: {
+    splitChunks: {
+      chunks: "all",
+      cacheGroups: {
+        styles: {
+          // 样式抽离
+          test: /\\.css$/, // 匹配样式模块
+          minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
+  },
+  module: {
+    rules: [
+      { test: /\\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
+  },
+  plugins: [
+    new CleanWebpackPlugin(),
+    new HtmlWebpackPlugin({
+      template: "./public/index.html",
+      chunks: ["index"],
+    }),
+    new MiniCssExtractPlugin({
+      filename: "[name].[hash:5].css",
+      // chunkFilename是配置来自于分割chunk的文件名
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
+
  1. 配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个 html 页面指定需要的 chunk,否则都会引入进去,这就造成了问题

js
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
+
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
+

我们必须手动的指定被分离出去的 chunk 名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

shell
npm i -D html-webpack-plugin@next
+
npm i -D html-webpack-plugin@next
+

做出以下配置即可:

js
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index"],
+});
+
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index"],
+});
+

它会自动的找到被 index 分离出去的 chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

  1. 原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  • 检查每个 chunk 编译的结果
  • 根据分包策略,找到那些满足策略的模块
  • 根据分包策略,生成新的 chunk 打包这些模块(代码有所变化)
  • 把打包出去的模块从原始包中移除,并修正原始包代码

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量 webpackJsonp,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码

2.3 代码压缩

单模块体积优化

  1. 为什么要进行代码压缩: 减少代码体积;破坏代码的可读性,提升破解成本;
  2. 什么时候要进行代码压缩: 生产环境
  3. 使用什么压缩工具: 目前最流行的代码压缩工具主要有两个:UglifyJs 和 Terser

UglifyJs 是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持 ES6 语法,所以目前的流行度已有所下降。

Terser 是一个新起的代码压缩工具,支持 ES6+语法,因此被很多构建工具内置使用。webpack 安装后会内置 Terser,当启用生产环境后即可用其进行代码压缩。

因此,我们选择 Terser

关于副作用 side effect

副作用:函数运行过程中,可能会对外部环境造成影响的功能

如果函数中包含以下代码,该函数叫做副作用函数:

  • 异步代码
  • localStorage
  • 对外部数据的修改

如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做纯函数(pure function)

纯函数非常有利于压缩优化。可以手动指定那些是纯函数:pure_funcs:['Math.random']

Terser

在 Terser 的官网可尝试它的压缩效果

Terser 官网:https://terser.org/

webpack+Terser

webpack 自动集成了 Terser

如果你想更改、添加压缩工具,又或者是想对 Terser 进行配置,使用下面的 webpack 配置即可

js
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
+module.exports = {
+  optimization: {
+    minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
+    ],
+  },
+};
+
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
+module.exports = {
+  optimization: {
+    minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
+    ],
+  },
+};
+

2.4 tree shaking

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

  1. 背景 某些模块导出的代码并不一定会被用到,第三方库就是个典型例子
js
// myMath.js
+export function add(a, b) {
+  console.log("add");
+  return a + b;
+}
+
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
+}
+// index.js
+import { add } from "./myMath";
+console.log(add(1, 2));
+
// myMath.js
+export function add(a, b) {
+  console.log("add");
+  return a + b;
+}
+
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
+}
+// index.js
+import { add } from "./myMath";
+console.log(add(1, 2));
+

tree shaking 用于移除掉不会用到的导出

  1. 使用

webpack2 开始就支持了 tree shaking

只要是生产环境,tree shaking 自动开启

  1. 原理

webpack 会从入口模块出发寻找依赖关系

当解析一个模块时,webpack 会根据 ES6 的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack 之所以选择 ES6 的模块导入语句,是因为 ES6 模块有以下特点:commonjs 不具备

  • 导入导出语句只能是顶层语句
  • import 的模块名只能是字符串常量
  • import 绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack 坚持的原则是:保证代码正常运行,然后再尽量 tree shaking

所以,如果你依赖的是一个导出的对象,由于 JS 语言的动态特性,以及 webpack 还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量:

  • 使用 export xxx 导出,而不使用 export default {xxx}导出。后者会整个导出,但是不一定都需要。
  • 使用 import {xxx} from "xxx"导入,而不使用 import xxx from "xxx"导入

依赖分析完毕后,webpack 会根据每个模块每个导出是否被使用,标记其他导出为 dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些 dead code 代码

  1. 使用第三方库

某些第三方库可能使用的是 commonjs 的方式导出,比如 lodash

又或者没有提供普通的 ES6 方式导出

对于这些库,tree shaking 是无法发挥作用的

因此要寻找这些库的 es6 版本,好在很多流行但没有使用的 ES6 的第三方库,都发布了它的 ES6 版本,比如 lodash-es

  1. 作用域分析

tree shaking 本身并没有完善的作用域分析,可能导致在一些 dead code 函数中的依赖仍然会被视为依赖 比如 a 引用 b,b 引用了 lodash,但是 a 没有用到 b 用 lodash 的导出代码 插件 webpack-deep-scope-plugin 提供了作用域分析,可解决这些问题

  1. 副作用问题

webpack 在 tree shaking 的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何 tree shaking

因此,当 webpack 无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

js
//common.js
+var n = Math.random();
+
+//index.js
+import "./common.js";
+
//common.js
+var n = Math.random();
+
+//index.js
+import "./common.js";
+

虽然我们根本没用有 common.js 的导出,但 webpack 担心 common.js 有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

在 package.json 中加入 sideEffects

json
{
+  "sideEffects": false
+}
+
{
+  "sideEffects": false
+}
+

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些 css 文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是 src/common.js 的文件,都有副作用
js
{
+    "sideEffects": ["!src/common.js"]
+}
+
{
+    "sideEffects": ["!src/common.js"]
+}
+

这种方式我们一般不处理,通常是一些第三方库在它们自己的 package.json 中标注

webpack 无法对 css 完成 tree shaking,因为 css 跟 es6 没有半毛钱关系。

因此对 css 的 tree shaking 需要其他插件完成。例如:purgecss-webpack-plugin。注意:purgecss-webpack-plugin 对 css module 无能为力

2.5 懒加载

可以理解为异步 chunk

js
// 异步加载使用import语法
+const btn = document.querySelector("button");
+btn.onclick = async function () {
+  //动态加载
+  //import 是ES6的草案
+  //浏览器会使用JSOP的方式远程去读取一个js模块
+  //import()会返回一个promise   (返回结果类似于 * as obj)
+  // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
+  const result = chunk([3, 5, 6, 7, 87], 2);
+  console.log(result);
+};
+
+// 因为是动态的,所以tree shaking没了
+// 如果想用该咋办?搞成静态依赖就行 所以加上了util.js
+
// 异步加载使用import语法
+const btn = document.querySelector("button");
+btn.onclick = async function () {
+  //动态加载
+  //import 是ES6的草案
+  //浏览器会使用JSOP的方式远程去读取一个js模块
+  //import()会返回一个promise   (返回结果类似于 * as obj)
+  // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
+  const result = chunk([3, 5, 6, 7, 87], 2);
+  console.log(result);
+};
+
+// 因为是动态的,所以tree shaking没了
+// 如果想用该咋办?搞成静态依赖就行 所以加上了util.js
+

2.6 gzip

gzip 是一种压缩文件的算法

B/S 结构中的压缩传输

  • 浏览器告诉服务器支持那些压缩方式。
  • 响应头:什么方式解压->ungzip

优点:传输效率可能得到大幅提升

缺点:服务器的压缩需要时间,客户端的解压需要时间

gzip 的原理

gizp 压缩是一种 http 请求优化方式,通过减少文件体积来提高加载速度。html、js、css 文件甚至 json 数据都可以用它压缩,可以减小 60%以上的体积。前端配置 gzip 压缩,并且服务端使用 nginx 开启 gzip,用来减小网络传输的流量大小。

使用 webpack 进行预压缩

使用 compression-webpack-plugin 插件对打包结果进行预压缩,可以移除服务器的压缩时间

js
plugins: [
+  // 参考文档配置即可,一般取默认
+  new CmpressionWebpackPlugin({
+    test: /\\.js/, //希望对js进行预压缩
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
+
plugins: [
+  // 参考文档配置即可,一般取默认
+  new CmpressionWebpackPlugin({
+    test: /\\.js/, //希望对js进行预压缩
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
+

2.7 按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 lodash 这种大型类库同样可以使用这个功能。

按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

2.8 其他

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css
  • 利⽤ CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为 CDN 上对应的路径。可以利⽤ webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

提取第三方库 vendor: 这是也是 webpack 大法的 code splitting,提取一些第三方的库,从而减小 app.js 的大小。 代码层面做好懒加载,网络层面把 CDN、本地缓存用好,前端页面问题基本解决一大半了。剩下主要就是接口层面和“视觉上的快”的优化了,骨架屏先搞起,渲染一个“假页面”占位;接口该合并的合并,该拆分的拆分,如果是可滚动的长页面,就分批次请求

总结:如果有一个工程打包特别大-如何进行优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。

3. 运行性能

运行性能是指,JS 代码在浏览器端的运行速度,它主要取决于我们如何书写高性能的代码

永远不要过早的关注于性能,因为你在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率 性能优化主要从上面三个维度入手,性能优化没有完美的解决方案,需要具体情况具体分析

4. webpack5 内置优化

  1. webpack scope hoisting

scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。

在未开启 scope hoisting 时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。

而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。

这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。

  1. 清除输出目录

webpack5清除输出目录开箱可用,无须安装clean-webpack-plugin,具体做法如下:

javascript
module.exports = {
+  output: {
+    clean: true,
+  },
+};
+
module.exports = {
+  output: {
+    clean: true,
+  },
+};
+
  1. top-level-await

webpack5现在允许在模块的顶级代码中直接使用await

javascript
// src/index.js
+const resp = await fetch("http://www.baidu.com");
+const jsonBody = await resp.json();
+export default jsonBody;
+
// src/index.js
+const resp = await fetch("http://www.baidu.com");
+const jsonBody = await resp.json();
+export default jsonBody;
+

目前,top-level-await还未成为正式标准,因此,对于webpack5而言,该功能是作为experiments发布的,需要在webpack.config.js中配置开启

javascript
// webpack.config.js
+module.exports = {
+  experiments: {
+    topLevelAwait: true,
+  },
+};
+
// webpack.config.js
+module.exports = {
+  experiments: {
+    topLevelAwait: true,
+  },
+};
+
  1. 打包体积优化

webpack5对模块的合并、作用域提升、tree shaking等处理更加智能

javascript
// webpack.config.js
+module.exports = {
+  mode: "production",
+  devtool: "source-map",
+  entry: {
+    index1: "./src/index1.js",
+    index2: "./src/index2.js",
+  },
+};
+
// webpack.config.js
+module.exports = {
+  mode: "production",
+  devtool: "source-map",
+  entry: {
+    index1: "./src/index1.js",
+    index2: "./src/index2.js",
+  },
+};
+

  1. 打包缓存开箱即用

webpack4中,需要使用cache-loader缓存打包结果以优化之后的打包性能

而在webpack5中,默认就已经开启了打包缓存,无须再安装cache-loader

默认情况下,webpack5是将模块的打包结果缓存到内存中,可以通过cache配置进行更改

javascript
const path = require("path");
+
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  cache: {
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
+    // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
+  },
+};
+
const path = require("path");
+
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  cache: {
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
+    // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
+  },
+};
+

关于cache的更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache

  1. 资源模块

webpack4中,针对资源型文件我们通常使用file-loaderurl-loaderraw-loader进行处理

由于大部分前端项目都会用到资源型文件,因此webpack5原生支持了资源型模块

详见:https://webpack.docschina.org/guides/asset-modules/

javascript
// index.js
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
+
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
+
// index.js
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
+
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
+
javascript
// webpack.config.js
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  devServer: {
+    port: 8080,
+  },
+  plugins: [new HtmlWebpackPlugin()],
+  output: {
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
+  },
+  module: {
+    rules: [
+      {
+        test: /\\.png/,
+        type: "asset/resource", // 作用类似于 file-loader
+      },
+      {
+        test: /\\.jpg/,
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
+      },
+      {
+        test: /\\.txt/,
+        type: "asset/source", // 作用类似于 raw-loader
+      },
+      {
+        test: /\\.gif/,
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        generator: {
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
+        },
+        parser: {
+          dataUrlCondition: {
+            maxSize: 4 * 1024, // 4kb以下使用 data uri
+            // 4kb以上就是文件
+          },
+        },
+      },
+    ],
+  },
+};
+
// webpack.config.js
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  devServer: {
+    port: 8080,
+  },
+  plugins: [new HtmlWebpackPlugin()],
+  output: {
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
+  },
+  module: {
+    rules: [
+      {
+        test: /\\.png/,
+        type: "asset/resource", // 作用类似于 file-loader
+      },
+      {
+        test: /\\.jpg/,
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
+      },
+      {
+        test: /\\.txt/,
+        type: "asset/source", // 作用类似于 raw-loader
+      },
+      {
+        test: /\\.gif/,
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        generator: {
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
+        },
+        parser: {
+          dataUrlCondition: {
+            maxSize: 4 * 1024, // 4kb以下使用 data uri
+            // 4kb以上就是文件
+          },
+        },
+      },
+    ],
+  },
+};
+

字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化

  1. 对传输性能的优化
  • 压缩和混淆 使用 Uglifyjs 或其他类似工具对打包结果进行压缩、混淆,可以有效的减少包体积
  • tree shaking 项目中尽量使用 ESM,可以有效利用 tree shaking 优化,降低包体积
  • 抽离公共模块 将一些公共代码单独打包,这样可以充分利用浏览器缓存,其他代码变动后,不影响公共代码,浏览器可以直接从缓存中找到公共代码。 具体方式有多种,比如 dll、splitChunks
  • 异步加载 对一些可以延迟执行的模块可以使用动态导入的方式异步加载它们,这样在打包结果中,它们会形成单独的包,同时,在页面一开始解析时并不需要加载它们,而是页面解析完成后,执行 JS 的过程中去加载它们。 这样可以显著提高页面的响应速度,在单页应用中尤其有用。
  • CDN 对一些知名的库使用 CDN,不仅可以节省打包时间,还可以显著提升库的加载速度
  • gzip 目前浏览器普遍支持 gzip 格式,因此可以将静态文件均使用 gzip 进行压缩
  • 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
  1. 对打包过程的优化
  • noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  • externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  • 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  • 开启 loader 缓存 可以利用 cache-loader 缓存 loader 的编译结果,避免在源码没有变动时反复编译
  • 开启多线程编译 可以利用 thread-loader 开启多线程编译,提升编译效率
  • 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库
  1. 对开发体验的优化
  • lint 使用 eslint、stylelint 等工具保证团队代码风格一致
  • HMR 使用热替换避免页面刷新导致的状态丢失,提升开发体验

CSS

TIP

CSS 渲染性能优化

  1. 使用 id selector 非常的高效。在使用 id selector 的时候需要注意一点:因为 id 是唯一的,所以不需要既指定 id 又指定 tagName:
css
/* Bad  */
+p#id1 {
+  color: red;
+}
+
+/* Good  */
+#id1 {
+  color: red;
+}
+
/* Bad  */
+p#id1 {
+  color: red;
+}
+
+/* Good  */
+#id1 {
+  color: red;
+}
+
  1. 不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配非常慢。更不要这样写:p[id="id1"]。这样将 id selector 退化成 attribute selector。
css
/* Bad  */
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
+/* Good  */
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
+
/* Bad  */
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
+/* Good  */
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
+
  1. 通常将浏览器前缀置于前面,将标准样式属性置于最后,类似:
css
.foo {
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+
.foo {
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+

这里推荐参阅 CSS 规范-优化方案:http://nec.netease.com/standard/css-optimize.html

  1. 遵守 CSSLint 规则

font-faces         不能使用超过 5 个 web 字体

import            禁止使用@import

regex-selectors      禁止使用属性选择器中的正则表达式选择器

universal-selector       禁止使用通用选择器*

unqualified-attributes    禁止使用不规范的属性选择器

zero-units        0 后面不要加单位

overqualified-elements    使用相邻选择器时,不要使用不必要的选择器

shorthand          简写样式属性

duplicate-background-images 相同的 url 在样式表中不超过一次

更多的 CSSLint 规则可以参阅:https://github.com/CSSLint/csslint

  1. 不要使用 @import

使用 @import 引入 CSS 会影响浏览器的并行下载。使用 @import 引用的 CSS 文件只有在引用它的那个 CSS 文件被下载、解析之后,浏览器才会知道还有另外一个 CSS 需要下载,这时才去下载,然后下载后开始解析、构建 Render Tree 等一系列操作。

多个 @import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在 @import 后面的 JS 文件先于 @import 下载,并且打乱甚至破坏 @import 自身的并行下载。

  1. 避免过分重排(Reflow)

所谓重排就是浏览器重新计算布局位置与大小。常见的重排元素:

width
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
+min-height
+
width
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
+min-height
+
  1. 依赖继承。如果某些属性可以继承,那么自然没有必要在写一遍。

  2. 其他:使用 id 选择器非常高效,因为 id 是唯一的;使用渐进增强的方案;值缩写;避免耗性能的属性;背景图优化合并;文件压缩

网络层面

总结

  • 优化打包体积:利用一些工具压缩、混淆最终打包代码,减少包体积
  • 多目标打包:利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积
  • 压缩:现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的
  • CDN:利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存
  • 缓存:对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存
  • http2:开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据
  • 雪碧图:对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的
  • defer、async:通过 defer 和 async 属性,可以让页面尽早加载 js 文件
  • prefetch、preload:通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源;通过 preload 属性,可以让页面预先下载本页面可能要用到的资源
  • 多个静态资源域:对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载 (http2 之前,浏览器开多个 tcp,同一个域下最大数 6 个,为了多,静态资源分多个域存储,突破 6 个的限制)

CDN

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的 CDN 系统构成:

  • 分发服务系统: 最基本的工作单元就是 Cache 设备,cache(边缘 cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时 cache 还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache 设备的数量、规模、总服务能力是衡量一个 CDN 系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的 cache 的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • 运营管理系统: 运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。

作用

CDN 一般会用来托管 Web 资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用 CDN 来加速这些资源的访问。

(1)在性能方面,引入 CDN 的作用在于:

  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
  • 部分资源请求分配给了 CDN,减少了服务器的负载

(2)在安全方面,CDN 有助于防御 DDoS、MITM 等网络攻击:

  • 针对 DDoS:通过监控分析异常流量,限制其请求频率
  • 针对 MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN 作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。 CDN 还会把文件最小化或者压缩文档的优化

原理

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

CDN 和 DNS 有着密不可分的联系,先来看一下 DNS 的解析域名过程,在浏览器输入 www.test.com 的解析过程如下:

  1. 检查浏览器缓存
  2. 检查操作系统缓存,常见的如 hosts 文件
  3. 检查路由器缓存
  4. 如果前几步都没没找到,会向 ISP(网络服务提供商)的 LDNS 服务器查询
  5. 如果 LDNS 服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
  • 根服务器返回顶级域名(TLD)服务器如.com,.cn,.org 等的地址,该例子中会返回.com 的地址
  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test 的地址
  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标 IP,本例子会返回www.test.com的地址
  • Local DNS Server 会缓存结果,并返回给用户,缓存在系统中

CDN 的工作原理:

  1. 用户未使用 CDN 缓存资源的过程:
  2. 浏览器通过 DNS 对域名进行解析(就是上面的 DNS 解析过程),依次得到此域名对应的 IP 地址
  3. 浏览器根据得到的 IP 地址,向域名的服务主机发送数据请求
  4. 服务器向浏览器返回响应数据

(2)用户使用 CDN 缓存资源的过程:

  1. 对于点击的数据的 URL,经过本地 DNS 系统的解析,发现该 URL 对应的是一个 CDN 专用的 DNS 服务器,DNS 系统就会将域名解析权交给 CNAME 指向的 CDN 专用的 DNS 服务器。
  2. CND 专用 DNS 服务器将 CND 的全局负载均衡设备 IP 地址返回给用户
  3. 用户向 CDN 的全局负载均衡设备发起数据请求
  4. CDN 的全局负载均衡设备根据用户的 IP 地址,以及用户请求的内容 URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求
  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的 IP 地址返回给全局负载均衡设备
  6. 全局负载均衡设备把服务器的 IP 地址返回给用户
  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。

CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的 IP 地址,或者该域名的一个 CNAME,然后再根据这个 CNAME 来查找对应的 IP 地址。

使用场景

  • 使用第三方的 CDN 服务:如果想要开源一些项目,可以使用第三方的 CDN 服务
  • 使用 CDN 进行静态资源的缓存:将自己网站的静态资源放在 CDN 上,比如 js、css、图片等。可以将整个项目放在 CDN 上,完成一键部署。
  • 直播传送:直播本质上是使用流媒体进行传送,CDN 也是支持流媒体传送的,所以直播完全可以使用 CDN 来提高访问速度。CDN 在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

TODO:cdn 加速原理,没有缓存到哪里拿,CDN 回源策略

分发的内容

静态内容:即使是静态内容也不是一直保存在 cdn,源服务器发送文件给 CDN 的时候就可以利用 HTTP 头部的 cache-control 可以设置文件的缓存形式,cdn 就知道哪些内容可以保存 no-cache,那些不能 no-store,那些保存多久 max-age

动态内容:

工作流程:

静态内容:源服务器把静态内容提前备份给 cdn(push),世界各地访问的时候就进的 cdn 服务器会把静态内容提供给用户,不需要每次劳烦源服务器。如果没有提前备份,cdn 问源服务器要(pull),然后 cdn 备份,其他请球的用户可以马上拿到。

动态内容:源服务器很难做到提前预测到每个用户的动态内容提前给到 cdn,如果等到用户索取动态内容 cdn 再向源服务器获取,这样 cdn 提供不了加速服务。但是有些是可以提供动态服务的:时间,有些 cdn 会提供可以运行在 cdn 上的接口,让源服务器用这些 cdn 接口,而不是源服务器自己的代码,用户就可以直接从 cdn 获取时间

问:cdn 用什么方式来转移流量实现负载均衡?

和 DNS 域名解析根服务器的做法相似:任播通信:服务器对外都拥有同一个 ip 地址,如果收到了请求,请求就会由距离用户最近的服务器响应,任播技术把流量转移给没超载的服务器可以缓解。CDN 还会用 TLS/SSL 证书对网站进行保护。 我们可以把项目中的所有静态资源都放到 CDN 上(收费),也可以利用现成免费的 CDN 获取公共库的资源

js

+首先我们需要告诉webpack不要对公共库进行打包
+// vue.config.js
+module.exports = {
+  configureWebpack: {
+    externals: {
+      vue: "Vue",
+      vuex: "Vuex",
+      "vue-router": "VueRouter",
+    }
+  },
+};
+然后在页面中手动加入cdn链接这里使用bootcn
+对于vuex和vue-router使用这种传统的方式引入的话会自动成为Vue的插件因此需要去掉Vue.use(xxx)
+
+我们可以使用下面的代码来进行兼容
+// store.js
+import Vue from "vue";
+import Vuex from "vuex";
+
+if(!window.Vuex){
+  // 没有使用传统的方式引入Vuex
+  Vue.use(Vuex);
+}
+
+// router.js
+import VueRouter from "vue-router";
+import Vue from "vue";
+
+if(!window.VueRouter){
+  // 没有使用传统的方式引入VueRouter
+  Vue.use(VueRouter);
+}
+

+首先,我们需要告诉webpack不要对公共库进行打包
+// vue.config.js
+module.exports = {
+  configureWebpack: {
+    externals: {
+      vue: "Vue",
+      vuex: "Vuex",
+      "vue-router": "VueRouter",
+    }
+  },
+};
+然后,在页面中手动加入cdn链接,这里使用bootcn
+对于vuex和vue-router,使用这种传统的方式引入的话会自动成为Vue的插件,因此需要去掉Vue.use(xxx)
+
+我们可以使用下面的代码来进行兼容
+// store.js
+import Vue from "vue";
+import Vuex from "vuex";
+
+if(!window.Vuex){
+  // 没有使用传统的方式引入Vuex
+  Vue.use(Vuex);
+}
+
+// router.js
+import VueRouter from "vue-router";
+import Vue from "vue";
+
+if(!window.VueRouter){
+  // 没有使用传统的方式引入VueRouter
+  Vue.use(VueRouter);
+}
+

增加带宽

增加带宽可以提高资源的访问速度,从而提高首批的加载速度,我司项目带宽由 2M 升级到 5M,效果明显。

http 内置优化

  • Http2
    • 头部压缩:专门的 HPACK 压缩算法
      • 索引表:客户端和服务器共同维护的一张表,表的内容分为 61 位的静态表(保存常用信息,例如:host/content-type)和动态表
      • 霍夫曼编码
  • 链路复用
    • Http1 建立起 Tcp 连接,发送请求之后,服务器在处理请求的等待期间,这个期间又没有数据去发送,称为空挡期。链接断开是在服务器响应回溯之后
      • keep-alive 链接保持一段时间
      • HTTP2 可以利用空档期
      • 不需要再重复建立链接
  • 二进制帧
    • Http1.1 文本字符分割的数据流,解析慢且容易出错
    • 二进制帧:帧长度、帧类型、帧标识 补充:采用 Http2 之后,可以减少资源合并的操作,因为首部压缩已经减少了多请求传输的数据量

数据传输层面

  • 缓存:浏览器缓存
    • 强缓存
      • cache-contorl: max-age=30
      • expires: Wed, 21 Oct 2021 07:28:00 GMT
  • 协商缓存
    • etag
    • last-modified
    • if-modified-since
    • if-none-match
  • 压缩
    • 数据压缩:gzip
    • 代码文件压缩:HTML/CSS/JS 中的注释、空格、长变量等
    • 静态资源:字体图标,去除元数据,缩小尺寸以及分辨率
    • 头与报文
      • http1.1 中减少不必要的头
      • 减少 cookie 数据量

Vue

Vue 开发优化

使用 key

对于通过循环生成的列表,应给每个列表项一个稳定且唯一的 key,这有利于在列表变动时,尽量少的删除、新增、改动元素

使用冻结的对象

冻结的对象不会被响应化

使用函数式组件

参见函数式组件

使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们

非实时绑定的表单项

当使用 v-model 绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致 vue 发生重渲染(rerender),这会带来一些性能的开销。

特别是当用户改变表单项时,页面有一些动画正在进行中,由于 JS 执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。

我们可以通过使用 lazy 或不使用 v-model 的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。

保持对象引用稳定

在绝大部分情况下,vue 触发 rerender 的时机是其依赖的数据发生变化

若数据没有发生变化,哪怕给数据重新赋值了,vue 也是不会做出任何处理的

下面是 vue 判断数据没有变化的源码

js
// value 为旧值, newVal 为新值
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
+}
+
// value 为旧值, newVal 为新值
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
+}
+

因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。

对于原始数据类型,保持其值不变即可

对于对象类型,保持其引用不变即可

从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染

使用 v-show 替代 v-if

对于频繁切换显示状态的元素,使用 v-show 可以保证虚拟 dom 树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量 dom 元素的节点,这一点极其重要

关键字:频繁切换显示状态、内部包含大量 dom 元素

使用延迟装载(defer)

首页白屏时间主要受到两个因素的影响:

  • 打包体积过大 巨型包需要消耗大量的传输时间,导致 JS 传输完成前页面只有一个<div>,没有可显示的内容 <div id="app">好看的东西<div>

  • 需要立即渲染的内容太多 JS 传输完成后,浏览器开始执行 JS 构造页面。 但可能一开始要渲染的组件太多,不仅 JS 执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏

打包体积过大需要自行优化打包体积,本节不予讨论

可以进行分包

本节仅讨论渲染内容太多的问题。

一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来

延迟装载是一个思路,本质上就是利用 requestAnimationFrame 事件分批渲染内容,它的具体实现多种多样

使用 keep-alive

keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态(不仅是数据的保留,还要真实 dom 的保留)。

keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。

受 keep-alive 的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后

原理 在具体的实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象

js
// keep-alive 内部的声明周期函数
+created () {
+  this.cache = Object.create(null)
+  this.keys = []
+}
+
+
// keep-alive 内部的声明周期函数
+created () {
+  this.cache = Object.create(null)
+  this.keys = []
+}
+
+

key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值 cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM

在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。 当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素

js
render(){
+  const slot = this.$slots.default; // 获取默认插槽
+  const vnode = getFirstComponentChild(slot); // 得到插槽中的第一个组件的vnode
+  const name = getComponentName(vnode.componentOptions); //获取组件名字
+  const { cache, keys } = this; // 获取当前的缓存对象和key数组
+  const key = ...; // 获取组件的key值,若没有,会按照规则自动生成
+  if (cache[key]) {
+    // 有缓存
+    // 重用组件实例
+    vnode.componentInstance = cache[key].componentInstance
+    remove(keys, key); // 删除key
+    // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
+    keys.push(key);
+  } else {
+    // 无缓存,进行缓存
+    cache[key] = vnode
+    keys.push(key)
+    if (this.max && keys.length > parseInt(this.max)) {
+      // 超过最大缓存数量,移除第一个key对应的缓存
+      pruneCacheEntry(cache, keys[0], keys, this._vnode)
+    }
+  }
+  return vnode;
+}
+
render(){
+  const slot = this.$slots.default; // 获取默认插槽
+  const vnode = getFirstComponentChild(slot); // 得到插槽中的第一个组件的vnode
+  const name = getComponentName(vnode.componentOptions); //获取组件名字
+  const { cache, keys } = this; // 获取当前的缓存对象和key数组
+  const key = ...; // 获取组件的key值,若没有,会按照规则自动生成
+  if (cache[key]) {
+    // 有缓存
+    // 重用组件实例
+    vnode.componentInstance = cache[key].componentInstance
+    remove(keys, key); // 删除key
+    // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
+    keys.push(key);
+  } else {
+    // 无缓存,进行缓存
+    cache[key] = vnode
+    keys.push(key)
+    if (this.max && keys.length > parseInt(this.max)) {
+      // 超过最大缓存数量,移除第一个key对应的缓存
+      pruneCacheEntry(cache, keys[0], keys, this._vnode)
+    }
+  }
+  return vnode;
+}
+

长列表优化

vue-virtual-scroller

首先这个库在使用上是很方便的,就是它提供了一个标签,相当于是对 div 标签的一个修改,可以实现列表的渲染等等功能

异步组件

在代码层面,vue 组件本质上是一个配置对象

js
var comp = {
+  props: xxx,
+  data: xxx,
+  computed: xxx,
+  methods: xxx,
+};
+
var comp = {
+  props: xxx,
+  data: xxx,
+  computed: xxx,
+  methods: xxx,
+};
+

但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:

  • 需要使用 ajax 获得某个数据之后才能加载该组件
  • 为了合理的分包,组件配置对象需要通过 import(xxx)动态加载

如果一个组件需要通过异步的方式得到组件配置对象,该组件可以把它做成一个异步组件

js
/**
+ * 异步组件本质上是一个函数
+ * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
+ */
+const AsyncComponent = () => import("./MyComp");
+
+var App = {
+  components: {
+    /**
+     * 你可以把该函数当做一个组件使用(异步组件)
+     * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
+     */
+    AsyncComponent,
+  },
+};
+
/**
+ * 异步组件本质上是一个函数
+ * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
+ */
+const AsyncComponent = () => import("./MyComp");
+
+var App = {
+  components: {
+    /**
+     * 你可以把该函数当做一个组件使用(异步组件)
+     * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
+     */
+    AsyncComponent,
+  },
+};
+

异步组件的函数不仅可以返回一个 Promise,还支持返回一个对象

应用:异步组件通常应用在路由懒加载中,以达到更好的分包;为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息

js
var routes = [
+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
+
var routes = [
+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
+

Vue3 内置优化

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
js

+// vue2 的静态节点
+render(){
+  createVNode("h1", null, "Hello World")
+  // ...
+}
+
+// vue3 的静态节点
+const hoisted = createVNode("h1", null, "Hello World")//这个节点永远创建一次
+function render(){
+  // 直接使用 hoisted 即可
+}
+

+// vue2 的静态节点
+render(){
+  createVNode("h1", null, "Hello World")
+  // ...
+}
+
+// vue3 的静态节点
+const hoisted = createVNode("h1", null, "Hello World")//这个节点永远创建一次
+function render(){
+  // 直接使用 hoisted 即可
+}
+

静态属性会被提升

js
<div class="user">
+  {{user.name}}
+</div>
+
+const hoisted = { class: "user" }
+
+function render(){
+  createVNode("div", hoisted, user.name)
+  // ...
+}
+
<div class="user">
+  {{user.name}}
+</div>
+
+const hoisted = { class: "user" }
+
+function render(){
+  createVNode("div", hoisted, user.name)
+  // ...
+}
+

预字符串化

js

+<div class="menu-bar-container">
+  <div class="logo">
+    <h1>logo</h1>
+  </div>
+  <ul class="nav">
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+  </ul>
+  <div class="user">
+    <span>{{ user.name }}</span>
+  </div>
+</div>
+

+<div class="menu-bar-container">
+  <div class="logo">
+    <h1>logo</h1>
+  </div>
+  <ul class="nav">
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+  </ul>
+  <div class="user">
+    <span>{{ user.name }}</span>
+  </div>
+</div>
+

当编译器遇到大量连续(少量则不会,目前是至少 20 个连续节点)的静态内容,会直接将其编译为一个普通字符串节点

js
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+

对 ssr 的作用非常明显

缓存事件处理函数

js
<button @click="count++">plus</button>
+
+// vue2
+render(ctx){
+  return createVNode("button", {
+    onClick: function($event){
+      ctx.count++;
+    }
+  })
+}
+
+// vue3
+render(ctx, _cache){
+  return createVNode("button", {//事件处理函数不变,可以缓存一下,保证事件处理函数只生成一次
+    onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
+  })
+}
+
+
<button @click="count++">plus</button>
+
+// vue2
+render(ctx){
+  return createVNode("button", {
+    onClick: function($event){
+      ctx.count++;
+    }
+  })
+}
+
+// vue3
+render(ctx, _cache){
+  return createVNode("button", {//事件处理函数不变,可以缓存一下,保证事件处理函数只生成一次
+    onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
+  })
+}
+
+

Block Tree

vue2 在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

Block 节点记录了那些是动态的节点,对比的时候只对比动态节点

html
<form>
+  <div>
+    <label>账号:</label>
+    <input v-model="user.loginId" />
+  </div>
+  <div>
+    <label>密码:</label>
+    <input v-model="user.loginPwd" />
+  </div>
+</form>
+
<form>
+  <div>
+    <label>账号:</label>
+    <input v-model="user.loginId" />
+  </div>
+  <div>
+    <label>密码:</label>
+    <input v-model="user.loginPwd" />
+  </div>
+</form>
+

编译器 会把所有的动态节点标记,存到到根节点的数组中 ,到时候对比的时候只对比 block 动态节点。如果树不稳定,会有其他方案。

PatchFlag

依托于 vue3 强大的编译器。vue2 在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

js
<div class="user" data-id="1" title="user name">
+  {{user.name}}
+</div>
+
<div class="user" data-id="1" title="user name">
+  {{user.name}}
+</div>
+

标识:

  • 标识 1:代表元素内容是动态的
  • 标识 2:Class
  • 标识 3:class+text

启用现代模式

为了兼容各种浏览器,vue-cli 在内部使用了@babel/present-env 对代码进行降级,你可以通过.browserlistrc 配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的 polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用 webpack 进行多次打包外,还可以利用 vue-cli 给我们提供的命令:

shell
vue-cli-service build --modern
+
vue-cli-service build --modern
+

问题梳理

如何实现 vue 项目中的性能优化?

编码阶段

  • 尽量减少 data 中的数据,data 中的数据都会增加 gettersetter,会收集对应的 watcher
  • v-ifv-for 不能连用
  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
  • SPA 页面采用 keep-alive 缓存组件
  • 在更多的情况下,使用 v-if 替代 v-show
  • key 保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO 优化

  • 预渲染
  • 服务端渲染 SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用 cdn 加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 优化

用户体验

  • 骨架屏
  • PWA

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。

vue 中的 spa 应用如何优化首屏加载速度?

优化首屏加载可以从这几个方面开始:

  • 请求优化:CDN 将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
  • 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
  • gzip:开启 gzip 压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。
  • http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。
  • 懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件
  • 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加 loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
  • 合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
  • 使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
  • 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
  • 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
  • 图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
  • 使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
  • 压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片

React

总结

shouldComponentUpdate 提供了两个参数 nextProps 和 nextState,表示下一次 props 和一次 state 的值,当函数返回 false 时候,render()方法不执行,组件也就不会渲染,返回 true 时,组件照常重渲染。此方法就是拿当前 props 中值和下一次 props 中的值进行对比,数据相等时,返回 false,反之返回 true。

需要注意,在进行新旧对比的时候,是**浅对比,**也就是说如果比较的数据时引用数据类型,只要数据的引用的地址没变,即使内容变了,也会被判定为 true。

面对这个问题,可以使用如下方法进行解决: (1)使用 setState 改变数据之前,先采用 ES6 中 assgin 进行拷贝,但是 assgin 只深拷贝的数据的第一层,所以说不是最完美的解决办法:

javascript
const o2 = Object.assign({}, this.state.obj);
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+
const o2 = Object.assign({}, this.state.obj);
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+

(2)使用 JSON.parse(JSON.stringfy())进行深拷贝,但是遇到数据为 undefined 和函数时就会错。

javascript
const o2 = JSON.parse(JSON.stringify(this.state.obj));
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+
const o2 = JSON.parse(JSON.stringify(this.state.obj));
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。

当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

避免不必要的 render

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:

  • shouldComponentUpdate 和 PureComponent

在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。

  • 利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

  • 使用 React.memo

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。

https://juejin.cn/post/6935584878071119885

高性能 JavaScript

开发注意

遵循严格模式:"use strict"

将 JavaScript 本放在页面底部,加快渲染页面

将 JavaScript 脚本将脚本成组打包,减少请求

使用非阻塞方式下载 JavaScript 脚本

尽量使用局部变量来保存全局变量

尽量减少使用闭包

使用 window 对象属性方法时,省略 window

尽量减少对象成员嵌套

缓存 DOM 节点的访问

通过避免使用 eval() 和 Function() 构造器

给 setTimeout() 和 setInterval() 传递函数而不是字符串作为参数

尽量使用直接量创建对象和数组

最小化重绘 (repaint) 和回流 (reflow)

懒加载

懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。 如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。

懒加载的实现原理

图片的加载是由 src 引起的,当对 src 赋值时,浏览器就会请求图片资源。根据这个原理,我们使用 HTML5 的 data-xxx 属性来储存图片的路径,在需要加载图片的时候,将 data-xxx 中图片的路径赋值给 src,这样就实现了图片的按需加载,即懒加载。

注意:data-xxx 中的 xxx 可以自定义,这里我们使用 data-src 来定义。 懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

使用原生 JavaScript 实现懒加载

  1. IntersectionObserver api
  2. window.innerHeight 是浏览器可视区的高度
  3. document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
  4. imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
  5. 图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
html
<div class="container">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+</div>
+<script>
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
+      }
+    }
+  }
+  window.onscroll = lozyLoad();
+</script>
+
<div class="container">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+</div>
+<script>
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
+      }
+    }
+  }
+  window.onscroll = lozyLoad();
+</script>
+

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

参考:https://juejin.cn/post/6844903455048335368#heading-5

回流与重绘

回流(重排)

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。 下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 激活 CSS 伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的 DOM 元素、
  • 操作 class 属性
  • 设置 style 属性:.style....style...----->.class{}

在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的 DOM 元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。

下面这些操作会导致回流:

  • color、background 相关属性:background-color、background-image 等
  • outline 相关属性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。

如何避免回流与重绘?

减少回流与重绘的措施:

  • 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
  • 不要使用 table 布局, 一个小的改动可能会使整个 table 进行重新布局
  • 使用 CSS 的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用 absolute 或者 fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作 DOM,可以创建一个文档片段 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
  • 将元素先设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 将 DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作 DOM,就就会导致页面的性能问题,我们可以将动画的 position 属性设置为 absolute 或者 fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

CPU 中央处理器,擅长逻辑运算

GPU 显卡,擅长图片绘制,高精度的浮点数运算。{家用,专业},尽量少复杂动画,即少了 GPU,烧性能。

在 gpu 层面上操作:改变 opacity 或者 transform:translate3d()/translatez();

最好添加 translatez(0); 小 hack 告诉浏览器告诉浏览器另起一个层

css 1、使用 transform 替代 top 2、使用 visibility 替换 display:none ,因为前者只会引起重绘,后者会引发回流(改变了布局 3、避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。 4、尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。 5、避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。 CSS3 硬件加速(GPU 加速),使用 css3 硬件加速,可以让 transform、opacity、filters 这些动画不会引起回流重绘。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 js 1、避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。 2、避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。 3、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 4、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

新方法:will-change:transform;专门处理 GPU 加速问题

应用:hover 上去后才告诉浏览器要开启新层,点击才触发,总之提前一刻告诉就行

css
div {
+  width: 100px;
+  height: 100px;
+}
+div.hover {
+  will-change: transform;
+}
+div.active {
+  transform: scale(2, 3);
+}
+
div {
+  width: 100px;
+  height: 100px;
+}
+div.hover {
+  will-change: transform;
+}
+div.active {
+  transform: scale(2, 3);
+}
+

浏览器刷新页面的频率 1s 60s

每 16.7mm 刷新一次

gpu 可以再一帧里渲染好页面,那么当你改动页面的元素或者实现动画的时候,将会非常流畅

documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN 中对 documentFragment 的解释:

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的 DOM 操作时,我们就可以将 DOM 元素插入 DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作 DOM 相比,将 DocumentFragment 节点插入 DOM 树时,不会触发页面的重绘,这样就大大提高了页面的性能。

防抖函数

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤ lodash.debounce 节流函数的适⽤场景:
  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
  • 照片使用 JPEG

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以 BMP 格式的图片通常是较大的文件。

(2)GIF 是无损的、采用索引色的点阵图。采用 LZW 压缩算法进行编码。文件小,是 GIF 格式的优点,同时,GIF 格式还具有支持动画以及透明的优点。但是 GIF 格式仅支持 8bit 的索引色,所以 GIF 格式适用于对色彩要求不高同时需要文件体积较小的场景。

(3)JPEG 是有损的、采用直接色的点阵图。JPEG 的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,与 GIF 相比,JPEG 不适合用来存储企业 Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较 GIF 更大。

(4)PNG-8 是无损的、使用索引色的点阵图。PNG 是一种比较新的图片格式,PNG-8 是非常好的 GIF 格式替代者,在可能的情况下,应该尽可能的使用 PNG-8 而不是 GIF,因为在相同的图片效果下,PNG-8 具有更小的文件体积。除此之外,PNG-8 还支持透明度的调节,而 GIF 并不支持。除非需要动画的支持,否则没有理由使用 GIF 而不是 PNG-8。

(5)PNG-24 是无损的、使用直接色的点阵图。PNG-24 的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小要比 BMP 小得多。当然,PNG24 的图片还是要比 JPEG、GIF、PNG-8 大得多。

(6)SVG 是无损的矢量图。SVG 是矢量图意味着 SVG 图片由直线和曲线以及绘制它们的方法组成。当放大 SVG 图片时,看到的还是线和曲线,而不会出现像素点。这意味着 SVG 图片在放大时,不会失真,所以它非常适合用来绘制 Logo、Icon 等。

(7)WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为 Web 而生的,什么叫为 Web 而生呢?就是说相同质量的图片,WebP 具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有 Chrome 浏览器和 Opera 浏览器支持 WebP 格式,兼容性不太好。WebP 图片格式支持图片透明度,一个无损压缩的 WebP 图片,如果要支持透明度只需要 22%的格外文件大小。

优化首屏响应

觉得快

loading

vue 页面需要通过 js 构建,因此在 js 下载到本地之前,页面上什么也没有 一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到 js 下载到本地并运行后,即会自动替换

nprogress

源码分析地址:https://blog.csdn.net/qq_31968791/article/details/106790179 使用到的库是什么 nprogress 进度条的实现原理知道吗 Nprogress 的原理非常简单,就是页面启动的时候,构建一个方法,创建一个 div,然后这个 div 靠近最顶部,用 fixed 定位住,至于样式就是按照自个或者默认走了。 怎么使用这个库的 主要采用的两个方法是 nprogress.start 和 nprogress.done 如何使用: 在请求拦截器中调用 nprogress.start 在响应拦截器中调用 nprogress.done

betterScroll

https://blog.csdn.net/weixin_37719279/article/details/82084342 使用的库是什么:better-scroll

骨架屏

骨架屏的原理:https://blog.csdn.net/csdn_yudong/article/details/103909178

你能说说为啥使用骨架屏吗?

现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 但即便如此,首屏的加载依然还是存在这个加载以及渲染的等待时间问题; 现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 目前主流,常见的解决方案是使用骨架屏技术,包括很多原生的 APP,在页面渲染时,也会使用骨架屏。(下图中,红圈中的部分,即为骨架屏在内容还没有出现之前的页面骨架填充,以免留白)

骨架屏的要怎么使用呢?骨架屏的原理知道吗?

  1. 在 index.html 中的 div#app 中来实现骨架屏,程序渲染后就会替换掉 index.html 里面的 div#app 骨架屏内容;
  2. 使用一个 Base64 的图片来作为骨架屏

使用图片作为骨架屏; 简单暴力,让 UI 同学花点功夫吧;小米商城的移动端页面采用的就是这个方法,它是使用了一个 Base64 的图片来作为骨架屏。 按照方案一的方案,将这个 Base64 的图片写在我们的 index.html 模块中的 div#app 里面。

  1. 使用 .vue 文件来完成骨架屏

真实快

webpack 怎么进行首屏加载的优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
`,593),E=[g];function k(q,v,f,D,w,x){return n(),a("div",null,E)}const P=s(h,[["render",k]]);export{_ as __pageData,P as default}; diff --git a/assets/front-end-engineering_performance.md.c74ee54e.lean.js b/assets/front-end-engineering_performance.md.c74ee54e.lean.js new file mode 100644 index 00000000..900d7d6e --- /dev/null +++ b/assets/front-end-engineering_performance.md.c74ee54e.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-01-27-11-37-14.86d37b7f.png",o="/blog/assets/2024-01-27-11-42-53.5bf914cc.png",e="/blog/assets/2024-01-27-11-43-39.3f42a6fd.png",c="/blog/assets/2024-01-27-11-43-48.dc786c83.png",r="/blog/assets/2024-01-27-11-45-28.86477da9.png",t="/blog/assets/2024-01-28-16-38-33.db813e08.png",B="/blog/assets/2024-01-28-16-43-50.943cbfac.png",y="/blog/assets/2024-01-28-16-45-11.6c1597c5.png",i="/blog/assets/2024-01-28-16-52-03.61de8fe4.png",F="/blog/assets/2024-01-28-16-52-40.1ee5db60.png",u="/blog/assets/2024-01-28-17-09-16.389ef971.png",d="/blog/assets/2024-01-28-17-09-22.8a8ef8fe.png",b="/blog/assets/2024-01-28-17-18-12.4f54b024.png",A="/blog/assets/2024-01-28-17-28-28.709acd8d.png",m="/blog/assets/2024-01-28-17-29-13.fbcf0c26.png",C="/blog/assets/2024-01-28-17-30-23.3db19902.png",_=JSON.parse('{"title":"前端性能优化方法论","description":"","frontmatter":{},"headers":[{"level":2,"title":"Webpack 优化","slug":"webpack-优化","link":"#webpack-优化","children":[{"level":3,"title":"1. 构建性能","slug":"_1-构建性能","link":"#_1-构建性能","children":[]},{"level":3,"title":"2. 传输性能","slug":"_2-传输性能","link":"#_2-传输性能","children":[]},{"level":3,"title":"3. 运行性能","slug":"_3-运行性能","link":"#_3-运行性能","children":[]},{"level":3,"title":"4. webpack5 内置优化","slug":"_4-webpack5-内置优化","link":"#_4-webpack5-内置优化","children":[]},{"level":3,"title":"字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化","slug":"字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","link":"#字节跳动面试题-说一下项目里有做过哪些-webpack-上的优化","children":[]}]},{"level":2,"title":"CSS","slug":"css","link":"#css","children":[]},{"level":2,"title":"网络层面","slug":"网络层面","link":"#网络层面","children":[{"level":3,"title":"总结","slug":"总结","link":"#总结","children":[]},{"level":3,"title":"CDN","slug":"cdn","link":"#cdn","children":[]},{"level":3,"title":"增加带宽","slug":"增加带宽","link":"#增加带宽","children":[]},{"level":3,"title":"http 内置优化","slug":"http-内置优化","link":"#http-内置优化","children":[]},{"level":3,"title":"数据传输层面","slug":"数据传输层面","link":"#数据传输层面","children":[]}]},{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[{"level":3,"title":"Vue 开发优化","slug":"vue-开发优化","link":"#vue-开发优化","children":[]},{"level":3,"title":"Vue3 内置优化","slug":"vue3-内置优化","link":"#vue3-内置优化","children":[]},{"level":3,"title":"问题梳理","slug":"问题梳理","link":"#问题梳理","children":[]}]},{"level":2,"title":"React","slug":"react","link":"#react","children":[{"level":3,"title":"总结","slug":"总结-1","link":"#总结-1","children":[]}]},{"level":2,"title":"高性能 JavaScript","slug":"高性能-javascript","link":"#高性能-javascript","children":[{"level":3,"title":"开发注意","slug":"开发注意","link":"#开发注意","children":[]},{"level":3,"title":"懒加载","slug":"懒加载","link":"#懒加载","children":[]},{"level":3,"title":"回流与重绘","slug":"回流与重绘","link":"#回流与重绘","children":[]},{"level":3,"title":"如何对项目中的图片进行优化?","slug":"如何对项目中的图片进行优化","link":"#如何对项目中的图片进行优化","children":[]}]},{"level":2,"title":"优化首屏响应","slug":"优化首屏响应","link":"#优化首屏响应","children":[{"level":3,"title":"觉得快","slug":"觉得快","link":"#觉得快","children":[]},{"level":3,"title":"真实快","slug":"真实快","link":"#真实快","children":[]}]}],"relativePath":"front-end-engineering/performance.md","lastUpdated":1718532121000}'),h={name:"front-end-engineering/performance.md"},g=l("",593),E=[g];function k(q,v,f,D,w,x){return n(),a("div",null,E)}const P=s(h,[["render",k]]);export{_ as __pageData,P as default}; diff --git "a/assets/front-end-engineering_pnpm\345\216\237\347\220\206.md.ad6b470a.js" "b/assets/front-end-engineering_pnpm\345\216\237\347\220\206.md.ad6b470a.js" new file mode 100644 index 00000000..69717606 --- /dev/null +++ "b/assets/front-end-engineering_pnpm\345\216\237\347\220\206.md.ad6b470a.js" @@ -0,0 +1,11 @@ +import{_ as p,o as a,c as e,a as n}from"./app.f983686f.js";const h=JSON.parse('{"title":"pnpm 原理","description":"","frontmatter":{},"headers":[{"level":3,"title":"1、文件的本质","slug":"_1、文件的本质","link":"#_1、文件的本质","children":[]},{"level":3,"title":"2、文件的拷贝","slug":"_2、文件的拷贝","link":"#_2、文件的拷贝","children":[]},{"level":3,"title":"3、硬链接 hard link","slug":"_3、硬链接-hard-link","link":"#_3、硬链接-hard-link","children":[]},{"level":3,"title":"4、符号链接 symbol link","slug":"_4、符号链接-symbol-link","link":"#_4、符号链接-symbol-link","children":[]},{"level":3,"title":"5、符号链接和硬链接的区别","slug":"_5、符号链接和硬链接的区别","link":"#_5、符号链接和硬链接的区别","children":[]},{"level":3,"title":"6、快捷方式","slug":"_6、快捷方式","link":"#_6、快捷方式","children":[]},{"level":3,"title":"7、node 环境对硬链接和符号链接的处理","slug":"_7、node-环境对硬链接和符号链接的处理","link":"#_7、node-环境对硬链接和符号链接的处理","children":[]},{"level":3,"title":"8、pnpm 原理","slug":"_8、pnpm-原理","link":"#_8、pnpm-原理","children":[]},{"level":2,"title":"其他资料","slug":"其他资料","link":"#其他资料","children":[]}],"relativePath":"front-end-engineering/pnpm原理.md","lastUpdated":1718524249000}'),i={name:"front-end-engineering/pnpm原理.md"},s=n(`

pnpm 原理

想要理解 pnpm 是怎么做的,需要一些操作系统的知识

1、文件的本质

在操作系统中,文件实际上是一个指针,只不过它指向的不是内存地址,而是一个外部存储地址(这里的外部存储可以是硬盘、U 盘、甚至是网络)

当我们删除文件时,删除的实际上是指针,因此,无论删除多么大的文件,速度都非常快。像我们的 U 盘、硬盘里的文件虽然说看起来已经删除了,但是其实数据恢复公司是可以恢复的,因为数据还是存在的,只要删除文件后再没有存储其它文件就可以恢复,所以真正删除一个文件就是可劲存可劲删

2、文件的拷贝

如果你复制一个文件,是将该文件指针指向的内容进行复制,然后产生一个新文件指向新的内容。

硬链接的概念来自于 Unix 操作系统,它是指将一个文件 A 指针复制到另一个文件 B 指针中,文件 B 就是文件 A 的硬链接。

通过硬链接,不会产生额外的磁盘占用,并且,两个文件都能找到相同的磁盘内容。

硬链接的数量没有限制,可以为同一个文件产生多个硬链接。

windows Vista 操作系统开始,支持了创建硬链接的操作,在 cmd 中使用下面的命令可以创建硬链接。

mklink /h  链接名称  目标文件
+
+
mklink /h  链接名称  目标文件
+
+

例:创建一个硬连接

1、首先创建一个文件夹 temp,并且在 temp 文件夹创建一个 article.txt 文本文件

2、接下来,我要在 temp 文件夹的根目录(pnpm 包管理器下面)创建一个硬链接

  • 按 window+R 调出窗口,以管理员身份运行,并且输入 cmd,回车
  • 由于 pnpm 包管理器文件夹在 F 盘,所以先切换到 F 盘,并且进入 pnpm 包管理器地址
  • 输入: mklink /h link.txt temp\\article.txt 回车
  • 此时就可以在编辑器看到新创建的硬链接 link.text,这样·article.txtlink.txt是一样的了
  • 这时当修改 link.txt 时,article.txt 也会跟着变,因为它们指向同一个磁盘空间

注意 ☛:

  1. 由于文件夹(目录)不存在文件内容,所以文件夹(目录)不能创建硬链接
  2. 在 windows 操作系统中,通常不要跨越盘符创建硬链接

符号链接又称为软连接,如果为某个文件或文件夹 A 创建符号连接 B,则 B 指向 A。

windows Vista 操作系统开始,支持了创建符号链接的操作,在 cmd 中使用下面的命令可以创建符号链接:

mklink  /d   链接名称   目标文件
+# /d表示创建的是目录的符号链接,不写则是文件的符号链接
+
+
mklink  /d   链接名称   目标文件
+# /d表示创建的是目录的符号链接,不写则是文件的符号链接
+
+

早期的 windows 系统不支持符号链接,但它提供了一个工具 junction 来达到类似的功能。

5、符号链接和硬链接的区别

  1. 硬链接仅能链接文件,而符号链接可以链接目录
  2. 硬链接在链接完成后仅和文件内容关联,和之前链接的文件没有任何关系。而符号链接始终和之前链接的文件关联,和文件内容不直接相关。

6、快捷方式

快捷方式类似于符号链接,是 windows 系统早期就支持的链接方式。它不仅仅是一个指向其他文件或目录的指针,其中还包含了各种信息:如权限、兼容性启动方式等其他各种属性,由于快捷方式是 windows 系统独有的,在跨平台的应用中一般不会使用。

7、node 环境对硬链接和符号链接的处理

硬链接:

硬链接是一个实实在在的文件,node 不对其做任何特殊处理,也无法区别对待,实际上,node 根本无从知晓该文件是不是一个硬链接

符号链接:

由于符号链接指向的是另一个文件或目录,当 node 执行符号链接下的 JS 文件时,会使用原始路径。比方说:我在 D 盘装了 LOL,在桌面创建了 LOL 快捷方式,相当于是符号链接,双击快捷方式运行游戏,在运行游戏的时候是按照 LOL 原始路径(D 盘路径)运行的。

8、pnpm 原理

pnpm 使用符号链接和硬链接来构建 node_modules 目录

下面用一个例子来说明它的构建方式

假设两个包 a 和 b,a 依赖 b:

假设我们的工程为 proj,直接依赖 a,则安装时,pnpm 会做下面的处理:

  1. 通过 package.json 查询依赖关系,得到最终要安装的包:a 和 b

  2. 在工程 proj 根目录中查看 a 和 b 是否已经有缓存,如果没有,下载到缓存中,如果有,则进入下一步

  3. 在 proj 中创建 node_modules 目录,并对目录进行结构初始化

  4. 从缓存的对应包中使用硬链接放置文件到相应包代码目录中

  5. 使用符号链接,将每个包的直接依赖放置到自己的目录中

    这样做的目的,是为了保证 a 的代码在执行过程中,可以读取到它们的直接依赖

  6. 新版本的 pnpm 为了解决一些书写不规范的包(读取间接依赖)的问题,又将所有的工程非直接依赖,使用符号链接加入到了 .pnpm/node_modules 中。如果 b 依赖 c,a 又要直接用 c,这种不规范的用法现在 pnpm 通过这种方式支持了。但对于那些使用绝对路径的奇葩写法,可能没有办法支持。

  7. 在工程的 node_modules 目录中使用符号链接,放置直接依赖

其他资料

https://juejin.cn/post/6916101419703468045

https://www.bilibili.com/video/BV1tg4y1x75Q/?p=3&spm_id_from=pageDriver&vd_source=5ca956a1f37d0ed72cd1c453c15a3c03

`,42),l=[s];function c(t,o,r,d,b,m){return a(),e("div",null,l)}const u=p(i,[["render",c]]);export{h as __pageData,u as default}; diff --git "a/assets/front-end-engineering_pnpm\345\216\237\347\220\206.md.ad6b470a.lean.js" "b/assets/front-end-engineering_pnpm\345\216\237\347\220\206.md.ad6b470a.lean.js" new file mode 100644 index 00000000..d902bbdd --- /dev/null +++ "b/assets/front-end-engineering_pnpm\345\216\237\347\220\206.md.ad6b470a.lean.js" @@ -0,0 +1 @@ +import{_ as p,o as a,c as e,a as n}from"./app.f983686f.js";const h=JSON.parse('{"title":"pnpm 原理","description":"","frontmatter":{},"headers":[{"level":3,"title":"1、文件的本质","slug":"_1、文件的本质","link":"#_1、文件的本质","children":[]},{"level":3,"title":"2、文件的拷贝","slug":"_2、文件的拷贝","link":"#_2、文件的拷贝","children":[]},{"level":3,"title":"3、硬链接 hard link","slug":"_3、硬链接-hard-link","link":"#_3、硬链接-hard-link","children":[]},{"level":3,"title":"4、符号链接 symbol link","slug":"_4、符号链接-symbol-link","link":"#_4、符号链接-symbol-link","children":[]},{"level":3,"title":"5、符号链接和硬链接的区别","slug":"_5、符号链接和硬链接的区别","link":"#_5、符号链接和硬链接的区别","children":[]},{"level":3,"title":"6、快捷方式","slug":"_6、快捷方式","link":"#_6、快捷方式","children":[]},{"level":3,"title":"7、node 环境对硬链接和符号链接的处理","slug":"_7、node-环境对硬链接和符号链接的处理","link":"#_7、node-环境对硬链接和符号链接的处理","children":[]},{"level":3,"title":"8、pnpm 原理","slug":"_8、pnpm-原理","link":"#_8、pnpm-原理","children":[]},{"level":2,"title":"其他资料","slug":"其他资料","link":"#其他资料","children":[]}],"relativePath":"front-end-engineering/pnpm原理.md","lastUpdated":1718524249000}'),i={name:"front-end-engineering/pnpm原理.md"},s=n("",42),l=[s];function c(t,o,r,d,b,m){return a(),e("div",null,l)}const u=p(i,[["render",c]]);export{h as __pageData,u as default}; diff --git a/assets/front-end-engineering_theme.md.dffc6011.js b/assets/front-end-engineering_theme.md.dffc6011.js new file mode 100644 index 00000000..c1fe21c4 --- /dev/null +++ b/assets/front-end-engineering_theme.md.dffc6011.js @@ -0,0 +1,663 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"主题切换","description":"","frontmatter":{},"headers":[{"level":2,"title":"前端工程中样式切换的解决方案","slug":"前端工程中样式切换的解决方案","link":"#前端工程中样式切换的解决方案","children":[]},{"level":2,"title":"通过 css 变量实现主题切换及工程化处理","slug":"通过-css-变量实现主题切换及工程化处理","link":"#通过-css-变量实现主题切换及工程化处理","children":[{"level":3,"title":"使用持久化存储","slug":"使用持久化存储","link":"#使用持久化存储","children":[]}]},{"level":2,"title":"跟随系统主题","slug":"跟随系统主题","link":"#跟随系统主题","children":[]}],"relativePath":"front-end-engineering/theme.md","lastUpdated":1715066864000}'),p={name:"front-end-engineering/theme.md"},o=l(`

主题切换

思路:

前端主题切换,其实就是切换 css

而且,css 中变换最大的,就是颜色相关的属性,图片,icon....

前端工程中样式切换的解决方案

1、css 变量 + css 样式【data-set [data-name='dark']】class='dark'

javascript
.primary{
+
+  --vt-c-bg:#fff;
+}
+
+.dark{
+  --vt-c-bg:#000
+}
+
+可以使用js点击某个按钮之后切换根元素的css样式样式切换之后意味着css变量的值改变了
+.layer{
+  background-color:var(--vt-c-bg);
+}
+
.primary{
+
+  --vt-c-bg:#fff;
+}
+
+.dark{
+  --vt-c-bg:#000
+}
+
+可以使用js,点击某个按钮之后,切换根元素的css样式,样式切换之后,意味着css变量的值改变了
+.layer{
+  background-color:var(--vt-c-bg);
+}
+

css 变量,是当我们点击某个按钮的时候做切换,可能如果变换比较大内容较多,会出现白屏的情况

2、css 预处理器(scss,less,styuls...) + css 样式 [data-set]

就是使用 css 预处理器强大的预编译功能,简单来说,就是把 css 先写好

javascript
html.primary .layer{
+  color:#fff
+}
+
+html.dark .layer{
+  color:#000
+}
+html.primary .container{}
+
+html.dark .container{}
+
+html.primary .link:hover{}
+html.dark .link:hover{}
+
html.primary .layer{
+  color:#fff
+}
+
+html.dark .layer{
+  color:#000
+}
+html.primary .container{}
+
+html.dark .container{}
+
+html.primary .link:hover{}
+html.dark .link:hover{}
+

css 预编译器,一开始,把所有的主题样式涉及到的内容全部写好,当切换的时候,就直接切换了

这种的方式,可能牺牲的是,一开始的白屏时间

通过 css 变量实现主题切换及工程化处理

使用持久化存储

1、安装 pinia 和持久化插件

js
npm install pinia pinia-plugin-persistedstate
+
npm install pinia pinia-plugin-persistedstate
+

2、创建 pinia

javascript
//stores/index.ts
+import { createPinia } from "pinia";
+import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
+
+// pinia persist
+const pinia = createPinia();
+pinia.use(piniaPluginPersistedstate);
+
+export default pinia;
+
//stores/index.ts
+import { createPinia } from "pinia";
+import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
+
+// pinia persist
+const pinia = createPinia();
+pinia.use(piniaPluginPersistedstate);
+
+export default pinia;
+

3、main.ts 中引用

javascript
import { createApp } from "vue";
+import "./style.css";
+import App from "./App.vue";
+// pinia store
+import pinia from "@/stores";
+
+createApp(App).use(pinia).mount("#app");
+
import { createApp } from "vue";
+import "./style.css";
+import App from "./App.vue";
+// pinia store
+import pinia from "@/stores";
+
+createApp(App).use(pinia).mount("#app");
+

4、设置 pinia 持久化配置

javascript
// config/piniaPersist.ts
+import { PersistedStateOptions } from "pinia-plugin-persistedstate";
+
+/**
+ * @description pinia 持久化参数配置
+ * @param {String} key 存储到持久化的 name
+ * @param {Array} paths 需要持久化的 state name
+ * @return persist
+ * */
+const piniaPersistConfig = (key: string, paths?: string[]) => {
+  const persist: PersistedStateOptions = {
+    key,
+    storage: localStorage,
+    // storage: sessionStorage,
+    paths,
+  };
+  return persist;
+};
+
+export default piniaPersistConfig;
+
// config/piniaPersist.ts
+import { PersistedStateOptions } from "pinia-plugin-persistedstate";
+
+/**
+ * @description pinia 持久化参数配置
+ * @param {String} key 存储到持久化的 name
+ * @param {Array} paths 需要持久化的 state name
+ * @return persist
+ * */
+const piniaPersistConfig = (key: string, paths?: string[]) => {
+  const persist: PersistedStateOptions = {
+    key,
+    storage: localStorage,
+    // storage: sessionStorage,
+    paths,
+  };
+  return persist;
+};
+
+export default piniaPersistConfig;
+

5、创建 global.ts

javascript
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState, ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: (): GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
+  persist: piniaPersistConfig("dy-global"),
+});
+
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState, ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: (): GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
+  persist: piniaPersistConfig("dy-global"),
+});
+

6、设置 actions 统一的的函数

diff
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState,ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: ():GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
++  actions: {
++    // Set GlobalState
++    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
++      this.$patch({ [args[0]]: args[1] });
++    }
++  },
+  persist: piniaPersistConfig("dy-global")
+});
+
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState,ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: ():GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
++  actions: {
++    // Set GlobalState
++    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
++      this.$patch({ [args[0]]: args[1] });
++    }
++  },
+  persist: piniaPersistConfig("dy-global")
+});
+

7、setGlobalState 函数参数的设定

javascript
export interface GlobalState {
+  isDark: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+
export interface GlobalState {
+  isDark: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+

8、修改 useTheme

javascript
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+
+export const useTheme = () => {
+
+  const globalStore = useGlobalStore();
+  const { isDark } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+    }
+    const html = document.documentElement as HTMLElement;
+    console.log(isDark.value)
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+
+  return {
+    switchTheme,
+    initTheme
+  }
+}
+
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+
+export const useTheme = () => {
+
+  const globalStore = useGlobalStore();
+  const { isDark } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+    }
+    const html = document.documentElement as HTMLElement;
+    console.log(isDark.value)
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+
+  return {
+    switchTheme,
+    initTheme
+  }
+}
+

9、界面调用

javascript
import { useTheme } from "@/hooks/useTheme";
+const { initTheme, switchTheme } = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  // const theme = document.documentElement.className
+  // document.documentElement.className = theme === 'primary' ? 'dark' : 'primary'
+  switchTheme();
+};
+
import { useTheme } from "@/hooks/useTheme";
+const { initTheme, switchTheme } = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  // const theme = document.documentElement.className
+  // document.documentElement.className = theme === 'primary' ? 'dark' : 'primary'
+  switchTheme();
+};
+

跟随系统主题

首先,使用媒体查询,可以实现跟随系统主题的效果,其实只有lightdark两种颜色

javascript
body{
+  margin: 0;
+  padding: 0;
+}
+
+.container{
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  transition:color .5s, background-color .5s;
+  color:#000;
+  background-color:#fff;
+}
+.layout{
+  display: flex;
+  justify-content: center;
+}
+
+.layer{
+  width: 300px;
+  height: 300px;
+  padding: 8px;
+  margin: 10px;
+  border:1px solid #000;
+}
+@media (prefers-color-scheme: dark) {
+  .container{
+    color:#fff;
+    background-color:#000;
+  }
+  .layer{
+    border:1px solid #fff;
+  }
+}
+
body{
+  margin: 0;
+  padding: 0;
+}
+
+.container{
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  transition:color .5s, background-color .5s;
+  color:#000;
+  background-color:#fff;
+}
+.layout{
+  display: flex;
+  justify-content: center;
+}
+
+.layer{
+  width: 300px;
+  height: 300px;
+  padding: 8px;
+  margin: 10px;
+  border:1px solid #000;
+}
+@media (prefers-color-scheme: dark) {
+  .container{
+    color:#fff;
+    background-color:#000;
+  }
+  .layer{
+    border:1px solid #fff;
+  }
+}
+

我们可以通过api - window.matchMedia,来获取当前系统主题是深色还是浅色

javascript
window.matchMedia("(prefers-color-scheme: dark)");
+
window.matchMedia("(prefers-color-scheme: dark)");
+

有了这个 API,我们完全就没必要修改之前的 css 代码,直接 js 判断

javascript
const media = window.matchMedia("(prefers-color-scheme: dark)");
+
+const followSystem = () => {
+  if (media.matches) {
+    document.documentElement.className = "dark";
+  } else {
+    document.documentElement.className = "primary";
+  }
+};
+
+followSystem();
+
+media.addEventListener("change", followSystem);
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
+
+const followSystem = () => {
+  if (media.matches) {
+    document.documentElement.className = "dark";
+  } else {
+    document.documentElement.className = "primary";
+  }
+};
+
+followSystem();
+
+media.addEventListener("change", followSystem);
+

直接在 vue3 项目中处理,添加界面系统主题图标,点击激活图标,表示激活跟随系统主题

1、添加图标

html
<button>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 16 16"
+    fill="none"
+    role="img"
+  >
+    <circle cx="8" cy="8" r="7.25" stroke="#5B5B66" stroke-width="1.5" />
+    <mask
+      id="a"
+      style="mask-type:alpha"
+      maskUnits="userSpaceOnUse"
+      x="0"
+      y="0"
+      width="16"
+      height="16"
+    >
+      <circle
+        cx="8"
+        cy="8"
+        r="7.25"
+        fill="#5B5B66"
+        stroke="#5B5B66"
+        stroke-width="1.5"
+      />
+    </mask>
+    <g mask="url(#a)">
+      <path fill="#5B5B66" d="M0 0h8v16H0z" />
+    </g>
+  </svg>
+</button>
+
+css .os-default{ background-color: transparent; border: none; padding: 0; color:
+var(--main-color); display: flex; justify-content: center; align-items: center;
+width: 20px; height: 20px; } .os-default-active{ background-color: transparent;
+border: 1px dotted var(--main-color); padding: 0px; color: var(--main-color);
+display: flex; justify-content: center; align-items: center; width: 20px;
+height: 20px; }
+
<button>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 16 16"
+    fill="none"
+    role="img"
+  >
+    <circle cx="8" cy="8" r="7.25" stroke="#5B5B66" stroke-width="1.5" />
+    <mask
+      id="a"
+      style="mask-type:alpha"
+      maskUnits="userSpaceOnUse"
+      x="0"
+      y="0"
+      width="16"
+      height="16"
+    >
+      <circle
+        cx="8"
+        cy="8"
+        r="7.25"
+        fill="#5B5B66"
+        stroke="#5B5B66"
+        stroke-width="1.5"
+      />
+    </mask>
+    <g mask="url(#a)">
+      <path fill="#5B5B66" d="M0 0h8v16H0z" />
+    </g>
+  </svg>
+</button>
+
+css .os-default{ background-color: transparent; border: none; padding: 0; color:
+var(--main-color); display: flex; justify-content: center; align-items: center;
+width: 20px; height: 20px; } .os-default-active{ background-color: transparent;
+border: 1px dotted var(--main-color); padding: 0px; color: var(--main-color);
+display: flex; justify-content: center; align-items: center; width: 20px;
+height: 20px; }
+

2、仓库添加属性

diff
export const useGlobalStore = defineStore({
+  id: "dy-global",
+  state: () => ({
+    // 深色模式
+    isDark: false,
++    // 系统颜色是否被激活
++    osThemeActive: false
+  }),
+  getters: {},
+  actions: {
+    // Set GlobalState
+    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
+      this.$patch({ [args[0]]: args[1] });
+    }
+  },
+  persist: piniaPersistConfig("dy-global")
+});
+
export const useGlobalStore = defineStore({
+  id: "dy-global",
+  state: () => ({
+    // 深色模式
+    isDark: false,
++    // 系统颜色是否被激活
++    osThemeActive: false
+  }),
+  getters: {},
+  actions: {
+    // Set GlobalState
+    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
+      this.$patch({ [args[0]]: args[1] });
+    }
+  },
+  persist: piniaPersistConfig("dy-global")
+});
+

添加了属性,就必须修改 GlobalState 的 ts 配置

diff
export interface GlobalState {
+  isDark: boolean;
++  osThemeActive: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+
export interface GlobalState {
+  isDark: boolean;
++  osThemeActive: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+

3、点击修改 css 样式

javascript
import { useTheme } from '@/hooks/useTheme'
+import { useGlobalStore } from "@/stores/modules/global";
+
+const globalStore = useGlobalStore();
+
+const {initTheme,switchTheme,activeOSTheme} = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  switchTheme()
+}
+
+const osThemeSwitch = () => {
+  activeOSTheme();
+}
+
+//html
+<button :class="globalStore.osThemeActive ? 'os-default-active' : 'os-default'">
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" role="img" @click="osThemeSwitch">
+	......省略
+  </svg>
+</button>
+
import { useTheme } from '@/hooks/useTheme'
+import { useGlobalStore } from "@/stores/modules/global";
+
+const globalStore = useGlobalStore();
+
+const {initTheme,switchTheme,activeOSTheme} = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  switchTheme()
+}
+
+const osThemeSwitch = () => {
+  activeOSTheme();
+}
+
+//html
+<button :class="globalStore.osThemeActive ? 'os-default-active' : 'os-default'">
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" role="img" @click="osThemeSwitch">
+	......省略
+  </svg>
+</button>
+

4、修改 useTheme.ts

javascript
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+import { watchEffect } from "vue";
+
+const media = window.matchMedia('(prefers-color-scheme: dark)');
+
+export const useTheme = () => {
+  const globalStore = useGlobalStore();
+  const { isDark,osThemeActive } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    //init参数为true时,表示是初始化页面或者直接跟随系统主题
+    //这个时候不需要手动切换明暗主题,也不需要将跟随系统主题设置为false
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+      //如果是直接点击切换明暗主题,那么跟随系统主题激活切换为false
+      globalStore.setGlobalState("osThemeActive", false);
+    }
+    const html = document.documentElement as HTMLElement;
+
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+
+  //点击激活需要改变osThemeActive的值
+  const activeOSTheme = () => {
+    globalStore.setGlobalState("osThemeActive", !osThemeActive.value);
+    followSystem();
+  }
+
+  const followSystem = () => {
+    const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+    //跟随系统主题设置明暗
+    globalStore.setGlobalState("isDark", isOsDark);
+    switchTheme(true);
+  }
+
+  watchEffect(() => {
+    //系统主题切换,需要监听change事件
+    if (osThemeActive.value) {
+      media.addEventListener('change', followSystem);
+    }
+    else {
+      media.removeEventListener('change', followSystem);
+    }
+  })
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+  return {
+    switchTheme,
+    initTheme,
+    activeOSTheme
+  }
+}
+
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+import { watchEffect } from "vue";
+
+const media = window.matchMedia('(prefers-color-scheme: dark)');
+
+export const useTheme = () => {
+  const globalStore = useGlobalStore();
+  const { isDark,osThemeActive } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    //init参数为true时,表示是初始化页面或者直接跟随系统主题
+    //这个时候不需要手动切换明暗主题,也不需要将跟随系统主题设置为false
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+      //如果是直接点击切换明暗主题,那么跟随系统主题激活切换为false
+      globalStore.setGlobalState("osThemeActive", false);
+    }
+    const html = document.documentElement as HTMLElement;
+
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+
+  //点击激活需要改变osThemeActive的值
+  const activeOSTheme = () => {
+    globalStore.setGlobalState("osThemeActive", !osThemeActive.value);
+    followSystem();
+  }
+
+  const followSystem = () => {
+    const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+    //跟随系统主题设置明暗
+    globalStore.setGlobalState("isDark", isOsDark);
+    switchTheme(true);
+  }
+
+  watchEffect(() => {
+    //系统主题切换,需要监听change事件
+    if (osThemeActive.value) {
+      media.addEventListener('change', followSystem);
+    }
+    else {
+      media.removeEventListener('change', followSystem);
+    }
+  })
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+  return {
+    switchTheme,
+    initTheme,
+    activeOSTheme
+  }
+}
+

所有源码均在:https://github.com/Sunny-117/blog/tree/main/code/theme-switch

`,52),e=[o];function c(t,r,B,y,i,F){return n(),a("div",null,e)}const b=s(p,[["render",c]]);export{u as __pageData,b as default}; diff --git a/assets/front-end-engineering_theme.md.dffc6011.lean.js b/assets/front-end-engineering_theme.md.dffc6011.lean.js new file mode 100644 index 00000000..66a9d205 --- /dev/null +++ b/assets/front-end-engineering_theme.md.dffc6011.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"主题切换","description":"","frontmatter":{},"headers":[{"level":2,"title":"前端工程中样式切换的解决方案","slug":"前端工程中样式切换的解决方案","link":"#前端工程中样式切换的解决方案","children":[]},{"level":2,"title":"通过 css 变量实现主题切换及工程化处理","slug":"通过-css-变量实现主题切换及工程化处理","link":"#通过-css-变量实现主题切换及工程化处理","children":[{"level":3,"title":"使用持久化存储","slug":"使用持久化存储","link":"#使用持久化存储","children":[]}]},{"level":2,"title":"跟随系统主题","slug":"跟随系统主题","link":"#跟随系统主题","children":[]}],"relativePath":"front-end-engineering/theme.md","lastUpdated":1715066864000}'),p={name:"front-end-engineering/theme.md"},o=l("",52),e=[o];function c(t,r,B,y,i,F){return n(),a("div",null,e)}const b=s(p,[["render",c]]);export{u as __pageData,b as default}; diff --git a/assets/front-end-engineering_webpack5-mf.md.5dfd34aa.js b/assets/front-end-engineering_webpack5-mf.md.5dfd34aa.js new file mode 100644 index 00000000..46d419b1 --- /dev/null +++ b/assets/front-end-engineering_webpack5-mf.md.5dfd34aa.js @@ -0,0 +1,103 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-57-09.02129cc3.png",e="/blog/assets/2023-01-06-16-58-00.54f4410f.png",m=JSON.parse('{"title":"webpack5 模块联邦","description":"","frontmatter":{},"headers":[{"level":3,"title":"示例","slug":"示例","link":"#示例","children":[]},{"level":3,"title":"暴露自身模块","slug":"暴露自身模块","link":"#暴露自身模块","children":[]},{"level":3,"title":"使用对方暴露的模块","slug":"使用对方暴露的模块","link":"#使用对方暴露的模块","children":[]},{"level":3,"title":"共享模块","slug":"共享模块","link":"#共享模块","children":[]}],"relativePath":"front-end-engineering/webpack5-mf.md","lastUpdated":1706438015000}'),o={name:"front-end-engineering/webpack5-mf.md"},c=l('

webpack5 模块联邦

在大型项目中,往往会把项目中的某个区域或功能模块作为单独的项目开发,最终形成「微前端」架构

在微前端架构中,不同的工程可能出现下面的场景

这涉及到很多非常棘手的问题:

  • 如何避免公共模块重复打包
  • 如何将某个项目中一部分模块分享出去,同时还要避免重复打包
  • 如何管理依赖的不同版本
  • 如何更新模块
  • ......

webpack5尝试着通过模块联邦来解决此类问题

示例

现有两个微前端工程,它们各自独立开发、测试、部署,但它们有一些相同的公共模块,并有一些自己的模块需要分享给其他工程使用,同时又要引入其他工程的模块。

暴露自身模块

如果一个项目需要把一部分模块暴露给其他项目使用,可以使用webpack5的模块联邦将这些模块暴露出去

javascript
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 模块联邦的名称
+      // 该名称将成为一个全部变量,通过该变量将可获取当前联邦的所有暴露模块
+      name: "home", 
+      // 模块联邦生成的文件名,全部变量将置入到该文件中
+      filename: "home-entry.js",
+      // 模块联邦暴露的所有模块
+      exposes: {
+        // key:相对于模块联邦的路径
+        // 这里的 ./Timer 将决定该模块的访问路径为 home/Timer
+        // value: 模块的具体路径
+        "./Timer": "./src/Timer.js",
+      },
+    }),
+  ]
+}
+
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 模块联邦的名称
+      // 该名称将成为一个全部变量,通过该变量将可获取当前联邦的所有暴露模块
+      name: "home", 
+      // 模块联邦生成的文件名,全部变量将置入到该文件中
+      filename: "home-entry.js",
+      // 模块联邦暴露的所有模块
+      exposes: {
+        // key:相对于模块联邦的路径
+        // 这里的 ./Timer 将决定该模块的访问路径为 home/Timer
+        // value: 模块的具体路径
+        "./Timer": "./src/Timer.js",
+      },
+    }),
+  ]
+}
+

使用对方暴露的模块

在模块联邦的配置中,不仅可以暴露自身模块,还可以使用其他项目暴露的模块

javascript
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 远程使用其他项目暴露的模块
+      remotes: {
+        // key: 自定义远程暴露的联邦名
+        // 比如为 abc, 则之后引用该联邦的模块则使用 import "abc/模块名"
+        // value: 模块联邦名@模块联邦访问地址
+        // 远程访问时,将从下面的地址加载
+        home: "home@http://localhost:8080/home-entry.js",
+      },
+    }),
+  ]
+}
+
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 远程使用其他项目暴露的模块
+      remotes: {
+        // key: 自定义远程暴露的联邦名
+        // 比如为 abc, 则之后引用该联邦的模块则使用 import "abc/模块名"
+        // value: 模块联邦名@模块联邦访问地址
+        // 远程访问时,将从下面的地址加载
+        home: "home@http://localhost:8080/home-entry.js",
+      },
+    }),
+  ]
+}
+

共享模块

不同的项目可能使用了一些公共的第三方库,可以将这些第三方库作为共享模块,避免反复打包

javascript
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      shared: ["jquery", "lodash"]
+    }),
+  ]
+}
+
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      shared: ["jquery", "lodash"]
+    }),
+  ]
+}
+

webpack会根据需要从合适的位置引入合适的版本

`,20),r=[c];function t(B,i,y,F,u,b){return n(),a("div",null,r)}const A=s(o,[["render",t]]);export{m as __pageData,A as default}; diff --git a/assets/front-end-engineering_webpack5-mf.md.5dfd34aa.lean.js b/assets/front-end-engineering_webpack5-mf.md.5dfd34aa.lean.js new file mode 100644 index 00000000..a7026712 --- /dev/null +++ b/assets/front-end-engineering_webpack5-mf.md.5dfd34aa.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-57-09.02129cc3.png",e="/blog/assets/2023-01-06-16-58-00.54f4410f.png",m=JSON.parse('{"title":"webpack5 模块联邦","description":"","frontmatter":{},"headers":[{"level":3,"title":"示例","slug":"示例","link":"#示例","children":[]},{"level":3,"title":"暴露自身模块","slug":"暴露自身模块","link":"#暴露自身模块","children":[]},{"level":3,"title":"使用对方暴露的模块","slug":"使用对方暴露的模块","link":"#使用对方暴露的模块","children":[]},{"level":3,"title":"共享模块","slug":"共享模块","link":"#共享模块","children":[]}],"relativePath":"front-end-engineering/webpack5-mf.md","lastUpdated":1706438015000}'),o={name:"front-end-engineering/webpack5-mf.md"},c=l("",20),r=[c];function t(B,i,y,F,u,b){return n(),a("div",null,r)}const A=s(o,[["render",t]]);export{m as __pageData,A as default}; diff --git "a/assets/front-end-engineering_webpack\345\270\270\347\224\250\346\213\223\345\261\225.md.5a4ef0f9.js" "b/assets/front-end-engineering_webpack\345\270\270\347\224\250\346\213\223\345\261\225.md.5a4ef0f9.js" new file mode 100644 index 00000000..7118dff1 --- /dev/null +++ "b/assets/front-end-engineering_webpack\345\270\270\347\224\250\346\213\223\345\261\225.md.5a4ef0f9.js" @@ -0,0 +1,205 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-01-47.0a0026be.png",b=JSON.parse('{"title":"webpack常用拓展","description":"","frontmatter":{},"headers":[{"level":2,"title":"清除输出目录","slug":"清除输出目录","link":"#清除输出目录","children":[]},{"level":2,"title":"自动生成页面","slug":"自动生成页面","link":"#自动生成页面","children":[]},{"level":2,"title":"复制静态资源","slug":"复制静态资源","link":"#复制静态资源","children":[]},{"level":2,"title":"开发服务器","slug":"开发服务器","link":"#开发服务器","children":[]},{"level":2,"title":"普通文件处理","slug":"普通文件处理","link":"#普通文件处理","children":[]},{"level":2,"title":"解决路径问题","slug":"解决路径问题","link":"#解决路径问题","children":[]},{"level":2,"title":"webpack内置插件","slug":"webpack内置插件","link":"#webpack内置插件","children":[{"level":3,"title":"DefinePlugin","slug":"defineplugin","link":"#defineplugin","children":[]},{"level":3,"title":"BannerPlugin","slug":"bannerplugin","link":"#bannerplugin","children":[]},{"level":3,"title":"ProvidePlugin","slug":"provideplugin","link":"#provideplugin","children":[]}]}],"relativePath":"front-end-engineering/webpack常用拓展.md","lastUpdated":1706436523000}'),e={name:"front-end-engineering/webpack常用拓展.md"},o=l(`

webpack常用拓展

清除输出目录

clean-webpack-plugin 当文件内容变化,重新打包,会自动删除原来打包的文件

js
module.exports = {
+    plugins: [
+        new CleanWebpackPlugin()
+    ],
+}
+
module.exports = {
+    plugins: [
+        new CleanWebpackPlugin()
+    ],
+}
+

自动生成页面

html-webpack-plugin

解决的问题:用打包后的js文件时,需要自行创建html文件,引入。但是当js文件变化,再次打包,生成的压缩文件名字变化了,并且用了clean-webpack-plugin插件,清楚了dist目录,把我写的html也删除了。 配置:

  1. 自己定义模版,让他按照模版生成页面
js
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "html/index.html"
+        })
+    ],
+}
+
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "html/index.html"
+        })
+    ],
+}
+
  1. 多入口(chunk)。最终生成的html会把两个打包后的js入口都引入。
js
entry:{
+    home: './src/index.js',
+    a: './src/a.js'
+}
+
entry:{
+    home: './src/index.js',
+    a: './src/a.js'
+}
+

可以配置进行选择性引入

js
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            chunks: ["home"] // 只使用home打包出来的js
+        })
+    ],
+}
+
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            chunks: ["home"] // 只使用home打包出来的js
+        })
+    ],
+}
+

生成多个html

js
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "home.html",
+            chunks: ['home']
+        }),
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "a.html",
+            chunks: ['a']
+        })
+    ],
+}
+
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "home.html",
+            chunks: ['home']
+        }),
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "a.html",
+            chunks: ['a']
+        })
+    ],
+}
+

复制静态资源

copy-webpack-plugin

js
module.exports = {
+    plugins: [
+        new CopyPlugin([
+            {from: 'source', to: 'dest'},
+            {from: 'other', to: 'public'},
+        ])
+    ]
+}
+
module.exports = {
+    plugins: [
+        new CopyPlugin([
+            {from: 'source', to: 'dest'},
+            {from: 'other', to: 'public'},
+        ])
+    ]
+}
+

开发服务器

开发阶段,目前遇到的问题是打包、运行、调试过程过于繁琐,回顾一下我们的操作流程:

  1. 编写代码
  2. 控制台运行命令完成打包
  3. 打开页面查看效果
  4. 继续编写代码,回到步骤2

并且,我们往往希望把最终生成的代码和页面部署到服务器上,来模拟真实环境

为了解决这些问题,webpack官方制作了一个单独的库:webpack-dev-server

既不是plugin也不是loader

先来看看它怎么用

  1. 安装
  2. 执行npx webpack-dev-server命令

webpack-dev-server命令几乎支持所有的webpack命令参数,如--config-env等等,你可以把它当作webpack命令使用

这个命令是专门为开发阶段服务的,真正部署的时候还是得使用webpack命令

当我们执行webpack-dev-server命令后,它做了以下操作:

  1. 内部执行webpack命令,传递命令参数
  2. 开启watch
  3. 注册hooks:类似于plugin,webpack-dev-server会向webpack中注册一些钩子函数,主要功能如下:
    • 将资源列表(aseets)保存起来
    • 禁止webpack输出文件
  4. 用express开启一个服务器,监听某个端口,当请求到达后,根据请求的路径,给予相应的资源内容

配置

针对webpack-dev-server的配置,参考:dev-server

常见配置有:

  • port:配置监听端口
  • proxy:配置代理,常用于跨域访问
  • stats:配置控制台输出内容

普通文件处理

file-loader: 生成依赖的文件到输出目录,然后将模块文件设置为:导出一个路径\\

javascript
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 生成一个具有相同文件内容的文件到输出目录
+	// 2. 返回一段代码   export default "文件名"
+}
+
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 生成一个具有相同文件内容的文件到输出目录
+	// 2. 返回一段代码   export default "文件名"
+}
+

url-loader:将依赖的文件转换为:导出一个base64格式的字符串,不生成文件

javascript
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 根据buffer生成一个base64编码
+	// 2. 返回一段代码   export default "base64编码"
+}
+
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 根据buffer生成一个base64编码
+	// 2. 返回一段代码   export default "base64编码"
+}
+
javascript
 module: {
+        rules: [{
+            test: /\\.(png)|(gif)|(jpg)$/,
+            use: [{
+                loader: "url-loader",
+                options: {
+                    // limit: false //不限制任何大小,所有经过loader的文件进行base64编码返回
+                    limit: 10 * 1024, //只要文件不超过 100*1024 字节,则使用base64编码,否则,交给file-loader进行处理
+                    name: "imgs/[name].[hash:5].[ext]" //ext表示保留原来图片格式
+                }
+            }]
+        }]
+    },
+
 module: {
+        rules: [{
+            test: /\\.(png)|(gif)|(jpg)$/,
+            use: [{
+                loader: "url-loader",
+                options: {
+                    // limit: false //不限制任何大小,所有经过loader的文件进行base64编码返回
+                    limit: 10 * 1024, //只要文件不超过 100*1024 字节,则使用base64编码,否则,交给file-loader进行处理
+                    name: "imgs/[name].[hash:5].[ext]" //ext表示保留原来图片格式
+                }
+            }]
+        }]
+    },
+

解决路径问题

在使用file-loader或url-loader时,可能会遇到一个非常有趣的问题

比如,通过webpack打包的目录结构如下:

yaml
dist
+    |—— img
+        |—— a.png  #file-loader生成的文件
+    |—— scripts
+        |—— main.js  #export default "img/a.png"
+    |—— html
+        |—— index.html #<script src="../scripts/main.js" ></script>
+
dist
+    |—— img
+        |—— a.png  #file-loader生成的文件
+    |—— scripts
+        |—— main.js  #export default "img/a.png"
+    |—— html
+        |—— index.html #<script src="../scripts/main.js" ></script>
+

这种问题发生的根本原因:模块中的路径来自于某个loader或plugin,当产生路径时,loader或plugin只有相对于dist目录的路径,并不知道该路径将在哪个资源中使用,从而无法确定最终正确的路径

面对这种情况,需要依靠webpack的配置publicPath解决

webpack内置插件

所有的webpack内置插件都作为webpack的静态属性存在的,使用下面的方式即可创建一个插件对象

javascript
const webpack = require("webpack")
+
+new webpack.插件名(options)
+
const webpack = require("webpack")
+
+new webpack.插件名(options)
+

DefinePlugin

全局常量定义插件,使用该插件通常定义一些常量值,例如:

javascript
new webpack.DefinePlugin({
+    PI: \`Math.PI\`, // PI = Math.PI
+    VERSION: \`"1.0.0"\`, // VERSION = "1.0.0"
+    DOMAIN: JSON.stringify("duyi.com")
+})
+
new webpack.DefinePlugin({
+    PI: \`Math.PI\`, // PI = Math.PI
+    VERSION: \`"1.0.0"\`, // VERSION = "1.0.0"
+    DOMAIN: JSON.stringify("duyi.com")
+})
+

这样一来,在源码中,我们可以直接使用插件中提供的常量,当webpack编译完成后,会自动替换为常量的值

BannerPlugin

它可以为每个chunk生成的文件头部添加一行注释,一般用于添加作者、公司、版权等信息

javascript
new webpack.BannerPlugin({
+  banner: \`
+  hash:[hash]
+  chunkhash:[chunkhash]
+  name:[name]
+  author:sunny-117
+  corporation:sunny
+  \`
+})
+
new webpack.BannerPlugin({
+  banner: \`
+  hash:[hash]
+  chunkhash:[chunkhash]
+  name:[name]
+  author:sunny-117
+  corporation:sunny
+  \`
+})
+

ProvidePlugin

自动加载模块,而不必到处 import 或 require

javascript
new webpack.ProvidePlugin({
+  $: 'jquery',
+  _: 'lodash'
+})
+
new webpack.ProvidePlugin({
+  $: 'jquery',
+  _: 'lodash'
+})
+

然后在我们任意源码中:

javascript
$('#item'); // <= 起作用
+_.drop([1, 2, 3], 2); // <= 起作用
+
$('#item'); // <= 起作用
+_.drop([1, 2, 3], 2); // <= 起作用
+
`,62),c=[o];function r(t,B,i,y,F,d){return n(),a("div",null,c)}const m=s(e,[["render",r]]);export{b as __pageData,m as default}; diff --git "a/assets/front-end-engineering_webpack\345\270\270\347\224\250\346\213\223\345\261\225.md.5a4ef0f9.lean.js" "b/assets/front-end-engineering_webpack\345\270\270\347\224\250\346\213\223\345\261\225.md.5a4ef0f9.lean.js" new file mode 100644 index 00000000..887121bf --- /dev/null +++ "b/assets/front-end-engineering_webpack\345\270\270\347\224\250\346\213\223\345\261\225.md.5a4ef0f9.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-01-06-16-01-47.0a0026be.png",b=JSON.parse('{"title":"webpack常用拓展","description":"","frontmatter":{},"headers":[{"level":2,"title":"清除输出目录","slug":"清除输出目录","link":"#清除输出目录","children":[]},{"level":2,"title":"自动生成页面","slug":"自动生成页面","link":"#自动生成页面","children":[]},{"level":2,"title":"复制静态资源","slug":"复制静态资源","link":"#复制静态资源","children":[]},{"level":2,"title":"开发服务器","slug":"开发服务器","link":"#开发服务器","children":[]},{"level":2,"title":"普通文件处理","slug":"普通文件处理","link":"#普通文件处理","children":[]},{"level":2,"title":"解决路径问题","slug":"解决路径问题","link":"#解决路径问题","children":[]},{"level":2,"title":"webpack内置插件","slug":"webpack内置插件","link":"#webpack内置插件","children":[{"level":3,"title":"DefinePlugin","slug":"defineplugin","link":"#defineplugin","children":[]},{"level":3,"title":"BannerPlugin","slug":"bannerplugin","link":"#bannerplugin","children":[]},{"level":3,"title":"ProvidePlugin","slug":"provideplugin","link":"#provideplugin","children":[]}]}],"relativePath":"front-end-engineering/webpack常用拓展.md","lastUpdated":1706436523000}'),e={name:"front-end-engineering/webpack常用拓展.md"},o=l("",62),c=[o];function r(t,B,i,y,F,d){return n(),a("div",null,c)}const m=s(e,[["render",r]]);export{b as __pageData,m as default}; diff --git "a/assets/front-end-engineering_\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.md.0256b7ba.js" "b/assets/front-end-engineering_\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.md.0256b7ba.js" new file mode 100644 index 00000000..18c59c8e --- /dev/null +++ "b/assets/front-end-engineering_\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.md.0256b7ba.js" @@ -0,0 +1,465 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const F=JSON.parse('{"title":"npx","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么会出现前端工程化","slug":"为什么会出现前端工程化","link":"#为什么会出现前端工程化","children":[]},{"level":2,"title":"nodejs对CommonJS的实现(面试)","slug":"nodejs对commonjs的实现-面试","link":"#nodejs对commonjs的实现-面试","children":[]},{"level":2,"title":"CommonJS","slug":"commonjs","link":"#commonjs","children":[]},{"level":2,"title":"node","slug":"node","link":"#node","children":[]},{"level":2,"title":"CommonJS标准和使用","slug":"commonjs标准和使用","link":"#commonjs标准和使用","children":[]},{"level":2,"title":"下面的代码执行结果是什么?","slug":"下面的代码执行结果是什么","link":"#下面的代码执行结果是什么","children":[]},{"level":2,"title":"ES6 module","slug":"es6-module","link":"#es6-module","children":[]},{"level":2,"title":"模块的引入","slug":"模块的引入","link":"#模块的引入","children":[]},{"level":2,"title":"标准和使用","slug":"标准和使用","link":"#标准和使用","children":[]},{"level":2,"title":"请对比一下CommonJS和ES Module","slug":"请对比一下commonjs和es-module","link":"#请对比一下commonjs和es-module","children":[]},{"level":2,"title":"说一下你对前端工程化,模块化,组件化的理解?","slug":"说一下你对前端工程化-模块化-组件化的理解","link":"#说一下你对前端工程化-模块化-组件化的理解","children":[]},{"level":2,"title":"webpack 中的 loader 属性和 plugins 属性的区别是什么?","slug":"webpack-中的-loader-属性和-plugins-属性的区别是什么","link":"#webpack-中的-loader-属性和-plugins-属性的区别是什么","children":[]},{"level":2,"title":"webpack 的核心概念都有哪些?","slug":"webpack-的核心概念都有哪些","link":"#webpack-的核心概念都有哪些","children":[]},{"level":2,"title":"ES6 中如何实现模块化的异步加载?","slug":"es6-中如何实现模块化的异步加载","link":"#es6-中如何实现模块化的异步加载","children":[]},{"level":2,"title":"说一下 webpack 中的几种 hash 的实现原理是什么?","slug":"说一下-webpack-中的几种-hash-的实现原理是什么","link":"#说一下-webpack-中的几种-hash-的实现原理是什么","children":[]},{"level":2,"title":"webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?","slug":"webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","link":"#webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","children":[]},{"level":2,"title":"webpack 中是如何处理图片的? (抖音直播)","slug":"webpack-中是如何处理图片的-抖音直播","link":"#webpack-中是如何处理图片的-抖音直播","children":[]},{"level":2,"title":"webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?","slug":"webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","link":"#webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","children":[]},{"level":2,"title":"webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?","slug":"webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","link":"#webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","children":[]},{"level":2,"title":"介绍一下 webpack4 中的 tree-shaking 的工作流程?","slug":"介绍一下-webpack4-中的-tree-shaking-的工作流程","link":"#介绍一下-webpack4-中的-tree-shaking-的工作流程","children":[]},{"level":2,"title":"说一下 webpack loader 的作用是什么?","slug":"说一下-webpack-loader-的作用是什么","link":"#说一下-webpack-loader-的作用是什么","children":[]},{"level":2,"title":"在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?","slug":"在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","link":"#在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","children":[]},{"level":2,"title":"export 和 export default 的区别是什么?","slug":"export-和-export-default-的区别是什么","link":"#export-和-export-default-的区别是什么","children":[]},{"level":2,"title":"webpack 打包原理是什么?","slug":"webpack-打包原理是什么","link":"#webpack-打包原理是什么","children":[]},{"level":2,"title":"webpack 热更新原理是什么?","slug":"webpack-热更新原理是什么","link":"#webpack-热更新原理是什么","children":[]},{"level":2,"title":"如何优化 webpack 的打包速度?","slug":"如何优化-webpack-的打包速度","link":"#如何优化-webpack-的打包速度","children":[]},{"level":2,"title":"webpack 如何实现动态导入?","slug":"webpack-如何实现动态导入","link":"#webpack-如何实现动态导入","children":[]},{"level":2,"title":"说一下 webpack 有哪几种文件指纹","slug":"说一下-webpack-有哪几种文件指纹","link":"#说一下-webpack-有哪几种文件指纹","children":[]},{"level":2,"title":"常用的 webpack Loader 都有哪些?","slug":"常用的-webpack-loader-都有哪些","link":"#常用的-webpack-loader-都有哪些","children":[]},{"level":2,"title":"说一下 webpack 常用插件都有哪些?","slug":"说一下-webpack-常用插件都有哪些","link":"#说一下-webpack-常用插件都有哪些","children":[]},{"level":2,"title":"使用 babel-loader 会有哪些问题,可以怎样优化?","slug":"使用-babel-loader-会有哪些问题-可以怎样优化","link":"#使用-babel-loader-会有哪些问题-可以怎样优化","children":[]},{"level":2,"title":"babel 是如何对 class 进行编译的?","slug":"babel-是如何对-class-进行编译的","link":"#babel-是如何对-class-进行编译的","children":[]},{"level":2,"title":"释一下 babel-polyfill 的作用是什么?","slug":"释一下-babel-polyfill-的作用是什么","link":"#释一下-babel-polyfill-的作用是什么","children":[]},{"level":2,"title":"解释一下 less 的&的操作符是做什么用的?","slug":"解释一下-less-的-的操作符是做什么用的","link":"#解释一下-less-的-的操作符是做什么用的","children":[]},{"level":2,"title":"webpack proxy 工作原理,为什么能解决跨域?","slug":"webpack-proxy-工作原理-为什么能解决跨域","link":"#webpack-proxy-工作原理-为什么能解决跨域","children":[]},{"level":2,"title":"组件发布的是不是所有依赖这个组件库的项目都需要升级?","slug":"组件发布的是不是所有依赖这个组件库的项目都需要升级","link":"#组件发布的是不是所有依赖这个组件库的项目都需要升级","children":[]},{"level":2,"title":"开发过程中,如何进行公共组件的设计?(字节跳动)","slug":"开发过程中-如何进行公共组件的设计-字节跳动","link":"#开发过程中-如何进行公共组件的设计-字节跳动","children":[]},{"level":2,"title":"具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)","slug":"具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","link":"#具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","children":[]},{"level":2,"title":"描述一下 webpack 的构建流程?(CVTE)","slug":"描述一下-webpack-的构建流程-cvte","link":"#描述一下-webpack-的构建流程-cvte","children":[]},{"level":2,"title":"解释一下 webpack 插件的实现原理?(CVTE)","slug":"解释一下-webpack-插件的实现原理-cvte","link":"#解释一下-webpack-插件的实现原理-cvte","children":[]},{"level":2,"title":"有用过哪些插件做项目的分析吗?(CVTE)","slug":"有用过哪些插件做项目的分析吗-cvte","link":"#有用过哪些插件做项目的分析吗-cvte","children":[]},{"level":2,"title":"什么是 babel,有什么作用?","slug":"什么是-babel-有什么作用","link":"#什么是-babel-有什么作用","children":[]},{"level":2,"title":"解释一下 npm 模块安装机制是什么?","slug":"解释一下-npm-模块安装机制是什么","link":"#解释一下-npm-模块安装机制是什么","children":[]},{"level":2,"title":"webpack与grunt、gulp的不同?","slug":"webpack与grunt、gulp的不同","link":"#webpack与grunt、gulp的不同","children":[]},{"level":2,"title":"2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?","slug":"_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","link":"#_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","children":[]},{"level":2,"title":"3.有哪些常见的Loader?他们是解决什么问题的?","slug":"_3-有哪些常见的loader-他们是解决什么问题的","link":"#_3-有哪些常见的loader-他们是解决什么问题的","children":[]},{"level":2,"title":"4.有哪些常见的Plugin?他们是解决什么问题的?","slug":"_4-有哪些常见的plugin-他们是解决什么问题的","link":"#_4-有哪些常见的plugin-他们是解决什么问题的","children":[]},{"level":2,"title":"5.Loader和Plugin的不同?","slug":"_5-loader和plugin的不同","link":"#_5-loader和plugin的不同","children":[]},{"level":2,"title":"6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全","slug":"_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","link":"#_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","children":[]},{"level":2,"title":"7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?","slug":"_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","link":"#_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","children":[]},{"level":2,"title":"8.webpack的热更新是如何做到的?说明其原理?","slug":"_8-webpack的热更新是如何做到的-说明其原理","link":"#_8-webpack的热更新是如何做到的-说明其原理","children":[]},{"level":2,"title":"9.如何利用webpack来优化前端性能?(提高性能和体验)","slug":"_9-如何利用webpack来优化前端性能-提高性能和体验","link":"#_9-如何利用webpack来优化前端性能-提高性能和体验","children":[]},{"level":2,"title":"10.如何提高webpack的构建速度?","slug":"_10-如何提高webpack的构建速度","link":"#_10-如何提高webpack的构建速度","children":[]},{"level":2,"title":"11.怎么配置单页应用?怎么配置多页应用?","slug":"_11-怎么配置单页应用-怎么配置多页应用","link":"#_11-怎么配置单页应用-怎么配置多页应用","children":[]},{"level":2,"title":"12.npm打包时需要注意哪些?如何利用webpack来更好的构建?","slug":"_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","link":"#_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","children":[]},{"level":2,"title":"13.如何在vue项目中实现按需加载?","slug":"_13-如何在vue项目中实现按需加载","link":"#_13-如何在vue项目中实现按需加载","children":[]},{"level":2,"title":"能说说webpack的作用吗?","slug":"能说说webpack的作用吗","link":"#能说说webpack的作用吗","children":[]},{"level":2,"title":"为什么要打包呢?","slug":"为什么要打包呢","link":"#为什么要打包呢","children":[]},{"level":2,"title":"能说说模块化的好处吗?","slug":"能说说模块化的好处吗","link":"#能说说模块化的好处吗","children":[]},{"level":2,"title":"模块化打包方案知道啥,可以说说吗?","slug":"模块化打包方案知道啥-可以说说吗","link":"#模块化打包方案知道啥-可以说说吗","children":[]},{"level":2,"title":"那ES6 模块与 CommonJS 模块的差异有哪些呢?","slug":"那es6-模块与-commonjs-模块的差异有哪些呢","link":"#那es6-模块与-commonjs-模块的差异有哪些呢","children":[]},{"level":2,"title":"webpack的编译(打包)流程说说","slug":"webpack的编译-打包-流程说说","link":"#webpack的编译-打包-流程说说","children":[]},{"level":2,"title":"说一下 Webpack 的热更新原理吧?","slug":"说一下-webpack-的热更新原理吧","link":"#说一下-webpack-的热更新原理吧","children":[]},{"level":2,"title":"路由懒加载的原理","slug":"路由懒加载的原理","link":"#路由懒加载的原理","children":[]},{"level":2,"title":"为什么要使用路由懒加载?","slug":"为什么要使用路由懒加载","link":"#为什么要使用路由懒加载","children":[]},{"level":2,"title":"懒加载的好处是什么?","slug":"懒加载的好处是什么","link":"#懒加载的好处是什么","children":[]},{"level":2,"title":"怎么使用的路由懒加载?","slug":"怎么使用的路由懒加载","link":"#怎么使用的路由懒加载","children":[]},{"level":2,"title":"路由懒加载的原理是什么?","slug":"路由懒加载的原理是什么","link":"#路由懒加载的原理是什么","children":[]},{"level":2,"title":"git","slug":"git","link":"#git","children":[{"level":3,"title":"打造前后端分离的开发环境,一般需要从哪几个方面进行设计","slug":"打造前后端分离的开发环境-一般需要从哪几个方面进行设计","link":"#打造前后端分离的开发环境-一般需要从哪几个方面进行设计","children":[]}]}],"relativePath":"front-end-engineering/【完结】前端工程化.md","lastUpdated":1718508254000}'),p={name:"front-end-engineering/【完结】前端工程化.md"},e=l(`

为什么会出现前端工程化

模块化:意味着咱们的 JS 总算可以开发大型项目(解决 JS 的复用问题)

组件化:解决 HTML、CSS、JS 的复用问题

工程化:因为咱们的前端项目越来越复杂、咱们的前端项目模块(组件)越来越多

前端项目越来越复杂?

在书写前端项目的时候,可能会涉及到使用其他的语言,typescript、less/sass、coffeescript,涉及到编译

在部署的时候,还需要对代码进行丑化、压缩

代码优化:为 CSS 代码添加兼容性前缀

前端项目模块(组件)越来越多?

专门有一个 node_modules 来管理这些可以复用的代码。

前端工程化的出现,归根结底,其实就是要解决开发环境和生产环境不一致的问题。

nodejs对CommonJS的实现(面试)

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

nodejs中导入模块,使用相对路径,并且必须以./或../开头,浏览器可以省略./,nodejs不行

  1. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
javascript
 (function(){
+     //模块中的代码
+ })()
+
 (function(){
+     //模块中的代码
+ })()
+
  1. 为了保证顺利的导出模块内容,nodejs做了以下处理
    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
javascript
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+

面试

javascript
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+

经典面试题 util.js

javascript
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+

index.js

javascript
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+

经验:对exports赋值无意义,建议用module.exports

  1. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

过去,JS很难编写大型应用,因为有以下两个问题:

  1. 全局变量污染
  2. 难以管理的依赖关系

这些问题,都导致了JS无法进行精细的模块划分,因为精细的模块划分会导致更多的全局污染以及更加复杂的依赖关系 于是,先后出现了两大模块化标准,用于解决以上两个问题:

  • CommonJS
  • ES6 Module

注意:上面提到的两个均是模块化标准,具体的实现需要依托于JS的宿主环境

CommonJS

node

目前,只有node环境才支持 CommonJS 模块化标准,所以,要使用 CommonJS,必须要先安装node 官网地址:https://nodejs.org/zh-cn/ nodejs直接运行某个js文件,该文件被称之为入口文件 nodejs遵循EcmaScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在nodejs中使用EcmaScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在nodejs中使用浏览器的 web api,例如:dom对象、window对象、document对象等

CommonJS标准和使用

node中的所有代码均在CommonJS规范下运行 具体规范如下:

  1. 一个JS文件即为一个模块,一个模块就是一个相对独立的功能,模块中的所有全局代码产生的变量、函数,均不会对全局造成任何污染,仅在模块内使用
  2. 如果一个模块需要暴露一些数据或功能供其他模块使用,需要使用代码module.exports = xxx,该过程称之为模块的导出
  3. 如果一个模块需要使用另一个模块导出的内容,需要使用代码require("模块路径")
    1. 路径必须以./../开头
    2. 如果模块文件后缀名为.js,可以省略后缀名
    3. require函数返回的是模块导出的内容
  4. 模块具有缓存,第一次导入模块时会缓存模块的导出,之后再导入同一个模块,直接使用之前缓存的结果。

有了CommonJS模块化,代码就会形成类似下面的结构: image.png 同时也解决了JS的两个问题 细节:

浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module
+
浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module
+

**原理:**require中的伪代码

javascript
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
  1. 根据传递的模块路径,得到模块完整的绝对路径。因为绝对路径不会重复。

image.png

  1. 判断缓存:防止重复执行

面试题 image.pngimage.png

  1. 如果没有缓存。真正运行模块代码的辅助函数
javascript
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
  1. 返回 module.exports

image.pngimage.png this === exports === module.exports === {} image.pngimage.png

下面的代码执行结果是什么?

javascript
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+

ES6 module

由于种种原因,CommonJS标准难以在浏览器中实现,因此一直在浏览器端一直没有合适的模块化标准,直到ES6标准出现 ES6规范了浏览器的模块化标准,一经发布,各大浏览器厂商纷纷在自己的浏览器中实现了该规范

模块的引入

浏览器使用以下方式引入一个ES6模块文件

html
<script src="JS文件" type="module">
+
<script src="JS文件" type="module">
+

标准和使用

  1. 模块的导出分为两种,基本导出(具名导出)默认导出基本导出导出的是该对象的某个属性,默认导出导出的是该对象的特殊属性default
javascript
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
  1. 基本导出可以有多个,默认导出只能有一个
  2. 基本导出必须要有名字,默认导出由于有特殊名字,所以可以不用写名字
javascript
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
  1. 模块的导入
javascript
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+

image.pngimage.png 注意

  1. ES6 module 采用依赖预加载模式,所有模块导入代码均会提升到代码顶部
  2. 不能将导入代码放置到判断、循环中
  3. 导入的内容放置到常量中,不可更改
  4. ES6 module 使用了缓存,保证每个模块仅加载一次

请对比一下CommonJS和ES Module

  1. CMJ 是社区标准,ESM 是官方标准
  2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
  3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
  4. CMJ 是动态的依赖,ESM 既支持动态,也支持静态
  5. ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值

CommonJS是社区模块化标准,node环境支持该标准。它不产生新的语法,是使用API实现的,本质是把要加载的模块放到一个闭包中执行(这里可以详细的阐述)。因此,node环境中的所有JS文件在执行的时候,都是放到一个函数环境中执行的,这对使得模块内部可以访问函数的参数,如exports、module、__dirname等,而且对this的指向也会有影响。CommonJS是动态的模块化标准,这就意味着依赖关系是在运行过程中确定的,同时也意味着在导入导出模块时,并不限制书写的位置。 ES Module是官方模块化标准,目前node和浏览器均支持该标准,它引入了新的语法。ES Module是静态的模块化标准,在模块执行前,会递归确定所有依赖关系,然后加载所有文件,加载完成后再运行,这在浏览器环境下会产生多次请求。由于使用的是静态依赖,因此,它要求导入导出的代码必须放置到顶层,因为只有这样才能在代码运行前就确定依赖关系。ES Module导入模块时会将导入的结果绑定到标识符中,该标识符是一个常量,不可更改。 CommonJS和ES Module都使用了缓存,保证每个模块仅执行一次。 1.讲讲模块化规范2.import和require的区别3.require是如何解析路径的

1、ESM是编译时导出结果,CommonJS是运⾏时导出结果 2、ESM是导出引⽤,CommonJS是导出⼀个值 3、ESM⽀持异步,CommonJS只⽀持同步

  1. 下面的模块导出了什么结果?
javascript
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+

说一下你对前端工程化,模块化,组件化的理解?

这三者中,模块化是基础,没有模块化,就没有组件化和工程化

模块化的出现,解决了困扰前端的两大难题:全局污染问题和依赖混乱问题,从而让精细的拆分前端工程成为了可能。

工程化的出现,解决了前端开发环境和生产环境要求不一致的矛盾。在开发环境中,我们希望代码使用尽可能的细分,代码格式尽可能的统一和规范,而在生产环境中,我们希望代码尽可能的被压缩、混淆,尽可能的优化体积。工程化的出现,就是为了解决这一矛盾,它可以让我们舒服的在开发环境中书写代码,然后经过打包,生成最合适的生产环境代码,这样就解放了开发者的精力,让开发者把更多的注意力集中在开发环境上即可。

组件化开发是一些前端框架带来的概念,它把一个网页,或者一个站点,甚至一个完整的产品线,划分为多个小的组件,组件是一个可以复用的单元,它包含了一个某个区域的完整功能。这样一来,前端便具备了开发复杂应用的能力。

webpack 中的 loader 属性和 plugins 属性的区别是什么?

参考答案:

它们都是 webpack 功能的扩展点。

loader 是加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等

plugins 是插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等

webpack 的核心概念都有哪些?

参考答案:

  • loader 加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
  • plugin 插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
  • module 模块。webpack 将所有依赖均视为模块,无论是 js、css、html、图片,统统都是模块
  • entry 入口。打包过程中的概念,webpack 以一个或多个文件作为入口点,分析整个依赖关系。
  • chunk 打包过程中的概念,一个 chunk 是一个相对独立的打包过程,以一个或多个文件为入口,分析整个依赖关系,最终完成打包合并
  • bundle webpack 打包结果
  • tree shaking 树摇优化。在打包结果中,去掉没有用到的代码。
  • HMR 热更新。是指在运行期间,遇到代码更改后,无须重启整个项目,只更新变动的那一部分代码。
  • dev server 开发服务器。在开发环境中搭建的临时服务器,用于承载对打包结果的访问

ES6 中如何实现模块化的异步加载?

使用动态导入即可,导入后,得到的是一个 Promise,完成后,得到一个模块对象,其中包含了所有的导出结果。

说一下 webpack 中的几种 hash 的实现原理是什么?

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?

参考答案:

不会。它跟关联的内容是否有变化有关系,如果没有变化,hash 就不会变。具体来说,contenthash 和具体的打包文件内容有关,chunkhash 和某一 entry 为起点的打包过程中涉及的内容有关,hash 和整个工程所有模块内容有关。

webpack 中是如何处理图片的? (抖音直播)

参考答案:

webpack 本身不处理图片,它会把图片内容仍然当做 JS 代码来解析,结果就是报错,打包失败。如果要处理图片,需要通过 loader 来处理。其中,url-loader 会把图片转换为 base64 编码,然后得到一个 dataurl,file-loader 则会将图片生成到打包目录中,然后得到一个资源路径。但无论是哪一种 loader,它们的核心功能,都是把图片内容转换成 JS 代码,因为只有转换成 JS 代码,webpack 才能识别。

webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?

说明:这道题的表述是有问题的,webpack 本身并不打包 html,相反,它如果遇到 html 代码会直接打包失败,因为 webpack 本身只能识别 JS。之所以能够打包出 html 文件,是因为插件或 loader 的作用,其中,比较常见的插件是 html-webpack-plugin。所以这道题的正确表述应该是:「html-webpack-plugin 打包出来的 html 为什么 style 放在头部 script 放在底部?」

webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?

要配置 CDN,有两个步骤:

  1. 在 html 模板中直接加入 cdn 引用
  2. 在 webpack 配置中,加入externals配置,告诉 webpack 不要打包其中的模块,转而使用全局变量

若要在开发环境中不使用 CDN,只需根据环境变量判断不同的环境,进行不同的打包处理即可。

  1. 在 html 模板中使用 ejs 模板语法进行判断,只有在生产环境中引入 CDN
  2. 在 webpack 配置中,可以根据process.env中的环境变量进行判断是否使用externals配置
  3. package.json脚本中设置不同的环境变量完成打包或开发启动。

介绍一下 webpack4 中的 tree-shaking 的工作流程?

推荐阅读:https://tsejx.github.io/webpack-guidebook/principle-analysis/operational-principle/tree-shaking

说一下 webpack loader 的作用是什么?

参考答案:

用于转换代码。有时是因为 webpack 无法识别某些内容,比如图片、css 等,需要由 loader 将其转换为 JS 代码。有时是因为某些代码需要被特殊处理,比如 JS 兼容性的处理,需要由 loader 将其进一步转换。不管是什么情况,loader 的作用只有一个,就是转换代码。

在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

export 和 export default 的区别是什么?

参考答案:

export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,比如变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出

export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出。

webpack 打包原理是什么?

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack 热更新原理是什么?

参考答案:

当开启热更新后,页面中会植入一段 websocket 脚本,同时,开发服务器也会和客户端建立 websocket 通信,当源码发生变动时,webpack 会进行以下处理:

  1. webpack 重新打包
  2. webpack-dev-server 检测到模块的变化,于是通过 webscoket 告知客户端变化已经发生
  3. 客户端收到消息后,通过 ajax 发送请求到开发服务器,以过去打包的 hash 值请求服务器的一个 json 文件
  4. 服务器告诉客户端哪些模块发生了变动,同时告诉客户端这次打包产生的新 hash 值
  5. 客户端再次用过去的 hash 值,以 JSONP 的方式请求变动的模块
  6. 服务器响应一个函数调用,用于更新模块的代码
  7. 此时,模块代码已经完成更新。客户端按照之前的监听配置,执行相应模块变动后的回调函数。

如何优化 webpack 的打包速度?

参考答案:

  1. noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  2. externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  3. 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  4. 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  5. 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  6. 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库

webpack 如何实现动态导入?

参考答案:

当遇到代码中包含动态导入语句时,webpack 会将导入的模块及其依赖分配到单独的一个 chunk 中进行打包,形成单独的打包结果。而动态导入的语句会被编译成一个普通的函数调用,该函数在执行时,会使用 JSONP 的方式动态的把分离出去的包加载到模块集合中。

说一下 webpack 有哪几种文件指纹

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

常用的 webpack Loader 都有哪些?

参考答案:

  • cache-loader:启用编译缓存
  • thread-loader:启用多线程编译
  • css-loader:编译 css 代码为 js
  • file-loader:保存文件到输出目录,将文件内容转换成文件路径
  • postcss-loader:将 css 代码使用 postcss 进行编译
  • url-loader:将文件内容转换成 dataurl
  • less-loader:将 less 代码转换成 css 代码
  • sass-loader:将 sass 代码转换成 css 代码
  • vue-loader:编译单文件组件
  • babel-loader:对 JS 代码进行降级处理

说一下 webpack 常用插件都有哪些?

参考答案:

  • clean-webpack-plugin:清除输出目录
  • copy-webpack-plugin:复制文件到输出目录
  • html-webpack-plugin:生成 HTML 文件
  • mini-css-extract-plugin:将 css 打包成单独文件的插件
  • HotModuleReplacementPlugin:热更新的插件
  • purifycss-webpack:去除无用的 css 代码
  • optimize-css-assets-webpack-plugin:优化 css 打包体积
  • uglify-js-plugin:对 JS 代码进行压缩、混淆
  • compression-webpack-plugin:gzip 压缩
  • webpack-bundle-analyzer:分析打包结果

使用 babel-loader 会有哪些问题,可以怎样优化?

参考答案:

  1. 如果不做特殊处理,babel-loader 会对所有匹配的模块进行降级,这对于那些已经处理好兼容性问题的第三方库显得多此一举,因此可以使用 exclude 配置排除掉这些第三方库
  2. 在旧版本的 babel-loader 中,默认开启了对 ESM 的转换,这样会导致 webpack 的 tree shaking 失效,因为 tree shaking 是需要保留 ESM 语法的,所以需要关闭 babel-loader 的 ESM 转换,在其新版本中已经默认关闭了。

babel 是如何对 class 进行编译的?

参考答案:

本质上就是把 class 语法转换成普通构造函数定义,并做了以下处理:

  1. 增加了对 this 指向的检测
  2. 将原型方法和静态方法变为不可枚举
  3. 将整个代码放到了立即执行函数中,运行后返回构造函数本身

释一下 babel-polyfill 的作用是什么?

说明:

babel-polyfill 已经是一个非常古老的项目了,babel 从 7.4 版本开始已不再支持它,转而使用更加强大的 core-js,此题也适用于问「core-js 的作用是什么」

解释一下 less 的&的操作符是做什么用的?

参考答案:

&符号后面的内容会和父级选择器合并书写,即中间不加入空格字符

webpack proxy 工作原理,为什么能解决跨域?

说明:

严格来说,webpack 只是一个打包工具,它并没有 proxy 的功能,甚至连服务器的功能都没有。之所以能够在 webpack 中使用 proxy 配置,是因为它的一个插件,即 webpack-dev-server 的能力。

所以,此题应该问做:「webpack-dev-server 工作原理,为什么能解决跨域?」

组件发布的是不是所有依赖这个组件库的项目都需要升级?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

开发过程中,如何进行公共组件的设计?(字节跳动)

参考答案:

  1. 确定使用场景 明确这个公共组件的需求是怎么产生的,它目前的使用场景有哪些,将来还可能出现哪些使用场景。 明确使用场景至关重要,它决定了这个组件的使用边界在哪,通用到什么程度,从而决定了这个组件的开发难度
  2. 设计组件功能 根据其使用场景,设计出组件的属性、事件、使用说明文档
  3. 测试用例 根据使用说明文档编写组件测试用例
  4. 完成开发 根据使用说明文档、测试用例完成开发

具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)

  1. 公共模块 比如某些多页应用会有多个入口,从而形成多个 chunk,而这些 chunk 中用到了一些公共模块,为了减少整体的包体积,可以使用 splitchunksplugin 将公共模块分离出来。 可以配置 minChunks 来指定被多少个 chunk 引用时进行分包
  2. 并行下载 由于 HTML5 支持 defer 和 async,因此可以同时下载多个 JS 文件以充分利用带宽。如果打包结果是一个很大的文件,就无法利用到这一点。 可以利用 splitchunks 插件将文件进行拆分,通过配置 maxSize 属性指定包体积达到多大时进行拆分

描述一下 webpack 的构建流程?(CVTE)

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

解释一下 webpack 插件的实现原理?(CVTE)

参考答案:

本质上,webpack 的插件是一个带有apply函数的对象。当 webpack 创建好 compiler 对象后,会执行注册插件的 apply 函数,同时将 compiler 对象作为参数传入。

在 apply 函数中,开发者可以通过 compiler 对象监听多个钩子函数的执行,不同的钩子函数对应 webpack 编译的不同阶段。当 webpack 进行到一定阶段后,会调用这些监听函数,同时将 compilation 对象传入。开发者可以使用 compilation 对象获取和改变 webpack 的各种信息,从而影响构建过程。

有用过哪些插件做项目的分析吗?(CVTE)

参考答案:

用过 webpack-bundle-analyzer 分析过打包结果,主要用于优化项目打包体积

什么是 babel,有什么作用?

参考答案:

babel 是一个 JS 编译器,主要用于将下一代的 JS 语言代码编译成兼容性更好的代码。

它其实本身做的事情并不多,它负责将 JS 代码编译成为 AST,然后依托其生态中的各种插件对 AST 中的语法和 API 进行处理

解释一下 npm 模块安装机制是什么?

参考答案:

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。 gruntgulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。 webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。 所以总结一下:

  • 从构建思路来说

gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

  • 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

3.有哪些常见的Loader?他们是解决什么问题的?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

4.有哪些常见的Plugin?他们是解决什么问题的?

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

5.Loader和Plugin的不同?

不同的作用

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。
  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。


7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。 编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。 相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。


8.webpack的热更新是如何做到的?说明其原理?

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 原理:image.png

首先要知道server端和client端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

9.如何利用webpack来优化前端性能?(提高性能和体验)

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css
  • 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
  • 提取公共代码。

10.如何提高webpack的构建速度?

  1. 多入口情况下,使用CommonsChunkPlugin来提取公共代码
  2. 通过externals配置来提取常用库
  3. 利用DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。
  4. 使用Happypack 实现多线程加速编译
  5. 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度
  6. 使用Tree-shaking和Scope Hoisting来剔除多余代码

11.怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可,这里不再赘述 多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

12.npm打包时需要注意哪些?如何利用webpack来更好的构建?

Npm是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建。 NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

  1. CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
  2. 输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
  3. Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
  4. 不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
  5. 对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:
javascript
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+

13.如何在vue项目中实现按需加载?

Vue UI组件库的按需加载 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。 不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了。

javascript
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+

单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。 通过import()语句来控制加载时机,webpack内置了对于import()的解析,会将import()中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import()语句时,会去加载Chunk对应生成的文件。import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill

能说说webpack的作用吗?

  • 模块打包(静态资源拓展)。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容(翻译官loader)。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展(plugins)。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

为什么要打包呢?

逻辑多、文件多、项目的复杂度高了,所以要打包 例如: 让前端代码具有校验能力===>出现了ts css不好用===>出现了sass、less webpack可以解决这些问题

能说说模块化的好处吗?

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化打包方案知道啥,可以说说吗?

1、commonjs commonjs 是 Node 中的模块规范,通过 require 及 exports 进行导入导出 commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。 总结:commonjs是用在服务器端的,同步的,如nodejs2、AMD AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

语句如下:通过define定义 image.png 可以更好的发现模块间的依赖关系 image.png总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs3、CMD CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 image.png总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs4、esm esm 是 tc39 对于 ESMAScript 的模块话规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。 它使用 import/export 进行模块导入导出. image.png 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。 export default命令,为模块指定默认输出

那ES6 模块与 CommonJS 模块的差异有哪些呢?

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

webpack的编译(打包)流程说说

  • 初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;
  • 开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
  • 确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
  • 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;
  • 输出完成:输出所有的chunk到文件系统;

说一下 Webpack 的热更新原理吧?

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为** HMR**。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS(无线路由)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

路由懒加载的原理

https://juejin.cn/post/6844904180285456398

为什么要使用路由懒加载?

当刚运行项目的时候,发现刚进入页面,就将所有的js文件和css文件加载了进来,这一进程十分的消耗时间。 如果打开哪个页面就对应的加载响应页面的js文件和css文件,那么页面加载速度会大大提升。

懒加载的好处是什么?

懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。

怎么使用的路由懒加载?

使用到的是es6的import语法,可以实现动态导入 image.pngimage.png

路由懒加载的原理是什么?

通过Webpack编译打包后,会把每个路由组件的代码分割成一一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。

作用就是webpack在打包的时候,对异步引入的库代码进行代码分割时(需要配置webpack的SplitChunkPlugin插件),为分割后的代码块取得名字 Vue中运用import的懒加载语句以及webpack的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。

npx

  1. 运行本地命令

使用npx 命令时,它会首先从本地工程的node_modules/.bin目录中寻找是否有对应的命令

例如:

shell
npx webpack
+
npx webpack
+

上面这条命令寻找本地工程的node_modules/.bin/webpack

如果将命令配置到package.jsonscripts中,可以省略npx

  1. 临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除

例如:

shell
npx prettyjson 1.json
+
npx prettyjson 1.json
+

npx会下载prettyjson包到临时目录,然后运行该命令

如果命令名称和需要下载的包名不一致时,可以手动指定报名

例如@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令

shell
npx -p @vue/cli vue create vue-app
+
npx -p @vue/cli vue create vue-app
+
  1. npm init

npm init通常用于初始化工程的package.json文件

除此之外,有时也可以充当npx的作用

shell
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+

git

git fetch git fetch origin 分支 git rebase FETCH_HEAD 本地master更新到最新:git fetch origin master; git rebase FETCH_HEAD

git 和 svn 的区别 经常使用的 git 命令? git pull 和 git fetch 的区别 git rebase 和 git merge 的区别 git rebase和merge git分支管理、分支开发 git回退操作 git reset 和 git reverse区别?你还用过什么别的指令? git遇到冲突怎么进行解决

git-flow git reset --hard 版本号 git 如何合并分支; git 中,提交了 a, 然后又提交了 b, 如何撤回 b ? git merge、git rebase的区别 假设 master 分支切出了 test 分支,供所有人合代码测试, 然后我们 从稳定的 master 切出 一个 feature 分支进行开发, 现在 我们开发完了 feature 分支并将其merge 到 test 分支进行测试, 测试过程中出现一些问题,由于疏忽你;忘记切回 feature ,而是直接在 test 分支进行了开发 导致: test 分支优先于你的featuce 分支,此时你应该怎么特 test 分支的修改带入 feature 知道git的原理吗?说说他的原理吧image.png Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作 Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中那些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。 Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。 Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库

require 与 import 的区别

动态引入:require支持,import不行 异步同步:require同步,import异步 导出值:require是值拷贝,导出值的变化不会影响到到导入值。import指向指针,导入值随导出值的变化

package.json 字段

name npm 包的名字,必须是一个小写的单词,可以包含连字符-和下划线_,发布时必填。

version npm 包的版本。需要遵循语义化版本格式x.x.x。

description npm 包的描述。

keywords npm 包的关键字。

homepage npm 包的主页地址。

bugs npm 包问题反馈的地址。

license 为 npm 包指定许可证

author npm 包的作者

files npm 包作为依赖安装时要包括的文件,格式是文件正则的数组。也可以使用 npmignore 来忽略个别文件。 以下文件总是被包含的,与配置无关 package.json README.md CHANGES / CHANGELOG / HISTORY LICENCE / LICENSE 以下文件总是被忽略的,与配置无关 .git .DS_Store node_modules .npmrc npm-debug.log package-lock.json ...

main 指定npm包的入口文件。

module 指定 ES 模块的入口文件。

typings 指定类型声明文件。

bin 开发可执行文件时,bin 字段可以帮助你设置链接,不需要手动设置 PATH。

repository npm 包托管的地方

scripts 可执行的命令。

dependencies npm包所依赖的其他npm包

devDependencies npm 包所依赖的构建和测试相关的 npm 包.

peerDependencies 指定 npm 包与主 npm 包的兼容性,当开发插件时是需要的.

engines 指定 npm 包可以使用的 Node 版本.

CSS 模块化

  1. 原生CSS;
  2. CSS 预处理器:Less、Sass、Stylus
  3. CSS 后处理器:PostCSS
  4. CSS-Modules
  5. Css In JS

CSS 预处理器

  • Sass:2007 年诞生,最早也是最成熟的 CSS 预处理器,拥有 Ruby 社区的支持和 Compass 这一最强大的 CSS 框架,目前受 LESS 影响,已经进化到了全面兼容 CSS 的 SCSS;
  • Less:2009年出现,受 SASS 的影响较大,但又使用 CSS 的语法,让大部分开发者和设计师更容易上手,在 Ruby 社区之外支持者远超过 SASS,其缺点是比起 SASS 来,可编程功能不够,不过优点是简单和兼容 CSS,反过来也影响了 SASS 演变到了 SCSS 的时代,著名的 Twitter Bootstrap 就是采用 LESS 做底层语言的;
  • Stylus:Stylus 是一个CSS的预处理框架,2010 年产生,来自 Node.js 社区,主要用来给 Node 项目进行 CSS 预处理支持,所以 Stylus 是一种新型语言,可以创建健壮的、动态的、富有表现力的 CSS。比较年轻,其本质上做的事情与 SASS/LESS 等类似;

优点:

  • 变量(variables);
  • 代码混合(mixins);
  • 嵌套(nested rules);
  • 模块化(modules);

CSS 后处理器 Postcss 被称为 css 界的 babel,实现原理是通过 ast 分析 css 代码并处理。

优点:

  • 支持配合 stylelint 校验 css 语法;
  • autoprefixer;
  • 编译 css next 语法;

CSS Modules

  • BEM 命名规范(Block、Element、Modifier):BEM 只是一套相对标准的命名规范约定,并不能完全避免命名冲突的问题;
  • CSS Modules:CSS Modules 指的是将 css 像 js 文件一样 import;

优点:

  • 局部样式;
  • 支持变量;
  • 模块化;

CSS In JS CSS In JS,即用 JS 写 CSS。

优点:

  • 局部样式;
  • 使用 JS 增强 CSS 语法;(变量、运算、嵌套)
  • 组件化; 收起 评分标准 2.5分及以下:不清楚CSS模块化方案; 3分:能清楚地说明至少一种CSS模块化方案; 3.5分:能清楚地说明至少三种CSS模块化方案; 4分:能清楚地说明CSS模块化历史和所有模块化方案;

ES Module VS. CommonJS

题目描述 ES Module 和 CommonJS 模块规范有什么区别? 答案

  1. 使用方式不同:
  • ES Module:export、export default、import
  • CommonJS:module.exports、exports、require
  1. 解析时机不同:
  • ES Module:静态的,编译时解析;
  • CommonJS:动态的,运行时解析;
  1. 导出值类型不同:
  • ES Module:导出的是值的只读引用;
  • CommonJS:导出的是值的副本;

进阶考察:

  1. webpack 可以对 CommonJS 模块进行 Tree Shaking 吗?为什么? 不能。CommonJS 是动态的,不能在编译时确认模块引用关系,因此无法实现 Tree Shaking。
  2. ES Module 可以在条件语句中使用 import 导入模块吗?为什么? 不能。ES Module 是静态的,不能在运行时动态导入模块。 3分:能基本说清楚使用方式、解析时机和导出值类型的区别; 3.5:能清晰地说明使用方式、解析时机和导出值类型的区别;并能全部回答进阶考察题;

构建工具webpack

题目描述 webpack构建过程中需要借助哪些手段来提升编译速度和减少编译体积 答案

  1. commonchunkplugin
  2. require.ensure 3、dllplugin 4.externals 5、happypack 6.uglify-parallel,
  3. tree-shaking, 8、gzip

打造前后端分离的开发环境,一般需要从哪几个方面进行设计

题目描述 打造前后端分离的开发环境,一般需要从哪几个方面进行设计 答案

  1. 如何选用web服务框架(express、koa);
  2. 如何使用http-proxy进行转发(CROS有何局限);
  3. 如何设计静态资源访问和搭建热更新服务;
  4. 如何实现模拟登陆请求(cookie模拟登录访问和安全控制);
  5. 如何实现资源的云端构建和压缩后的代码预览
  6. 如何读取后端资源模板语法

参考:https://segmentfault.com/a/1190000009266900?_ea=2040096

评分标准 3分:本地server、mock数据、接口请求转发、后端模板解析、本地代码调试方案合理 得3分; 3.5+分: 模拟登录、https、组件预览、云端构建、前后端同构等有相对合理 得3.5

前端组件化看法?

  • 有写过组件么?你对前端组件化的看法是什么?
  • 建立组件规范考虑哪些因素?
  • 对组件复用有什么看法?如何划分?

组件化作用:

  • 分而治之的开发维护方式
  • 复用

组件规范:

  • 代码组织结构:同一类型的组件有一致的代码组织结构、编码规范和调用方式
  • 版本控制:每次改动升级都有具体的说明和独立版本号,方便维护
  • 独立作用域:保证作用域隔离,组件内部代码不侵入其他组件
  • 生命周期:组件从创建到消亡中的每个时间点有回调钩子,方便开发使用人员控制
  • 通信模式:规范父子组件通信模式、独立组件之间的通信模式

组件复用:这块比较有争议,看面试者能否自圆其说

参考: https://github.com/xufei/blog/issues/19

webpack插件编写

题目描述

  1. 有用过webpack么?说说该工具的优缺点?
  2. 有开发过webpack插件么?
  3. 假如要在构建过程中去除掉html中的一些字符,如何编写这个插件? 答案 webpack优缺点:
  • 概念牛,但文档差,使用起来费劲
  • 模块化,让我们可以把复杂的程序细化为小的文件
  • require机制强大,一切文件介资源
  • 代码分隔
  • 丰富的插件,解决less、sass编译

开发插件的两个关键点Compiler和Compilation:

  • compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并在所有可操作的设置中被配置,包括原始配置,loader 和插件。当在 webpack 环境中应用一个插件时,插件将收到一个编译器对象的引用。可以使用它来访问 webpack 的主环境。
  • compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用。

插件编写可参考:https://doc.webpack-china.org/development/how-to-write-a-plugin

请简要描述ES6 module require、exports以及module.exports的区别

题目描述 考察候选人对es6,commonjs等js模块化标准的区别和理解 答案

  • CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
  • ES6 模块是动态引用,如果使用 import 从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
  • CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。
  • export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性
  • 混合使用介绍:https://github.com/ShowJoy-com/showjoy-blog/issues/39 收起 评分标准
  1. 2.5分及以下:不知道js模块化标准之间的区别
  2. 3.0分:能答对commonjs是运行时确定和es6 modules是静态化,编译时确定这个核心的不同
  3. 3.5分:对commonjs的标准和es6 modules的转换关系有一定的了解,es6 moudules有一定的使用经验,在浏览器中使用方式接触过
  4. 4.0分:了解umd,知道webpack将es6编译回es5的操作细节,知道tree-shaking等优化手段。

  • git merge、git rebase 的区别

  • 项目工程化有什么了解

  • 前端最新技术发展有什么了解?

    • 内部商业数据平台,商业数据的可视化展示,报表系统,数据分析、资料管理。
    • 项目技术选型
    • 假如系统已经发布上线了,用户反馈了一个错误,他错误里面可能就截了一个 JS 报错的截图。我怎么通过这个 JS 去找到到底是哪一行 TypeScript 报错的呢?
      • 不知道
      • 应该回答  webpack 设置 devtool: 'inline-source-map'  还得细查
    • 项目业务问题:
      • 我们为什么需要云间数据迁移呢?
        • 有些企业或者国有机构会选择将一部分数据部署在华为云上,一部分数据部署在阿里云上,可能出于数据安全性的考虑,不会把数据单独的存放在一个云上。那么他之后如果可能考虑到,如果当前的数据不想继续存在某个云上,那他就要是把一个云上的数据迁到另外一个云上了,那这个时候就需要用到我们这个系统去方便他进行不同云平台之间的数据的迁移。如果没有这个系统的话,那他就需要很麻烦的把数据先迁移到本地,再迁移到另一个云上。
      • 很多云平台提供了数据在云之间迁移的功能?
        • 根据我们的调研结果显示,目前云平台大部分类型的数据文件没有直接支持迁移到另一个云平台上的功能,只有少数的,例如虚拟机迁移是有的。
      • 系统是怎么部署的?
        • 比如说阿里云迁到华为云,假如阿里云的某个网络下的服务器要迁移的,那么这个网络下就要部署一台代理主机,然后同时要部署一台控制中心的主机,然后华为云上也是在某个网络下部里部署代理主机。云的每个网络下都要去部署数据传输的节点。
      • 如何通知用户这个迁移任务完成?或者怎么告诉他这个中间的各种进度。
        • 查看日志、查看任务状态图标
        • 后续迭代功能: \\1. 提供日志的实时更新的功能; \\2. 与后端建立websocket的连接,让后端主动推送任务完成状态信息,前端通过消息弹框展示; \\3. 通过提供短信或者邮件通知的方式去告知用户当前迁移任务的完成情况。
    • **云的很大的问题:云供应商锁定。**在不同的供应商之间,他们虽然提供的都是这种 PaaS 产品,但他们的 PaaS 产品的能力可能是不能完全对齐的。你很难把一部分功能从一个云计算厂商迁移到另外一个云计算厂商。
      • 我们还没有到应用迁移的层面
      • 再问:比如说比较关键的这个触发器,不同的数据库厂商对这个触发器的支持也是不相同的。如果你客户使用了某个厂商数据库中的触发器的功能,那另一个云可能是没有这个功能的,该怎么办?
      • 答;那首先就是客户他我们以及客户要先做好这个技术调研,比如说同类的产品一个云支持,一个云不支持。那么对于这个不支持的这个情况,那你就要去跟甲方企业去进行沟通,看他们能不能允许这个不支持的情况的存在。如果数据迁移,我们就可能要先进行测试。数据迁移迁过去可以正常使用的,没有问题,那就去再进行迁移。
    • 怎么跟服务端同学进行协作的呢?
      • 前期的时候是我们大家一起去沟通,当前这个系统应该有什么样的功能需求,划分模块,然后再去针对具体的模块去定义接口。定好这个接口之后,我们就先各自分离的开发。我开发完一部分功能之后,我们就进行联调的测试。测试完之后就是一部分功能实现了,然后我们就把这个就代码上传到 git 上,然后阶段性的进行联调和测试。
    • 前端怎么调用这个服务端的接口呢?服务端是把它他们的服务部署在他们的自己的机器上,我们去直接调用吗?
      • 他们后端的代码部署也在云服务器上,提供给一个 swaga UI 这个接口文档。前端本地要配置跨域,通过配置CORS解决。
  • 说一下 git merge和git rebase的区别

  • git 开发和上线的时候,分支怎么管理?

  • CICD 自动部署,用的什么工具?

  • CICD部署的命令是什么?

    • 把编译好的文件放到云服务器中tomcat指定的文件地址就可以。
  • 文件是怎么生成的?

    • gitlab 的runner 执行了npm run build指令
  • 需要install吗

    • 需要,具体的install的流程是根据package.json文件是否发生变化来判断是否需要install的
    • 如何实现?
    • CICD工具自动实现的
  • runner完了生成的产物是什么?

    • dist文件
  • 使用tomcat是基于什么选型?

    • 后端同学要用
  • 了解过别的部署的服务器吗?

    • nginx
  • 对比过nginx和tomcat的区别吗?

    • 没有
  • 有针对性能做过什么优化?

    • 组件懒加载
    • 减少非必要的数据监听
  • 有针对webpack做过什么配置用于性能优化吗?

  1. 需求:页面功能类似,需要重复用一个组件,组件上面是搜索框,下面是具体的内容。 现在有多个页面用到用一个组件,要求页面切换的时候能保留原先页面的内容,该如何做。
    • 答:方法一:keep-alive组件;方法二:将每个页面的关键字存到sessionStorage中,当页面切换回去的时候读取该页面对应的关键字值。
    • 再问,假如要求页面刷新还能保留页面内容,该如何做?假如有十多个页面,具体从关键字存储方式、存储时机、存储结构三个角度回答。
    • 答:那就不能用keep-alive了。要存关键字。
      • 存储方式:localStorage(经过面试官的提醒,有问到为什么用sessionStorage,我想了想用localStorage更合适,还提到了indexDb,但是我不熟悉)
        • 最开始有提到存到父组件中,但是存到父组件的话,页面刷新就没有了。
        • 问:数据存到父组件中,那么父组件把这些真实的数据存到哪儿了?存到什么属性里了?具体的值是什么?刷新后会怎么样? 我不知道
      • 存储结构:sessionStorage只能存键值对,而在页面很多的情况下,存储单个关键字显然很不方便。经过面试官提醒后想到,可以存对象转换后的JSON,读取的时候将JSON再转换为对象就可以。
      • 存储时机:在组件销毁前,beforeDestory钩子中存下关键字对象。在created或者beforeMount钩子中读取关键字,向后端请求数据。

我这边的方向基本上主要是负责一些核心服务的开发与维护,还有一些新的项目。项目的种类的话主要包括有我们这个公司内部的 CSE 的平台,因为我们内部是容器化的。然后我们自己的发布的流程比如刚刚你说的你们这边也有那些 CICD 的那些流程。然后我们这边的 CICD 的研发的平台就是我这边在开发的,然后包含了它一整套的生态系统,包括告警,日志,监控。这是一个很很大的一个产品,它里面可能包含很多很多模块。

另外就是我们这边有一个给客户那边用的一个系统,我们这边是做能源的。他们肯定有很多那种什么硬件的设备,这种可能需要获取它上面的数据去做一个可视化的展示。通过微前端把他们很多个功能整合到一起,这样的话就是做一个门户的网站,去给客户去使用。

其他还有一个就是我们前端基础的,比如说我们用的那个组件库是我们这边自己在维护,但是它是基于antd的基础上做了二次的开发,然后再维护。

我们这边会有专门的一个平台去做我的这个可视化大屏展示。然后比如说我这个项目需要用到,我就可以把它接入进来,我只要是满足它的一个数据接入的数据结构。我们这边现在的话,前端团队都已经就是 30 大几个人了。对整体来说规模还是很大的。

  • 系统有上线吗?系统有什么收益?

  • 项目周期是怎么管理的?之前的开发预期是多久?

  • 技术选型是怎么考虑的?

    • UI框架:ant design 、 element UI
    • 数据存储:vuex
    • 用户状态:JWT、cookie
      • 追问:JWT的组成有哪些
  • 项目除了研究价值,实际的收益是怎么样的?比如,会考虑商业化运作吗?或者对整个社会或者其他方面有什么推动作用?

    • 降低企业数据迁移成本
  • 除了前端,对系统的后端有什么了解?

    • 系统部署架构
    • 主机和用户的认证机制,证书认证

虚拟滚动

列举一个近期做的最能体现设计能力的项目

请举出一个你近期做的项目,项目需要最能体现设计能力,  请从以下角度说明:

  1. 项目描述
  2. 技术选型
  3. 模块化
  4. 模块之间通信
  5. 工程化
  6. 前后端数据流

答案 这是一个开放式的工程设计题目,没有固定答案,评分参考评分标准 评分标准 这是一个开放式的工程设计题目,主要考核候选人工程设计能力。 前面6个点各自按照标准进行评分,最后合并得分得出最后分数。 1、项目描述 要求:逻辑清晰、重点突出、难点 2.5及以下: 思维不清晰,描述不清晰,项目疑点较多(可能不是其自己做的) 3分: 逻辑清晰,能够清晰描述项目 3.5分: 满足(1)的基础上,描述中项目的重点突出、难点也了解比较清晰 2、技术选型 要求: 2.5分及以下: (1)、没有选型埋头就干 (2)、没有比对过任何其他的技术框架、不了解项目特性、只按照自己的知识面进行选型 (3)、不了解其他可能可用的框架的优势和不足,自己埋头造轮子 3分 :了解项目的特性, 能够比对市面上大多数的开源框架,大体了解这些框架的优点和缺点,能够客观选择框架 3.5分: 能够了解项目特性,清晰了解市面上大多数开源框架的优缺点,了解这些框架的痛点,在选型完成后能够在这些框架之上做一些优化,解决这些框架使用过程中的痛点。或者在足够了解这些框架存在的不足的情况下,自己能够沉淀出一套框架解决前面框架的痛点问题 3、项目模块化 2.5分以及以下: 模块划分不清,不能将常规业务逻辑解耦 3分: 模块划分清晰,业务能够解耦合理 3.5 满足3分的情况下,对未来的项目扩展有清晰的考虑,模块结构上对未来扩展留有足够的空间 4、模块之间的通信 2.5分以及以下: 对模块之间的通信没有概念 3分 能够清晰了解当前框架的模块通信机制,并且合理利用 3.5分 清晰了解当前框架模块通信的优点和缺点,也了解其他框架的模块之间通信的优缺点,自己主动在框架上 5、工程化方案 2.5分以及以下: 不了解项目的编译脚本等 3分 了解项目的编译(线上、线下)等,并且能合理的利用 3.5分 了解当前的工程化方案,了解市面上其他工程化方案的优点和缺点,在当前工程化方案上有过自己的探索,比如选用webpack,有写webpack的插件去满足当前项目的需求 6、前后端数据流 2.5分以及以下: 不了解项目后端到前端的整体数据流程、程序流程,没有概念 3分 大体了解项目的核心数据流程、程序流程 3.5分 了解项目的数据流程和程序流程,能够快速清晰画出核心数据流程图、程序流程图

登录表单设计/扫码登录/第三方登录

题目描述

  1. 请实现一个登录表单
  2. 用GET方法行不行?csrf是什么?如何防御?
  3. cookie-sesssion的工作机制
  4. 你已经登录产品的App端,要在web实现扫码登录,该如何设计?
  5. 接入第三方登录(如微信),如何设计? 答案
  6. 正确书写html
  7. 正确回答GET和POST的区别,从语义、弊端、安全等方面。csrf的防御:token,samesite,referer校验(弊端)等
  8. 正确理解cookie-session的工作机制,sessionId的设计,存储
  9. 考察对司空见惯的扫码登录,是否有思考其实现。正确设计 Client/Server/App 三方流程,设计二维码存储的内容,client通知有轮训或websocket等解决方案
  10. 正确理解 Client/Server/App/Weixin Server 四方流程,理解oauth2协议
  11. 2.5分及以下: 无法精确理解GET/POST差别,不了解常见安全防御手段,不理解cookie-session的工作机制
  12. 3.0分: 对前4问都达到75%的基本覆盖。
  13. 3.5分: 能够给出清晰、完整的流程设计,给出完备的前端实现
  14. 4.0分: 了解server端的一些工作内容(session存储,密码存储等)。 理解oauth2协议。 会考虑到非自己App扫二维码的时,根据UA跳转不同的下载网页。

如果数据库中采用64位长整型存储一个数据的id,前端通过api拿到这个id的话,会有什么问题?应该怎么解决?

题目描述 如果数据库中采用64位长整型存储一个数据的id,前端通过api拿到这个id的话,会有什么问题?怎么解决? 答案 考察一下JS中整数的安全范围的概念,在头条经常会遇到长整型到前端被截断的问题,需要补一个字符串形式的id供前端使用。 主要会涉及到JS中的最大安全整数问题 https://segmentfault.com/a/1190000002608050

如何实现微信扫码登录?

题目描述 综合题,考察网络、前端、认证等多方面知识 答案 参考答案: https://zhuanlan.zhihu.com/p/22032787 具体步骤:

  1. 用户 A 访问微信网页版,微信服务器为这个会话生成一个全局唯一的 ID,上面的 URL 中 obsbQ-Dzag== 就是这个 ID,此时系统并不知道访问者是谁。
  2. 用户A打开自己的手机微信并扫描这个二维码,并提示用户是否确认登录。
  3. 手机上的微信是登录状态,用户点击确认登录后,手机上的微信客户端将微信账号和这个扫描得到的 ID 一起提交到服务器
  4. 服务器将这个 ID 和用户 A 的微信号绑定在一起,并通知网页版微信,这个 ID 对应的微信号为用户 A,网页版微信加载用户 A 的微信信息,至此,扫码登录全部流程完成

富文本编辑器https://juejin.cn/post/6940904354090057764vue-quill-editor还知道哪些其他的库吗为什么不用vue2-editor呢?

主要是我把我这边项目和我做的一些优化讲的很细,然后他都听懂了,然后他好像就问了一些很多这种代码安全性的问题,然后让我设计一个Vs code的插件啊,这样子的,然后基于什么K的一些whos什么P和一些什么,提交之后,这些东西去设计一个插件来保证我们的代码不会被冲突,或者说不会改,因为改代

码而发生错误,然后再利用发布订阅模式去设计这个东西,

我在vscode里订阅这个函数,那么这个西数其他被修改提交时,lab会自动读取commit信息把我拉进review名单里

结合gitlab和sso做个发布订阅,再lab code reivew的时候去拉一下订阅者

穿越沙漠:大致就是A,乙两个点,一个是起点, 一个是终点,中间有很多的补给点,怎么 样过才最安全(不是最短路径,得考虑两个补给点之问的尽量的短,而且短的路 径尽量多)

直接无脑贪心+bfs,

有个问题:内网网站需要链接vpn才能访问,如果之前访问过了,去掉vpn为啥不走缓存?而是直接无法访问?

语音识别做出来,挑战

场景方案设计题:结合gitlab和sso账号做个发布订阅,再lab code reivew的时候去拉一下订阅者,然让我做我也做不出来呀,我但是我会说呀,对吧,就结合地的一些勾子西数嘛,然后你在Vs code的插件,首先你要登录Vs code嘛,然后Vs code会拉到 你本地的一个字节的一个账号信息,然后,如果你订阅了这个西数的话,它会保存下来,嗯,你的一个订阅这个西数是在哪个项目? 然后哪个页面那个什么什么什么下面,然后当这个东西去改变的时候,然后我去那个记得记提交的户里面查看一下原来的K记录,他有没有被订阅过?那有被订阅过,我就会通知所有的订阅者。

呃,你对单侧有了解吗?那如果说呃,现在是有AB2个同学,或者说ABCD很多同学共同开发一个代码库,那么,可能a在改了某个工具函数之后,他把代码提交上线了,然后a没有发现这个西数,他本身是可能会对其他人人的逻辑造造成影响的,然后他把他的代码上线了,然后B这边的代码,因为a的上线,B之前写的代码被影响了,就是可能会报错之类的,那么,有没有什么办法可以呃,让B之前就提前知道了,然后并且去做一个修改呢。

Node.js 所使用的 CommonJS 模块规范是如何处理循环依赖加载的Node.js 运维相关,如何排查内存泄漏,如何做进程守护V8 引擎内存回收实现算法是什么,JSHeap 默认大小是多少Node.js 模块中的 __dirname、exports、__filename 等变量是哪来的

服务器时间获取错误

假设有服务api功能为下发服务器时间戳,功能正常 但在网页中应用后,某些用户会拿到错误的时间,忽前忽后,但不会超前于真实的服务器时间 有哪些造成这个问题的原因? 一般来说是中了运营商的劫持,运营商对同一url的返回值做了缓存,而代码在请求时又没有加随机数

  1. 没思路的,说是浏览器缓存引起的(这个贴边,校招生可以提示一下忽前忽后,看反应)
  2. 3.0分:能正确回答的,并且能给出解决办法,随机数、post等都行

为什么需要child-process?

node是异步非阻塞的,这对高并发非常有效.可是我们还有其它一些常用需求,比如和操作系统shell命令交互,调用可执行文件,创建子进程进行阻塞式访问或高CPU计算等,child-process就是为满足这些需求而生的.child-process顾名思义,就是把node阻塞的工作交给子进程去做.https://github.com/jimuyouyou/node-interview-questions#起源

基于Node的前后端分离

  1. 讲解下架构是怎样的?Node层主要负责了哪些事情?
  2. Node层是如何调用后端服务的?
  3. 前后端分离优缺点?
  • 前后端分离,node一般承担路由/模板渲染功能
  • node服务可以通过rpc、restful接口等方式调用后端服务
  • 进一步降低前后端开发上耦合,提升前后端开发效率
  • 前端能够在业务层面(view+controller)有更多的控制力度,后端只关注数据提供服务
  • 摆脱后端的一些历史包袱,比如:框架版本过久、新特性支持跟不上等
  • 前端可用熟悉的javascript搭建node服务,无需切换技术栈
  • 前端可以尝试更多的优化手段
  • 分层会有一定的性能损耗,职责清晰、方便协作上的收益会弥补这块的损失
  • 前端承担了业务上更多的工作,缓解后端人力资源
  • 前端多了一个 Node 层开发/运维的工作,Node 层的基础设施服务需要持续建设

参考: http://admin.bytedance.com/surfbird/best_practice/#/node-idea

https://tech.meituan.com/node-fullstack-development-practice.html

  1. 3.0分:了解或参与过具体实践,能够比较完整的说清楚架构
  2. 3.5分:熟悉架构,并能够完整详细的给出前后端分离的一些注意点和关键技术

介绍一下你了解的 WebSocket

简单介绍一下 WebSocket,ws 协议和 http 协议的关系是什么,WebSocket 如何校验权限? WebSocket 如何实现 SSL 协议的安全连接? WebSocket 是基于 http 的,所以建立 WebSocket 连接前, 浏览器会通过 http 的方式请求服务器建立连接, 这个时候可以通过 http 的权限校验方式来校验 WebSocket,比如设置 Cookie。 同理,WebSocket 实现 SSL 协议也同 https 类似,会升级为 wss 连接。 另外,当然也可以在 WebSocket 中还可以通过加密或者 token 等方式,实现自己额外的加密传输和权限判断方式。 更多可参考 https://security.tencent.com/index.php/blog/msg/119

  1. 2.5分及以下:对 WebSocket 了解不多,没做过任何实时类业务;
  2. 3.0分:能够介绍出 WebSocket 是基于 http 的;
  3. 3.5分:能够介绍出全部内容;
  4. 4.0分:能够介绍出内容,并且结合自身项目经验,介绍了更多内容;

存储在 Cookie 中每个 request 都会带上,而放在 localStorage 中,仅有浏览器中会存储。

  1. 2.5分及以下:介绍了 cookie 和 localStorage 的简单区别;
  2. 3.0分:能够说出某些数据存在 cookie 中比 localStorage 合适,就是做了对比;
  3. 3.5分:能够说明 cookie 在每个请求中都会自动携带,可能会增加请求的大小;
  4. 4.0分:能够说出更多,比如 cookie 中可能有安全问题,服务端可以开 httpOnly 防止 JS 读取 cookie,总而言之是介绍了更多细节;

请介绍一下Oauth2.0 的认证过程

可以参考 http://www.jianshu.com/p/0db71eb445c8 或者 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 的答案, 回答的一个重点是 code(授权码)仅一次有效,并且要有失效时间,而且很短,比如一分钟, 因为浏览器收到会立刻跳转。 还有就是服务端可以根据 code 结合相应的 sercet 去获取 token,要说清楚。

  1. 2.5分及以下:只是介绍了 Oauth 的用途,比如第三方可以登录、可以获取账号信息,无法介绍技术流程;
  2. 3.0分:回答清楚了整体流程;
  3. 3.5分:能够介绍出 code 仅一次有效,申请方需要有 sercet,并且结合 code 去换取 token 的过程及原因;
  4. 4.0分:还能够介绍出可能的其他安全问题;
  • CSRF: 文件流steam(字节2020校招)
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
  • 安全相关:XSS,CSRF(字节2020校招)
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
  • 数据劫持(字节2020校招)
  • CSP,阻止iframe的嵌入(字节2020校招)
  • 进程和线程的区别知道吗?他们的通信方式有哪些?(字节2020校招)
  • js为什么是单线程(字节2020校招 + 京东2020校招)
  • section是用来做什么的(字节2020社招)
  • 了解SSO吗(字节2020社招)
  • 服务端渲染的方案有哪些?next.js和nuxt.js的区别(字节2020社招)
  • cdn的原理(字节2020社招)
  • JSBridge的原理(字节2020社招)
  • 了解过Serverless,什么是Serverless(字节2020社招)
无服务器~
+云计算?
+
无服务器~
+云计算?
+
  • 你还知道哪些新的前端技术(微前端、Low Code、可视化、BI、BFF)(字节2020社招)
  • 权限系统设计模型(DAC、MAC、RBAC、ABAC)(字节2020社招)
  • 什么是微前端(字节2020社招)
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
  • Node了解多少,有实际的项目经验吗(字节2020社招)
  • Linux了解多少,linux上查看日志的命令是什么(字节2020社招)
  • 强缓存和协商缓存(腾讯2020校招)
  • node可以做大型服务器吗,为什么(腾讯2020校招)
  • 用express还是koa(腾讯2020校招)
  • 用node写过哪些接口,登录验证是怎么做的(腾讯2020实习)
  • 清理node模块缓存的方法(腾讯2020校招)
  • 模仿一个xss的场景(从攻击到防御的流程)(腾讯2020校招)
  • Event loop知道吗?Node的eventloop的生命周期(京东2020校招)
  • JWT单点登录实现原理(小米2020校招)
  • 博客项目为什么使用Mysql?(小米2020校招)
  • 如何看待Node.js的中间件?它的好处是什么?(小米2020校招)
  • 为什么Node.js支持高并发请求?(小米2020校招)
  • 博客项目是SSR渲染还是客户端渲染?(小米2020校招)
  • web socket 和 socket io(广州长视科技2020校招)
  • 是否使⽤过webpack(不是通过vuecli的⽅式)(掌数科技2020社招)
  • 是否了解过express或者koa(掌数科技2020社招)
  • node如何实现热更新\u2029(橙智科技2020社招)
  • Node 知道哪些(express, sequelize,一面也问了)(微医云2020校招)
  • 用node做过什么(微医云2020校招)
  • Rest接口(微医云2020校招)
  • Linux指令
  • 测试工具什么什么
  • Linux指令
  • 测试工具什么什么
  • 浏览器和node环境的事件循环机制。(流利说2020校招)
  • https建立连接的过程。(流利说2020校招)
  • 讲述下http缓存机制,强缓存,协商缓存。(流利说2020校招)
  • jwt原理(北京蒸汽记忆2020校招)
  • 需求:实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后退时正确响应。给出你的技术实现方案?
  • 对Node的优点和缺点提出了自己的看法
  • nodejs的适用场景
  • (如果会用node)知道route, middleware, cluster, nodemon, pm2, server-side rendering么?
  • 解释一下 Backbone 的 MVC 实现方式?
  • 什么是“前端路由”?什么时候适合使用“前端路由”? “前端路由”有哪些优点和缺点?

操作系统:进程线程、死锁条件 Redis的底层数据结构 mongo的实务说一下, redis缓存机制 happypack 原理 【多进程打包 又讲了下进程和线程的区别】

  • koa 的原理 与express 的对比
  • 非js写的Node.js模块是如何使用Node.js调用的 【代码转换过程】 除了都会哪些后端语言 【一开始说自己用Node.js,面试官又问有没有其他的,楼主大学学过java,不过没怎么实践过】
  • Mysql的存储引擎 【这个直接不知道了 TAT】
  • 两个场景设计题感觉自己的设计方案还是有瑕疵的不是面试官想要的,并且问到mysql存储引擎直接无言以对了,感觉自己知识的广度还是有一定欠缺。最后问到性能优化正好自己之前优化过自己的博客网站做过相关的实践
  • koa了解吗

koa洋葱圈模型运行机制 express和koa区别 nginx原理 正向代理与反向代理 手写中间件测试请求时间

  • 进程和线程的区别

mongo的实务

node模块加载机制(require()原理)

路径转变,缓存

koa用的多吗

redis缓存机制是啥

8、 mysql如何创建索引 9、 mysql如何处理博客的点赞问题

完成prosimeFy babel 原理,class 是转换成什么 https://juejin.cn/post/6844904024928419848

javascript
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
javascript
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+

进程线程

  1. 页面缓存cache-control

  2. node写过东西吗

  3. node模块,path模块

  4. 对比一下commonjs模块化和es6 module

  5. 两个文件export的时候导出了相同的变量,避免重复--as

  6. exports和module.export的区别

  7. http和https

  8. 非对称加密和对称加密

  9. 打乱一个数组Math.random()的范围

  10. 问的特别全面,基础几乎全问

  11. 网站安全,CSRF,XSS,SQL注入攻击

  12. token是做什么的

node koa \\4. 由于简历写了了解Nodejs的底层原理。问了Node.js内存泄漏和如何定位内存泄漏 常见的内存场景:1. 闭包 2. http.globalAgent 开启keepAlive的时候,未清除事件监听 定位内存泄漏:1. heapdump 2. 用chromedev 生成三次内存快照 \\5. Node.js垃圾回收的原理 \\4. 客户端发请求给服务端发生了什么 \\6. 你知道MySQL和MongDB区别吗 \\7. 你平时怎么使用nodeJS的 \\8. restful风格规范是什么 新生代(from空间,to空间),老生代(标记清除,引用计数) NodeJs的EventLoop和V8的有什么区别 Node作为服务端语言,跟其他服务端语言比有什么特性?优势和劣势是什么? Mysql接触过?跨表查询外键索引(就知道个关键字所以没往下问了) 1、介绍一下项目的难点以及怎么解决的 5、移动端的业务有做过吗? 6、介绍一下你对中间件的理解7、怎么保证后端服务稳定性,怎么做容灾8、怎么让数据库查询更快9、数据库是用的什么?10、为什么用mysql1、如何用Node开启一个服务器 2、你的项目当中哪些地方用到了Node,为什么这个地方要用Node处理

  • 后端语言,涉及各种类型,主要都是TS,Java等,nodeJS作为动态语言,你怎么看?
  • 怎么理解后端,后端主要功能是做什么?
  • 校验器怎么做的?
  • 你对校验器做了什么封装?你用的这个库提供哪些功能?怎么实现两个关联参数校验?
  1. 进程与线程的概念
  2. 进程和线程的区别
  3. 浏览器渲染进程的线程有哪些
  4. 进程之前的通信方式
  5. 僵尸进程和孤儿进程是什么?
  6. 死锁产生的原因?如果解决死锁的问题?
  7. 如何实现浏览器内多个标签页之间的通信?
  8. 对Service Worker的理解

Mysql两种引擎

· 数据库表编码格式,UTF8和GBK区别

· 一个表里面姓名和学号两列,一行sql查询姓名重复的信息

· 防范XSS攻击

· 过滤或者编码哪些字符

  1. export和module.export区别
  2. Comonjs和es6 import区别

· Video标签可以播放的视频格式

  1. JWT
  2. 中间件有了解吗
  3. 进程和线程是什么
  4. nodejs的process的nexttick 为什么比微任务快
  5. 进程之间如何实现数据通信和数据同步
  6. node服务层如何封装接口
  7. 深挖底层原理 一直在拓展问

网易:谈谈同步10和异步10和它们的应用场景。https://zhuanlan.zhihu.com/p/115912936 队列解决:实现bfs返回树中值为value的节点 网易:

javascript
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+

你知道MySQL和MongDB区别吗

  1. · 如何防止sql注入

3 如何判断请求的资源已经全部返回。(从http角度,不要从api角度)

8 异步缓冲队列实现。例如需要发送十个请求,但最多只支持同时发送五个请求,需要等有请求完成后再进行剩余请求的发送。

2.用写websocket进度条以及使用场景

3.写回到上次浏览的位置(我说的记住位置存在sessionstorage/localstorage里),他问窗口大小变动怎么办,我说获取当前窗口大小等比例缩放scolltop。。。

6.正向代理,反向代理以及他们的应用场景

针对博客问了一下session,讲了cookie、session以及jwt,最后问如果不用cookie和localstorage,怎么做校验,比如匿名用户

你觉得你做过最牛逼的事情是什么

如果你有一天成了技术大牛,你最想做的事情是什么

介绍了下简历里各个项目的背景

react native ;node

博客技术文章怎么写的

monrepo技术栈,好处

node 中间件原理

发布订阅once加一个标识

是否能用于生产环境

父子组件执行顺序的原因

10w数据:虚拟滚动,bff(缓存,解决渲染问题)——新元

•如果你的技术方案和一起合作的同学不一致,你该怎么说服他? • 压力 •我觉得你说的这个方案不可行 • 稳定性 • 看到你家在深圳,计划长期在北京发展吗?

  • 个轮播组件的API • 案例问题 •假设我们现在面临着着会怎么排查?-个故障,部分用户反馈无法进入登录页面,你将 会怎么排查?

egg又不是不可替代的,一个bff而已,egg团队去开发artus了,Artus是更上层的框架,还是基于egg的

node里面for循环处理10亿数据,出现两个问题,js卡死,内存不够

为什么老是有题目处理101数据

我跟他讲:分开循环解决js卡死,把结果用node里面的fs.writefile写入本地文件

解決内存不够

bam就是浏览器上作为一个代理 但实际上mock数据还是在那个「API管理平台上的」

[]==![] 是true

[]== [] 是false

双等号 两边数据类型一样 如果是引用类型,就判断引用地址是否相同,两边数据类型不一样便会隐式转换。

有一点反直觉

如何在一个图片长列表页使用懒加载进行性能优化 ,然后说了图片替换和监听窗口滚动实现图片按需加载,那么如何实现监听呢?,然后扯到节流监听滚动条变化执行函数 输入框要求每次输入都要有联想功能,即随时向后台请求数据,但是频繁输入时,如何防止频繁多次请求 你对操作系统,编译原理的理解

  • 设计一个扫码登录的逻辑,为什么你扫码就可以登录你自己号,同时有这么多二维码
  • 讲清楚服务端,PC端,和手机端的逻辑
  • 怎么保存登陆态

项目权限管理(高低权限怎么区分,如果网络传输中途被人改包,怎么防止这一潜在危险) 在淘宝里面,商品数据量很大,前端怎么优化使加载速度更快、用户体验更好(severless + indexdb)  \\8. 在7的条件下,已知用户当前在商品列表页,并且下一步操作是点击某个商品进入详情页,怎么加快详情页的渲染速度

npm安装和查找机制

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装(不需要网络)
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

npm 缓存相关命令

shell
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
`,446),o=[e];function r(t,c,i,B,y,d){return a(),n("div",null,o)}const b=s(p,[["render",r]]);export{F as __pageData,b as default}; diff --git "a/assets/front-end-engineering_\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.md.0256b7ba.lean.js" "b/assets/front-end-engineering_\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.md.0256b7ba.lean.js" new file mode 100644 index 00000000..18c59c8e --- /dev/null +++ "b/assets/front-end-engineering_\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.md.0256b7ba.lean.js" @@ -0,0 +1,465 @@ +import{_ as s,o as a,c as n,a as l}from"./app.f983686f.js";const F=JSON.parse('{"title":"npx","description":"","frontmatter":{},"headers":[{"level":2,"title":"为什么会出现前端工程化","slug":"为什么会出现前端工程化","link":"#为什么会出现前端工程化","children":[]},{"level":2,"title":"nodejs对CommonJS的实现(面试)","slug":"nodejs对commonjs的实现-面试","link":"#nodejs对commonjs的实现-面试","children":[]},{"level":2,"title":"CommonJS","slug":"commonjs","link":"#commonjs","children":[]},{"level":2,"title":"node","slug":"node","link":"#node","children":[]},{"level":2,"title":"CommonJS标准和使用","slug":"commonjs标准和使用","link":"#commonjs标准和使用","children":[]},{"level":2,"title":"下面的代码执行结果是什么?","slug":"下面的代码执行结果是什么","link":"#下面的代码执行结果是什么","children":[]},{"level":2,"title":"ES6 module","slug":"es6-module","link":"#es6-module","children":[]},{"level":2,"title":"模块的引入","slug":"模块的引入","link":"#模块的引入","children":[]},{"level":2,"title":"标准和使用","slug":"标准和使用","link":"#标准和使用","children":[]},{"level":2,"title":"请对比一下CommonJS和ES Module","slug":"请对比一下commonjs和es-module","link":"#请对比一下commonjs和es-module","children":[]},{"level":2,"title":"说一下你对前端工程化,模块化,组件化的理解?","slug":"说一下你对前端工程化-模块化-组件化的理解","link":"#说一下你对前端工程化-模块化-组件化的理解","children":[]},{"level":2,"title":"webpack 中的 loader 属性和 plugins 属性的区别是什么?","slug":"webpack-中的-loader-属性和-plugins-属性的区别是什么","link":"#webpack-中的-loader-属性和-plugins-属性的区别是什么","children":[]},{"level":2,"title":"webpack 的核心概念都有哪些?","slug":"webpack-的核心概念都有哪些","link":"#webpack-的核心概念都有哪些","children":[]},{"level":2,"title":"ES6 中如何实现模块化的异步加载?","slug":"es6-中如何实现模块化的异步加载","link":"#es6-中如何实现模块化的异步加载","children":[]},{"level":2,"title":"说一下 webpack 中的几种 hash 的实现原理是什么?","slug":"说一下-webpack-中的几种-hash-的实现原理是什么","link":"#说一下-webpack-中的几种-hash-的实现原理是什么","children":[]},{"level":2,"title":"webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?","slug":"webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","link":"#webpack-如果使用了-hash-命名-那是每次都会重新生成-hash-吗","children":[]},{"level":2,"title":"webpack 中是如何处理图片的? (抖音直播)","slug":"webpack-中是如何处理图片的-抖音直播","link":"#webpack-中是如何处理图片的-抖音直播","children":[]},{"level":2,"title":"webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?","slug":"webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","link":"#webpack-打包出来的-html-为什么-style-放在头部-script-放在底部","children":[]},{"level":2,"title":"webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?","slug":"webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","link":"#webpack-配置如何实现开发环境不使用-cdn、生产环境使用-cdn","children":[]},{"level":2,"title":"介绍一下 webpack4 中的 tree-shaking 的工作流程?","slug":"介绍一下-webpack4-中的-tree-shaking-的工作流程","link":"#介绍一下-webpack4-中的-tree-shaking-的工作流程","children":[]},{"level":2,"title":"说一下 webpack loader 的作用是什么?","slug":"说一下-webpack-loader-的作用是什么","link":"#说一下-webpack-loader-的作用是什么","children":[]},{"level":2,"title":"在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?","slug":"在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","link":"#在开发过程中如果需要对已有模块进行扩展-如何进行开发保证调用方不受影响","children":[]},{"level":2,"title":"export 和 export default 的区别是什么?","slug":"export-和-export-default-的区别是什么","link":"#export-和-export-default-的区别是什么","children":[]},{"level":2,"title":"webpack 打包原理是什么?","slug":"webpack-打包原理是什么","link":"#webpack-打包原理是什么","children":[]},{"level":2,"title":"webpack 热更新原理是什么?","slug":"webpack-热更新原理是什么","link":"#webpack-热更新原理是什么","children":[]},{"level":2,"title":"如何优化 webpack 的打包速度?","slug":"如何优化-webpack-的打包速度","link":"#如何优化-webpack-的打包速度","children":[]},{"level":2,"title":"webpack 如何实现动态导入?","slug":"webpack-如何实现动态导入","link":"#webpack-如何实现动态导入","children":[]},{"level":2,"title":"说一下 webpack 有哪几种文件指纹","slug":"说一下-webpack-有哪几种文件指纹","link":"#说一下-webpack-有哪几种文件指纹","children":[]},{"level":2,"title":"常用的 webpack Loader 都有哪些?","slug":"常用的-webpack-loader-都有哪些","link":"#常用的-webpack-loader-都有哪些","children":[]},{"level":2,"title":"说一下 webpack 常用插件都有哪些?","slug":"说一下-webpack-常用插件都有哪些","link":"#说一下-webpack-常用插件都有哪些","children":[]},{"level":2,"title":"使用 babel-loader 会有哪些问题,可以怎样优化?","slug":"使用-babel-loader-会有哪些问题-可以怎样优化","link":"#使用-babel-loader-会有哪些问题-可以怎样优化","children":[]},{"level":2,"title":"babel 是如何对 class 进行编译的?","slug":"babel-是如何对-class-进行编译的","link":"#babel-是如何对-class-进行编译的","children":[]},{"level":2,"title":"释一下 babel-polyfill 的作用是什么?","slug":"释一下-babel-polyfill-的作用是什么","link":"#释一下-babel-polyfill-的作用是什么","children":[]},{"level":2,"title":"解释一下 less 的&的操作符是做什么用的?","slug":"解释一下-less-的-的操作符是做什么用的","link":"#解释一下-less-的-的操作符是做什么用的","children":[]},{"level":2,"title":"webpack proxy 工作原理,为什么能解决跨域?","slug":"webpack-proxy-工作原理-为什么能解决跨域","link":"#webpack-proxy-工作原理-为什么能解决跨域","children":[]},{"level":2,"title":"组件发布的是不是所有依赖这个组件库的项目都需要升级?","slug":"组件发布的是不是所有依赖这个组件库的项目都需要升级","link":"#组件发布的是不是所有依赖这个组件库的项目都需要升级","children":[]},{"level":2,"title":"开发过程中,如何进行公共组件的设计?(字节跳动)","slug":"开发过程中-如何进行公共组件的设计-字节跳动","link":"#开发过程中-如何进行公共组件的设计-字节跳动","children":[]},{"level":2,"title":"具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)","slug":"具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","link":"#具体说一下-splitchunksplugin-的使用场景及使用方法。-字节跳动","children":[]},{"level":2,"title":"描述一下 webpack 的构建流程?(CVTE)","slug":"描述一下-webpack-的构建流程-cvte","link":"#描述一下-webpack-的构建流程-cvte","children":[]},{"level":2,"title":"解释一下 webpack 插件的实现原理?(CVTE)","slug":"解释一下-webpack-插件的实现原理-cvte","link":"#解释一下-webpack-插件的实现原理-cvte","children":[]},{"level":2,"title":"有用过哪些插件做项目的分析吗?(CVTE)","slug":"有用过哪些插件做项目的分析吗-cvte","link":"#有用过哪些插件做项目的分析吗-cvte","children":[]},{"level":2,"title":"什么是 babel,有什么作用?","slug":"什么是-babel-有什么作用","link":"#什么是-babel-有什么作用","children":[]},{"level":2,"title":"解释一下 npm 模块安装机制是什么?","slug":"解释一下-npm-模块安装机制是什么","link":"#解释一下-npm-模块安装机制是什么","children":[]},{"level":2,"title":"webpack与grunt、gulp的不同?","slug":"webpack与grunt、gulp的不同","link":"#webpack与grunt、gulp的不同","children":[]},{"level":2,"title":"2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?","slug":"_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","link":"#_2-与webpack类似的工具还有哪些-谈谈你为什么最终选择-或放弃-使用webpack","children":[]},{"level":2,"title":"3.有哪些常见的Loader?他们是解决什么问题的?","slug":"_3-有哪些常见的loader-他们是解决什么问题的","link":"#_3-有哪些常见的loader-他们是解决什么问题的","children":[]},{"level":2,"title":"4.有哪些常见的Plugin?他们是解决什么问题的?","slug":"_4-有哪些常见的plugin-他们是解决什么问题的","link":"#_4-有哪些常见的plugin-他们是解决什么问题的","children":[]},{"level":2,"title":"5.Loader和Plugin的不同?","slug":"_5-loader和plugin的不同","link":"#_5-loader和plugin的不同","children":[]},{"level":2,"title":"6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全","slug":"_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","link":"#_6-webpack的构建流程是什么-从读取配置到输出文件这个过程尽量说全","children":[]},{"level":2,"title":"7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?","slug":"_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","link":"#_7-是否写过loader和plugin-描述一下编写loader或plugin的思路","children":[]},{"level":2,"title":"8.webpack的热更新是如何做到的?说明其原理?","slug":"_8-webpack的热更新是如何做到的-说明其原理","link":"#_8-webpack的热更新是如何做到的-说明其原理","children":[]},{"level":2,"title":"9.如何利用webpack来优化前端性能?(提高性能和体验)","slug":"_9-如何利用webpack来优化前端性能-提高性能和体验","link":"#_9-如何利用webpack来优化前端性能-提高性能和体验","children":[]},{"level":2,"title":"10.如何提高webpack的构建速度?","slug":"_10-如何提高webpack的构建速度","link":"#_10-如何提高webpack的构建速度","children":[]},{"level":2,"title":"11.怎么配置单页应用?怎么配置多页应用?","slug":"_11-怎么配置单页应用-怎么配置多页应用","link":"#_11-怎么配置单页应用-怎么配置多页应用","children":[]},{"level":2,"title":"12.npm打包时需要注意哪些?如何利用webpack来更好的构建?","slug":"_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","link":"#_12-npm打包时需要注意哪些-如何利用webpack来更好的构建","children":[]},{"level":2,"title":"13.如何在vue项目中实现按需加载?","slug":"_13-如何在vue项目中实现按需加载","link":"#_13-如何在vue项目中实现按需加载","children":[]},{"level":2,"title":"能说说webpack的作用吗?","slug":"能说说webpack的作用吗","link":"#能说说webpack的作用吗","children":[]},{"level":2,"title":"为什么要打包呢?","slug":"为什么要打包呢","link":"#为什么要打包呢","children":[]},{"level":2,"title":"能说说模块化的好处吗?","slug":"能说说模块化的好处吗","link":"#能说说模块化的好处吗","children":[]},{"level":2,"title":"模块化打包方案知道啥,可以说说吗?","slug":"模块化打包方案知道啥-可以说说吗","link":"#模块化打包方案知道啥-可以说说吗","children":[]},{"level":2,"title":"那ES6 模块与 CommonJS 模块的差异有哪些呢?","slug":"那es6-模块与-commonjs-模块的差异有哪些呢","link":"#那es6-模块与-commonjs-模块的差异有哪些呢","children":[]},{"level":2,"title":"webpack的编译(打包)流程说说","slug":"webpack的编译-打包-流程说说","link":"#webpack的编译-打包-流程说说","children":[]},{"level":2,"title":"说一下 Webpack 的热更新原理吧?","slug":"说一下-webpack-的热更新原理吧","link":"#说一下-webpack-的热更新原理吧","children":[]},{"level":2,"title":"路由懒加载的原理","slug":"路由懒加载的原理","link":"#路由懒加载的原理","children":[]},{"level":2,"title":"为什么要使用路由懒加载?","slug":"为什么要使用路由懒加载","link":"#为什么要使用路由懒加载","children":[]},{"level":2,"title":"懒加载的好处是什么?","slug":"懒加载的好处是什么","link":"#懒加载的好处是什么","children":[]},{"level":2,"title":"怎么使用的路由懒加载?","slug":"怎么使用的路由懒加载","link":"#怎么使用的路由懒加载","children":[]},{"level":2,"title":"路由懒加载的原理是什么?","slug":"路由懒加载的原理是什么","link":"#路由懒加载的原理是什么","children":[]},{"level":2,"title":"git","slug":"git","link":"#git","children":[{"level":3,"title":"打造前后端分离的开发环境,一般需要从哪几个方面进行设计","slug":"打造前后端分离的开发环境-一般需要从哪几个方面进行设计","link":"#打造前后端分离的开发环境-一般需要从哪几个方面进行设计","children":[]}]}],"relativePath":"front-end-engineering/【完结】前端工程化.md","lastUpdated":1718508254000}'),p={name:"front-end-engineering/【完结】前端工程化.md"},e=l(`

为什么会出现前端工程化

模块化:意味着咱们的 JS 总算可以开发大型项目(解决 JS 的复用问题)

组件化:解决 HTML、CSS、JS 的复用问题

工程化:因为咱们的前端项目越来越复杂、咱们的前端项目模块(组件)越来越多

前端项目越来越复杂?

在书写前端项目的时候,可能会涉及到使用其他的语言,typescript、less/sass、coffeescript,涉及到编译

在部署的时候,还需要对代码进行丑化、压缩

代码优化:为 CSS 代码添加兼容性前缀

前端项目模块(组件)越来越多?

专门有一个 node_modules 来管理这些可以复用的代码。

前端工程化的出现,归根结底,其实就是要解决开发环境和生产环境不一致的问题。

nodejs对CommonJS的实现(面试)

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

nodejs中导入模块,使用相对路径,并且必须以./或../开头,浏览器可以省略./,nodejs不行

  1. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
javascript
 (function(){
+     //模块中的代码
+ })()
+
 (function(){
+     //模块中的代码
+ })()
+
  1. 为了保证顺利的导出模块内容,nodejs做了以下处理
    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
javascript
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+

面试

javascript
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+

经典面试题 util.js

javascript
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+

index.js

javascript
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+

经验:对exports赋值无意义,建议用module.exports

  1. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

过去,JS很难编写大型应用,因为有以下两个问题:

  1. 全局变量污染
  2. 难以管理的依赖关系

这些问题,都导致了JS无法进行精细的模块划分,因为精细的模块划分会导致更多的全局污染以及更加复杂的依赖关系 于是,先后出现了两大模块化标准,用于解决以上两个问题:

  • CommonJS
  • ES6 Module

注意:上面提到的两个均是模块化标准,具体的实现需要依托于JS的宿主环境

CommonJS

node

目前,只有node环境才支持 CommonJS 模块化标准,所以,要使用 CommonJS,必须要先安装node 官网地址:https://nodejs.org/zh-cn/ nodejs直接运行某个js文件,该文件被称之为入口文件 nodejs遵循EcmaScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在nodejs中使用EcmaScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在nodejs中使用浏览器的 web api,例如:dom对象、window对象、document对象等

CommonJS标准和使用

node中的所有代码均在CommonJS规范下运行 具体规范如下:

  1. 一个JS文件即为一个模块,一个模块就是一个相对独立的功能,模块中的所有全局代码产生的变量、函数,均不会对全局造成任何污染,仅在模块内使用
  2. 如果一个模块需要暴露一些数据或功能供其他模块使用,需要使用代码module.exports = xxx,该过程称之为模块的导出
  3. 如果一个模块需要使用另一个模块导出的内容,需要使用代码require("模块路径")
    1. 路径必须以./../开头
    2. 如果模块文件后缀名为.js,可以省略后缀名
    3. require函数返回的是模块导出的内容
  4. 模块具有缓存,第一次导入模块时会缓存模块的导出,之后再导入同一个模块,直接使用之前缓存的结果。

有了CommonJS模块化,代码就会形成类似下面的结构: image.png 同时也解决了JS的两个问题 细节:

浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module
+
浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module
+

**原理:**require中的伪代码

javascript
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
  1. 根据传递的模块路径,得到模块完整的绝对路径。因为绝对路径不会重复。

image.png

  1. 判断缓存:防止重复执行

面试题 image.pngimage.png

  1. 如果没有缓存。真正运行模块代码的辅助函数
javascript
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
  1. 返回 module.exports

image.pngimage.png this === exports === module.exports === {} image.pngimage.png

下面的代码执行结果是什么?

javascript
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+

ES6 module

由于种种原因,CommonJS标准难以在浏览器中实现,因此一直在浏览器端一直没有合适的模块化标准,直到ES6标准出现 ES6规范了浏览器的模块化标准,一经发布,各大浏览器厂商纷纷在自己的浏览器中实现了该规范

模块的引入

浏览器使用以下方式引入一个ES6模块文件

html
<script src="JS文件" type="module">
+
<script src="JS文件" type="module">
+

标准和使用

  1. 模块的导出分为两种,基本导出(具名导出)默认导出基本导出导出的是该对象的某个属性,默认导出导出的是该对象的特殊属性default
javascript
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
  1. 基本导出可以有多个,默认导出只能有一个
  2. 基本导出必须要有名字,默认导出由于有特殊名字,所以可以不用写名字
javascript
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
  1. 模块的导入
javascript
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+

image.pngimage.png 注意

  1. ES6 module 采用依赖预加载模式,所有模块导入代码均会提升到代码顶部
  2. 不能将导入代码放置到判断、循环中
  3. 导入的内容放置到常量中,不可更改
  4. ES6 module 使用了缓存,保证每个模块仅加载一次

请对比一下CommonJS和ES Module

  1. CMJ 是社区标准,ESM 是官方标准
  2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
  3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
  4. CMJ 是动态的依赖,ESM 既支持动态,也支持静态
  5. ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值

CommonJS是社区模块化标准,node环境支持该标准。它不产生新的语法,是使用API实现的,本质是把要加载的模块放到一个闭包中执行(这里可以详细的阐述)。因此,node环境中的所有JS文件在执行的时候,都是放到一个函数环境中执行的,这对使得模块内部可以访问函数的参数,如exports、module、__dirname等,而且对this的指向也会有影响。CommonJS是动态的模块化标准,这就意味着依赖关系是在运行过程中确定的,同时也意味着在导入导出模块时,并不限制书写的位置。 ES Module是官方模块化标准,目前node和浏览器均支持该标准,它引入了新的语法。ES Module是静态的模块化标准,在模块执行前,会递归确定所有依赖关系,然后加载所有文件,加载完成后再运行,这在浏览器环境下会产生多次请求。由于使用的是静态依赖,因此,它要求导入导出的代码必须放置到顶层,因为只有这样才能在代码运行前就确定依赖关系。ES Module导入模块时会将导入的结果绑定到标识符中,该标识符是一个常量,不可更改。 CommonJS和ES Module都使用了缓存,保证每个模块仅执行一次。 1.讲讲模块化规范2.import和require的区别3.require是如何解析路径的

1、ESM是编译时导出结果,CommonJS是运⾏时导出结果 2、ESM是导出引⽤,CommonJS是导出⼀个值 3、ESM⽀持异步,CommonJS只⽀持同步

  1. 下面的模块导出了什么结果?
javascript
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+

说一下你对前端工程化,模块化,组件化的理解?

这三者中,模块化是基础,没有模块化,就没有组件化和工程化

模块化的出现,解决了困扰前端的两大难题:全局污染问题和依赖混乱问题,从而让精细的拆分前端工程成为了可能。

工程化的出现,解决了前端开发环境和生产环境要求不一致的矛盾。在开发环境中,我们希望代码使用尽可能的细分,代码格式尽可能的统一和规范,而在生产环境中,我们希望代码尽可能的被压缩、混淆,尽可能的优化体积。工程化的出现,就是为了解决这一矛盾,它可以让我们舒服的在开发环境中书写代码,然后经过打包,生成最合适的生产环境代码,这样就解放了开发者的精力,让开发者把更多的注意力集中在开发环境上即可。

组件化开发是一些前端框架带来的概念,它把一个网页,或者一个站点,甚至一个完整的产品线,划分为多个小的组件,组件是一个可以复用的单元,它包含了一个某个区域的完整功能。这样一来,前端便具备了开发复杂应用的能力。

webpack 中的 loader 属性和 plugins 属性的区别是什么?

参考答案:

它们都是 webpack 功能的扩展点。

loader 是加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等

plugins 是插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等

webpack 的核心概念都有哪些?

参考答案:

  • loader 加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
  • plugin 插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
  • module 模块。webpack 将所有依赖均视为模块,无论是 js、css、html、图片,统统都是模块
  • entry 入口。打包过程中的概念,webpack 以一个或多个文件作为入口点,分析整个依赖关系。
  • chunk 打包过程中的概念,一个 chunk 是一个相对独立的打包过程,以一个或多个文件为入口,分析整个依赖关系,最终完成打包合并
  • bundle webpack 打包结果
  • tree shaking 树摇优化。在打包结果中,去掉没有用到的代码。
  • HMR 热更新。是指在运行期间,遇到代码更改后,无须重启整个项目,只更新变动的那一部分代码。
  • dev server 开发服务器。在开发环境中搭建的临时服务器,用于承载对打包结果的访问

ES6 中如何实现模块化的异步加载?

使用动态导入即可,导入后,得到的是一个 Promise,完成后,得到一个模块对象,其中包含了所有的导出结果。

说一下 webpack 中的几种 hash 的实现原理是什么?

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?

参考答案:

不会。它跟关联的内容是否有变化有关系,如果没有变化,hash 就不会变。具体来说,contenthash 和具体的打包文件内容有关,chunkhash 和某一 entry 为起点的打包过程中涉及的内容有关,hash 和整个工程所有模块内容有关。

webpack 中是如何处理图片的? (抖音直播)

参考答案:

webpack 本身不处理图片,它会把图片内容仍然当做 JS 代码来解析,结果就是报错,打包失败。如果要处理图片,需要通过 loader 来处理。其中,url-loader 会把图片转换为 base64 编码,然后得到一个 dataurl,file-loader 则会将图片生成到打包目录中,然后得到一个资源路径。但无论是哪一种 loader,它们的核心功能,都是把图片内容转换成 JS 代码,因为只有转换成 JS 代码,webpack 才能识别。

webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?

说明:这道题的表述是有问题的,webpack 本身并不打包 html,相反,它如果遇到 html 代码会直接打包失败,因为 webpack 本身只能识别 JS。之所以能够打包出 html 文件,是因为插件或 loader 的作用,其中,比较常见的插件是 html-webpack-plugin。所以这道题的正确表述应该是:「html-webpack-plugin 打包出来的 html 为什么 style 放在头部 script 放在底部?」

webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?

要配置 CDN,有两个步骤:

  1. 在 html 模板中直接加入 cdn 引用
  2. 在 webpack 配置中,加入externals配置,告诉 webpack 不要打包其中的模块,转而使用全局变量

若要在开发环境中不使用 CDN,只需根据环境变量判断不同的环境,进行不同的打包处理即可。

  1. 在 html 模板中使用 ejs 模板语法进行判断,只有在生产环境中引入 CDN
  2. 在 webpack 配置中,可以根据process.env中的环境变量进行判断是否使用externals配置
  3. package.json脚本中设置不同的环境变量完成打包或开发启动。

介绍一下 webpack4 中的 tree-shaking 的工作流程?

推荐阅读:https://tsejx.github.io/webpack-guidebook/principle-analysis/operational-principle/tree-shaking

说一下 webpack loader 的作用是什么?

参考答案:

用于转换代码。有时是因为 webpack 无法识别某些内容,比如图片、css 等,需要由 loader 将其转换为 JS 代码。有时是因为某些代码需要被特殊处理,比如 JS 兼容性的处理,需要由 loader 将其进一步转换。不管是什么情况,loader 的作用只有一个,就是转换代码。

在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

export 和 export default 的区别是什么?

参考答案:

export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,比如变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出

export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出。

webpack 打包原理是什么?

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack 热更新原理是什么?

参考答案:

当开启热更新后,页面中会植入一段 websocket 脚本,同时,开发服务器也会和客户端建立 websocket 通信,当源码发生变动时,webpack 会进行以下处理:

  1. webpack 重新打包
  2. webpack-dev-server 检测到模块的变化,于是通过 webscoket 告知客户端变化已经发生
  3. 客户端收到消息后,通过 ajax 发送请求到开发服务器,以过去打包的 hash 值请求服务器的一个 json 文件
  4. 服务器告诉客户端哪些模块发生了变动,同时告诉客户端这次打包产生的新 hash 值
  5. 客户端再次用过去的 hash 值,以 JSONP 的方式请求变动的模块
  6. 服务器响应一个函数调用,用于更新模块的代码
  7. 此时,模块代码已经完成更新。客户端按照之前的监听配置,执行相应模块变动后的回调函数。

如何优化 webpack 的打包速度?

参考答案:

  1. noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  2. externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  3. 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  4. 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  5. 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  6. 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库

webpack 如何实现动态导入?

参考答案:

当遇到代码中包含动态导入语句时,webpack 会将导入的模块及其依赖分配到单独的一个 chunk 中进行打包,形成单独的打包结果。而动态导入的语句会被编译成一个普通的函数调用,该函数在执行时,会使用 JSONP 的方式动态的把分离出去的包加载到模块集合中。

说一下 webpack 有哪几种文件指纹

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

常用的 webpack Loader 都有哪些?

参考答案:

  • cache-loader:启用编译缓存
  • thread-loader:启用多线程编译
  • css-loader:编译 css 代码为 js
  • file-loader:保存文件到输出目录,将文件内容转换成文件路径
  • postcss-loader:将 css 代码使用 postcss 进行编译
  • url-loader:将文件内容转换成 dataurl
  • less-loader:将 less 代码转换成 css 代码
  • sass-loader:将 sass 代码转换成 css 代码
  • vue-loader:编译单文件组件
  • babel-loader:对 JS 代码进行降级处理

说一下 webpack 常用插件都有哪些?

参考答案:

  • clean-webpack-plugin:清除输出目录
  • copy-webpack-plugin:复制文件到输出目录
  • html-webpack-plugin:生成 HTML 文件
  • mini-css-extract-plugin:将 css 打包成单独文件的插件
  • HotModuleReplacementPlugin:热更新的插件
  • purifycss-webpack:去除无用的 css 代码
  • optimize-css-assets-webpack-plugin:优化 css 打包体积
  • uglify-js-plugin:对 JS 代码进行压缩、混淆
  • compression-webpack-plugin:gzip 压缩
  • webpack-bundle-analyzer:分析打包结果

使用 babel-loader 会有哪些问题,可以怎样优化?

参考答案:

  1. 如果不做特殊处理,babel-loader 会对所有匹配的模块进行降级,这对于那些已经处理好兼容性问题的第三方库显得多此一举,因此可以使用 exclude 配置排除掉这些第三方库
  2. 在旧版本的 babel-loader 中,默认开启了对 ESM 的转换,这样会导致 webpack 的 tree shaking 失效,因为 tree shaking 是需要保留 ESM 语法的,所以需要关闭 babel-loader 的 ESM 转换,在其新版本中已经默认关闭了。

babel 是如何对 class 进行编译的?

参考答案:

本质上就是把 class 语法转换成普通构造函数定义,并做了以下处理:

  1. 增加了对 this 指向的检测
  2. 将原型方法和静态方法变为不可枚举
  3. 将整个代码放到了立即执行函数中,运行后返回构造函数本身

释一下 babel-polyfill 的作用是什么?

说明:

babel-polyfill 已经是一个非常古老的项目了,babel 从 7.4 版本开始已不再支持它,转而使用更加强大的 core-js,此题也适用于问「core-js 的作用是什么」

解释一下 less 的&的操作符是做什么用的?

参考答案:

&符号后面的内容会和父级选择器合并书写,即中间不加入空格字符

webpack proxy 工作原理,为什么能解决跨域?

说明:

严格来说,webpack 只是一个打包工具,它并没有 proxy 的功能,甚至连服务器的功能都没有。之所以能够在 webpack 中使用 proxy 配置,是因为它的一个插件,即 webpack-dev-server 的能力。

所以,此题应该问做:「webpack-dev-server 工作原理,为什么能解决跨域?」

组件发布的是不是所有依赖这个组件库的项目都需要升级?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

开发过程中,如何进行公共组件的设计?(字节跳动)

参考答案:

  1. 确定使用场景 明确这个公共组件的需求是怎么产生的,它目前的使用场景有哪些,将来还可能出现哪些使用场景。 明确使用场景至关重要,它决定了这个组件的使用边界在哪,通用到什么程度,从而决定了这个组件的开发难度
  2. 设计组件功能 根据其使用场景,设计出组件的属性、事件、使用说明文档
  3. 测试用例 根据使用说明文档编写组件测试用例
  4. 完成开发 根据使用说明文档、测试用例完成开发

具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)

  1. 公共模块 比如某些多页应用会有多个入口,从而形成多个 chunk,而这些 chunk 中用到了一些公共模块,为了减少整体的包体积,可以使用 splitchunksplugin 将公共模块分离出来。 可以配置 minChunks 来指定被多少个 chunk 引用时进行分包
  2. 并行下载 由于 HTML5 支持 defer 和 async,因此可以同时下载多个 JS 文件以充分利用带宽。如果打包结果是一个很大的文件,就无法利用到这一点。 可以利用 splitchunks 插件将文件进行拆分,通过配置 maxSize 属性指定包体积达到多大时进行拆分

描述一下 webpack 的构建流程?(CVTE)

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

解释一下 webpack 插件的实现原理?(CVTE)

参考答案:

本质上,webpack 的插件是一个带有apply函数的对象。当 webpack 创建好 compiler 对象后,会执行注册插件的 apply 函数,同时将 compiler 对象作为参数传入。

在 apply 函数中,开发者可以通过 compiler 对象监听多个钩子函数的执行,不同的钩子函数对应 webpack 编译的不同阶段。当 webpack 进行到一定阶段后,会调用这些监听函数,同时将 compilation 对象传入。开发者可以使用 compilation 对象获取和改变 webpack 的各种信息,从而影响构建过程。

有用过哪些插件做项目的分析吗?(CVTE)

参考答案:

用过 webpack-bundle-analyzer 分析过打包结果,主要用于优化项目打包体积

什么是 babel,有什么作用?

参考答案:

babel 是一个 JS 编译器,主要用于将下一代的 JS 语言代码编译成兼容性更好的代码。

它其实本身做的事情并不多,它负责将 JS 代码编译成为 AST,然后依托其生态中的各种插件对 AST 中的语法和 API 进行处理

解释一下 npm 模块安装机制是什么?

参考答案:

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。 gruntgulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。 webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。 所以总结一下:

  • 从构建思路来说

gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

  • 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

3.有哪些常见的Loader?他们是解决什么问题的?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

4.有哪些常见的Plugin?他们是解决什么问题的?

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

5.Loader和Plugin的不同?

不同的作用

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。
  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。


7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。 编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。 相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。


8.webpack的热更新是如何做到的?说明其原理?

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 原理:image.png

首先要知道server端和client端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

9.如何利用webpack来优化前端性能?(提高性能和体验)

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css
  • 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
  • 提取公共代码。

10.如何提高webpack的构建速度?

  1. 多入口情况下,使用CommonsChunkPlugin来提取公共代码
  2. 通过externals配置来提取常用库
  3. 利用DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。
  4. 使用Happypack 实现多线程加速编译
  5. 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度
  6. 使用Tree-shaking和Scope Hoisting来剔除多余代码

11.怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可,这里不再赘述 多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

12.npm打包时需要注意哪些?如何利用webpack来更好的构建?

Npm是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建。 NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

  1. CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
  2. 输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
  3. Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
  4. 不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
  5. 对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:
javascript
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+

13.如何在vue项目中实现按需加载?

Vue UI组件库的按需加载 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。 不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了。

javascript
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+

单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。 通过import()语句来控制加载时机,webpack内置了对于import()的解析,会将import()中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import()语句时,会去加载Chunk对应生成的文件。import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill

能说说webpack的作用吗?

  • 模块打包(静态资源拓展)。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容(翻译官loader)。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展(plugins)。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

为什么要打包呢?

逻辑多、文件多、项目的复杂度高了,所以要打包 例如: 让前端代码具有校验能力===>出现了ts css不好用===>出现了sass、less webpack可以解决这些问题

能说说模块化的好处吗?

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化打包方案知道啥,可以说说吗?

1、commonjs commonjs 是 Node 中的模块规范,通过 require 及 exports 进行导入导出 commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。 总结:commonjs是用在服务器端的,同步的,如nodejs2、AMD AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

语句如下:通过define定义 image.png 可以更好的发现模块间的依赖关系 image.png总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs3、CMD CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 image.png总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs4、esm esm 是 tc39 对于 ESMAScript 的模块话规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。 它使用 import/export 进行模块导入导出. image.png 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。 export default命令,为模块指定默认输出

那ES6 模块与 CommonJS 模块的差异有哪些呢?

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

webpack的编译(打包)流程说说

  • 初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;
  • 开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
  • 确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
  • 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;
  • 输出完成:输出所有的chunk到文件系统;

说一下 Webpack 的热更新原理吧?

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为** HMR**。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS(无线路由)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

路由懒加载的原理

https://juejin.cn/post/6844904180285456398

为什么要使用路由懒加载?

当刚运行项目的时候,发现刚进入页面,就将所有的js文件和css文件加载了进来,这一进程十分的消耗时间。 如果打开哪个页面就对应的加载响应页面的js文件和css文件,那么页面加载速度会大大提升。

懒加载的好处是什么?

懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。

怎么使用的路由懒加载?

使用到的是es6的import语法,可以实现动态导入 image.pngimage.png

路由懒加载的原理是什么?

通过Webpack编译打包后,会把每个路由组件的代码分割成一一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。

作用就是webpack在打包的时候,对异步引入的库代码进行代码分割时(需要配置webpack的SplitChunkPlugin插件),为分割后的代码块取得名字 Vue中运用import的懒加载语句以及webpack的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。

npx

  1. 运行本地命令

使用npx 命令时,它会首先从本地工程的node_modules/.bin目录中寻找是否有对应的命令

例如:

shell
npx webpack
+
npx webpack
+

上面这条命令寻找本地工程的node_modules/.bin/webpack

如果将命令配置到package.jsonscripts中,可以省略npx

  1. 临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除

例如:

shell
npx prettyjson 1.json
+
npx prettyjson 1.json
+

npx会下载prettyjson包到临时目录,然后运行该命令

如果命令名称和需要下载的包名不一致时,可以手动指定报名

例如@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令

shell
npx -p @vue/cli vue create vue-app
+
npx -p @vue/cli vue create vue-app
+
  1. npm init

npm init通常用于初始化工程的package.json文件

除此之外,有时也可以充当npx的作用

shell
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+

git

git fetch git fetch origin 分支 git rebase FETCH_HEAD 本地master更新到最新:git fetch origin master; git rebase FETCH_HEAD

git 和 svn 的区别 经常使用的 git 命令? git pull 和 git fetch 的区别 git rebase 和 git merge 的区别 git rebase和merge git分支管理、分支开发 git回退操作 git reset 和 git reverse区别?你还用过什么别的指令? git遇到冲突怎么进行解决

git-flow git reset --hard 版本号 git 如何合并分支; git 中,提交了 a, 然后又提交了 b, 如何撤回 b ? git merge、git rebase的区别 假设 master 分支切出了 test 分支,供所有人合代码测试, 然后我们 从稳定的 master 切出 一个 feature 分支进行开发, 现在 我们开发完了 feature 分支并将其merge 到 test 分支进行测试, 测试过程中出现一些问题,由于疏忽你;忘记切回 feature ,而是直接在 test 分支进行了开发 导致: test 分支优先于你的featuce 分支,此时你应该怎么特 test 分支的修改带入 feature 知道git的原理吗?说说他的原理吧image.png Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作 Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中那些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。 Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。 Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库

require 与 import 的区别

动态引入:require支持,import不行 异步同步:require同步,import异步 导出值:require是值拷贝,导出值的变化不会影响到到导入值。import指向指针,导入值随导出值的变化

package.json 字段

name npm 包的名字,必须是一个小写的单词,可以包含连字符-和下划线_,发布时必填。

version npm 包的版本。需要遵循语义化版本格式x.x.x。

description npm 包的描述。

keywords npm 包的关键字。

homepage npm 包的主页地址。

bugs npm 包问题反馈的地址。

license 为 npm 包指定许可证

author npm 包的作者

files npm 包作为依赖安装时要包括的文件,格式是文件正则的数组。也可以使用 npmignore 来忽略个别文件。 以下文件总是被包含的,与配置无关 package.json README.md CHANGES / CHANGELOG / HISTORY LICENCE / LICENSE 以下文件总是被忽略的,与配置无关 .git .DS_Store node_modules .npmrc npm-debug.log package-lock.json ...

main 指定npm包的入口文件。

module 指定 ES 模块的入口文件。

typings 指定类型声明文件。

bin 开发可执行文件时,bin 字段可以帮助你设置链接,不需要手动设置 PATH。

repository npm 包托管的地方

scripts 可执行的命令。

dependencies npm包所依赖的其他npm包

devDependencies npm 包所依赖的构建和测试相关的 npm 包.

peerDependencies 指定 npm 包与主 npm 包的兼容性,当开发插件时是需要的.

engines 指定 npm 包可以使用的 Node 版本.

CSS 模块化

  1. 原生CSS;
  2. CSS 预处理器:Less、Sass、Stylus
  3. CSS 后处理器:PostCSS
  4. CSS-Modules
  5. Css In JS

CSS 预处理器

  • Sass:2007 年诞生,最早也是最成熟的 CSS 预处理器,拥有 Ruby 社区的支持和 Compass 这一最强大的 CSS 框架,目前受 LESS 影响,已经进化到了全面兼容 CSS 的 SCSS;
  • Less:2009年出现,受 SASS 的影响较大,但又使用 CSS 的语法,让大部分开发者和设计师更容易上手,在 Ruby 社区之外支持者远超过 SASS,其缺点是比起 SASS 来,可编程功能不够,不过优点是简单和兼容 CSS,反过来也影响了 SASS 演变到了 SCSS 的时代,著名的 Twitter Bootstrap 就是采用 LESS 做底层语言的;
  • Stylus:Stylus 是一个CSS的预处理框架,2010 年产生,来自 Node.js 社区,主要用来给 Node 项目进行 CSS 预处理支持,所以 Stylus 是一种新型语言,可以创建健壮的、动态的、富有表现力的 CSS。比较年轻,其本质上做的事情与 SASS/LESS 等类似;

优点:

  • 变量(variables);
  • 代码混合(mixins);
  • 嵌套(nested rules);
  • 模块化(modules);

CSS 后处理器 Postcss 被称为 css 界的 babel,实现原理是通过 ast 分析 css 代码并处理。

优点:

  • 支持配合 stylelint 校验 css 语法;
  • autoprefixer;
  • 编译 css next 语法;

CSS Modules

  • BEM 命名规范(Block、Element、Modifier):BEM 只是一套相对标准的命名规范约定,并不能完全避免命名冲突的问题;
  • CSS Modules:CSS Modules 指的是将 css 像 js 文件一样 import;

优点:

  • 局部样式;
  • 支持变量;
  • 模块化;

CSS In JS CSS In JS,即用 JS 写 CSS。

优点:

  • 局部样式;
  • 使用 JS 增强 CSS 语法;(变量、运算、嵌套)
  • 组件化; 收起 评分标准 2.5分及以下:不清楚CSS模块化方案; 3分:能清楚地说明至少一种CSS模块化方案; 3.5分:能清楚地说明至少三种CSS模块化方案; 4分:能清楚地说明CSS模块化历史和所有模块化方案;

ES Module VS. CommonJS

题目描述 ES Module 和 CommonJS 模块规范有什么区别? 答案

  1. 使用方式不同:
  • ES Module:export、export default、import
  • CommonJS:module.exports、exports、require
  1. 解析时机不同:
  • ES Module:静态的,编译时解析;
  • CommonJS:动态的,运行时解析;
  1. 导出值类型不同:
  • ES Module:导出的是值的只读引用;
  • CommonJS:导出的是值的副本;

进阶考察:

  1. webpack 可以对 CommonJS 模块进行 Tree Shaking 吗?为什么? 不能。CommonJS 是动态的,不能在编译时确认模块引用关系,因此无法实现 Tree Shaking。
  2. ES Module 可以在条件语句中使用 import 导入模块吗?为什么? 不能。ES Module 是静态的,不能在运行时动态导入模块。 3分:能基本说清楚使用方式、解析时机和导出值类型的区别; 3.5:能清晰地说明使用方式、解析时机和导出值类型的区别;并能全部回答进阶考察题;

构建工具webpack

题目描述 webpack构建过程中需要借助哪些手段来提升编译速度和减少编译体积 答案

  1. commonchunkplugin
  2. require.ensure 3、dllplugin 4.externals 5、happypack 6.uglify-parallel,
  3. tree-shaking, 8、gzip

打造前后端分离的开发环境,一般需要从哪几个方面进行设计

题目描述 打造前后端分离的开发环境,一般需要从哪几个方面进行设计 答案

  1. 如何选用web服务框架(express、koa);
  2. 如何使用http-proxy进行转发(CROS有何局限);
  3. 如何设计静态资源访问和搭建热更新服务;
  4. 如何实现模拟登陆请求(cookie模拟登录访问和安全控制);
  5. 如何实现资源的云端构建和压缩后的代码预览
  6. 如何读取后端资源模板语法

参考:https://segmentfault.com/a/1190000009266900?_ea=2040096

评分标准 3分:本地server、mock数据、接口请求转发、后端模板解析、本地代码调试方案合理 得3分; 3.5+分: 模拟登录、https、组件预览、云端构建、前后端同构等有相对合理 得3.5

前端组件化看法?

  • 有写过组件么?你对前端组件化的看法是什么?
  • 建立组件规范考虑哪些因素?
  • 对组件复用有什么看法?如何划分?

组件化作用:

  • 分而治之的开发维护方式
  • 复用

组件规范:

  • 代码组织结构:同一类型的组件有一致的代码组织结构、编码规范和调用方式
  • 版本控制:每次改动升级都有具体的说明和独立版本号,方便维护
  • 独立作用域:保证作用域隔离,组件内部代码不侵入其他组件
  • 生命周期:组件从创建到消亡中的每个时间点有回调钩子,方便开发使用人员控制
  • 通信模式:规范父子组件通信模式、独立组件之间的通信模式

组件复用:这块比较有争议,看面试者能否自圆其说

参考: https://github.com/xufei/blog/issues/19

webpack插件编写

题目描述

  1. 有用过webpack么?说说该工具的优缺点?
  2. 有开发过webpack插件么?
  3. 假如要在构建过程中去除掉html中的一些字符,如何编写这个插件? 答案 webpack优缺点:
  • 概念牛,但文档差,使用起来费劲
  • 模块化,让我们可以把复杂的程序细化为小的文件
  • require机制强大,一切文件介资源
  • 代码分隔
  • 丰富的插件,解决less、sass编译

开发插件的两个关键点Compiler和Compilation:

  • compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并在所有可操作的设置中被配置,包括原始配置,loader 和插件。当在 webpack 环境中应用一个插件时,插件将收到一个编译器对象的引用。可以使用它来访问 webpack 的主环境。
  • compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用。

插件编写可参考:https://doc.webpack-china.org/development/how-to-write-a-plugin

请简要描述ES6 module require、exports以及module.exports的区别

题目描述 考察候选人对es6,commonjs等js模块化标准的区别和理解 答案

  • CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
  • ES6 模块是动态引用,如果使用 import 从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
  • CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。
  • export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性
  • 混合使用介绍:https://github.com/ShowJoy-com/showjoy-blog/issues/39 收起 评分标准
  1. 2.5分及以下:不知道js模块化标准之间的区别
  2. 3.0分:能答对commonjs是运行时确定和es6 modules是静态化,编译时确定这个核心的不同
  3. 3.5分:对commonjs的标准和es6 modules的转换关系有一定的了解,es6 moudules有一定的使用经验,在浏览器中使用方式接触过
  4. 4.0分:了解umd,知道webpack将es6编译回es5的操作细节,知道tree-shaking等优化手段。

  • git merge、git rebase 的区别

  • 项目工程化有什么了解

  • 前端最新技术发展有什么了解?

    • 内部商业数据平台,商业数据的可视化展示,报表系统,数据分析、资料管理。
    • 项目技术选型
    • 假如系统已经发布上线了,用户反馈了一个错误,他错误里面可能就截了一个 JS 报错的截图。我怎么通过这个 JS 去找到到底是哪一行 TypeScript 报错的呢?
      • 不知道
      • 应该回答  webpack 设置 devtool: 'inline-source-map'  还得细查
    • 项目业务问题:
      • 我们为什么需要云间数据迁移呢?
        • 有些企业或者国有机构会选择将一部分数据部署在华为云上,一部分数据部署在阿里云上,可能出于数据安全性的考虑,不会把数据单独的存放在一个云上。那么他之后如果可能考虑到,如果当前的数据不想继续存在某个云上,那他就要是把一个云上的数据迁到另外一个云上了,那这个时候就需要用到我们这个系统去方便他进行不同云平台之间的数据的迁移。如果没有这个系统的话,那他就需要很麻烦的把数据先迁移到本地,再迁移到另一个云上。
      • 很多云平台提供了数据在云之间迁移的功能?
        • 根据我们的调研结果显示,目前云平台大部分类型的数据文件没有直接支持迁移到另一个云平台上的功能,只有少数的,例如虚拟机迁移是有的。
      • 系统是怎么部署的?
        • 比如说阿里云迁到华为云,假如阿里云的某个网络下的服务器要迁移的,那么这个网络下就要部署一台代理主机,然后同时要部署一台控制中心的主机,然后华为云上也是在某个网络下部里部署代理主机。云的每个网络下都要去部署数据传输的节点。
      • 如何通知用户这个迁移任务完成?或者怎么告诉他这个中间的各种进度。
        • 查看日志、查看任务状态图标
        • 后续迭代功能: \\1. 提供日志的实时更新的功能; \\2. 与后端建立websocket的连接,让后端主动推送任务完成状态信息,前端通过消息弹框展示; \\3. 通过提供短信或者邮件通知的方式去告知用户当前迁移任务的完成情况。
    • **云的很大的问题:云供应商锁定。**在不同的供应商之间,他们虽然提供的都是这种 PaaS 产品,但他们的 PaaS 产品的能力可能是不能完全对齐的。你很难把一部分功能从一个云计算厂商迁移到另外一个云计算厂商。
      • 我们还没有到应用迁移的层面
      • 再问:比如说比较关键的这个触发器,不同的数据库厂商对这个触发器的支持也是不相同的。如果你客户使用了某个厂商数据库中的触发器的功能,那另一个云可能是没有这个功能的,该怎么办?
      • 答;那首先就是客户他我们以及客户要先做好这个技术调研,比如说同类的产品一个云支持,一个云不支持。那么对于这个不支持的这个情况,那你就要去跟甲方企业去进行沟通,看他们能不能允许这个不支持的情况的存在。如果数据迁移,我们就可能要先进行测试。数据迁移迁过去可以正常使用的,没有问题,那就去再进行迁移。
    • 怎么跟服务端同学进行协作的呢?
      • 前期的时候是我们大家一起去沟通,当前这个系统应该有什么样的功能需求,划分模块,然后再去针对具体的模块去定义接口。定好这个接口之后,我们就先各自分离的开发。我开发完一部分功能之后,我们就进行联调的测试。测试完之后就是一部分功能实现了,然后我们就把这个就代码上传到 git 上,然后阶段性的进行联调和测试。
    • 前端怎么调用这个服务端的接口呢?服务端是把它他们的服务部署在他们的自己的机器上,我们去直接调用吗?
      • 他们后端的代码部署也在云服务器上,提供给一个 swaga UI 这个接口文档。前端本地要配置跨域,通过配置CORS解决。
  • 说一下 git merge和git rebase的区别

  • git 开发和上线的时候,分支怎么管理?

  • CICD 自动部署,用的什么工具?

  • CICD部署的命令是什么?

    • 把编译好的文件放到云服务器中tomcat指定的文件地址就可以。
  • 文件是怎么生成的?

    • gitlab 的runner 执行了npm run build指令
  • 需要install吗

    • 需要,具体的install的流程是根据package.json文件是否发生变化来判断是否需要install的
    • 如何实现?
    • CICD工具自动实现的
  • runner完了生成的产物是什么?

    • dist文件
  • 使用tomcat是基于什么选型?

    • 后端同学要用
  • 了解过别的部署的服务器吗?

    • nginx
  • 对比过nginx和tomcat的区别吗?

    • 没有
  • 有针对性能做过什么优化?

    • 组件懒加载
    • 减少非必要的数据监听
  • 有针对webpack做过什么配置用于性能优化吗?

  1. 需求:页面功能类似,需要重复用一个组件,组件上面是搜索框,下面是具体的内容。 现在有多个页面用到用一个组件,要求页面切换的时候能保留原先页面的内容,该如何做。
    • 答:方法一:keep-alive组件;方法二:将每个页面的关键字存到sessionStorage中,当页面切换回去的时候读取该页面对应的关键字值。
    • 再问,假如要求页面刷新还能保留页面内容,该如何做?假如有十多个页面,具体从关键字存储方式、存储时机、存储结构三个角度回答。
    • 答:那就不能用keep-alive了。要存关键字。
      • 存储方式:localStorage(经过面试官的提醒,有问到为什么用sessionStorage,我想了想用localStorage更合适,还提到了indexDb,但是我不熟悉)
        • 最开始有提到存到父组件中,但是存到父组件的话,页面刷新就没有了。
        • 问:数据存到父组件中,那么父组件把这些真实的数据存到哪儿了?存到什么属性里了?具体的值是什么?刷新后会怎么样? 我不知道
      • 存储结构:sessionStorage只能存键值对,而在页面很多的情况下,存储单个关键字显然很不方便。经过面试官提醒后想到,可以存对象转换后的JSON,读取的时候将JSON再转换为对象就可以。
      • 存储时机:在组件销毁前,beforeDestory钩子中存下关键字对象。在created或者beforeMount钩子中读取关键字,向后端请求数据。

我这边的方向基本上主要是负责一些核心服务的开发与维护,还有一些新的项目。项目的种类的话主要包括有我们这个公司内部的 CSE 的平台,因为我们内部是容器化的。然后我们自己的发布的流程比如刚刚你说的你们这边也有那些 CICD 的那些流程。然后我们这边的 CICD 的研发的平台就是我这边在开发的,然后包含了它一整套的生态系统,包括告警,日志,监控。这是一个很很大的一个产品,它里面可能包含很多很多模块。

另外就是我们这边有一个给客户那边用的一个系统,我们这边是做能源的。他们肯定有很多那种什么硬件的设备,这种可能需要获取它上面的数据去做一个可视化的展示。通过微前端把他们很多个功能整合到一起,这样的话就是做一个门户的网站,去给客户去使用。

其他还有一个就是我们前端基础的,比如说我们用的那个组件库是我们这边自己在维护,但是它是基于antd的基础上做了二次的开发,然后再维护。

我们这边会有专门的一个平台去做我的这个可视化大屏展示。然后比如说我这个项目需要用到,我就可以把它接入进来,我只要是满足它的一个数据接入的数据结构。我们这边现在的话,前端团队都已经就是 30 大几个人了。对整体来说规模还是很大的。

  • 系统有上线吗?系统有什么收益?

  • 项目周期是怎么管理的?之前的开发预期是多久?

  • 技术选型是怎么考虑的?

    • UI框架:ant design 、 element UI
    • 数据存储:vuex
    • 用户状态:JWT、cookie
      • 追问:JWT的组成有哪些
  • 项目除了研究价值,实际的收益是怎么样的?比如,会考虑商业化运作吗?或者对整个社会或者其他方面有什么推动作用?

    • 降低企业数据迁移成本
  • 除了前端,对系统的后端有什么了解?

    • 系统部署架构
    • 主机和用户的认证机制,证书认证

虚拟滚动

列举一个近期做的最能体现设计能力的项目

请举出一个你近期做的项目,项目需要最能体现设计能力,  请从以下角度说明:

  1. 项目描述
  2. 技术选型
  3. 模块化
  4. 模块之间通信
  5. 工程化
  6. 前后端数据流

答案 这是一个开放式的工程设计题目,没有固定答案,评分参考评分标准 评分标准 这是一个开放式的工程设计题目,主要考核候选人工程设计能力。 前面6个点各自按照标准进行评分,最后合并得分得出最后分数。 1、项目描述 要求:逻辑清晰、重点突出、难点 2.5及以下: 思维不清晰,描述不清晰,项目疑点较多(可能不是其自己做的) 3分: 逻辑清晰,能够清晰描述项目 3.5分: 满足(1)的基础上,描述中项目的重点突出、难点也了解比较清晰 2、技术选型 要求: 2.5分及以下: (1)、没有选型埋头就干 (2)、没有比对过任何其他的技术框架、不了解项目特性、只按照自己的知识面进行选型 (3)、不了解其他可能可用的框架的优势和不足,自己埋头造轮子 3分 :了解项目的特性, 能够比对市面上大多数的开源框架,大体了解这些框架的优点和缺点,能够客观选择框架 3.5分: 能够了解项目特性,清晰了解市面上大多数开源框架的优缺点,了解这些框架的痛点,在选型完成后能够在这些框架之上做一些优化,解决这些框架使用过程中的痛点。或者在足够了解这些框架存在的不足的情况下,自己能够沉淀出一套框架解决前面框架的痛点问题 3、项目模块化 2.5分以及以下: 模块划分不清,不能将常规业务逻辑解耦 3分: 模块划分清晰,业务能够解耦合理 3.5 满足3分的情况下,对未来的项目扩展有清晰的考虑,模块结构上对未来扩展留有足够的空间 4、模块之间的通信 2.5分以及以下: 对模块之间的通信没有概念 3分 能够清晰了解当前框架的模块通信机制,并且合理利用 3.5分 清晰了解当前框架模块通信的优点和缺点,也了解其他框架的模块之间通信的优缺点,自己主动在框架上 5、工程化方案 2.5分以及以下: 不了解项目的编译脚本等 3分 了解项目的编译(线上、线下)等,并且能合理的利用 3.5分 了解当前的工程化方案,了解市面上其他工程化方案的优点和缺点,在当前工程化方案上有过自己的探索,比如选用webpack,有写webpack的插件去满足当前项目的需求 6、前后端数据流 2.5分以及以下: 不了解项目后端到前端的整体数据流程、程序流程,没有概念 3分 大体了解项目的核心数据流程、程序流程 3.5分 了解项目的数据流程和程序流程,能够快速清晰画出核心数据流程图、程序流程图

登录表单设计/扫码登录/第三方登录

题目描述

  1. 请实现一个登录表单
  2. 用GET方法行不行?csrf是什么?如何防御?
  3. cookie-sesssion的工作机制
  4. 你已经登录产品的App端,要在web实现扫码登录,该如何设计?
  5. 接入第三方登录(如微信),如何设计? 答案
  6. 正确书写html
  7. 正确回答GET和POST的区别,从语义、弊端、安全等方面。csrf的防御:token,samesite,referer校验(弊端)等
  8. 正确理解cookie-session的工作机制,sessionId的设计,存储
  9. 考察对司空见惯的扫码登录,是否有思考其实现。正确设计 Client/Server/App 三方流程,设计二维码存储的内容,client通知有轮训或websocket等解决方案
  10. 正确理解 Client/Server/App/Weixin Server 四方流程,理解oauth2协议
  11. 2.5分及以下: 无法精确理解GET/POST差别,不了解常见安全防御手段,不理解cookie-session的工作机制
  12. 3.0分: 对前4问都达到75%的基本覆盖。
  13. 3.5分: 能够给出清晰、完整的流程设计,给出完备的前端实现
  14. 4.0分: 了解server端的一些工作内容(session存储,密码存储等)。 理解oauth2协议。 会考虑到非自己App扫二维码的时,根据UA跳转不同的下载网页。

如果数据库中采用64位长整型存储一个数据的id,前端通过api拿到这个id的话,会有什么问题?应该怎么解决?

题目描述 如果数据库中采用64位长整型存储一个数据的id,前端通过api拿到这个id的话,会有什么问题?怎么解决? 答案 考察一下JS中整数的安全范围的概念,在头条经常会遇到长整型到前端被截断的问题,需要补一个字符串形式的id供前端使用。 主要会涉及到JS中的最大安全整数问题 https://segmentfault.com/a/1190000002608050

如何实现微信扫码登录?

题目描述 综合题,考察网络、前端、认证等多方面知识 答案 参考答案: https://zhuanlan.zhihu.com/p/22032787 具体步骤:

  1. 用户 A 访问微信网页版,微信服务器为这个会话生成一个全局唯一的 ID,上面的 URL 中 obsbQ-Dzag== 就是这个 ID,此时系统并不知道访问者是谁。
  2. 用户A打开自己的手机微信并扫描这个二维码,并提示用户是否确认登录。
  3. 手机上的微信是登录状态,用户点击确认登录后,手机上的微信客户端将微信账号和这个扫描得到的 ID 一起提交到服务器
  4. 服务器将这个 ID 和用户 A 的微信号绑定在一起,并通知网页版微信,这个 ID 对应的微信号为用户 A,网页版微信加载用户 A 的微信信息,至此,扫码登录全部流程完成

富文本编辑器https://juejin.cn/post/6940904354090057764vue-quill-editor还知道哪些其他的库吗为什么不用vue2-editor呢?

主要是我把我这边项目和我做的一些优化讲的很细,然后他都听懂了,然后他好像就问了一些很多这种代码安全性的问题,然后让我设计一个Vs code的插件啊,这样子的,然后基于什么K的一些whos什么P和一些什么,提交之后,这些东西去设计一个插件来保证我们的代码不会被冲突,或者说不会改,因为改代

码而发生错误,然后再利用发布订阅模式去设计这个东西,

我在vscode里订阅这个函数,那么这个西数其他被修改提交时,lab会自动读取commit信息把我拉进review名单里

结合gitlab和sso做个发布订阅,再lab code reivew的时候去拉一下订阅者

穿越沙漠:大致就是A,乙两个点,一个是起点, 一个是终点,中间有很多的补给点,怎么 样过才最安全(不是最短路径,得考虑两个补给点之问的尽量的短,而且短的路 径尽量多)

直接无脑贪心+bfs,

有个问题:内网网站需要链接vpn才能访问,如果之前访问过了,去掉vpn为啥不走缓存?而是直接无法访问?

语音识别做出来,挑战

场景方案设计题:结合gitlab和sso账号做个发布订阅,再lab code reivew的时候去拉一下订阅者,然让我做我也做不出来呀,我但是我会说呀,对吧,就结合地的一些勾子西数嘛,然后你在Vs code的插件,首先你要登录Vs code嘛,然后Vs code会拉到 你本地的一个字节的一个账号信息,然后,如果你订阅了这个西数的话,它会保存下来,嗯,你的一个订阅这个西数是在哪个项目? 然后哪个页面那个什么什么什么下面,然后当这个东西去改变的时候,然后我去那个记得记提交的户里面查看一下原来的K记录,他有没有被订阅过?那有被订阅过,我就会通知所有的订阅者。

呃,你对单侧有了解吗?那如果说呃,现在是有AB2个同学,或者说ABCD很多同学共同开发一个代码库,那么,可能a在改了某个工具函数之后,他把代码提交上线了,然后a没有发现这个西数,他本身是可能会对其他人人的逻辑造造成影响的,然后他把他的代码上线了,然后B这边的代码,因为a的上线,B之前写的代码被影响了,就是可能会报错之类的,那么,有没有什么办法可以呃,让B之前就提前知道了,然后并且去做一个修改呢。

Node.js 所使用的 CommonJS 模块规范是如何处理循环依赖加载的Node.js 运维相关,如何排查内存泄漏,如何做进程守护V8 引擎内存回收实现算法是什么,JSHeap 默认大小是多少Node.js 模块中的 __dirname、exports、__filename 等变量是哪来的

服务器时间获取错误

假设有服务api功能为下发服务器时间戳,功能正常 但在网页中应用后,某些用户会拿到错误的时间,忽前忽后,但不会超前于真实的服务器时间 有哪些造成这个问题的原因? 一般来说是中了运营商的劫持,运营商对同一url的返回值做了缓存,而代码在请求时又没有加随机数

  1. 没思路的,说是浏览器缓存引起的(这个贴边,校招生可以提示一下忽前忽后,看反应)
  2. 3.0分:能正确回答的,并且能给出解决办法,随机数、post等都行

为什么需要child-process?

node是异步非阻塞的,这对高并发非常有效.可是我们还有其它一些常用需求,比如和操作系统shell命令交互,调用可执行文件,创建子进程进行阻塞式访问或高CPU计算等,child-process就是为满足这些需求而生的.child-process顾名思义,就是把node阻塞的工作交给子进程去做.https://github.com/jimuyouyou/node-interview-questions#起源

基于Node的前后端分离

  1. 讲解下架构是怎样的?Node层主要负责了哪些事情?
  2. Node层是如何调用后端服务的?
  3. 前后端分离优缺点?
  • 前后端分离,node一般承担路由/模板渲染功能
  • node服务可以通过rpc、restful接口等方式调用后端服务
  • 进一步降低前后端开发上耦合,提升前后端开发效率
  • 前端能够在业务层面(view+controller)有更多的控制力度,后端只关注数据提供服务
  • 摆脱后端的一些历史包袱,比如:框架版本过久、新特性支持跟不上等
  • 前端可用熟悉的javascript搭建node服务,无需切换技术栈
  • 前端可以尝试更多的优化手段
  • 分层会有一定的性能损耗,职责清晰、方便协作上的收益会弥补这块的损失
  • 前端承担了业务上更多的工作,缓解后端人力资源
  • 前端多了一个 Node 层开发/运维的工作,Node 层的基础设施服务需要持续建设

参考: http://admin.bytedance.com/surfbird/best_practice/#/node-idea

https://tech.meituan.com/node-fullstack-development-practice.html

  1. 3.0分:了解或参与过具体实践,能够比较完整的说清楚架构
  2. 3.5分:熟悉架构,并能够完整详细的给出前后端分离的一些注意点和关键技术

介绍一下你了解的 WebSocket

简单介绍一下 WebSocket,ws 协议和 http 协议的关系是什么,WebSocket 如何校验权限? WebSocket 如何实现 SSL 协议的安全连接? WebSocket 是基于 http 的,所以建立 WebSocket 连接前, 浏览器会通过 http 的方式请求服务器建立连接, 这个时候可以通过 http 的权限校验方式来校验 WebSocket,比如设置 Cookie。 同理,WebSocket 实现 SSL 协议也同 https 类似,会升级为 wss 连接。 另外,当然也可以在 WebSocket 中还可以通过加密或者 token 等方式,实现自己额外的加密传输和权限判断方式。 更多可参考 https://security.tencent.com/index.php/blog/msg/119

  1. 2.5分及以下:对 WebSocket 了解不多,没做过任何实时类业务;
  2. 3.0分:能够介绍出 WebSocket 是基于 http 的;
  3. 3.5分:能够介绍出全部内容;
  4. 4.0分:能够介绍出内容,并且结合自身项目经验,介绍了更多内容;

存储在 Cookie 中每个 request 都会带上,而放在 localStorage 中,仅有浏览器中会存储。

  1. 2.5分及以下:介绍了 cookie 和 localStorage 的简单区别;
  2. 3.0分:能够说出某些数据存在 cookie 中比 localStorage 合适,就是做了对比;
  3. 3.5分:能够说明 cookie 在每个请求中都会自动携带,可能会增加请求的大小;
  4. 4.0分:能够说出更多,比如 cookie 中可能有安全问题,服务端可以开 httpOnly 防止 JS 读取 cookie,总而言之是介绍了更多细节;

请介绍一下Oauth2.0 的认证过程

可以参考 http://www.jianshu.com/p/0db71eb445c8 或者 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 的答案, 回答的一个重点是 code(授权码)仅一次有效,并且要有失效时间,而且很短,比如一分钟, 因为浏览器收到会立刻跳转。 还有就是服务端可以根据 code 结合相应的 sercet 去获取 token,要说清楚。

  1. 2.5分及以下:只是介绍了 Oauth 的用途,比如第三方可以登录、可以获取账号信息,无法介绍技术流程;
  2. 3.0分:回答清楚了整体流程;
  3. 3.5分:能够介绍出 code 仅一次有效,申请方需要有 sercet,并且结合 code 去换取 token 的过程及原因;
  4. 4.0分:还能够介绍出可能的其他安全问题;
  • CSRF: 文件流steam(字节2020校招)
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
  • 安全相关:XSS,CSRF(字节2020校招)
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
  • 数据劫持(字节2020校招)
  • CSP,阻止iframe的嵌入(字节2020校招)
  • 进程和线程的区别知道吗?他们的通信方式有哪些?(字节2020校招)
  • js为什么是单线程(字节2020校招 + 京东2020校招)
  • section是用来做什么的(字节2020社招)
  • 了解SSO吗(字节2020社招)
  • 服务端渲染的方案有哪些?next.js和nuxt.js的区别(字节2020社招)
  • cdn的原理(字节2020社招)
  • JSBridge的原理(字节2020社招)
  • 了解过Serverless,什么是Serverless(字节2020社招)
无服务器~
+云计算?
+
无服务器~
+云计算?
+
  • 你还知道哪些新的前端技术(微前端、Low Code、可视化、BI、BFF)(字节2020社招)
  • 权限系统设计模型(DAC、MAC、RBAC、ABAC)(字节2020社招)
  • 什么是微前端(字节2020社招)
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
  • Node了解多少,有实际的项目经验吗(字节2020社招)
  • Linux了解多少,linux上查看日志的命令是什么(字节2020社招)
  • 强缓存和协商缓存(腾讯2020校招)
  • node可以做大型服务器吗,为什么(腾讯2020校招)
  • 用express还是koa(腾讯2020校招)
  • 用node写过哪些接口,登录验证是怎么做的(腾讯2020实习)
  • 清理node模块缓存的方法(腾讯2020校招)
  • 模仿一个xss的场景(从攻击到防御的流程)(腾讯2020校招)
  • Event loop知道吗?Node的eventloop的生命周期(京东2020校招)
  • JWT单点登录实现原理(小米2020校招)
  • 博客项目为什么使用Mysql?(小米2020校招)
  • 如何看待Node.js的中间件?它的好处是什么?(小米2020校招)
  • 为什么Node.js支持高并发请求?(小米2020校招)
  • 博客项目是SSR渲染还是客户端渲染?(小米2020校招)
  • web socket 和 socket io(广州长视科技2020校招)
  • 是否使⽤过webpack(不是通过vuecli的⽅式)(掌数科技2020社招)
  • 是否了解过express或者koa(掌数科技2020社招)
  • node如何实现热更新\u2029(橙智科技2020社招)
  • Node 知道哪些(express, sequelize,一面也问了)(微医云2020校招)
  • 用node做过什么(微医云2020校招)
  • Rest接口(微医云2020校招)
  • Linux指令
  • 测试工具什么什么
  • Linux指令
  • 测试工具什么什么
  • 浏览器和node环境的事件循环机制。(流利说2020校招)
  • https建立连接的过程。(流利说2020校招)
  • 讲述下http缓存机制,强缓存,协商缓存。(流利说2020校招)
  • jwt原理(北京蒸汽记忆2020校招)
  • 需求:实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后退时正确响应。给出你的技术实现方案?
  • 对Node的优点和缺点提出了自己的看法
  • nodejs的适用场景
  • (如果会用node)知道route, middleware, cluster, nodemon, pm2, server-side rendering么?
  • 解释一下 Backbone 的 MVC 实现方式?
  • 什么是“前端路由”?什么时候适合使用“前端路由”? “前端路由”有哪些优点和缺点?

操作系统:进程线程、死锁条件 Redis的底层数据结构 mongo的实务说一下, redis缓存机制 happypack 原理 【多进程打包 又讲了下进程和线程的区别】

  • koa 的原理 与express 的对比
  • 非js写的Node.js模块是如何使用Node.js调用的 【代码转换过程】 除了都会哪些后端语言 【一开始说自己用Node.js,面试官又问有没有其他的,楼主大学学过java,不过没怎么实践过】
  • Mysql的存储引擎 【这个直接不知道了 TAT】
  • 两个场景设计题感觉自己的设计方案还是有瑕疵的不是面试官想要的,并且问到mysql存储引擎直接无言以对了,感觉自己知识的广度还是有一定欠缺。最后问到性能优化正好自己之前优化过自己的博客网站做过相关的实践
  • koa了解吗

koa洋葱圈模型运行机制 express和koa区别 nginx原理 正向代理与反向代理 手写中间件测试请求时间

  • 进程和线程的区别

mongo的实务

node模块加载机制(require()原理)

路径转变,缓存

koa用的多吗

redis缓存机制是啥

8、 mysql如何创建索引 9、 mysql如何处理博客的点赞问题

完成prosimeFy babel 原理,class 是转换成什么 https://juejin.cn/post/6844904024928419848

javascript
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
javascript
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+

进程线程

  1. 页面缓存cache-control

  2. node写过东西吗

  3. node模块,path模块

  4. 对比一下commonjs模块化和es6 module

  5. 两个文件export的时候导出了相同的变量,避免重复--as

  6. exports和module.export的区别

  7. http和https

  8. 非对称加密和对称加密

  9. 打乱一个数组Math.random()的范围

  10. 问的特别全面,基础几乎全问

  11. 网站安全,CSRF,XSS,SQL注入攻击

  12. token是做什么的

node koa \\4. 由于简历写了了解Nodejs的底层原理。问了Node.js内存泄漏和如何定位内存泄漏 常见的内存场景:1. 闭包 2. http.globalAgent 开启keepAlive的时候,未清除事件监听 定位内存泄漏:1. heapdump 2. 用chromedev 生成三次内存快照 \\5. Node.js垃圾回收的原理 \\4. 客户端发请求给服务端发生了什么 \\6. 你知道MySQL和MongDB区别吗 \\7. 你平时怎么使用nodeJS的 \\8. restful风格规范是什么 新生代(from空间,to空间),老生代(标记清除,引用计数) NodeJs的EventLoop和V8的有什么区别 Node作为服务端语言,跟其他服务端语言比有什么特性?优势和劣势是什么? Mysql接触过?跨表查询外键索引(就知道个关键字所以没往下问了) 1、介绍一下项目的难点以及怎么解决的 5、移动端的业务有做过吗? 6、介绍一下你对中间件的理解7、怎么保证后端服务稳定性,怎么做容灾8、怎么让数据库查询更快9、数据库是用的什么?10、为什么用mysql1、如何用Node开启一个服务器 2、你的项目当中哪些地方用到了Node,为什么这个地方要用Node处理

  • 后端语言,涉及各种类型,主要都是TS,Java等,nodeJS作为动态语言,你怎么看?
  • 怎么理解后端,后端主要功能是做什么?
  • 校验器怎么做的?
  • 你对校验器做了什么封装?你用的这个库提供哪些功能?怎么实现两个关联参数校验?
  1. 进程与线程的概念
  2. 进程和线程的区别
  3. 浏览器渲染进程的线程有哪些
  4. 进程之前的通信方式
  5. 僵尸进程和孤儿进程是什么?
  6. 死锁产生的原因?如果解决死锁的问题?
  7. 如何实现浏览器内多个标签页之间的通信?
  8. 对Service Worker的理解

Mysql两种引擎

· 数据库表编码格式,UTF8和GBK区别

· 一个表里面姓名和学号两列,一行sql查询姓名重复的信息

· 防范XSS攻击

· 过滤或者编码哪些字符

  1. export和module.export区别
  2. Comonjs和es6 import区别

· Video标签可以播放的视频格式

  1. JWT
  2. 中间件有了解吗
  3. 进程和线程是什么
  4. nodejs的process的nexttick 为什么比微任务快
  5. 进程之间如何实现数据通信和数据同步
  6. node服务层如何封装接口
  7. 深挖底层原理 一直在拓展问

网易:谈谈同步10和异步10和它们的应用场景。https://zhuanlan.zhihu.com/p/115912936 队列解决:实现bfs返回树中值为value的节点 网易:

javascript
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+

你知道MySQL和MongDB区别吗

  1. · 如何防止sql注入

3 如何判断请求的资源已经全部返回。(从http角度,不要从api角度)

8 异步缓冲队列实现。例如需要发送十个请求,但最多只支持同时发送五个请求,需要等有请求完成后再进行剩余请求的发送。

2.用写websocket进度条以及使用场景

3.写回到上次浏览的位置(我说的记住位置存在sessionstorage/localstorage里),他问窗口大小变动怎么办,我说获取当前窗口大小等比例缩放scolltop。。。

6.正向代理,反向代理以及他们的应用场景

针对博客问了一下session,讲了cookie、session以及jwt,最后问如果不用cookie和localstorage,怎么做校验,比如匿名用户

你觉得你做过最牛逼的事情是什么

如果你有一天成了技术大牛,你最想做的事情是什么

介绍了下简历里各个项目的背景

react native ;node

博客技术文章怎么写的

monrepo技术栈,好处

node 中间件原理

发布订阅once加一个标识

是否能用于生产环境

父子组件执行顺序的原因

10w数据:虚拟滚动,bff(缓存,解决渲染问题)——新元

•如果你的技术方案和一起合作的同学不一致,你该怎么说服他? • 压力 •我觉得你说的这个方案不可行 • 稳定性 • 看到你家在深圳,计划长期在北京发展吗?

  • 个轮播组件的API • 案例问题 •假设我们现在面临着着会怎么排查?-个故障,部分用户反馈无法进入登录页面,你将 会怎么排查?

egg又不是不可替代的,一个bff而已,egg团队去开发artus了,Artus是更上层的框架,还是基于egg的

node里面for循环处理10亿数据,出现两个问题,js卡死,内存不够

为什么老是有题目处理101数据

我跟他讲:分开循环解决js卡死,把结果用node里面的fs.writefile写入本地文件

解決内存不够

bam就是浏览器上作为一个代理 但实际上mock数据还是在那个「API管理平台上的」

[]==![] 是true

[]== [] 是false

双等号 两边数据类型一样 如果是引用类型,就判断引用地址是否相同,两边数据类型不一样便会隐式转换。

有一点反直觉

如何在一个图片长列表页使用懒加载进行性能优化 ,然后说了图片替换和监听窗口滚动实现图片按需加载,那么如何实现监听呢?,然后扯到节流监听滚动条变化执行函数 输入框要求每次输入都要有联想功能,即随时向后台请求数据,但是频繁输入时,如何防止频繁多次请求 你对操作系统,编译原理的理解

  • 设计一个扫码登录的逻辑,为什么你扫码就可以登录你自己号,同时有这么多二维码
  • 讲清楚服务端,PC端,和手机端的逻辑
  • 怎么保存登陆态

项目权限管理(高低权限怎么区分,如果网络传输中途被人改包,怎么防止这一潜在危险) 在淘宝里面,商品数据量很大,前端怎么优化使加载速度更快、用户体验更好(severless + indexdb)  \\8. 在7的条件下,已知用户当前在商品列表页,并且下一步操作是点击某个商品进入详情页,怎么加快详情页的渲染速度

npm安装和查找机制

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装(不需要网络)
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

npm 缓存相关命令

shell
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
`,446),o=[e];function r(t,c,i,B,y,d){return a(),n("div",null,o)}const b=s(p,[["render",r]]);export{F as __pageData,b as default}; diff --git "a/assets/front-end-engineering_\345\220\216\345\244\204\347\220\206\345\231\250.md.b789bac1.js" "b/assets/front-end-engineering_\345\220\216\345\244\204\347\220\206\345\231\250.md.b789bac1.js" new file mode 100644 index 00000000..bbda05a5 --- /dev/null +++ "b/assets/front-end-engineering_\345\220\216\345\244\204\347\220\206\345\231\250.md.b789bac1.js" @@ -0,0 +1,791 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const d=JSON.parse('{"title":"PostCSS 后处理器","description":"","frontmatter":{},"headers":[{"level":3,"title":"后处理器的概念","slug":"后处理器的概念","link":"#后处理器的概念","children":[]},{"level":3,"title":"PostCSS 快速上手","slug":"postcss-快速上手","link":"#postcss-快速上手","children":[]},{"level":2,"title":"postcss-cli 和配置文件","slug":"postcss-cli-和配置文件","link":"#postcss-cli-和配置文件","children":[{"level":3,"title":"postcss-cli","slug":"postcss-cli","link":"#postcss-cli","children":[]},{"level":3,"title":"配置文件","slug":"配置文件","link":"#配置文件","children":[]}]},{"level":2,"title":"postcss 主流插件 part1","slug":"postcss-主流插件-part1","link":"#postcss-主流插件-part1","children":[{"level":3,"title":"autoprefixer","slug":"autoprefixer","link":"#autoprefixer","children":[]},{"level":3,"title":"cssnano","slug":"cssnano","link":"#cssnano","children":[]},{"level":3,"title":"stylelint","slug":"stylelint","link":"#stylelint","children":[]}]},{"level":2,"title":"postcss 主流插件 part2","slug":"postcss-主流插件-part2","link":"#postcss-主流插件-part2","children":[{"level":3,"title":"postcss-preset-env","slug":"postcss-preset-env","link":"#postcss-preset-env","children":[]},{"level":3,"title":"postcss-import","slug":"postcss-import","link":"#postcss-import","children":[]},{"level":3,"title":"purgecss","slug":"purgecss","link":"#purgecss","children":[]}]},{"level":2,"title":"抽象语法树","slug":"抽象语法树","link":"#抽象语法树","children":[]},{"level":2,"title":"自定义插件","slug":"自定义插件","link":"#自定义插件","children":[]}],"relativePath":"front-end-engineering/后处理器.md","lastUpdated":1715070178000}'),p={name:"front-end-engineering/后处理器.md"},o=l(`

PostCSS 后处理器

后处理器的概念

前面我们学习了 CSS 预处理器,CSS 预处理器(Sass、Less、Stylus)为我们提供了一套特殊的语法,让我们可以以编程的方式(变量、嵌套、内置函数、自定义函数、流程控制)来书写 CSS 样式。因此我们在学习 CSS 预处理器的时候,主要就是学习它们的语法。

CSS 后处理器不会提供专门的语法,它是在原生的 CSS 代码的基础上面做处理,常见的处理工作如下:

  1. 兼容性处理:自动添加浏览器前缀(如 -webkit-、-moz- 和 -ms-)以确保跨浏览器兼容性。这种后处理器的一个典型例子是 autoprefixer

  2. 代码优化与压缩:移除多余的空格、注释和未使用的规则,以减小 CSS 文件的大小。例如,cssnano 是一个流行的 CSS 压缩工具。

  3. 功能增强:添加新的 CSS 特性,使开发者能够使用尚未在所有浏览器中实现的 CSS 功能。例如,PostCSS 是一个强大的 CSS 后处理器,提供了很多插件来扩展 CSS 的功能。

  4. 代码检查与规范:检查 CSS 代码的质量,以确保代码符合特定的编码规范和最佳实践。例如,stylelint 是一个强大的 CSS 检查工具,可以帮助你发现和修复潜在的问题。

后处理器实际上是有非常非常多的,autoprefixer、cssnano、stylelint 像这些工具都算是在对原生 CSS 做后处理工作。

这里就会涉及到一个问题,能够对 CSS 做后处理的工具(后处理器)非常非常多,此时就会存在我要将原生的 CSS 先放入到 A 工具进行处理,处理完成后放入到 B 工具进行处理,之后在 C、D、E、F.... 这种手动操作显然是比较费时费力的,我们期望有一种工具,能够自动化的完成这些后处理工作,这个工具就是 PostCSS。

PostCSS 是一个使用 JavaScript 编写的 CSS 后处理器,它更像是一个平台,类似于 Babel,它本身是不做什么具体的事情,它只负责一件事情,将原生 CSS 转换为 CSS 的抽象语法树(CSS AST),之后的事情就完全交给其他的插件。目前整个 PostCSS 插件生态有 200+ 的插件,每个插件可以帮助我们处理一种 CSS 后处理场景。

你可以在官网:https://www.postcss.parts/ 看到 PostCSS 里面所有的插件。

学习 PostCSS 其实主要就是学习里面常用的插件:

  • autoprefixer:自动为 CSS 中的属性添加浏览器前缀,以确保跨浏览器兼容性。
  • cssnext:使开发者能够使用尚未在所有浏览器中实现的 CSS 特性,如自定义属性(变量)、颜色函数等。
  • cssnano:优化并压缩 CSS 代码,以减小文件大小。
  • postcss-import:在一个 CSS 文件中导入其他 CSS 文件,实现 CSS 代码的模块化。
  • postcss-nested:支持 CSS 规则的嵌套,使 CSS 代码更加组织化和易于维护。
  • postcss-custom-properties:支持使用原生 CSS 变量(自定义属性)。
  • stylelintCSS 代码检查工具,旨在帮助开发者发现和修复潜在的 CSS 代码问题。

PostCSS 快速上手

首先创建一个项目目录 postcss-demo,使用 pnpm init 进行初始化,之后安装 postcss 依赖:

bash
pnpm add postcss autoprefixer -D
+
pnpm add postcss autoprefixer -D
+

接下来在 src 创建一个 index.css,书写测试的 CSS 代码:

css
body {
+  background-color: beige;
+  font-size: 16px;
+}
+
+.box1 {
+  transform: translate(100px);
+}
+
body {
+  background-color: beige;
+  font-size: 16px;
+}
+
+.box1 {
+  transform: translate(100px);
+}
+

接下来我们要对上面的代码进行后处理,创建一个 index.js,代码如下:

js
// 读取 CSS 文件
+// 使用 PostCSS 来对读取的 CSS 文件做后处理
+
+const fs = require("fs"); // 负责处理和文件读取相关的事情
+const postcss = require("postcss");
+// 引入插件,该插件负责为 CSS 代码添加浏览器前缀
+const autoprefixer = require("autoprefixer");
+
+const style = fs.readFileSync("src/index.css", "utf8");
+
+postcss([
+  autoprefixer({
+    overrideBrowserslist: "last 10 versions",
+  }),
+])
+  .process(style, { from: undefined })
+  .then((res) => {
+    console.log(res.css);
+  });
+
// 读取 CSS 文件
+// 使用 PostCSS 来对读取的 CSS 文件做后处理
+
+const fs = require("fs"); // 负责处理和文件读取相关的事情
+const postcss = require("postcss");
+// 引入插件,该插件负责为 CSS 代码添加浏览器前缀
+const autoprefixer = require("autoprefixer");
+
+const style = fs.readFileSync("src/index.css", "utf8");
+
+postcss([
+  autoprefixer({
+    overrideBrowserslist: "last 10 versions",
+  }),
+])
+  .process(style, { from: undefined })
+  .then((res) => {
+    console.log(res.css);
+  });
+

在上面的 JS 代码中,我们首先读取了 index.css 里面的 CSS 代码,然后通过 postcss 来做后处理器,注意 postcss 本身不做任何事情,它只负责将原生的 CSS 代码转为 CSS AST,具体的事情需要插件来完成。

上面的代码我们配置了 autoprefixer 这个插件,负责为 CSS 添加浏览器前缀。

postcss-cli 和配置文件

postcss-cli

cli 是一组单词的缩写(command line interface),为你提供了一组在命令行中可以操作的命令来进行处理。

postcss-cli 通过给我们提供一些命令行的命令来简化 postcss 的使用。

首先第一步还是安装:

bash
pnpm add postcss-cli -D
+
pnpm add postcss-cli -D
+

安装完成后,我们就可以通过 postcss-cli 所提供的命令来进行文件的编译操作,在 package.json 里面添加如下的脚本:

json
"scripts": {
+  	...
+    "build": "postcss src/index.css -o ./build.css"
+},
+
"scripts": {
+  	...
+    "build": "postcss src/index.css -o ./build.css"
+},
+
  • -o:表示编译后的输出文件,编译后的文件默认是带有源码映射。
  • --no-map:不需要源码映射
  • --watch:用于做文件变化的监听工作,当文件有变化的时候,会自动重新执行命令。注意如果使用了 --watch 来做源码文件变化的监听工作,那么一般建议把编辑器的自动保存功能关闭掉

关于 postcss-cli 这个命令行工具还提供了哪些命令以及哪些配置项目,可以参阅:https://www.npmjs.com/package/postcss-cli

配置文件

一般我们会把插件的配置书写到配置文件里面,在配置文件里面,我们就可以指定使用哪些插件,以及插件具体的配置选项。

要使用配置文件功能,可以在项目的根目录下面创建一个名为 postcss.config.js 的文件,当你使用 postcss-cli 或者构建工具(webpack、vite)来进行集成的时候,postcss 会自动加载配置文件。

在 postcss.config.js 文件中书写如下的配置:

js
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+

postcss 配置文件最主要的其实就是做插件的配置。postcss 官网没有提供配置文件相关的文档,但是我们可以在:https://github.com/postcss/postcss-load-config 这个地方看到 postcss 配置文件所支持的配置项目。

接下来我们来看一个 postcss 配置文件具体支持的配置项目:

  1. plugins:一个数组,里面包含要使用到的 postcss 的插件以及相关的插件配置。
js
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")({ preset: "default" })],
+};
+
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")({ preset: "default" })],
+};
+
  1. map:是否生成源码映射,对应的值为一个对象
js
module.exports = {
+  map: { inline: false },
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
module.exports = {
+  map: { inline: false },
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+

默认值为 false,因为源码映射一般是会单独存放在一个文件里面。

  1. syntax:用于指定 postcss 应该使用的 CSS 语法,默认情况下 postcss 处理的是标准的 CSS,但是有可能你的 CSS 是使用预处理器来写的,这个时候 postcss 是不认识的,所以这个时候需要安装对应的插件并且在配置中指明 syntax
js
module.exports = {
+  syntax: "postcss-scss",
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
module.exports = {
+  syntax: "postcss-scss",
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+

安装 postcss-scss 这个插件,并且在配置文件中指定 syntax 为 postcss-scss,之后 PostCSS 就能够认识你的 sass 语法。

  1. parser:配置自定义解析器。Postcss 默认的解析器为 postcss-safe-parser,负责将 CSS 字符串解析为 CSS AST,如果你要用其他的解析器,那么可以配置一下
js
const customParser = require("my-custom-parser");
+
+module.exports = {
+  parser: customParser,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
const customParser = require("my-custom-parser");
+
+module.exports = {
+  parser: customParser,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
  1. stringifier:自定义字符串化器。用于将 CSS AST 转回 CSS 字符串。如果你要使用其他的字符串化器,那么也是可以在配置文件中国呢进行指定的。
js
const customStringifier = require("my-custom-stringifier");
+
+module.exports = {
+  stringifier: customStringifier,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
const customStringifier = require("my-custom-stringifier");
+
+module.exports = {
+  stringifier: customStringifier,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+

最后还剩下两个配置项:from、to,这两个选项官方是不建议你配置的,而且你配置的大概率还会报错,报错信息如下:

Config Error: Can not set from or to options in config file, use CLI arguments instead

这个提示的意思是让我们不要在配置文件里面进行配置,而是通过命令行参数的形式来指定。

至于为什么,官方其实解释得很清楚了:

In most cases options.from && options.to are set by the third-party which integrates this package (CLI, gulp, webpack). It's unlikely one needs to set/use options.from && options.to within a config file.

因为在实际开发中,我们更多的是会使用构建工具(webpack、vite),这些工具会去指定入口文件和出口文件。

postcss 主流插件 part1

  • autoprefixer
  • cssnano
  • stylelint

autoprefixer

这里我们再来复习一下:

css
/* 编译前 */
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+
/* 编译前 */
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+
css
/* 编译后 */
+::-webkit-input-placeholder {
+  color: gray;
+}
+
+::-moz-placeholder {
+  color: gray;
+}
+
+:-ms-input-placeholder {
+  color: gray;
+}
+
+::-ms-input-placeholder {
+  color: gray;
+}
+
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+
/* 编译后 */
+::-webkit-input-placeholder {
+  color: gray;
+}
+
+::-moz-placeholder {
+  color: gray;
+}
+
+:-ms-input-placeholder {
+  color: gray;
+}
+
+::-ms-input-placeholder {
+  color: gray;
+}
+
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+

关于 autoprefixer 这个插件,本身没有什么好讲的,主要是要介绍 browerslist 这个知识点,这个 browerslist 主要是用来配置兼容的浏览器的范围。从第一款浏览器诞生到现在,浏览的种类以及版本是非常非常多的,因此我们在做兼容的时候,不可能把所有的浏览器都做兼容,并且也没有意义,一般是需要指定范围的。

browserslist 就包含了一些配置规则,我们可以通过这些配置规则来指定要兼容的浏览器的范围:

  • last n versions:支持最近的 n 个浏览器版本。last 2 versions 表示支持最近的两个浏览器版本
  • n% :支持全球使用率超过 n% 的浏览器。 > 1% 表示要支持全球使用率超过 1% 的浏览器
  • cover n%:覆盖 n% 的主流浏览器
  • not dead:支持所有“非死亡”的浏览器,已死亡的浏览器指的是那些已经停止更新的浏览器
  • not ie<11:排除 ie 11 以下的浏览器
  • chrome>=n :支持 chrome 浏览器大于等于 n 的版本

你可以在 https://github.com/browserslist/browserslist#full-list 看到 browserslist 可以配置的所有的值。

另外你可以在 https://browserslist.dev/?q=PiAxJQ%3D%3D 看到 browserslist 配置的值所对应的浏览器具体范围。

还有一点就是关于 browserslist 配置的值是可以有多个,如果有多条规则,可以使用关键词 or、and、not 来指定多条规则之间的关系。关于这些关键词如何组合不同的规则,可以参阅:https://github.com/browserslist/browserslist#query-composition

接下来我们来看一下如何配置 browserslist,常见的有三种方式:

  1. 在项目的根目录下面创建一个 .browerslistrc 的文件,在里面书写范围列表(这种方式是最推荐的)
js
>1%
+last 2 versions of
+not dead
+
>1%
+last 2 versions of
+not dead
+
  1. 在 package.json 里面添加一个 browserslist 字段,然后进行配置:
json
{
+  "name": "xxx",
+  "version": : "xxx",
+  ...
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}
+
{
+  "name": "xxx",
+  "version": : "xxx",
+  ...
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}
+
  1. 可以在 postcss.config.js 配置文件中进行配置
js
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+

cssnano

这是一个使用率非常高的插件,因为该插件做的事情是对 CSS 进行一个压缩

cssnano 对应的官网地址:https://cssnano.co/

使用之前第一步还是安装:

bash
pnpm add cssnano -D
+
pnpm add cssnano -D
+

之后就是在配置文件中配置这个插件:

js
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")],
+};
+
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")],
+};
+

简单的使用方式演示完了之后,接下来延伸出来了两个问题:

  • 现在我们插件的数量从之前的一个变成多个,插件之间是否有顺序关系?
    • 在 postcss.config.js 文件里面配置插件的时候,一定要注意插件的顺序,这一点是非常重要的,因为有一些插件依赖于其他的插件的输出,你可以将 plugins 对应的数组看作是一个流水线的操作。先交给数组的第一项插件进行处理,之后将处理结果交给数组配置的第二项插件进行处理,以此类推...
  • cssnano 是否需要传入配置
    • 理论上来讲,是不需要的,因为 cssnano 默认的预设就已经非常好了,一般我们不需要做其他的配置
    • cssnano 本身又是由一些其他的插件组成的
      • postcss-discard-comments:删除 CSS 中的注释。
      • postcss-discard-duplicates:删除 CSS 中的重复规则。
      • postcss-discard-empty:删除空的规则、媒体查询和声明。
      • postcss-discard-overridden:删除被后来的相同规则覆盖的无效规则。
      • postcss-normalize-url:优化和缩短 URL。
      • postcss-minify-font-values:最小化字体属性值。
      • postcss-minify-gradients:最小化渐变表示。
      • postcss-minify-params:最小化@规则的参数。
      • postcss-minify-selectors:最小化选择器。
      • postcss-normalize-charset:确保只有一个有效的字符集 @规则。
      • postcss-normalize-display-values:规范化 display 属性值。
      • postcss-normalize-positions:规范化背景位置属性。
      • postcss-normalize-repeat-style:规范化背景重复样式。
      • postcss-normalize-string:规范化引号。
      • postcss-normalize-timing-functions:规范化时间函数。
      • postcss-normalize-unicode:规范化 unicode-range 描述符。
      • postcss-normalize-whitespace:规范化空白字符。
      • postcss-ordered-values:规范化属性值的顺序。
      • postcss-reduce-initial:将初始值替换为更短的等效值。
      • postcss-reduce-transforms:减少变换属性中的冗余值。
      • postcss-svgo:优化和压缩内联 SVG。
      • postcss-unique-selectors:删除重复的选择器。
      • postcss-zindex:重新计算 z-index 值,以减小文件大小。

因此我们可以定制具体某一个插件的行为,例如:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("autoprefixer"),
+    require("cssnano")({
+      preset: [
+        "default",
+        {
+          discardComments: false,
+          discardEmpty: false,
+        },
+      ],
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("autoprefixer"),
+    require("cssnano")({
+      preset: [
+        "default",
+        {
+          discardComments: false,
+          discardEmpty: false,
+        },
+      ],
+    }),
+  ],
+};
+

配置项目的名字可以在 https://cssnano.co/docs/what-are-optimisations/ 这里找到。

stylelint

stylelint 是规范我们 CSS 代码的,能够将 CSS 代码统一风格。

bash
pnpm add stylelint stylelint-config-standard -D
+
pnpm add stylelint stylelint-config-standard -D
+

这里我们安装了两个依赖:

  • stylelint:做 CSS 代码风格校验,但是具体的校验规则它是不知道了,需要我们提供具体的校验规则
  • stylelint-config-standard:这是 stylelint 的一套校验规则,并且是一套标准规则

接下来我们就需要在项目的根目录下面创建一个 .stylelintrc ,这个文件就使用用来指定你的具体校验规则

js
{
+    "extends": "stylelint-config-standard"
+}
+
{
+    "extends": "stylelint-config-standard"
+}
+

在上面的代码中,我们指定了校验规则继承 stylelint-config-standard 这一套校验规则

之后在 postcss.config.js 里面进行插件的配置,配置的时候注意顺序

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [require("stylelint"), require("autoprefixer"), require("cssnano")],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [require("stylelint"), require("autoprefixer"), require("cssnano")],
+};
+
  • 能否在继承了 stylelint-config-standard 这一套校验规则的基础上自定义校验规则

    • 肯定是可以的。因为不同的公司编码规范会有不同,一套标准校验规则是没有办法覆盖所有的公司编码规范
    js
    {
    +    "extends": "stylelint-config-standard",
    +    "rules": {
    +        "comment-empty-line-before": null
    +    }
    +}
    +
    {
    +    "extends": "stylelint-config-standard",
    +    "rules": {
    +        "comment-empty-line-before": null
    +    }
    +}
    +

    通过上面的方式,我们就可以自定义校验规则。

    至于有哪些校验规则,可以在 stylelint 官网查询到的:https://stylelint.io/user-guide/rules/

  • 检查出来的问题能否自动修复

    • 当然也是可以修复的,但是要注意没有办法修复所有类型的问题,只有部分问题能够被修复
    • 要自动修复非常简单,只需要将 stylelint 插件的 fix 配置项配置为 true 即可
    js
    // postcss 配置主要其实就是做插件的配置
    +
    +module.exports = {
    +  plugins: [
    +    require("stylelint")({
    +      fix: true,
    +    }),
    +    require("autoprefixer"),
    +    // require("cssnano")
    +  ],
    +};
    +
    // postcss 配置主要其实就是做插件的配置
    +
    +module.exports = {
    +  plugins: [
    +    require("stylelint")({
    +      fix: true,
    +    }),
    +    require("autoprefixer"),
    +    // require("cssnano")
    +  ],
    +};
    +

postcss 主流插件 part2

这一小节继续介绍 postcss 里面的主流插件:

  • postcss-preset-env
  • postcss-import
  • purgecss

postcss-preset-env

postcss-preset-env 主要就是让开发者可以使用最新的的 CSS 语法,同时为了兼容会自动的将这些最新的 CSS 语法转换为旧版本浏览器能够支持的代码。

postcss-preset-env 的主要作用是:

  1. 让你能够使用最新的 CSS 语法,如:CSS Grid(网格布局)、CSS Variables(变量)等。
  2. 自动为你的 CSS 代码添加浏览器厂商前缀,如:-webkit-、-moz- 等。
  3. 根据你的浏览器兼容性需求,将 CSS 代码转换为旧版浏览器兼容的语法。
  4. 优化 CSS 代码,如:合并规则、删除重复的代码等。

在正式演示 postcss-preset-env 之前,首先我们需要了解一个知识点,叫做 CSSDB,这是一个跟踪 CSS 新功能和特性的数据库。我们的 CSS 规范一共可以分为 5 个阶段:从 stage0(草案) 到 stage4(已经纳入 W3C 标准)。CSSDB 就可以非常方便的查询某一个特性目前处于哪一个阶段,具体的实现情况目前是什么样的。

下面是关于 stage0 到 stage4 各个阶段的介绍:

  • Stage 0:草案 - 此阶段的规范还在非正式的讨论和探讨阶段,可能会有很多变化。通常不建议在生产环境中使用这些特性。
  • Stage 1:提案 - 此阶段的规范已经有了一个正式的文件,描述了新特性的初步设计。这些特性可能在未来变成标准,但仍然可能发生较大的改变。
  • Stage 2:草稿 - 在这个阶段,规范已经相对稳定,描述了功能的详细设计。一般来说,浏览器厂商会开始实现并测试这些特性。开发者可以在实验性的项目中尝试使用这些功能,但要注意跟踪规范的变化。
  • Stage 3:候选推荐 - 此阶段的规范已经基本稳定,主要进行浏览器兼容性测试和微调。开发者可以考虑在生产环境中使用这些特性,但需要确保兼容目标浏览器。
  • Stage 4:已纳入 W3C 标准 - 这些特性已经成为 W3C CSS 标准的一部分,已经得到了广泛支持。开发者可以放心在生产环境中使用这些特性。

你可以在 https://cssdb.org/ 这里看到各种新特性目前处于哪一个阶段。

假设现在我们书写如下的 CSS 代码:

css
.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+

这个 CSS 代码使用到了嵌套,这是原本只能在 CSS 预处理器里面才能使用的语法,目前官方已经在考虑原生支持了。

但是现在会涉及到一个问题,如果我们目前直接书写嵌套的语法,那么很多浏览器是不支持的,但是我又想使用最新的语法,我们就可以使用 postcss-preset-env 这个插件对最新的 CSS 语法进行降级处理。

首先第一步需要安装:

bash
pnpm add postcss-preset-env -D
+
pnpm add postcss-preset-env -D
+

该插件提供了一个配置选项:

  • stage:设置要使用的 CSS 特性的阶段,默认值为 20-4)。数字越小,包含的 CSS 草案特性越多,但稳定性可能较低。
  • browsers:设置目标浏览器范围,如:'last 2 versions' 或 '> 1%'。
  • autoprefixer:设置自动添加浏览器厂商前缀的配置,如:{ grid: true }。
  • preserve:是否保留原始 CSS 代码,默认为 false。如果设置为 true,则会在转换后的代码后面保留原始代码,以便新浏览器优先使用新语法。

在 postcss.config.js 配置文件中,配置该插件:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+

之后编译生成的 CSS 代码如下:

css
.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+
.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+

通过这个插件,我们就可以使用 CSS 规范中最时髦的语法,而不用担心浏览器的兼容问题。

postcss-import

该插件主要用于处理 CSS 文件中 @import 规则。在原生的 CSS 中,存在 @import,可以引入其他的 CSS 文件,但是在引入的时候会存在一个问题,就是客户端在解析 CSS 文件时,发现有 @import 就会发送 HTTP 请求去获取对应的 CSS 文件。

使用 postcss-import:

  • 将多个 CSS 文件合并为一个文件
  • 避免了浏览器对 @import 规则的额外请求,因为减少了 HTTP 请求,所以提高了性能
bash
pnpm add postcss-import -D
+
pnpm add postcss-import -D
+

安装完成后,在配置文件中进行一个简单的配置:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import"),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import"),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+

最终编译后的 CSS 结果如下:

css
.box1 {
+  background-color: red;
+}
+.box2 {
+  background-color: blue;
+}
+.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+
.box1 {
+  background-color: red;
+}
+.box2 {
+  background-color: blue;
+}
+.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+

另外该插件也提供了一些配置项:

  • path:设置查找 CSS 文件的路径,默认为当前文件夹。
  • plugins:允许你指定在处理被 @import 引入的 CSS 文件时使用的其他 PostCSS 插件。这些插件将在 postcss-import 合并文件之前对被引入的文件进行处理,之后再进行文件的合并

例如:

js
module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+      plugins: [postcssNested()],
+    }),
+    // 其他插件...
+  ],
+};
+
module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+      plugins: [postcssNested()],
+    }),
+    // 其他插件...
+  ],
+};
+

在上面的配置中,插件会在 src/css 目录下面去查找被引入的文件,另外文件在被合并到 index.css 之前,会被 postcssNested 这个插件先处理一遍,然后才会被合并到 index.css 里面。

你可以在 https://github.com/postcss/postcss-import 这里看到 postcss-import 所支持的所有配置项。

purgecss

该插件专门用于移除没有使用到的 CSS 样式的工具,相当于是 CSS 版本的 tree shaking(树摇),它会找到你文件中实际使用的 CSS 类名,并且移除没有使用到的样式,这样可以有效的减少 CSS 文件的大小,提升传输速度。

官网地址:https://purgecss.com/

首先我们还是安装该插件:

bash
pnpm add @fullhuman/postcss-purgecss -D
+
pnpm add @fullhuman/postcss-purgecss -D
+

接下来我们在 src 下面创建一个 index.html,书写如下的代码:

html
<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Document</title>
+  <link rel="stylesheet" href="./index.css" />
+</head>
+<body>
+  <div class="container">
+    <div class="box1"></div>
+  </div>
+</body>
+
<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Document</title>
+  <link rel="stylesheet" href="./index.css" />
+</head>
+<body>
+  <div class="container">
+    <div class="box1"></div>
+  </div>
+</body>
+

该 html 只用到了少量的 CSS 样式类。

目前我们的 index.css 代码如下:

css
@import "a.css";
+@import "b.css";
+
+.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
+.container {
+  font-size: 20px;
+}
+
+p {
+  color: red;
+}
+
@import "a.css";
+@import "b.css";
+
+.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
+.container {
+  font-size: 20px;
+}
+
+p {
+  color: red;
+}
+

接下来我在 postcss.config.js 配置文件中引入 @fullhuman/postcss-purgecss 这个插件,具体的配置如下:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+    }),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+    require("@fullhuman/postcss-purgecss")({
+      content: ["./src/**/*.html"],
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+    }),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+    require("@fullhuman/postcss-purgecss")({
+      content: ["./src/**/*.html"],
+    }),
+  ],
+};
+

我们引入 @fullhuman/postcss-purgecss 这个插件后,还做了 content 这个配置项目的相关配置,该配置项表示我具体的参照文件。也就是说,CSS 样式类有没有用上需要有一个具体的参照文件。

最终编译出来的结果如下:

css
.box1 {
+  background-color: red;
+}
+.container {
+  font-size: 20px;
+}
+
.box1 {
+  background-color: red;
+}
+.container {
+  font-size: 20px;
+}
+

@fullhuman/postcss-purgecss 这个插件除了 content 这个配置项,还有一个配置项也非常的常用:

  • safelist:可以指定一个字符串的值,或者指定一个正则表达式,该配置项目所对应的值(CSS 样式规则)始终保留,即便在参照文件中没有使用到也需要保留
js
const purgecss = require("@fullhuman/postcss-purgecss");
+
+module.exports = {
+  plugins: [
+    // 其他插件...
+    purgecss({
+      content: ["./src/**/*.html", "./src/**/*.js"],
+      safelist: [/^active-/],
+    }),
+  ],
+};
+
const purgecss = require("@fullhuman/postcss-purgecss");
+
+module.exports = {
+  plugins: [
+    // 其他插件...
+    purgecss({
+      content: ["./src/**/*.html", "./src/**/*.js"],
+      safelist: [/^active-/],
+    }),
+  ],
+};
+

safelist 所对应的值的含义为:匹配 active- 开头的类名,这些类名即便在项目文件中没有使用到,但是也不要删除。

抽象语法树

自定义插件第一小节我们来看一下抽象语法树。

官方对 Postcss 的原理介绍如下:

PostCSS takes a CSS file and provides an API to analyze and modify its rules (by transforming them into an Abstract Syntax Tree). This API can then be used by plugins to do a lot of useful things, e.g., to find errors automatically, or to insert vendor prefixes.

Postcss 的工作流程如下图所示:

image-20230626193847754

关于抽象语法树这个概念其实是非常重要的,你在很多地方都能看到它。

当我们遇到一个难以理解的术语的时候,有一个最简单的方式就是“拆词”。“抽象语法树”经过拆词就可以拆解为三个词:

  • 抽象
  • 语法

我们首先来看树。“树”实际上是一种数据结构。

所谓数据结构,就是指数据在计算机中组织和存储的一种方式。数据结构通常会分为两类:

  • 线性数据结构
    • 数组(Array):一种连续存储空间中的固定大小的数据项集合。数组将相同类型的元素存储在连续的内存位置中,允许通过索引快速访问元素。
    • 链表(Linked List):一种由节点组成的线性集合,每个节点包含数据和指向下一个节点的指针。链表允许在不重新分配整个数据结构的情况下插入和删除元素。
    • 栈(Stack):一种遵循后进先出(LIFO,Last In First Out)原则的线性数据结构。在栈中,数据项的添加和移除都在同一端进行,称为栈顶。
    • 队列(Queue):一种遵循先进先出(FIFO,First In First Out)原则的线性数据结构。在队列中,数据项的添加在一端进行(队尾),移除在另一端进行(队头)。
  • 非线性数据结构
    • 树(Tree):一种分层结构,由节点组成,其中有一个特殊的节点称为根节点,其余节点按照层级组织。每个节点(除根节点外)都有一个父节点,可以有多个子节点。常见的树结构有二叉树、红黑树、AVL 树等。
    • 图(Graph):一种由顶点(节点)和边组成的数据结构,边连接了顶点。图可以是有向的(边有方向)或无向的(边无方向)。图可用于表示具有复杂关系的数据集合。

接下来我们聚焦到“树”这种数据结构,树这种非线性的数据结构,在解决某些问题的时候有一些显著的特点:

  • 层次关系:通过树结构能够非常自然的表示出数据之间的层次关系,这是其他数据结构办不到的。
  • 搜索效率:通过树的结构(平衡二叉树),在执行搜索、插入以及删除等操作时,效率是比较高的,时间复杂度通常为 O(log n),n是树的节点数量。一般比线性的数据结构(数组、链表)要高很多。
  • 动态数据集合:与数组等固定大小的数据结构相比,树结构可以方便地添加、删除和重新组织节点。这使得树结构非常适合用于动态变化的数据集合。
  • 有序存储:在二叉搜索树等有序树结构中,数据按照一定的顺序进行组织。这允许我们在 O(log n) 时间内完成有序数据集合的操作,如查找最大值、最小值和前驱、后继等。
  • 空间优化:在某些应用场景中,树结构可以有效地节省空间。例如,字典树(Trie)可以用于存储大量字符串,同时节省空间,因为公共前缀只存储一次。
  • 分治策略:树结构天然地适应分治策略,可以将复杂问题分解为较小的子问题并递归求解。许多高效的算法都基于树结构,如排序算法(归并排序、快速排序)、图算法(最小生成树、最短路径等)。

上面的这些优点,如果你没有系统的学习过数据结构相关的知识,你是没有办法很多的进行理解。但是这个并不影响我们学习抽象语法树。上面所罗列的这些特点只是为了说明一点:树这种数据结构是存在很多优点的,所以我们能够在很多地方看到树的身影,例如:DOM树、CSSOM树、语法树。

接下来我们重点来说语法树。什么是语法树?简单来讲,就是将我们所书写的源代码转为树的结构。

js
var a = 42;
+var b = 5;
+function addA(d) {
+    return a + d;
+}
+var c = addA(2) + b;
+
var a = 42;
+var b = 5;
+function addA(d) {
+    return a + d;
+}
+var c = addA(2) + b;
+

对于编译器或者解释器来讲,上面的代码它们并不能够理解。上面的这些代码对于编译器或者解释器来讲,无非就是一段字符串而已:

js
'var a = 42;var b = 5;function addA(d) {return a + d;}var c = addA(2) + b;'
+
'var a = 42;var b = 5;function addA(d) {return a + d;}var c = addA(2) + b;'
+

因此要执行这个代码,编译器或解释器首先第一步就是要分析出来这个字符串里面哪些是关键字,哪些是标志符,哪些是运算符。之后会形成一个一个的 token,例如上面的代码,最终就会形成各种各样的 token(不可再拆分、最小的单位):

js
Keyword(var) Identifier(a) Punctuator(=) Numeric(42) Punctuator(;) Keyword(var) 
+Identifier(b) Punctuator(=) Numeric(5) Punctuator(;) Keyword(function) 
+Identifier(addA) Punctuator(() Identifier(d) Punctuator()) Punctuator({) 
+Keyword(return) Identifier(a) Punctuator(+) Identifier(d) Punctuator(;) 
+Punctuator(}) Keyword(var) Identifier(c) Punctuator(=) Identifier(addA) 
+Punctuator(() Numeric(2) Punctuator()) Punctuator(+) Identifier(b) Punctuator(;)
+
Keyword(var) Identifier(a) Punctuator(=) Numeric(42) Punctuator(;) Keyword(var) 
+Identifier(b) Punctuator(=) Numeric(5) Punctuator(;) Keyword(function) 
+Identifier(addA) Punctuator(() Identifier(d) Punctuator()) Punctuator({) 
+Keyword(return) Identifier(a) Punctuator(+) Identifier(d) Punctuator(;) 
+Punctuator(}) Keyword(var) Identifier(c) Punctuator(=) Identifier(addA) 
+Punctuator(() Numeric(2) Punctuator()) Punctuator(+) Identifier(b) Punctuator(;)
+

拆解成一个一个的 token 之后,会将这些 token 以树的形式来存储,最终会形成如下的一个树结构:

image-20230626200342918

https://www.jointjs.com/demos/abstract-syntax-tree 这个网站可以看到 JS 代码所形成的抽象语法树长什么样子。

至此,你就知道什么叫做语法树。

最后我们还需要解释一下什么叫做“抽象语法树”。

抽象(abstraction)是一种思维方式。所谓抽象,指的是从具体的事物里面提取出本质特征、规律,忽略不相关、不重要的细节。在计算机科学和编程里面,抽象是一种非常重要的方法,因为抽象能够将一个复杂的问题抽离成简单的、更加容易理解的问题

抽象语法树是将源代码抽象成一种更高阶别的表示方式,只关注代码的结构和语法,会去忽略空格、换行、制表符之类的表达细节。

最后说一下抽象语法树的优点:

  1. 易于操作和遍历:可以更方便地进行操作和遍历。AST 中的每个节点都有确定的类型和结构,这使得插件作者可以轻松地定位和修改特定类型的节点,而无需解析和操作原始 CSS 文本。

  2. 易于扩展:使用 AST 可以轻松地支持新的 CSS 语法和特性。只需在 AST 中添加相应的节点类型和规则,就可以在插件中处理新的语法结构,而无需对整个解析器进行重大改动。

  3. 提高性能:将 CSS 代码转换为 AST 后,可以对整个树进行一次遍历,同时应用多个插件的变换操作。这样可以减少重复解析和操作 CSS 文本的开销,从而提高处理性能。

  4. 代码重用和模块化:由于 AST 的结构化特性,插件开发者可以在多个插件之间重用和共享操作 AST 的代码。这有助于降低插件间的冗余,并提高代码的模块化程度。

  5. 易于调试和错误处理:AST 中的每个节点都包含有关其源代码位置的元信息。这使得插件可以在出现错误时提供更具体的错误信息和上下文,从而帮助开发者快速定位和解决问题。

著名的 babel 项目在处理 JS 的时候就是会先将 JS 转为抽象语法树,然后再交给其他的插件做处理。ESlint 工具检查代码是否规范,那它怎么检查的?它其实也是先将代码转为抽象语法树,然后再去检查。我们这里所学习的 postcss 也是同样的原理,只不过它是将 css 代码转为对应的 css 抽象语法树。

自定义插件

在 PostCSS 官网,实际上已经介绍了如何去编写一个自定义插件:https://postcss.org/docs/writing-a-postcss-plugin

  1. 需要有一个模板
js
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: 'PLUGIN NAME'
+    // Plugin listeners
+  }
+}
+module.exports.postcss = true
+
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: 'PLUGIN NAME'
+    // Plugin listeners
+  }
+}
+module.exports.postcss = true
+

接下来就可以在插件里面添加一组监听器,对应的能够设置的监听器如下:

  • Root: node of the top of the tree, which represent CSS file.
  • AtRule: statements begin with @ like @charset "UTF-8" or @media (screen) {}.
  • Rule: selector with declaration inside. For instance input, button {}.
  • Declaration: key-value pair like color: black;
  • Comment: stand-alone comment. Comments inside selectors, at-rule parameters and values are stored in node’s raws property.
  1. 具体示例

现在在我们的 src 中新建一个 my-plugin.js 的文件,代码如下:

js
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "PLUGIN NAME",
+    Declaration(decl){
+        console.log(decl.prop, decl.value)
+    }
+  };
+};
+module.exports.postcss = true;
+
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "PLUGIN NAME",
+    Declaration(decl){
+        console.log(decl.prop, decl.value)
+    }
+  };
+};
+module.exports.postcss = true;
+

在上面的代码中,我们添加了 Declaration 的监听器,通过该监听器能够拿到 CSS 文件中所有的声明。

接下来我们就可以对其进行相应的操作。

现在我们来做一个具体的示例:编写一个插件,该插件能够将 CSS 代码中所有的颜色统一转为十六进制。

这里我们需要使用到一个依赖包:color 该依赖就是专门做颜色处理的

bash
pnpm add color -D
+
pnpm add color -D
+

之后通过该依赖所提供的 hex 方法来进行颜色值的修改,具体代码如下:

js
const Color = require("color");
+
+module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "convertColorsToHex",
+    Declaration(decl) {
+      // 先创建一个正则表达式,提取出如下的声明
+      // 因为如下的声明对应的值一般都是颜色值
+      const colorRegex = /(^color)|(^background(-color)?)/;
+      if (colorRegex.test(decl.prop)) {
+        try {
+          // 将颜色值转为 Color 对象,因为这个 Color 对象对应了一系列的方法
+          // 方便我们进行转换
+          const color = Color(decl.value);
+          // 将颜色值转换为十六进制
+          const hex = color.hex();
+          // 更新属性值
+          decl.value = hex;
+        } catch (err) {
+          console.error(
+            \`[convertColorsToHex] Error processing \${decl.prop}: \${error.message}\`
+          );
+        }
+      }
+    },
+  };
+};
+module.exports.postcss = true;
+
const Color = require("color");
+
+module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "convertColorsToHex",
+    Declaration(decl) {
+      // 先创建一个正则表达式,提取出如下的声明
+      // 因为如下的声明对应的值一般都是颜色值
+      const colorRegex = /(^color)|(^background(-color)?)/;
+      if (colorRegex.test(decl.prop)) {
+        try {
+          // 将颜色值转为 Color 对象,因为这个 Color 对象对应了一系列的方法
+          // 方便我们进行转换
+          const color = Color(decl.value);
+          // 将颜色值转换为十六进制
+          const hex = color.hex();
+          // 更新属性值
+          decl.value = hex;
+        } catch (err) {
+          console.error(
+            \`[convertColorsToHex] Error processing \${decl.prop}: \${error.message}\`
+          );
+        }
+      }
+    },
+  };
+};
+module.exports.postcss = true;
+
`,202),e=[o];function r(c,t,B,i,y,F){return n(),a("div",null,e)}const b=s(p,[["render",r]]);export{d as __pageData,b as default}; diff --git "a/assets/front-end-engineering_\345\220\216\345\244\204\347\220\206\345\231\250.md.b789bac1.lean.js" "b/assets/front-end-engineering_\345\220\216\345\244\204\347\220\206\345\231\250.md.b789bac1.lean.js" new file mode 100644 index 00000000..b505a121 --- /dev/null +++ "b/assets/front-end-engineering_\345\220\216\345\244\204\347\220\206\345\231\250.md.b789bac1.lean.js" @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const d=JSON.parse('{"title":"PostCSS 后处理器","description":"","frontmatter":{},"headers":[{"level":3,"title":"后处理器的概念","slug":"后处理器的概念","link":"#后处理器的概念","children":[]},{"level":3,"title":"PostCSS 快速上手","slug":"postcss-快速上手","link":"#postcss-快速上手","children":[]},{"level":2,"title":"postcss-cli 和配置文件","slug":"postcss-cli-和配置文件","link":"#postcss-cli-和配置文件","children":[{"level":3,"title":"postcss-cli","slug":"postcss-cli","link":"#postcss-cli","children":[]},{"level":3,"title":"配置文件","slug":"配置文件","link":"#配置文件","children":[]}]},{"level":2,"title":"postcss 主流插件 part1","slug":"postcss-主流插件-part1","link":"#postcss-主流插件-part1","children":[{"level":3,"title":"autoprefixer","slug":"autoprefixer","link":"#autoprefixer","children":[]},{"level":3,"title":"cssnano","slug":"cssnano","link":"#cssnano","children":[]},{"level":3,"title":"stylelint","slug":"stylelint","link":"#stylelint","children":[]}]},{"level":2,"title":"postcss 主流插件 part2","slug":"postcss-主流插件-part2","link":"#postcss-主流插件-part2","children":[{"level":3,"title":"postcss-preset-env","slug":"postcss-preset-env","link":"#postcss-preset-env","children":[]},{"level":3,"title":"postcss-import","slug":"postcss-import","link":"#postcss-import","children":[]},{"level":3,"title":"purgecss","slug":"purgecss","link":"#purgecss","children":[]}]},{"level":2,"title":"抽象语法树","slug":"抽象语法树","link":"#抽象语法树","children":[]},{"level":2,"title":"自定义插件","slug":"自定义插件","link":"#自定义插件","children":[]}],"relativePath":"front-end-engineering/后处理器.md","lastUpdated":1715070178000}'),p={name:"front-end-engineering/后处理器.md"},o=l("",202),e=[o];function r(c,t,B,i,y,F){return n(),a("div",null,e)}const b=s(p,[["render",r]]);export{d as __pageData,b as default}; diff --git a/assets/getting-started.md.fd6d908e.js b/assets/getting-started.md.fd6d908e.js new file mode 100644 index 00000000..4f51309f --- /dev/null +++ b/assets/getting-started.md.fd6d908e.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as i,a as n}from"./app.f983686f.js";const a="/blog/assets/js-c.40a46866.png",r="/blog/assets/mini-any.2d945c6b.png",y=JSON.parse('{"title":"My Projects","description":"","frontmatter":{},"headers":[{"level":3,"title":"js-challenges","slug":"js-challenges","link":"#js-challenges","children":[]},{"level":3,"title":"mini-anythings","slug":"mini-anythings","link":"#mini-anythings","children":[]},{"level":3,"title":"BOSScript","slug":"bosscript","link":"#bosscript","children":[]},{"level":3,"title":"rc-design","slug":"rc-design","link":"#rc-design","children":[]},{"level":3,"title":"cherry","slug":"cherry","link":"#cherry","children":[]},{"level":3,"title":"rollup-plugin-alias","slug":"rollup-plugin-alias","link":"#rollup-plugin-alias","children":[]},{"level":3,"title":"commencer","slug":"commencer","link":"#commencer","children":[]},{"level":3,"title":"tiny-react","slug":"tiny-react","link":"#tiny-react","children":[]},{"level":3,"title":"treejs","slug":"treejs","link":"#treejs","children":[]},{"level":3,"title":"lodash-ts","slug":"lodash-ts","link":"#lodash-ts","children":[]},{"level":3,"title":"tiny-vue","slug":"tiny-vue","link":"#tiny-vue","children":[]},{"level":3,"title":"Native-project","slug":"native-project","link":"#native-project","children":[]},{"level":3,"title":"shooks","slug":"shooks","link":"#shooks","children":[]},{"level":3,"title":"mini-webpack","slug":"mini-webpack","link":"#mini-webpack","children":[]},{"level":3,"title":"ts-lib-vite","slug":"ts-lib-vite","link":"#ts-lib-vite","children":[]},{"level":3,"title":"esbuild-plugins","slug":"esbuild-plugins","link":"#esbuild-plugins","children":[]},{"level":3,"title":"tiny-vite","slug":"tiny-vite","link":"#tiny-vite","children":[]},{"level":3,"title":"vite-plugins","slug":"vite-plugins","link":"#vite-plugins","children":[]},{"level":3,"title":"tiny-complier","slug":"tiny-complier","link":"#tiny-complier","children":[]},{"level":3,"title":"keep-everyday","slug":"keep-everyday","link":"#keep-everyday","children":[]},{"level":3,"title":"text-image","slug":"text-image","link":"#text-image","children":[]},{"level":3,"title":"jsx-compilation","slug":"jsx-compilation","link":"#jsx-compilation","children":[]},{"level":3,"title":"awesome-native","slug":"awesome-native","link":"#awesome-native","children":[]},{"level":3,"title":"vsc-delete-func","slug":"vsc-delete-func","link":"#vsc-delete-func","children":[]},{"level":3,"title":"eslint-plugin-reviewget","slug":"eslint-plugin-reviewget","link":"#eslint-plugin-reviewget","children":[]},{"level":3,"title":"babel-plugin-dev-debug","slug":"babel-plugin-dev-debug","link":"#babel-plugin-dev-debug","children":[]},{"level":3,"title":"webpack-expand-lib","slug":"webpack-expand-lib","link":"#webpack-expand-lib","children":[]},{"level":3,"title":"network-speed-js","slug":"network-speed-js","link":"#network-speed-js","children":[]},{"level":2,"title":"TODO ...","slug":"todo","link":"#todo","children":[]}],"relativePath":"getting-started.md","lastUpdated":1710671456000}'),l={name:"getting-started.md"},s=n('

My Projects

TODO:分类

js-challenges

https://github.com/Sunny-117/js-challenges

✨✨✨ Challenge your JavaScript programming limits step by step

mini-anythings

https://github.com/Sunny-117/mini-anything

🚀 Explore the source code of the front-end library and implement a super mini version

BOSScript

https://github.com/Sunny-117/BOSScript

Boss's direct recruitment and delivery, shutdown, one-stop service of the oil monkey script, allowing you to submit resumes overseas in just 2 minutes

rc-design

https://github.com/Sunny-117/rc-design

🗃️ rc-design is a component library developed for react, providing developers with a more lightweight and concise component library choice. Use tsx to write logic, less to write styles, dumi2 to write documentation sites, and jest+ts-jest+react-testing-library for unit testing.

cherry

https://github.com/Sunny-117/cherry

✨ A lightweight JavaScript packaging library based on magic-string and acorn, supporting tree-shaking

rollup-plugin-alias

https://github.com/Sunny-117/rollup-plugin-alias

🍣 A Rollup plugin for defining aliases when bundling packages.

commencer

https://github.com/Sunny-117/commencer

Starter template for xxx

tiny-react

https://github.com/Sunny-117/tiny-react

🌱 The closest implementation to the React source code

treejs

https://github.com/Sunny-117/treejs

🌱 Easy to learn, high-performance, and highly scalable Tree components, supporting Vuejs and React simultaneously

lodash-ts

https://github.com/Sunny-117/lodash-ts

tiny-vue

https://github.com/Sunny-117/tiny-vue

Native-project

https://github.com/Sunny-117/Native-project

Native JavaScript project collection, Github China latest version

shooks

https://github.com/Sunny-117/shooks

📦️ A high-quality & reliable React Hooks library.

mini-webpack

https://github.com/Sunny-117/mini-webpack

手写一个简易版的webpack

ts-lib-vite

https://github.com/Sunny-117/ts-lib-vite

A foundation for developing front-end utility libraries using Vite and TypeScript.

esbuild-plugins

https://github.com/Sunny-117/esbuild-plugins

packages of esbuild plugins

tiny-vite

https://github.com/Sunny-117/tiny-vite

⚡️ a lightweight frontend build tool designed to deliver swift development experiences and efficient build processes

vite-plugins

https://github.com/Sunny-117/vite-plugins

tiny-complier

实现超级 mini 的编译器 | codegen&compiler 生成代码 | 只需要 200 行代码 | 前端编译原理

https://github.com/Sunny-117/tiny-complier

keep-everyday

https://github.com/Sunny-117/keep-everyday

使用 Github Actions 来完成自动创建 issues 任务

text-image

https://github.com/Sunny-117/text-image

🐛🐛🐛 text-image 可以将文字、图片、视频进行「文本化」,只需要通过简单的配置即可使用

jsx-compilation

https://github.com/Sunny-117/jsx-compilation

🍻 实现 JSX 语法转成 JS 语法的编译器

awesome-native

https://github.com/Sunny-117/awesome-native

🔧 Collection of native JavaScript projects

vsc-delete-func

https://github.com/Sunny-117/vsc-delete-func

🍻🍻🍻 vscode plugins

eslint-plugin-reviewget

https://github.com/Sunny-117/eslint-plugin-reviewget

🚀当用户使用 getXXX get开头的函数的时候 如果不返回值的话 那么就会报错 🐛可以 fix 🎉用户可以自行配置是否 fix

babel-plugin-dev-debug

https://github.com/Sunny-117/babel-plugin-dev-debug

an babel plugin that for dev debug

webpack-expand-lib

https://github.com/Sunny-117/webpack-expand-lib

🚀 some expansion libs of webpack

network-speed-js

https://github.com/Sunny-117/network-speed-js

A small tool for testing network speed. It also has the ability to test internal and external networks.

TODO ...

',86),h=[s];function p(o,c,d,u,g,b){return t(),i("div",null,h)}const v=e(l,[["render",p]]);export{y as __pageData,v as default}; diff --git a/assets/getting-started.md.fd6d908e.lean.js b/assets/getting-started.md.fd6d908e.lean.js new file mode 100644 index 00000000..c4ffdab2 --- /dev/null +++ b/assets/getting-started.md.fd6d908e.lean.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as i,a as n}from"./app.f983686f.js";const a="/blog/assets/js-c.40a46866.png",r="/blog/assets/mini-any.2d945c6b.png",y=JSON.parse('{"title":"My Projects","description":"","frontmatter":{},"headers":[{"level":3,"title":"js-challenges","slug":"js-challenges","link":"#js-challenges","children":[]},{"level":3,"title":"mini-anythings","slug":"mini-anythings","link":"#mini-anythings","children":[]},{"level":3,"title":"BOSScript","slug":"bosscript","link":"#bosscript","children":[]},{"level":3,"title":"rc-design","slug":"rc-design","link":"#rc-design","children":[]},{"level":3,"title":"cherry","slug":"cherry","link":"#cherry","children":[]},{"level":3,"title":"rollup-plugin-alias","slug":"rollup-plugin-alias","link":"#rollup-plugin-alias","children":[]},{"level":3,"title":"commencer","slug":"commencer","link":"#commencer","children":[]},{"level":3,"title":"tiny-react","slug":"tiny-react","link":"#tiny-react","children":[]},{"level":3,"title":"treejs","slug":"treejs","link":"#treejs","children":[]},{"level":3,"title":"lodash-ts","slug":"lodash-ts","link":"#lodash-ts","children":[]},{"level":3,"title":"tiny-vue","slug":"tiny-vue","link":"#tiny-vue","children":[]},{"level":3,"title":"Native-project","slug":"native-project","link":"#native-project","children":[]},{"level":3,"title":"shooks","slug":"shooks","link":"#shooks","children":[]},{"level":3,"title":"mini-webpack","slug":"mini-webpack","link":"#mini-webpack","children":[]},{"level":3,"title":"ts-lib-vite","slug":"ts-lib-vite","link":"#ts-lib-vite","children":[]},{"level":3,"title":"esbuild-plugins","slug":"esbuild-plugins","link":"#esbuild-plugins","children":[]},{"level":3,"title":"tiny-vite","slug":"tiny-vite","link":"#tiny-vite","children":[]},{"level":3,"title":"vite-plugins","slug":"vite-plugins","link":"#vite-plugins","children":[]},{"level":3,"title":"tiny-complier","slug":"tiny-complier","link":"#tiny-complier","children":[]},{"level":3,"title":"keep-everyday","slug":"keep-everyday","link":"#keep-everyday","children":[]},{"level":3,"title":"text-image","slug":"text-image","link":"#text-image","children":[]},{"level":3,"title":"jsx-compilation","slug":"jsx-compilation","link":"#jsx-compilation","children":[]},{"level":3,"title":"awesome-native","slug":"awesome-native","link":"#awesome-native","children":[]},{"level":3,"title":"vsc-delete-func","slug":"vsc-delete-func","link":"#vsc-delete-func","children":[]},{"level":3,"title":"eslint-plugin-reviewget","slug":"eslint-plugin-reviewget","link":"#eslint-plugin-reviewget","children":[]},{"level":3,"title":"babel-plugin-dev-debug","slug":"babel-plugin-dev-debug","link":"#babel-plugin-dev-debug","children":[]},{"level":3,"title":"webpack-expand-lib","slug":"webpack-expand-lib","link":"#webpack-expand-lib","children":[]},{"level":3,"title":"network-speed-js","slug":"network-speed-js","link":"#network-speed-js","children":[]},{"level":2,"title":"TODO ...","slug":"todo","link":"#todo","children":[]}],"relativePath":"getting-started.md","lastUpdated":1710671456000}'),l={name:"getting-started.md"},s=n("",86),h=[s];function p(o,c,d,u,g,b){return t(),i("div",null,h)}const v=e(l,[["render",p]]);export{y as __pageData,v as default}; diff --git a/assets/html-css_CSS.md.53af0be0.js b/assets/html-css_CSS.md.53af0be0.js new file mode 100644 index 00000000..0c81ca62 --- /dev/null +++ b/assets/html-css_CSS.md.53af0be0.js @@ -0,0 +1,3035 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-21-02-59.47222bd8.png",o="/blog/assets/2023-02-01-20-57-45.daee2fcd.png",u=JSON.parse('{"title":"CSS3","description":"","frontmatter":{},"headers":[{"level":2,"title":"introduction","slug":"introduction","link":"#introduction","children":[{"level":3,"title":"1.历史","slug":"_1-历史","link":"#_1-历史","children":[]},{"level":3,"title":"2.处理器","slug":"_2-处理器","link":"#_2-处理器","children":[]},{"level":3,"title":"3.怎么用","slug":"_3-怎么用","link":"#_3-怎么用","children":[]},{"level":3,"title":"4.CSS3 进化到编程化:cssNext","slug":"_4-css3-进化到编程化-cssnext","link":"#_4-css3-进化到编程化-cssnext","children":[]}]},{"level":2,"title":"border","slug":"border","link":"#border","children":[{"level":3,"title":"1.border","slug":"_1-border","link":"#_1-border","children":[]},{"level":3,"title":"2.box-shallow","slug":"_2-box-shallow","link":"#_2-box-shallow","children":[]},{"level":3,"title":"3.border-image","slug":"_3-border-image","link":"#_3-border-image","children":[]}]},{"level":2,"title":"background","slug":"background","link":"#background","children":[{"level":3,"title":"1.background-origin:","slug":"_1-background-origin","link":"#_1-background-origin","children":[]},{"level":3,"title":"2.background-clip","slug":"_2-background-clip","link":"#_2-background-clip","children":[]},{"level":3,"title":"3.background-repeat","slug":"_3-background-repeat","link":"#_3-background-repeat","children":[]},{"level":3,"title":"4.background-attachment","slug":"_4-background-attachment","link":"#_4-background-attachment","children":[]},{"level":3,"title":"5.background-size","slug":"_5-background-size","link":"#_5-background-size","children":[]}]},{"level":2,"title":"text","slug":"text","link":"#text","children":[{"level":3,"title":"1.text-shadow","slug":"_1-text-shadow","link":"#_1-text-shadow","children":[]},{"level":3,"title":"2.   text 系列","slug":"_2-text-系列","link":"#_2-text-系列","children":[]}]},{"level":2,"title":"box","slug":"box","link":"#box","children":[{"level":3,"title":"1.IE6 混杂模式盒子","slug":"_1-ie6-混杂模式盒子","link":"#_1-ie6-混杂模式盒子","children":[]},{"level":3,"title":"2.flex 弹性盒子","slug":"_2-flex-弹性盒子","link":"#_2-flex-弹性盒子","children":[]}]},{"level":2,"title":"响应式网站开发","slug":"响应式网站开发","link":"#响应式网站开发","children":[{"level":3,"title":"2.响应式网页设计","slug":"_2-响应式网页设计","link":"#_2-响应式网页设计","children":[]},{"level":3,"title":"3.设置视口","slug":"_3-设置视口","link":"#_3-设置视口","children":[]},{"level":3,"title":"4.响应式网页开发方法","slug":"_4-响应式网页开发方法","link":"#_4-响应式网页开发方法","children":[]},{"level":3,"title":"5.媒体查询","slug":"_5-媒体查询","link":"#_5-媒体查询","children":[]},{"level":3,"title":"6.媒体类型","slug":"_6-媒体类型","link":"#_6-媒体类型","children":[]},{"level":3,"title":"7.单位值","slug":"_7-单位值","link":"#_7-单位值","children":[]}]}],"relativePath":"html-css/CSS.md","lastUpdated":1675259332000}'),e={name:"html-css/CSS.md"},c=l(`

CSS3

introduction

兼容性前缀

prefix(前缀)browser
-webkitchrome/safari
-mozfirefox
-msIE
-oopera

1.历史

更新迭代,兼容性 ---- 加不加前缀

css
div {
+  border-radius: ;
+  -webkit-border-radius: ;
+  -o-border-radius: ;
+  -moz-border-radius: ;
+}
+
div {
+  border-radius: ;
+  -webkit-border-radius: ;
+  -o-border-radius: ;
+  -moz-border-radius: ;
+}
+

兼容性手册网站

http://css.doyoe.com

http://caniuse.com

2.处理器

预处理器:pre-processor

less/sass cssNext 插件

利用 sass 工具编辑(遵循人家的语法):减少人工时间

sass 演示

css
div {
+  span {
+  }
+}
+
div {
+  span {
+  }
+}
+
css
$font-stack: arial, ...;
+#mysituation-color: #444;
+div {
+  span {
+    color: #mysituation-color;
+  }
+  p {
+    font: 100% $font-stack;
+  }
+}
+#only {
+  &.demo {
+    color: $mysituation-color;
+  }
+}
+
$font-stack: arial, ...;
+#mysituation-color: #444;
+div {
+  span {
+    color: #mysituation-color;
+  }
+  p {
+    font: 100% $font-stack;
+  }
+}
+#only {
+  &.demo {
+    color: $mysituation-color;
+  }
+}
+

后处理器:post-processor

CSS 自动补足前缀插件(基于 caniuse 网站)autoprefixer后处理器需要在其环境内编写

优点:如果有一天,属性可以再各大浏览器应用,不需要加前缀,那么我们写的代码本身就符合规范了。可维护性好。而 sass less 不能。

3.怎么用

postCss + 插件(充分体现扩展性,200 多个)

postCss 并不能划分成什么处理器,要加上插件才能变成相应的处理器

用 js 实现 css 抽象的语法树,AST(abstract Syntax Tree),剩下的事情留给后人来做

后处理器好处:

如果浏览器都实现兼容了,用不到兼容了,就可以不用后处理器了,利于维护代码,升级。预处理器办不到。

4.CSS3 进化到编程化:cssNext

css
:root {
+  --headline-color: #333;
+}
+@custom-selector: --headline h1,h2,h3,h4,h5,h6
+    : --headline {
+  color: var(--headline-color);
+}
+
:root {
+  --headline-color: #333;
+}
+@custom-selector: --headline h1,h2,h3,h4,h5,h6
+    : --headline {
+  color: var(--headline-color);
+}
+

cssNext 用来实现一些未来的标准的(未完全在各大浏览器)

border

1.border

css
.demo1 {
+  width: 100px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-radius: 50px; /*圆角:相对于宽而言的*/
+}
+
.demo1 {
+  width: 100px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-radius: 50px; /*圆角:相对于宽而言的*/
+}
+

border-radius拆分

css
border-radius: 10px 20px 30px 40px;
+/*左上-右上-右下-左下*/
+border-radius: 10px 40px;
+/*左上右下--右上左下*/
+border-radius: 10px 20px 30px;
+/*中间代表两个方向:右上左下*/
+
border-radius: 10px 20px 30px 40px;
+/*左上-右上-右下-左下*/
+border-radius: 10px 40px;
+/*左上右下--右上左下*/
+border-radius: 10px 20px 30px;
+/*中间代表两个方向:右上左下*/
+

继续拆分

css
border-top-left-radius: 10px;
+border-top-right-radius: 20px;
+border-bottom-right-radius: 30px;
+border-bottom-left-radius: 40px;
+/*等价*/
+border-top-left-radius: 10px 10px;
+border-top-right-radius: 20px 20px;
+border-bottom-right-radius: 30px 30px;
+border-bottom-left-radius: 40px 40px;
+
border-top-left-radius: 10px;
+border-top-right-radius: 20px;
+border-bottom-right-radius: 30px;
+border-bottom-left-radius: 40px;
+/*等价*/
+border-top-left-radius: 10px 10px;
+border-top-right-radius: 20px 20px;
+border-bottom-right-radius: 30px 30px;
+border-bottom-left-radius: 40px 40px;
+

半径小于长方形的长度

1/4 圆

css
/*必须正方形的width=border-top-left-radius:100px 100px*/
+
/*必须正方形的width=border-top-left-radius:100px 100px*/
+

半圆

css
/*长方形宽大一倍*/
+.demo2 {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-top-left-radius: 100px 100px;
+  border-top-right-radius: 100px 100px;
+}
+
/*长方形宽大一倍*/
+.demo2 {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-top-left-radius: 100px 100px;
+  border-top-right-radius: 100px 100px;
+}
+

叶子模型

css
.demo {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  background-color: green;
+  border-top-left-radius: 100px 100px;
+  border-bottom-right-radius: 100px 100px;
+}
+
.demo {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  background-color: green;
+  border-top-left-radius: 100px 100px;
+  border-bottom-right-radius: 100px 100px;
+}
+

新写法

css
border-radius: 10px 20px 30px 40px / 10px 20px 30px 40px;
+
border-radius: 10px 20px 30px 40px / 10px 20px 30px 40px;
+

2.box-shallow

外阴影&&内阴影

css
body{
+  background-color: #000;
+}
+div{
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: transparent;
+  border: 1px solid #fff;
+
+  不写就是外阴影
+  box-shadow: 0px 0px 0px 10px #0ff;/*水平偏移量 垂直偏移量 模糊范围(基于原来边框位置向边框两边同时模糊) 传播距离(水平垂直同时增加10) 阴影颜色 */
+
+  内阴影
+  /*box-shadow: inset 1px 0px 5px 0px #fff;*/
+
+  内外阴影
+  /*box-shadow: 0px 0px 10px #fff,inset 0px 0px 10px #fff;*/
+
+
+  /*四边不同颜色模糊实现*/
+  box-shadow: inset 0px 0px 10px #fff,
+    3px 0px 10px #f0f,
+    0px -3px 10px #0ff,
+    -3px 0px 10px #00f,
+    0px 3px 10px #ff0;
+  /*关于Z轴覆盖,哪个阴影先设置,谁就在上*/
+}
+
body{
+  background-color: #000;
+}
+div{
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: transparent;
+  border: 1px solid #fff;
+
+  不写就是外阴影
+  box-shadow: 0px 0px 0px 10px #0ff;/*水平偏移量 垂直偏移量 模糊范围(基于原来边框位置向边框两边同时模糊) 传播距离(水平垂直同时增加10) 阴影颜色 */
+
+  内阴影
+  /*box-shadow: inset 1px 0px 5px 0px #fff;*/
+
+  内外阴影
+  /*box-shadow: 0px 0px 10px #fff,inset 0px 0px 10px #fff;*/
+
+
+  /*四边不同颜色模糊实现*/
+  box-shadow: inset 0px 0px 10px #fff,
+    3px 0px 10px #f0f,
+    0px -3px 10px #0ff,
+    -3px 0px 10px #00f,
+    0px 3px 10px #ff0;
+  /*关于Z轴覆盖,哪个阴影先设置,谁就在上*/
+}
+

3 个 demo

DEMO1

css
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 150px);
+  top: calc(50% - 150px);
+  width: 300px;
+  height: 300px;
+  border: 1px solid #fff;
+  border-radius: 50%;
+  box-shadow: inset 0px 0px 50px #fff, inset 10px 0px 80px #f0f,
+    inset -10px 0px 80px #0ff, inset 10px 0px 300px #f0f,
+    inset -10px 0px 300px #0ff, 0px 0px 50px #fff, -10px 0px 80px #f0f, 10px 0px
+      80px #0ff;
+}
+
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 150px);
+  top: calc(50% - 150px);
+  width: 300px;
+  height: 300px;
+  border: 1px solid #fff;
+  border-radius: 50%;
+  box-shadow: inset 0px 0px 50px #fff, inset 10px 0px 80px #f0f,
+    inset -10px 0px 80px #0ff, inset 10px 0px 300px #f0f,
+    inset -10px 0px 300px #0ff, 0px 0px 50px #fff, -10px 0px 80px #f0f, 10px 0px
+      80px #0ff;
+}
+

太阳

css
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 25px);
+  top: calc(50% - 25px);
+  width: 50px;
+  height: 50px;
+  background-color: #fff;
+  border-radius: 50%;
+  box-shadow: 0px 0px 100px 50px #fff, 0px 0px 250px 125px #ff0;
+}
+
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 25px);
+  top: calc(50% - 25px);
+  width: 50px;
+  height: 50px;
+  background-color: #fff;
+  border-radius: 50%;
+  box-shadow: 0px 0px 100px 50px #fff, 0px 0px 250px 125px #ff0;
+}
+

DEMO3

css
div {
+  position: absolute;
+  border-radius: 5px;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: #fff;
+  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
+  transition: all 0.6s;
+}
+div::after {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  border-radius: 5px;
+  box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.3);
+  opacity: 0;
+  transition: all 0.6s;
+}
+
+div:hover::after {
+  opacity: 1;
+}
+
+div:hover {
+  transform: scale(1.25, 1.25);
+}
+
div {
+  position: absolute;
+  border-radius: 5px;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: #fff;
+  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
+  transition: all 0.6s;
+}
+div::after {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  border-radius: 5px;
+  box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.3);
+  opacity: 0;
+  transition: all 0.6s;
+}
+
+div:hover::after {
+  opacity: 1;
+}
+
+div:hover {
+  transform: scale(1.25, 1.25);
+}
+

注意:背景颜色在阴影下面,文字在阴影上面

3.border-image

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 10px solid black;
+  /*支持渐变色*/
+  border-color: lightpink;
+  border-image-source: linear-gradient(red, yellow);
+  /* 实现背景颜色渐变 */
+  /* border-image-source: url(.//border.png); 
+    实现边框用背景图片渐变渲染
+    */
+  border-image-slice: 10;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 10px solid black;
+  /*支持渐变色*/
+  border-color: lightpink;
+  border-image-source: linear-gradient(red, yellow);
+  /* 实现背景颜色渐变 */
+  /* border-image-source: url(.//border.png); 
+    实现边框用背景图片渐变渲染
+    */
+  border-image-slice: 10;
+}
+

关于 slice(截取)

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* border-image-slice: 100 50 100 100; */
+  /* 上右下左 */
+  /*slice不填的话,默认100%*/
+  border-image-repeat: stretch;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* border-image-slice: 100 50 100 100; */
+  /* 上右下左 */
+  /*slice不填的话,默认100%*/
+  border-image-repeat: stretch;
+}
+

几个附加值

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* 让背景图片往外延伸 */
+  border-image-outset: 100px;
+  border-image-width: 1; /* border里面能显示图片背景的宽度 */
+  /* 默认为1,不同于 border-width */
+  border-image-width: auto; /* 相当于slice + px  */
+  /* border-image-slice: 100 100 fill; */
+  /* fill填充内容区 */
+  border-image-slice: 100 100;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* 让背景图片往外延伸 */
+  border-image-outset: 100px;
+  border-image-width: 1; /* border里面能显示图片背景的宽度 */
+  /* 默认为1,不同于 border-width */
+  border-image-width: auto; /* 相当于slice + px  */
+  /* border-image-slice: 100 100 fill; */
+  /* fill填充内容区 */
+  border-image-slice: 100 100;
+}
+

repeat

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100 100 fill;
+  border-image-repeat: round;
+  /* stretch默认值:拉伸
+    round:平铺
+    repeat:平铺
+    space:平铺
+    */
+  border-image-repeat: round stretch;
+  /*也可以两个:水平垂直*/
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100 100 fill;
+  border-image-repeat: round;
+  /* stretch默认值:拉伸
+    round:平铺
+    repeat:平铺
+    space:平铺
+    */
+  border-image-repeat: round stretch;
+  /*也可以两个:水平垂直*/
+}
+

另一种写法:border-image:source slice repeat;

css
border-image: url(source/red.png) 100 repeat;
+
border-image: url(source/red.png) 100 repeat;
+

background

渐变颜色生成器:linear-gradient();  radial-gradient();

css
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  /* background-image: linear-gradient(#0f0, #f00); */
+  /* linear-gradient只能当成背景图片来使用,background-color不好使 */
+
+  /*两张图片在一个容器中展示*/
+  background-image: url() url(); /* 可以添加多个背景图片 */
+  background-size: 100px 200px, 100px 200px;
+  background-repeat: no-repeat;
+  background-position: 0 0, 100px 0;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  /* background-image: linear-gradient(#0f0, #f00); */
+  /* linear-gradient只能当成背景图片来使用,background-color不好使 */
+
+  /*两张图片在一个容器中展示*/
+  background-image: url() url(); /* 可以添加多个背景图片 */
+  background-size: 100px 200px, 100px 200px;
+  background-repeat: no-repeat;
+  background-position: 0 0, 100px 0;
+}
+

容错机制:容错背景图片展示

1.background-origin:

图片从哪里起始 没规定在哪结束,就开始重复图片 repeat,这就是为什么看起来像是 border-box 开始的

css
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  border: 20px solid transparent;
+  background-image: url(.//source/pic1.jpeg);
+
+  background-origin: padding-box;
+  /* border-box padding-box(默认值) content-box */
+  background-repeat: no-repeat;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  border: 20px solid transparent;
+  background-image: url(.//source/pic1.jpeg);
+
+  background-origin: padding-box;
+  /* border-box padding-box(默认值) content-box */
+  background-repeat: no-repeat;
+}
+

background-position 由 background-origin 决定的。position 相对于 origin 定位。

2.background-clip

背景图片从哪块开始截断,从哪块以外的部分都不显示背景图片,即从哪块结束

css
background-clip: border-box;
+/* border-box(默认,废弃值) padding-box content-box text */
+
background-clip: border-box;
+/* border-box(默认,废弃值) padding-box content-box text */
+

text 精解:文字内容区反切背景图片

css
* {
+  margin: 0;
+  padding: 0;
+}
+
+div:hover {
+  background-position: center center;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 200px);
+  top: 100px;
+  height: 100px;
+  line-height: 100px;
+  font-size: 80px;
+  width: 400px;
+  background-image: url(.//source/pic2.jpeg);
+  -webkit-background-clip: text;
+  /* 固定写法三件套,配合background-clip */
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  background-position: 0 0;
+  transition: all 0.6s;
+}
+
* {
+  margin: 0;
+  padding: 0;
+}
+
+div:hover {
+  background-position: center center;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 200px);
+  top: 100px;
+  height: 100px;
+  line-height: 100px;
+  font-size: 80px;
+  width: 400px;
+  background-image: url(.//source/pic2.jpeg);
+  -webkit-background-clip: text;
+  /* 固定写法三件套,配合background-clip */
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  background-position: 0 0;
+  transition: all 0.6s;
+}
+

3.background-repeat

css
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  background-image: url(.//source/pic4.jpeg);
+  background-size: 50px 50px;
+  /* background-repeat: no-repeat; */
+  /* background-repeat: repeat-x; */
+  /* background-repeat: repeat-y; */
+  /* background-repeat: round;深到一定程度蹦进来一张图 */
+  /* background-repeat: space;空白填充,冲到一定程度填充一张图片 */
+  /* background-repeat: round space;分别是x,y */
+  /* background-repeat:repeat-x相当于repeat-x no-repeat; */
+}
+
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  background-image: url(.//source/pic4.jpeg);
+  background-size: 50px 50px;
+  /* background-repeat: no-repeat; */
+  /* background-repeat: repeat-x; */
+  /* background-repeat: repeat-y; */
+  /* background-repeat: round;深到一定程度蹦进来一张图 */
+  /* background-repeat: space;空白填充,冲到一定程度填充一张图片 */
+  /* background-repeat: round space;分别是x,y */
+  /* background-repeat:repeat-x相当于repeat-x no-repeat; */
+}
+

4.background-attachment

改变定位属性的

css
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  /* overflow: hidden; */
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-position: 100px 100px;
+  /* background-attachment: local; */
+  /* 相对于内容区定位 */
+  /* background-attachment: scroll;默认 */
+  /* 默认scroll:相对于容器定位 */
+  /* background-attachment: fixed; */
+  /* 相对于真正的可视区视口定位 */
+}
+
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  /* overflow: hidden; */
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-position: 100px 100px;
+  /* background-attachment: local; */
+  /* 相对于内容区定位 */
+  /* background-attachment: scroll;默认 */
+  /* 默认scroll:相对于容器定位 */
+  /* background-attachment: fixed; */
+  /* 相对于真正的可视区视口定位 */
+}
+

5.background-size

css
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-size: cover;
+  /* contain不改变宽高比,让容器包含一张完整图片,即便会出现repeat
+    cover填充满容器,不改变宽高比 */
+  background-attachment: scroll;
+}
+
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-size: cover;
+  /* contain不改变宽高比,让容器包含一张完整图片,即便会出现repeat
+    cover填充满容器,不改变宽高比 */
+  background-attachment: scroll;
+}
+

效果

contain(可能 repeat):一条边对齐,另一条小于等于容器另一条

cover(可能超出):一条边对齐,另一条大于等于容器另一条

渐变生成器

linear-gradient

css
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: linear-gradient(red, white);
+    background-image: linear-gradient(to right, red, green);
+    background-image: linear-gradient(to left, red, green);
+    background-image: linear-gradient(to bottom, red, green);
+    background-image: linear-gradient(to top, red, green);
+    background-image: linear-gradient(to top right, red, green); */
+  /* background-image: linear-gradient(0deg, red, green);90deg 180deg */
+  /* background-image: linear-gradient(90deg, red, 20px, green);颜色的终止位置 */
+  /* background-image: linear-gradient(90deg, red 20px, green 60px); */
+}
+
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: linear-gradient(red, white);
+    background-image: linear-gradient(to right, red, green);
+    background-image: linear-gradient(to left, red, green);
+    background-image: linear-gradient(to bottom, red, green);
+    background-image: linear-gradient(to top, red, green);
+    background-image: linear-gradient(to top right, red, green); */
+  /* background-image: linear-gradient(0deg, red, green);90deg 180deg */
+  /* background-image: linear-gradient(90deg, red, 20px, green);颜色的终止位置 */
+  /* background-image: linear-gradient(90deg, red 20px, green 60px); */
+}
+

镜像放射 radial-gradient()

css
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: radial-gradient(red, green, #0ff); */
+  /* background-image: radial-gradient(red 20%, green 50px, #0ff 40%); */
+  /* background-image: radial-gradient(circle at 100px 0px, red, green, #0ff); */
+  /* background-image: radial-gradient(ellipse at 20px 30px, red, green, #0ff); */
+  /* 放射半径 */
+  /* closest-corner
+    closest-side
+    farthest-corner
+    farthest-side */
+  /* background-image: radial-gradient(ellipse farthest-corner at 50px 50px, red, green, #0ff); */
+}
+
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: radial-gradient(red, green, #0ff); */
+  /* background-image: radial-gradient(red 20%, green 50px, #0ff 40%); */
+  /* background-image: radial-gradient(circle at 100px 0px, red, green, #0ff); */
+  /* background-image: radial-gradient(ellipse at 20px 30px, red, green, #0ff); */
+  /* 放射半径 */
+  /* closest-corner
+    closest-side
+    farthest-corner
+    farthest-side */
+  /* background-image: radial-gradient(ellipse farthest-corner at 50px 50px, red, green, #0ff); */
+}
+

颜色

HSL

HSLA

currentColor 中转颜色

css
div {
+  width: 100px;
+  height: 100px;
+  border-width: 1px;
+  border-style: solid;
+  color: red; /*border竟然颜色改变了。一旦不设置border-color的时候,会继承color,currentColor作为中转*/
+  /* css1 css2 border-color:color currentColor*/
+  border-color: currentColor;
+}
+
div {
+  width: 100px;
+  height: 100px;
+  border-width: 1px;
+  border-style: solid;
+  color: red; /*border竟然颜色改变了。一旦不设置border-color的时候,会继承color,currentColor作为中转*/
+  /* css1 css2 border-color:color currentColor*/
+  border-color: currentColor;
+}
+

text

1.text-shadow

x, y, blur, color

css
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  text-shadow: 3px 3px 3px #000;
+  text-shadow: 3px 3px 3px #000, -10px -10px 3px #30f;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  text-shadow: 3px 3px 3px #000;
+  text-shadow: 3px 3px 3px #000, -10px -10px 3px #30f;
+}
+

demo

浮雕效果

css
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 1px 1px #000, -1px -1px #fff;
+}
+
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 1px 1px #000, -1px -1px #fff;
+}
+

镂刻效果

css
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: -1px -1px #000, 1px 1px #fff;
+}
+
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: -1px -1px #000, 1px 1px #fff;
+}
+

阴影加强

css
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 0px 0px 10px #0f0, 0px 0px 20px #0f0, 0px 0px 30px #0f0;
+  transition: all 0.3s;
+}
+
+div:hover {
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20;
+}
+
+div::before {
+  content: "NO ";
+  opacity: 0;
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20, 0px -5px
+      20px #f10, 0px -10px 20px #f20, 0px -15px 30px #f10;
+  transition: all 3s;
+}
+div:hover::before {
+  opacity: 1;
+}
+
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 0px 0px 10px #0f0, 0px 0px 20px #0f0, 0px 0px 30px #0f0;
+  transition: all 0.3s;
+}
+
+div:hover {
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20;
+}
+
+div::before {
+  content: "NO ";
+  opacity: 0;
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20, 0px -5px
+      20px #f10, 0px -10px 20px #f20, 0px -15px 30px #f10;
+  transition: all 3s;
+}
+div:hover::before {
+  opacity: 1;
+}
+

死神来了

css
body {
+  /* background-color: #000; */
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 400px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  background-repeat: no-repeat;
+  background-position: -400px -50px;
+  background-image: url(.//source/eye.jpeg);
+  background-size: 400px 300px;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  transition: all 3s;
+  /* text-shadow: 10px 10px 3px #000; 
+   		这个不能展现效果?阴影跑到了文字内容上面,因为background-clip:text,文字就变成了背景的一部分
+    */
+  text-shadow: 10px 10px 3px rgba(0, 0, 0, 0.1);
+}
+
+div:hover {
+  background-position: 0 -50px;
+}
+
body {
+  /* background-color: #000; */
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 400px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  background-repeat: no-repeat;
+  background-position: -400px -50px;
+  background-image: url(.//source/eye.jpeg);
+  background-size: 400px 300px;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  transition: all 3s;
+  /* text-shadow: 10px 10px 3px #000; 
+   		这个不能展现效果?阴影跑到了文字内容上面,因为background-clip:text,文字就变成了背景的一部分
+    */
+  text-shadow: 10px 10px 3px rgba(0, 0, 0, 0.1);
+}
+
+div:hover {
+  background-position: 0 -50px;
+}
+

分身

css
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 60px;
+  font-weight: bold;
+  color: #f10;
+
+  text-shadow: 0px 0px 5px #f10, 0px 0px 10px #f20, 0px 0px 15px #f30;
+  transition: all 1.5s;
+}
+
+div:hover {
+  text-shadow: 10px -10px 10px #f00, 10px 10px 15px #ff0;
+}
+
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 60px;
+  font-weight: bold;
+  color: #f10;
+
+  text-shadow: 0px 0px 5px #f10, 0px 0px 10px #f20, 0px 0px 15px #f30;
+  transition: all 1.5s;
+}
+
+div:hover {
+  text-shadow: 10px -10px 10px #f00, 10px 10px 15px #ff0;
+}
+

描边效果 stroke

css
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 80px;
+  font-weight: bold;
+  -webkit-text-stroke: 2px red;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 80px;
+  font-weight: bold;
+  -webkit-text-stroke: 2px red;
+}
+

字体描边效果 stroke

css
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  color: transparent;
+  font-family: simsun;
+  -webkit-text-stroke: 1px red;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  color: transparent;
+  font-family: simsun;
+  -webkit-text-stroke: 1px red;
+}
+

font-face 字体包使用方法

css
@font-face {
+    font-family: 'abc';字体包名字
+    src: url();
+}
+
+div {
+    font-family: 'abc';引入字体包
+}
+
@font-face {
+    font-family: 'abc';字体包名字
+    src: url();
+}
+
+div {
+    font-family: 'abc';引入字体包
+}
+

引入折磨多图片格式的原因:

字体格式

  1. TureType 微软 苹果 .ttf
  2. opentype 微软 adobe 。opt
  3. woff .woff
  4. ie .eat
  5. h5 svg

MIME 协议(.ttf .txt .pdf)

如果不认识,format 让浏览器加强对字体格式的认识。format 产生映射

2.   text 系列

white-space

css
div{
+    white-space:pre;原封不动的保留你输入时的状态,空格、换行都会保留,并且当文字超出边界时不换行
+    white-space:nowarp:强制所有文本在同一行内显示。
+}
+
div{
+    white-space:pre;原封不动的保留你输入时的状态,空格、换行都会保留,并且当文字超出边界时不换行
+    white-space:nowarp:强制所有文本在同一行内显示。
+}
+

word-break

css
div {
+  width: 200px;
+  border: 1px solid black;
+  /* word-break: keep-all; */
+  /* 不换行 */
+  /* word-break: break-all; */
+  /* 强制换行 */
+  /* word-break: break-word; */
+  /* 尽可能保留英文单词完整性 */
+}
+
div {
+  width: 200px;
+  border: 1px solid black;
+  /* word-break: keep-all; */
+  /* 不换行 */
+  /* word-break: break-all; */
+  /* 强制换行 */
+  /* word-break: break-word; */
+  /* 尽可能保留英文单词完整性 */
+}
+

columns 报纸布局

css
div {
+    /* columns: column-width||column-count;; */
+    /* columns: 300px 4; */
+    column-count: 3;
+    column-gap: 30px;
+    /* 空隙 */
+    column-rule: 1px solid black;
+    /* 空隙分割线 */
+}
+p {
+    margin: 10px 0;
+    column-span: all;默认1
+    /* 横穿整个列 */
+}
+
div {
+    /* columns: column-width||column-count;; */
+    /* columns: 300px 4; */
+    column-count: 3;
+    column-gap: 30px;
+    /* 空隙 */
+    column-rule: 1px solid black;
+    /* 空隙分割线 */
+}
+p {
+    margin: 10px 0;
+    column-span: all;默认1
+    /* 横穿整个列 */
+}
+
css
-webkit-column-break-before: always;
+/* 前面断列,另起一列 */
+-webkit-column-break-after: always;
+/* 后面断列,另起一列 */
+
-webkit-column-break-before: always;
+/* 前面断列,另起一列 */
+-webkit-column-break-after: always;
+/* 后面断列,另起一列 */
+

column-with

css
/* column-count: 3;自适应 */
+column-width: 300px;
+/* 不太准,会自适应 */
+
/* column-count: 3;自适应 */
+column-width: 300px;
+/* 不太准,会自适应 */
+

瀑布流布局 :他尽量让高的成为多数的。一个一个试试就试出来了

css
div {
+  /*内部机制不适宜瀑布流布局 */
+  column-count: 4;
+  column-rule-style: solid;
+}
+
div {
+  /*内部机制不适宜瀑布流布局 */
+  column-count: 4;
+  column-rule-style: solid;
+}
+

column 应用:小说阅读

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+
+      div {
+        width: 300px;
+        height: 500px;
+        border: 1px solid red;
+      }
+
+      .content {
+        column-width: 300px;
+        column-gap: 20px;
+        border: none;
+
+        transition: all 2s;
+      }
+
+      div:hover .content {
+        transform: translateX(-320px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)。
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+
+      div {
+        width: 300px;
+        height: 500px;
+        border: 1px solid red;
+      }
+
+      .content {
+        column-width: 300px;
+        column-gap: 20px;
+        border: none;
+
+        transition: all 2s;
+      }
+
+      div:hover .content {
+        transform: translateX(-320px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)。
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+      </div>
+    </div>
+  </body>
+</html>
+

滑动——slide 插件

box

1.IE6 混杂模式盒子

css
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  border: 10px solid black;
+  padding: 10px;
+  /* 触发 */
+  box-sizing: border-box;
+  /*默认content-box*/
+}
+
+/* 
+原来:boxWidth=width+border*2+padding*2;
+怪异:boxWidth=width 
+	 contentWidth=width - border*2 - padding*2 
+*/
+
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  border: 10px solid black;
+  padding: 10px;
+  /* 触发 */
+  box-sizing: border-box;
+  /*默认content-box*/
+}
+
+/* 
+原来:boxWidth=width+border*2+padding*2;
+怪异:boxWidth=width 
+	 contentWidth=width - border*2 - padding*2 
+*/
+

宽度不固定,内边距 padding 固定

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 100%;
+        height: 300px;
+        border: 1px solid black;
+      }
+
+      .content:first-of-type {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: yellow;
+        padding: 0 10px;
+        box-sizing: border-box;
+      }
+
+      .content {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: red;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 100%;
+        height: 300px;
+        border: 1px solid black;
+      }
+
+      .content:first-of-type {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: yellow;
+        padding: 0 10px;
+        box-sizing: border-box;
+      }
+
+      .content {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: red;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

输入框宽度不固定,内边距固定

html
<style>
+    .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+    }
+
+    input {
+        width: 100%;
+        box-sizing: border-box;
+    }
+
+    /* input天生带2px的border,解决就用怪异盒子 */
+</style>
+</head>
+
+<body>
+    <div class="wrapper">
+        <input type="text">
+    </div>
+</body>
+
<style>
+    .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+    }
+
+    input {
+        width: 100%;
+        box-sizing: border-box;
+    }
+
+    /* input天生带2px的border,解决就用怪异盒子 */
+</style>
+</head>
+
+<body>
+    <div class="wrapper">
+        <input type="text">
+    </div>
+</body>
+

产品需求接受形式

css
input {
+  width: 300px;
+  height: 50px;
+  box-sizing: border-box;
+  padding: 0 10px;
+}
+
input {
+  width: 300px;
+  height: 50px;
+  box-sizing: border-box;
+  padding: 0 10px;
+}
+

宽度用户自定,后端传的,padding border 固定

css
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+}
+
+.content {
+  box-sizing: border-box;
+  float: left;
+  width: 100px;
+  height: 100px;
+  border: 1px solid #fff;
+  background-color: black;
+}
+
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+}
+
+.content {
+  box-sizing: border-box;
+  float: left;
+  width: 100px;
+  height: 100px;
+  border: 1px solid #fff;
+  background-color: black;
+}
+
html
<div class="wrapper">
+  <div class="content"></div>
+  <div class="content"></div>
+  <div class="content"></div>
+</div>
+
<div class="wrapper">
+  <div class="content"></div>
+  <div class="content"></div>
+  <div class="content"></div>
+</div>
+

两个值

1.overflow

css
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* overflow: scroll; */
+  /* 除了当overflow-x,overflow-y之一设置了非 visible时,另一个属性会自动将默认值visible设置为auto */
+  overflow-x: scroll; /* 就会overflow-y:auto */
+}
+
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* overflow: scroll; */
+  /* 除了当overflow-x,overflow-y之一设置了非 visible时,另一个属性会自动将默认值visible设置为auto */
+  overflow-x: scroll; /* 就会overflow-y:auto */
+}
+

移动端

明星左右滑动模块

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 100px;
+        border: 1px solid black;
+        overflow-x: scroll;
+        overflow-y: auto;
+      }
+
+      /* 最外层溢出部分隐藏,里面盒子=图片总体宽度 */
+
+      .box {
+        width: 800px;
+        height: 100px;
+      }
+
+      img {
+        float: left;
+        display: inline;
+        width: 100px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="box">
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 100px;
+        border: 1px solid black;
+        overflow-x: scroll;
+        overflow-y: auto;
+      }
+
+      /* 最外层溢出部分隐藏,里面盒子=图片总体宽度 */
+
+      .box {
+        width: 800px;
+        height: 100px;
+      }
+
+      img {
+        float: left;
+        display: inline;
+        width: 100px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="box">
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+      </div>
+    </div>
+  </body>
+</html>
+

2.resize 调节元素大小 :会导致重排和重绘,性能消耗

css
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* resize: both; */
+  /* resize: vertical;竖直 */
+  overflow: scroll;
+}
+
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* resize: both; */
+  /* resize: vertical;竖直 */
+  overflow: scroll;
+}
+

2.flex 弹性盒子

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        /* display: inline-flex; */
+        flex-direction: row;
+        /* 默认主轴水平方向,自左向右 */
+        flex-direction: column;
+        /* 垂直 */
+        flex-direction: row-reverse;
+        /* 逆反 */
+        flex-direction: column-reverse;
+        /* 垂直逆反 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        /* display: inline-flex; */
+        flex-direction: row;
+        /* 默认主轴水平方向,自左向右 */
+        flex-direction: column;
+        /* 垂直 */
+        flex-direction: row-reverse;
+        /* 逆反 */
+        flex-direction: column-reverse;
+        /* 垂直逆反 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

换行实现:

flex-wrap

justify-content:基于主轴对齐方式

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* justify-content: flex-start;默认 */
+        /* justify-content: flex-end; 主轴向右*/
+        /* justify-content: center; */
+        /* justify-content: space-between; */
+        /* 两边站住,中间自适应 */
+        /* justify-content: space-around; */
+        /* 元素元素之间间距相等 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* justify-content: flex-start;默认 */
+        /* justify-content: flex-end; 主轴向右*/
+        /* justify-content: center; */
+        /* justify-content: space-between; */
+        /* 两边站住,中间自适应 */
+        /* justify-content: space-around; */
+        /* 元素元素之间间距相等 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

align-items 垂直方向

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* align-items: baseline; */
+        /* 基于文字对齐 */
+        /* align-items: stretch默认; */
+        /* 未设置内容区高度的话,实现拉伸;如果设置了,就不好使了 */
+        /* align-items: flex-start; */
+        /* align-items: flex-end; */
+        /* align-items: center; */
+      }
+
+      .content {
+        width: 100px;
+        /* height: 100px; */
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        /* margin-top: 10px; 验证baseline*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* align-items: baseline; */
+        /* 基于文字对齐 */
+        /* align-items: stretch默认; */
+        /* 未设置内容区高度的话,实现拉伸;如果设置了,就不好使了 */
+        /* align-items: flex-start; */
+        /* align-items: flex-end; */
+        /* align-items: center; */
+      }
+
+      .content {
+        width: 100px;
+        /* height: 100px; */
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        /* margin-top: 10px; 验证baseline*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

单行居中

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: wrap;
+        align-items: center; /* 主要针对单行元素来处理对齐方式的 */
+
+        align-content: center; /* 必须作用于多行元素 */
+        justify-content: center;
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: wrap;
+        align-items: center; /* 主要针对单行元素来处理对齐方式的 */
+
+        align-content: center; /* 必须作用于多行元素 */
+        justify-content: center;
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

align-content 看官方文档自学

以上都是设置在父级上的

以下是子级

order 相当于 z-index,默认 0,最好添负值;小的在前

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: stretch;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  order: -2;
+}
+.content:nth-of-type(2) {
+  order: -1;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: stretch;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  order: -2;
+}
+.content:nth-of-type(2) {
+  order: -1;
+}
+

align-self

听从自己

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+

强于 align-items(父级),弱于 align-content(父级)

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-content: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-content: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+

弹性盒子之弹性

flex-grow

当这一行还有剩余空间的时候,按照比例分配剩余空间,最终调整大小

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* */
+        /* flex-grow: 1; */
+        /* 默认0 伸展开了 */
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        flex-grow: 1;
+      }
+
+      .content:nth-of-type(2) {
+        flex-grow: 2;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* */
+        /* flex-grow: 1; */
+        /* 默认0 伸展开了 */
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        flex-grow: 1;
+      }
+
+      .content:nth-of-type(2) {
+        flex-grow: 2;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

flex-shrink   默认 1

超出,不换行,启动压缩

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+.content {
+  flex-shrink: 1;
+  width: 200px;
+  height: 100px;
+}
+/* 压缩加权算法 */
+/* 200px * 1 + 200px * 1 + 400px * 3 = 1600px */
+/* 	
+200px * 1
+---------   * 200px  = 25px
+1600 
+*/
+.content:nth-of-type(3) {
+  flex-grow: 3;
+  width: 400px;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+.content {
+  flex-shrink: 1;
+  width: 200px;
+  height: 100px;
+}
+/* 压缩加权算法 */
+/* 200px * 1 + 200px * 1 + 400px * 3 = 1600px */
+/* 	
+200px * 1
+---------   * 200px  = 25px
+1600 
+*/
+.content:nth-of-type(3) {
+  flex-grow: 3;
+  width: 400px;
+}
+

深入剖析:加边框

得出最终结论:真实内容区大小*shrink + (...)=加权值

flex-basis 相当于 width 权重大于 width

默认 auto

basis 与 width 区别

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  flex-basis: 100px;
+  /* 根据内容撑开 */
+  flex-shrink: 1;
+  /* width: 100px; 覆盖*/
+  height: 200px;
+  background-color: #f0f;
+  /* 
+    	元素撑开的话,得出以下结论:
+        只写basis或者basis>width,代表元素的最小宽度           
+        设置了width并且basis小于width 
+        basis<realWidth<width
+    */
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+}
+
+.content:nth-of-type(3) {
+  background-color: #0ff;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  flex-basis: 100px;
+  /* 根据内容撑开 */
+  flex-shrink: 1;
+  /* width: 100px; 覆盖*/
+  height: 200px;
+  background-color: #f0f;
+  /* 
+    	元素撑开的话,得出以下结论:
+        只写basis或者basis>width,代表元素的最小宽度           
+        设置了width并且basis小于width 
+        basis<realWidth<width
+    */
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+}
+
+.content:nth-of-type(3) {
+  background-color: #0ff;
+}
+

想换行,加汉字或者设置换行

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  height: 200px;
+  /* word-break: break-word;可换行,就可以参与压缩了 */
+}
+
+.content:nth-of-type(1) {
+  background-color: #0f0;
+  flex-basis: 200px;
+  flex-shrink: 5;
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+  flex-basis: 200px;
+  flex-shrink: 1;
+}
+
+.content:nth-of-type(3) {
+  flex-basis: 400px;
+  background-color: #0ff;
+  flex-shrink: 1;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  height: 200px;
+  /* word-break: break-word;可换行,就可以参与压缩了 */
+}
+
+.content:nth-of-type(1) {
+  background-color: #0f0;
+  flex-basis: 200px;
+  flex-shrink: 5;
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+  flex-basis: 200px;
+  flex-shrink: 1;
+}
+
+.content:nth-of-type(3) {
+  flex-basis: 400px;
+  background-color: #0ff;
+  flex-shrink: 1;
+}
+

以上,总结

当你设置宽的时候,如果 basis 设置有值,且小于 width,那么真实的宽的范围在 basis <realWidth<Width 当你不设置 width 的时候,设置 basis,元素真实的宽 min-width 当不换行内容超过内容区 会撑开容易 无论什么情况,被不换行内容撑开的容器,不会被压缩计算

探究 flex 应用 flex:0 1 auto 默认 因为设置了 flex 后会自动压缩

基本应用

html
.wrapper { width: 600px; height: 600px; border: 1px solid black; display: flex;
+/*flex-wrap: wrap;*/ align-content: flex-start; } .content { background-color:
+red; flex: 1 1 auto; width: 250px; height: 250px; }
+
.wrapper { width: 600px; height: 600px; border: 1px solid black; display: flex;
+/*flex-wrap: wrap;*/ align-content: flex-start; } .content { background-color:
+red; flex: 1 1 auto; width: 250px; height: 250px; }
+

1.实现居中

css
.wrapper {
+  resize: both; //配合overflow使用
+  overflow: hidden;
+  width: 300px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+div.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid black;
+}
+
.wrapper {
+  resize: both; //配合overflow使用
+  overflow: hidden;
+  width: 300px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+div.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid black;
+}
+

2.可动态增加的导航栏

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        align-items: center;
+      }
+
+      .item {
+        flex: 1 1 auto;
+        height: 30px;
+        text-align: center;
+        line-height: 30px;
+        font-size: 20px;
+        color: #f20;
+        border-radius: 5px;
+      }
+
+      .item:hover {
+        background-color: #f20;
+        color: #fff;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="item">天猫</div>
+      <div class="item">淘宝</div>
+      <div class="item">聚划算</div>
+      <!-- 无论多少,都等分 -->
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        align-items: center;
+      }
+
+      .item {
+        flex: 1 1 auto;
+        height: 30px;
+        text-align: center;
+        line-height: 30px;
+        font-size: 20px;
+        color: #f20;
+        border-radius: 5px;
+      }
+
+      .item:hover {
+        background-color: #f20;
+        color: #fff;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="item">天猫</div>
+      <div class="item">淘宝</div>
+      <div class="item">聚划算</div>
+      <!-- 无论多少,都等分 -->
+    </div>
+  </body>
+</html>
+

3.等分布局(4 等分,2 等分,中间可加 margin)

实现中间固定,两边等比例自适应

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 200px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* flex: 0 1 auto; 默认*/
+        flex: 1 1 auto;
+        /* margin: 0 10px; */
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+
+      .content:nth-of-type(2) {
+        flex: 0 0 200px; /*中间固定,不能伸不能缩,即0,0,*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 200px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* flex: 0 1 auto; 默认*/
+        flex: 1 1 auto;
+        /* margin: 0 10px; */
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+
+      .content:nth-of-type(2) {
+        flex: 0 0 200px; /*中间固定,不能伸不能缩,即0,0,*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

实现中间固定,两边不比例自适应

css
加上 .content:nth-of-type(3) {
+  flex: 2 2 auto;
+}
+
加上 .content:nth-of-type(3) {
+  flex: 2 2 auto;
+}
+

其中一个固定宽度的布局(固定一个,固定两个)

4.圣杯布局

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: column;
+        //align-items:stretch//默认  交叉轴如果没有设置,就拉伸
+      }
+
+      .header,
+      .footer,
+      .left,
+      .right {
+        flex: 0 0 20%; /*不参与伸缩,占20%*/
+        border: 1px solid black;
+        box-sizing: border-box;
+      }
+
+      .contain {
+        flex: 1 1 auto; //把中间内容区噔开
+        display: flex;
+      }
+
+      .center {
+        flex: 1 1 auto;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="header">header</div>
+      <div class="contain">
+        <div class="left">left</div>
+        <div class="center">center</div>
+        <div class="right">right</div>
+      </div>
+      <div class="footer">footer</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: column;
+        //align-items:stretch//默认  交叉轴如果没有设置,就拉伸
+      }
+
+      .header,
+      .footer,
+      .left,
+      .right {
+        flex: 0 0 20%; /*不参与伸缩,占20%*/
+        border: 1px solid black;
+        box-sizing: border-box;
+      }
+
+      .contain {
+        flex: 1 1 auto; //把中间内容区噔开
+        display: flex;
+      }
+
+      .center {
+        flex: 1 1 auto;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="header">header</div>
+      <div class="contain">
+        <div class="left">left</div>
+        <div class="center">center</div>
+        <div class="right">right</div>
+      </div>
+      <div class="footer">footer</div>
+    </div>
+  </body>
+</html>
+

5.流式布局

模拟 float 布局

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 800px;
+        border: 1px solid black;
+        display: flex;
+        flex-wrap: wrap;
+        align-content: flex-start; //模拟float
+      }
+
+      .content {
+        width: 100px;
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 800px;
+        border: 1px solid black;
+        display: flex;
+        flex-wrap: wrap;
+        align-content: flex-start; //模拟float
+      }
+
+      .content {
+        width: 100px;
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

作业:刘德华

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      .wrapper {
+        background-color: red;
+        resize: both;
+        overflow: hidden;
+        display: flex;
+      }
+      .left {
+        margin-top: 20px;
+        flex: 0 0 auto;
+      }
+      img {
+        width: 150px;
+        height: 200px;
+      }
+      .right {
+        flex: 1 1 auto;
+        margin-left: 40px;
+      }
+      em {
+        color: #fff;
+        font-size: 30px;
+      }
+      p {
+        margin-top: 20px;
+        color: #fff;
+        font-size: 60px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="left">
+        <img src=".\\1.jpg" alt="" />
+      </div>
+      <div class="right">
+        <p>刘德华</p>
+        <em>演员演员演员演员演员演员演员</em>
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      .wrapper {
+        background-color: red;
+        resize: both;
+        overflow: hidden;
+        display: flex;
+      }
+      .left {
+        margin-top: 20px;
+        flex: 0 0 auto;
+      }
+      img {
+        width: 150px;
+        height: 200px;
+      }
+      .right {
+        flex: 1 1 auto;
+        margin-left: 40px;
+      }
+      em {
+        color: #fff;
+        font-size: 30px;
+      }
+      p {
+        margin-top: 20px;
+        color: #fff;
+        font-size: 60px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="left">
+        <img src=".\\1.jpg" alt="" />
+      </div>
+      <div class="right">
+        <p>刘德华</p>
+        <em>演员演员演员演员演员演员演员</em>
+      </div>
+    </div>
+  </body>
+</html>
+

响应式网站开发

demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      div {
+        font-size: 14px;
+      }
+
+      .wrapper {
+        width: 1500px;
+        font-size: 0;
+      }
+
+      .content {
+        width: 300px;
+        height: 200px;
+        border: 1px solid black;
+        display: inline-block; //不用浮动,而是用这种方法
+        box-sizing: border-box;
+        /* 
+                换行?
+                1.凡是带有inline-block都有文字特性,制表符=文字大小
+                2.border
+                */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      div {
+        font-size: 14px;
+      }
+
+      .wrapper {
+        width: 1500px;
+        font-size: 0;
+      }
+
+      .content {
+        width: 300px;
+        height: 200px;
+        border: 1px solid black;
+        display: inline-block; //不用浮动,而是用这种方法
+        box-sizing: border-box;
+        /* 
+                换行?
+                1.凡是带有inline-block都有文字特性,制表符=文字大小
+                2.border
+                */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

2.响应式网页设计

真正的响应式设计方法不仅仅是根据可视区域大小而改变网页布局,而是要从整体上颠覆当前网页的设计方法,是针对任意设备的网页内容进行完美布局的一种显示机制。

用一套代码解决几乎所有设备的页面展示问题

设计工作由产品经理或者美工来出

详解 meta:将页面大小 根据分辨率不同进行相应的调节   以展示给用户的大小感觉上差不多

1css 像素  != 设备像素 (根据屏幕分辨率 相应的调整)

html
<meta
+  name="viewport"
+  content="width=device-width, initial-scale=1.0, user-scalable=no"
+/>
+<meta http-equiv="X-UA-Compatible" content="ie=edge" />
+
<meta
+  name="viewport"
+  content="width=device-width, initial-scale=1.0, user-scalable=no"
+/>
+<meta http-equiv="X-UA-Compatible" content="ie=edge" />
+

3.设置视口

模拟移动端的 meta

html
<meta
+  name="viewport"
+  content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"
+/>
+
<meta
+  name="viewport"
+  content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"
+/>
+

width: 可视区宽度

device-width:  设备宽度

minimum-scale: 最小缩放比

maximum-scale: 最大缩放比

width = device-width :  iphone 或者 ipad 上横竖屏的宽度 =   竖屏时候的宽度   不能自适应的问题

initial-scale=1.0    :  windows phone ie 浏览器 上横竖屏的宽度 =   竖屏时候的宽度 不能自适应的问题

user-scalable: 是否允许用户缩放

Css 像素根据设备像素进行计算   1css 像素  == 1 是设备像素     根据设备的分辨率  dpi 值来计算 css 像素真正展现的大小

meta 功能即适配各种不同分辨率的设备

4.响应式网页开发方法

  1. 流体网格:可伸缩的网格 (大小宽高   都是可伸缩(可用 flex 或者百分比来控制大小)float)---》 布局上面 元素大小不固定可伸缩
  2. 弹性图片:图片宽高不固定(可设置 min-width: 100%)
  3. 媒体查询:让网页在不同的终端上面展示效果相同(用户体验相同 à 让用户用着更爽) 在不同的设备(大小不同 分辨率不同)上面均展示合适的页面
  4. 主要断点: 设备宽度的临界点 大小的区别 ---》 宽度不同   ---》 根据不同宽度展示不同的样式 响应式网页开发主要是在 css 样式(异步加载)上面进行操作

5.媒体查询

媒体查询是向不同设备提供不同样式的一种方式,它为每种类型的用户提供了最佳的体验。

css2: media type

media type(媒体类型)是 css 2 中的一个非常有用的属性,通过 media type 我们可以对不同的设备指定特定的样式,从而实现更丰富的界面。

css3: media query

media query 是 CSS3 对 media type 的增强,事实上我们可以将 media query 看成是 media type+css 属性(媒体特性 Media features)判断。

如何使用媒体查询?

媒体查询的引用方法有很多种:

  1. link 标签
  2. @import url(example.css) screen and (width:800px);
  3. css3 新增的@media

媒体查询不占用权重

使用方法

媒体类型(Media Type): all(全部)、screen(屏幕)、print(页面打印或打印预览模式)

媒体特性(Media features): width(渲染区宽度)、device-width(设备宽度)...

Media Query 是 CSS3 对 Media Type 的增强版,其实可以将 Media Query 看成 Media Type(判断条件)+CSS(符合条件的样式规则)

6.媒体类型

媒体特性(media features) 逻辑操作符 合并多个媒体属性 and

css
@media screen and (min-width: 600px) and (max-width: 100px);
+
@media screen and (min-width: 600px) and (max-width: 100px);
+

合并多个媒体属性或合并媒体属性与媒体类型, 一个基本的媒体查询,即一个媒体属性与默认指定的 screen 媒体类型。 指定备用功能

html
@media screen and (min-width: 769px), print and (min-width: 6in)“
+
@media screen and (min-width: 769px), print and (min-width: 6in)“
+

没有 or 关键词可用于指定备用的媒体功能。相反,可以将备用功能以逗号分割列表的形式列出 这会将样式应用到宽度超过 769 像素的屏幕或使用至少 6 英寸宽的纸张的打印设备。 指定否定条件

css
@media not screen and (monochrome);
+
@media not screen and (monochrome);
+

要指定否定条件,可以在媒体声明中添加关键字 not,不能在单个条件前使用 not。该关键字必须位于声明的开头,而且它会否定整个声明。所以,上面的示例会应用于除单色屏幕外的所有设备。 向早期浏览器隐藏媒体查询

css
media="only screen and (min-width: 401px) and (max-width: 600px)"
+
media="only screen and (min-width: 401px) and (max-width: 600px)"
+

媒体查询规范还提供了关键字 only,它用于向早期浏览器隐藏媒体查询。类似于 not,该关键字必须位于声明的开头。Only 指定某种特定的媒体类型   为了兼容不支持媒体查询的浏览器 早期浏览器应该将以下语句 media="screen and (min-width: 401px) and (max-width: 600px)" 解释为 media="screen": 换句话说,它应该将样式规则应用于所有屏幕设备,即使它不知道媒体查询的含义。 无法识别媒体查询的浏览器要求获得逗号分割的媒体类型列表,规范要求,它们应该在第一个不是连字符的非数字字母字符之前截断每个值。所以,早期浏览器应该将上面的示例解释为:media="only"  因为没有 only 这样的媒体类型,所以样式表被忽略。 Query -à css3

易混淆的宽度

css
device-width/height    width/height来做为的判定值。
+
device-width/height    width/height来做为的判定值。
+

device-width/device-height 是设备的宽度(如电脑手机的宽度 不是浏览器的宽度) width/height 使用 documentElement.clientWidth/Height 即 viewport 的值。渲染宽度/高度 视口宽度/

7.单位值

Rem:rem 是 CSS3 新增的一个相对单位(root em,根 em)相对的只是 HTML 根元素。

Em:em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。

Px: px 像素(Pixel)。相对长度单位。像素 px 是相对于显示器屏幕分辨率而言的。

Vw:相对于视口的宽度。视口被均分为 100 单位的 vw

Vh:相对于视口的高度。视口被均分为 100 单位的 vh

Vmax: 相对于视口的宽度或高度中较大的那个。其中最大的那个被均分为 100 单位的 vmax

Vmin:相对于视口的宽度或高度中较小的那个。其中最小的那个被均分为 100 单位的 vmin

html
rem 相对于html元素的font-size大小 em 相对于本身的font-size大小
+font-size属性是可以继承的 vw/ vh 相对于视口而言的 会把视口分成100份 vmax
+区视口宽高中最大的一边分成100份 vmin 区视口宽高中最小的一边分成100份 css样式引入
+媒体查询不占用权重
+
rem 相对于html元素的font-size大小 em 相对于本身的font-size大小
+font-size属性是可以继承的 vw/ vh 相对于视口而言的 会把视口分成100份 vmax
+区视口宽高中最大的一边分成100份 vmin 区视口宽高中最小的一边分成100份 css样式引入
+媒体查询不占用权重
+
`,278),r=[c];function t(B,y,F,i,A,b){return n(),a("div",null,r)}const m=s(e,[["render",t]]);export{u as __pageData,m as default}; diff --git a/assets/html-css_CSS.md.53af0be0.lean.js b/assets/html-css_CSS.md.53af0be0.lean.js new file mode 100644 index 00000000..6e515f83 --- /dev/null +++ b/assets/html-css_CSS.md.53af0be0.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-21-02-59.47222bd8.png",o="/blog/assets/2023-02-01-20-57-45.daee2fcd.png",u=JSON.parse('{"title":"CSS3","description":"","frontmatter":{},"headers":[{"level":2,"title":"introduction","slug":"introduction","link":"#introduction","children":[{"level":3,"title":"1.历史","slug":"_1-历史","link":"#_1-历史","children":[]},{"level":3,"title":"2.处理器","slug":"_2-处理器","link":"#_2-处理器","children":[]},{"level":3,"title":"3.怎么用","slug":"_3-怎么用","link":"#_3-怎么用","children":[]},{"level":3,"title":"4.CSS3 进化到编程化:cssNext","slug":"_4-css3-进化到编程化-cssnext","link":"#_4-css3-进化到编程化-cssnext","children":[]}]},{"level":2,"title":"border","slug":"border","link":"#border","children":[{"level":3,"title":"1.border","slug":"_1-border","link":"#_1-border","children":[]},{"level":3,"title":"2.box-shallow","slug":"_2-box-shallow","link":"#_2-box-shallow","children":[]},{"level":3,"title":"3.border-image","slug":"_3-border-image","link":"#_3-border-image","children":[]}]},{"level":2,"title":"background","slug":"background","link":"#background","children":[{"level":3,"title":"1.background-origin:","slug":"_1-background-origin","link":"#_1-background-origin","children":[]},{"level":3,"title":"2.background-clip","slug":"_2-background-clip","link":"#_2-background-clip","children":[]},{"level":3,"title":"3.background-repeat","slug":"_3-background-repeat","link":"#_3-background-repeat","children":[]},{"level":3,"title":"4.background-attachment","slug":"_4-background-attachment","link":"#_4-background-attachment","children":[]},{"level":3,"title":"5.background-size","slug":"_5-background-size","link":"#_5-background-size","children":[]}]},{"level":2,"title":"text","slug":"text","link":"#text","children":[{"level":3,"title":"1.text-shadow","slug":"_1-text-shadow","link":"#_1-text-shadow","children":[]},{"level":3,"title":"2.   text 系列","slug":"_2-text-系列","link":"#_2-text-系列","children":[]}]},{"level":2,"title":"box","slug":"box","link":"#box","children":[{"level":3,"title":"1.IE6 混杂模式盒子","slug":"_1-ie6-混杂模式盒子","link":"#_1-ie6-混杂模式盒子","children":[]},{"level":3,"title":"2.flex 弹性盒子","slug":"_2-flex-弹性盒子","link":"#_2-flex-弹性盒子","children":[]}]},{"level":2,"title":"响应式网站开发","slug":"响应式网站开发","link":"#响应式网站开发","children":[{"level":3,"title":"2.响应式网页设计","slug":"_2-响应式网页设计","link":"#_2-响应式网页设计","children":[]},{"level":3,"title":"3.设置视口","slug":"_3-设置视口","link":"#_3-设置视口","children":[]},{"level":3,"title":"4.响应式网页开发方法","slug":"_4-响应式网页开发方法","link":"#_4-响应式网页开发方法","children":[]},{"level":3,"title":"5.媒体查询","slug":"_5-媒体查询","link":"#_5-媒体查询","children":[]},{"level":3,"title":"6.媒体类型","slug":"_6-媒体类型","link":"#_6-媒体类型","children":[]},{"level":3,"title":"7.单位值","slug":"_7-单位值","link":"#_7-单位值","children":[]}]}],"relativePath":"html-css/CSS.md","lastUpdated":1675259332000}'),e={name:"html-css/CSS.md"},c=l("",278),r=[c];function t(B,y,F,i,A,b){return n(),a("div",null,r)}const m=s(e,[["render",t]]);export{u as __pageData,m as default}; diff --git a/assets/html-css_HTML.md.c1393b52.js b/assets/html-css_HTML.md.c1393b52.js new file mode 100644 index 00000000..2c6a6d3c --- /dev/null +++ b/assets/html-css_HTML.md.c1393b52.js @@ -0,0 +1,651 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"HTML5","description":"","frontmatter":{},"headers":[{"level":2,"title":"大纲","slug":"大纲","link":"#大纲","children":[{"level":3,"title":"新增的属性","slug":"新增的属性","link":"#新增的属性","children":[]},{"level":3,"title":"新增的标签","slug":"新增的标签","link":"#新增的标签","children":[]},{"level":3,"title":"API","slug":"api","link":"#api","children":[]}]},{"level":2,"title":"属性篇_input 新增 type","slug":"属性篇-input-新增-type","link":"#属性篇-input-新增-type","children":[{"level":3,"title":"1.placeholder","slug":"_1-placeholder","link":"#_1-placeholder","children":[]},{"level":3,"title":"2.input 新增 type","slug":"_2-input-新增-type","link":"#_2-input-新增-type","children":[]}]},{"level":2,"title":"ContentEditable","slug":"contenteditable","link":"#contenteditable","children":[]},{"level":2,"title":"标签篇_语义化标签","slug":"标签篇-语义化标签","link":"#标签篇-语义化标签","children":[]},{"level":2,"title":"audio 与 video 播放器","slug":"audio-与-video-播放器","link":"#audio-与-video-播放器","children":[]},{"level":2,"title":"视频播放器","slug":"视频播放器","link":"#视频播放器","children":[]},{"level":2,"title":"geolocation","slug":"geolocation","link":"#geolocation","children":[]},{"level":2,"title":"四行写个服务器","slug":"四行写个服务器","link":"#四行写个服务器","children":[]},{"level":2,"title":"deviceorientation","slug":"deviceorientation","link":"#deviceorientation","children":[]},{"level":2,"title":"手机访问电脑","slug":"手机访问电脑","link":"#手机访问电脑","children":[]},{"level":2,"title":"devicemotion","slug":"devicemotion","link":"#devicemotion","children":[]},{"level":2,"title":"requestAnimationFrame","slug":"requestanimationframe","link":"#requestanimationframe","children":[]},{"level":2,"title":"localStrorage","slug":"localstrorage","link":"#localstrorage","children":[]},{"level":2,"title":"history","slug":"history","link":"#history","children":[]},{"level":2,"title":"worker","slug":"worker","link":"#worker","children":[]}],"relativePath":"html-css/HTML.md","lastUpdated":1675256330000}'),p={name:"html-css/HTML.md"},o=l(`

HTML5

大纲

新增的属性

  • placeholder
  • Calendar, date, time, email, url, search
  • ContentEditable
  • Draggable
  • Hidden
  • Content-menu
  • Data-Val(自定义属性)

新增的标签

  • 语义化标签
  • canvas
  • svg
  • Audio(声音播放)
  • Video(视频播放)

API

  • 移动端网页开发一般指的是 h5
  • 定位(需要地理位置的功能)
  • 重力感应(手机里面的陀螺仪(微信摇一摇,赛车转弯))
  • request-animation-frame(动画优化)
  • History 历史界面(控制当前页面的历史记录)
  • LocalStorage(本地存储,电脑/浏览器关闭都会保留);SessionStorage,(会话存储:窗口关闭就消失)。 都是存储信息(比如历史最高记录)
  • WebSocket(在线聊天,聊天室)
  • FileReader(文件读取,预览图)
  • WebWoker(文件的异步,提升性能,提升交互体验)
  • Fetch(传说中要替代 AJAX 的东西)

属性篇_input 新增 type

1.placeholder

html
<input type="text" placeholder="用户名/手机/邮箱" />
+<input type="password" placeholder="请输入密码" />
+
<input type="text" placeholder="用户名/手机/邮箱" />
+<input type="password" placeholder="请输入密码" />
+

2.input 新增 type

以前学的

html
<input type="radio" />
+<input type="checkbox" />
+<input type="file" />
+
<input type="radio" />
+<input type="checkbox" />
+<input type="file" />
+

input 新增 type

html
<form>
+  <!-- Calendar类 -->
+  <input type="date" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input type="time" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="week"
+  /><!-- 第几周  不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="datetime-local"
+  /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <br />
+  <input
+    type="number"
+  /><!-- 限制输入,仅数字可以。不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="email"
+  /><!-- 邮箱格式 不常用原因之一:chrome,火狐支持,Safari,IE不支持-->
+  <input
+    type="color"
+  /><!-- 颜色选择器 不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="range"
+    min="1"
+    max="100"
+    name="range"
+  /><!-- chrome,Safar支持 ,火狐,IE不支持-->
+  <input
+    type="search"
+    name="search"
+  /><!-- 自动提示历史搜索.chrome支持,Safar支持一点,IE不支持 -->
+  <input type="url" /><!-- chrome,火狐支持,Safar,IE不支持 -->
+
+  <input type="submit" />
+</form>
+
<form>
+  <!-- Calendar类 -->
+  <input type="date" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input type="time" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="week"
+  /><!-- 第几周  不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="datetime-local"
+  /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <br />
+  <input
+    type="number"
+  /><!-- 限制输入,仅数字可以。不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="email"
+  /><!-- 邮箱格式 不常用原因之一:chrome,火狐支持,Safari,IE不支持-->
+  <input
+    type="color"
+  /><!-- 颜色选择器 不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="range"
+    min="1"
+    max="100"
+    name="range"
+  /><!-- chrome,Safar支持 ,火狐,IE不支持-->
+  <input
+    type="search"
+    name="search"
+  /><!-- 自动提示历史搜索.chrome支持,Safar支持一点,IE不支持 -->
+  <input type="url" /><!-- chrome,火狐支持,Safar,IE不支持 -->
+
+  <input type="submit" />
+</form>
+

ContentEditable

html
<div>Panda</div>
+
<div>Panda</div>
+

想修改内容 原生方法:增加点击事件修改 新方法

html
<div contenteditable="true">Panda</div>
+
<div contenteditable="true">Panda</div>
+

默认值 false,没有兼容性问题,可以继承(包裹的子元素),可以覆盖(后来设置的覆盖前面设置的) 常见误区:

html
<div contenteditable="true">
+  <span contenteditable="false">姓名:</span>Panda<br />
+  <span contenteditable="false">姓别:</span>男<br />
+  <!-- 会导致删除br等标签 -->
+</div>
+
<div contenteditable="true">
+  <span contenteditable="false">姓名:</span>Panda<br />
+  <span contenteditable="false">姓别:</span>男<br />
+  <!-- 会导致删除br等标签 -->
+</div>
+

总结:实战开发可用属性:contenteditable, placeholder

标签篇_语义化标签

全是 div,只是语义化

html
<header></header>
+<footer></footer>
+<nav></nav>
+<article></article>
+<!--文章,可以直接被引用拿走的-->
+<section></section>
+<!--段落结构,一般section放在article里面-->
+<aside></aside>
+<!--侧边栏-->
+
<header></header>
+<footer></footer>
+<nav></nav>
+<article></article>
+<!--文章,可以直接被引用拿走的-->
+<section></section>
+<!--段落结构,一般section放在article里面-->
+<aside></aside>
+<!--侧边栏-->
+

audio 与 video 播放器

html
<audio src="" controls></audio> <video src="" controls></video>
+
<audio src="" controls></audio> <video src="" controls></video>
+

以上:太丑,不同浏览器不统一

视频播放器

加载不出来https://blog.csdn.net/qq_40340478/article/details/108309492

只有 http 协议中视频资源带有 Content-Range 属性,才能设置时间进行跳转

html
var express = require("express"); var app = new express();
+app.use(express.static('./')); app.listen(12306); // 如果不能改变进度条
+就用第36节的黑科技 访问127.0.0.1:12306/test.html
+
var express = require("express"); var app = new express();
+app.use(express.static('./')); app.listen(12306); // 如果不能改变进度条
+就用第36节的黑科技 访问127.0.0.1:12306/test.html
+

geolocation

html
<script>
+  // 获取地理信息
+  // 一些系统,不支持这个功能
+  // GPS定位。台式机几乎都没有GPS,笔记本大多数没有GPS,智能手机几乎都有GPS
+  // 网络定位 来粗略估计地理位置
+  window.navigator.geolocation.getCurrentPosition(
+    function (position) {
+      console.log("======"); //成功的回调函数
+      console.log(position);
+    },
+    function () {
+      //失败的回调函数
+      console.log("++++++");
+    }
+  );
+  //可以访问的方式:https协议,file协议,http协议下不能获取
+  // 经度最大值180,纬度最大值90
+</script>
+
<script>
+  // 获取地理信息
+  // 一些系统,不支持这个功能
+  // GPS定位。台式机几乎都没有GPS,笔记本大多数没有GPS,智能手机几乎都有GPS
+  // 网络定位 来粗略估计地理位置
+  window.navigator.geolocation.getCurrentPosition(
+    function (position) {
+      console.log("======"); //成功的回调函数
+      console.log(position);
+    },
+    function () {
+      //失败的回调函数
+      console.log("++++++");
+    }
+  );
+  //可以访问的方式:https协议,file协议,http协议下不能获取
+  // 经度最大值180,纬度最大值90
+</script>
+

https 还是不能访问:

  1. 谷歌浏览器打开谷歌地图,无法定位
  2. 利用翻墙可以实现

四行写个服务器

手机访问电脑 sever.js npm init npm i express

css
var express = require('express');
+var app = new express();
+app.use(express.static("./page"));
+app.listen(12306);//端口号大于8000或者等于80
+// 默认访问80端口,express默认访问index.html
+想访问里面的hello.html
+127.0.0.1:12306
+127.0.0.1:12306/hello.html
+
var express = require('express');
+var app = new express();
+app.use(express.static("./page"));
+app.listen(12306);//端口号大于8000或者等于80
+// 默认访问80端口,express默认访问index.html
+想访问里面的hello.html
+127.0.0.1:12306
+127.0.0.1:12306/hello.html
+

test.7z 命令框或者 vscode 客户端,进入项目路径,node server.js

deviceorientation

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <link rel="stylesheet" href="" />
+  </head>
+  <body>
+    <div id="main"></div>
+    <script>
+      // 陀螺仪,只有支持陀螺仪的设备才支持体感
+      // 苹果设备的页面只有在https协议下,才能使用这些接口
+      // 11.1.X以及之前,可以使用。微信的浏览器
+
+      // alpha:指北(指南针) [0,360) 当为0的时候指北。180指南
+      // beta:平放的时候beta值为0。当手机立起来(短边接触桌面),直立的时候beta为90;
+      // gamma:平放的时候gamma值为零。手机立起来(长边接触桌面),直立的时候gamma值为90
+
+      window.addEventListener("deviceorientation", function (event) {
+        // console.log(event);
+        document.getElementById("main").innerHTML =
+          "alpha:" +
+          event.alpha +
+          "<br/>" +
+          "beta:" +
+          event.beta +
+          "<br/>" +
+          "gamma:" +
+          event.gamma;
+      });
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <link rel="stylesheet" href="" />
+  </head>
+  <body>
+    <div id="main"></div>
+    <script>
+      // 陀螺仪,只有支持陀螺仪的设备才支持体感
+      // 苹果设备的页面只有在https协议下,才能使用这些接口
+      // 11.1.X以及之前,可以使用。微信的浏览器
+
+      // alpha:指北(指南针) [0,360) 当为0的时候指北。180指南
+      // beta:平放的时候beta值为0。当手机立起来(短边接触桌面),直立的时候beta为90;
+      // gamma:平放的时候gamma值为零。手机立起来(长边接触桌面),直立的时候gamma值为90
+
+      window.addEventListener("deviceorientation", function (event) {
+        // console.log(event);
+        document.getElementById("main").innerHTML =
+          "alpha:" +
+          event.alpha +
+          "<br/>" +
+          "beta:" +
+          event.beta +
+          "<br/>" +
+          "gamma:" +
+          event.gamma;
+      });
+    </script>
+  </body>
+</html>
+

手机访问电脑

1.手机和电脑在同一个局域网下 2.获取电脑的 IP 地址 windows 获取 ip:终端输入 ipconfig 3.在手机上输入相应的 IP 和端口进行访问

devicemotion

html
<script>
+  // 摇一摇
+  window.addEventListener("devicemotion", function (event) {
+    document.getElementById("main").innerHTML =
+      event.accelertion.x +
+      "<br/>" +
+      event.accelertion.y +
+      "<br/>" +
+      event.accelertion.z;
+    if (
+      Math.abs(event.accelertion.x) > 9 ||
+      Math.abs(event.accelertion.y) > 9 ||
+      Math.abs(event.accelertion.z) > 9
+    ) {
+      alert("在晃");
+    }
+  });
+</script>
+
<script>
+  // 摇一摇
+  window.addEventListener("devicemotion", function (event) {
+    document.getElementById("main").innerHTML =
+      event.accelertion.x +
+      "<br/>" +
+      event.accelertion.y +
+      "<br/>" +
+      event.accelertion.z;
+    if (
+      Math.abs(event.accelertion.x) > 9 ||
+      Math.abs(event.accelertion.y) > 9 ||
+      Math.abs(event.accelertion.z) > 9
+    ) {
+      alert("在晃");
+    }
+  });
+</script>
+

requestAnimationFrame

javascript
/*
+    function move(){
+    	var square = document.getElementById("main");
+    	if(square.offsetLeft > 700){
+    		return;
+    	}
+    	square.style.left = square.offsetLeft + 20 +"px";  
+    }
+    setInterval(move, 10);
+    */
+// 屏幕刷新频率:每秒60次
+// 如果变化一秒超过60次,就会有动画针会被丢掉
+
+// 实现均匀移动,用requestAnimationFrame,是每秒60针
+// 将计就计setInterval(move, 1000/60);会实现同样效果吗
+// 1针少于1/60秒,requestAnimationFrame可以准时执行每一帧的
+// requestAnimationFrame(move);//移动一次
+var timer = null;
+function move() {
+  var square = document.getElementById("main");
+  if (square.offsetLeft > 700) {
+    cancelAnimationFrame(timer);
+    return;
+  }
+  square.style.left = square.offsetLeft + 20 + "px";
+  timer = requestAnimationFrame(move);
+}
+move();
+// cancelAnimationFrame基本相当于clearTimeout
+// requestAnimationFrame兼容性极差
+
/*
+    function move(){
+    	var square = document.getElementById("main");
+    	if(square.offsetLeft > 700){
+    		return;
+    	}
+    	square.style.left = square.offsetLeft + 20 +"px";  
+    }
+    setInterval(move, 10);
+    */
+// 屏幕刷新频率:每秒60次
+// 如果变化一秒超过60次,就会有动画针会被丢掉
+
+// 实现均匀移动,用requestAnimationFrame,是每秒60针
+// 将计就计setInterval(move, 1000/60);会实现同样效果吗
+// 1针少于1/60秒,requestAnimationFrame可以准时执行每一帧的
+// requestAnimationFrame(move);//移动一次
+var timer = null;
+function move() {
+  var square = document.getElementById("main");
+  if (square.offsetLeft > 700) {
+    cancelAnimationFrame(timer);
+    return;
+  }
+  square.style.left = square.offsetLeft + 20 + "px";
+  timer = requestAnimationFrame(move);
+}
+move();
+// cancelAnimationFrame基本相当于clearTimeout
+// requestAnimationFrame兼容性极差
+

兼容性极差还想使用咋办?

javascript
window.cancelAnimationFrame = (function () {
+  return (
+    window.cancelAnimationFrame ||
+    window.webkitCancelAnimationFrame ||
+    window.mozCancelAnimationFrame ||
+    function (id) {
+      window.clearTimeOut(id);
+    }
+  );
+})();
+
+window.requestAnimationFrame = (function () {
+  return (
+    window.requestAnimationFrame ||
+    window.webkitRequestAnimationFrame ||
+    window.mozRequestAnimationFrame ||
+    function (id) {
+      window.setTimeOut(id, 1000 / 60);
+    }
+  );
+})();
+
window.cancelAnimationFrame = (function () {
+  return (
+    window.cancelAnimationFrame ||
+    window.webkitCancelAnimationFrame ||
+    window.mozCancelAnimationFrame ||
+    function (id) {
+      window.clearTimeOut(id);
+    }
+  );
+})();
+
+window.requestAnimationFrame = (function () {
+  return (
+    window.requestAnimationFrame ||
+    window.webkitRequestAnimationFrame ||
+    window.mozRequestAnimationFrame ||
+    function (id) {
+      window.setTimeOut(id, 1000 / 60);
+    }
+  );
+})();
+

localStrorage

cookie:每次请求都有可能传送许多无用的信息到后端 localStroage:长期存放在浏览器,无论窗口是否关闭

javascript
localStorage.name = "panda";
+// localStroage.arr = [1, 2, 3];
+// console.log(localStroage.arr);
+// localStroage只能存储字符串,
+
+要想存储数组;
+localStorage.arr = JSON.stringify([1, 2, 3]);
+// console.log(localStorage.arr);
+console.log(JSON.parse(localStorage.arr));
+
+localStorage.obj = JSON.stringify({
+  name: "panda",
+  age: 18,
+});
+console.log(JSON.parse(localStorage.obj));
+
localStorage.name = "panda";
+// localStroage.arr = [1, 2, 3];
+// console.log(localStroage.arr);
+// localStroage只能存储字符串,
+
+要想存储数组;
+localStorage.arr = JSON.stringify([1, 2, 3]);
+// console.log(localStorage.arr);
+console.log(JSON.parse(localStorage.arr));
+
+localStorage.obj = JSON.stringify({
+  name: "panda",
+  age: 18,
+});
+console.log(JSON.parse(localStorage.obj));
+

sessionStroage:这次回话临时需要存储时的变量。每次窗口关闭的时候,seccionStroage 自动清空

html
sessionStorage.name = "panda";
+
sessionStorage.name = "panda";
+

localStorage 与 cookie

1.localStorage 在发送请求的时候不会把数据发出去,cookie 会把所有数据带出去    2.cookie 存储的内容比较(4k) ,localStroage 可以存放较多内容(5M)

另一种写法

html
localStorage.setItem("name", "monkey"); localStorage.getItem("name");
+localStorage.removeItem("name");
+
localStorage.setItem("name", "monkey"); localStorage.getItem("name");
+localStorage.removeItem("name");
+

相同协议,相同域名,相同端口称为一个域

注意:

www.baidu.com不是一个域。 http://www.baidu.com  https://www.baidu.com,是域。这是不同域

history

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <style type="text/css"></style>
+    <script>
+      // A -> B - C
+      // 为了网页的性能,单页面操作
+      var data = [
+        {
+          name: "HTML",
+        },
+        {
+          name: "CSS",
+        },
+        {
+          name: "JS",
+        },
+        {
+          name: "panda",
+        },
+        {
+          name: "dengge",
+        },
+      ];
+      function search() {
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+        history.pushState({ inpVal: value }, null, "#" + value);
+      }
+      function render(renderData) {
+        var content = "";
+        for (var i = 0; i < renderData.length; i++) {
+          content += "<div>" + renderData[i].name + "</div>";
+        }
+        document.getElementById("main").innerHTML = content;
+      }
+      window.addEventListener("popstate", function (e) {
+        // console.log(e);
+        document.getElementById("search").value = e.state.inpVal
+          ? e.state.inpVal
+          : "";
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+      });
+      // 关于popstate和hashchange
+      // 只要url变了,就会触发popstate
+      // 锚点变了(hash值变了),就会触发hashchange
+      window.addEventListener("hashchange", function (e) {
+        console.log(e);
+      });
+    </script>
+  </head>
+
+  <body>
+    <input type="text" id="search" /><button onclick="search()">搜索</button>
+    <div id="main"></div>
+    <script>
+      render(data);
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <style type="text/css"></style>
+    <script>
+      // A -> B - C
+      // 为了网页的性能,单页面操作
+      var data = [
+        {
+          name: "HTML",
+        },
+        {
+          name: "CSS",
+        },
+        {
+          name: "JS",
+        },
+        {
+          name: "panda",
+        },
+        {
+          name: "dengge",
+        },
+      ];
+      function search() {
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+        history.pushState({ inpVal: value }, null, "#" + value);
+      }
+      function render(renderData) {
+        var content = "";
+        for (var i = 0; i < renderData.length; i++) {
+          content += "<div>" + renderData[i].name + "</div>";
+        }
+        document.getElementById("main").innerHTML = content;
+      }
+      window.addEventListener("popstate", function (e) {
+        // console.log(e);
+        document.getElementById("search").value = e.state.inpVal
+          ? e.state.inpVal
+          : "";
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+      });
+      // 关于popstate和hashchange
+      // 只要url变了,就会触发popstate
+      // 锚点变了(hash值变了),就会触发hashchange
+      window.addEventListener("hashchange", function (e) {
+        console.log(e);
+      });
+    </script>
+  </head>
+
+  <body>
+    <input type="text" id="search" /><button onclick="search()">搜索</button>
+    <div id="main"></div>
+    <script>
+      render(data);
+    </script>
+  </body>
+</html>
+

worker

html
<script>
+  // js都是单线程的
+  // worker是多线程的, 真的多线程, 不是伪多线程
+  // worker不能操作DOM,没有window对象,不能读取本地文件。可以发ajax,可以计算
+  // 在worker中可以创建worker吗?
+  // 在理论上可以,但是没有一款浏览器支持
+  /*
+        console.log("=======");
+        console.log("=======");
+        var a = 1000;
+        var result = 0;
+        for (var i = 0; i < a; i++) {
+            result += i;
+        }
+        console.log(result);
+        console.log("=======");
+        console.log("=======");
+        */
+  // 后两个等号只有等待算完才执行
+
+  var beginTime = Data.now();
+  console.log("=======");
+  console.log("=======");
+  var a = 1000;
+  var worker = new Worker("./worker.js");
+  worker.postMessage({
+    num: a,
+  });
+  worker.onmessage = function (e) {
+    console.log(e.data);
+  };
+  console.log("=======");
+  console.log("=======");
+  var endTime = Data.now();
+  console.log(endTime - beginTime);
+  worker.terminate(); //停止
+  this.close(); //自己停止
+</script>
+
<script>
+  // js都是单线程的
+  // worker是多线程的, 真的多线程, 不是伪多线程
+  // worker不能操作DOM,没有window对象,不能读取本地文件。可以发ajax,可以计算
+  // 在worker中可以创建worker吗?
+  // 在理论上可以,但是没有一款浏览器支持
+  /*
+        console.log("=======");
+        console.log("=======");
+        var a = 1000;
+        var result = 0;
+        for (var i = 0; i < a; i++) {
+            result += i;
+        }
+        console.log(result);
+        console.log("=======");
+        console.log("=======");
+        */
+  // 后两个等号只有等待算完才执行
+
+  var beginTime = Data.now();
+  console.log("=======");
+  console.log("=======");
+  var a = 1000;
+  var worker = new Worker("./worker.js");
+  worker.postMessage({
+    num: a,
+  });
+  worker.onmessage = function (e) {
+    console.log(e.data);
+  };
+  console.log("=======");
+  console.log("=======");
+  var endTime = Data.now();
+  console.log(endTime - beginTime);
+  worker.terminate(); //停止
+  this.close(); //自己停止
+</script>
+

worker.js

javascript
this.onmessage = function (e) {
+  //接受消息
+  // console.log(e);
+  var result = 0;
+  for (var i = 0; i < e.data.num; i++) {
+    result += i;
+  }
+  this.postMessage(result);
+};
+
this.onmessage = function (e) {
+  //接受消息
+  // console.log(e);
+  var result = 0;
+  for (var i = 0; i < e.data.num; i++) {
+    result += i;
+  }
+  this.postMessage(result);
+};
+

worker.js 里面可以通过 importScripts("./index.js")引入外部 js 文件

`,70),e=[o];function t(c,r,B,y,F,i){return n(),a("div",null,e)}const b=s(p,[["render",t]]);export{u as __pageData,b as default}; diff --git a/assets/html-css_HTML.md.c1393b52.lean.js b/assets/html-css_HTML.md.c1393b52.lean.js new file mode 100644 index 00000000..28f87f7d --- /dev/null +++ b/assets/html-css_HTML.md.c1393b52.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"HTML5","description":"","frontmatter":{},"headers":[{"level":2,"title":"大纲","slug":"大纲","link":"#大纲","children":[{"level":3,"title":"新增的属性","slug":"新增的属性","link":"#新增的属性","children":[]},{"level":3,"title":"新增的标签","slug":"新增的标签","link":"#新增的标签","children":[]},{"level":3,"title":"API","slug":"api","link":"#api","children":[]}]},{"level":2,"title":"属性篇_input 新增 type","slug":"属性篇-input-新增-type","link":"#属性篇-input-新增-type","children":[{"level":3,"title":"1.placeholder","slug":"_1-placeholder","link":"#_1-placeholder","children":[]},{"level":3,"title":"2.input 新增 type","slug":"_2-input-新增-type","link":"#_2-input-新增-type","children":[]}]},{"level":2,"title":"ContentEditable","slug":"contenteditable","link":"#contenteditable","children":[]},{"level":2,"title":"标签篇_语义化标签","slug":"标签篇-语义化标签","link":"#标签篇-语义化标签","children":[]},{"level":2,"title":"audio 与 video 播放器","slug":"audio-与-video-播放器","link":"#audio-与-video-播放器","children":[]},{"level":2,"title":"视频播放器","slug":"视频播放器","link":"#视频播放器","children":[]},{"level":2,"title":"geolocation","slug":"geolocation","link":"#geolocation","children":[]},{"level":2,"title":"四行写个服务器","slug":"四行写个服务器","link":"#四行写个服务器","children":[]},{"level":2,"title":"deviceorientation","slug":"deviceorientation","link":"#deviceorientation","children":[]},{"level":2,"title":"手机访问电脑","slug":"手机访问电脑","link":"#手机访问电脑","children":[]},{"level":2,"title":"devicemotion","slug":"devicemotion","link":"#devicemotion","children":[]},{"level":2,"title":"requestAnimationFrame","slug":"requestanimationframe","link":"#requestanimationframe","children":[]},{"level":2,"title":"localStrorage","slug":"localstrorage","link":"#localstrorage","children":[]},{"level":2,"title":"history","slug":"history","link":"#history","children":[]},{"level":2,"title":"worker","slug":"worker","link":"#worker","children":[]}],"relativePath":"html-css/HTML.md","lastUpdated":1675256330000}'),p={name:"html-css/HTML.md"},o=l("",70),e=[o];function t(c,r,B,y,F,i){return n(),a("div",null,e)}const b=s(p,[["render",t]]);export{u as __pageData,b as default}; diff --git a/assets/html-css_animation.md.da2d65b2.js b/assets/html-css_animation.md.da2d65b2.js new file mode 100644 index 00000000..28d735cb --- /dev/null +++ b/assets/html-css_animation.md.da2d65b2.js @@ -0,0 +1,1561 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-21-04-28.4121323f.png",o="/blog/assets/2023-02-01-21-04-38.6dfd96aa.png",m=JSON.parse('{"title":"动画","description":"","frontmatter":{},"headers":[{"level":2,"title":"1.transition 过渡动画","slug":"_1-transition-过渡动画","link":"#_1-transition-过渡动画","children":[]},{"level":2,"title":"2.cubic-bezier","slug":"_2-cubic-bezier","link":"#_2-cubic-bezier","children":[]},{"level":2,"title":"3.animation","slug":"_3-animation","link":"#_3-animation","children":[]},{"level":2,"title":"4.step 跳转动画","slug":"_4-step-跳转动画","link":"#_4-step-跳转动画","children":[]},{"level":2,"title":"5.rotate3D 变换","slug":"_5-rotate3d-变换","link":"#_5-rotate3d-变换","children":[]},{"level":2,"title":"6.scale 伸缩","slug":"_6-scale-伸缩","link":"#_6-scale-伸缩","children":[]},{"level":2,"title":"7.skew 倾斜","slug":"_7-skew-倾斜","link":"#_7-skew-倾斜","children":[]},{"level":2,"title":"8.translate+perspective","slug":"_8-translate-perspective","link":"#_8-translate-perspective","children":[]},{"level":2,"title":"9.matrix","slug":"_9-matrix","link":"#_9-matrix","children":[]}],"relativePath":"html-css/animation.md","lastUpdated":1675259332000}'),e={name:"html-css/animation.md"},c=l(`

动画

1.transition 过渡动画

css
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* transition: transition-property, transition-duration,transition-timing-function,transition-delay; */
+  /* transition-property: all; */
+  /* 第一个监听属性 */
+  /* transition-property: width, height; */
+  /* transition-duration: ; */
+  /* 第二个时间间隔 */
+  /* transition-timing-function:linear ; */
+  /* 第三个运动状态 */
+  /* transition-delay: ; */
+  /* 第四个延迟 */
+  transition: width 2s linear 1s;
+  /* 总共3s,1s后开始宽度增加,增加2s */
+  /* 添加方式
+    前两个必须有,后两个默认
+    */
+}
+div:hover {
+  width: 200px;
+  height: 200px;
+}
+
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* transition: transition-property, transition-duration,transition-timing-function,transition-delay; */
+  /* transition-property: all; */
+  /* 第一个监听属性 */
+  /* transition-property: width, height; */
+  /* transition-duration: ; */
+  /* 第二个时间间隔 */
+  /* transition-timing-function:linear ; */
+  /* 第三个运动状态 */
+  /* transition-delay: ; */
+  /* 第四个延迟 */
+  transition: width 2s linear 1s;
+  /* 总共3s,1s后开始宽度增加,增加2s */
+  /* 添加方式
+    前两个必须有,后两个默认
+    */
+}
+div:hover {
+  width: 200px;
+  height: 200px;
+}
+

动画 demo

css
div {
+  position: absolute;
+  left: 0;
+  top: 0;
+  opacity: 0.5;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  transition: all 2s;
+}
+
+div:hover {
+  opacity: 1;
+  top: 100px;
+  left: 100px;
+  height: 200px;
+  width: 200px;
+}
+
div {
+  position: absolute;
+  left: 0;
+  top: 0;
+  opacity: 0.5;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  transition: all 2s;
+}
+
+div:hover {
+  opacity: 1;
+  top: 100px;
+  left: 100px;
+  height: 200px;
+  width: 200px;
+}
+

2.cubic-bezier

css
div {
+  width: 100px;
+  height: 50px;
+  border: 1px solid red;
+  border-radius: 10px;
+  transition: all 1s cubic-bezier(0.5, -1.5, 0.8, 2); /*代表两个坐标点
+    	x:(0,1)
+    	y:都可
+    */
+}
+div:hover {
+  width: 300px;
+}
+
div {
+  width: 100px;
+  height: 50px;
+  border: 1px solid red;
+  border-radius: 10px;
+  transition: all 1s cubic-bezier(0.5, -1.5, 0.8, 2); /*代表两个坐标点
+    	x:(0,1)
+    	y:都可
+    */
+}
+div:hover {
+  width: 300px;
+}
+

3.animation

css
@keyframes run {
+  0% {
+    /* 0%相当于from */
+    left: 0;
+    top: 0;
+    /* background-color: red; */
+  }
+  25% {
+    left: 100px;
+    top: 0;
+    /* background-color: green; */
+  }
+  50% {
+    left: 100px;
+    top: 100px;
+    /* background-color: blue; */
+  }
+  75% {
+    left: 0;
+    top: 100px;
+    /* background-color: coral; */
+  }
+  100% {
+    /* 100%相当于to  */
+    left: 0;
+    top: 0;
+  }
+}
+@keyframes color-change {
+  0% {
+    background-color: red;
+  }
+  60% {
+    background-color: blue;
+  }
+  100% {
+    background-color: black;
+  }
+}
+div {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* animation: run 4s; */
+  /* animation: run 4s, color-change 4s;两个动画同时运行 */
+  /* animation: run 4s cubic-bezier(.5,1,1,1);其实是是每一段的运动状态 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s;延迟1s执行动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2; 执行2次动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s infinite; 死循环 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s reverse;倒着走 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2 alternate;先证者走,在倒着走,意味着次数>=2(单摆) */
+}
+
@keyframes run {
+  0% {
+    /* 0%相当于from */
+    left: 0;
+    top: 0;
+    /* background-color: red; */
+  }
+  25% {
+    left: 100px;
+    top: 0;
+    /* background-color: green; */
+  }
+  50% {
+    left: 100px;
+    top: 100px;
+    /* background-color: blue; */
+  }
+  75% {
+    left: 0;
+    top: 100px;
+    /* background-color: coral; */
+  }
+  100% {
+    /* 100%相当于to  */
+    left: 0;
+    top: 0;
+  }
+}
+@keyframes color-change {
+  0% {
+    background-color: red;
+  }
+  60% {
+    background-color: blue;
+  }
+  100% {
+    background-color: black;
+  }
+}
+div {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* animation: run 4s; */
+  /* animation: run 4s, color-change 4s;两个动画同时运行 */
+  /* animation: run 4s cubic-bezier(.5,1,1,1);其实是是每一段的运动状态 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s;延迟1s执行动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2; 执行2次动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s infinite; 死循环 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s reverse;倒着走 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2 alternate;先证者走,在倒着走,意味着次数>=2(单摆) */
+}
+

animation-fill-mode:

forwards: 设置对象状态为动画结束时候的状态

backwards:设置对象状态为动画开始时候第一针的状态

both:设置对象状态为动画结束和开始的状态的综合体

太阳月亮 demo——文件夹

4.step 跳转动画

css
@keyframes change-color {
+  0% {
+    background-color: red;
+  }
+
+  25% {
+    background-color: green;
+  }
+
+  50% {
+    background-color: blue;
+  }
+
+  75% {
+    background-color: black;
+  }
+
+  100% {
+    background-color: #fff;
+  }
+}
+div {
+  width: 100px;
+  height: 100px;
+  background-color: rgb(247, 20, 247);
+  /* animation: change-color 4s steps(1, end); */
+  /* 不过度 一步到位 */
+  /* animation: change-color 4s steps(2, end); */
+  /* 每一段用2步实现,动画更加细腻了 */
+  /* animation: change-color 4s steps(1, start); */
+  /* start与end 
+    	end保留当前帧状态,直到这个动画时间结束    忽略最后一针
+    	start保留下一针状态,直到这段动画时间结束   忽略第一针
+    */
+  /* 要想弥补时间段   想看见最后一针
+    	animation: change-color 4s steps(1, end) forwards;
+    */
+}
+
@keyframes change-color {
+  0% {
+    background-color: red;
+  }
+
+  25% {
+    background-color: green;
+  }
+
+  50% {
+    background-color: blue;
+  }
+
+  75% {
+    background-color: black;
+  }
+
+  100% {
+    background-color: #fff;
+  }
+}
+div {
+  width: 100px;
+  height: 100px;
+  background-color: rgb(247, 20, 247);
+  /* animation: change-color 4s steps(1, end); */
+  /* 不过度 一步到位 */
+  /* animation: change-color 4s steps(2, end); */
+  /* 每一段用2步实现,动画更加细腻了 */
+  /* animation: change-color 4s steps(1, start); */
+  /* start与end 
+    	end保留当前帧状态,直到这个动画时间结束    忽略最后一针
+    	start保留下一针状态,直到这段动画时间结束   忽略第一针
+    */
+  /* 要想弥补时间段   想看见最后一针
+    	animation: change-color 4s steps(1, end) forwards;
+    */
+}
+

区别 end 与 start

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes run {
+        0% {
+          left: 0;
+        }
+
+        25% {
+          left: 100px;
+        }
+
+        50% {
+          left: 200px;
+        }
+
+        75% {
+          left: 300px;
+        }
+
+        100% {
+          left: 400px;
+        }
+      }
+
+      .demo1,
+      .demo2 {
+        position: absolute;
+        left: 0;
+        background-color: black;
+        width: 100px;
+        height: 100px;
+        color: #fff;
+      }
+
+      .demo1 {
+        animation: run 4s steps(1, start);
+      }
+
+      .demo2 {
+        top: 100px;
+        /* animation: run 4s steps(1, end); */
+        animation: run 4s steps(1, end) forwards;
+      }
+
+      /* 
+            steps(1,end);===step-end
+            steps(1,start);===step-start
+            */
+    </style>
+  </head>
+
+  <body>
+    <div class="demo1">start</div>
+    <div class="demo2">end</div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes run {
+        0% {
+          left: 0;
+        }
+
+        25% {
+          left: 100px;
+        }
+
+        50% {
+          left: 200px;
+        }
+
+        75% {
+          left: 300px;
+        }
+
+        100% {
+          left: 400px;
+        }
+      }
+
+      .demo1,
+      .demo2 {
+        position: absolute;
+        left: 0;
+        background-color: black;
+        width: 100px;
+        height: 100px;
+        color: #fff;
+      }
+
+      .demo1 {
+        animation: run 4s steps(1, start);
+      }
+
+      .demo2 {
+        top: 100px;
+        /* animation: run 4s steps(1, end); */
+        animation: run 4s steps(1, end) forwards;
+      }
+
+      /* 
+            steps(1,end);===step-end
+            steps(1,start);===step-start
+            */
+    </style>
+  </head>
+
+  <body>
+    <div class="demo1">start</div>
+    <div class="demo2">end</div>
+  </body>
+</html>
+

打字效果

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes cursor {
+        0% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+
+        50% {
+          border-left-color: rgba(0, 0, 0, 1);
+        }
+
+        100% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+      }
+
+      @keyframes cover {
+        0% {
+          left: 0;
+        }
+
+        100% {
+          left: 100%;
+        }
+      }
+
+      div {
+        position: relative;
+        display: inline-block;
+        height: 100px;
+        /* background-color: red; */
+        font-size: 80px;
+        line-height: 100px;
+        font-family: monospace;
+      }
+
+      div::after {
+        content: "";
+        position: absolute;
+        left: 0;
+        top: 10px;
+        height: 90px;
+        width: 100%;
+        background-color: #fff;
+        border-left: 2px solid black;
+        box-sizing: border-box;
+        animation: cursor 1s steps(1, end) infinite, cover 12s steps(12, end);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div>sdknajnakjna</div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes cursor {
+        0% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+
+        50% {
+          border-left-color: rgba(0, 0, 0, 1);
+        }
+
+        100% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+      }
+
+      @keyframes cover {
+        0% {
+          left: 0;
+        }
+
+        100% {
+          left: 100%;
+        }
+      }
+
+      div {
+        position: relative;
+        display: inline-block;
+        height: 100px;
+        /* background-color: red; */
+        font-size: 80px;
+        line-height: 100px;
+        font-family: monospace;
+      }
+
+      div::after {
+        content: "";
+        position: absolute;
+        left: 0;
+        top: 10px;
+        height: 90px;
+        width: 100%;
+        background-color: #fff;
+        border-left: 2px solid black;
+        box-sizing: border-box;
+        animation: cursor 1s steps(1, end) infinite, cover 12s steps(12, end);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div>sdknajnakjna</div>
+  </body>
+</html>
+

表盘效果——文件夹

跑马效果

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      @keyframes run {
+        0% {
+          background-position: 0 0;
+        }
+
+        100% {
+          background-position: -2400px 0;
+        }
+      }
+      div {
+        width: 200px;
+        height: 100px;
+        background-image: url(./web/horse.png);
+        background-repeat: no-repeat;
+        background-position: 0 0;
+        animation: run 0.3s steps(12, end) infinite;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="horse"></div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      @keyframes run {
+        0% {
+          background-position: 0 0;
+        }
+
+        100% {
+          background-position: -2400px 0;
+        }
+      }
+      div {
+        width: 200px;
+        height: 100px;
+        background-image: url(./web/horse.png);
+        background-repeat: no-repeat;
+        background-position: 0 0;
+        animation: run 0.3s steps(12, end) infinite;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="horse"></div>
+  </body>
+</html>
+

5.rotate3D 变换

rotate:2d 变换

rotateX,rotateY,rotateZ:3d 变换

旋转

css
@keyframes round {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform: rotate(0deg);
+  /* transform-origin: center center; */
+  /* 圆心给谁设置,参考的就是谁 */
+  transform-origin: 0 0;
+  animation: round 2s infinite;
+}
+
@keyframes round {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform: rotate(0deg);
+  /* transform-origin: center center; */
+  /* 圆心给谁设置,参考的就是谁 */
+  transform-origin: 0 0;
+  animation: round 2s infinite;
+}
+

3D 变换

css
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  /* transform: rotateX(0deg); */
+  /* transform: rotateX(0deg) rotateY(0deg); */
+  /* 旋转顺序不同,结果不同 */
+}
+
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  /* transform: rotateX(0deg); */
+  /* transform: rotateX(0deg) rotateY(0deg); */
+  /* 旋转顺序不同,结果不同 */
+}
+

rotate3d()

transform: rotate3d(x, y, z, angle); 矢量和作为旋转轴

小练习图片钟摆效果

css
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes change {
+  0% {
+    transform: rotateX(-45deg) rotateY(90deg);
+  }
+
+  100% {
+    transform: rotateX(45deg) rotateY(90deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  animation: change 2s cubic-bezier(0.5, 0, 0.5, 1) infinite alternate;
+}
+
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes change {
+  0% {
+    transform: rotateX(-45deg) rotateY(90deg);
+  }
+
+  100% {
+    transform: rotateX(45deg) rotateY(90deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  animation: change 2s cubic-bezier(0.5, 0, 0.5, 1) infinite alternate;
+}
+

6.scale 伸缩

css
/* transform: scale(1, 2); 2d x,y轴*/
+/* 
+    transform: scaleX();
+    transform: scaleY();
+    transform: scalez();
+    transform: scale3d();  就是叠加 
+*/
+/* scale:
+    1.伸缩元素变化坐标轴的刻度 ,translateX,Y验证。伸缩之后,translateX(100)产生的效果是200
+    2.设置两次,则第二次在第一次基础上叠加
+    3.旋转伸缩在一个轴进行
+		4.雁过留声  伸缩过的影响一直保留 
+*/
+/* transform: scale() rotate();位置讲究  先后scale不一样*/
+
/* transform: scale(1, 2); 2d x,y轴*/
+/* 
+    transform: scaleX();
+    transform: scaleY();
+    transform: scalez();
+    transform: scale3d();  就是叠加 
+*/
+/* scale:
+    1.伸缩元素变化坐标轴的刻度 ,translateX,Y验证。伸缩之后,translateX(100)产生的效果是200
+    2.设置两次,则第二次在第一次基础上叠加
+    3.旋转伸缩在一个轴进行
+		4.雁过留声  伸缩过的影响一直保留 
+*/
+/* transform: scale() rotate();位置讲究  先后scale不一样*/
+

7.skew 倾斜

skew(x, y);

skewx();

skewy();

demo

css
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes skewchange {
+  0% {
+    transform: skew(45deg, 45deg);
+  }
+
+  50% {
+    transform: skew(0, 0);
+  }
+
+  100% {
+    transform: skew(-45deg, -45deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: center center;
+  /* transform: skew(0deg, 0deg); */
+  /* 倾斜的不是元素本身,而是坐标轴
+    坐标轴倾斜,刻度被拉伸 */
+  /* 
+        skew(x,y);
+        skewx()
+        skewy() 
+    */
+  animation: skewchange 4s cubic-bezier(0, 0, 1, 1) infinite alternate;
+}
+
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes skewchange {
+  0% {
+    transform: skew(45deg, 45deg);
+  }
+
+  50% {
+    transform: skew(0, 0);
+  }
+
+  100% {
+    transform: skew(-45deg, -45deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: center center;
+  /* transform: skew(0deg, 0deg); */
+  /* 倾斜的不是元素本身,而是坐标轴
+    坐标轴倾斜,刻度被拉伸 */
+  /* 
+        skew(x,y);
+        skewx()
+        skewy() 
+    */
+  animation: skewchange 4s cubic-bezier(0, 0, 1, 1) infinite alternate;
+}
+

8.translate+perspective

2d translate(x, y) translatex() translatey() translatex() translate3d()

css
/* transform: translate(100px); */
+/* transform: translate3d(100px, 100px 100px); */
+
+/* 
+	transform: translatex()
+	translatez() 
+*/
+transform: rotatey(90deg) translatez(100px);
+transform: translatez(100px) rotatey(90deg);
+
/* transform: translate(100px); */
+/* transform: translate3d(100px, 100px 100px); */
+
+/* 
+	transform: translatex()
+	translatez() 
+*/
+transform: rotatey(90deg) translatez(100px);
+transform: translatez(100px) rotatey(90deg);
+

小应用:calc(50% - 0.5*宽高),不知道宽高:

css
left: 50%;
+transform: translatex(-50%) 半个身位;
+
left: 50%;
+transform: translatex(-50%) 半个身位;
+

关于 translatez()

css
body {
+  perspective: 800px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*transform: translatez(100px) rotatey(90deg);*/
+  /*z没起作用?旋转晚了*/
+  transform: rotatey(90deg) translatez(100px);
+}
+
body {
+  perspective: 800px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*transform: translatez(100px) rotatey(90deg);*/
+  /*z没起作用?旋转晚了*/
+  transform: rotatey(90deg) translatez(100px);
+}
+

demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root {
+        height: 100%;
+      }
+
+      body {
+        perspective: 800px;
+        height: 100%; //需要有高度才能实现鼠标移动到哪里都能触发事件
+      }
+
+      .content1,
+      .content2,
+      .content3,
+      .content4,
+      .content5 {
+        width: 200px;
+        height: 200px;
+        background-image: url(./source/pic3.jpeg);
+        background-size: cover;
+        position: absolute;
+        top: 200px;
+        transform: rotateY(45deg);
+      }
+
+      .content1 {
+        left: 200px;
+      }
+
+      .content2 {
+        left: 400px;
+      }
+
+      .content3 {
+        left: 600px;
+      }
+
+      .content4 {
+        left: 800px;
+      }
+
+      .content5 {
+        left: 1000px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="content1"></div>
+    <div class="content2"></div>
+    <div class="content3"></div>
+    <div class="content4"></div>
+    <div class="content5"></div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root {
+        height: 100%;
+      }
+
+      body {
+        perspective: 800px;
+        height: 100%; //需要有高度才能实现鼠标移动到哪里都能触发事件
+      }
+
+      .content1,
+      .content2,
+      .content3,
+      .content4,
+      .content5 {
+        width: 200px;
+        height: 200px;
+        background-image: url(./source/pic3.jpeg);
+        background-size: cover;
+        position: absolute;
+        top: 200px;
+        transform: rotateY(45deg);
+      }
+
+      .content1 {
+        left: 200px;
+      }
+
+      .content2 {
+        left: 400px;
+      }
+
+      .content3 {
+        left: 600px;
+      }
+
+      .content4 {
+        left: 800px;
+      }
+
+      .content5 {
+        left: 1000px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="content1"></div>
+    <div class="content2"></div>
+    <div class="content3"></div>
+    <div class="content4"></div>
+    <div class="content5"></div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+

与 perspective 类似的一个东西: transform: perspective(800px) rotateY(45deg); 写在前面并且元素本身,眼睛就在 center 不能调 perspective 在父级才有效 景深可以叠加 深入理解 perspevtive

css
body {
+  perspective: 800px; /*移动眼睛*/
+  perspective-origin: 300px 300px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*移动物体*/
+  /*transform: translatez(100px);*/
+  /*快接近800就见不到了,快后脑勺了*/
+  transform: translatez(100px);
+}
+
body {
+  perspective: 800px; /*移动眼睛*/
+  perspective-origin: 300px 300px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*移动物体*/
+  /*transform: translatez(100px);*/
+  /*快接近800就见不到了,快后脑勺了*/
+  transform: translatez(100px);
+}
+

深入理解 perspective

transform-style

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+      }
+
+      .wrapper {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-color: red;
+        transform: rotateY(0deg);
+        /*父级旋转会带儿子旋转,因为浏览器渲染不了,所以z轴体现不出来,想立体,给父级(不能祖父)加上transform-style: preserve-3d;*/
+      }
+
+      .demo {
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        transform: translateZ(100px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="demo"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+      }
+
+      .wrapper {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-color: red;
+        transform: rotateY(0deg);
+        /*父级旋转会带儿子旋转,因为浏览器渲染不了,所以z轴体现不出来,想立体,给父级(不能祖父)加上transform-style: preserve-3d;*/
+      }
+
+      .demo {
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        transform: translateZ(100px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="demo"></div>
+    </div>
+  </body>
+</html>
+

transform-origin

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+        transform-style: preserve-3d;
+      }
+
+      @keyframes move {
+        0% {
+          transform: rotateY(0deg);
+        }
+        100% {
+          transform: rotateY(360deg);
+        }
+      }
+      div {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        animation: move 2s linear infinite;
+        transform-origin: 100px 100px 100px;
+        /* 可以设置空间点中心旋转 */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div></div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+        transform-style: preserve-3d;
+      }
+
+      @keyframes move {
+        0% {
+          transform: rotateY(0deg);
+        }
+        100% {
+          transform: rotateY(360deg);
+        }
+      }
+      div {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        animation: move 2s linear infinite;
+        transform-origin: 100px 100px 100px;
+        /* 可以设置空间点中心旋转 */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div></div>
+  </body>
+</html>
+

照片墙

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root,
+      body {
+        height: 100%;
+      }
+
+      body {
+        perspective: 3000px;
+        transform-style: preserve-3d;
+        /* 一旦设置了这两个属性当中一条,他就变成了定位的参照物元素,所以这里没高度的话就导致没高度了 */
+      }
+
+      @keyframes round {
+        /*让父级转    简单*/
+        0% {
+          transform: translate(-50%, -50%) rotateY(0deg);
+        }
+
+        100% {
+          transform: translate(-50%, -50%) rotateY(360deg);
+        }
+      }
+
+      div.wrapper {
+        position: absolute;
+        left: calc(50%);
+        top: calc(50%);
+        /* 写了top没生效的重要原因:父级没有高度,因为body,解决就把body上面 height打开 */
+        width: 300px;
+        height: 300px;
+        transform: translate(-50%, -50%);
+        transform-style: preserve-3d;
+        animation: round 5s linear infinite;
+      }
+
+      img {
+        position: absolute;
+        width: 300px;
+        /* backface-visibility: hidden; */
+        /* 图片背部 */
+      }
+
+      img:nth-of-type(1) {
+        transform: rotateY(45deg) translatez(800px);
+        /*沿着自己的z轴向外拓*/
+      }
+
+      img:nth-of-type(2) {
+        transform: rotateY(90deg) translatez(500px);
+        /* 可以改变y值,实现层叠 */
+      }
+
+      img:nth-of-type(3) {
+        transform: rotateY(135deg) translatez(500px);
+      }
+
+      img:nth-of-type(4) {
+        transform: rotateY(180deg) translatez(500px);
+      }
+
+      img:nth-of-type(5) {
+        transform: rotateY(225deg) translatez(500px);
+      }
+
+      img:nth-of-type(6) {
+        transform: rotateY(270deg) translatez(500px);
+      }
+
+      img:nth-of-type(7) {
+        transform: rotateY(315deg) translatez(500px);
+      }
+
+      img:nth-of-type(8) {
+        transform: rotateY(360deg) translatez(500px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+    </div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root,
+      body {
+        height: 100%;
+      }
+
+      body {
+        perspective: 3000px;
+        transform-style: preserve-3d;
+        /* 一旦设置了这两个属性当中一条,他就变成了定位的参照物元素,所以这里没高度的话就导致没高度了 */
+      }
+
+      @keyframes round {
+        /*让父级转    简单*/
+        0% {
+          transform: translate(-50%, -50%) rotateY(0deg);
+        }
+
+        100% {
+          transform: translate(-50%, -50%) rotateY(360deg);
+        }
+      }
+
+      div.wrapper {
+        position: absolute;
+        left: calc(50%);
+        top: calc(50%);
+        /* 写了top没生效的重要原因:父级没有高度,因为body,解决就把body上面 height打开 */
+        width: 300px;
+        height: 300px;
+        transform: translate(-50%, -50%);
+        transform-style: preserve-3d;
+        animation: round 5s linear infinite;
+      }
+
+      img {
+        position: absolute;
+        width: 300px;
+        /* backface-visibility: hidden; */
+        /* 图片背部 */
+      }
+
+      img:nth-of-type(1) {
+        transform: rotateY(45deg) translatez(800px);
+        /*沿着自己的z轴向外拓*/
+      }
+
+      img:nth-of-type(2) {
+        transform: rotateY(90deg) translatez(500px);
+        /* 可以改变y值,实现层叠 */
+      }
+
+      img:nth-of-type(3) {
+        transform: rotateY(135deg) translatez(500px);
+      }
+
+      img:nth-of-type(4) {
+        transform: rotateY(180deg) translatez(500px);
+      }
+
+      img:nth-of-type(5) {
+        transform: rotateY(225deg) translatez(500px);
+      }
+
+      img:nth-of-type(6) {
+        transform: rotateY(270deg) translatez(500px);
+      }
+
+      img:nth-of-type(7) {
+        transform: rotateY(315deg) translatez(500px);
+      }
+
+      img:nth-of-type(8) {
+        transform: rotateY(360deg) translatez(500px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+    </div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+

作业:3D 魔方 知识点是一样的

9.matrix

矩阵就是 transform 给咱们选中的计算规则 矩阵函数传的参数是矩阵的前两行

css
平移// translate
+| 1 0 e|			|x|			|x + e|
+| 0 1 f|   *  |y| =   |y + f|
+| 0 0 1|  		|z|			|1    |
+matrix(1,0,0,1,e,f); === translate(x, y);
+// scale
+| a,0,0 |     | x |      | ax |
+| 0,d,0 |  *  | y |   =  | dy |
+| 0,0,1 |     | 1 |      | 1  |
+matrix(a,0,0,d,0,0); === scale(x, y);
+// rotate
+matrix(cos(θ),sin(θ),-sin(θ),cos(θ),0,0); === rotate(θ);
+| cos(θ),-sin(θ),e |     | x |
+| sin(θ),cos(θ) ,f |  *  | y |
+| 0     ,0      ,1 |     | 1 |
+x1 = cos(θ)x - sin(θ)y + 0
+y2 = sin(θ)x + cos(θ)y + 0
+matrix(1,tan(θy),tan(θx),1,0,0)
+
+
+matrix(1,0,0,0,0,1,0,0,0,0,1,0,x,y,z,1) 缩放
+matrix(x,0,0,0,0,y,0,0,0,0,z,0,0,0,0,1) 平移
+
平移// translate
+| 1 0 e|			|x|			|x + e|
+| 0 1 f|   *  |y| =   |y + f|
+| 0 0 1|  		|z|			|1    |
+matrix(1,0,0,1,e,f); === translate(x, y);
+// scale
+| a,0,0 |     | x |      | ax |
+| 0,d,0 |  *  | y |   =  | dy |
+| 0,0,1 |     | 1 |      | 1  |
+matrix(a,0,0,d,0,0); === scale(x, y);
+// rotate
+matrix(cos(θ),sin(θ),-sin(θ),cos(θ),0,0); === rotate(θ);
+| cos(θ),-sin(θ),e |     | x |
+| sin(θ),cos(θ) ,f |  *  | y |
+| 0     ,0      ,1 |     | 1 |
+x1 = cos(θ)x - sin(θ)y + 0
+y2 = sin(θ)x + cos(θ)y + 0
+matrix(1,tan(θy),tan(θx),1,0,0)
+
+
+matrix(1,0,0,0,0,1,0,0,0,0,1,0,x,y,z,1) 缩放
+matrix(x,0,0,0,0,y,0,0,0,0,z,0,0,0,0,1) 平移
+

利用矩阵实现镜像:核心是符号取反

css
div{
+    width:200px;
+    height:200px;
+    background-image:url(source/pic3.jpeg);
+    background-size: cover;
+    transform:matrix(-1,0,0,1,0,0);
+}
+|-1,0, 0 |     	| x |      | -x |  x取反,反推第一个矩阵表达式
+| 0, 1, 0 |  *  | y |   =  | -y |
+| 0, 0, 1 |     | 1 |      | 1  |
+
div{
+    width:200px;
+    height:200px;
+    background-image:url(source/pic3.jpeg);
+    background-size: cover;
+    transform:matrix(-1,0,0,1,0,0);
+}
+|-1,0, 0 |     	| x |      | -x |  x取反,反推第一个矩阵表达式
+| 0, 1, 0 |  *  | y |   =  | -y |
+| 0, 0, 1 |     | 1 |      | 1  |
+

浏览器渲染过程:

html
download html download css download js css rules tree(construct) domAPI domTree
+cssrulestree cssomAPI 最终cssomTree domtree cssomTree renderTree | | layout布局
+---- > paint喷色 (reflow重构) (repaint) 逻辑图(多层矢量图) ----->
+实际绘制(栅格化) 不设置就用cpu绘制 google chrome 自动调用 gpu
+
download html download css download js css rules tree(construct) domAPI domTree
+cssrulestree cssomAPI 最终cssomTree domtree cssomTree renderTree | | layout布局
+---- > paint喷色 (reflow重构) (repaint) 逻辑图(多层矢量图) ----->
+实际绘制(栅格化) 不设置就用cpu绘制 google chrome 自动调用 gpu
+
`,70),r=[c];function t(B,y,F,i,A,b){return n(),a("div",null,r)}const d=s(e,[["render",t]]);export{m as __pageData,d as default}; diff --git a/assets/html-css_animation.md.da2d65b2.lean.js b/assets/html-css_animation.md.da2d65b2.lean.js new file mode 100644 index 00000000..dc81a7e4 --- /dev/null +++ b/assets/html-css_animation.md.da2d65b2.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-21-04-28.4121323f.png",o="/blog/assets/2023-02-01-21-04-38.6dfd96aa.png",m=JSON.parse('{"title":"动画","description":"","frontmatter":{},"headers":[{"level":2,"title":"1.transition 过渡动画","slug":"_1-transition-过渡动画","link":"#_1-transition-过渡动画","children":[]},{"level":2,"title":"2.cubic-bezier","slug":"_2-cubic-bezier","link":"#_2-cubic-bezier","children":[]},{"level":2,"title":"3.animation","slug":"_3-animation","link":"#_3-animation","children":[]},{"level":2,"title":"4.step 跳转动画","slug":"_4-step-跳转动画","link":"#_4-step-跳转动画","children":[]},{"level":2,"title":"5.rotate3D 变换","slug":"_5-rotate3d-变换","link":"#_5-rotate3d-变换","children":[]},{"level":2,"title":"6.scale 伸缩","slug":"_6-scale-伸缩","link":"#_6-scale-伸缩","children":[]},{"level":2,"title":"7.skew 倾斜","slug":"_7-skew-倾斜","link":"#_7-skew-倾斜","children":[]},{"level":2,"title":"8.translate+perspective","slug":"_8-translate-perspective","link":"#_8-translate-perspective","children":[]},{"level":2,"title":"9.matrix","slug":"_9-matrix","link":"#_9-matrix","children":[]}],"relativePath":"html-css/animation.md","lastUpdated":1675259332000}'),e={name:"html-css/animation.md"},c=l("",70),r=[c];function t(B,y,F,i,A,b){return n(),a("div",null,r)}const d=s(e,[["render",t]]);export{m as __pageData,d as default}; diff --git a/assets/html-css_canvas-svg.md.38b21f10.js b/assets/html-css_canvas-svg.md.38b21f10.js new file mode 100644 index 00000000..ac4292aa --- /dev/null +++ b/assets/html-css_canvas-svg.md.38b21f10.js @@ -0,0 +1,797 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"canvas 和 svg","description":"","frontmatter":{},"headers":[{"level":2,"title":"canvas 画线","slug":"canvas-画线","link":"#canvas-画线","children":[]},{"level":2,"title":"canvas 画矩形","slug":"canvas-画矩形","link":"#canvas-画矩形","children":[]},{"level":2,"title":"小方块下落","slug":"小方块下落","link":"#小方块下落","children":[]},{"level":2,"title":"canvas 画圆","slug":"canvas-画圆","link":"#canvas-画圆","children":[]},{"level":2,"title":"画圆角矩形","slug":"画圆角矩形","link":"#画圆角矩形","children":[]},{"level":2,"title":"canvas 贝塞尔曲线","slug":"canvas-贝塞尔曲线","link":"#canvas-贝塞尔曲线","children":[]},{"level":2,"title":"坐标平移旋转与缩放","slug":"坐标平移旋转与缩放","link":"#坐标平移旋转与缩放","children":[]},{"level":2,"title":"canvas 的 save 和 restore","slug":"canvas-的-save-和-restore","link":"#canvas-的-save-和-restore","children":[]},{"level":2,"title":"canvas 背景填充","slug":"canvas-背景填充","link":"#canvas-背景填充","children":[]},{"level":2,"title":"线性渐变","slug":"线性渐变","link":"#线性渐变","children":[]},{"level":2,"title":"canvas 辐射渐变","slug":"canvas-辐射渐变","link":"#canvas-辐射渐变","children":[]},{"level":2,"title":"canvas 阴影","slug":"canvas-阴影","link":"#canvas-阴影","children":[]},{"level":2,"title":"canvas 渲染文字","slug":"canvas-渲染文字","link":"#canvas-渲染文字","children":[]},{"level":2,"title":"canvas 线端样式","slug":"canvas-线端样式","link":"#canvas-线端样式","children":[]},{"level":2,"title":"SVG 画线与矩形","slug":"svg-画线与矩形","link":"#svg-画线与矩形","children":[]},{"level":2,"title":"svg 画圈,椭圆,直线","slug":"svg-画圈-椭圆-直线","link":"#svg-画圈-椭圆-直线","children":[]},{"level":2,"title":"svg 画多边形和文本","slug":"svg-画多边形和文本","link":"#svg-画多边形和文本","children":[]},{"level":2,"title":"SVG 透明度与线条样式","slug":"svg-透明度与线条样式","link":"#svg-透明度与线条样式","children":[]},{"level":2,"title":"SVG 的 path 标签","slug":"svg-的-path-标签","link":"#svg-的-path-标签","children":[]},{"level":2,"title":"path 画弧","slug":"path-画弧","link":"#path-画弧","children":[]},{"level":2,"title":"svg 线性渐变","slug":"svg-线性渐变","link":"#svg-线性渐变","children":[]},{"level":2,"title":"svg 高斯模糊","slug":"svg-高斯模糊","link":"#svg-高斯模糊","children":[]},{"level":2,"title":"SVG 虚线及简单动画","slug":"svg-虚线及简单动画","link":"#svg-虚线及简单动画","children":[]},{"level":2,"title":"svg 的 viewbox(比例尺)","slug":"svg-的-viewbox-比例尺","link":"#svg-的-viewbox-比例尺","children":[]}],"relativePath":"html-css/canvas-svg.md","lastUpdated":1675256330000}'),p={name:"html-css/canvas-svg.md"},o=l(`

canvas 和 svg

canvas 画线

html
<canvas id="can" width="500px" height="300px"></canvas>
+
<canvas id="can" width="500px" height="300px"></canvas>
+

注意:只能在行间样式设置大小,不能通过 css

javascript
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+
javascript
ctx.moveTo(100, 100); //起点
+ctx.lineTo(200, 100); //终点
+ctx.stroke(); //画上去
+ctx.closePath(); // 连续线,形成闭合
+ctx.fill(); //填充
+ctx.lineWidth = 10; // 设置线的粗细   写在哪,都相当于写在moveto的后面????咋不管用
+
ctx.moveTo(100, 100); //起点
+ctx.lineTo(200, 100); //终点
+ctx.stroke(); //画上去
+ctx.closePath(); // 连续线,形成闭合
+ctx.fill(); //填充
+ctx.lineWidth = 10; // 设置线的粗细   写在哪,都相当于写在moveto的后面????咋不管用
+

想实现一个细,一个粗

一个图形,一笔画出来的,只能一个粗细,想实现,必须开启新图像

javascript
ctx.beginPath();
+
ctx.beginPath();
+

closePath()是图形闭合,不是一个图,不能闭合

canvas 画矩形

javascript
ctx.rect(100, 100, 150, 100);
+ctx.stroke();
+ctx.fill();
+
ctx.rect(100, 100, 150, 100);
+ctx.stroke();
+ctx.fill();
+

简化

javascript
ctx.strokeRect(100, 100, 200, 100); //矩形
+ctx.fillRect(100, 100, 200, 100); //填充矩形
+
ctx.strokeRect(100, 100, 200, 100); //矩形
+ctx.fillRect(100, 100, 200, 100); //填充矩形
+

小方块下落

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      var height = 100;
+      var timer = setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300); //橡皮擦功能,清屏
+        ctx.strokeRect(100, height, 50, 50);
+        height += 5; //每次画的新的,但是旧的没删除,所以要加上清屏
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      var height = 100;
+      var timer = setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300); //橡皮擦功能,清屏
+        ctx.strokeRect(100, height, 50, 50);
+        height += 5; //每次画的新的,但是旧的没删除,所以要加上清屏
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+

作业:自由落体

canvas 画圆

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // 圆心(x,y),半径(r),弧度(起始弧度,结束弧度),方向
+  ctx.arc(100, 100, 50, 0, Math.PI * 1.8, 0); //顺时针0;逆时针1
+  ctx.lineTo(100, 100);
+  ctx.closePath();
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // 圆心(x,y),半径(r),弧度(起始弧度,结束弧度),方向
+  ctx.arc(100, 100, 50, 0, Math.PI * 1.8, 0); //顺时针0;逆时针1
+  ctx.lineTo(100, 100);
+  ctx.closePath();
+  ctx.stroke();
+</script>
+

画圆角矩形

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // ABC为矩形端点
+  // B(x,y),C(x,y),圆角大小(相当于border-radius)
+  ctx.moveTo(100, 110);
+  ctx.arcTo(100, 200, 200, 200, 10);
+  ctx.arcTo(200, 200, 200, 100, 10);
+  ctx.arcTo(200, 100, 100, 100, 10);
+  ctx.arcTo(100, 100, 100, 200, 10);
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // ABC为矩形端点
+  // B(x,y),C(x,y),圆角大小(相当于border-radius)
+  ctx.moveTo(100, 110);
+  ctx.arcTo(100, 200, 200, 200, 10);
+  ctx.arcTo(200, 200, 200, 100, 10);
+  ctx.arcTo(200, 100, 100, 100, 10);
+  ctx.arcTo(100, 100, 100, 200, 10);
+  ctx.stroke();
+</script>
+

canvas 贝塞尔曲线

贝塞尔曲线

javascript
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+ctx.beginPath();
+ctx.moveTo(100, 100);
+// ctx.quadraticCurveTo(200, 200, 300, 100);二次
+// ctx.quadraticCurveTo(200, 200, 300, 100, 400 200); 三次
+ctx.stroke();
+
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+ctx.beginPath();
+ctx.moveTo(100, 100);
+// ctx.quadraticCurveTo(200, 200, 300, 100);二次
+// ctx.quadraticCurveTo(200, 200, 300, 100, 400 200); 三次
+ctx.stroke();
+

波浪

注意:初始化

html
ctx.beginPath();
+
ctx.beginPath();
+

波浪 demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var width = 500;
+      var height = 300;
+      var offset = 0;
+      var num = 0;
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300);
+        ctx.beginPath();
+        ctx.moveTo(0 + offset - 500, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset - 500,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset - 500,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset - 500,
+          height / 2 - Math.sin(num) * 120,
+          width + offset - 500,
+          height / 2
+        );
+        // 整体向左平移整个宽度形成完整的衔接
+        ctx.moveTo(0 + offset, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset,
+          height / 2 - Math.sin(num) * 120,
+          width + offset,
+          height / 2
+        );
+        ctx.stroke();
+        offset += 5;
+        offset %= 500;
+        num += 0.02;
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var width = 500;
+      var height = 300;
+      var offset = 0;
+      var num = 0;
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300);
+        ctx.beginPath();
+        ctx.moveTo(0 + offset - 500, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset - 500,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset - 500,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset - 500,
+          height / 2 - Math.sin(num) * 120,
+          width + offset - 500,
+          height / 2
+        );
+        // 整体向左平移整个宽度形成完整的衔接
+        ctx.moveTo(0 + offset, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset,
+          height / 2 - Math.sin(num) * 120,
+          width + offset,
+          height / 2
+        );
+        ctx.stroke();
+        offset += 5;
+        offset %= 500;
+        num += 0.02;
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+

坐标平移旋转与缩放

旋转平移

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.rotate(Math.PI / 6); //根据画布的原点进行旋转
+  // 要想不根据画布原点,则translate坐标系平移
+  // ctx.translate(100, 100);坐标原点在(100,100),此时配套旋转
+  ctx.moveTo(0, 0);
+  ctx.lineTo(100, 100);
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.rotate(Math.PI / 6); //根据画布的原点进行旋转
+  // 要想不根据画布原点,则translate坐标系平移
+  // ctx.translate(100, 100);坐标原点在(100,100),此时配套旋转
+  ctx.moveTo(0, 0);
+  ctx.lineTo(100, 100);
+  ctx.stroke();
+</script>
+

缩放

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.scale(2, 2); //x乘以他的系数,y乘以他的系数
+  ctx.strokeRect(100, 100, 100, 100);
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.scale(2, 2); //x乘以他的系数,y乘以他的系数
+  ctx.strokeRect(100, 100, 100, 100);
+</script>
+

canvas 的 save 和 restore

不想让其他的受到之前设置的影响

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.save(); //保存坐标系的平移数据,缩放数据,旋转数据
+  ctx.beginPath();
+  ctx.translate(100, 100);
+  ctx.rotate(Math.PI / 4);
+  ctx.strokeRect(0, 0, 100, 50);
+  ctx.beginPath();
+  ctx.restore(); //一旦restore,就恢复save时候的状态
+  ctx.fillRect(100, 0, 100, 50);
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.save(); //保存坐标系的平移数据,缩放数据,旋转数据
+  ctx.beginPath();
+  ctx.translate(100, 100);
+  ctx.rotate(Math.PI / 4);
+  ctx.strokeRect(0, 0, 100, 50);
+  ctx.beginPath();
+  ctx.restore(); //一旦restore,就恢复save时候的状态
+  ctx.fillRect(100, 0, 100, 50);
+</script>
+

canvas 背景填充

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  var img = new Image();
+  img.src = "file:///C:/Users/f1981/Desktop/source/pic3.jpeg";
+  img.onload = function () {
+    //因为图片异步加载
+    ctx.beginPath();
+    ctx.translate(100, 100); //改变坐标系的位置
+    var bg = ctx.createPattern(img, "no-repeat");
+    // 图片填充,是以坐标系原点开始填充的
+    // ctx.fillStyle = "blue";
+    ctx.fillStyle = "bg";
+    ctx.fillRect(0, 0, 200, 100);
+  };
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  var img = new Image();
+  img.src = "file:///C:/Users/f1981/Desktop/source/pic3.jpeg";
+  img.onload = function () {
+    //因为图片异步加载
+    ctx.beginPath();
+    ctx.translate(100, 100); //改变坐标系的位置
+    var bg = ctx.createPattern(img, "no-repeat");
+    // 图片填充,是以坐标系原点开始填充的
+    // ctx.fillStyle = "blue";
+    ctx.fillStyle = "bg";
+    ctx.fillRect(0, 0, 200, 100);
+  };
+</script>
+

图片疑难问题 探索 open in live server 只能打开同目录下的 img

线性渐变

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  var bg = ctx.createLinearGradient(0, 0, 200, 200);
+  bg.addColorStop(0, "white"); //数字为0-1之间
+  // bg.addColorStop(0.5, "blue");
+  bg.addColorStop(1, "black");
+  ctx.fillStyle = bg;
+  ctx.translate(100, 100); //起始点依旧是坐标系原点
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  var bg = ctx.createLinearGradient(0, 0, 200, 200);
+  bg.addColorStop(0, "white"); //数字为0-1之间
+  // bg.addColorStop(0.5, "blue");
+  bg.addColorStop(1, "black");
+  ctx.fillStyle = bg;
+  ctx.translate(100, 100); //起始点依旧是坐标系原点
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+

canvas 辐射渐变

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  // var bg = ctx.createRadialGradient(x1,y1,r1,x2,y2,r2);起始圆,结束圆
+  var bg = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
+  // var bg = ctx.createRadialGradient(100, 100, 100, 100, 100, 100);起始圆里面的颜色全是开始的颜色
+  bg.addColorStop(0, "red");
+  bg.addColorStop(0.5, "green");
+  bg.addColorStop(1, "blue");
+  ctx.fillStyle = bg;
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  // var bg = ctx.createRadialGradient(x1,y1,r1,x2,y2,r2);起始圆,结束圆
+  var bg = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
+  // var bg = ctx.createRadialGradient(100, 100, 100, 100, 100, 100);起始圆里面的颜色全是开始的颜色
+  bg.addColorStop(0, "red");
+  bg.addColorStop(0.5, "green");
+  bg.addColorStop(1, "blue");
+  ctx.fillStyle = bg;
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+

canvas 阴影

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.shadowColor = "blue";
+  ctx.shadowBlur = 20;
+  ctx.shadowOffsetX = 15;
+  ctx.shadowOffsetY = 15;
+  ctx.strokeRect(0, 0, 200, 200);
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.shadowColor = "blue";
+  ctx.shadowBlur = 20;
+  ctx.shadowOffsetX = 15;
+  ctx.shadowOffsetY = 15;
+  ctx.strokeRect(0, 0, 200, 200);
+</script>
+

canvas 渲染文字

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.strokeRect(0, 0, 200, 200);
+
+  ctx.fillStyle = "red";
+  ctx.font = "30px Georgia"; //对stroke和fill都起作用
+  ctx.strokeText("panda", 200, 100); //文字描边
+  ctx.fillText("monkey", 200, 250); //文字填充
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.strokeRect(0, 0, 200, 200);
+
+  ctx.fillStyle = "red";
+  ctx.font = "30px Georgia"; //对stroke和fill都起作用
+  ctx.strokeText("panda", 200, 100); //文字描边
+  ctx.fillText("monkey", 200, 250); //文字填充
+</script>
+

canvas 线端样式

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.lineWidth = 15;
+  ctx.moveTo(100, 100);
+  ctx.lineTo(200, 100);
+  ctx.lineTo(100, 130);
+  ctx.lineCap = "square"; //butt round
+  ctx.lineJoin = "miter"; //线接触时候// round bevel miter(miterLimit)
+  ctx.miterLimit = 5;
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.lineWidth = 15;
+  ctx.moveTo(100, 100);
+  ctx.lineTo(200, 100);
+  ctx.lineTo(100, 130);
+  ctx.lineCap = "square"; //butt round
+  ctx.lineJoin = "miter"; //线接触时候// round bevel miter(miterLimit)
+  ctx.miterLimit = 5;
+  ctx.stroke();
+</script>
+

SVG 画线与矩形

svg 滤镜 https://www.runoob.com/svg/svg-fegaussianblur.html h5 考试题最后一题

svg 与 canvas 区别

svg:矢量图,放大不会失真,适合大面积的贴图,通常动画较少或者较简单,标签和 css 画

Canvas:适合用于小面积绘图,适合动画,js 画

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 3px;
+      }
+
+      .line2 {
+        stroke: red;
+        stroke-width: 5px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+      <line x1="200" y1="100" x2="200" y2="200" class="line2"></line>
+      <rect height="50" width="100" x="0" y="0"></rect>
+      <!-- 所有闭合的图形,在svg中,默认都是天生充满并且画出来的 -->
+      <rect height="50" width="100" x="0" y="0" rx="10" ry="20"></rect>
+    </svg>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 3px;
+      }
+
+      .line2 {
+        stroke: red;
+        stroke-width: 5px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+      <line x1="200" y1="100" x2="200" y2="200" class="line2"></line>
+      <rect height="50" width="100" x="0" y="0"></rect>
+      <!-- 所有闭合的图形,在svg中,默认都是天生充满并且画出来的 -->
+      <rect height="50" width="100" x="0" y="0" rx="10" ry="20"></rect>
+    </svg>
+  </body>
+</html>
+

svg 画圈,椭圆,直线

css
<style>
+polyline {
+    fill: transparent;
+    stroke: blueviolet;
+    stroke-width: 3px;
+}
+</style>
+
<style>
+polyline {
+    fill: transparent;
+    stroke: blueviolet;
+    stroke-width: 3px;
+}
+</style>
+
html
<svg width="500px" height="300px" style="border:1px solid">
+  <circle r="50" cx="50" cy="220"></circle>
+  <!--圆-->
+  <ellipse rx="100" ry="30" cx="400" cy="200"></ellipse>
+  <!-- 椭圆 -->
+  <polyline points="0 0, 50 50, 50 100,100 100,100 50"></polyline>
+  <!-- 曲线:默认填充 -->
+</svg>
+
<svg width="500px" height="300px" style="border:1px solid">
+  <circle r="50" cx="50" cy="220"></circle>
+  <!--圆-->
+  <ellipse rx="100" ry="30" cx="400" cy="200"></ellipse>
+  <!-- 椭圆 -->
+  <polyline points="0 0, 50 50, 50 100,100 100,100 50"></polyline>
+  <!-- 曲线:默认填充 -->
+</svg>
+

svg 画多边形和文本

html
<polygon points="0 0, 50 50, 50 100,100 100,100 50"> </polygon
+><!-- 与polylkine区别:多边形会自动首位相连 -->
+<text x="300" y="50">邓哥身体好</text>
+
<polygon points="0 0, 50 50, 50 100,100 100,100 50"> </polygon
+><!-- 与polylkine区别:多边形会自动首位相连 -->
+<text x="300" y="50">邓哥身体好</text>
+
css
polygon {
+  fill: transparent;
+  stroke: black;
+  stroke-width: 3px;
+}
+
+text {
+  stroke: blue;
+  stroke-width: 3px;
+}
+
polygon {
+  fill: transparent;
+  stroke: black;
+  stroke-width: 3px;
+}
+
+text {
+  stroke: blue;
+  stroke-width: 3px;
+}
+

SVG 透明度与线条样式

透明

css
stroke-opacity: 0.5; /* 边框半透明  */
+fill-opacity: 0.3; /* 填充半透明 */
+
stroke-opacity: 0.5; /* 边框半透明  */
+fill-opacity: 0.3; /* 填充半透明 */
+

线条样式

css
stroke-linecap: butt; /* (帽子)round square 都是额外加的长度*/
+
stroke-linecap: butt; /* (帽子)round square 都是额外加的长度*/
+
css
stroke-linejoin: ; /* 两个线相交的时候 bevel round miter */
+
stroke-linejoin: ; /* 两个线相交的时候 bevel round miter */
+

SVG 的 path 标签

html
<path d="M 100 100 L 200 100"></path>
+<path d="M 100 100 L 200 100 L 200 200"></path
+><!-- 默认有填充 -->
+<path d="M 100 100 L 200 100 l 100 100"></path>
+<!-- 以上:大写字母代表绝对位置,小写字母表示相对位置 -->
+<path d="M 100 100 H 200 V 200"></path
+><!-- H水平  V竖直 -->
+<path d="M 100 100 H 200 V 200 z"></path
+><!-- z表示闭合区间,不区分大小写 -->
+
<path d="M 100 100 L 200 100"></path>
+<path d="M 100 100 L 200 100 L 200 200"></path
+><!-- 默认有填充 -->
+<path d="M 100 100 L 200 100 l 100 100"></path>
+<!-- 以上:大写字母代表绝对位置,小写字母表示相对位置 -->
+<path d="M 100 100 H 200 V 200"></path
+><!-- H水平  V竖直 -->
+<path d="M 100 100 H 200 V 200 z"></path
+><!-- z表示闭合区间,不区分大小写 -->
+
css
path {
+  stroke: red;
+  fill: transparent;
+}
+
path {
+  stroke: red;
+  fill: transparent;
+}
+

path 画弧

html
<path d="M 100 100 A 100 50 0 1 1 150 200"></path>
+<!-- A代表圆弧指令,以M100 100为起点,150 200为终点 ,半径100,短半径50 ,旋转角度为0,1大圆弧,1顺时针 -->
+
<path d="M 100 100 A 100 50 0 1 1 150 200"></path>
+<!-- A代表圆弧指令,以M100 100为起点,150 200为终点 ,半径100,短半径50 ,旋转角度为0,1大圆弧,1顺时针 -->
+

svg 线性渐变

html
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+  </defs>
+  <rect x="100" y="100" height="100" width="200" style="fill:url(#bg1)"></rect>
+</svg>
+
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+  </defs>
+  <rect x="100" y="100" height="100" width="200" style="fill:url(#bg1)"></rect>
+</svg>
+

svg 高斯模糊

html
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+    <filter id="Gaussian">
+      <feGaussianBlur in="SourceGraphic" stdDeviation="20"></feGaussianBlur>
+    </filter>
+  </defs>
+  <rect
+    x="100"
+    y="100"
+    height="100"
+    width="200"
+    style="fill:url(#bg1);filter:url(#Gaussian)"
+  ></rect>
+</svg>
+
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+    <filter id="Gaussian">
+      <feGaussianBlur in="SourceGraphic" stdDeviation="20"></feGaussianBlur>
+    </filter>
+  </defs>
+  <rect
+    x="100"
+    y="100"
+    height="100"
+    width="200"
+    style="fill:url(#bg1);filter:url(#Gaussian)"
+  ></rect>
+</svg>
+

SVG 虚线及简单动画

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 10px;
+        /* stroke-dasharray: 10px; */
+        /* stroke-dasharray: 10px 20px;1,2,3,依次取两个值(数组) */
+        /* stroke-dasharray: 10px 20px 30px;1,2,3,依次取两个值 */
+        /* stroke-dashoffset: 10px;偏移 */
+        /* stroke-dashoffset: 200px--->0;可以实现谈满又情空 */
+        stroke-dashoffset: 200px;
+        animation: move 2s linear infinite alternate-reverse;
+      }
+
+      @keyframes move {
+        0% {
+          stroke-dashoffset: 200px;
+        }
+
+        100% {
+          stroke-dashoffset: 0px;
+        }
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+    </svg>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 10px;
+        /* stroke-dasharray: 10px; */
+        /* stroke-dasharray: 10px 20px;1,2,3,依次取两个值(数组) */
+        /* stroke-dasharray: 10px 20px 30px;1,2,3,依次取两个值 */
+        /* stroke-dashoffset: 10px;偏移 */
+        /* stroke-dashoffset: 200px--->0;可以实现谈满又情空 */
+        stroke-dashoffset: 200px;
+        animation: move 2s linear infinite alternate-reverse;
+      }
+
+      @keyframes move {
+        0% {
+          stroke-dashoffset: 200px;
+        }
+
+        100% {
+          stroke-dashoffset: 0px;
+        }
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+    </svg>
+  </body>
+</html>
+

svg 的 viewbox(比例尺)

html
<svg
+  width="500px"
+  height="300px"
+  viewbox="0,0,250,150"
+  style="border:1px solid"
+>
+  <!-- viewbox是宽高的一半 -->
+  <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+</svg>
+
<svg
+  width="500px"
+  height="300px"
+  viewbox="0,0,250,150"
+  style="border:1px solid"
+>
+  <!-- viewbox是宽高的一半 -->
+  <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+</svg>
+

总结:SVG 开发中不太用

`,82),e=[o];function t(c,B,r,y,F,i){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{u as __pageData,d as default}; diff --git a/assets/html-css_canvas-svg.md.38b21f10.lean.js b/assets/html-css_canvas-svg.md.38b21f10.lean.js new file mode 100644 index 00000000..1ec967c3 --- /dev/null +++ b/assets/html-css_canvas-svg.md.38b21f10.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const u=JSON.parse('{"title":"canvas 和 svg","description":"","frontmatter":{},"headers":[{"level":2,"title":"canvas 画线","slug":"canvas-画线","link":"#canvas-画线","children":[]},{"level":2,"title":"canvas 画矩形","slug":"canvas-画矩形","link":"#canvas-画矩形","children":[]},{"level":2,"title":"小方块下落","slug":"小方块下落","link":"#小方块下落","children":[]},{"level":2,"title":"canvas 画圆","slug":"canvas-画圆","link":"#canvas-画圆","children":[]},{"level":2,"title":"画圆角矩形","slug":"画圆角矩形","link":"#画圆角矩形","children":[]},{"level":2,"title":"canvas 贝塞尔曲线","slug":"canvas-贝塞尔曲线","link":"#canvas-贝塞尔曲线","children":[]},{"level":2,"title":"坐标平移旋转与缩放","slug":"坐标平移旋转与缩放","link":"#坐标平移旋转与缩放","children":[]},{"level":2,"title":"canvas 的 save 和 restore","slug":"canvas-的-save-和-restore","link":"#canvas-的-save-和-restore","children":[]},{"level":2,"title":"canvas 背景填充","slug":"canvas-背景填充","link":"#canvas-背景填充","children":[]},{"level":2,"title":"线性渐变","slug":"线性渐变","link":"#线性渐变","children":[]},{"level":2,"title":"canvas 辐射渐变","slug":"canvas-辐射渐变","link":"#canvas-辐射渐变","children":[]},{"level":2,"title":"canvas 阴影","slug":"canvas-阴影","link":"#canvas-阴影","children":[]},{"level":2,"title":"canvas 渲染文字","slug":"canvas-渲染文字","link":"#canvas-渲染文字","children":[]},{"level":2,"title":"canvas 线端样式","slug":"canvas-线端样式","link":"#canvas-线端样式","children":[]},{"level":2,"title":"SVG 画线与矩形","slug":"svg-画线与矩形","link":"#svg-画线与矩形","children":[]},{"level":2,"title":"svg 画圈,椭圆,直线","slug":"svg-画圈-椭圆-直线","link":"#svg-画圈-椭圆-直线","children":[]},{"level":2,"title":"svg 画多边形和文本","slug":"svg-画多边形和文本","link":"#svg-画多边形和文本","children":[]},{"level":2,"title":"SVG 透明度与线条样式","slug":"svg-透明度与线条样式","link":"#svg-透明度与线条样式","children":[]},{"level":2,"title":"SVG 的 path 标签","slug":"svg-的-path-标签","link":"#svg-的-path-标签","children":[]},{"level":2,"title":"path 画弧","slug":"path-画弧","link":"#path-画弧","children":[]},{"level":2,"title":"svg 线性渐变","slug":"svg-线性渐变","link":"#svg-线性渐变","children":[]},{"level":2,"title":"svg 高斯模糊","slug":"svg-高斯模糊","link":"#svg-高斯模糊","children":[]},{"level":2,"title":"SVG 虚线及简单动画","slug":"svg-虚线及简单动画","link":"#svg-虚线及简单动画","children":[]},{"level":2,"title":"svg 的 viewbox(比例尺)","slug":"svg-的-viewbox-比例尺","link":"#svg-的-viewbox-比例尺","children":[]}],"relativePath":"html-css/canvas-svg.md","lastUpdated":1675256330000}'),p={name:"html-css/canvas-svg.md"},o=l("",82),e=[o];function t(c,B,r,y,F,i){return n(),a("div",null,e)}const d=s(p,[["render",t]]);export{u as __pageData,d as default}; diff --git a/assets/html-css_drag.md.e7a4f3a8.js b/assets/html-css_drag.md.e7a4f3a8.js new file mode 100644 index 00000000..e8dba362 --- /dev/null +++ b/assets/html-css_drag.md.e7a4f3a8.js @@ -0,0 +1,331 @@ +import{_ as n,o as a,c as l,a as s,b as p}from"./app.f983686f.js";const g=JSON.parse('{"title":"拖拽 API","description":"","frontmatter":{},"headers":[{"level":2,"title":"Drag 被拖拽元素","slug":"drag-被拖拽元素","link":"#drag-被拖拽元素","children":[]},{"level":2,"title":"Drag 目标元素(目标区域)","slug":"drag-目标元素-目标区域","link":"#drag-目标元素-目标区域","children":[]},{"level":2,"title":"拖拽 demo","slug":"拖拽-demo","link":"#拖拽-demo","children":[]},{"level":2,"title":"dataTransfer 补充属性","slug":"datatransfer-补充属性","link":"#datatransfer-补充属性","children":[]}],"relativePath":"html-css/drag.md","lastUpdated":1675256330000}'),o={name:"html-css/drag.md"},e=s(`

拖拽 API

Drag 被拖拽元素

html
<div class="a" draggable="true"></div>
+<!-- 谷歌,safari可以,火狐,Ie不支持 -->
+
<div class="a" draggable="true"></div>
+<!-- 谷歌,safari可以,火狐,Ie不支持 -->
+

默认值 false

默认值为 true 的标签(默认带有拖拽功能的标签):a 标签和 img 标签

拖拽生命周期

  1. 拖拽开始,拖拽进行中,拖拽结束

  2. 组成:被拖拽的物体,目标区域 拖拽事件

js
var oDragDiv = document.getElementsByClassName("a")[0];
+oDragDiv.ondragstart = function (e) {
+  console.log(e);
+}; // 按下物体的瞬间不触发事件,只有拖动才会触发开始事件
+oDragDiv.ondrag = function (e) {
+  //移动事件
+  console.log(e);
+};
+oDragDiv.ondragend = function (e) {
+  console.log(e);
+};
+// 从而得出移动多少点
+
var oDragDiv = document.getElementsByClassName("a")[0];
+oDragDiv.ondragstart = function (e) {
+  console.log(e);
+}; // 按下物体的瞬间不触发事件,只有拖动才会触发开始事件
+oDragDiv.ondrag = function (e) {
+  //移动事件
+  console.log(e);
+};
+oDragDiv.ondragend = function (e) {
+  console.log(e);
+};
+// 从而得出移动多少点
+
`,8),t=p("p",{"position:absolute":""},"小功能:.a",-1),c=s(`
html
<div class="a" draggable="true"></div>
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  var beginX = 0;
+  var beginY = 0;
+  oDragDiv.ondragstart = function (e) {
+    beginX = e.clientX;
+    beginY = e.clientY;
+    console.log(e);
+  };
+  oDragDiv.ondragend = function (e) {
+    var x = e.clientX - beginX;
+    var y = e.clientY - beginY;
+    oDragDiv.style.left = oDragDiv.offsetLeft + x + "px";
+    oDragDiv.style.top = oDragDiv.offsetTop + y + "px";
+  };
+</script>
+
<div class="a" draggable="true"></div>
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  var beginX = 0;
+  var beginY = 0;
+  oDragDiv.ondragstart = function (e) {
+    beginX = e.clientX;
+    beginY = e.clientY;
+    console.log(e);
+  };
+  oDragDiv.ondragend = function (e) {
+    var x = e.clientX - beginX;
+    var y = e.clientY - beginY;
+    oDragDiv.style.left = oDragDiv.offsetLeft + x + "px";
+    oDragDiv.style.top = oDragDiv.offsetTop + y + "px";
+  };
+</script>
+

Drag 目标元素(目标区域)

css
.a {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  position: absolute;
+}
+.target {
+  width: 200px;
+  height: 200px;
+  border: 1px solid;
+  position: absolute;
+  left: 600px;
+}
+
.a {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  position: absolute;
+}
+.target {
+  width: 200px;
+  height: 200px;
+  border: 1px solid;
+  position: absolute;
+  left: 600px;
+}
+
html
<div class="a" draggable="true"></div>
+<div class="target"></div>
+
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  oDragDiv.ondragstart = function (e) {};
+  oDragDiv.ondrag = function (e) {
+    //只要移动就不停的触发
+  };
+  oDragDiv.ondragend = function (e) {};
+  var oDragTarget = document.getElementsByClassName("target")[0];
+  oDragTarget.ondragenter = function (e) {
+    //不是元素图形进入就触发的而是拖拽的鼠标进入才触发的
+    // console.log(e);
+  };
+  oDragTarget.ondragover = function (e) {
+    //类似于ondrag,只要在区域内移动,就不停的触发
+    // console.log(e);
+    // ondragover--回到原处 / 执行drop事件
+  };
+  oDragTarget.ondragleave = function (e) {
+    console.log(e);
+    // 离开就触发
+  };
+  oDragTarget.ondrop = function (e) {
+    console.log(e);
+    // 所有标签元素,当拖拽周期结束时,默认事件是回到原处,要想执行ondrop,必须在ondragover里面加上e.preventDefault();
+    // 事件是由行为触发,一个行为可以不只触发一个事件
+    // 抬起的时候,有ondragover默认回到原处的事件,只要阻止回到原处,就可以执行drop事件
+
+    // A -> B(阻止) -> C             想阻止c,只能在B上阻止  责任链模式
+  };
+</script>
+
<div class="a" draggable="true"></div>
+<div class="target"></div>
+
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  oDragDiv.ondragstart = function (e) {};
+  oDragDiv.ondrag = function (e) {
+    //只要移动就不停的触发
+  };
+  oDragDiv.ondragend = function (e) {};
+  var oDragTarget = document.getElementsByClassName("target")[0];
+  oDragTarget.ondragenter = function (e) {
+    //不是元素图形进入就触发的而是拖拽的鼠标进入才触发的
+    // console.log(e);
+  };
+  oDragTarget.ondragover = function (e) {
+    //类似于ondrag,只要在区域内移动,就不停的触发
+    // console.log(e);
+    // ondragover--回到原处 / 执行drop事件
+  };
+  oDragTarget.ondragleave = function (e) {
+    console.log(e);
+    // 离开就触发
+  };
+  oDragTarget.ondrop = function (e) {
+    console.log(e);
+    // 所有标签元素,当拖拽周期结束时,默认事件是回到原处,要想执行ondrop,必须在ondragover里面加上e.preventDefault();
+    // 事件是由行为触发,一个行为可以不只触发一个事件
+    // 抬起的时候,有ondragover默认回到原处的事件,只要阻止回到原处,就可以执行drop事件
+
+    // A -> B(阻止) -> C             想阻止c,只能在B上阻止  责任链模式
+  };
+</script>
+

拖拽 demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .box1 {
+        position: absolute;
+        width: 150px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      .box2 {
+        position: absolute;
+        width: 150px;
+        left: 300px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      li {
+        position: relative;
+        width: 100px;
+        height: 30px;
+        background: #abcdef;
+        margin: 10px auto 0px auto;
+        /* 居中,上下10px */
+        list-style: none;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="box1">
+      <ul>
+        <li></li>
+        <li></li>
+        <li></li>
+      </ul>
+    </div>
+    <div class="box2"></div>
+    <script>
+      var dragDom;
+      var liList = document.getElementsByTagName("li");
+      for (var i = 0; i < liList.length; i++) {
+        liList[i].setAttribute("draggable", true); //赋予class
+        liList[i].ondragstart = function (e) {
+          console.log(e.target);
+          dragDom = e.target;
+        };
+      }
+      var box2 = document.getElementsByClassName("box2")[0];
+      box2.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box2.ondrop = function (e) {
+        // console.log(dragDom);
+        box2.appendChild(dragDom);
+        dragDom = null;
+      };
+
+      // 拖回去
+      var box1 = document.getElementsByClassName("box1")[0];
+      box1.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box1.ondrop = function (e) {
+        // console.log(dragDom);
+        box1.appendChild(dragDom);
+        dragDom = null;
+      };
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .box1 {
+        position: absolute;
+        width: 150px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      .box2 {
+        position: absolute;
+        width: 150px;
+        left: 300px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      li {
+        position: relative;
+        width: 100px;
+        height: 30px;
+        background: #abcdef;
+        margin: 10px auto 0px auto;
+        /* 居中,上下10px */
+        list-style: none;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="box1">
+      <ul>
+        <li></li>
+        <li></li>
+        <li></li>
+      </ul>
+    </div>
+    <div class="box2"></div>
+    <script>
+      var dragDom;
+      var liList = document.getElementsByTagName("li");
+      for (var i = 0; i < liList.length; i++) {
+        liList[i].setAttribute("draggable", true); //赋予class
+        liList[i].ondragstart = function (e) {
+          console.log(e.target);
+          dragDom = e.target;
+        };
+      }
+      var box2 = document.getElementsByClassName("box2")[0];
+      box2.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box2.ondrop = function (e) {
+        // console.log(dragDom);
+        box2.appendChild(dragDom);
+        dragDom = null;
+      };
+
+      // 拖回去
+      var box1 = document.getElementsByClassName("box1")[0];
+      box1.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box1.ondrop = function (e) {
+        // console.log(dragDom);
+        box1.appendChild(dragDom);
+        dragDom = null;
+      };
+    </script>
+  </body>
+</html>
+

dataTransfer 补充属性

effectAllowed

js
oDragDiv.ondragstart = function (e) { e.dataTransfer.effectAllowed =
+"link";//指针是什么样子,只能在ondragstart里面设置 } /其他光标:link copy move
+copyMove linkMove all
+
oDragDiv.ondragstart = function (e) { e.dataTransfer.effectAllowed =
+"link";//指针是什么样子,只能在ondragstart里面设置 } /其他光标:link copy move
+copyMove linkMove all
+

dropEffect

js
oDragTarget.ondrop = function (e) { e.dataTransfer.dropEffect =
+"link";//放下时候的效果,只在drop里面设置 }
+
oDragTarget.ondrop = function (e) { e.dataTransfer.dropEffect =
+"link";//放下时候的效果,只在drop里面设置 }
+

试验不通过??

`,12),r=[e,t,c];function B(y,F,i,A,b,u){return a(),l("div",null,r)}const C=n(o,[["render",B]]);export{g as __pageData,C as default}; diff --git a/assets/html-css_drag.md.e7a4f3a8.lean.js b/assets/html-css_drag.md.e7a4f3a8.lean.js new file mode 100644 index 00000000..b04187f4 --- /dev/null +++ b/assets/html-css_drag.md.e7a4f3a8.lean.js @@ -0,0 +1 @@ +import{_ as n,o as a,c as l,a as s,b as p}from"./app.f983686f.js";const g=JSON.parse('{"title":"拖拽 API","description":"","frontmatter":{},"headers":[{"level":2,"title":"Drag 被拖拽元素","slug":"drag-被拖拽元素","link":"#drag-被拖拽元素","children":[]},{"level":2,"title":"Drag 目标元素(目标区域)","slug":"drag-目标元素-目标区域","link":"#drag-目标元素-目标区域","children":[]},{"level":2,"title":"拖拽 demo","slug":"拖拽-demo","link":"#拖拽-demo","children":[]},{"level":2,"title":"dataTransfer 补充属性","slug":"datatransfer-补充属性","link":"#datatransfer-补充属性","children":[]}],"relativePath":"html-css/drag.md","lastUpdated":1675256330000}'),o={name:"html-css/drag.md"},e=s("",8),t=p("p",{"position:absolute":""},"小功能:.a",-1),c=s("",12),r=[e,t,c];function B(y,F,i,A,b,u){return a(),l("div",null,r)}const C=n(o,[["render",B]]);export{g as __pageData,C as default}; diff --git a/assets/html-css_flex.md.1ad45d13.js b/assets/html-css_flex.md.1ad45d13.js new file mode 100644 index 00000000..a628c7c4 --- /dev/null +++ b/assets/html-css_flex.md.1ad45d13.js @@ -0,0 +1,141 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-15-16-29-04.a6a9af91.png",o="/blog/assets/2024-04-15-16-29-22.7bb9f883.png",e="/blog/assets/2024-04-15-16-30-16.8834c182.png",t="/blog/assets/2024-04-15-16-32-00.7b8a2c55.png",c="/blog/assets/2024-04-15-16-32-46.4ffe7e55.png",r="/blog/assets/2024-04-15-16-33-30.53b81e69.png",B="/blog/assets/2024-04-15-16-34-19.29fd84eb.png",y="/blog/assets/2024-04-15-16-37-04.e38b03b0.png",F="/blog/assets/2024-04-15-16-38-36.b5c1eb05.png",i="/blog/assets/2024-04-15-16-38-54.d9fefeed.png",A="/blog/assets/2024-04-15-16-40-03.8b67fcb7.png",d="/blog/assets/2024-04-15-16-40-30.c5b0dc7e.png",b="/blog/assets/2024-04-15-16-40-38.28de0ed4.png",u="/blog/assets/2024-04-15-16-41-42.7d56b4af.png",x="/blog/assets/2024-04-15-16-42-00.7e19cb0b.png",w=JSON.parse('{"title":"一文搞懂 flex:0,1,auto,none","description":"","frontmatter":{},"headers":[{"level":2,"title":"flex 属性介绍","slug":"flex-属性介绍","link":"#flex-属性介绍","children":[{"level":3,"title":"flex-grow","slug":"flex-grow","link":"#flex-grow","children":[]},{"level":3,"title":"flex-shrink","slug":"flex-shrink","link":"#flex-shrink","children":[]},{"level":3,"title":"flex-basis","slug":"flex-basis","link":"#flex-basis","children":[]}]},{"level":2,"title":"flex 缩写的等值","slug":"flex-缩写的等值","link":"#flex-缩写的等值","children":[{"level":3,"title":"flex: initial","slug":"flex-initial","link":"#flex-initial","children":[]},{"level":3,"title":"适用场景","slug":"适用场景","link":"#适用场景","children":[]},{"level":3,"title":"flex:0 和 flex:none","slug":"flex-0-和-flex-none","link":"#flex-0-和-flex-none","children":[]},{"level":3,"title":"适用场景 flex-0","slug":"适用场景-flex-0","link":"#适用场景-flex-0","children":[]},{"level":3,"title":"适用场景 flex-none","slug":"适用场景-flex-none","link":"#适用场景-flex-none","children":[]},{"level":3,"title":"flex:1 和 flex:auto","slug":"flex-1-和-flex-auto","link":"#flex-1-和-flex-auto","children":[]}]}],"relativePath":"html-css/flex.md","lastUpdated":1713170553000}'),g={name:"html-css/flex.md"},f=l('

一文搞懂 flex:0,1,auto,none

flex 属性介绍

首先, flex 属性其实是一种简写,是 flex-grow , flex-shrink 和 flex-basis 的缩写形式。 默认值为 0 1 auto 。后两个属性可选。

flex-grow

flex-grow 属性定义项目的放大比例,默认为 0 ,即如果存在剩余空间,也不放大。 如果所有项目的 flex-grow 属性都为 1,则它们将等分剩余空间(如果有的话)。 如果一个项目的 flex-grow 属性为 2,其他项目都为 1,则前者占据的剩余空间将比其他项多一倍。

flex-shrink

flex-shrink 属性定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。 如果所有项目的 flex-shrink 属性都为 1,当空间不足时,都将等比例缩小。 如果一个项目的 flex-shrink 属性为 0,其他项目都为 1,则空间不足时,前者不缩小。

flex-basis

flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。 浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为 auto,即项目的本来大小。 它可以设为跟 width 或 height 属性一样的值(比如 350px),则项目将占据固定空间。

flex 缩写的等值

了解了三个属性各自的含义之后,可以看下三个属性对应的等值。

flex: initial

flex:initial 等同于设置 flex: 0 1 auto ,是 flex 属性的默认值。

举例,外容器是红色,内里元素蓝色边框,比较少,会有下图效果,剩余空间仍有保留。剩余空间有,但是因为 flex-grow 属性是 0,所以没有填补空白。

html
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+  }
+</style>
+
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+  }
+</style>
+

如果子项内容很多,由于 flex-shrink:1 ,因此,会缩小,表现效果就是文字换行,效果如下图所示。

适用场景

initial 表示 CSS 属性的初始值,通常用来还原已经设置的 CSS 属性。因此日常开发不会专门设置 flex:initial 声明。flex:initial 声明适用于下图所示的布局效果。

上图所示的布局效果常见于按钮、标题、小图标等小部件的排版布局,因为这些小部件的宽度都不会很宽,水平位置的控制多使用 justify-content 和 margin-left:auto/margin-right:auto 实现。

除了上图所示的布局效果外, flex:initial 声明还适用于一侧内容宽度固定,另外一侧内容宽度任意的两栏自适应布局场景,布局轮廓如图下图所示(点点点表示文本内容)。

此时,无需任何其他 Flex 布局相关的 CSS 设置,只需要容器元素设置 display:flex 即可。

总结下就是那些希望元素尺寸收缩,同时元素内容万一较多又能自动换行的场景可以不做任何 flex 属性设置。

flex:0 和 flex:none

flex:0 等同于设置 flex: 0 1 0% 。

html
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 0;
+  }
+</style>
+
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 0;
+  }
+</style>
+

flex:none 等同于设置 flex: 0 0 auto 。只是把 css 中的 flex 属性设置为 none ,不再展示代码。

对比看来可以看到 flex-0 时候会表现为最小内容宽度,会将高度撑高(当前没有设置高度,如果设置高度文字会超过设置的高度,如下图)flex-none 时候会表现为最大内容宽度,字数过多时候会超过容器宽度。

适用场景 flex-0

由于应用了 flex:0 的元素表现为最小内容宽度,因此,适合使用 flex:0 的场景并不多。

其中上图左侧部分的矩形表示一个图像,图像下方会有文字内容不定的描述信息,此时,左侧内容就适合设置 flex:0 ,这样,无论文字的内容如何设置,左侧内容的宽度都是图像的宽度。

适用场景 flex-none

flex-none 比 flex-0 的适用场景多,如内容文字固定不换行,宽度为内容宽度就适用该属性。

没设置 flex-none 代码如下:

html
<div class="aa">
+  <img src="a.png" />
+  <p>右侧按钮没有设置flex-none</p>
+  <button>按钮</button>
+</div>
+<style>
+  .aa {
+    width: 300px;
+    border: 1px solid #000;
+    display: flex;
+  }
+
+  .aa img {
+    width: 100px;
+    height: 100px;
+  }
+
+  .aa buttton {
+    height: 50px;
+    align-self: center;
+  }
+</style>
+
<div class="aa">
+  <img src="a.png" />
+  <p>右侧按钮没有设置flex-none</p>
+  <button>按钮</button>
+</div>
+<style>
+  .aa {
+    width: 300px;
+    border: 1px solid #000;
+    display: flex;
+  }
+
+  .aa img {
+    width: 100px;
+    height: 100px;
+  }
+
+  .aa buttton {
+    height: 50px;
+    align-self: center;
+  }
+</style>
+

flex:1 和 flex:auto

flex:1 时代码如下

html
<div class="container">
+  <div class="item">嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿</div>
+  <div class="item">哈哈</div>
+  <div class="item">呵呵</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 1;
+  }
+</style>
+
<div class="container">
+  <div class="item">嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿</div>
+  <div class="item">哈哈</div>
+  <div class="item">呵呵</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 1;
+  }
+</style>
+

虽然都是充分分配容器的尺寸,但是 flex:1 的尺寸表现更为内敛(优先牺牲自己的尺寸), flex:auto 的尺寸表现则更为霸道(优先扩展自己的尺寸)。

适合使用 flex:1 的场景

当希望元素充分利用剩余空间,同时不会侵占其他元素应有的宽度的时候,适合使用 flex:1 ,这样的场景在 Flex 布局中非常的多。

例如所有的等分列表,或者等比例列表都适合使用 flex:1 或者其他 flex 数值,适合的布局效果轮廓如下图所示。

适合使用 flex:auto 的场景

当希望元素充分利用剩余空间,但是各自的尺寸按照各自内容进行分配的时候,适合使用 flex:auto 。例如导航栏。整体设置为 200px,内部设置 flex:auto,会自动按照内容比例进行分配宽度。

',58),m=[f];function h(v,C,D,_,E,q){return n(),a("div",null,m)}const S=s(g,[["render",h]]);export{w as __pageData,S as default}; diff --git a/assets/html-css_flex.md.1ad45d13.lean.js b/assets/html-css_flex.md.1ad45d13.lean.js new file mode 100644 index 00000000..6cd0a7db --- /dev/null +++ b/assets/html-css_flex.md.1ad45d13.lean.js @@ -0,0 +1 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2024-04-15-16-29-04.a6a9af91.png",o="/blog/assets/2024-04-15-16-29-22.7bb9f883.png",e="/blog/assets/2024-04-15-16-30-16.8834c182.png",t="/blog/assets/2024-04-15-16-32-00.7b8a2c55.png",c="/blog/assets/2024-04-15-16-32-46.4ffe7e55.png",r="/blog/assets/2024-04-15-16-33-30.53b81e69.png",B="/blog/assets/2024-04-15-16-34-19.29fd84eb.png",y="/blog/assets/2024-04-15-16-37-04.e38b03b0.png",F="/blog/assets/2024-04-15-16-38-36.b5c1eb05.png",i="/blog/assets/2024-04-15-16-38-54.d9fefeed.png",A="/blog/assets/2024-04-15-16-40-03.8b67fcb7.png",d="/blog/assets/2024-04-15-16-40-30.c5b0dc7e.png",b="/blog/assets/2024-04-15-16-40-38.28de0ed4.png",u="/blog/assets/2024-04-15-16-41-42.7d56b4af.png",x="/blog/assets/2024-04-15-16-42-00.7e19cb0b.png",w=JSON.parse('{"title":"一文搞懂 flex:0,1,auto,none","description":"","frontmatter":{},"headers":[{"level":2,"title":"flex 属性介绍","slug":"flex-属性介绍","link":"#flex-属性介绍","children":[{"level":3,"title":"flex-grow","slug":"flex-grow","link":"#flex-grow","children":[]},{"level":3,"title":"flex-shrink","slug":"flex-shrink","link":"#flex-shrink","children":[]},{"level":3,"title":"flex-basis","slug":"flex-basis","link":"#flex-basis","children":[]}]},{"level":2,"title":"flex 缩写的等值","slug":"flex-缩写的等值","link":"#flex-缩写的等值","children":[{"level":3,"title":"flex: initial","slug":"flex-initial","link":"#flex-initial","children":[]},{"level":3,"title":"适用场景","slug":"适用场景","link":"#适用场景","children":[]},{"level":3,"title":"flex:0 和 flex:none","slug":"flex-0-和-flex-none","link":"#flex-0-和-flex-none","children":[]},{"level":3,"title":"适用场景 flex-0","slug":"适用场景-flex-0","link":"#适用场景-flex-0","children":[]},{"level":3,"title":"适用场景 flex-none","slug":"适用场景-flex-none","link":"#适用场景-flex-none","children":[]},{"level":3,"title":"flex:1 和 flex:auto","slug":"flex-1-和-flex-auto","link":"#flex-1-和-flex-auto","children":[]}]}],"relativePath":"html-css/flex.md","lastUpdated":1713170553000}'),g={name:"html-css/flex.md"},f=l("",58),m=[f];function h(v,C,D,_,E,q){return n(),a("div",null,m)}const S=s(g,[["render",h]]);export{w as __pageData,S as default}; diff --git a/assets/html-css_interview.md.c1ea6a41.js b/assets/html-css_interview.md.c1ea6a41.js new file mode 100644 index 00000000..b72260a3 --- /dev/null +++ b/assets/html-css_interview.md.c1ea6a41.js @@ -0,0 +1,2495 @@ +import{_ as s,o as n,c as a,a as l}from"./app.f983686f.js";const p="/blog/assets/2023-02-01-21-38-25.04ce3663.png",A=JSON.parse('{"title":"HTML_CSS 面试题","description":"","frontmatter":{},"headers":[{"level":2,"title":"1. 什么是 ?是否需要在 HTML5 中使用?","slug":"_1-什么是-doctype-是否需要在-html5-中使用","link":"#_1-什么是-doctype-是否需要在-html5-中使用","children":[]},{"level":2,"title":"2. 什么是可替换元素,什么是非可替换元素,它们各自有什么特点?","slug":"_2-什么是可替换元素-什么是非可替换元素-它们各自有什么特点","link":"#_2-什么是可替换元素-什么是非可替换元素-它们各自有什么特点","children":[]},{"level":2,"title":"3. src 和 href 的区别","slug":"_3-src-和-href-的区别","link":"#_3-src-和-href-的区别","children":[]},{"level":2,"title":"4. 说说常用的 meta 标签","slug":"_4-说说常用的-meta-标签","link":"#_4-说说常用的-meta-标签","children":[]},{"level":2,"title":"5. 说说对 html 语义化的理解","slug":"_5-说说对-html-语义化的理解","link":"#_5-说说对-html-语义化的理解","children":[]},{"level":2,"title":"6. label 的作用是什么?是怎么用的?","slug":"_6-label-的作用是什么-是怎么用的","link":"#_6-label-的作用是什么-是怎么用的","children":[]},{"level":2,"title":"7. iframe 框架有那些优缺点?","slug":"_7-iframe-框架有那些优缺点","link":"#_7-iframe-框架有那些优缺点","children":[]},{"level":2,"title":"8. HTML 与 XHTML 二者有什么区别,你觉得应该使用哪一个并说出理由。 不同于 XML","slug":"_8-html-与-xhtml-二者有什么区别-你觉得应该使用哪一个并说出理由。-不同于-xml","link":"#_8-html-与-xhtml-二者有什么区别-你觉得应该使用哪一个并说出理由。-不同于-xml","children":[]},{"level":2,"title":"9. HTML5 的 form 如何关闭自动完成功能?","slug":"_9-html5-的-form-如何关闭自动完成功能","link":"#_9-html5-的-form-如何关闭自动完成功能","children":[]},{"level":2,"title":"10. title 与 h1 的区别、b 与 strong 的区别、i 与 em 的区别?","slug":"_10-title-与-h1-的区别、b-与-strong-的区别、i-与-em-的区别","link":"#_10-title-与-h1-的区别、b-与-strong-的区别、i-与-em-的区别","children":[]},{"level":2,"title":"11. 请描述下 SEO 中的 TDK","slug":"_11-请描述下-seo-中的-tdk","link":"#_11-请描述下-seo-中的-tdk","children":[]},{"level":2,"title":"13. 什么是严格模式与混杂模式?","slug":"_13-什么是严格模式与混杂模式","link":"#_13-什么是严格模式与混杂模式","children":[]},{"level":2,"title":"14. 对于 WEB 标准以及 W3C 的理解与认识问题","slug":"_14-对于-web-标准以及-w3c-的理解与认识问题","link":"#_14-对于-web-标准以及-w3c-的理解与认识问题","children":[]},{"level":2,"title":"15. 列举 IE 与其他浏览器不一样的特性?","slug":"_15-列举-ie-与其他浏览器不一样的特性","link":"#_15-列举-ie-与其他浏览器不一样的特性","children":[]},{"level":2,"title":"17. 页面可见性(Page Visibility)API 可以有哪些用途?","slug":"_17-页面可见性-page-visibility-api-可以有哪些用途","link":"#_17-页面可见性-page-visibility-api-可以有哪些用途","children":[]},{"level":2,"title":"19. div+css 的布局较 table 布局有什么优点?","slug":"_19-div-css-的布局较-table-布局有什么优点","link":"#_19-div-css-的布局较-table-布局有什么优点","children":[]},{"level":2,"title":"24. HTML 全局属性(global attribute)有哪些","slug":"_24-html-全局属性-global-attribute-有哪些","link":"#_24-html-全局属性-global-attribute-有哪些","children":[]},{"level":2,"title":"28.  iframe 的作用","slug":"_28-iframe-的作用","link":"#_28-iframe-的作用","children":[]},{"level":2,"title":"29. img 上 title 与 alt","slug":"_29-img-上-title-与-alt","link":"#_29-img-上-title-与-alt","children":[]},{"level":2,"title":"31. 行内元素和块级元素区别,有哪些,怎样转换?(顶呱呱)","slug":"_31-行内元素和块级元素区别-有哪些-怎样转换-顶呱呱","link":"#_31-行内元素和块级元素区别-有哪些-怎样转换-顶呱呱","children":[]},{"level":2,"title":"34.  h5 和 html5 区别","slug":"_34-h5-和-html5-区别","link":"#_34-h5-和-html5-区别","children":[]},{"level":2,"title":"35. form 表单上传文件时需要进行什么样的声明","slug":"_35-form-表单上传文件时需要进行什么样的声明","link":"#_35-form-表单上传文件时需要进行什么样的声明","children":[]},{"level":2,"title":"37. 如何在一张图片上的某一个区域做到点击事件","slug":"_37-如何在一张图片上的某一个区域做到点击事件","link":"#_37-如何在一张图片上的某一个区域做到点击事件","children":[]},{"level":2,"title":"39. 什么是锚点?","slug":"_39-什么是锚点","link":"#_39-什么是锚点","children":[]},{"level":2,"title":"40. 图片与 span 元素混排图像下方会出现几像素的空隙的原因是什么?","slug":"_40-图片与-span-元素混排图像下方会出现几像素的空隙的原因是什么","link":"#_40-图片与-span-元素混排图像下方会出现几像素的空隙的原因是什么","children":[]},{"level":2,"title":"41. a 元素除了用于导航外,还可以有什么作用?","slug":"_41-a-元素除了用于导航外-还可以有什么作用","link":"#_41-a-元素除了用于导航外-还可以有什么作用","children":[{"level":3,"title":"1.   介绍下 BFC 及其应用","slug":"_1-介绍下-bfc-及其应用","link":"#_1-介绍下-bfc-及其应用","children":[]},{"level":3,"title":"2. 介绍下 BFC、IFC、GFC 和 FFC","slug":"_2-介绍下-bfc、ifc、gfc-和-ffc","link":"#_2-介绍下-bfc、ifc、gfc-和-ffc","children":[]},{"level":3,"title":"3. flex 骰子","slug":"_3-flex-骰子","link":"#_3-flex-骰子","children":[]},{"level":3,"title":"5. 分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景。","slug":"_5-分析比较-opacity-0、visibility-hidden、display-none-优劣和适用场景。","link":"#_5-分析比较-opacity-0、visibility-hidden、display-none-优劣和适用场景。","children":[]},{"level":3,"title":"6. 已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。","slug":"_6-已知如下代码-如何修改才能让图片宽度为-300px-注意下面代码不可修改。","link":"#_6-已知如下代码-如何修改才能让图片宽度为-300px-注意下面代码不可修改。","children":[]},{"level":3,"title":"7. 如何用 css 或 js 实现多行文本溢出省略效果,考虑兼容性","slug":"_7-如何用-css-或-js-实现多行文本溢出省略效果-考虑兼容性","link":"#_7-如何用-css-或-js-实现多行文本溢出省略效果-考虑兼容性","children":[]},{"level":3,"title":"8. 居中为什么要使用 transform(为什么不使用 marginLeft/Top)(阿里)","slug":"_8-居中为什么要使用-transform-为什么不使用-marginleft-top-阿里","link":"#_8-居中为什么要使用-transform-为什么不使用-marginleft-top-阿里","children":[]},{"level":3,"title":"10. 说出 space-between 和 space-around 的区别?(携程)","slug":"_10-说出-space-between-和-space-around-的区别-携程","link":"#_10-说出-space-between-和-space-around-的区别-携程","children":[]},{"level":3,"title":"11. CSS3 中 transition 和 animation 的属性分别有哪些","slug":"_11-css3-中-transition-和-animation-的属性分别有哪些","link":"#_11-css3-中-transition-和-animation-的属性分别有哪些","children":[]},{"level":3,"title":"12. 隐藏页面中的某个元素的方法有哪些?","slug":"_12-隐藏页面中的某个元素的方法有哪些","link":"#_12-隐藏页面中的某个元素的方法有哪些","children":[]},{"level":3,"title":"13. 层叠上下文","slug":"_13-层叠上下文","link":"#_13-层叠上下文","children":[]},{"level":3,"title":"15. 讲一下png8、png16、png32的区别,并简单讲讲 png 的压缩原理","slug":"_15-讲一下png8、png16、png32的区别-并简单讲讲-png-的压缩原理","link":"#_15-讲一下png8、png16、png32的区别-并简单讲讲-png-的压缩原理","children":[]},{"level":3,"title":"16. 说说渐进增强和优雅降级","slug":"_16-说说渐进增强和优雅降级","link":"#_16-说说渐进增强和优雅降级","children":[]},{"level":3,"title":"17. 介绍下 positon 属性","slug":"_17-介绍下-positon-属性","link":"#_17-介绍下-positon-属性","children":[]},{"level":3,"title":"19. 如何实现一个自适应的正方形","slug":"_19-如何实现一个自适应的正方形","link":"#_19-如何实现一个自适应的正方形","children":[]},{"level":3,"title":"20. 如何实现三栏布局","slug":"_20-如何实现三栏布局","link":"#_20-如何实现三栏布局","children":[]},{"level":3,"title":"21. import 和 link 区别","slug":"_21-import-和-link-区别","link":"#_21-import-和-link-区别","children":[]},{"level":3,"title":"23. 清除浮动的方法","slug":"_23-清除浮动的方法","link":"#_23-清除浮动的方法","children":[]},{"level":3,"title":"使用 clear 属性清除浮动的原理?","slug":"使用-clear-属性清除浮动的原理","link":"#使用-clear-属性清除浮动的原理","children":[]},{"level":3,"title":"对 requestAnimationframe 的理解","slug":"对-requestanimationframe-的理解","link":"#对-requestanimationframe-的理解","children":[]},{"level":3,"title":"伪元素和伪类的区别和作用?","slug":"伪元素和伪类的区别和作用","link":"#伪元素和伪类的区别和作用","children":[]},{"level":3,"title":"25. css reset 和 normalize.css 有什么区别?","slug":"_25-css-reset-和-normalize-css-有什么区别","link":"#_25-css-reset-和-normalize-css-有什么区别","children":[]},{"level":3,"title":"27. 如何避免重绘或者重排?","slug":"_27-如何避免重绘或者重排","link":"#_27-如何避免重绘或者重排","children":[]},{"level":3,"title":"28. 如何触发重排和重绘?","slug":"_28-如何触发重排和重绘","link":"#_28-如何触发重排和重绘","children":[]},{"level":3,"title":"29. 重绘与重排的区别?","slug":"_29-重绘与重排的区别","link":"#_29-重绘与重排的区别","children":[]},{"level":3,"title":"30. 如何优化图片","slug":"_30-如何优化图片","link":"#_30-如何优化图片","children":[]},{"level":3,"title":"34. 什么是渐进式渲染(progressive rendering)?","slug":"_34-什么是渐进式渲染-progressive-rendering","link":"#_34-什么是渐进式渲染-progressive-rendering","children":[]},{"level":3,"title":"39. img 标签在页面中有 1px 的边框,怎么处理","slug":"_39-img-标签在页面中有-1px-的边框-怎么处理","link":"#_39-img-标签在页面中有-1px-的边框-怎么处理","children":[]},{"level":3,"title":"40. 如何绝对居中(不用定位)","slug":"_40-如何绝对居中-不用定位","link":"#_40-如何绝对居中-不用定位","children":[]},{"level":3,"title":"42. 我有 5 个 div 在一行,我要让 div 与 div 直接间距 10px 且最左最右两边的 div 据边框 10px,同时在我改变窗口大小时,这个 10px 不能变,div 根据窗口改变大小(长天星斗)","slug":"_42-我有-5-个-div-在一行-我要让-div-与-div-直接间距-10px-且最左最右两边的-div-据边框-10px-同时在我改变窗口大小时-这个-10px-不能变-div-根据窗口改变大小-长天星斗","link":"#_42-我有-5-个-div-在一行-我要让-div-与-div-直接间距-10px-且最左最右两边的-div-据边框-10px-同时在我改变窗口大小时-这个-10px-不能变-div-根据窗口改变大小-长天星斗","children":[]},{"level":3,"title":"48. px 和 em 的区别","slug":"_48-px-和-em-的区别","link":"#_48-px-和-em-的区别","children":[]},{"level":3,"title":"49. div 之间的间隙是怎么产生的,应该怎么消除?","slug":"_49-div-之间的间隙是怎么产生的-应该怎么消除","link":"#_49-div-之间的间隙是怎么产生的-应该怎么消除","children":[]},{"level":3,"title":"24. display:inline-block 什么时候会显示间隙?","slug":"_24-display-inline-block-什么时候会显示间隙","link":"#_24-display-inline-block-什么时候会显示间隙","children":[]},{"level":3,"title":"57. 你怎么处理页面兼容性问题?","slug":"_57-你怎么处理页面兼容性问题","link":"#_57-你怎么处理页面兼容性问题","children":[]},{"level":3,"title":"63. 什么是继承?CSS 中哪些属性可以继承?哪些不可以继承?","slug":"_63-什么是继承-css-中哪些属性可以继承-哪些不可以继承","link":"#_63-什么是继承-css-中哪些属性可以继承-哪些不可以继承","children":[]},{"level":3,"title":"65. CSS 的计算属性知道吗","slug":"_65-css-的计算属性知道吗","link":"#_65-css-的计算属性知道吗","children":[]},{"level":3,"title":"66. 为何 CSS 放在 HTML 头部?","slug":"_66-为何-css-放在-html-头部","link":"#_66-为何-css-放在-html-头部","children":[]},{"level":3,"title":"67. background-size 有哪 4 种值类型?","slug":"_67-background-size-有哪-4-种值类型","link":"#_67-background-size-有哪-4-种值类型","children":[]},{"level":3,"title":"74. 有一个高度自适应的 div,里面有 2 个 div,一个高度 100px,希望另一个填满剩下的高度?(CSS 实现)","slug":"_74-有一个高度自适应的-div-里面有-2-个-div-一个高度-100px-希望另一个填满剩下的高度-css-实现","link":"#_74-有一个高度自适应的-div-里面有-2-个-div-一个高度-100px-希望另一个填满剩下的高度-css-实现","children":[]},{"level":3,"title":"inline 和 block 区别(长宽,padding,margin)","slug":"inline-和-block-区别-长宽-padding-margin","link":"#inline-和-block-区别-长宽-padding-margin","children":[]},{"level":3,"title":"77. 当 margin-top、padding-top 的值是百分比时,分别是如何计算的?","slug":"_77-当-margin-top、padding-top-的值是百分比时-分别是如何计算的","link":"#_77-当-margin-top、padding-top-的值是百分比时-分别是如何计算的","children":[]},{"level":3,"title":"81. 在 rem 自适应页面中使用 sprite 会出现背景图不随元素放大缩小的情况,如何解决?","slug":"_81-在-rem-自适应页面中使用-sprite-会出现背景图不随元素放大缩小的情况-如何解决","link":"#_81-在-rem-自适应页面中使用-sprite-会出现背景图不随元素放大缩小的情况-如何解决","children":[]},{"level":3,"title":"98. 如何将大量可变化的图片显示到页面并不影响浏览器性能?","slug":"_98-如何将大量可变化的图片显示到页面并不影响浏览器性能","link":"#_98-如何将大量可变化的图片显示到页面并不影响浏览器性能","children":[]},{"level":3,"title":"99. 图片与图片之间默认存在的间距如何去除?","slug":"_99-图片与图片之间默认存在的间距如何去除","link":"#_99-图片与图片之间默认存在的间距如何去除","children":[]},{"level":3,"title":"102. 不使用 border 属性,使用其他属性模拟边框。","slug":"_102-不使用-border-属性-使用其他属性模拟边框。","link":"#_102-不使用-border-属性-使用其他属性模拟边框。","children":[]},{"level":3,"title":"103. 阅读代码,计算 ul 的高度。","slug":"_103-阅读代码-计算-ul-的高度。","link":"#_103-阅读代码-计算-ul-的高度。","children":[]},{"level":3,"title":"105. 如何使用媒体查询实现视口宽度大于 320px 小于 640px 时 div 元素宽度变成 30%?","slug":"_105-如何使用媒体查询实现视口宽度大于-320px-小于-640px-时-div-元素宽度变成-30","link":"#_105-如何使用媒体查询实现视口宽度大于-320px-小于-640px-时-div-元素宽度变成-30","children":[]},{"level":3,"title":"106. 什么是 HTML 实体?","slug":"_106-什么是-html-实体","link":"#_106-什么是-html-实体","children":[]},{"level":3,"title":"109. 在定位属性 position 中,哪个值会脱离文档流?","slug":"_109-在定位属性-position-中-哪个值会脱离文档流","link":"#_109-在定位属性-position-中-哪个值会脱离文档流","children":[]},{"level":3,"title":"画一条 0.5px 的线","slug":"画一条-0-5px-的线","link":"#画一条-0-5px-的线","children":[]},{"level":3,"title":"6. 如何解决 1px 问题?","slug":"_6-如何解决-1px-问题","link":"#_6-如何解决-1px-问题","children":[]},{"level":3,"title":"118. 绝对定位和浮动有什么异同?","slug":"_118-绝对定位和浮动有什么异同","link":"#_118-绝对定位和浮动有什么异同","children":[]},{"level":3,"title":"120. 文本“强制换行”的属性是什么?","slug":"_120-文本-强制换行-的属性是什么","link":"#_120-文本-强制换行-的属性是什么","children":[]},{"level":3,"title":"121. 阅读代码,分析最终执行结果 div 的水平、垂直位置距离。","slug":"_121-阅读代码-分析最终执行结果-div-的水平、垂直位置距离。","link":"#_121-阅读代码-分析最终执行结果-div-的水平、垂直位置距离。","children":[]},{"level":3,"title":"122. 设置了元素的过渡后需要有触发条件才能看到效果,列举出可用的触发条件。","slug":"_122-设置了元素的过渡后需要有触发条件才能看到效果-列举出可用的触发条件。","link":"#_122-设置了元素的过渡后需要有触发条件才能看到效果-列举出可用的触发条件。","children":[]},{"level":3,"title":"123. 怎样把背景图附着在内容上?","slug":"_123-怎样把背景图附着在内容上","link":"#_123-怎样把背景图附着在内容上","children":[]},{"level":3,"title":"124. CSS 优化、提高性能的方法有哪些?","slug":"_124-css-优化、提高性能的方法有哪些","link":"#_124-css-优化、提高性能的方法有哪些","children":[]},{"level":3,"title":"125. 网页中应该使用奇数还是偶数的字体?","slug":"_125-网页中应该使用奇数还是偶数的字体","link":"#_125-网页中应该使用奇数还是偶数的字体","children":[]},{"level":3,"title":"126. margin 和 padding 分别适合什么场景使用?","slug":"_126-margin-和-padding-分别适合什么场景使用","link":"#_126-margin-和-padding-分别适合什么场景使用","children":[]},{"level":3,"title":"127. 对于 line-height 是如何理解的?","slug":"_127-对于-line-height-是如何理解的","link":"#_127-对于-line-height-是如何理解的","children":[]},{"level":3,"title":"128. 如何让 Chrome 支持小于 12px 的文字?","slug":"_128-如何让-chrome-支持小于-12px-的文字","link":"#_128-如何让-chrome-支持小于-12px-的文字","children":[]},{"level":3,"title":"129. 如果需要手动写动画,你认为最小时间间隔是多久?","slug":"_129-如果需要手动写动画-你认为最小时间间隔是多久","link":"#_129-如果需要手动写动画-你认为最小时间间隔是多久","children":[]},{"level":3,"title":"131. style 标签写在 body 后面与 body 前面有什么区别?","slug":"_131-style-标签写在-body-后面与-body-前面有什么区别","link":"#_131-style-标签写在-body-后面与-body-前面有什么区别","children":[]},{"level":3,"title":"132. 阐述一下 CSS Sprites","slug":"_132-阐述一下-css-sprites","link":"#_132-阐述一下-css-sprites","children":[]},{"level":3,"title":"133. 写出背景色渐变的 CSS 代码","slug":"_133-写出背景色渐变的-css-代码","link":"#_133-写出背景色渐变的-css-代码","children":[]},{"level":3,"title":"134. 写出添加下划线的 CSS 代码","slug":"_134-写出添加下划线的-css-代码","link":"#_134-写出添加下划线的-css-代码","children":[]},{"level":3,"title":"135. 写出让对象顺时针旋转 90 度的 CSS 代码(最好附带动画效果)","slug":"_135-写出让对象顺时针旋转-90-度的-css-代码-最好附带动画效果","link":"#_135-写出让对象顺时针旋转-90-度的-css-代码-最好附带动画效果","children":[]},{"level":3,"title":"136. CSS 优先级顺序正确的是( )","slug":"_136-css-优先级顺序正确的是","link":"#_136-css-优先级顺序正确的是","children":[]},{"level":3,"title":"137. 如何产生带有正方形项目的列表","slug":"_137-如何产生带有正方形项目的列表","link":"#_137-如何产生带有正方形项目的列表","children":[]},{"level":3,"title":"138. 手写一个三栏布局,要求:垂直三栏布局,所有两栏宽度固定,中间自适应","slug":"_138-手写一个三栏布局-要求-垂直三栏布局-所有两栏宽度固定-中间自适应","link":"#_138-手写一个三栏布局-要求-垂直三栏布局-所有两栏宽度固定-中间自适应","children":[]},{"level":3,"title":"css 如何做一个 loading 动画(关键帧、动画循环)","slug":"css-如何做一个-loading-动画-关键帧、动画循环","link":"#css-如何做一个-loading-动画-关键帧、动画循环","children":[]},{"level":3,"title":"left 和 margin-left 哪性能好","slug":"left-和-margin-left-哪性能好","link":"#left-和-margin-left-哪性能好","children":[]},{"level":3,"title":"心跳","slug":"心跳","link":"#心跳","children":[]},{"level":3,"title":"如何写一个移动端 html 抖音界面和刷视频的功能实现 【手撕代码】","slug":"如何写一个移动端-html-抖音界面和刷视频的功能实现-【手撕代码】","link":"#如何写一个移动端-html-抖音界面和刷视频的功能实现-【手撕代码】","children":[]},{"level":3,"title":"flex 骰子","slug":"flex-骰子","link":"#flex-骰子","children":[]},{"level":3,"title":"常见的 CSS 布局单位","slug":"常见的-css-布局单位","link":"#常见的-css-布局单位","children":[]},{"level":3,"title":"img 的 style 里面有 important 怎么覆盖掉","slug":"img-的-style-里面有-important-怎么覆盖掉","link":"#img-的-style-里面有-important-怎么覆盖掉","children":[]},{"level":3,"title":"实现一个布局 1+(2,3,4(4 是在 3 的右上角)flex","slug":"实现一个布局-1-2-3-4-4-是在-3-的右上角-flex","link":"#实现一个布局-1-2-3-4-4-是在-3-的右上角-flex","children":[]},{"level":3,"title":"移动端的点击事件的有延迟,时间是多久,为什么会有? 怎么解决这个 延时?","slug":"移动端的点击事件的有延迟-时间是多久-为什么会有-怎么解决这个-延时","link":"#移动端的点击事件的有延迟-时间是多久-为什么会有-怎么解决这个-延时","children":[]},{"level":3,"title":"一个 span 能不能改变长宽高。 --行元素不能改变宽高 假如 span 已经被 position 设置好了,能不能改变长宽高","slug":"一个-span-能不能改变长宽高。-行元素不能改变宽高-假如-span-已经被-position-设置好了-能不能改变长宽高","link":"#一个-span-能不能改变长宽高。-行元素不能改变宽高-假如-span-已经被-position-设置好了-能不能改变长宽高","children":[]},{"level":3,"title":"实现内容多时页面滑动,内容少时保持占据一页","slug":"实现内容多时页面滑动-内容少时保持占据一页","link":"#实现内容多时页面滑动-内容少时保持占据一页","children":[]},{"level":3,"title":"base64 优缺点","slug":"base64-优缺点","link":"#base64-优缺点","children":[]},{"level":3,"title":"用 font-weight 和 b 标签有什么区别","slug":"用-font-weight-和-b-标签有什么区别","link":"#用-font-weight-和-b-标签有什么区别","children":[]},{"level":3,"title":"移动端 fastclick 原理是什么","slug":"移动端-fastclick-原理是什么","link":"#移动端-fastclick-原理是什么","children":[]},{"level":3,"title":"grid 布局","slug":"grid-布局","link":"#grid-布局","children":[]},{"level":3,"title":"判断元素是否到达可视区域","slug":"判断元素是否到达可视区域","link":"#判断元素是否到达可视区域","children":[]},{"level":3,"title":"富文本编辑器,文本设置大小不同,怎么设置 line-height","slug":"富文本编辑器-文本设置大小不同-怎么设置-line-height","link":"#富文本编辑器-文本设置大小不同-怎么设置-line-height","children":[]},{"level":3,"title":"translate 可以设置 x y,如果右边也有一个元素,右边元素会不会被挤掉。","slug":"translate-可以设置-x-y-如果右边也有一个元素-右边元素会不会被挤掉。","link":"#translate-可以设置-x-y-如果右边也有一个元素-右边元素会不会被挤掉。","children":[]},{"level":3,"title":"图片尺寸不固定(可能比盒子大,小),盒子长宽固定,图片比盒子大,希望长宽达到 100%(等比缩放),比盒子小,希望垂直居中。两个 class,一个针对图片大,一个针对图片小,js 获取图片大小,更改 class","slug":"图片尺寸不固定-可能比盒子大-小-盒子长宽固定-图片比盒子大-希望长宽达到-100-等比缩放-比盒子小-希望垂直居中。两个-class-一个针对图片大-一个针对图片小-js-获取图片大小-更改-class","link":"#图片尺寸不固定-可能比盒子大-小-盒子长宽固定-图片比盒子大-希望长宽达到-100-等比缩放-比盒子小-希望垂直居中。两个-class-一个针对图片大-一个针对图片小-js-获取图片大小-更改-class","children":[]},{"level":3,"title":"rem 原理 ,rem 怎么计算出来的","slug":"rem-原理-rem-怎么计算出来的","link":"#rem-原理-rem-怎么计算出来的","children":[]},{"level":3,"title":"清除图片下方出现像素空白间隙","slug":"清除图片下方出现像素空白间隙","link":"#清除图片下方出现像素空白间隙","children":[]},{"level":3,"title":"说一下 SEO 优化","slug":"说一下-seo-优化","link":"#说一下-seo-优化","children":[]},{"level":3,"title":"浏览器乱码的原因是什么?如何解决?","slug":"浏览器乱码的原因是什么-如何解决","link":"#浏览器乱码的原因是什么-如何解决","children":[]},{"level":3,"title":"head 标签有什么作用,其中什么标签必不可少?","slug":"head-标签有什么作用-其中什么标签必不可少","link":"#head-标签有什么作用-其中什么标签必不可少","children":[]},{"level":3,"title":"对 line-height 的理解及其赋值方式","slug":"对-line-height-的理解及其赋值方式","link":"#对-line-height-的理解及其赋值方式","children":[]},{"level":3,"title":"z-index 属性在什么情况下会失效","slug":"z-index-属性在什么情况下会失效","link":"#z-index-属性在什么情况下会失效","children":[]},{"level":3,"title":"z-index 工作原理和适用范围","slug":"z-index-工作原理和适用范围","link":"#z-index-工作原理和适用范围","children":[]},{"level":3,"title":"CSS 环形进度条","slug":"css-环形进度条","link":"#css-环形进度条","children":[]},{"level":3,"title":"不考虑其它因素,下面哪种的渲染性能比较高?","slug":"不考虑其它因素-下面哪种的渲染性能比较高","link":"#不考虑其它因素-下面哪种的渲染性能比较高","children":[]},{"level":3,"title":"script 标签中 defer 和 async 的区别","slug":"script-标签中-defer-和-async-的区别","link":"#script-标签中-defer-和-async-的区别","children":[]},{"level":3,"title":"10. HTML5 的离线储存怎么使用,它的工作原理是什么","slug":"_10-html5-的离线储存怎么使用-它的工作原理是什么","link":"#_10-html5-的离线储存怎么使用-它的工作原理是什么","children":[]},{"level":3,"title":"浏览器是如何对 HTML5 的离线储存资源进行管理和加载?","slug":"浏览器是如何对-html5-的离线储存资源进行管理和加载","link":"#浏览器是如何对-html5-的离线储存资源进行管理和加载","children":[]}]},{"level":2,"title":"CSS 自适应正方形","slug":"css-自适应正方形","link":"#css-自适应正方形","children":[]},{"level":2,"title":"css3 transform 变换顺序考察","slug":"css3-transform-变换顺序考察","link":"#css3-transform-变换顺序考察","children":[]},{"level":2,"title":"css 单位的百分比","slug":"css-单位的百分比","link":"#css-单位的百分比","children":[]},{"level":2,"title":"CSS3 新增伪类有那些?","slug":"css3-新增伪类有那些","link":"#css3-新增伪类有那些","children":[]},{"level":2,"title":"请问 span 标签设置宽高有效吗?设置 margin、padding 有效吗?","slug":"请问-span-标签设置宽高有效吗-设置-margin、padding-有效吗","link":"#请问-span-标签设置宽高有效吗-设置-margin、padding-有效吗","children":[]},{"level":2,"title":"弹性盒子中 flex: 0 1 auto 表示什么意思?","slug":"弹性盒子中-flex-0-1-auto-表示什么意思","link":"#弹性盒子中-flex-0-1-auto-表示什么意思","children":[]},{"level":2,"title":"请简述响应式设计解决方案以及其应用","slug":"请简述响应式设计解决方案以及其应用","link":"#请简述响应式设计解决方案以及其应用","children":[]},{"level":2,"title":"inline-block 元素间隙处理","slug":"inline-block-元素间隙处理","link":"#inline-block-元素间隙处理","children":[]},{"level":2,"title":"line-height 取值","slug":"line-height-取值","link":"#line-height-取值","children":[]},{"level":2,"title":"【CSS】1rem、1em、1vh、1px 这几个值的实际意义","slug":"【css】1rem、1em、1vh、1px-这几个值的实际意义","link":"#【css】1rem、1em、1vh、1px-这几个值的实际意义","children":[]},{"level":2,"title":"css 百分比","slug":"css-百分比","link":"#css-百分比","children":[]},{"level":2,"title":"CSS 中 px、em、rem 的区别","slug":"css-中-px、em、rem-的区别","link":"#css-中-px、em、rem-的区别","children":[]},{"level":2,"title":"div 设置成 inline-block,有空隙是什么原因呢?如何解决","slug":"div-设置成-inline-block-有空隙是什么原因呢-如何解决","link":"#div-设置成-inline-block-有空隙是什么原因呢-如何解决","children":[]},{"level":2,"title":"css 中 z-index 属性考察","slug":"css-中-z-index-属性考察","link":"#css-中-z-index-属性考察","children":[]},{"level":2,"title":"css3 GPU 加速","slug":"css3-gpu-加速","link":"#css3-gpu-加速","children":[]},{"level":2,"title":"请问 HTML 中如何通过 + + +
Skip to content
On this page

常用命令

git merge 和 git rebase

git pull 和 git fetch

创建 SSH Key

shell
$ ssh-keygen -t rsa -C "youremail@example.com"
+
$ ssh-keygen -t rsa -C "youremail@example.com"
+

测试 key 是否配置成功

shell
$ ssh -T git@gitee.com
+
$ ssh -T git@gitee.com
+

配置用户信息

shell
$ git config --global user.name "Your Name"
+$ git config --global user.email "email@example.com"
+
$ git config --global user.name "Your Name"
+$ git config --global user.email "email@example.com"
+

仓库

在当前目录新建一个 Git 代码库

shell
$ git init
+
$ git init
+

新建一个目录,将其初始化为 Git 代码库

shell
$ git init [project-name]
+
$ git init [project-name]
+

下载一个项目和它的整个代码历史

shell
$ git clone [url]
+
$ git clone [url]
+

增加/删除文件

添加指定文件到暂存区

shell
$ git add [file1] [file2] ...
+
$ git add [file1] [file2] ...
+

添加指定目录到暂存区,包括子目录

shell
$ git add [dir]
+
$ git add [dir]
+

添加当前目录的所有文件到暂存区

shell
$ git add .
+
$ git add .
+

添加每个变化前,都会要求确认 对于同一个文件的多处变化,可以实现分次提交

shell
$ git add -p
+
$ git add -p
+

删除工作区文件,并且将这次删除放入暂存区

shell
$ git rm [file1] [file2] ...
+
$ git rm [file1] [file2] ...
+

停止追踪指定文件,但该文件会保留在工作区

shell
$ git rm --cached [file]
+
$ git rm --cached [file]
+

改名文件,并且将这个改名放入暂存区

shell
$ git mv [file-original] [file-renamed]
+
$ git mv [file-original] [file-renamed]
+

代码提交

提交暂存区到仓库区

shell
$ git commit -m [message]
+
$ git commit -m [message]
+

提交工作区自上次 commit 之后的变化,直接到仓库区

shell
$ git commit -a
+
$ git commit -a
+

提交时显示所有 diff 信息

shell
$ git commit -v
+
$ git commit -v
+

使用一次新的 commit,替代上一次提交 如果代码没有任何新变化,则用来改写上一次 commit 的提交信息

shell
$ git commit --amend -m [message]
+
$ git commit --amend -m [message]
+

重做上一次 commit,并包括指定文件的新变化

shell
$ git commit --amend [file1] [file2] ...
+
$ git commit --amend [file1] [file2] ...
+

查看信息

显示有变更的文件

shell
$ git status
+
$ git status
+

显示当前分支的版本历史

shell
$ git log
+
$ git log
+

显示 commit 历史,以及每次 commit 发生变更的文件

shell
$ git log --stat
+
$ git log --stat
+

搜索提交历史,根据关键词

shell
$ git log -S [keyword]
+
$ git log -S [keyword]
+

显示某个 commit 之后的所有变动,每个 commit 占据一行

shell
$ git log [tag] HEAD --pretty=format:%s
+
$ git log [tag] HEAD --pretty=format:%s
+

显示某个 commit 之后的所有变动,其"提交说明"必须符合搜索条件

shell
$ git log [tag] HEAD --grep feature
+
$ git log [tag] HEAD --grep feature
+

显示某个文件的版本历史,包括文件改名

shell
$ git log --follow [file]
+
$ git log --follow [file]
+

显示指定文件相关的每一次 diff

shell
$ git log -p [file]
+
$ git log -p [file]
+

显示过去 5 次提交

shell
$ git log -5 --pretty --oneline
+
$ git log -5 --pretty --oneline
+

显示所有提交过的用户,按提交次数排序

shell
$ git shortlog -sn
+
$ git shortlog -sn
+

显示指定文件是什么人在什么时间修改过

shell
$ git blame [file]
+
$ git blame [file]
+

显示暂存区和工作区的差异

shell
$ git diff
+
$ git diff
+

显示暂存区和上一个 commit 的差异

shell
$ git diff --cached [file]
+
$ git diff --cached [file]
+

显示工作区与当前分支最新 commit 之间的差异

shell
$ git diff HEAD
+
$ git diff HEAD
+

显示两次提交之间的差异

shell
$ git diff [first-branch]...[second-branch]
+
$ git diff [first-branch]...[second-branch]
+

显示今天你写了多少行代码

shell
$ git diff --shortstat "@{0 day ago}"
+
$ git diff --shortstat "@{0 day ago}"
+

显示某次提交的元数据和内容变化

shell
$ git show [commit]
+
$ git show [commit]
+

显示某次提交发生变化的文件

shell
$ git show --name-only [commit]
+
$ git show --name-only [commit]
+

显示某次提交时,某个文件的内容

shell
$ git show [commit]:[filename]
+
$ git show [commit]:[filename]
+

显示当前分支的最近几次提交

shell
$ git reflog
+
$ git reflog
+

分支

列出所有本地分支

shell
$ git branch
+
$ git branch
+

列出所有远程分支

shell
$ git branch -r
+
$ git branch -r
+

列出所有本地分支和远程分支

shell
$ git branch -a
+
$ git branch -a
+

新建一个分支,但依然停留在当前分支

shell
$ git branch [branch-name]
+
$ git branch [branch-name]
+

新建一个分支,并切换到该分支

shell
$ git checkout -b [branch]
+
$ git checkout -b [branch]
+

新建一个分支,指向指定 commit

shell
$ git branch [branch] [commit]
+
$ git branch [branch] [commit]
+

新建一个分支,与指定的远程分支建立追踪关系

shell
$ git branch --track [branch] [remote-branch]
+
$ git branch --track [branch] [remote-branch]
+

切换到指定分支,并更新工作区

shell
$ git checkout [branch-name]
+
$ git checkout [branch-name]
+

切换到上一个分支

shell
$ git checkout -
+
$ git checkout -
+

建立追踪关系,在现有分支与指定的远程分支之间

shell
$ git branch --set-upstream [branch] [remote-branch]
+
$ git branch --set-upstream [branch] [remote-branch]
+

合并指定分支到当前分支

shell
$ git merge [branch]
+
$ git merge [branch]
+

选择一个 commit,合并进当前分支

shell
$ git cherry-pick [commit]
+
$ git cherry-pick [commit]
+

删除分支

shell
$ git branch -d [branch-name]
+
$ git branch -d [branch-name]
+

删除远程分支

shell
$ git push origin --delete [branch-name]
+
$ git push origin --delete [branch-name]
+

标签

列出所有 tag

shell
$ git tag
+
$ git tag
+

新建一个 tag 在当前 commit

shell
$ git tag [tag]
+
$ git tag [tag]
+

新建一个 tag 在指定 commit

shell
$ git tag [tag] [commit]
+
$ git tag [tag] [commit]
+

删除本地 tag

shell
$ git tag -d [tag]
+
$ git tag -d [tag]
+

删除远程 tag

shell
$ git push origin :refs/tags/[tagName]
+
$ git push origin :refs/tags/[tagName]
+

查看 tag 信息

shell
$ git show [tag]
+
$ git show [tag]
+

提交指定 tag

shell
$ git push [remote] [tag]
+
$ git push [remote] [tag]
+

提交所有 tag

shell
$ git push [remote] --tags
+
$ git push [remote] --tags
+

新建一个分支,指向某个 tag

shell
$ git checkout -b [branch] [tag]
+
$ git checkout -b [branch] [tag]
+

远程同步

下载远程仓库的所有变动

shell
$ git fetch [remote]
+
$ git fetch [remote]
+

显示所有远程仓库

shell
$ git remote -v
+
$ git remote -v
+

显示某个远程仓库的信息

shell
$ git remote show [remote]
+
$ git remote show [remote]
+

增加一个新的远程仓库,并命名

shell
$ git remote add [shortname] [url]
+
$ git remote add [shortname] [url]
+

取回远程仓库的变化,并与本地分支合并

shell
$ git pull [remote] [branch]
+
+$ git pull origin ding-dev
+
$ git pull [remote] [branch]
+
+$ git pull origin ding-dev
+

允许不相关历史提交,并强制合并

shell
$ git pull origin master --allow-unrelated-histories
+
$ git pull origin master --allow-unrelated-histories
+

上传本地指定分支到远程仓库

shell
$ git push [remote] [branch]
+
$ git push [remote] [branch]
+

强行推送当前分支到远程仓库,即使有冲突

shell
$ git push [remote] --force
+
$ git push [remote] --force
+

推送所有分支到远程仓库

shell
$ git push [remote] --all
+
$ git push [remote] --all
+

撤销

恢复暂存区的指定文件到工作区

shell
$ git checkout [file]
+
$ git checkout [file]
+

恢复某个 commit 的指定文件到暂存区和工作区

shell
$ git checkout [commit] [file]
+
$ git checkout [commit] [file]
+

恢复暂存区的所有文件到工作区

shell
$ git checkout .
+
$ git checkout .
+

重置暂存区的指定文件,与上一次 commit 保持一致,但工作区不变

shell
$ git reset [file]
+
$ git reset [file]
+

重置暂存区与工作区,与上一次 commit 保持一致

shell
$ git reset --hard
+
$ git reset --hard
+

重置当前分支的指针为指定 commit,同时重置暂存区,但工作区不变

shell
$ git reset [commit]
+
$ git reset [commit]
+

版本回退:重置当前分支的 HEAD 为指定 commit,同时重置暂存区和工作区,与指定 commit 一致

shell
$ git reset --hard [commit]
+
$ git reset --hard [commit]
+

重置当前 HEAD 为指定 commit,但保持暂存区和工作区不变

shell
$ git reset --keep [commit]
+
$ git reset --keep [commit]
+

新建一个 commit,用来撤销指定 commit > 后者的所有变化都将被前者抵消,并且应用到当前分支

shell
$ git revert [commit]
+
$ git revert [commit]
+

暂时将未提交的变化移除,稍后再移入

shell
$ git stash
+$ git stash pop
+
$ git stash
+$ git stash pop
+

reset:回到了三岁

revert:记住当前错误,继续往下走,对之前的进行重写

忽略文件配置(.gitignore)

1、配置语法:

以斜杠“/”开头表示目录;

以星号“*”通配多个字符;

以问号“?”通配单个字符

以方括号“[]”包含单个字符的匹配列表;

以叹号“!”表示不忽略(跟踪)匹配到的文件或目录;

此外,git 对于 .ignore 配置文件是按行从上到下进行规则匹配的,意味着如果前面的规则匹配的范围更大,则后面的规则将不会生效;

2、示例:

(1)规则:fd1/*        说明:忽略目录 fd1 下的全部内容;注意,不管是根目录下的 /fd1/ 目录,还是某个子目录 /child/fd1/ 目录,都会被忽略;

(2)规则:/fd1/*        说明:忽略根目录下的 /fd1/ 目录的全部内容;

(3)规则:

/* !.gitignore !/fw/bin/ !/fw/sf/

说明:忽略全部内容,但是不忽略 .gitignore 文件、根目录下的 /fw/bin/ 和 /fw/sf/ 目录;

github创建仓库
+
1. 桌面打开命令行,克隆git clone ...
+2. 打开克隆好的文件夹,在里面输入命令
+3. 切换分支 git checkout -b first
+4. 写代码
+5. git add .
+6. git commit -m '内容提示'
+7. git push origin first
+切回主分支:git checkout master
+
1. 桌面打开命令行,克隆git clone ...
+2. 打开克隆好的文件夹,在里面输入命令
+3. 切换分支 git checkout -b first
+4. 写代码
+5. git add .
+6. git commit -m '内容提示'
+7. git push origin first
+切回主分支:git checkout master
+

参考资料

local(每一个人的计算机上面也有一个项目仓库)

配置用户名

git config --global user.name 你的名字

配置邮箱

git config --global user.emial 你的邮箱

查看配置

  • git config --list

  • git config user.name

  • git config user.email

+ + + + + \ No newline at end of file diff --git "a/fe-utils/js\345\267\245\345\205\267\345\272\223.html" "b/fe-utils/js\345\267\245\345\205\267\345\272\223.html" new file mode 100644 index 00000000..00c97499 --- /dev/null +++ "b/fe-utils/js\345\267\245\345\205\267\345\272\223.html" @@ -0,0 +1,298 @@ + + + + + + 前端 JavaScript 必会工具库合集 | Sunny's blog + + + + + + + + +
Skip to content
On this page

前端 JavaScript 必会工具库合集

工具库集合

jQuery: 让操作DOM变得更容易

官网:https://jquery.com/

中文网:https://jquery.cuishifeng.cn/

Lodash: 你能想到的工具函数它都帮你写了

官网:https://lodash.com/docs

中文网:https://www.lodashjs.com/

Animate.css: 常见的CSS动画效果都帮你写好了

官网:https://animate.style/

Axios:让网络请求变得更简单

官网:https://axios-http.com/zh/

MockJS:ajax拦截和模拟数据生成

官网:http://mockjs.com/

Moment:让日期处理更容易

官网:https://momentjs.com/

中文网:http://momentjs.cn/

ECharts:搞定所有你能想到的图表📈

官网:https://echarts.apache.org/zh

animejs:简单好用的JS动画库

官网:https://animejs.com/

editormd:markdown编辑器

官网:https://pandao.github.io/editor.md | |

validate:简单好用的JS对象验证库

官网:http://validatejs.org/

date-fns:功能和Moment几乎相同

官网:https://date-fns.org/

支持tree shaking

zepto:功能和jQuery几乎相同

官网:https://zeptojs.com/

对移动端支持更好,包体积更小

nprogress:简单好用的进度条插件YouTube就使用的是它

官网:https://github.com/rstacruz/nprogress

qs:一个用于解析url的小工具

官网:https://github.com/ljharb/qs

使用方式

对于第三方库,除了下载使用,还可以通过CDN在线使用

科普知识:CDN

CDN称之为内容分发网络(Content Delivery Network)。

简单来说,就是提供很多的服务器,用户访问时,自动就近选择服务器给用户提供资源

国内使用广泛的免费CDN站点:https://www.bootcdn.cn/

JQuery

官网:https://jquery.com/

中文网:https://jquery.cuishifeng.cn/

CDN:https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js

针对DOM的操作无非以下几种:

  • 获取它
  • 创建它
  • 监听它
  • 改变它

JQuery可以让上面整个过程更加轻松

jQuery函数

jQuery提供了一个函数,名称为jQuery,也可以写作$

该函数提供了强大的DOM控制能力

通过下面的示例,可以快速理解jQuery的核心功能

javascript
// 获取类样式为container的所有DOM
+const container = $(".container")
+
+// 获取container后面的兄弟元素,元素类样式必须包含list
+container.nextAll(".list");
+
+// 删除元素
+container.remove();
+
+// 找到所有类样式为list元素的后代li元素,给它们加上类样式item
+$(".list li").addClass("item");
+
+// 为所有p元素添加一些style
+$("p").css({ "color": "#ff0011", "background": "blue" });
+
+// 注册DOMContentLoaded事件
+$(function(){ 
+	// ...
+})
+
+// 给所有li元素注册点击事件
+$("li").click(function(){
+  // ...
+})
+
+// 创建一个a元素,设置其内容为link,然后将它作为子元素追加到类样式为.list的元素中
+$("<a>").text("link").appendTo(".list");
+
// 获取类样式为container的所有DOM
+const container = $(".container")
+
+// 获取container后面的兄弟元素,元素类样式必须包含list
+container.nextAll(".list");
+
+// 删除元素
+container.remove();
+
+// 找到所有类样式为list元素的后代li元素,给它们加上类样式item
+$(".list li").addClass("item");
+
+// 为所有p元素添加一些style
+$("p").css({ "color": "#ff0011", "background": "blue" });
+
+// 注册DOMContentLoaded事件
+$(function(){ 
+	// ...
+})
+
+// 给所有li元素注册点击事件
+$("li").click(function(){
+  // ...
+})
+
+// 创建一个a元素,设置其内容为link,然后将它作为子元素追加到类样式为.list的元素中
+$("<a>").text("link").appendTo(".list");
+

下面依次介绍jQuery中的核心概念,以便于文档查阅

jQuery对象和DOM对象

通过jQuery得到的元素是一个jQuery对象,而不是传统的DOM

jQuery对象是一个伪数组,它和DOM元素的关系如下

jQuery对象和DOM之间可以互相转换

javascript
// jQuery -> DOM
+jQuery对象[索引]
+jQuery对象.get(索引)
+
+// DOM -> jQuery
+$(DOM对象)
+
// jQuery -> DOM
+jQuery对象[索引]
+jQuery对象.get(索引)
+
+// DOM -> jQuery
+$(DOM对象)
+

官网文档中的目录

目录名内容
选择器选择器是一个字符串,用于描述要选中哪些元素
筛选在当前jQuery对象的基础上,进一步选中元素
文档处理更改HTML文档结构,例如删除元素、清空元素内容、改变元素之间的关系
属性控制元素属性,例如修改类样式、读取和设置文本框的value、读取和设置img的src
css控制元素style样式,例如改变字体颜色、设置背景、获取元素尺寸、获取和设置滚动位置
事件监听元素的事件,例如监听文档加载完成、监听元素被点击
ajaxjQuery封装了XHR,使ajax访问更加方便
这部分功能目前已被其他第三方库全面超越

Lodash

官网:https://lodash.com/docs

中文网:https://www.lodashjs.com/

CDN:https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js

Lodash是一个针对ES的古老工具库,它出现在ES5之前

Lodash提供了大量的API,弥补了ES中对象、函数、数组API不足的问题

你可以想到的大部分工具函数,都可以在Lodash中找到

如果你不编写框架或通用库,一般不会用到Lodash

Animate.css

官网:https://animate.style/

CDN:https://cdn.bootcdn.net/ajax/libs/animate.css/4.1.1/animate.min.css

Animate.css 库提供了大量的动画效果,开发者仅需使用它提供的类名即可

注意:animate.css中的动画对行盒无效

基本使用

类名格式为:

animate__animated animate__效果名
+
animate__animated animate__效果名
+

效果名分为以下几个大类,你可以从官网中找到对应的分类,每个分类下有多种效果名可供使用

分类含义
Attention seekers强调
Back entrances进入
Back exits退出
Bouncing entrances弹跳进入
Bouncing exits弹跳退出
Fading entrances淡入
Fading exits淡出
Flippers翻转
Lightspeed光速
Rotating entrances旋转进入
Rotating exits旋转退出
Specials特殊效果
Zooming entrances缩放进入
Zooming exits缩放退出
Sliding entrances滑动进入
Sliding exits滑动退出

工具类

Animate.css 还提供了多个工具类,可以控制动画的延时重复次数速度

  • 延时
css
/* 默认无延时 */
+animate__delay-1s  /* 延时1秒 */
+animate__delay-2s  /* 延时2秒 */
+animate__delay-3s  /* 延时3秒 */
+animate__delay-4s  /* 延时4秒 */
+animate__delay-5s  /* 延时5秒 */
+
/* 默认无延时 */
+animate__delay-1s  /* 延时1秒 */
+animate__delay-2s  /* 延时2秒 */
+animate__delay-3s  /* 延时3秒 */
+animate__delay-4s  /* 延时4秒 */
+animate__delay-5s  /* 延时5秒 */
+
  • 重复次数
css
/* 默认重复1次 */
+animate__repeat-2	/* 重复2次 */
+animate__repeat-3	/* 重复3次 */
+animate__infinite	/* 重复无限次 */
+
/* 默认重复1次 */
+animate__repeat-2	/* 重复2次 */
+animate__repeat-3	/* 重复3次 */
+animate__infinite	/* 重复无限次 */
+
  • 速度
css
/* 默认1秒内完成动画 */
+animate__slow /* 2秒内完成动画 */
+animate__slower	/* 3秒内完成动画 */
+animate__fast	/* 800毫秒内完成动画 */
+animate__faster	/* 500毫秒内完成动画 */
+
/* 默认1秒内完成动画 */
+animate__slow /* 2秒内完成动画 */
+animate__slower	/* 3秒内完成动画 */
+animate__fast	/* 800毫秒内完成动画 */
+animate__faster	/* 500毫秒内完成动画 */
+

示例:

html
<!-- 
+使用animate.css
+动画名:bounce
+速度:快
+重复:无限次
+延迟:1秒
+-->
+<h1
+    class="
+           animate__animated
+           animate__bounce
+           animate_fast
+           animate__infinite
+           animate__delay-1s
+           "
+    >
+  Hello Animate
+</h1>
+
<!-- 
+使用animate.css
+动画名:bounce
+速度:快
+重复:无限次
+延迟:1秒
+-->
+<h1
+    class="
+           animate__animated
+           animate__bounce
+           animate_fast
+           animate__infinite
+           animate__delay-1s
+           "
+    >
+  Hello Animate
+</h1>
+

Axios

官网:https://axios-http.com/zh/

CDN:https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js

axios是一个请求库,在浏览器环境中,它封装了XHR,提供更加便捷的API发送请求

基本使用

javascript
// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 get 请求到 https://study.duyiedu.com/api/user/exists?loginId=abc,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/user/exists", {
+  params: { // 这里可以配置 query,axios会自动将其进行Url编码
+    loginId: "abc"
+  },
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 post 请求到 https://study.duyiedu.com/api/user/reg
+// axios 会将第二个参数转换为JSON格式的请求体
+axios.post("https://study.duyiedu.com/api/user/reg", {
+  loginId: 'abc',
+  loginPwd: '123123',
+  nickname: '棒棒鸡'
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 get 请求到 https://study.duyiedu.com/api/user/exists?loginId=abc,输出响应体的内容
+axios.get("https://study.duyiedu.com/api/user/exists", {
+  params: { // 这里可以配置 query,axios会自动将其进行Url编码
+    loginId: "abc"
+  },
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
+// 发送 post 请求到 https://study.duyiedu.com/api/user/reg
+// axios 会将第二个参数转换为JSON格式的请求体
+axios.post("https://study.duyiedu.com/api/user/reg", {
+  loginId: 'abc',
+  loginPwd: '123123',
+  nickname: '棒棒鸡'
+}).then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+

axios的基本用法为:

javascript
axios.get(url地址, [请求配置]);
+axios.post(url地址, [请求体对象], [请求配置]);
+
+// 或直接使用 axios 方法,在请求配置中填写请求方法
+axios(请求配置);
+
axios.get(url地址, [请求配置]);
+axios.post(url地址, [请求体对象], [请求配置]);
+
+// 或直接使用 axios 方法,在请求配置中填写请求方法
+axios(请求配置);
+

实例

axios允许开发者先创建一个实例,后续通过使用实例进行请求

这样做的好处是可以预先进行某些配置

示例:

javascript
// 创建实例
+const instance = axios.create({
+  baseURL: 'https://study.duyiedu.com/'
+});
+
+// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+instance.get("/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+
// 创建实例
+const instance = axios.create({
+  baseURL: 'https://study.duyiedu.com/'
+});
+
+// 发送 get 请求到 https://study.duyiedu.com/api/herolist,输出响应体的内容
+instance.get("/api/herolist").then(resp=>{
+  console.log(resp.data); // resp.data 为响应体的数据,axios会自动解析JSON格式
+})
+

拦截器

有时,我们可能需要对所有的请求或响应做一些统一的处理

比如,在请求时,如果发现本地有token,需要附带到请求头

又比如,在拿到响应后,我们仅需要取响应体中的data属性

再比如,如果发生错误,我们需要做一个弹窗显示

这些统一的行为就非常适合使用拦截器

javascript
// 添加请求拦截器
+axios.interceptors.request.use(function (config) {
+  // config 为当前的请求配置
+  // 在发送请求之前做些什么
+  // 这里,我们添加一个请求头
+  const token = localStorage.getItem('token');
+  if(token){
+    config.headers.authorization = token;
+  }
+  return config; // 返回处理后的配置
+});
+
+// 添加响应拦截器
+axios.interceptors.response.use(function (resp) {
+  // 2xx 范围内的状态码都会触发该函数。
+  // 对响应数据做点什么
+  return resp.data.data; // 仅得到响应体中的data属性
+}, function (error) {
+  // 超出 2xx 范围的状态码都会触发该函数。
+  // 对响应错误做点什么
+  alert(error.message); // 弹出错误消息
+});
+
// 添加请求拦截器
+axios.interceptors.request.use(function (config) {
+  // config 为当前的请求配置
+  // 在发送请求之前做些什么
+  // 这里,我们添加一个请求头
+  const token = localStorage.getItem('token');
+  if(token){
+    config.headers.authorization = token;
+  }
+  return config; // 返回处理后的配置
+});
+
+// 添加响应拦截器
+axios.interceptors.response.use(function (resp) {
+  // 2xx 范围内的状态码都会触发该函数。
+  // 对响应数据做点什么
+  return resp.data.data; // 仅得到响应体中的data属性
+}, function (error) {
+  // 超出 2xx 范围的状态码都会触发该函数。
+  // 对响应错误做点什么
+  alert(error.message); // 弹出错误消息
+});
+

设置好拦截器后,后续的请求和响应都会触发对应的函数

拦截器可以针对axios实例进行设置

MockJS

官网:http://mockjs.com/

CDN:https://cdn.bootcdn.net/ajax/libs/Mock.js/1.0.0/mock-min.js

MockJS有两个作用:

  1. 产生模拟数据
  2. 拦截Ajax

下面两张图说明MockJS的作用

仅模拟数据

javascript
Mock.mock(数据模板)
+
Mock.mock(数据模板)
+

数据模板有其特有的书写规范,具体写法见官网

拦截+模拟数据

javascript
Mock.mock(要拦截的url, 要拦截的请求方法, 数据模板)
+
Mock.mock(要拦截的url, 要拦截的请求方法, 数据模板)
+

更多用法见官网

注意,MockJS拦截数据的原理是重写了XHR,因此它仅能拦截XHR的数据请求,而无法拦截使用fetch发出的请求

具体的,MockJS可以拦截:

  • 原生XmlHttpRequest
  • jQuery中的$.ajax
  • axios

MockJS可以模拟网络延时,用法为:

javascript
Mock.setup({
+  timeout: 400 // 网络延时400毫秒
+})
+
+Mock.setup({
+  timeout: '200-600' // 网络延时200-600毫秒
+})
+
Mock.setup({
+  timeout: 400 // 网络延时400毫秒
+})
+
+Mock.setup({
+  timeout: '200-600' // 网络延时200-600毫秒
+})
+

Moment

官网:https://momentjs.com/

中文网:http://momentjs.cn/

CDN:https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js

各种语言包:https://www.bootcdn.cn/moment.js/

Moment提供了强大的日期处理能力

时间基础知识

单位

单位名称换算
hour小时1 day = 24 hours
minute分钟1 hour = 60 minutes
second1 minute = 60 seconds
millisecond (ms)毫秒1 second = 1000 ms
nanosecond (ns)纳秒1 ms = 1000 ns

GMT和UTC

世界划分为24个时区,北京在东8区,格林威治在0时区。

GMT:Greenwish Mean Time 格林威治世界时。太阳时,精确到毫秒。

UTC:Universal Time Coodinated 世界协调时。以原子时间为计时标准,精确到纳秒。

国际标准中,已全面使用UTC时间,而不再使用GMT时间

GMT和UTC时间在文本表示格式上是一致的,均为星期缩写, 日期 月份 年份 时间 GMT,例如:

Thu, 27 Aug 2020 08:01:44 GMT
+
Thu, 27 Aug 2020 08:01:44 GMT
+

另外,ISO 8601标准规定,建议使用以下方式表示时间:

YYYY-MM-DDTHH:mm:ss.msZ
+例如:
+2020-08-27T08:01:44.000Z
+
YYYY-MM-DDTHH:mm:ss.msZ
+例如:
+2020-08-27T08:01:44.000Z
+

GMT、UTC、ISO 8601都表示的是零时区的时间

Unix 时间戳

Unix 时间戳(Unix Timestamp)是Unix系统最早提出的概念

它将UTC时间1970年1月1日凌晨作为起始时间,到指定时间经过的秒数(毫秒数)

程序中的时间处理

程序对时间的计算、存储务必使用UTC时间,或者时间戳

在和用户交互时,将UTC时间或时间戳转换为更加友好的文本

思考下面的问题:

  1. 用户的生日是本地时间还是UTC时间?
  2. 如果要比较两个日期的大小,是比较本地时间还是比较UTC时间?
  3. 如果要显示文章的发布日期,是显示本地时间还是显示UTC时间?
  4. 北京时间2020-8-28 10:00:00格林威治2020-8-28 02:00:00,两个时间哪个大,哪个小?
  5. 北京的时间戳为0格林威治的时间戳为0,它们的时间一样吗?
  6. 一个中国用户注册时填写的生日是1970-1-1,它出生的UTC时间是多少?时间戳是多少?

Moment的核心用法

Moment的使用分为两个部分:

  1. 获得Moment对象
  2. 针对Moment对象做各种操作
+ + + + + \ No newline at end of file diff --git a/fe-utils/tool.html b/fe-utils/tool.html new file mode 100644 index 00000000..e6ec2a54 --- /dev/null +++ b/fe-utils/tool.html @@ -0,0 +1,20 @@ + + + + + + 涌现出来的新tools | Sunny's blog + + + + + + + + +
Skip to content
On this page

涌现出来的新tools

apifox:一款国产的 API 管理神器

集成了 Postman + Swagger + Mock + JMeter 众多功能

可以让生成 api 文档、api调试、api Mock、api 自动化测试 变的十分高效,简单。

eolink | 国内第一个集Swagger+Postman+Mock+Jmeter单点工具于一身的 api 管理平台 | 以 api 为中心的前后端开发流程

+ + + + + \ No newline at end of file diff --git a/fragment/Monorepo.html b/fragment/Monorepo.html new file mode 100644 index 00000000..040cf940 --- /dev/null +++ b/fragment/Monorepo.html @@ -0,0 +1,20 @@ + + + + + + monorepo | Sunny's blog + + + + + + + + +
Skip to content
On this page

monorepo

npm 的 install 流程

package-lock.json

package-lock.json 的作用是进行锁版本号,保证整个开发团队的版本号统一,使用 monorepo 的项目有可能会提到一个最外层进行一个管理。

  • 为什么需要这个 package-lock.json package.json 的 semantic versioning(语意化版本控制),在不同的时间会安装不同的版本,如果没有 package-lock.json,不同的开发者可能就会得到不同版本的依赖,如果因为这个出现了一个 bug 的话,那排查起来也许会非常困难。
  • 更新规则 Npm v 5.4.2 以上:当 package.json 声明的版本依赖规范和 package-lock.json 安装版本兼容,则根据 package-lock json 安装依赖:如果两者不兼容,那么按照 package.json 安装依赖,并更新 package- lock.json 跟随 package.json 的语意化版本控制来进行更新,如果 package.json 中的依赖 a 的版本是^1.0.0,在这个时候如果 package-lock.json 中的版本锁定为 1.12.1 就是符合要求的不必重写,但如果是 0.12.11 即第一个数字变了,那就需要重写 package-lock.json 了。
  • 版本规则
    • ^: 只会执行不更改最左边非零数字的更新。 如果写入的是 ^0.13.0,则当运行 npm update 时,可以更新到 0.13.1、0.13.2 等,但不能更新到 0.14.0 或更高版本。 如果写入的是 ^1.13.0,则当运行 npm update 时,可以更新到 1.13.1、1.14.0 等,但不能更新到 2.0.0 或更高版本。
    • ~: 如果写入的是 〜0.13.0,则当运行 npm update 时,会更新到补丁版本:即 0.13.1 可以,但 0.14.0 不可以。
    • : 接受高于指定版本的任何版本。

    • =: 接受等于或高于指定版本的任何版本。

    • =: 接受确切的版本。
    • -: 接受一定范围的版本。例如:2.1.0 - 2.6.2。
    • ||: 组合集合。例如 < 2.1 || > 2.6。

npm 和 yarn

缺点。下面将两者进行比较

  1. 性能

每当 Yarn 或 npm 需要安装包时,它们都会执行一系列任务。在 npm 中,这些任务是按包顺序执行的,这意味着它会等待一个包完全安装,然后再继续下一个。相比之下,Yarn 并行执行这些任务,从而提高了性能。 虽然这两个管理器都提供缓存机制,但 Yarn 似乎做得更好一些。 尽管 Yarn 有一些优势,但 Yarn 和 npm 在它们的最新版本中的速度相当。所以我们不能评判孰优孰劣。

  1. 依赖版本

早期的时候 yarn 有 yarn.lock 来锁定版本,这一点上比 package.json 要强很多,而后面 npm 也推出了 package-lock.json,所以这一点上已经没太多差异了。

  1. 安全性

从版本 6 开始,npm 会在安装过程中审核软件包并告诉您是否发现了任何漏洞。我们可以通过 npm audit 针对已安装的软件包运行来手动执行此检查。如果发现任何漏洞,npm 会给我们安全建议。 Yarn 和 npm 都使用加密哈希算法来确保包的完整性。

  1. 工作区

工作区允许您拥有一个 monorepo 来管理跨多个项目的依赖项。这意味着您有一个单一的顶级根包,其中包含多个称为工作区的子包。

  1. 用哪个?

目前 2021 年,yarn 的安装速度还是比 npm 快,其他地方的差异并不大,基本上可以忽略,用哪个都行。

monorepo

monorepo 方案的优势

  1. 代码重用将变得非常容易:由于所有的项目代码都集中于一个代码仓库,我们将很容易抽离出各个项目共用的业务组件或工具,并通过 TypeScript,Lerna 或其他工具进行代码内引用;
  2. 依赖管理将变得非常简单:同理,由于项目之间的引用路径内化在同一个仓库之中,我们很容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用一些工具,我们将很容易地做到版本依赖管理和版本号自动升级;
  3. 代码重构将变得非常便捷:想想究竟是什么在阻止您进行代码重构,很多时候,原因来自于「不确定性」,您不确定对某个项目的修改是否对于其他项目而言是「致命的」,出于对未知的恐惧,您会倾向于不重构代码,这将导致整个项目代码的腐烂度会以惊人的速度增长。而在 monorepo 策略的指导下,您能够明确知道您的代码的影响范围,并且能够对被影响的项目可以进行统一的测试,这会鼓励您不断优化代码;
  4. 它倡导了一种开放,透明,共享的组织文化,这有利于开发者成长,代码质量的提升:在 monorepo 策略下,每个开发者都被鼓励去查看,修改他人的代码(只要有必要),同时,也会激起开发者维护代码,和编写单元测试的责任心(毕竟朋友来访之前,我们从不介意自己的房子究竟有多乱),这将会形成一种良性的技术氛围,从而保障整个组织的代码质量。

monorepo 方案的劣势

  1. 项目粒度的权限管理变得非常复杂:无论是 Git 还是其他 VCS 系统,在支持 monorepo 策略中项目粒度的权限管理上都没有令人满意的方案,这意味着 A 部门的 a 项目若是不想被 B 部门的开发者看到就很难了。(好在我们可以将 monorepo 策略实践在「项目级」这个层次上,这才是我们这篇文章的主题,我们后面会再次明确它);
  2. 新员工的学习成本变高:不同于一个项目一个代码仓库这种模式下,组织新人只要熟悉特定代码仓库下的代码逻辑,在 monorepo 策略下,新人可能不得不花更多精力来理清各个代码仓库之间的相互逻辑,当然这个成本可以通过新人文档的方式来解决,但维护文档的新鲜又需要消耗额外的人力;
  3. 对于公司级别的 monorepo 策略而言,需要专门的 VFS 系统,自动重构工具的支持:设想一下 Google 这样的企业是如何将十亿行的代码存储在一个仓库之中的?开发人员每次拉取代码需要等待多久?各个项目代码之间又如何实现权限管理,敏捷发布?任何简单的策略乘以足够的规模量级都会产生一个奇迹(不管是好是坏),对于中小企业而言,如果没有像 Google,Facebook 这样雄厚的人力资源,把所有项目代码放在同一个仓库里这个美好的愿望就只能是个空中楼阁。

如何取舍?

没错,软件开发领域从来没有「银弹」。monorepo 策略也并不完美,并且,我在实践中发现,要想完美在组织中运用 monorepo 策略,所需要的不仅是出色的编程技巧和耐心。团队日程,组织文化和个人影响力相互碰撞的最终结果才决定了想法最终是否能被实现。

但是请别灰心的太早,因为虽然让组织作出改变,统一施行 monorepo 策略困难重重,但这却并不意味着我们需要彻底跟 monorepo 策略说再见。我们还可以把 monorepo 策略实践在「项目」这个级别,即从逻辑上确定项目与项目之间的关联性,然后把相关联的项目整合在同一个仓库下,通常情况下,我们不会有太多相互关联的项目,这意味着我们能够免费得到 monorepo 策略的所有好处,并且可以拒绝支付大型 monorepo 架构的利息。

+ + + + + \ No newline at end of file diff --git a/fragment/api-no-repeat.html b/fragment/api-no-repeat.html new file mode 100644 index 00000000..9821a1df --- /dev/null +++ b/fragment/api-no-repeat.html @@ -0,0 +1,316 @@ + + + + + + 前端接口防止重复请求实现方案 | Sunny's blog + + + + + + + + +
Skip to content
On this page

前端接口防止重复请求实现方案

前言

前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加限制吗?前端加限制能够拦截的毕竟有限。可老板就是执意要前端搞一下子,行吧,搞就搞吧。 虽然大部分的接口处理我们都是加了 loading 的,但又不能确保真的是每个接口都加了的,可是如果要一个接口一个接口的排查,那这维护了四五年的系统,成百上千的接口肯定要耗费非常多的精力,根本就是不现实的,所以就只能去做 全局处理 。下面就来总结一下这次的防重复请求的实现方案:

方案一:使用 axios 拦截器

这个方案是最容易想到也是最 朴实无华 的一个方案:通过使用 axios 拦截器,在 请求拦截器 中开启全屏 Loading,然后在 响应拦截器 中将 Loading 关闭。

这个方案固然已经可以满足我们目前的需求,但不管三七二十一,直接搞个全屏 Loading 还是 不太美观 ,何况在目前项目的接口处理逻辑中还有一些 局部 Loading ,就有可能会出现 Loading 套 Loading 的情况,两个圈一起转,头皮发麻。

方案二:根据请求生成对应的 key

加 Loading 的方案不太友好,而对于同一个接口,如果传参都是一样的,一般来说都没有必要连续请求多次吧。那我们可不可以通过代码逻辑直接 把完全相同的请求给拦截掉 ,不让它到达服务端呢?这个思路不错,我们说干就干。 首先,我们要判断 什么样的请求属于是相同请求 :

一个请求包含的内容不外乎就是 请求方法 , 地址 , 参数 以及请求发出的 页面 hash 。那我们是不是就可以根据这几个数据把这个请求生成一个 key 来作为这个 请求的标识 呢?

js
// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+
// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+

有了请求的 key,我们就可以在请求拦截器中把每次发起的请求给 收集起来 ,后续如果有相同请求进来,那都去这个集合中去比对, 如果已经存在了,说明就是一个重复的请求 ,我们就给拦截掉。当请求完成响应后,再将这个请求从集合中移除。 合理,nice! 具体实现如下:

是不是觉得这种方案还不错,万事大吉?no,no,no! 这个方案虽然理论上是解决了接口防重复请求这个问题,但是它会引发更多的问题。比如,我有这样一个接口处理:

这里我连续点击了 4 次按钮,可以看到,的确是只有一个请求发送出去,可是因为在代码逻辑中,我们对错误进行了一些处理,所以就将报错消息提示了 3 次,这样是很不友好的,而且,如果在错误捕获中有做更多的逻辑处理,那么很有可能会导致整个程序的异常。

而且,这种方案还会有另外一个 比较严重的问题 :我们在上面在生成请求 key 的时候把 hash 考虑进去了( 如果是 history 路由,可以将 pathname 加入生成 key ),这是因为项目中会有一些数据字典型的接口,这些接口可能有不同页面都需要去调用,如果第一个页面请求的字典接口比较慢,第二个页面的接口就被拦截了,最后就会导致第二个页面逻辑错误。那么这么一看,我们生成 key 的时候加入了 hash ,讲道理就没问题了呀。

可是倘若我这 两个请求是来自同一个页面 呢?比如,一个页面同时加载两个组件,而这两个组件都需要调用某个接口时:

那么此时, 后调接口的组件就无法拿到正确数据了 。啊这,真是难顶!

方案三:相同的请求共享

延续我们方案二的前面思路,仍然是拦截相同请求,但这次我们可不可以 不直接把请求挂掉 ,而是 对于相同的请求我们先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求 。

思路我们已经明确了,但这里有几个需要注意的点:

  • 我们在拿到响应结果后,返回给之前我们 挂起的请求 时,我们要用到 发布订阅模式 (日常在面试题中看到,这次终于让我给用上了( ^▽^ ))
  • 对于挂起的请求,我们需要将它 拦截 ,不能让它执行正常的请求逻辑,所以一定要在 请求拦截器 中通过 return Promise.reject() 来直接中断请求,并做一些 特殊的标记 ,以便于 在响应拦截器中进行特殊处理 。
js
import axios from "axios";
+
+let instance = axios.create({
+  baseURL: "/api/",
+});
+
+// 发布订阅
+class EventEmitter {
+  constructor() {
+    this.event = {};
+  }
+  on(type, cbres, cbrej) {
+    if (!this.event[type]) {
+      this.event[type] = [[cbres, cbrej]];
+    } else {
+      this.event[type].push([cbres, cbrej]);
+    }
+  }
+
+  emit(type, res, ansType) {
+    if (!this.event[type]) return;
+    else {
+      this.event[type].forEach((cbArr) => {
+        if (ansType === "resolve") {
+          cbArr[0](res);
+        } else {
+          cbArr[1](res);
+        }
+      });
+    }
+  }
+}
+
+// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+
+// 存储已发送但未响应的请求
+const pendingRequest = new Set();
+// 发布订阅容器
+const ev = new EventEmitter();
+
+// 添加请求拦截器
+instance.interceptors.request.use(
+  async (config) => {
+    let hash = location.hash;
+    // 生成请求Key
+    let reqKey = generateReqKey(config, hash);
+
+    if (pendingRequest.has(reqKey)) {
+      // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
+      // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
+      let res = null;
+      try {
+        // 接口成功响应
+        res = await new Promise((resolve, reject) => {
+          ev.on(reqKey, resolve, reject);
+        });
+        return Promise.reject({
+          type: "limiteResSuccess",
+          val: res,
+        });
+      } catch (limitFunErr) {
+        // 接口报错
+        return Promise.reject({
+          type: "limiteResError",
+          val: limitFunErr,
+        });
+      }
+    } else {
+      // 将请求的key保存在config
+      config.pendKey = reqKey;
+      pendingRequest.add(reqKey);
+    }
+
+    return config;
+  },
+  function (error) {
+    return Promise.reject(error);
+  }
+);
+
+// 添加响应拦截器
+instance.interceptors.response.use(
+  function (response) {
+    // 将拿到的结果发布给其他相同的接口
+    handleSuccessResponse_limit(response);
+    return response;
+  },
+  function (error) {
+    return handleErrorResponse_limit(error);
+  }
+);
+
+// 接口响应成功
+function handleSuccessResponse_limit(response) {
+  const reqKey = response.config.pendKey;
+  if (pendingRequest.has(reqKey)) {
+    let x = null;
+    try {
+      x = JSON.parse(JSON.stringify(response));
+    } catch (e) {
+      x = response;
+    }
+    pendingRequest.delete(reqKey);
+    ev.emit(reqKey, x, "resolve");
+    delete ev.reqKey;
+  }
+}
+
+// 接口走失败响应
+function handleErrorResponse_limit(error) {
+  if (error.type && error.type === "limiteResSuccess") {
+    return Promise.resolve(error.val);
+  } else if (error.type && error.type === "limiteResError") {
+    return Promise.reject(error.val);
+  } else {
+    const reqKey = error.config.pendKey;
+    if (pendingRequest.has(reqKey)) {
+      let x = null;
+      try {
+        x = JSON.parse(JSON.stringify(error));
+      } catch (e) {
+        x = error;
+      }
+      pendingRequest.delete(reqKey);
+      ev.emit(reqKey, x, "reject");
+      delete ev.reqKey;
+    }
+  }
+  return Promise.reject(error);
+}
+
+export default instance;
+
import axios from "axios";
+
+let instance = axios.create({
+  baseURL: "/api/",
+});
+
+// 发布订阅
+class EventEmitter {
+  constructor() {
+    this.event = {};
+  }
+  on(type, cbres, cbrej) {
+    if (!this.event[type]) {
+      this.event[type] = [[cbres, cbrej]];
+    } else {
+      this.event[type].push([cbres, cbrej]);
+    }
+  }
+
+  emit(type, res, ansType) {
+    if (!this.event[type]) return;
+    else {
+      this.event[type].forEach((cbArr) => {
+        if (ansType === "resolve") {
+          cbArr[0](res);
+        } else {
+          cbArr[1](res);
+        }
+      });
+    }
+  }
+}
+
+// 根据请求生成对应的key
+function generateReqKey(config, hash) {
+  const { method, url, params, data } = config;
+  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
+    "&"
+  );
+}
+
+// 存储已发送但未响应的请求
+const pendingRequest = new Set();
+// 发布订阅容器
+const ev = new EventEmitter();
+
+// 添加请求拦截器
+instance.interceptors.request.use(
+  async (config) => {
+    let hash = location.hash;
+    // 生成请求Key
+    let reqKey = generateReqKey(config, hash);
+
+    if (pendingRequest.has(reqKey)) {
+      // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
+      // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
+      let res = null;
+      try {
+        // 接口成功响应
+        res = await new Promise((resolve, reject) => {
+          ev.on(reqKey, resolve, reject);
+        });
+        return Promise.reject({
+          type: "limiteResSuccess",
+          val: res,
+        });
+      } catch (limitFunErr) {
+        // 接口报错
+        return Promise.reject({
+          type: "limiteResError",
+          val: limitFunErr,
+        });
+      }
+    } else {
+      // 将请求的key保存在config
+      config.pendKey = reqKey;
+      pendingRequest.add(reqKey);
+    }
+
+    return config;
+  },
+  function (error) {
+    return Promise.reject(error);
+  }
+);
+
+// 添加响应拦截器
+instance.interceptors.response.use(
+  function (response) {
+    // 将拿到的结果发布给其他相同的接口
+    handleSuccessResponse_limit(response);
+    return response;
+  },
+  function (error) {
+    return handleErrorResponse_limit(error);
+  }
+);
+
+// 接口响应成功
+function handleSuccessResponse_limit(response) {
+  const reqKey = response.config.pendKey;
+  if (pendingRequest.has(reqKey)) {
+    let x = null;
+    try {
+      x = JSON.parse(JSON.stringify(response));
+    } catch (e) {
+      x = response;
+    }
+    pendingRequest.delete(reqKey);
+    ev.emit(reqKey, x, "resolve");
+    delete ev.reqKey;
+  }
+}
+
+// 接口走失败响应
+function handleErrorResponse_limit(error) {
+  if (error.type && error.type === "limiteResSuccess") {
+    return Promise.resolve(error.val);
+  } else if (error.type && error.type === "limiteResError") {
+    return Promise.reject(error.val);
+  } else {
+    const reqKey = error.config.pendKey;
+    if (pendingRequest.has(reqKey)) {
+      let x = null;
+      try {
+        x = JSON.parse(JSON.stringify(error));
+      } catch (e) {
+        x = error;
+      }
+      pendingRequest.delete(reqKey);
+      ev.emit(reqKey, x, "reject");
+      delete ev.reqKey;
+    }
+  }
+  return Promise.reject(error);
+}
+
+export default instance;
+

补充

到这里,这么一通操作下来上面的代码讲道理是万无一失了,但不得不说,线上的情况仍然是 复杂多样 的。而其中一个比较特殊的情况就是 文件上传 。

我在这里是上传了两个 不同的文件 的,但只调用了一次上传接口。按理说是两个不同的请求,可为什么会被我们前面写的逻辑给拦截掉一个呢?

可以看到, 请求体 data 中的数据是 FormData 类型 ,而我们在生成 请求 key 的时候,是通过 JSON.stringify 方法进行操作的,而对于 FormData 类型 的数据执行该函数得到的只有 {} 。所以,对于文件上传,尽管我们上传了不同的文件,但它们所发出的请求生成的 key 都是一样的,这么一来就触发了我们前面的拦截机制。 那么我们接下来我们只需要在我们原来的拦截逻辑中判断一下请求体的数据类型即可,如果含有 FormData 类型 的数据,我们就 直接放行 不再关注这个请求就是了。

js
function isFileUploadApi(config) {
+  return Object.prototype.toString.call(config.data) === "[object FormData]";
+}
+
function isFileUploadApi(config) {
+  return Object.prototype.toString.call(config.data) === "[object FormData]";
+}
+

demo

+ + + + + \ No newline at end of file diff --git a/fragment/auto-try-catch.html b/fragment/auto-try-catch.html new file mode 100644 index 00000000..a244b80e --- /dev/null +++ b/fragment/auto-try-catch.html @@ -0,0 +1,898 @@ + + + + + + 如何给所有的 async 函数添加 try/catch? | Sunny's blog + + + + + + + + +
Skip to content
On this page

如何给所有的 async 函数添加 try/catch?

前言

阿里三面的时候被问到了这个问题,当时思路虽然正确,可惜表述的不够清晰

后来花了一些时间整理了下思路,那么如何实现给所有的 async 函数添加 try/catch 呢?

async 如果不加 try/catch 会发生什么事?

js
// 示例
+async function fn() {
+  let value = await new Promise((resolve, reject) => {
+    reject("failure");
+  });
+  console.log("do something...");
+}
+fn();
+
// 示例
+async function fn() {
+  let value = await new Promise((resolve, reject) => {
+    reject("failure");
+  });
+  console.log("do something...");
+}
+fn();
+

导致浏览器报错:一个未捕获的错误

在开发过程中,为了保证系统健壮性,或者是为了捕获异步的错误,需要频繁的在 async 函数中添加 try/catch,避免出现上述示例的情况

可是我很懒,不想一个个加,懒惰使我们进步😂

下面,通过手写一个 babel 插件,来给所有的 async 函数添加 try/catch

babel 插件的最终效果

原始代码:

js
async function fn() {
+  await new Promise((resolve, reject) => reject("报错"));
+  await new Promise((resolve) => resolve(1));
+  console.log("do something...");
+}
+fn();
+
async function fn() {
+  await new Promise((resolve, reject) => reject("报错"));
+  await new Promise((resolve) => resolve(1));
+  console.log("do something...");
+}
+fn();
+

使用插件转化后的代码:

js
async function fn() {
+  try {
+    await new Promise((resolve, reject) => reject("报错"));
+    await new Promise((resolve) => resolve(1));
+    console.log("do something...");
+  } catch (e) {
+    console.log("\nfilePath: E:\\myapp\\src\\main.js\nfuncName: fn\nError:", e);
+  }
+}
+fn();
+
async function fn() {
+  try {
+    await new Promise((resolve, reject) => reject("报错"));
+    await new Promise((resolve) => resolve(1));
+    console.log("do something...");
+  } catch (e) {
+    console.log("\nfilePath: E:\\myapp\\src\\main.js\nfuncName: fn\nError:", e);
+  }
+}
+fn();
+

通过详细的报错信息,帮助我们快速找到目标文件和具体的报错方法,方便去定位问题

babel 插件的实现思路

1)借助 AST 抽象语法树,遍历查找代码中的 await 关键字

2)找到 await 节点后,从父路径中查找声明的 async 函数,获取该函数的 body(函数中包含的代码)

3)创建 try/catch 语句,将原来 async 的 body 放入其中

4)最后将 async 的 body 替换成创建的 try/catch 语句

babel 的核心:AST

先聊聊 AST 这个帅小伙 🤠,不然后面的开发流程走不下去

AST 是代码的树形结构,生成 AST 分为两个阶段:词法分析和  语法分析

词法分析

词法分析阶段把字符串形式的代码转换为令牌(tokens) ,可以把 tokens 看作是一个扁平的语法片段数组,描述了代码片段在整个代码中的位置和记录当前值的一些信息

语法分析

语法分析阶段会把 token 转换成 AST 的形式,这个阶段会使用 token 中的信息把它们转换成一个 AST 的表述结构,使用 type 属性记录当前的类型

例如 let 代表着一个变量声明的关键字,所以它的 type 为 VariableDeclaration,而 a = 1 会作为 let 的声明描述,它的 type 为 VariableDeclarator

AST 在线查看工具:AST explorer

再举个 🌰,加深对 AST 的理解

js
function demo(n) {
+  return n * n;
+}
+
function demo(n) {
+  return n * n;
+}
+

转化成 AST 的结构

js
{
+  "type": "Program", // 整段代码的主体
+  "body": [
+    {
+      "type": "FunctionDeclaration", // function 的类型叫函数声明;
+      "id": { // id 为函数声明的 id
+        "type": "Identifier", // 标识符 类型
+        "name": "demo" // 标识符 具有名字
+      },
+      "expression": false,
+      "generator": false,
+      "async": false, // 代表是否 是 async function
+      "params": [ // 同级 函数的参数
+        {
+          "type": "Identifier",// 参数类型也是 Identifier
+          "name": "n"
+        }
+      ],
+      "body": { // 函数体内容 整个格式呈现一种树的格式
+        "type": "BlockStatement", // 整个函数体内容 为一个块状代码块类型
+        "body": [
+          {
+            "type": "ReturnStatement", // return 类型
+            "argument": {
+              "type": "BinaryExpression",// BinaryExpression 二进制表达式类型
+              "start": 30,
+              "end": 35,
+              "left": { // 分左 右 中 结构
+                "type": "Identifier",
+                "name": "n"
+              },
+              "operator": "*", // 属于操作符
+              "right": {
+                "type": "Identifier",
+                "name": "n"
+              }
+            }
+          }
+        ]
+      }
+    }
+  ],
+  "sourceType": "module"
+}
+
{
+  "type": "Program", // 整段代码的主体
+  "body": [
+    {
+      "type": "FunctionDeclaration", // function 的类型叫函数声明;
+      "id": { // id 为函数声明的 id
+        "type": "Identifier", // 标识符 类型
+        "name": "demo" // 标识符 具有名字
+      },
+      "expression": false,
+      "generator": false,
+      "async": false, // 代表是否 是 async function
+      "params": [ // 同级 函数的参数
+        {
+          "type": "Identifier",// 参数类型也是 Identifier
+          "name": "n"
+        }
+      ],
+      "body": { // 函数体内容 整个格式呈现一种树的格式
+        "type": "BlockStatement", // 整个函数体内容 为一个块状代码块类型
+        "body": [
+          {
+            "type": "ReturnStatement", // return 类型
+            "argument": {
+              "type": "BinaryExpression",// BinaryExpression 二进制表达式类型
+              "start": 30,
+              "end": 35,
+              "left": { // 分左 右 中 结构
+                "type": "Identifier",
+                "name": "n"
+              },
+              "operator": "*", // 属于操作符
+              "right": {
+                "type": "Identifier",
+                "name": "n"
+              }
+            }
+          }
+        ]
+      }
+    }
+  ],
+  "sourceType": "module"
+}
+

常用的 AST 节点类型对照表

类型原名称中文名称描述
Program程序主体整段代码的主体
VariableDeclaration变量声明声明一个变量,例如 var let const
FunctionDeclaration函数声明声明一个函数,例如 function
ExpressionStatement表达式语句通常是调用一个函数,例如 console.log()
BlockStatement块语句包裹在 {} 块内的代码,例如 if (condition)
BreakStatement中断语句通常指 break
ContinueStatement持续语句通常指 continue
ReturnStatement返回语句通常指 return
SwitchStatementSwitch 语句通常指 Switch Case 语句中的 Switch
IfStatementIf 控制流语句控制流语句,通常指 if(condition){}else{}
Identifier标识符标识,例如声明变量时 var identi = 5 中的 identi
CallExpression调用表达式通常指调用一个函数,例如 console.log()
BinaryExpression二进制表达式通常指运算,例如 1+2
MemberExpression成员表达式通常指调用对象的成员,例如 console 对象的 log 成员
ArrayExpression数组表达式通常指一个数组,例如 [1, 3, 5]
FunctionExpression函数表达式例如 const func = function () {}
ArrowFunctionExpression箭头函数表达式例如 const func = ()=> {}
AwaitExpressionawait 表达式例如 let val = await f()
ObjectMethod对象中定义的方法例如 let obj = { fn ()
NewExpressionNew 表达式通常指使用 New 关键词
AssignmentExpression赋值表达式通常指将函数的返回值赋值给变量
UpdateExpression更新表达式通常指更新成员值,例如 i++
Literal字面量字面量
BooleanLiteral布尔型字面量布尔值,例如 true false
NumericLiteral数字型字面量数字,例如 100
StringLiteral字符型字面量字符串,例如 vansenb
SwitchCaseCase 语句通常指 Switch 语句中的 Case

await 节点对应的 AST 结构

1)原始代码

js
async function fn() {
+  await f();
+}
+
async function fn() {
+  await f();
+}
+

2)增加 try catch 后的代码

js
async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+
async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+

通过 AST 结构对比,插件的核心就是将原始函数的 body 放到 try 语句中

babel 插件开发

作者曾在《「历时 8 个月」10 万字前端知识体系总结(工程化篇)🔥》中聊过如何开发一个 babel 插件

这里简单回顾一下

插件的基本格式示例

js
module.exports = function (babel) {
+   let t = babel.type
+   return {
+     visitor: {
+       // 设置需要范围的节点类型
+       CallExression: (path, state) => {
+         do soming ……
+       }
+     }
+   }
+ }
+
module.exports = function (babel) {
+   let t = babel.type
+   return {
+     visitor: {
+       // 设置需要范围的节点类型
+       CallExression: (path, state) => {
+         do soming ……
+       }
+     }
+   }
+ }
+

1)通过 babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等

2)visitor:定义了一个访问者,可以设置需要访问的节点类型,当访问到目标节点后,做相应的处理来实现插件的功能

寻找 await 节点

回到业务需求,现在需要找到 await 节点,可以通过AwaitExpression表达式获取

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+      },
+    },
+  };
+};
+

向上查找 async 函数

通过findParent方法,在父节点中搜寻 async 节点

js
// async节点的属性为true
+const asyncPath = path.findParent((p) => p.node.async);
+
// async节点的属性为true
+const asyncPath = path.findParent((p) => p.node.async);
+

这里要注意,async 函数分为 4 种情况:函数声明 、箭头函数 、函数表达式 、函数为对象的方法

js
// 1️⃣:函数声明
+async function fn() {
+  await f();
+}
+
+// 2️⃣:函数表达式
+const fn = async function () {
+  await f();
+};
+
+// 3️⃣:箭头函数
+const fn = async () => {
+  await f();
+};
+
+// 4️⃣:async函数定义在对象中
+const obj = {
+  async fn() {
+    await f();
+  },
+};
+
// 1️⃣:函数声明
+async function fn() {
+  await f();
+}
+
+// 2️⃣:函数表达式
+const fn = async function () {
+  await f();
+};
+
+// 3️⃣:箭头函数
+const fn = async () => {
+  await f();
+};
+
+// 4️⃣:async函数定义在对象中
+const obj = {
+  async fn() {
+    await f();
+  },
+};
+

需要对这几种情况进行分别判断

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+        // 查找async函数的节点
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      // 设置AwaitExpression
+      AwaitExpression(path) {
+        // 获取当前的await节点
+        let node = path.node;
+        // 查找async函数的节点
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+      },
+    },
+  };
+};
+

利用 babel-template 生成 try/catch 节点

babel-template可以用以字符串形式的代码来构建 AST 树节点,快速优雅开发插件

js
// 引入babel-template
+const template = require("babel-template");
+
+// 定义try/catch语句模板
+let tryTemplate = `
+try {
+} catch (e) {
+console.log(CatchError:e)
+}`;
+
+// 创建模板
+const temp = template(tryTemplate);
+
+// 给模版增加key,添加console.log打印信息
+let tempArgumentObj = {
+  // 通过types.stringLiteral创建字符串字面量
+  CatchError: types.stringLiteral("Error"),
+};
+
+// 通过temp创建try语句的AST节点
+let tryNode = temp(tempArgumentObj);
+
// 引入babel-template
+const template = require("babel-template");
+
+// 定义try/catch语句模板
+let tryTemplate = `
+try {
+} catch (e) {
+console.log(CatchError:e)
+}`;
+
+// 创建模板
+const temp = template(tryTemplate);
+
+// 给模版增加key,添加console.log打印信息
+let tempArgumentObj = {
+  // 通过types.stringLiteral创建字符串字面量
+  CatchError: types.stringLiteral("Error"),
+};
+
+// 通过temp创建try语句的AST节点
+let tryNode = temp(tempArgumentObj);
+

async 函数体替换成 try 语句

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+
+        let tryNode = temp(tempArgumentObj);
+
+        // 获取父节点的函数体body
+        let info = asyncPath.node.body;
+
+        // 将函数体放到try语句的body中
+        tryNode.block.body.push(...info.body);
+
+        // 将父节点的body替换成新创建的try语句
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+
+        let tryNode = temp(tempArgumentObj);
+
+        // 获取父节点的函数体body
+        let info = asyncPath.node.body;
+
+        // 将函数体放到try语句的body中
+        tryNode.block.body.push(...info.body);
+
+        // 将父节点的body替换成新创建的try语句
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+

到这里,插件的基本结构已经成型,但还有点问题,如果函数已存在 try/catch,该怎么处理判断呢?

若函数已存在 try/catch,则不处理

js
// 示例代码,不再添加try/catch
+async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+
// 示例代码,不再添加try/catch
+async function fn() {
+  try {
+    await f();
+  } catch (e) {
+    console.log(e);
+  }
+}
+

通过isTryStatement判断是否已存在 try 语句

js
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        // 判断父路径中是否已存在try语句,若存在直接返回
+        if (path.findParent((p) => p.isTryStatement())) {
+          return false;
+        }
+
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+        let tryNode = temp(tempArgumentObj);
+        let info = asyncPath.node.body;
+        tryNode.block.body.push(...info.body);
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+
module.exports = function (babel) {
+  let t = babel.type;
+  return {
+    visitor: {
+      AwaitExpression(path) {
+        // 判断父路径中是否已存在try语句,若存在直接返回
+        if (path.findParent((p) => p.isTryStatement())) {
+          return false;
+        }
+
+        let node = path.node;
+        const asyncPath = path.findParent(
+          (p) =>
+            p.node.async &&
+            (p.isFunctionDeclaration() ||
+              p.isArrowFunctionExpression() ||
+              p.isFunctionExpression() ||
+              p.isObjectMethod())
+        );
+        let tryNode = temp(tempArgumentObj);
+        let info = asyncPath.node.body;
+        tryNode.block.body.push(...info.body);
+        info.body = [tryNode];
+      },
+    },
+  };
+};
+

添加报错信息

获取报错时的文件路径 filePath 和方法名称 funcName,方便快速定位问题

获取文件路径

js
// 获取编译目标文件的路径,如:E:\myapp\src\App.vue
+const filePath = this.filename || this.file.opts.filename || "unknown";
+
// 获取编译目标文件的路径,如:E:\myapp\src\App.vue
+const filePath = this.filename || this.file.opts.filename || "unknown";
+

获取报错的方法名称

js
// 定义方法名
+let asyncName = "";
+
+// 获取async节点的type类型
+let type = asyncPath.node.type;
+
+switch (type) {
+  // 1️⃣函数表达式
+  // 情况1:普通函数,如const func = async function () {}
+  // 情况2:箭头函数,如const func = async () => {}
+  case "FunctionExpression":
+  case "ArrowFunctionExpression":
+    // 使用path.getSibling(index)来获得同级的id路径
+    let identifier = asyncPath.getSibling("id");
+    // 获取func方法名
+    asyncName = identifier && identifier.node ? identifier.node.name : "";
+    break;
+
+  // 2️⃣函数声明,如async function fn2() {}
+  case "FunctionDeclaration":
+    asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+    break;
+
+  // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+  case "ObjectMethod":
+    asyncName = asyncPath.node.key.name || "";
+    break;
+}
+
+// 若asyncName不存在,通过argument.callee获取当前执行函数的name
+let funcName =
+  asyncName || (node.argument.callee && node.argument.callee.name) || "";
+
// 定义方法名
+let asyncName = "";
+
+// 获取async节点的type类型
+let type = asyncPath.node.type;
+
+switch (type) {
+  // 1️⃣函数表达式
+  // 情况1:普通函数,如const func = async function () {}
+  // 情况2:箭头函数,如const func = async () => {}
+  case "FunctionExpression":
+  case "ArrowFunctionExpression":
+    // 使用path.getSibling(index)来获得同级的id路径
+    let identifier = asyncPath.getSibling("id");
+    // 获取func方法名
+    asyncName = identifier && identifier.node ? identifier.node.name : "";
+    break;
+
+  // 2️⃣函数声明,如async function fn2() {}
+  case "FunctionDeclaration":
+    asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+    break;
+
+  // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+  case "ObjectMethod":
+    asyncName = asyncPath.node.key.name || "";
+    break;
+}
+
+// 若asyncName不存在,通过argument.callee获取当前执行函数的name
+let funcName =
+  asyncName || (node.argument.callee && node.argument.callee.name) || "";
+

添加用户选项

用户引入插件时,可以设置excludeincludecustomLog选项

exclude: 设置需要排除的文件,不对该文件进行处理

include: 设置需要处理的文件,只对该文件进行处理

customLog: 用户自定义的打印信息

最终代码

入口文件 index.js

js
// babel-template 用于将字符串形式的代码来构建AST树节点
+const template = require("babel-template");
+
+const {
+  tryTemplate,
+  catchConsole,
+  mergeOptions,
+  matchesFile,
+} = require("./util");
+
+module.exports = function (babel) {
+  // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
+  let types = babel.types;
+
+  // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
+  const visitor = {
+    AwaitExpression(path) {
+      // 通过this.opts 获取用户的配置
+      if (this.opts && !typeof this.opts === "object") {
+        return console.error(
+          "[babel-plugin-await-add-trycatch]: options need to be an object."
+        );
+      }
+
+      // 判断父路径中是否已存在try语句,若存在直接返回
+      if (path.findParent((p) => p.isTryStatement())) {
+        return false;
+      }
+
+      // 合并插件的选项
+      const options = mergeOptions(this.opts);
+
+      // 获取编译目标文件的路径,如:E:\myapp\src\App.vue
+      const filePath = this.filename || this.file.opts.filename || "unknown";
+
+      // 在排除列表的文件不编译
+      if (matchesFile(options.exclude, filePath)) {
+        return;
+      }
+
+      // 如果设置了include,只编译include中的文件
+      if (options.include.length && !matchesFile(options.include, filePath)) {
+        return;
+      }
+
+      // 获取当前的await节点
+      let node = path.node;
+
+      // 在父路径节点中查找声明 async 函数的节点
+      // async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
+      const asyncPath = path.findParent(
+        (p) =>
+          p.node.async &&
+          (p.isFunctionDeclaration() ||
+            p.isArrowFunctionExpression() ||
+            p.isFunctionExpression() ||
+            p.isObjectMethod())
+      );
+
+      // 获取async的方法名
+      let asyncName = "";
+
+      let type = asyncPath.node.type;
+
+      switch (type) {
+        // 1️⃣函数表达式
+        // 情况1:普通函数,如const func = async function () {}
+        // 情况2:箭头函数,如const func = async () => {}
+        case "FunctionExpression":
+        case "ArrowFunctionExpression":
+          // 使用path.getSibling(index)来获得同级的id路径
+          let identifier = asyncPath.getSibling("id");
+          // 获取func方法名
+          asyncName = identifier && identifier.node ? identifier.node.name : "";
+          break;
+
+        // 2️⃣函数声明,如async function fn2() {}
+        case "FunctionDeclaration":
+          asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+          break;
+
+        // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+        case "ObjectMethod":
+          asyncName = asyncPath.node.key.name || "";
+          break;
+      }
+
+      // 若asyncName不存在,通过argument.callee获取当前执行函数的name
+      let funcName =
+        asyncName || (node.argument.callee && node.argument.callee.name) || "";
+
+      const temp = template(tryTemplate);
+
+      // 给模版增加key,添加console.log打印信息
+      let tempArgumentObj = {
+        // 通过types.stringLiteral创建字符串字面量
+        CatchError: types.stringLiteral(
+          catchConsole(filePath, funcName, options.customLog)
+        ),
+      };
+
+      // 通过temp创建try语句
+      let tryNode = temp(tempArgumentObj);
+
+      // 获取async节点(父节点)的函数体
+      let info = asyncPath.node.body;
+
+      // 将父节点原来的函数体放到try语句中
+      tryNode.block.body.push(...info.body);
+
+      // 将父节点的内容替换成新创建的try语句
+      info.body = [tryNode];
+    },
+  };
+  return {
+    name: "babel-plugin-await-add-trycatch",
+    visitor,
+  };
+};
+
// babel-template 用于将字符串形式的代码来构建AST树节点
+const template = require("babel-template");
+
+const {
+  tryTemplate,
+  catchConsole,
+  mergeOptions,
+  matchesFile,
+} = require("./util");
+
+module.exports = function (babel) {
+  // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等
+  let types = babel.types;
+
+  // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式
+  const visitor = {
+    AwaitExpression(path) {
+      // 通过this.opts 获取用户的配置
+      if (this.opts && !typeof this.opts === "object") {
+        return console.error(
+          "[babel-plugin-await-add-trycatch]: options need to be an object."
+        );
+      }
+
+      // 判断父路径中是否已存在try语句,若存在直接返回
+      if (path.findParent((p) => p.isTryStatement())) {
+        return false;
+      }
+
+      // 合并插件的选项
+      const options = mergeOptions(this.opts);
+
+      // 获取编译目标文件的路径,如:E:\myapp\src\App.vue
+      const filePath = this.filename || this.file.opts.filename || "unknown";
+
+      // 在排除列表的文件不编译
+      if (matchesFile(options.exclude, filePath)) {
+        return;
+      }
+
+      // 如果设置了include,只编译include中的文件
+      if (options.include.length && !matchesFile(options.include, filePath)) {
+        return;
+      }
+
+      // 获取当前的await节点
+      let node = path.node;
+
+      // 在父路径节点中查找声明 async 函数的节点
+      // async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法
+      const asyncPath = path.findParent(
+        (p) =>
+          p.node.async &&
+          (p.isFunctionDeclaration() ||
+            p.isArrowFunctionExpression() ||
+            p.isFunctionExpression() ||
+            p.isObjectMethod())
+      );
+
+      // 获取async的方法名
+      let asyncName = "";
+
+      let type = asyncPath.node.type;
+
+      switch (type) {
+        // 1️⃣函数表达式
+        // 情况1:普通函数,如const func = async function () {}
+        // 情况2:箭头函数,如const func = async () => {}
+        case "FunctionExpression":
+        case "ArrowFunctionExpression":
+          // 使用path.getSibling(index)来获得同级的id路径
+          let identifier = asyncPath.getSibling("id");
+          // 获取func方法名
+          asyncName = identifier && identifier.node ? identifier.node.name : "";
+          break;
+
+        // 2️⃣函数声明,如async function fn2() {}
+        case "FunctionDeclaration":
+          asyncName = (asyncPath.node.id && asyncPath.node.id.name) || "";
+          break;
+
+        // 3️⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
+        case "ObjectMethod":
+          asyncName = asyncPath.node.key.name || "";
+          break;
+      }
+
+      // 若asyncName不存在,通过argument.callee获取当前执行函数的name
+      let funcName =
+        asyncName || (node.argument.callee && node.argument.callee.name) || "";
+
+      const temp = template(tryTemplate);
+
+      // 给模版增加key,添加console.log打印信息
+      let tempArgumentObj = {
+        // 通过types.stringLiteral创建字符串字面量
+        CatchError: types.stringLiteral(
+          catchConsole(filePath, funcName, options.customLog)
+        ),
+      };
+
+      // 通过temp创建try语句
+      let tryNode = temp(tempArgumentObj);
+
+      // 获取async节点(父节点)的函数体
+      let info = asyncPath.node.body;
+
+      // 将父节点原来的函数体放到try语句中
+      tryNode.block.body.push(...info.body);
+
+      // 将父节点的内容替换成新创建的try语句
+      info.body = [tryNode];
+    },
+  };
+  return {
+    name: "babel-plugin-await-add-trycatch",
+    visitor,
+  };
+};
+

util.js

js
const merge = require("deepmerge");
+
+// 定义try语句模板
+let tryTemplate = `
+try {
+} catch (e) {
+console.log(CatchError,e)
+}`;
+
+/*
+ * catch要打印的信息
+ * @param {string} filePath - 当前执行文件的路径
+ * @param {string} funcName - 当前执行方法的名称
+ * @param {string} customLog - 用户自定义的打印信息
+ */
+let catchConsole = (filePath, funcName, customLog) => `
+filePath: ${filePath}
+funcName: ${funcName}
+${customLog}:`;
+
+// 默认配置
+const defaultOptions = {
+  customLog: "Error",
+  exclude: ["node_modules"],
+  include: [],
+};
+
+// 判断执行的file文件 是否在 exclude/include 选项内
+function matchesFile(list, filename) {
+  return list.find((name) => name && filename.includes(name));
+}
+
+// 合并选项
+function mergeOptions(options) {
+  let { exclude, include } = options;
+  if (exclude) options.exclude = toArray(exclude);
+  if (include) options.include = toArray(include);
+  // 使用merge进行合并
+  return merge.all([defaultOptions, options]);
+}
+
+function toArray(value) {
+  return Array.isArray(value) ? value : [value];
+}
+
+module.exports = {
+  tryTemplate,
+  catchConsole,
+  defaultOptions,
+  mergeOptions,
+  matchesFile,
+  toArray,
+};
+
const merge = require("deepmerge");
+
+// 定义try语句模板
+let tryTemplate = `
+try {
+} catch (e) {
+console.log(CatchError,e)
+}`;
+
+/*
+ * catch要打印的信息
+ * @param {string} filePath - 当前执行文件的路径
+ * @param {string} funcName - 当前执行方法的名称
+ * @param {string} customLog - 用户自定义的打印信息
+ */
+let catchConsole = (filePath, funcName, customLog) => `
+filePath: ${filePath}
+funcName: ${funcName}
+${customLog}:`;
+
+// 默认配置
+const defaultOptions = {
+  customLog: "Error",
+  exclude: ["node_modules"],
+  include: [],
+};
+
+// 判断执行的file文件 是否在 exclude/include 选项内
+function matchesFile(list, filename) {
+  return list.find((name) => name && filename.includes(name));
+}
+
+// 合并选项
+function mergeOptions(options) {
+  let { exclude, include } = options;
+  if (exclude) options.exclude = toArray(exclude);
+  if (include) options.include = toArray(include);
+  // 使用merge进行合并
+  return merge.all([defaultOptions, options]);
+}
+
+function toArray(value) {
+  return Array.isArray(value) ? value : [value];
+}
+
+module.exports = {
+  tryTemplate,
+  catchConsole,
+  defaultOptions,
+  mergeOptions,
+  matchesFile,
+  toArray,
+};
+

本文章作者的 github 仓库

+ + + + + \ No newline at end of file diff --git a/fragment/babel-console.html b/fragment/babel-console.html new file mode 100644 index 00000000..04e13e33 --- /dev/null +++ b/fragment/babel-console.html @@ -0,0 +1,550 @@ + + + + + + 🔥 手撕 babel 插件-消灭 console! | Sunny's blog + + + + + + + + +
Skip to content
On this page

🔥 手撕 babel 插件-消灭 console!

写一个小插件来去除 生产环境 的 console.log

我们的目的是 去除 console.log ,我们首先需要通过 ast 查看语法树的结构。我们以下面的 console 为例: 注意 因为我们要写 babel 插件 所以我们选择 @babel/parser 库生成 ast,因为 babel 内部是使用这个库生成 ast 的

初见 AST

AST 是对源码的抽象,字面量、标识符、表达式、语句、模块语法、class 语法都有各自的 AST。

Program

program 是代表整个程序的节点,它有 body 属性代表程序体,存放 statement 数组,就是具体执行的语句的集合。 可以看到我们这里的 body 只有一个 ExpressionStatement 语句,即 console.log。

ExpressionStatement

statement 是语句,它是可以独立执行的单位,expression 是表达式,它俩唯一的区别是表达式执行完以后有返回值。所以 ExpressionStatement 表示这个表达式是被当作语句执行的。 ExpressionStatement 类型的 AST 有一个 expression 属性,代表当前的表达式。

CallExpression

expression 是表达式,CallExpression 表示调用表达式,console.log 就是一个调用表达式。 CallExpression 类型的 AST 有一个 callee 属性,指向被调用的函数。这里 console.log 就是 callee 的值。 CallExpression 类型的 AST 有一个 arguments 属性,指向参数。这里“我会被清除”就是 arguments 的值。

MemberExpression

Member Expression 通常是用于访问对象成员的。他有几种形式:

js
a.b
+a["b"]
+new.target
+super.b
+
a.b
+a["b"]
+new.target
+super.b
+

我们这里的 console.log 就是访问对象成员 log。

为什么 MemberExpression 外层有一个 CallExpression 呢?

实际上,我们可以理解为,MemberExpression 中的某一子结构具有函数调用,那么整个表达式就成为了一个 Call Expression。 MemberExpression 有一个属性 object 表示被访问的对象。这里 console 就是 object 的值。 MemberExpression 有一个属性 property 表示对象的属性。这里 log 就是 property 的值。 MemberExpression 有一个属性 computed 表示访问对象是何种方式。computed 为 true 表示[],false 表示. 。

Identifier

Identifer 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是 Identifer。

我们这里的 console 就是一个 identifier。Identifier 有一个属性 name 表示标识符的名字

StringLiteral

表示字符串字面量。我们这里的 log 就是一个字符串字面量。StringLiteral 有一个属性 value 表示字符串的值

公共属性

每种 AST 都有自己的属性,但是它们也有一些公共的属性:

  • type:AST 节点的类型
  • start、end、loc:start 和 end 代表该节点在源码中的开始和结束下标。而 loc 属性是一个对象,有 line 和 column 属性分别记录开始和结束的行列号
  • leadingComments、innerComments、trailingComments:表示开始的注释、中间的注释、结尾的注释,每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,想拿到某个 AST 的注释就通过这三个属性。

如何写一个 babel 插件?

babel 插件是作用在 transform 阶段。

transform 阶段有@babel/traverse,可以遍历 AST,并调用 visitor 函数修改 AST。 我们可以新建一个 js 文件,其中导出一个方法,返回一个对象,对象存在一个 visitor 属性,里面可以编写我们具体需要修改 AST 的逻辑。

js
export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+复制代码;
+
export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+复制代码;
+

构造 visitor 方法

path 是记录遍历路径的 api,它记录了父子节点的引用,还有很多增删改查 AST 的 api

js
const visitor = {
+  CallExpression(path, { opts }) {
+    //当traverse遍历到类型为CallExpression的AST时,会进入函数内部,我们需要在函数内部修改
+  },
+};
+
const visitor = {
+  CallExpression(path, { opts }) {
+    //当traverse遍历到类型为CallExpression的AST时,会进入函数内部,我们需要在函数内部修改
+  },
+};
+

我们需要遍历所有调用函数表达式 所以使用 CallExpression 。

去除所有 console

将所有的 console.log 去掉

path.get 表示获取某个属性的 path

path.matchesPattern 检查某个节点是否符合某种模式

path.remove 删除当前节点

js
CallExpression(path, { opts }) {
+  //获取callee的path
+  const calleePath = path.get("callee");
+  //检查callee中是否符合“console”这种模式
+  if (calleePath && calleePath.matchesPattern("console", true)) {
+    //如果符合 直接删除节点
+    path.remove();
+  }
+},
+
CallExpression(path, { opts }) {
+  //获取callee的path
+  const calleePath = path.get("callee");
+  //检查callee中是否符合“console”这种模式
+  if (calleePath && calleePath.matchesPattern("console", true)) {
+    //如果符合 直接删除节点
+    path.remove();
+  }
+},
+

增加 env api

一般去除 console.log 都是在生产环境执行 所以增加 env 参数

AST 的第二个参数 opt 中有插件传入的配置

js
const isProduction = process.env.NODE_ENV === "production";
+CallExpression(path, { opts }) {
+    const { env } = opts;
+    if (env === "production" || isProduction) {
+        path.remove();
+    }
+},
+
const isProduction = process.env.NODE_ENV === "production";
+CallExpression(path, { opts }) {
+    const { env } = opts;
+    if (env === "production" || isProduction) {
+        path.remove();
+    }
+},
+

增加 exclude api

我们上面去除了所有的 console,不管是 error、warning、table 都会清除,所以我们加一个 exclude api,传一个数组,可以去除想要去除的 console 类型

js
const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+
+const { env, exclude } = opts;
+if (env === "production" || isProduction) {
+  removeConsoleExpression(path, calleePath, exclude);
+}
+const removeConsoleExpression = (path, calleePath, exclude) => {
+  if (isArray(exclude)) {
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    //匹配上直接返回不进行操作
+    if (hasTarget) return;
+  }
+  path.remove();
+};
+
const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+
+const { env, exclude } = opts;
+if (env === "production" || isProduction) {
+  removeConsoleExpression(path, calleePath, exclude);
+}
+const removeConsoleExpression = (path, calleePath, exclude) => {
+  if (isArray(exclude)) {
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    //匹配上直接返回不进行操作
+    if (hasTarget) return;
+  }
+  path.remove();
+};
+

增加 commentWords api

某些时候 我们希望一些 console 不被删除 我们可以给他添加一些注释 比如

js
//no remove
+console.log("测试1");
+console.log("测试2"); //reserse
+//hhhhh
+console.log("测试3");
+
//no remove
+console.log("测试1");
+console.log("测试2"); //reserse
+//hhhhh
+console.log("测试3");
+

如上 我们希望带有 no remove 前缀注释的 console 和带有 reserse 后缀注释的 console 保留不被删除 之前我们提到 babel 给我们提供了 leadingComments(前缀注释)和 trailingComments(后缀注释)我们可以利用他们 由 AST 可知 她和 CallExpression 同级,所以我们需要获取他的父节点 然后获取父节点的属性

path.parentPath 获取父 path path.node 获取当前节点

js
const { exclude, commentWords, env } = opts;
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+// 判断是否有前缀注释
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+// 判断是否有后缀注释
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+//判断是否有关键字匹配 默认no remove || reserve 且如果commentWords和默认值是相斥的
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\b)|(reserve\b)/.test(node.value))
+  );
+};
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  //获取父path
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+  //标识是否有前缀注释
+  let leadingReserve = false;
+  //标识是否有后缀注释
+  let trailReserve = false;
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    parentNode.leadingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        leadingReserve = true;
+      }
+    });
+  }
+  if (hasTrailingComments(parentNode)) {
+    //traverse
+    parentNode.trailingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        trailReserve = true;
+      }
+    });
+  }
+  //如果没有前缀节点和后缀节点 直接删除节点
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+
const { exclude, commentWords, env } = opts;
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+// 判断是否有前缀注释
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+// 判断是否有后缀注释
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+//判断是否有关键字匹配 默认no remove || reserve 且如果commentWords和默认值是相斥的
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\b)|(reserve\b)/.test(node.value))
+  );
+};
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  //获取父path
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+  //标识是否有前缀注释
+  let leadingReserve = false;
+  //标识是否有后缀注释
+  let trailReserve = false;
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    parentNode.leadingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        leadingReserve = true;
+      }
+    });
+  }
+  if (hasTrailingComments(parentNode)) {
+    //traverse
+    parentNode.trailingComments.forEach((comment) => {
+      if (isReserveComment(comment, commentWords)) {
+        trailReserve = true;
+      }
+    });
+  }
+  //如果没有前缀节点和后缀节点 直接删除节点
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+

细节完善

我们大致完成了插件 我们引进项目里面进行测试

js
console.log("测试 1");
+//no remove
+console.log("测试 2");
+console.log("测试 3"); //reserve
+console.log("测试 4");
+
console.log("测试 1");
+//no remove
+console.log("测试 2");
+console.log("测试 3"); //reserve
+console.log("测试 4");
+

新建.babelrc 引入插件

json
{
+  "plugins": [
+    [
+      "../dist/index.cjs",
+      {
+        "env": "production"
+      }
+    ]
+  ]
+}
+
{
+  "plugins": [
+    [
+      "../dist/index.cjs",
+      {
+        "env": "production"
+      }
+    ]
+  ]
+}
+

理论上应该移除测试 1、测试 4,但是我们惊讶的发现 竟然一个 console 没有删除!!经过排查 我们大致确定了问题所在。 因为测试 2 的前缀注释同时也被 AST 纳入了测试 1 的后缀注释中了,而测试 3 的后缀注释同时也被 AST 纳入了测试 4 的前缀注释中了 所以测试 1 存在后缀注释 测试 4 存在前缀注释 所以测试 1 和测试 4 没有被删除 那么我们怎么判断呢?

对于后缀注释

我们可以判断后缀注释是否与当前的调用表达式处于同一行,如果不是同一行,则不将其归纳为后缀注释

js
if (hasTrailingComments(parentNode)) {
+  const {
+    start: { line: currentLine },
+  } = parentNode.loc;
+  //traverse
+  // @ts-ignore
+  parentNode.trailingComments.forEach((comment) => {
+    const {
+      start: { line: currentCommentLine },
+    } = comment.loc;
+
+    if (currentLine === currentCommentLine) {
+      comment.belongCurrentLine = true;
+    }
+    //属于当前行才将其设置为后缀注释
+    if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) {
+      trailReserve = true;
+    }
+  });
+}
+
if (hasTrailingComments(parentNode)) {
+  const {
+    start: { line: currentLine },
+  } = parentNode.loc;
+  //traverse
+  // @ts-ignore
+  parentNode.trailingComments.forEach((comment) => {
+    const {
+      start: { line: currentCommentLine },
+    } = comment.loc;
+
+    if (currentLine === currentCommentLine) {
+      comment.belongCurrentLine = true;
+    }
+    //属于当前行才将其设置为后缀注释
+    if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) {
+      trailReserve = true;
+    }
+  });
+}
+

对于前缀注释

因为我们在后缀注释的节点中添加了一个变量 belongCurrentLine,表示该注释是否是和节点属于同一行。 那么对于前缀注释,我们只需要判断是否存在 belongCurrentLine,如果存在 belongCurrentLine,表示不能将其当作前缀注释。

js
if (hasLeadingComments(parentNode)) {
+  parentNode.leadingComments.forEach((comment) => {
+    if (isReserveComment(comment, commentWords) && !comment.belongCurrentLine) {
+      leadingReserve = true;
+    }
+  });
+}
+
if (hasLeadingComments(parentNode)) {
+  parentNode.leadingComments.forEach((comment) => {
+    if (isReserveComment(comment, commentWords) && !comment.belongCurrentLine) {
+      leadingReserve = true;
+    }
+  });
+}
+

code

js
// @ts-ignore
+const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+// @ts-ignore
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+
+const isProduction = process.env.NODE_ENV === "production";
+
+// @ts-ignore
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\b)|(reserve\b)/.test(node.value))
+  );
+};
+
+// @ts-ignore
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  // if has exclude key exclude this
+  if (isArray(exclude)) {
+    // @ts-ignore
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    if (hasTarget) return;
+  }
+
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+
+  let leadingReserve = false;
+  let trailReserve = false;
+
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    // @ts-ignore
+    parentNode.leadingComments.forEach((comment) => {
+      if (
+        isReserveComment(comment, commentWords) &&
+        !comment.belongCurrentLine
+      ) {
+        leadingReserve = true;
+      }
+    });
+  }
+
+  if (hasTrailingComments(parentNode)) {
+    const {
+      start: { line: currentLine },
+    } = parentNode.loc;
+    //traverse
+    // @ts-ignore
+    parentNode.trailingComments.forEach((comment) => {
+      const {
+        start: { line: currentCommentLine },
+      } = comment.loc;
+
+      if (currentLine === currentCommentLine) {
+        comment.belongCurrentLine = true;
+      }
+
+      if (
+        isReserveComment(comment, commentWords) &&
+        comment.belongCurrentLine
+      ) {
+        trailReserve = true;
+      }
+    });
+  }
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+
+// @ts-ignore
+// has leading comments
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+
+// @ts-ignore
+// has trailing comments
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+
+const visitor = {
+  // @ts-ignore
+  CallExpression(path, { opts }) {
+    const calleePath = path.get("callee");
+
+    const { exclude, commentWords, env } = opts;
+
+    if (calleePath && calleePath.matchesPattern("console", true)) {
+      if (env === "production" || isProduction) {
+        removeConsoleExpression(path, calleePath, exclude, commentWords);
+      }
+    }
+  },
+};
+
+export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+
// @ts-ignore
+const isArray = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Array]";
+// @ts-ignore
+const isFunction = (arg) =>
+  Object.prototype.toString.call(arg) === "[object Function]";
+
+const isProduction = process.env.NODE_ENV === "production";
+
+// @ts-ignore
+const isReserveComment = (node, commentWords) => {
+  if (isFunction(commentWords)) {
+    return commentWords(node.value);
+  }
+  return (
+    ["CommentBlock", "CommentLine"].includes(node.type) &&
+    (isArray(commentWords)
+      ? commentWords.includes(node.value)
+      : /(no[t]? remove\b)|(reserve\b)/.test(node.value))
+  );
+};
+
+// @ts-ignore
+const removeConsoleExpression = (path, calleePath, exclude, commentWords) => {
+  // if has exclude key exclude this
+  if (isArray(exclude)) {
+    // @ts-ignore
+    const hasTarget = exclude.some((type) => {
+      return calleePath.matchesPattern("console." + type);
+    });
+    if (hasTarget) return;
+  }
+
+  const parentPath = path.parentPath;
+  const parentNode = parentPath.node;
+
+  let leadingReserve = false;
+  let trailReserve = false;
+
+  if (hasLeadingComments(parentNode)) {
+    //traverse
+    // @ts-ignore
+    parentNode.leadingComments.forEach((comment) => {
+      if (
+        isReserveComment(comment, commentWords) &&
+        !comment.belongCurrentLine
+      ) {
+        leadingReserve = true;
+      }
+    });
+  }
+
+  if (hasTrailingComments(parentNode)) {
+    const {
+      start: { line: currentLine },
+    } = parentNode.loc;
+    //traverse
+    // @ts-ignore
+    parentNode.trailingComments.forEach((comment) => {
+      const {
+        start: { line: currentCommentLine },
+      } = comment.loc;
+
+      if (currentLine === currentCommentLine) {
+        comment.belongCurrentLine = true;
+      }
+
+      if (
+        isReserveComment(comment, commentWords) &&
+        comment.belongCurrentLine
+      ) {
+        trailReserve = true;
+      }
+    });
+  }
+  if (!leadingReserve && !trailReserve) {
+    path.remove();
+  }
+};
+
+// @ts-ignore
+// has leading comments
+const hasLeadingComments = (node) => {
+  const leadingComments = node.leadingComments;
+  return leadingComments && leadingComments.length;
+};
+
+// @ts-ignore
+// has trailing comments
+const hasTrailingComments = (node) => {
+  const trailingComments = node.trailingComments;
+  return trailingComments && trailingComments.length;
+};
+
+const visitor = {
+  // @ts-ignore
+  CallExpression(path, { opts }) {
+    const calleePath = path.get("callee");
+
+    const { exclude, commentWords, env } = opts;
+
+    if (calleePath && calleePath.matchesPattern("console", true)) {
+      if (env === "production" || isProduction) {
+        removeConsoleExpression(path, calleePath, exclude, commentWords);
+      }
+    }
+  },
+};
+
+export default () => {
+  return {
+    name: "@sunny-117/babel-plugin-console",
+    visitor,
+  };
+};
+
+ + + + + \ No newline at end of file diff --git a/fragment/const.html b/fragment/const.html new file mode 100644 index 00000000..a053b584 --- /dev/null +++ b/fragment/const.html @@ -0,0 +1,82 @@ + + + + + + 如何在 ES5 环境下实现一个 const ? | Sunny's blog + + + + + + + + +
Skip to content
On this page

如何在 ES5 环境下实现一个 const ?

属性描述符

详解

const 实现原理

由于 ES5 环境没有 block 的概念,所以是无法百分百实现 const,只能是挂载到某个对象下,要么是全局的 window,要么就是自定义一个 object 来当容器

js
var __const = function __const(data, value) {
+  window.data = value; // 把要定义的data挂载到window下,并赋值value
+  Object.defineProperty(window, data, {
+    // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
+    enumerable: false,
+    configurable: false,
+    get: function () {
+      return value;
+    },
+    set: function (data) {
+      if (data !== value) {
+        // 当要对当前属性进行赋值时,则抛出错误!
+        throw new TypeError("Assignment to constant variable.");
+      } else {
+        return value;
+      }
+    },
+  });
+};
+__const("a", 10);
+console.log(a);
+delete a;
+console.log(a);
+for (let item in window) {
+  // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
+  if (item === "a") {
+    // 因为不可枚举,所以不执行
+    console.log(window[item]);
+  }
+}
+a = 20; // 报错
+
var __const = function __const(data, value) {
+  window.data = value; // 把要定义的data挂载到window下,并赋值value
+  Object.defineProperty(window, data, {
+    // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
+    enumerable: false,
+    configurable: false,
+    get: function () {
+      return value;
+    },
+    set: function (data) {
+      if (data !== value) {
+        // 当要对当前属性进行赋值时,则抛出错误!
+        throw new TypeError("Assignment to constant variable.");
+      } else {
+        return value;
+      }
+    },
+  });
+};
+__const("a", 10);
+console.log(a);
+delete a;
+console.log(a);
+for (let item in window) {
+  // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
+  if (item === "a") {
+    // 因为不可枚举,所以不执行
+    console.log(window[item]);
+  }
+}
+a = 20; // 报错
+
+ + + + + \ No newline at end of file diff --git a/fragment/disable-debugger.html b/fragment/disable-debugger.html new file mode 100644 index 00000000..50725543 --- /dev/null +++ b/fragment/disable-debugger.html @@ -0,0 +1,186 @@ + + + + + + 禁止别人调试自己的前端页面代码 | Sunny's blog + + + + + + + + +
Skip to content
On this page

禁止别人调试自己的前端页面代码

🎈 为啥要禁止?

  • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析, 破解后获取数据
  • 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码

🎈 无限 debugger

  • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行
  • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的
  • 基础代码如下:
js
/**
+ * 基础禁止调试代码
+ */
+(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
/**
+ * 基础禁止调试代码
+ */
+(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+

🎈 无限 debugger 的对策

  • 如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大
  • 可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger
  • 这种方式虽然能去掉碍眼的 debugger ,但是无法通过左侧的行号添加 breakpoint

🎈 禁止断点的对策

  • 如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpoint 为 false 也无用
  • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的
js
(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
(() => {
+  function ban() {
+    setInterval(() => {
+      debugger;
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+

🎈 忽略执行的代码

  • 通过添加 add script ignore list 需要忽略执行代码行或文件
  • 也可以达到禁止无限 debugger

🎈 忽略执行代码的对策

  • 那如何针对上面操作的恶意用户呢
  • 可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对
  • Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件
  • 当然使用的时候,为了更加的安全,最好使用加密后的脚本
js
// 加密前
+(() => {
+  function ban() {
+    setInterval(() => {
+      Function("debugger")();
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
+// 加密后
+eval(
+  (function (c, g, a, b, d, e) {
+    d = String;
+    if (!"".replace(/^/, String)) {
+      for (; a--; ) e[a] = b[a] || a;
+      b = [
+        function (f) {
+          return e[f];
+        },
+      ];
+      d = function () {
+        return "w+";
+      };
+      a = 1;
+    }
+    for (; a--; )
+      b[a] && (c = c.replace(new RegExp("\b" + d(a) + "\b", "g"), b[a]));
+    return c;
+  })(
+    '(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',
+    9,
+    9,
+    "block function setInterval Function debugger 50 try catch err".split(" "),
+    0,
+    {}
+  )
+);
+
// 加密前
+(() => {
+  function ban() {
+    setInterval(() => {
+      Function("debugger")();
+    }, 50);
+  }
+  try {
+    ban();
+  } catch (err) {}
+})();
+
+// 加密后
+eval(
+  (function (c, g, a, b, d, e) {
+    d = String;
+    if (!"".replace(/^/, String)) {
+      for (; a--; ) e[a] = b[a] || a;
+      b = [
+        function (f) {
+          return e[f];
+        },
+      ];
+      d = function () {
+        return "w+";
+      };
+      a = 1;
+    }
+    for (; a--; )
+      b[a] && (c = c.replace(new RegExp("\b" + d(a) + "\b", "g"), b[a]));
+    return c;
+  })(
+    '(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',
+    9,
+    9,
+    "block function setInterval Function debugger 50 try catch err".split(" "),
+    0,
+    {}
+  )
+);
+

🎈 终极增强防调试代码

  • 为了让自己写出来的代码更加的晦涩难懂,需要对上面的代码再优化一下
  • Function('debugger').call() 改成 (function(){return false;})['constructor']('debugger')['call']();
  • 并且添加条件,当窗口外部宽高和内部宽高的差值大于一定的值 ,我把 body 里的内容换成指定内容
  • 当然使用的时候,为了更加的安全,最好加密后再使用
js
(() => {
+  function block() {
+    if (
+      window.outerHeight - window.innerHeight > 200 ||
+      window.outerWidth - window.innerWidth > 200
+    ) {
+      document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
+    }
+    setInterval(() => {
+      (function () {
+        return false;
+      })
+        ["constructor"]("debugger")
+        ["call"]();
+    }, 50);
+  }
+  try {
+    block();
+  } catch (err) {}
+})();
+
(() => {
+  function block() {
+    if (
+      window.outerHeight - window.innerHeight > 200 ||
+      window.outerWidth - window.innerWidth > 200
+    ) {
+      document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
+    }
+    setInterval(() => {
+      (function () {
+        return false;
+      })
+        ["constructor"]("debugger")
+        ["call"]();
+    }, 50);
+  }
+  try {
+    block();
+  } catch (err) {}
+})();
+
+ + + + + \ No newline at end of file diff --git a/fragment/fetch-pause.html b/fragment/fetch-pause.html new file mode 100644 index 00000000..1de68dce --- /dev/null +++ b/fragment/fetch-pause.html @@ -0,0 +1,154 @@ + + + + + + 前端中 JS 发起的请求可以暂停吗? | Sunny's blog + + + + + + + + +
Skip to content
On this page

前端中 JS 发起的请求可以暂停吗?

JS 发起的请求可以暂停吗?这一句话当中有两个概念需要明确,一是什么样的状态才能称之为 暂停 ?二是 JS 发起的请求 是什么?

怎么样才算暂停?暂停 全称暂时停止,在已开始未结束的过程中临时停止可以称之为暂停,意味着这个过程可以在某个时间点截断然后在另一个时间点重新续上。

请求应该是什么?

这里得先介绍一下 TCP/IP 网络模型 , 网络模型自上而下分为 应用层、传输层、网络层和网络接口层。

上图表示的意思是,每次网络传输,应用数据在发送至目标前都需要通过网络模型一层一层的包装,就像寄快递一样,把要寄的物品先打包好登记一下大小,再装在盒子里登记一下目的地,然后再装到车上,最后送往目的地。 请求(Request) 这个概念就可以理解为客户端通过若干次数据网络传输,将单份数据完整发给服务端的行为,而针对某次请求服务端往客户端发送的答复数据则可以称之为 响应(Response) 。 理论上应用层的协议可以通过类似于标记数据包序列号等等一系列手段来实现暂停机制。但是 TCP 协议 并不支持 ,TCP 协议的数据传输是流式的,数据被视为一连串的字节流。客户端发送的数据会被拆分成多个 TCP 段(TCP segments),而这些段在网络中是独立传输的,无法直接控制每个 TCP 段的传输,因此也无法实现暂停请求或者暂停响应的功能。

解答提问

如果请求是指网络模型中的一次请求传输,那理所当然是不可能暂停的。 来看看提问者的使用场景 —— JS 发起的请求 ,那么可以认为问题当中的请求,应该是指在 JS 运行时中发起的 XMLHttpRequest 或者是 fetch 请求,而请求既然已经发起,那问的自然就是 响应是否能够被暂停 。 我们都知道像大文件分片上传、以及分片下载之类的功能本质上是将分片顺序定好之后按顺序请求,然后就可以通过中断顺序并记录中断点来实现暂停重传的机制,而单个请求并不具备这样的环境。

用 JS 实现 ”假暂停” 机制

虽然不能真正意义上实现暂停请求,但是我们其实可以模拟一个 假暂停 的功能,在前端的业务场景上,数据不是收到就可以直接打在客户脸上的(什么光速打击),前端开发者需要对这些数据进行处理之后渲染在界面上,如果我们能在请求发起之前增加一个控制器,在请求回来时,如果控制器为暂停状态则不处理数据,等待控制器恢复后再进行处理,是不是也能到达到目的?让我们试着实现一下。

假如我们使用 fetch 来请求。我们可以设计一个控制器 Promise 和请求放在一起用 Promise.all 包裹,当 fetch 完成时判断这个控制器的暂停状态,如果没有被暂停,则控制器也直接 resolve,同时整个 Promise.all 也 resolve 抛出。

ts
function _request() {
+  return new Promise<number>((res) =>
+    setTimeout(() => {
+      res(123);
+    }, 3000)
+  );
+}
+
+// 原本想使用 class extends Promise 来实现
+// 结果一直出现这个问题 https://github.com/nodejs/node/issues/13678
+function createPauseControllerPromise() {
+  const result = {
+    isPause: false,
+    resolveWhenResume: false,
+    resolve(value?: any) {},
+    pause() {
+      this.isPause = true;
+    },
+    resume() {
+      if (!this.isPause) return;
+      this.isPause = false;
+      if (this.resolveWhenResume) {
+        this.resolve();
+      }
+    },
+    promise: Promise.resolve(),
+  };
+
+  const promise = new Promise<void>((res) => {
+    result.resolve = res;
+  });
+
+  result.promise = promise;
+
+  return result;
+}
+
+function requestWithPauseControl<T extends () => Promise<any>>(request: T) {
+  const controller = createPauseControllerPromise();
+
+  const controlRequest = request().then((data) => {
+    if (!controller.isPause) controller.resolve();
+    controller.resolveWhenResume = controller.isPause;
+    return data;
+  });
+
+  const result = Promise.all([controlRequest, controller.promise]).then(
+    (data) => data[0]
+  );
+
+  result.finally(() => controller.resolve())(result as any).pause =
+    controller.pause.bind(controller);
+  (result as any).resume = controller.resume.bind(controller);
+
+  return result as ReturnType<T> & { pause: () => void; resume: () => void };
+}
+
function _request() {
+  return new Promise<number>((res) =>
+    setTimeout(() => {
+      res(123);
+    }, 3000)
+  );
+}
+
+// 原本想使用 class extends Promise 来实现
+// 结果一直出现这个问题 https://github.com/nodejs/node/issues/13678
+function createPauseControllerPromise() {
+  const result = {
+    isPause: false,
+    resolveWhenResume: false,
+    resolve(value?: any) {},
+    pause() {
+      this.isPause = true;
+    },
+    resume() {
+      if (!this.isPause) return;
+      this.isPause = false;
+      if (this.resolveWhenResume) {
+        this.resolve();
+      }
+    },
+    promise: Promise.resolve(),
+  };
+
+  const promise = new Promise<void>((res) => {
+    result.resolve = res;
+  });
+
+  result.promise = promise;
+
+  return result;
+}
+
+function requestWithPauseControl<T extends () => Promise<any>>(request: T) {
+  const controller = createPauseControllerPromise();
+
+  const controlRequest = request().then((data) => {
+    if (!controller.isPause) controller.resolve();
+    controller.resolveWhenResume = controller.isPause;
+    return data;
+  });
+
+  const result = Promise.all([controlRequest, controller.promise]).then(
+    (data) => data[0]
+  );
+
+  result.finally(() => controller.resolve())(result as any).pause =
+    controller.pause.bind(controller);
+  (result as any).resume = controller.resume.bind(controller);
+
+  return result as ReturnType<T> & { pause: () => void; resume: () => void };
+}
+

用法

我们可以通过调用 requestWithPauseControl(_request) 来替代调用 _request 使用,通过返回的 pause 和 resume 方法控制暂停和继续。

ts
const result = requestWithPauseControl(_request).then((data) => {
+  console.log(data);
+});
+
+if (Math.random() > 0.5) {
+  result.pause();
+}
+
+setTimeout(() => {
+  result.resume();
+}, 4000);
+
const result = requestWithPauseControl(_request).then((data) => {
+  console.log(data);
+});
+
+if (Math.random() > 0.5) {
+  result.pause();
+}
+
+setTimeout(() => {
+  result.resume();
+}, 4000);
+

执行原理

流程设计上是这样,设计一个控制器,发起请求并请求返回后,判断控制器的状态,若控制器不处于 “暂停” 状态时,正常返回数据;当控制器处于 “暂停状态” 时,控制器将置为 “一旦调用恢复方法就返回数据” 的状态。 代码中是利用了 Promise.all 捆绑一个控制器 Promise ,如果控制器处于暂停状态下,则不释放 Promise.all ,再将对应的 pause 方法和 resume 方法暴露出去给外界使用。

补充

有些同学错误的认为网络请求和响应是绝对不可以暂停的,我特意在文章前面提到了有关数据传输的内容,并且挂了一句“理论上应用层的协议可以通过类似于标记数据包序列号等等一系列手段来实现暂停机制”,这句话的意思是,如果你魔改 HTTP 或者自己设计实现一个应用层协议(例如像 socket、vmess 这些协议),只要双端支持该协议,是可以实现请求暂停或者响应暂停的,而且这不会影响到 TCP 连接,但是实现暂停机制需要对各种场景和 TCP 策略兜底才能有较好的可靠性。 例如,提供一类控制报文用于控制传输暂停,首先需要对所有数据包的序列号标记顺序,当需要暂停时,发送该序列号的暂停报文给接收端,接收端收到暂停报文就将已接收数据包的块标记返回给发送端等等(这和分片上传机制一样)。

+ + + + + \ No newline at end of file diff --git a/fragment/fetch.html b/fragment/fetch.html new file mode 100644 index 00000000..e58e2b38 --- /dev/null +++ b/fragment/fetch.html @@ -0,0 +1,116 @@ + + + + + + 如何取消 Fetch 请求 | Sunny's blog + + + + + + + + +
Skip to content
On this page

如何取消 Fetch 请求

JavaScript 的 promise 一直是该语言的一大胜利——它们引发了异步编程的革命,极大地改善了 Web 性能。原生 promise 的一个缺点是,到目前为止,还没有可以取消 fetch 的真正方法。 JavaScript 规范中添加了新的 AbortController ,允许开发人员使用信号中止一个或多个 fetch 调用。 以下是取消 fetch 调用的工作流程:

  • 创建一个 AbortController 实例
  • 该实例具有 signal 属性
  • 将 signal 传递给 fetch option 的 signal
  • 调用 AbortController 的 abort 属性来取消所有使用该信号的 fetch。

以下是取消 Fetch 请求的基本步骤:

js
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(`Request 1 is complete!`);
+  })
+  .catch((e) => {
+    console.warn(`Fetch 1 error: ${e.message}`);
+  });
+
+// Abort request
+controller.abort();
+
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(`Request 1 is complete!`);
+  })
+  .catch((e) => {
+    console.warn(`Fetch 1 error: ${e.message}`);
+  });
+
+// Abort request
+controller.abort();
+

在 abort 调用时发生 AbortError ,因此你可以通过比较错误名称来侦听 catch 中的中止操作。

js
catch(e => {
+    if(e.name === "AbortError") {
+        // We know it's been canceled!
+    }
+});
+
catch(e => {
+    if(e.name === "AbortError") {
+        // We know it's been canceled!
+    }
+});
+

将相同的信号传递给多个 fetch 调用将会取消该信号的所有请求:

js
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(`Request 1 is complete!`);
+  })
+  .catch((e) => {
+    console.warn(`Fetch 1 error: ${e.message}`);
+  });
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(`Request 2 is complete!`);
+  })
+  .catch((e) => {
+    console.warn(`Fetch 2 error: ${e.message}`);
+  });
+
+// Wait 2 seconds to abort both requests
+setTimeout(() => controller.abort(), 2000);
+
const controller = new AbortController();
+const { signal } = controller;
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(`Request 1 is complete!`);
+  })
+  .catch((e) => {
+    console.warn(`Fetch 1 error: ${e.message}`);
+  });
+
+fetch("http://localhost:8000", { signal })
+  .then((response) => {
+    console.log(`Request 2 is complete!`);
+  })
+  .catch((e) => {
+    console.warn(`Fetch 2 error: ${e.message}`);
+  });
+
+// Wait 2 seconds to abort both requests
+setTimeout(() => controller.abort(), 2000);
+

杰克·阿奇博尔德(Jack Archibald)在他的文章 Abortable fetch 中,详细介绍了一个很好的应用,它能够用于创建可中止的 Fetch,而无需所有样板

js
function abortableFetch(request, opts) {
+  const controller = new AbortController();
+  const signal = controller.signal;
+
+  return {
+    abort: () => controller.abort(),
+    ready: fetch(request, { ...opts, signal }),
+  };
+}
+
function abortableFetch(request, opts) {
+  const controller = new AbortController();
+  const signal = controller.signal;
+
+  return {
+    abort: () => controller.abort(),
+    ready: fetch(request, { ...opts, signal }),
+  };
+}
+

说实话,我对取消 Fetch 的方法并不感到兴奋。在理想的世界中,通过 Fetch 返回的 Promise 中的 .cancel() 会很酷,但是也会带来一些问题。无论如何,我为能够取消 Fetch 调用而感到高兴,你也应该如此!

[译] 如何取消你的 Promise?

+ + + + + \ No newline at end of file diff --git a/fragment/forEach.html b/fragment/forEach.html new file mode 100644 index 00000000..bbf695b6 --- /dev/null +++ b/fragment/forEach.html @@ -0,0 +1,102 @@ + + + + + + 面试官问我 JS 中 forEach 能不能跳出循环 | Sunny's blog + + + + + + + + +
Skip to content
On this page

面试官问我 JS 中 forEach 能不能跳出循环

forEach 是 不能使用任何手段跳出循环 的!为什么呢?我们知道 forEach 接收一个函数,它一般有两个参数,第一个是循环的当前元素,第二个是该元素对应的下标,手动实现一下伪代码:

js
Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    fn(this[i], i, this);
+  }
+};
+
Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    fn(this[i], i, this);
+  }
+};
+

forEach 是不是真的这么实现我无从考究,但是以上这个简单的伪代码确实满足 forEach 的特性,而且也很明显就是不能跳出循环,因为根本没有办法操作到真正的 for 循环体。

后来经过查阅文档,发现官方对 forEach 的定义根本不是我认为的语法糖,它的标准说法是 forEach 为每个数组元素执行一次你所提供的函数。官方文档也有这么一段话:

除抛出异常之外,没有其他方法可以停止或中断循环。如果您需要这种行为,则该 forEach()方法是错误的工具。

使用抛出异常来跳出 foreach 循环

js
let arr = [0, 1, "stop", 3, 4];
+try {
+  arr.forEach((element) => {
+    if (element === "stop") {
+      throw new Error("forEachBreak");
+    }
+    console.log(element); // 输出 0 1 后面不输出
+  });
+} catch (e) {
+  console.log(e.message); // forEachBreak
+}
+
let arr = [0, 1, "stop", 3, 4];
+try {
+  arr.forEach((element) => {
+    if (element === "stop") {
+      throw new Error("forEachBreak");
+    }
+    console.log(element); // 输出 0 1 后面不输出
+  });
+} catch (e) {
+  console.log(e.message); // forEachBreak
+}
+

那么可不可以认为,forEach 可以跳出循环,使用抛出异常就可以了?这点我认为仁者见仁智者见智吧,在 forEach 的设计中并没有中断循环的设计,而使用 try-catch 包裹时,当 循环体过大性能会随之下降 ,这是无法避免的,所以抛出异常可以作为一种中断 forEach 的手段,但并不是为解决 forEach 问题而存在的银弹。

再次回归到开头写的那段伪代码,对它进行一些优化,在真正的 for 循环中加入对传入函数的判断:

js
// 为避免争议此处不再覆写原有forEach函数
+Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    let ret = fn(this[i], i, this);
+    if (typeof ret !== "undefined" && (ret == null || ret == false)) break;
+  }
+};
+
// 为避免争议此处不再覆写原有forEach函数
+Array.prototype.myForEach = function (fn) {
+  for (let i = 0; i < this.length; i++) {
+    let ret = fn(this[i], i, this);
+    if (typeof ret !== "undefined" && (ret == null || ret == false)) break;
+  }
+};
+

这样的话就能根据 return 值来进行循环跳出啦:

js
let arr = [0, 1, "stop", 3, 4];
+
+arr.myForEach((x) => {
+  if (x === "stop") return false;
+  console.log(x); // 输出 0 1 后面不输出
+});
+
+// return即为continue:
+arr.myForEach((x) => {
+  if (x === "stop") return;
+  console.log(x); // 0 1 3 4
+});
+
let arr = [0, 1, "stop", 3, 4];
+
+arr.myForEach((x) => {
+  if (x === "stop") return false;
+  console.log(x); // 输出 0 1 后面不输出
+});
+
+// return即为continue:
+arr.myForEach((x) => {
+  if (x === "stop") return;
+  console.log(x); // 0 1 3 4
+});
+

文档中还提到 forEach 需要一个同步函数 ,也就是说在使用异步函数或 Promise 作为回调时会发生预期以外的结果,所以 forEach 还是需要慎用。

当然,用简单的 for 循环去完成一切事情也不失为一种办法, 代码首先是写给人看的,附带能在机器上运行的作用 ,forEach 在很多时候用起来更加顺手,但也务必在理解 JS 如何设计这些工具函数的前提下来编写我们的业务代码。

我们可以在遍历数组时使用 for..of.. ,在遍历对象时使用 for..in.. ,而官方也在 forEach 文档下列举了其它一些工具函数,这里不做过多展开:

js
Array.prototype.find();
+Array.prototype.findIndex();
+Array.prototype.map();
+Array.prototype.filter();
+Array.prototype.every();
+Array.prototype.some();
+
Array.prototype.find();
+Array.prototype.findIndex();
+Array.prototype.map();
+Array.prototype.filter();
+Array.prototype.every();
+Array.prototype.some();
+

如何根据不同的业务场景,选择使用对应的工具函数来更有效地处理业务逻辑,才是我们真正应该思考的,或许这也是面试当中真正想考察的吧。

+ + + + + \ No newline at end of file diff --git a/fragment/nextTick.html b/fragment/nextTick.html new file mode 100644 index 00000000..980658be --- /dev/null +++ b/fragment/nextTick.html @@ -0,0 +1,266 @@ + + + + + + vue nextTick 实现原理,必拿下! | Sunny's blog + + + + + + + + +
Skip to content
On this page

vue nextTick 实现原理,必拿下!

为什么会有 nextTick 这个东西的存在?

因为 vue 采用的 异步更新策略 ,当监听到数据发生变化的时候不会立即去更新 DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更。

这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作 DOM 的次数,如果不采用这种方法,假设数据改变 100 次就要去更新 100 次 DOM,而频繁的 DOM 更新是很耗性能的。

nextTick 的作用?

nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到 DOM 更新后才执行;想要操作 基于最新数据生成的 DOM 时,就将这个操作放在 nextTick 的回调中;

nextTick 实现原理

将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务; nextTick 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0)

源码解读

源码位置 core/util/next-tick

源码并不复杂,三个函数,60 几行代码,沉下心去看!

ts
import { noop } from "shared/util";
+import { handleError } from "./error";
+import { isIE, isIOS, isNative } from "./env";
+
+//  noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
+//  handleError 错误处理函数
+//  isIE, isIOS, isNative 环境判断函数,
+//  isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false
+
+export let isUsingMicroTask = false; // 标记 nextTick 最终是否以微任务执行
+
+const callbacks = []; // 存放调用 nextTick 时传入的回调函数
+let pending = false; // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
+// 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
+
+// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
+// 回调的 this 自动绑定到调用它的实例上
+export function nextTick(cb?: Function, ctx?: Object) {
+  let _resolve;
+  // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
+  callbacks.push(() => {
+    if (cb) {
+      // 对传入的回调进行 try catch 错误捕获
+      try {
+        cb.call(ctx);
+      } catch (e) {
+        // 进行统一的错误处理
+        handleError(e, ctx, "nextTick");
+      }
+    } else if (_resolve) {
+      _resolve(ctx);
+    }
+  });
+
+  // 如果当前没有在 pending 的回调,
+  // 就执行 timeFunc 函数选择当前环境优先支持的异步方法
+  if (!pending) {
+    pending = true;
+    timerFunc();
+  }
+
+  // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
+  // 在返回的这个 promise.then 中 DOM 已经更新好了,
+  if (!cb && typeof Promise !== "undefined") {
+    return new Promise((resolve) => {
+      _resolve = resolve;
+    });
+  }
+}
+
+// 判断当前环境优先支持的异步方法,优先选择微任务
+// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
+// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
+// setImmediate 在 IE10 和 node 中支持
+
+// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次
+
+let timerFunc;
+// 判断当前环境是否原生支持 promise
+if (typeof Promise !== "undefined" && isNative(Promise)) {
+  // 支持 promise
+  const p = Promise.resolve();
+  timerFunc = () => {
+    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
+    p.then(flushCallbacks);
+    if (isIOS) setTimeout(noop);
+  };
+  // 标记当前 nextTick 使用的微任务
+  isUsingMicroTask = true;
+
+  // 如果不支持 promise,就判断是否支持 MutationObserver
+  // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
+} else if (
+  !isIE &&
+  typeof MutationObserver !== "undefined" &&
+  (isNative(MutationObserver) ||
+    MutationObserver.toString() === "[object MutationObserverConstructor]")
+) {
+  let counter = 1;
+  // new 一个 MutationObserver 类
+  const observer = new MutationObserver(flushCallbacks);
+  // 创建一个文本节点
+  const textNode = document.createTextNode(String(counter));
+  // 监听这个文本节点,当数据发生变化就执行 flushCallbacks
+  observer.observe(textNode, { characterData: true });
+  timerFunc = () => {
+    counter = (counter + 1) % 2;
+    textNode.data = String(counter); // 数据更新
+  };
+  isUsingMicroTask = true; // 标记当前 nextTick 使用的微任务
+
+  // 判断当前环境是否原生支持 setImmediate
+} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
+  timerFunc = () => {
+    setImmediate(flushCallbacks);
+  };
+} else {
+  // 以上三种都不支持就选择 setTimeout
+  timerFunc = () => {
+    setTimeout(flushCallbacks, 0);
+  };
+}
+
+// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
+// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
+function flushCallbacks() {
+  pending = false;
+  const copies = callbacks.slice(0); // 拷贝一份 callbacks
+  callbacks.length = 0; // 清空 callbacks
+  for (let i = 0; i < copies.length; i++) {
+    // 遍历执行传入的回调
+    copies[i]();
+  }
+}
+
+// 为什么要拷贝一份 callbacks
+
+// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
+// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
+// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
+// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
+// 否则就可能出现一直循环的情况,
+// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调
+
import { noop } from "shared/util";
+import { handleError } from "./error";
+import { isIE, isIOS, isNative } from "./env";
+
+//  noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
+//  handleError 错误处理函数
+//  isIE, isIOS, isNative 环境判断函数,
+//  isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false
+
+export let isUsingMicroTask = false; // 标记 nextTick 最终是否以微任务执行
+
+const callbacks = []; // 存放调用 nextTick 时传入的回调函数
+let pending = false; // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
+// 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
+
+// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
+// 回调的 this 自动绑定到调用它的实例上
+export function nextTick(cb?: Function, ctx?: Object) {
+  let _resolve;
+  // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
+  callbacks.push(() => {
+    if (cb) {
+      // 对传入的回调进行 try catch 错误捕获
+      try {
+        cb.call(ctx);
+      } catch (e) {
+        // 进行统一的错误处理
+        handleError(e, ctx, "nextTick");
+      }
+    } else if (_resolve) {
+      _resolve(ctx);
+    }
+  });
+
+  // 如果当前没有在 pending 的回调,
+  // 就执行 timeFunc 函数选择当前环境优先支持的异步方法
+  if (!pending) {
+    pending = true;
+    timerFunc();
+  }
+
+  // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
+  // 在返回的这个 promise.then 中 DOM 已经更新好了,
+  if (!cb && typeof Promise !== "undefined") {
+    return new Promise((resolve) => {
+      _resolve = resolve;
+    });
+  }
+}
+
+// 判断当前环境优先支持的异步方法,优先选择微任务
+// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
+// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
+// setImmediate 在 IE10 和 node 中支持
+
+// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次
+
+let timerFunc;
+// 判断当前环境是否原生支持 promise
+if (typeof Promise !== "undefined" && isNative(Promise)) {
+  // 支持 promise
+  const p = Promise.resolve();
+  timerFunc = () => {
+    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
+    p.then(flushCallbacks);
+    if (isIOS) setTimeout(noop);
+  };
+  // 标记当前 nextTick 使用的微任务
+  isUsingMicroTask = true;
+
+  // 如果不支持 promise,就判断是否支持 MutationObserver
+  // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
+} else if (
+  !isIE &&
+  typeof MutationObserver !== "undefined" &&
+  (isNative(MutationObserver) ||
+    MutationObserver.toString() === "[object MutationObserverConstructor]")
+) {
+  let counter = 1;
+  // new 一个 MutationObserver 类
+  const observer = new MutationObserver(flushCallbacks);
+  // 创建一个文本节点
+  const textNode = document.createTextNode(String(counter));
+  // 监听这个文本节点,当数据发生变化就执行 flushCallbacks
+  observer.observe(textNode, { characterData: true });
+  timerFunc = () => {
+    counter = (counter + 1) % 2;
+    textNode.data = String(counter); // 数据更新
+  };
+  isUsingMicroTask = true; // 标记当前 nextTick 使用的微任务
+
+  // 判断当前环境是否原生支持 setImmediate
+} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
+  timerFunc = () => {
+    setImmediate(flushCallbacks);
+  };
+} else {
+  // 以上三种都不支持就选择 setTimeout
+  timerFunc = () => {
+    setTimeout(flushCallbacks, 0);
+  };
+}
+
+// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
+// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
+function flushCallbacks() {
+  pending = false;
+  const copies = callbacks.slice(0); // 拷贝一份 callbacks
+  callbacks.length = 0; // 清空 callbacks
+  for (let i = 0; i < copies.length; i++) {
+    // 遍历执行传入的回调
+    copies[i]();
+  }
+}
+
+// 为什么要拷贝一份 callbacks
+
+// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
+// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
+// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
+// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
+// 否则就可能出现一直循环的情况,
+// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调
+
+ + + + + \ No newline at end of file diff --git a/fragment/npm-scripts.html b/fragment/npm-scripts.html new file mode 100644 index 00000000..cce9bbdf --- /dev/null +++ b/fragment/npm-scripts.html @@ -0,0 +1,38 @@ + + + + + + 输入 npm run xxx 后发生了什么? | Sunny's blog + + + + + + + + +
Skip to content
On this page

输入 npm run xxx 后发生了什么?

在前端开发中,我们经常使用 npm run dev 来启动本地开发环境。那么,当输入完类似 npm run xxx 的命令后,到底发生了什么呢?项目又是如何被启动的?

首先我们知道,node 可以把 .js 文件当做脚本来运行,例如启动后台项目时经常会输入:

shell
node app.js
+
node app.js
+

这个是直接使用全局安装的 node 命令来执行了 ./src 目录下的 app.js 文件。

而当我们使用 npm (或者 yarn)来管理项目时,会在根目录下生成一个 package.json 文件,其中的 scripts 属性,就是用于配置 npm run xxx 命令的,比如以下配置:

json
"scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+},
+
"scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+},
+

再看对应的 node_modules 目录下的.bin 目录,里面有一个 vite 文件

打开 vite 文件可以看到 #!/bin/sh,表示这是一个脚本(node 可以执行)。

当执行 npm run dev 时,首先会去 package.json 文件中找到 script 属性的"dev",然后再到 node_modules 目录下的.bin 目录中找到"dev"对应的"vite",执行 vite(由前面可知 vite 是一个脚本,类似于前面提过的 node app.js)。 再如:

json
{
+  "dev": "./node_modules/.bin/nodemon bin/www"
+}
+
{
+  "dev": "./node_modules/.bin/nodemon bin/www"
+}
+

当执行 npm run dev 时,相当于执行 "./node_modules/.bin/nodemon bin/www",其中 bin/www 作为参数传入。

总之:先去 package.json 的 scripts 属性中查找 xxx1,再去./node_modules/.bin 中查找 xxx1 对应的 sss1,执行 sss1 文件)

问题分析

TIP

问题 1: 为什么不直接执行 sss 而要执行 xxx 呢?

直接执行 sss 会报错,因为操作系统中不存在 sss 这一条指令。

TIP

问题 2: 为什么执行 npm run xxx(或者 yarn xxx)时就能成功呢?

因为我们在安装依赖的时候,是通过 npm i xxx (或者 yarn ...)来执行的,例如 npm i @vue/cli-service,npm 在 安装这个依赖的时候,就会 node_modules/.bin/ 目录中创建好名为 vue-cli-service 的几个可执行文件了。

.bin 目录下的文件,是一个个的软链接,打开文件可以看到文件顶部写着 #!/bin/sh ,表示这是一个脚本,可由 node 来执行。当使用 npm run xxx 执行 sss 时,虽然没有安装 sss 的全局命令,但是 npm 会到 ./node_modules/.bin 中找到 sss 文件作为脚本来执行,则相当于执行了 ./node_modules/.bin/sss。

即: 运行 npm run xxx 的时候,npm 会先在当前目录的 node_modules/.bin 查找要执行的程序,如果找到则运行;没有找到则从全局的 node_modules/.bin 中查找; 全局目录还是没找到,就会从 window 的 path 环境变量中查找有没有其他同名的可执行程序。

浅析 npm install 机制阅读

+ + + + + \ No newline at end of file diff --git a/fragment/promise-cancel.html b/fragment/promise-cancel.html new file mode 100644 index 00000000..39c1a279 --- /dev/null +++ b/fragment/promise-cancel.html @@ -0,0 +1,372 @@ + + + + + + 面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:? | Sunny's blog + + + + + + + + +
Skip to content
On this page

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:?

问题:点击一个 button,触发一次这个函数,但是既没有走成功回调,也没有走失败回调。问下一次点击,类似取消上次 promise 的操作,但没有这种方法,怎么去做。我又说了提供变量控制,这个可以,还有就说不出来了。面试官说可以包装 promise,写一个类,提供一个 cancel 方法,类似这种,主要是不想拿到期望的 promise 值。

原面经链接: 美团暑期一面_牛客网 (nowcoder.com)

取消功能

我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它 永远停留至 pending 状态 。奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了 🤔

js
const p = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    // handler data, no resolve and reject
+  }, 1000);
+});
+console.log(p); // Promise {<pending>} 💡
+
const p = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    // handler data, no resolve and reject
+  }, 1000);
+});
+console.log(p); // Promise {<pending>} 💡
+

但注意我们的需求条件,是 在状态转换过程中 ,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。 其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:

js
const getData = () =>
+  new Promise((resolve) => {
+    setTimeout(() => {
+      console.log("发送网络请求获取数据"); // ❗
+      resolve("success get Data");
+    }, 2500);
+  });
+
+const timer = () =>
+  new Promise((_, reject) => {
+    setTimeout(() => {
+      reject("timeout");
+    }, 2000);
+  });
+
+const p = Promise.race([getData(), timer()])
+  .then((res) => {
+    console.log("获取数据:", res);
+  })
+  .catch((err) => {
+    console.log("超时: ", err);
+  });
+
const getData = () =>
+  new Promise((resolve) => {
+    setTimeout(() => {
+      console.log("发送网络请求获取数据"); // ❗
+      resolve("success get Data");
+    }, 2500);
+  });
+
+const timer = () =>
+  new Promise((_, reject) => {
+    setTimeout(() => {
+      reject("timeout");
+    }, 2000);
+  });
+
+const p = Promise.race([getData(), timer()])
+  .then((res) => {
+    console.log("获取数据:", res);
+  })
+  .catch((err) => {
+    console.log("超时: ", err);
+  });
+

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎 🤔,即使超时网络请求还会发出。

而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。比如让用户进行控制, 一个按钮用来表示发送请求,一个按钮表示取消 ,来中断 promise 的流程:当然这里我们不讨论关于请求的取消操作,重点在 Promise 上

其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生, clearTimeout 、 clearInterval 嘛。

OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <button id="send">Send</button>
+    <button id="cancel">Cancel</button>
+
+    <script>
+      class CancelToken {
+        constructor(cancelFn) {
+          this.promise = new Promise((resolve, reject) => {
+            cancelFn(() => {
+              console.log("delay cancelled");
+              resolve();
+            });
+          });
+        }
+      }
+      const sendButton = document.querySelector("#send");
+      const cancelButton = document.querySelector("#cancel");
+
+      function cancellableDelayedResolve(delay) {
+        console.log("prepare send request");
+        return new Promise((resolve, reject) => {
+          const id = setTimeout(() => {
+            console.log("ajax get data");
+            resolve();
+          }, delay);
+
+          const cancelToken = new CancelToken((cancelCallback) =>
+            cancelButton.addEventListener("click", cancelCallback)
+          );
+          cancelToken.promise.then(() => clearTimeout(id));
+        });
+      }
+      sendButton.addEventListener("click", () =>
+        cancellableDelayedResolve(1000)
+      );
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <button id="send">Send</button>
+    <button id="cancel">Cancel</button>
+
+    <script>
+      class CancelToken {
+        constructor(cancelFn) {
+          this.promise = new Promise((resolve, reject) => {
+            cancelFn(() => {
+              console.log("delay cancelled");
+              resolve();
+            });
+          });
+        }
+      }
+      const sendButton = document.querySelector("#send");
+      const cancelButton = document.querySelector("#cancel");
+
+      function cancellableDelayedResolve(delay) {
+        console.log("prepare send request");
+        return new Promise((resolve, reject) => {
+          const id = setTimeout(() => {
+            console.log("ajax get data");
+            resolve();
+          }, delay);
+
+          const cancelToken = new CancelToken((cancelCallback) =>
+            cancelButton.addEventListener("click", cancelCallback)
+          );
+          cancelToken.promise.then(() => clearTimeout(id));
+        });
+      }
+      sendButton.addEventListener("click", () =>
+        cancellableDelayedResolve(1000)
+      );
+    </script>
+  </body>
+</html>
+

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:

首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为 取消功能期限 ,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。

这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理 🤔?

介于自己没理解,我就按照自己的思路封装个不一样的 🤣:

js
const sendButton = document.querySelector("#send");
+const cancelButton = document.querySelector("#cancel");
+
+class CancelPromise {
+  // delay: 取消功能期限  request:获取数据请求(必须返回 promise)
+  constructor(delay, request) {
+    this.req = request;
+    this.delay = delay;
+    this.timer = null;
+  }
+
+  delayResolve() {
+    return new Promise((resolve, reject) => {
+      console.log("prepare request");
+      this.timer = setTimeout(() => {
+        console.log("send request");
+        this.timer = null;
+        this.req().then(
+          (res) => resolve(res),
+          (err) => reject(err)
+        );
+      }, this.delay);
+    });
+  }
+
+  cancelResolve() {
+    console.log("cancel promise");
+    this.timer && clearTimeout(this.timer);
+  }
+}
+
+// 模拟网络请求
+function getData() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve("this is data");
+    }, 2000);
+  });
+}
+
+const cp = new CancelPromise(1000, getData);
+
+sendButton.addEventListener("click", () =>
+  cp.delayResolve().then((res) => {
+    console.log("拿到数据:", res);
+  })
+);
+cancelButton.addEventListener("click", () => cp.cancelResolve());
+
const sendButton = document.querySelector("#send");
+const cancelButton = document.querySelector("#cancel");
+
+class CancelPromise {
+  // delay: 取消功能期限  request:获取数据请求(必须返回 promise)
+  constructor(delay, request) {
+    this.req = request;
+    this.delay = delay;
+    this.timer = null;
+  }
+
+  delayResolve() {
+    return new Promise((resolve, reject) => {
+      console.log("prepare request");
+      this.timer = setTimeout(() => {
+        console.log("send request");
+        this.timer = null;
+        this.req().then(
+          (res) => resolve(res),
+          (err) => reject(err)
+        );
+      }, this.delay);
+    });
+  }
+
+  cancelResolve() {
+    console.log("cancel promise");
+    this.timer && clearTimeout(this.timer);
+  }
+}
+
+// 模拟网络请求
+function getData() {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve("this is data");
+    }, 2000);
+  });
+}
+
+const cp = new CancelPromise(1000, getData);
+
+sendButton.addEventListener("click", () =>
+  cp.delayResolve().then((res) => {
+    console.log("拿到数据:", res);
+  })
+);
+cancelButton.addEventListener("click", () => cp.cancelResolve());
+

进度通知功能

进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:

执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用

这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:

js
class TrackablePromise extends Promise {
+  constructor(executor) {
+    const notifyHandlers = [];
+    super((resolve, reject) => {
+      return executor(resolve, reject, (status) => {
+        notifyHandlers.map((handler) => handler(status));
+      });
+    });
+    this.notifyHandlers = notifyHandlers;
+  }
+  notify(notifyHandler) {
+    this.notifyHandlers.push(notifyHandler);
+    return this;
+  }
+}
+let p = new TrackablePromise((resolve, reject, notify) => {
+  function countdown(x) {
+    if (x > 0) {
+      notify(`${20 * x}% remaining`);
+      setTimeout(() => countdown(x - 1), 1000);
+    } else {
+      resolve();
+    }
+  }
+  countdown(5);
+});
+
+p.notify((x) => setTimeout(console.log, 0, "progress:", x));
+p.then(() => setTimeout(console.log, 0, "completed"));
+
class TrackablePromise extends Promise {
+  constructor(executor) {
+    const notifyHandlers = [];
+    super((resolve, reject) => {
+      return executor(resolve, reject, (status) => {
+        notifyHandlers.map((handler) => handler(status));
+      });
+    });
+    this.notifyHandlers = notifyHandlers;
+  }
+  notify(notifyHandler) {
+    this.notifyHandlers.push(notifyHandler);
+    return this;
+  }
+}
+let p = new TrackablePromise((resolve, reject, notify) => {
+  function countdown(x) {
+    if (x > 0) {
+      notify(`${20 * x}% remaining`);
+      setTimeout(() => countdown(x - 1), 1000);
+    } else {
+      resolve();
+    }
+  }
+  countdown(5);
+});
+
+p.notify((x) => setTimeout(console.log, 0, "progress:", x));
+p.then(() => setTimeout(console.log, 0, "completed"));
+

emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?

不好就自己再写一个 🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:

js
// 模拟数据请求
+function getData(timer, value) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(value);
+    }, timer);
+  });
+}
+
+let p = new TrackablePromise(async (resolve, reject, notify) => {
+  try {
+    const res1 = await getData1();
+    notify("已获取到一阶段数据");
+    const res2 = await getData2();
+    notify("已获取到二阶段数据");
+    const res3 = await getData3();
+    notify("已获取到三阶段数据");
+    resolve([res1, res2, res3]);
+  } catch (error) {
+    notify("出错!");
+    reject(error);
+  }
+});
+
+p.notify((x) => console.log(x));
+p.then((res) => console.log("Get All Data:", res));
+
// 模拟数据请求
+function getData(timer, value) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve(value);
+    }, timer);
+  });
+}
+
+let p = new TrackablePromise(async (resolve, reject, notify) => {
+  try {
+    const res1 = await getData1();
+    notify("已获取到一阶段数据");
+    const res2 = await getData2();
+    notify("已获取到二阶段数据");
+    const res3 = await getData3();
+    notify("已获取到三阶段数据");
+    resolve([res1, res2, res3]);
+  } catch (error) {
+    notify("出错!");
+    reject(error);
+  }
+});
+
+p.notify((x) => console.log(x));
+p.then((res) => console.log("Get All Data:", res));
+

End

关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。

实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...

+ + + + + \ No newline at end of file diff --git a/fragment/react-duplicate.html b/fragment/react-duplicate.html new file mode 100644 index 00000000..a5001a98 --- /dev/null +++ b/fragment/react-duplicate.html @@ -0,0 +1,44 @@ + + + + + + 解决 npm link 本地代码后 react 库存在多份实例的问题 | Sunny's blog + + + + + + + + +
Skip to content
On this page

解决 npm link 本地代码后 react 库存在多份实例的问题

非法 Hook 调用错误

在调试本地组件库代码时,link 到主项目时报错了如下错误

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
+1. You might have mismatching versions of React and the renderer (such as React DOM)
+2. You might be breaking the Rules of Hooks
+3. You might have more than one copy of React in the same app See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
+
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
+1. You might have mismatching versions of React and the renderer (such as React DOM)
+2. You might be breaking the Rules of Hooks
+3. You might have more than one copy of React in the same app See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
+

但是将代码发布到本地仓库后,install 下来是不会有问题的。

报错原因

React 官方文档上解释了这个原因。

为了使 Hook 正常工作,你应用代码中的 react 依赖以及 react-dom 的 package 内部使用的 react 依赖,必须解析为同一个模块。

也就是说,在你的应用里找到了两份 react 实例,并且 react-dom 中引用的 react 实例与直接 import 的 react 实例不是同一份。对应到我报错的场景里,就是 link 到本地组件库时,组件库中 import 了一份 react-dom, 而这一份 react-dom 引用的 react 实例,是本地组件库中安装的 react 实例。而非主应用中安装的那一份 react 实例。

npm install 在安装依赖的时候,会合并相同依赖,安装在最外层的 node_modules 中。所以只有一份依赖。而 npm link 是直接引用本地目录,本地目录的 node_modules 会和主项目的 node_modules 同时存在两份相同的引用。

怎么解决这个问题

要解决这个问题,就需要将组件库 react-dom 依赖的 react 指向主项目 node_modules 中安装的那一份 react 实例。我们可以通过两种方式来完成这个操作。

shell
cd [PACKAGE]
+npm link [PROJECT]/node_modules/react
+
cd [PACKAGE]
+npm link [PROJECT]/node_modules/react
+

使用 alias 为 Nodejs 指定 react 加载目录

js
// /build/wepack.base.config.js
+const config = {
+  alias: {
+    react: path.join(__dirname, "../node_modules/react"),
+  },
+};
+
// /build/wepack.base.config.js
+const config = {
+  alias: {
+    react: path.join(__dirname, "../node_modules/react"),
+  },
+};
+
+ + + + + \ No newline at end of file diff --git a/fragment/react-hooks-timer.html b/fragment/react-hooks-timer.html new file mode 100644 index 00000000..9379c20f --- /dev/null +++ b/fragment/react-hooks-timer.html @@ -0,0 +1,230 @@ + + + + + + React Hooks 实现倒计时及避坑 | Sunny's blog + + + + + + + + +
Skip to content
On this page

React Hooks 实现倒计时及避坑

正确示例 1

jsx
import { useState, useEffect } from "react";
+
+// 入参是一个时间段,比如6000s,不是具体的过期时间点,因为考虑到用户可能会修改电脑的时间,这样在获取当前时间的时候就会产生误差
+export const CountDown = ({ countDown = 4 }) => {
+  let cd = countDown;
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const handleData = () => {
+    if (cd <= 0) {
+      setTime("到时间啦");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(`倒计时: ${d}${h}${m}${s}秒`);
+    cd--;
+    timer = setTimeout(() => {
+      handleData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    handleData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+export default CountDown;
+
import { useState, useEffect } from "react";
+
+// 入参是一个时间段,比如6000s,不是具体的过期时间点,因为考虑到用户可能会修改电脑的时间,这样在获取当前时间的时候就会产生误差
+export const CountDown = ({ countDown = 4 }) => {
+  let cd = countDown;
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const handleData = () => {
+    if (cd <= 0) {
+      setTime("到时间啦");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(`倒计时: ${d}${h}${m}${s}秒`);
+    cd--;
+    timer = setTimeout(() => {
+      handleData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    handleData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+export default CountDown;
+

正确示例 2

jsx
import { useState, useEffect, useRef } from "react";
+
+const CountDown = ({ countDown }) => {
+  const cd = useRef(countDown);
+  const timer = useRef(null);
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd.current <= 0) {
+      setTime("");
+      return timer.current && clearTimeout(timer.current);
+    }
+    const d = parseInt(cd.current / (24 * 60 * 60) + "");
+    const h = parseInt(((cd.current / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd.current / 60) % 60) + "");
+    const s = parseInt((cd.current % 60) + "");
+    setTime(`倒计时: ${d}${h}${m}${s}秒`);
+    cd.current--;
+    timer.current = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer.current && clearTimeout(timer.current);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+
import { useState, useEffect, useRef } from "react";
+
+const CountDown = ({ countDown }) => {
+  const cd = useRef(countDown);
+  const timer = useRef(null);
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd.current <= 0) {
+      setTime("");
+      return timer.current && clearTimeout(timer.current);
+    }
+    const d = parseInt(cd.current / (24 * 60 * 60) + "");
+    const h = parseInt(((cd.current / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd.current / 60) % 60) + "");
+    const s = parseInt((cd.current % 60) + "");
+    setTime(`倒计时: ${d}${h}${m}${s}秒`);
+    cd.current--;
+    timer.current = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer.current && clearTimeout(timer.current);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+

错误示例

jsx
import { useState, useEffect } from "react";
+
+const CountDown = ({ countDown = 5 }) => {
+  let [cd, setCd] = useState(countDown);
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd <= 0) {
+      setTime("");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(`倒计时: ${d}${h}${m}${s}秒`);
+    setCd(cd--); // 应该是setCd(cd - 1)
+    timer = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+
import { useState, useEffect } from "react";
+
+const CountDown = ({ countDown = 5 }) => {
+  let [cd, setCd] = useState(countDown);
+  let timer = null;
+
+  const [time, setTime] = useState("");
+
+  const dealData = () => {
+    if (cd <= 0) {
+      setTime("");
+      return timer && clearTimeout(timer);
+    }
+    const d = parseInt(cd / (24 * 60 * 60) + "");
+    const h = parseInt(((cd / (60 * 60)) % 24) + "");
+    const m = parseInt(((cd / 60) % 60) + "");
+    const s = parseInt((cd % 60) + "");
+    setTime(`倒计时: ${d}${h}${m}${s}秒`);
+    setCd(cd--); // 应该是setCd(cd - 1)
+    timer = setTimeout(() => {
+      dealData();
+    }, 1000);
+  };
+
+  useEffect(() => {
+    dealData();
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, []);
+
+  return <div>{time}</div>;
+};
+
+export default CountDown;
+

错误示例会出现的问题

  • cd 在 useEffect 中始终是初始值;
  • 在 DOM 元素上,会出现先是初始值,然后从初始值减 1 ,之后就不会在变化

原因

  • 因为 useEffect 的第二个参数是 [] ,所以 re-render (更新渲染)的时候不会重新执行 effect 函数,所以 cd 在 useEffect 中始终是初始值;
  • 因为 useEffect 第一次执行的时候即初始化的时候利用 setCd(cd--) 重新对 cd 赋值,所以会触发视图重新渲染一次;
  • 如果 useEffect 没有第二个参数 [] ,既没有依赖,那么就相当于 didMount 和 DidUpdate 两个生命周期的合集,所以更新的时候会重新执行 useEffect 【注意】: useEffect 的第一个参数是一个匿名函数,匿名函数执行完会立即被销毁,所以在组件重新更新的时候,会重新调用匿名函数,并获取更改后的 cd 值

解决方案

如上示例 1 , 2 ,因为方式 1 打破了 React 纯函数的规则,所以更加建议方式 2

TIP

第一段代码使用了普通的变量 cd 和 timer,它们被定义在组件函数的顶层。这意味着每次组件函数被调用时,都会重新创建新的 cd 和 timer,而不是保留它们的状态。这样做违反了 React 纯函数组件的规则,因为它引入了外部的状态管理。

第二段代码使用了 useRef 来创建了 cd 和 timer 这两个变量的引用。这意味着它们会被保存在组件的生命周期之外,并且在组件的多次渲染之间保持不变。因此,即使组件函数被重新调用,这些引用的值也会保持不变。这样做遵守了 React 纯函数组件的规则,因为它不引入外部状态管理。

因此,第二段代码符合 React 纯函数组件的规则,而第一段代码打破了这些规则。

【注意】

  • 在倒计时为 0 的时候一定要销毁倒计时
  • 在组件销毁的时候一定要手动销毁倒计时,否则即使跳转到其他页面,倒计时依旧在进行
+ + + + + \ No newline at end of file diff --git a/fragment/react-useState.html b/fragment/react-useState.html new file mode 100644 index 00000000..5f2643de --- /dev/null +++ b/fragment/react-useState.html @@ -0,0 +1,374 @@ + + + + + + react 函数组件中使用 useState 改变值后立刻获取最新值 | Sunny's blog + + + + + + + + +
Skip to content
On this page

react 函数组件中使用 useState 改变值后立刻获取最新值

为什么说 redux 和 react-hooks 有些渊源?

redux 是基于发布订阅模式,在内部实现一个状态,然后加一些约定和限制,外部不可以直接改变这个状态,只能通过 redux 提供的一个代理方法去触发这个状态的改变,使用者可以通过 redux 提供的订阅方法订阅事件,当状态改变的时候,redux 帮助我们发布这个事件,使得各个订阅者获得最新状态,是不是很简单,大道至简~

下面提供了个简版的 redux 代码

js
/**
+ *
+ * @param {*} reducer 处理状态方法
+ * @param {*} preloadState 初始状态
+ */
+function createStore(reducer, preloadState) {
+  /** 存储状态 */
+  let state = preloadState;
+
+  /** 存储事件 */
+  const listeners = [];
+
+  /** 获取状态方法,返回state */
+  const getState = () => {
+    return state;
+  };
+
+  /**
+   * 订阅事件,将事件存储到listeners中
+   * @param {*} listener 事件
+   */
+  const subscribe = (listener) => {
+    listeners.push(listener);
+  };
+
+  /**
+   * 派发action,发布事件
+   * @param {*} action 动作
+   */
+  const dispatch = (action) => {
+    state = reducer(state, action);
+    listeners.forEach((listener) => listener());
+  };
+
+  /** 状态机 */
+  const store = {
+    getState,
+    subscribe,
+    dispatch,
+  };
+
+  return store;
+}
+
+export default createStore;
+
/**
+ *
+ * @param {*} reducer 处理状态方法
+ * @param {*} preloadState 初始状态
+ */
+function createStore(reducer, preloadState) {
+  /** 存储状态 */
+  let state = preloadState;
+
+  /** 存储事件 */
+  const listeners = [];
+
+  /** 获取状态方法,返回state */
+  const getState = () => {
+    return state;
+  };
+
+  /**
+   * 订阅事件,将事件存储到listeners中
+   * @param {*} listener 事件
+   */
+  const subscribe = (listener) => {
+    listeners.push(listener);
+  };
+
+  /**
+   * 派发action,发布事件
+   * @param {*} action 动作
+   */
+  const dispatch = (action) => {
+    state = reducer(state, action);
+    listeners.forEach((listener) => listener());
+  };
+
+  /** 状态机 */
+  const store = {
+    getState,
+    subscribe,
+    dispatch,
+  };
+
+  return store;
+}
+
+export default createStore;
+

为什么说 react-hooks 跟 redux 有渊源,因为 Dan 总把 redux 的这种思想带到了 react-hooks 中,通过创建一个公共 hookStates 来存储所有的 hooks,通过添加一个索引 hookIndex 来顺序执行每一个 hook,所以 react-hooks 规定 hooks 不能使用在 if 语句和 for 语句中,来保持 hooks 按顺序执行,在 react-hooks 中有一个叫 useReducer 的 hooks,正是按 redux 的这个思想实现的,而我们常用的 useState 只是 useReducer 的一个语法糖,有人可能会说这个 hookStates 会不会很大,所有的 hooks 都存在里边,毕竟一个项目中有那么多组件,我说的只是简单的实现原理,我们知道 react 在 16 的几个版本之后就加入了 fiber 的概念,在 react 内部实现中,会把每个组件的 hookStates 存放到 fiber 节点上,来维护各个节点的状态,react 每次 render 之后都会把 hookIndex 置为 0,来保证下次 hooks 执行是从第一个开始执行,每个 hooks 执行完也会把 hookIndex++,下面是 useReducer 的简单实现:

js
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} reducer 状态处理函数
+ * @param {*} initialState 初始状态,可以传入函数实现懒加载
+ * @returns
+ */
+function useReducer(reducer, initialState) {
+  hookStates[hookIndex] =
+    hookStates[hookIndex] || typeof initialState === "function"
+      ? initialState()
+      : initialState;
+
+  /**
+   *
+   * dispatch
+   * @param {*} action 动作
+   * @returns
+   */
+  const dispatch = (action) => {
+    hookStates[hookIndex] = reducer
+      ? reducer(hookStates[hookIndex], action)
+      : action;
+    /** React内部方法实现重新render */
+    scheduleUpdate();
+  };
+  return [hookStates[hookIndex++], dispatch];
+}
+
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} reducer 状态处理函数
+ * @param {*} initialState 初始状态,可以传入函数实现懒加载
+ * @returns
+ */
+function useReducer(reducer, initialState) {
+  hookStates[hookIndex] =
+    hookStates[hookIndex] || typeof initialState === "function"
+      ? initialState()
+      : initialState;
+
+  /**
+   *
+   * dispatch
+   * @param {*} action 动作
+   * @returns
+   */
+  const dispatch = (action) => {
+    hookStates[hookIndex] = reducer
+      ? reducer(hookStates[hookIndex], action)
+      : action;
+    /** React内部方法实现重新render */
+    scheduleUpdate();
+  };
+  return [hookStates[hookIndex++], dispatch];
+}
+

前文说过 useState 是 useReducer 的语法糖,所以下面是 useState 的实现

js
/**
+ *
+ * useState 直接将reducer传为null,调用useReducer并返回
+ * @param {*} initialState 初始化状态
+ * @returns
+ */
+function useState(initialState) {
+  return useReducer(null, initialState);
+}
+
/**
+ *
+ * useState 直接将reducer传为null,调用useReducer并返回
+ * @param {*} initialState 初始化状态
+ * @returns
+ */
+function useState(initialState) {
+  return useReducer(null, initialState);
+}
+

如何实现 useState 改变值之后立刻获取最新的状态?

react-hooks 的思想其实是同步逻辑,但是在 react 的合成事件中状态更新是异步的,看一下下面这个场景:

jsx
function App() {
+  const [state, setstate] = useState(0);
+
+  const setT = () => {
+    setstate(2);
+    func();
+  };
+
+  const func = () => {
+    console.log(state);
+  };
+
+  return (
+    <div className="App">
+      <header className="App-header">
+        <button onClick={setT}>set 2</button>
+      </header>
+    </div>
+  );
+}
+
function App() {
+  const [state, setstate] = useState(0);
+
+  const setT = () => {
+    setstate(2);
+    func();
+  };
+
+  const func = () => {
+    console.log(state);
+  };
+
+  return (
+    <div className="App">
+      <header className="App-header">
+        <button onClick={setT}>set 2</button>
+      </header>
+    </div>
+  );
+}
+

当我们点击 set 2 按钮时,控制台打印的还是上一次的值 0,而不是最新的 2

因为在 react 合成事件中改变状态是异步的,出于减少 render 次数,react 会收集所有状态变更,然后比对优化,最后做一次变更,在代码中可以看出,func 的调用和 setstate 在同一个宏任务中,这是 react 还没有 render,所以直接使用 state 获取的肯定是上一次闭包里的值 0

有的人可能会说,直接将最新的值当作参数传递给 func 不就行了吗,对,这也是一种解决办法,但是有时不只是一个状态,可能要传递的参数很多,再有也是出于对 react-hooks 的深入研究,所以我选择通过自定义 hooks 实现在 useState 改变值之后立刻获取到最新的值,我们先看下实现的效果,代码变更如下:

如何实现 useSyncCallback?

js
// param: callback为回调函数
+// 用法 const newFunc = useSyncCallback(yourCallback)
+import { useEffect, useState, useCallback } from "react";
+
+const useSyncCallback = (callback) => {
+  const [proxyState, setProxyState] = useState({ current: false });
+
+  const Func = useCallback(() => {
+    setProxyState({ current: true });
+  }, [proxyState]);
+
+  useEffect(() => {
+    if (proxyState.current === true) setProxyState({ current: false });
+  }, [proxyState]);
+
+  useEffect(() => {
+    proxyState.current && callback();
+  });
+
+  return Func;
+};
+
+export default useSyncCallback;
+
// param: callback为回调函数
+// 用法 const newFunc = useSyncCallback(yourCallback)
+import { useEffect, useState, useCallback } from "react";
+
+const useSyncCallback = (callback) => {
+  const [proxyState, setProxyState] = useState({ current: false });
+
+  const Func = useCallback(() => {
+    setProxyState({ current: true });
+  }, [proxyState]);
+
+  useEffect(() => {
+    if (proxyState.current === true) setProxyState({ current: false });
+  }, [proxyState]);
+
+  useEffect(() => {
+    proxyState.current && callback();
+  });
+
+  return Func;
+};
+
+export default useSyncCallback;
+

在这里还需要介绍一个知识点 useEffect,useEffect 会在每次函数式组件 render 之后执行,可以通过传递第二个参数,配置依赖项,当依赖项变更 useEffect 才会更新,如果第二个参数传递一个空数组[],那么 useEffect 就会只执行一次,react-hooks 正是使用 useEffect 来模拟 componentDidMount 和 componentDidUpdate 两个生命周期,也可以通过在 useEffect 中 return 一个函数模拟 componentWillUnmount,那么 useEffect 是如何实现在 render 之后调用的呢,原理就是在 useEffect hooks 中会封装一个宏任务,然后把传进来的回调函数放到宏任务中去执行,下面实现了一个简版的 useEffect:

js
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} callback 副作用函数
+ * @param {*} dependencies 依赖项
+ * @returns
+ */
+function useEffect(callback, dependencies) {
+  /** 判断当前索引下hookStates是否存在值 */
+  if (hookStates[hookIndex]) {
+    /** 如果存在就取出,并结构出destroyFunc销毁函数,和上一次依赖项 */
+    const [destroyFunc, lastDep] = hookStates[hookIndex];
+    /** 判断当前依赖项中的值和上次依赖项中的值有没有变化 */
+    const isSame =
+      dependencies &&
+      dependencies.every((dep, index) => dep === lastDep[index]);
+    if (isSame) {
+      /** 如果没有变化就把索引加一,hooks向后遍历 */
+      hookIndex++;
+    } else {
+      /** 如果有变化,并且存在销毁函数,就先调用销毁函数 */
+      destroyFunc && destroyFunc();
+      /** 创建一个新的宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+      setTimeout(() => {
+        const destroyFunction = callback();
+        hookStates[hookIndex++] = [destroyFunction, dependencies];
+      });
+    }
+  } else {
+    /** 如果hookStates中不存在,证明是首次执行,直接创建一个宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+    setTimeout(() => {
+      const destroyFunction = callback();
+      hookStates[hookIndex++] = [destroyFunction, dependencies];
+    });
+  }
+}
+
/** @param hookStates 存储所有hooks */
+const hookStates = [];
+
+/** @param hookIndex 索引 */
+let hookIndex = 0;
+
+/**
+ *
+ * useReducer
+ * @param {*} callback 副作用函数
+ * @param {*} dependencies 依赖项
+ * @returns
+ */
+function useEffect(callback, dependencies) {
+  /** 判断当前索引下hookStates是否存在值 */
+  if (hookStates[hookIndex]) {
+    /** 如果存在就取出,并结构出destroyFunc销毁函数,和上一次依赖项 */
+    const [destroyFunc, lastDep] = hookStates[hookIndex];
+    /** 判断当前依赖项中的值和上次依赖项中的值有没有变化 */
+    const isSame =
+      dependencies &&
+      dependencies.every((dep, index) => dep === lastDep[index]);
+    if (isSame) {
+      /** 如果没有变化就把索引加一,hooks向后遍历 */
+      hookIndex++;
+    } else {
+      /** 如果有变化,并且存在销毁函数,就先调用销毁函数 */
+      destroyFunc && destroyFunc();
+      /** 创建一个新的宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+      setTimeout(() => {
+        const destroyFunction = callback();
+        hookStates[hookIndex++] = [destroyFunction, dependencies];
+      });
+    }
+  } else {
+    /** 如果hookStates中不存在,证明是首次执行,直接创建一个宏任务,调用callback获取最新的销毁函数,并将销毁函数和依赖项存入hookStates中 */
+    setTimeout(() => {
+      const destroyFunction = callback();
+      hookStates[hookIndex++] = [destroyFunction, dependencies];
+    });
+  }
+}
+

接下来我们就来使用 useEffect 实现我们想要的结果,因为 useEffect 是在 react 组件 render 之后才会执行,所以在 useEffect 获取的状态一定是最新的,所以利用这一点,把我们写的函数放到 useEffect 执行,函数里获取的状态就一定是最新的

js
useEffect(() => {
+  proxyState.current && callback();
+});
+
useEffect(() => {
+  proxyState.current && callback();
+});
+

首先,在 useSyncCallback 中创建一个标示 proxyState,初始的时候会把 proxyState 的 current 值赋成 false,在 callback 执行之前会先判断 current 是否为 true,如果为 true 就允许 callback 执行,若果为 false,就跳过不执行,因为 useEffect 在组件 render 之后,只要依赖项有变化就会执行,所以我们无法掌控我们的函数执行,在 useSyncCallback 中创建一个新的函数 Func,并返回,通过这个 Func 来模拟函数调用,

js
const [proxyState, setProxyState] = useState({ current: false });
+
const [proxyState, setProxyState] = useState({ current: false });
+

在这个函数中我们要做的就是变更 prxoyState 的 current 值为 true,来使得让 callback 被调用的条件成立,同时触发 react 组件 render 这样内部的 useEffect 就会执行,随后调用 callback 实现我们想要的效果。

+ + + + + \ No newline at end of file diff --git a/fragment/return-await.html b/fragment/return-await.html new file mode 100644 index 00000000..4ddd030c --- /dev/null +++ b/fragment/return-await.html @@ -0,0 +1,104 @@ + + + + + + 思考为什么不要使用 return await | Sunny's blog + + + + + + + + +
Skip to content
On this page

思考为什么不要使用 return await

一句话概括:因为 async 函数的返回值一定为 promise 包裹,所以即使你 return await promise,await 出来的值依旧要包裹一层 promise,所以 await 等于白 await 了

stackoverflow

在 ESLint 中有一条规则 no-return-await 。下面的代码会触发该条规则校验不通过:

js
async function foo() {
+  return await bar();
+}
+
async function foo() {
+  return await bar();
+}
+

这条规则的解释是, async function 的返回值总是封装在 Promise.resolve 中 , return await 实际上并没有做任何事情,只是在 Promise resolve 或 reject 之前增加了额外的时间。

Promise.resolve

Promise.resolve() 作用是将给定的参数转换为 Promise 对象

js
Promise.resolve("foo");
+// 等价于
+new Promise((resolve) => resolve("foo"));
+
Promise.resolve("foo");
+// 等价于
+new Promise((resolve) => resolve("foo"));
+

那么其实上面的问题所在就是 Promise.resolve() 的参数情况:《 ECMAScript 6 入门 》的 Promise.resolve()

问题分析

函数 foo() 是一个 async function ,会返回一个 Promise 对象,这个 Promise 对象其实是将函数内 return 语句后面的值使用 Promise.resolve 封装后返回。所以,执行 foo() 之后,我们明确知道得到的结果是一个 Promise 对象,并且在 foo() 的外部我们还会以某种方式比如 await 来继续使用这个结果。 另外,我们知道 await 命令后面如果是一个 Promise 对象,则返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。所以函数内 return 后的 await 的作用就是获取 bar() 的执行结果。 如此一来, async function 中使用 return await 相当于先获取结果然后又将结果变成 Promise 实例。 无论 return await 后面跟的是不是 Promise 对象,那么我们在函数外面都会得到一个 Promise 对象,所以函数内 return 后面的 await 就显得多余了。

虽然上面的写法不推荐使用,但是并不会产生运行错误,只是会造成性能上的损失。唯一影响,可能是其他开发者看到后,会笑话你基础不扎实吧。

推荐写法

首先,如果 async function 没有 return 语句的话,默认返回 undefined ,在 Devtool 中测试如下:

js
async function foo() {}
+foo();
+// Promise {<fulfilled>: undefined}
+//  [[PromiseState]]: "fulfilled"
+//  [[PromiseResult]]: undefined
+
async function foo() {}
+foo();
+// Promise {<fulfilled>: undefined}
+//  [[PromiseState]]: "fulfilled"
+//  [[PromiseResult]]: undefined
+

既然 return await 中的 await 是多余的,那么最直接的推荐写法就是去掉 await :

js
async function foo() {
+  return bar();
+}
+
async function foo() {
+  return bar();
+}
+

无论 bar() 执行结果是一个 Promise 还是一个普通值,我们将它交给 foo 函数外面的程序去处理。如果非要在 foo 函数中求得 bar() 的执行结果,那么也可以像下面这样写:

js
async function foo() {
+  const x = await bar();
+  return x;
+}
+
async function foo() {
+  const x = await bar();
+  return x;
+}
+

但是我觉得这完全只是为了规避 ESLint 的报错而写的一种障眼法,实际它 和 return await bar() 一样 没有任何好处。

return await promiseValue 与 return promiseValue 的比较

返回值隐式的传递给 Promise.resolve ,并不意味着 return await promiseValue; 和 return promiseValue; 在功能上相同。 return foo; 和 return await foo; ,有一些细微的差异:

  • return foo; 不管 foo 是 promise 还是 rejects 都将会直接返回 foo
  • 相反地,如果 foo 是一个 Promise , return await foo; 将等待 foo 执行(resolve)或拒绝(reject),如果是拒绝,将会在返回前抛出异常
js
function bar() {
+  throw new Error("报错了!");
+}
+
+async function foo() {
+  try {
+    return await bar();
+  } catch (error) {
+    console.log("知道了");
+  }
+}
+foo();
+// 知道了
+// Promise {<fulfilled>: undefined}
+
+async function foo2() {
+  try {
+    return bar();
+  } catch (error) {
+    console.log("我不知道");
+  }
+}
+foo2();
+// undefined
+
function bar() {
+  throw new Error("报错了!");
+}
+
+async function foo() {
+  try {
+    return await bar();
+  } catch (error) {
+    console.log("知道了");
+  }
+}
+foo();
+// 知道了
+// Promise {<fulfilled>: undefined}
+
+async function foo2() {
+  try {
+    return bar();
+  } catch (error) {
+    console.log("我不知道");
+  }
+}
+foo2();
+// undefined
+

如果想要在调试的堆栈中得到 bar() 抛出的错误信息,那么此时应该使用 return await 。

+ + + + + \ No newline at end of file diff --git a/fragment/setTimeout.html b/fragment/setTimeout.html new file mode 100644 index 00000000..dc7ed330 --- /dev/null +++ b/fragment/setTimeout.html @@ -0,0 +1,112 @@ + + + + + + 如何实现准时的 setTimeout | Sunny's blog + + + + + + + + +
Skip to content
On this page

如何实现准时的 setTimeout

为什么不准?

因为 setTimeout 是一个宏任务,它的指定时间指的是: 进入主线程的时间。

js
setTimeout(callback, 进入主线程的时间);
+
setTimeout(callback, 进入主线程的时间);
+

所以什么时候可以执行 callback,需要看 主线程前面还有多少任务待执行 。

如何实现准时的 “setTimeout”

requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。

该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,回调函数执行次数通常是每秒 60 次,也就是每 16.7ms 执行一次,但是并不一定保证为 16.7 ms。

js
// 模拟代码
+function setTimeout(cb, delay) {
+  let startTime = Date.now();
+  loop();
+
+  function loop() {
+    const now = Date.now();
+    if (now - startTime >= delay) {
+      cb();
+      return;
+    }
+    requestAnimationFrame(loop);
+  }
+}
+
// 模拟代码
+function setTimeout(cb, delay) {
+  let startTime = Date.now();
+  loop();
+
+  function loop() {
+    const now = Date.now();
+    if (now - startTime >= delay) {
+      cb();
+      return;
+    }
+    requestAnimationFrame(loop);
+  }
+}
+

由于 16.7 ms 间隔执行,在使用间隔很小的定时器,很容易导致时间的不准确。因此这种方案仍然不是一种好的方案。

while

想得到准确的,我们第一反应就是如果我们能够主动去触发,获取到最开始的时间,以及不断去轮询当前时间,如果差值是预期的时间,那么这个定时器肯定是准确的,那么用 while 可以实现这个功能。

js
function timer(time) {
+  const startTime = Date.now();
+  while (true) {
+    const now = Date.now();
+    if (now - startTime >= time) {
+      console.log("误差", now - startTime - time);
+      return;
+    }
+  }
+}
+timer(5000);
+
function timer(time) {
+  const startTime = Date.now();
+  while (true) {
+    const now = Date.now();
+    if (now - startTime >= time) {
+      console.log("误差", now - startTime - time);
+      return;
+    }
+  }
+}
+timer(5000);
+

这样的方式很精确,但是我们知道 js 是单线程运行,使用这样的方式强行霸占线程会使得页面进入卡死状态,这样的结果显然是不合适的。

setTimeout 系统时间补偿

我们来看看此方案和原方案的区别

原方案:

setTimeout 系统时间补偿:

当每一次定时器执行时后,都去获取系统的时间来进行修正,虽然每次运行可能会有误差,但是通过系统时间对每次运行的修复,能够让后面每一次时间都得到一个补偿。

js
function timer() {
+  let speed = 500,
+    counter = 1,
+    start = new Date().getTime();
+
+  function instance() {
+    let real = counter * speed,
+      ideal = new Date().getTime() - start;
+
+    counter++;
+    let diff = ideal - real;
+    setTimeout(function () {
+      instance();
+    }, speed - diff); // 通过系统时间进行修复
+  }
+
+  setTimeout(function () {
+    instance();
+  }, speed);
+}
+
function timer() {
+  let speed = 500,
+    counter = 1,
+    start = new Date().getTime();
+
+  function instance() {
+    let real = counter * speed,
+      ideal = new Date().getTime() - start;
+
+    counter++;
+    let diff = ideal - real;
+    setTimeout(function () {
+      instance();
+    }, speed - diff); // 通过系统时间进行修复
+  }
+
+  setTimeout(function () {
+    instance();
+  }, speed);
+}
+
+ + + + + \ No newline at end of file diff --git a/fragment/tree-shaking.html b/fragment/tree-shaking.html new file mode 100644 index 00000000..f27e918a --- /dev/null +++ b/fragment/tree-shaking.html @@ -0,0 +1,20 @@ + + + + + + 面试官:tree-shaking 的原理是什么? | Sunny's blog + + + + + + + + +
Skip to content
+ + + + + \ No newline at end of file diff --git a/fragment/useRequest.html b/fragment/useRequest.html new file mode 100644 index 00000000..5fb3f74d --- /dev/null +++ b/fragment/useRequest.html @@ -0,0 +1,272 @@ + + + + + + 使用 react 的 hook 实现一个 useRequest | Sunny's blog + + + + + + + + +
Skip to content
On this page

使用 react 的 hook 实现一个 useRequest

我们在使用 react 开发前端应用时,不可避免地要进行数据请求,而在数据请求的前后都要执行很多的处理,例如

  1. 展示 loading 效果告诉用户后台正在处理请求;
  2. 若要写 async-await,每次都要 try-catch 进行包裹;
  3. 接口异常时要进行错误处理;

当请求比较多时,每次都要重复这样的操作。这里我们可以利用 react 提供的 hook,自己来封装一个 useRequest 。

基础结构

明确 useRequest 的输入和输出。

要输入的数据:

  • url:要请求的接口地址;
  • data:请求的数据(默认只有 get 方式);
  • config:其他一些配置,可选,如(manual?: boolean; // 是否需要手动触发);

要返回的数据:

  • loading:数据是否在请求中;
  • data:接口返回的数据;
  • error:接口异常时的错误;

具体的实现

2.1 不带 config 配置的

js
const useRequest = (url, data, config) => {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  const request = async () => {
+    setLoading(true);
+    try {
+      const result = await axios({
+        url,
+        params: data,
+        method: "get",
+      });
+      if (result && result.status >= 200 && result.status <= 304) {
+        setResult(result.data);
+      } else {
+        setError(new Error("get data error in useRequest"));
+      }
+    } catch (reason) {
+      setError(reason);
+    }
+    setLoading(false);
+  };
+  useEffect(() => {
+    request();
+  }, []);
+
+  return {
+    loading,
+    result,
+    error,
+  };
+};
+
const useRequest = (url, data, config) => {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  const request = async () => {
+    setLoading(true);
+    try {
+      const result = await axios({
+        url,
+        params: data,
+        method: "get",
+      });
+      if (result && result.status >= 200 && result.status <= 304) {
+        setResult(result.data);
+      } else {
+        setError(new Error("get data error in useRequest"));
+      }
+    } catch (reason) {
+      setError(reason);
+    }
+    setLoading(false);
+  };
+  useEffect(() => {
+    request();
+  }, []);
+
+  return {
+    loading,
+    result,
+    error,
+  };
+};
+

在我们不考虑第三个配置 config 的情况下,这个 useRequest 就已经可以使用了。

js
const App = () => {
+  const { loading, result, error } = useRequest(url);
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+    </div>
+  );
+};
+
const App = () => {
+  const { loading, result, error } = useRequest(url);
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+    </div>
+  );
+};
+

2.2 添加取消请求

我们在请求接口过程中,可能接口还没没有返回到数据,组件就已经被销毁了,因此我们还要添加上取消请求的操作,避免操作已经不存在的组件。关于 axios 如何取消请求,您可以查看之前写的一篇文章: axios 源码系列之如何取消请求

js
const request = useCallback(() => {
+  setLoading(true);
+  const CancelToken = axios.CancelToken;
+  const source = CancelToken.source();
+
+  axios({
+    url,
+    params: data,
+    method: "get",
+    cancelToken: source.token, // 将token注入到请求中
+  })
+    .then((result) => {
+      setResult(result.data);
+      setLoading(false);
+    })
+    .catch((thrown) => {
+      // 只有在非取消的请求时,才调用setError和setLoading
+      // 否则会报组件已被卸载还会调用方法的错误
+      if (!axios.isCancel(thrown)) {
+        setError(thrown);
+        setLoading(false);
+      }
+    });
+
+  return source;
+}, [url, data]);
+
+useEffect(() => {
+  const source = request();
+  return () => source.cancel("Operation canceled by the user.");
+}, [request]);
+
const request = useCallback(() => {
+  setLoading(true);
+  const CancelToken = axios.CancelToken;
+  const source = CancelToken.source();
+
+  axios({
+    url,
+    params: data,
+    method: "get",
+    cancelToken: source.token, // 将token注入到请求中
+  })
+    .then((result) => {
+      setResult(result.data);
+      setLoading(false);
+    })
+    .catch((thrown) => {
+      // 只有在非取消的请求时,才调用setError和setLoading
+      // 否则会报组件已被卸载还会调用方法的错误
+      if (!axios.isCancel(thrown)) {
+        setError(thrown);
+        setLoading(false);
+      }
+    });
+
+  return source;
+}, [url, data]);
+
+useEffect(() => {
+  const source = request();
+  return () => source.cancel("Operation canceled by the user.");
+}, [request]);
+

2.3 带有 config 配置的

然后,接着我们就要考虑把配置加上了,在上面的调用中,只要调用了 useRequest,就会马上触发。但若在符合某种情况下才触发怎办呢(例如用户点击后才产生接口请求)? 这里我们就需要再加上一个配置。

js
{
+  "manual": false, // 是否需要手动触发,若为false则立刻产生请求,若为true
+  "ready": false // 当manual为true时生效,为true时才产生请求
+}
+
{
+  "manual": false, // 是否需要手动触发,若为false则立刻产生请求,若为true
+  "ready": false // 当manual为true时生效,为true时才产生请求
+}
+

这里我们就要考虑 useRequest 中触发 reqeust 请求的条件了:

  • 没有 config 配置,或者有 config 且 manual 为 false;
  • 有 config 配置,且 manual 和 ready 均为 true;
js
const useRequest = () => {
+  // 其他代码保持不变
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && config.ready)) {
+      const source = request();
+      return () => source.cancel("Operation canceled by the user.");
+    }
+  }, [config]);
+};
+
const useRequest = () => {
+  // 其他代码保持不变
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && config.ready)) {
+      const source = request();
+      return () => source.cancel("Operation canceled by the user.");
+    }
+  }, [config]);
+};
+

使用方法

js
const App = () => {
+  const [ready, setReady] = useState(false);
+  const { loading, result, error } = useRequest(url, null, {
+    manual: true,
+    ready,
+  });
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+      <button onClick={() => setReady(true)}>产生请求</button>
+    </div>
+  );
+};
+
const App = () => {
+  const [ready, setReady] = useState(false);
+  const { loading, result, error } = useRequest(url, null, {
+    manual: true,
+    ready,
+  });
+
+  return (
+    <div>
+      <p>loading: {loading}</p>
+      <p>{JSON.stringify(result)}</p>
+      <button onClick={() => setReady(true)}>产生请求</button>
+    </div>
+  );
+};
+

当然,实现手动触发的方式有很多,我在这里是用一个 config.ready 来触发,在 umi 框架中,是对外返回了一个 run 函数,然后执行 run 函数再来触发这个 useRequest 的执行。

js
const useRequest = () => {
+  // 其他代码保持不变,并暂时忽略
+  const [ready, setReady] = useState(false);
+
+  const run = (r: boolean) => {
+    setReady(r);
+  };
+
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && ready)) {
+      if (loading) {
+        return;
+      }
+      setLoading(true);
+      const source = request();
+
+      return () => {
+        setLoading(false);
+        setReady(false);
+        source.cancel("Operation canceled by the user.");
+      };
+    }
+  }, [config, ready]);
+};
+
const useRequest = () => {
+  // 其他代码保持不变,并暂时忽略
+  const [ready, setReady] = useState(false);
+
+  const run = (r: boolean) => {
+    setReady(r);
+  };
+
+  useEffect(() => {
+    if (!config || !config.manual || (config.manual && ready)) {
+      if (loading) {
+        return;
+      }
+      setLoading(true);
+      const source = request();
+
+      return () => {
+        setLoading(false);
+        setReady(false);
+        source.cancel("Operation canceled by the user.");
+      };
+    }
+  }, [config, ready]);
+};
+
+ + + + + \ No newline at end of file diff --git a/fragment/var-array.html b/fragment/var-array.html new file mode 100644 index 00000000..60e82cb7 --- /dev/null +++ b/fragment/var-array.html @@ -0,0 +1,76 @@ + + + + + + 如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功? | Sunny's blog + + + + + + + + +
Skip to content
On this page

如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?

在 JavaScript 中,解构赋值语法的左侧是一个数组,而右侧则应该是一个具有迭代器接口的对象(如数组、Map、Set 等)。因此,将对象 {a: 1, b: 2} 解构赋值给 [a, b] 会导致语法错误

我们首先来看看报错是什么样的:

var [a, b] = {a: 1, b: 2} TypeError: {(intermediate value)(intermediate value)} is not iterable

这个错误是个类型错误,并且是对象有问题,因为对象是一个不具备迭代器属性的数据结构。所以我们可以知道,这个面试题就是考验我们对于迭代器属性的认识,我们再来个场景加深下理解。

js
let arr = [1, 2, 3];
+let obj = {
+  a: 1,
+  b: 2,
+  c: 3,
+};
+for (let item of arr) {
+  console.log(item);
+}
+for (let item of obj) {
+  console.log(item);
+}
+
let arr = [1, 2, 3];
+let obj = {
+  a: 1,
+  b: 2,
+  c: 3,
+};
+for (let item of arr) {
+  console.log(item);
+}
+for (let item of obj) {
+  console.log(item);
+}
+

我们知道 for of 只能遍历具有迭代器属性的,在遍历数组的时候会打印出 1 2 3 ,遍历对象时会报这样的一个错误 TypeError: obj is not iterable。 我们可以在最下面发现,数组原型上有 Symbol.iterator 这样一个属性,这个属性显然是从 Array 身上继承到的,并且这个属性的值是一个函数体,如果我们调用一下这个函数体会怎么样?我们打印来看看

js
console.log(arr.__proto__[Symbol.iterator]());
+// Object [Array Iterator] {}
+
console.log(arr.__proto__[Symbol.iterator]());
+// Object [Array Iterator] {}
+

最重要的点来了 🔥🔥🔥🔥

它返回的是一个对象类型,并且是一个迭代器对象!!!所以一个可迭代对象的基本结构是这样的:

js
interable
+{
+    [Symbol.iterator]: function () {
+        return 迭代器 (可通过next()就能读取到值)
+    }
+}
+
interable
+{
+    [Symbol.iterator]: function () {
+        return 迭代器 (可通过next()就能读取到值)
+    }
+}
+

我们可以得出只要一个数据结构身上,具有 [Symbol.iterator] 这样一个属性,且值是一个函数体,可以返回一个迭代器的话,我们就称这个数据结构是可迭代的。 这时候我们回到面试题之中,面试官要我们让 var [a, b] = {a: 1, b: 2} 这个等式成立,那么有了上面的铺垫,我们可以知道,我们接下来的操作就是:人为的为对象打造一个迭代器出来,也就是让对象的隐式原型可以继承到迭代器属性,我们可以先这样做:

js
Object.prototype[Symbol.iterator] = function () {};
+
+var [a, b] = { a: 1, b: 2 };
+console.log(a, b);
+
Object.prototype[Symbol.iterator] = function () {};
+
+var [a, b] = { a: 1, b: 2 };
+console.log(a, b);
+

这样的话,报错就改变了,变成:

TypeError: Result of the Symbol.iterator method is not an object

接下来,我们知道 var [a, b] = [1, 2] 这是肯定没有问题的,所以我们可以将对象身上的迭代器,打造成和数组身上的迭代器( arr[Symbol.iterator] )一样,代码如下:

js
Object.prototype[Symbol.iterator] = function () {
+  // 使用 Object.values(this) 方法获取对象的所有值,并返回这些值的迭代器对象
+  return Object.values(this)[Symbol.iterator]();
+};
+
Object.prototype[Symbol.iterator] = function () {
+  // 使用 Object.values(this) 方法获取对象的所有值,并返回这些值的迭代器对象
+  return Object.values(this)[Symbol.iterator]();
+};
+

这段代码是将 Object.prototype 上的 [Symbol.iterator] 方法重新定义为一个新的函数。新的函数通过调用 Object.values(this) 方法获取对象的所有值,并返回这些值的迭代器对象。 通过这个代码,我们可以使得任何 JavaScript 对象都具有了迭代能力。例如,对于一个对象 obj ,我们可以直接使用 for...of 循环或者 ... 操作符来遍历它的所有值。

+ + + + + \ No newline at end of file diff --git a/fragment/video.html b/fragment/video.html new file mode 100644 index 00000000..d2d64d5c --- /dev/null +++ b/fragment/video.html @@ -0,0 +1,534 @@ + + + + + + 前端录制回放系统初体验 | Sunny's blog + + + + + + + + +
Skip to content
On this page

前端录制回放系统初体验

问题背景

什么是前端录制回放?

顾名思义,就是录制用户在网页中的各种操作,并且支持能随时回放操作。

为什么需要?

说到需要就不得不说一个经典的场景,一般前端做异常监控和错误上报,会采用自研或接入第三方 SDK 的形式,来收集和上报网站交互过程中 JavaScript 的报错信息和其它相关数据,也就是埋点。 在传统的埋点方案中,根据 SourceMap 能定位到具体报错代码文件和行列信息等。基本能定位大部分场景问题,但有一些情况下是很难复现错误,多是在测试扯皮的时候,程序员口头禅之一(我这里没有报错呀,是不是你电脑有问题)。 要是能把出错的操作过程录制下来就好了,这样就能方便我们复现场景了,且留存证据,好像是自己给自己挖了个坑。

如何实现

前端能实现录视频?我第一反应就是质疑,接着我就是一波 Google ,发现确实有可行方案。 在 Google 之前,我想到了通过设定定时器,对视图窗口进行截图,截图可用 canvas2html 的方式来实现,但这种方式无疑会造成性能问题,立马否决。 下面介绍我所「知道」的 Google 的方案

思路初现

网页本质上是一个 DOM 节点形式存在,通过浏览器渲染出来。我们是否可以把 DOM 以某种方式保存起来,并且在不同时间节点持续记录 DOM 数据状态。再将数据还原成 DOM 节点渲染出来完成回放呢?

操作记录

通过 document.documentElement.cloneNode() 克隆到 DOM 的数据对象,此时这个数据不能直接通过接口传输给后端,需要进行一些格式化预处理,处理成方便传输及存储的数据格式。最简单的方式就是进行序列化,也就是转换成 JSON 数据格式。

js
// 序列化后
+let docJSON = {
+  type: "Document",
+  childNodes: [
+    {
+      type: "Element",
+      tagName: "html",
+      attributes: {},
+      childNodes: [
+        {
+          type: "Element",
+          tagName: "head",
+          attributes: {},
+          childNodes: [],
+        },
+      ],
+    },
+  ],
+};
+
// 序列化后
+let docJSON = {
+  type: "Document",
+  childNodes: [
+    {
+      type: "Element",
+      tagName: "html",
+      attributes: {},
+      childNodes: [
+        {
+          type: "Element",
+          tagName: "head",
+          attributes: {},
+          childNodes: [],
+        },
+      ],
+    },
+  ],
+};
+

有完整的 DOM 数据之后,还需要在 DOM 变化时进行监听,记录每次变化的 DOM 节点信息。对数据进行监听可用 MutationObserver ,它是一个可以监听 DOM 变化的 API 。

js
const observer = new MutationObserver((mutationsList) => {
+  console.log(mutationsList); // 发生变化的数据
+});
+// 以上述配置开始观察目标节点
+observer.observe(document, {});
+
const observer = new MutationObserver((mutationsList) => {
+  console.log(mutationsList); // 发生变化的数据
+});
+// 以上述配置开始观察目标节点
+observer.observe(document, {});
+

除了对 DOM 变化进行监听以外,还有一个就是事件监听,用户与网页的交互多是通过鼠标,键盘等输入设备来进行。而这些交互的背后就是 JavaScript 的事件监听。事件监听可以通过绑定系统事件来完成,同样是需要记录下来,以鼠标移动为例:

js
// 鼠标移动
+document.addEventListener("mousemove", (e) => {
+  // 伪代码 获取鼠标移动的信息并记录下来
+  positions.push({
+    x: clientX,
+    y: clientY,
+    timeOffset: Date.now() - timeBaseline,
+  });
+});
+
// 鼠标移动
+document.addEventListener("mousemove", (e) => {
+  // 伪代码 获取鼠标移动的信息并记录下来
+  positions.push({
+    x: clientX,
+    y: clientY,
+    timeOffset: Date.now() - timeBaseline,
+  });
+});
+

回放操作

数据已经有了,接着就是回放,回放本质上是将 JSON 数据还原成 DOM 节点渲染出来。那就将快照数据还原就可以啊「嘴强王者」,数据还原并非那么容易啊!

渲染环境

首先为了确保回放过程代码隔离,需要沙箱环境, iframe 标签可以做到,并且 iframe 提供了 sandbox 属性可配置沙箱。沙箱环境的作用是确保代码安全并且不被干扰。

html
<iframe sandbox srcdoc></iframe>
+
<iframe sandbox srcdoc></iframe>
+

sanbox 属性可以做到沙箱作用, 点击查看文档 srcdoc 可以直接设置成一段 html 代码

数据还原

快照重组主要是 DOM 节点的重组,有点像虚拟 DOM 转成真实文档节点的过程,但是事件类型快照是不需要重组。

定时器

有了数据和环境,还需要定时器。通过定时器不停渲染 DOM ,实质上就是一个播放视频的效果, requestAnimationFrame 是最合适的。

requestAnimationFrame 执行机制在浏览器下一次 repaint(重绘)之前执行,执行频率取决于浏览器刷新频率,更适合制作动画效果

至此有一个大概的想法,距离落地还是有段距离。得益于开源,我们可上 Github 看看有没有合适的轮子可复制(借鉴),刚好有现成的一框架 「rrweb」 ,不妨一起看看。

rrweb 框架

rrweb 是一个前端录制和回放的框架。全称 record and replay the web ,顾名思义就是可以录制和回放 web 界面中的操作,其核心原理就是上面介绍的方案。

rrweb 包含三个部分:

  • rrweb-snapshot 主要处理 DOM 结构序列化和重组;
  • rrweb 主要功能是录制和回放;
  • rrweb-player 一个视频播放器 UI 空间

通过 rrweb.record 方法来录制页面, emit 回调可接受到录制的数据。

js
// 1.录制
+let events = []; // 记录快照
+
+rrweb.record({
+  emit(event) {
+    // 将 event 存入 events 数组中
+    events.push(event);
+  },
+});
+
// 1.录制
+let events = []; // 记录快照
+
+rrweb.record({
+  emit(event) {
+    // 将 event 存入 events 数组中
+    events.push(event);
+  },
+});
+

通过 rrweb.Replayer 可回放视频,需要传递录制好的数据。

js
// 2.回放
+const replayer = new rrweb.Replayer(events);
+replayer.play();
+
// 2.回放
+const replayer = new rrweb.Replayer(events);
+replayer.play();
+

rrweb 源码

按照以上所说的思路,接下来会解析其中一些关键代码,当然只是在我个人理解上做的一些分析,实际上 rrweb 源码远不止这些。

核心部分为三大块: record (录制)、 replay 回放、 snapshot 快照。

Record 录制

在 DOM 加载完成后, record 会做一次完整的 DOM 序列化,我们把它叫做全量快照,全量快照记录了整个 HTML 数据结构。 在 record.ts 中找到关键的入口函数的定义 init ,入口函数是会在 document 加载完成或(可交互,完成)时调用了 takeFullSnapshot 以及 observe(document) 函数。

js
if (
+    document.readyState === 'interactive' ||
+    document.readyState === 'complete'
+) {
+    init();
+} else {
+    on('load',() => { init(); },),
+}
+const init = () => {
+    takeFullSnapshot(); // 生成全量快照
+    handlers.push(observe(document)); //监听器
+};
+
if (
+    document.readyState === 'interactive' ||
+    document.readyState === 'complete'
+) {
+    init();
+} else {
+    on('load',() => { init(); },),
+}
+const init = () => {
+    takeFullSnapshot(); // 生成全量快照
+    handlers.push(observe(document)); //监听器
+};
+

document.readyState 包含三种状态:

  1. 可交互 interactive ;
  2. 正在加载中 loading ;
  3. 完成 complete

takeFullSnapshot 从字面意思能看出其作用是生成「完整」的快照,也就是会将 document 序列化出一个完整的数据,称之为 「全量快照」 。 所有序列化相关操作都是使用 snapshot 完成, snapshot 接受一个 dom 对象和一个配置对象传递 document 将整个页面序列化得到完成的快照数据。

js
// 生成全量快照
+takeFullSnapshot = (isCheckout = false) => {
+  const [node, idNodeMap] = snapshot(document, {
+    //...一些配置项
+  });
+};
+
// 生成全量快照
+takeFullSnapshot = (isCheckout = false) => {
+  const [node, idNodeMap] = snapshot(document, {
+    //...一些配置项
+  });
+};
+

idNodeMap 是一个 id 为 key , DOM 对象为 value 的 key-value 键值对对象

observe(document) 是一些监听器的初始化,同样是将整个 document 对象传过去进行监听,通过调用 initObservers 来初始化一些监听器。

js
const observe = (doc: Document) => {
+  return initObservers();
+};
+
const observe = (doc: Document) => {
+  return initObservers();
+};
+

在 observer.ts 文件中可以找到 initObservers 函数定义,该函数初始化了 11 个监听器,可以分为 DOM 类型 / Event 事件类型 / Media 媒体三大类:

js
export function initObservers(
+// dom
+const mutationObserver = initMutationObserver();
+const mousemoveHandler = initMoveObserver();
+const mouseInteractionHandler = initMouseInteractionObserver();
+const scrollHandler = initScrollObserver();
+const viewportResizeHandler = initViewportResizeObserver();
+// ...
+)
+
export function initObservers(
+// dom
+const mutationObserver = initMutationObserver();
+const mousemoveHandler = initMoveObserver();
+const mouseInteractionHandler = initMouseInteractionObserver();
+const scrollHandler = initScrollObserver();
+const viewportResizeHandler = initViewportResizeObserver();
+// ...
+)
+
  • DOM 变化监听器,主要有 DOM 变化(增删改), 样式变化,核心是通过 MutationObserver 来实现
js
let mutationObserverCtor = window.MutationObserver;
+
+const observer = new mutationObserverCtor(
+  // 处理变化的数据
+  mutationBuffer.processMutations.bind(mutationBuffer)
+);
+observer.observe(doc, {});
+return observer;
+
let mutationObserverCtor = window.MutationObserver;
+
+const observer = new mutationObserverCtor(
+  // 处理变化的数据
+  mutationBuffer.processMutations.bind(mutationBuffer)
+);
+observer.observe(doc, {});
+return observer;
+
  • 交互监听-以鼠标移动 initMoveObserver 为例
js
// 鼠标移动记录
+function initMoveObserver() {
+  const updatePosition =
+    (throttle < MouseEvent) |
+    (TouchEvent >
+      ((evt) => {
+        positions.push({
+          x: clientX,
+          y: clientY,
+        });
+      }));
+  const handlers = [
+    on("mousemove", updatePosition, doc),
+    on("touchmove", updatePosition, doc),
+  ];
+}
+
// 鼠标移动记录
+function initMoveObserver() {
+  const updatePosition =
+    (throttle < MouseEvent) |
+    (TouchEvent >
+      ((evt) => {
+        positions.push({
+          x: clientX,
+          y: clientY,
+        });
+      }));
+  const handlers = [
+    on("mousemove", updatePosition, doc),
+    on("touchmove", updatePosition, doc),
+  ];
+}
+
  • 媒体类型监听器,有 canvas / video / audio ,以 video 为例,本质上记录播放和暂停状态, mediaInteractionCb 将 play / pause 状态回调出来。
js
function initMediaInteractionObserver(): listenerHandler {
+    mediaInteractionCb({
+        type: type === 'play' ? MediaInteractions.Play : MediaInteractions.Pause,
+        id: mirror.getId(target as INode),
+    });
+}
+
+
function initMediaInteractionObserver(): listenerHandler {
+    mediaInteractionCb({
+        type: type === 'play' ? MediaInteractions.Play : MediaInteractions.Pause,
+        id: mirror.getId(target as INode),
+    });
+}
+
+

Snapshot 快照

snapshot 负责序列化和重组的功能,主要通过 serializeNodeWithId 处理 DOM 序列化和 rebuildWithSN 函数处理 DOM 重组。 serializeNodeWithId 函数负责序列化,主要做了三件事:

  • 调用 serializeNode 序列化 Node ;
  • 通过 genId() 生成唯一 ID 并绑定到 Node 中;
  • 递归实现序列化子节点,并最终返回一个带 ID 的对象
js
// 序列化一个带有ID的DOM
+export function serializeNodeWithId(n) {
+  // 1. 序列化 核心函数 serializeNode
+  const _serializedNode = serializeNode(n);
+  // 2. 生成唯一ID
+  let id = genId();
+  // 绑定ID
+  const serializedNode = Object.assign(_serializedNode, { id });
+
+  // 3.子节点序列化-递归
+  for (const childN of Array.from(n.childNodes)) {
+    const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
+    if (serializedChildNode) {
+      serializedNode.childNodes.push(serializedChildNode);
+    }
+  }
+}
+
// 序列化一个带有ID的DOM
+export function serializeNodeWithId(n) {
+  // 1. 序列化 核心函数 serializeNode
+  const _serializedNode = serializeNode(n);
+  // 2. 生成唯一ID
+  let id = genId();
+  // 绑定ID
+  const serializedNode = Object.assign(_serializedNode, { id });
+
+  // 3.子节点序列化-递归
+  for (const childN of Array.from(n.childNodes)) {
+    const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
+    if (serializedChildNode) {
+      serializedNode.childNodes.push(serializedChildNode);
+    }
+  }
+}
+

serializeNodeWithId 核心是通过 serializeNode 序列化 DOM ,针对不同的节点分别做了一些特殊处理。

节点属性的处理:

js
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
+    attributes[name] = transformAttribute(doc, tagName, name, value);
+}
+
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
+    attributes[name] = transformAttribute(doc, tagName, name, value);
+}
+

处理外联 css 样式,通过 getCssRulesString 获取到具体样式代码,并且储存到 attributes 中。

js
const cssText = getCssRulesString(stylesheet as CSSStyleSheet);
+if (cssText) {
+    attributes._cssText = absoluteToStylesheet(
+        cssText,
+        stylesheet!.href!,
+    );
+}
+
const cssText = getCssRulesString(stylesheet as CSSStyleSheet);
+if (cssText) {
+    attributes._cssText = absoluteToStylesheet(
+        cssText,
+        stylesheet!.href!,
+    );
+}
+

处理 form 表单,逻辑是保存选中状态,并且做了一些安全处理,例如密码框内容替换成 * 。

js
if (
+    attributes.type !== 'radio' &&
+    attributes.type !== 'checkbox' &&
+    // ...
+) {
+    attributes.value = maskInputOptions[tagName]
+        ? '*'.repeat(value.length)
+        : value;
+  } else if (n.checked) {
+    attributes.checked = n.checked;
+  }
+
if (
+    attributes.type !== 'radio' &&
+    attributes.type !== 'checkbox' &&
+    // ...
+) {
+    attributes.value = maskInputOptions[tagName]
+        ? '*'.repeat(value.length)
+        : value;
+  } else if (n.checked) {
+    attributes.checked = n.checked;
+  }
+

canvas 状态保存通过 toDataURL 保存 canvas 数据:

js
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
+
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
+

rebuild 负责重建 DOM :

  • 通过 buildNodeWithSN 函数重组 Node
  • 递归调用 重组子节点
js
export function buildNodeWithSN(n) {
+  // DOM 重组核心函数 buildNode
+  let node = buildNode(n, { doc, hackCss });
+  // 子节点重建并且appendChild
+  for (const childN of n.childNodes) {
+    const childNode = buildNodeWithSN(childN);
+    if (afterAppend) {
+      afterAppend(childNode);
+    }
+  }
+}
+
export function buildNodeWithSN(n) {
+  // DOM 重组核心函数 buildNode
+  let node = buildNode(n, { doc, hackCss });
+  // 子节点重建并且appendChild
+  for (const childN of n.childNodes) {
+    const childNode = buildNodeWithSN(childN);
+    if (afterAppend) {
+      afterAppend(childNode);
+    }
+  }
+}
+

Replay 回放

回放部分在 replay.ts 文件中,先创建沙箱环境,接着或进行重建 document 全量快照,在通过 requestAnimationFrame 模拟定时器的方式来播放增量快照。 replay 的构造函数接收两个参数,快照数据 events 和 配置项 config

js
export class Replayer {
+  constructor(events, config) {
+    // 1.创建沙箱环境
+    this.setupDom();
+    // 2.定时器
+    const timer = new Timer();
+    // 3.播放服务
+    this.service = new createPlayerService(events, timer);
+    this.service.start();
+  }
+}
+
export class Replayer {
+  constructor(events, config) {
+    // 1.创建沙箱环境
+    this.setupDom();
+    // 2.定时器
+    const timer = new Timer();
+    // 3.播放服务
+    this.service = new createPlayerService(events, timer);
+    this.service.start();
+  }
+}
+

构造函数中最核心三步,创建沙箱环境,定时器,和初始化播放器并且启动。播放器创建依赖 events 和 timer ,本质上还是使用 timer 来实现播放。

沙箱环境

首先,在 replay.ts 的构造函数中可以找打 this.setupDom 的调用, setupDom 核心是通过 iframe 来创建出一个沙箱环境。

js
private setupDom() {
+  // 创建iframe
+  this.iframe = document.createElement('iframe');
+  this.iframe.style.display = 'none';
+  this.iframe.setAttribute('sandbox', attributes.join(' '));
+}
+
private setupDom() {
+  // 创建iframe
+  this.iframe = document.createElement('iframe');
+  this.iframe.style.display = 'none';
+  this.iframe.setAttribute('sandbox', attributes.join(' '));
+}
+

播放服务

同样在 replay.ts 构造函数中,调用 createPlayerService 函数来创建播放器服务器,该函数在同级目录下的 machine.ts 中定义了,核心思路是通过给定时器 timer 加入需要执行的快照动作 actions , 在调用 timer.start() 开始回放快照。

js
export function createPlayerService() {
+    //...
+    play(ctx) {
+        // 获取每个 event 执行的 doAction 函数
+        for (const event of needEvents) {
+            //..
+            const castFn = getCastFn(event);
+            actions.push({
+                doAction: () => {
+                    castFn();
+                }
+            })
+            //..
+         }
+         // 添加到定时器队列中
+         timer.addActions(actions);
+         // 启动定时器播放 视频
+         timer.start();
+    },
+    //...
+}
+
export function createPlayerService() {
+    //...
+    play(ctx) {
+        // 获取每个 event 执行的 doAction 函数
+        for (const event of needEvents) {
+            //..
+            const castFn = getCastFn(event);
+            actions.push({
+                doAction: () => {
+                    castFn();
+                }
+            })
+            //..
+         }
+         // 添加到定时器队列中
+         timer.addActions(actions);
+         // 启动定时器播放 视频
+         timer.start();
+    },
+    //...
+}
+

播放服务使用到第三方库 @xstate/fsm 状态机来控制各种状态(播放,暂停,直播)

定时器 timer.ts 也是在同级目录下,核心是通过 requestAnimationFrame 实现了定时器功能, 并对快照回放,以队列的形式存储需要播放的快照 actions ,接着在 start 中递归调用 action.doAction 来实现对应时间节点的快照还原。

js
export class Timer {
+    // 添加队列
+    public addActions(actions: actionWithDelay[]) {
+        this.actions = this.actions.concat(actions);
+    }
+    // 播放队列
+    public start() {
+        function check() {
+            // ...
+            // 循环调用actions中的doAction 也就是 castFn 函数
+            while (actions.length) {
+                const action = actions[0];
+                actions.shift();
+                // doAction 会对快照进行回放动作,针对不同快照会执行不同动作
+                action.doAction();
+            }
+            if (actions.length > 0 || self.liveMode) {
+                self.raf = requestAnimationFrame(check);
+            }
+        }
+        this.raf = requestAnimationFrame(check);
+    }
+}
+
export class Timer {
+    // 添加队列
+    public addActions(actions: actionWithDelay[]) {
+        this.actions = this.actions.concat(actions);
+    }
+    // 播放队列
+    public start() {
+        function check() {
+            // ...
+            // 循环调用actions中的doAction 也就是 castFn 函数
+            while (actions.length) {
+                const action = actions[0];
+                actions.shift();
+                // doAction 会对快照进行回放动作,针对不同快照会执行不同动作
+                action.doAction();
+            }
+            if (actions.length > 0 || self.liveMode) {
+                self.raf = requestAnimationFrame(check);
+            }
+        }
+        this.raf = requestAnimationFrame(check);
+    }
+}
+

doAction 在不同类型快照会执行不同动作,在播放服务中 doAction 最终会调用 getCastFn 函数来做了一些 case :

js
private getCastFn(event: eventWithTime, isSync = false) {
+    switch (event.type) {
+        case EventType.DomContentLoaded: //dom 加载解析完成
+        case EventType.FullSnapshot: // 全量快照
+        case EventType.IncrementalSnapshot: //增量
+            castFn = () => {
+                this.applyIncremental(event, isSync);
+            }
+    }
+}
+
private getCastFn(event: eventWithTime, isSync = false) {
+    switch (event.type) {
+        case EventType.DomContentLoaded: //dom 加载解析完成
+        case EventType.FullSnapshot: // 全量快照
+        case EventType.IncrementalSnapshot: //增量
+            castFn = () => {
+                this.applyIncremental(event, isSync);
+            }
+    }
+}
+

applyIncremental 函数会增对不同的增量快照做不同处理,包含 DOM 增量, 鼠标交互,页面滚动等,以 DOM 增量快照的 case 为例,最终会走到 applyMutation 中:

js
private applyIncremental(){
+  switch (d.source) {
+      case IncrementalSource.Mutation: {
+        this.applyMutation(d, isSync); // DOM变化
+        break;
+      }
+      case IncrementalSource.MouseMove: //鼠标移动
+      case IncrementalSource.MouseInteraction: //鼠标点击事件
+
+  }
+}
+
private applyIncremental(){
+  switch (d.source) {
+      case IncrementalSource.Mutation: {
+        this.applyMutation(d, isSync); // DOM变化
+        break;
+      }
+      case IncrementalSource.MouseMove: //鼠标移动
+      case IncrementalSource.MouseInteraction: //鼠标点击事件
+
+  }
+}
+

applyMutation 才是最终执行 DOM 还原操作的地方,包含 DOM 的增删改步骤:

js
private applyMutation(d: mutationData, useVirtualParent: boolean) {
+    d.removes.forEach((mutation) => {
+        //.. 移除dom
+    });
+    const appendNode = (mutation: addedNodeMutation) => {
+        // 添加dom到具体节点下
+    };
+    d.adds.forEach((mutation) => {
+        // 添加
+        appendNode(mutation);
+    });
+    d.texts.forEach((mutation) => {
+        //...文本处理
+    });
+    d.attributes.forEach((mutation) => {
+        //...属性处理
+    });
+}
+
private applyMutation(d: mutationData, useVirtualParent: boolean) {
+    d.removes.forEach((mutation) => {
+        //.. 移除dom
+    });
+    const appendNode = (mutation: addedNodeMutation) => {
+        // 添加dom到具体节点下
+    };
+    d.adds.forEach((mutation) => {
+        // 添加
+        appendNode(mutation);
+    });
+    d.texts.forEach((mutation) => {
+        //...文本处理
+    });
+    d.attributes.forEach((mutation) => {
+        //...属性处理
+    });
+}
+

以上就是回放的关键流程实现代码, rrweb 中不仅仅是做了这些,还包含数据压缩,移动端处理,隐私问题等等细节处理,有兴趣可自行查看源码。

+ + + + + \ No newline at end of file diff --git "a/fragment/\345\211\215\346\262\277\346\212\200\346\234\257.html" "b/fragment/\345\211\215\346\262\277\346\212\200\346\234\257.html" new file mode 100644 index 00000000..d92e9033 --- /dev/null +++ "b/fragment/\345\211\215\346\262\277\346\212\200\346\234\257.html" @@ -0,0 +1,20 @@ + + + + + + 前沿技术 | Sunny's blog + + + + + + + + +
Skip to content
+ + + + + \ No newline at end of file diff --git "a/fragment/\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.html" "b/fragment/\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.html" new file mode 100644 index 00000000..c8de545d --- /dev/null +++ "b/fragment/\345\276\256\345\206\205\346\240\270\346\236\266\346\236\204.html" @@ -0,0 +1,292 @@ + + + + + + 🔥 微内核架构在前端的实现及其应用 | Sunny's blog + + + + + + + + +
Skip to content
On this page

🔥 微内核架构在前端的实现及其应用

前置知识

微内核架构,大白话讲就是插件系统,就像下面这张图:

从上图中可以看出,微内核架构主要由两部分组成:Core + plugin,也就是一个内核和多个插件。通常来说:

  • 内核主要负责一些基础功能、核心功能以及插件的管理;
  • 插件则是一个独立的功能模块,用来丰富和加强内核的能力。

内核和插件通常是解耦的,在不使用插件的情况下,内核也能够独立运行。看得出来这种架构拓展性极强,在前端主流框架中都能看到它的影子:

  • 你在用 vue 的时候,可以用 Vue.use()
  • webpack 用的一些 plugins
  • babel 用的一些 plugins
  • 浏览器插件
  • vscode 插件
  • koa 中间件(中间件也可以理解为插件)
  • 甚至是 jQuery 插件
  • ...

如何实现

话不多说,接下来就直接开撸,实现一个微内核系统。不过在做之前,我们要先明确要做什么东西,这里就以文档编辑器为例子吧,每篇文档由多个 Block 区块组成,每个 Block 又可以展示不同的内容(列表、图片、图表等),这种情况下,我们要想扩展文档没有的功能(比如脑图),就很适合用插件系统做啦!

如何调用

通常在开发之前,我们会先确定一下期望的调用方式,大抵应该是下面这个样子 👇🏻:

js
// 内核初始化
+const core = new Core();
+// 使用插件
+core.use(new Plugin1());
+core.use(new Plugin2());
+// 开始运行
+core.run();
+
// 内核初始化
+const core = new Core();
+// 使用插件
+core.use(new Plugin1());
+core.use(new Plugin2());
+// 开始运行
+core.run();
+

Core 和 Plugin 的实现

插件是围绕内核而生的,所以我们先来实现内核的部分吧,也就是 Core 类。通常内核有两个用途:一个是实现基础功能;一个是管理插件。基础功能要看具体使用场景,它是基于业务的,这里就略过了。我们的重点在插件,要想能够在内核中使用插件肯定是要在内核中开个口子的,最基本的问题就是如何注册、如何执行。一般来说插件的格式会长下面这个样子:

ts
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力,通常是个函数,也可以叫 apply、exec、handle */
+  fn: Function;
+}
+
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力,通常是个函数,也可以叫 apply、exec、handle */
+  fn: Function;
+}
+

再看看前面定义的调用方式, core.use() 就是向内核中注册插件了,而 core.run() 则是执行,也就是说 Core 中需要有 use 和 run 这两个方法,于是我们就能简单写出如下代码 👇🏻:

ts
/** 内核 Core :基础功能 + 插件调度器 */
+class Core {
+  pluginMap: Map<string, IPlugin> = new Map();
+  constructor() {
+    console.log("实现内核基础功能:文档初始化");
+  }
+  /** 插件注册,也可以叫 register,通常以注册表的形式实现,其实就是个对象映射 */
+  use(plugin: IPlugin): Core {
+    this.pluginMap.set(plugin.name, plugin);
+    return this; // 方便链式调用
+  }
+  /** 插件执行,也可以叫 start */
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn();
+    });
+  }
+}
+class Plugin1 implements IPlugin {
+  name = "Block1";
+  fn() {
+    console.log("扩展文档功能:Block1");
+  }
+}
+class Plugin2 implements IPlugin {
+  name = "Block2";
+  fn() {
+    console.log("扩展文档功能:Block2");
+  }
+}
+// 运行结果如下:
+// 实现内核基础功能:文档初始化
+// 扩展文档功能:Block1
+// 扩展文档功能:Block2
+
/** 内核 Core :基础功能 + 插件调度器 */
+class Core {
+  pluginMap: Map<string, IPlugin> = new Map();
+  constructor() {
+    console.log("实现内核基础功能:文档初始化");
+  }
+  /** 插件注册,也可以叫 register,通常以注册表的形式实现,其实就是个对象映射 */
+  use(plugin: IPlugin): Core {
+    this.pluginMap.set(plugin.name, plugin);
+    return this; // 方便链式调用
+  }
+  /** 插件执行,也可以叫 start */
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn();
+    });
+  }
+}
+class Plugin1 implements IPlugin {
+  name = "Block1";
+  fn() {
+    console.log("扩展文档功能:Block1");
+  }
+}
+class Plugin2 implements IPlugin {
+  name = "Block2";
+  fn() {
+    console.log("扩展文档功能:Block2");
+  }
+}
+// 运行结果如下:
+// 实现内核基础功能:文档初始化
+// 扩展文档功能:Block1
+// 扩展文档功能:Block2
+

设计思想

一个好的架构肯定是能够体现一些设计模式思想的:

  • 单一职责:每个插件相互独立,只负责其对应的功能模块,也方便进行单独测试。如果把功能点都写在内核里,就容易造成高耦合。
  • 开放封闭:也就是我们扩展某个功能,不会去修改老代码,每个插件的接入成本是一样的,我们也不用关心内核是怎么实现的,只要知道它暴露了什么 api,会用就行。
  • 策略模式:比如我们在渲染文档的某个 Block 区块时,需要根据不同 Block 类型(插件名、策略名)去执行不同的渲染方法(插件方法、策略),当然了,当前的代码体现的可能不是很明显。
  • 还有一个就是控制反转:这个暂时还没体现,但是下文会提到,简单来说就是通过依赖注入实现控制权的反转

重点问题

通讯问题

前面我们说道,内核和插件是解耦的,那如果我需要在插件中用到一些内核的功能,该怎么和内核通信呢?插件与插件之间又该怎么通信呢?别急,先来解决第一个问题,这个很简单,既然插件要用内核的东西,那我把内核暴露给插件不就好了,就像下面这样 👇🏻:

ts
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力:这个 ctx 就是内核 */
+  fn(ctx: Core): void;
+}
+class Core {
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this); // 注意这里,我们把 this 传递了进去
+    });
+  }
+}
+
interface IPlugin {
+  /** 插件名字 */
+  name: string;
+  /** 插件能力:这个 ctx 就是内核 */
+  fn(ctx: Core): void;
+}
+class Core {
+  run() {
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this); // 注意这里,我们把 this 传递了进去
+    });
+  }
+}
+

这样一来我们就能在插件中获取 Core 实例了(也可以叫做上下文),相应的就能够用内核的一些东西了。比如内核中通常会有一些系统基础配置信息(isDev、platform、version 等),那我们在插件中就能根据不同环境、不同平台、不同版本进行进一步处理,甚至是更改内核的内容。 这个暴露内核的过程其实就是前文提到的控制反转,什么意思呢?比如通常情况下我们要在一个类中要使用另外一个类,你会怎么做呢?是不是会直接在这个类中直接实例化另外一个类,这个就是正常的控制权。现在呢,我们虽然需要在内核中使用插件,但是我们把内核实例暴露给了插件,变成了在插件中使用内核,注意,此时主动权已经在插件这里了,这就是通过依赖注入实现了控制反转,可以好仔体会一下 🤯。 这种方式虽然我们能够引用和修改到内核的一些信息,但是好像不能干预内核初始化和执行的过程,要是想在内核初始化和执行过程中做一些处理该怎么办呢?我们可以引入事件机制,也就是发布订阅模式,在内核执行过程中抛出一些事件,然后由插件去监听相应的事件,我们可以叫 events,也可以叫 hooks(webpack 里面就是 hooks),具体实现就像下面这样:

js
import { EventEmitter } from "events"; // webpack 中使用 tapable,很强大的一个发布订阅库
+
+class Core {
+  events: EventEmitter = new EventEmitter(); // 也可以叫 hooks,就是发布订阅
+  constructor() {
+    this.events.emit("beforeInit");
+    console.log("实现内核基础功能:文档初始化");
+    this.events.emit("afterInit");
+  }
+  run() {
+    this.events.emit("before all plugins");
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this);
+    });
+    this.events.emit("after all plugins");
+  }
+}
+// 插件中可以这样调用:this.events.on('xxxx');
+
import { EventEmitter } from "events"; // webpack 中使用 tapable,很强大的一个发布订阅库
+
+class Core {
+  events: EventEmitter = new EventEmitter(); // 也可以叫 hooks,就是发布订阅
+  constructor() {
+    this.events.emit("beforeInit");
+    console.log("实现内核基础功能:文档初始化");
+    this.events.emit("afterInit");
+  }
+  run() {
+    this.events.emit("before all plugins");
+    this.pluginMap.forEach((plugin) => {
+      plugin.fn(this);
+    });
+    this.events.emit("after all plugins");
+  }
+}
+// 插件中可以这样调用:this.events.on('xxxx');
+

我们也可以改成生命周期的形式,比如:

ts
class Core {
+  constructor(opts?: IOption) {
+    opts?.beforeCreate();
+    console.log("实现内核基础功能:文档初始化");
+    opts?.afterCreate();
+  }
+}
+
class Core {
+  constructor(opts?: IOption) {
+    opts?.beforeCreate();
+    console.log("实现内核基础功能:文档初始化");
+    opts?.afterCreate();
+  }
+}
+

这就是生命周期钩子。除了内核本身的生命周期之外,插件也可以有,一般分为加载、运行和卸载三部分,道理类似。当然这里要注意上面两种通信方式的区别, hooks 侧重的是时机,它会在特定阶段触发,而 ctx 则侧重于共享信息的传递。 至于插件和插件的通信,通常来说在这个模式中使用相互影响的插件是不友好的,也不建议这么做,但是如果一定需要的话可以通过内核和一些 hooks 来做桥接。

插件的调度

我们思考一个问题,插件的加载顺序对内核有影响吗?多个插件之间应该如何运行,它们是怎样的关系?看看我们的代码是直接 forEach 遍历执行的,这样的对吗?此处可以停下来思考几秒种 🤔。

通常来说多个插件有以下几种运行方式:

具体采用哪种方式就要看我们的具体业务场景了,比如我们的业务是文档编辑器,插件主要用于扩展文档的 Block 功能,所以顺序不是很重要,相当于上面的第三种模式,主要就是加强功能。那管道式呢,管道式的特点就是上一步的输入是下一步的输出,也就是管道流,这种就是对顺序有要求的,改变插件的顺序,结果也是不一样的,比如我们的业务核心是处理数据,input 就是原始数据,中间的 plugin 就是各种数据处理步骤(比如采样、均化、归一等),output 就是最终处理后的结果;又比如构建工具 gulp 也是如此,有兴趣的可以自行了解下。最后是洋葱式,这个最典型的应用场景就是 koa 中间件了,每个路由都会经过层层中间件,又层层返回;还有 babel 遍历过程中访问器 vistor 的进出过程也是如此。了解上图的三种模式你就大概能知道何时何地加载、运行和销毁插件了。 那我们除了用 use 来使用插件外,还可以用什么方式来注册插件呢?可以用声明式注入,也就是通过配置文件来告诉系统应该去哪里去取什么插件,系统运行时会按照约定的配置去加载对应的插件。比如 babel 就可以通过在配置文件中填写插件名称,运行时就会去自行查找对应的插件并加载(可以想想我们平时开发时的各种 config.js 配置文件)。而编程式的就是系统提供某种注册 api,开发者通过将插件传入该 api 中来完成注册,也就是本文使用的方式。 另外我们再来说个小问题,就是插件的管理,也就是插件池,有时我们会用 map,有时我们会用数组,就像这样:

js
// map: {'pluginName1': plugin1, 'pluginName2': plugin2, ...}
+// 数组:[new Plugin1(), new Plugin2(), ...]
+
// map: {'pluginName1': plugin1, 'pluginName2': plugin2, ...}
+// 数组:[new Plugin1(), new Plugin2(), ...]
+

这两种用法在日常开发中也很常见,那它们有什么区别呢?这就要看你要求了,用 map 一般都是要具名的,目的是为了存取方便,尤其是取,就像缓存的感觉;而如果我们只需要单纯的遍历执行插件用数组就可以了,当然如果复杂度较高的情况下,可以两者一起使用,就像下面这样 👇🏻:

ts
class Core {
+  plugins: IPlugin[] = [];
+  pluginMap: Map<string, IPlugin> = new Map();
+  use(plugin: IPlugin): Core {
+    this.plugins.push(plugin);
+    this.pluginMap.set(plugin.name, plugin);
+    return this;
+  }
+}
+
class Core {
+  plugins: IPlugin[] = [];
+  pluginMap: Map<string, IPlugin> = new Map();
+  use(plugin: IPlugin): Core {
+    this.plugins.push(plugin);
+    this.pluginMap.set(plugin.name, plugin);
+    return this;
+  }
+}
+

安全性和稳定性

因为这种架构的扩展性相当强,并且插件也有能力去修改内核,所以安全性和稳定性的问题也是必须要考虑的问题 😬。 为了保障稳定性,在插件运行异常时,我们应当进行相应的容错处理,由内核来捕获并继续往下执行,不能让系统轻易崩掉,可以给个警告或错误的提示,或者通过系统级事件通知内核 重启 或 关闭 该插件。 关于安全性,我们可以只暴露必要的信息给插件,而不是全部(整个内核),这是什么意思呢,其实就是要搞个简单的沙箱,在前端这边具体点就是用 with+proxy+白名单 来处理,node 用 vm 模块来处理,当然在浏览器中要想完全隔绝是很难的,不过已经有个提案的 api 在路上了,可以期待一下。

presets 预设

presets 这个字眼在前端中应该还是挺常见的,它其实是什么意思呢,中文我们叫预设,本质就是一些插件的集合,亦即 Core + presets(几个插件) ,内核可以有不同的预设,presets1、presets2 等等,每个 presets 就是几个不同插件的组合,主要用途就是方便,不用我们一个一个去引入去设置,比如 babel 的预设、vue 脚手架中的预设。当然除了预设之外,我们也可以将一些必备的插件固化为内置插件,这个度由开发者自己去把握。

应用场景

webpack

js
module.exports = {
+  plugins: [
+    // 用配置的方式注册插件
+    new webpack.ProgressPlugin(),
+    new HtmlWebpackPlugin({ template: "./src/index.html" }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    // 用配置的方式注册插件
+    new webpack.ProgressPlugin(),
+    new HtmlWebpackPlugin({ template: "./src/index.html" }),
+  ],
+};
+
js
const pluginName = "ConsoleLogOnBuildWebpackPlugin";
+
+class ConsoleLogOnBuildWebpackPlugin {
+  apply(compiler) {
+    // apply就是插件的fn,compiler就是内核上下文
+    compiler.hooks.run.tap(pluginName, (compilation) => {
+      // hooks就是发布订阅
+      console.log("The webpack build process is starting!");
+    });
+  }
+}
+
+module.exports = ConsoleLogOnBuildWebpackPlugin;
+
const pluginName = "ConsoleLogOnBuildWebpackPlugin";
+
+class ConsoleLogOnBuildWebpackPlugin {
+  apply(compiler) {
+    // apply就是插件的fn,compiler就是内核上下文
+    compiler.hooks.run.tap(pluginName, (compilation) => {
+      // hooks就是发布订阅
+      console.log("The webpack build process is starting!");
+    });
+  }
+}
+
+module.exports = ConsoleLogOnBuildWebpackPlugin;
+

babel

配置文件方式注册插件

json
{
+  "plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]
+}
+
{
+  "plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]
+}
+

插件开发

js
export default function () {
+  return {
+    visitor: {
+      Identifier(path) {
+        // 插件的执行内容,看起来特殊一些,不过path可以理解为内核上下文
+        const name = path.node.name;
+        path.node.name = name.split("").reverse().join("");
+      },
+    },
+  };
+}
+
export default function () {
+  return {
+    visitor: {
+      Identifier(path) {
+        // 插件的执行内容,看起来特殊一些,不过path可以理解为内核上下文
+        const name = path.node.name;
+        path.node.name = name.split("").reverse().join("");
+      },
+    },
+  };
+}
+

Vue

js
const app = createApp();
+
+app.use(myPlugin, {});
+
const app = createApp();
+
+app.use(myPlugin, {});
+
js
const myPlugin = {
+  install(app, options) {},
+};
+
const myPlugin = {
+  install(app, options) {},
+};
+

类似 jQuery、window 从某种角度上来说也可以看做是内核,通过 $.fn() 或者在 prototype 中扩展方法也是一种插件的形式。

小结

最后我们再来巩固一下这篇文章的核心思想,微内核架构主要由两部分组成: Core + Plugin,其中: Core 需要具备的能力有:

  • 基础功能,视业务情况而定
  • 一些配置信息(环境变量、全局变量)
  • 插件管理:通信、调度、稳定性
  • 生命周期钩子 hooks:约束一些特定阶段,用较少的钩子尽可能覆盖大部分场景 plugin 应该具备的能力有:
  • 能被内核调用
  • 可以更改内核的一些东西
  • 相互独立
  • 插件一般先注册后干预执行,有需要再提供卸载功能 其实微内核架构的核心思想就是在系统内部预留一些入口,系统本身功能不变,但是通过这个入口可以集百家之长以丰富系统自身 🍺。

🔗 原文链接: https://juejin.cn/post/716307803160...

+ + + + + \ No newline at end of file diff --git "a/fragment/\346\216\245\345\217\243\350\256\276\350\256\241.html" "b/fragment/\346\216\245\345\217\243\350\256\276\350\256\241.html" new file mode 100644 index 00000000..133ad2d5 --- /dev/null +++ "b/fragment/\346\216\245\345\217\243\350\256\276\350\256\241.html" @@ -0,0 +1,20 @@ + + + + + + 接口设计 | Sunny's blog + + + + + + + + +
Skip to content
On this page

接口设计

防止用户伪造请求:如果接口允许用户直接携带 ID,很容易被恶意用户利用,尝试修改其他用户的信息。使用 cookie(尤其是带有认证信息的 cookie)可以确保请求来自于正确的用户,并且认证信息通常在服务器端进行验证,增加了安全性。

+ + + + + \ No newline at end of file diff --git "a/fragment/\346\262\231\347\233\222.html" "b/fragment/\346\262\231\347\233\222.html" new file mode 100644 index 00000000..56265f57 --- /dev/null +++ "b/fragment/\346\262\231\347\233\222.html" @@ -0,0 +1,470 @@ + + + + + + 请设计一个不能操作 DOM 和调接口的环境 | Sunny's blog + + + + + + + + +
Skip to content
On this page

请设计一个不能操作 DOM 和调接口的环境

实现思路

1)利用 iframe 创建沙箱,取出其中的原生浏览器全局对象作为沙箱的全局对象

2)设置一个黑名单,若访问黑名单中的变量,则直接报错,实现阻止\隔离的效果

3)在黑名单中添加 document 字段,来实现禁止开发者操作 DOM

4)在黑名单中添加 XMLHttpRequest、fetch、WebSocket 字段,实现禁用原生的方式调用接口

5)若访问当前全局对象中不存在的变量,则直接报错,实现禁用三方库调接口

6)最后还要拦截对 window 对象的访问,防止通过 window.document 来操作 DOM,避免沙箱逃逸

下面聊一聊,为何这样设计,以及中间会遇到什么问题

如何禁止开发者操作 DOM ?

在页面中,可以通过 document 对象来获取 HTML 元素,进行增删改查的 DOM 操作

如何禁止开发者操作 DOM,转化为如何阻止开发者获取 document 对象

1)传统思路

简单粗暴点,直接修改 window.document 的值,让开发者无法获取 document

js
// 将document设置为null
+window.document = null;
+
+// 设置无效,打印结果还是document
+console.log(window.document);
+
+// 删除document
+delete window.document;
+
+// 删除无效,打印结果还是document
+console.log(window.document);
+
// 将document设置为null
+window.document = null;
+
+// 设置无效,打印结果还是document
+console.log(window.document);
+
+// 删除document
+delete window.document;
+
+// 删除无效,打印结果还是document
+console.log(window.document);
+

好吧,document 修改不了也删除不了 🤔

使用 Object.getOwnPropertyDescriptor 查看,会发现 window.document 的 configurable 属性为 false(不可配置的)

js
Object.getOwnPropertyDescriptor(window, "document");
+// {get: ƒ, set: undefined, enumerable: true, configurable: false}
+
Object.getOwnPropertyDescriptor(window, "document");
+// {get: ƒ, set: undefined, enumerable: true, configurable: false}
+

configurable 决定了是否可以修改属性描述对象,也就是说,configurable 为 false 时,value、writable、enumerable 和 configurable 都不能被修改,以及无法被删除

2)有点高大上的思路

既然 document 对象修改不了,那如果环境中原本就没有 document 对象,是不是就可以实现该需求? 说到环境中没有 document 对象, Web Worker 直呼内行,作者曾在 《一文彻底了解 Web Worker,十万、百万条数据都是弟弟 🔥》 中聊过如何使用 Web Worker,和对应的特性

并且 Web Worker 更狠,不但没有 document 对象,连 window 对象也没有 😂

在 worker 线程中打印 window

js
onmessage = function (e) {
+  console.log(window); // 浏览器直接报错
+  postMessage();
+};
+
onmessage = function (e) {
+  console.log(window); // 浏览器直接报错
+  postMessage();
+};
+

在 Web Worker 线程的运行环境中无法访问 document 对象,这一条符合当前的需求,但是该环境中能获取 XMLHttpRequest 对象,可以发送 ajax 请求,不符合不能调接口的要求

如何禁止开发者调接口 ?

常规调接口方式有:

1)原生方式:XMLHttpRequest、fetch、WebSocket、jsonp、form 表单

2)三方实现:axios、jquery、request 等众多开源库

禁用原生方式调接口的思路:

1)XMLHttpRequest、fetch、WebSocket 这几种情况,可以禁止用户访问这些对象

2)jsonp、form 这两种方式,需要创建 script 或 form 标签,依然可以通过禁止开发者操作 DOM 的方式解决,不需要单独处理

如何禁用三方库调接口呢?三方库很多,没办法全部列出来,来进行逐一排除,禁止调接口的路好像也被封死了……😰

最终方案:沙箱(Sandbox)

通过上面的分析,传统的思路确实解决不了当前的需求

阻止开发者操作 DOM 和调接口,沙箱说:这个我熟啊,拦截隔离这类的活,我最拿手了 😀

沙箱(Sandbox) 是一种安全机制,为运行中的程序提供隔离环境,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行

前端沙箱的使用场景:

1)Chrome 浏览器打开的每个页面就是一个沙箱,保证彼此独立互不影响

2)执行 jsonp 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码

3)Vue 模板表达式的计算是运行在一个沙箱中,模板字符串中的表达式只能获取部分全局对象,详情见 源码

4)微前端框架 qiankun ,为了实现 js 隔离,在多种场景下均使用了沙箱

沙箱的多种实现方式

先聊下 js with 这个关键字:作用在于改变作用域,可以将某个对象添加到作用域链的顶部

with 对于沙箱的意义:可以实现所有变量均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值,相当于做了一层拦截,实现隔离的效果

简陋的沙箱

实现这样一个沙箱,要求程序中访问的所有变量,均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值

举个 🌰: ctx 作为执行上下文对象,待执行程序 code 可以访问到的变量,必须都来自 ctx 对象

js
// ctx 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 待执行程序
+const code = `func(foo)`;
+
// ctx 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 待执行程序
+const code = `func(foo)`;
+

沙箱示例:

js
// 定义全局变量foo
+var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 非常简陋的沙箱
+function veryPoorSandbox(code, ctx) {
+  // 使用with,将eval函数执行时的执行上下文指定为ctx
+  with (ctx) {
+    eval(code);
+  }
+}
+
+// 待执行程序
+const code = `func(foo)`;
+
+veryPoorSandbox(code, ctx);
+// 打印结果:"f1",不是最外层的全局变量"foo1"
+
// 定义全局变量foo
+var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+  foo: "f1",
+};
+
+// 非常简陋的沙箱
+function veryPoorSandbox(code, ctx) {
+  // 使用with,将eval函数执行时的执行上下文指定为ctx
+  with (ctx) {
+    eval(code);
+  }
+}
+
+// 待执行程序
+const code = `func(foo)`;
+
+veryPoorSandbox(code, ctx);
+// 打印结果:"f1",不是最外层的全局变量"foo1"
+

这个沙箱有一个明显的问题,若提供的 ctx 上下文对象中,没有找到某个变量时,代码仍会沿着作用域链一层层向上查找 假如上文示例中的 ctx 对象没有设置 foo 属性,打印的结果还是外层作用域的 foo1

With + Proxy 实现沙箱

希望沙箱中的代码只在手动提供的上下文对象中查找变量,如果上下文对象中不存在该变量,则提示对应的错误

举个 🌰: ctx 作为执行上下文对象,待执行程序 code 可以访问到的变量,必须都来自 ctx 对象,如果 ctx 对象中不存在该变量,直接报错,不再通过作用域链向上查找

实现步骤:

1)使用 Proxy.has() 来拦截 with 代码块中的任意变量的访问

2)设置一个白名单,在白名单内的变量可以正常走作用域链的访问方式,不在白名单内的变量,会继续判断是否存 ctx 对象中,存在则正常访问,不存在则直接报错

3)使用 new Function 替代 eval,使用 new Function() 运行代码比 eval 更为好一些,函数的参数提供了清晰的接口来运行代码

new Function 与 eval 的区别

沙箱示例:

js
var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+};
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(shadow) {" + code + "}";
+  return new Function("shadow", code);
+}
+
+// 可访问全局作用域的白名单列表
+const access_white_list = ["func"];
+
+// 待执行程序
+const code = `func(foo)`;
+
+// 执行上下文对象的代理对象
+const ctxProxy = new Proxy(ctx, {
+  has: (target, prop) => {
+    // has 可以拦截 with 代码块中任意属性的访问
+    if (access_white_list.includes(prop)) {
+      // 在可访问的白名单内,可继续向上查找
+      return target.hasOwnProperty(prop);
+    }
+    if (!target.hasOwnProperty(prop)) {
+      throw new Error(`Not found - ${prop}!`);
+    }
+    return true;
+  },
+});
+
+// 没那么简陋的沙箱
+function littlePoorSandbox(code, ctx) {
+  // 将 this 指向手动构造的全局代理对象
+  withedYourCode(code).call(ctx, ctx);
+}
+littlePoorSandbox(code, ctxProxy);
+
+// 执行func(foo),报错: Uncaught Error: Not found - foo!
+
var foo = "foo1";
+
+// 执行上下文对象
+const ctx = {
+  func: (variable) => {
+    console.log(variable);
+  },
+};
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(shadow) {" + code + "}";
+  return new Function("shadow", code);
+}
+
+// 可访问全局作用域的白名单列表
+const access_white_list = ["func"];
+
+// 待执行程序
+const code = `func(foo)`;
+
+// 执行上下文对象的代理对象
+const ctxProxy = new Proxy(ctx, {
+  has: (target, prop) => {
+    // has 可以拦截 with 代码块中任意属性的访问
+    if (access_white_list.includes(prop)) {
+      // 在可访问的白名单内,可继续向上查找
+      return target.hasOwnProperty(prop);
+    }
+    if (!target.hasOwnProperty(prop)) {
+      throw new Error(`Not found - ${prop}!`);
+    }
+    return true;
+  },
+});
+
+// 没那么简陋的沙箱
+function littlePoorSandbox(code, ctx) {
+  // 将 this 指向手动构造的全局代理对象
+  withedYourCode(code).call(ctx, ctx);
+}
+littlePoorSandbox(code, ctxProxy);
+
+// 执行func(foo),报错: Uncaught Error: Not found - foo!
+

天然的优质沙箱(iframe)

iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离 利用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法,可以把 iframe.contentWindow 作为沙箱执行的全局 window 对象

沙箱示例:

js
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(sharedState) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // sandboxGlobal作为沙箱运行时的全局对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      has: (target, prop) => {
+        // has 可以拦截 with 代码块中任意属性的访问
+        if (sharedState.includes(prop)) {
+          // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
+          return false;
+        }
+
+        // 如果没有该属性,直接报错
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(`Not find: ${prop}!`);
+        }
+
+        // 属性存在,返回sandboxGlobal中的值
+        return true;
+      },
+    });
+  }
+}
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+function maybeAvailableSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 要执行的代码
+const code = `
+  console.log(history == window.history) // 打印结果为 false
+  window.abc = 'sandbox'
+  Object.prototype.toString = () => {
+      console.log('Traped!')
+  }
+  console.log(window.abc) // sandbox
+`;
+
+// sharedGlobal作为与外部执行环境共享的全局对象
+// code中获取的history为最外层作用域的history
+const sharedGlobal = ["history"];
+
+const globalProxy = new SandboxGlobalProxy(sharedGlobal);
+
+maybeAvailableSandbox(code, globalProxy);
+
+// 对外层的window对象没有影响
+console.log(window.abc); // undefined
+Object.prototype.toString(); // 并没有打印 Traped
+
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(sharedState) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // sandboxGlobal作为沙箱运行时的全局对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      has: (target, prop) => {
+        // has 可以拦截 with 代码块中任意属性的访问
+        if (sharedState.includes(prop)) {
+          // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
+          return false;
+        }
+
+        // 如果没有该属性,直接报错
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(`Not find: ${prop}!`);
+        }
+
+        // 属性存在,返回sandboxGlobal中的值
+        return true;
+      },
+    });
+  }
+}
+
+// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+function maybeAvailableSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 要执行的代码
+const code = `
+  console.log(history == window.history) // 打印结果为 false
+  window.abc = 'sandbox'
+  Object.prototype.toString = () => {
+      console.log('Traped!')
+  }
+  console.log(window.abc) // sandbox
+`;
+
+// sharedGlobal作为与外部执行环境共享的全局对象
+// code中获取的history为最外层作用域的history
+const sharedGlobal = ["history"];
+
+const globalProxy = new SandboxGlobalProxy(sharedGlobal);
+
+maybeAvailableSandbox(code, globalProxy);
+
+// 对外层的window对象没有影响
+console.log(window.abc); // undefined
+Object.prototype.toString(); // 并没有打印 Traped
+

可以看到,沙箱中对 window 的所有操作,都没有影响到外层的 window,实现了隔离的效果 😘

需求实现

继续使用上述的 iframe 标签来创建沙箱,代码主要修改点

1)设置 blacklist 黑名单,添加 document、XMLHttpRequest、fetch、WebSocket 来禁止开发者操作 DOM 和调接口

2)判断要访问的变量,是否在当前环境的 window 对象中,不在的直接报错,实现禁止通过三方库调接口

js
// 设置黑名单
+const blacklist = ["document", "XMLHttpRequest", "fetch", "WebSocket"];
+
+// 黑名单中的变量禁止访问
+if (blacklist.includes(prop)) {
+  throw new Error(`Can't use: ${prop}!`);
+}
+
// 设置黑名单
+const blacklist = ["document", "XMLHttpRequest", "fetch", "WebSocket"];
+
+// 黑名单中的变量禁止访问
+if (blacklist.includes(prop)) {
+  throw new Error(`Can't use: ${prop}!`);
+}
+

但有个很严重的漏洞,如果开发者通过 window.document 来获取 document 对象,依然是可以操作 DOM 的 😱 需要在黑名单中加入 window 字段,来解决这个沙箱逃逸的漏洞,虽然把 window 加入了黑名单,但 window 上的方法,如 open、close 等,依然是可以正常获取使用的

最终代码:

js
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(blacklist) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // 获取当前HTMLIFrameElement的Window对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      // has 可以拦截 with 代码块中任意属性的访问
+      has: (target, prop) => {
+        // 黑名单中的变量禁止访问
+        if (blacklist.includes(prop)) {
+          throw new Error(`Can't use: ${prop}!`);
+        }
+        // sandboxGlobal对象上不存在的属性,直接报错,实现禁用三方库调接口
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(`Not find: ${prop}!`);
+        }
+
+        // 返回true,获取当前提供上下文对象中的变量;如果返回false,会继续向上层作用域链中查找
+        return true;
+      },
+    });
+  }
+}
+
+// 使用with关键字,来改变作用域
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+
+// 将指定的上下文对象,添加到待执行代码作用域的顶部
+function makeSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 待执行的代码code,获取document对象
+const code = `console.log(document)`;
+
+// 设置黑名单
+// 经过小伙伴的指导,新添加Image字段,禁止使用new Image来调接口
+const blacklist = [
+  "window",
+  "document",
+  "XMLHttpRequest",
+  "fetch",
+  "WebSocket",
+  "Image",
+];
+
+// 将globalProxy对象,添加到新环境作用域链的顶部
+const globalProxy = new SandboxGlobalProxy(blacklist);
+
+makeSandbox(code, globalProxy);
+
// 沙箱全局代理对象类
+class SandboxGlobalProxy {
+  constructor(blacklist) {
+    // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
+    const iframe = document.createElement("iframe", { url: "about:blank" });
+    iframe.style.display = "none";
+    document.body.appendChild(iframe);
+
+    // 获取当前HTMLIFrameElement的Window对象
+    const sandboxGlobal = iframe.contentWindow;
+
+    return new Proxy(sandboxGlobal, {
+      // has 可以拦截 with 代码块中任意属性的访问
+      has: (target, prop) => {
+        // 黑名单中的变量禁止访问
+        if (blacklist.includes(prop)) {
+          throw new Error(`Can't use: ${prop}!`);
+        }
+        // sandboxGlobal对象上不存在的属性,直接报错,实现禁用三方库调接口
+        if (!target.hasOwnProperty(prop)) {
+          throw new Error(`Not find: ${prop}!`);
+        }
+
+        // 返回true,获取当前提供上下文对象中的变量;如果返回false,会继续向上层作用域链中查找
+        return true;
+      },
+    });
+  }
+}
+
+// 使用with关键字,来改变作用域
+function withedYourCode(code) {
+  code = "with(sandbox) {" + code + "}";
+  return new Function("sandbox", code);
+}
+
+// 将指定的上下文对象,添加到待执行代码作用域的顶部
+function makeSandbox(code, ctx) {
+  withedYourCode(code).call(ctx, ctx);
+}
+
+// 待执行的代码code,获取document对象
+const code = `console.log(document)`;
+
+// 设置黑名单
+// 经过小伙伴的指导,新添加Image字段,禁止使用new Image来调接口
+const blacklist = [
+  "window",
+  "document",
+  "XMLHttpRequest",
+  "fetch",
+  "WebSocket",
+  "Image",
+];
+
+// 将globalProxy对象,添加到新环境作用域链的顶部
+const globalProxy = new SandboxGlobalProxy(blacklist);
+
+makeSandbox(code, globalProxy);
+

持续优化

经过与评论区小伙伴的交流,可以通过 new Image() 调接口,确实是个漏洞

js
// 不需要创建DOM 发送图片请求
+let img = new Image();
+img.src = "http://www.test.com/img.gif";
+
// 不需要创建DOM 发送图片请求
+let img = new Image();
+img.src = "http://www.test.com/img.gif";
+

黑名单中添加'Image'字段,堵上这个漏洞

+ + + + + \ No newline at end of file diff --git "a/fragment/\351\273\221\347\231\275.html" "b/fragment/\351\273\221\347\231\275.html" new file mode 100644 index 00000000..05e37339 --- /dev/null +++ "b/fragment/\351\273\221\347\231\275.html" @@ -0,0 +1,56 @@ + + + + + + 一行代码,让网页变为黑白配色 | Sunny's blog + + + + + + + + +
Skip to content
On this page

一行代码,让网页变为黑白配色

让网页变为黑白配色,是个常见的诉求。而且往往是突如其来的诉求,是无法预知的。当发生这样的需求时,我们需要迅速完成变更发布。

一行代码

css
div {
+  filter: grayscale(1);
+}
+
div {
+  filter: grayscale(1);
+}
+

为了使整个网页生效,你可以把它放在 <html> 标签的样式里。直接写到 html 文件内,例如:

html
<style>
+  html {
+    filter: grayscale(1);
+  }
+</style>
+
<style>
+  html {
+    filter: grayscale(1);
+  }
+</style>
+

你也可以用内联样式,只要没用 important CSS 语法,内联样式优先级最高:

html
<html style="filter:grayscale(1)">
+  ...
+</html>
+
<html style="filter:grayscale(1)">
+  ...
+</html>
+

为了更好的兼容性,你可以补一个带 -webkit- 前缀的样式,放在 filter 后面:

html
<html style="filter:grayscale(1);-webkit-filter:grayscale(1)">
+  ...
+</html>
+
<html style="filter:grayscale(1);-webkit-filter:grayscale(1)">
+  ...
+</html>
+

原理

我们使用了 CSS 特性 filter,并用了 grayscale 对图片进行灰度转换,允许有一个参数,可以是数字(0 到 1)或百分比,0% 到 100% 之间的值会使灰度线性变化。

如果你不想完全灰掉。可以设置个相对小的数字。

兼容性

我们使用了 CSS 特性 filter,兼容性还不错

如果你想获得更好的兼容性,可以加一个前缀 -webkit- :

css
div {
+  filter: grayscale(0.95);
+  -webkit-filter: grayscale(0.95);
+}
+
div {
+  filter: grayscale(0.95);
+  -webkit-filter: grayscale(0.95);
+}
+

filter 样式加到 html 还是 body 上

把 filter 样式加到了 <body> 元素上。通常这没有问题。

但如果你的网页内有「绝对和固定定位」元素,一定要把 filter 样式加到 <html> 上。 原因见: drafts.fxtf.org/filter-effe…

引用:

A value other than none for the filter property results in the creation of a >containing block for absolute and fixed positioned descendants unless the element it > applies to is a document root element in the current browsing context.

翻译:

若 filter 属性的值不是 none,会给「绝对和固定定位的后代」创建一个 containing block

除非 filter 对应的元素是「当前浏览上下文中的文档根元素」(即 <html> )。

因此,兼容性最好的方法是把 filter 样式加到 <html> 上。这样不会影响「绝对和固定定位的后代」。 这里小程序有个坑,如果你的页面代码有「绝对和固定定位的后代」,就不能把 filter 样式 加到 <page> 上,而是要找个元素,这个元素没有「绝对和固定定位的后代」,你可以把 filter 样式加到这个元素上。

+ + + + + \ No newline at end of file diff --git a/front-end-engineering/2023-01-06-16-01-47.png b/front-end-engineering/2023-01-06-16-01-47.png new file mode 100644 index 00000000..f7552083 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-01-47.png differ diff --git a/front-end-engineering/2023-01-06-16-14-44.png b/front-end-engineering/2023-01-06-16-14-44.png new file mode 100644 index 00000000..5e105ed1 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-14-44.png differ diff --git a/front-end-engineering/2023-01-06-16-16-45.png b/front-end-engineering/2023-01-06-16-16-45.png new file mode 100644 index 00000000..1edb0006 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-16-45.png differ diff --git a/front-end-engineering/2023-01-06-16-16-57.png b/front-end-engineering/2023-01-06-16-16-57.png new file mode 100644 index 00000000..27609fb3 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-16-57.png differ diff --git a/front-end-engineering/2023-01-06-16-17-35.png b/front-end-engineering/2023-01-06-16-17-35.png new file mode 100644 index 00000000..fca0ec7c Binary files /dev/null and b/front-end-engineering/2023-01-06-16-17-35.png differ diff --git a/front-end-engineering/2023-01-06-16-23-08.png b/front-end-engineering/2023-01-06-16-23-08.png new file mode 100644 index 00000000..75a873ac Binary files /dev/null and b/front-end-engineering/2023-01-06-16-23-08.png differ diff --git a/front-end-engineering/2023-01-06-16-23-16.png b/front-end-engineering/2023-01-06-16-23-16.png new file mode 100644 index 00000000..3b4c2ed7 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-23-16.png differ diff --git a/front-end-engineering/2023-01-06-16-37-27.png b/front-end-engineering/2023-01-06-16-37-27.png new file mode 100644 index 00000000..bd1b16cf Binary files /dev/null and b/front-end-engineering/2023-01-06-16-37-27.png differ diff --git a/front-end-engineering/2023-01-06-16-37-43.png b/front-end-engineering/2023-01-06-16-37-43.png new file mode 100644 index 00000000..5d31f174 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-37-43.png differ diff --git a/front-end-engineering/2023-01-06-16-37-56.png b/front-end-engineering/2023-01-06-16-37-56.png new file mode 100644 index 00000000..5b005692 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-37-56.png differ diff --git a/front-end-engineering/2023-01-06-16-38-33.png b/front-end-engineering/2023-01-06-16-38-33.png new file mode 100644 index 00000000..a518868c Binary files /dev/null and b/front-end-engineering/2023-01-06-16-38-33.png differ diff --git a/front-end-engineering/2023-01-06-16-46-05.png b/front-end-engineering/2023-01-06-16-46-05.png new file mode 100644 index 00000000..0a6ea9ef Binary files /dev/null and b/front-end-engineering/2023-01-06-16-46-05.png differ diff --git a/front-end-engineering/2023-01-06-16-46-13.png b/front-end-engineering/2023-01-06-16-46-13.png new file mode 100644 index 00000000..9e8d957b Binary files /dev/null and b/front-end-engineering/2023-01-06-16-46-13.png differ diff --git a/front-end-engineering/2023-01-06-16-49-52.png b/front-end-engineering/2023-01-06-16-49-52.png new file mode 100644 index 00000000..bcea6851 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-49-52.png differ diff --git a/front-end-engineering/2023-01-06-16-50-09.png b/front-end-engineering/2023-01-06-16-50-09.png new file mode 100644 index 00000000..b361fd95 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-50-09.png differ diff --git a/front-end-engineering/2023-01-06-16-57-09.png b/front-end-engineering/2023-01-06-16-57-09.png new file mode 100644 index 00000000..d20e9d95 Binary files /dev/null and b/front-end-engineering/2023-01-06-16-57-09.png differ diff --git a/front-end-engineering/2023-01-06-16-58-00.png b/front-end-engineering/2023-01-06-16-58-00.png new file mode 100644 index 00000000..6a59f1bc Binary files /dev/null and b/front-end-engineering/2023-01-06-16-58-00.png differ diff --git a/front-end-engineering/2023-01-06-18-09-43.png b/front-end-engineering/2023-01-06-18-09-43.png new file mode 100644 index 00000000..41464d22 Binary files /dev/null and b/front-end-engineering/2023-01-06-18-09-43.png differ diff --git a/front-end-engineering/2023-02-01-21-59-20.png b/front-end-engineering/2023-02-01-21-59-20.png new file mode 100644 index 00000000..7f9ed7d5 Binary files /dev/null and b/front-end-engineering/2023-02-01-21-59-20.png differ diff --git a/front-end-engineering/2023-02-01-21-59-43.png b/front-end-engineering/2023-02-01-21-59-43.png new file mode 100644 index 00000000..fa2a660f Binary files /dev/null and b/front-end-engineering/2023-02-01-21-59-43.png differ diff --git a/front-end-engineering/2023-02-01-22-00-22.png b/front-end-engineering/2023-02-01-22-00-22.png new file mode 100644 index 00000000..f8731f1d Binary files /dev/null and b/front-end-engineering/2023-02-01-22-00-22.png differ diff --git a/front-end-engineering/2023-02-01-22-00-43.png b/front-end-engineering/2023-02-01-22-00-43.png new file mode 100644 index 00000000..81385dce Binary files /dev/null and b/front-end-engineering/2023-02-01-22-00-43.png differ diff --git a/front-end-engineering/2023-02-01-22-01-01.png b/front-end-engineering/2023-02-01-22-01-01.png new file mode 100644 index 00000000..3638af0c Binary files /dev/null and b/front-end-engineering/2023-02-01-22-01-01.png differ diff --git a/front-end-engineering/2023-02-01-22-01-19.png b/front-end-engineering/2023-02-01-22-01-19.png new file mode 100644 index 00000000..67abd835 Binary files /dev/null and b/front-end-engineering/2023-02-01-22-01-19.png differ diff --git a/front-end-engineering/2023-02-01-22-03-34.png b/front-end-engineering/2023-02-01-22-03-34.png new file mode 100644 index 00000000..b86fc088 Binary files /dev/null and b/front-end-engineering/2023-02-01-22-03-34.png differ diff --git a/front-end-engineering/2023-02-01-22-03-43.png b/front-end-engineering/2023-02-01-22-03-43.png new file mode 100644 index 00000000..699aebe2 Binary files /dev/null and b/front-end-engineering/2023-02-01-22-03-43.png differ diff --git a/front-end-engineering/2023-02-01-22-03-59.png b/front-end-engineering/2023-02-01-22-03-59.png new file mode 100644 index 00000000..1a9f9032 Binary files /dev/null and b/front-end-engineering/2023-02-01-22-03-59.png differ diff --git a/front-end-engineering/2023-02-01-22-04-06.png b/front-end-engineering/2023-02-01-22-04-06.png new file mode 100644 index 00000000..97a4aadc Binary files /dev/null and b/front-end-engineering/2023-02-01-22-04-06.png differ diff --git a/front-end-engineering/2023-02-01-22-05-00.png b/front-end-engineering/2023-02-01-22-05-00.png new file mode 100644 index 00000000..b20867dd Binary files /dev/null and b/front-end-engineering/2023-02-01-22-05-00.png differ diff --git a/front-end-engineering/2024-01-27-11-42-53.png b/front-end-engineering/2024-01-27-11-42-53.png new file mode 100644 index 00000000..15262500 Binary files /dev/null and b/front-end-engineering/2024-01-27-11-42-53.png differ diff --git a/front-end-engineering/2024-01-27-11-43-39.png b/front-end-engineering/2024-01-27-11-43-39.png new file mode 100644 index 00000000..74943299 Binary files /dev/null and b/front-end-engineering/2024-01-27-11-43-39.png differ diff --git a/front-end-engineering/2024-01-27-11-43-48.png b/front-end-engineering/2024-01-27-11-43-48.png new file mode 100644 index 00000000..db18322f Binary files /dev/null and b/front-end-engineering/2024-01-27-11-43-48.png differ diff --git a/front-end-engineering/2024-01-27-11-45-28.png b/front-end-engineering/2024-01-27-11-45-28.png new file mode 100644 index 00000000..3d0ea180 Binary files /dev/null and b/front-end-engineering/2024-01-27-11-45-28.png differ diff --git a/front-end-engineering/2024-01-28-16-38-33.png b/front-end-engineering/2024-01-28-16-38-33.png new file mode 100644 index 00000000..89f9175a Binary files /dev/null and b/front-end-engineering/2024-01-28-16-38-33.png differ diff --git a/front-end-engineering/2024-01-28-16-43-50.png b/front-end-engineering/2024-01-28-16-43-50.png new file mode 100644 index 00000000..918339bb Binary files /dev/null and b/front-end-engineering/2024-01-28-16-43-50.png differ diff --git a/front-end-engineering/2024-01-28-16-45-11.png b/front-end-engineering/2024-01-28-16-45-11.png new file mode 100644 index 00000000..38bfcf27 Binary files /dev/null and b/front-end-engineering/2024-01-28-16-45-11.png differ diff --git a/front-end-engineering/2024-01-28-16-52-03.png b/front-end-engineering/2024-01-28-16-52-03.png new file mode 100644 index 00000000..2547d6db Binary files /dev/null and b/front-end-engineering/2024-01-28-16-52-03.png differ diff --git a/front-end-engineering/2024-01-28-16-52-40.png b/front-end-engineering/2024-01-28-16-52-40.png new file mode 100644 index 00000000..899c7f20 Binary files /dev/null and b/front-end-engineering/2024-01-28-16-52-40.png differ diff --git a/front-end-engineering/2024-01-28-17-09-16.png b/front-end-engineering/2024-01-28-17-09-16.png new file mode 100644 index 00000000..315cfea0 Binary files /dev/null and b/front-end-engineering/2024-01-28-17-09-16.png differ diff --git a/front-end-engineering/2024-01-28-17-09-22.png b/front-end-engineering/2024-01-28-17-09-22.png new file mode 100644 index 00000000..ed16f560 Binary files /dev/null and b/front-end-engineering/2024-01-28-17-09-22.png differ diff --git a/front-end-engineering/2024-01-28-17-18-12.png b/front-end-engineering/2024-01-28-17-18-12.png new file mode 100644 index 00000000..3f05783d Binary files /dev/null and b/front-end-engineering/2024-01-28-17-18-12.png differ diff --git a/front-end-engineering/2024-01-28-17-28-28.png b/front-end-engineering/2024-01-28-17-28-28.png new file mode 100644 index 00000000..c416a2d4 Binary files /dev/null and b/front-end-engineering/2024-01-28-17-28-28.png differ diff --git a/front-end-engineering/2024-01-28-17-29-13.png b/front-end-engineering/2024-01-28-17-29-13.png new file mode 100644 index 00000000..99396040 Binary files /dev/null and b/front-end-engineering/2024-01-28-17-29-13.png differ diff --git a/front-end-engineering/2024-01-28-17-30-23.png b/front-end-engineering/2024-01-28-17-30-23.png new file mode 100644 index 00000000..650b32c3 Binary files /dev/null and b/front-end-engineering/2024-01-28-17-30-23.png differ diff --git a/front-end-engineering/2024-04-09-16-41-09.png b/front-end-engineering/2024-04-09-16-41-09.png new file mode 100644 index 00000000..86795762 Binary files /dev/null and b/front-end-engineering/2024-04-09-16-41-09.png differ diff --git a/front-end-engineering/2024-04-09-16-41-29.png b/front-end-engineering/2024-04-09-16-41-29.png new file mode 100644 index 00000000..2a95b635 Binary files /dev/null and b/front-end-engineering/2024-04-09-16-41-29.png differ diff --git a/front-end-engineering/2024-04-09-16-52-36.png b/front-end-engineering/2024-04-09-16-52-36.png new file mode 100644 index 00000000..3a2b1c8e Binary files /dev/null and b/front-end-engineering/2024-04-09-16-52-36.png differ diff --git a/front-end-engineering/2024-04-09-20-41-59.png b/front-end-engineering/2024-04-09-20-41-59.png new file mode 100644 index 00000000..58ca8600 Binary files /dev/null and b/front-end-engineering/2024-04-09-20-41-59.png differ diff --git a/front-end-engineering/2024-04-15-11-10-48.png b/front-end-engineering/2024-04-15-11-10-48.png new file mode 100644 index 00000000..a2277e7f Binary files /dev/null and b/front-end-engineering/2024-04-15-11-10-48.png differ diff --git a/front-end-engineering/2024-04-15-11-37-55.png b/front-end-engineering/2024-04-15-11-37-55.png new file mode 100644 index 00000000..6b63990b Binary files /dev/null and b/front-end-engineering/2024-04-15-11-37-55.png differ diff --git a/front-end-engineering/2024-04-15-11-45-10.png b/front-end-engineering/2024-04-15-11-45-10.png new file mode 100644 index 00000000..a486b281 Binary files /dev/null and b/front-end-engineering/2024-04-15-11-45-10.png differ diff --git a/front-end-engineering/2024-04-15-14-51-24.png b/front-end-engineering/2024-04-15-14-51-24.png new file mode 100644 index 00000000..86a1fb2b Binary files /dev/null and b/front-end-engineering/2024-04-15-14-51-24.png differ diff --git a/front-end-engineering/2024-04-15-16-29-04.png b/front-end-engineering/2024-04-15-16-29-04.png new file mode 100644 index 00000000..6f7dc864 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-29-04.png differ diff --git a/front-end-engineering/2024-04-15-16-29-22.png b/front-end-engineering/2024-04-15-16-29-22.png new file mode 100644 index 00000000..4d8b7ad7 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-29-22.png differ diff --git a/front-end-engineering/2024-04-15-16-30-16.png b/front-end-engineering/2024-04-15-16-30-16.png new file mode 100644 index 00000000..ff8b9fbd Binary files /dev/null and b/front-end-engineering/2024-04-15-16-30-16.png differ diff --git a/front-end-engineering/2024-04-15-16-32-00.png b/front-end-engineering/2024-04-15-16-32-00.png new file mode 100644 index 00000000..990f1893 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-32-00.png differ diff --git a/front-end-engineering/2024-04-15-16-32-35.png b/front-end-engineering/2024-04-15-16-32-35.png new file mode 100644 index 00000000..4b08300d Binary files /dev/null and b/front-end-engineering/2024-04-15-16-32-35.png differ diff --git a/front-end-engineering/2024-04-15-16-32-46.png b/front-end-engineering/2024-04-15-16-32-46.png new file mode 100644 index 00000000..6abf64f4 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-32-46.png differ diff --git a/front-end-engineering/2024-04-15-16-33-30.png b/front-end-engineering/2024-04-15-16-33-30.png new file mode 100644 index 00000000..b4e73817 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-33-30.png differ diff --git a/front-end-engineering/2024-04-15-16-34-19.png b/front-end-engineering/2024-04-15-16-34-19.png new file mode 100644 index 00000000..71f2ffd5 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-34-19.png differ diff --git a/front-end-engineering/2024-04-15-16-37-04.png b/front-end-engineering/2024-04-15-16-37-04.png new file mode 100644 index 00000000..a4c58cf7 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-37-04.png differ diff --git a/front-end-engineering/2024-04-15-16-38-36.png b/front-end-engineering/2024-04-15-16-38-36.png new file mode 100644 index 00000000..73fece0d Binary files /dev/null and b/front-end-engineering/2024-04-15-16-38-36.png differ diff --git a/front-end-engineering/2024-04-15-16-38-54.png b/front-end-engineering/2024-04-15-16-38-54.png new file mode 100644 index 00000000..c8e3aa3d Binary files /dev/null and b/front-end-engineering/2024-04-15-16-38-54.png differ diff --git a/front-end-engineering/2024-04-15-16-40-03.png b/front-end-engineering/2024-04-15-16-40-03.png new file mode 100644 index 00000000..46b75e05 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-40-03.png differ diff --git a/front-end-engineering/2024-04-15-16-40-30.png b/front-end-engineering/2024-04-15-16-40-30.png new file mode 100644 index 00000000..d5c7b38a Binary files /dev/null and b/front-end-engineering/2024-04-15-16-40-30.png differ diff --git a/front-end-engineering/2024-04-15-16-40-38.png b/front-end-engineering/2024-04-15-16-40-38.png new file mode 100644 index 00000000..9eaf07bd Binary files /dev/null and b/front-end-engineering/2024-04-15-16-40-38.png differ diff --git a/front-end-engineering/2024-04-15-16-41-42.png b/front-end-engineering/2024-04-15-16-41-42.png new file mode 100644 index 00000000..05684123 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-41-42.png differ diff --git a/front-end-engineering/2024-04-15-16-42-00.png b/front-end-engineering/2024-04-15-16-42-00.png new file mode 100644 index 00000000..53e83e99 Binary files /dev/null and b/front-end-engineering/2024-04-15-16-42-00.png differ diff --git a/front-end-engineering/2024-04-15-19-41-41.png b/front-end-engineering/2024-04-15-19-41-41.png new file mode 100644 index 00000000..34d0dab6 Binary files /dev/null and b/front-end-engineering/2024-04-15-19-41-41.png differ diff --git a/front-end-engineering/2024-04-16-14-40-53.png b/front-end-engineering/2024-04-16-14-40-53.png new file mode 100644 index 00000000..615e54ab Binary files /dev/null and b/front-end-engineering/2024-04-16-14-40-53.png differ diff --git a/front-end-engineering/2024-04-16-14-41-08.png b/front-end-engineering/2024-04-16-14-41-08.png new file mode 100644 index 00000000..14e75fe6 Binary files /dev/null and b/front-end-engineering/2024-04-16-14-41-08.png differ diff --git a/front-end-engineering/2024-04-16-15-37-25.png b/front-end-engineering/2024-04-16-15-37-25.png new file mode 100644 index 00000000..8a3431d2 Binary files /dev/null and b/front-end-engineering/2024-04-16-15-37-25.png differ diff --git a/front-end-engineering/2024-04-16-16-01-56.png b/front-end-engineering/2024-04-16-16-01-56.png new file mode 100644 index 00000000..a82e18ef Binary files /dev/null and b/front-end-engineering/2024-04-16-16-01-56.png differ diff --git a/front-end-engineering/2024-04-16-17-22-00.png b/front-end-engineering/2024-04-16-17-22-00.png new file mode 100644 index 00000000..de6c3a5d Binary files /dev/null and b/front-end-engineering/2024-04-16-17-22-00.png differ diff --git a/front-end-engineering/2024-04-16-17-23-18.png b/front-end-engineering/2024-04-16-17-23-18.png new file mode 100644 index 00000000..6d7d480b Binary files /dev/null and b/front-end-engineering/2024-04-16-17-23-18.png differ diff --git a/front-end-engineering/2024-04-16-17-25-36.png b/front-end-engineering/2024-04-16-17-25-36.png new file mode 100644 index 00000000..fad314a3 Binary files /dev/null and b/front-end-engineering/2024-04-16-17-25-36.png differ diff --git a/front-end-engineering/2024-06-16-15-36-15.png b/front-end-engineering/2024-06-16-15-36-15.png new file mode 100644 index 00000000..efb11cbc Binary files /dev/null and b/front-end-engineering/2024-06-16-15-36-15.png differ diff --git "a/front-end-engineering/CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.html" "b/front-end-engineering/CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.html" new file mode 100644 index 00000000..54ea3718 --- /dev/null +++ "b/front-end-engineering/CSS \351\242\204\345\244\204\347\220\206\345\231\250\344\271\213SCSS.html" @@ -0,0 +1,2988 @@ + + + + + + CSS 预处理器之 SCSS | Sunny's blog + + + + + + + + +
Skip to content
On this page

CSS 预处理器之 SCSS

概述

前端目前要做的事情是越来越多了,实际上有一部分工作是和业务逻辑无关的

  • 文件优化:压缩 JavaScriptCSSHTML 代码,压缩合并图片等。
  • 代码转换:将 TypeScript/ES 6 编译成 JavaScript、将 SCSS 编译成 CSS 等。
  • 代码优化:为 CSS 代码添加兼容性前缀等。

这些工作虽然和业务逻辑无关,但是你又不得不做。既然这些工作都要做,那么谁来做这些事情?

这里我们会通过一些工具来完成这些事情,但是这里又遇到一个问题,往往上面的这些事情需要好几个工作来完成。这里就会存在一种情况:需要先将项目拖入到工具 A 进行处理,拖入到工具 B 进行处理、C、D、E...

上面的步骤显得非常的麻烦,我们期望有那么一个工具能够帮助我们把上面的事情按照一定的顺序自动化的完成,这个工具就是我们前端构建工具。

总的来讲,“构建工具”里面“构建”二字是一个重点,这个工具究竟构建了一个啥?

将我们开发环境下的项目代码构建为能够部署上线的代码

通过构建工具,我们就可以省去繁杂的步骤,直接一条指令就能将开发环境的项目构建为生产环境的项目代码,之后要做的就是部署上线即可。

目前前端领域有非常的多的构建工具,整体来讲可以分为三代:

  • 第一代构建工具:以 grunt、gulp 为代表的构建工具
  • 第二代构建工具:以 webpack 为代表的构建工具
  • 第三代构建工具:以 Vite 为代表的构建工具
  1. 模块化

模块化是将 CSS 代码分解成独立的、可重用的模块,从而提高代码的可维护性和可读性。通常,每个模块都关注一个特定的功能或组件的样式。这有助于减少样式之间的依赖和冲突,也使得找到和修改相关样式变得更容易。模块化的实现可以通过原生的 CSS 文件拆分,或使用预处理器(如 Sass)的功能(例如 @import 和 @use)来实现。

  1. 命名规范

CSS 类名和选择器定义一致的命名规范有助于提高代码的可读性和可维护性。

以下是一些常见的命名规范:

BEMBlock, Element, Modifier):BEM 是一种命名规范,将类名分为三个部分:块(Block)、元素(Element)和修饰符(Modifier)。这种方法有助于表示组件之间的层级关系和状态变化。例如,navigation__link--active

OOCSS(面向对象的 CSS):OOCSS 旨在将可重用的样式划分为独立的“对象”,从而提高代码的可维护性和可扩展性。这种方法强调将结构(如布局)与皮肤(如颜色和字体样式)分离。这样可以让你更容易地复用和组合样式,创建更灵活的 UI 组件。

html
<button class="btn btn--primary">Primary Button</button>
+<button class="btn btn--secondary">Secondary Button</button>
+
<button class="btn btn--primary">Primary Button</button>
+<button class="btn btn--secondary">Secondary Button</button>
+
css
/* 结构样式 */
+.btn {
+  display: inline-block;
+  padding: 10px 20px;
+  border: none;
+  cursor: pointer;
+  font-size: 16px;
+}
+
+/* 皮肤样式 */
+.btn--primary {
+  background-color: blue;
+  color: white;
+}
+
+.btn--secondary {
+  background-color: gray;
+  color: white;
+}
+
/* 结构样式 */
+.btn {
+  display: inline-block;
+  padding: 10px 20px;
+  border: none;
+  cursor: pointer;
+  font-size: 16px;
+}
+
+/* 皮肤样式 */
+.btn--primary {
+  background-color: blue;
+  color: white;
+}
+
+.btn--secondary {
+  background-color: gray;
+  color: white;
+}
+

SMACSS(可扩展和模块化的 CSS 架构):是一种 CSS 编写方法,旨在提高 CSS 代码的可维护性、可扩展性和灵活性。SMACSS 将样式分为五个基本类别:Base、Layout、Module、StateTheme。这有助于组织 CSS 代码并使其易于理解和修改。

  • Base:包含全局样式和元素默认样式,例如:浏览器重置、全局字体设置等。
  • Layout:描述页面布局的大致结构,例如:页面分区、网格系统等。
  • Module:表示可重用的 UI 组件,例如:按钮、卡片、表单等。
  • State:表示 UI 组件的状态,例如:激活、禁用、隐藏等。通常,状态类会与其他类一起使用以修改其显示。
  • Theme:表示 UI 组件的视觉样式,例如:颜色、字体等。通常,主题类用于支持多个主题或在不同上下文中使用相同的组件。
  1. 预处理器

CSS 预处理器(如 Sass、LessStylus)是一种编程式的 CSS 语言,可以在开发过程中提供更高级的功能,如变量、嵌套、函数和混合等。预处理器将这些扩展语法编译为普通的 CSS 代码,以便浏览器能够解析。

  1. 后处理器

CSS 后处理器(如 PostCSS)可以在生成的 CSS 代码上执行各种操作,如添加浏览器前缀、优化规则和转换现代 CSS 功能以兼容旧浏览器等。它通常与构建工具(例如 Webpack)一起使用,以自动化这些任务。

  1. 代码优化

代码优化旨在减少 CSS 文件的大小、删除无用代码和提高性能。一些常见的优化工具包括:

  • CSSOCSSO 是一个 CSS 优化工具,可以压缩代码、删除冗余规则和合并相似的声明。

  • PurgeCSSPurgeCSS 是一个用于删除无用 CSS 规则的工具。它通过分析项目的 HTML、JS 和模板文件来检测实际使用的样式,并删除未使用的样式。

  • clean-cssclean-css 是一个高效的 CSS 压缩工具,可以删除空格、注释和重复的规则等,以减小文件大小。

  1. 构建工具和自动化

构建工具(如 WebpackGulpGrunt)可以帮助您自动化开发过程中的任务,如编译预处理器代码、合并和压缩 CSS 文件、优化图片等。这可以提高开发效率,确保项目的一致性,并简化部署流程。这些工具通常可以通过插件和配置来定制,以满足项目的特定需求。

  1. 响应式设计和移动优先

响应式设计是一种使网站在不同设备和屏幕尺寸上都能保持良好显示效果的方法。这通常通过使用媒体查询、弹性布局(如 FlexboxCSS Grid)和其他技术实现。移动优先策略是从最小屏幕尺寸(如手机)开始设计样式,然后逐步增强以适应更大的屏幕尺寸(如平板和桌面)。这种方法有助于保持代码的简洁性,并确保网站在移动设备上的性能优先。

  1. 设计系统和组件库

设计系统是一套规范,为开发人员和设计师提供统一的样式指南(如颜色、排版、间距等)、组件库和最佳实践。这有助于提高项目的一致性、可维护性和协作效率。组件库通常包含一系列预定义的可复用组件(如按钮、输入框、卡片等),可以快速集成到项目中。一些流行的组件库和 UI 框架包括 Bootstrap、FoundationMaterial-UI 等。

因此整个 CSS 都是逐渐在向工程化靠近的,上面所罗列的那么几点都是 CSS 在工程化方面的一些体现。

CSS 预处理器

预处理器基本介绍

平时在工作的时候,经常会面临这样的情况:需要书写很多的样式代码,特别是面对比较大的项目的时候,代码量会急剧提升,普通的 CSS 书写方式不方便维护以及扩展还有复用。

通过 CSS 预处理技术就可以解决上述的问题。基于预处理技术的语言,我们可以称之为 CSS 预处理语言,该语言会为你提供一套增强语法,我们在书写 CSS 的时候就使用增强语法来书写,书写完毕后通过预处理技术编译为普通的 CSS 语言即可。

CSS 预处理器的出现,解决了如下的问题:

  • 代码组织:通过嵌套、模块化和文件引入等功能,使 CSS 代码结构更加清晰,便于管理和维护。
  • 变量和函数:支持自定义变量和函数,增强代码的可重用性和一致性。
  • 代码简洁:通过简洁的语法结构和内置函数,减少代码冗余,提高开发效率。
  • 易于扩展:可以通过插件系统扩展预处理器的功能,方便地添加新特性。

目前前端领域常见的 CSS 预处理器有三个:

  • Sass
  • Less
  • Stylus

下面我们对这三个 CSS 预处理器做一个简单的介绍。

Sass

Sass 是最早出现的 CSS 预处理器,出现的时间为 2006 年,该预处理器有两种语法格式:

  • 缩进式语法(2006 年)
sass
$primary-color: #4CAF50
+
+.container
+	background-color: $primary-color
+	padding: 20px
+
+  .title
+    font-size: 24px
+    color: white
+
$primary-color: #4CAF50
+
+.container
+	background-color: $primary-color
+	padding: 20px
+
+  .title
+    font-size: 24px
+    color: white
+
  • 类 CSS 语法(2009 年)
scss
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+

Sass 提供了很多丰富的功能,例如有声明变量、嵌套、混合、继承、函数等,另外 Sass 还有强大的社区支持以及丰富的插件资源,因此 Sass 比较适合大型项目以及团队协作。

  • Less:Less 也是一个比较流行的 CSS 预处理器,该预处理器是在 2009 年出现的,该预处理器借鉴了 Sass 的长处,并在此基础上兼容 CSS 语法,让开发者开发起来更加的得心应手
less
@primary-color: #4caf50;
+
+.container {
+  background-color: @primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+
@primary-color: #4caf50;
+
+.container {
+  background-color: @primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+

早期的 Sass 一开始只有缩进式语法,所以 Less 的出现降低了开发者的学习成本,因为 Less 兼容原生 CSS 语法,相较于 Sass,Less 学习曲线平滑、语法简单,但是编程功能相比 Sass 要弱一些,并且插件和社区也没有 Sass 那么强大,Less 的出现反而让 Sass 出现了第二版语法(类 CSS 语法)

  • Stylus:Stylus 是一种灵活且强大的 CSS 预处理器,其语法非常简洁,具有很高的自定义性。Stylus 支持多种语法风格,包括缩进式和类 CSS 语法。Stylus 提供了丰富的功能,如变量、嵌套、混合、条件语句、循环等。相较于 SassLessStylus 的社区规模较小,但仍有不少开发者喜欢其简洁灵活的特点。
stylus
primary-color = #4CAF50
+
+.container
+  background-color primary-color
+  padding 20px
+
+  .title
+    font-size 24px
+    color white
+
primary-color = #4CAF50
+
+.container
+  background-color primary-color
+  padding 20px
+
+  .title
+    font-size 24px
+    color white
+

Sass 快速入门

Sass 最早是由 Hampton Catlin 于 2006 年开发的,并且于 2007 年首次发布。

在最初的时候, Sass 采用的是缩进敏感语法,文件的扩展名为 .sass,编写完毕之后,需要通过基于 ruby 的 ruby sass 的编译器编译为普通的 CSS。因此在最早期的时候,如果你想要使用 sass,你是需要安装 Ruby 环境。

为什么早期选择了采用 Ruby 呢?这和当时的 Web 开发大环境有关系,在 2006 - 2010 左右,当时 Web 服务器端开发就特别流行使用基于 Ruby 的 Web 框架 Ruby on Rails。像早期的 github、Twitter 一开始都是使用的 ruby。

到了 2009 年的时候, Less 的出现给 Sass 带来竞争压力,因为 Less 是基于原生 CSS 语法进行扩展,使用者没有什么学习压力,于是 Natalie WeizenbaumChris Eppstein 为 Sass 引入了新的类 CSS 语法,也是基于原生的 CSS 进行语法扩展,文件的后缀名为 scss。虽然文件的后缀以及语法更新了,但是功能上面仍然支持之前 sass 所支持的所有功能,加上 sass 本身插件以及社区就比 less 强大,因此 Sass 重新变为了主流。

接下来还需要说一下关于编译器。随着社区的发展和技术的进步,Sass 已经不在局限于 Ruby,目前已经有多种语言实现了 Sass 的编译器:

  • Dart Sass:由 Sass 官方团队维护,使用 Dart 语言编写。它是目前推荐的 Sass 编译器,因为它是最新的、功能最全的实现。

    GitHub 仓库:https://github.com/sass/dart-sass

  • LibSass:使用 C/C++ 编写的 Sass 编译器,它的性能优于 Ruby 版本。LibSass 有多个绑定,例如 Node SassNode.js 绑定)和 SassCC 绑定)。

    GitHub 仓库:https://github.com/sass/libsass

  • Ruby Sass:最早的 Ruby 实现,已被官方废弃,并建议迁移到 Dart Sass

官方推荐使用 Dart Sass 来作为 Sass 的编译器,并且在 npm 上面发布了 Dart Sass 的包,直接通过 npm 安装即可。

接下来我们来看一个 Sass 的快速上手示例,首先创建一个新的项目目录 sass-demo,使用 pnpm init 进行项目初始化,之后安装 sass 依赖:

bash
pnpm add sass -D
+
pnpm add sass -D
+

接下来在 src/index.scss 里面写入如下的 scss 代码:

scss
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+
$primary-color: #4caf50;
+
+.container {
+  background-color: $primary-color;
+  padding: 20px;
+
+  .title {
+    font-size: 24px;
+    color: white;
+  }
+}
+

接下来我们就会遇到第一个问题,编译。

这里我们就可以使用 sass 提供的 compile 方法进行编译。在 src 目录下面创建一个 index.js 文件,记入如下的代码:

js
// 读取 scss 文件
+// 调用 sass 依赖提供的 complie 进行文件的编译
+// 最终在 dist 目录下面新生成一个 index.css(编译后的文件)
+
+const sass = require("sass"); // 做 scss 文件编译的
+const path = require("path"); // 处理路径相关的
+const fs = require("fs"); // 处理文件读写相关的
+
+// 定义一下输入和输出的文件路径
+const scssPath = path.resolve("src", "index.scss");
+const cssDir = "dist"; // 之后编译的 index.css 要存储的目录名
+const cssPath = path.resolve(cssDir, "index.css");
+
+// 编译
+const result = sass.compile(scssPath);
+console.log(result.css);
+
+// 将编译后的字符串写入到文件里面
+if (!fs.existsSync(cssDir)) {
+  // 说明不存在,那就创建
+  fs.mkdirSync(cssDir);
+}
+
+fs.writeFileSync(cssPath, result.css);
+
// 读取 scss 文件
+// 调用 sass 依赖提供的 complie 进行文件的编译
+// 最终在 dist 目录下面新生成一个 index.css(编译后的文件)
+
+const sass = require("sass"); // 做 scss 文件编译的
+const path = require("path"); // 处理路径相关的
+const fs = require("fs"); // 处理文件读写相关的
+
+// 定义一下输入和输出的文件路径
+const scssPath = path.resolve("src", "index.scss");
+const cssDir = "dist"; // 之后编译的 index.css 要存储的目录名
+const cssPath = path.resolve(cssDir, "index.css");
+
+// 编译
+const result = sass.compile(scssPath);
+console.log(result.css);
+
+// 将编译后的字符串写入到文件里面
+if (!fs.existsSync(cssDir)) {
+  // 说明不存在,那就创建
+  fs.mkdirSync(cssDir);
+}
+
+fs.writeFileSync(cssPath, result.css);
+

上面的方式,每次需要手动的运行 index.js 来进行编译,而且还需要手动的指定要编译的文件是哪一个,比较麻烦。早期的时候出现了一个专门做 sass 编译的 GUI 工具,叫做考拉,对应的官网地址为:http://koala-app.com/

现在随着 Vscode 这个集大成的 IDE 的出现,可以直接安装 Sass 相关的插件来做编译操作,例如 scss-to-css

注意在一开始安装的时候,该插件进行 scss 转换时会压缩 CSS 代码,这个是可以进行配置的。

Sass 基础语法

注释

CSS 里面的注释 /* */ ,Sass 中支持 // 来进行注释,// 类型的注释再编译后是会消失

scss
/*
+  Hello
+*/
+
+// World
+
/*
+  Hello
+*/
+
+// World
+
css
/*
+  Hello
+*/
+
/*
+  Hello
+*/
+

如果是压缩输出模式,那么注释也会被去掉,这个时候可以在多行注释的第一个字符书写一个 ! ,此时即便是在压缩模式,这条注释也会被保留,通常用于添加版权信息

scss
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+
+.test {
+  width: 300px;
+}
+
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+
+.test {
+  width: 300px;
+}
+
css
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+.test {
+  width: 300px;
+}
+
/*!
+  该 CSS 作者 XXX
+  创建于 xxxx年xx月xx日
+*/
+.test {
+  width: 300px;
+}
+

变量

这是当初 Sass 推出时一个极大的亮点,支持变量的声明,声明方式很简单,通过 $ 开头来进行声明,赋值方法和 CSS 属性的写法是一致的。

scss
// 声明变量
+$width: 1600px;
+$pen-size: 3em;
+
+div {
+  width: $width;
+  font-size: $pen-size;
+}
+
// 声明变量
+$width: 1600px;
+$pen-size: 3em;
+
+div {
+  width: $width;
+  font-size: $pen-size;
+}
+
css
div {
+  width: 1600px;
+  font-size: 3em;
+}
+
div {
+  width: 1600px;
+  font-size: 3em;
+}
+

变量的声明时支持块级作用域的,如果是在一个嵌套规则内部定义的变量,那么就只能在嵌套规则内部使用(局部变量),如果不是在嵌套规则内定义的变量那就是全局变量。

scss
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red;
+
+  p.one {
+    width: $width; /* 800px */
+    color: $color; /* red */
+  }
+}
+
+p.two {
+  width: $width; /* 1600px */
+  color: $color; /* 报错,因为 $color 是一个局部变量 */
+}
+
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red;
+
+  p.one {
+    width: $width; /* 800px */
+    color: $color; /* red */
+  }
+}
+
+p.two {
+  width: $width; /* 1600px */
+  color: $color; /* 报错,因为 $color 是一个局部变量 */
+}
+

可以通过一个 !global 将一个局部变量转换为全局变量

scss
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red !global;
+
+  p.one {
+    width: $width;
+    color: $color;
+  }
+}
+
+p.two {
+  width: $width;
+  color: $color;
+}
+
// 声明变量
+$width: 1600px;
+
+div {
+  $width: 800px;
+  $color: red !global;
+
+  p.one {
+    width: $width;
+    color: $color;
+  }
+}
+
+p.two {
+  width: $width;
+  color: $color;
+}
+
css
div p.one {
+  width: 800px;
+  color: red;
+}
+
+p.two {
+  width: 1600px;
+  color: red;
+}
+
div p.one {
+  width: 800px;
+  color: red;
+}
+
+p.two {
+  width: 1600px;
+  color: red;
+}
+

数据类型

因为 CSS 预处理器就是针对 CSS 这一块融入编程语言的特性进去,所以自然会有数据类型。

在 Sass 中支持 7 种数据类型:

  • 数值类型:1、2、13、10px
  • 字符串类型:有引号字符串和无引号字符串 "foo"、'bar'、baz
  • 布尔类型:true、false
  • 空值:null
  • 数组(list):用空格或者逗号来进行分隔,1px 10px 15px 5px、1px,10px,15px,5px
  • 字典(map):用一个小括号扩起来,里面是一对一对的键值对 (key1:value1, key2:value2)
  • 颜色类型:blue、#04a012、rgba(0,0,12,0.5)

数值类型

Sass 里面支持两种数值类型:带单位数值不带单位的数值,数字可以是正负数以及浮点数

scss
$my-age: 19;
+$your-age: 19.5;
+$height: 120px;
+
$my-age: 19;
+$your-age: 19.5;
+$height: 120px;
+

字符串类型

支持两种:有引号字符串无引号字符串

并且引号可以是单引号也可以是双引号

scss
$name: "Tom Bob";
+$container: "top bottom";
+$what: heart;
+
+div {
+  background-image: url($what + ".png");
+}
+
$name: "Tom Bob";
+$container: "top bottom";
+$what: heart;
+
+div {
+  background-image: url($what + ".png");
+}
+
css
div {
+  background-image: url(heart.png);
+}
+
div {
+  background-image: url(heart.png);
+}
+

布尔类型

该类型就两个值:true 和 false,可以进行逻辑运算,支持 and、or、not 来做逻辑运算

scss
$a: 1>0 and 0>5; // false
+$b: "a" == a; // true
+$c: false; // false
+$d: not $c; // true
+
$a: 1>0 and 0>5; // false
+$b: "a" == a; // true
+$c: false; // false
+$d: not $c; // true
+

空值类型

就一个值:null 表示为空的意思

scss
$value: null;
+
$value: null;
+

因为是空值,因此不能够使用它和其他类型进行算数运算

数组类型

数组有两种表示方式:通过空格来间隔 以及 通过逗号来间隔

例如:

scss
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+

关于数组,有如下的注意事项:

  1. 数组里面可以包含子数组,例如 1px 2px, 5px 6px 就是包含了两个数组,1px 2px 是一个数组,5px 6px 又是一个数组,如果内外数组的分隔方式相同,例如都是采用空格来分隔,这个时候可以使用一个小括号来分隔 (1px 2px) (5px 6px)
  2. 添加了小括号的内容最终被编译为 CSS 的时候,是会被去除掉小括号的,例如 (1px 2px) (5px 6px) ---> 1px 2px 5px 6px
scss
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+
+div {
+  padding: $list2;
+}
+
$list0: 1px 2px 5px 6px;
+$list1: 1px 2px, 5px 6px;
+$list2: (1px 2px) (5px 6px);
+
+div {
+  padding: $list2;
+}
+
css
div {
+  padding: 1px 2px 5px 6px;
+}
+
div {
+  padding: 1px 2px 5px 6px;
+}
+
  1. 小括号如果为空,则表示是一个空数组,空数组是不可以直接编译为 CSS 的
scss
$list2: ();
+
+div {
+  padding: $list2; // 报错
+}
+
$list2: ();
+
+div {
+  padding: $list2; // 报错
+}
+

但是如果是数组里面包含空数组或者 null 空值,编译能够成功,空数组以及空值会被去除掉

scss
$list2: 1px 2px null 3px;
+$list3: 1px 2px () 3px;
+
+div {
+  padding: $list2;
+}
+
+.div2 {
+  padding: $list3;
+}
+
$list2: 1px 2px null 3px;
+$list3: 1px 2px () 3px;
+
+div {
+  padding: $list2;
+}
+
+.div2 {
+  padding: $list3;
+}
+
  1. 可以使用 nth 函数去访问数组里面的值,注意数组的下标是从 1 开始的。
scss
// 创建一个 List
+$font-sizes: 12px 14px 16px 18px 24px;
+
+// 通过索引访问 List 中的值
+$base-font-size: nth($font-sizes, 3);
+
+// 使用 List 中的值为元素设置样式
+body {
+  font-size: $base-font-size;
+}
+
// 创建一个 List
+$font-sizes: 12px 14px 16px 18px 24px;
+
+// 通过索引访问 List 中的值
+$base-font-size: nth($font-sizes, 3);
+
+// 使用 List 中的值为元素设置样式
+body {
+  font-size: $base-font-size;
+}
+
css
body {
+  font-size: 16px;
+}
+
body {
+  font-size: 16px;
+}
+

最后我们来看一个实际开发中用到数组的典型案例:

scss
$sizes: 40px 50px 60px;
+
+@each $s in $sizes {
+  .icon-#{$s} {
+    font-size: $s;
+    width: $s;
+    height: $s;
+  }
+}
+
$sizes: 40px 50px 60px;
+
+@each $s in $sizes {
+  .icon-#{$s} {
+    font-size: $s;
+    width: $s;
+    height: $s;
+  }
+}
+
css
.icon-40px {
+  font-size: 40px;
+  width: 40px;
+  height: 40px;
+}
+
+.icon-50px {
+  font-size: 50px;
+  width: 50px;
+  height: 50px;
+}
+
+.icon-60px {
+  font-size: 60px;
+  width: 60px;
+  height: 60px;
+}
+
.icon-40px {
+  font-size: 40px;
+  width: 40px;
+  height: 40px;
+}
+
+.icon-50px {
+  font-size: 50px;
+  width: 50px;
+  height: 50px;
+}
+
+.icon-60px {
+  font-size: 60px;
+  width: 60px;
+  height: 60px;
+}
+

字典类型

字典类型必须要使用小括号扩起来,小括号里面是一对一对的键值对

scss
$a: (
+  $key1: value1,
+  $key2: value2,
+);
+
$a: (
+  $key1: value1,
+  $key2: value2,
+);
+

可以通过 map-get 方法来获取字典值

scss
// 创建一个 Map
+$colors: (
+  "primary": #4caf50,
+  "secondary": #ff9800,
+  "accent": #2196f3,
+);
+
+$primary: map-get($colors, "primary");
+
+button {
+  background-color: $primary;
+}
+
// 创建一个 Map
+$colors: (
+  "primary": #4caf50,
+  "secondary": #ff9800,
+  "accent": #2196f3,
+);
+
+$primary: map-get($colors, "primary");
+
+button {
+  background-color: $primary;
+}
+
css
button {
+  background-color: #4caf50;
+}
+
button {
+  background-color: #4caf50;
+}
+

接下来还是看一个实际开发中的示例:

scss
$icons: (
+  "eye": "\f112",
+  "start": "\f12e",
+  "stop": "\f12f",
+);
+
+@each $key, $value in $icons {
+  .icon-#{$key}:before {
+    display: inline-block;
+    font-family: "Open Sans";
+    content: $value;
+  }
+}
+
$icons: (
+  "eye": "\f112",
+  "start": "\f12e",
+  "stop": "\f12f",
+);
+
+@each $key, $value in $icons {
+  .icon-#{$key}:before {
+    display: inline-block;
+    font-family: "Open Sans";
+    content: $value;
+  }
+}
+
css
.icon-eye:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\f112";
+}
+
+.icon-start:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\f12e";
+}
+
+.icon-stop:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\f12f";
+}
+
.icon-eye:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\f112";
+}
+
+.icon-start:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\f12e";
+}
+
+.icon-stop:before {
+  display: inline-block;
+  font-family: "Open Sans";
+  content: "\f12f";
+}
+

颜色类型

支持原生 CSS 中各种颜色的表示方式,十六进制、RGB、RGBA、HSL、HSLA、颜色英语单词。

Sass 还提供了内置的 Colors 相关的各种函数,可以方便我们对颜色进行一个颜色值的调整和操作。

  • lighten 和 darken:调整颜色的亮度,lighten 是增加亮度、darken 是减少亮度
scss
$color: red;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: lighten($color, 10%); // 亮度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: darken($color, 10%); // 亮度减少10%
+}
+
$color: red;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: lighten($color, 10%); // 亮度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: darken($color, 10%); // 亮度减少10%
+}
+
css
.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: #ff3333;
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: #cc0000;
+}
+
.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: #ff3333;
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: #cc0000;
+}
+
  • saturate 和 desaturate:调整颜色的饱和度
scss
$color: #4caf50;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: saturate($color, 10%); // 饱和度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: desaturate($color, 10%); // 饱和度减少10%
+}
+
$color: #4caf50;
+
+.div1 {
+  width: 200px;
+  height: 200px;
+  background-color: saturate($color, 10%); // 饱和度增加10%
+}
+
+.div2 {
+  width: 200px;
+  height: 200px;
+  background-color: desaturate($color, 10%); // 饱和度减少10%
+}
+
  • Adjust Hue:通过调整颜色的色相来创建新颜色。
scss
$color: #4caf50;
+$new-hue: adjust-hue($color, 30); // 色相增加 30 度
+
$color: #4caf50;
+$new-hue: adjust-hue($color, 30); // 色相增加 30 度
+
  • RGBA:为颜色添加透明度。
scss
$color: #4caf50;
+$transparent: rgba($color, 0.5); // 添加 50% 透明度
+
$color: #4caf50;
+$transparent: rgba($color, 0.5); // 添加 50% 透明度
+
  • Mix:混合两种颜色。
scss
$color1: #4caf50;
+$color2: #2196f3;
+$mixed: mix($color1, $color2, 50%); // 混合两种颜色,权重 50%
+
$color1: #4caf50;
+$color2: #2196f3;
+$mixed: mix($color1, $color2, 50%); // 混合两种颜色,权重 50%
+
  • Complementary:获取颜色的补充颜色。
scss
$color: #4caf50;
+$complementary: adjust-hue($color, 180); // 色相增加 180 度,获取补充颜色
+
$color: #4caf50;
+$complementary: adjust-hue($color, 180); // 色相增加 180 度,获取补充颜色
+

如果想要查阅具体有哪些颜色相关的函数,可以参阅官方文档:https://sass-lang.com/documentation/modules/color

嵌套语法

插值语法

运算

关于运算相关的一些函数:

  • calc
  • max 和 min
  • clamp

calc

该方法是 CSS3 提供的一个方法,用于做属性值的计算。

scss
.container {
+  width: 80%;
+  padding: 0 20px;
+
+  .element {
+    width: calc(100% - 40px);
+  }
+
+  .element2 {
+    width: calc(100px - 40px);
+  }
+}
+
.container {
+  width: 80%;
+  padding: 0 20px;
+
+  .element {
+    width: calc(100% - 40px);
+  }
+
+  .element2 {
+    width: calc(100px - 40px);
+  }
+}
+
css
.container {
+  width: 80%;
+  padding: 0 20px;
+}
+.container .element {
+  width: calc(100% - 40px);
+}
+.container .element2 {
+  width: 60px;
+}
+
.container {
+  width: 80%;
+  padding: 0 20px;
+}
+.container .element {
+  width: calc(100% - 40px);
+}
+.container .element2 {
+  width: 60px;
+}
+

注意,在上面的编译当中,如果单位相同,Sass 在做编译的时候会直接运行 calc 运算表达式,得到计算出来的最终值,但是如果单位不相同,会保留 calc 运算表达式。

min 和 max

min 是在一组数据里面找出最小值,max 就是在一组数据里面找到最大值。

scss
$width1: 500px;
+$width2: 600px;
+
+.element {
+  width: min($width1, $width2);
+}
+
$width1: 500px;
+$width2: 600px;
+
+.element {
+  width: min($width1, $width2);
+}
+
css
.element {
+  width: 500px;
+}
+
.element {
+  width: 500px;
+}
+

clamp

这个也是 CSS3 提供的函数,语法为:

css
clamp(min, value, max)
+
clamp(min, value, max)
+

min 代表下限,max 代表上限,value 是需要限制的值。clamp 的作用就是将 value 限制在 min 和 max 之间,如果 value 小于了 min 那么就取 min 作为值,如果 vlaue 大于了 max,那么就取 max 作为值。如果 value 在 min 和 max 之间,那么就返回 value 值本身。

scss
$min-font-size: 16px;
+$max-font-size: 24px;
+
+body {
+  font-size: clamp($min-font-size, 1.25vw + 1rem, $max-font-size);
+}
+
$min-font-size: 16px;
+$max-font-size: 24px;
+
+body {
+  font-size: clamp($min-font-size, 1.25vw + 1rem, $max-font-size);
+}
+
css
body {
+  font-size: clamp(16px, 1.25vw + 1rem, 24px);
+}
+
body {
+  font-size: clamp(16px, 1.25vw + 1rem, 24px);
+}
+

在上面的 CSS 代码中,我们希望通过视口宽度动态的调整 body 的字体大小。value 部分为 1.25vw + 1rem(这个计算会在浏览器环境中进行计算)。当视口宽度较小时,1.25vw + 1rem 计算结果可能是小于 16px,那么此时就取 16px。当视口宽度较大时,1.25vw + 1rem 计算结果可能大于 24px,那么此时就取 24px。如果 1.25vw + 1rem 计算值在 16px - 24px 之间,那么就取计算值结果。

Sass 控制指令

前面我们说了 CSS 预处理器最大的特点就是将编程语言的特性融入到了 CSS 里面,因此既然 CSS 预处理器里面都有变量、数据类型,自然而然也会有流程控制。

三元运算符

三元运算符

scss
if(expression, value1, value2)
+
if(expression, value1, value2)
+

示例如下:

scss
p {
+  color: if(1+1==2, green, yellow);
+}
+
+div {
+  color: if(1+1==3, green, yellow);
+}
+
p {
+  color: if(1+1==2, green, yellow);
+}
+
+div {
+  color: if(1+1==3, green, yellow);
+}
+
css
p {
+  color: green;
+}
+
+div {
+  color: yellow;
+}
+
p {
+  color: green;
+}
+
+div {
+  color: yellow;
+}
+

@if 分支

这个表示是分支。分支又分为三种:

  • 单分支
  • 双分支
  • 多分支

单分支

scss
p {
+  @if 1+1 == 2 {
+    color: red;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  }
+  margin: 10px;
+}
+
p {
+  @if 1+1 == 2 {
+    color: red;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  }
+  margin: 10px;
+}
+
css
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  margin: 10px;
+}
+
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  margin: 10px;
+}
+

双分支

仍然使用的是 @else

scss
p {
+  @if 1+1 == 2 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
p {
+  @if 1+1 == 2 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
+div {
+  @if 1+1 == 3 {
+    color: red;
+  } @else {
+    color: blue;
+  }
+  margin: 10px;
+}
+
css
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  color: blue;
+  margin: 10px;
+}
+
p {
+  color: red;
+  margin: 10px;
+}
+
+div {
+  color: blue;
+  margin: 10px;
+}
+

多分支

使用 @else if 来写多分支

scss
$type: monster;
+p {
+  @if $type == ocean {
+    color: blue;
+  } @else if $type == matador {
+    color: red;
+  } @else if $type == monster {
+    color: green;
+  } @else {
+    color: black;
+  }
+}
+
$type: monster;
+p {
+  @if $type == ocean {
+    color: blue;
+  } @else if $type == matador {
+    color: red;
+  } @else if $type == monster {
+    color: green;
+  } @else {
+    color: black;
+  }
+}
+
css
p {
+  color: green;
+}
+
p {
+  color: green;
+}
+

@for 循环

语法如下:

scss
@for $var
+  from <start>
+  through <end>
+  #会包含
+  end 结束值
+  // 或者
+  @for
+  $var
+  from <start>
+  to
+  <end>
+  #to
+  不会包含
+  end 结束值
+;
+
@for $var
+  from <start>
+  through <end>
+  #会包含
+  end 结束值
+  // 或者
+  @for
+  $var
+  from <start>
+  to
+  <end>
+  #to
+  不会包含
+  end 结束值
+;
+
scss
@for $i from 1 to 3 {
+  .item-#{$i} {
+    width: $i * 2em;
+  }
+}
+
+@for $i from 1 through 3 {
+  .item2-#{$i} {
+    width: $i * 2em;
+  }
+}
+
@for $i from 1 to 3 {
+  .item-#{$i} {
+    width: $i * 2em;
+  }
+}
+
+@for $i from 1 through 3 {
+  .item2-#{$i} {
+    width: $i * 2em;
+  }
+}
+
css
.item-1 {
+  width: 2em;
+}
+
+.item-2 {
+  width: 4em;
+}
+
+.item2-1 {
+  width: 2em;
+}
+
+.item2-2 {
+  width: 4em;
+}
+
+.item2-3 {
+  width: 6em;
+}
+
.item-1 {
+  width: 2em;
+}
+
+.item-2 {
+  width: 4em;
+}
+
+.item2-1 {
+  width: 2em;
+}
+
+.item2-2 {
+  width: 4em;
+}
+
+.item2-3 {
+  width: 6em;
+}
+

@while 循环

语法如下:

scss
@while expression ;
+
@while expression ;
+
scss
$i: 6;
+@while $i > 0 {
+  .item-#{$i} {
+    width: 2em * $i;
+  }
+  $i: $i - 2;
+}
+
$i: 6;
+@while $i > 0 {
+  .item-#{$i} {
+    width: 2em * $i;
+  }
+  $i: $i - 2;
+}
+
css
.item-6 {
+  width: 12em;
+}
+
+.item-4 {
+  width: 8em;
+}
+
+.item-2 {
+  width: 4em;
+}
+
.item-6 {
+  width: 12em;
+}
+
+.item-4 {
+  width: 8em;
+}
+
+.item-2 {
+  width: 4em;
+}
+

注意,一定要要在 while 里面书写改变变量的表达式,否则就会形成一个死循环。

@each 循环

这个优有点类似于 JS 里面的 for...of 循环,会把数组或者字典类型的每一项值挨着挨着取出来

scss
@each $var in $vars ;
+
@each $var in $vars ;
+

$var 可以是任意的变量名,但是 $vars 只能是 list 或者 maps

下面是一个遍历列表(数组)

scss
$arr: puma, sea-slug, egret, salamander;
+
+@each $animal in $arr {
+  .#{$animal}-icon {
+    background-image: url("/images/#{$animal}.png");
+  }
+}
+
$arr: puma, sea-slug, egret, salamander;
+
+@each $animal in $arr {
+  .#{$animal}-icon {
+    background-image: url("/images/#{$animal}.png");
+  }
+}
+
css
.puma-icon {
+  background-image: url("/images/puma.png");
+}
+
+.sea-slug-icon {
+  background-image: url("/images/sea-slug.png");
+}
+
+.egret-icon {
+  background-image: url("/images/egret.png");
+}
+
+.salamander-icon {
+  background-image: url("/images/salamander.png");
+}
+
.puma-icon {
+  background-image: url("/images/puma.png");
+}
+
+.sea-slug-icon {
+  background-image: url("/images/sea-slug.png");
+}
+
+.egret-icon {
+  background-image: url("/images/egret.png");
+}
+
+.salamander-icon {
+  background-image: url("/images/salamander.png");
+}
+

下面是一个遍历字典类型的示例:

scss
$dict: (
+  h1: 2em,
+  h2: 1.5em,
+  h3: 1.2em,
+  h4: 1em,
+);
+
+@each $header, $size in $dict {
+  #{$header} {
+    font-size: $size;
+  }
+}
+
$dict: (
+  h1: 2em,
+  h2: 1.5em,
+  h3: 1.2em,
+  h4: 1em,
+);
+
+@each $header, $size in $dict {
+  #{$header} {
+    font-size: $size;
+  }
+}
+
css
h1 {
+  font-size: 2em;
+}
+
+h2 {
+  font-size: 1.5em;
+}
+
+h3 {
+  font-size: 1.2em;
+}
+
+h4 {
+  font-size: 1em;
+}
+
h1 {
+  font-size: 2em;
+}
+
+h2 {
+  font-size: 1.5em;
+}
+
+h3 {
+  font-size: 1.2em;
+}
+
+h4 {
+  font-size: 1em;
+}
+

Sass 混合指令

在 Sass 里面存在一种叫做混合指令的东西,它最大的特点就是允许我们创建可以重用的代码片段,从而避免了代码的重复,提供代码的可维护性。

混合指令基本的使用

首先要使用混合指令,我们需要先定义一个混合指令:

scss
@mixin name {
+  // 样式。。。。
+}
+
@mixin name {
+  // 样式。。。。
+}
+

例如:

scss
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+

接下来是如何使用指令,需要使用到 @include,后面跟上混合指令的名称即可。

scss
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+
+p {
+  @include large-text;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  @include large-text;
+}
+
// 创建了一个指令
+@mixin large-text {
+  font: {
+    family: "Open Sans", sans-serif;
+    size: 20px;
+    weight: bold;
+  }
+  color: #ff0000;
+}
+
+p {
+  @include large-text;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  @include large-text;
+}
+
css
p {
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+}
+
p {
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+  padding: 20px;
+}
+
+div {
+  width: 200px;
+  height: 200px;
+  background-color: #fff;
+  font-family: "Open Sans", sans-serif;
+  font-size: 20px;
+  font-weight: bold;
+  color: #ff0000;
+}
+

我们发现,混合指令编译之后,就是将混合指令内部的 CSS 样式放入到了 @include 的地方,因此我们可以很明显的感受到混合指令就是提取公共的样式出来,方便复用和维护。

在混合指令中,我们也可以引用其他的混合指令:

scss
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  @include background;
+  @include header-text;
+}
+
+p {
+  @include compound;
+}
+
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  @include background;
+  @include header-text;
+}
+
+p {
+  @include compound;
+}
+
css
p {
+  background-color: #fc0;
+  font-size: 20px;
+}
+
p {
+  background-color: #fc0;
+  font-size: 20px;
+}
+

混合指令是可以直接在最外层使用的,但是对混合指令本身有一些要求。要求混合指令的内部要有选择器。

scss
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  div {
+    @include background;
+    @include header-text;
+  }
+}
+
+@include compound;
+
@mixin background {
+  background-color: #fc0;
+}
+
+@mixin header-text {
+  font-size: 20px;
+}
+
+@mixin compound {
+  div {
+    @include background;
+    @include header-text;
+  }
+}
+
+@include compound;
+
css
div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+
div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+

例如在上面的示例中,compound 混合指令里面不再是单纯的属性声明,而是有选择器在里面,这样的话就可以直接在最外层使用。

一般来讲,我们要在外部直接使用,我们一般会将其作为一个后代选择器。例如:

scss
.box {
+  @include compound;
+}
+
.box {
+  @include compound;
+}
+
css
.box div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+
.box div {
+  background-color: #fc0;
+  font-size: 20px;
+}
+

混合指令的参数

混合指令能够设置参数,只需要在混合指令的后面添加一对圆括号即可,括号里面书写对应的形参。

例如:

scss
@mixin bg-color($color, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(red, 10px);
+}
+
+.box2 {
+  @include bg-color(blue, 20px);
+}
+
@mixin bg-color($color, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(red, 10px);
+}
+
+.box2 {
+  @include bg-color(blue, 20px);
+}
+
css
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: red;
+  border-radius: 10px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: red;
+  border-radius: 10px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+

既然在定义混合指令的时候,指定了参数,那么在使用的时候,传递的参数的个数一定要和形参一致,否则编译会出错。

在定义的时候,支持给形参添加默认值,例如:

scss
@mixin bg-color($color, $radius: 20px) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(blue);
+}
+
@mixin bg-color($color, $radius: 20px) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color(blue);
+}
+

上面的示例是可以通过编译的,因为在定义 bg-color 的时候,我们为 $radius 设置了默认值。所以在使用的时候,即便没有传递第二个参数,也是 OK 的,因为会直接使用默认值。

在传递参数的时候,还支持关键词传参,就是指定哪一个参数是对应的哪一个形参,例如:

scss
@mixin bg-color($color: blue, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color($radius: 20px, $color: pink);
+}
+
+.box2 {
+  @include bg-color($radius: 20px);
+}
+
@mixin bg-color($color: blue, $radius) {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: $color;
+  border-radius: $radius;
+}
+
+.box1 {
+  @include bg-color($radius: 20px, $color: pink);
+}
+
+.box2 {
+  @include bg-color($radius: 20px);
+}
+
css
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: pink;
+  border-radius: 20px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+
.box1 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: pink;
+  border-radius: 20px;
+}
+
+.box2 {
+  width: 200px;
+  height: 200px;
+  margin: 10px;
+  background-color: blue;
+  border-radius: 20px;
+}
+

在定义混合指令的时候,如果不确定使用混合指令的地方会传入多少个参数,可以使用 ... 来声明(类似于 js 里面的不定参数),Sass 会把这些值当作一个列表来进行处理。

scss
@mixin box-shadow($shadow...) {
+  // ...
+  box-shadow: $shadow;
+}
+
+.box1 {
+  @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
+}
+
+.box2 {
+  @include box-shadow(
+    0 1px 2px rgba(0, 0, 0, 0.5),
+    0 2px 5px rgba(100, 0, 0, 0.5)
+  );
+}
+
@mixin box-shadow($shadow...) {
+  // ...
+  box-shadow: $shadow;
+}
+
+.box1 {
+  @include box-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
+}
+
+.box2 {
+  @include box-shadow(
+    0 1px 2px rgba(0, 0, 0, 0.5),
+    0 2px 5px rgba(100, 0, 0, 0.5)
+  );
+}
+
css
.box1 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+.box2 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 2px 5px rgba(100, 0, 0, 0.5);
+}
+
.box1 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+.box2 {
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 2px 5px rgba(100, 0, 0, 0.5);
+}
+

在 Sass 中,... 有些时候也可以表示为参数展开的含义,例如:

scss
@mixin colors($text, $background, $border) {
+  color: $text;
+  background-color: $background;
+  border-color: $border;
+}
+
+$values: red, blue, pink;
+
+.box {
+  @include colors($values...);
+}
+
@mixin colors($text, $background, $border) {
+  color: $text;
+  background-color: $background;
+  border-color: $border;
+}
+
+$values: red, blue, pink;
+
+.box {
+  @include colors($values...);
+}
+

@content

@content 表示占位的意思,在使用混合指令的时候,会将指令大括号里面的内容放置到 @content 的位置,有点类似于插槽。

scss
@mixin test {
+  html {
+    @content;
+  }
+}
+
+@include test {
+  background-color: red;
+  .logo {
+    width: 600px;
+  }
+}
+
+@include test {
+  color: blue;
+  .box {
+    width: 200px;
+    height: 200px;
+  }
+}
+
@mixin test {
+  html {
+    @content;
+  }
+}
+
+@include test {
+  background-color: red;
+  .logo {
+    width: 600px;
+  }
+}
+
+@include test {
+  color: blue;
+  .box {
+    width: 200px;
+    height: 200px;
+  }
+}
+
css
html {
+  background-color: red;
+}
+html .logo {
+  width: 600px;
+}
+
+html {
+  color: blue;
+}
+html .box {
+  width: 200px;
+  height: 200px;
+}
+
html {
+  background-color: red;
+}
+html .logo {
+  width: 600px;
+}
+
+html {
+  color: blue;
+}
+html .box {
+  width: 200px;
+  height: 200px;
+}
+

下面是一个实际开发中的例子:

scss
@mixin button-theme($color) {
+  background-color: $color;
+  border: 1px solid darken($color, 15%);
+
+  &:hover {
+    background-color: lighten($color, 5%);
+    border-color: darken($color, 10%);
+  }
+
+  @content;
+}
+
+.button-primary {
+  @include button-theme(#007bff) {
+    width: 500px;
+    height: 400px;
+  }
+}
+
+.button-secondary {
+  @include button-theme(#6c757d) {
+    width: 300px;
+    height: 200px;
+  }
+}
+
@mixin button-theme($color) {
+  background-color: $color;
+  border: 1px solid darken($color, 15%);
+
+  &:hover {
+    background-color: lighten($color, 5%);
+    border-color: darken($color, 10%);
+  }
+
+  @content;
+}
+
+.button-primary {
+  @include button-theme(#007bff) {
+    width: 500px;
+    height: 400px;
+  }
+}
+
+.button-secondary {
+  @include button-theme(#6c757d) {
+    width: 300px;
+    height: 200px;
+  }
+}
+
css
.button-primary {
+  background-color: #007bff;
+  border: 1px solid #0056b3;
+  width: 500px;
+  height: 400px;
+}
+.button-primary:hover {
+  background-color: #1a88ff;
+  border-color: #0062cc;
+}
+
+.button-secondary {
+  background-color: #6c757d;
+  border: 1px solid #494f54;
+  width: 300px;
+  height: 200px;
+}
+.button-secondary:hover {
+  background-color: #78828a;
+  border-color: #545b62;
+}
+
.button-primary {
+  background-color: #007bff;
+  border: 1px solid #0056b3;
+  width: 500px;
+  height: 400px;
+}
+.button-primary:hover {
+  background-color: #1a88ff;
+  border-color: #0062cc;
+}
+
+.button-secondary {
+  background-color: #6c757d;
+  border: 1px solid #494f54;
+  width: 300px;
+  height: 200px;
+}
+.button-secondary:hover {
+  background-color: #78828a;
+  border-color: #545b62;
+}
+

最后我们需要说一先关于 @content 的作用域的问题。

在混合指令的局部作用域里面所定义的变量不会影响 @content 代码块中的变量,同样,在 @content 代码块中定义的变量不会影响到混合指令中的其他变量,两者之间的作用域是隔离的

scss
@mixin scope-test {
+  $test-variable: "mixin";
+
+  .mixin {
+    content: $test-variable;
+  }
+
+  @content;
+}
+
+.test {
+  $test-variable: "test";
+  @include scope-test {
+    .content {
+      content: $test-variable;
+    }
+  }
+}
+
@mixin scope-test {
+  $test-variable: "mixin";
+
+  .mixin {
+    content: $test-variable;
+  }
+
+  @content;
+}
+
+.test {
+  $test-variable: "test";
+  @include scope-test {
+    .content {
+      content: $test-variable;
+    }
+  }
+}
+
css
.test .mixin {
+  content: "mixin";
+}
+.test .content {
+  content: "test";
+}
+
.test .mixin {
+  content: "mixin";
+}
+.test .content {
+  content: "test";
+}
+

Sass 函数指令

上一小节所学习的混合指令虽然有点像函数,但是它并不是函数,因为混合指令所做的事情就是单纯的代码的替换工作,里面并不存在任何的计算。在 Sass 里面是支持函数指令。

自定义函数

在 Sass 里面自定义函数的语法如下:

scss
@function fn-name($params...) {
+  @return XXX;
+}
+
@function fn-name($params...) {
+  @return XXX;
+}
+

具体示例如下:

scss
@function divide($a, $b) {
+  @return $a / $b;
+}
+
+.container {
+  width: divide(100px, 2);
+}
+
@function divide($a, $b) {
+  @return $a / $b;
+}
+
+.container {
+  width: divide(100px, 2);
+}
+
css
.container {
+  width: 50px;
+}
+
.container {
+  width: 50px;
+}
+

函数可以接收多个参数,如果不确定会传递几个参数,那么可以使用前面介绍过的不定参数的形式。

scss
@function sum($nums...) {
+  $sum: 0;
+  @each $n in $nums {
+    $sum: $sum + $n;
+  }
+  @return $sum;
+}
+
+.box1 {
+  width: sum(1, 2, 3) + px;
+}
+
+.box2 {
+  width: sum(1, 2, 3, 4, 5, 6) + px;
+}
+
@function sum($nums...) {
+  $sum: 0;
+  @each $n in $nums {
+    $sum: $sum + $n;
+  }
+  @return $sum;
+}
+
+.box1 {
+  width: sum(1, 2, 3) + px;
+}
+
+.box2 {
+  width: sum(1, 2, 3, 4, 5, 6) + px;
+}
+
css
.box1 {
+  width: 6px;
+}
+
+.box2 {
+  width: 21px;
+}
+
.box1 {
+  width: 6px;
+}
+
+.box2 {
+  width: 21px;
+}
+

最后我们还是来看一个实际开发中的示例:

scss
// 根据传入的 $background-color 返回适当的文字颜色
+@function contrast-color($background-color) {
+  // 计算背景颜色的亮度
+  $brightness: red($background-color) * 0.299 + green($background-color) *
+    0.587 + blue($background-color) * 0.114;
+
+  // 根据亮度来返回黑色或者白色的文字颜色
+  @if $brightness > 128 {
+    @return #000;
+  } @else {
+    @return #fff;
+  }
+}
+
+.button {
+  $background-color: #007bff;
+  background-color: $background-color;
+  color: contrast-color($background-color);
+}
+
// 根据传入的 $background-color 返回适当的文字颜色
+@function contrast-color($background-color) {
+  // 计算背景颜色的亮度
+  $brightness: red($background-color) * 0.299 + green($background-color) *
+    0.587 + blue($background-color) * 0.114;
+
+  // 根据亮度来返回黑色或者白色的文字颜色
+  @if $brightness > 128 {
+    @return #000;
+  } @else {
+    @return #fff;
+  }
+}
+
+.button {
+  $background-color: #007bff;
+  background-color: $background-color;
+  color: contrast-color($background-color);
+}
+

在上面的代码示例中,我们首先定义了一个名为 contrast-color 的函数,该函数接收一个背景颜色参数,函数内部会根据这个背景颜色来决定文字应该是白色还是黑色。

css
.button {
+  background-color: #007bff;
+  color: #fff;
+}
+
.button {
+  background-color: #007bff;
+  color: #fff;
+}
+

内置函数

除了自定义函数,Sass 里面还提供了非常多的内置函数,你可以在官方文档:

https://sass-lang.com/documentation/modules

字符串相关内置函数

函数名和参数类型函数作用
quote($string)添加引号
unquote($string)除去引号
to-lower-case($string)变为小写
to-upper-case($string)变为大写
str-length($string)返回$string 的长度(汉字算一个)
str-index($string,$substring)返回$substring在$string 的位置
str-insert($string, $insert, $index)在$string的$index 处插入$insert
str-slice($string, $start-at, $end-at)截取$string的$start-at 和$end-at 之间的字符串

注意索引是从 1 开始的,如果书写 -1,那么就是倒着来的。两边都是闭区间

scss
$str: "Hello world!";
+
+.slice1 {
+  content: str-slice($str, 1, 5);
+}
+
+.slice2 {
+  content: str-slice($str, -1);
+}
+
$str: "Hello world!";
+
+.slice1 {
+  content: str-slice($str, 1, 5);
+}
+
+.slice2 {
+  content: str-slice($str, -1);
+}
+
css
.slice1 {
+  content: "Hello";
+}
+
+.slice2 {
+  content: "!";
+}
+
.slice1 {
+  content: "Hello";
+}
+
+.slice2 {
+  content: "!";
+}
+

数字相关内置函数

函数名和参数类型函数作用
percentage($number)转换为百分比形式
round($number)四舍五入为整数
ceil($number)数值向上取整
floor($number)数值向下取整
abs($number)获取绝对值
min($number...)获取最小值
max($number...)获取最大值
random($number?:number)不传入值:获得 0-1 的随机数;传入正整数 n:获得 0-n 的随机整数(左开右闭)
scss
.item {
+  width: percentage(2/5);
+  height: random(100) + px;
+  color: rgb(random(255), random(255), random(255));
+}
+
.item {
+  width: percentage(2/5);
+  height: random(100) + px;
+  color: rgb(random(255), random(255), random(255));
+}
+
css
.item {
+  width: 40%;
+  height: 83px;
+  color: rgb(31, 86, 159);
+}
+
.item {
+  width: 40%;
+  height: 83px;
+  color: rgb(31, 86, 159);
+}
+

数组相关内置函数

函数名和参数类型函数作用
length($list)获取数组长度
nth($list, n)获取指定下标的元素
set-nth($list, $n, $value)向$list的$n 处插入$value
join($list1, $list2, $separator)拼接$list1和list2;$separator 为新 list 的分隔符,默认为 auto,可选择 comma、space
append($list, $val, $separator)向$list的末尾添加$val;$separator 为新 list 的分隔符,默认为 auto,可选择 comma、space
index($list, $value)返回$value值在$list 中的索引值
zip($lists…)将几个列表结合成一个多维的列表;要求每个的列表个数值必须是相同的

下面是一个具体的示例:

scss
// 演示的是 join 方法
+$list1: 1px solid, 2px dotted;
+$list2: 3px dashed, 4px double;
+$combined-list: join($list1, $list2, comma);
+
+// 演示的是 append 方法
+$base-colors: red, green, blue;
+$extended-colors: append($base-colors, yellow, comma);
+
+// 演示 zip 方法
+$fonts: "Arial", "Helvetica", "Verdana";
+$weights: "normal", "bold", "italic";
+// $font-pair: ("Arial", "normal"),("Helvetica", "bold"),("Verdana","italic")
+$font-pair: zip($fonts, $weights);
+
+// 接下来我们来生成一下具体的样式
+@each $border-style in $combined-list {
+  .border-#{index($combined-list, $border-style)} {
+    border: $border-style;
+  }
+}
+
+@each $color in $extended-colors {
+  .bg-#{index($extended-colors, $color)} {
+    background-color: $color;
+  }
+}
+
+@each $pair in $font-pair {
+  $font: nth($pair, 1);
+  $weight: nth($pair, 2);
+  .text-#{index($font-pair, $pair)} {
+    font-family: $font;
+    font-weight: $weight;
+  }
+}
+
// 演示的是 join 方法
+$list1: 1px solid, 2px dotted;
+$list2: 3px dashed, 4px double;
+$combined-list: join($list1, $list2, comma);
+
+// 演示的是 append 方法
+$base-colors: red, green, blue;
+$extended-colors: append($base-colors, yellow, comma);
+
+// 演示 zip 方法
+$fonts: "Arial", "Helvetica", "Verdana";
+$weights: "normal", "bold", "italic";
+// $font-pair: ("Arial", "normal"),("Helvetica", "bold"),("Verdana","italic")
+$font-pair: zip($fonts, $weights);
+
+// 接下来我们来生成一下具体的样式
+@each $border-style in $combined-list {
+  .border-#{index($combined-list, $border-style)} {
+    border: $border-style;
+  }
+}
+
+@each $color in $extended-colors {
+  .bg-#{index($extended-colors, $color)} {
+    background-color: $color;
+  }
+}
+
+@each $pair in $font-pair {
+  $font: nth($pair, 1);
+  $weight: nth($pair, 2);
+  .text-#{index($font-pair, $pair)} {
+    font-family: $font;
+    font-weight: $weight;
+  }
+}
+
css
.border-1 {
+  border: 1px solid;
+}
+
+.border-2 {
+  border: 2px dotted;
+}
+
+.border-3 {
+  border: 3px dashed;
+}
+
+.border-4 {
+  border: 4px double;
+}
+
+.bg-1 {
+  background-color: red;
+}
+
+.bg-2 {
+  background-color: green;
+}
+
+.bg-3 {
+  background-color: blue;
+}
+
+.bg-4 {
+  background-color: yellow;
+}
+
+.text-1 {
+  font-family: "Arial";
+  font-weight: "normal";
+}
+
+.text-2 {
+  font-family: "Helvetica";
+  font-weight: "bold";
+}
+
+.text-3 {
+  font-family: "Verdana";
+  font-weight: "italic";
+}
+
.border-1 {
+  border: 1px solid;
+}
+
+.border-2 {
+  border: 2px dotted;
+}
+
+.border-3 {
+  border: 3px dashed;
+}
+
+.border-4 {
+  border: 4px double;
+}
+
+.bg-1 {
+  background-color: red;
+}
+
+.bg-2 {
+  background-color: green;
+}
+
+.bg-3 {
+  background-color: blue;
+}
+
+.bg-4 {
+  background-color: yellow;
+}
+
+.text-1 {
+  font-family: "Arial";
+  font-weight: "normal";
+}
+
+.text-2 {
+  font-family: "Helvetica";
+  font-weight: "bold";
+}
+
+.text-3 {
+  font-family: "Verdana";
+  font-weight: "italic";
+}
+

字典相关内置函数

函数名和参数类型函数作用
map-get($map, $key)获取$map中$key 对应的$value
map-merge($map1, $map2)合并$map1和$map2,返回一个新$map
map-remove($map, $key)从$map中删除$key,返回一个新$map
map-keys($map)返回$map所有的$key
map-values($map)返回$map所有的$value
map-has-key($map, $key)判断$map中是否存在$key,返回对应的布尔值
keywords($args)返回一个函数的参数,并可以动态修改其值

下面是一个使用了字典内置方法的相关示例:

scss
// 创建一个颜色映射表
+$colors: (
+  "primary": #007bff,
+  "secondary": #6c757d,
+  "success": #28a745,
+  "info": #17a2b8,
+  "warning": #ffc107,
+  "danger": #dc3545,
+);
+
+// 演示通过 map-get 获取对应的值
+@function btn-color($color-name) {
+  @return map-get($colors, $color-name);
+}
+
+// 演示通过 map-keys 获取映射表所有的 keys
+$color-keys: map-keys($colors);
+
+// 一个新的颜色映射表
+$more-colors: (
+  "light": #f8f9fa,
+  "dark": #343a40,
+);
+
+// 要将新的颜色映射表合并到 $colors 里面
+
+$all-colors: map-merge($colors, $more-colors);
+
+// 接下来我们来根据颜色映射表生成样式
+@each $color-key, $color-value in $all-colors {
+  .text-#{$color-key} {
+    color: $color-value;
+  }
+}
+
+button {
+  color: btn-color("primary");
+}
+
// 创建一个颜色映射表
+$colors: (
+  "primary": #007bff,
+  "secondary": #6c757d,
+  "success": #28a745,
+  "info": #17a2b8,
+  "warning": #ffc107,
+  "danger": #dc3545,
+);
+
+// 演示通过 map-get 获取对应的值
+@function btn-color($color-name) {
+  @return map-get($colors, $color-name);
+}
+
+// 演示通过 map-keys 获取映射表所有的 keys
+$color-keys: map-keys($colors);
+
+// 一个新的颜色映射表
+$more-colors: (
+  "light": #f8f9fa,
+  "dark": #343a40,
+);
+
+// 要将新的颜色映射表合并到 $colors 里面
+
+$all-colors: map-merge($colors, $more-colors);
+
+// 接下来我们来根据颜色映射表生成样式
+@each $color-key, $color-value in $all-colors {
+  .text-#{$color-key} {
+    color: $color-value;
+  }
+}
+
+button {
+  color: btn-color("primary");
+}
+
css
.text-primary {
+  color: #007bff;
+}
+
+.text-secondary {
+  color: #6c757d;
+}
+
+.text-success {
+  color: #28a745;
+}
+
+.text-info {
+  color: #17a2b8;
+}
+
+.text-warning {
+  color: #ffc107;
+}
+
+.text-danger {
+  color: #dc3545;
+}
+
+.text-light {
+  color: #f8f9fa;
+}
+
+.text-dark {
+  color: #343a40;
+}
+
+button {
+  color: #007bff;
+}
+
.text-primary {
+  color: #007bff;
+}
+
+.text-secondary {
+  color: #6c757d;
+}
+
+.text-success {
+  color: #28a745;
+}
+
+.text-info {
+  color: #17a2b8;
+}
+
+.text-warning {
+  color: #ffc107;
+}
+
+.text-danger {
+  color: #dc3545;
+}
+
+.text-light {
+  color: #f8f9fa;
+}
+
+.text-dark {
+  color: #343a40;
+}
+
+button {
+  color: #007bff;
+}
+

颜色相关内置函数

RGB 函数

函数名和参数类型函数作用
rgb($red, $green, $blue)返回一个 16 进制颜色值
rgba($red,$green,$blue,$alpha)返回一个 rgba;$red,$green 和$blue 可被当作一个整体以颜色单词、hsl、rgb 或 16 进制形式传入
red($color)从$color 中获取其中红色值
green($color)从$color 中获取其中绿色值
blue($color)从$color 中获取其中蓝色值
mix($color1,$color2,$weight?)按照$weight比例,将$color1 和$color2 混合为一个新颜色

HSL 函数

函数名和参数类型函数作用
hsl($hue,$saturation,$lightness)通过色相(hue)、饱和度(saturation)和亮度(lightness)的值创建一个颜色
hsla($hue,$saturation,$lightness,$alpha)通过色相(hue)、饱和度(saturation)、亮度(lightness)和透明(alpha)的值创建一个颜色
saturation($color)从一个颜色中获取饱和度(saturation)值
lightness($color)从一个颜色中获取亮度(lightness)值
adjust-hue($color,$degrees)通过改变一个颜色的色相值,创建一个新的颜色
lighten($color,$amount)通过改变颜色的亮度值,让颜色变亮,创建一个新的颜色
darken($color,$amount)通过改变颜色的亮度值,让颜色变暗,创建一个新的颜色
hue($color)从一个颜色中获取亮度色相(hue)值

Opacity 函数

函数名和参数类型函数作用
alpha($color)/opacity($color)获取颜色透明度值
rgba($color,$alpha)改变颜色的透明度
opacify($color, $amount) / fade-in($color, $amount)使颜色更不透明
transparentize($color, $amount) / fade-out($color, $amount)使颜色更加透明

其他内置函数

函数名和参数类型函数作用
type-of($value)返回$value 的类型
unit($number)返回$number 的单位
unitless($number)判断$number 是否没用带单位,返回对应的布尔值,没有带单位为 true
comparable($number1, $number2)判断$number1和$number2 是否可以做加、减和合并,返回对应的布尔值

示例如下:

scss
$value: 42;
+
+$value-type: type-of($value); // number
+
+$length: 10px;
+$length-unit: unit($length); // "px"
+
+$is-unitless: unitless(42); // true
+
+$can-compare: comparable(1px, 2em); // false
+$can-compare2: comparable(1px, 2px); // true
+
+// 根据 type-of 函数的结果生成样式
+.box {
+  content: "Value type: #{$value-type}";
+}
+
+// 根据 unit 函数的结果生成样式
+.length-label {
+  content: "Length unit: #{$length-unit}";
+}
+
+// 根据 unitless 函数的结果生成样式
+.unitless-label {
+  content: "Is unitless: #{$is-unitless}";
+}
+
+// 根据 comparable 函数的结果生成样式
+.comparable-label {
+  content: "Can compare: #{$can-compare}";
+}
+
+.comparable-label2 {
+  content: "Can compare: #{$can-compare2}";
+}
+
$value: 42;
+
+$value-type: type-of($value); // number
+
+$length: 10px;
+$length-unit: unit($length); // "px"
+
+$is-unitless: unitless(42); // true
+
+$can-compare: comparable(1px, 2em); // false
+$can-compare2: comparable(1px, 2px); // true
+
+// 根据 type-of 函数的结果生成样式
+.box {
+  content: "Value type: #{$value-type}";
+}
+
+// 根据 unit 函数的结果生成样式
+.length-label {
+  content: "Length unit: #{$length-unit}";
+}
+
+// 根据 unitless 函数的结果生成样式
+.unitless-label {
+  content: "Is unitless: #{$is-unitless}";
+}
+
+// 根据 comparable 函数的结果生成样式
+.comparable-label {
+  content: "Can compare: #{$can-compare}";
+}
+
+.comparable-label2 {
+  content: "Can compare: #{$can-compare2}";
+}
+
css
.box {
+  content: "Value type: number";
+}
+
+.length-label {
+  content: "Length unit: px";
+}
+
+.unitless-label {
+  content: "Is unitless: true";
+}
+
+.comparable-label {
+  content: "Can compare: false";
+}
+
+.comparable-label2 {
+  content: "Can compare: true";
+}
+
.box {
+  content: "Value type: number";
+}
+
+.length-label {
+  content: "Length unit: px";
+}
+
+.unitless-label {
+  content: "Is unitless: true";
+}
+
+.comparable-label {
+  content: "Can compare: false";
+}
+
+.comparable-label2 {
+  content: "Can compare: true";
+}
+

@规则

在原生 CSS 中,存在一些 @ 开头的规则,例如 @import、@media,Sass 对这些 @ 规则完全支持,不仅支持,还在原有的基础上做了一些扩展。

@import

在原生的 CSS 里面,@import 是导入其他的 CSS 文件,Sass 再此基础上做了一些增强:

  1. 编译时合并:Sass 在编译时将导入的文件内容合并到生成的 CSS 文件中,这意味着只会生成一个 CSS 文件,而不是像原生 CSS 那样需要额外的 HTTP 请求去加载导入的文件。

  2. 变量、函数和混合体共享:Sass 允许在导入的文件之间共享变量、函数和混合体,这有助于组织代码并避免重复。

  3. 部分文件:Sass 支持将文件名前缀为 _ 的文件称为部分文件(partials)。当使用 @import 指令导入部分文件时,Sass 不会生成一个单独的 CSS 文件,而是将其内容合并到主文件中。这有助于更好地组织项目。

  4. 文件扩展名可选:在 Sass 中,使用 @import 指令时可以省略文件扩展名(.scss 或 .sass),Sass 会自动识别并导入正确的文件。

  5. 嵌套导入:Sass 允许在一个文件中嵌套导入其他文件,但请注意,嵌套导入的文件将在父级上下文中编译,这可能会导致输出的 CSS 文件中的选择器层级不符合预期。

接下来,我们来看一个具体的例子:

src/
+  ├── _variable.scss
+  ├── _mixins.scss
+  ├── _header.scss
+  └── index.scss
+
src/
+  ├── _variable.scss
+  ├── _mixins.scss
+  ├── _header.scss
+  └── index.scss
+
scss
// _variable.scss
+$primary-color: #007bff;
+$secondary-color: #6c757d;
+
// _variable.scss
+$primary-color: #007bff;
+$secondary-color: #6c757d;
+
scss
// _mixins.scss
+@mixin reset-margin-padding {
+  margin: 0;
+  padding: 0;
+}
+
// _mixins.scss
+@mixin reset-margin-padding {
+  margin: 0;
+  padding: 0;
+}
+
scss
// _header.scss
+header {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+
// _header.scss
+header {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+

可以看出,在 _header.scss 里面使用了另外两个 scss 所定义的变量以及混合体,说明变量、函数和混合体是可以共享的。

之后我们在 index.scss 里面导入了这三个 scss

scss
@import "variable";
+@import "mixins";
+@import "header";
+
+body {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+
@import "variable";
+@import "mixins";
+@import "header";
+
+body {
+  background-color: $primary-color;
+  color: $secondary-color;
+  @include reset-margin-padding;
+}
+

最终生成的 css 如下:

css
header {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+
header {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  background-color: #007bff;
+  color: #6c757d;
+  margin: 0;
+  padding: 0;
+}
+

最终只会生成一个 css。

通常情况下,我们在通过 @import 导入文件的时候,不给后缀名,会自动的寻找 sass 文件并将其导入。但是有一些情况下,会编译为普通的 CSS 语句,并不会导入任何文件:

  • 文件拓展名是 .css
  • 文件名以 http😕/ 开头;
  • 文件名是 url();
  • @import 包含 media queries

例如:

scss
@import "foo.css" @import "foo" screen;
+@import "http://foo.com/bar";
+@import url(foo);
+
@import "foo.css" @import "foo" screen;
+@import "http://foo.com/bar";
+@import url(foo);
+

@media

这个规则在原生 CSS 里面是做媒体查询,Sass 里面是完全支持的,并且做了一些增强操作。

  1. Sass 里面允许你讲 @media 嵌套在选择器内部
scss
.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: 768px) {
+    flex-direction: column;
+  }
+}
+
.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: 768px) {
+    flex-direction: column;
+  }
+}
+
css
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
  1. 允许使用变量
scss
$mobile-breakpoint: 768px;
+
+.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: $mobile-breakpoint) {
+    flex-direction: column;
+  }
+}
+
$mobile-breakpoint: 768px;
+
+.navigation {
+  display: flex;
+  justify-content: flex-end;
+
+  @media (max-width: $mobile-breakpoint) {
+    flex-direction: column;
+  }
+}
+
css
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
.navigation {
+  display: flex;
+  justify-content: flex-end;
+}
+@media (max-width: 768px) {
+  .navigation {
+    flex-direction: column;
+  }
+}
+
  1. 可以使用混合体
scss
@mixin respond-to($breakpoint) {
+  @if $breakpoint == "mobile" {
+    @media (max-width: 768px) {
+      @content;
+    }
+  } @else if $breakpoint == "tablet" {
+    @media (min-width: 769px) and (max-width: 1024px) {
+      @content;
+    }
+  } @else if $breakpoint == "desktop" {
+    @media (min-width: 1025px) {
+      @content;
+    }
+  }
+}
+
+.container {
+  width: 80%;
+  @include respond-to("mobile") {
+    width: 100%;
+  }
+  @include respond-to("desktop") {
+    width: 70%;
+  }
+}
+
@mixin respond-to($breakpoint) {
+  @if $breakpoint == "mobile" {
+    @media (max-width: 768px) {
+      @content;
+    }
+  } @else if $breakpoint == "tablet" {
+    @media (min-width: 769px) and (max-width: 1024px) {
+      @content;
+    }
+  } @else if $breakpoint == "desktop" {
+    @media (min-width: 1025px) {
+      @content;
+    }
+  }
+}
+
+.container {
+  width: 80%;
+  @include respond-to("mobile") {
+    width: 100%;
+  }
+  @include respond-to("desktop") {
+    width: 70%;
+  }
+}
+
css
.container {
+  width: 80%;
+}
+@media (max-width: 768px) {
+  .container {
+    width: 100%;
+  }
+}
+@media (min-width: 1025px) {
+  .container {
+    width: 70%;
+  }
+}
+
.container {
+  width: 80%;
+}
+@media (max-width: 768px) {
+  .container {
+    width: 100%;
+  }
+}
+@media (min-width: 1025px) {
+  .container {
+    width: 70%;
+  }
+}
+

@extend

我们在书写 CSS 样式的时候,经常会遇到一种情况:一个元素使用的样式和另外一个元素基本相同,但是又增加了一些额外的样式。这个时候就可以使用继承。Sass 里面提供了@extend 规则来实现继承,让一个选择器能够继承另外一个选择器的样式规则。

scss
.button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend .button;
+  background-color: blue;
+}
+
.button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend .button;
+  background-color: blue;
+}
+
css
.button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+
.button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+

如果是刚接触的同学,可能会觉得 @extend 和 @mixin 比较相似,感觉都是把公共的样式提取出来了,但是两者其实是不同的。

  • 参数支持:@mixin 支持传递参数,使其更具灵活性;而 @extend 不支持参数传递。
  • 生成的 CSS:@extend 会将选择器合并,生成更紧凑的 CSS,并且所继承的样式在最终生成的 CSS 样式中也是真实存在的;而 @mixin 会在每个 @include 处生成完整的 CSS 代码,做的就是一个简单的 CSS 替换。
  • 使用场景:@extend 更适用于继承已有样式的情况,例如 UI 框架中的通用样式;而 @mixin 更适用于需要自定义参数的情况,例如为不同组件生成类似的样式。

接下来我们来看一个复杂的例子:

scss
.box {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  @extend .box;
+  border-width: 3px;
+}
+
+.box.a {
+  background-image: url("/image/abc.png");
+}
+
.box {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  @extend .box;
+  border-width: 3px;
+}
+
+.box.a {
+  background-image: url("/image/abc.png");
+}
+
css
.box,
+.container {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  border-width: 3px;
+}
+
+.box.a,
+.a.container {
+  background-image: url("/image/abc.png");
+}
+
.box,
+.container {
+  border: 1px #f00;
+  background-color: #fdd;
+}
+
+.container {
+  border-width: 3px;
+}
+
+.box.a,
+.a.container {
+  background-image: url("/image/abc.png");
+}
+

在上面的代码中,container 是继承了 box 里面的所有样式,假设一个元素需要有 box 和 a 这两个类才能对应一段样式(abc),由于 box 类所对应的样式,如果是挂 container 这个类的话,这些样式也会有,所以一个元素如果挂了 container 和 a 这两个类,同样应该应用对应 abc 样式。

有些时候,我们需要定义一套用于继承的样式,不希望 Sass 单独编译输出,那么这种情况下就可以使用 % 作为占位符。

scss
%button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend %button;
+  background-color: blue;
+}
+
+.secondary-button {
+  @extend %button;
+  background-color: pink;
+}
+
%button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  @extend %button;
+  background-color: blue;
+}
+
+.secondary-button {
+  @extend %button;
+  background-color: pink;
+}
+
css
.secondary-button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+
+.secondary-button {
+  background-color: pink;
+}
+
.secondary-button,
+.primary-button {
+  display: inline-block;
+  padding: 20px;
+  background-color: red;
+  color: white;
+}
+
+.primary-button {
+  background-color: blue;
+}
+
+.secondary-button {
+  background-color: pink;
+}
+

@at-root

有些时候,我们可能会涉及到将嵌套规则移动到根级别(声明的时候并没有写在根级别)。这个时候就可以使用 @at-root

scss
.parent {
+  color: red;
+
+  @at-root .child {
+    color: blue;
+  }
+}
+
.parent {
+  color: red;
+
+  @at-root .child {
+    color: blue;
+  }
+}
+
css
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+

如果你想要移动的是一组规则,这个时候需要在 @at-root 后面添加一对大括号,将想要移动的这一组样式放入到大括号里面

scss
.parent {
+  color: red;
+
+  @at-root {
+    .child {
+      color: blue;
+    }
+    .test {
+      color: pink;
+    }
+    .test2 {
+      color: purple;
+    }
+  }
+}
+
.parent {
+  color: red;
+
+  @at-root {
+    .child {
+      color: blue;
+    }
+    .test {
+      color: pink;
+    }
+    .test2 {
+      color: purple;
+    }
+  }
+}
+
css
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+
+.test {
+  color: pink;
+}
+
+.test2 {
+  color: purple;
+}
+
.parent {
+  color: red;
+}
+.child {
+  color: blue;
+}
+
+.test {
+  color: pink;
+}
+
+.test2 {
+  color: purple;
+}
+

@debug、@warn、@error

这三个规则是和调试相关的,可以让我们在编译过程中输出一条信息,有助于调试和诊断代码中的问题。

Sass 最佳实践与展望

最佳实践

  1. 合理的组织你的文件和目录结构:可以将一个大的 Sass 文件拆分成一个一个小的模块,我们可以按照功能、页面、组件来划分我们的文件结构,这样的话更加利于我们的项目维护。
scss
sass/
+|
+|-- base/
+|   |-- _reset.scss  // 重置样式
+|   |-- _typography.scss  // 排版相关样式
+|   ...
+|
+|-- components/
+|   |-- _buttons.scss  // 按钮相关样式
+|   |-- _forms.scss  // 表单相关样式
+|   ...
+|
+|-- layout/
+|   |-- _header.scss  // 页眉相关样式
+|   |-- _footer.scss  // 页脚相关样式
+|   ...
+|
+|-- pages/
+|   |-- _home.scss  // 首页相关样式
+|   |-- _about.scss  // 关于页面相关样式
+|   ...
+|
+|-- utils/
+|   |-- _variables.scss  // 变量定义
+|   |-- _mixins.scss  // 混入定义
+|   |-- _functions.scss  // 函数定义
+|   ...
+|
+|-- main.scss // 主入口文件
+
sass/
+|
+|-- base/
+|   |-- _reset.scss  // 重置样式
+|   |-- _typography.scss  // 排版相关样式
+|   ...
+|
+|-- components/
+|   |-- _buttons.scss  // 按钮相关样式
+|   |-- _forms.scss  // 表单相关样式
+|   ...
+|
+|-- layout/
+|   |-- _header.scss  // 页眉相关样式
+|   |-- _footer.scss  // 页脚相关样式
+|   ...
+|
+|-- pages/
+|   |-- _home.scss  // 首页相关样式
+|   |-- _about.scss  // 关于页面相关样式
+|   ...
+|
+|-- utils/
+|   |-- _variables.scss  // 变量定义
+|   |-- _mixins.scss  // 混入定义
+|   |-- _functions.scss  // 函数定义
+|   ...
+|
+|-- main.scss // 主入口文件
+
  1. 多使用变量:在开发的时候经常会遇到一些会重复使用的值(颜色、字体、尺寸),我们就可以将这些值定义为变量,方便在项目中统一进行管理和修改。

  2. 关于嵌套:嵌套非常好用,但是要避免层数过多的嵌套,通常来讲不要超过 3 层

scss
.a {
+  .....
+  .b {
+    ....
+  }
+}
+
.a {
+  .....
+  .b {
+    ....
+  }
+}
+
css
.a {
+  ....;
+}
+.a .b {
+  ....;
+}
+
.a {
+  ....;
+}
+.a .b {
+  ....;
+}
+

如果嵌套层数过多:

scss
.a{
+  ...
+  .b{
+    ...
+    .c {
+      ...
+      .d {
+        ....
+        .e{
+          ....
+        }
+      }
+    }
+  }
+}
+
.a{
+  ...
+  .b{
+    ...
+    .c {
+      ...
+      .d {
+        ....
+        .e{
+          ....
+        }
+      }
+    }
+  }
+}
+
css
.a .b .c .d .e {
+  .....;
+}
+
.a .b .c .d .e {
+  .....;
+}
+
  1. 多使用混合指令:混合指令可以将公共的部分抽离出来,提高了代码的复用性。但是要清楚混合指令和 @extend 之间的区别,具体使用哪一个,取决于你写项目时的具体场景,不是说某一个就比另一个绝对的好。
  2. 使用函数:可以编写自定义函数来处理一些复杂的计算和操作。而且 Sass 还提供了很多非常好用的内置函数。
  3. 遵循常见的 Sass 编码规范:

Sass 未来发展

我们如果想要获取到 Sass 的最新动向,通常可以去 Sass 的社区看一下。

注意:一门成熟的技术,是一定会有对应社区的。理论上来讲,社区的形式是不限的,但是通常是以论坛的形式存在的,大家可以在论坛社区自由的讨论这门技术相关的话题。

社区往往包含了这门技术最新的动态,甚至有一些优秀的技术解决方案是先来自于社区,之后才慢慢成为正式的标准语法的。

目前市面上又很多 CSS 库都是基于 Sass 来进行构建了,例如:

  1. Compass - 老牌 Sass 框架,提供大量 Sass mixins 和函数,方便开发。
  2. Bourbon - 轻量级的 Sass mixin 库,提供常用的 mixins,简化 CSS 开发。
  3. Neat - 构建具有响应式网格布局的网站,基于 SassBourbon,容易上手。
  4. Materialize - 实现 Material Design 风格,基于 Sass 构建,提供丰富组件和元素。
  5. Bulma - 现代 CSS 框架,提供弹性网格和常见组件,可与 Sass 一起使用。
  6. Foundation - 老牌前端框架,基于 Sass,提供全面的组件和工具,适合构建复杂项目。
  7. Semantic UI - 设计美观的 UI 套件,基于 Sass 构建,提供丰富样式和交互。
  8. Spectre.css - 轻量级、响应式和现代的 CSS 框架,可以与 Sass 结合使用。

因此,基本上目前 Sass 已经成为了前端开发人员首选的 CSS 预处理器。因为 Sass 相比其他两个 CSS 预处理器,功能是最强大的,特性是最多的,社区也是最活跃的。

关于 Sass 官方团队,未来再对 Sass 进行更新的时候,基本上会往以下几个方面做出努力:

  • 性能优化
  • 持续的与现代 Web 技术的集成
  • 新功能的改进

本文所有源码均在 https://github.com/Sunny-117/blog/tree/main/code/sass-demo

+ + + + + \ No newline at end of file diff --git "a/front-end-engineering/CSS\345\267\245\347\250\213\345\214\226.html" "b/front-end-engineering/CSS\345\267\245\347\250\213\345\214\226.html" new file mode 100644 index 00000000..fc545daf --- /dev/null +++ "b/front-end-engineering/CSS\345\267\245\347\250\213\345\214\226.html" @@ -0,0 +1,684 @@ + + + + + + CSS 工程化 | Sunny's blog + + + + + + + + +
Skip to content
On this page

CSS 工程化

css 的问题

类名冲突的问题

当你写一个 css 类的时候,你是写全局的类呢,还是写多个层级选择后的类呢?

你会发现,怎么都不好

  • 过深的层级不利于编写、阅读、压缩、复用
  • 过浅的层级容易导致类名冲突

一旦样式多起来,这个问题就会变得越发严重,其实归根结底,就是类名冲突不好解决的问题

重复样式

这种问题就更普遍了,一些重复的样式值总是不断的出现在 css 代码中,维护起来极其困难

比如,一个网站的颜色一般就那么几种:

  • primary
  • info
  • warn
  • error
  • success

如果有更多的颜色,都是从这些色调中自然变化得来,可以想象,这些颜色会到处充斥到诸如背景、文字、边框中,一旦要做颜色调整,是一个非常大的工程

css 文件细分问题

在大型项目中,css 也需要更细的拆分,这样有利于 css 代码的维护。

比如,有一个做轮播图的模块,它不仅需要依赖 js 功能,还需要依赖 css 样式,既然依赖的 js 功能仅关心轮播图,那 css 样式也应该仅关心轮播图,由此类推,不同的功能依赖不同的 css 样式、公共样式可以单独抽离,这样就形成了不同于过去的 css 文件结构:文件更多、拆分的更细

而同时,在真实的运行环境下,我们却希望文件越少越好,这种情况和 JS 遇到的情况是一致的

因此,对于 css,也需要工程化管理

从另一个角度来说,css 的工程化会遇到更多的挑战,因为 css 不像 JS,它的语法本身经过这么多年并没有发生多少的变化(css3 也仅仅是多了一些属性而已),对于 css 语法本身的改变也是一个工程化的课题

如何解决

这么多年来,官方一直没有提出方案来解决上述问题

一些第三方机构针对不同的问题,提出了自己的解决方案

解决类名冲突

一些第三方机构提出了一些方案来解决该问题,常见的解决方案如下:

命名约定

即提供一种命名的标准,来解决冲突,常见的标准有:

  • BEM
  • OOCSS
  • AMCSS
  • SMACSS
  • 其他

css in js

这种方案非常大胆,它觉得,css 语言本身几乎无可救药了,干脆直接用 js 对象来表示样式,然后把样式直接应用到元素的 style 中

这样一来,css 变成了一个一个的对象,就可以完全利用到 js 语言的优势,你可以:

  • 通过一个函数返回一个样式对象
  • 把公共的样式提取到公共模块中返回
  • 应用 js 的各种特性操作对象,比如:混合、提取、拆分
  • 更多的花样

这种方案在手机端的 React Native 中大行其道

css module

非常有趣和好用的 css 模块化方案,编写简单,绝对不重名

具体的课程中详细介绍

解决重复样式的问题

css in js

这种方案虽然可以利用 js 语言解决重复样式值的问题,但由于太过激进,很多习惯写 css 的开发者编写起来并不是很适应

预编译器

有些第三方搞出一套 css 语言的进化版来解决这个问题,它支持变量、函数等高级语法,然后经过编译器将其编译成为正常的 css

这种方案特别像构建工具,不过它仅针对 css

常见的预编译器支持的语言有:

  • less
  • sass

解决 css 文件细分问题

这一部分,就要依靠构建工具,例如 webpack 来解决了

利用一些 loader 或 plugin 来打包、合并、压缩 css 文件

利用 webpack 拆分 css

要拆分 css,就必须把 css 当成像 js 那样的模块;要把 css 当成模块,就必须有一个构建工具(webpack),它具备合并代码的能力

而 webpack 本身只能读取 css 文件的内容、将其当作 JS 代码进行分析,因此,会导致错误

于是,就必须有一个 loader,能够将 css 代码转换为 js 代码

css-loader

css-loader 的作用,就是将 css 代码转换为 js 代码

它的处理原理极其简单:将 css 代码作为字符串导出

例如:

css
.red {
+  color: "#f40";
+}
+
.red {
+  color: "#f40";
+}
+

经过 css-loader 转换后变成 js 代码:

javascript
module.exports = `.red{
+    color:"#f40";
+}`;
+
module.exports = `.red{
+    color:"#f40";
+}`;
+

上面的 js 代码是经过我简化后的,不代表真实的 css-loader 的转换后代码,css-loader 转换后的代码会有些复杂,同时会导出更多的信息,但核心思想不变

再例如:

css
.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+
.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+

经过 css-loader 转换后变成 js 代码:

javascript
var import1 = require("./bg.png");
+module.exports = `.red{
+    color:"#f40";
+    background:url("${import1}")
+}`;
+
var import1 = require("./bg.png");
+module.exports = `.red{
+    color:"#f40";
+    background:url("${import1}")
+}`;
+

这样一来,经过 webpack 的后续处理,会把依赖./bg.png添加到模块列表,然后再将代码转换为

javascript
var import1 = __webpack_require__("./src/bg.png");
+module.exports = `.red{
+    color:"#f40";
+    background:url("${import1}")
+}`;
+
var import1 = __webpack_require__("./src/bg.png");
+module.exports = `.red{
+    color:"#f40";
+    background:url("${import1}")
+}`;
+

再例如:

css
@import "./reset.css";
+.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+
@import "./reset.css";
+.red {
+  color: "#f40";
+  background: url("./bg.png");
+}
+

会转换为:

javascript
var import1 = require("./reset.css");
+var import2 = require("./bg.png");
+module.exports = `${import1}
+.red{
+    color:"#f40";
+    background:url("${import2}")
+}`;
+
var import1 = require("./reset.css");
+var import2 = require("./bg.png");
+module.exports = `${import1}
+.red{
+    color:"#f40";
+    background:url("${import2}")
+}`;
+

总结,css-loader 干了什么:

  1. 将 css 文件的内容作为字符串导出
  2. 将 css 中的其他依赖作为 require 导入,以便 webpack 分析依赖

style-loader

由于 css-loader 仅提供了将 css 转换为字符串导出的能力,剩余的事情要交给其他 loader 或 plugin 来处理

style-loader 可以将 css-loader 转换后的代码进一步处理,将 css-loader 导出的字符串加入到页面的 style 元素中

例如:

css
.red {
+  color: "#f40";
+}
+
.red {
+  color: "#f40";
+}
+

经过 css-loader 转换后变成 js 代码:

javascript
module.exports = `.red{
+    color:"#f40";
+}`;
+
module.exports = `.red{
+    color:"#f40";
+}`;
+

经过 style-loader 转换后变成:

javascript
module.exports = `.red{
+    color:"#f40";
+}`;
+var style = module.exports;
+var styleElem = document.createElement("style");
+styleElem.innerHTML = style;
+document.head.appendChild(styleElem);
+module.exports = {};
+
module.exports = `.red{
+    color:"#f40";
+}`;
+var style = module.exports;
+var styleElem = document.createElement("style");
+styleElem.innerHTML = style;
+document.head.appendChild(styleElem);
+module.exports = {};
+

以上代码均为简化后的代码,并不代表真实的代码 style-loader 有能力避免同一个样式的重复导入

配置:先用 css-loader 在 style-loader

js
rules: [
+  {
+    test: /\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+];
+
rules: [
+  {
+    test: /\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+];
+

BEM

BEM 是一套针对 css 类样式的命名方法。

其他命名方法还有:OOCSS、AMCSS、SMACSS 等等

BEM 全称是:Block Element Modifier

一个完整的 BEM 类名:block**element_modifier,例如:banner**dot_selected,可以表示:轮播图中,处于选中状态的小圆点

一切类名都采用顶级的,不分级

css
banner__dot_selected {
+}
+banner_dot_unselected {
+}
+nav__dot_selected {
+}
+
banner__dot_selected {
+}
+banner_dot_unselected {
+}
+nav__dot_selected {
+}
+

三个部分的具体含义为:

  • Block:页面中的大区域,表示最顶级的划分,例如:轮播图(banner)、布局(layout)、文章(article)等等
  • element:区域中的组成部分,例如:轮播图中的横幅图片(banner__img)、轮播图中的容器(banner__container)、布局中的头部(layout__header)、文章中的标题(article_title)
  • modifier:可选。通常表示状态,例如:处于展开状态的布局左边栏(layout__left_expand)、处于选中状态的轮播图小圆点(banner__dot_selected)

在某些大型工程中,如果使用 BEM 命名法,还可能会增加一个前缀,来表示类名的用途,常见的前缀有:

  • l: layout,表示这个样式是用于布局的
  • c: component,表示这个样式是一个组件,即一个功能区域
  • u: util,表示这个样式是一个通用的、工具性质的样式
  • j: javascript,表示这个样式没有实际意义,是专门提供给 js 获取元素使用的

css in js

css in js 的核心思想是:用一个 JS 对象来描述样式,而不是 css 样式表

例如下面的对象就是一个用于描述样式的对象:

javascript
const styles = {
+  backgroundColor: "#f40",
+  color: "#fff",
+  width: "400px",
+  height: "500px",
+  margin: "0 auto",
+};
+
const styles = {
+  backgroundColor: "#f40",
+  color: "#fff",
+  width: "400px",
+  height: "500px",
+  margin: "0 auto",
+};
+

由于这种描述样式的方式根本就不存在类名,自然不会有类名冲突

至于如何把样式应用到界面上,不是它所关心的事情,你可以用任何技术、任何框架、任何方式将它应用到界面。

后续学习的 vue、react 都支持 css in js,可以非常轻松的应用到界面

css in js 的特点:

  • 绝无冲突的可能:由于它根本不存在类名,所以绝不可能出现类名冲突
  • 更加灵活:可以充分利用 JS 语言灵活的特点,用各种招式来处理样式
  • 应用面更广:只要支持 js 语言,就可以支持 css in js,因此,在一些用 JS 语言开发移动端应用的时候非常好用,因为移动端应用很有可能并不支持 css
  • 书写不便:书写样式,特别是公共样式的时候,处理起来不是很方便
  • 在页面中增加了大量冗余内容:在页面中处理 css in js 时,往往是将样式加入到元素的 style 属性中,会大量增加元素的内联样式,并且可能会有大量重复,不易阅读最终的页面代码

css module

通过命名规范来限制类名太过死板,而 css in js 虽然足够灵活,但是书写不便。 css module 开辟一种全新的思路来解决类名冲突的问题

思路

css module 遵循以下思路解决类名冲突问题:

  1. css 的类名冲突往往发生在大型项目中
  2. 大型项目往往会使用构建工具(webpack 等)搭建工程
  3. 构建工具允许将 css 样式切分为更加精细的模块
  4. 同 JS 的变量一样,每个 css 模块文件中难以出现冲突的类名,冲突的类名往往发生在不同的 css 模块文件中
  5. 只需要保证构建工具在合并样式代码后不会出现类名冲突即可

实现原理

在 webpack 中,作为处理 css 的 css-loader,它实现了 css module 的思想,要启用 css module,需要将 css-loader 的配置modules设置为true

配置方法 1:

js
module.exports = {
+  rules: [
+    {
+      test: /\.css$/,
+      use: ["style-loader", "css-loader"],
+    },
+  ],
+};
+
module.exports = {
+  rules: [
+    {
+      test: /\.css$/,
+      use: ["style-loader", "css-loader"],
+    },
+  ],
+};
+

配置方法 2:

js
module.exports = {
+  rules: [
+    {
+      test: /\.css$/,
+      use: [
+        "style-loader",
+        {
+          loader: "css-loader",
+          options: {
+            module: true,
+          },
+        },
+      ],
+    },
+  ],
+};
+
module.exports = {
+  rules: [
+    {
+      test: /\.css$/,
+      use: [
+        "style-loader",
+        {
+          loader: "css-loader",
+          options: {
+            module: true,
+          },
+        },
+      ],
+    },
+  ],
+};
+

css-loader 的实现方式如下:

原理极其简单,开启了 css module 后,css-loader 会将样式中的类名进行转换,转换为一个唯一的 hash 值。

由于 hash 值是根据模块路径和类名生成的,因此,不同的 css 模块,哪怕具有相同的类名,转换后的 hash 值也不一样。

如何应用样式

css module 带来了一个新的问题:源代码的类名和最终生成的类名是不一样的,而开发者只知道自己写的源代码中的类名,并不知道最终的类名是什么,那如何应用类名到元素上呢?

为了解决这个问题,css-loader 会导出原类名和最终类名的对应关系,该关系是通过一个对象描述的

这样一来,我们就可以在 js 代码中获取到 css 模块导出的结果,从而应用类名了

style-loader 为了我们更加方便的应用类名,会去除掉其他信息,仅暴露对应关系

javascript
rules: [
+  {
+    // test: /\.css$/, use: ["style-loader", {
+    //     loader: "css-loader",
+    //     options: {
+    //         // modules: {
+    //         //     localIdentName: "[local]-[hash:5]"
+    //         // }
+    //         modules:true
+    //     }
+    // }]
+    // 配置
+    // 从右向左规则
+    test: /\.css$/,
+    use: ["style-loader", "css-loader?modules"],
+  },
+];
+
rules: [
+  {
+    // test: /\.css$/, use: ["style-loader", {
+    //     loader: "css-loader",
+    //     options: {
+    //         // modules: {
+    //         //     localIdentName: "[local]-[hash:5]"
+    //         // }
+    //         modules:true
+    //     }
+    // }]
+    // 配置
+    // 从右向左规则
+    test: /\.css$/,
+    use: ["style-loader", "css-loader?modules"],
+  },
+];
+

其他操作

全局类名

某些类名是全局的、静态的,不需要进行转换,仅需要在类名位置使用一个特殊的语法即可:

css
:global(.main) {
+  ...;
+}
+
:global(.main) {
+  ...;
+}
+

使用了 global 的类名不会进行转换,相反的,没有使用 global 的类名,表示默认使用了 local

css
:local(.main) {
+  ...;
+}
+
:local(.main) {
+  ...;
+}
+

使用了 local 的类名表示局部类名,是可能会造成冲突的类名,会被 css module 进行转换

如何控制最终的类名

绝大部分情况下,我们都不需要控制最终的类名,因为控制它没有任何意义

如果一定要控制最终的类名,需要配置 css-loader 的localIdentName

其他注意事项

  • css module 往往配合构建工具使用
  • css module 仅处理顶级类名,尽量不要书写嵌套的类名,也没有这个必要
  • css module 仅处理类名,不处理其他选择器
  • css module 还会处理 id 选择器,不过任何时候都没有使用 id 选择器的理由
  • 使用了 css module 后,只要能做到让类名望文知意即可,不需要遵守其他任何的命名规范

CSS 预编译器

基本原理

编写 css 时,受限于 css 语言本身,常常难以处理一些问题:

  • 重复的样式值:例如常用颜色、常用尺寸
  • 重复的代码段:例如绝对定位居中、清除浮动
  • 重复的嵌套书写

由于官方迟迟不对 css 语言本身做出改进,一些第三方机构开始想办法来解决这些问题

其中一种方案,便是预编译器

预编译器的原理很简单,即使用一种更加优雅的方式来书写样式代码,通过一个编译器,将其转换为可被浏览器识别的传统 css 代码

目前,最流行的预编译器有LESSSASS,由于它们两者特别相似,因此仅学习一种即可

less 官网:http://lesscss.org/

less 中文文档 1(非官方):http://lesscss.cn/

less 中文文档 2(非官方):https://less.bootcss.com/

sass 官网:https://sass-lang.com/

sass 中文文档 1(非官方):https://www.sass.hk/

sass 中文文档 2(非官方):https://sass.bootcss.com/

LESS 的安装和使用

从原理可知,要使用 LESS,必须要安装 LESS 编译器

LESS 编译器是基于 node 开发的,可以通过 npm 下载安装

shell
npm i -D less
+
npm i -D less
+

安装好了 less 之后,它提供了一个 CLI 工具lessc,通过该工具即可完成编译

shell
lessc less代码文件 编译后的文件
+
lessc less代码文件 编译后的文件
+

试一试:

新建一个index.less文件,编写内容如下:

less
// less代码
+@red: #f40;
+
+.redcolor {
+  color: @red;
+}
+
// less代码
+@red: #f40;
+
+.redcolor {
+  color: @red;
+}
+

在 css 文件夹终端中运行命令:

shell
npx lessc index.less index.css
+
npx lessc index.less index.css
+

可以看到编译之后的代码:

css
.redcolor {
+  color: #f40;
+}
+
.redcolor {
+  color: #f40;
+}
+

LESS 的基本使用

具体的使用见文档:https://less.bootcss.com/

  • 变量
  • 混合
  • 嵌套
  • 运算
  • 函数
  • 作用域
  • 注释
  • 导入

webpack 中使用 less

转换过程: less --- 用 less-loader ---> css ---用 css loader ---> js -----用 style-loader ---> 放到 style 元素 只需要进行配置 webpack:less-loader

javascript
rules: [
+  {
+    test: /\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+  {
+    test: /\.less$/,
+    use: ["style-loader", "css-loader?modules", "less-loader"],
+  },
+];
+
rules: [
+  {
+    test: /\.css$/,
+    use: ["style-loader", "css-loader"],
+  },
+  {
+    test: /\.less$/,
+    use: ["style-loader", "css-loader?modules", "less-loader"],
+  },
+];
+

PostCss

什么是 PostCss

学习到现在,可以看出,CSS 工程化面临着诸多问题,而解决这些问题的方案多种多样。

如果把 CSS 单独拎出来看,光是样式本身,就有很多事情要处理。

既然有这么多事情要处理,何不把这些事情集中到一起统一处理呢?

PostCss 就是基于这样的理念出现的。

PostCss 类似于一个编译器,可以将样式源码编译成最终的 CSS 代码

看上去是不是和 LESS、SASS 一样呢?

但 PostCss 和 LESS、SASS 的思路不同,它其实只做一些代码分析之类的事情,将分析的结果交给插件,具体的代码转换操作是插件去完成的。

官方的一张图更能说明 postcss 的处理流程:

这一点有点像 webpack,webpack 本身仅做依赖分析、抽象语法树分析,其他的操作是靠插件和加载器完成的。

官网地址:https://postcss.org/

github 地址:https://github.com/postcss/postcss

安装

PostCss 是基于 node 编写的,因此可以使用 npm 安装

shell
npm i -D postcss
+
npm i -D postcss
+

postcss 库提供了对应的 js api 用于转换代码,如果你想使用 postcss 的一些高级功能,或者想开发 postcss 插件,就要 api 使用 postcss,api 的文档地址是:http://api.postcss.org/

后缀:.postcss .pcss .sss 安装插件:postcss-sugarss-language 可以识别拓展名

不过绝大部分时候,我们都是使用者,并不希望使用代码的方式来使用 PostCss

因此,我们可以再安装一个 postcss-cli,通过命令行来完成编译

shell
npm i -D postcss-cli
+
npm i -D postcss-cli
+

postcss-cli 提供一个命令,它调用 postcss 中的 api 来完成编译

命令的使用方式为:

shell
postcss 源码文件 -o 输出文件
+
postcss 源码文件 -o 输出文件
+

javascript
rules: [
+  {
+    test: /\.pcss$/,
+    use: ["style-loader", "css-loader?modules", "postcss-loader"],
+  },
+];
+
rules: [
+  {
+    test: /\.pcss$/,
+    use: ["style-loader", "css-loader?modules", "postcss-loader"],
+  },
+];
+

编译:npm start

配置文件

和 webpack 类似,postcss 有自己的配置文件,该配置文件会影响 postcss 的某些编译行为。

配置文件的默认名称是:postcss.config.js

例如:

javascript
module.exports = {
+  map: false, //关闭source-map
+};
+
module.exports = {
+  map: false, //关闭source-map
+};
+

插件

光使用 postcss 是没有多少意义的,要让它真正的发挥作用,需要插件

postcss 的插件市场:https://www.postcss.parts/

下面罗列一些 postcss 的常用插件

postcss-preset-env

过去使用 postcss 的时候,往往会使用大量的插件,它们各自解决一些问题

这样导致的结果是安装插件、配置插件都特别的繁琐

于是出现了这么一个插件postcss-preset-env,它称之为postcss预设环境,大意就是它整合了很多的常用插件到一起,并帮你完成了基本的配置,你只需要安装它一个插件,就相当于安装了很多插件了。

安装好该插件后,在 postcss 配置中加入下面的配置

javascript
module.exports = {
+  plugins: {
+    "postcss-preset-env": {}, // {} 中可以填写插件的配置
+  },
+};
+
module.exports = {
+  plugins: {
+    "postcss-preset-env": {}, // {} 中可以填写插件的配置
+  },
+};
+

该插件的功能很多,下面一一介绍

自动的厂商前缀

某些新的 css 样式需要在旧版本浏览器中使用厂商前缀方可实现

例如

css
::placeholder {
+  color: red;
+}
+
::placeholder {
+  color: red;
+}
+

该功能在不同的旧版本浏览器中需要书写为

css
::-webkit-input-placeholder {
+  color: red;
+}
+::-moz-placeholder {
+  color: red;
+}
+:-ms-input-placeholder {
+  color: red;
+}
+::-ms-input-placeholder {
+  color: red;
+}
+::placeholder {
+  color: red;
+}
+
::-webkit-input-placeholder {
+  color: red;
+}
+::-moz-placeholder {
+  color: red;
+}
+:-ms-input-placeholder {
+  color: red;
+}
+::-ms-input-placeholder {
+  color: red;
+}
+::placeholder {
+  color: red;
+}
+

要完成这件事情,需要使用autoprefixer库。

postcss-preset-env内部包含了该库,自动有了该功能。

如果需要调整兼容的浏览器范围,可以通过下面的方式进行配置

方式 1:在 postcss-preset-env 的配置中加入 browsers

javascript
module.exports = {
+  plugins: {
+    "postcss-preset-env": {
+      browsers: ["last 2 version", "> 1%"],
+    },
+  },
+};
+
module.exports = {
+  plugins: {
+    "postcss-preset-env": {
+      browsers: ["last 2 version", "> 1%"],
+    },
+  },
+};
+

方式 2【推荐】:添加 .browserslistrc 文件 --通用

创建文件.browserslistrc,填写配置内容

last 2 version
+> 1%
+
last 2 version
+> 1%
+

方式 3【推荐】:在 package.json 的配置中加入 browserslist

json
"browserslist": [
+    "last 2 version",
+    "> 1%"
+]
+
"browserslist": [
+    "last 2 version",
+    "> 1%"
+]
+

browserslist是一个多行的(数组形式的)标准字符串。

它的书写规范多而繁琐,详情见:https://github.com/browserslist/browserslist

一般情况下,大部分网站都使用下面的格式进行书写

last 2 version
+> 1% in CN
+not ie <= 8
+
last 2 version
+> 1% in CN
+not ie <= 8
+
  • last 2 version: 浏览器的兼容最近期的两个版本
  • > 1% in CN: 匹配中国大于 1%的人使用的浏览器, in CN可省略
  • not ie <= 8: 排除掉版本号小于等于 8 的 IE 浏览器

默认情况下,匹配的结果求的是并集

你可以通过网站:https://browserl.ist/ 对配置结果覆盖的浏览器进行查询,查询时,多行之间使用英文逗号分割

browserlist 的数据来自于CanIUse网站,由于数据并非实时的,所以不会特别准确

未来的 CSS 语法

CSS 的某些前沿语法正在制定过程中,没有形成真正的标准,如果希望使用这部分语法,为了浏览器兼容性,需要进行编译

过去,完成该语法编译的是cssnext库,不过有了postcss-preset-env后,它自动包含了该功能。

你可以通过postcss-preset-envstage配置,告知postcss-preset-env需要对哪个阶段的 css 语法进行兼容处理,它的默认值为 2

javascript
"postcss-preset-env": {
+    stage: 0
+}
+
"postcss-preset-env": {
+    stage: 0
+}
+

一共有 5 个阶段可配置:

  • Stage 0: Aspirational - 只是一个早期草案,极其不稳定
  • Stage 1: Experimental - 仍然极其不稳定,但是提议已被 W3C 公认
  • Stage 2: Allowable - 虽然还是不稳定,但已经可以使用了
  • Stage 3: Embraced - 比较稳定,可能将来会发生一些小的变化,它即将成为最终的标准
  • Stage 4: Standardized - 所有主流浏览器都应该支持的 W3C 标准

了解了以上知识后,接下来了解一下未来的 css 语法,尽管某些语法仍处于非常早期的阶段,但是有该插件存在,编译后仍然可以被浏览器识别

变量

未来的 css 语法是天然支持变量的

:root{}中定义常用变量,使用--前缀命名变量

css
:root {
+  --lightColor: #ddd;
+  --darkColor: #333;
+}
+
+a {
+  color: var(--lightColor);
+  background: var(--darkColor);
+}
+
:root {
+  --lightColor: #ddd;
+  --darkColor: #333;
+}
+
+a {
+  color: var(--lightColor);
+  background: var(--darkColor);
+}
+

编译后,仍然可以看到原语法,因为某些新语法的存在并不会影响浏览器的渲染,尽管浏览器可能不认识 如果不希望在结果中看到新语法,可以配置postcss-preset-envpreservefalse

自定义选择器

css
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
+@custom-selector :--enter :focus, :hover;
+
+a:--enter {
+  color: #f40;
+}
+
+:--heading {
+  font-weight: bold;
+}
+
+:--heading.active {
+  font-weight: bold;
+}
+
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
+@custom-selector :--enter :focus, :hover;
+
+a:--enter {
+  color: #f40;
+}
+
+:--heading {
+  font-weight: bold;
+}
+
+:--heading.active {
+  font-weight: bold;
+}
+

编译后

css
a:focus,
+a:hover {
+  color: #f40;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-weight: bold;
+}
+
+h1.active,
+h2.active,
+h3.active,
+h4.active,
+h5.active,
+h6.active {
+  font-weight: bold;
+}
+
a:focus,
+a:hover {
+  color: #f40;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-weight: bold;
+}
+
+h1.active,
+h2.active,
+h3.active,
+h4.active,
+h5.active,
+h6.active {
+  font-weight: bold;
+}
+

嵌套

与 LESS 相同,只不过嵌套的选择器前必须使用符号&

less
.a {
+  color: red;
+  & .b {
+    color: green;
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
.a {
+  color: red;
+  & .b {
+    color: green;
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+

编译后

css
.a {
+  color: red;
+}
+
+.a .b {
+  color: green;
+}
+
+.a > .b {
+  color: blue;
+}
+
+.a:hover {
+  color: #000;
+}
+
.a {
+  color: red;
+}
+
+.a .b {
+  color: green;
+}
+
+.a > .b {
+  color: blue;
+}
+
+.a:hover {
+  color: #000;
+}
+

postcss-apply

该插件可以支持在 css 中书写属性集

类似于 LESS 中的混入,可以利用 CSS 的新语法定义一个 CSS 代码片段,然后在需要的时候应用它

less
:root {
+  --center: {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.item {
+  @apply --center;
+}
+
:root {
+  --center: {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.item {
+  @apply --center;
+}
+

编译后

css
.item {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  -webkit-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
+}
+
.item {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  -webkit-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
+}
+

实际上,该功能也属于 cssnext,不知为何postcss-preset-env没有支持

postcss-color-function

该插件支持在源码中使用一些颜色函数

less
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: color(#aabbcc);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: color(#aabbcc a(90%));
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: color(#aabbcc red(90%));
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: color(#aabbcc tint(50%));
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: color(#aabbcc shade(50%));
+}
+
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: color(#aabbcc);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: color(#aabbcc a(90%));
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: color(#aabbcc red(90%));
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: color(#aabbcc tint(50%));
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: color(#aabbcc shade(50%));
+}
+

编译后

css
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: rgb(170, 187, 204);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: rgba(170, 187, 204, 0.9);
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: rgb(230, 187, 204);
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: rgb(213, 221, 230);
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: rgb(85, 94, 102);
+}
+
body {
+  /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */
+  color: rgb(170, 187, 204);
+  /* 将颜色#aabbcc透明度设置为90% */
+  color: rgba(170, 187, 204, 0.9);
+  /* 将颜色#aabbcc的红色部分设置为90% */
+  color: rgb(230, 187, 204);
+  /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */
+  color: rgb(213, 221, 230);
+  /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */
+  color: rgb(85, 94, 102);
+}
+

postcss-import

该插件可以让你在postcss文件中导入其他样式代码,通过该插件可以将它们合并

由于后续的知识点中,会将 postcss 加入到 webpack 中,而 webpack 本身具有依赖分析的功能,所以该插件的实际意义不大

stylelint

官网:https://stylelint.io/

在实际的开发中,我们可能会错误的或不规范的书写一些 css 代码,stylelint 插件会即时的发现错误

由于不同的公司可能使用不同的 CSS 书写规范,stylelint 为了保持灵活,它本身并没有提供具体的规则验证

你需要安装或自行编写规则验证方案

通常,我们会安装stylelint-config-standard库来提供标准的 CSS 规则判定

安装好后,我们需要告诉 stylelint 使用该库来进行规则验证

告知的方式有多种,比较常见的是使用文件.stylelintrc

json
//.styleintrc
+{
+  "extends": "stylelint-config-standard"
+}
+
//.styleintrc
+{
+  "extends": "stylelint-config-standard"
+}
+

此时,如果你的代码出现不规范的地方,编译时将会报出错误

css
body {
+  background: #f4;
+}
+
body {
+  background: #f4;
+}
+

发生了两处错误:

  1. 缩进应该只有两个空格
  2. 十六进制的颜色值不正确

如果某些规则并非你所期望的,可以在配置中进行设置

json
{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "indentation": null
+  }
+}
+
{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "indentation": null
+  }
+}
+

设置为null可以禁用该规则,或者设置为 4,表示一个缩进有 4 个空格。具体的设置需要参见 stylelint 文档:https://stylelint.io/

但是这种错误报告需要在编译时才会发生,如果我希望在编写代码时就自动在编辑器里报错呢?

既然想在编辑器里达到该功能,那么就要在编辑器里做文章

安装 vscode 的插件stylelint即可,它会读取你工程中的配置文件,按照配置进行实时报错

实际上,如果你拥有了stylelint插件,可以不需要在 postcss 中使用该插件了

抽离 css 文件

目前,css 代码被 css-loader 转换后,交给的是 style-loader 进行处理。

style-loader 使用的方式是用一段 js 代码,将样式加入到 style 元素中。

而实际的开发中,我们往往希望依赖的样式最终形成一个 css 文件

此时,就需要用到一个库:mini-css-extract-plugin

该库提供了 1 个 plugin 和 1 个 loader

  • plugin:负责生成 css 文件
  • loader:负责记录要生成的 css 文件的内容,同时导出开启 css-module 后的样式对象

使用方式:

javascript
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.css$/,
+        use: [MiniCssExtractPlugin.loader, "css-loader?modules"],
+      },
+    ],
+  },
+  plugins: [
+    new MiniCssExtractPlugin(), //负责生成css文件
+  ],
+};
+
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.css$/,
+        use: [MiniCssExtractPlugin.loader, "css-loader?modules"],
+      },
+    ],
+  },
+  plugins: [
+    new MiniCssExtractPlugin(), //负责生成css文件
+  ],
+};
+

配置生成的文件名

output.filename的含义一样,即根据 chunk 生成的样式文件名

配置生成的文件名,例如[name].[contenthash:5].css

默认情况下,每个 chunk 对应一个 css 文件

原子化 CSS(了解)

+ + + + + \ No newline at end of file diff --git a/front-end-engineering/PackageManager.html b/front-end-engineering/PackageManager.html new file mode 100644 index 00000000..ac72e0e6 --- /dev/null +++ b/front-end-engineering/PackageManager.html @@ -0,0 +1,254 @@ + + + + + + 包管理器 | Sunny's blog + + + + + + + + +
Skip to content
On this page

包管理器

包管理工具概述

1.概念

模块(module)

通常以单个文件形式存在的功能片段,入口文件通常称之为入口模块主模块

库(library,简称 lib)

以一个或多个模块组成的完整功能块,为开发中某一方面的问题提供完整的解决方案

包(package)

包含元数据的库,这些元数据包括:名称、描述、git 主页、许可证协议、作者、依赖等等

2.背景

CommonJS 的出现,使 node 环境下的 JS 代码可以用模块更加细粒度的划分。一个类、一个函数、一个对象、一个配置等等均可以作为模块,这种细粒度的划分,是开发大型应用的基石。 为了解决在开发过程中遇到的常见问题,比如加密、提供常见的工具方法、模拟数据等等,一时间,在前端社区涌现了大量的第三方库。这些库使用 CommonJS 标准书写而成,非常容易使用。 然而,在下载使用这些第三方库的时候,遇到难以处理的问题:

  • 下载过程繁琐
    • 进入官网或 github 主页
    • 找到并下载相应的版本
    • 拷贝到工程的目录中
    • 如果遇到有同名的库,需要更改名称
  • 如果该库需要依赖其他库,还需要按照要求先下载其他库
  • 开发环境中安装的大量的库如何在生产环境中还原,又如何区分
  • 更新一个库极度麻烦
  • 自己开发的库,如何在下一次开发使用

以上问题,就是包管理工具要解决的问题

3.前端包管理器

几乎可以这样认为,前端所有的包管理器都是基于 npm 的,目前,npm 即是一个包管理器,也是其他包管理的基石 npm 全称为 node package manager,即 node 包管理器,它运行在 node 环境中,让开发者可以用简单的方式完成包的查找、安装、更新、卸载、上传等操作

npm 之所以要运行在 node 环境,而不是浏览器环境,根本原因是因为浏览器环境无法提供下载、删除、读取本地文件的功能。而 node 属于服务器环境,没有浏览器的种种限制,理论上可以完全掌控运行 node 的计算机。

npm 的出现,弥补了 node 没有包管理器的缺陷,于是很快,node 在安装文件中内置了 npm,当开发者安装好 node 之后,就自动安装了 npm,不仅如此,node 环境还专门为 npm 提供了良好的支持,使用 npm 下载的包更加方便了。

npm 由三部分组成:

  • registry:入口
    • 可以把它想象成一个庞大的数据库
    • 第三方库的开发者,将自己的库按照 npm 的规范,打包上传到数据库中
    • 使用者通过统一的地址下载第三方包
  • 官网:https://www.npmjs.com/
    • 查询包
    • 注册、登录、管理个人信息
  • CLI:command-line interface 命令行接口
    • 这一部分是本门课讲解的重点
    • 安装好 npm 后,通过 CLI 来使用 npm 的各种功能
    • vscode 里面 Ctrl+j

cmd 常用命令 换到 e 盘:e: 查看目录:dir vscode 查看 npm 的当前版本 输入 npm -v 或者 npm --version 注意空格

node 和 npm 是互相成就的,node 的出现让 npm 火了,npm 的火爆带动了大量的第三方库的发展,很多优秀的第三方库打包上传到了 npm,这些第三方库又为 node 带来了大量的用户

npm

包的安装

安装(install)即下载包 由于 npm 的官方 registry 服务器位于国外,可能受网速影响导致下载缓慢或失败。因此,安装好 npm 之后,需要重新设置 registry 的地址为国内地址。目前,淘宝 https://registry.npm.taobao.org 提供了国内的 registry 地址,先设置到该地址。设置方式为npm config set registry [https://registry.npm.taobao.org](https://registry.npm.taobao.org)。设置好后,通过命令npm config get registry进行检查

npm 安装一个包,分为两种安装方式:

  1. 本地安装
  2. 全局安装

1.本地安装

使用命令npm install 包名npm i 包名即可完成本地安装 终端 clear 是清除命令 本地安装的包出现在当前目录下的node_modules目录中

随着开发的进展,node_modules目录会变得异常庞大,目录下的内容不适合直接传输到生产环境,因此通常使用.gitignore文件忽略该目录中的内容 本地安装适用于绝大部分的包,它会在当前目录及其子目录中发挥作用 通常在项目的根目录中使用本地安装 安装一个包的时候,npm 会自动管理依赖,它会下载该包的依赖包到node_modules目录中 例如:react 如果本地安装的包带有 CLI,npm 会将它的 CLI 脚本文件放置到node_modules/.bin下,使用命令npx 命令名即可调用(npx mocha) 例如:mocha

演示 安装 jQuery:命令 npm install jquery

2.全局安装

全局安装的包放置在一个特殊的全局目录,该目录可以通过命令npm config get prefix查看 使用命令npm install --global 包名npm i -g 包名 重要:全局安装的包并非所有工程可用,它仅提供全局的 CLI 工具 大部分情况下,都不需要全局安装包,除非:

  1. 包的版本非常稳定,很少有大的更新
  2. 提供的 CLI 工具在各个工程中使用的非常频繁
  3. CLI 工具仅为开发环境提供支持,而非部署环境

包配置

前节补充:同时下载两个包:命令 npm i jquery loadsh

目前遇到的问题:

  1. 拷贝工程后如何还原?
  2. 如何区分开发依赖和生产依赖?
  3. 如果自身的项目也是一个包,如何描述包的信息

以上这些问题都需要通过包的配置文件解决

1.配置文件

npm 将每个使用 npm 的工程本身都看作是一个包,包的信息需要通过一个名称固定的配置文件来描述 配置文件的名称固定为:package.json 可以手动创建该文件,而更多的时候,是通过命令npm init创建的 配置文件中可以描述大量的信息,包括:

  • name:包的名称,该名称必须是英文单词字符,支持连接符
  • version:版本
    • 版本规范:主版本号.次版本号.补丁版本号
    • 主版本号:仅当程序发生了重大变化时才会增长,如新增了重要功能、新增了大量的 API、技术架构发生了重大变化
    • 次版本号:仅当程序发生了一些小变化时才会增长,如新增了一些小功能、新增了一些辅助型的 API
    • 补丁版本号:仅当解决了一些 bug 或 进行了一些局部优化时更新,如修复了某个函数的 bug、提升了某个函数的运行效率
  • description:包的描述
  • homepage:官网地址
  • author:包的作者,必须是有效的 npm 账户名,书写规范是 account <mail>,例如:zhangsan <zhangsan@gmail.com>,不正确的账号和邮箱可能导致发布包时失败
  • repository:包的仓储地址,通常指 git 或 svn 的地址,它是一个对象
    • type:仓储类型,git 或 svn
    • url:地址

命令:get remote -v

  • main:包的入口文件,使用包的人默认从该入口文件导入包的内容
  • keywords: 搜索关键字,发布包后,可以通过该数组中的关键字搜索到包

    一步到位简化配置 json 文件

使用npm init --yesnpm init -y可以在生成配置文件时自动填充默认配置 操作:创建一个英文文件夹,右键终端打开,输入npm init --yesnpm init -y

2.保存依赖关系

大部分时候,我们仅仅是开发项目,并不会把它打包发布出去,尽管如此,我们仍然需要 package.json 文件 package.json 文件最重要的作用,是记录当前工程的依赖

  • dependencies:生产环境的依赖包
  • devDependencies:仅开发环境的依赖包
json
"dependencies": {
+    "jquery": "latest",//不推荐最新,防止开发冲突
+    "lodash": "4.17.15"
+},
+"devDependencies": {
+    "mocha": "6.2.2"
+}
+
"dependencies": {
+    "jquery": "latest",//不推荐最新,防止开发冲突
+    "lodash": "4.17.15"
+},
+"devDependencies": {
+    "mocha": "6.2.2"
+}
+

配置好依赖后,使用下面的命令即可安装依赖

shell
## 本地安装所有依赖 dependencies + devDependencies
+npm install
+npm i
+
+## 仅安装生产环境的依赖 dependencies
+npm install --production
+
## 本地安装所有依赖 dependencies + devDependencies
+npm install
+npm i
+
+## 仅安装生产环境的依赖 dependencies
+npm install --production
+

这样一来,代码移植就不是问题了,只需要移植源代码和 package.json 文件,不用移植 node_modules 目录,然后在移植之后通过命令即可重新恢复安装 为了更加方便的添加依赖,npm 支持在使用 install 命令时,加入一些额外的参数,用于将安装的依赖包保存到 package.json 文件中(自动地) 涉及的命令如下

shell
## 安装依赖到生产环境
+npm i 包名
+npm i --save 包名
+npm i -S 包名
+
+## 安装依赖到开发环境
+npm i --save-dev 包名
+npm i -D 包名
+
## 安装依赖到生产环境
+npm i 包名
+npm i --save 包名
+npm i -S 包名
+
+## 安装依赖到开发环境
+npm i --save-dev 包名
+npm i -D 包名
+

安装包之后 package.json 会自动生成:

json
 "dependencies": {
+     "jquery": "^3.4.1",
+     "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+     "mocha": "^6.2.2"
+ }
+
 "dependencies": {
+     "jquery": "^3.4.1",
+     "lodash": "^4.17.15"
+ },
+ "devDependencies": {
+     "mocha": "^6.2.2"
+ }
+

自动保存的依赖版本,例如^15.1.3,这种书写方式叫做语义版本号(semver version),具体规则后续讲解

包的使用

引入模块:npm init---chapter2---回车----回车----npm i lodash 安装成功

nodejs 对 npm 支持非常良好 使用 nodejs 导入模块时传统方法:

javascript
var _ = require("./node_modules/lodash/index");
+// 习惯用下划线命名
+console.log(_);
+运行命令node index.js
+
var _ = require("./node_modules/lodash/index");
+// 习惯用下划线命名
+console.log(_);
+运行命令:node index.js
+

返回的里面有很多方法,例如:compact

javascript
var arr = _.compact([1, 3, 0, "", false, 5]);
+// compact会把数组里面判定为假的除去,返回新数组
+console.log(arr);
+
var arr = _.compact([1, 3, 0, "", false, 5]);
+// compact会把数组里面判定为假的除去,返回新数组
+console.log(arr);
+

当使用 nodejs 导入模块时,如果模块路径不是以 ./ 或 ../ 开头,则 node 会认为导入的模块来自于 node_modules 目录,例如:

javascript
var _ = require("lodash");
+
var _ = require("lodash");
+

它首先会从当前目录的以下位置寻找文件

shell
node_modules/lodash.js
+node_modules/lodash/入口文件
+
node_modules/lodash.js
+node_modules/lodash/入口文件
+

若当前目录没有这样的文件,则会回溯到上级目录按照同样的方式查找

运行命令:包管理器\2. npm\2-3. 包的使用> node .\sub\test.js

如果到顶级目录都无法找到文件,则抛出错误 Cannot find module 'jquery' 上面提到的入口文件按照以下规则确定

  1. 查看导入包的 package.json 文件,读取 main 字段作为入口文件
  2. 若不包含 main 字段,则使用 index.js 作为入口文件

    入口文件的规则同样适用于自己工程中的模块 自己工程中的模块为 a 作为举例:

html
// 首先,查看当前目录是否有 a.js //
+把a当作文件夹,并且,把该文件夹当作一个包,看该包中是否有package.json文件,读取main字段。。。。
+var a = require("./a"); console.log(a)
+
// 首先,查看当前目录是否有 a.js //
+把a当作文件夹,并且,把该文件夹当作一个包,看该包中是否有package.json文件,读取main字段。。。。
+var a = require("./a"); console.log(a)
+

在 node 中,还可以手动指定路径来导入相应的文件,这种情况比较少见

javascript
var test = require("lodash/fp/add");
+console.log(test);
+
var test = require("lodash/fp/add");
+console.log(test);
+

简易数据爬虫

补充: require 导入一个包,如果以./或../开头,就导入自己写的文件。如果没有写,就导入 node_modules 下面的模块 特殊情况:导入模块是一个特殊名字,使用 nodejs 自带的模块。(require("fs"))

初始化:命令 npm init 生成 package.js 文件 将豆瓣电影的电影数据抓取下来,保存到本地文件 movie.json 中 需要用到的包:用法通过 npm 网站查询

  1. axios:专门用于在各种环境中发送网络请求,并获取到服务器响应结果

    安装 npm i axios,   运行 node .\index.js

  2. cheerio:jquery 的核心逻辑包,支持所有环境,可用于讲一个 html 字符串转换成为 jquery 对象,并通过 jquery 对象完成后续操作

  3. fs:node 核心模块,专门用于文件处理

    • fs.writeFile(文件名, 数据)

语义版本

思考:如果你编写了一个包 A,依赖另外一个包 B,你在编写代码时,包 B 的版本是 2.4.1,你是希望使用你包的人一定要安装包 B,并且是 2.4.1 版本,还是希望他可以安装更高的版本,如果你希望它安装更高的版本,高的什么程度呢 回顾:版本号规则 版本规范:主版本号.次版本号.补丁版本号

  • 主版本号:仅当程序发生了重大变化时才会增长,如新增了重要功能、新增了大量的 API、技术架构发生了重大变化
  • 次版本号:仅当程序发生了一些小变化时才会增长,如新增了一些小功能、新增了一些辅助型的 API
  • 补丁版本号:仅当解决了一些 bug 或 进行了一些局部优化时更新,如修复了某个函数的 bug、提升了某个函数的运行效率

有的时候,我们希望:安装我的依赖包的时候,次版本号和补丁版本号是可以有提升的,但是主版本号不能变化

有的时候,我们又希望:安装我的依赖包的时候,只有补丁版本号可以提升,其他都不能提升

甚至我们希望依赖包保持固定的版本,尽管这比较少见

这样一来,就需要在配置文件中描述清楚具体的依赖规则,而不是直接写上版本号那么简单。

这种规则的描述,即语义版本

语义版本的书写规则非常丰富,下面列出了一些常见的书写方式

符号描述示例示例描述
>大于某个版本>1.2.1大于 1.2.1 版本
>=大于等于某个版本>=1.2.1大于等于 1.2.1 版本
<小于某个版本<1.2.1小于 1.2.1 版本
<=小于等于某个版本<=1.2.1小于等于 1.2.1 版本
-介于两个版本之间1.2.1 - 1.4.5介于 1.2.1 和 1.4.5 之间
x不固定的版本号1.3.x只要保证主版本号是 1,次版本号是 3 即可
~补丁版本号可增~1.3.4保证主版本号是 1,次版本号是 3,补丁版本号大于等于 4
^此版本和补丁版本可增^1.3.4保证主版本号是 1,次版本号可以大于等于 3,补丁版本号可以大于等于 4
*最新版本*始终安装最新版本

1.避免还原的差异

版本依赖控制始终是一个两难的问题 如果允许版本增加,可以让依赖包的 bug 得以修复(补丁版本号),可以带来一些意外的惊喜(次版本号),但同样可能带来不确定的风险(新的 bug) 如果不允许版本增加,可以获得最好的稳定性,但失去了依赖包自我优化的能力 而有的时候情况更加复杂,如果依赖包升级后,依赖也发生了变化,会有更多不确定的情况出现 基于此,npm 在安装包的时候,会自动生成一个 package-lock.json 文件,该文件记录了安装包时的确切依赖关系 当移植工程时,如果移植了 package-lock.json 文件,恢复安装时,会按照 package-lock.json 文件中的确切依赖进行安装,最大限度的避免了差异

2.[扩展]npm 的差异版本处理

如果两个包依赖同一个包的不同版本,如下图

面对这种情况,在 node_modules 目录中,不会使用扁平的目录结构,而会形成嵌套的目录,如下图:

html
├── node_modules │ ├── a │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── a包的文件 │ ├── b │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── b包的文件
+
├── node_modules │ ├── a │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── a包的文件 │ ├── b │ │ ├── node_modules │ │ │ ├── c │ │ │ | |—— c包的文件
+│ │ │── b包的文件
+

npm 脚本 (npm scripts)

在开发的过程中,我们可能会反复使用很多的 CLI 命令,例如:

  • 启动工程命令(node 或 一些第三方包提供的 CLI 命令)
  • 部署工程命令(一些第三方包提供的 CLI 命令)
  • 测试工程命令(一些第三方包提供的 CLI 命令)

这些命令纷繁复杂,根据第三方包的不同命令也会不一样,非常难以记忆 于是,npm 非常贴心的支持了脚本,只需要在 package.json 中配置 scripts 字段,即可配置各种脚本名称 之后,我们就可以运行简单的指令来完成各种操作了 运行方式是 npm run 脚本名称 不仅如此,npm 还对某些常用的脚本名称进行了简化,下面的脚本名称是不需要使用 run 的:

  • start 可以简化成 npm start
  • stop
  • test

一些细节:

  • 脚本中可以省略 npx
  • start 脚本有默认值:node server.js

演示

启动 node 方式一:

js 文件终端打开,初始化创建 package.json 文件,node .\index.js

启动 node 方式二:

第三方工具 npm in nodemon 输入命令:npx nodemon .\index.js 可以不断刷新运行,想停止:ctrl+c 以上两种方式要记着,太麻烦了 所以在 package.json 里面加上

json
{
+  "scripts": {
+    "start": "nodemon index.js",
+    "start": "npx nodemon index.js",
+    "trick": "chrome https://www.baidu.com",
+    "trick": "dir"
+  }
+}
+
{
+  "scripts": {
+    "start": "nodemon index.js",
+    "start": "npx nodemon index.js",
+    "trick": "chrome https://www.baidu.com",
+    "trick": "dir"
+  }
+}
+

向运行 node,这次就直接 npm run start/trick 这里可以配置各种命令

运行环境配置

我们书写的代码一般有三种运行环境:

  1. 开发环境
  2. 生产环境
  3. 测试环境

有的时候,我们可能需要在 node 代码中根据不同的环境做出不同的处理 如何优雅的让 node 知道处于什么环境,是极其重要的 通常我们使用如下的处理方式: node 中有一个全局变量 global (可以类比浏览器环境的 window),该变量是一个对象,对象中的所有属性均可以直接使用 global 有一个属性是 process,该属性是一个对象,包含了当前运行 node 程序的计算机的很多信息,其中有一个信息是 env,是一个对象,包含了计算机中所有的系统变量 设置环境变量: 名字:NODE_ENV 值:development 通常,我们通过系统变量 NODE_ENV 的值,来判定 node 程序处于何种环境

javascript
var a = "没有环境变量";
+// 此电脑属性高级系统设置进行配置
+// console.log(process.env.NODE_ENV)//development
+
+if (process.env.NODE_ENV === "development") {
+  a = "开发环境";
+} else if (process.env.NODE_ENV === "production") {
+  a = "生产环境";
+} else if (process.env.NODE_ENV === "test") {
+  a = "测试环境";
+}
+console.log(a);
+
var a = "没有环境变量";
+// 此电脑属性高级系统设置进行配置
+// console.log(process.env.NODE_ENV)//development
+
+if (process.env.NODE_ENV === "development") {
+  a = "开发环境";
+} else if (process.env.NODE_ENV === "production") {
+  a = "生产环境";
+} else if (process.env.NODE_ENV === "test") {
+  a = "测试环境";
+}
+console.log(a);
+

有两种方式设置 NODE_ENV 的值

  1. 永久设置
  2. 临时设置

我们一般使用临时设置

因此,我们可以配置 scripts 脚本,在设置好了 NODE_ENV 后启动程序

javascript
//开发环境
+"start": "set NODE_ENV=development&&node index.js",//这里不能有空格
+ //在package.json里面配置:相当于设置NODE_ENV为development并且执行index.js
+//生产环境
+"build": "set NODE_ENV=production&&node index.js
+//测试环境
+"build": "set NODE_ENV=test&&node index.js
+
//开发环境
+"start": "set NODE_ENV=development&&node index.js",//这里不能有空格
+ //在package.json里面配置:相当于设置NODE_ENV为development并且执行index.js
+//生产环境
+"build": "set NODE_ENV=production&&node index.js
+//测试环境
+"build": "set NODE_ENV=test&&node index.js
+

为了避免不同系统(macOS windows 不同)的设置方式的差异,可以使用第三方库 cross-env 对环境变量进行设置

安装 npm i --D cross-env 表示开发依赖

javascript
"start": "cross-env NODE_ENV=development node index.js",
+"build": "cross-env NODE_ENV=production node index.js",
+"test": "cross-env NODE_ENV=test node index.js"
+
+
+npm start;
+npm run build;
+npm test;
+
"start": "cross-env NODE_ENV=development node index.js",
+"build": "cross-env NODE_ENV=production node index.js",
+"test": "cross-env NODE_ENV=test node index.js"
+
+
+npm start;
+npm run build;
+npm test;
+

在 node 中读取 package.json

有的时候,我们可能在 package.json 中配置一些自定义的字段,这些字段需要在 node 中读取 在 node 中,可以直接导入一个 json 格式的文件,它会自动将其转换为 js 对象

javascript
//读取package.json文件中的版本号
+
+var config = require("./package.json");
+console.log(config.version);
+console.log(config.a);
+
//读取package.json文件中的版本号
+
+var config = require("./package.json");
+console.log(config.version);
+console.log(config.a);
+

其他 npm 命令

1.安装

  1. 精确安装最新版本
shell
npm install --save-exact 包名
+npm install -E 包名
+
npm install --save-exact 包名
+npm install -E 包名
+
  1. 安装指定版本
shell
npm install 包名@版本号
+
npm install 包名@版本号
+

2.查询

  1. 查询包安装路径
shell
npm root [-g]
+
npm root [-g]
+
  1. 查看包信息
shell
npm view 包名 [子信息]
+## view aliases:v info show
+
+依赖的包:npm v react dependencies
+版本:npm v react version
+
npm view 包名 [子信息]
+## view aliases:v info show
+
+依赖的包:npm v react dependencies
+版本:npm v react version
+
  1. 查询安装包
shell
npm list [-g] [--depth=依赖深度]//0开始
+## list aliases: ls  la  ll
+
npm list [-g] [--depth=依赖深度]//0开始
+## list aliases: ls  la  ll
+

3.更新

  1. 检查有哪些包需要更新
shell
npm outdated
+
npm outdated
+
  1. 更新包
shell
npm update [-g] [包名]
+## update 别名(aliases):up、upgrade
+
npm update [-g] [包名]
+## update 别名(aliases):up、upgrade
+

用 npm 安装最新版的 npm npm i -g npm

4.卸载包

shell
npm uninstall [-g] 包名
+## uninstall aliases: remove, rm, r, un, unlink
+
npm uninstall [-g] 包名
+## uninstall aliases: remove, rm, r, un, unlink
+

5.npm 配置

npm 的配置会对其他命令产生或多或少的影响 安装好 npm 之后,最终会产生两个配置文件,一个是用户配置,一个是系统配置,当两个文件的配置项有冲突的时候,用户配置会覆盖系统配置 通常,我们不关心具体的配置文件,而只关心最终生效的配置 通过下面的命令可以查询目前生效的各种配置

shell
npm config ls [-l] [--json]
+
npm config ls [-l] [--json]
+

另外,可以通过下面的命令操作配置

  1. 获取某个配置项
shell
npm config get 配置项
+
npm config get 配置项
+
  1. 设置某个配置项
shell
npm config set 配置项=值
+
npm config set 配置项=值
+
  1. 移除某个配置项
shell
npm config delete 配置项
+
npm config delete 配置项
+

发布包

1.准备工作

  1. 移除淘宝镜像源

命令:npm config delete registry 检查:npm config get registry

  1. 到 npm 官网注册一个账号,并完成邮箱认证
  2. 本地使用 npm cli 进行登录
    1. 使用命令npm login登录
    2. 使用命令npm whoami查看当前登录的账号
    3. 使用命令npm logout注销
  3. 创建工程根目录
  4. 使用 npm init 进行初始化(包名必须是小写) author:bnagbangji 1111@qq.com
  5. 创建 LICENSE 使用刚才使用的协议

2.发布

  1. 开发
  2. 确定版本
  3. 使用命令npm publish完成发布

开源协议

可以通过网站 http://choosealicense.online/appendix/ 选择协议,并复制协议内容

yarn

yarn 简介

yarn 官网:https://www.yarnpkg.com/zh-Hans/ yarn -v 是否安装成功

yarn 是由 Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具,它仍然使用 npm 的 registry,不过提供了全新 CLI 来对包进行管理 过去,yarn 的出现极大的抢夺了 npm 的市场,甚至有人戏言,npm 只剩下一个 registry 了。 之所以会出现这种情况,是因为在过去,npm 存在下面的问题:

  • 依赖目录嵌套层次深:过去,npm 的依赖是嵌套的,这在 windows 系统上是一个极大的问题,由于众所周知的原因,windows 系统无法支持太深的目录
  • 下载速度慢
    • 由于嵌套层次的问题,所以 npm 对包的下载只能是串行的,即前一个包下载完后才会下载下一个包,导致带宽资源没有完全利用
    • 多个相同版本的包被重复的下载
  • 控制台输出繁杂:过去,npm 安装包的时候,每安装一个依赖,就会输出依赖的详细信息,导致一次安装有大量的信息输出到控制台,遇到错误极难查看
  • 工程移植问题:由于 npm 的版本依赖可以是模糊的,可能会导致工程移植后,依赖的确切版本不一致。

针对上述问题,yarn 从诞生那天就已经解决,它用到了以下的手段:

  • 使用扁平的目录结构
  • 并行下载
  • 使用本地缓存
  • 控制台仅输出关键信息
  • 使用 yanr-lock 文件记录确切依赖

不仅如此,yarn 还优化了以下内容:

  • 增加了某些功能强大的命令
  • 让既有的命令更加语义化
  • 本地安装的 CLI 工具可以使用 yarn 直接启动
  • 将全局安装的目录当作一个普通的工程,生成 package.json 文件,便于全局安装移植

全局安装目录: npm root -g yarn 的出现给 npm 带来了巨大的压力,很快,npm 学习了 yarn 先进的理念,不断的对自身进行优化,到了目前的 npm6 版本,几乎完全解决了上面的问题:

  • 目录扁平化
  • 并行下载
  • 本地缓存 (清空缓存:npm chache clean)
  • 使用 package-lock 记录确切依赖
  • 增加了大量的命令别名
  • 内置了 npx,可以启动本地的 CLI 工具
  • 极大的简化了控制台输出

总结 npm6 之后,可以说 npm 已经和 yarn 非常接近,甚至没有差距了。很多新的项目,又重新从 yarn 转回到 npm。 这两个包管理器是目前的主流,都必须要学习。

yarn 的核心命令

  1. 初始化

初始化:yarn init [--yes/-y]

  1. 安装

添加指定包:yarn [global] add package-name [--dev/-D] [--exact/-E] 全局安装    

javascript
yarn global add nodemon
+
yarn global add nodemon
+

安装 package.json 中的所有依赖:yarn install [--production/--prod]

  1. 脚本和本地 CLI

运行脚本:yarn run 脚本名

start、stop、test 可以省略 run

运行本地安装的 CLI:yarn run CLI名

  1. 查询

查看 bin 目录:yarn [global] bin 查询包信息:yarn info 包名 [子字段]yarn info 包名 version 列举已安装的依赖:yarn [global] list [--depth=依赖深度]

yarn 的 list 命令和 npm 的 list 不同,yarn 输出的信息更加丰富,包括顶级目录结构、每个包的依赖版本号

  1. 更新

列举需要更新的包:yarn outdated 更新包:yarn [global] upgrade [包名]

  1. 卸载

卸载包:yarn remove 包名

yarn 的特别礼物

在终端命令上,yarn 不仅仅是对 npm 的命令做了一个改名,还增加了一些原本没有的命令,这些命令在某些时候使用起来非常方便

  1. yarn check

使用yarn check命令,可以验证 package.json 文件的依赖记录和 lock 文件是否一致

这对于防止篡改非常有用

  1. yarn audit

使用yarn audit命令,可以检查本地安装的包有哪些已知漏洞,以表格的形式列出,漏洞级别分为以下几种:

  • INFO:信息级别
javascript
module.exports = function (...args) {
+  // 求和,没有考虑到字符串输入会导致拼接
+  return args.reduce((s, item) => s + item, 0);
+};
+
module.exports = function (...args) {
+  // 求和,没有考虑到字符串输入会导致拼接
+  return args.reduce((s, item) => s + item, 0);
+};
+
  • LOW: 低级别
  • MODERATE:中级别
  • HIGH:高级别
  • CRITICAL:关键级别
  1. yarn why

使用yarn why 包名命令,可以在控制台打印出为什么安装了这个包,哪些包会用到它

  1. yarn create

非常有趣的命令

今后,我们会学习一些脚手架,所谓脚手架,就是使用一个命令来搭建一个工程结构

过去,我们都是使用如下的做法:

  1. 全局安装脚手架工具
  2. 使用全局命令搭建脚手架

由于大部分脚手架工具都是以create-xxx的方式命名的,比如 react 的官方脚手架名称为create-react-app

因此,可以使用yarn create命令来一步完成安装和搭建

例如:

shell
yarn create react-app my-app
+# 等同于下面的两条命令
+yarn global add create-react-app
+create-react-app my-app
+
yarn create react-app my-app
+# 等同于下面的两条命令
+yarn global add create-react-app
+create-react-app my-app
+

其他包管理器

cnpm

官网地址:https://npm.taobao.org/

为解决国内用户连接 npm registry 缓慢的问题,淘宝搭建了自己的 registry,即淘宝 npm 镜像源

过去,npm 没有提供修改 registry 的功能,因此,淘宝提供了一个 CLI 工具即 cnpm,它支持除了npm publish以外的所有命令,只不过连接的是淘宝镜像源

如今,npm 已经支持修改 registry 了,可能 cnpm 唯一的作用就是和 npm 共存,即如果要使用官方源,则使用 npm,如果使用淘宝源,则使用 cnpm

javascript
npm install -g cnpm --registry=https://registry.npm.taobao.org
+
npm install -g cnpm --registry=https://registry.npm.taobao.org
+

nvm

nvm 并非包管理器,它是用于管理多个 node 版本的工具

在实际的开发中,可能会出现多个项目分别使用的是不同的 node 版本,在这种场景下,管理不同的 node 版本就显得尤为重要

nvm 就是用于切换版本的一个工具

1.下载和安装

最新版下载地址:https://github.com/coreybutler/nvm-windows/releases

下载 nvm-setup.zip 后,直接安装 一路下一步,不要改动安装路径(开发类工具尽量不该动)

2.使用 nvm

nvm 提供了 CLI 工具,用于管理 node 版本

管理员运行输入 nvm,以查看各种可用命令 nvm arch:打印系统版本和默认 node 架构类型 nvm install 8.5.4:nvm 安装指定的 node 版本 nvm list :列出目前电脑上使用的以及已经安装过的那些 node 版本 npm list available:node 版本

为了加快下载速度,建议设置淘宝镜像 node 淘宝镜像:https://npm.taobao.org/mirrors/node/ npm 淘宝镜像:https://npm.taobao.org/mirrors/npm/

nvm node _mirror https://npm.taobao.org/mirrors/node/ nvm npm_mirror https://npm.taobao.org/mirrors/npm/ 查看全局安装包:npm -g list --depth=0 切换 node 版本:nvm use 10.18.0 看 node 版本:node -v 卸载:nvm uninstall 10.18.0

pnpm

pnpm 是一种新起的包管理器,从 npm 的下载量看,目前还没有超过 yarn,但它的实现方式值得主流包管理器学习,某些开发者极力推荐使用 pnpm

从结果上来看,它具有以下优势:

  1. 目前,安装效率高于 npm 和 yarn 的最新版
  2. 极其简洁的 node_modules 目录
  3. 避免了开发时使用间接依赖的问题(之前的包管理器可以使用间接依赖不报错)pnpm 没有把间接依赖直接放入 node_modules 里面
  4. 能极大的降低磁盘空间的占用
  5. 使用缓存

1.安装和使用

全局安装 pnpm

shell
npm install -g pnpm
+
npm install -g pnpm
+

之后在使用时,只需要把 npm 替换为 pnpm 即可

如果要执行安装在本地的 CLI,可以使用 pnpx,它和 npx 的功能完全一样,唯一不同的是,在使用 pnpx 执行一个需要安装的命令时,会使用 pnpm 进行安装

比如npx mocha执行本地的mocha命令时,如果mocha没有安装,则 npx 会自动的、临时的安装 mocha,安装好后,自动运行 mocha 命令 类似:npx create-react-app my-app 临时下载 create-react-app,然后自动运行命令

2.pnpm 原理

  1. 同 yarn 和 npm 一样,pnpm 仍然使用缓存来保存已经安装过的包,以及使用 pnpm-lock.yaml 来记录详细的依赖版本

缓存位于工程所在盘的根目录,所以位置不固定

  1. 不同于 yarn 和 npm, pnpm 使用符号链接和硬链接(可将它们想象成快捷方式)的做法来放置依赖,从而规避了从缓存中拷贝文件的时间,使得安装和卸载的速度更快
  2. 由于使用了符号链接和硬链接,pnpm 可以规避 windows 操作系统路径过长的问题,因此,它选择使用树形的依赖结果,有着几乎完美的依赖管理。也因为如此,项目中只能使用直接依赖,而不能使用间接依赖

3.注意事项

由于 pnpm 会改动 node_modules 目录结构,使得每个包只能使用直接依赖,而不能使用间接依赖,因此,如果使用 pnpm 安装的包中包含间接依赖,则会出现问题(现在不会了,除非使用了绝对路径)

由于 pnpm 超高的安装卸载效率,越来越多的包开始修正之前的间接依赖代码

bower

浏览器端

过时了

+ + + + + \ No newline at end of file diff --git a/front-end-engineering/engineering-onepage.html b/front-end-engineering/engineering-onepage.html new file mode 100644 index 00000000..8ea5cbe8 --- /dev/null +++ b/front-end-engineering/engineering-onepage.html @@ -0,0 +1,482 @@ + + + + + + 前端工程化 OnePage | Sunny's blog + + + + + + + + +
Skip to content
On this page

前端工程化 OnePage

为什么会出现前端工程化

模块化:意味着 JS 总算可以开发大型项目(解决 JS 的复用问题)

组件化:解决 HTML、CSS、JS 的复用问题

工程化:前端项目越来越复杂、前端项目模块(组件)越来越多

为什么前端项目越来越复杂?

在书写前端项目的时候,可能会涉及到使用其他的语言,typescript、less/sass、coffeescript,涉及到编译

在部署的时候,还需要对代码进行丑化、压缩

代码优化:为 CSS 代码添加兼容性前缀

前端项目模块(组件)越来越多?

专门有一个 node_modules 来管理这些可以复用的代码。

前端工程化的出现,归根结底,其实就是要解决开发环境和生产环境不一致的问题。

nodejs对CommonJS的实现

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

nodejs中导入模块,使用相对路径,并且必须以./或../开头,浏览器可以省略./,nodejs不行

  1. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
javascript
 (function(){
+     //模块中的代码
+ })()
+
 (function(){
+     //模块中的代码
+ })()
+
  1. 为了保证顺利的导出模块内容,nodejs做了以下处理
    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
javascript
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+

面试

javascript
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+

经典面试题 util.js

javascript
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+

index.js

javascript
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+

经验:对exports赋值无意义,建议用module.exports

  1. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

过去,JS很难编写大型应用,因为有以下两个问题:

  1. 全局变量污染
  2. 难以管理的依赖关系

这些问题,都导致了JS无法进行精细的模块划分,因为精细的模块划分会导致更多的全局污染以及更加复杂的依赖关系 于是,先后出现了两大模块化标准,用于解决以上两个问题:

  • CommonJS
  • ES6 Module

注意:上面提到的两个均是模块化标准,具体的实现需要依托于JS的宿主环境

nodejs

node环境支持 CommonJS 模块化标准,所以,要使用 CommonJS,必须要先安装node

官网地址:https://nodejs.org/zh-cn/

nodejs直接运行某个js文件,该文件被称之为入口文件

nodejs遵循EcmaScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在nodejs中使用EcmaScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在nodejs中使用浏览器的 web api,例如:dom对象、window对象、document对象等

CommonJS标准和使用

node中的所有代码均在CommonJS规范下运行 具体规范如下:

  1. 一个JS文件即为一个模块,一个模块就是一个相对独立的功能,模块中的所有全局代码产生的变量、函数,均不会对全局造成任何污染,仅在模块内使用
  2. 如果一个模块需要暴露一些数据或功能供其他模块使用,需要使用代码module.exports = xxx,该过程称之为模块的导出
  3. 如果一个模块需要使用另一个模块导出的内容,需要使用代码require("模块路径")
    1. 路径必须以./../开头
    2. 如果模块文件后缀名为.js,可以省略后缀名
    3. require函数返回的是模块导出的内容
  4. 模块具有缓存,第一次导入模块时会缓存模块的导出,之后再导入同一个模块,直接使用之前缓存的结果。

同时也解决了JS的两个问题 浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module

原理: require中的伪代码

javascript
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
  1. 根据传递的模块路径,得到模块完整的绝对路径。因为绝对路径不会重复。

  2. require引入相同模块会有缓存,不会重复加载

  3. 如果没有缓存。真正运行模块代码的辅助函数

javascript
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
  1. 返回 module.exports

this === exports === module.exports === {}

下面的代码执行结果是什么?

javascript
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+

ES6 module

由于种种原因,CommonJS标准难以在浏览器中实现,因此一直在浏览器端一直没有合适的模块化标准,直到ES6标准出现 ES6规范了浏览器的模块化标准,一经发布,各大浏览器厂商纷纷在自己的浏览器中实现了该规范

模块的引入:浏览器使用以下方式引入一个ES6模块文件

html
<script src="JS文件" type="module">
+
<script src="JS文件" type="module">
+
  1. 模块的导出分为两种,基本导出(具名导出)默认导出基本导出导出的是该对象的某个属性,默认导出导出的是该对象的特殊属性default
javascript
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
  1. 基本导出可以有多个,默认导出只能有一个
  2. 基本导出必须要有名字,默认导出由于有特殊名字,所以可以不用写名字
javascript
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
  1. 模块的导入
javascript
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+

注意

  1. ES6 module 采用依赖预加载模式,所有模块导入代码均会提升到代码顶部
  2. 不能将导入代码放置到判断、循环中
  3. 导入的内容放置到常量中,不可更改
  4. ES6 module 使用了缓存,保证每个模块仅加载一次

请对比一下CommonJS和ES Module

  1. CMJ 是社区标准,ESM 是官方标准
  2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
  3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
  4. CMJ 是动态的依赖,ESM 既支持动态,也支持静态
  5. ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值

CommonJS是社区模块化标准,node环境支持该标准。它不产生新的语法,是使用API实现的,本质是把要加载的模块放到一个闭包中执行(这里可以详细的阐述)。因此,node环境中的所有JS文件在执行的时候,都是放到一个函数环境中执行的,这对使得模块内部可以访问函数的参数,如exports、module、__dirname等,而且对this的指向也会有影响。CommonJS是动态的模块化标准,这就意味着依赖关系是在运行过程中确定的,同时也意味着在导入导出模块时,并不限制书写的位置。 ES Module是官方模块化标准,目前node和浏览器均支持该标准,它引入了新的语法。ES Module是静态的模块化标准,在模块执行前,会递归确定所有依赖关系,然后加载所有文件,加载完成后再运行,这在浏览器环境下会产生多次请求。由于使用的是静态依赖,因此,它要求导入导出的代码必须放置到顶层,因为只有这样才能在代码运行前就确定依赖关系。ES Module导入模块时会将导入的结果绑定到标识符中,该标识符是一个常量,不可更改。 CommonJS和ES Module都使用了缓存,保证每个模块仅执行一次。

1.讲讲模块化规范

2.import和require的区别

3.require是如何解析路径的

1、ESM是编译时导出结果,CommonJS是运⾏时导出结果

2、ESM是导出引⽤,CommonJS是导出⼀个值

3、ESM⽀持异步,CommonJS只⽀持同步

下面的模块导出了什么结果?

javascript
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+

对前端工程化,模块化,组件化的理解?

这三者中,模块化是基础,没有模块化,就没有组件化和工程化

模块化的出现,解决了困扰前端的两大难题:全局污染问题和依赖混乱问题,从而让精细的拆分前端工程成为了可能。

工程化的出现,解决了前端开发环境和生产环境要求不一致的矛盾。在开发环境中,我们希望代码使用尽可能的细分,代码格式尽可能的统一和规范,而在生产环境中,我们希望代码尽可能的被压缩、混淆,尽可能的优化体积。工程化的出现,就是为了解决这一矛盾,它可以让我们舒服的在开发环境中书写代码,然后经过打包,生成最合适的生产环境代码,这样就解放了开发者的精力,让开发者把更多的注意力集中在开发环境上即可。

组件化开发是一些前端框架带来的概念,它把一个网页,或者一个站点,甚至一个完整的产品线,划分为多个小的组件,组件是一个可以复用的单元,它包含了一个某个区域的完整功能。这样一来,前端便具备了开发复杂应用的能力。

webpack 中的 loader 属性和 plugins 属性的区别是什么?

它们都是 webpack 功能的扩展点。

loader 是加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等

plugins 是插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等

webpack 的核心概念都有哪些?

参考答案:

  • loader 加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
  • plugin 插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
  • module 模块。webpack 将所有依赖均视为模块,无论是 js、css、html、图片,统统都是模块
  • entry 入口。打包过程中的概念,webpack 以一个或多个文件作为入口点,分析整个依赖关系。
  • chunk 打包过程中的概念,一个 chunk 是一个相对独立的打包过程,以一个或多个文件为入口,分析整个依赖关系,最终完成打包合并
  • bundle webpack 打包结果
  • tree shaking 树摇优化。在打包结果中,去掉没有用到的代码。
  • HMR 热更新。是指在运行期间,遇到代码更改后,无须重启整个项目,只更新变动的那一部分代码。
  • dev server 开发服务器。在开发环境中搭建的临时服务器,用于承载对打包结果的访问

ES6 中如何实现模块化的异步加载?

使用动态导入即可,导入后,得到的是一个 Promise,完成后,得到一个模块对象,其中包含了所有的导出结果。

说一下 webpack 中的几种 hash 的实现原理是什么?

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?

参考答案:

不会。它跟关联的内容是否有变化有关系,如果没有变化,hash 就不会变。具体来说,contenthash 和具体的打包文件内容有关,chunkhash 和某一 entry 为起点的打包过程中涉及的内容有关,hash 和整个工程所有模块内容有关。

webpack 中是如何处理图片的?

参考答案:

webpack 本身不处理图片,它会把图片内容仍然当做 JS 代码来解析,结果就是报错,打包失败。如果要处理图片,需要通过 loader 来处理。其中,url-loader 会把图片转换为 base64 编码,然后得到一个 dataurl,file-loader 则会将图片生成到打包目录中,然后得到一个资源路径。但无论是哪一种 loader,它们的核心功能,都是把图片内容转换成 JS 代码,因为只有转换成 JS 代码,webpack 才能识别。

webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?

说明:这道题的表述是有问题的,webpack 本身并不打包 html,相反,它如果遇到 html 代码会直接打包失败,因为 webpack 本身只能识别 JS。之所以能够打包出 html 文件,是因为插件或 loader 的作用,其中,比较常见的插件是 html-webpack-plugin。所以这道题的正确表述应该是:「html-webpack-plugin 打包出来的 html 为什么 style 放在头部 script 放在底部?」

webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?

要配置 CDN,有两个步骤:

  1. 在 html 模板中直接加入 cdn 引用
  2. 在 webpack 配置中,加入externals配置,告诉 webpack 不要打包其中的模块,转而使用全局变量

若要在开发环境中不使用 CDN,只需根据环境变量判断不同的环境,进行不同的打包处理即可。

  1. 在 html 模板中使用 ejs 模板语法进行判断,只有在生产环境中引入 CDN
  2. 在 webpack 配置中,可以根据process.env中的环境变量进行判断是否使用externals配置
  3. package.json脚本中设置不同的环境变量完成打包或开发启动。

介绍一下 webpack4 中的 tree-shaking 的工作流程?

推荐阅读:https://tsejx.github.io/webpack-guidebook/principle-analysis/operational-principle/tree-shaking

说一下 webpack loader 的作用是什么?

参考答案:

用于转换代码。有时是因为 webpack 无法识别某些内容,比如图片、css 等,需要由 loader 将其转换为 JS 代码。有时是因为某些代码需要被特殊处理,比如 JS 兼容性的处理,需要由 loader 将其进一步转换。不管是什么情况,loader 的作用只有一个,就是转换代码。

在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

export 和 export default 的区别是什么?

参考答案:

export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,比如变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出

export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出。

webpack 打包原理是什么?

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack 热更新原理是什么?

参考答案:

当开启热更新后,页面中会植入一段 websocket 脚本,同时,开发服务器也会和客户端建立 websocket 通信,当源码发生变动时,webpack 会进行以下处理:

  1. webpack 重新打包
  2. webpack-dev-server 检测到模块的变化,于是通过 webscoket 告知客户端变化已经发生
  3. 客户端收到消息后,通过 ajax 发送请求到开发服务器,以过去打包的 hash 值请求服务器的一个 json 文件
  4. 服务器告诉客户端哪些模块发生了变动,同时告诉客户端这次打包产生的新 hash 值
  5. 客户端再次用过去的 hash 值,以 JSONP 的方式请求变动的模块
  6. 服务器响应一个函数调用,用于更新模块的代码
  7. 此时,模块代码已经完成更新。客户端按照之前的监听配置,执行相应模块变动后的回调函数。

如何优化 webpack 的打包速度?

参考答案:

  1. noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  2. externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  3. 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  4. 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  5. 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  6. 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库

webpack 如何实现动态导入?

参考答案:

当遇到代码中包含动态导入语句时,webpack 会将导入的模块及其依赖分配到单独的一个 chunk 中进行打包,形成单独的打包结果。而动态导入的语句会被编译成一个普通的函数调用,该函数在执行时,会使用 JSONP 的方式动态的把分离出去的包加载到模块集合中。

说一下 webpack 有哪几种文件指纹

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

常用的 webpack Loader 都有哪些?

参考答案:

  • cache-loader:启用编译缓存
  • thread-loader:启用多线程编译
  • css-loader:编译 css 代码为 js
  • file-loader:保存文件到输出目录,将文件内容转换成文件路径
  • postcss-loader:将 css 代码使用 postcss 进行编译
  • url-loader:将文件内容转换成 dataurl
  • less-loader:将 less 代码转换成 css 代码
  • sass-loader:将 sass 代码转换成 css 代码
  • vue-loader:编译单文件组件
  • babel-loader:对 JS 代码进行降级处理

说一下 webpack 常用插件都有哪些?

参考答案:

  • clean-webpack-plugin:清除输出目录
  • copy-webpack-plugin:复制文件到输出目录
  • html-webpack-plugin:生成 HTML 文件
  • mini-css-extract-plugin:将 css 打包成单独文件的插件
  • HotModuleReplacementPlugin:热更新的插件
  • purifycss-webpack:去除无用的 css 代码
  • optimize-css-assets-webpack-plugin:优化 css 打包体积
  • uglify-js-plugin:对 JS 代码进行压缩、混淆
  • compression-webpack-plugin:gzip 压缩
  • webpack-bundle-analyzer:分析打包结果

使用 babel-loader 会有哪些问题,可以怎样优化?

参考答案:

  1. 如果不做特殊处理,babel-loader 会对所有匹配的模块进行降级,这对于那些已经处理好兼容性问题的第三方库显得多此一举,因此可以使用 exclude 配置排除掉这些第三方库
  2. 在旧版本的 babel-loader 中,默认开启了对 ESM 的转换,这样会导致 webpack 的 tree shaking 失效,因为 tree shaking 是需要保留 ESM 语法的,所以需要关闭 babel-loader 的 ESM 转换,在其新版本中已经默认关闭了。

babel 是如何对 class 进行编译的?

参考答案:

本质上就是把 class 语法转换成普通构造函数定义,并做了以下处理:

  1. 增加了对 this 指向的检测
  2. 将原型方法和静态方法变为不可枚举
  3. 将整个代码放到了立即执行函数中,运行后返回构造函数本身

释一下 babel-polyfill 的作用是什么?

说明:

babel-polyfill 已经是一个非常古老的项目了,babel 从 7.4 版本开始已不再支持它,转而使用更加强大的 core-js,此题也适用于问「core-js 的作用是什么」

解释一下 less 的&的操作符是做什么用的?

参考答案:

&符号后面的内容会和父级选择器合并书写,即中间不加入空格字符

webpack proxy 工作原理,为什么能解决跨域?

说明:

严格来说,webpack 只是一个打包工具,它并没有 proxy 的功能,甚至连服务器的功能都没有。之所以能够在 webpack 中使用 proxy 配置,是因为它的一个插件,即 webpack-dev-server 的能力。

所以,此题应该问做:「webpack-dev-server 工作原理,为什么能解决跨域?」

组件发布的是不是所有依赖这个组件库的项目都需要升级?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)

  1. 公共模块 比如某些多页应用会有多个入口,从而形成多个 chunk,而这些 chunk 中用到了一些公共模块,为了减少整体的包体积,可以使用 splitchunksplugin 将公共模块分离出来。 可以配置 minChunks 来指定被多少个 chunk 引用时进行分包
  2. 并行下载 由于 HTML5 支持 defer 和 async,因此可以同时下载多个 JS 文件以充分利用带宽。如果打包结果是一个很大的文件,就无法利用到这一点。 可以利用 splitchunks 插件将文件进行拆分,通过配置 maxSize 属性指定包体积达到多大时进行拆分

描述一下 webpack 的构建流程?(CVTE)

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

解释一下 webpack 插件的实现原理?(CVTE)

参考答案:

本质上,webpack 的插件是一个带有apply函数的对象。当 webpack 创建好 compiler 对象后,会执行注册插件的 apply 函数,同时将 compiler 对象作为参数传入。

在 apply 函数中,开发者可以通过 compiler 对象监听多个钩子函数的执行,不同的钩子函数对应 webpack 编译的不同阶段。当 webpack 进行到一定阶段后,会调用这些监听函数,同时将 compilation 对象传入。开发者可以使用 compilation 对象获取和改变 webpack 的各种信息,从而影响构建过程。

有用过哪些插件做项目的分析吗?(CVTE)

参考答案:

用过 webpack-bundle-analyzer 分析过打包结果,主要用于优化项目打包体积

什么是 babel,有什么作用?

参考答案:

babel 是一个 JS 编译器,主要用于将下一代的 JS 语言代码编译成兼容性更好的代码。

它其实本身做的事情并不多,它负责将 JS 代码编译成为 AST,然后依托其生态中的各种插件对 AST 中的语法和 API 进行处理

解释一下 npm 模块安装机制是什么?

参考答案:

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。 gruntgulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。 webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。 所以总结一下:

  • 从构建思路来说

gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

  • 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

3.有哪些常见的Loader?他们是解决什么问题的?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

4.有哪些常见的Plugin?他们是解决什么问题的?

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

5.Loader和Plugin的不同?

不同的作用

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。
  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。


7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。 编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。 相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

11.怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可,这里不再赘述 多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

12.npm打包时需要注意哪些?如何利用webpack来更好的构建?

Npm是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建。 NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

  1. CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
  2. 输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
  3. Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
  4. 不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
  5. 对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:
javascript
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+

13.如何在vue项目中实现按需加载?

Vue UI组件库的按需加载 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。 不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了。

javascript
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+

单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。 通过import()语句来控制加载时机,webpack内置了对于import()的解析,会将import()中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import()语句时,会去加载Chunk对应生成的文件。import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill

能说说webpack的作用吗?

  • 模块打包(静态资源拓展)。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容(翻译官loader)。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展(plugins)。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

为什么要打包呢?

逻辑多、文件多、项目的复杂度高了,所以要打包 例如: 让前端代码具有校验能力===>出现了ts css不好用===>出现了sass、less webpack可以解决这些问题

能说说模块化的好处吗?

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化打包方案知道啥,可以说说吗?

1、commonjs commonjs 是 Node 中的模块规范,通过 require 及 exports 进行导入导出 commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。 总结:commonjs是用在服务器端的,同步的,如nodejs2、AMD AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。 总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs3、CMD CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs4、esm esm 是 tc39 对于 ESMAScript 的模块话规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。 它使用 import/export 进行模块导入导出. 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。 export default命令,为模块指定默认输出

那ES6 模块与 CommonJS 模块的差异有哪些呢?

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

webpack的编译(打包)流程说说

  • 初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;
  • 开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
  • 确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
  • 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;
  • 输出完成:输出所有的chunk到文件系统;

说一下 Webpack 的热更新原理吧?

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为** HMR**。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS(无线路由)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

路由懒加载的原理

https://juejin.cn/post/6844904180285456398

路由懒加载?

当刚运行项目的时候,发现刚进入页面,就将所有的js文件和css文件加载了进来,这一进程十分的消耗时间。 如果打开哪个页面就对应的加载响应页面的js文件和css文件,那么页面加载速度会大大提升。

懒加载的好处是什么?

懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。

怎么使用的路由懒加载?

使用到的是es6的import语法,可以实现动态导入

路由懒加载的原理是什么

通过Webpack编译打包后,会把每个路由组件的代码分割成一一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。

作用就是webpack在打包的时候,对异步引入的库代码进行代码分割时(需要配置webpack的SplitChunkPlugin插件),为分割后的代码块取得名字 Vue中运用import的懒加载语句以及webpack的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。

npx

  1. 运行本地命令

使用npx 命令时,它会首先从本地工程的node_modules/.bin目录中寻找是否有对应的命令

例如:

shell
npx webpack
+
npx webpack
+

上面这条命令寻找本地工程的node_modules/.bin/webpack

如果将命令配置到package.jsonscripts中,可以省略npx

  1. 临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除

例如:

shell
npx prettyjson 1.json
+
npx prettyjson 1.json
+

npx会下载prettyjson包到临时目录,然后运行该命令

如果命令名称和需要下载的包名不一致时,可以手动指定报名

例如@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令

shell
npx -p @vue/cli vue create vue-app
+
npx -p @vue/cli vue create vue-app
+
  1. npm init

npm init通常用于初始化工程的package.json文件

除此之外,有时也可以充当npx的作用

shell
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+

git 指令

git fetch git fetch origin 分支 git rebase FETCH_HEAD 本地master更新到最新:git fetch origin master; git rebase FETCH_HEAD

git 和 svn 的区别 经常使用的 git 命令? git pull 和 git fetch 的区别 git rebase 和 git merge 的区别 git rebase和merge git分支管理、分支开发 git回退操作 git reset 和 git reverse区别?你还用过什么别的指令? git遇到冲突怎么进行解决

git-flow git reset --hard 版本号 git 如何合并分支; git 中,提交了 a, 然后又提交了 b, 如何撤回 b ? git merge、git rebase的区别 假设 master 分支切出了 test 分支,供所有人合代码测试, 然后我们 从稳定的 master 切出 一个 feature 分支进行开发, 现在 我们开发完了 feature 分支并将其merge 到 test 分支进行测试, 测试过程中出现一些问题,由于疏忽你;忘记切回 feature ,而是直接在 test 分支进行了开发 导致: test 分支优先于你的featuce 分支,此时你应该怎么特 test 分支的修改带入 feature

知道git的原理吗?说说他的原理吧

Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作 Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中那些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。 Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。 Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库

package.json 字段

name npm 包的名字,必须是一个小写的单词,可以包含连字符-和下划线_,发布时必填。

version npm 包的版本。需要遵循语义化版本格式x.x.x。

description npm 包的描述。

keywords npm 包的关键字。

homepage npm 包的主页地址。

bugs npm 包问题反馈的地址。

license 为 npm 包指定许可证

author npm 包的作者

files npm 包作为依赖安装时要包括的文件,格式是文件正则的数组。也可以使用 npmignore 来忽略个别文件。 以下文件总是被包含的,与配置无关 package.json README.md CHANGES / CHANGELOG / HISTORY LICENCE / LICENSE 以下文件总是被忽略的,与配置无关 .git .DS_Store node_modules .npmrc npm-debug.log package-lock.json ...

main 指定npm包的入口文件。

module 指定 ES 模块的入口文件。

typings 指定类型声明文件。

bin 开发可执行文件时,bin 字段可以帮助你设置链接,不需要手动设置 PATH。

repository npm 包托管的地方

scripts 可执行的命令。

dependencies npm包所依赖的其他npm包

devDependencies npm 包所依赖的构建和测试相关的 npm 包.

peerDependencies 指定 npm 包与主 npm 包的兼容性,当开发插件时是需要的.

engines 指定 npm 包可以使用的 Node 版本.

  • CSRF: 文件流steam(字节2020校招)
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
  • 安全相关:XSS,CSRF(字节2020校招)
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
  • 数据劫持(字节2020校招)
  • CSP,阻止iframe的嵌入(字节2020校招)
  • 进程和线程的区别知道吗?他们的通信方式有哪些?(字节2020校招)
  • js为什么是单线程(字节2020校招 + 京东2020校招)
  • section是用来做什么的(字节2020社招)
  • 了解SSO吗(字节2020社招)
  • 服务端渲染的方案有哪些?next.js和nuxt.js的区别(字节2020社招)
  • cdn的原理(字节2020社招)
  • JSBridge的原理(字节2020社招)
  • 了解过Serverless,什么是Serverless(字节2020社招)
无服务器~
+云计算?
+
无服务器~
+云计算?
+
  • 你还知道哪些新的前端技术(微前端、Low Code、可视化、BI、BFF)(字节2020社招)
  • 权限系统设计模型(DAC、MAC、RBAC、ABAC)(字节2020社招)
  • 什么是微前端(字节2020社招)
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
  • Node了解多少,有实际的项目经验吗(字节2020社招)
  • Linux了解多少,linux上查看日志的命令是什么(字节2020社招)
  • 强缓存和协商缓存(腾讯2020校招)
  • node可以做大型服务器吗,为什么(腾讯2020校招)
  • 用express还是koa(腾讯2020校招)
  • 用node写过哪些接口,登录验证是怎么做的(腾讯2020实习)
  • 清理node模块缓存的方法(腾讯2020校招)
  • 模仿一个xss的场景(从攻击到防御的流程)(腾讯2020校招)
  • Event loop知道吗?Node的eventloop的生命周期(京东2020校招)
  • JWT单点登录实现原理(小米2020校招)
  • 博客项目为什么使用Mysql?(小米2020校招)
  • 如何看待Node.js的中间件?它的好处是什么?(小米2020校招)
  • 为什么Node.js支持高并发请求?(小米2020校招)
  • 博客项目是SSR渲染还是客户端渲染?(小米2020校招)
  • web socket 和 socket io(广州长视科技2020校招)
  • 是否使⽤过webpack(不是通过vuecli的⽅式)(掌数科技2020社招)
  • 是否了解过express或者koa(掌数科技2020社招)
  • node如何实现热更新
(橙智科技2020社招)
  • Node 知道哪些(express, sequelize,一面也问了)(微医云2020校招)
  • 用node做过什么(微医云2020校招)
  • Rest接口(微医云2020校招)
  • Linux指令
  • 测试工具什么什么
  • Linux指令
  • 测试工具什么什么
  • 浏览器和node环境的事件循环机制。(流利说2020校招)
  • https建立连接的过程。(流利说2020校招)
  • 讲述下http缓存机制,强缓存,协商缓存。(流利说2020校招)
  • jwt原理(北京蒸汽记忆2020校招)
  • 需求:实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后退时正确响应。给出你的技术实现方案?
  • 对Node的优点和缺点提出了自己的看法
  • nodejs的适用场景
  • (如果会用node)知道route, middleware, cluster, nodemon, pm2, server-side rendering么?
  • 解释一下 Backbone 的 MVC 实现方式?
  • 什么是“前端路由”?什么时候适合使用“前端路由”? “前端路由”有哪些优点和缺点?

操作系统:进程线程、死锁条件 Redis的底层数据结构 mongo的实务说一下, redis缓存机制 happypack 原理 【多进程打包 又讲了下进程和线程的区别】

  • koa 的原理 与express 的对比
  • 非js写的Node.js模块是如何使用Node.js调用的 【代码转换过程】 除了都会哪些后端语言 【一开始说自己用Node.js,面试官又问有没有其他的,楼主大学学过java,不过没怎么实践过】
  • Mysql的存储引擎 【这个直接不知道了 TAT】
  • 两个场景设计题感觉自己的设计方案还是有瑕疵的不是面试官想要的,并且问到mysql存储引擎直接无言以对了,感觉自己知识的广度还是有一定欠缺。最后问到性能优化正好自己之前优化过自己的博客网站做过相关的实践
  • koa了解吗

koa洋葱圈模型运行机制 express和koa区别 nginx原理 正向代理与反向代理 手写中间件测试请求时间

  • 进程和线程的区别

mongo的实务

node模块加载机制(require()原理)

路径转变,缓存

koa用的多吗

redis缓存机制是啥

8、 mysql如何创建索引 9、 mysql如何处理博客的点赞问题

完成prosimeFy babel 原理,class 是转换成什么 https://juejin.cn/post/6844904024928419848

javascript
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
javascript
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+

进程线程

  1. 页面缓存cache-control

  2. node写过东西吗

  3. node模块,path模块

  4. 对比一下commonjs模块化和es6 module

  5. 两个文件export的时候导出了相同的变量,避免重复--as

  6. exports和module.export的区别

  7. http和https

  8. 非对称加密和对称加密

  9. 打乱一个数组Math.random()的范围

  10. 问的特别全面,基础几乎全问

  11. 网站安全,CSRF,XSS,SQL注入攻击

  12. token是做什么的

node koa \4. 由于简历写了了解Nodejs的底层原理。问了Node.js内存泄漏和如何定位内存泄漏 常见的内存场景:1. 闭包 2. http.globalAgent 开启keepAlive的时候,未清除事件监听 定位内存泄漏:1. heapdump 2. 用chromedev 生成三次内存快照 \5. Node.js垃圾回收的原理 \4. 客户端发请求给服务端发生了什么 \6. 你知道MySQL和MongDB区别吗 \7. 你平时怎么使用nodeJS的 \8. restful风格规范是什么 新生代(from空间,to空间),老生代(标记清除,引用计数) NodeJs的EventLoop和V8的有什么区别 Node作为服务端语言,跟其他服务端语言比有什么特性?优势和劣势是什么? Mysql接触过?跨表查询外键索引(就知道个关键字所以没往下问了) 1、介绍一下项目的难点以及怎么解决的 5、移动端的业务有做过吗? 6、介绍一下你对中间件的理解7、怎么保证后端服务稳定性,怎么做容灾8、怎么让数据库查询更快9、数据库是用的什么?10、为什么用mysql1、如何用Node开启一个服务器 2、你的项目当中哪些地方用到了Node,为什么这个地方要用Node处理

  • 后端语言,涉及各种类型,主要都是TS,Java等,nodeJS作为动态语言,你怎么看?
  • 怎么理解后端,后端主要功能是做什么?
  • 校验器怎么做的?
  • 你对校验器做了什么封装?你用的这个库提供哪些功能?怎么实现两个关联参数校验?
  1. 进程与线程的概念
  2. 进程和线程的区别
  3. 浏览器渲染进程的线程有哪些
  4. 进程之前的通信方式
  5. 僵尸进程和孤儿进程是什么?
  6. 死锁产生的原因?如果解决死锁的问题?
  7. 如何实现浏览器内多个标签页之间的通信?
  8. 对Service Worker的理解

Mysql两种引擎

· 数据库表编码格式,UTF8和GBK区别

· 一个表里面姓名和学号两列,一行sql查询姓名重复的信息

· 防范XSS攻击

· 过滤或者编码哪些字符

  1. export和module.export区别
  2. Comonjs和es6 import区别

· Video标签可以播放的视频格式

  1. JWT
  2. 中间件有了解吗
  3. 进程和线程是什么
  4. nodejs的process的nexttick 为什么比微任务快
  5. 进程之间如何实现数据通信和数据同步
  6. node服务层如何封装接口
  7. 深挖底层原理 一直在拓展问

网易:谈谈同步10和异步10和它们的应用场景。https://zhuanlan.zhihu.com/p/115912936 队列解决:实现bfs返回树中值为value的节点 网易:

javascript
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+

你知道MySQL和MongDB区别吗

  1. · 如何防止sql注入

3 如何判断请求的资源已经全部返回。(从http角度,不要从api角度)

8 异步缓冲队列实现。例如需要发送十个请求,但最多只支持同时发送五个请求,需要等有请求完成后再进行剩余请求的发送。

2.用写websocket进度条以及使用场景

3.写回到上次浏览的位置(我说的记住位置存在sessionstorage/localstorage里),他问窗口大小变动怎么办,我说获取当前窗口大小等比例缩放scolltop。。。

6.正向代理,反向代理以及他们的应用场景

针对博客问了一下session,讲了cookie、session以及jwt,最后问如果不用cookie和localstorage,怎么做校验,比如匿名用户

你觉得你做过最牛逼的事情是什么

如果你有一天成了技术大牛,你最想做的事情是什么

介绍了下简历里各个项目的背景

react native ;node

博客技术文章怎么写的

monrepo技术栈,好处

node 中间件原理

发布订阅once加一个标识

是否能用于生产环境

父子组件执行顺序的原因

10w数据:虚拟滚动,bff(缓存,解决渲染问题)——新元

•如果你的技术方案和一起合作的同学不一致,你该怎么说服他? • 压力 •我觉得你说的这个方案不可行 • 稳定性 • 看到你家在深圳,计划长期在北京发展吗?

  • 个轮播组件的API • 案例问题 •假设我们现在面临着着会怎么排查?-个故障,部分用户反馈无法进入登录页面,你将 会怎么排查?

egg又不是不可替代的,一个bff而已,egg团队去开发artus了,Artus是更上层的框架,还是基于egg的

node里面for循环处理10亿数据,出现两个问题,js卡死,内存不够

为什么老是有题目处理101数据

我跟他讲:分开循环解决js卡死,把结果用node里面的fs.writefile写入本地文件

解決内存不够

bam就是浏览器上作为一个代理 但实际上mock数据还是在那个「API管理平台上的」

[]==![] 是true

[]== [] 是false

双等号 两边数据类型一样 如果是引用类型,就判断引用地址是否相同,两边数据类型不一样便会隐式转换。

有一点反直觉

如何在一个图片长列表页使用懒加载进行性能优化 ,然后说了图片替换和监听窗口滚动实现图片按需加载,那么如何实现监听呢?,然后扯到节流监听滚动条变化执行函数 输入框要求每次输入都要有联想功能,即随时向后台请求数据,但是频繁输入时,如何防止频繁多次请求 你对操作系统,编译原理的理解

  • 设计一个扫码登录的逻辑,为什么你扫码就可以登录你自己号,同时有这么多二维码
  • 讲清楚服务端,PC端,和手机端的逻辑
  • 怎么保存登陆态

项目权限管理(高低权限怎么区分,如果网络传输中途被人改包,怎么防止这一潜在危险) 在淘宝里面,商品数据量很大,前端怎么优化使加载速度更快、用户体验更好(severless + indexdb)  \8. 在7的条件下,已知用户当前在商品列表页,并且下一步操作是点击某个商品进入详情页,怎么加快详情页的渲染速度

npm安装和查找机制

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装(不需要网络)
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

npm 缓存相关命令

shell
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
+ + + + + \ No newline at end of file diff --git a/front-end-engineering/jscompatibility.html b/front-end-engineering/jscompatibility.html new file mode 100644 index 00000000..3866f91b --- /dev/null +++ b/front-end-engineering/jscompatibility.html @@ -0,0 +1,232 @@ + + + + + + JS兼容性 | Sunny's blog + + + + + + + + +
Skip to content
On this page

JS兼容性

babel

官网:https://babeljs.io/

民间中文网:https://www.babeljs.cn/

babel简介

babel一词来自于希伯来语,直译为巴别塔

巴别塔象征的统一的国度、统一的语言

而今天的JS世界缺少一座巴别塔,不同版本的浏览器能识别的ES标准并不相同,就导致了开发者面对不同版本的浏览器要使用不同的语言,和古巴比伦一样,前端开发也面临着这样的困境。

babel的出现,就是用于解决这样的问题,它是一个编译器,可以把不同标准书写的语言,编译为统一的、能被各种浏览器识别的语言

由于语言的转换工作灵活多样,babel的做法和postcss、webpack差不多,它本身仅提供一些分析功能,真正的转换需要依托于插件完成

babel的安装

babel可以和构建工具联合使用,也可以独立使用

如果要独立的使用babel,需要安装下面两个库:

  • @babel/core:babel核心库,提供了编译所需的所有api
  • @babel/cli:提供一个命令行工具,调用核心库的api完成编译
shell
npm i -D @babel/core @babel/cli
+
npm i -D @babel/core @babel/cli
+

babel的使用

@babel/cli的使用极其简单

它提供了一个命令babel

shell
# 按文件编译
+babel 要编译的文件 -o 编辑结果文件
+npx babel js/a.js -o js/b.js
+
+# 按目录编译
+babel 要编译的整个目录 -d 编译结果放置的目录
+npx babel js -d dist
+
# 按文件编译
+babel 要编译的文件 -o 编辑结果文件
+npx babel js/a.js -o js/b.js
+
+# 按目录编译
+babel 要编译的整个目录 -d 编译结果放置的目录
+npx babel js -d dist
+

babel的配置

可以看到,babel本身没有做任何事情,真正的编译要依托于babel插件babel预设来完成

babel预设和postcss预设含义一样,是多个插件的集合体,用于解决一系列常见的兼容问题

如何告诉babel要使用哪些插件或预设呢?需要通过一个配置文件.babelrc

json
{
+    "presets": [],
+    "plugins": []
+}
+
{
+    "presets": [],
+    "plugins": []
+}
+

babel预设

babel有多种预设,最常见的预设是@babel/preset-env

安装 @babel/preset-env可以让你使用最新的JS语法,而无需针对每种语法转换设置具体的插件

配置

json
{
+    "presets": [
+        "@babel/preset-env"
+    ]
+}
+
{
+    "presets": [
+        "@babel/preset-env"
+    ]
+}
+

兼容的浏览器

@babel/preset-env需要根据兼容的浏览器范围来确定如何编译,和postcss一样,可以使用文件.browserslistrc来描述浏览器的兼容范围

last 3 version
+> 1%
+not ie <= 8
+
last 3 version
+> 1%
+not ie <= 8
+

自身的配置

postcss-preset-env一样,@babel/preset-env自身也有一些配置

具体的配置见:https://www.babeljs.cn/docs/babel-preset-env#options

配置方式是:

json
{
+    "presets": [
+        ["@babel/preset-env", {
+            "配置项1": "配置值",
+            "配置项2": "配置值",
+            "配置项3": "配置值"
+        }]
+    ]
+}
+
{
+    "presets": [
+        ["@babel/preset-env", {
+            "配置项1": "配置值",
+            "配置项2": "配置值",
+            "配置项3": "配置值"
+        }]
+    ]
+}
+

其中一个比较常见的配置项是usebuiltins,该配置的默认值是false

它有什么用呢?由于该预设仅转换新的语法,并不对新的API进行任何处理

例如:

javascript
new Promise(resolve => {
+    resolve()
+})
+
new Promise(resolve => {
+    resolve()
+})
+

转换的结果为

javascript
new Promise(function (resolve) {
+  resolve();
+});
+
new Promise(function (resolve) {
+  resolve();
+});
+

如果遇到没有Promise构造函数的旧版本浏览器,该代码就会报错

而配置usebuiltins可以在编译结果中注入这些新的API,它的值默认为false,表示不注入任何新的API,可以将其设置为usage,表示根据API的使用情况,按需导入API

对于新的api npm i core-js 对于新的语法变成api实现 generator-runtime

json
{
+    "presets": [
+        ["@babel/preset-env", {
+            "useBuiltIns": "usage",
+            "corejs": 3//默认会使用2,我需要使用3版本
+        }]
+    ]
+}
+
{
+    "presets": [
+        ["@babel/preset-env", {
+            "useBuiltIns": "usage",
+            "corejs": 3//默认会使用2,我需要使用3版本
+        }]
+    ]
+}
+

babel插件

先安装在使用 npm i -D ...

注意:@babel/polyfill 已过时,目前被core-jsgenerator-runtime所取代

除了预设可以转换代码之外,插件也可以转换代码,它们的顺序是:

  • 插件在 Presets 前运行。
  • 插件顺序从前往后排列。
  • Preset 顺序是颠倒的(从后往前)。
js
{
+  "preset": ['a','b'],
+  "plugins":['c','d']
+}
+// cdba
+
{
+  "preset": ['a','b'],
+  "plugins":['c','d']
+}
+// cdba
+

通常情况下,@babel/preset-env只转换那些已经形成正式标准的语法,对于某些处于早期阶段、还没有确定的语法不做转换。

如果要转换这些语法,就要单独使用插件

下面随便列举一些插件

@babel/plugin-proposal-class-properties

该插件可以让你在类中书写初始化字段

javascript
class A {
+    a = 1;
+    constructor(){
+        this.b = 3;
+    }
+}
+
class A {
+    a = 1;
+    constructor(){
+        this.b = 3;
+    }
+}
+

@babel/plugin-proposal-function-bind

该插件可以让你轻松的为某个方法绑定this

javascript
function Print() {
+    console.log(this.loginId);
+}
+
+const obj = {
+    loginId: "abc"
+};
+
+obj::Print(); //相当于:Print.call(obj);
+
function Print() {
+    console.log(this.loginId);
+}
+
+const obj = {
+    loginId: "abc"
+};
+
+obj::Print(); //相当于:Print.call(obj);
+

遗憾的是,目前vscode无法识别该语法,会在代码中报错,虽然并不会有什么实际性的危害,但是影响观感

@babel/plugin-proposal-optional-chaining

javascript
const obj = {
+  foo: {
+    bar: {
+      baz: 42,
+    },
+  },
+};
+
+const baz = obj?.foo?.bar?.baz; // 42
+
+const safe = obj?.qux?.baz; // undefined
+
const obj = {
+  foo: {
+    bar: {
+      baz: 42,
+    },
+  },
+};
+
+const baz = obj?.foo?.bar?.baz; // 42
+
+const safe = obj?.qux?.baz; // undefined
+

babel-plugin-transform-remove-console

该插件会移除源码中的控制台输出语句

@babel/plugin-transform-runtime

用于提供一些公共的API,这些API会帮助代码转换 使用的时候发现依赖其他库,所以还要安装babel runtime

ESLint

ESLint是一个针对JS的代码风格检查工具,当不满足其要求的风格时,会给予警告或错误

官网:https://eslint.org/

民间中文网:https://eslint.bootcss.com/

使用

ESLint通常配合编辑器使用

  1. 在vscode中安装ESLint

该工具会自动检查工程中的JS文件

检查的工作交给eslint库,如果当前工程没有,则会去全局库中查找,如果都没有,则无法完成检查

另外,检查的依据是eslint的配置文件.eslintrc,如果找不到工程中的配置文件,也无法完成检查

  1. 安装eslint

npm i [-g] eslint

  1. 创建配置文件

可以通过eslint交互式命令创建配置文件

由于windows环境中git窗口对交互式命名支持不是很好,建议使用powershell

npx eslint --init

eslint会识别工程中的.eslintrc.*文件,也能够识别package.json中的eslintConfig字段

配置

env

配置代码的运行环境

  • browser:代码是否在浏览器环境中运行
  • es6:是否启用ES6的全局API,例如Promise

parserOptions

该配置指定eslint对哪些语法的支持

  • ecmaVersion: 支持的ES语法版本
  • sourceType
    • script:传统脚本
    • module:模块化脚本

parser

eslint的工作原理是先将代码进行解析,然后按照规则进行分析

eslint 默认使用Espree作为其解析器,你可以在配置文件中指定一个不同的解析器。

globals

配置可以使用的额外的全局变量

json
{
+  "globals": {
+    "var1": "readonly",
+    "var2": "writable"
+  }
+}
+
{
+  "globals": {
+    "var1": "readonly",
+    "var2": "writable"
+  }
+}
+

eslint支持注释形式的配置,在代码中使用下面的注释也可以完成配置

javascript
/* global var1, var2 */
+/* global var3:writable, var4:writable */
+
/* global var1, var2 */
+/* global var3:writable, var4:writable */
+

extends

该配置继承自哪里

它的值可以是字符串或者数组

比如:

json
{
+  "extends": "eslint:recommended"
+}
+
{
+  "extends": "eslint:recommended"
+}
+

表示,该配置缺失的位置,使用eslint推荐的规则

ignoreFiles

排除掉某些不需要验证的文件

.eslintignore 要放在根目录

dist/**/*.js
+node_modules// 自动忽略
+
dist/**/*.js
+node_modules// 自动忽略
+

rules

eslint规则集

每条规则影响某个方面的代码风格

每条规则都有下面几个取值:

  • off 或 0 或 false: 关闭该规则的检查
  • warn 或 1 或 true:警告,不会导致程序退出
  • error 或 2:错误,当被触发的时候,程序会退出

除了在配置文件中使用规则外,还可以在注释中使用:

javascript
/* eslint eqeqeq: "off", curly: "error" */
+
/* eslint eqeqeq: "off", curly: "error" */
+

https://eslint.bootcss.com/docs/rules/ 带有🔧的可以自动修复:npx eslint --fix src/index.js

ESLint官网:https://eslint.org/

ESLint民间中文网:https://eslint.bootcss.com/

ESLint的由来

JavaScript是一个过于灵活的语言,因此在企业开发中,往往会遇到下面两个问题:

  • 如何让所有员工书写高质量的代码? 比如使用===替代==
  • 如何让所有员工书写的代码风格保持统一? 比如字符串统一使用单引号

上面两个问题,一个代表着代码的质量,一个代表着代码的风格。

如果纯依靠人工进行检查,不仅费时费力,而且还容易出错。

ESLint由此诞生,它是一个工具,预先配置好各种规则,通过这些规则来自动化的验证代码,甚至自动修复

如何验证

shell
# 验证单个文件
+npx eslint 文件名
+# 验证全部文件
+npx eslint src/**
+
# 验证单个文件
+npx eslint 文件名
+# 验证全部文件
+npx eslint src/**
+

配置规则

eslint会自动寻找根目录中的配置文件,它支持三种配置文件:

  • .eslintrc JSON格式
  • .eslintrc.js JS格式
  • .eslintrc.yml YAML格式

这里以.eslintrc.js为例:

javascript
// ESLint 配置
+module.exports = {
+  // 配置规则
+  rules: {
+    规则名1: 级别,
+    规则名2: 级别,
+    ...
+  },
+};
+
// ESLint 配置
+module.exports = {
+  // 配置规则
+  rules: {
+    规则名1: 级别,
+    规则名2: 级别,
+    ...
+  },
+};
+

每条规则由名称和级别组成

规则名称决定了要检查什么

规则级别决定了检查没通过时的处理方式

所有的规则名称看这里:

所有级别如下:

  • 0 或 'off':关闭规则
  • 1 或 'warn':验证不通过提出警告
  • 2 或 'error':验证不通过报错,退出程序

在VSCode中及时发现问题

每次都要输入命令发现问题非常麻烦

可以安装VSCode插件ESLint,只要项目的node_modules中有eslint,它就会按照项目根目录下的规则自动检测

使用继承

ESLint的规则非常庞大,全部自定义过于麻烦

一般我们继承其他企业开源的方案来简化配置

这方面做的比较好的是一家叫Airbnb的公司,他们在开发前端项目的时候自定义了一套开源规则,受到全世界的认可

我们只需要安装它即可

shell
# 为了避免版本问题,不要直接安装eslint,直接安装下面的包,会自动安装相应版本的eslint
+npm i -D eslint-config-airbnb
+
# 为了避免版本问题,不要直接安装eslint,直接安装下面的包,会自动安装相应版本的eslint
+npm i -D eslint-config-airbnb
+

然后稍作配置

shell
module.exports = {
+	extends: 'airbnb' # 配置继承自 airbnb
+}
+
module.exports = {
+	extends: 'airbnb' # 配置继承自 airbnb
+}
+

企业开发的实际情况

我们要做什么?

  • 安装好VSCode的ESLint插件
  • 学会查看ESLint错误提示
+ + + + + \ No newline at end of file diff --git a/front-end-engineering/modularization.html b/front-end-engineering/modularization.html new file mode 100644 index 00000000..0ae11b30 --- /dev/null +++ b/front-end-engineering/modularization.html @@ -0,0 +1,252 @@ + + + + + + 模块化 | Sunny's blog + + + + + + + + +
Skip to content
On this page

模块化

1.JavaScript 模块化发展史

第一阶段

在 JavaScript 语言刚刚诞生的时候,它仅仅用于实现页面中的一些小效果 那个时候,一个页面所用到的 JS 可能只有区区几百行的代码 在这种情况下,语言本身所存在的一些缺陷往往被大家有意的忽略,因为程序的规模实在太小,只要开发人员小心谨慎,往往不会造成什么问题 在这个阶段,也不存在专业的前端工程师,由于前端要做的事情实在太少,因此这一部分工作往往由后端工程师顺带完成 第一阶段发生的大事件:

  • 1996 年,NetScape 将 JavaScript 语言提交给欧洲的一个标准制定组织 ECMA(欧洲计算机制造商协会)
  • 1998 年,NetScape 在与微软浏览器 IE 的竞争中失利,宣布破产

第二阶段

ajax 的出现,逐渐改变了 JavaScript 在浏览器中扮演的角色。现在,它不仅可以实现小的效果,还可以和服务器之间进行交互,以更好的体验来改变数据 JS 代码的数量开始逐渐增长,从最初的几百行,到后来的几万行,前端程序逐渐变得复杂 后端开发者压力逐渐增加,致使一些公司开始招募专业的前端开发者 但此时,前端开发者的待遇远不及后端开发者,因为前端开发者承担的开发任务相对于后端开发来说,还是比较简单的,通过短短一个月的时间集训,就可以成为满足前端开发的需要 究其根本原因,是因为前端开发还有几个大的问题没有解决,这些问题都严重的制约了前端程序的规模进一步扩大:

  1. 浏览器解释执行 JS 的速度太慢
  2. 用户端的电脑配置不足
  3. 更多的代码带来了全局变量污染、依赖关系混乱等问题

上面三个问题,就像是阿喀琉斯之踵,成为前端开发挥之不去的阴影和原罪。 在这个阶段,前端开发处在一个非常尴尬的境地,它在传统的开发模式和前后端分离之间无助的徘徊 第二阶段的大事件:

  1. IE 浏览器制霸市场后,几乎不再更新
  2. ES4.0 流产,导致 JS 语言 10 年间几乎毫无变化
  3. 2008 年 ES5 发布,仅解决了一些 JS API 不足的糟糕局面

第三阶段

时间继续向前推移,到了 2008 年,谷歌的 V8 引擎发布(面试),将 JS 的执行速度推上了一个新的台阶,甚至可以和后端语言媲美。 摩尔定律持续发酵,个人电脑的配置开始飞跃 突然间,制约前端发展的两大问题得以解决,此时,只剩下最后一个问题还在负隅顽抗,即全局变量污染和依赖混乱的问题,解决了它,前端便可以突破一切障碍,未来无可限量。 于是,全世界的前端开发者在社区中激烈的讨论,想要为这个问题寻求解决之道...... 2008 年,有一个名叫 Ryan Dahl 小伙子正在为一件事焦头烂额,它需要在服务器端手写一个高性能的 web 服务,该服务对于性能要求之高,以至于目前市面上已有的 web 服务产品都满足不了需求。

服务器开发

新浪的服务器(电脑)收到请求 其中一个应用程序在做以下的事情 web 服务

  1. 监听 80 端口
  2. 将请求进行分析
  3. 将分析的结果交给相应的程序(php,Java)进行处理
  4. 把程序处理的结果返还给客户端

经过分析,它确定,如果要实现高性能,那么必须要尽可能的减少线程,而要减少线程,避免不了要实用异步的处理方案。 一开始,他打算自己实用 C/C++语言来编写,可是这一过程实在太痛苦。 就在他一筹莫展的时候,谷歌 V8 引擎的发布引起了他的注意,他突然发现,JS 不就是最好的实现 web 服务的语言吗?它天生就是单线程,并且是基于异步的!有了 V8 引擎的支撑,它的执行速度完全可以撑起一个服务器。而且 V8 是鼎鼎大名的谷歌公司发布的,谷歌一定会不断的优化 V8,有这种又省钱又省力的好事,我干嘛还要自己去写呢? 典型异步场景

javascript
setTimeout(function () {
+  console.log("a");
+}, 1000);
+//后面不会阻塞
+//JS单线程指的是执行线程,不代表浏览器单线程
+
setTimeout(function () {
+  console.log("a");
+}, 1000);
+//后面不会阻塞
+//JS单线程指的是执行线程,不代表浏览器单线程
+

于是,它基于开源的 V8 引擎,对源代码作了一些修改,便快速的完成了该项目。 2009 年,Ryan 推出了该 web 服务项目,命名为 nodejs。 从此,JS 第一次堂堂正正的入主后端,不再是必须附属于浏览器的“玩具”语言了。 也是从此刻开始,人们认识到,JS(ES)是一门真正的语言,它依附于运行环境(运行时)(宿主程序)而执行

nodejs 的诞生,便把 JS 中的最后一个问题放到了台前,即全局变量污染和依赖混乱问题 要直到,nodejs 是服务器端,如果不解决这个问题,分模块开发就无从实现,而模块化开发是所有后端程序必不可少的内容 经过社区的激烈讨论,最终,形成了一个模块化方案,即鼎鼎大名的 CommonJS,该方案,彻底解决了全局变量污染和依赖混乱的问题 该方案一出,立即被 nodejs 支持,于是,nodejs 成为了第一个为 JS 语言实现模块化的平台,为前端接下来的迅猛发展奠定了实践基础 该阶段发生的大事件:

  • 2008 年,V8 发布
  • IE 的市场逐步被 firefox 和 chrome 蚕食,现已无力回天
  • 2009 年,nodejs 发布,并附带 commonjs 模块化标准

第四阶段

CommonJS 的出现打开了前端开发者的思路

既然后端可以使用模块化的 JS,作为 JS 语言的老东家浏览器为什么不行呢?

于是,开始有人想办法把 CommonJS 运用到浏览器中

可是这里面存在诸多的困难(课程中详解)

办法总比困难多,有些开发者就想,既然 CommonJS 运用到浏览器困难,我们干嘛不自己重新定一个模块化的标准出来,难道就一定要用 CommonJS 标准吗?

于是很快,AMD 规范出炉,它解决的问题和 CommonJS 一样,但是可以更好的适应浏览器环境

相继的,CMD 规范出炉,它对 AMD 规范进行了改进

这些行为,都受到了 ECMA 官方的密切关注......

2015 年,ES6 发布,它提出了官方的模块化解决方案 —— ES6 模块化

从此以后,模块化成为了 JS 本身特有的性质,这门语言终于有了和其他语言较量的资本,成为了可以编写大型应用的正式语言

于此同时,很多开发者、技术厂商早已预见到 JS 的无穷潜力,于是有了下面的故事

  • 既然 JS 也能编写大型应用,那么自然也需要像其他语言那样有解决复杂问题的开发框架
    • Angular、React、Vue 等前端开发框架出现
    • Express、Koa 等后端开发框架出现
    • 各种后端数据库驱动出现
  • 要开发大型应用,自然少不了各种实用的第三方库的支持
    • npm 包管理器出现,实用第三方库变得极其方便
    • webpack 等构建工具出现,专门用于打包和部署
  • 既然 JS 可以放到服务器环境,为什么不能放到其他终端环境呢?
    • Electron 发布,可以使用 JS 语言开发桌面应用程序
    • RN 和 Vuex 等技术发布,可以使用 JS 语言编写移动端应用程序
    • 各种小程序出现,可以使用 JS 编写依附于其他应用的小程序
    • 目前还有很多厂商致力于将 JS 应用到各种其他的终端设备,最终形成大前端生态

可以看到,模块化的出现,是 JS 通向大型应用的基石,学习好模块化,变具备了编写大型应用的基本功。

2. CommonJS

nodejs 遵循 EcmaScript 标准,但由于脱离了浏览器环境,因此:

  1. 你可以在 nodejs 中使用 EcmaScript 标准的任何语法或 api,例如:循环、判断、数组、对象等
  2. 你不能在 nodejs 中使用浏览器的 web api,例如:dom 对象、window 对象、document 对象等

由于大部分开发者是从浏览器端开发转向 nodejs 开发的,为了降低开发者的学习成本,nodejs 中提供了一些和浏览器 web api 同样的对象或函数,例如:console、setTimeout、setInterval 等

2-2. CommonJS

在 nodejs 中,由于有且仅有一个入口文件(启动文件),而开发一个应用肯定会涉及到多个文件配合,因此,nodejs 对模块化的需求比浏览器端要大的多 由于 nodejs 刚刚发布的时候,前端没有统一的、官方的模块化规范,因此,它选择使用社区提供的 CommonJS 作为模块化规范 在学习 CommonJS 之前,首先认识两个重要的概念:模块的导出模块的导入

模块的导出

要理解模块的导出,首先要理解模块的含义 什么是模块? 模块就是一个 JS 文件,它实现了一部分功能,并隐藏自己的内部实现,同时提供了一些接口供其他模块使用 模块有两个核心要素:隐藏暴露demo

javascript
var count = 0; //需要隐藏的内部实现
+function getNumber() {
+  //要暴露的接口
+  count++;
+  return count;
+}
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+
var count = 0; //需要隐藏的内部实现
+function getNumber() {
+  //要暴露的接口
+  count++;
+  return count;
+}
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+console.log(getNumber());
+

隐藏的,是自己内部的实现 暴露的,是希望外部使用的接口 任何一个正常的模块化标准,都应该默认隐藏模块中的所有实现,而通过一些语法或 api 调用来暴露接口 暴露接口的过程即模块的导出

模块的导入

当需要使用一个模块时,使用的是该模块暴露的部分(导出的部分),隐藏的部分是永远无法使用的。 当通过某种语法或 api 去使用一个模块时,这个过程叫做模块的导入

CommonJS 规范

CommonJS 使用exports导出模块,require导入模块 具体规范如下:

  1. 如果一个 JS 文件中存在exportsrequire,该 JS 文件是一个模块
  2. 模块内的所有代码均为隐藏代码,包括全局变量、全局函数,这些全局的内容均不应该对全局变量造成任何污染
  3. 如果一个模块需要暴露一些 API 提供给外部使用,需要通过exports导出,exports是一个空的对象,你可以为该对象添加任何需要导出的内容
  4. 如果一个模块需要导入其他模块,通过require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容

导出

javascript
// 原本相当于exports={}
+exports.getNumber = getNumber;
+相当于;
+// exports: {
+//     getNumber: getNumber
+// }
+exports.abc = 123;
+相当于;
+// exports: {
+//     getNumber: fn,
+//         abc: 123
+// }
+
// 原本相当于exports={}
+exports.getNumber = getNumber;
+相当于;
+// exports: {
+//     getNumber: getNumber
+// }
+exports.abc = 123;
+相当于;
+// exports: {
+//     getNumber: fn,
+//         abc: 123
+// }
+

导入

javascript
var a = require("./util.js");
+console.log(a);//a是对象
+count不能访问因为没有导出
+
var a = require("./util.js");
+console.log(a);//a是对象
+count不能访问,因为没有导出
+

3.AMD 和 CMD

3-1 浏览器端模块化的难题

本节为重点:AMD CMD 不常用了 CommonJS 的工作原理 当使用require(模块路径)导入一个模块时,node 会做以下两件事情(不考虑模块缓存):

  1. 通过模块路径找到本机文件,并读取文件内容
  2. 将文件中的代码放入到一个函数环境中执行,并将执行后 module.exports 的值作为 require 函数的返回结果

正是这两个步骤,使得 CommonJS 在 node 端可以良好的被支持 可以认为,CommonJS 是同步的,必须要等到加载完文件并执行完代码后才能继续向后执行 当浏览器遇到 CommonJS 当想要把 CommonJS 放到浏览器端时,就遇到了一些挑战

  1. 浏览器要加载 JS 文件,需要远程从服务器读取,而网络传输的效率远远低于 node 环境中读取本地文件的效率。由于 CommonJS 是同步的,这会极大的降低运行性能
  2. 如果需要读取 JS 文件内容并把它放入到一个环境中执行,需要浏览器厂商的支持,可是浏览器厂商不愿意提供支持,最大的原因是 CommonJS 属于社区标准,并非官方标准

新的规范 基于以上两点原因,浏览器无法支持模块化 可这并不代表模块化不能在浏览器中实现 要在浏览器中实现模块化,只要能解决上面的两个问题就行了 解决办法其实很简单:

  1. 远程加载 JS 浪费了时间?做成异步即可,加载完成后调用一个回调就行了
javascript
require("./a.js", function () {}); //回调
+
require("./a.js", function () {}); //回调
+
  1. 模块中的代码需要放置到函数中执行?编写模块时,直接放函数中就行了

基于这种简单有效的思路,出现了 AMD 和 CMD 规范,有效的解决了浏览器模块化的问题。

3-2AMD

全称是 Asynchronous Module Definition,即异步模块加载机制 require.js 实现了 AMD 规范

html
<script data-main="./js/index.js" src="./js/require.js"></script>
+<!-- index.js入口 , 必须引用require.js-->
+
<script data-main="./js/index.js" src="./js/require.js"></script>
+<!-- index.js入口 , 必须引用require.js-->
+

在 AMD 中,导入和导出模块的代码,都必须放置在 define 函数中

javascript
define([依赖的模块列表], function (模块名称列表) {
+  //模块内部的代码
+  return 导出的内容;
+});
+
define([依赖的模块列表], function (模块名称列表) {
+  //模块内部的代码
+  return 导出的内容;
+});
+

require.js 里面提供了个全局方法 define() 写法:

javascript
define(123); //导出123
+define({ a: 1, b: 2 }); //导出对象
+define(function () {
+  var a = 1; //不会污染
+  var b = 234;
+  return {
+    name: "b模块",
+    data: "b模块的数据",
+  };
+});
+
define(123); //导出123
+define({ a: 1, b: 2 }); //导出对象
+define(function () {
+  var a = 1; //不会污染
+  var b = 234;
+  return {
+    name: "b模块",
+    data: "b模块的数据",
+  };
+});
+

3-3CMD

全称是 Common Module Definition,公共模块定义规范 sea.js 实现了 CMD 规范

html
<script src="./js/sea.js"></script>
+<script>
+  seajs.use("./js/index");
+</script>
+
<script src="./js/sea.js"></script>
+<script>
+  seajs.use("./js/index");
+</script>
+

在 CMD 中,导入和导出模块的代码,都必须放置在 define 函数中

javascript
define(function (require, exports, module) {
+  //模块内部的代码
+});
+
define(function (require, exports, module) {
+  //模块内部的代码
+});
+

可以使用异步

javascript
define((require, exports, module) => {
+  require.async("a", function (a) {
+    console.log(a);
+  });
+  require.async("b", function (b) {
+    console.log(b);
+  });
+});
+
define((require, exports, module) => {
+  require.async("a", function (a) {
+    console.log(a);
+  });
+  require.async("b", function (b) {
+    console.log(b);
+  });
+});
+

4.es6 模块化

4-1.ES6 模块化简介

ECMA 组织参考了众多社区模块化标准,终于在 2015 年,随着 ES6 发布了官方的模块化标准,后成为 ES6 模块化 ES6 模块化具有以下的特点

  1. 使用依赖预声明的方式导入模块
    1. 依赖延迟声明(commonjs)
      1. 优点:某些时候可以提高效率
      2. 缺点:无法在一开始确定模块依赖关系(比较模糊)
    2. 依赖预声明(AMD)
      1. 优点:在一开始可以确定模块依赖关系
      2. 缺点:某些时候效率较低
  2. 灵活的多种导入导出方式(相对于 module.export 较简单)
  3. 规范的路径表示法:所有路径必须以./或../开头

4-2.基本导入导出

模块的引入

注意:这一部分非模块化标准 目前,浏览器使用以下方式引入一个 ES6 模块文件

html
<script src="入口文件" type="module">
+  //module作为模块运行
+  //当成了模块,就不会污染全局变量
+
<script src="入口文件" type="module">
+  //module作为模块运行
+  //当成了模块,就不会污染全局变量
+

模块的基本导出和导入

ES6 中的模块导入导出分为两种:

  1. 基本导入导出
  2. 默认导入导出

基本导出

类似于 exports.xxx = xxxx 基本导出可以有多个,每个必须有名称 基本导出的语法如下:

javascript
export 声明表达式  //必须是声明语句
+
export 声明表达式  //必须是声明语句
+

举例

javascript
export var a = 1; //导出a,值为1,类似于CommonJS中的exports.a = 1
+export function test() {
+  //导出test,值为一个函数,类似于CommonJS中的exports.test = function (){}
+}
+export class Person {}
+
+export const name = "abc";
+
export var a = 1; //导出a,值为1,类似于CommonJS中的exports.a = 1
+export function test() {
+  //导出test,值为一个函数,类似于CommonJS中的exports.test = function (){}
+}
+export class Person {}
+
+export const name = "abc";
+

javascript
export { 具名符号 }; // 大括号不是对象
+
export { 具名符号 }; // 大括号不是对象
+

举例

javascript
var age = 18;
+var sex = 1;
+
+export { age, sex }; //将age变量的名称作为导出的名称,age变量的值,作为导出的值
+
var age = 18;
+var sex = 1;
+
+export { age, sex }; //将age变量的名称作为导出的名称,age变量的值,作为导出的值
+

由于基本导出必须具有名称,所以要求导出内容必须跟上声明表达式具名符号

基本导入

由于使用的是依赖预加载,因此,导入任何其他模块,导入代码必须放置到所有代码之前 对于基本导出,如果要进行导入,使用下面的代码

javascript
import { 导入的符号列表 } from "模块路径";
+
import { 导入的符号列表 } from "模块路径";
+

举例

javascript
import { name, age } from "./a.js";
+
import { name, age } from "./a.js";
+

注意以下细节:

  • 导入时,可以通过关键字as对导入的符号进行重命名
javascript
import { name as name1, age as age1 } from "./a.js";
+var b = 3;
+console.log(b2);
+console.log(name1, age);
+console.log(b);
+
import { name as name1, age as age1 } from "./a.js";
+var b = 3;
+console.log(b2);
+console.log(name1, age);
+console.log(b);
+
  • 导入时使用的符号是常量,不可修改
  • 可以使用*号导入所有的基本导出,形成一个对象
javascript
import * as a from "./a.js";
+
import * as a from "./a.js";
+
javascript
import "./b.js"; //这条导入语句,仅会运行模块,不适用它内部的任何导出
+// 适用于初始化代码init
+
import "./b.js"; //这条导入语句,仅会运行模块,不适用它内部的任何导出
+// 适用于初始化代码init
+

4-3. 默认导入导出

默认导出

每个模块,除了允许有多个基本导出之外,还允许有一个默认导出 默认导出类似于 CommonJS 中的module.exports,由于只有一个,因此无需具名 具体的语法是

javascript
export default 默认导出的数据;
+
export default 默认导出的数据;
+

举例

javascript
export default 123;
+export default a;
+export default {
+  fn: function () { },
+  name: "adsfaf"
+}
+
export default 123;
+export default a;
+export default {
+  fn: function () { },
+  name: "adsfaf"
+}
+

javascript
export { 默认导出的数据 as default };
+
export { 默认导出的数据 as default };
+

举例

javascript
export { a as abc };
+
export { a as abc };
+

由于每个模块仅允许有一个默认导出,因此,每个模块不能出现多个默认导出语句

默认导入

需要想要导入一个模块的默认导出,需要使用下面的语法

javascript
import {导入的符号列表} from "模块路径
+
import {导入的符号列表} from "模块路径”
+

举例

javascript
import data from "./a.js"; //将a.js模块中的默认导出放置到常量data中
+
import data from "./a.js"; //将a.js模块中的默认导出放置到常量data中
+

类似于 CommonJS 中的

javascript
var 接受变量名 = require("模块路径	");
+
var 接受变量名 = require("模块路径	");
+

由于默认导入时变量名是自行定义的,因此没有别名一说 如果希望同时导入某个模块的默认导出和基本导出,可以使用下面的语法

javascript
import 接收默认导出的变量,{接收基本导出的变量} from "模块变量"
+
import 接收默认导出的变量,{接收基本导出的变量} from "模块变量"
+

注:如果使用*号,会将所有基本导出和默认导出聚合到一个对象中,默认导出会作为属性 default 存在

javascript
import data, { a, b } from "./a.js";
+// a里面默认导出放在data里面,基本导出放在a,b里面
+
import data, { a, b } from "./a.js";
+// a里面默认导出放在data里面,基本导出放在a,b里面
+

4-4.ES6 模块化的其他细节

  1. 尽量导出不可变值

当导出一个内容时,尽量保证该内容是不可变的(大部分情况都是如此) 因为,虽然导入后,无法更改导入内容,但是在导入的模块内部却有可能发生更改,这将导致一些无法预料的事情发生

javascript
export const name = "模块a"; //用const更加坐实了这点
+
export const name = "模块a"; //用const更加坐实了这点
+
  1. 可以使用无绑定的导入用于执行一些初始化代码

如果我们只是想执行模块中的一些代码,而不需要导入它的任何内容,可以使用无绑定的导入:

javascript
import "模块路径";
+
import "模块路径";
+

举例 arrayPatcher.js

javascript
Array.prototype.print = function () {
+  console.log(this);
+};
+//一些其他的代码
+
Array.prototype.print = function () {
+  console.log(this);
+};
+//一些其他的代码
+
javascript
import "./arrayPatcher.js"; //无绑定的导入
+var arr = [3, 4, 6, 6, 7];
+arr.print();
+
import "./arrayPatcher.js"; //无绑定的导入
+var arr = [3, 4, 6, 6, 7];
+arr.print();
+
  1. 可以使用绑定再导出,来重新导出来自另一个模块的内容

有的时候,我们可能需要用一个模块封装多个模块,然后有选择的将多个模块的内容分别导出,可以使用下面的语法轻松完成

javascript
export { 绑定的标识符 } from "模块路径";
+
export { 绑定的标识符 } from "模块路径";
+

举例

javascript
export { a, b } from "./m1.js";
+export { k, default, a as m2a } from "./m2.js";
+export const r = "m-r";
+
export { a, b } from "./m1.js";
+export { k, default, a as m2a } from "./m2.js";
+export const r = "m-r";
+
+ + + + + \ No newline at end of file diff --git a/front-end-engineering/node.html b/front-end-engineering/node.html new file mode 100644 index 00000000..b1a08195 --- /dev/null +++ b/front-end-engineering/node.html @@ -0,0 +1,20 @@ + + + + + + Node 组成原理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Node 组成原理

Node.js 是一个开源的、跨平台的 JavaScript 运行环境,依赖于 Google V8 引擎,用于构建高性能的网络应用程序。Node.js 采用事件驱动、非阻塞 I/O 模型,使得它能够处理大量并发连接,适用于构建实时应用、高吞吐量的后端服务和网络代理等。

Node.js 广泛应用于 Web 开发、服务器端开发、实时通信、大数据处理等领域,被许多大型互联网公司和开发者使用和推崇。

Node.js 的特点包括:

  1. 单线程和事件驱动:Node.js 采用单线程的事件循环模型,通过异步 I/O 和事件驱动处理并发请求,避免了传统多线程模型中的线程切换和资源开销,提高了性能和可扩展性。
  2. 跨平台:Node.js 可运行于多个操作系统平台,包括 Windows、Linux 和 Mac OS 等。
  3. 高性能:由于基于 V8 引擎和非阻塞 I/O 模型,Node.js 具有快速的执行速度和高吞吐量,适用于处理大量并发请求的场景。
  4. 模块化和包管理:Node.js 支持模块化开发,可以通过 npm(Node Package Manager)进行包的管理和发布,方便了代码的组织和复用。
  5. 强大的社区支持:Node.js 拥有庞大的开发者社区,提供了丰富的第三方模块和工具,方便开发者进行开发和调试。

Node.js 组成

  1. 用户代码:JS 代码,开发者编写的
  2. 第三方库:大部分仍然是 JS 代码,由其他开发者编写
  3. 本地模块:Node.js 内置了一些核心模块,这些模块提供了基础的功能,如文件操作(fs 模块)、网络通信(http 模块)、加密(crypto 模块)、操作系统信息(os 模块)等。这些模块可以直接通过 require 函数进行引入使用。
  4. 内置模块:Node.js 有一个丰富的第三方模块生态系统,开发者可以通过 NPM 安装这些模块,并在自己的项目中引入使用。
  5. libuv:libuv 是一个跨平台的异步 I/O 库,它为 Node.js 提供了非阻塞的事件驱动的 I/O 操作。它可以处理文件系统操作、网络请求、定时器等等,在 Node.js 中用于处理事件循环。
  6. os api:将 Node.js 可运行于多个操作系统平台,包括 Windows、Linux 和 Mac OS 等。
  7. V8 引擎:Node.js 使用了 Google 开发的 V8 引擎作为其 JavaScript 执行引擎。V8 引擎可以将 JavaScript 代码直接转化为机器码,以提供高性能的执行效率。(c/c++代码,作用:把 JS 代码解释成为机器码。可以通过 v8 引擎的某种机制,扩展其功能。V8 引擎的扩展和对扩展的编译,是通过一个工具:gyp 工具。某些第三方库需要使用 node-gyp 工具进行构建,因此需要先安装 node-gyp)

其他资料

https://github.com/theanarkh/understand-nodejs

+ + + + + \ No newline at end of file diff --git a/front-end-engineering/performance.html b/front-end-engineering/performance.html new file mode 100644 index 00000000..4cdc4942 --- /dev/null +++ b/front-end-engineering/performance.html @@ -0,0 +1,1370 @@ + + + + + + 前端性能优化方法论 | Sunny's blog + + + + + + + + +
Skip to content
On this page

前端性能优化方法论

https://juejin.cn/post/6993137683841155080

我们可以从两个方面来看性能优化的意义:

  1. 用户角度:网站优化能够让页面加载得更快,响应更加及时,极大提升用户体验。
  2. 服务商角度:优化会减少页面资源请求数,减小请求资源所占带宽大小,从而节省可观的带宽资源。

网站优化的目标就是减少网站加载时间,提高响应速度。 Google 和亚马逊的研究表明,Google 页面加载的时间从 0.4 秒提升到 0.9 秒导致丢失了 20% 流量和广告收入,对于亚马逊,页面加载时间每增加 100ms 就意味着 1% 的销售额损失。 可见,页面的加载速度对于用户有着至关重要的影响。

Webpack 优化

如何分析打包结果?webpack-bundle-analyzer

1. 构建性能

TIP

这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能

优化的目标,是降低从打包开始,到代码效果呈现所经过的时间

构建性能会影响开发效率。构建性能越高,开发过程中时间的浪费越少

1.1 减少模块解析

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

如果某个模块不做解析,该模块经过 loader 处理后的代码就是最终代码。

如果没有 loader 对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间,那么哪些模块不需要解析呢?

模块中无其他依赖:一些已经打包好的第三方库,比如 jquery,所以可以配置 module.noParse,它是一个正则,被正则匹配到的模块不会解析

js
module.exports = {
+  mode: "development",
+  module: {
+    noParse: /jquery/,
+  },
+};
+
module.exports = {
+  mode: "development",
+  module: {
+    noParse: /jquery/,
+  },
+};
+

1.2 优化 loader 性能

  1. 进一步限制 loader 的应用范围。对于某些库,不使用 loader

例如:babel-loader 可以转换 ES6 或更高版本的语法,可是有些库本身就是用 ES5 语法书写的,不需要转换,使用 babel-loader 反而会浪费构建时间 lodash 就是这样的一个库,lodash 是在 ES5 之前出现的库,使用的是 ES3 语法 通过 module.rule.exclude 或 module.rule.include,排除或仅包含需要应用 loader 的场景

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /lodash/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

如果暴力一点,甚至可以排除掉 node_modules 目录中的模块,或仅转换 src 目录的模块

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        //或
+        // include: /src/,
+        use: "babel-loader",
+      },
+    ],
+  },
+};
+

这种做法是对 loader 的范围进行进一步的限制,和 noParse 不冲突

  1. 缓存 loader 的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的 loader 解析后,解析后的结果也不变 于是,可以将 loader 的解析结果保存下来,让后续的解析直接使用保存的结果 cache-loader 可以实现这样的功能:第一次打包会慢,因为有缓存的过程,以后就快了

js
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        use: ["cache-loader", ...loaders],
+      },
+    ],
+  },
+};
+
module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        use: ["cache-loader", ...loaders],
+      },
+    ],
+  },
+};
+

有趣的是,cache-loader 放到最前面,却能够决定后续的 loader 是否运行。实际上,loader 的运行过程中,还包含一个过程,即 pitch

cache-loader 还可以实现各自自定义的配置,具体方式见文档

  1. 为 loader 的运行开启多线程

thread-loader 会开启一个线程池,线程池中包含适量的线程

它会把后续的 loader 放到线程池的线程中运行,以提高构建效率。由于后续的 loader 会放到新的线程中,所以,后续的 loader 不能:

  • 使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定 thread-loader 放到什么位置

特别注意,开启和管理线程需要消耗时间,在小型项目中使用 thread-loader 反而会增加构建时间

HappyPack:受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。

HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

js
module: {
+  loaders: [
+    {
+      test: /\.js$/,
+      include: [resolve('src')],
+      exclude: /node_modules/,
+      // id 后面的内容对应下面
+      loader: 'happypack/loader?id=happybabel'
+    }
+  ]
+},
+plugins: [
+  new HappyPack({
+    id: 'happybabel',
+    loaders: ['babel-loader?cacheDirectory'],
+    // 开启 4 个线程
+    threads: 4
+  })
+]
+
module: {
+  loaders: [
+    {
+      test: /\.js$/,
+      include: [resolve('src')],
+      exclude: /node_modules/,
+      // id 后面的内容对应下面
+      loader: 'happypack/loader?id=happybabel'
+    }
+  ]
+},
+plugins: [
+  new HappyPack({
+    id: 'happybabel',
+    loaders: ['babel-loader?cacheDirectory'],
+    // 开启 4 个线程
+    threads: 4
+  })
+]
+

1.3 热替换 HMR

热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间 当使用 webpack-dev-server 时,考虑代码改动到效果呈现的过程

原理

  1. 更改配置
js
module.exports = {
+  devServer: {
+    hot: true, // 开启HMR
+  },
+  plugins: [
+    // 可选
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
+
module.exports = {
+  devServer: {
+    hot: true, // 开启HMR
+  },
+  plugins: [
+    // 可选
+    new webpack.HotModuleReplacementPlugin(),
+  ],
+};
+
  1. 更改代码
js
// index.js
+
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
+}
+
// index.js
+
+if (module.hot) {
+  // 是否开启了热更新
+  module.hot.accept(); // 接受热更新
+}
+

首先,这段代码会参与最终运行!当开启了热更新后,webpack-dev-server会向打包结果中注入module.hot属性。默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

但如果运行了module.hot.accept(),将改变这一行为module.hot.accept()的作用是让webpack-dev-server通过 socket 管道,把服务器更新的内容发送到浏览器

然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行 插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行 所以,热替换发生在代码运行期

样式热替换

对于样式也是可以使用热替换的,但需要使用style-loader

因为热替换发生时,HotModuleReplacementPlugin只会简单的重新运行模块代码

因此style-loader的代码一运行,就会重新设置 style 元素中的样式

mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的

思考:webpack 的热更新是如何做到的?说明其原理?

webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

首先要知道 server 端和 client 端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

1.4 其他提升构建性能

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPluginDllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的 npm 包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shakingScope Hoisting 来剔除多余代码

思考:如何利用 webpack 来优化前端性能?(提高性能和体验)

用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS 文件, 利用 cssnano(css-loader?minimize)来压缩 css
  • 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径。可以利用 webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动 webpack 时追加参数--optimize-minimize 来实现
  • 提取公共代码。

思考:如何提高 webpack 的构建速度?

  1. 多入口情况下,使用 CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常用库
  3. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使用 Happypack 实现多线程加速编译
  5. 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
  6. 使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码

2. 传输性能

TIP

传输性能是指,打包后的 JS 代码传输到浏览器经过的时间,在优化传输性能时要考虑到:

  1. 总传输量:所有需要传输的 JS 文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
  2. 文件数量:当访问页面时,需要传输的 JS 文件数量,文件数量越多,http 请求越多,响应速度越慢
  3. 浏览器缓存:JS 文件会被浏览器缓存,被缓存的文件不会再进行传输

2.1 手动分包(极大提升构建性能)

默认情况下,vue-cli会利用webpacksrc目录中的所有代码打包成一个bundle

这样就导致访问一个页面时,需要加载所有页面的 js 代码

我们可以利用 webpack 对动态 import 的支持,从而达到把不同页面的代码打包到不同文件中

js
// routes
+export default [
+  {
+    name: "Home",
+    path: "/",
+    component: () => import(/* webpackChunkName: "home" */ "@/views/Home"),
+  },
+  {
+    name: "About",
+    path: "/about",
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
+];
+
// routes
+export default [
+  {
+    name: "Home",
+    path: "/",
+    component: () => import(/* webpackChunkName: "home" */ "@/views/Home"),
+  },
+  {
+    name: "About",
+    path: "/about",
+    component: () => import(/* webpackChunkName: "about" */ "@/views/About"),
+  },
+];
+

什么是分包:将一个整体的代码,分布到不同的打包文件中

什么时候要分包?

  • 多个 chunk 引入了公共模块
  • 公共模块体积较大或较少的变动

基本原理

手动分包的总体思路是:

  1. 先单独的打包公共模块

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构

js
//源码,入口文件index.js
+import $ from "jquery";
+import _ from "lodash";
+_.isArray($(".red"));
+
//源码,入口文件index.js
+import $ from "jquery";
+import _ from "lodash";
+_.isArray($(".red"));
+

由于资源清单中包含 jquery 和 lodash 两个模块,因此打包结果的大致格式是:

js
(function (modules) {
+  //...
+})({
+  // index.js文件的打包结果并没有变化
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
+    _.isArray($(".red"));
+  },
+  // 由于资源清单中存在,jquery的代码并不会出现在这里
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
+  },
+  // 由于资源清单中存在,lodash的代码并不会出现在这里
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = lodash;
+  },
+});
+
(function (modules) {
+  //...
+})({
+  // index.js文件的打包结果并没有变化
+  "./src/index.js": function (module, exports, __webpack_require__) {
+    var $ = __webpack_require__("./node_modules/jquery/index.js");
+    var _ = __webpack_require__("./node_modules/lodash/index.js");
+    _.isArray($(".red"));
+  },
+  // 由于资源清单中存在,jquery的代码并不会出现在这里
+  "./node_modules/jquery/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = jquery; // 直接导出资源清单的名字
+  },
+  // 由于资源清单中存在,lodash的代码并不会出现在这里
+  "./node_modules/lodash/index.js": function (
+    module,
+    exports,
+    __webpack_require__
+  ) {
+    module.exports = lodash;
+  },
+});
+
  1. 打包公共模块

打包公共模块是一个独立的打包过程

  1. 单独打包公共模块,暴露变量名 . npm run dll
js
// webpack.dll.config.js
+module.exports = {
+  mode: "production",
+  entry: {
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
+  },
+  output: {
+    filename: "dll/[name].js",
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
+};
+
// webpack.dll.config.js
+module.exports = {
+  mode: "production",
+  entry: {
+    jquery: ["jquery"], //数组
+    lodash: ["lodash"],
+  },
+  output: {
+    filename: "dll/[name].js",
+    library: "[name]", // 每个bundle暴露的全局变量名
+  },
+};
+

利用DllPlugin生成资源清单

js
// webpack.dll.config.js
+module.exports = {
+  plugins: [
+    new webpack.DllPlugin({
+      path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
+};
+
// webpack.dll.config.js
+module.exports = {
+  plugins: [
+    new webpack.DllPlugin({
+      path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
+      name: "[name]", //资源清单中,暴露的变量名
+    }),
+  ],
+};
+

运行后,即可完成公共模块打包

使用公共模块

  1. 在页面中手动引入公共模块
html
<script src="./dll/jquery.js"></script>
+<script src="./dll/lodash.js"></script>
+
<script src="./dll/jquery.js"></script>
+<script src="./dll/lodash.js"></script>
+

重新设置clean-webpack-plugin。如果使用了插件clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置

js
new CleanWebpackPlugin({
+  // 要清除的文件或目录
+  // 排除掉dll目录本身和它里面的文件
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
+
new CleanWebpackPlugin({
+  // 要清除的文件或目录
+  // 排除掉dll目录本身和它里面的文件
+  cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"],
+});
+

目录和文件的匹配规则使用的是globbing patterns语法

使用 DllReferencePlugin 控制打包结果

js
module.exports = {
+  plugins: [
+    // 资源清单
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/jquery.manifest.json"),
+    }),
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    // 资源清单
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/jquery.manifest.json"),
+    }),
+    new webpack.DllReferencePlugin({
+      manifest: require("./dll/lodash.manifest.json"),
+    }),
+  ],
+};
+

总结

手动打包的过程:

  1. 开启 output.library 暴露公共模块
  2. 用 DllPlugin 创建资源清单
  3. 用 DllReferencePlugin 使用资源清单

手动打包的注意事项:

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共 JS,以及避免被删除
  3. 不要对小型的公共 JS 库使用

优点:

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点:

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想

详解 dllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin 的使用方法如下:

js
// 单独配置在一个文件中
+// webpack.dll.conf.js
+const path = require("path");
+const webpack = require("webpack");
+module.exports = {
+  entry: {
+    // 想统一打包的类库
+    vendor: ["react"],
+  },
+  output: {
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
+  },
+  plugins: [
+    new webpack.DllPlugin({
+      // name 必须和 output.library 一致
+      name: "[name]-[hash]",
+      // 该属性需要与 DllReferencePlugin 中一致
+      context: __dirname,
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
+
// 单独配置在一个文件中
+// webpack.dll.conf.js
+const path = require("path");
+const webpack = require("webpack");
+module.exports = {
+  entry: {
+    // 想统一打包的类库
+    vendor: ["react"],
+  },
+  output: {
+    path: path.join(__dirname, "dist"),
+    filename: "[name].dll.js",
+    library: "[name]-[hash]",
+  },
+  plugins: [
+    new webpack.DllPlugin({
+      // name 必须和 output.library 一致
+      name: "[name]-[hash]",
+      // 该属性需要与 DllReferencePlugin 中一致
+      context: __dirname,
+      path: path.join(__dirname, "dist", "[name]-manifest.json"),
+    }),
+  ],
+};
+

然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中

js
// webpack.conf.js
+module.exports = {
+  // ...省略其他配置
+  plugins: [
+    new webpack.DllReferencePlugin({
+      context: __dirname,
+      // manifest 就是之前打包出来的 json 文件
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
+
// webpack.conf.js
+module.exports = {
+  // ...省略其他配置
+  plugins: [
+    new webpack.DllReferencePlugin({
+      context: __dirname,
+      // manifest 就是之前打包出来的 json 文件
+      manifest: require("./dist/vendor-manifest.json"),
+    }),
+  ],
+};
+

可以通过一些小的优化点来加快打包速度

  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

2.2 自动分包(会降低构建效率,开发效率提升,新的模块不需要手动处理了)

  1. 基本原理

不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack 会自动的按照策略进行分包

实际上,webpack 在内部是使用 SplitChunksPlugin 进行分包的 过去有一个库 CommonsChunkPlugin 也可以实现分包,不过由于该库某些地方并不完善,到了 webpack4 之后,已被 SplitChunksPlugin 取代

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack 开启了一个新的 chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新 chunk 的产物
  1. 分包策略的基本配置

webpack 提供了 optimization 配置项,用于配置一些优化信息

其中 splitChunks 是分包策略的配置

js
module.exports = {
+  optimization: {
+    splitChunks: {
+      // 分包策略
+    },
+  },
+};
+
module.exports = {
+  optimization: {
+    splitChunks: {
+      // 分包策略
+    },
+  },
+};
+

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

chunks

该配置项用于配置需要应用分包策略的 chunk

我们知道,分包是从已有的 chunk 中分离出新的 chunk,那么哪些 chunk 需要分离呢

chunks 有三个取值,分别是:

  • all: 对于所有的 chunk 都要应用分包策略
  • async:【默认】仅针对异步 chunk 应用分包策略
  • initial:仅针对普通 chunk 应用分包策略

所以,你只需要配置 chunks 为 all 即可

maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则 webpack 会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和 tree shaking

  1. 分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新 chunk 名称的分隔符,默认值~

  • minChunks:一个模块被多少个 chunk 使用时,才会进行分包,默认值 1。如果我自己写一个文件,默认也不分包,因为自己写的那个太小,没达到拆分的条件,所以要配合 minSize 使用。

  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值 30000

  1. 缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack 按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack 提供了两个缓存组:

js
module.exports = {
+  optimization: {
+    splitChunks: {
+      //全局配置
+      cacheGroups: {
+        // 属性名是缓存组名称,会影响到分包的chunk名
+        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
+        vendors: {
+          test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+        },
+        default: {
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
+          priority: -20, // 优先级
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
+
module.exports = {
+  optimization: {
+    splitChunks: {
+      //全局配置
+      cacheGroups: {
+        // 属性名是缓存组名称,会影响到分包的chunk名
+        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
+        vendors: {
+          test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
+          priority: -10, // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
+        },
+        default: {
+          minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
+          priority: -20, // 优先级
+          reuseExistingChunk: true, // 重用已经被分离出去的chunk
+        },
+      },
+    },
+  },
+};
+

很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了

但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离

js
module.exports = {
+  optimization: {
+    splitChunks: {
+      chunks: "all",
+      cacheGroups: {
+        styles: {
+          // 样式抽离
+          test: /\.css$/, // 匹配样式模块
+          minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
+  },
+  module: {
+    rules: [
+      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
+  },
+  plugins: [
+    new CleanWebpackPlugin(),
+    new HtmlWebpackPlugin({
+      template: "./public/index.html",
+      chunks: ["index"],
+    }),
+    new MiniCssExtractPlugin({
+      filename: "[name].[hash:5].css",
+      // chunkFilename是配置来自于分割chunk的文件名
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
+
module.exports = {
+  optimization: {
+    splitChunks: {
+      chunks: "all",
+      cacheGroups: {
+        styles: {
+          // 样式抽离
+          test: /\.css$/, // 匹配样式模块
+          minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
+          minChunks: 2, // 覆盖默认的最小chunk引用数
+        },
+      },
+    },
+  },
+  module: {
+    rules: [
+      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
+    ],
+  },
+  plugins: [
+    new CleanWebpackPlugin(),
+    new HtmlWebpackPlugin({
+      template: "./public/index.html",
+      chunks: ["index"],
+    }),
+    new MiniCssExtractPlugin({
+      filename: "[name].[hash:5].css",
+      // chunkFilename是配置来自于分割chunk的文件名
+      chunkFilename: "common.[hash:5].css",
+    }),
+  ],
+};
+
  1. 配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个 html 页面指定需要的 chunk,否则都会引入进去,这就造成了问题

js
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
+
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index~other", "vendors~index~other", "index"],
+});
+

我们必须手动的指定被分离出去的 chunk 名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

shell
npm i -D html-webpack-plugin@next
+
npm i -D html-webpack-plugin@next
+

做出以下配置即可:

js
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index"],
+});
+
new HtmlWebpackPlugin({
+  template: "./public/index.html",
+  chunks: ["index"],
+});
+

它会自动的找到被 index 分离出去的 chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

  1. 原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  • 检查每个 chunk 编译的结果
  • 根据分包策略,找到那些满足策略的模块
  • 根据分包策略,生成新的 chunk 打包这些模块(代码有所变化)
  • 把打包出去的模块从原始包中移除,并修正原始包代码

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量 webpackJsonp,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码

2.3 代码压缩

单模块体积优化

  1. 为什么要进行代码压缩: 减少代码体积;破坏代码的可读性,提升破解成本;
  2. 什么时候要进行代码压缩: 生产环境
  3. 使用什么压缩工具: 目前最流行的代码压缩工具主要有两个:UglifyJs 和 Terser

UglifyJs 是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持 ES6 语法,所以目前的流行度已有所下降。

Terser 是一个新起的代码压缩工具,支持 ES6+语法,因此被很多构建工具内置使用。webpack 安装后会内置 Terser,当启用生产环境后即可用其进行代码压缩。

因此,我们选择 Terser

关于副作用 side effect

副作用:函数运行过程中,可能会对外部环境造成影响的功能

如果函数中包含以下代码,该函数叫做副作用函数:

  • 异步代码
  • localStorage
  • 对外部数据的修改

如果一个函数没有副作用,同时,函数的返回结果仅依赖参数,则该函数叫做纯函数(pure function)

纯函数非常有利于压缩优化。可以手动指定那些是纯函数:pure_funcs:['Math.random']

Terser

在 Terser 的官网可尝试它的压缩效果

Terser 官网:https://terser.org/

webpack+Terser

webpack 自动集成了 Terser

如果你想更改、添加压缩工具,又或者是想对 Terser 进行配置,使用下面的 webpack 配置即可

js
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
+module.exports = {
+  optimization: {
+    minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
+    ],
+  },
+};
+
const TerserPlugin = require("terser-webpack-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
+module.exports = {
+  optimization: {
+    minimize: true, // 是否要启用压缩,默认情况下,生产环境会自动开启
+    minimizer: [
+      // 压缩时使用的插件,可以有多个
+      new TerserPlugin(),
+      new OptimizeCSSAssetsPlugin(),
+    ],
+  },
+};
+

2.4 tree shaking

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

  1. 背景 某些模块导出的代码并不一定会被用到,第三方库就是个典型例子
js
// myMath.js
+export function add(a, b) {
+  console.log("add");
+  return a + b;
+}
+
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
+}
+// index.js
+import { add } from "./myMath";
+console.log(add(1, 2));
+
// myMath.js
+export function add(a, b) {
+  console.log("add");
+  return a + b;
+}
+
+export function sub(a, b) {
+  console.log("sub");
+  return a - b;
+}
+// index.js
+import { add } from "./myMath";
+console.log(add(1, 2));
+

tree shaking 用于移除掉不会用到的导出

  1. 使用

webpack2 开始就支持了 tree shaking

只要是生产环境,tree shaking 自动开启

  1. 原理

webpack 会从入口模块出发寻找依赖关系

当解析一个模块时,webpack 会根据 ES6 的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack 之所以选择 ES6 的模块导入语句,是因为 ES6 模块有以下特点:commonjs 不具备

  • 导入导出语句只能是顶层语句
  • import 的模块名只能是字符串常量
  • import 绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack 坚持的原则是:保证代码正常运行,然后再尽量 tree shaking

所以,如果你依赖的是一个导出的对象,由于 JS 语言的动态特性,以及 webpack 还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量:

  • 使用 export xxx 导出,而不使用 export default {xxx}导出。后者会整个导出,但是不一定都需要。
  • 使用 import {xxx} from "xxx"导入,而不使用 import xxx from "xxx"导入

依赖分析完毕后,webpack 会根据每个模块每个导出是否被使用,标记其他导出为 dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些 dead code 代码

  1. 使用第三方库

某些第三方库可能使用的是 commonjs 的方式导出,比如 lodash

又或者没有提供普通的 ES6 方式导出

对于这些库,tree shaking 是无法发挥作用的

因此要寻找这些库的 es6 版本,好在很多流行但没有使用的 ES6 的第三方库,都发布了它的 ES6 版本,比如 lodash-es

  1. 作用域分析

tree shaking 本身并没有完善的作用域分析,可能导致在一些 dead code 函数中的依赖仍然会被视为依赖 比如 a 引用 b,b 引用了 lodash,但是 a 没有用到 b 用 lodash 的导出代码 插件 webpack-deep-scope-plugin 提供了作用域分析,可解决这些问题

  1. 副作用问题

webpack 在 tree shaking 的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何 tree shaking

因此,当 webpack 无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

js
//common.js
+var n = Math.random();
+
+//index.js
+import "./common.js";
+
//common.js
+var n = Math.random();
+
+//index.js
+import "./common.js";
+

虽然我们根本没用有 common.js 的导出,但 webpack 担心 common.js 有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

在 package.json 中加入 sideEffects

json
{
+  "sideEffects": false
+}
+
{
+  "sideEffects": false
+}
+

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些 css 文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是 src/common.js 的文件,都有副作用
js
{
+    "sideEffects": ["!src/common.js"]
+}
+
{
+    "sideEffects": ["!src/common.js"]
+}
+

这种方式我们一般不处理,通常是一些第三方库在它们自己的 package.json 中标注

webpack 无法对 css 完成 tree shaking,因为 css 跟 es6 没有半毛钱关系。

因此对 css 的 tree shaking 需要其他插件完成。例如:purgecss-webpack-plugin。注意:purgecss-webpack-plugin 对 css module 无能为力

2.5 懒加载

可以理解为异步 chunk

js
// 异步加载使用import语法
+const btn = document.querySelector("button");
+btn.onclick = async function () {
+  //动态加载
+  //import 是ES6的草案
+  //浏览器会使用JSOP的方式远程去读取一个js模块
+  //import()会返回一个promise   (返回结果类似于 * as obj)
+  // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
+  const result = chunk([3, 5, 6, 7, 87], 2);
+  console.log(result);
+};
+
+// 因为是动态的,所以tree shaking没了
+// 如果想用该咋办?搞成静态依赖就行 所以加上了util.js
+
// 异步加载使用import语法
+const btn = document.querySelector("button");
+btn.onclick = async function () {
+  //动态加载
+  //import 是ES6的草案
+  //浏览器会使用JSOP的方式远程去读取一个js模块
+  //import()会返回一个promise   (返回结果类似于 * as obj)
+  // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
+  const { chunk } = await import("./util"); // 搞成静态依赖就行 所以加上了util.js
+  const result = chunk([3, 5, 6, 7, 87], 2);
+  console.log(result);
+};
+
+// 因为是动态的,所以tree shaking没了
+// 如果想用该咋办?搞成静态依赖就行 所以加上了util.js
+

2.6 gzip

gzip 是一种压缩文件的算法

B/S 结构中的压缩传输

  • 浏览器告诉服务器支持那些压缩方式。
  • 响应头:什么方式解压->ungzip

优点:传输效率可能得到大幅提升

缺点:服务器的压缩需要时间,客户端的解压需要时间

gzip 的原理

gizp 压缩是一种 http 请求优化方式,通过减少文件体积来提高加载速度。html、js、css 文件甚至 json 数据都可以用它压缩,可以减小 60%以上的体积。前端配置 gzip 压缩,并且服务端使用 nginx 开启 gzip,用来减小网络传输的流量大小。

使用 webpack 进行预压缩

使用 compression-webpack-plugin 插件对打包结果进行预压缩,可以移除服务器的压缩时间

js
plugins: [
+  // 参考文档配置即可,一般取默认
+  new CmpressionWebpackPlugin({
+    test: /\.js/, //希望对js进行预压缩
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
+
plugins: [
+  // 参考文档配置即可,一般取默认
+  new CmpressionWebpackPlugin({
+    test: /\.js/, //希望对js进行预压缩
+    minRatio: 0.5, // 小于0.5才会压缩
+  }),
+];
+

2.7 按需加载

在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 lodash 这种大型类库同样可以使用这个功能。

按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。

2.8 其他

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css
  • 利⽤ CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为 CDN 上对应的路径。可以利⽤ webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径
  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

提取第三方库 vendor: 这是也是 webpack 大法的 code splitting,提取一些第三方的库,从而减小 app.js 的大小。 代码层面做好懒加载,网络层面把 CDN、本地缓存用好,前端页面问题基本解决一大半了。剩下主要就是接口层面和“视觉上的快”的优化了,骨架屏先搞起,渲染一个“假页面”占位;接口该合并的合并,该拆分的拆分,如果是可滚动的长页面,就分批次请求

总结:如果有一个工程打包特别大-如何进行优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。

3. 运行性能

运行性能是指,JS 代码在浏览器端的运行速度,它主要取决于我们如何书写高性能的代码

永远不要过早的关注于性能,因为你在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率 性能优化主要从上面三个维度入手,性能优化没有完美的解决方案,需要具体情况具体分析

4. webpack5 内置优化

  1. webpack scope hoisting

scope hoisting 是 webpack 的内置优化,它是针对模块的优化,在生产环境打包时会自动开启。

在未开启 scope hoisting 时,webpack 会将每个模块的代码放置在一个独立的函数环境中,这样是为了保证模块的作用域互不干扰。

而 scope hoisting 的作用恰恰相反,是把多个模块的代码合并到一个函数环境中执行。在这一过程中,webpack 会按照顺序正确的合并模块代码,同时对涉及的标识符做适当处理以避免重名。

这样做的好处是减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

但 scope hoisting 的启用是有前提的,如果遇到某些模块多次被其他模块引用,或者使用了动态导入的模块,或者是非 ESM 的模块,都不会有 scope hoisting。

  1. 清除输出目录

webpack5清除输出目录开箱可用,无须安装clean-webpack-plugin,具体做法如下:

javascript
module.exports = {
+  output: {
+    clean: true,
+  },
+};
+
module.exports = {
+  output: {
+    clean: true,
+  },
+};
+
  1. top-level-await

webpack5现在允许在模块的顶级代码中直接使用await

javascript
// src/index.js
+const resp = await fetch("http://www.baidu.com");
+const jsonBody = await resp.json();
+export default jsonBody;
+
// src/index.js
+const resp = await fetch("http://www.baidu.com");
+const jsonBody = await resp.json();
+export default jsonBody;
+

目前,top-level-await还未成为正式标准,因此,对于webpack5而言,该功能是作为experiments发布的,需要在webpack.config.js中配置开启

javascript
// webpack.config.js
+module.exports = {
+  experiments: {
+    topLevelAwait: true,
+  },
+};
+
// webpack.config.js
+module.exports = {
+  experiments: {
+    topLevelAwait: true,
+  },
+};
+
  1. 打包体积优化

webpack5对模块的合并、作用域提升、tree shaking等处理更加智能

javascript
// webpack.config.js
+module.exports = {
+  mode: "production",
+  devtool: "source-map",
+  entry: {
+    index1: "./src/index1.js",
+    index2: "./src/index2.js",
+  },
+};
+
// webpack.config.js
+module.exports = {
+  mode: "production",
+  devtool: "source-map",
+  entry: {
+    index1: "./src/index1.js",
+    index2: "./src/index2.js",
+  },
+};
+

  1. 打包缓存开箱即用

webpack4中,需要使用cache-loader缓存打包结果以优化之后的打包性能

而在webpack5中,默认就已经开启了打包缓存,无须再安装cache-loader

默认情况下,webpack5是将模块的打包结果缓存到内存中,可以通过cache配置进行更改

javascript
const path = require("path");
+
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  cache: {
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
+    // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
+  },
+};
+
const path = require("path");
+
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  cache: {
+    type: "filesystem", // 缓存类型,支持:memory、filesystem
+    cacheDirectory: path.resolve(__dirname, "node_modules/.cache/webpack"), // 缓存目录,仅类型为 filesystem 有效
+    // 更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache
+  },
+};
+

关于cache的更多配置参考:https://webpack.docschina.org/configuration/other-options/#cache

  1. 资源模块

webpack4中,针对资源型文件我们通常使用file-loaderurl-loaderraw-loader进行处理

由于大部分前端项目都会用到资源型文件,因此webpack5原生支持了资源型模块

详见:https://webpack.docschina.org/guides/asset-modules/

javascript
// index.js
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
+
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
+
// index.js
+import bigPic from "./assets/big-pic.png"; // 期望得到路径
+import smallPic from "./assets/small-pic.jpg"; // 期望得到base64
+import yueyunpeng from "./assets/yueyunpeng.gif"; // 期望根据文件大小决定是路径还是base64
+import raw from "./assets/raw.txt"; // 期望得到原始文件内容
+
+console.log("big-pic.png", bigPic);
+console.log("small-pic.jpg", smallPic);
+console.log("yueyunpeng.gif", yueyunpeng);
+console.log("raw.txt", raw);
+
javascript
// webpack.config.js
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  devServer: {
+    port: 8080,
+  },
+  plugins: [new HtmlWebpackPlugin()],
+  output: {
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
+  },
+  module: {
+    rules: [
+      {
+        test: /\.png/,
+        type: "asset/resource", // 作用类似于 file-loader
+      },
+      {
+        test: /\.jpg/,
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
+      },
+      {
+        test: /\.txt/,
+        type: "asset/source", // 作用类似于 raw-loader
+      },
+      {
+        test: /\.gif/,
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        generator: {
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
+        },
+        parser: {
+          dataUrlCondition: {
+            maxSize: 4 * 1024, // 4kb以下使用 data uri
+            // 4kb以上就是文件
+          },
+        },
+      },
+    ],
+  },
+};
+
// webpack.config.js
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+module.exports = {
+  mode: "development",
+  devtool: "source-map",
+  entry: "./src/index.js",
+  devServer: {
+    port: 8080,
+  },
+  plugins: [new HtmlWebpackPlugin()],
+  output: {
+    filename: "main.js",
+    path: path.resolve(__dirname, "dist"),
+    assetModuleFilename: "assets/[hash:5][ext]", // 在这里自定义资源文件保存的文件名
+  },
+  module: {
+    rules: [
+      {
+        test: /\.png/,
+        type: "asset/resource", // 作用类似于 file-loader
+      },
+      {
+        test: /\.jpg/,
+        type: "asset/inline", // 作用类似于 url-loader 文件大小不足的场景
+      },
+      {
+        test: /\.txt/,
+        type: "asset/source", // 作用类似于 raw-loader
+      },
+      {
+        test: /\.gif/,
+        type: "asset", // 作用类似于 url-loader。在导出一个 data uri 和发送一个单独的文件之间自动选择
+        generator: {
+          filename: "gif/[hash:5][ext]", // 这里的配置会覆盖 assetModuleFilename
+        },
+        parser: {
+          dataUrlCondition: {
+            maxSize: 4 * 1024, // 4kb以下使用 data uri
+            // 4kb以上就是文件
+          },
+        },
+      },
+    ],
+  },
+};
+

字节跳动面试题:说一下项目里有做过哪些 webpack 上的优化

  1. 对传输性能的优化
  • 压缩和混淆 使用 Uglifyjs 或其他类似工具对打包结果进行压缩、混淆,可以有效的减少包体积
  • tree shaking 项目中尽量使用 ESM,可以有效利用 tree shaking 优化,降低包体积
  • 抽离公共模块 将一些公共代码单独打包,这样可以充分利用浏览器缓存,其他代码变动后,不影响公共代码,浏览器可以直接从缓存中找到公共代码。 具体方式有多种,比如 dll、splitChunks
  • 异步加载 对一些可以延迟执行的模块可以使用动态导入的方式异步加载它们,这样在打包结果中,它们会形成单独的包,同时,在页面一开始解析时并不需要加载它们,而是页面解析完成后,执行 JS 的过程中去加载它们。 这样可以显著提高页面的响应速度,在单页应用中尤其有用。
  • CDN 对一些知名的库使用 CDN,不仅可以节省打包时间,还可以显著提升库的加载速度
  • gzip 目前浏览器普遍支持 gzip 格式,因此可以将静态文件均使用 gzip 进行压缩
  • 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
  1. 对打包过程的优化
  • noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  • externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  • 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  • 开启 loader 缓存 可以利用 cache-loader 缓存 loader 的编译结果,避免在源码没有变动时反复编译
  • 开启多线程编译 可以利用 thread-loader 开启多线程编译,提升编译效率
  • 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库
  1. 对开发体验的优化
  • lint 使用 eslint、stylelint 等工具保证团队代码风格一致
  • HMR 使用热替换避免页面刷新导致的状态丢失,提升开发体验

CSS

TIP

CSS 渲染性能优化

  1. 使用 id selector 非常的高效。在使用 id selector 的时候需要注意一点:因为 id 是唯一的,所以不需要既指定 id 又指定 tagName:
css
/* Bad  */
+p#id1 {
+  color: red;
+}
+
+/* Good  */
+#id1 {
+  color: red;
+}
+
/* Bad  */
+p#id1 {
+  color: red;
+}
+
+/* Good  */
+#id1 {
+  color: red;
+}
+
  1. 不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配非常慢。更不要这样写:p[id="id1"]。这样将 id selector 退化成 attribute selector。
css
/* Bad  */
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
+/* Good  */
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
+
/* Bad  */
+p[id="jartto"] {
+  color: red;
+}
+p[class="blog"] {
+  color: red;
+}
+/* Good  */
+#jartto {
+  color: red;
+}
+.blog {
+  color: red;
+}
+
  1. 通常将浏览器前缀置于前面,将标准样式属性置于最后,类似:
css
.foo {
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+
.foo {
+  -moz-border-radius: 5px;
+  border-radius: 5px;
+}
+

这里推荐参阅 CSS 规范-优化方案:http://nec.netease.com/standard/css-optimize.html

  1. 遵守 CSSLint 规则

font-faces         不能使用超过 5 个 web 字体

import            禁止使用@import

regex-selectors      禁止使用属性选择器中的正则表达式选择器

universal-selector       禁止使用通用选择器*

unqualified-attributes    禁止使用不规范的属性选择器

zero-units        0 后面不要加单位

overqualified-elements    使用相邻选择器时,不要使用不必要的选择器

shorthand          简写样式属性

duplicate-background-images 相同的 url 在样式表中不超过一次

更多的 CSSLint 规则可以参阅:https://github.com/CSSLint/csslint

  1. 不要使用 @import

使用 @import 引入 CSS 会影响浏览器的并行下载。使用 @import 引用的 CSS 文件只有在引用它的那个 CSS 文件被下载、解析之后,浏览器才会知道还有另外一个 CSS 需要下载,这时才去下载,然后下载后开始解析、构建 Render Tree 等一系列操作。

多个 @import 会导致下载顺序紊乱。在 IE 中,@import 会引发资源文件的下载顺序被打乱,即排列在 @import 后面的 JS 文件先于 @import 下载,并且打乱甚至破坏 @import 自身的并行下载。

  1. 避免过分重排(Reflow)

所谓重排就是浏览器重新计算布局位置与大小。常见的重排元素:

width
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
+min-height
+
width
+height
+padding
+margin
+display
+border-width
+border
+top
+position
+font-size
+float
+text-align
+overflow-y
+font-weight
+overflow
+left
+font-family
+line-height
+vertical-align
+right
+clear
+white-space
+bottom
+min-height
+
  1. 依赖继承。如果某些属性可以继承,那么自然没有必要在写一遍。

  2. 其他:使用 id 选择器非常高效,因为 id 是唯一的;使用渐进增强的方案;值缩写;避免耗性能的属性;背景图优化合并;文件压缩

网络层面

总结

  • 优化打包体积:利用一些工具压缩、混淆最终打包代码,减少包体积
  • 多目标打包:利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积
  • 压缩:现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的
  • CDN:利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存
  • 缓存:对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存
  • http2:开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据
  • 雪碧图:对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的
  • defer、async:通过 defer 和 async 属性,可以让页面尽早加载 js 文件
  • prefetch、preload:通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源;通过 preload 属性,可以让页面预先下载本页面可能要用到的资源
  • 多个静态资源域:对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载 (http2 之前,浏览器开多个 tcp,同一个域下最大数 6 个,为了多,静态资源分多个域存储,突破 6 个的限制)

CDN

CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的 CDN 系统构成:

  • 分发服务系统: 最基本的工作单元就是 Cache 设备,cache(边缘 cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时 cache 还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache 设备的数量、规模、总服务能力是衡量一个 CDN 系统服务能力的最基本的指标。

  • 负载均衡系统: 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。全局负载均衡主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的 cache 的物理位置。本地负载均衡主要负责节点内部的设备负载均衡

  • 运营管理系统: 运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。

作用

CDN 一般会用来托管 Web 资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用 CDN 来加速这些资源的访问。

(1)在性能方面,引入 CDN 的作用在于:

  • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
  • 部分资源请求分配给了 CDN,减少了服务器的负载

(2)在安全方面,CDN 有助于防御 DDoS、MITM 等网络攻击:

  • 针对 DDoS:通过监控分析异常流量,限制其请求频率
  • 针对 MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN 作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。 CDN 还会把文件最小化或者压缩文档的优化

原理

它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源

CDN 和 DNS 有着密不可分的联系,先来看一下 DNS 的解析域名过程,在浏览器输入 www.test.com 的解析过程如下:

  1. 检查浏览器缓存
  2. 检查操作系统缓存,常见的如 hosts 文件
  3. 检查路由器缓存
  4. 如果前几步都没没找到,会向 ISP(网络服务提供商)的 LDNS 服务器查询
  5. 如果 LDNS 服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
  • 根服务器返回顶级域名(TLD)服务器如.com,.cn,.org 等的地址,该例子中会返回.com 的地址
  • 接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回.test 的地址
  • 接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标 IP,本例子会返回www.test.com的地址
  • Local DNS Server 会缓存结果,并返回给用户,缓存在系统中

CDN 的工作原理:

  1. 用户未使用 CDN 缓存资源的过程:
  2. 浏览器通过 DNS 对域名进行解析(就是上面的 DNS 解析过程),依次得到此域名对应的 IP 地址
  3. 浏览器根据得到的 IP 地址,向域名的服务主机发送数据请求
  4. 服务器向浏览器返回响应数据

(2)用户使用 CDN 缓存资源的过程:

  1. 对于点击的数据的 URL,经过本地 DNS 系统的解析,发现该 URL 对应的是一个 CDN 专用的 DNS 服务器,DNS 系统就会将域名解析权交给 CNAME 指向的 CDN 专用的 DNS 服务器。
  2. CND 专用 DNS 服务器将 CND 的全局负载均衡设备 IP 地址返回给用户
  3. 用户向 CDN 的全局负载均衡设备发起数据请求
  4. CDN 的全局负载均衡设备根据用户的 IP 地址,以及用户请求的内容 URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求
  5. 区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的 IP 地址返回给全局负载均衡设备
  6. 全局负载均衡设备把服务器的 IP 地址返回给用户
  7. 用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。

CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的 IP 地址,或者该域名的一个 CNAME,然后再根据这个 CNAME 来查找对应的 IP 地址。

使用场景

  • 使用第三方的 CDN 服务:如果想要开源一些项目,可以使用第三方的 CDN 服务
  • 使用 CDN 进行静态资源的缓存:将自己网站的静态资源放在 CDN 上,比如 js、css、图片等。可以将整个项目放在 CDN 上,完成一键部署。
  • 直播传送:直播本质上是使用流媒体进行传送,CDN 也是支持流媒体传送的,所以直播完全可以使用 CDN 来提高访问速度。CDN 在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

TODO:cdn 加速原理,没有缓存到哪里拿,CDN 回源策略

分发的内容

静态内容:即使是静态内容也不是一直保存在 cdn,源服务器发送文件给 CDN 的时候就可以利用 HTTP 头部的 cache-control 可以设置文件的缓存形式,cdn 就知道哪些内容可以保存 no-cache,那些不能 no-store,那些保存多久 max-age

动态内容:

工作流程:

静态内容:源服务器把静态内容提前备份给 cdn(push),世界各地访问的时候就进的 cdn 服务器会把静态内容提供给用户,不需要每次劳烦源服务器。如果没有提前备份,cdn 问源服务器要(pull),然后 cdn 备份,其他请球的用户可以马上拿到。

动态内容:源服务器很难做到提前预测到每个用户的动态内容提前给到 cdn,如果等到用户索取动态内容 cdn 再向源服务器获取,这样 cdn 提供不了加速服务。但是有些是可以提供动态服务的:时间,有些 cdn 会提供可以运行在 cdn 上的接口,让源服务器用这些 cdn 接口,而不是源服务器自己的代码,用户就可以直接从 cdn 获取时间

问:cdn 用什么方式来转移流量实现负载均衡?

和 DNS 域名解析根服务器的做法相似:任播通信:服务器对外都拥有同一个 ip 地址,如果收到了请求,请求就会由距离用户最近的服务器响应,任播技术把流量转移给没超载的服务器可以缓解。CDN 还会用 TLS/SSL 证书对网站进行保护。 我们可以把项目中的所有静态资源都放到 CDN 上(收费),也可以利用现成免费的 CDN 获取公共库的资源

js

+首先我们需要告诉webpack不要对公共库进行打包
+// vue.config.js
+module.exports = {
+  configureWebpack: {
+    externals: {
+      vue: "Vue",
+      vuex: "Vuex",
+      "vue-router": "VueRouter",
+    }
+  },
+};
+然后在页面中手动加入cdn链接这里使用bootcn
+对于vuex和vue-router使用这种传统的方式引入的话会自动成为Vue的插件因此需要去掉Vue.use(xxx)
+
+我们可以使用下面的代码来进行兼容
+// store.js
+import Vue from "vue";
+import Vuex from "vuex";
+
+if(!window.Vuex){
+  // 没有使用传统的方式引入Vuex
+  Vue.use(Vuex);
+}
+
+// router.js
+import VueRouter from "vue-router";
+import Vue from "vue";
+
+if(!window.VueRouter){
+  // 没有使用传统的方式引入VueRouter
+  Vue.use(VueRouter);
+}
+

+首先,我们需要告诉webpack不要对公共库进行打包
+// vue.config.js
+module.exports = {
+  configureWebpack: {
+    externals: {
+      vue: "Vue",
+      vuex: "Vuex",
+      "vue-router": "VueRouter",
+    }
+  },
+};
+然后,在页面中手动加入cdn链接,这里使用bootcn
+对于vuex和vue-router,使用这种传统的方式引入的话会自动成为Vue的插件,因此需要去掉Vue.use(xxx)
+
+我们可以使用下面的代码来进行兼容
+// store.js
+import Vue from "vue";
+import Vuex from "vuex";
+
+if(!window.Vuex){
+  // 没有使用传统的方式引入Vuex
+  Vue.use(Vuex);
+}
+
+// router.js
+import VueRouter from "vue-router";
+import Vue from "vue";
+
+if(!window.VueRouter){
+  // 没有使用传统的方式引入VueRouter
+  Vue.use(VueRouter);
+}
+

增加带宽

增加带宽可以提高资源的访问速度,从而提高首批的加载速度,我司项目带宽由 2M 升级到 5M,效果明显。

http 内置优化

  • Http2
    • 头部压缩:专门的 HPACK 压缩算法
      • 索引表:客户端和服务器共同维护的一张表,表的内容分为 61 位的静态表(保存常用信息,例如:host/content-type)和动态表
      • 霍夫曼编码
  • 链路复用
    • Http1 建立起 Tcp 连接,发送请求之后,服务器在处理请求的等待期间,这个期间又没有数据去发送,称为空挡期。链接断开是在服务器响应回溯之后
      • keep-alive 链接保持一段时间
      • HTTP2 可以利用空档期
      • 不需要再重复建立链接
  • 二进制帧
    • Http1.1 文本字符分割的数据流,解析慢且容易出错
    • 二进制帧:帧长度、帧类型、帧标识 补充:采用 Http2 之后,可以减少资源合并的操作,因为首部压缩已经减少了多请求传输的数据量

数据传输层面

  • 缓存:浏览器缓存
    • 强缓存
      • cache-contorl: max-age=30
      • expires: Wed, 21 Oct 2021 07:28:00 GMT
  • 协商缓存
    • etag
    • last-modified
    • if-modified-since
    • if-none-match
  • 压缩
    • 数据压缩:gzip
    • 代码文件压缩:HTML/CSS/JS 中的注释、空格、长变量等
    • 静态资源:字体图标,去除元数据,缩小尺寸以及分辨率
    • 头与报文
      • http1.1 中减少不必要的头
      • 减少 cookie 数据量

Vue

Vue 开发优化

使用 key

对于通过循环生成的列表,应给每个列表项一个稳定且唯一的 key,这有利于在列表变动时,尽量少的删除、新增、改动元素

使用冻结的对象

冻结的对象不会被响应化

使用函数式组件

参见函数式组件

使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们

非实时绑定的表单项

当使用 v-model 绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致 vue 发生重渲染(rerender),这会带来一些性能的开销。

特别是当用户改变表单项时,页面有一些动画正在进行中,由于 JS 执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。

我们可以通过使用 lazy 或不使用 v-model 的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。

保持对象引用稳定

在绝大部分情况下,vue 触发 rerender 的时机是其依赖的数据发生变化

若数据没有发生变化,哪怕给数据重新赋值了,vue 也是不会做出任何处理的

下面是 vue 判断数据没有变化的源码

js
// value 为旧值, newVal 为新值
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
+}
+
// value 为旧值, newVal 为新值
+if (newVal === value || (newVal !== newVal && value !== value)) {
+  //NaN
+  return;
+}
+

因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。

对于原始数据类型,保持其值不变即可

对于对象类型,保持其引用不变即可

从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染

使用 v-show 替代 v-if

对于频繁切换显示状态的元素,使用 v-show 可以保证虚拟 dom 树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量 dom 元素的节点,这一点极其重要

关键字:频繁切换显示状态、内部包含大量 dom 元素

使用延迟装载(defer)

首页白屏时间主要受到两个因素的影响:

  • 打包体积过大 巨型包需要消耗大量的传输时间,导致 JS 传输完成前页面只有一个<div>,没有可显示的内容 <div id="app">好看的东西<div>

  • 需要立即渲染的内容太多 JS 传输完成后,浏览器开始执行 JS 构造页面。 但可能一开始要渲染的组件太多,不仅 JS 执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏

打包体积过大需要自行优化打包体积,本节不予讨论

可以进行分包

本节仅讨论渲染内容太多的问题。

一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来

延迟装载是一个思路,本质上就是利用 requestAnimationFrame 事件分批渲染内容,它的具体实现多种多样

使用 keep-alive

keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态(不仅是数据的保留,还要真实 dom 的保留)。

keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。

受 keep-alive 的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后

原理 在具体的实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象

js
// keep-alive 内部的声明周期函数
+created () {
+  this.cache = Object.create(null)
+  this.keys = []
+}
+
+
// keep-alive 内部的声明周期函数
+created () {
+  this.cache = Object.create(null)
+  this.keys = []
+}
+
+

key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值 cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM

在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。 当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素

js
render(){
+  const slot = this.$slots.default; // 获取默认插槽
+  const vnode = getFirstComponentChild(slot); // 得到插槽中的第一个组件的vnode
+  const name = getComponentName(vnode.componentOptions); //获取组件名字
+  const { cache, keys } = this; // 获取当前的缓存对象和key数组
+  const key = ...; // 获取组件的key值,若没有,会按照规则自动生成
+  if (cache[key]) {
+    // 有缓存
+    // 重用组件实例
+    vnode.componentInstance = cache[key].componentInstance
+    remove(keys, key); // 删除key
+    // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
+    keys.push(key);
+  } else {
+    // 无缓存,进行缓存
+    cache[key] = vnode
+    keys.push(key)
+    if (this.max && keys.length > parseInt(this.max)) {
+      // 超过最大缓存数量,移除第一个key对应的缓存
+      pruneCacheEntry(cache, keys[0], keys, this._vnode)
+    }
+  }
+  return vnode;
+}
+
render(){
+  const slot = this.$slots.default; // 获取默认插槽
+  const vnode = getFirstComponentChild(slot); // 得到插槽中的第一个组件的vnode
+  const name = getComponentName(vnode.componentOptions); //获取组件名字
+  const { cache, keys } = this; // 获取当前的缓存对象和key数组
+  const key = ...; // 获取组件的key值,若没有,会按照规则自动生成
+  if (cache[key]) {
+    // 有缓存
+    // 重用组件实例
+    vnode.componentInstance = cache[key].componentInstance
+    remove(keys, key); // 删除key
+    // 将key加入到数组末尾,这样是为了保证最近使用的组件在数组中靠后,反之靠前
+    keys.push(key);
+  } else {
+    // 无缓存,进行缓存
+    cache[key] = vnode
+    keys.push(key)
+    if (this.max && keys.length > parseInt(this.max)) {
+      // 超过最大缓存数量,移除第一个key对应的缓存
+      pruneCacheEntry(cache, keys[0], keys, this._vnode)
+    }
+  }
+  return vnode;
+}
+

长列表优化

vue-virtual-scroller

首先这个库在使用上是很方便的,就是它提供了一个标签,相当于是对 div 标签的一个修改,可以实现列表的渲染等等功能

异步组件

在代码层面,vue 组件本质上是一个配置对象

js
var comp = {
+  props: xxx,
+  data: xxx,
+  computed: xxx,
+  methods: xxx,
+};
+
var comp = {
+  props: xxx,
+  data: xxx,
+  computed: xxx,
+  methods: xxx,
+};
+

但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:

  • 需要使用 ajax 获得某个数据之后才能加载该组件
  • 为了合理的分包,组件配置对象需要通过 import(xxx)动态加载

如果一个组件需要通过异步的方式得到组件配置对象,该组件可以把它做成一个异步组件

js
/**
+ * 异步组件本质上是一个函数
+ * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
+ */
+const AsyncComponent = () => import("./MyComp");
+
+var App = {
+  components: {
+    /**
+     * 你可以把该函数当做一个组件使用(异步组件)
+     * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
+     */
+    AsyncComponent,
+  },
+};
+
/**
+ * 异步组件本质上是一个函数
+ * 该函数调用后返回一个Promise,Promise成功的结果是一个组件配置对象
+ */
+const AsyncComponent = () => import("./MyComp");
+
+var App = {
+  components: {
+    /**
+     * 你可以把该函数当做一个组件使用(异步组件)
+     * Vue会调用该函数,并等待Promise完成,完成之前该组件位置什么也不渲染
+     */
+    AsyncComponent,
+  },
+};
+

异步组件的函数不仅可以返回一个 Promise,还支持返回一个对象

应用:异步组件通常应用在路由懒加载中,以达到更好的分包;为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息

js
var routes = [
+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
+
var routes = [
+  {
+    path: "/",
+    component: async () => {
+      console.log("组件开始加载");
+      const HomeComp = await import("./Views/Home.vue");
+      console.log("组件加载完毕");
+      return HomeComp;
+    },
+  },
+];
+

Vue3 内置优化

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
js

+// vue2 的静态节点
+render(){
+  createVNode("h1", null, "Hello World")
+  // ...
+}
+
+// vue3 的静态节点
+const hoisted = createVNode("h1", null, "Hello World")//这个节点永远创建一次
+function render(){
+  // 直接使用 hoisted 即可
+}
+

+// vue2 的静态节点
+render(){
+  createVNode("h1", null, "Hello World")
+  // ...
+}
+
+// vue3 的静态节点
+const hoisted = createVNode("h1", null, "Hello World")//这个节点永远创建一次
+function render(){
+  // 直接使用 hoisted 即可
+}
+

静态属性会被提升

js
<div class="user">
+  {{user.name}}
+</div>
+
+const hoisted = { class: "user" }
+
+function render(){
+  createVNode("div", hoisted, user.name)
+  // ...
+}
+
<div class="user">
+  {{user.name}}
+</div>
+
+const hoisted = { class: "user" }
+
+function render(){
+  createVNode("div", hoisted, user.name)
+  // ...
+}
+

预字符串化

js

+<div class="menu-bar-container">
+  <div class="logo">
+    <h1>logo</h1>
+  </div>
+  <ul class="nav">
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+  </ul>
+  <div class="user">
+    <span>{{ user.name }}</span>
+  </div>
+</div>
+

+<div class="menu-bar-container">
+  <div class="logo">
+    <h1>logo</h1>
+  </div>
+  <ul class="nav">
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+    <li><a href="">menu</a></li>
+  </ul>
+  <div class="user">
+    <span>{{ user.name }}</span>
+  </div>
+</div>
+

当编译器遇到大量连续(少量则不会,目前是至少 20 个连续节点)的静态内容,会直接将其编译为一个普通字符串节点

js
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+
const _hoisted_2 = _createStaticVNode(
+  '<div class="logo"><h1>logo</h1></div><ul class="nav"><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li><li><a href="">menu</a></li></ul>'
+);
+

对 ssr 的作用非常明显

缓存事件处理函数

js
<button @click="count++">plus</button>
+
+// vue2
+render(ctx){
+  return createVNode("button", {
+    onClick: function($event){
+      ctx.count++;
+    }
+  })
+}
+
+// vue3
+render(ctx, _cache){
+  return createVNode("button", {//事件处理函数不变,可以缓存一下,保证事件处理函数只生成一次
+    onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
+  })
+}
+
+
<button @click="count++">plus</button>
+
+// vue2
+render(ctx){
+  return createVNode("button", {
+    onClick: function($event){
+      ctx.count++;
+    }
+  })
+}
+
+// vue3
+render(ctx, _cache){
+  return createVNode("button", {//事件处理函数不变,可以缓存一下,保证事件处理函数只生成一次
+    onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))//有的话,缓存;没有,count++
+  })
+}
+
+

Block Tree

vue2 在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

Block 节点记录了那些是动态的节点,对比的时候只对比动态节点

html
<form>
+  <div>
+    <label>账号:</label>
+    <input v-model="user.loginId" />
+  </div>
+  <div>
+    <label>密码:</label>
+    <input v-model="user.loginPwd" />
+  </div>
+</form>
+
<form>
+  <div>
+    <label>账号:</label>
+    <input v-model="user.loginId" />
+  </div>
+  <div>
+    <label>密码:</label>
+    <input v-model="user.loginPwd" />
+  </div>
+</form>
+

编译器 会把所有的动态节点标记,存到到根节点的数组中 ,到时候对比的时候只对比 block 动态节点。如果树不稳定,会有其他方案。

PatchFlag

依托于 vue3 强大的编译器。vue2 在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

js
<div class="user" data-id="1" title="user name">
+  {{user.name}}
+</div>
+
<div class="user" data-id="1" title="user name">
+  {{user.name}}
+</div>
+

标识:

  • 标识 1:代表元素内容是动态的
  • 标识 2:Class
  • 标识 3:class+text

启用现代模式

为了兼容各种浏览器,vue-cli 在内部使用了@babel/present-env 对代码进行降级,你可以通过.browserlistrc 配置来设置需要兼容的目标浏览器

这是一种比较偷懒的办法,因为对于那些使用现代浏览器的用户,它们也被迫使用了降级之后的代码,而降低的代码中包含了大量的 polyfill,从而提升了包的体积

因此,我们希望提供两种打包结果:

  1. 降级后的包(大),提供给旧浏览器用户使用
  2. 未降级的包(小),提供给现代浏览器用户使用

除了应用 webpack 进行多次打包外,还可以利用 vue-cli 给我们提供的命令:

shell
vue-cli-service build --modern
+
vue-cli-service build --modern
+

问题梳理

如何实现 vue 项目中的性能优化?

编码阶段

  • 尽量减少 data 中的数据,data 中的数据都会增加 gettersetter,会收集对应的 watcher
  • v-ifv-for 不能连用
  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
  • SPA 页面采用 keep-alive 缓存组件
  • 在更多的情况下,使用 v-if 替代 v-show
  • key 保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO 优化

  • 预渲染
  • 服务端渲染 SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用 cdn 加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • sourceMap 优化

用户体验

  • 骨架屏
  • PWA

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。

vue 中的 spa 应用如何优化首屏加载速度?

优化首屏加载可以从这几个方面开始:

  • 请求优化:CDN 将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
  • 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
  • gzip:开启 gzip 压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。
  • http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。
  • 懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件
  • 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加 loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
  • 合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
  • 使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
  • 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
  • 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
  • 图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
  • 使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
  • 压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片

React

总结

shouldComponentUpdate 提供了两个参数 nextProps 和 nextState,表示下一次 props 和一次 state 的值,当函数返回 false 时候,render()方法不执行,组件也就不会渲染,返回 true 时,组件照常重渲染。此方法就是拿当前 props 中值和下一次 props 中的值进行对比,数据相等时,返回 false,反之返回 true。

需要注意,在进行新旧对比的时候,是**浅对比,**也就是说如果比较的数据时引用数据类型,只要数据的引用的地址没变,即使内容变了,也会被判定为 true。

面对这个问题,可以使用如下方法进行解决: (1)使用 setState 改变数据之前,先采用 ES6 中 assgin 进行拷贝,但是 assgin 只深拷贝的数据的第一层,所以说不是最完美的解决办法:

javascript
const o2 = Object.assign({}, this.state.obj);
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+
const o2 = Object.assign({}, this.state.obj);
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+

(2)使用 JSON.parse(JSON.stringfy())进行深拷贝,但是遇到数据为 undefined 和函数时就会错。

javascript
const o2 = JSON.parse(JSON.stringify(this.state.obj));
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+
const o2 = JSON.parse(JSON.stringify(this.state.obj));
+o2.student.count = "00000";
+this.setState({
+  obj: o2,
+});
+

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。

当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

避免不必要的 render

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:

  • shouldComponentUpdate 和 PureComponent

在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。

  • 利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

  • 使用 React.memo

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。

https://juejin.cn/post/6935584878071119885

高性能 JavaScript

开发注意

遵循严格模式:"use strict"

将 JavaScript 本放在页面底部,加快渲染页面

将 JavaScript 脚本将脚本成组打包,减少请求

使用非阻塞方式下载 JavaScript 脚本

尽量使用局部变量来保存全局变量

尽量减少使用闭包

使用 window 对象属性方法时,省略 window

尽量减少对象成员嵌套

缓存 DOM 节点的访问

通过避免使用 eval() 和 Function() 构造器

给 setTimeout() 和 setInterval() 传递函数而不是字符串作为参数

尽量使用直接量创建对象和数组

最小化重绘 (repaint) 和回流 (reflow)

懒加载

懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。 如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

懒加载的特点

  • 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
  • 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
  • 防止加载过多图片而影响其他资源文件的加载 :会影响网站应用的正常使用。

懒加载的实现原理

图片的加载是由 src 引起的,当对 src 赋值时,浏览器就会请求图片资源。根据这个原理,我们使用 HTML5 的 data-xxx 属性来储存图片的路径,在需要加载图片的时候,将 data-xxx 中图片的路径赋值给 src,这样就实现了图片的按需加载,即懒加载。

注意:data-xxx 中的 xxx 可以自定义,这里我们使用 data-src 来定义。 懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。

使用原生 JavaScript 实现懒加载

  1. IntersectionObserver api
  2. window.innerHeight 是浏览器可视区的高度
  3. document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
  4. imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
  5. 图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
html
<div class="container">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+</div>
+<script>
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
+      }
+    }
+  }
+  window.onscroll = lozyLoad();
+</script>
+
<div class="container">
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+  <img src="loading.gif" data-src="pic.png" />
+</div>
+<script>
+  var imgs = document.querySelectorAll("img");
+  function lozyLoad() {
+    var scrollTop =
+      document.body.scrollTop || document.documentElement.scrollTop;
+    var winHeight = window.innerHeight;
+    for (var i = 0; i < imgs.length; i++) {
+      if (imgs[i].offsetTop < scrollTop + winHeight) {
+        imgs[i].src = imgs[i].getAttribute("data-src");
+      }
+    }
+  }
+  window.onscroll = lozyLoad();
+</script>
+

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力

  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

参考:https://juejin.cn/post/6844903455048335368#heading-5

回流与重绘

回流(重排)

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。 下面这些操作会导致回流:

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者位置发生变化
  • 元素的字体大小发生变化
  • 激活 CSS 伪类
  • 查询某些属性或者调用某些方法
  • 添加或者删除可见的 DOM 元素、
  • 操作 class 属性
  • 设置 style 属性:.style....style...----->.class{}

在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的 DOM 元素重新排列,它的影响范围有两种:

  • 全局范围:从根节点开始,对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。

下面这些操作会导致回流:

  • color、background 相关属性:background-color、background-image 等
  • outline 相关属性:outline-color、outline-width 、text-decoration
  • border-radius、visibility、box-shadow

注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。

如何避免回流与重绘?

减少回流与重绘的措施:

  • 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
  • 不要使用 table 布局, 一个小的改动可能会使整个 table 进行重新布局
  • 使用 CSS 的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用 absolute 或者 fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作 DOM,可以创建一个文档片段 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
  • 将元素先设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 将 DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制。

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

如何优化动画?

对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作 DOM,就就会导致页面的性能问题,我们可以将动画的 position 属性设置为 absolute 或者 fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。

CPU 中央处理器,擅长逻辑运算

GPU 显卡,擅长图片绘制,高精度的浮点数运算。{家用,专业},尽量少复杂动画,即少了 GPU,烧性能。

在 gpu 层面上操作:改变 opacity 或者 transform:translate3d()/translatez();

最好添加 translatez(0); 小 hack 告诉浏览器告诉浏览器另起一个层

css 1、使用 transform 替代 top 2、使用 visibility 替换 display:none ,因为前者只会引起重绘,后者会引发回流(改变了布局 3、避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。 4、尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。 5、避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。 CSS3 硬件加速(GPU 加速),使用 css3 硬件加速,可以让 transform、opacity、filters 这些动画不会引起回流重绘。但是对于动画的其它属性,比如 background-color 这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。 js 1、避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。 2、避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。 3、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。 4、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

新方法:will-change:transform;专门处理 GPU 加速问题

应用:hover 上去后才告诉浏览器要开启新层,点击才触发,总之提前一刻告诉就行

css
div {
+  width: 100px;
+  height: 100px;
+}
+div.hover {
+  will-change: transform;
+}
+div.active {
+  transform: scale(2, 3);
+}
+
div {
+  width: 100px;
+  height: 100px;
+}
+div.hover {
+  will-change: transform;
+}
+div.active {
+  transform: scale(2, 3);
+}
+

浏览器刷新页面的频率 1s 60s

每 16.7mm 刷新一次

gpu 可以再一帧里渲染好页面,那么当你改动页面的元素或者实现动画的时候,将会非常流畅

documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN 中对 documentFragment 的解释:

DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。

当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的 DOM 操作时,我们就可以将 DOM 元素插入 DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作 DOM 相比,将 DocumentFragment 节点插入 DOM 树时,不会触发页面的重绘,这样就大大提高了页面的性能。

防抖函数

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤ lodash.debounce 节流函数的适⽤场景:
  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
  • 照片使用 JPEG

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以 BMP 格式的图片通常是较大的文件。

(2)GIF 是无损的、采用索引色的点阵图。采用 LZW 压缩算法进行编码。文件小,是 GIF 格式的优点,同时,GIF 格式还具有支持动画以及透明的优点。但是 GIF 格式仅支持 8bit 的索引色,所以 GIF 格式适用于对色彩要求不高同时需要文件体积较小的场景。

(3)JPEG 是有损的、采用直接色的点阵图。JPEG 的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,与 GIF 相比,JPEG 不适合用来存储企业 Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较 GIF 更大。

(4)PNG-8 是无损的、使用索引色的点阵图。PNG 是一种比较新的图片格式,PNG-8 是非常好的 GIF 格式替代者,在可能的情况下,应该尽可能的使用 PNG-8 而不是 GIF,因为在相同的图片效果下,PNG-8 具有更小的文件体积。除此之外,PNG-8 还支持透明度的调节,而 GIF 并不支持。除非需要动画的支持,否则没有理由使用 GIF 而不是 PNG-8。

(5)PNG-24 是无损的、使用直接色的点阵图。PNG-24 的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小要比 BMP 小得多。当然,PNG24 的图片还是要比 JPEG、GIF、PNG-8 大得多。

(6)SVG 是无损的矢量图。SVG 是矢量图意味着 SVG 图片由直线和曲线以及绘制它们的方法组成。当放大 SVG 图片时,看到的还是线和曲线,而不会出现像素点。这意味着 SVG 图片在放大时,不会失真,所以它非常适合用来绘制 Logo、Icon 等。

(7)WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为 Web 而生的,什么叫为 Web 而生呢?就是说相同质量的图片,WebP 具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有 Chrome 浏览器和 Opera 浏览器支持 WebP 格式,兼容性不太好。WebP 图片格式支持图片透明度,一个无损压缩的 WebP 图片,如果要支持透明度只需要 22%的格外文件大小。

优化首屏响应

觉得快

loading

vue 页面需要通过 js 构建,因此在 js 下载到本地之前,页面上什么也没有 一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到 js 下载到本地并运行后,即会自动替换

nprogress

源码分析地址:https://blog.csdn.net/qq_31968791/article/details/106790179 使用到的库是什么 nprogress 进度条的实现原理知道吗 Nprogress 的原理非常简单,就是页面启动的时候,构建一个方法,创建一个 div,然后这个 div 靠近最顶部,用 fixed 定位住,至于样式就是按照自个或者默认走了。 怎么使用这个库的 主要采用的两个方法是 nprogress.start 和 nprogress.done 如何使用: 在请求拦截器中调用 nprogress.start 在响应拦截器中调用 nprogress.done

betterScroll

https://blog.csdn.net/weixin_37719279/article/details/82084342 使用的库是什么:better-scroll

骨架屏

骨架屏的原理:https://blog.csdn.net/csdn_yudong/article/details/103909178

你能说说为啥使用骨架屏吗?

现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 但即便如此,首屏的加载依然还是存在这个加载以及渲染的等待时间问题; 现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPA,MPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。 webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。 目前主流,常见的解决方案是使用骨架屏技术,包括很多原生的 APP,在页面渲染时,也会使用骨架屏。(下图中,红圈中的部分,即为骨架屏在内容还没有出现之前的页面骨架填充,以免留白)

骨架屏的要怎么使用呢?骨架屏的原理知道吗?

  1. 在 index.html 中的 div#app 中来实现骨架屏,程序渲染后就会替换掉 index.html 里面的 div#app 骨架屏内容;
  2. 使用一个 Base64 的图片来作为骨架屏

使用图片作为骨架屏; 简单暴力,让 UI 同学花点功夫吧;小米商城的移动端页面采用的就是这个方法,它是使用了一个 Base64 的图片来作为骨架屏。 按照方案一的方案,将这个 Base64 的图片写在我们的 index.html 模块中的 div#app 里面。

  1. 使用 .vue 文件来完成骨架屏

真实快

webpack 怎么进行首屏加载的优化?

  1. CDN 如果工程中使用了一些知名的第三方库,可以考虑使用 CDN,而不进行打包
  2. 抽离公共模块 如果工程中用到了一些大的公共库,可以考虑将其分割出来单独打包
  3. 异步加载 对于那些不需要在一开始就执行的模块,可以考虑使用动态导入的方式异步加载它们,以尽量减少主包的体积
  4. 压缩、混淆
  5. tree shaking 尽量使用 ESM 语法进行导入导出,充分利用 tree shaking 去除无用代码
  6. gzip 开启 gzip 压缩,进一步减少包体积
  7. 环境适配 有些打包结果中包含了大量兼容性处理的代码,但在新版本浏览器中这些代码毫无意义。因此,可以把浏览器分为多个层次,为不同层次的浏览器给予不同的打包结果。
+ + + + + \ No newline at end of file diff --git "a/front-end-engineering/pnpm\345\216\237\347\220\206.html" "b/front-end-engineering/pnpm\345\216\237\347\220\206.html" new file mode 100644 index 00000000..63d19418 --- /dev/null +++ "b/front-end-engineering/pnpm\345\216\237\347\220\206.html" @@ -0,0 +1,30 @@ + + + + + + pnpm 原理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

pnpm 原理

想要理解 pnpm 是怎么做的,需要一些操作系统的知识

1、文件的本质

在操作系统中,文件实际上是一个指针,只不过它指向的不是内存地址,而是一个外部存储地址(这里的外部存储可以是硬盘、U 盘、甚至是网络)

当我们删除文件时,删除的实际上是指针,因此,无论删除多么大的文件,速度都非常快。像我们的 U 盘、硬盘里的文件虽然说看起来已经删除了,但是其实数据恢复公司是可以恢复的,因为数据还是存在的,只要删除文件后再没有存储其它文件就可以恢复,所以真正删除一个文件就是可劲存可劲删

2、文件的拷贝

如果你复制一个文件,是将该文件指针指向的内容进行复制,然后产生一个新文件指向新的内容。

硬链接的概念来自于 Unix 操作系统,它是指将一个文件 A 指针复制到另一个文件 B 指针中,文件 B 就是文件 A 的硬链接。

通过硬链接,不会产生额外的磁盘占用,并且,两个文件都能找到相同的磁盘内容。

硬链接的数量没有限制,可以为同一个文件产生多个硬链接。

windows Vista 操作系统开始,支持了创建硬链接的操作,在 cmd 中使用下面的命令可以创建硬链接。

mklink /h  链接名称  目标文件
+
+
mklink /h  链接名称  目标文件
+
+

例:创建一个硬连接

1、首先创建一个文件夹 temp,并且在 temp 文件夹创建一个 article.txt 文本文件

2、接下来,我要在 temp 文件夹的根目录(pnpm 包管理器下面)创建一个硬链接

  • 按 window+R 调出窗口,以管理员身份运行,并且输入 cmd,回车
  • 由于 pnpm 包管理器文件夹在 F 盘,所以先切换到 F 盘,并且进入 pnpm 包管理器地址
  • 输入: mklink /h link.txt temp\article.txt 回车
  • 此时就可以在编辑器看到新创建的硬链接 link.text,这样·article.txtlink.txt是一样的了
  • 这时当修改 link.txt 时,article.txt 也会跟着变,因为它们指向同一个磁盘空间

注意 ☛:

  1. 由于文件夹(目录)不存在文件内容,所以文件夹(目录)不能创建硬链接
  2. 在 windows 操作系统中,通常不要跨越盘符创建硬链接

符号链接又称为软连接,如果为某个文件或文件夹 A 创建符号连接 B,则 B 指向 A。

windows Vista 操作系统开始,支持了创建符号链接的操作,在 cmd 中使用下面的命令可以创建符号链接:

mklink  /d   链接名称   目标文件
+# /d表示创建的是目录的符号链接,不写则是文件的符号链接
+
+
mklink  /d   链接名称   目标文件
+# /d表示创建的是目录的符号链接,不写则是文件的符号链接
+
+

早期的 windows 系统不支持符号链接,但它提供了一个工具 junction 来达到类似的功能。

5、符号链接和硬链接的区别

  1. 硬链接仅能链接文件,而符号链接可以链接目录
  2. 硬链接在链接完成后仅和文件内容关联,和之前链接的文件没有任何关系。而符号链接始终和之前链接的文件关联,和文件内容不直接相关。

6、快捷方式

快捷方式类似于符号链接,是 windows 系统早期就支持的链接方式。它不仅仅是一个指向其他文件或目录的指针,其中还包含了各种信息:如权限、兼容性启动方式等其他各种属性,由于快捷方式是 windows 系统独有的,在跨平台的应用中一般不会使用。

7、node 环境对硬链接和符号链接的处理

硬链接:

硬链接是一个实实在在的文件,node 不对其做任何特殊处理,也无法区别对待,实际上,node 根本无从知晓该文件是不是一个硬链接

符号链接:

由于符号链接指向的是另一个文件或目录,当 node 执行符号链接下的 JS 文件时,会使用原始路径。比方说:我在 D 盘装了 LOL,在桌面创建了 LOL 快捷方式,相当于是符号链接,双击快捷方式运行游戏,在运行游戏的时候是按照 LOL 原始路径(D 盘路径)运行的。

8、pnpm 原理

pnpm 使用符号链接和硬链接来构建 node_modules 目录

下面用一个例子来说明它的构建方式

假设两个包 a 和 b,a 依赖 b:

假设我们的工程为 proj,直接依赖 a,则安装时,pnpm 会做下面的处理:

  1. 通过 package.json 查询依赖关系,得到最终要安装的包:a 和 b

  2. 在工程 proj 根目录中查看 a 和 b 是否已经有缓存,如果没有,下载到缓存中,如果有,则进入下一步

  3. 在 proj 中创建 node_modules 目录,并对目录进行结构初始化

  4. 从缓存的对应包中使用硬链接放置文件到相应包代码目录中

  5. 使用符号链接,将每个包的直接依赖放置到自己的目录中

    这样做的目的,是为了保证 a 的代码在执行过程中,可以读取到它们的直接依赖

  6. 新版本的 pnpm 为了解决一些书写不规范的包(读取间接依赖)的问题,又将所有的工程非直接依赖,使用符号链接加入到了 .pnpm/node_modules 中。如果 b 依赖 c,a 又要直接用 c,这种不规范的用法现在 pnpm 通过这种方式支持了。但对于那些使用绝对路径的奇葩写法,可能没有办法支持。

  7. 在工程的 node_modules 目录中使用符号链接,放置直接依赖

其他资料

https://juejin.cn/post/6916101419703468045

https://www.bilibili.com/video/BV1tg4y1x75Q/?p=3&spm_id_from=pageDriver&vd_source=5ca956a1f37d0ed72cd1c453c15a3c03

+ + + + + \ No newline at end of file diff --git a/front-end-engineering/theme.html b/front-end-engineering/theme.html new file mode 100644 index 00000000..e0a0f5fb --- /dev/null +++ b/front-end-engineering/theme.html @@ -0,0 +1,682 @@ + + + + + + 主题切换 | Sunny's blog + + + + + + + + +
Skip to content
On this page

主题切换

思路:

前端主题切换,其实就是切换 css

而且,css 中变换最大的,就是颜色相关的属性,图片,icon....

前端工程中样式切换的解决方案

1、css 变量 + css 样式【data-set [data-name='dark']】class='dark'

javascript
.primary{
+
+  --vt-c-bg:#fff;
+}
+
+.dark{
+  --vt-c-bg:#000
+}
+
+可以使用js点击某个按钮之后切换根元素的css样式样式切换之后意味着css变量的值改变了
+.layer{
+  background-color:var(--vt-c-bg);
+}
+
.primary{
+
+  --vt-c-bg:#fff;
+}
+
+.dark{
+  --vt-c-bg:#000
+}
+
+可以使用js,点击某个按钮之后,切换根元素的css样式,样式切换之后,意味着css变量的值改变了
+.layer{
+  background-color:var(--vt-c-bg);
+}
+

css 变量,是当我们点击某个按钮的时候做切换,可能如果变换比较大内容较多,会出现白屏的情况

2、css 预处理器(scss,less,styuls...) + css 样式 [data-set]

就是使用 css 预处理器强大的预编译功能,简单来说,就是把 css 先写好

javascript
html.primary .layer{
+  color:#fff
+}
+
+html.dark .layer{
+  color:#000
+}
+html.primary .container{}
+
+html.dark .container{}
+
+html.primary .link:hover{}
+html.dark .link:hover{}
+
html.primary .layer{
+  color:#fff
+}
+
+html.dark .layer{
+  color:#000
+}
+html.primary .container{}
+
+html.dark .container{}
+
+html.primary .link:hover{}
+html.dark .link:hover{}
+

css 预编译器,一开始,把所有的主题样式涉及到的内容全部写好,当切换的时候,就直接切换了

这种的方式,可能牺牲的是,一开始的白屏时间

通过 css 变量实现主题切换及工程化处理

使用持久化存储

1、安装 pinia 和持久化插件

js
npm install pinia pinia-plugin-persistedstate
+
npm install pinia pinia-plugin-persistedstate
+

2、创建 pinia

javascript
//stores/index.ts
+import { createPinia } from "pinia";
+import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
+
+// pinia persist
+const pinia = createPinia();
+pinia.use(piniaPluginPersistedstate);
+
+export default pinia;
+
//stores/index.ts
+import { createPinia } from "pinia";
+import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
+
+// pinia persist
+const pinia = createPinia();
+pinia.use(piniaPluginPersistedstate);
+
+export default pinia;
+

3、main.ts 中引用

javascript
import { createApp } from "vue";
+import "./style.css";
+import App from "./App.vue";
+// pinia store
+import pinia from "@/stores";
+
+createApp(App).use(pinia).mount("#app");
+
import { createApp } from "vue";
+import "./style.css";
+import App from "./App.vue";
+// pinia store
+import pinia from "@/stores";
+
+createApp(App).use(pinia).mount("#app");
+

4、设置 pinia 持久化配置

javascript
// config/piniaPersist.ts
+import { PersistedStateOptions } from "pinia-plugin-persistedstate";
+
+/**
+ * @description pinia 持久化参数配置
+ * @param {String} key 存储到持久化的 name
+ * @param {Array} paths 需要持久化的 state name
+ * @return persist
+ * */
+const piniaPersistConfig = (key: string, paths?: string[]) => {
+  const persist: PersistedStateOptions = {
+    key,
+    storage: localStorage,
+    // storage: sessionStorage,
+    paths,
+  };
+  return persist;
+};
+
+export default piniaPersistConfig;
+
// config/piniaPersist.ts
+import { PersistedStateOptions } from "pinia-plugin-persistedstate";
+
+/**
+ * @description pinia 持久化参数配置
+ * @param {String} key 存储到持久化的 name
+ * @param {Array} paths 需要持久化的 state name
+ * @return persist
+ * */
+const piniaPersistConfig = (key: string, paths?: string[]) => {
+  const persist: PersistedStateOptions = {
+    key,
+    storage: localStorage,
+    // storage: sessionStorage,
+    paths,
+  };
+  return persist;
+};
+
+export default piniaPersistConfig;
+

5、创建 global.ts

javascript
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState, ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: (): GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
+  persist: piniaPersistConfig("dy-global"),
+});
+
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState, ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: (): GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
+  persist: piniaPersistConfig("dy-global"),
+});
+

6、设置 actions 统一的的函数

diff
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState,ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: ():GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
++  actions: {
++    // Set GlobalState
++    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
++      this.$patch({ [args[0]]: args[1] });
++    }
++  },
+  persist: piniaPersistConfig("dy-global")
+});
+
import { defineStore } from "pinia";
+import piniaPersistConfig from "@/config/piniaPersist";
+import { GlobalState,ObjToKeyValArray } from "@/stores/interface";
+
+export const useGlobalStore = defineStore({
+  id: "dy-global",
+  // 修改默认值之后,需清除 localStorage 数据
+  state: ():GlobalState => ({
+    // 深色模式
+    isDark: false,
+  }),
++  actions: {
++    // Set GlobalState
++    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
++      this.$patch({ [args[0]]: args[1] });
++    }
++  },
+  persist: piniaPersistConfig("dy-global")
+});
+

7、setGlobalState 函数参数的设定

javascript
export interface GlobalState {
+  isDark: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+
export interface GlobalState {
+  isDark: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+

8、修改 useTheme

javascript
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+
+export const useTheme = () => {
+
+  const globalStore = useGlobalStore();
+  const { isDark } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+    }
+    const html = document.documentElement as HTMLElement;
+    console.log(isDark.value)
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+
+  return {
+    switchTheme,
+    initTheme
+  }
+}
+
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+
+export const useTheme = () => {
+
+  const globalStore = useGlobalStore();
+  const { isDark } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+    }
+    const html = document.documentElement as HTMLElement;
+    console.log(isDark.value)
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+
+  return {
+    switchTheme,
+    initTheme
+  }
+}
+

9、界面调用

javascript
import { useTheme } from "@/hooks/useTheme";
+const { initTheme, switchTheme } = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  // const theme = document.documentElement.className
+  // document.documentElement.className = theme === 'primary' ? 'dark' : 'primary'
+  switchTheme();
+};
+
import { useTheme } from "@/hooks/useTheme";
+const { initTheme, switchTheme } = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  // const theme = document.documentElement.className
+  // document.documentElement.className = theme === 'primary' ? 'dark' : 'primary'
+  switchTheme();
+};
+

跟随系统主题

首先,使用媒体查询,可以实现跟随系统主题的效果,其实只有lightdark两种颜色

javascript
body{
+  margin: 0;
+  padding: 0;
+}
+
+.container{
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  transition:color .5s, background-color .5s;
+  color:#000;
+  background-color:#fff;
+}
+.layout{
+  display: flex;
+  justify-content: center;
+}
+
+.layer{
+  width: 300px;
+  height: 300px;
+  padding: 8px;
+  margin: 10px;
+  border:1px solid #000;
+}
+@media (prefers-color-scheme: dark) {
+  .container{
+    color:#fff;
+    background-color:#000;
+  }
+  .layer{
+    border:1px solid #fff;
+  }
+}
+
body{
+  margin: 0;
+  padding: 0;
+}
+
+.container{
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  transition:color .5s, background-color .5s;
+  color:#000;
+  background-color:#fff;
+}
+.layout{
+  display: flex;
+  justify-content: center;
+}
+
+.layer{
+  width: 300px;
+  height: 300px;
+  padding: 8px;
+  margin: 10px;
+  border:1px solid #000;
+}
+@media (prefers-color-scheme: dark) {
+  .container{
+    color:#fff;
+    background-color:#000;
+  }
+  .layer{
+    border:1px solid #fff;
+  }
+}
+

我们可以通过api - window.matchMedia,来获取当前系统主题是深色还是浅色

javascript
window.matchMedia("(prefers-color-scheme: dark)");
+
window.matchMedia("(prefers-color-scheme: dark)");
+

有了这个 API,我们完全就没必要修改之前的 css 代码,直接 js 判断

javascript
const media = window.matchMedia("(prefers-color-scheme: dark)");
+
+const followSystem = () => {
+  if (media.matches) {
+    document.documentElement.className = "dark";
+  } else {
+    document.documentElement.className = "primary";
+  }
+};
+
+followSystem();
+
+media.addEventListener("change", followSystem);
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
+
+const followSystem = () => {
+  if (media.matches) {
+    document.documentElement.className = "dark";
+  } else {
+    document.documentElement.className = "primary";
+  }
+};
+
+followSystem();
+
+media.addEventListener("change", followSystem);
+

直接在 vue3 项目中处理,添加界面系统主题图标,点击激活图标,表示激活跟随系统主题

1、添加图标

html
<button>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 16 16"
+    fill="none"
+    role="img"
+  >
+    <circle cx="8" cy="8" r="7.25" stroke="#5B5B66" stroke-width="1.5" />
+    <mask
+      id="a"
+      style="mask-type:alpha"
+      maskUnits="userSpaceOnUse"
+      x="0"
+      y="0"
+      width="16"
+      height="16"
+    >
+      <circle
+        cx="8"
+        cy="8"
+        r="7.25"
+        fill="#5B5B66"
+        stroke="#5B5B66"
+        stroke-width="1.5"
+      />
+    </mask>
+    <g mask="url(#a)">
+      <path fill="#5B5B66" d="M0 0h8v16H0z" />
+    </g>
+  </svg>
+</button>
+
+css .os-default{ background-color: transparent; border: none; padding: 0; color:
+var(--main-color); display: flex; justify-content: center; align-items: center;
+width: 20px; height: 20px; } .os-default-active{ background-color: transparent;
+border: 1px dotted var(--main-color); padding: 0px; color: var(--main-color);
+display: flex; justify-content: center; align-items: center; width: 20px;
+height: 20px; }
+
<button>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 16 16"
+    fill="none"
+    role="img"
+  >
+    <circle cx="8" cy="8" r="7.25" stroke="#5B5B66" stroke-width="1.5" />
+    <mask
+      id="a"
+      style="mask-type:alpha"
+      maskUnits="userSpaceOnUse"
+      x="0"
+      y="0"
+      width="16"
+      height="16"
+    >
+      <circle
+        cx="8"
+        cy="8"
+        r="7.25"
+        fill="#5B5B66"
+        stroke="#5B5B66"
+        stroke-width="1.5"
+      />
+    </mask>
+    <g mask="url(#a)">
+      <path fill="#5B5B66" d="M0 0h8v16H0z" />
+    </g>
+  </svg>
+</button>
+
+css .os-default{ background-color: transparent; border: none; padding: 0; color:
+var(--main-color); display: flex; justify-content: center; align-items: center;
+width: 20px; height: 20px; } .os-default-active{ background-color: transparent;
+border: 1px dotted var(--main-color); padding: 0px; color: var(--main-color);
+display: flex; justify-content: center; align-items: center; width: 20px;
+height: 20px; }
+

2、仓库添加属性

diff
export const useGlobalStore = defineStore({
+  id: "dy-global",
+  state: () => ({
+    // 深色模式
+    isDark: false,
++    // 系统颜色是否被激活
++    osThemeActive: false
+  }),
+  getters: {},
+  actions: {
+    // Set GlobalState
+    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
+      this.$patch({ [args[0]]: args[1] });
+    }
+  },
+  persist: piniaPersistConfig("dy-global")
+});
+
export const useGlobalStore = defineStore({
+  id: "dy-global",
+  state: () => ({
+    // 深色模式
+    isDark: false,
++    // 系统颜色是否被激活
++    osThemeActive: false
+  }),
+  getters: {},
+  actions: {
+    // Set GlobalState
+    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
+      this.$patch({ [args[0]]: args[1] });
+    }
+  },
+  persist: piniaPersistConfig("dy-global")
+});
+

添加了属性,就必须修改 GlobalState 的 ts 配置

diff
export interface GlobalState {
+  isDark: boolean;
++  osThemeActive: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+
export interface GlobalState {
+  isDark: boolean;
++  osThemeActive: boolean;
+}
+export type ObjToKeyValArray<T> = {
+  [K in keyof T]: [K, T[K]];
+}[keyof T];
+

3、点击修改 css 样式

javascript
import { useTheme } from '@/hooks/useTheme'
+import { useGlobalStore } from "@/stores/modules/global";
+
+const globalStore = useGlobalStore();
+
+const {initTheme,switchTheme,activeOSTheme} = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  switchTheme()
+}
+
+const osThemeSwitch = () => {
+  activeOSTheme();
+}
+
+//html
+<button :class="globalStore.osThemeActive ? 'os-default-active' : 'os-default'">
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" role="img" @click="osThemeSwitch">
+	......省略
+  </svg>
+</button>
+
import { useTheme } from '@/hooks/useTheme'
+import { useGlobalStore } from "@/stores/modules/global";
+
+const globalStore = useGlobalStore();
+
+const {initTheme,switchTheme,activeOSTheme} = useTheme();
+
+initTheme();
+
+const modeSwitch = () => {
+  switchTheme()
+}
+
+const osThemeSwitch = () => {
+  activeOSTheme();
+}
+
+//html
+<button :class="globalStore.osThemeActive ? 'os-default-active' : 'os-default'">
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" role="img" @click="osThemeSwitch">
+	......省略
+  </svg>
+</button>
+

4、修改 useTheme.ts

javascript
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+import { watchEffect } from "vue";
+
+const media = window.matchMedia('(prefers-color-scheme: dark)');
+
+export const useTheme = () => {
+  const globalStore = useGlobalStore();
+  const { isDark,osThemeActive } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    //init参数为true时,表示是初始化页面或者直接跟随系统主题
+    //这个时候不需要手动切换明暗主题,也不需要将跟随系统主题设置为false
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+      //如果是直接点击切换明暗主题,那么跟随系统主题激活切换为false
+      globalStore.setGlobalState("osThemeActive", false);
+    }
+    const html = document.documentElement as HTMLElement;
+
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+
+  //点击激活需要改变osThemeActive的值
+  const activeOSTheme = () => {
+    globalStore.setGlobalState("osThemeActive", !osThemeActive.value);
+    followSystem();
+  }
+
+  const followSystem = () => {
+    const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+    //跟随系统主题设置明暗
+    globalStore.setGlobalState("isDark", isOsDark);
+    switchTheme(true);
+  }
+
+  watchEffect(() => {
+    //系统主题切换,需要监听change事件
+    if (osThemeActive.value) {
+      media.addEventListener('change', followSystem);
+    }
+    else {
+      media.removeEventListener('change', followSystem);
+    }
+  })
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+  return {
+    switchTheme,
+    initTheme,
+    activeOSTheme
+  }
+}
+
import { useGlobalStore } from "@/stores/modules/global";
+import { storeToRefs } from "pinia";
+import { watchEffect } from "vue";
+
+const media = window.matchMedia('(prefers-color-scheme: dark)');
+
+export const useTheme = () => {
+  const globalStore = useGlobalStore();
+  const { isDark,osThemeActive } = storeToRefs(globalStore);
+
+  const switchTheme = (init?: Boolean) => {
+    //init参数为true时,表示是初始化页面或者直接跟随系统主题
+    //这个时候不需要手动切换明暗主题,也不需要将跟随系统主题设置为false
+    if (!init) {
+      globalStore.setGlobalState("isDark", !isDark.value);
+      //如果是直接点击切换明暗主题,那么跟随系统主题激活切换为false
+      globalStore.setGlobalState("osThemeActive", false);
+    }
+    const html = document.documentElement as HTMLElement;
+
+    if (isDark.value) html.setAttribute("class", "dark");
+    else html.setAttribute("class", "primary");
+  }
+
+
+  //点击激活需要改变osThemeActive的值
+  const activeOSTheme = () => {
+    globalStore.setGlobalState("osThemeActive", !osThemeActive.value);
+    followSystem();
+  }
+
+  const followSystem = () => {
+    const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+    //跟随系统主题设置明暗
+    globalStore.setGlobalState("isDark", isOsDark);
+    switchTheme(true);
+  }
+
+  watchEffect(() => {
+    //系统主题切换,需要监听change事件
+    if (osThemeActive.value) {
+      media.addEventListener('change', followSystem);
+    }
+    else {
+      media.removeEventListener('change', followSystem);
+    }
+  })
+
+  const initTheme = () => {
+    switchTheme(true);
+  };
+
+  return {
+    switchTheme,
+    initTheme,
+    activeOSTheme
+  }
+}
+

所有源码均在:https://github.com/Sunny-117/blog/tree/main/code/theme-switch

+ + + + + \ No newline at end of file diff --git a/front-end-engineering/webpack5-mf.html b/front-end-engineering/webpack5-mf.html new file mode 100644 index 00000000..8cd05b40 --- /dev/null +++ b/front-end-engineering/webpack5-mf.html @@ -0,0 +1,122 @@ + + + + + + webpack5 模块联邦 | Sunny's blog + + + + + + + + +
Skip to content
On this page

webpack5 模块联邦

在大型项目中,往往会把项目中的某个区域或功能模块作为单独的项目开发,最终形成「微前端」架构

在微前端架构中,不同的工程可能出现下面的场景

这涉及到很多非常棘手的问题:

  • 如何避免公共模块重复打包
  • 如何将某个项目中一部分模块分享出去,同时还要避免重复打包
  • 如何管理依赖的不同版本
  • 如何更新模块
  • ......

webpack5尝试着通过模块联邦来解决此类问题

示例

现有两个微前端工程,它们各自独立开发、测试、部署,但它们有一些相同的公共模块,并有一些自己的模块需要分享给其他工程使用,同时又要引入其他工程的模块。

暴露自身模块

如果一个项目需要把一部分模块暴露给其他项目使用,可以使用webpack5的模块联邦将这些模块暴露出去

javascript
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 模块联邦的名称
+      // 该名称将成为一个全部变量,通过该变量将可获取当前联邦的所有暴露模块
+      name: "home", 
+      // 模块联邦生成的文件名,全部变量将置入到该文件中
+      filename: "home-entry.js",
+      // 模块联邦暴露的所有模块
+      exposes: {
+        // key:相对于模块联邦的路径
+        // 这里的 ./Timer 将决定该模块的访问路径为 home/Timer
+        // value: 模块的具体路径
+        "./Timer": "./src/Timer.js",
+      },
+    }),
+  ]
+}
+
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 模块联邦的名称
+      // 该名称将成为一个全部变量,通过该变量将可获取当前联邦的所有暴露模块
+      name: "home", 
+      // 模块联邦生成的文件名,全部变量将置入到该文件中
+      filename: "home-entry.js",
+      // 模块联邦暴露的所有模块
+      exposes: {
+        // key:相对于模块联邦的路径
+        // 这里的 ./Timer 将决定该模块的访问路径为 home/Timer
+        // value: 模块的具体路径
+        "./Timer": "./src/Timer.js",
+      },
+    }),
+  ]
+}
+

使用对方暴露的模块

在模块联邦的配置中,不仅可以暴露自身模块,还可以使用其他项目暴露的模块

javascript
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 远程使用其他项目暴露的模块
+      remotes: {
+        // key: 自定义远程暴露的联邦名
+        // 比如为 abc, 则之后引用该联邦的模块则使用 import "abc/模块名"
+        // value: 模块联邦名@模块联邦访问地址
+        // 远程访问时,将从下面的地址加载
+        home: "home@http://localhost:8080/home-entry.js",
+      },
+    }),
+  ]
+}
+
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      // 远程使用其他项目暴露的模块
+      remotes: {
+        // key: 自定义远程暴露的联邦名
+        // 比如为 abc, 则之后引用该联邦的模块则使用 import "abc/模块名"
+        // value: 模块联邦名@模块联邦访问地址
+        // 远程访问时,将从下面的地址加载
+        home: "home@http://localhost:8080/home-entry.js",
+      },
+    }),
+  ]
+}
+

共享模块

不同的项目可能使用了一些公共的第三方库,可以将这些第三方库作为共享模块,避免反复打包

javascript
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      shared: ["jquery", "lodash"]
+    }),
+  ]
+}
+
// webpack.config.js
+const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
+
+module.exports = {
+  plugins: [
+    // 使用模块联邦插件
+    new ModuleFederationPlugin({
+      shared: ["jquery", "lodash"]
+    }),
+  ]
+}
+

webpack会根据需要从合适的位置引入合适的版本

+ + + + + \ No newline at end of file diff --git "a/front-end-engineering/webpack\345\270\270\347\224\250\346\213\223\345\261\225.html" "b/front-end-engineering/webpack\345\270\270\347\224\250\346\213\223\345\261\225.html" new file mode 100644 index 00000000..c83a33d6 --- /dev/null +++ "b/front-end-engineering/webpack\345\270\270\347\224\250\346\213\223\345\261\225.html" @@ -0,0 +1,224 @@ + + + + + + webpack常用拓展 | Sunny's blog + + + + + + + + +
Skip to content
On this page

webpack常用拓展

清除输出目录

clean-webpack-plugin 当文件内容变化,重新打包,会自动删除原来打包的文件

js
module.exports = {
+    plugins: [
+        new CleanWebpackPlugin()
+    ],
+}
+
module.exports = {
+    plugins: [
+        new CleanWebpackPlugin()
+    ],
+}
+

自动生成页面

html-webpack-plugin

解决的问题:用打包后的js文件时,需要自行创建html文件,引入。但是当js文件变化,再次打包,生成的压缩文件名字变化了,并且用了clean-webpack-plugin插件,清楚了dist目录,把我写的html也删除了。 配置:

  1. 自己定义模版,让他按照模版生成页面
js
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "html/index.html"
+        })
+    ],
+}
+
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "html/index.html"
+        })
+    ],
+}
+
  1. 多入口(chunk)。最终生成的html会把两个打包后的js入口都引入。
js
entry:{
+    home: './src/index.js',
+    a: './src/a.js'
+}
+
entry:{
+    home: './src/index.js',
+    a: './src/a.js'
+}
+

可以配置进行选择性引入

js
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            chunks: ["home"] // 只使用home打包出来的js
+        })
+    ],
+}
+
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            chunks: ["home"] // 只使用home打包出来的js
+        })
+    ],
+}
+

生成多个html

js
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "home.html",
+            chunks: ['home']
+        }),
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "a.html",
+            chunks: ['a']
+        })
+    ],
+}
+
module.exports = {
+ plugins: [
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "home.html",
+            chunks: ['home']
+        }),
+        new HtmlWebpackPlugin({
+            template: "./public/index.html",
+            filename: "a.html",
+            chunks: ['a']
+        })
+    ],
+}
+

复制静态资源

copy-webpack-plugin

js
module.exports = {
+    plugins: [
+        new CopyPlugin([
+            {from: 'source', to: 'dest'},
+            {from: 'other', to: 'public'},
+        ])
+    ]
+}
+
module.exports = {
+    plugins: [
+        new CopyPlugin([
+            {from: 'source', to: 'dest'},
+            {from: 'other', to: 'public'},
+        ])
+    ]
+}
+

开发服务器

开发阶段,目前遇到的问题是打包、运行、调试过程过于繁琐,回顾一下我们的操作流程:

  1. 编写代码
  2. 控制台运行命令完成打包
  3. 打开页面查看效果
  4. 继续编写代码,回到步骤2

并且,我们往往希望把最终生成的代码和页面部署到服务器上,来模拟真实环境

为了解决这些问题,webpack官方制作了一个单独的库:webpack-dev-server

既不是plugin也不是loader

先来看看它怎么用

  1. 安装
  2. 执行npx webpack-dev-server命令

webpack-dev-server命令几乎支持所有的webpack命令参数,如--config-env等等,你可以把它当作webpack命令使用

这个命令是专门为开发阶段服务的,真正部署的时候还是得使用webpack命令

当我们执行webpack-dev-server命令后,它做了以下操作:

  1. 内部执行webpack命令,传递命令参数
  2. 开启watch
  3. 注册hooks:类似于plugin,webpack-dev-server会向webpack中注册一些钩子函数,主要功能如下:
    • 将资源列表(aseets)保存起来
    • 禁止webpack输出文件
  4. 用express开启一个服务器,监听某个端口,当请求到达后,根据请求的路径,给予相应的资源内容

配置

针对webpack-dev-server的配置,参考:dev-server

常见配置有:

  • port:配置监听端口
  • proxy:配置代理,常用于跨域访问
  • stats:配置控制台输出内容

普通文件处理

file-loader: 生成依赖的文件到输出目录,然后将模块文件设置为:导出一个路径\

javascript
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 生成一个具有相同文件内容的文件到输出目录
+	// 2. 返回一段代码   export default "文件名"
+}
+
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 生成一个具有相同文件内容的文件到输出目录
+	// 2. 返回一段代码   export default "文件名"
+}
+

url-loader:将依赖的文件转换为:导出一个base64格式的字符串,不生成文件

javascript
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 根据buffer生成一个base64编码
+	// 2. 返回一段代码   export default "base64编码"
+}
+
//file-loader
+function loader(source){
+	// source:文件内容(图片内容 buffer)
+	// 1. 根据buffer生成一个base64编码
+	// 2. 返回一段代码   export default "base64编码"
+}
+
javascript
 module: {
+        rules: [{
+            test: /\.(png)|(gif)|(jpg)$/,
+            use: [{
+                loader: "url-loader",
+                options: {
+                    // limit: false //不限制任何大小,所有经过loader的文件进行base64编码返回
+                    limit: 10 * 1024, //只要文件不超过 100*1024 字节,则使用base64编码,否则,交给file-loader进行处理
+                    name: "imgs/[name].[hash:5].[ext]" //ext表示保留原来图片格式
+                }
+            }]
+        }]
+    },
+
 module: {
+        rules: [{
+            test: /\.(png)|(gif)|(jpg)$/,
+            use: [{
+                loader: "url-loader",
+                options: {
+                    // limit: false //不限制任何大小,所有经过loader的文件进行base64编码返回
+                    limit: 10 * 1024, //只要文件不超过 100*1024 字节,则使用base64编码,否则,交给file-loader进行处理
+                    name: "imgs/[name].[hash:5].[ext]" //ext表示保留原来图片格式
+                }
+            }]
+        }]
+    },
+

解决路径问题

在使用file-loader或url-loader时,可能会遇到一个非常有趣的问题

比如,通过webpack打包的目录结构如下:

yaml
dist
+    |—— img
+        |—— a.png  #file-loader生成的文件
+    |—— scripts
+        |—— main.js  #export default "img/a.png"
+    |—— html
+        |—— index.html #<script src="../scripts/main.js" ></script>
+
dist
+    |—— img
+        |—— a.png  #file-loader生成的文件
+    |—— scripts
+        |—— main.js  #export default "img/a.png"
+    |—— html
+        |—— index.html #<script src="../scripts/main.js" ></script>
+

这种问题发生的根本原因:模块中的路径来自于某个loader或plugin,当产生路径时,loader或plugin只有相对于dist目录的路径,并不知道该路径将在哪个资源中使用,从而无法确定最终正确的路径

面对这种情况,需要依靠webpack的配置publicPath解决

webpack内置插件

所有的webpack内置插件都作为webpack的静态属性存在的,使用下面的方式即可创建一个插件对象

javascript
const webpack = require("webpack")
+
+new webpack.插件名(options)
+
const webpack = require("webpack")
+
+new webpack.插件名(options)
+

DefinePlugin

全局常量定义插件,使用该插件通常定义一些常量值,例如:

javascript
new webpack.DefinePlugin({
+    PI: `Math.PI`, // PI = Math.PI
+    VERSION: `"1.0.0"`, // VERSION = "1.0.0"
+    DOMAIN: JSON.stringify("duyi.com")
+})
+
new webpack.DefinePlugin({
+    PI: `Math.PI`, // PI = Math.PI
+    VERSION: `"1.0.0"`, // VERSION = "1.0.0"
+    DOMAIN: JSON.stringify("duyi.com")
+})
+

这样一来,在源码中,我们可以直接使用插件中提供的常量,当webpack编译完成后,会自动替换为常量的值

BannerPlugin

它可以为每个chunk生成的文件头部添加一行注释,一般用于添加作者、公司、版权等信息

javascript
new webpack.BannerPlugin({
+  banner: `
+  hash:[hash]
+  chunkhash:[chunkhash]
+  name:[name]
+  author:sunny-117
+  corporation:sunny
+  `
+})
+
new webpack.BannerPlugin({
+  banner: `
+  hash:[hash]
+  chunkhash:[chunkhash]
+  name:[name]
+  author:sunny-117
+  corporation:sunny
+  `
+})
+

ProvidePlugin

自动加载模块,而不必到处 import 或 require

javascript
new webpack.ProvidePlugin({
+  $: 'jquery',
+  _: 'lodash'
+})
+
new webpack.ProvidePlugin({
+  $: 'jquery',
+  _: 'lodash'
+})
+

然后在我们任意源码中:

javascript
$('#item'); // <= 起作用
+_.drop([1, 2, 3], 2); // <= 起作用
+
$('#item'); // <= 起作用
+_.drop([1, 2, 3], 2); // <= 起作用
+
+ + + + + \ No newline at end of file diff --git "a/front-end-engineering/\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.html" "b/front-end-engineering/\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.html" new file mode 100644 index 00000000..6a925e0d --- /dev/null +++ "b/front-end-engineering/\343\200\220\345\256\214\347\273\223\343\200\221\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226.html" @@ -0,0 +1,484 @@ + + + + + + npx | Sunny's blog + + + + + + + + +
Skip to content
On this page

为什么会出现前端工程化

模块化:意味着咱们的 JS 总算可以开发大型项目(解决 JS 的复用问题)

组件化:解决 HTML、CSS、JS 的复用问题

工程化:因为咱们的前端项目越来越复杂、咱们的前端项目模块(组件)越来越多

前端项目越来越复杂?

在书写前端项目的时候,可能会涉及到使用其他的语言,typescript、less/sass、coffeescript,涉及到编译

在部署的时候,还需要对代码进行丑化、压缩

代码优化:为 CSS 代码添加兼容性前缀

前端项目模块(组件)越来越多?

专门有一个 node_modules 来管理这些可以复用的代码。

前端工程化的出现,归根结底,其实就是要解决开发环境和生产环境不一致的问题。

nodejs对CommonJS的实现(面试)

为了实现CommonJS规范,nodejs对模块做出了以下处理

  1. 为了保证高效的执行,仅加载必要的模块。nodejs只有执行到require函数时才会加载并执行模块

nodejs中导入模块,使用相对路径,并且必须以./或../开头,浏览器可以省略./,nodejs不行

  1. 为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行,以保证不污染全局变量。
javascript
 (function(){
+     //模块中的代码
+ })()
+
 (function(){
+     //模块中的代码
+ })()
+
  1. 为了保证顺利的导出模块内容,nodejs做了以下处理
    1. 在模块开始执行前,初始化一个值module.exports = {}
    2. module.exports即模块的导出值
    3. 为了方便开发者便捷的导出,nodejs在初始化完module.exports后,又声明了一个变量exports = module.exports
javascript
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+
 (function(module){
+     module.exports = {};
+     var exports = module.exports;
+     //模块中的代码
+     return module.exports;
+ })()
+//面试题经常考module.exports与module几乎没区别,只是最后返回module.exports
+

面试

javascript
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+
var util = require('./index.js');
+
+console.log(module.exports == exports);//没区别
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+console.log(module.exports == exports);//module被赋值了,exports={},他两个不一样了。应用:下一题
+// 最终导出的是module.exports
+

经典面试题 util.js

javascript
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+
var count = 0;
+module.exports = {
+  getNumber: function () {
+    count++;
+    return count;
+  },
+  abc: 123
+}
+exports.bcd = 456;
+

index.js

javascript
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+
var util = require('./util.js');
+console.log(util.bcd)//undefined
+// 因为最终返回module.exports,当前面被重新赋值,意味着module.exports和exports无关了
+

经验:对exports赋值无意义,建议用module.exports

  1. 为了避免反复加载同一个模块,nodejs默认开启了模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

过去,JS很难编写大型应用,因为有以下两个问题:

  1. 全局变量污染
  2. 难以管理的依赖关系

这些问题,都导致了JS无法进行精细的模块划分,因为精细的模块划分会导致更多的全局污染以及更加复杂的依赖关系 于是,先后出现了两大模块化标准,用于解决以上两个问题:

  • CommonJS
  • ES6 Module

注意:上面提到的两个均是模块化标准,具体的实现需要依托于JS的宿主环境

CommonJS

node

目前,只有node环境才支持 CommonJS 模块化标准,所以,要使用 CommonJS,必须要先安装node 官网地址:https://nodejs.org/zh-cn/ nodejs直接运行某个js文件,该文件被称之为入口文件 nodejs遵循EcmaScript标准,但由于脱离了浏览器环境,因此:

  1. 你可以在nodejs中使用EcmaScript标准的任何语法或api,例如:循环、判断、数组、对象等
  2. 你不能在nodejs中使用浏览器的 web api,例如:dom对象、window对象、document对象等

CommonJS标准和使用

node中的所有代码均在CommonJS规范下运行 具体规范如下:

  1. 一个JS文件即为一个模块,一个模块就是一个相对独立的功能,模块中的所有全局代码产生的变量、函数,均不会对全局造成任何污染,仅在模块内使用
  2. 如果一个模块需要暴露一些数据或功能供其他模块使用,需要使用代码module.exports = xxx,该过程称之为模块的导出
  3. 如果一个模块需要使用另一个模块导出的内容,需要使用代码require("模块路径")
    1. 路径必须以./../开头
    2. 如果模块文件后缀名为.js,可以省略后缀名
    3. require函数返回的是模块导出的内容
  4. 模块具有缓存,第一次导入模块时会缓存模块的导出,之后再导入同一个模块,直接使用之前缓存的结果。

有了CommonJS模块化,代码就会形成类似下面的结构: image.png 同时也解决了JS的两个问题 细节:

浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module
+
浏览器里面可以直接用的函数变量都在全局,但是commonjs里面require不在全局,undefined,同理module
+

**原理:**require中的伪代码

javascript
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
function require(modulePath){
+  //1. 根据传递的模块路径,得到模块完整的绝对路径
+  var moduleId = require.resolve(modulePath);
+  //2. 判断缓存
+  if(cache[moduleId]){
+    return cache[moduleId];
+  }
+  //3. 真正运行模块代码的辅助函数
+  function _require(exports, require, module, __filename, __dirname){
+    // 目标模块的代码在这里
+  }
+  //4. 准备并运行辅助函数
+  var module = {
+    exports: {}
+  };
+  var exports = module.exports;
+  var __filename = moduleId; // 得到模块文件的绝对路径
+  var __dirname = ...; // 得到模块所在目录的绝对路径
+  _require.call(exports, exports, _require, module, __filename, __dirname);// exports绑定this,说明this == exports
+  //5. 缓存 module.exports
+  cache[moduleId] = module.exports;
+  //6. 返回 module.exports
+  return module.exports;
+}
+// 根据传递的模块路径,得到模块完整的绝对路径
+require.resolve = function(modulePath){
+  // 略
+}
+
  1. 根据传递的模块路径,得到模块完整的绝对路径。因为绝对路径不会重复。

image.png

  1. 判断缓存:防止重复执行

面试题 image.pngimage.png

  1. 如果没有缓存。真正运行模块代码的辅助函数
javascript
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
function _require(exports, require, module, __filename, __dirname) {
+  // 目标模块的代码在这里
+}
+
  1. 返回 module.exports

image.pngimage.png this === exports === module.exports === {} image.pngimage.png

下面的代码执行结果是什么?

javascript
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+
// a.js
+exports.d = 4;//this === exports ==={a:1,d:4}
+this.e = 5;//{a:1,d:4,e:5}
+console.log(this === exports);//true
+console.log(this === module.exports);//false
+console.log(exports === module.exports);//false
+
+// index.js
+var a = arguments[1]("./a.js");
+// 原理可知 arguments[1]相当于require
+// a === module.exports ===fn {c:3}
+console.log(typeof a);//function
+console.log(a.a, a.b, a.c, a.d, a.e);//undefined  undefined 3 undefined undefined
+console.log(arguments.length);//5
+

ES6 module

由于种种原因,CommonJS标准难以在浏览器中实现,因此一直在浏览器端一直没有合适的模块化标准,直到ES6标准出现 ES6规范了浏览器的模块化标准,一经发布,各大浏览器厂商纷纷在自己的浏览器中实现了该规范

模块的引入

浏览器使用以下方式引入一个ES6模块文件

html
<script src="JS文件" type="module">
+
<script src="JS文件" type="module">
+

标准和使用

  1. 模块的导出分为两种,基本导出(具名导出)默认导出基本导出导出的是该对象的某个属性,默认导出导出的是该对象的特殊属性default
javascript
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
//导出结果:想象成一个对象
+{
+    a: xxx, //基本导出  具名导出  named exports
+    b: xxx, //基本导出
+    default: xxx, //默认导出
+    c: xxx //基本导出
+}
+
  1. 基本导出可以有多个,默认导出只能有一个
  2. 基本导出必须要有名字,默认导出由于有特殊名字,所以可以不用写名字
javascript
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
export var a = 1 //基本导出 a = 1
+export var b = function(){} //基本导出 b = function(){}
+export function method(){}  //基本导出 method = function(){}
+var c = 3;
+export { c } //基本导出 c = 3
+export { c as temp } //基本导出 temp = 3
+
+export default 3 //默认导出 default = 3
+export default function(){} //默认导出 default = function(){}
+export { c as default } //默认导出 default = 3
+
+export {a, b, c as default} //基本导出 a=1, b=function(){}, 默认导出 default = 3
+
  1. 模块的导入
javascript
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+
import {a,b} from "模块路径"   //导入属性 a、b,放到变量a、b中
+import {a as temp1, b as temp2} from "模块路径" //导入属性a、b,放到变量temp1、temp2 中
+import {default as a} from "模块路径" //导入属性default,放入变量a中,default是关键字,不能作为变量名,必须定义别名
+import {default as a, b} from "模块路径" //导入属性default、b,放入变量a、b中
+import c from "模块路径"  //相当于 import {default as c} from "模块路径"
+import c, {a,b} from "模块路径" //相当于 import {default as c, a, b} from "模块路径"
+import * as obj from "模块路径" //将模块对象放入到变量obj中
+import "模块路径" //不导入任何内容,仅执行一次模块
+

image.pngimage.png 注意

  1. ES6 module 采用依赖预加载模式,所有模块导入代码均会提升到代码顶部
  2. 不能将导入代码放置到判断、循环中
  3. 导入的内容放置到常量中,不可更改
  4. ES6 module 使用了缓存,保证每个模块仅加载一次

请对比一下CommonJS和ES Module

  1. CMJ 是社区标准,ESM 是官方标准
  2. CMJ 是使用 API 实现的模块化,ESM 是使用新语法实现的模块化
  3. CMJ 仅在 node 环境中支持,ESM 各种环境均支持
  4. CMJ 是动态的依赖,ESM 既支持动态,也支持静态
  5. ESM 导入时有符号绑定,CMJ 只是普通函数调用和赋值

CommonJS是社区模块化标准,node环境支持该标准。它不产生新的语法,是使用API实现的,本质是把要加载的模块放到一个闭包中执行(这里可以详细的阐述)。因此,node环境中的所有JS文件在执行的时候,都是放到一个函数环境中执行的,这对使得模块内部可以访问函数的参数,如exports、module、__dirname等,而且对this的指向也会有影响。CommonJS是动态的模块化标准,这就意味着依赖关系是在运行过程中确定的,同时也意味着在导入导出模块时,并不限制书写的位置。 ES Module是官方模块化标准,目前node和浏览器均支持该标准,它引入了新的语法。ES Module是静态的模块化标准,在模块执行前,会递归确定所有依赖关系,然后加载所有文件,加载完成后再运行,这在浏览器环境下会产生多次请求。由于使用的是静态依赖,因此,它要求导入导出的代码必须放置到顶层,因为只有这样才能在代码运行前就确定依赖关系。ES Module导入模块时会将导入的结果绑定到标识符中,该标识符是一个常量,不可更改。 CommonJS和ES Module都使用了缓存,保证每个模块仅执行一次。 1.讲讲模块化规范2.import和require的区别3.require是如何解析路径的

1、ESM是编译时导出结果,CommonJS是运⾏时导出结果 2、ESM是导出引⽤,CommonJS是导出⼀个值 3、ESM⽀持异步,CommonJS只⽀持同步

  1. 下面的模块导出了什么结果?
javascript
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+
exports.a = 'a';
+module.exports.b = 'b';
+this.c = 'c';
+module.exports = {
+  d: 'd'
+}
+

说一下你对前端工程化,模块化,组件化的理解?

这三者中,模块化是基础,没有模块化,就没有组件化和工程化

模块化的出现,解决了困扰前端的两大难题:全局污染问题和依赖混乱问题,从而让精细的拆分前端工程成为了可能。

工程化的出现,解决了前端开发环境和生产环境要求不一致的矛盾。在开发环境中,我们希望代码使用尽可能的细分,代码格式尽可能的统一和规范,而在生产环境中,我们希望代码尽可能的被压缩、混淆,尽可能的优化体积。工程化的出现,就是为了解决这一矛盾,它可以让我们舒服的在开发环境中书写代码,然后经过打包,生成最合适的生产环境代码,这样就解放了开发者的精力,让开发者把更多的注意力集中在开发环境上即可。

组件化开发是一些前端框架带来的概念,它把一个网页,或者一个站点,甚至一个完整的产品线,划分为多个小的组件,组件是一个可以复用的单元,它包含了一个某个区域的完整功能。这样一来,前端便具备了开发复杂应用的能力。

webpack 中的 loader 属性和 plugins 属性的区别是什么?

参考答案:

它们都是 webpack 功能的扩展点。

loader 是加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等

plugins 是插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等

webpack 的核心概念都有哪些?

参考答案:

  • loader 加载器,主要用于代码转换,比如 JS 代码降级,CSS 预编译、模块化等
  • plugin 插件,webpack 打包流程中每个环节都提供了钩子函数,可以利用这些钩子函数参与到打包生命周期中,更改或增加 webpack 的某些功能,比如生成页面和 css 文件、压缩打包结果等
  • module 模块。webpack 将所有依赖均视为模块,无论是 js、css、html、图片,统统都是模块
  • entry 入口。打包过程中的概念,webpack 以一个或多个文件作为入口点,分析整个依赖关系。
  • chunk 打包过程中的概念,一个 chunk 是一个相对独立的打包过程,以一个或多个文件为入口,分析整个依赖关系,最终完成打包合并
  • bundle webpack 打包结果
  • tree shaking 树摇优化。在打包结果中,去掉没有用到的代码。
  • HMR 热更新。是指在运行期间,遇到代码更改后,无须重启整个项目,只更新变动的那一部分代码。
  • dev server 开发服务器。在开发环境中搭建的临时服务器,用于承载对打包结果的访问

ES6 中如何实现模块化的异步加载?

使用动态导入即可,导入后,得到的是一个 Promise,完成后,得到一个模块对象,其中包含了所有的导出结果。

说一下 webpack 中的几种 hash 的实现原理是什么?

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

webpack 如果使用了 hash 命名,那是每次都会重新生成 hash 吗?

参考答案:

不会。它跟关联的内容是否有变化有关系,如果没有变化,hash 就不会变。具体来说,contenthash 和具体的打包文件内容有关,chunkhash 和某一 entry 为起点的打包过程中涉及的内容有关,hash 和整个工程所有模块内容有关。

webpack 中是如何处理图片的? (抖音直播)

参考答案:

webpack 本身不处理图片,它会把图片内容仍然当做 JS 代码来解析,结果就是报错,打包失败。如果要处理图片,需要通过 loader 来处理。其中,url-loader 会把图片转换为 base64 编码,然后得到一个 dataurl,file-loader 则会将图片生成到打包目录中,然后得到一个资源路径。但无论是哪一种 loader,它们的核心功能,都是把图片内容转换成 JS 代码,因为只有转换成 JS 代码,webpack 才能识别。

webpack 打包出来的 html 为什么 style 放在头部 script 放在底部?

说明:这道题的表述是有问题的,webpack 本身并不打包 html,相反,它如果遇到 html 代码会直接打包失败,因为 webpack 本身只能识别 JS。之所以能够打包出 html 文件,是因为插件或 loader 的作用,其中,比较常见的插件是 html-webpack-plugin。所以这道题的正确表述应该是:「html-webpack-plugin 打包出来的 html 为什么 style 放在头部 script 放在底部?」

webpack 配置如何实现开发环境不使用 cdn、生产环境使用 cdn?

要配置 CDN,有两个步骤:

  1. 在 html 模板中直接加入 cdn 引用
  2. 在 webpack 配置中,加入externals配置,告诉 webpack 不要打包其中的模块,转而使用全局变量

若要在开发环境中不使用 CDN,只需根据环境变量判断不同的环境,进行不同的打包处理即可。

  1. 在 html 模板中使用 ejs 模板语法进行判断,只有在生产环境中引入 CDN
  2. 在 webpack 配置中,可以根据process.env中的环境变量进行判断是否使用externals配置
  3. package.json脚本中设置不同的环境变量完成打包或开发启动。

介绍一下 webpack4 中的 tree-shaking 的工作流程?

推荐阅读:https://tsejx.github.io/webpack-guidebook/principle-analysis/operational-principle/tree-shaking

说一下 webpack loader 的作用是什么?

参考答案:

用于转换代码。有时是因为 webpack 无法识别某些内容,比如图片、css 等,需要由 loader 将其转换为 JS 代码。有时是因为某些代码需要被特殊处理,比如 JS 兼容性的处理,需要由 loader 将其进一步转换。不管是什么情况,loader 的作用只有一个,就是转换代码。

在开发过程中如果需要对已有模块进行扩展,如何进行开发保证调用方不受影响?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

export 和 export default 的区别是什么?

参考答案:

export 为普通导出,又叫做具名导出,顾名思义,它导出的数据必须带有命名,比如变量定义、函数定义这种带有命名的语句。在导出的模块对象中,命名即为模块对象的属性名。在一个模块中可以有多个具名导出

export default 为默认导出,在模块对象中名称固定为 default,因此无须命名,通常导出一个表达式或字面量。在一个模块中只能有一个默认导出。

webpack 打包原理是什么?

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

webpack 热更新原理是什么?

参考答案:

当开启热更新后,页面中会植入一段 websocket 脚本,同时,开发服务器也会和客户端建立 websocket 通信,当源码发生变动时,webpack 会进行以下处理:

  1. webpack 重新打包
  2. webpack-dev-server 检测到模块的变化,于是通过 webscoket 告知客户端变化已经发生
  3. 客户端收到消息后,通过 ajax 发送请求到开发服务器,以过去打包的 hash 值请求服务器的一个 json 文件
  4. 服务器告诉客户端哪些模块发生了变动,同时告诉客户端这次打包产生的新 hash 值
  5. 客户端再次用过去的 hash 值,以 JSONP 的方式请求变动的模块
  6. 服务器响应一个函数调用,用于更新模块的代码
  7. 此时,模块代码已经完成更新。客户端按照之前的监听配置,执行相应模块变动后的回调函数。

如何优化 webpack 的打包速度?

参考答案:

  1. noParse 很多第三方库本身就是已经打包好的代码,对于这种代码无须再进行解析,可以使用 noParse 配置排除掉这些第三方库
  2. externals 对于一些知名的第三方库可以使用 CDN,这部分库可以通过 externals 配置不进行打包
  3. 限制 loader 的范围 在使用 loader 的时候,可以通过 exclude 排除掉一些不必要的编译,比如 babel-loader 对于那些已经完成打包的第三方库没有必要再降级一次,可以排除掉
  4. 开启 loader 缓存 可以利用cache-loader缓存 loader 的编译结果,避免在源码没有变动时反复编译
  5. 开启多线程编译 可以利用thread-loader开启多线程编译,提升编译效率
  6. 动态链接库 对于某些需要打包的第三方库,可以使用 dll 的方式单独对其打包,然后 DLLPlugin 将其整合到当前项目中,这样就避免了在开发中频繁去打包这些库

webpack 如何实现动态导入?

参考答案:

当遇到代码中包含动态导入语句时,webpack 会将导入的模块及其依赖分配到单独的一个 chunk 中进行打包,形成单独的打包结果。而动态导入的语句会被编译成一个普通的函数调用,该函数在执行时,会使用 JSONP 的方式动态的把分离出去的包加载到模块集合中。

说一下 webpack 有哪几种文件指纹

参考答案:

  • hash hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash 每个打包过程单独的 hash 值,如果一个项目有多个 entry,则每个 entry 维护自己的 chunkhash。
  • contenthash 每个文件内容单独的 hash 值,它和打包结果文件内容有关,只要文件内容不变,contenthash 不变。

常用的 webpack Loader 都有哪些?

参考答案:

  • cache-loader:启用编译缓存
  • thread-loader:启用多线程编译
  • css-loader:编译 css 代码为 js
  • file-loader:保存文件到输出目录,将文件内容转换成文件路径
  • postcss-loader:将 css 代码使用 postcss 进行编译
  • url-loader:将文件内容转换成 dataurl
  • less-loader:将 less 代码转换成 css 代码
  • sass-loader:将 sass 代码转换成 css 代码
  • vue-loader:编译单文件组件
  • babel-loader:对 JS 代码进行降级处理

说一下 webpack 常用插件都有哪些?

参考答案:

  • clean-webpack-plugin:清除输出目录
  • copy-webpack-plugin:复制文件到输出目录
  • html-webpack-plugin:生成 HTML 文件
  • mini-css-extract-plugin:将 css 打包成单独文件的插件
  • HotModuleReplacementPlugin:热更新的插件
  • purifycss-webpack:去除无用的 css 代码
  • optimize-css-assets-webpack-plugin:优化 css 打包体积
  • uglify-js-plugin:对 JS 代码进行压缩、混淆
  • compression-webpack-plugin:gzip 压缩
  • webpack-bundle-analyzer:分析打包结果

使用 babel-loader 会有哪些问题,可以怎样优化?

参考答案:

  1. 如果不做特殊处理,babel-loader 会对所有匹配的模块进行降级,这对于那些已经处理好兼容性问题的第三方库显得多此一举,因此可以使用 exclude 配置排除掉这些第三方库
  2. 在旧版本的 babel-loader 中,默认开启了对 ESM 的转换,这样会导致 webpack 的 tree shaking 失效,因为 tree shaking 是需要保留 ESM 语法的,所以需要关闭 babel-loader 的 ESM 转换,在其新版本中已经默认关闭了。

babel 是如何对 class 进行编译的?

参考答案:

本质上就是把 class 语法转换成普通构造函数定义,并做了以下处理:

  1. 增加了对 this 指向的检测
  2. 将原型方法和静态方法变为不可枚举
  3. 将整个代码放到了立即执行函数中,运行后返回构造函数本身

释一下 babel-polyfill 的作用是什么?

说明:

babel-polyfill 已经是一个非常古老的项目了,babel 从 7.4 版本开始已不再支持它,转而使用更加强大的 core-js,此题也适用于问「core-js 的作用是什么」

解释一下 less 的&的操作符是做什么用的?

参考答案:

&符号后面的内容会和父级选择器合并书写,即中间不加入空格字符

webpack proxy 工作原理,为什么能解决跨域?

说明:

严格来说,webpack 只是一个打包工具,它并没有 proxy 的功能,甚至连服务器的功能都没有。之所以能够在 webpack 中使用 proxy 配置,是因为它的一个插件,即 webpack-dev-server 的能力。

所以,此题应该问做:「webpack-dev-server 工作原理,为什么能解决跨域?」

组件发布的是不是所有依赖这个组件库的项目都需要升级?

参考答案:

实际上就是一个版本管理的问题。

如果此次模块升级只是修复了某一些 bug,作为补丁版本升级即可,不影响主版本和次版本号

如果此次模块升级会新增一些内容,完全兼容之前的 API,作为次版本升级即可

如果此次模块升级会修改之前的 API,则作为主版本升级

在开发项目时,让项目依赖模块的主版本,因此,当模块更新时,只要不是主版本更新,项目都可以非常方便的升级模块版本,无须改动任何代码。但若涉及主版本更新,项目可以完全无视此次版本更新,仍然使用之前的旧版本,无须改动任何代码;当然也可以升级主版本,但就会涉及代码的改动,这就好比跟将 vue2 升级到 vue3 会涉及大量改动一样。

而在开发模块时,在一开始就要精心设计 API,尽量保证 API 的接口稳定,不要经常变动主版本号。如果实在要更新主版本,就需要在一段时间内同时维护两个版本(新的主版本,旧的主版本),给予其他项目一定的升级时间。

开发过程中,如何进行公共组件的设计?(字节跳动)

参考答案:

  1. 确定使用场景 明确这个公共组件的需求是怎么产生的,它目前的使用场景有哪些,将来还可能出现哪些使用场景。 明确使用场景至关重要,它决定了这个组件的使用边界在哪,通用到什么程度,从而决定了这个组件的开发难度
  2. 设计组件功能 根据其使用场景,设计出组件的属性、事件、使用说明文档
  3. 测试用例 根据使用说明文档编写组件测试用例
  4. 完成开发 根据使用说明文档、测试用例完成开发

具体说一下 splitchunksplugin 的使用场景及使用方法。(字节跳动)

  1. 公共模块 比如某些多页应用会有多个入口,从而形成多个 chunk,而这些 chunk 中用到了一些公共模块,为了减少整体的包体积,可以使用 splitchunksplugin 将公共模块分离出来。 可以配置 minChunks 来指定被多少个 chunk 引用时进行分包
  2. 并行下载 由于 HTML5 支持 defer 和 async,因此可以同时下载多个 JS 文件以充分利用带宽。如果打包结果是一个很大的文件,就无法利用到这一点。 可以利用 splitchunks 插件将文件进行拆分,通过配置 maxSize 属性指定包体积达到多大时进行拆分

描述一下 webpack 的构建流程?(CVTE)

参考答案:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再把翻译后的内容转换成 AST,通过对 AST 的分析找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的 依赖关系图
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

解释一下 webpack 插件的实现原理?(CVTE)

参考答案:

本质上,webpack 的插件是一个带有apply函数的对象。当 webpack 创建好 compiler 对象后,会执行注册插件的 apply 函数,同时将 compiler 对象作为参数传入。

在 apply 函数中,开发者可以通过 compiler 对象监听多个钩子函数的执行,不同的钩子函数对应 webpack 编译的不同阶段。当 webpack 进行到一定阶段后,会调用这些监听函数,同时将 compilation 对象传入。开发者可以使用 compilation 对象获取和改变 webpack 的各种信息,从而影响构建过程。

有用过哪些插件做项目的分析吗?(CVTE)

参考答案:

用过 webpack-bundle-analyzer 分析过打包结果,主要用于优化项目打包体积

什么是 babel,有什么作用?

参考答案:

babel 是一个 JS 编译器,主要用于将下一代的 JS 语言代码编译成兼容性更好的代码。

它其实本身做的事情并不多,它负责将 JS 代码编译成为 AST,然后依托其生态中的各种插件对 AST 中的语法和 API 进行处理

解释一下 npm 模块安装机制是什么?

参考答案:

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。 gruntgulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。 webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。 所以总结一下:

  • 从构建思路来说

gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

  • 对于知识背景来说 gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel

3.有哪些常见的Loader?他们是解决什么问题的?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

4.有哪些常见的Plugin?他们是解决什么问题的?

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

5.Loader和Plugin的不同?

不同的作用

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。
  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

6.webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。


7.是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。 编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。 相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。


8.webpack的热更新是如何做到的?说明其原理?

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 原理:image.png

首先要知道server端和client端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

9.如何利用webpack来优化前端性能?(提高性能和体验)

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css
  • 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
  • 提取公共代码。

10.如何提高webpack的构建速度?

  1. 多入口情况下,使用CommonsChunkPlugin来提取公共代码
  2. 通过externals配置来提取常用库
  3. 利用DllPlugin和DllReferencePlugin预编译资源模块 通过DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin将预编译的模块加载进来。
  4. 使用Happypack 实现多线程加速编译
  5. 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。 原理上webpack-uglify-parallel采用了多核并行压缩来提升压缩速度
  6. 使用Tree-shaking和Scope Hoisting来剔除多余代码

11.怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可,这里不再赘述 多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

12.npm打包时需要注意哪些?如何利用webpack来更好的构建?

Npm是目前最大的 JavaScript 模块仓库,里面有来自全世界开发者上传的可复用模块。你可能只是JS模块的使用者,但是有些情况你也会去选择上传自己开发的模块。 关于NPM模块上传的方法可以去官网上进行学习,这里只讲解如何利用webpack来构建。 NPM模块需要注意以下问题:

  1. 要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则。
  2. Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
  3. Npm包大小应该是尽量小(有些仓库会限制包大小)
  4. 发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
  5. UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化:

  1. CommonJS模块化规范的解决方案: 设置output.libraryTarget='commonjs2'使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用
  2. 输出ES5代码的解决方案:使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: 'source-map'输出SourceMap以发布调试。
  3. Npm包大小尽量小的解决方案:Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件
  4. 不能将依赖模块打包到NPM模块中的解决方案:使用externals配置项来告诉webpack哪些模块不需要打包。
  5. 对于依赖的资源文件打包的解决方案:通过css-loader和extract-text-webpack-plugin来实现,配置如下:
javascript
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+
const ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+  module: {
+    rules: [
+      {
+        // 增加对 CSS 文件的支持
+        test: /\.css/,
+        // 提取出 Chunk 中的 CSS 代码到单独的文件中
+        use: ExtractTextPlugin.extract({
+          use: ['css-loader']
+        }),
+      },
+    ]
+  },
+  plugins: [
+    new ExtractTextPlugin({
+      // 输出的 CSS 文件名称
+      filename: 'index.css',
+    }),
+  ],
+};
+

13.如何在vue项目中实现按需加载?

Vue UI组件库的按需加载 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。 而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。 不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了。

javascript
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+
{
+  "presets": [["es2015", { "modules": false }]],
+  "plugins": [
+    [
+      "component",
+      {
+        "libraryName": "element-ui",
+        "styleLibraryName": "theme-chalk"
+      }
+    ]
+  ]
+}
+

单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。 通过import()语句来控制加载时机,webpack内置了对于import()的解析,会将import()中引入的模块作为一个新的入口在生成一个chunk。 当代码执行到import()语句时,会去加载Chunk对应生成的文件。import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill

能说说webpack的作用吗?

  • 模块打包(静态资源拓展)。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容(翻译官loader)。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展(plugins)。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

为什么要打包呢?

逻辑多、文件多、项目的复杂度高了,所以要打包 例如: 让前端代码具有校验能力===>出现了ts css不好用===>出现了sass、less webpack可以解决这些问题

能说说模块化的好处吗?

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化打包方案知道啥,可以说说吗?

1、commonjs commonjs 是 Node 中的模块规范,通过 require 及 exports 进行导入导出 commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。 总结:commonjs是用在服务器端的,同步的,如nodejs2、AMD AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

语句如下:通过define定义 image.png 可以更好的发现模块间的依赖关系 image.png总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs3、CMD CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。 image.png总结:amd, cmd是用在浏览器端的,异步的,如requirejs和seajs4、esm esm 是 tc39 对于 ESMAScript 的模块话规范,正因是语言层规范,因此在 Node 及 浏览器中均会支持。 它使用 import/export 进行模块导入导出. image.png 如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法。 export default命令,为模块指定默认输出

那ES6 模块与 CommonJS 模块的差异有哪些呢?

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝(浅拷贝),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

webpack的编译(打包)流程说说

  • 初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;
  • 开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
  • 确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
  • 编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;
  • 输出完成:输出所有的chunk到文件系统;

说一下 Webpack 的热更新原理吧?

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为** HMR**。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS(无线路由)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。 后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

路由懒加载的原理

https://juejin.cn/post/6844904180285456398

为什么要使用路由懒加载?

当刚运行项目的时候,发现刚进入页面,就将所有的js文件和css文件加载了进来,这一进程十分的消耗时间。 如果打开哪个页面就对应的加载响应页面的js文件和css文件,那么页面加载速度会大大提升。

懒加载的好处是什么?

懒加载简单来说就是延迟加载或按需加载,即在需要的时候的时候进行加载。

怎么使用的路由懒加载?

使用到的是es6的import语法,可以实现动态导入 image.pngimage.png

路由懒加载的原理是什么?

通过Webpack编译打包后,会把每个路由组件的代码分割成一一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。

作用就是webpack在打包的时候,对异步引入的库代码进行代码分割时(需要配置webpack的SplitChunkPlugin插件),为分割后的代码块取得名字 Vue中运用import的懒加载语句以及webpack的魔法注释,在项目进行webpack打包的时候,对不同模块进行代码分割,在首屏加载时,用到哪个模块再加载哪个模块,实现懒加载进行页面的优化。

npx

  1. 运行本地命令

使用npx 命令时,它会首先从本地工程的node_modules/.bin目录中寻找是否有对应的命令

例如:

shell
npx webpack
+
npx webpack
+

上面这条命令寻找本地工程的node_modules/.bin/webpack

如果将命令配置到package.jsonscripts中,可以省略npx

  1. 临时下载执行

当执行某个命令时,如果无法从本地工程中找到对应命令,则会把命令对应的包下载到一个临时目录,下载完成后执行,临时目录中的命令会在适当的时候删除

例如:

shell
npx prettyjson 1.json
+
npx prettyjson 1.json
+

npx会下载prettyjson包到临时目录,然后运行该命令

如果命令名称和需要下载的包名不一致时,可以手动指定报名

例如@vue/cli是包名,vue是命令名,两者不一致,可以使用下面的命令

shell
npx -p @vue/cli vue create vue-app
+
npx -p @vue/cli vue create vue-app
+
  1. npm init

npm init通常用于初始化工程的package.json文件

除此之外,有时也可以充当npx的作用

shell
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+
npm init 包名 # 等效于 npx create-包名
+npm init @命名空间 # 等效于 npx @命名空间/create
+npm init @命名空间/包名 # 等效于 npx @命名空间/create-包名
+

git

git fetch git fetch origin 分支 git rebase FETCH_HEAD 本地master更新到最新:git fetch origin master; git rebase FETCH_HEAD

git 和 svn 的区别 经常使用的 git 命令? git pull 和 git fetch 的区别 git rebase 和 git merge 的区别 git rebase和merge git分支管理、分支开发 git回退操作 git reset 和 git reverse区别?你还用过什么别的指令? git遇到冲突怎么进行解决

git-flow git reset --hard 版本号 git 如何合并分支; git 中,提交了 a, 然后又提交了 b, 如何撤回 b ? git merge、git rebase的区别 假设 master 分支切出了 test 分支,供所有人合代码测试, 然后我们 从稳定的 master 切出 一个 feature 分支进行开发, 现在 我们开发完了 feature 分支并将其merge 到 test 分支进行测试, 测试过程中出现一些问题,由于疏忽你;忘记切回 feature ,而是直接在 test 分支进行了开发 导致: test 分支优先于你的featuce 分支,此时你应该怎么特 test 分支的修改带入 feature 知道git的原理吗?说说他的原理吧image.png Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作 Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中那些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。 Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。 Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库

require 与 import 的区别

动态引入:require支持,import不行 异步同步:require同步,import异步 导出值:require是值拷贝,导出值的变化不会影响到到导入值。import指向指针,导入值随导出值的变化

package.json 字段

name npm 包的名字,必须是一个小写的单词,可以包含连字符-和下划线_,发布时必填。

version npm 包的版本。需要遵循语义化版本格式x.x.x。

description npm 包的描述。

keywords npm 包的关键字。

homepage npm 包的主页地址。

bugs npm 包问题反馈的地址。

license 为 npm 包指定许可证

author npm 包的作者

files npm 包作为依赖安装时要包括的文件,格式是文件正则的数组。也可以使用 npmignore 来忽略个别文件。 以下文件总是被包含的,与配置无关 package.json README.md CHANGES / CHANGELOG / HISTORY LICENCE / LICENSE 以下文件总是被忽略的,与配置无关 .git .DS_Store node_modules .npmrc npm-debug.log package-lock.json ...

main 指定npm包的入口文件。

module 指定 ES 模块的入口文件。

typings 指定类型声明文件。

bin 开发可执行文件时,bin 字段可以帮助你设置链接,不需要手动设置 PATH。

repository npm 包托管的地方

scripts 可执行的命令。

dependencies npm包所依赖的其他npm包

devDependencies npm 包所依赖的构建和测试相关的 npm 包.

peerDependencies 指定 npm 包与主 npm 包的兼容性,当开发插件时是需要的.

engines 指定 npm 包可以使用的 Node 版本.

CSS 模块化

  1. 原生CSS;
  2. CSS 预处理器:Less、Sass、Stylus
  3. CSS 后处理器:PostCSS
  4. CSS-Modules
  5. Css In JS

CSS 预处理器

  • Sass:2007 年诞生,最早也是最成熟的 CSS 预处理器,拥有 Ruby 社区的支持和 Compass 这一最强大的 CSS 框架,目前受 LESS 影响,已经进化到了全面兼容 CSS 的 SCSS;
  • Less:2009年出现,受 SASS 的影响较大,但又使用 CSS 的语法,让大部分开发者和设计师更容易上手,在 Ruby 社区之外支持者远超过 SASS,其缺点是比起 SASS 来,可编程功能不够,不过优点是简单和兼容 CSS,反过来也影响了 SASS 演变到了 SCSS 的时代,著名的 Twitter Bootstrap 就是采用 LESS 做底层语言的;
  • Stylus:Stylus 是一个CSS的预处理框架,2010 年产生,来自 Node.js 社区,主要用来给 Node 项目进行 CSS 预处理支持,所以 Stylus 是一种新型语言,可以创建健壮的、动态的、富有表现力的 CSS。比较年轻,其本质上做的事情与 SASS/LESS 等类似;

优点:

  • 变量(variables);
  • 代码混合(mixins);
  • 嵌套(nested rules);
  • 模块化(modules);

CSS 后处理器 Postcss 被称为 css 界的 babel,实现原理是通过 ast 分析 css 代码并处理。

优点:

  • 支持配合 stylelint 校验 css 语法;
  • autoprefixer;
  • 编译 css next 语法;

CSS Modules

  • BEM 命名规范(Block、Element、Modifier):BEM 只是一套相对标准的命名规范约定,并不能完全避免命名冲突的问题;
  • CSS Modules:CSS Modules 指的是将 css 像 js 文件一样 import;

优点:

  • 局部样式;
  • 支持变量;
  • 模块化;

CSS In JS CSS In JS,即用 JS 写 CSS。

优点:

  • 局部样式;
  • 使用 JS 增强 CSS 语法;(变量、运算、嵌套)
  • 组件化; 收起 评分标准 2.5分及以下:不清楚CSS模块化方案; 3分:能清楚地说明至少一种CSS模块化方案; 3.5分:能清楚地说明至少三种CSS模块化方案; 4分:能清楚地说明CSS模块化历史和所有模块化方案;

ES Module VS. CommonJS

题目描述 ES Module 和 CommonJS 模块规范有什么区别? 答案

  1. 使用方式不同:
  • ES Module:export、export default、import
  • CommonJS:module.exports、exports、require
  1. 解析时机不同:
  • ES Module:静态的,编译时解析;
  • CommonJS:动态的,运行时解析;
  1. 导出值类型不同:
  • ES Module:导出的是值的只读引用;
  • CommonJS:导出的是值的副本;

进阶考察:

  1. webpack 可以对 CommonJS 模块进行 Tree Shaking 吗?为什么? 不能。CommonJS 是动态的,不能在编译时确认模块引用关系,因此无法实现 Tree Shaking。
  2. ES Module 可以在条件语句中使用 import 导入模块吗?为什么? 不能。ES Module 是静态的,不能在运行时动态导入模块。 3分:能基本说清楚使用方式、解析时机和导出值类型的区别; 3.5:能清晰地说明使用方式、解析时机和导出值类型的区别;并能全部回答进阶考察题;

构建工具webpack

题目描述 webpack构建过程中需要借助哪些手段来提升编译速度和减少编译体积 答案

  1. commonchunkplugin
  2. require.ensure 3、dllplugin 4.externals 5、happypack 6.uglify-parallel,
  3. tree-shaking, 8、gzip

打造前后端分离的开发环境,一般需要从哪几个方面进行设计

题目描述 打造前后端分离的开发环境,一般需要从哪几个方面进行设计 答案

  1. 如何选用web服务框架(express、koa);
  2. 如何使用http-proxy进行转发(CROS有何局限);
  3. 如何设计静态资源访问和搭建热更新服务;
  4. 如何实现模拟登陆请求(cookie模拟登录访问和安全控制);
  5. 如何实现资源的云端构建和压缩后的代码预览
  6. 如何读取后端资源模板语法

参考:https://segmentfault.com/a/1190000009266900?_ea=2040096

评分标准 3分:本地server、mock数据、接口请求转发、后端模板解析、本地代码调试方案合理 得3分; 3.5+分: 模拟登录、https、组件预览、云端构建、前后端同构等有相对合理 得3.5

前端组件化看法?

  • 有写过组件么?你对前端组件化的看法是什么?
  • 建立组件规范考虑哪些因素?
  • 对组件复用有什么看法?如何划分?

组件化作用:

  • 分而治之的开发维护方式
  • 复用

组件规范:

  • 代码组织结构:同一类型的组件有一致的代码组织结构、编码规范和调用方式
  • 版本控制:每次改动升级都有具体的说明和独立版本号,方便维护
  • 独立作用域:保证作用域隔离,组件内部代码不侵入其他组件
  • 生命周期:组件从创建到消亡中的每个时间点有回调钩子,方便开发使用人员控制
  • 通信模式:规范父子组件通信模式、独立组件之间的通信模式

组件复用:这块比较有争议,看面试者能否自圆其说

参考: https://github.com/xufei/blog/issues/19

webpack插件编写

题目描述

  1. 有用过webpack么?说说该工具的优缺点?
  2. 有开发过webpack插件么?
  3. 假如要在构建过程中去除掉html中的一些字符,如何编写这个插件? 答案 webpack优缺点:
  • 概念牛,但文档差,使用起来费劲
  • 模块化,让我们可以把复杂的程序细化为小的文件
  • require机制强大,一切文件介资源
  • 代码分隔
  • 丰富的插件,解决less、sass编译

开发插件的两个关键点Compiler和Compilation:

  • compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并在所有可操作的设置中被配置,包括原始配置,loader 和插件。当在 webpack 环境中应用一个插件时,插件将收到一个编译器对象的引用。可以使用它来访问 webpack 的主环境。
  • compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用。

插件编写可参考:https://doc.webpack-china.org/development/how-to-write-a-plugin

请简要描述ES6 module require、exports以及module.exports的区别

题目描述 考察候选人对es6,commonjs等js模块化标准的区别和理解 答案

  • CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
  • ES6 模块是动态引用,如果使用 import 从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
  • CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。
  • export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性
  • 混合使用介绍:https://github.com/ShowJoy-com/showjoy-blog/issues/39 收起 评分标准
  1. 2.5分及以下:不知道js模块化标准之间的区别
  2. 3.0分:能答对commonjs是运行时确定和es6 modules是静态化,编译时确定这个核心的不同
  3. 3.5分:对commonjs的标准和es6 modules的转换关系有一定的了解,es6 moudules有一定的使用经验,在浏览器中使用方式接触过
  4. 4.0分:了解umd,知道webpack将es6编译回es5的操作细节,知道tree-shaking等优化手段。

  • git merge、git rebase 的区别

  • 项目工程化有什么了解

  • 前端最新技术发展有什么了解?

    • 内部商业数据平台,商业数据的可视化展示,报表系统,数据分析、资料管理。
    • 项目技术选型
    • 假如系统已经发布上线了,用户反馈了一个错误,他错误里面可能就截了一个 JS 报错的截图。我怎么通过这个 JS 去找到到底是哪一行 TypeScript 报错的呢?
      • 不知道
      • 应该回答  webpack 设置 devtool: 'inline-source-map'  还得细查
    • 项目业务问题:
      • 我们为什么需要云间数据迁移呢?
        • 有些企业或者国有机构会选择将一部分数据部署在华为云上,一部分数据部署在阿里云上,可能出于数据安全性的考虑,不会把数据单独的存放在一个云上。那么他之后如果可能考虑到,如果当前的数据不想继续存在某个云上,那他就要是把一个云上的数据迁到另外一个云上了,那这个时候就需要用到我们这个系统去方便他进行不同云平台之间的数据的迁移。如果没有这个系统的话,那他就需要很麻烦的把数据先迁移到本地,再迁移到另一个云上。
      • 很多云平台提供了数据在云之间迁移的功能?
        • 根据我们的调研结果显示,目前云平台大部分类型的数据文件没有直接支持迁移到另一个云平台上的功能,只有少数的,例如虚拟机迁移是有的。
      • 系统是怎么部署的?
        • 比如说阿里云迁到华为云,假如阿里云的某个网络下的服务器要迁移的,那么这个网络下就要部署一台代理主机,然后同时要部署一台控制中心的主机,然后华为云上也是在某个网络下部里部署代理主机。云的每个网络下都要去部署数据传输的节点。
      • 如何通知用户这个迁移任务完成?或者怎么告诉他这个中间的各种进度。
        • 查看日志、查看任务状态图标
        • 后续迭代功能: \1. 提供日志的实时更新的功能; \2. 与后端建立websocket的连接,让后端主动推送任务完成状态信息,前端通过消息弹框展示; \3. 通过提供短信或者邮件通知的方式去告知用户当前迁移任务的完成情况。
    • **云的很大的问题:云供应商锁定。**在不同的供应商之间,他们虽然提供的都是这种 PaaS 产品,但他们的 PaaS 产品的能力可能是不能完全对齐的。你很难把一部分功能从一个云计算厂商迁移到另外一个云计算厂商。
      • 我们还没有到应用迁移的层面
      • 再问:比如说比较关键的这个触发器,不同的数据库厂商对这个触发器的支持也是不相同的。如果你客户使用了某个厂商数据库中的触发器的功能,那另一个云可能是没有这个功能的,该怎么办?
      • 答;那首先就是客户他我们以及客户要先做好这个技术调研,比如说同类的产品一个云支持,一个云不支持。那么对于这个不支持的这个情况,那你就要去跟甲方企业去进行沟通,看他们能不能允许这个不支持的情况的存在。如果数据迁移,我们就可能要先进行测试。数据迁移迁过去可以正常使用的,没有问题,那就去再进行迁移。
    • 怎么跟服务端同学进行协作的呢?
      • 前期的时候是我们大家一起去沟通,当前这个系统应该有什么样的功能需求,划分模块,然后再去针对具体的模块去定义接口。定好这个接口之后,我们就先各自分离的开发。我开发完一部分功能之后,我们就进行联调的测试。测试完之后就是一部分功能实现了,然后我们就把这个就代码上传到 git 上,然后阶段性的进行联调和测试。
    • 前端怎么调用这个服务端的接口呢?服务端是把它他们的服务部署在他们的自己的机器上,我们去直接调用吗?
      • 他们后端的代码部署也在云服务器上,提供给一个 swaga UI 这个接口文档。前端本地要配置跨域,通过配置CORS解决。
  • 说一下 git merge和git rebase的区别

  • git 开发和上线的时候,分支怎么管理?

  • CICD 自动部署,用的什么工具?

  • CICD部署的命令是什么?

    • 把编译好的文件放到云服务器中tomcat指定的文件地址就可以。
  • 文件是怎么生成的?

    • gitlab 的runner 执行了npm run build指令
  • 需要install吗

    • 需要,具体的install的流程是根据package.json文件是否发生变化来判断是否需要install的
    • 如何实现?
    • CICD工具自动实现的
  • runner完了生成的产物是什么?

    • dist文件
  • 使用tomcat是基于什么选型?

    • 后端同学要用
  • 了解过别的部署的服务器吗?

    • nginx
  • 对比过nginx和tomcat的区别吗?

    • 没有
  • 有针对性能做过什么优化?

    • 组件懒加载
    • 减少非必要的数据监听
  • 有针对webpack做过什么配置用于性能优化吗?

  1. 需求:页面功能类似,需要重复用一个组件,组件上面是搜索框,下面是具体的内容。 现在有多个页面用到用一个组件,要求页面切换的时候能保留原先页面的内容,该如何做。
    • 答:方法一:keep-alive组件;方法二:将每个页面的关键字存到sessionStorage中,当页面切换回去的时候读取该页面对应的关键字值。
    • 再问,假如要求页面刷新还能保留页面内容,该如何做?假如有十多个页面,具体从关键字存储方式、存储时机、存储结构三个角度回答。
    • 答:那就不能用keep-alive了。要存关键字。
      • 存储方式:localStorage(经过面试官的提醒,有问到为什么用sessionStorage,我想了想用localStorage更合适,还提到了indexDb,但是我不熟悉)
        • 最开始有提到存到父组件中,但是存到父组件的话,页面刷新就没有了。
        • 问:数据存到父组件中,那么父组件把这些真实的数据存到哪儿了?存到什么属性里了?具体的值是什么?刷新后会怎么样? 我不知道
      • 存储结构:sessionStorage只能存键值对,而在页面很多的情况下,存储单个关键字显然很不方便。经过面试官提醒后想到,可以存对象转换后的JSON,读取的时候将JSON再转换为对象就可以。
      • 存储时机:在组件销毁前,beforeDestory钩子中存下关键字对象。在created或者beforeMount钩子中读取关键字,向后端请求数据。

我这边的方向基本上主要是负责一些核心服务的开发与维护,还有一些新的项目。项目的种类的话主要包括有我们这个公司内部的 CSE 的平台,因为我们内部是容器化的。然后我们自己的发布的流程比如刚刚你说的你们这边也有那些 CICD 的那些流程。然后我们这边的 CICD 的研发的平台就是我这边在开发的,然后包含了它一整套的生态系统,包括告警,日志,监控。这是一个很很大的一个产品,它里面可能包含很多很多模块。

另外就是我们这边有一个给客户那边用的一个系统,我们这边是做能源的。他们肯定有很多那种什么硬件的设备,这种可能需要获取它上面的数据去做一个可视化的展示。通过微前端把他们很多个功能整合到一起,这样的话就是做一个门户的网站,去给客户去使用。

其他还有一个就是我们前端基础的,比如说我们用的那个组件库是我们这边自己在维护,但是它是基于antd的基础上做了二次的开发,然后再维护。

我们这边会有专门的一个平台去做我的这个可视化大屏展示。然后比如说我这个项目需要用到,我就可以把它接入进来,我只要是满足它的一个数据接入的数据结构。我们这边现在的话,前端团队都已经就是 30 大几个人了。对整体来说规模还是很大的。

  • 系统有上线吗?系统有什么收益?

  • 项目周期是怎么管理的?之前的开发预期是多久?

  • 技术选型是怎么考虑的?

    • UI框架:ant design 、 element UI
    • 数据存储:vuex
    • 用户状态:JWT、cookie
      • 追问:JWT的组成有哪些
  • 项目除了研究价值,实际的收益是怎么样的?比如,会考虑商业化运作吗?或者对整个社会或者其他方面有什么推动作用?

    • 降低企业数据迁移成本
  • 除了前端,对系统的后端有什么了解?

    • 系统部署架构
    • 主机和用户的认证机制,证书认证

虚拟滚动

列举一个近期做的最能体现设计能力的项目

请举出一个你近期做的项目,项目需要最能体现设计能力,  请从以下角度说明:

  1. 项目描述
  2. 技术选型
  3. 模块化
  4. 模块之间通信
  5. 工程化
  6. 前后端数据流

答案 这是一个开放式的工程设计题目,没有固定答案,评分参考评分标准 评分标准 这是一个开放式的工程设计题目,主要考核候选人工程设计能力。 前面6个点各自按照标准进行评分,最后合并得分得出最后分数。 1、项目描述 要求:逻辑清晰、重点突出、难点 2.5及以下: 思维不清晰,描述不清晰,项目疑点较多(可能不是其自己做的) 3分: 逻辑清晰,能够清晰描述项目 3.5分: 满足(1)的基础上,描述中项目的重点突出、难点也了解比较清晰 2、技术选型 要求: 2.5分及以下: (1)、没有选型埋头就干 (2)、没有比对过任何其他的技术框架、不了解项目特性、只按照自己的知识面进行选型 (3)、不了解其他可能可用的框架的优势和不足,自己埋头造轮子 3分 :了解项目的特性, 能够比对市面上大多数的开源框架,大体了解这些框架的优点和缺点,能够客观选择框架 3.5分: 能够了解项目特性,清晰了解市面上大多数开源框架的优缺点,了解这些框架的痛点,在选型完成后能够在这些框架之上做一些优化,解决这些框架使用过程中的痛点。或者在足够了解这些框架存在的不足的情况下,自己能够沉淀出一套框架解决前面框架的痛点问题 3、项目模块化 2.5分以及以下: 模块划分不清,不能将常规业务逻辑解耦 3分: 模块划分清晰,业务能够解耦合理 3.5 满足3分的情况下,对未来的项目扩展有清晰的考虑,模块结构上对未来扩展留有足够的空间 4、模块之间的通信 2.5分以及以下: 对模块之间的通信没有概念 3分 能够清晰了解当前框架的模块通信机制,并且合理利用 3.5分 清晰了解当前框架模块通信的优点和缺点,也了解其他框架的模块之间通信的优缺点,自己主动在框架上 5、工程化方案 2.5分以及以下: 不了解项目的编译脚本等 3分 了解项目的编译(线上、线下)等,并且能合理的利用 3.5分 了解当前的工程化方案,了解市面上其他工程化方案的优点和缺点,在当前工程化方案上有过自己的探索,比如选用webpack,有写webpack的插件去满足当前项目的需求 6、前后端数据流 2.5分以及以下: 不了解项目后端到前端的整体数据流程、程序流程,没有概念 3分 大体了解项目的核心数据流程、程序流程 3.5分 了解项目的数据流程和程序流程,能够快速清晰画出核心数据流程图、程序流程图

登录表单设计/扫码登录/第三方登录

题目描述

  1. 请实现一个登录表单
  2. 用GET方法行不行?csrf是什么?如何防御?
  3. cookie-sesssion的工作机制
  4. 你已经登录产品的App端,要在web实现扫码登录,该如何设计?
  5. 接入第三方登录(如微信),如何设计? 答案
  6. 正确书写html
  7. 正确回答GET和POST的区别,从语义、弊端、安全等方面。csrf的防御:token,samesite,referer校验(弊端)等
  8. 正确理解cookie-session的工作机制,sessionId的设计,存储
  9. 考察对司空见惯的扫码登录,是否有思考其实现。正确设计 Client/Server/App 三方流程,设计二维码存储的内容,client通知有轮训或websocket等解决方案
  10. 正确理解 Client/Server/App/Weixin Server 四方流程,理解oauth2协议
  11. 2.5分及以下: 无法精确理解GET/POST差别,不了解常见安全防御手段,不理解cookie-session的工作机制
  12. 3.0分: 对前4问都达到75%的基本覆盖。
  13. 3.5分: 能够给出清晰、完整的流程设计,给出完备的前端实现
  14. 4.0分: 了解server端的一些工作内容(session存储,密码存储等)。 理解oauth2协议。 会考虑到非自己App扫二维码的时,根据UA跳转不同的下载网页。

如果数据库中采用64位长整型存储一个数据的id,前端通过api拿到这个id的话,会有什么问题?应该怎么解决?

题目描述 如果数据库中采用64位长整型存储一个数据的id,前端通过api拿到这个id的话,会有什么问题?怎么解决? 答案 考察一下JS中整数的安全范围的概念,在头条经常会遇到长整型到前端被截断的问题,需要补一个字符串形式的id供前端使用。 主要会涉及到JS中的最大安全整数问题 https://segmentfault.com/a/1190000002608050

如何实现微信扫码登录?

题目描述 综合题,考察网络、前端、认证等多方面知识 答案 参考答案: https://zhuanlan.zhihu.com/p/22032787 具体步骤:

  1. 用户 A 访问微信网页版,微信服务器为这个会话生成一个全局唯一的 ID,上面的 URL 中 obsbQ-Dzag== 就是这个 ID,此时系统并不知道访问者是谁。
  2. 用户A打开自己的手机微信并扫描这个二维码,并提示用户是否确认登录。
  3. 手机上的微信是登录状态,用户点击确认登录后,手机上的微信客户端将微信账号和这个扫描得到的 ID 一起提交到服务器
  4. 服务器将这个 ID 和用户 A 的微信号绑定在一起,并通知网页版微信,这个 ID 对应的微信号为用户 A,网页版微信加载用户 A 的微信信息,至此,扫码登录全部流程完成

富文本编辑器https://juejin.cn/post/6940904354090057764vue-quill-editor还知道哪些其他的库吗为什么不用vue2-editor呢?

主要是我把我这边项目和我做的一些优化讲的很细,然后他都听懂了,然后他好像就问了一些很多这种代码安全性的问题,然后让我设计一个Vs code的插件啊,这样子的,然后基于什么K的一些whos什么P和一些什么,提交之后,这些东西去设计一个插件来保证我们的代码不会被冲突,或者说不会改,因为改代

码而发生错误,然后再利用发布订阅模式去设计这个东西,

我在vscode里订阅这个函数,那么这个西数其他被修改提交时,lab会自动读取commit信息把我拉进review名单里

结合gitlab和sso做个发布订阅,再lab code reivew的时候去拉一下订阅者

穿越沙漠:大致就是A,乙两个点,一个是起点, 一个是终点,中间有很多的补给点,怎么 样过才最安全(不是最短路径,得考虑两个补给点之问的尽量的短,而且短的路 径尽量多)

直接无脑贪心+bfs,

有个问题:内网网站需要链接vpn才能访问,如果之前访问过了,去掉vpn为啥不走缓存?而是直接无法访问?

语音识别做出来,挑战

场景方案设计题:结合gitlab和sso账号做个发布订阅,再lab code reivew的时候去拉一下订阅者,然让我做我也做不出来呀,我但是我会说呀,对吧,就结合地的一些勾子西数嘛,然后你在Vs code的插件,首先你要登录Vs code嘛,然后Vs code会拉到 你本地的一个字节的一个账号信息,然后,如果你订阅了这个西数的话,它会保存下来,嗯,你的一个订阅这个西数是在哪个项目? 然后哪个页面那个什么什么什么下面,然后当这个东西去改变的时候,然后我去那个记得记提交的户里面查看一下原来的K记录,他有没有被订阅过?那有被订阅过,我就会通知所有的订阅者。

呃,你对单侧有了解吗?那如果说呃,现在是有AB2个同学,或者说ABCD很多同学共同开发一个代码库,那么,可能a在改了某个工具函数之后,他把代码提交上线了,然后a没有发现这个西数,他本身是可能会对其他人人的逻辑造造成影响的,然后他把他的代码上线了,然后B这边的代码,因为a的上线,B之前写的代码被影响了,就是可能会报错之类的,那么,有没有什么办法可以呃,让B之前就提前知道了,然后并且去做一个修改呢。

Node.js 所使用的 CommonJS 模块规范是如何处理循环依赖加载的Node.js 运维相关,如何排查内存泄漏,如何做进程守护V8 引擎内存回收实现算法是什么,JSHeap 默认大小是多少Node.js 模块中的 __dirname、exports、__filename 等变量是哪来的

服务器时间获取错误

假设有服务api功能为下发服务器时间戳,功能正常 但在网页中应用后,某些用户会拿到错误的时间,忽前忽后,但不会超前于真实的服务器时间 有哪些造成这个问题的原因? 一般来说是中了运营商的劫持,运营商对同一url的返回值做了缓存,而代码在请求时又没有加随机数

  1. 没思路的,说是浏览器缓存引起的(这个贴边,校招生可以提示一下忽前忽后,看反应)
  2. 3.0分:能正确回答的,并且能给出解决办法,随机数、post等都行

为什么需要child-process?

node是异步非阻塞的,这对高并发非常有效.可是我们还有其它一些常用需求,比如和操作系统shell命令交互,调用可执行文件,创建子进程进行阻塞式访问或高CPU计算等,child-process就是为满足这些需求而生的.child-process顾名思义,就是把node阻塞的工作交给子进程去做.https://github.com/jimuyouyou/node-interview-questions#起源

基于Node的前后端分离

  1. 讲解下架构是怎样的?Node层主要负责了哪些事情?
  2. Node层是如何调用后端服务的?
  3. 前后端分离优缺点?
  • 前后端分离,node一般承担路由/模板渲染功能
  • node服务可以通过rpc、restful接口等方式调用后端服务
  • 进一步降低前后端开发上耦合,提升前后端开发效率
  • 前端能够在业务层面(view+controller)有更多的控制力度,后端只关注数据提供服务
  • 摆脱后端的一些历史包袱,比如:框架版本过久、新特性支持跟不上等
  • 前端可用熟悉的javascript搭建node服务,无需切换技术栈
  • 前端可以尝试更多的优化手段
  • 分层会有一定的性能损耗,职责清晰、方便协作上的收益会弥补这块的损失
  • 前端承担了业务上更多的工作,缓解后端人力资源
  • 前端多了一个 Node 层开发/运维的工作,Node 层的基础设施服务需要持续建设

参考: http://admin.bytedance.com/surfbird/best_practice/#/node-idea

https://tech.meituan.com/node-fullstack-development-practice.html

  1. 3.0分:了解或参与过具体实践,能够比较完整的说清楚架构
  2. 3.5分:熟悉架构,并能够完整详细的给出前后端分离的一些注意点和关键技术

介绍一下你了解的 WebSocket

简单介绍一下 WebSocket,ws 协议和 http 协议的关系是什么,WebSocket 如何校验权限? WebSocket 如何实现 SSL 协议的安全连接? WebSocket 是基于 http 的,所以建立 WebSocket 连接前, 浏览器会通过 http 的方式请求服务器建立连接, 这个时候可以通过 http 的权限校验方式来校验 WebSocket,比如设置 Cookie。 同理,WebSocket 实现 SSL 协议也同 https 类似,会升级为 wss 连接。 另外,当然也可以在 WebSocket 中还可以通过加密或者 token 等方式,实现自己额外的加密传输和权限判断方式。 更多可参考 https://security.tencent.com/index.php/blog/msg/119

  1. 2.5分及以下:对 WebSocket 了解不多,没做过任何实时类业务;
  2. 3.0分:能够介绍出 WebSocket 是基于 http 的;
  3. 3.5分:能够介绍出全部内容;
  4. 4.0分:能够介绍出内容,并且结合自身项目经验,介绍了更多内容;

存储在 Cookie 中每个 request 都会带上,而放在 localStorage 中,仅有浏览器中会存储。

  1. 2.5分及以下:介绍了 cookie 和 localStorage 的简单区别;
  2. 3.0分:能够说出某些数据存在 cookie 中比 localStorage 合适,就是做了对比;
  3. 3.5分:能够说明 cookie 在每个请求中都会自动携带,可能会增加请求的大小;
  4. 4.0分:能够说出更多,比如 cookie 中可能有安全问题,服务端可以开 httpOnly 防止 JS 读取 cookie,总而言之是介绍了更多细节;

请介绍一下Oauth2.0 的认证过程

可以参考 http://www.jianshu.com/p/0db71eb445c8 或者 http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 的答案, 回答的一个重点是 code(授权码)仅一次有效,并且要有失效时间,而且很短,比如一分钟, 因为浏览器收到会立刻跳转。 还有就是服务端可以根据 code 结合相应的 sercet 去获取 token,要说清楚。

  1. 2.5分及以下:只是介绍了 Oauth 的用途,比如第三方可以登录、可以获取账号信息,无法介绍技术流程;
  2. 3.0分:回答清楚了整体流程;
  3. 3.5分:能够介绍出 code 仅一次有效,申请方需要有 sercet,并且结合 code 去换取 token 的过程及原因;
  4. 4.0分:还能够介绍出可能的其他安全问题;
  • CSRF: 文件流steam(字节2020校招)
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
文件流就是内存数据和磁盘文件数据之间的流动
+通过fs模块当中的方法实现
+
  • 安全相关:XSS,CSRF(字节2020校招)
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
CSRF: 跨站点请求伪造, 用户访问正规网站进行登录的操作,然后进入恶意网站,恶意网站会返回一个带有攻击性的代码,在用户没有意识的情况下去请求正规网站的信息, 而正规网站没有进行来源校验,不知道是不是通过正规网站如果访问的就会将用户信息返回,导致恶意攻击	
+	解决方案: 
+  	1. 判断用户来源refer
+    2. 为cookie设置 仅限同意网站连接(SameSite)  老版本浏览器不支持
+    3. 使用非cookie 令牌
+    4. 发送验证码
+    5. 二次验证
+ 
+ XSS: Cross Site Scripting  跨站脚本攻击
+    1. 恶意用户提交了恶意内容到服务器
+    防御方式: 防止脚本存入数据库  可以将脚本进行转码
+    2. 恶意用户转发给正常用户一个正常网站但地址里面存在恶意内容
+    防御方式: 过滤脚本信息
+
  • 数据劫持(字节2020校招)
  • CSP,阻止iframe的嵌入(字节2020校招)
  • 进程和线程的区别知道吗?他们的通信方式有哪些?(字节2020校招)
  • js为什么是单线程(字节2020校招 + 京东2020校招)
  • section是用来做什么的(字节2020社招)
  • 了解SSO吗(字节2020社招)
  • 服务端渲染的方案有哪些?next.js和nuxt.js的区别(字节2020社招)
  • cdn的原理(字节2020社招)
  • JSBridge的原理(字节2020社招)
  • 了解过Serverless,什么是Serverless(字节2020社招)
无服务器~
+云计算?
+
无服务器~
+云计算?
+
  • 你还知道哪些新的前端技术(微前端、Low Code、可视化、BI、BFF)(字节2020社招)
  • 权限系统设计模型(DAC、MAC、RBAC、ABAC)(字节2020社招)
  • 什么是微前端(字节2020社招)
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
微前端是一种前端架构,当前端业务发展到一定的业务程度的时候需要一种用于分散复杂度的架构模式于是出了微前端
+
  • Node了解多少,有实际的项目经验吗(字节2020社招)
  • Linux了解多少,linux上查看日志的命令是什么(字节2020社招)
  • 强缓存和协商缓存(腾讯2020校招)
  • node可以做大型服务器吗,为什么(腾讯2020校招)
  • 用express还是koa(腾讯2020校招)
  • 用node写过哪些接口,登录验证是怎么做的(腾讯2020实习)
  • 清理node模块缓存的方法(腾讯2020校招)
  • 模仿一个xss的场景(从攻击到防御的流程)(腾讯2020校招)
  • Event loop知道吗?Node的eventloop的生命周期(京东2020校招)
  • JWT单点登录实现原理(小米2020校招)
  • 博客项目为什么使用Mysql?(小米2020校招)
  • 如何看待Node.js的中间件?它的好处是什么?(小米2020校招)
  • 为什么Node.js支持高并发请求?(小米2020校招)
  • 博客项目是SSR渲染还是客户端渲染?(小米2020校招)
  • web socket 和 socket io(广州长视科技2020校招)
  • 是否使⽤过webpack(不是通过vuecli的⽅式)(掌数科技2020社招)
  • 是否了解过express或者koa(掌数科技2020社招)
  • node如何实现热更新
(橙智科技2020社招)
  • Node 知道哪些(express, sequelize,一面也问了)(微医云2020校招)
  • 用node做过什么(微医云2020校招)
  • Rest接口(微医云2020校招)
  • Linux指令
  • 测试工具什么什么
  • Linux指令
  • 测试工具什么什么
  • 浏览器和node环境的事件循环机制。(流利说2020校招)
  • https建立连接的过程。(流利说2020校招)
  • 讲述下http缓存机制,强缓存,协商缓存。(流利说2020校招)
  • jwt原理(北京蒸汽记忆2020校招)
  • 需求:实现一个页面操作不会整页刷新的网站,并且能在浏览器前进、后退时正确响应。给出你的技术实现方案?
  • 对Node的优点和缺点提出了自己的看法
  • nodejs的适用场景
  • (如果会用node)知道route, middleware, cluster, nodemon, pm2, server-side rendering么?
  • 解释一下 Backbone 的 MVC 实现方式?
  • 什么是“前端路由”?什么时候适合使用“前端路由”? “前端路由”有哪些优点和缺点?

操作系统:进程线程、死锁条件 Redis的底层数据结构 mongo的实务说一下, redis缓存机制 happypack 原理 【多进程打包 又讲了下进程和线程的区别】

  • koa 的原理 与express 的对比
  • 非js写的Node.js模块是如何使用Node.js调用的 【代码转换过程】 除了都会哪些后端语言 【一开始说自己用Node.js,面试官又问有没有其他的,楼主大学学过java,不过没怎么实践过】
  • Mysql的存储引擎 【这个直接不知道了 TAT】
  • 两个场景设计题感觉自己的设计方案还是有瑕疵的不是面试官想要的,并且问到mysql存储引擎直接无言以对了,感觉自己知识的广度还是有一定欠缺。最后问到性能优化正好自己之前优化过自己的博客网站做过相关的实践
  • koa了解吗

koa洋葱圈模型运行机制 express和koa区别 nginx原理 正向代理与反向代理 手写中间件测试请求时间

  • 进程和线程的区别

mongo的实务

node模块加载机制(require()原理)

路径转变,缓存

koa用的多吗

redis缓存机制是啥

8、 mysql如何创建索引 9、 mysql如何处理博客的点赞问题

完成prosimeFy babel 原理,class 是转换成什么 https://juejin.cn/post/6844904024928419848

javascript
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
const fs = require("fs");
+
+fs.readFile("./index.js", function (err, file) {
+  console.log(file, "file");
+});
+
+function prosimeFy(fn) {
+  return function (...args) {
+    return new Promise((resolve, reject) => {
+      let callback = function (...args) {
+        resolve(args);
+      };
+      fn.apply(null, [...args, callback]);
+    });
+  };
+}
+
+const fsPromiseReadFile = prosimeFy(fs.readFile);
+
+fsPromiseReadFile("./index.js").then((err, file) => {
+  console.log(file, "file");
+});
+
+
javascript
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+
const wrapperFun = promisify(fun);
+const fun = (callback) => {
+  setTimeout(() => {
+    callback();
+  }, 1000);
+};
+async () => {
+  //   await wrapperFun(); //需要return这个
+};
+
+async function promisify() {
+  // to do
+}
+

进程线程

  1. 页面缓存cache-control

  2. node写过东西吗

  3. node模块,path模块

  4. 对比一下commonjs模块化和es6 module

  5. 两个文件export的时候导出了相同的变量,避免重复--as

  6. exports和module.export的区别

  7. http和https

  8. 非对称加密和对称加密

  9. 打乱一个数组Math.random()的范围

  10. 问的特别全面,基础几乎全问

  11. 网站安全,CSRF,XSS,SQL注入攻击

  12. token是做什么的

node koa \4. 由于简历写了了解Nodejs的底层原理。问了Node.js内存泄漏和如何定位内存泄漏 常见的内存场景:1. 闭包 2. http.globalAgent 开启keepAlive的时候,未清除事件监听 定位内存泄漏:1. heapdump 2. 用chromedev 生成三次内存快照 \5. Node.js垃圾回收的原理 \4. 客户端发请求给服务端发生了什么 \6. 你知道MySQL和MongDB区别吗 \7. 你平时怎么使用nodeJS的 \8. restful风格规范是什么 新生代(from空间,to空间),老生代(标记清除,引用计数) NodeJs的EventLoop和V8的有什么区别 Node作为服务端语言,跟其他服务端语言比有什么特性?优势和劣势是什么? Mysql接触过?跨表查询外键索引(就知道个关键字所以没往下问了) 1、介绍一下项目的难点以及怎么解决的 5、移动端的业务有做过吗? 6、介绍一下你对中间件的理解7、怎么保证后端服务稳定性,怎么做容灾8、怎么让数据库查询更快9、数据库是用的什么?10、为什么用mysql1、如何用Node开启一个服务器 2、你的项目当中哪些地方用到了Node,为什么这个地方要用Node处理

  • 后端语言,涉及各种类型,主要都是TS,Java等,nodeJS作为动态语言,你怎么看?
  • 怎么理解后端,后端主要功能是做什么?
  • 校验器怎么做的?
  • 你对校验器做了什么封装?你用的这个库提供哪些功能?怎么实现两个关联参数校验?
  1. 进程与线程的概念
  2. 进程和线程的区别
  3. 浏览器渲染进程的线程有哪些
  4. 进程之前的通信方式
  5. 僵尸进程和孤儿进程是什么?
  6. 死锁产生的原因?如果解决死锁的问题?
  7. 如何实现浏览器内多个标签页之间的通信?
  8. 对Service Worker的理解

Mysql两种引擎

· 数据库表编码格式,UTF8和GBK区别

· 一个表里面姓名和学号两列,一行sql查询姓名重复的信息

· 防范XSS攻击

· 过滤或者编码哪些字符

  1. export和module.export区别
  2. Comonjs和es6 import区别

· Video标签可以播放的视频格式

  1. JWT
  2. 中间件有了解吗
  3. 进程和线程是什么
  4. nodejs的process的nexttick 为什么比微任务快
  5. 进程之间如何实现数据通信和数据同步
  6. node服务层如何封装接口
  7. 深挖底层原理 一直在拓展问

网易:谈谈同步10和异步10和它们的应用场景。https://zhuanlan.zhihu.com/p/115912936 队列解决:实现bfs返回树中值为value的节点 网易:

javascript
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+
function A() {
+  this.n = 0;
+}
+A.prototype.callMe = function () {
+  console.log(this.n);
+};
+let a = new A();
+document.addEventListener("click", a.callMe); //undefined。一个this指向window,相当于把a.callMe隐式赋给了cb。
+document.addEventListener("click", () => {
+  a.callMe(); //0
+});
+
+document.addEventListener("click", function () {
+  a.callMe(); //0
+});
+
+

你知道MySQL和MongDB区别吗

  1. · 如何防止sql注入

3 如何判断请求的资源已经全部返回。(从http角度,不要从api角度)

8 异步缓冲队列实现。例如需要发送十个请求,但最多只支持同时发送五个请求,需要等有请求完成后再进行剩余请求的发送。

2.用写websocket进度条以及使用场景

3.写回到上次浏览的位置(我说的记住位置存在sessionstorage/localstorage里),他问窗口大小变动怎么办,我说获取当前窗口大小等比例缩放scolltop。。。

6.正向代理,反向代理以及他们的应用场景

针对博客问了一下session,讲了cookie、session以及jwt,最后问如果不用cookie和localstorage,怎么做校验,比如匿名用户

你觉得你做过最牛逼的事情是什么

如果你有一天成了技术大牛,你最想做的事情是什么

介绍了下简历里各个项目的背景

react native ;node

博客技术文章怎么写的

monrepo技术栈,好处

node 中间件原理

发布订阅once加一个标识

是否能用于生产环境

父子组件执行顺序的原因

10w数据:虚拟滚动,bff(缓存,解决渲染问题)——新元

•如果你的技术方案和一起合作的同学不一致,你该怎么说服他? • 压力 •我觉得你说的这个方案不可行 • 稳定性 • 看到你家在深圳,计划长期在北京发展吗?

  • 个轮播组件的API • 案例问题 •假设我们现在面临着着会怎么排查?-个故障,部分用户反馈无法进入登录页面,你将 会怎么排查?

egg又不是不可替代的,一个bff而已,egg团队去开发artus了,Artus是更上层的框架,还是基于egg的

node里面for循环处理10亿数据,出现两个问题,js卡死,内存不够

为什么老是有题目处理101数据

我跟他讲:分开循环解决js卡死,把结果用node里面的fs.writefile写入本地文件

解決内存不够

bam就是浏览器上作为一个代理 但实际上mock数据还是在那个「API管理平台上的」

[]==![] 是true

[]== [] 是false

双等号 两边数据类型一样 如果是引用类型,就判断引用地址是否相同,两边数据类型不一样便会隐式转换。

有一点反直觉

如何在一个图片长列表页使用懒加载进行性能优化 ,然后说了图片替换和监听窗口滚动实现图片按需加载,那么如何实现监听呢?,然后扯到节流监听滚动条变化执行函数 输入框要求每次输入都要有联想功能,即随时向后台请求数据,但是频繁输入时,如何防止频繁多次请求 你对操作系统,编译原理的理解

  • 设计一个扫码登录的逻辑,为什么你扫码就可以登录你自己号,同时有这么多二维码
  • 讲清楚服务端,PC端,和手机端的逻辑
  • 怎么保存登陆态

项目权限管理(高低权限怎么区分,如果网络传输中途被人改包,怎么防止这一潜在危险) 在淘宝里面,商品数据量很大,前端怎么优化使加载速度更快、用户体验更好(severless + indexdb)  \8. 在7的条件下,已知用户当前在商品列表页,并且下一步操作是点击某个商品进入详情页,怎么加快详情页的渲染速度

npm安装和查找机制

  1. npm 会检查本地的 node_modules 目录中是否已经安装过该模块,如果已经安装,则不再重新安装
  2. npm 检查缓存中是否有相同的模块,如果有,直接从缓存中读取安装(不需要网络)
  3. 如果本地和缓存中均不存在,npm 会从 registry 指定的地址下载安装包,然后将其写入到本地的 node_modules 目录中,同时缓存起来。

npm 缓存相关命令

shell
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
# 清除缓存
+npm cache clean -f
+
+# 获取缓存位置
+npm config get cache
+
+# 设置缓存位置
+npm config set cache "新的缓存路径"
+
+ + + + + \ No newline at end of file diff --git "a/front-end-engineering/\345\220\216\345\244\204\347\220\206\345\231\250.html" "b/front-end-engineering/\345\220\216\345\244\204\347\220\206\345\231\250.html" new file mode 100644 index 00000000..3322876b --- /dev/null +++ "b/front-end-engineering/\345\220\216\345\244\204\347\220\206\345\231\250.html" @@ -0,0 +1,810 @@ + + + + + + PostCSS 后处理器 | Sunny's blog + + + + + + + + +
Skip to content
On this page

PostCSS 后处理器

后处理器的概念

前面我们学习了 CSS 预处理器,CSS 预处理器(Sass、Less、Stylus)为我们提供了一套特殊的语法,让我们可以以编程的方式(变量、嵌套、内置函数、自定义函数、流程控制)来书写 CSS 样式。因此我们在学习 CSS 预处理器的时候,主要就是学习它们的语法。

CSS 后处理器不会提供专门的语法,它是在原生的 CSS 代码的基础上面做处理,常见的处理工作如下:

  1. 兼容性处理:自动添加浏览器前缀(如 -webkit-、-moz- 和 -ms-)以确保跨浏览器兼容性。这种后处理器的一个典型例子是 autoprefixer

  2. 代码优化与压缩:移除多余的空格、注释和未使用的规则,以减小 CSS 文件的大小。例如,cssnano 是一个流行的 CSS 压缩工具。

  3. 功能增强:添加新的 CSS 特性,使开发者能够使用尚未在所有浏览器中实现的 CSS 功能。例如,PostCSS 是一个强大的 CSS 后处理器,提供了很多插件来扩展 CSS 的功能。

  4. 代码检查与规范:检查 CSS 代码的质量,以确保代码符合特定的编码规范和最佳实践。例如,stylelint 是一个强大的 CSS 检查工具,可以帮助你发现和修复潜在的问题。

后处理器实际上是有非常非常多的,autoprefixer、cssnano、stylelint 像这些工具都算是在对原生 CSS 做后处理工作。

这里就会涉及到一个问题,能够对 CSS 做后处理的工具(后处理器)非常非常多,此时就会存在我要将原生的 CSS 先放入到 A 工具进行处理,处理完成后放入到 B 工具进行处理,之后在 C、D、E、F.... 这种手动操作显然是比较费时费力的,我们期望有一种工具,能够自动化的完成这些后处理工作,这个工具就是 PostCSS。

PostCSS 是一个使用 JavaScript 编写的 CSS 后处理器,它更像是一个平台,类似于 Babel,它本身是不做什么具体的事情,它只负责一件事情,将原生 CSS 转换为 CSS 的抽象语法树(CSS AST),之后的事情就完全交给其他的插件。目前整个 PostCSS 插件生态有 200+ 的插件,每个插件可以帮助我们处理一种 CSS 后处理场景。

你可以在官网:https://www.postcss.parts/ 看到 PostCSS 里面所有的插件。

学习 PostCSS 其实主要就是学习里面常用的插件:

  • autoprefixer:自动为 CSS 中的属性添加浏览器前缀,以确保跨浏览器兼容性。
  • cssnext:使开发者能够使用尚未在所有浏览器中实现的 CSS 特性,如自定义属性(变量)、颜色函数等。
  • cssnano:优化并压缩 CSS 代码,以减小文件大小。
  • postcss-import:在一个 CSS 文件中导入其他 CSS 文件,实现 CSS 代码的模块化。
  • postcss-nested:支持 CSS 规则的嵌套,使 CSS 代码更加组织化和易于维护。
  • postcss-custom-properties:支持使用原生 CSS 变量(自定义属性)。
  • stylelintCSS 代码检查工具,旨在帮助开发者发现和修复潜在的 CSS 代码问题。

PostCSS 快速上手

首先创建一个项目目录 postcss-demo,使用 pnpm init 进行初始化,之后安装 postcss 依赖:

bash
pnpm add postcss autoprefixer -D
+
pnpm add postcss autoprefixer -D
+

接下来在 src 创建一个 index.css,书写测试的 CSS 代码:

css
body {
+  background-color: beige;
+  font-size: 16px;
+}
+
+.box1 {
+  transform: translate(100px);
+}
+
body {
+  background-color: beige;
+  font-size: 16px;
+}
+
+.box1 {
+  transform: translate(100px);
+}
+

接下来我们要对上面的代码进行后处理,创建一个 index.js,代码如下:

js
// 读取 CSS 文件
+// 使用 PostCSS 来对读取的 CSS 文件做后处理
+
+const fs = require("fs"); // 负责处理和文件读取相关的事情
+const postcss = require("postcss");
+// 引入插件,该插件负责为 CSS 代码添加浏览器前缀
+const autoprefixer = require("autoprefixer");
+
+const style = fs.readFileSync("src/index.css", "utf8");
+
+postcss([
+  autoprefixer({
+    overrideBrowserslist: "last 10 versions",
+  }),
+])
+  .process(style, { from: undefined })
+  .then((res) => {
+    console.log(res.css);
+  });
+
// 读取 CSS 文件
+// 使用 PostCSS 来对读取的 CSS 文件做后处理
+
+const fs = require("fs"); // 负责处理和文件读取相关的事情
+const postcss = require("postcss");
+// 引入插件,该插件负责为 CSS 代码添加浏览器前缀
+const autoprefixer = require("autoprefixer");
+
+const style = fs.readFileSync("src/index.css", "utf8");
+
+postcss([
+  autoprefixer({
+    overrideBrowserslist: "last 10 versions",
+  }),
+])
+  .process(style, { from: undefined })
+  .then((res) => {
+    console.log(res.css);
+  });
+

在上面的 JS 代码中,我们首先读取了 index.css 里面的 CSS 代码,然后通过 postcss 来做后处理器,注意 postcss 本身不做任何事情,它只负责将原生的 CSS 代码转为 CSS AST,具体的事情需要插件来完成。

上面的代码我们配置了 autoprefixer 这个插件,负责为 CSS 添加浏览器前缀。

postcss-cli 和配置文件

postcss-cli

cli 是一组单词的缩写(command line interface),为你提供了一组在命令行中可以操作的命令来进行处理。

postcss-cli 通过给我们提供一些命令行的命令来简化 postcss 的使用。

首先第一步还是安装:

bash
pnpm add postcss-cli -D
+
pnpm add postcss-cli -D
+

安装完成后,我们就可以通过 postcss-cli 所提供的命令来进行文件的编译操作,在 package.json 里面添加如下的脚本:

json
"scripts": {
+  	...
+    "build": "postcss src/index.css -o ./build.css"
+},
+
"scripts": {
+  	...
+    "build": "postcss src/index.css -o ./build.css"
+},
+
  • -o:表示编译后的输出文件,编译后的文件默认是带有源码映射。
  • --no-map:不需要源码映射
  • --watch:用于做文件变化的监听工作,当文件有变化的时候,会自动重新执行命令。注意如果使用了 --watch 来做源码文件变化的监听工作,那么一般建议把编辑器的自动保存功能关闭掉

关于 postcss-cli 这个命令行工具还提供了哪些命令以及哪些配置项目,可以参阅:https://www.npmjs.com/package/postcss-cli

配置文件

一般我们会把插件的配置书写到配置文件里面,在配置文件里面,我们就可以指定使用哪些插件,以及插件具体的配置选项。

要使用配置文件功能,可以在项目的根目录下面创建一个名为 postcss.config.js 的文件,当你使用 postcss-cli 或者构建工具(webpack、vite)来进行集成的时候,postcss 会自动加载配置文件。

在 postcss.config.js 文件中书写如下的配置:

js
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+

postcss 配置文件最主要的其实就是做插件的配置。postcss 官网没有提供配置文件相关的文档,但是我们可以在:https://github.com/postcss/postcss-load-config 这个地方看到 postcss 配置文件所支持的配置项目。

接下来我们来看一个 postcss 配置文件具体支持的配置项目:

  1. plugins:一个数组,里面包含要使用到的 postcss 的插件以及相关的插件配置。
js
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")({ preset: "default" })],
+};
+
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")({ preset: "default" })],
+};
+
  1. map:是否生成源码映射,对应的值为一个对象
js
module.exports = {
+  map: { inline: false },
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
module.exports = {
+  map: { inline: false },
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+

默认值为 false,因为源码映射一般是会单独存放在一个文件里面。

  1. syntax:用于指定 postcss 应该使用的 CSS 语法,默认情况下 postcss 处理的是标准的 CSS,但是有可能你的 CSS 是使用预处理器来写的,这个时候 postcss 是不认识的,所以这个时候需要安装对应的插件并且在配置中指明 syntax
js
module.exports = {
+  syntax: "postcss-scss",
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
module.exports = {
+  syntax: "postcss-scss",
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+

安装 postcss-scss 这个插件,并且在配置文件中指定 syntax 为 postcss-scss,之后 PostCSS 就能够认识你的 sass 语法。

  1. parser:配置自定义解析器。Postcss 默认的解析器为 postcss-safe-parser,负责将 CSS 字符串解析为 CSS AST,如果你要用其他的解析器,那么可以配置一下
js
const customParser = require("my-custom-parser");
+
+module.exports = {
+  parser: customParser,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
const customParser = require("my-custom-parser");
+
+module.exports = {
+  parser: customParser,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
  1. stringifier:自定义字符串化器。用于将 CSS AST 转回 CSS 字符串。如果你要使用其他的字符串化器,那么也是可以在配置文件中国呢进行指定的。
js
const customStringifier = require("my-custom-stringifier");
+
+module.exports = {
+  stringifier: customStringifier,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+
const customStringifier = require("my-custom-stringifier");
+
+module.exports = {
+  stringifier: customStringifier,
+  plugins: [
+    /* Your plugins here */
+  ],
+};
+

最后还剩下两个配置项:from、to,这两个选项官方是不建议你配置的,而且你配置的大概率还会报错,报错信息如下:

Config Error: Can not set from or to options in config file, use CLI arguments instead

这个提示的意思是让我们不要在配置文件里面进行配置,而是通过命令行参数的形式来指定。

至于为什么,官方其实解释得很清楚了:

In most cases options.from && options.to are set by the third-party which integrates this package (CLI, gulp, webpack). It's unlikely one needs to set/use options.from && options.to within a config file.

因为在实际开发中,我们更多的是会使用构建工具(webpack、vite),这些工具会去指定入口文件和出口文件。

postcss 主流插件 part1

  • autoprefixer
  • cssnano
  • stylelint

autoprefixer

这里我们再来复习一下:

css
/* 编译前 */
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+
/* 编译前 */
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+
css
/* 编译后 */
+::-webkit-input-placeholder {
+  color: gray;
+}
+
+::-moz-placeholder {
+  color: gray;
+}
+
+:-ms-input-placeholder {
+  color: gray;
+}
+
+::-ms-input-placeholder {
+  color: gray;
+}
+
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+
/* 编译后 */
+::-webkit-input-placeholder {
+  color: gray;
+}
+
+::-moz-placeholder {
+  color: gray;
+}
+
+:-ms-input-placeholder {
+  color: gray;
+}
+
+::-ms-input-placeholder {
+  color: gray;
+}
+
+::placeholder {
+  color: gray;
+}
+
+.image {
+  background-image: url(image@1x.png);
+}
+
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
+  .image {
+    background-image: url(image@2x.png);
+  }
+}
+

关于 autoprefixer 这个插件,本身没有什么好讲的,主要是要介绍 browerslist 这个知识点,这个 browerslist 主要是用来配置兼容的浏览器的范围。从第一款浏览器诞生到现在,浏览的种类以及版本是非常非常多的,因此我们在做兼容的时候,不可能把所有的浏览器都做兼容,并且也没有意义,一般是需要指定范围的。

browserslist 就包含了一些配置规则,我们可以通过这些配置规则来指定要兼容的浏览器的范围:

  • last n versions:支持最近的 n 个浏览器版本。last 2 versions 表示支持最近的两个浏览器版本
  • n% :支持全球使用率超过 n% 的浏览器。 > 1% 表示要支持全球使用率超过 1% 的浏览器
  • cover n%:覆盖 n% 的主流浏览器
  • not dead:支持所有“非死亡”的浏览器,已死亡的浏览器指的是那些已经停止更新的浏览器
  • not ie<11:排除 ie 11 以下的浏览器
  • chrome>=n :支持 chrome 浏览器大于等于 n 的版本

你可以在 https://github.com/browserslist/browserslist#full-list 看到 browserslist 可以配置的所有的值。

另外你可以在 https://browserslist.dev/?q=PiAxJQ%3D%3D 看到 browserslist 配置的值所对应的浏览器具体范围。

还有一点就是关于 browserslist 配置的值是可以有多个,如果有多条规则,可以使用关键词 or、and、not 来指定多条规则之间的关系。关于这些关键词如何组合不同的规则,可以参阅:https://github.com/browserslist/browserslist#query-composition

接下来我们来看一下如何配置 browserslist,常见的有三种方式:

  1. 在项目的根目录下面创建一个 .browerslistrc 的文件,在里面书写范围列表(这种方式是最推荐的)
js
>1%
+last 2 versions of
+not dead
+
>1%
+last 2 versions of
+not dead
+
  1. 在 package.json 里面添加一个 browserslist 字段,然后进行配置:
json
{
+  "name": "xxx",
+  "version": : "xxx",
+  ...
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}
+
{
+  "name": "xxx",
+  "version": : "xxx",
+  ...
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}
+
  1. 可以在 postcss.config.js 配置文件中进行配置
js
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+
module.exports = {
+  plugins: [
+    require("autoprefixer")({
+      overrideBrowserslist: "last 10 versions",
+    }),
+  ],
+};
+

cssnano

这是一个使用率非常高的插件,因为该插件做的事情是对 CSS 进行一个压缩

cssnano 对应的官网地址:https://cssnano.co/

使用之前第一步还是安装:

bash
pnpm add cssnano -D
+
pnpm add cssnano -D
+

之后就是在配置文件中配置这个插件:

js
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")],
+};
+
module.exports = {
+  plugins: [require("autoprefixer"), require("cssnano")],
+};
+

简单的使用方式演示完了之后,接下来延伸出来了两个问题:

  • 现在我们插件的数量从之前的一个变成多个,插件之间是否有顺序关系?
    • 在 postcss.config.js 文件里面配置插件的时候,一定要注意插件的顺序,这一点是非常重要的,因为有一些插件依赖于其他的插件的输出,你可以将 plugins 对应的数组看作是一个流水线的操作。先交给数组的第一项插件进行处理,之后将处理结果交给数组配置的第二项插件进行处理,以此类推...
  • cssnano 是否需要传入配置
    • 理论上来讲,是不需要的,因为 cssnano 默认的预设就已经非常好了,一般我们不需要做其他的配置
    • cssnano 本身又是由一些其他的插件组成的
      • postcss-discard-comments:删除 CSS 中的注释。
      • postcss-discard-duplicates:删除 CSS 中的重复规则。
      • postcss-discard-empty:删除空的规则、媒体查询和声明。
      • postcss-discard-overridden:删除被后来的相同规则覆盖的无效规则。
      • postcss-normalize-url:优化和缩短 URL。
      • postcss-minify-font-values:最小化字体属性值。
      • postcss-minify-gradients:最小化渐变表示。
      • postcss-minify-params:最小化@规则的参数。
      • postcss-minify-selectors:最小化选择器。
      • postcss-normalize-charset:确保只有一个有效的字符集 @规则。
      • postcss-normalize-display-values:规范化 display 属性值。
      • postcss-normalize-positions:规范化背景位置属性。
      • postcss-normalize-repeat-style:规范化背景重复样式。
      • postcss-normalize-string:规范化引号。
      • postcss-normalize-timing-functions:规范化时间函数。
      • postcss-normalize-unicode:规范化 unicode-range 描述符。
      • postcss-normalize-whitespace:规范化空白字符。
      • postcss-ordered-values:规范化属性值的顺序。
      • postcss-reduce-initial:将初始值替换为更短的等效值。
      • postcss-reduce-transforms:减少变换属性中的冗余值。
      • postcss-svgo:优化和压缩内联 SVG。
      • postcss-unique-selectors:删除重复的选择器。
      • postcss-zindex:重新计算 z-index 值,以减小文件大小。

因此我们可以定制具体某一个插件的行为,例如:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("autoprefixer"),
+    require("cssnano")({
+      preset: [
+        "default",
+        {
+          discardComments: false,
+          discardEmpty: false,
+        },
+      ],
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("autoprefixer"),
+    require("cssnano")({
+      preset: [
+        "default",
+        {
+          discardComments: false,
+          discardEmpty: false,
+        },
+      ],
+    }),
+  ],
+};
+

配置项目的名字可以在 https://cssnano.co/docs/what-are-optimisations/ 这里找到。

stylelint

stylelint 是规范我们 CSS 代码的,能够将 CSS 代码统一风格。

bash
pnpm add stylelint stylelint-config-standard -D
+
pnpm add stylelint stylelint-config-standard -D
+

这里我们安装了两个依赖:

  • stylelint:做 CSS 代码风格校验,但是具体的校验规则它是不知道了,需要我们提供具体的校验规则
  • stylelint-config-standard:这是 stylelint 的一套校验规则,并且是一套标准规则

接下来我们就需要在项目的根目录下面创建一个 .stylelintrc ,这个文件就使用用来指定你的具体校验规则

js
{
+    "extends": "stylelint-config-standard"
+}
+
{
+    "extends": "stylelint-config-standard"
+}
+

在上面的代码中,我们指定了校验规则继承 stylelint-config-standard 这一套校验规则

之后在 postcss.config.js 里面进行插件的配置,配置的时候注意顺序

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [require("stylelint"), require("autoprefixer"), require("cssnano")],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [require("stylelint"), require("autoprefixer"), require("cssnano")],
+};
+
  • 能否在继承了 stylelint-config-standard 这一套校验规则的基础上自定义校验规则

    • 肯定是可以的。因为不同的公司编码规范会有不同,一套标准校验规则是没有办法覆盖所有的公司编码规范
    js
    {
    +    "extends": "stylelint-config-standard",
    +    "rules": {
    +        "comment-empty-line-before": null
    +    }
    +}
    +
    {
    +    "extends": "stylelint-config-standard",
    +    "rules": {
    +        "comment-empty-line-before": null
    +    }
    +}
    +

    通过上面的方式,我们就可以自定义校验规则。

    至于有哪些校验规则,可以在 stylelint 官网查询到的:https://stylelint.io/user-guide/rules/

  • 检查出来的问题能否自动修复

    • 当然也是可以修复的,但是要注意没有办法修复所有类型的问题,只有部分问题能够被修复
    • 要自动修复非常简单,只需要将 stylelint 插件的 fix 配置项配置为 true 即可
    js
    // postcss 配置主要其实就是做插件的配置
    +
    +module.exports = {
    +  plugins: [
    +    require("stylelint")({
    +      fix: true,
    +    }),
    +    require("autoprefixer"),
    +    // require("cssnano")
    +  ],
    +};
    +
    // postcss 配置主要其实就是做插件的配置
    +
    +module.exports = {
    +  plugins: [
    +    require("stylelint")({
    +      fix: true,
    +    }),
    +    require("autoprefixer"),
    +    // require("cssnano")
    +  ],
    +};
    +

postcss 主流插件 part2

这一小节继续介绍 postcss 里面的主流插件:

  • postcss-preset-env
  • postcss-import
  • purgecss

postcss-preset-env

postcss-preset-env 主要就是让开发者可以使用最新的的 CSS 语法,同时为了兼容会自动的将这些最新的 CSS 语法转换为旧版本浏览器能够支持的代码。

postcss-preset-env 的主要作用是:

  1. 让你能够使用最新的 CSS 语法,如:CSS Grid(网格布局)、CSS Variables(变量)等。
  2. 自动为你的 CSS 代码添加浏览器厂商前缀,如:-webkit-、-moz- 等。
  3. 根据你的浏览器兼容性需求,将 CSS 代码转换为旧版浏览器兼容的语法。
  4. 优化 CSS 代码,如:合并规则、删除重复的代码等。

在正式演示 postcss-preset-env 之前,首先我们需要了解一个知识点,叫做 CSSDB,这是一个跟踪 CSS 新功能和特性的数据库。我们的 CSS 规范一共可以分为 5 个阶段:从 stage0(草案) 到 stage4(已经纳入 W3C 标准)。CSSDB 就可以非常方便的查询某一个特性目前处于哪一个阶段,具体的实现情况目前是什么样的。

下面是关于 stage0 到 stage4 各个阶段的介绍:

  • Stage 0:草案 - 此阶段的规范还在非正式的讨论和探讨阶段,可能会有很多变化。通常不建议在生产环境中使用这些特性。
  • Stage 1:提案 - 此阶段的规范已经有了一个正式的文件,描述了新特性的初步设计。这些特性可能在未来变成标准,但仍然可能发生较大的改变。
  • Stage 2:草稿 - 在这个阶段,规范已经相对稳定,描述了功能的详细设计。一般来说,浏览器厂商会开始实现并测试这些特性。开发者可以在实验性的项目中尝试使用这些功能,但要注意跟踪规范的变化。
  • Stage 3:候选推荐 - 此阶段的规范已经基本稳定,主要进行浏览器兼容性测试和微调。开发者可以考虑在生产环境中使用这些特性,但需要确保兼容目标浏览器。
  • Stage 4:已纳入 W3C 标准 - 这些特性已经成为 W3C CSS 标准的一部分,已经得到了广泛支持。开发者可以放心在生产环境中使用这些特性。

你可以在 https://cssdb.org/ 这里看到各种新特性目前处于哪一个阶段。

假设现在我们书写如下的 CSS 代码:

css
.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+

这个 CSS 代码使用到了嵌套,这是原本只能在 CSS 预处理器里面才能使用的语法,目前官方已经在考虑原生支持了。

但是现在会涉及到一个问题,如果我们目前直接书写嵌套的语法,那么很多浏览器是不支持的,但是我又想使用最新的语法,我们就可以使用 postcss-preset-env 这个插件对最新的 CSS 语法进行降级处理。

首先第一步需要安装:

bash
pnpm add postcss-preset-env -D
+
pnpm add postcss-preset-env -D
+

该插件提供了一个配置选项:

  • stage:设置要使用的 CSS 特性的阶段,默认值为 20-4)。数字越小,包含的 CSS 草案特性越多,但稳定性可能较低。
  • browsers:设置目标浏览器范围,如:'last 2 versions' 或 '> 1%'。
  • autoprefixer:设置自动添加浏览器厂商前缀的配置,如:{ grid: true }。
  • preserve:是否保留原始 CSS 代码,默认为 false。如果设置为 true,则会在转换后的代码后面保留原始代码,以便新浏览器优先使用新语法。

在 postcss.config.js 配置文件中,配置该插件:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+

之后编译生成的 CSS 代码如下:

css
.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+
.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+

通过这个插件,我们就可以使用 CSS 规范中最时髦的语法,而不用担心浏览器的兼容问题。

postcss-import

该插件主要用于处理 CSS 文件中 @import 规则。在原生的 CSS 中,存在 @import,可以引入其他的 CSS 文件,但是在引入的时候会存在一个问题,就是客户端在解析 CSS 文件时,发现有 @import 就会发送 HTTP 请求去获取对应的 CSS 文件。

使用 postcss-import:

  • 将多个 CSS 文件合并为一个文件
  • 避免了浏览器对 @import 规则的额外请求,因为减少了 HTTP 请求,所以提高了性能
bash
pnpm add postcss-import -D
+
pnpm add postcss-import -D
+

安装完成后,在配置文件中进行一个简单的配置:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import"),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import"),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+  ],
+};
+

最终编译后的 CSS 结果如下:

css
.box1 {
+  background-color: red;
+}
+.box2 {
+  background-color: blue;
+}
+.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+
.box1 {
+  background-color: red;
+}
+.box2 {
+  background-color: blue;
+}
+.a {
+  color: red;
+}
+.a.b {
+  color: green;
+  -webkit-transform: translate(100px);
+  -ms-transform: translate(100px);
+  transform: translate(100px);
+}
+.a > .b {
+  color: blue;
+}
+.a:hover {
+  color: #000;
+}
+

另外该插件也提供了一些配置项:

  • path:设置查找 CSS 文件的路径,默认为当前文件夹。
  • plugins:允许你指定在处理被 @import 引入的 CSS 文件时使用的其他 PostCSS 插件。这些插件将在 postcss-import 合并文件之前对被引入的文件进行处理,之后再进行文件的合并

例如:

js
module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+      plugins: [postcssNested()],
+    }),
+    // 其他插件...
+  ],
+};
+
module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+      plugins: [postcssNested()],
+    }),
+    // 其他插件...
+  ],
+};
+

在上面的配置中,插件会在 src/css 目录下面去查找被引入的文件,另外文件在被合并到 index.css 之前,会被 postcssNested 这个插件先处理一遍,然后才会被合并到 index.css 里面。

你可以在 https://github.com/postcss/postcss-import 这里看到 postcss-import 所支持的所有配置项。

purgecss

该插件专门用于移除没有使用到的 CSS 样式的工具,相当于是 CSS 版本的 tree shaking(树摇),它会找到你文件中实际使用的 CSS 类名,并且移除没有使用到的样式,这样可以有效的减少 CSS 文件的大小,提升传输速度。

官网地址:https://purgecss.com/

首先我们还是安装该插件:

bash
pnpm add @fullhuman/postcss-purgecss -D
+
pnpm add @fullhuman/postcss-purgecss -D
+

接下来我们在 src 下面创建一个 index.html,书写如下的代码:

html
<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Document</title>
+  <link rel="stylesheet" href="./index.css" />
+</head>
+<body>
+  <div class="container">
+    <div class="box1"></div>
+  </div>
+</body>
+
<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Document</title>
+  <link rel="stylesheet" href="./index.css" />
+</head>
+<body>
+  <div class="container">
+    <div class="box1"></div>
+  </div>
+</body>
+

该 html 只用到了少量的 CSS 样式类。

目前我们的 index.css 代码如下:

css
@import "a.css";
+@import "b.css";
+
+.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
+.container {
+  font-size: 20px;
+}
+
+p {
+  color: red;
+}
+
@import "a.css";
+@import "b.css";
+
+.a {
+  color: red;
+
+  &.b {
+    color: green;
+    transform: translate(100px);
+  }
+
+  & > .b {
+    color: blue;
+  }
+
+  &:hover {
+    color: #000;
+  }
+}
+
+.container {
+  font-size: 20px;
+}
+
+p {
+  color: red;
+}
+

接下来我在 postcss.config.js 配置文件中引入 @fullhuman/postcss-purgecss 这个插件,具体的配置如下:

js
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+    }),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+    require("@fullhuman/postcss-purgecss")({
+      content: ["./src/**/*.html"],
+    }),
+  ],
+};
+
// postcss 配置主要其实就是做插件的配置
+
+module.exports = {
+  plugins: [
+    require("postcss-import")({
+      path: ["src/css"],
+    }),
+    require("postcss-preset-env")({
+      stage: 2,
+    }),
+    require("@fullhuman/postcss-purgecss")({
+      content: ["./src/**/*.html"],
+    }),
+  ],
+};
+

我们引入 @fullhuman/postcss-purgecss 这个插件后,还做了 content 这个配置项目的相关配置,该配置项表示我具体的参照文件。也就是说,CSS 样式类有没有用上需要有一个具体的参照文件。

最终编译出来的结果如下:

css
.box1 {
+  background-color: red;
+}
+.container {
+  font-size: 20px;
+}
+
.box1 {
+  background-color: red;
+}
+.container {
+  font-size: 20px;
+}
+

@fullhuman/postcss-purgecss 这个插件除了 content 这个配置项,还有一个配置项也非常的常用:

  • safelist:可以指定一个字符串的值,或者指定一个正则表达式,该配置项目所对应的值(CSS 样式规则)始终保留,即便在参照文件中没有使用到也需要保留
js
const purgecss = require("@fullhuman/postcss-purgecss");
+
+module.exports = {
+  plugins: [
+    // 其他插件...
+    purgecss({
+      content: ["./src/**/*.html", "./src/**/*.js"],
+      safelist: [/^active-/],
+    }),
+  ],
+};
+
const purgecss = require("@fullhuman/postcss-purgecss");
+
+module.exports = {
+  plugins: [
+    // 其他插件...
+    purgecss({
+      content: ["./src/**/*.html", "./src/**/*.js"],
+      safelist: [/^active-/],
+    }),
+  ],
+};
+

safelist 所对应的值的含义为:匹配 active- 开头的类名,这些类名即便在项目文件中没有使用到,但是也不要删除。

抽象语法树

自定义插件第一小节我们来看一下抽象语法树。

官方对 Postcss 的原理介绍如下:

PostCSS takes a CSS file and provides an API to analyze and modify its rules (by transforming them into an Abstract Syntax Tree). This API can then be used by plugins to do a lot of useful things, e.g., to find errors automatically, or to insert vendor prefixes.

Postcss 的工作流程如下图所示:

image-20230626193847754

关于抽象语法树这个概念其实是非常重要的,你在很多地方都能看到它。

当我们遇到一个难以理解的术语的时候,有一个最简单的方式就是“拆词”。“抽象语法树”经过拆词就可以拆解为三个词:

  • 抽象
  • 语法

我们首先来看树。“树”实际上是一种数据结构。

所谓数据结构,就是指数据在计算机中组织和存储的一种方式。数据结构通常会分为两类:

  • 线性数据结构
    • 数组(Array):一种连续存储空间中的固定大小的数据项集合。数组将相同类型的元素存储在连续的内存位置中,允许通过索引快速访问元素。
    • 链表(Linked List):一种由节点组成的线性集合,每个节点包含数据和指向下一个节点的指针。链表允许在不重新分配整个数据结构的情况下插入和删除元素。
    • 栈(Stack):一种遵循后进先出(LIFO,Last In First Out)原则的线性数据结构。在栈中,数据项的添加和移除都在同一端进行,称为栈顶。
    • 队列(Queue):一种遵循先进先出(FIFO,First In First Out)原则的线性数据结构。在队列中,数据项的添加在一端进行(队尾),移除在另一端进行(队头)。
  • 非线性数据结构
    • 树(Tree):一种分层结构,由节点组成,其中有一个特殊的节点称为根节点,其余节点按照层级组织。每个节点(除根节点外)都有一个父节点,可以有多个子节点。常见的树结构有二叉树、红黑树、AVL 树等。
    • 图(Graph):一种由顶点(节点)和边组成的数据结构,边连接了顶点。图可以是有向的(边有方向)或无向的(边无方向)。图可用于表示具有复杂关系的数据集合。

接下来我们聚焦到“树”这种数据结构,树这种非线性的数据结构,在解决某些问题的时候有一些显著的特点:

  • 层次关系:通过树结构能够非常自然的表示出数据之间的层次关系,这是其他数据结构办不到的。
  • 搜索效率:通过树的结构(平衡二叉树),在执行搜索、插入以及删除等操作时,效率是比较高的,时间复杂度通常为 O(log n),n是树的节点数量。一般比线性的数据结构(数组、链表)要高很多。
  • 动态数据集合:与数组等固定大小的数据结构相比,树结构可以方便地添加、删除和重新组织节点。这使得树结构非常适合用于动态变化的数据集合。
  • 有序存储:在二叉搜索树等有序树结构中,数据按照一定的顺序进行组织。这允许我们在 O(log n) 时间内完成有序数据集合的操作,如查找最大值、最小值和前驱、后继等。
  • 空间优化:在某些应用场景中,树结构可以有效地节省空间。例如,字典树(Trie)可以用于存储大量字符串,同时节省空间,因为公共前缀只存储一次。
  • 分治策略:树结构天然地适应分治策略,可以将复杂问题分解为较小的子问题并递归求解。许多高效的算法都基于树结构,如排序算法(归并排序、快速排序)、图算法(最小生成树、最短路径等)。

上面的这些优点,如果你没有系统的学习过数据结构相关的知识,你是没有办法很多的进行理解。但是这个并不影响我们学习抽象语法树。上面所罗列的这些特点只是为了说明一点:树这种数据结构是存在很多优点的,所以我们能够在很多地方看到树的身影,例如:DOM树、CSSOM树、语法树。

接下来我们重点来说语法树。什么是语法树?简单来讲,就是将我们所书写的源代码转为树的结构。

js
var a = 42;
+var b = 5;
+function addA(d) {
+    return a + d;
+}
+var c = addA(2) + b;
+
var a = 42;
+var b = 5;
+function addA(d) {
+    return a + d;
+}
+var c = addA(2) + b;
+

对于编译器或者解释器来讲,上面的代码它们并不能够理解。上面的这些代码对于编译器或者解释器来讲,无非就是一段字符串而已:

js
'var a = 42;var b = 5;function addA(d) {return a + d;}var c = addA(2) + b;'
+
'var a = 42;var b = 5;function addA(d) {return a + d;}var c = addA(2) + b;'
+

因此要执行这个代码,编译器或解释器首先第一步就是要分析出来这个字符串里面哪些是关键字,哪些是标志符,哪些是运算符。之后会形成一个一个的 token,例如上面的代码,最终就会形成各种各样的 token(不可再拆分、最小的单位):

js
Keyword(var) Identifier(a) Punctuator(=) Numeric(42) Punctuator(;) Keyword(var) 
+Identifier(b) Punctuator(=) Numeric(5) Punctuator(;) Keyword(function) 
+Identifier(addA) Punctuator(() Identifier(d) Punctuator()) Punctuator({) 
+Keyword(return) Identifier(a) Punctuator(+) Identifier(d) Punctuator(;) 
+Punctuator(}) Keyword(var) Identifier(c) Punctuator(=) Identifier(addA) 
+Punctuator(() Numeric(2) Punctuator()) Punctuator(+) Identifier(b) Punctuator(;)
+
Keyword(var) Identifier(a) Punctuator(=) Numeric(42) Punctuator(;) Keyword(var) 
+Identifier(b) Punctuator(=) Numeric(5) Punctuator(;) Keyword(function) 
+Identifier(addA) Punctuator(() Identifier(d) Punctuator()) Punctuator({) 
+Keyword(return) Identifier(a) Punctuator(+) Identifier(d) Punctuator(;) 
+Punctuator(}) Keyword(var) Identifier(c) Punctuator(=) Identifier(addA) 
+Punctuator(() Numeric(2) Punctuator()) Punctuator(+) Identifier(b) Punctuator(;)
+

拆解成一个一个的 token 之后,会将这些 token 以树的形式来存储,最终会形成如下的一个树结构:

image-20230626200342918

https://www.jointjs.com/demos/abstract-syntax-tree 这个网站可以看到 JS 代码所形成的抽象语法树长什么样子。

至此,你就知道什么叫做语法树。

最后我们还需要解释一下什么叫做“抽象语法树”。

抽象(abstraction)是一种思维方式。所谓抽象,指的是从具体的事物里面提取出本质特征、规律,忽略不相关、不重要的细节。在计算机科学和编程里面,抽象是一种非常重要的方法,因为抽象能够将一个复杂的问题抽离成简单的、更加容易理解的问题

抽象语法树是将源代码抽象成一种更高阶别的表示方式,只关注代码的结构和语法,会去忽略空格、换行、制表符之类的表达细节。

最后说一下抽象语法树的优点:

  1. 易于操作和遍历:可以更方便地进行操作和遍历。AST 中的每个节点都有确定的类型和结构,这使得插件作者可以轻松地定位和修改特定类型的节点,而无需解析和操作原始 CSS 文本。

  2. 易于扩展:使用 AST 可以轻松地支持新的 CSS 语法和特性。只需在 AST 中添加相应的节点类型和规则,就可以在插件中处理新的语法结构,而无需对整个解析器进行重大改动。

  3. 提高性能:将 CSS 代码转换为 AST 后,可以对整个树进行一次遍历,同时应用多个插件的变换操作。这样可以减少重复解析和操作 CSS 文本的开销,从而提高处理性能。

  4. 代码重用和模块化:由于 AST 的结构化特性,插件开发者可以在多个插件之间重用和共享操作 AST 的代码。这有助于降低插件间的冗余,并提高代码的模块化程度。

  5. 易于调试和错误处理:AST 中的每个节点都包含有关其源代码位置的元信息。这使得插件可以在出现错误时提供更具体的错误信息和上下文,从而帮助开发者快速定位和解决问题。

著名的 babel 项目在处理 JS 的时候就是会先将 JS 转为抽象语法树,然后再交给其他的插件做处理。ESlint 工具检查代码是否规范,那它怎么检查的?它其实也是先将代码转为抽象语法树,然后再去检查。我们这里所学习的 postcss 也是同样的原理,只不过它是将 css 代码转为对应的 css 抽象语法树。

自定义插件

在 PostCSS 官网,实际上已经介绍了如何去编写一个自定义插件:https://postcss.org/docs/writing-a-postcss-plugin

  1. 需要有一个模板
js
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: 'PLUGIN NAME'
+    // Plugin listeners
+  }
+}
+module.exports.postcss = true
+
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: 'PLUGIN NAME'
+    // Plugin listeners
+  }
+}
+module.exports.postcss = true
+

接下来就可以在插件里面添加一组监听器,对应的能够设置的监听器如下:

  • Root: node of the top of the tree, which represent CSS file.
  • AtRule: statements begin with @ like @charset "UTF-8" or @media (screen) {}.
  • Rule: selector with declaration inside. For instance input, button {}.
  • Declaration: key-value pair like color: black;
  • Comment: stand-alone comment. Comments inside selectors, at-rule parameters and values are stored in node’s raws property.
  1. 具体示例

现在在我们的 src 中新建一个 my-plugin.js 的文件,代码如下:

js
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "PLUGIN NAME",
+    Declaration(decl){
+        console.log(decl.prop, decl.value)
+    }
+  };
+};
+module.exports.postcss = true;
+
module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "PLUGIN NAME",
+    Declaration(decl){
+        console.log(decl.prop, decl.value)
+    }
+  };
+};
+module.exports.postcss = true;
+

在上面的代码中,我们添加了 Declaration 的监听器,通过该监听器能够拿到 CSS 文件中所有的声明。

接下来我们就可以对其进行相应的操作。

现在我们来做一个具体的示例:编写一个插件,该插件能够将 CSS 代码中所有的颜色统一转为十六进制。

这里我们需要使用到一个依赖包:color 该依赖就是专门做颜色处理的

bash
pnpm add color -D
+
pnpm add color -D
+

之后通过该依赖所提供的 hex 方法来进行颜色值的修改,具体代码如下:

js
const Color = require("color");
+
+module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "convertColorsToHex",
+    Declaration(decl) {
+      // 先创建一个正则表达式,提取出如下的声明
+      // 因为如下的声明对应的值一般都是颜色值
+      const colorRegex = /(^color)|(^background(-color)?)/;
+      if (colorRegex.test(decl.prop)) {
+        try {
+          // 将颜色值转为 Color 对象,因为这个 Color 对象对应了一系列的方法
+          // 方便我们进行转换
+          const color = Color(decl.value);
+          // 将颜色值转换为十六进制
+          const hex = color.hex();
+          // 更新属性值
+          decl.value = hex;
+        } catch (err) {
+          console.error(
+            `[convertColorsToHex] Error processing ${decl.prop}: ${error.message}`
+          );
+        }
+      }
+    },
+  };
+};
+module.exports.postcss = true;
+
const Color = require("color");
+
+module.exports = (opts = {}) => {
+  // Plugin creator to check options or prepare caches
+  return {
+    postcssPlugin: "convertColorsToHex",
+    Declaration(decl) {
+      // 先创建一个正则表达式,提取出如下的声明
+      // 因为如下的声明对应的值一般都是颜色值
+      const colorRegex = /(^color)|(^background(-color)?)/;
+      if (colorRegex.test(decl.prop)) {
+        try {
+          // 将颜色值转为 Color 对象,因为这个 Color 对象对应了一系列的方法
+          // 方便我们进行转换
+          const color = Color(decl.value);
+          // 将颜色值转换为十六进制
+          const hex = color.hex();
+          // 更新属性值
+          decl.value = hex;
+        } catch (err) {
+          console.error(
+            `[convertColorsToHex] Error processing ${decl.prop}: ${error.message}`
+          );
+        }
+      }
+    },
+  };
+};
+module.exports.postcss = true;
+
+ + + + + \ No newline at end of file diff --git a/getting-started.html b/getting-started.html new file mode 100644 index 00000000..e544eb80 --- /dev/null +++ b/getting-started.html @@ -0,0 +1,20 @@ + + + + + + My Projects | Sunny's blog + + + + + + + + +
Skip to content
On this page

My Projects

TODO:分类

js-challenges

https://github.com/Sunny-117/js-challenges

✨✨✨ Challenge your JavaScript programming limits step by step

mini-anythings

https://github.com/Sunny-117/mini-anything

🚀 Explore the source code of the front-end library and implement a super mini version

BOSScript

https://github.com/Sunny-117/BOSScript

Boss's direct recruitment and delivery, shutdown, one-stop service of the oil monkey script, allowing you to submit resumes overseas in just 2 minutes

rc-design

https://github.com/Sunny-117/rc-design

🗃️ rc-design is a component library developed for react, providing developers with a more lightweight and concise component library choice. Use tsx to write logic, less to write styles, dumi2 to write documentation sites, and jest+ts-jest+react-testing-library for unit testing.

cherry

https://github.com/Sunny-117/cherry

✨ A lightweight JavaScript packaging library based on magic-string and acorn, supporting tree-shaking

rollup-plugin-alias

https://github.com/Sunny-117/rollup-plugin-alias

🍣 A Rollup plugin for defining aliases when bundling packages.

commencer

https://github.com/Sunny-117/commencer

Starter template for xxx

tiny-react

https://github.com/Sunny-117/tiny-react

🌱 The closest implementation to the React source code

treejs

https://github.com/Sunny-117/treejs

🌱 Easy to learn, high-performance, and highly scalable Tree components, supporting Vuejs and React simultaneously

lodash-ts

https://github.com/Sunny-117/lodash-ts

tiny-vue

https://github.com/Sunny-117/tiny-vue

Native-project

https://github.com/Sunny-117/Native-project

Native JavaScript project collection, Github China latest version

shooks

https://github.com/Sunny-117/shooks

📦️ A high-quality & reliable React Hooks library.

mini-webpack

https://github.com/Sunny-117/mini-webpack

手写一个简易版的webpack

ts-lib-vite

https://github.com/Sunny-117/ts-lib-vite

A foundation for developing front-end utility libraries using Vite and TypeScript.

esbuild-plugins

https://github.com/Sunny-117/esbuild-plugins

packages of esbuild plugins

tiny-vite

https://github.com/Sunny-117/tiny-vite

⚡️ a lightweight frontend build tool designed to deliver swift development experiences and efficient build processes

vite-plugins

https://github.com/Sunny-117/vite-plugins

tiny-complier

实现超级 mini 的编译器 | codegen&compiler 生成代码 | 只需要 200 行代码 | 前端编译原理

https://github.com/Sunny-117/tiny-complier

keep-everyday

https://github.com/Sunny-117/keep-everyday

使用 Github Actions 来完成自动创建 issues 任务

text-image

https://github.com/Sunny-117/text-image

🐛🐛🐛 text-image 可以将文字、图片、视频进行「文本化」,只需要通过简单的配置即可使用

jsx-compilation

https://github.com/Sunny-117/jsx-compilation

🍻 实现 JSX 语法转成 JS 语法的编译器

awesome-native

https://github.com/Sunny-117/awesome-native

🔧 Collection of native JavaScript projects

vsc-delete-func

https://github.com/Sunny-117/vsc-delete-func

🍻🍻🍻 vscode plugins

eslint-plugin-reviewget

https://github.com/Sunny-117/eslint-plugin-reviewget

🚀当用户使用 getXXX get开头的函数的时候 如果不返回值的话 那么就会报错 🐛可以 fix 🎉用户可以自行配置是否 fix

babel-plugin-dev-debug

https://github.com/Sunny-117/babel-plugin-dev-debug

an babel plugin that for dev debug

webpack-expand-lib

https://github.com/Sunny-117/webpack-expand-lib

🚀 some expansion libs of webpack

network-speed-js

https://github.com/Sunny-117/network-speed-js

A small tool for testing network speed. It also has the ability to test internal and external networks.

TODO ...

+ + + + + \ No newline at end of file diff --git a/hashmap.json b/hashmap.json new file mode 100644 index 00000000..6e502cba --- /dev/null +++ b/hashmap.json @@ -0,0 +1 @@ +{"fragment_monorepo.md":"3da7c407","fragment_npm-scripts.md":"051d45f0","fe-utils_tool.md":"1bfe9ae2","fragment_api-no-repeat.md":"9e7ac149","fragment_tree-shaking.md":"8743095c","fragment_foreach.md":"6ec35e76","article_cms.md":"013c968f","fragment_promise-cancel.md":"6a4fe1e8","fragment_const.md":"f419959d","fe-utils_git.md":"6038ff7a","fragment_fetch-pause.md":"95306d7b","algorithm_🔥刷题之探索最优解.md":"da461927","fragment_disable-debugger.md":"5f8672f6","fragment_react-duplicate.md":"37494912","fragment_return-await.md":"21a919eb","fragment_前沿技术.md":"c7fe3df8","fragment_userequest.md":"2d50c7c0","fragment_fetch.md":"b657f3c7","fragment_settimeout.md":"dadee543","fe-utils_js工具库.md":"361d6082","fragment_nexttick.md":"f9431411","fragment_react-usestate.md":"cf32534b","fragment_var-array.md":"e64e0b6d","fragment_接口设计.md":"49cc865f","fragment_react-hooks-timer.md":"7fb4e725","fragment_黑白.md":"e8f52bb8","fragment_微内核架构.md":"3d9cf8a5","fragment_babel-console.md":"920be39c","fragment_auto-try-catch.md":"80aa30a2","fragment_video.md":"a2bb9154","fragment_沙盒.md":"4cce94e9","front-end-engineering_packagemanager.md":"d80a1c87","front-end-engineering_node.md":"78e6017c","front-end-engineering_jscompatibility.md":"73ef711a","front-end-engineering_modularization.md":"14ea7e8d","front-end-engineering_pnpm原理.md":"ad6b470a","front-end-engineering_css工程化.md":"6acce3ef","front-end-engineering_engineering-onepage.md":"c6fbbc99","front-end-engineering_webpack5-mf.md":"5dfd34aa","front-end-engineering_webpack常用拓展.md":"5a4ef0f9","front-end-engineering_theme.md":"dffc6011","front-end-engineering_css 预处理器之scss.md":"8d9a6c7d","getting-started.md":"fd6d908e","front-end-engineering_performance.md":"c74ee54e","front-end-engineering_【完结】前端工程化.md":"0256b7ba","front-end-engineering_后处理器.md":"b789bac1","html-css_html.md":"c1393b52","html-css_flex.md":"1ad45d13","html-css_drag.md":"e7a4f3a8","html-css_animation.md":"da2d65b2","html-css_principle.md":"d637fc73","index.md":"16278743","html-css_temop.md":"bacccf09","html-css_canvas-svg.md":"38b21f10","interview_面试官:你还有问题要问我吗.md":"d3badbc7","interview_算法笔试.md":"7bf3353f","html-css_css.md":"53af0be0","html-css_selector.md":"4178cabb","js_代理与反射.md":"e34f5583","html-css_interview.md":"c1ea6a41","react_fiber.md":"3f8bb345","react_component-communication.md":"e5f7bfba","react_redux.md":"6c7ced8c","react_dva.md":"805093c2","react_event.md":"a7d2e7ef","react_context.md":"005a5cd9","react_reactrouter.md":"b4ae71c0","react_hooks.md":"17fd5126","js_迭代器和生成器.md":"d06869c0","react_lifecycle.md":"3ebedebc","react_react-redux-router.md":"a2de93f2","react_umi.md":"0d2b5a11","react_utils.md":"e16e8b31","react_transition.md":"9ddb83f5","js_异步处理.md":"fcb17763","vue_ssr.md":"1f9a7105","react_render.md":"215c0afa","react_index.md":"db4270e9","vue_computed.md":"b9359f35","vue_slot.md":"abd3ee95","vue_v-model.md":"3760a411","vue_interviewer.md":"f871c6cd","vue_vue-cli.md":"7c91d109","vue_nexttick.md":"6004e53e","vue_directive.md":"e54758a7","vue_keep-alive-lru.md":"18e2348a","vue_lifecycle.md":"8db47172","vue_component-communication.md":"05aef57e","vue_vs.md":"03ab930a","vue_diff.md":"97272472","vue_vue-compile.md":"a2ee6a41","vue_vdom.md":"6239519b","vue_challages.md":"4d86ba94","react_react-interview.md":"c653edc1","vue_vue-router.md":"dc40da6f","vue_reactive.md":"e5f72eae","vue_vuex.md":"afa7cd7e","vue_vue-interview.md":"139da849","vue_vue3-onepage.md":"3fc4dc1a","ts_typescript-onepage.md":"201f3eb3"} diff --git a/html-css/CSS.html b/html-css/CSS.html new file mode 100644 index 00000000..18870d99 --- /dev/null +++ b/html-css/CSS.html @@ -0,0 +1,3054 @@ + + + + + + CSS3 | Sunny's blog + + + + + + + + +
Skip to content
On this page

CSS3

introduction

兼容性前缀

prefix(前缀)browser
-webkitchrome/safari
-mozfirefox
-msIE
-oopera

1.历史

更新迭代,兼容性 ---- 加不加前缀

css
div {
+  border-radius: ;
+  -webkit-border-radius: ;
+  -o-border-radius: ;
+  -moz-border-radius: ;
+}
+
div {
+  border-radius: ;
+  -webkit-border-radius: ;
+  -o-border-radius: ;
+  -moz-border-radius: ;
+}
+

兼容性手册网站

http://css.doyoe.com

http://caniuse.com

2.处理器

预处理器:pre-processor

less/sass cssNext 插件

利用 sass 工具编辑(遵循人家的语法):减少人工时间

sass 演示

css
div {
+  span {
+  }
+}
+
div {
+  span {
+  }
+}
+
css
$font-stack: arial, ...;
+#mysituation-color: #444;
+div {
+  span {
+    color: #mysituation-color;
+  }
+  p {
+    font: 100% $font-stack;
+  }
+}
+#only {
+  &.demo {
+    color: $mysituation-color;
+  }
+}
+
$font-stack: arial, ...;
+#mysituation-color: #444;
+div {
+  span {
+    color: #mysituation-color;
+  }
+  p {
+    font: 100% $font-stack;
+  }
+}
+#only {
+  &.demo {
+    color: $mysituation-color;
+  }
+}
+

后处理器:post-processor

CSS 自动补足前缀插件(基于 caniuse 网站)autoprefixer后处理器需要在其环境内编写

优点:如果有一天,属性可以再各大浏览器应用,不需要加前缀,那么我们写的代码本身就符合规范了。可维护性好。而 sass less 不能。

3.怎么用

postCss + 插件(充分体现扩展性,200 多个)

postCss 并不能划分成什么处理器,要加上插件才能变成相应的处理器

用 js 实现 css 抽象的语法树,AST(abstract Syntax Tree),剩下的事情留给后人来做

后处理器好处:

如果浏览器都实现兼容了,用不到兼容了,就可以不用后处理器了,利于维护代码,升级。预处理器办不到。

4.CSS3 进化到编程化:cssNext

css
:root {
+  --headline-color: #333;
+}
+@custom-selector: --headline h1,h2,h3,h4,h5,h6
+    : --headline {
+  color: var(--headline-color);
+}
+
:root {
+  --headline-color: #333;
+}
+@custom-selector: --headline h1,h2,h3,h4,h5,h6
+    : --headline {
+  color: var(--headline-color);
+}
+

cssNext 用来实现一些未来的标准的(未完全在各大浏览器)

border

1.border

css
.demo1 {
+  width: 100px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-radius: 50px; /*圆角:相对于宽而言的*/
+}
+
.demo1 {
+  width: 100px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-radius: 50px; /*圆角:相对于宽而言的*/
+}
+

border-radius拆分

css
border-radius: 10px 20px 30px 40px;
+/*左上-右上-右下-左下*/
+border-radius: 10px 40px;
+/*左上右下--右上左下*/
+border-radius: 10px 20px 30px;
+/*中间代表两个方向:右上左下*/
+
border-radius: 10px 20px 30px 40px;
+/*左上-右上-右下-左下*/
+border-radius: 10px 40px;
+/*左上右下--右上左下*/
+border-radius: 10px 20px 30px;
+/*中间代表两个方向:右上左下*/
+

继续拆分

css
border-top-left-radius: 10px;
+border-top-right-radius: 20px;
+border-bottom-right-radius: 30px;
+border-bottom-left-radius: 40px;
+/*等价*/
+border-top-left-radius: 10px 10px;
+border-top-right-radius: 20px 20px;
+border-bottom-right-radius: 30px 30px;
+border-bottom-left-radius: 40px 40px;
+
border-top-left-radius: 10px;
+border-top-right-radius: 20px;
+border-bottom-right-radius: 30px;
+border-bottom-left-radius: 40px;
+/*等价*/
+border-top-left-radius: 10px 10px;
+border-top-right-radius: 20px 20px;
+border-bottom-right-radius: 30px 30px;
+border-bottom-left-radius: 40px 40px;
+

半径小于长方形的长度

1/4 圆

css
/*必须正方形的width=border-top-left-radius:100px 100px*/
+
/*必须正方形的width=border-top-left-radius:100px 100px*/
+

半圆

css
/*长方形宽大一倍*/
+.demo2 {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-top-left-radius: 100px 100px;
+  border-top-right-radius: 100px 100px;
+}
+
/*长方形宽大一倍*/
+.demo2 {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  border-top-left-radius: 100px 100px;
+  border-top-right-radius: 100px 100px;
+}
+

叶子模型

css
.demo {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  background-color: green;
+  border-top-left-radius: 100px 100px;
+  border-bottom-right-radius: 100px 100px;
+}
+
.demo {
+  width: 200px;
+  height: 100px;
+  border: 1px solid #000;
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  background-color: green;
+  border-top-left-radius: 100px 100px;
+  border-bottom-right-radius: 100px 100px;
+}
+

新写法

css
border-radius: 10px 20px 30px 40px / 10px 20px 30px 40px;
+
border-radius: 10px 20px 30px 40px / 10px 20px 30px 40px;
+

2.box-shallow

外阴影&&内阴影

css
body{
+  background-color: #000;
+}
+div{
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: transparent;
+  border: 1px solid #fff;
+
+  不写就是外阴影
+  box-shadow: 0px 0px 0px 10px #0ff;/*水平偏移量 垂直偏移量 模糊范围(基于原来边框位置向边框两边同时模糊) 传播距离(水平垂直同时增加10) 阴影颜色 */
+
+  内阴影
+  /*box-shadow: inset 1px 0px 5px 0px #fff;*/
+
+  内外阴影
+  /*box-shadow: 0px 0px 10px #fff,inset 0px 0px 10px #fff;*/
+
+
+  /*四边不同颜色模糊实现*/
+  box-shadow: inset 0px 0px 10px #fff,
+    3px 0px 10px #f0f,
+    0px -3px 10px #0ff,
+    -3px 0px 10px #00f,
+    0px 3px 10px #ff0;
+  /*关于Z轴覆盖,哪个阴影先设置,谁就在上*/
+}
+
body{
+  background-color: #000;
+}
+div{
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: transparent;
+  border: 1px solid #fff;
+
+  不写就是外阴影
+  box-shadow: 0px 0px 0px 10px #0ff;/*水平偏移量 垂直偏移量 模糊范围(基于原来边框位置向边框两边同时模糊) 传播距离(水平垂直同时增加10) 阴影颜色 */
+
+  内阴影
+  /*box-shadow: inset 1px 0px 5px 0px #fff;*/
+
+  内外阴影
+  /*box-shadow: 0px 0px 10px #fff,inset 0px 0px 10px #fff;*/
+
+
+  /*四边不同颜色模糊实现*/
+  box-shadow: inset 0px 0px 10px #fff,
+    3px 0px 10px #f0f,
+    0px -3px 10px #0ff,
+    -3px 0px 10px #00f,
+    0px 3px 10px #ff0;
+  /*关于Z轴覆盖,哪个阴影先设置,谁就在上*/
+}
+

3 个 demo

DEMO1

css
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 150px);
+  top: calc(50% - 150px);
+  width: 300px;
+  height: 300px;
+  border: 1px solid #fff;
+  border-radius: 50%;
+  box-shadow: inset 0px 0px 50px #fff, inset 10px 0px 80px #f0f,
+    inset -10px 0px 80px #0ff, inset 10px 0px 300px #f0f,
+    inset -10px 0px 300px #0ff, 0px 0px 50px #fff, -10px 0px 80px #f0f, 10px 0px
+      80px #0ff;
+}
+
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 150px);
+  top: calc(50% - 150px);
+  width: 300px;
+  height: 300px;
+  border: 1px solid #fff;
+  border-radius: 50%;
+  box-shadow: inset 0px 0px 50px #fff, inset 10px 0px 80px #f0f,
+    inset -10px 0px 80px #0ff, inset 10px 0px 300px #f0f,
+    inset -10px 0px 300px #0ff, 0px 0px 50px #fff, -10px 0px 80px #f0f, 10px 0px
+      80px #0ff;
+}
+

太阳

css
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 25px);
+  top: calc(50% - 25px);
+  width: 50px;
+  height: 50px;
+  background-color: #fff;
+  border-radius: 50%;
+  box-shadow: 0px 0px 100px 50px #fff, 0px 0px 250px 125px #ff0;
+}
+
body {
+  background-color: #000;
+}
+div {
+  position: absolute;
+  left: calc(50% - 25px);
+  top: calc(50% - 25px);
+  width: 50px;
+  height: 50px;
+  background-color: #fff;
+  border-radius: 50%;
+  box-shadow: 0px 0px 100px 50px #fff, 0px 0px 250px 125px #ff0;
+}
+

DEMO3

css
div {
+  position: absolute;
+  border-radius: 5px;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: #fff;
+  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
+  transition: all 0.6s;
+}
+div::after {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  border-radius: 5px;
+  box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.3);
+  opacity: 0;
+  transition: all 0.6s;
+}
+
+div:hover::after {
+  opacity: 1;
+}
+
+div:hover {
+  transform: scale(1.25, 1.25);
+}
+
div {
+  position: absolute;
+  border-radius: 5px;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  background-color: #fff;
+  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
+  transition: all 0.6s;
+}
+div::after {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  border-radius: 5px;
+  box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.3);
+  opacity: 0;
+  transition: all 0.6s;
+}
+
+div:hover::after {
+  opacity: 1;
+}
+
+div:hover {
+  transform: scale(1.25, 1.25);
+}
+

注意:背景颜色在阴影下面,文字在阴影上面

3.border-image

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 10px solid black;
+  /*支持渐变色*/
+  border-color: lightpink;
+  border-image-source: linear-gradient(red, yellow);
+  /* 实现背景颜色渐变 */
+  /* border-image-source: url(.//border.png); 
+    实现边框用背景图片渐变渲染
+    */
+  border-image-slice: 10;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 10px solid black;
+  /*支持渐变色*/
+  border-color: lightpink;
+  border-image-source: linear-gradient(red, yellow);
+  /* 实现背景颜色渐变 */
+  /* border-image-source: url(.//border.png); 
+    实现边框用背景图片渐变渲染
+    */
+  border-image-slice: 10;
+}
+

关于 slice(截取)

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* border-image-slice: 100 50 100 100; */
+  /* 上右下左 */
+  /*slice不填的话,默认100%*/
+  border-image-repeat: stretch;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* border-image-slice: 100 50 100 100; */
+  /* 上右下左 */
+  /*slice不填的话,默认100%*/
+  border-image-repeat: stretch;
+}
+

几个附加值

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* 让背景图片往外延伸 */
+  border-image-outset: 100px;
+  border-image-width: 1; /* border里面能显示图片背景的宽度 */
+  /* 默认为1,不同于 border-width */
+  border-image-width: auto; /* 相当于slice + px  */
+  /* border-image-slice: 100 100 fill; */
+  /* fill填充内容区 */
+  border-image-slice: 100 100;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100;
+  /* 让背景图片往外延伸 */
+  border-image-outset: 100px;
+  border-image-width: 1; /* border里面能显示图片背景的宽度 */
+  /* 默认为1,不同于 border-width */
+  border-image-width: auto; /* 相当于slice + px  */
+  /* border-image-slice: 100 100 fill; */
+  /* fill填充内容区 */
+  border-image-slice: 100 100;
+}
+

repeat

css
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100 100 fill;
+  border-image-repeat: round;
+  /* stretch默认值:拉伸
+    round:平铺
+    repeat:平铺
+    space:平铺
+    */
+  border-image-repeat: round stretch;
+  /*也可以两个:水平垂直*/
+}
+
div {
+  position: absolute;
+  left: calc(50% - 50px);
+  top: calc(50% - 50px);
+  width: 100px;
+  height: 100px;
+  border: 100px solid black;
+  border-color: lightpink;
+  border-image-source: url(.//border.png);
+  border-image-slice: 100 100 fill;
+  border-image-repeat: round;
+  /* stretch默认值:拉伸
+    round:平铺
+    repeat:平铺
+    space:平铺
+    */
+  border-image-repeat: round stretch;
+  /*也可以两个:水平垂直*/
+}
+

另一种写法:border-image:source slice repeat;

css
border-image: url(source/red.png) 100 repeat;
+
border-image: url(source/red.png) 100 repeat;
+

background

渐变颜色生成器:linear-gradient();  radial-gradient();

css
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  /* background-image: linear-gradient(#0f0, #f00); */
+  /* linear-gradient只能当成背景图片来使用,background-color不好使 */
+
+  /*两张图片在一个容器中展示*/
+  background-image: url() url(); /* 可以添加多个背景图片 */
+  background-size: 100px 200px, 100px 200px;
+  background-repeat: no-repeat;
+  background-position: 0 0, 100px 0;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  /* background-image: linear-gradient(#0f0, #f00); */
+  /* linear-gradient只能当成背景图片来使用,background-color不好使 */
+
+  /*两张图片在一个容器中展示*/
+  background-image: url() url(); /* 可以添加多个背景图片 */
+  background-size: 100px 200px, 100px 200px;
+  background-repeat: no-repeat;
+  background-position: 0 0, 100px 0;
+}
+

容错机制:容错背景图片展示

1.background-origin:

图片从哪里起始 没规定在哪结束,就开始重复图片 repeat,这就是为什么看起来像是 border-box 开始的

css
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  border: 20px solid transparent;
+  background-image: url(.//source/pic1.jpeg);
+
+  background-origin: padding-box;
+  /* border-box padding-box(默认值) content-box */
+  background-repeat: no-repeat;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  border: 20px solid transparent;
+  background-image: url(.//source/pic1.jpeg);
+
+  background-origin: padding-box;
+  /* border-box padding-box(默认值) content-box */
+  background-repeat: no-repeat;
+}
+

background-position 由 background-origin 决定的。position 相对于 origin 定位。

2.background-clip

背景图片从哪块开始截断,从哪块以外的部分都不显示背景图片,即从哪块结束

css
background-clip: border-box;
+/* border-box(默认,废弃值) padding-box content-box text */
+
background-clip: border-box;
+/* border-box(默认,废弃值) padding-box content-box text */
+

text 精解:文字内容区反切背景图片

css
* {
+  margin: 0;
+  padding: 0;
+}
+
+div:hover {
+  background-position: center center;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 200px);
+  top: 100px;
+  height: 100px;
+  line-height: 100px;
+  font-size: 80px;
+  width: 400px;
+  background-image: url(.//source/pic2.jpeg);
+  -webkit-background-clip: text;
+  /* 固定写法三件套,配合background-clip */
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  background-position: 0 0;
+  transition: all 0.6s;
+}
+
* {
+  margin: 0;
+  padding: 0;
+}
+
+div:hover {
+  background-position: center center;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 200px);
+  top: 100px;
+  height: 100px;
+  line-height: 100px;
+  font-size: 80px;
+  width: 400px;
+  background-image: url(.//source/pic2.jpeg);
+  -webkit-background-clip: text;
+  /* 固定写法三件套,配合background-clip */
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  background-position: 0 0;
+  transition: all 0.6s;
+}
+

3.background-repeat

css
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  background-image: url(.//source/pic4.jpeg);
+  background-size: 50px 50px;
+  /* background-repeat: no-repeat; */
+  /* background-repeat: repeat-x; */
+  /* background-repeat: repeat-y; */
+  /* background-repeat: round;深到一定程度蹦进来一张图 */
+  /* background-repeat: space;空白填充,冲到一定程度填充一张图片 */
+  /* background-repeat: round space;分别是x,y */
+  /* background-repeat:repeat-x相当于repeat-x no-repeat; */
+}
+
div {
+  position: absolute;
+  left: calc(50% - 100px);
+  top: calc(50% - 100px);
+  width: 200px;
+  height: 200px;
+  padding: 20px;
+  background-image: url(.//source/pic4.jpeg);
+  background-size: 50px 50px;
+  /* background-repeat: no-repeat; */
+  /* background-repeat: repeat-x; */
+  /* background-repeat: repeat-y; */
+  /* background-repeat: round;深到一定程度蹦进来一张图 */
+  /* background-repeat: space;空白填充,冲到一定程度填充一张图片 */
+  /* background-repeat: round space;分别是x,y */
+  /* background-repeat:repeat-x相当于repeat-x no-repeat; */
+}
+

4.background-attachment

改变定位属性的

css
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  /* overflow: hidden; */
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-position: 100px 100px;
+  /* background-attachment: local; */
+  /* 相对于内容区定位 */
+  /* background-attachment: scroll;默认 */
+  /* 默认scroll:相对于容器定位 */
+  /* background-attachment: fixed; */
+  /* 相对于真正的可视区视口定位 */
+}
+
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  /* overflow: hidden; */
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-position: 100px 100px;
+  /* background-attachment: local; */
+  /* 相对于内容区定位 */
+  /* background-attachment: scroll;默认 */
+  /* 默认scroll:相对于容器定位 */
+  /* background-attachment: fixed; */
+  /* 相对于真正的可视区视口定位 */
+}
+

5.background-size

css
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-size: cover;
+  /* contain不改变宽高比,让容器包含一张完整图片,即便会出现repeat
+    cover填充满容器,不改变宽高比 */
+  background-attachment: scroll;
+}
+
div {
+  width: 500px;
+  height: 700px;
+  border: 1px solid red;
+  overflow: scroll;
+  background-image: url(.//source/pic6.jpeg);
+  background-size: 300px 300px;
+  background-repeat: no-repeat;
+  background-size: cover;
+  /* contain不改变宽高比,让容器包含一张完整图片,即便会出现repeat
+    cover填充满容器,不改变宽高比 */
+  background-attachment: scroll;
+}
+

效果

contain(可能 repeat):一条边对齐,另一条小于等于容器另一条

cover(可能超出):一条边对齐,另一条大于等于容器另一条

渐变生成器

linear-gradient

css
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: linear-gradient(red, white);
+    background-image: linear-gradient(to right, red, green);
+    background-image: linear-gradient(to left, red, green);
+    background-image: linear-gradient(to bottom, red, green);
+    background-image: linear-gradient(to top, red, green);
+    background-image: linear-gradient(to top right, red, green); */
+  /* background-image: linear-gradient(0deg, red, green);90deg 180deg */
+  /* background-image: linear-gradient(90deg, red, 20px, green);颜色的终止位置 */
+  /* background-image: linear-gradient(90deg, red 20px, green 60px); */
+}
+
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: linear-gradient(red, white);
+    background-image: linear-gradient(to right, red, green);
+    background-image: linear-gradient(to left, red, green);
+    background-image: linear-gradient(to bottom, red, green);
+    background-image: linear-gradient(to top, red, green);
+    background-image: linear-gradient(to top right, red, green); */
+  /* background-image: linear-gradient(0deg, red, green);90deg 180deg */
+  /* background-image: linear-gradient(90deg, red, 20px, green);颜色的终止位置 */
+  /* background-image: linear-gradient(90deg, red 20px, green 60px); */
+}
+

镜像放射 radial-gradient()

css
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: radial-gradient(red, green, #0ff); */
+  /* background-image: radial-gradient(red 20%, green 50px, #0ff 40%); */
+  /* background-image: radial-gradient(circle at 100px 0px, red, green, #0ff); */
+  /* background-image: radial-gradient(ellipse at 20px 30px, red, green, #0ff); */
+  /* 放射半径 */
+  /* closest-corner
+    closest-side
+    farthest-corner
+    farthest-side */
+  /* background-image: radial-gradient(ellipse farthest-corner at 50px 50px, red, green, #0ff); */
+}
+
div {
+  width: 200px;
+  height: 100px;
+  /* background-image: radial-gradient(red, green, #0ff); */
+  /* background-image: radial-gradient(red 20%, green 50px, #0ff 40%); */
+  /* background-image: radial-gradient(circle at 100px 0px, red, green, #0ff); */
+  /* background-image: radial-gradient(ellipse at 20px 30px, red, green, #0ff); */
+  /* 放射半径 */
+  /* closest-corner
+    closest-side
+    farthest-corner
+    farthest-side */
+  /* background-image: radial-gradient(ellipse farthest-corner at 50px 50px, red, green, #0ff); */
+}
+

颜色

HSL

HSLA

currentColor 中转颜色

css
div {
+  width: 100px;
+  height: 100px;
+  border-width: 1px;
+  border-style: solid;
+  color: red; /*border竟然颜色改变了。一旦不设置border-color的时候,会继承color,currentColor作为中转*/
+  /* css1 css2 border-color:color currentColor*/
+  border-color: currentColor;
+}
+
div {
+  width: 100px;
+  height: 100px;
+  border-width: 1px;
+  border-style: solid;
+  color: red; /*border竟然颜色改变了。一旦不设置border-color的时候,会继承color,currentColor作为中转*/
+  /* css1 css2 border-color:color currentColor*/
+  border-color: currentColor;
+}
+

text

1.text-shadow

x, y, blur, color

css
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  text-shadow: 3px 3px 3px #000;
+  text-shadow: 3px 3px 3px #000, -10px -10px 3px #30f;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  text-shadow: 3px 3px 3px #000;
+  text-shadow: 3px 3px 3px #000, -10px -10px 3px #30f;
+}
+

demo

浮雕效果

css
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 1px 1px #000, -1px -1px #fff;
+}
+
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 1px 1px #000, -1px -1px #fff;
+}
+

镂刻效果

css
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: -1px -1px #000, 1px 1px #fff;
+}
+
body {
+  background-color: #0ff;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: -1px -1px #000, 1px 1px #fff;
+}
+

阴影加强

css
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 0px 0px 10px #0f0, 0px 0px 20px #0f0, 0px 0px 30px #0f0;
+  transition: all 0.3s;
+}
+
+div:hover {
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20;
+}
+
+div::before {
+  content: "NO ";
+  opacity: 0;
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20, 0px -5px
+      20px #f10, 0px -10px 20px #f20, 0px -15px 30px #f10;
+  transition: all 3s;
+}
+div:hover::before {
+  opacity: 1;
+}
+
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 700px;
+  height: 100px;
+  font-size: 80px;
+  color: #ddd;
+  text-shadow: 0px 0px 10px #0f0, 0px 0px 20px #0f0, 0px 0px 30px #0f0;
+  transition: all 0.3s;
+}
+
+div:hover {
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20;
+}
+
+div::before {
+  content: "NO ";
+  opacity: 0;
+  text-shadow: 0px 0px 10px #f00, 0px 0px 20px #f10, 0px 0px 30px #f20, 0px -5px
+      20px #f10, 0px -10px 20px #f20, 0px -15px 30px #f10;
+  transition: all 3s;
+}
+div:hover::before {
+  opacity: 1;
+}
+

死神来了

css
body {
+  /* background-color: #000; */
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 400px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  background-repeat: no-repeat;
+  background-position: -400px -50px;
+  background-image: url(.//source/eye.jpeg);
+  background-size: 400px 300px;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  transition: all 3s;
+  /* text-shadow: 10px 10px 3px #000; 
+   		这个不能展现效果?阴影跑到了文字内容上面,因为background-clip:text,文字就变成了背景的一部分
+    */
+  text-shadow: 10px 10px 3px rgba(0, 0, 0, 0.1);
+}
+
+div:hover {
+  background-position: 0 -50px;
+}
+
body {
+  /* background-color: #000; */
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 400px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  background-repeat: no-repeat;
+  background-position: -400px -50px;
+  background-image: url(.//source/eye.jpeg);
+  background-size: 400px 300px;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  text-fill-color: transparent;
+  transition: all 3s;
+  /* text-shadow: 10px 10px 3px #000; 
+   		这个不能展现效果?阴影跑到了文字内容上面,因为background-clip:text,文字就变成了背景的一部分
+    */
+  text-shadow: 10px 10px 3px rgba(0, 0, 0, 0.1);
+}
+
+div:hover {
+  background-position: 0 -50px;
+}
+

分身

css
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 60px;
+  font-weight: bold;
+  color: #f10;
+
+  text-shadow: 0px 0px 5px #f10, 0px 0px 10px #f20, 0px 0px 15px #f30;
+  transition: all 1.5s;
+}
+
+div:hover {
+  text-shadow: 10px -10px 10px #f00, 10px 10px 15px #ff0;
+}
+
body {
+  background-color: #000;
+}
+
+div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 60px;
+  font-weight: bold;
+  color: #f10;
+
+  text-shadow: 0px 0px 5px #f10, 0px 0px 10px #f20, 0px 0px 15px #f30;
+  transition: all 1.5s;
+}
+
+div:hover {
+  text-shadow: 10px -10px 10px #f00, 10px 10px 15px #ff0;
+}
+

描边效果 stroke

css
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 80px;
+  font-weight: bold;
+  -webkit-text-stroke: 2px red;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 80px;
+  font-weight: bold;
+  -webkit-text-stroke: 2px red;
+}
+

字体描边效果 stroke

css
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  color: transparent;
+  font-family: simsun;
+  -webkit-text-stroke: 1px red;
+}
+
div {
+  position: absolute;
+  left: calc(50% - 350px);
+  top: 100px;
+  width: 500px;
+  height: 150px;
+  font-size: 100px;
+  font-weight: bold;
+  color: transparent;
+  font-family: simsun;
+  -webkit-text-stroke: 1px red;
+}
+

font-face 字体包使用方法

css
@font-face {
+    font-family: 'abc';字体包名字
+    src: url();
+}
+
+div {
+    font-family: 'abc';引入字体包
+}
+
@font-face {
+    font-family: 'abc';字体包名字
+    src: url();
+}
+
+div {
+    font-family: 'abc';引入字体包
+}
+

引入折磨多图片格式的原因:

字体格式

  1. TureType 微软 苹果 .ttf
  2. opentype 微软 adobe 。opt
  3. woff .woff
  4. ie .eat
  5. h5 svg

MIME 协议(.ttf .txt .pdf)

如果不认识,format 让浏览器加强对字体格式的认识。format 产生映射

2.   text 系列

white-space

css
div{
+    white-space:pre;原封不动的保留你输入时的状态,空格、换行都会保留,并且当文字超出边界时不换行
+    white-space:nowarp:强制所有文本在同一行内显示。
+}
+
div{
+    white-space:pre;原封不动的保留你输入时的状态,空格、换行都会保留,并且当文字超出边界时不换行
+    white-space:nowarp:强制所有文本在同一行内显示。
+}
+

word-break

css
div {
+  width: 200px;
+  border: 1px solid black;
+  /* word-break: keep-all; */
+  /* 不换行 */
+  /* word-break: break-all; */
+  /* 强制换行 */
+  /* word-break: break-word; */
+  /* 尽可能保留英文单词完整性 */
+}
+
div {
+  width: 200px;
+  border: 1px solid black;
+  /* word-break: keep-all; */
+  /* 不换行 */
+  /* word-break: break-all; */
+  /* 强制换行 */
+  /* word-break: break-word; */
+  /* 尽可能保留英文单词完整性 */
+}
+

columns 报纸布局

css
div {
+    /* columns: column-width||column-count;; */
+    /* columns: 300px 4; */
+    column-count: 3;
+    column-gap: 30px;
+    /* 空隙 */
+    column-rule: 1px solid black;
+    /* 空隙分割线 */
+}
+p {
+    margin: 10px 0;
+    column-span: all;默认1
+    /* 横穿整个列 */
+}
+
div {
+    /* columns: column-width||column-count;; */
+    /* columns: 300px 4; */
+    column-count: 3;
+    column-gap: 30px;
+    /* 空隙 */
+    column-rule: 1px solid black;
+    /* 空隙分割线 */
+}
+p {
+    margin: 10px 0;
+    column-span: all;默认1
+    /* 横穿整个列 */
+}
+
css
-webkit-column-break-before: always;
+/* 前面断列,另起一列 */
+-webkit-column-break-after: always;
+/* 后面断列,另起一列 */
+
-webkit-column-break-before: always;
+/* 前面断列,另起一列 */
+-webkit-column-break-after: always;
+/* 后面断列,另起一列 */
+

column-with

css
/* column-count: 3;自适应 */
+column-width: 300px;
+/* 不太准,会自适应 */
+
/* column-count: 3;自适应 */
+column-width: 300px;
+/* 不太准,会自适应 */
+

瀑布流布局 :他尽量让高的成为多数的。一个一个试试就试出来了

css
div {
+  /*内部机制不适宜瀑布流布局 */
+  column-count: 4;
+  column-rule-style: solid;
+}
+
div {
+  /*内部机制不适宜瀑布流布局 */
+  column-count: 4;
+  column-rule-style: solid;
+}
+

column 应用:小说阅读

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+
+      div {
+        width: 300px;
+        height: 500px;
+        border: 1px solid red;
+      }
+
+      .content {
+        column-width: 300px;
+        column-gap: 20px;
+        border: none;
+
+        transition: all 2s;
+      }
+
+      div:hover .content {
+        transform: translateX(-320px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)。
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+
+      div {
+        width: 300px;
+        height: 500px;
+        border: 1px solid red;
+      }
+
+      .content {
+        column-width: 300px;
+        column-gap: 20px;
+        border: none;
+
+        transition: all 2s;
+      }
+
+      div:hover .content {
+        transform: translateX(-320px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)。
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+        第6号)》,公告称,根据《中华人民共和国
+        药品管理法》第八十三条规定,国家药品监督管理局
+        组织对酚酞片和酚酞含片进行上市后评价,评价认为酚
+        酞片和酚酞含片存在严重不良反应,在我国使用风险大于
+        益,决定自即日起停止酚酞片和酚酞含片在我国的生产、销
+        售和使用,注销药品注册证书(药品批准文号)红星资本局从国家药监局官网获悉,1月
+        14日,国家药监局发布《关于注销酚酞片和酚酞含片药品注册证书的公告(2021年
+      </div>
+    </div>
+  </body>
+</html>
+

滑动——slide 插件

box

1.IE6 混杂模式盒子

css
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  border: 10px solid black;
+  padding: 10px;
+  /* 触发 */
+  box-sizing: border-box;
+  /*默认content-box*/
+}
+
+/* 
+原来:boxWidth=width+border*2+padding*2;
+怪异:boxWidth=width 
+	 contentWidth=width - border*2 - padding*2 
+*/
+
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  border: 10px solid black;
+  padding: 10px;
+  /* 触发 */
+  box-sizing: border-box;
+  /*默认content-box*/
+}
+
+/* 
+原来:boxWidth=width+border*2+padding*2;
+怪异:boxWidth=width 
+	 contentWidth=width - border*2 - padding*2 
+*/
+

宽度不固定,内边距 padding 固定

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 100%;
+        height: 300px;
+        border: 1px solid black;
+      }
+
+      .content:first-of-type {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: yellow;
+        padding: 0 10px;
+        box-sizing: border-box;
+      }
+
+      .content {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: red;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 100%;
+        height: 300px;
+        border: 1px solid black;
+      }
+
+      .content:first-of-type {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: yellow;
+        padding: 0 10px;
+        box-sizing: border-box;
+      }
+
+      .content {
+        float: left;
+        width: 50%;
+        height: 100px;
+        background-color: red;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

输入框宽度不固定,内边距固定

html
<style>
+    .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+    }
+
+    input {
+        width: 100%;
+        box-sizing: border-box;
+    }
+
+    /* input天生带2px的border,解决就用怪异盒子 */
+</style>
+</head>
+
+<body>
+    <div class="wrapper">
+        <input type="text">
+    </div>
+</body>
+
<style>
+    .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+    }
+
+    input {
+        width: 100%;
+        box-sizing: border-box;
+    }
+
+    /* input天生带2px的border,解决就用怪异盒子 */
+</style>
+</head>
+
+<body>
+    <div class="wrapper">
+        <input type="text">
+    </div>
+</body>
+

产品需求接受形式

css
input {
+  width: 300px;
+  height: 50px;
+  box-sizing: border-box;
+  padding: 0 10px;
+}
+
input {
+  width: 300px;
+  height: 50px;
+  box-sizing: border-box;
+  padding: 0 10px;
+}
+

宽度用户自定,后端传的,padding border 固定

css
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+}
+
+.content {
+  box-sizing: border-box;
+  float: left;
+  width: 100px;
+  height: 100px;
+  border: 1px solid #fff;
+  background-color: black;
+}
+
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+}
+
+.content {
+  box-sizing: border-box;
+  float: left;
+  width: 100px;
+  height: 100px;
+  border: 1px solid #fff;
+  background-color: black;
+}
+
html
<div class="wrapper">
+  <div class="content"></div>
+  <div class="content"></div>
+  <div class="content"></div>
+</div>
+
<div class="wrapper">
+  <div class="content"></div>
+  <div class="content"></div>
+  <div class="content"></div>
+</div>
+

两个值

1.overflow

css
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* overflow: scroll; */
+  /* 除了当overflow-x,overflow-y之一设置了非 visible时,另一个属性会自动将默认值visible设置为auto */
+  overflow-x: scroll; /* 就会overflow-y:auto */
+}
+
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* overflow: scroll; */
+  /* 除了当overflow-x,overflow-y之一设置了非 visible时,另一个属性会自动将默认值visible设置为auto */
+  overflow-x: scroll; /* 就会overflow-y:auto */
+}
+

移动端

明星左右滑动模块

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 100px;
+        border: 1px solid black;
+        overflow-x: scroll;
+        overflow-y: auto;
+      }
+
+      /* 最外层溢出部分隐藏,里面盒子=图片总体宽度 */
+
+      .box {
+        width: 800px;
+        height: 100px;
+      }
+
+      img {
+        float: left;
+        display: inline;
+        width: 100px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="box">
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 100px;
+        border: 1px solid black;
+        overflow-x: scroll;
+        overflow-y: auto;
+      }
+
+      /* 最外层溢出部分隐藏,里面盒子=图片总体宽度 */
+
+      .box {
+        width: 800px;
+        height: 100px;
+      }
+
+      img {
+        float: left;
+        display: inline;
+        width: 100px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="box">
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+        <img src="./1.jpg" alt="" />
+      </div>
+    </div>
+  </body>
+</html>
+

2.resize 调节元素大小 :会导致重排和重绘,性能消耗

css
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* resize: both; */
+  /* resize: vertical;竖直 */
+  overflow: scroll;
+}
+
.wrapper {
+  width: 300px;
+  height: 300px;
+  border: 1px solid red;
+  /* resize: both; */
+  /* resize: vertical;竖直 */
+  overflow: scroll;
+}
+

2.flex 弹性盒子

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        /* display: inline-flex; */
+        flex-direction: row;
+        /* 默认主轴水平方向,自左向右 */
+        flex-direction: column;
+        /* 垂直 */
+        flex-direction: row-reverse;
+        /* 逆反 */
+        flex-direction: column-reverse;
+        /* 垂直逆反 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        /* display: inline-flex; */
+        flex-direction: row;
+        /* 默认主轴水平方向,自左向右 */
+        flex-direction: column;
+        /* 垂直 */
+        flex-direction: row-reverse;
+        /* 逆反 */
+        flex-direction: column-reverse;
+        /* 垂直逆反 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

换行实现:

flex-wrap

justify-content:基于主轴对齐方式

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* justify-content: flex-start;默认 */
+        /* justify-content: flex-end; 主轴向右*/
+        /* justify-content: center; */
+        /* justify-content: space-between; */
+        /* 两边站住,中间自适应 */
+        /* justify-content: space-around; */
+        /* 元素元素之间间距相等 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* justify-content: flex-start;默认 */
+        /* justify-content: flex-end; 主轴向右*/
+        /* justify-content: center; */
+        /* justify-content: space-between; */
+        /* 两边站住,中间自适应 */
+        /* justify-content: space-around; */
+        /* 元素元素之间间距相等 */
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

align-items 垂直方向

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* align-items: baseline; */
+        /* 基于文字对齐 */
+        /* align-items: stretch默认; */
+        /* 未设置内容区高度的话,实现拉伸;如果设置了,就不好使了 */
+        /* align-items: flex-start; */
+        /* align-items: flex-end; */
+        /* align-items: center; */
+      }
+
+      .content {
+        width: 100px;
+        /* height: 100px; */
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        /* margin-top: 10px; 验证baseline*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: nowrap;
+        /* align-items: baseline; */
+        /* 基于文字对齐 */
+        /* align-items: stretch默认; */
+        /* 未设置内容区高度的话,实现拉伸;如果设置了,就不好使了 */
+        /* align-items: flex-start; */
+        /* align-items: flex-end; */
+        /* align-items: center; */
+      }
+
+      .content {
+        width: 100px;
+        /* height: 100px; */
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        /* margin-top: 10px; 验证baseline*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

单行居中

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: wrap;
+        align-items: center; /* 主要针对单行元素来处理对齐方式的 */
+
+        align-content: center; /* 必须作用于多行元素 */
+        justify-content: center;
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: wrap;
+        align-items: center; /* 主要针对单行元素来处理对齐方式的 */
+
+        align-content: center; /* 必须作用于多行元素 */
+        justify-content: center;
+      }
+
+      .content {
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

align-content 看官方文档自学

以上都是设置在父级上的

以下是子级

order 相当于 z-index,默认 0,最好添负值;小的在前

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: stretch;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  order: -2;
+}
+.content:nth-of-type(2) {
+  order: -1;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: stretch;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  order: -2;
+}
+.content:nth-of-type(2) {
+  order: -1;
+}
+

align-self

听从自己

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+

强于 align-items(父级),弱于 align-content(父级)

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-content: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  flex-direction: nowrap;
+  align-content: center;
+  flex-wrap: wrap;
+}
+
+.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid green;
+  box-sizing: border-box;
+}
+
+.content:first-of-type {
+  align-self: flex-start;
+}
+
+.content:nth-of-type(2) {
+  align-self: flex-end;
+}
+

弹性盒子之弹性

flex-grow

当这一行还有剩余空间的时候,按照比例分配剩余空间,最终调整大小

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* */
+        /* flex-grow: 1; */
+        /* 默认0 伸展开了 */
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        flex-grow: 1;
+      }
+
+      .content:nth-of-type(2) {
+        flex-grow: 2;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 600px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* */
+        /* flex-grow: 1; */
+        /* 默认0 伸展开了 */
+        width: 100px;
+        height: 100px;
+        border: 1px solid green;
+        box-sizing: border-box;
+      }
+
+      .content:first-of-type {
+        flex-grow: 1;
+      }
+
+      .content:nth-of-type(2) {
+        flex-grow: 2;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content">1</div>
+      <div class="content">2</div>
+      <div class="content">3</div>
+    </div>
+  </body>
+</html>
+

flex-shrink   默认 1

超出,不换行,启动压缩

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+.content {
+  flex-shrink: 1;
+  width: 200px;
+  height: 100px;
+}
+/* 压缩加权算法 */
+/* 200px * 1 + 200px * 1 + 400px * 3 = 1600px */
+/* 	
+200px * 1
+---------   * 200px  = 25px
+1600 
+*/
+.content:nth-of-type(3) {
+  flex-grow: 3;
+  width: 400px;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+.content {
+  flex-shrink: 1;
+  width: 200px;
+  height: 100px;
+}
+/* 压缩加权算法 */
+/* 200px * 1 + 200px * 1 + 400px * 3 = 1600px */
+/* 	
+200px * 1
+---------   * 200px  = 25px
+1600 
+*/
+.content:nth-of-type(3) {
+  flex-grow: 3;
+  width: 400px;
+}
+

深入剖析:加边框

得出最终结论:真实内容区大小*shrink + (...)=加权值

flex-basis 相当于 width 权重大于 width

默认 auto

basis 与 width 区别

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  flex-basis: 100px;
+  /* 根据内容撑开 */
+  flex-shrink: 1;
+  /* width: 100px; 覆盖*/
+  height: 200px;
+  background-color: #f0f;
+  /* 
+    	元素撑开的话,得出以下结论:
+        只写basis或者basis>width,代表元素的最小宽度           
+        设置了width并且basis小于width 
+        basis<realWidth<width
+    */
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+}
+
+.content:nth-of-type(3) {
+  background-color: #0ff;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  flex-basis: 100px;
+  /* 根据内容撑开 */
+  flex-shrink: 1;
+  /* width: 100px; 覆盖*/
+  height: 200px;
+  background-color: #f0f;
+  /* 
+    	元素撑开的话,得出以下结论:
+        只写basis或者basis>width,代表元素的最小宽度           
+        设置了width并且basis小于width 
+        basis<realWidth<width
+    */
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+}
+
+.content:nth-of-type(3) {
+  background-color: #0ff;
+}
+

想换行,加汉字或者设置换行

css
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  height: 200px;
+  /* word-break: break-word;可换行,就可以参与压缩了 */
+}
+
+.content:nth-of-type(1) {
+  background-color: #0f0;
+  flex-basis: 200px;
+  flex-shrink: 5;
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+  flex-basis: 200px;
+  flex-shrink: 1;
+}
+
+.content:nth-of-type(3) {
+  flex-basis: 400px;
+  background-color: #0ff;
+  flex-shrink: 1;
+}
+
.wrapper {
+  width: 600px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+}
+
+.content {
+  height: 200px;
+  /* word-break: break-word;可换行,就可以参与压缩了 */
+}
+
+.content:nth-of-type(1) {
+  background-color: #0f0;
+  flex-basis: 200px;
+  flex-shrink: 5;
+}
+
+.content:nth-of-type(2) {
+  background-color: #ff0;
+  flex-basis: 200px;
+  flex-shrink: 1;
+}
+
+.content:nth-of-type(3) {
+  flex-basis: 400px;
+  background-color: #0ff;
+  flex-shrink: 1;
+}
+

以上,总结

当你设置宽的时候,如果 basis 设置有值,且小于 width,那么真实的宽的范围在 basis <realWidth<Width 当你不设置 width 的时候,设置 basis,元素真实的宽 min-width 当不换行内容超过内容区 会撑开容易 无论什么情况,被不换行内容撑开的容器,不会被压缩计算

探究 flex 应用 flex:0 1 auto 默认 因为设置了 flex 后会自动压缩

基本应用

html
.wrapper { width: 600px; height: 600px; border: 1px solid black; display: flex;
+/*flex-wrap: wrap;*/ align-content: flex-start; } .content { background-color:
+red; flex: 1 1 auto; width: 250px; height: 250px; }
+
.wrapper { width: 600px; height: 600px; border: 1px solid black; display: flex;
+/*flex-wrap: wrap;*/ align-content: flex-start; } .content { background-color:
+red; flex: 1 1 auto; width: 250px; height: 250px; }
+

1.实现居中

css
.wrapper {
+  resize: both; //配合overflow使用
+  overflow: hidden;
+  width: 300px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+div.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid black;
+}
+
.wrapper {
+  resize: both; //配合overflow使用
+  overflow: hidden;
+  width: 300px;
+  height: 300px;
+  border: 1px solid black;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+div.content {
+  width: 100px;
+  height: 100px;
+  border: 1px solid black;
+}
+

2.可动态增加的导航栏

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        align-items: center;
+      }
+
+      .item {
+        flex: 1 1 auto;
+        height: 30px;
+        text-align: center;
+        line-height: 30px;
+        font-size: 20px;
+        color: #f20;
+        border-radius: 5px;
+      }
+
+      .item:hover {
+        background-color: #f20;
+        color: #fff;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="item">天猫</div>
+      <div class="item">淘宝</div>
+      <div class="item">聚划算</div>
+      <!-- 无论多少,都等分 -->
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        align-items: center;
+      }
+
+      .item {
+        flex: 1 1 auto;
+        height: 30px;
+        text-align: center;
+        line-height: 30px;
+        font-size: 20px;
+        color: #f20;
+        border-radius: 5px;
+      }
+
+      .item:hover {
+        background-color: #f20;
+        color: #fff;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="item">天猫</div>
+      <div class="item">淘宝</div>
+      <div class="item">聚划算</div>
+      <!-- 无论多少,都等分 -->
+    </div>
+  </body>
+</html>
+

3.等分布局(4 等分,2 等分,中间可加 margin)

实现中间固定,两边等比例自适应

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 200px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* flex: 0 1 auto; 默认*/
+        flex: 1 1 auto;
+        /* margin: 0 10px; */
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+
+      .content:nth-of-type(2) {
+        flex: 0 0 200px; /*中间固定,不能伸不能缩,即0,0,*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 200px;
+        border: 1px solid black;
+        display: flex;
+      }
+
+      .content {
+        /* flex: 0 1 auto; 默认*/
+        flex: 1 1 auto;
+        /* margin: 0 10px; */
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+
+      .content:nth-of-type(2) {
+        flex: 0 0 200px; /*中间固定,不能伸不能缩,即0,0,*/
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

实现中间固定,两边不比例自适应

css
加上 .content:nth-of-type(3) {
+  flex: 2 2 auto;
+}
+
加上 .content:nth-of-type(3) {
+  flex: 2 2 auto;
+}
+

其中一个固定宽度的布局(固定一个,固定两个)

4.圣杯布局

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: column;
+        //align-items:stretch//默认  交叉轴如果没有设置,就拉伸
+      }
+
+      .header,
+      .footer,
+      .left,
+      .right {
+        flex: 0 0 20%; /*不参与伸缩,占20%*/
+        border: 1px solid black;
+        box-sizing: border-box;
+      }
+
+      .contain {
+        flex: 1 1 auto; //把中间内容区噔开
+        display: flex;
+      }
+
+      .center {
+        flex: 1 1 auto;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="header">header</div>
+      <div class="contain">
+        <div class="left">left</div>
+        <div class="center">center</div>
+        <div class="right">right</div>
+      </div>
+      <div class="footer">footer</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: column;
+        //align-items:stretch//默认  交叉轴如果没有设置,就拉伸
+      }
+
+      .header,
+      .footer,
+      .left,
+      .right {
+        flex: 0 0 20%; /*不参与伸缩,占20%*/
+        border: 1px solid black;
+        box-sizing: border-box;
+      }
+
+      .contain {
+        flex: 1 1 auto; //把中间内容区噔开
+        display: flex;
+      }
+
+      .center {
+        flex: 1 1 auto;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="header">header</div>
+      <div class="contain">
+        <div class="left">left</div>
+        <div class="center">center</div>
+        <div class="right">right</div>
+      </div>
+      <div class="footer">footer</div>
+    </div>
+  </body>
+</html>
+

5.流式布局

模拟 float 布局

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 800px;
+        border: 1px solid black;
+        display: flex;
+        flex-wrap: wrap;
+        align-content: flex-start; //模拟float
+      }
+
+      .content {
+        width: 100px;
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 400px;
+        height: 800px;
+        border: 1px solid black;
+        display: flex;
+        flex-wrap: wrap;
+        align-content: flex-start; //模拟float
+      }
+
+      .content {
+        width: 100px;
+        border: 1px solid green;
+        height: 100px;
+        box-sizing: border-box;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

作业:刘德华

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      .wrapper {
+        background-color: red;
+        resize: both;
+        overflow: hidden;
+        display: flex;
+      }
+      .left {
+        margin-top: 20px;
+        flex: 0 0 auto;
+      }
+      img {
+        width: 150px;
+        height: 200px;
+      }
+      .right {
+        flex: 1 1 auto;
+        margin-left: 40px;
+      }
+      em {
+        color: #fff;
+        font-size: 30px;
+      }
+      p {
+        margin-top: 20px;
+        color: #fff;
+        font-size: 60px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="left">
+        <img src=".\1.jpg" alt="" />
+      </div>
+      <div class="right">
+        <p>刘德华</p>
+        <em>演员演员演员演员演员演员演员</em>
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      .wrapper {
+        background-color: red;
+        resize: both;
+        overflow: hidden;
+        display: flex;
+      }
+      .left {
+        margin-top: 20px;
+        flex: 0 0 auto;
+      }
+      img {
+        width: 150px;
+        height: 200px;
+      }
+      .right {
+        flex: 1 1 auto;
+        margin-left: 40px;
+      }
+      em {
+        color: #fff;
+        font-size: 30px;
+      }
+      p {
+        margin-top: 20px;
+        color: #fff;
+        font-size: 60px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="left">
+        <img src=".\1.jpg" alt="" />
+      </div>
+      <div class="right">
+        <p>刘德华</p>
+        <em>演员演员演员演员演员演员演员</em>
+      </div>
+    </div>
+  </body>
+</html>
+

响应式网站开发

demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      div {
+        font-size: 14px;
+      }
+
+      .wrapper {
+        width: 1500px;
+        font-size: 0;
+      }
+
+      .content {
+        width: 300px;
+        height: 200px;
+        border: 1px solid black;
+        display: inline-block; //不用浮动,而是用这种方法
+        box-sizing: border-box;
+        /* 
+                换行?
+                1.凡是带有inline-block都有文字特性,制表符=文字大小
+                2.border
+                */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      div {
+        font-size: 14px;
+      }
+
+      .wrapper {
+        width: 1500px;
+        font-size: 0;
+      }
+
+      .content {
+        width: 300px;
+        height: 200px;
+        border: 1px solid black;
+        display: inline-block; //不用浮动,而是用这种方法
+        box-sizing: border-box;
+        /* 
+                换行?
+                1.凡是带有inline-block都有文字特性,制表符=文字大小
+                2.border
+                */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+      <div class="content"></div>
+    </div>
+  </body>
+</html>
+

2.响应式网页设计

真正的响应式设计方法不仅仅是根据可视区域大小而改变网页布局,而是要从整体上颠覆当前网页的设计方法,是针对任意设备的网页内容进行完美布局的一种显示机制。

用一套代码解决几乎所有设备的页面展示问题

设计工作由产品经理或者美工来出

详解 meta:将页面大小 根据分辨率不同进行相应的调节   以展示给用户的大小感觉上差不多

1css 像素  != 设备像素 (根据屏幕分辨率 相应的调整)

html
<meta
+  name="viewport"
+  content="width=device-width, initial-scale=1.0, user-scalable=no"
+/>
+<meta http-equiv="X-UA-Compatible" content="ie=edge" />
+
<meta
+  name="viewport"
+  content="width=device-width, initial-scale=1.0, user-scalable=no"
+/>
+<meta http-equiv="X-UA-Compatible" content="ie=edge" />
+

3.设置视口

模拟移动端的 meta

html
<meta
+  name="viewport"
+  content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"
+/>
+
<meta
+  name="viewport"
+  content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"
+/>
+

width: 可视区宽度

device-width:  设备宽度

minimum-scale: 最小缩放比

maximum-scale: 最大缩放比

width = device-width :  iphone 或者 ipad 上横竖屏的宽度 =   竖屏时候的宽度   不能自适应的问题

initial-scale=1.0    :  windows phone ie 浏览器 上横竖屏的宽度 =   竖屏时候的宽度 不能自适应的问题

user-scalable: 是否允许用户缩放

Css 像素根据设备像素进行计算   1css 像素  == 1 是设备像素     根据设备的分辨率  dpi 值来计算 css 像素真正展现的大小

meta 功能即适配各种不同分辨率的设备

4.响应式网页开发方法

  1. 流体网格:可伸缩的网格 (大小宽高   都是可伸缩(可用 flex 或者百分比来控制大小)float)---》 布局上面 元素大小不固定可伸缩
  2. 弹性图片:图片宽高不固定(可设置 min-width: 100%)
  3. 媒体查询:让网页在不同的终端上面展示效果相同(用户体验相同 à 让用户用着更爽) 在不同的设备(大小不同 分辨率不同)上面均展示合适的页面
  4. 主要断点: 设备宽度的临界点 大小的区别 ---》 宽度不同   ---》 根据不同宽度展示不同的样式 响应式网页开发主要是在 css 样式(异步加载)上面进行操作

5.媒体查询

媒体查询是向不同设备提供不同样式的一种方式,它为每种类型的用户提供了最佳的体验。

css2: media type

media type(媒体类型)是 css 2 中的一个非常有用的属性,通过 media type 我们可以对不同的设备指定特定的样式,从而实现更丰富的界面。

css3: media query

media query 是 CSS3 对 media type 的增强,事实上我们可以将 media query 看成是 media type+css 属性(媒体特性 Media features)判断。

如何使用媒体查询?

媒体查询的引用方法有很多种:

  1. link 标签
  2. @import url(example.css) screen and (width:800px);
  3. css3 新增的@media

媒体查询不占用权重

使用方法

媒体类型(Media Type): all(全部)、screen(屏幕)、print(页面打印或打印预览模式)

媒体特性(Media features): width(渲染区宽度)、device-width(设备宽度)...

Media Query 是 CSS3 对 Media Type 的增强版,其实可以将 Media Query 看成 Media Type(判断条件)+CSS(符合条件的样式规则)

6.媒体类型

媒体特性(media features) 逻辑操作符 合并多个媒体属性 and

css
@media screen and (min-width: 600px) and (max-width: 100px);
+
@media screen and (min-width: 600px) and (max-width: 100px);
+

合并多个媒体属性或合并媒体属性与媒体类型, 一个基本的媒体查询,即一个媒体属性与默认指定的 screen 媒体类型。 指定备用功能

html
@media screen and (min-width: 769px), print and (min-width: 6in)“
+
@media screen and (min-width: 769px), print and (min-width: 6in)“
+

没有 or 关键词可用于指定备用的媒体功能。相反,可以将备用功能以逗号分割列表的形式列出 这会将样式应用到宽度超过 769 像素的屏幕或使用至少 6 英寸宽的纸张的打印设备。 指定否定条件

css
@media not screen and (monochrome);
+
@media not screen and (monochrome);
+

要指定否定条件,可以在媒体声明中添加关键字 not,不能在单个条件前使用 not。该关键字必须位于声明的开头,而且它会否定整个声明。所以,上面的示例会应用于除单色屏幕外的所有设备。 向早期浏览器隐藏媒体查询

css
media="only screen and (min-width: 401px) and (max-width: 600px)"
+
media="only screen and (min-width: 401px) and (max-width: 600px)"
+

媒体查询规范还提供了关键字 only,它用于向早期浏览器隐藏媒体查询。类似于 not,该关键字必须位于声明的开头。Only 指定某种特定的媒体类型   为了兼容不支持媒体查询的浏览器 早期浏览器应该将以下语句 media="screen and (min-width: 401px) and (max-width: 600px)" 解释为 media="screen": 换句话说,它应该将样式规则应用于所有屏幕设备,即使它不知道媒体查询的含义。 无法识别媒体查询的浏览器要求获得逗号分割的媒体类型列表,规范要求,它们应该在第一个不是连字符的非数字字母字符之前截断每个值。所以,早期浏览器应该将上面的示例解释为:media="only"  因为没有 only 这样的媒体类型,所以样式表被忽略。 Query -à css3

易混淆的宽度

css
device-width/height    width/height来做为的判定值。
+
device-width/height    width/height来做为的判定值。
+

device-width/device-height 是设备的宽度(如电脑手机的宽度 不是浏览器的宽度) width/height 使用 documentElement.clientWidth/Height 即 viewport 的值。渲染宽度/高度 视口宽度/

7.单位值

Rem:rem 是 CSS3 新增的一个相对单位(root em,根 em)相对的只是 HTML 根元素。

Em:em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。

Px: px 像素(Pixel)。相对长度单位。像素 px 是相对于显示器屏幕分辨率而言的。

Vw:相对于视口的宽度。视口被均分为 100 单位的 vw

Vh:相对于视口的高度。视口被均分为 100 单位的 vh

Vmax: 相对于视口的宽度或高度中较大的那个。其中最大的那个被均分为 100 单位的 vmax

Vmin:相对于视口的宽度或高度中较小的那个。其中最小的那个被均分为 100 单位的 vmin

html
rem 相对于html元素的font-size大小 em 相对于本身的font-size大小
+font-size属性是可以继承的 vw/ vh 相对于视口而言的 会把视口分成100份 vmax
+区视口宽高中最大的一边分成100份 vmin 区视口宽高中最小的一边分成100份 css样式引入
+媒体查询不占用权重
+
rem 相对于html元素的font-size大小 em 相对于本身的font-size大小
+font-size属性是可以继承的 vw/ vh 相对于视口而言的 会把视口分成100份 vmax
+区视口宽高中最大的一边分成100份 vmin 区视口宽高中最小的一边分成100份 css样式引入
+媒体查询不占用权重
+
+ + + + + \ No newline at end of file diff --git a/html-css/HTML.html b/html-css/HTML.html new file mode 100644 index 00000000..cfbae761 --- /dev/null +++ b/html-css/HTML.html @@ -0,0 +1,670 @@ + + + + + + HTML5 | Sunny's blog + + + + + + + + +
Skip to content
On this page

HTML5

大纲

新增的属性

  • placeholder
  • Calendar, date, time, email, url, search
  • ContentEditable
  • Draggable
  • Hidden
  • Content-menu
  • Data-Val(自定义属性)

新增的标签

  • 语义化标签
  • canvas
  • svg
  • Audio(声音播放)
  • Video(视频播放)

API

  • 移动端网页开发一般指的是 h5
  • 定位(需要地理位置的功能)
  • 重力感应(手机里面的陀螺仪(微信摇一摇,赛车转弯))
  • request-animation-frame(动画优化)
  • History 历史界面(控制当前页面的历史记录)
  • LocalStorage(本地存储,电脑/浏览器关闭都会保留);SessionStorage,(会话存储:窗口关闭就消失)。 都是存储信息(比如历史最高记录)
  • WebSocket(在线聊天,聊天室)
  • FileReader(文件读取,预览图)
  • WebWoker(文件的异步,提升性能,提升交互体验)
  • Fetch(传说中要替代 AJAX 的东西)

属性篇_input 新增 type

1.placeholder

html
<input type="text" placeholder="用户名/手机/邮箱" />
+<input type="password" placeholder="请输入密码" />
+
<input type="text" placeholder="用户名/手机/邮箱" />
+<input type="password" placeholder="请输入密码" />
+

2.input 新增 type

以前学的

html
<input type="radio" />
+<input type="checkbox" />
+<input type="file" />
+
<input type="radio" />
+<input type="checkbox" />
+<input type="file" />
+

input 新增 type

html
<form>
+  <!-- Calendar类 -->
+  <input type="date" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input type="time" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="week"
+  /><!-- 第几周  不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="datetime-local"
+  /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <br />
+  <input
+    type="number"
+  /><!-- 限制输入,仅数字可以。不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="email"
+  /><!-- 邮箱格式 不常用原因之一:chrome,火狐支持,Safari,IE不支持-->
+  <input
+    type="color"
+  /><!-- 颜色选择器 不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="range"
+    min="1"
+    max="100"
+    name="range"
+  /><!-- chrome,Safar支持 ,火狐,IE不支持-->
+  <input
+    type="search"
+    name="search"
+  /><!-- 自动提示历史搜索.chrome支持,Safar支持一点,IE不支持 -->
+  <input type="url" /><!-- chrome,火狐支持,Safar,IE不支持 -->
+
+  <input type="submit" />
+</form>
+
<form>
+  <!-- Calendar类 -->
+  <input type="date" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input type="time" /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="week"
+  /><!-- 第几周  不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <input
+    type="datetime-local"
+  /><!-- 不常用原因之一:chrome支持,Safari,IE不支持 -->
+  <br />
+  <input
+    type="number"
+  /><!-- 限制输入,仅数字可以。不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="email"
+  /><!-- 邮箱格式 不常用原因之一:chrome,火狐支持,Safari,IE不支持-->
+  <input
+    type="color"
+  /><!-- 颜色选择器 不常用原因之一:chrome支持,Safari,IE不支持-->
+  <input
+    type="range"
+    min="1"
+    max="100"
+    name="range"
+  /><!-- chrome,Safar支持 ,火狐,IE不支持-->
+  <input
+    type="search"
+    name="search"
+  /><!-- 自动提示历史搜索.chrome支持,Safar支持一点,IE不支持 -->
+  <input type="url" /><!-- chrome,火狐支持,Safar,IE不支持 -->
+
+  <input type="submit" />
+</form>
+

ContentEditable

html
<div>Panda</div>
+
<div>Panda</div>
+

想修改内容 原生方法:增加点击事件修改 新方法

html
<div contenteditable="true">Panda</div>
+
<div contenteditable="true">Panda</div>
+

默认值 false,没有兼容性问题,可以继承(包裹的子元素),可以覆盖(后来设置的覆盖前面设置的) 常见误区:

html
<div contenteditable="true">
+  <span contenteditable="false">姓名:</span>Panda<br />
+  <span contenteditable="false">姓别:</span>男<br />
+  <!-- 会导致删除br等标签 -->
+</div>
+
<div contenteditable="true">
+  <span contenteditable="false">姓名:</span>Panda<br />
+  <span contenteditable="false">姓别:</span>男<br />
+  <!-- 会导致删除br等标签 -->
+</div>
+

总结:实战开发可用属性:contenteditable, placeholder

标签篇_语义化标签

全是 div,只是语义化

html
<header></header>
+<footer></footer>
+<nav></nav>
+<article></article>
+<!--文章,可以直接被引用拿走的-->
+<section></section>
+<!--段落结构,一般section放在article里面-->
+<aside></aside>
+<!--侧边栏-->
+
<header></header>
+<footer></footer>
+<nav></nav>
+<article></article>
+<!--文章,可以直接被引用拿走的-->
+<section></section>
+<!--段落结构,一般section放在article里面-->
+<aside></aside>
+<!--侧边栏-->
+

audio 与 video 播放器

html
<audio src="" controls></audio> <video src="" controls></video>
+
<audio src="" controls></audio> <video src="" controls></video>
+

以上:太丑,不同浏览器不统一

视频播放器

加载不出来https://blog.csdn.net/qq_40340478/article/details/108309492

只有 http 协议中视频资源带有 Content-Range 属性,才能设置时间进行跳转

html
var express = require("express"); var app = new express();
+app.use(express.static('./')); app.listen(12306); // 如果不能改变进度条
+就用第36节的黑科技 访问127.0.0.1:12306/test.html
+
var express = require("express"); var app = new express();
+app.use(express.static('./')); app.listen(12306); // 如果不能改变进度条
+就用第36节的黑科技 访问127.0.0.1:12306/test.html
+

geolocation

html
<script>
+  // 获取地理信息
+  // 一些系统,不支持这个功能
+  // GPS定位。台式机几乎都没有GPS,笔记本大多数没有GPS,智能手机几乎都有GPS
+  // 网络定位 来粗略估计地理位置
+  window.navigator.geolocation.getCurrentPosition(
+    function (position) {
+      console.log("======"); //成功的回调函数
+      console.log(position);
+    },
+    function () {
+      //失败的回调函数
+      console.log("++++++");
+    }
+  );
+  //可以访问的方式:https协议,file协议,http协议下不能获取
+  // 经度最大值180,纬度最大值90
+</script>
+
<script>
+  // 获取地理信息
+  // 一些系统,不支持这个功能
+  // GPS定位。台式机几乎都没有GPS,笔记本大多数没有GPS,智能手机几乎都有GPS
+  // 网络定位 来粗略估计地理位置
+  window.navigator.geolocation.getCurrentPosition(
+    function (position) {
+      console.log("======"); //成功的回调函数
+      console.log(position);
+    },
+    function () {
+      //失败的回调函数
+      console.log("++++++");
+    }
+  );
+  //可以访问的方式:https协议,file协议,http协议下不能获取
+  // 经度最大值180,纬度最大值90
+</script>
+

https 还是不能访问:

  1. 谷歌浏览器打开谷歌地图,无法定位
  2. 利用翻墙可以实现

四行写个服务器

手机访问电脑 sever.js npm init npm i express

css
var express = require('express');
+var app = new express();
+app.use(express.static("./page"));
+app.listen(12306);//端口号大于8000或者等于80
+// 默认访问80端口,express默认访问index.html
+想访问里面的hello.html
+127.0.0.1:12306
+127.0.0.1:12306/hello.html
+
var express = require('express');
+var app = new express();
+app.use(express.static("./page"));
+app.listen(12306);//端口号大于8000或者等于80
+// 默认访问80端口,express默认访问index.html
+想访问里面的hello.html
+127.0.0.1:12306
+127.0.0.1:12306/hello.html
+

test.7z 命令框或者 vscode 客户端,进入项目路径,node server.js

deviceorientation

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <link rel="stylesheet" href="" />
+  </head>
+  <body>
+    <div id="main"></div>
+    <script>
+      // 陀螺仪,只有支持陀螺仪的设备才支持体感
+      // 苹果设备的页面只有在https协议下,才能使用这些接口
+      // 11.1.X以及之前,可以使用。微信的浏览器
+
+      // alpha:指北(指南针) [0,360) 当为0的时候指北。180指南
+      // beta:平放的时候beta值为0。当手机立起来(短边接触桌面),直立的时候beta为90;
+      // gamma:平放的时候gamma值为零。手机立起来(长边接触桌面),直立的时候gamma值为90
+
+      window.addEventListener("deviceorientation", function (event) {
+        // console.log(event);
+        document.getElementById("main").innerHTML =
+          "alpha:" +
+          event.alpha +
+          "<br/>" +
+          "beta:" +
+          event.beta +
+          "<br/>" +
+          "gamma:" +
+          event.gamma;
+      });
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <link rel="stylesheet" href="" />
+  </head>
+  <body>
+    <div id="main"></div>
+    <script>
+      // 陀螺仪,只有支持陀螺仪的设备才支持体感
+      // 苹果设备的页面只有在https协议下,才能使用这些接口
+      // 11.1.X以及之前,可以使用。微信的浏览器
+
+      // alpha:指北(指南针) [0,360) 当为0的时候指北。180指南
+      // beta:平放的时候beta值为0。当手机立起来(短边接触桌面),直立的时候beta为90;
+      // gamma:平放的时候gamma值为零。手机立起来(长边接触桌面),直立的时候gamma值为90
+
+      window.addEventListener("deviceorientation", function (event) {
+        // console.log(event);
+        document.getElementById("main").innerHTML =
+          "alpha:" +
+          event.alpha +
+          "<br/>" +
+          "beta:" +
+          event.beta +
+          "<br/>" +
+          "gamma:" +
+          event.gamma;
+      });
+    </script>
+  </body>
+</html>
+

手机访问电脑

1.手机和电脑在同一个局域网下 2.获取电脑的 IP 地址 windows 获取 ip:终端输入 ipconfig 3.在手机上输入相应的 IP 和端口进行访问

devicemotion

html
<script>
+  // 摇一摇
+  window.addEventListener("devicemotion", function (event) {
+    document.getElementById("main").innerHTML =
+      event.accelertion.x +
+      "<br/>" +
+      event.accelertion.y +
+      "<br/>" +
+      event.accelertion.z;
+    if (
+      Math.abs(event.accelertion.x) > 9 ||
+      Math.abs(event.accelertion.y) > 9 ||
+      Math.abs(event.accelertion.z) > 9
+    ) {
+      alert("在晃");
+    }
+  });
+</script>
+
<script>
+  // 摇一摇
+  window.addEventListener("devicemotion", function (event) {
+    document.getElementById("main").innerHTML =
+      event.accelertion.x +
+      "<br/>" +
+      event.accelertion.y +
+      "<br/>" +
+      event.accelertion.z;
+    if (
+      Math.abs(event.accelertion.x) > 9 ||
+      Math.abs(event.accelertion.y) > 9 ||
+      Math.abs(event.accelertion.z) > 9
+    ) {
+      alert("在晃");
+    }
+  });
+</script>
+

requestAnimationFrame

javascript
/*
+    function move(){
+    	var square = document.getElementById("main");
+    	if(square.offsetLeft > 700){
+    		return;
+    	}
+    	square.style.left = square.offsetLeft + 20 +"px";  
+    }
+    setInterval(move, 10);
+    */
+// 屏幕刷新频率:每秒60次
+// 如果变化一秒超过60次,就会有动画针会被丢掉
+
+// 实现均匀移动,用requestAnimationFrame,是每秒60针
+// 将计就计setInterval(move, 1000/60);会实现同样效果吗
+// 1针少于1/60秒,requestAnimationFrame可以准时执行每一帧的
+// requestAnimationFrame(move);//移动一次
+var timer = null;
+function move() {
+  var square = document.getElementById("main");
+  if (square.offsetLeft > 700) {
+    cancelAnimationFrame(timer);
+    return;
+  }
+  square.style.left = square.offsetLeft + 20 + "px";
+  timer = requestAnimationFrame(move);
+}
+move();
+// cancelAnimationFrame基本相当于clearTimeout
+// requestAnimationFrame兼容性极差
+
/*
+    function move(){
+    	var square = document.getElementById("main");
+    	if(square.offsetLeft > 700){
+    		return;
+    	}
+    	square.style.left = square.offsetLeft + 20 +"px";  
+    }
+    setInterval(move, 10);
+    */
+// 屏幕刷新频率:每秒60次
+// 如果变化一秒超过60次,就会有动画针会被丢掉
+
+// 实现均匀移动,用requestAnimationFrame,是每秒60针
+// 将计就计setInterval(move, 1000/60);会实现同样效果吗
+// 1针少于1/60秒,requestAnimationFrame可以准时执行每一帧的
+// requestAnimationFrame(move);//移动一次
+var timer = null;
+function move() {
+  var square = document.getElementById("main");
+  if (square.offsetLeft > 700) {
+    cancelAnimationFrame(timer);
+    return;
+  }
+  square.style.left = square.offsetLeft + 20 + "px";
+  timer = requestAnimationFrame(move);
+}
+move();
+// cancelAnimationFrame基本相当于clearTimeout
+// requestAnimationFrame兼容性极差
+

兼容性极差还想使用咋办?

javascript
window.cancelAnimationFrame = (function () {
+  return (
+    window.cancelAnimationFrame ||
+    window.webkitCancelAnimationFrame ||
+    window.mozCancelAnimationFrame ||
+    function (id) {
+      window.clearTimeOut(id);
+    }
+  );
+})();
+
+window.requestAnimationFrame = (function () {
+  return (
+    window.requestAnimationFrame ||
+    window.webkitRequestAnimationFrame ||
+    window.mozRequestAnimationFrame ||
+    function (id) {
+      window.setTimeOut(id, 1000 / 60);
+    }
+  );
+})();
+
window.cancelAnimationFrame = (function () {
+  return (
+    window.cancelAnimationFrame ||
+    window.webkitCancelAnimationFrame ||
+    window.mozCancelAnimationFrame ||
+    function (id) {
+      window.clearTimeOut(id);
+    }
+  );
+})();
+
+window.requestAnimationFrame = (function () {
+  return (
+    window.requestAnimationFrame ||
+    window.webkitRequestAnimationFrame ||
+    window.mozRequestAnimationFrame ||
+    function (id) {
+      window.setTimeOut(id, 1000 / 60);
+    }
+  );
+})();
+

localStrorage

cookie:每次请求都有可能传送许多无用的信息到后端 localStroage:长期存放在浏览器,无论窗口是否关闭

javascript
localStorage.name = "panda";
+// localStroage.arr = [1, 2, 3];
+// console.log(localStroage.arr);
+// localStroage只能存储字符串,
+
+要想存储数组;
+localStorage.arr = JSON.stringify([1, 2, 3]);
+// console.log(localStorage.arr);
+console.log(JSON.parse(localStorage.arr));
+
+localStorage.obj = JSON.stringify({
+  name: "panda",
+  age: 18,
+});
+console.log(JSON.parse(localStorage.obj));
+
localStorage.name = "panda";
+// localStroage.arr = [1, 2, 3];
+// console.log(localStroage.arr);
+// localStroage只能存储字符串,
+
+要想存储数组;
+localStorage.arr = JSON.stringify([1, 2, 3]);
+// console.log(localStorage.arr);
+console.log(JSON.parse(localStorage.arr));
+
+localStorage.obj = JSON.stringify({
+  name: "panda",
+  age: 18,
+});
+console.log(JSON.parse(localStorage.obj));
+

sessionStroage:这次回话临时需要存储时的变量。每次窗口关闭的时候,seccionStroage 自动清空

html
sessionStorage.name = "panda";
+
sessionStorage.name = "panda";
+

localStorage 与 cookie

1.localStorage 在发送请求的时候不会把数据发出去,cookie 会把所有数据带出去    2.cookie 存储的内容比较(4k) ,localStroage 可以存放较多内容(5M)

另一种写法

html
localStorage.setItem("name", "monkey"); localStorage.getItem("name");
+localStorage.removeItem("name");
+
localStorage.setItem("name", "monkey"); localStorage.getItem("name");
+localStorage.removeItem("name");
+

相同协议,相同域名,相同端口称为一个域

注意:

www.baidu.com不是一个域。 http://www.baidu.com  https://www.baidu.com,是域。这是不同域

history

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <style type="text/css"></style>
+    <script>
+      // A -> B - C
+      // 为了网页的性能,单页面操作
+      var data = [
+        {
+          name: "HTML",
+        },
+        {
+          name: "CSS",
+        },
+        {
+          name: "JS",
+        },
+        {
+          name: "panda",
+        },
+        {
+          name: "dengge",
+        },
+      ];
+      function search() {
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+        history.pushState({ inpVal: value }, null, "#" + value);
+      }
+      function render(renderData) {
+        var content = "";
+        for (var i = 0; i < renderData.length; i++) {
+          content += "<div>" + renderData[i].name + "</div>";
+        }
+        document.getElementById("main").innerHTML = content;
+      }
+      window.addEventListener("popstate", function (e) {
+        // console.log(e);
+        document.getElementById("search").value = e.state.inpVal
+          ? e.state.inpVal
+          : "";
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+      });
+      // 关于popstate和hashchange
+      // 只要url变了,就会触发popstate
+      // 锚点变了(hash值变了),就会触发hashchange
+      window.addEventListener("hashchange", function (e) {
+        console.log(e);
+      });
+    </script>
+  </head>
+
+  <body>
+    <input type="text" id="search" /><button onclick="search()">搜索</button>
+    <div id="main"></div>
+    <script>
+      render(data);
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>document</title>
+    <style type="text/css"></style>
+    <script>
+      // A -> B - C
+      // 为了网页的性能,单页面操作
+      var data = [
+        {
+          name: "HTML",
+        },
+        {
+          name: "CSS",
+        },
+        {
+          name: "JS",
+        },
+        {
+          name: "panda",
+        },
+        {
+          name: "dengge",
+        },
+      ];
+      function search() {
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+        history.pushState({ inpVal: value }, null, "#" + value);
+      }
+      function render(renderData) {
+        var content = "";
+        for (var i = 0; i < renderData.length; i++) {
+          content += "<div>" + renderData[i].name + "</div>";
+        }
+        document.getElementById("main").innerHTML = content;
+      }
+      window.addEventListener("popstate", function (e) {
+        // console.log(e);
+        document.getElementById("search").value = e.state.inpVal
+          ? e.state.inpVal
+          : "";
+        var value = document.getElementById("search").value;
+        var result = data.filter(function (obj) {
+          if (obj.name.indexOf(value) > -1) {
+            return obj;
+          }
+        });
+        render(result);
+      });
+      // 关于popstate和hashchange
+      // 只要url变了,就会触发popstate
+      // 锚点变了(hash值变了),就会触发hashchange
+      window.addEventListener("hashchange", function (e) {
+        console.log(e);
+      });
+    </script>
+  </head>
+
+  <body>
+    <input type="text" id="search" /><button onclick="search()">搜索</button>
+    <div id="main"></div>
+    <script>
+      render(data);
+    </script>
+  </body>
+</html>
+

worker

html
<script>
+  // js都是单线程的
+  // worker是多线程的, 真的多线程, 不是伪多线程
+  // worker不能操作DOM,没有window对象,不能读取本地文件。可以发ajax,可以计算
+  // 在worker中可以创建worker吗?
+  // 在理论上可以,但是没有一款浏览器支持
+  /*
+        console.log("=======");
+        console.log("=======");
+        var a = 1000;
+        var result = 0;
+        for (var i = 0; i < a; i++) {
+            result += i;
+        }
+        console.log(result);
+        console.log("=======");
+        console.log("=======");
+        */
+  // 后两个等号只有等待算完才执行
+
+  var beginTime = Data.now();
+  console.log("=======");
+  console.log("=======");
+  var a = 1000;
+  var worker = new Worker("./worker.js");
+  worker.postMessage({
+    num: a,
+  });
+  worker.onmessage = function (e) {
+    console.log(e.data);
+  };
+  console.log("=======");
+  console.log("=======");
+  var endTime = Data.now();
+  console.log(endTime - beginTime);
+  worker.terminate(); //停止
+  this.close(); //自己停止
+</script>
+
<script>
+  // js都是单线程的
+  // worker是多线程的, 真的多线程, 不是伪多线程
+  // worker不能操作DOM,没有window对象,不能读取本地文件。可以发ajax,可以计算
+  // 在worker中可以创建worker吗?
+  // 在理论上可以,但是没有一款浏览器支持
+  /*
+        console.log("=======");
+        console.log("=======");
+        var a = 1000;
+        var result = 0;
+        for (var i = 0; i < a; i++) {
+            result += i;
+        }
+        console.log(result);
+        console.log("=======");
+        console.log("=======");
+        */
+  // 后两个等号只有等待算完才执行
+
+  var beginTime = Data.now();
+  console.log("=======");
+  console.log("=======");
+  var a = 1000;
+  var worker = new Worker("./worker.js");
+  worker.postMessage({
+    num: a,
+  });
+  worker.onmessage = function (e) {
+    console.log(e.data);
+  };
+  console.log("=======");
+  console.log("=======");
+  var endTime = Data.now();
+  console.log(endTime - beginTime);
+  worker.terminate(); //停止
+  this.close(); //自己停止
+</script>
+

worker.js

javascript
this.onmessage = function (e) {
+  //接受消息
+  // console.log(e);
+  var result = 0;
+  for (var i = 0; i < e.data.num; i++) {
+    result += i;
+  }
+  this.postMessage(result);
+};
+
this.onmessage = function (e) {
+  //接受消息
+  // console.log(e);
+  var result = 0;
+  for (var i = 0; i < e.data.num; i++) {
+    result += i;
+  }
+  this.postMessage(result);
+};
+

worker.js 里面可以通过 importScripts("./index.js")引入外部 js 文件

+ + + + + \ No newline at end of file diff --git a/html-css/animation.html b/html-css/animation.html new file mode 100644 index 00000000..218e44ba --- /dev/null +++ b/html-css/animation.html @@ -0,0 +1,1580 @@ + + + + + + 动画 | Sunny's blog + + + + + + + + +
Skip to content
On this page

动画

1.transition 过渡动画

css
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* transition: transition-property, transition-duration,transition-timing-function,transition-delay; */
+  /* transition-property: all; */
+  /* 第一个监听属性 */
+  /* transition-property: width, height; */
+  /* transition-duration: ; */
+  /* 第二个时间间隔 */
+  /* transition-timing-function:linear ; */
+  /* 第三个运动状态 */
+  /* transition-delay: ; */
+  /* 第四个延迟 */
+  transition: width 2s linear 1s;
+  /* 总共3s,1s后开始宽度增加,增加2s */
+  /* 添加方式
+    前两个必须有,后两个默认
+    */
+}
+div:hover {
+  width: 200px;
+  height: 200px;
+}
+
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* transition: transition-property, transition-duration,transition-timing-function,transition-delay; */
+  /* transition-property: all; */
+  /* 第一个监听属性 */
+  /* transition-property: width, height; */
+  /* transition-duration: ; */
+  /* 第二个时间间隔 */
+  /* transition-timing-function:linear ; */
+  /* 第三个运动状态 */
+  /* transition-delay: ; */
+  /* 第四个延迟 */
+  transition: width 2s linear 1s;
+  /* 总共3s,1s后开始宽度增加,增加2s */
+  /* 添加方式
+    前两个必须有,后两个默认
+    */
+}
+div:hover {
+  width: 200px;
+  height: 200px;
+}
+

动画 demo

css
div {
+  position: absolute;
+  left: 0;
+  top: 0;
+  opacity: 0.5;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  transition: all 2s;
+}
+
+div:hover {
+  opacity: 1;
+  top: 100px;
+  left: 100px;
+  height: 200px;
+  width: 200px;
+}
+
div {
+  position: absolute;
+  left: 0;
+  top: 0;
+  opacity: 0.5;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  transition: all 2s;
+}
+
+div:hover {
+  opacity: 1;
+  top: 100px;
+  left: 100px;
+  height: 200px;
+  width: 200px;
+}
+

2.cubic-bezier

css
div {
+  width: 100px;
+  height: 50px;
+  border: 1px solid red;
+  border-radius: 10px;
+  transition: all 1s cubic-bezier(0.5, -1.5, 0.8, 2); /*代表两个坐标点
+    	x:(0,1)
+    	y:都可
+    */
+}
+div:hover {
+  width: 300px;
+}
+
div {
+  width: 100px;
+  height: 50px;
+  border: 1px solid red;
+  border-radius: 10px;
+  transition: all 1s cubic-bezier(0.5, -1.5, 0.8, 2); /*代表两个坐标点
+    	x:(0,1)
+    	y:都可
+    */
+}
+div:hover {
+  width: 300px;
+}
+

3.animation

css
@keyframes run {
+  0% {
+    /* 0%相当于from */
+    left: 0;
+    top: 0;
+    /* background-color: red; */
+  }
+  25% {
+    left: 100px;
+    top: 0;
+    /* background-color: green; */
+  }
+  50% {
+    left: 100px;
+    top: 100px;
+    /* background-color: blue; */
+  }
+  75% {
+    left: 0;
+    top: 100px;
+    /* background-color: coral; */
+  }
+  100% {
+    /* 100%相当于to  */
+    left: 0;
+    top: 0;
+  }
+}
+@keyframes color-change {
+  0% {
+    background-color: red;
+  }
+  60% {
+    background-color: blue;
+  }
+  100% {
+    background-color: black;
+  }
+}
+div {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* animation: run 4s; */
+  /* animation: run 4s, color-change 4s;两个动画同时运行 */
+  /* animation: run 4s cubic-bezier(.5,1,1,1);其实是是每一段的运动状态 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s;延迟1s执行动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2; 执行2次动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s infinite; 死循环 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s reverse;倒着走 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2 alternate;先证者走,在倒着走,意味着次数>=2(单摆) */
+}
+
@keyframes run {
+  0% {
+    /* 0%相当于from */
+    left: 0;
+    top: 0;
+    /* background-color: red; */
+  }
+  25% {
+    left: 100px;
+    top: 0;
+    /* background-color: green; */
+  }
+  50% {
+    left: 100px;
+    top: 100px;
+    /* background-color: blue; */
+  }
+  75% {
+    left: 0;
+    top: 100px;
+    /* background-color: coral; */
+  }
+  100% {
+    /* 100%相当于to  */
+    left: 0;
+    top: 0;
+  }
+}
+@keyframes color-change {
+  0% {
+    background-color: red;
+  }
+  60% {
+    background-color: blue;
+  }
+  100% {
+    background-color: black;
+  }
+}
+div {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  /* animation: run 4s; */
+  /* animation: run 4s, color-change 4s;两个动画同时运行 */
+  /* animation: run 4s cubic-bezier(.5,1,1,1);其实是是每一段的运动状态 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s;延迟1s执行动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2; 执行2次动画 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s infinite; 死循环 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s reverse;倒着走 */
+  /* animation: run 4s cubic-bezier(.5, 1, 1, 1) 1s 2 alternate;先证者走,在倒着走,意味着次数>=2(单摆) */
+}
+

animation-fill-mode:

forwards: 设置对象状态为动画结束时候的状态

backwards:设置对象状态为动画开始时候第一针的状态

both:设置对象状态为动画结束和开始的状态的综合体

太阳月亮 demo——文件夹

4.step 跳转动画

css
@keyframes change-color {
+  0% {
+    background-color: red;
+  }
+
+  25% {
+    background-color: green;
+  }
+
+  50% {
+    background-color: blue;
+  }
+
+  75% {
+    background-color: black;
+  }
+
+  100% {
+    background-color: #fff;
+  }
+}
+div {
+  width: 100px;
+  height: 100px;
+  background-color: rgb(247, 20, 247);
+  /* animation: change-color 4s steps(1, end); */
+  /* 不过度 一步到位 */
+  /* animation: change-color 4s steps(2, end); */
+  /* 每一段用2步实现,动画更加细腻了 */
+  /* animation: change-color 4s steps(1, start); */
+  /* start与end 
+    	end保留当前帧状态,直到这个动画时间结束    忽略最后一针
+    	start保留下一针状态,直到这段动画时间结束   忽略第一针
+    */
+  /* 要想弥补时间段   想看见最后一针
+    	animation: change-color 4s steps(1, end) forwards;
+    */
+}
+
@keyframes change-color {
+  0% {
+    background-color: red;
+  }
+
+  25% {
+    background-color: green;
+  }
+
+  50% {
+    background-color: blue;
+  }
+
+  75% {
+    background-color: black;
+  }
+
+  100% {
+    background-color: #fff;
+  }
+}
+div {
+  width: 100px;
+  height: 100px;
+  background-color: rgb(247, 20, 247);
+  /* animation: change-color 4s steps(1, end); */
+  /* 不过度 一步到位 */
+  /* animation: change-color 4s steps(2, end); */
+  /* 每一段用2步实现,动画更加细腻了 */
+  /* animation: change-color 4s steps(1, start); */
+  /* start与end 
+    	end保留当前帧状态,直到这个动画时间结束    忽略最后一针
+    	start保留下一针状态,直到这段动画时间结束   忽略第一针
+    */
+  /* 要想弥补时间段   想看见最后一针
+    	animation: change-color 4s steps(1, end) forwards;
+    */
+}
+

区别 end 与 start

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes run {
+        0% {
+          left: 0;
+        }
+
+        25% {
+          left: 100px;
+        }
+
+        50% {
+          left: 200px;
+        }
+
+        75% {
+          left: 300px;
+        }
+
+        100% {
+          left: 400px;
+        }
+      }
+
+      .demo1,
+      .demo2 {
+        position: absolute;
+        left: 0;
+        background-color: black;
+        width: 100px;
+        height: 100px;
+        color: #fff;
+      }
+
+      .demo1 {
+        animation: run 4s steps(1, start);
+      }
+
+      .demo2 {
+        top: 100px;
+        /* animation: run 4s steps(1, end); */
+        animation: run 4s steps(1, end) forwards;
+      }
+
+      /* 
+            steps(1,end);===step-end
+            steps(1,start);===step-start
+            */
+    </style>
+  </head>
+
+  <body>
+    <div class="demo1">start</div>
+    <div class="demo2">end</div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes run {
+        0% {
+          left: 0;
+        }
+
+        25% {
+          left: 100px;
+        }
+
+        50% {
+          left: 200px;
+        }
+
+        75% {
+          left: 300px;
+        }
+
+        100% {
+          left: 400px;
+        }
+      }
+
+      .demo1,
+      .demo2 {
+        position: absolute;
+        left: 0;
+        background-color: black;
+        width: 100px;
+        height: 100px;
+        color: #fff;
+      }
+
+      .demo1 {
+        animation: run 4s steps(1, start);
+      }
+
+      .demo2 {
+        top: 100px;
+        /* animation: run 4s steps(1, end); */
+        animation: run 4s steps(1, end) forwards;
+      }
+
+      /* 
+            steps(1,end);===step-end
+            steps(1,start);===step-start
+            */
+    </style>
+  </head>
+
+  <body>
+    <div class="demo1">start</div>
+    <div class="demo2">end</div>
+  </body>
+</html>
+

打字效果

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes cursor {
+        0% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+
+        50% {
+          border-left-color: rgba(0, 0, 0, 1);
+        }
+
+        100% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+      }
+
+      @keyframes cover {
+        0% {
+          left: 0;
+        }
+
+        100% {
+          left: 100%;
+        }
+      }
+
+      div {
+        position: relative;
+        display: inline-block;
+        height: 100px;
+        /* background-color: red; */
+        font-size: 80px;
+        line-height: 100px;
+        font-family: monospace;
+      }
+
+      div::after {
+        content: "";
+        position: absolute;
+        left: 0;
+        top: 10px;
+        height: 90px;
+        width: 100%;
+        background-color: #fff;
+        border-left: 2px solid black;
+        box-sizing: border-box;
+        animation: cursor 1s steps(1, end) infinite, cover 12s steps(12, end);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div>sdknajnakjna</div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      @keyframes cursor {
+        0% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+
+        50% {
+          border-left-color: rgba(0, 0, 0, 1);
+        }
+
+        100% {
+          border-left-color: rgba(0, 0, 0, 0);
+        }
+      }
+
+      @keyframes cover {
+        0% {
+          left: 0;
+        }
+
+        100% {
+          left: 100%;
+        }
+      }
+
+      div {
+        position: relative;
+        display: inline-block;
+        height: 100px;
+        /* background-color: red; */
+        font-size: 80px;
+        line-height: 100px;
+        font-family: monospace;
+      }
+
+      div::after {
+        content: "";
+        position: absolute;
+        left: 0;
+        top: 10px;
+        height: 90px;
+        width: 100%;
+        background-color: #fff;
+        border-left: 2px solid black;
+        box-sizing: border-box;
+        animation: cursor 1s steps(1, end) infinite, cover 12s steps(12, end);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div>sdknajnakjna</div>
+  </body>
+</html>
+

表盘效果——文件夹

跑马效果

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      @keyframes run {
+        0% {
+          background-position: 0 0;
+        }
+
+        100% {
+          background-position: -2400px 0;
+        }
+      }
+      div {
+        width: 200px;
+        height: 100px;
+        background-image: url(./web/horse.png);
+        background-repeat: no-repeat;
+        background-position: 0 0;
+        animation: run 0.3s steps(12, end) infinite;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="horse"></div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      @keyframes run {
+        0% {
+          background-position: 0 0;
+        }
+
+        100% {
+          background-position: -2400px 0;
+        }
+      }
+      div {
+        width: 200px;
+        height: 100px;
+        background-image: url(./web/horse.png);
+        background-repeat: no-repeat;
+        background-position: 0 0;
+        animation: run 0.3s steps(12, end) infinite;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="horse"></div>
+  </body>
+</html>
+

5.rotate3D 变换

rotate:2d 变换

rotateX,rotateY,rotateZ:3d 变换

旋转

css
@keyframes round {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform: rotate(0deg);
+  /* transform-origin: center center; */
+  /* 圆心给谁设置,参考的就是谁 */
+  transform-origin: 0 0;
+  animation: round 2s infinite;
+}
+
@keyframes round {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform: rotate(0deg);
+  /* transform-origin: center center; */
+  /* 圆心给谁设置,参考的就是谁 */
+  transform-origin: 0 0;
+  animation: round 2s infinite;
+}
+

3D 变换

css
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  /* transform: rotateX(0deg); */
+  /* transform: rotateX(0deg) rotateY(0deg); */
+  /* 旋转顺序不同,结果不同 */
+}
+
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  /* transform: rotateX(0deg); */
+  /* transform: rotateX(0deg) rotateY(0deg); */
+  /* 旋转顺序不同,结果不同 */
+}
+

rotate3d()

transform: rotate3d(x, y, z, angle); 矢量和作为旋转轴

小练习图片钟摆效果

css
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes change {
+  0% {
+    transform: rotateX(-45deg) rotateY(90deg);
+  }
+
+  100% {
+    transform: rotateX(45deg) rotateY(90deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  animation: change 2s cubic-bezier(0.5, 0, 0.5, 1) infinite alternate;
+}
+
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes change {
+  0% {
+    transform: rotateX(-45deg) rotateY(90deg);
+  }
+
+  100% {
+    transform: rotateX(45deg) rotateY(90deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: 0 0;
+  animation: change 2s cubic-bezier(0.5, 0, 0.5, 1) infinite alternate;
+}
+

6.scale 伸缩

css
/* transform: scale(1, 2); 2d x,y轴*/
+/* 
+    transform: scaleX();
+    transform: scaleY();
+    transform: scalez();
+    transform: scale3d();  就是叠加 
+*/
+/* scale:
+    1.伸缩元素变化坐标轴的刻度 ,translateX,Y验证。伸缩之后,translateX(100)产生的效果是200
+    2.设置两次,则第二次在第一次基础上叠加
+    3.旋转伸缩在一个轴进行
+		4.雁过留声  伸缩过的影响一直保留 
+*/
+/* transform: scale() rotate();位置讲究  先后scale不一样*/
+
/* transform: scale(1, 2); 2d x,y轴*/
+/* 
+    transform: scaleX();
+    transform: scaleY();
+    transform: scalez();
+    transform: scale3d();  就是叠加 
+*/
+/* scale:
+    1.伸缩元素变化坐标轴的刻度 ,translateX,Y验证。伸缩之后,translateX(100)产生的效果是200
+    2.设置两次,则第二次在第一次基础上叠加
+    3.旋转伸缩在一个轴进行
+		4.雁过留声  伸缩过的影响一直保留 
+*/
+/* transform: scale() rotate();位置讲究  先后scale不一样*/
+

7.skew 倾斜

skew(x, y);

skewx();

skewy();

demo

css
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes skewchange {
+  0% {
+    transform: skew(45deg, 45deg);
+  }
+
+  50% {
+    transform: skew(0, 0);
+  }
+
+  100% {
+    transform: skew(-45deg, -45deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: center center;
+  /* transform: skew(0deg, 0deg); */
+  /* 倾斜的不是元素本身,而是坐标轴
+    坐标轴倾斜,刻度被拉伸 */
+  /* 
+        skew(x,y);
+        skewx()
+        skewy() 
+    */
+  animation: skewchange 4s cubic-bezier(0, 0, 1, 1) infinite alternate;
+}
+
body {
+  perspective: 800px;
+  transform-style: preserve-3d;
+  perspective-origin: 300px 300px;
+}
+
+@keyframes skewchange {
+  0% {
+    transform: skew(45deg, 45deg);
+  }
+
+  50% {
+    transform: skew(0, 0);
+  }
+
+  100% {
+    transform: skew(-45deg, -45deg);
+  }
+}
+
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(./source/pic6.jpeg);
+  background-size: cover;
+  transform-origin: center center;
+  /* transform: skew(0deg, 0deg); */
+  /* 倾斜的不是元素本身,而是坐标轴
+    坐标轴倾斜,刻度被拉伸 */
+  /* 
+        skew(x,y);
+        skewx()
+        skewy() 
+    */
+  animation: skewchange 4s cubic-bezier(0, 0, 1, 1) infinite alternate;
+}
+

8.translate+perspective

2d translate(x, y) translatex() translatey() translatex() translate3d()

css
/* transform: translate(100px); */
+/* transform: translate3d(100px, 100px 100px); */
+
+/* 
+	transform: translatex()
+	translatez() 
+*/
+transform: rotatey(90deg) translatez(100px);
+transform: translatez(100px) rotatey(90deg);
+
/* transform: translate(100px); */
+/* transform: translate3d(100px, 100px 100px); */
+
+/* 
+	transform: translatex()
+	translatez() 
+*/
+transform: rotatey(90deg) translatez(100px);
+transform: translatez(100px) rotatey(90deg);
+

小应用:calc(50% - 0.5*宽高),不知道宽高:

css
left: 50%;
+transform: translatex(-50%) 半个身位;
+
left: 50%;
+transform: translatex(-50%) 半个身位;
+

关于 translatez()

css
body {
+  perspective: 800px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*transform: translatez(100px) rotatey(90deg);*/
+  /*z没起作用?旋转晚了*/
+  transform: rotatey(90deg) translatez(100px);
+}
+
body {
+  perspective: 800px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*transform: translatez(100px) rotatey(90deg);*/
+  /*z没起作用?旋转晚了*/
+  transform: rotatey(90deg) translatez(100px);
+}
+

demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root {
+        height: 100%;
+      }
+
+      body {
+        perspective: 800px;
+        height: 100%; //需要有高度才能实现鼠标移动到哪里都能触发事件
+      }
+
+      .content1,
+      .content2,
+      .content3,
+      .content4,
+      .content5 {
+        width: 200px;
+        height: 200px;
+        background-image: url(./source/pic3.jpeg);
+        background-size: cover;
+        position: absolute;
+        top: 200px;
+        transform: rotateY(45deg);
+      }
+
+      .content1 {
+        left: 200px;
+      }
+
+      .content2 {
+        left: 400px;
+      }
+
+      .content3 {
+        left: 600px;
+      }
+
+      .content4 {
+        left: 800px;
+      }
+
+      .content5 {
+        left: 1000px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="content1"></div>
+    <div class="content2"></div>
+    <div class="content3"></div>
+    <div class="content4"></div>
+    <div class="content5"></div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root {
+        height: 100%;
+      }
+
+      body {
+        perspective: 800px;
+        height: 100%; //需要有高度才能实现鼠标移动到哪里都能触发事件
+      }
+
+      .content1,
+      .content2,
+      .content3,
+      .content4,
+      .content5 {
+        width: 200px;
+        height: 200px;
+        background-image: url(./source/pic3.jpeg);
+        background-size: cover;
+        position: absolute;
+        top: 200px;
+        transform: rotateY(45deg);
+      }
+
+      .content1 {
+        left: 200px;
+      }
+
+      .content2 {
+        left: 400px;
+      }
+
+      .content3 {
+        left: 600px;
+      }
+
+      .content4 {
+        left: 800px;
+      }
+
+      .content5 {
+        left: 1000px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="content1"></div>
+    <div class="content2"></div>
+    <div class="content3"></div>
+    <div class="content4"></div>
+    <div class="content5"></div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+

与 perspective 类似的一个东西: transform: perspective(800px) rotateY(45deg); 写在前面并且元素本身,眼睛就在 center 不能调 perspective 在父级才有效 景深可以叠加 深入理解 perspevtive

css
body {
+  perspective: 800px; /*移动眼睛*/
+  perspective-origin: 300px 300px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*移动物体*/
+  /*transform: translatez(100px);*/
+  /*快接近800就见不到了,快后脑勺了*/
+  transform: translatez(100px);
+}
+
body {
+  perspective: 800px; /*移动眼睛*/
+  perspective-origin: 300px 300px;
+}
+div {
+  position: absolute;
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+  background-image: url(demo/u.png);
+  background-size: cover;
+  /*移动物体*/
+  /*transform: translatez(100px);*/
+  /*快接近800就见不到了,快后脑勺了*/
+  transform: translatez(100px);
+}
+

深入理解 perspective

transform-style

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+      }
+
+      .wrapper {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-color: red;
+        transform: rotateY(0deg);
+        /*父级旋转会带儿子旋转,因为浏览器渲染不了,所以z轴体现不出来,想立体,给父级(不能祖父)加上transform-style: preserve-3d;*/
+      }
+
+      .demo {
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        transform: translateZ(100px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="demo"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+      }
+
+      .wrapper {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-color: red;
+        transform: rotateY(0deg);
+        /*父级旋转会带儿子旋转,因为浏览器渲染不了,所以z轴体现不出来,想立体,给父级(不能祖父)加上transform-style: preserve-3d;*/
+      }
+
+      .demo {
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        transform: translateZ(100px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="demo"></div>
+    </div>
+  </body>
+</html>
+

transform-origin

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+        transform-style: preserve-3d;
+      }
+
+      @keyframes move {
+        0% {
+          transform: rotateY(0deg);
+        }
+        100% {
+          transform: rotateY(360deg);
+        }
+      }
+      div {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        animation: move 2s linear infinite;
+        transform-origin: 100px 100px 100px;
+        /* 可以设置空间点中心旋转 */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div></div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      body {
+        perspective: 800px;
+        perspective-origin: 300px 100px;
+        transform-style: preserve-3d;
+      }
+
+      @keyframes move {
+        0% {
+          transform: rotateY(0deg);
+        }
+        100% {
+          transform: rotateY(360deg);
+        }
+      }
+      div {
+        position: absolute;
+        left: 200px;
+        top: 200px;
+        width: 200px;
+        height: 200px;
+        background-image: url(demo/u.png);
+        background-size: cover;
+        animation: move 2s linear infinite;
+        transform-origin: 100px 100px 100px;
+        /* 可以设置空间点中心旋转 */
+      }
+    </style>
+  </head>
+
+  <body>
+    <div></div>
+  </body>
+</html>
+

照片墙

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root,
+      body {
+        height: 100%;
+      }
+
+      body {
+        perspective: 3000px;
+        transform-style: preserve-3d;
+        /* 一旦设置了这两个属性当中一条,他就变成了定位的参照物元素,所以这里没高度的话就导致没高度了 */
+      }
+
+      @keyframes round {
+        /*让父级转    简单*/
+        0% {
+          transform: translate(-50%, -50%) rotateY(0deg);
+        }
+
+        100% {
+          transform: translate(-50%, -50%) rotateY(360deg);
+        }
+      }
+
+      div.wrapper {
+        position: absolute;
+        left: calc(50%);
+        top: calc(50%);
+        /* 写了top没生效的重要原因:父级没有高度,因为body,解决就把body上面 height打开 */
+        width: 300px;
+        height: 300px;
+        transform: translate(-50%, -50%);
+        transform-style: preserve-3d;
+        animation: round 5s linear infinite;
+      }
+
+      img {
+        position: absolute;
+        width: 300px;
+        /* backface-visibility: hidden; */
+        /* 图片背部 */
+      }
+
+      img:nth-of-type(1) {
+        transform: rotateY(45deg) translatez(800px);
+        /*沿着自己的z轴向外拓*/
+      }
+
+      img:nth-of-type(2) {
+        transform: rotateY(90deg) translatez(500px);
+        /* 可以改变y值,实现层叠 */
+      }
+
+      img:nth-of-type(3) {
+        transform: rotateY(135deg) translatez(500px);
+      }
+
+      img:nth-of-type(4) {
+        transform: rotateY(180deg) translatez(500px);
+      }
+
+      img:nth-of-type(5) {
+        transform: rotateY(225deg) translatez(500px);
+      }
+
+      img:nth-of-type(6) {
+        transform: rotateY(270deg) translatez(500px);
+      }
+
+      img:nth-of-type(7) {
+        transform: rotateY(315deg) translatez(500px);
+      }
+
+      img:nth-of-type(8) {
+        transform: rotateY(360deg) translatez(500px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+    </div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      :root,
+      body {
+        height: 100%;
+      }
+
+      body {
+        perspective: 3000px;
+        transform-style: preserve-3d;
+        /* 一旦设置了这两个属性当中一条,他就变成了定位的参照物元素,所以这里没高度的话就导致没高度了 */
+      }
+
+      @keyframes round {
+        /*让父级转    简单*/
+        0% {
+          transform: translate(-50%, -50%) rotateY(0deg);
+        }
+
+        100% {
+          transform: translate(-50%, -50%) rotateY(360deg);
+        }
+      }
+
+      div.wrapper {
+        position: absolute;
+        left: calc(50%);
+        top: calc(50%);
+        /* 写了top没生效的重要原因:父级没有高度,因为body,解决就把body上面 height打开 */
+        width: 300px;
+        height: 300px;
+        transform: translate(-50%, -50%);
+        transform-style: preserve-3d;
+        animation: round 5s linear infinite;
+      }
+
+      img {
+        position: absolute;
+        width: 300px;
+        /* backface-visibility: hidden; */
+        /* 图片背部 */
+      }
+
+      img:nth-of-type(1) {
+        transform: rotateY(45deg) translatez(800px);
+        /*沿着自己的z轴向外拓*/
+      }
+
+      img:nth-of-type(2) {
+        transform: rotateY(90deg) translatez(500px);
+        /* 可以改变y值,实现层叠 */
+      }
+
+      img:nth-of-type(3) {
+        transform: rotateY(135deg) translatez(500px);
+      }
+
+      img:nth-of-type(4) {
+        transform: rotateY(180deg) translatez(500px);
+      }
+
+      img:nth-of-type(5) {
+        transform: rotateY(225deg) translatez(500px);
+      }
+
+      img:nth-of-type(6) {
+        transform: rotateY(270deg) translatez(500px);
+      }
+
+      img:nth-of-type(7) {
+        transform: rotateY(315deg) translatez(500px);
+      }
+
+      img:nth-of-type(8) {
+        transform: rotateY(360deg) translatez(500px);
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+      <img src="demo/u.png" alt="" />
+    </div>
+    <script>
+      document.body.onmousemove = function (e) {
+        this.style.perspectiveOrigin = "" + e.pageX + "px " + e.pageY + "px";
+      };
+    </script>
+  </body>
+</html>
+

作业:3D 魔方 知识点是一样的

9.matrix

矩阵就是 transform 给咱们选中的计算规则 矩阵函数传的参数是矩阵的前两行

css
平移// translate
+| 1 0 e|			|x|			|x + e|
+| 0 1 f|   *  |y| =   |y + f|
+| 0 0 1|  		|z|			|1    |
+matrix(1,0,0,1,e,f); === translate(x, y);
+// scale
+| a,0,0 |     | x |      | ax |
+| 0,d,0 |  *  | y |   =  | dy |
+| 0,0,1 |     | 1 |      | 1  |
+matrix(a,0,0,d,0,0); === scale(x, y);
+// rotate
+matrix(cos(θ),sin(θ),-sin(θ),cos(θ),0,0); === rotate(θ);
+| cos(θ),-sin(θ),e |     | x |
+| sin(θ),cos(θ) ,f |  *  | y |
+| 0     ,0      ,1 |     | 1 |
+x1 = cos(θ)x - sin(θ)y + 0
+y2 = sin(θ)x + cos(θ)y + 0
+matrix(1,tan(θy),tan(θx),1,0,0)
+
+
+matrix(1,0,0,0,0,1,0,0,0,0,1,0,x,y,z,1) 缩放
+matrix(x,0,0,0,0,y,0,0,0,0,z,0,0,0,0,1) 平移
+
平移// translate
+| 1 0 e|			|x|			|x + e|
+| 0 1 f|   *  |y| =   |y + f|
+| 0 0 1|  		|z|			|1    |
+matrix(1,0,0,1,e,f); === translate(x, y);
+// scale
+| a,0,0 |     | x |      | ax |
+| 0,d,0 |  *  | y |   =  | dy |
+| 0,0,1 |     | 1 |      | 1  |
+matrix(a,0,0,d,0,0); === scale(x, y);
+// rotate
+matrix(cos(θ),sin(θ),-sin(θ),cos(θ),0,0); === rotate(θ);
+| cos(θ),-sin(θ),e |     | x |
+| sin(θ),cos(θ) ,f |  *  | y |
+| 0     ,0      ,1 |     | 1 |
+x1 = cos(θ)x - sin(θ)y + 0
+y2 = sin(θ)x + cos(θ)y + 0
+matrix(1,tan(θy),tan(θx),1,0,0)
+
+
+matrix(1,0,0,0,0,1,0,0,0,0,1,0,x,y,z,1) 缩放
+matrix(x,0,0,0,0,y,0,0,0,0,z,0,0,0,0,1) 平移
+

利用矩阵实现镜像:核心是符号取反

css
div{
+    width:200px;
+    height:200px;
+    background-image:url(source/pic3.jpeg);
+    background-size: cover;
+    transform:matrix(-1,0,0,1,0,0);
+}
+|-1,0, 0 |     	| x |      | -x |  x取反,反推第一个矩阵表达式
+| 0, 1, 0 |  *  | y |   =  | -y |
+| 0, 0, 1 |     | 1 |      | 1  |
+
div{
+    width:200px;
+    height:200px;
+    background-image:url(source/pic3.jpeg);
+    background-size: cover;
+    transform:matrix(-1,0,0,1,0,0);
+}
+|-1,0, 0 |     	| x |      | -x |  x取反,反推第一个矩阵表达式
+| 0, 1, 0 |  *  | y |   =  | -y |
+| 0, 0, 1 |     | 1 |      | 1  |
+

浏览器渲染过程:

html
download html download css download js css rules tree(construct) domAPI domTree
+cssrulestree cssomAPI 最终cssomTree domtree cssomTree renderTree | | layout布局
+---- > paint喷色 (reflow重构) (repaint) 逻辑图(多层矢量图) ----->
+实际绘制(栅格化) 不设置就用cpu绘制 google chrome 自动调用 gpu
+
download html download css download js css rules tree(construct) domAPI domTree
+cssrulestree cssomAPI 最终cssomTree domtree cssomTree renderTree | | layout布局
+---- > paint喷色 (reflow重构) (repaint) 逻辑图(多层矢量图) ----->
+实际绘制(栅格化) 不设置就用cpu绘制 google chrome 自动调用 gpu
+
+ + + + + \ No newline at end of file diff --git a/html-css/canvas-svg.html b/html-css/canvas-svg.html new file mode 100644 index 00000000..f56964e9 --- /dev/null +++ b/html-css/canvas-svg.html @@ -0,0 +1,816 @@ + + + + + + canvas 和 svg | Sunny's blog + + + + + + + + +
Skip to content
On this page

canvas 和 svg

canvas 画线

html
<canvas id="can" width="500px" height="300px"></canvas>
+
<canvas id="can" width="500px" height="300px"></canvas>
+

注意:只能在行间样式设置大小,不能通过 css

javascript
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+
javascript
ctx.moveTo(100, 100); //起点
+ctx.lineTo(200, 100); //终点
+ctx.stroke(); //画上去
+ctx.closePath(); // 连续线,形成闭合
+ctx.fill(); //填充
+ctx.lineWidth = 10; // 设置线的粗细   写在哪,都相当于写在moveto的后面????咋不管用
+
ctx.moveTo(100, 100); //起点
+ctx.lineTo(200, 100); //终点
+ctx.stroke(); //画上去
+ctx.closePath(); // 连续线,形成闭合
+ctx.fill(); //填充
+ctx.lineWidth = 10; // 设置线的粗细   写在哪,都相当于写在moveto的后面????咋不管用
+

想实现一个细,一个粗

一个图形,一笔画出来的,只能一个粗细,想实现,必须开启新图像

javascript
ctx.beginPath();
+
ctx.beginPath();
+

closePath()是图形闭合,不是一个图,不能闭合

canvas 画矩形

javascript
ctx.rect(100, 100, 150, 100);
+ctx.stroke();
+ctx.fill();
+
ctx.rect(100, 100, 150, 100);
+ctx.stroke();
+ctx.fill();
+

简化

javascript
ctx.strokeRect(100, 100, 200, 100); //矩形
+ctx.fillRect(100, 100, 200, 100); //填充矩形
+
ctx.strokeRect(100, 100, 200, 100); //矩形
+ctx.fillRect(100, 100, 200, 100); //填充矩形
+

小方块下落

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      var height = 100;
+      var timer = setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300); //橡皮擦功能,清屏
+        ctx.strokeRect(100, height, 50, 50);
+        height += 5; //每次画的新的,但是旧的没删除,所以要加上清屏
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      var height = 100;
+      var timer = setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300); //橡皮擦功能,清屏
+        ctx.strokeRect(100, height, 50, 50);
+        height += 5; //每次画的新的,但是旧的没删除,所以要加上清屏
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+

作业:自由落体

canvas 画圆

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // 圆心(x,y),半径(r),弧度(起始弧度,结束弧度),方向
+  ctx.arc(100, 100, 50, 0, Math.PI * 1.8, 0); //顺时针0;逆时针1
+  ctx.lineTo(100, 100);
+  ctx.closePath();
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // 圆心(x,y),半径(r),弧度(起始弧度,结束弧度),方向
+  ctx.arc(100, 100, 50, 0, Math.PI * 1.8, 0); //顺时针0;逆时针1
+  ctx.lineTo(100, 100);
+  ctx.closePath();
+  ctx.stroke();
+</script>
+

画圆角矩形

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // ABC为矩形端点
+  // B(x,y),C(x,y),圆角大小(相当于border-radius)
+  ctx.moveTo(100, 110);
+  ctx.arcTo(100, 200, 200, 200, 10);
+  ctx.arcTo(200, 200, 200, 100, 10);
+  ctx.arcTo(200, 100, 100, 100, 10);
+  ctx.arcTo(100, 100, 100, 200, 10);
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  // ABC为矩形端点
+  // B(x,y),C(x,y),圆角大小(相当于border-radius)
+  ctx.moveTo(100, 110);
+  ctx.arcTo(100, 200, 200, 200, 10);
+  ctx.arcTo(200, 200, 200, 100, 10);
+  ctx.arcTo(200, 100, 100, 100, 10);
+  ctx.arcTo(100, 100, 100, 200, 10);
+  ctx.stroke();
+</script>
+

canvas 贝塞尔曲线

贝塞尔曲线

javascript
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+ctx.beginPath();
+ctx.moveTo(100, 100);
+// ctx.quadraticCurveTo(200, 200, 300, 100);二次
+// ctx.quadraticCurveTo(200, 200, 300, 100, 400 200); 三次
+ctx.stroke();
+
var canvas = document.getElementById("can"); //画布
+var ctx = canvas.getContext("2d"); //画笔
+ctx.beginPath();
+ctx.moveTo(100, 100);
+// ctx.quadraticCurveTo(200, 200, 300, 100);二次
+// ctx.quadraticCurveTo(200, 200, 300, 100, 400 200); 三次
+ctx.stroke();
+

波浪

注意:初始化

html
ctx.beginPath();
+
ctx.beginPath();
+

波浪 demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var width = 500;
+      var height = 300;
+      var offset = 0;
+      var num = 0;
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300);
+        ctx.beginPath();
+        ctx.moveTo(0 + offset - 500, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset - 500,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset - 500,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset - 500,
+          height / 2 - Math.sin(num) * 120,
+          width + offset - 500,
+          height / 2
+        );
+        // 整体向左平移整个宽度形成完整的衔接
+        ctx.moveTo(0 + offset, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset,
+          height / 2 - Math.sin(num) * 120,
+          width + offset,
+          height / 2
+        );
+        ctx.stroke();
+        offset += 5;
+        offset %= 500;
+        num += 0.02;
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      canvas {
+        width: 500px;
+        height: 300px;
+        border: 1px solid;
+      }
+    </style>
+  </head>
+
+  <body>
+    <canvas id="can" width="500px" height="300px"></canvas>
+    <!--  -->
+    <script>
+      var width = 500;
+      var height = 300;
+      var offset = 0;
+      var num = 0;
+      var canvas = document.getElementById("can"); //画布
+      var ctx = canvas.getContext("2d"); //画笔
+      setInterval(function () {
+        ctx.clearRect(0, 0, 500, 300);
+        ctx.beginPath();
+        ctx.moveTo(0 + offset - 500, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset - 500,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset - 500,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset - 500,
+          height / 2 - Math.sin(num) * 120,
+          width + offset - 500,
+          height / 2
+        );
+        // 整体向左平移整个宽度形成完整的衔接
+        ctx.moveTo(0 + offset, height / 2);
+        ctx.quadraticCurveTo(
+          width / 4 + offset,
+          height / 2 + Math.sin(num) * 120,
+          width / 2 + offset,
+          height / 2
+        );
+        ctx.quadraticCurveTo(
+          (width / 4) * 3 + offset,
+          height / 2 - Math.sin(num) * 120,
+          width + offset,
+          height / 2
+        );
+        ctx.stroke();
+        offset += 5;
+        offset %= 500;
+        num += 0.02;
+      }, 1000 / 30);
+    </script>
+  </body>
+</html>
+

坐标平移旋转与缩放

旋转平移

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.rotate(Math.PI / 6); //根据画布的原点进行旋转
+  // 要想不根据画布原点,则translate坐标系平移
+  // ctx.translate(100, 100);坐标原点在(100,100),此时配套旋转
+  ctx.moveTo(0, 0);
+  ctx.lineTo(100, 100);
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.rotate(Math.PI / 6); //根据画布的原点进行旋转
+  // 要想不根据画布原点,则translate坐标系平移
+  // ctx.translate(100, 100);坐标原点在(100,100),此时配套旋转
+  ctx.moveTo(0, 0);
+  ctx.lineTo(100, 100);
+  ctx.stroke();
+</script>
+

缩放

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.scale(2, 2); //x乘以他的系数,y乘以他的系数
+  ctx.strokeRect(100, 100, 100, 100);
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.beginPath();
+  ctx.scale(2, 2); //x乘以他的系数,y乘以他的系数
+  ctx.strokeRect(100, 100, 100, 100);
+</script>
+

canvas 的 save 和 restore

不想让其他的受到之前设置的影响

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.save(); //保存坐标系的平移数据,缩放数据,旋转数据
+  ctx.beginPath();
+  ctx.translate(100, 100);
+  ctx.rotate(Math.PI / 4);
+  ctx.strokeRect(0, 0, 100, 50);
+  ctx.beginPath();
+  ctx.restore(); //一旦restore,就恢复save时候的状态
+  ctx.fillRect(100, 0, 100, 50);
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  ctx.save(); //保存坐标系的平移数据,缩放数据,旋转数据
+  ctx.beginPath();
+  ctx.translate(100, 100);
+  ctx.rotate(Math.PI / 4);
+  ctx.strokeRect(0, 0, 100, 50);
+  ctx.beginPath();
+  ctx.restore(); //一旦restore,就恢复save时候的状态
+  ctx.fillRect(100, 0, 100, 50);
+</script>
+

canvas 背景填充

html
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  var img = new Image();
+  img.src = "file:///C:/Users/f1981/Desktop/source/pic3.jpeg";
+  img.onload = function () {
+    //因为图片异步加载
+    ctx.beginPath();
+    ctx.translate(100, 100); //改变坐标系的位置
+    var bg = ctx.createPattern(img, "no-repeat");
+    // 图片填充,是以坐标系原点开始填充的
+    // ctx.fillStyle = "blue";
+    ctx.fillStyle = "bg";
+    ctx.fillRect(0, 0, 200, 100);
+  };
+</script>
+
<script>
+  var canvas = document.getElementById("can"); //画布
+  var ctx = canvas.getContext("2d"); //画笔
+  var img = new Image();
+  img.src = "file:///C:/Users/f1981/Desktop/source/pic3.jpeg";
+  img.onload = function () {
+    //因为图片异步加载
+    ctx.beginPath();
+    ctx.translate(100, 100); //改变坐标系的位置
+    var bg = ctx.createPattern(img, "no-repeat");
+    // 图片填充,是以坐标系原点开始填充的
+    // ctx.fillStyle = "blue";
+    ctx.fillStyle = "bg";
+    ctx.fillRect(0, 0, 200, 100);
+  };
+</script>
+

图片疑难问题 探索 open in live server 只能打开同目录下的 img

线性渐变

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  var bg = ctx.createLinearGradient(0, 0, 200, 200);
+  bg.addColorStop(0, "white"); //数字为0-1之间
+  // bg.addColorStop(0.5, "blue");
+  bg.addColorStop(1, "black");
+  ctx.fillStyle = bg;
+  ctx.translate(100, 100); //起始点依旧是坐标系原点
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  var bg = ctx.createLinearGradient(0, 0, 200, 200);
+  bg.addColorStop(0, "white"); //数字为0-1之间
+  // bg.addColorStop(0.5, "blue");
+  bg.addColorStop(1, "black");
+  ctx.fillStyle = bg;
+  ctx.translate(100, 100); //起始点依旧是坐标系原点
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+

canvas 辐射渐变

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  // var bg = ctx.createRadialGradient(x1,y1,r1,x2,y2,r2);起始圆,结束圆
+  var bg = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
+  // var bg = ctx.createRadialGradient(100, 100, 100, 100, 100, 100);起始圆里面的颜色全是开始的颜色
+  bg.addColorStop(0, "red");
+  bg.addColorStop(0.5, "green");
+  bg.addColorStop(1, "blue");
+  ctx.fillStyle = bg;
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  // var bg = ctx.createRadialGradient(x1,y1,r1,x2,y2,r2);起始圆,结束圆
+  var bg = ctx.createRadialGradient(100, 100, 0, 100, 100, 100);
+  // var bg = ctx.createRadialGradient(100, 100, 100, 100, 100, 100);起始圆里面的颜色全是开始的颜色
+  bg.addColorStop(0, "red");
+  bg.addColorStop(0.5, "green");
+  bg.addColorStop(1, "blue");
+  ctx.fillStyle = bg;
+  ctx.fillRect(0, 0, 200, 200);
+</script>
+

canvas 阴影

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.shadowColor = "blue";
+  ctx.shadowBlur = 20;
+  ctx.shadowOffsetX = 15;
+  ctx.shadowOffsetY = 15;
+  ctx.strokeRect(0, 0, 200, 200);
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.shadowColor = "blue";
+  ctx.shadowBlur = 20;
+  ctx.shadowOffsetX = 15;
+  ctx.shadowOffsetY = 15;
+  ctx.strokeRect(0, 0, 200, 200);
+</script>
+

canvas 渲染文字

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.strokeRect(0, 0, 200, 200);
+
+  ctx.fillStyle = "red";
+  ctx.font = "30px Georgia"; //对stroke和fill都起作用
+  ctx.strokeText("panda", 200, 100); //文字描边
+  ctx.fillText("monkey", 200, 250); //文字填充
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.strokeRect(0, 0, 200, 200);
+
+  ctx.fillStyle = "red";
+  ctx.font = "30px Georgia"; //对stroke和fill都起作用
+  ctx.strokeText("panda", 200, 100); //文字描边
+  ctx.fillText("monkey", 200, 250); //文字填充
+</script>
+

canvas 线端样式

html
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.lineWidth = 15;
+  ctx.moveTo(100, 100);
+  ctx.lineTo(200, 100);
+  ctx.lineTo(100, 130);
+  ctx.lineCap = "square"; //butt round
+  ctx.lineJoin = "miter"; //线接触时候// round bevel miter(miterLimit)
+  ctx.miterLimit = 5;
+  ctx.stroke();
+</script>
+
<script>
+  var canvas = document.getElementById("can");
+  var ctx = canvas.getContext("2d");
+  ctx.beginPath();
+  ctx.lineWidth = 15;
+  ctx.moveTo(100, 100);
+  ctx.lineTo(200, 100);
+  ctx.lineTo(100, 130);
+  ctx.lineCap = "square"; //butt round
+  ctx.lineJoin = "miter"; //线接触时候// round bevel miter(miterLimit)
+  ctx.miterLimit = 5;
+  ctx.stroke();
+</script>
+

SVG 画线与矩形

svg 滤镜 https://www.runoob.com/svg/svg-fegaussianblur.html h5 考试题最后一题

svg 与 canvas 区别

svg:矢量图,放大不会失真,适合大面积的贴图,通常动画较少或者较简单,标签和 css 画

Canvas:适合用于小面积绘图,适合动画,js 画

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 3px;
+      }
+
+      .line2 {
+        stroke: red;
+        stroke-width: 5px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+      <line x1="200" y1="100" x2="200" y2="200" class="line2"></line>
+      <rect height="50" width="100" x="0" y="0"></rect>
+      <!-- 所有闭合的图形,在svg中,默认都是天生充满并且画出来的 -->
+      <rect height="50" width="100" x="0" y="0" rx="10" ry="20"></rect>
+    </svg>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 3px;
+      }
+
+      .line2 {
+        stroke: red;
+        stroke-width: 5px;
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+      <line x1="200" y1="100" x2="200" y2="200" class="line2"></line>
+      <rect height="50" width="100" x="0" y="0"></rect>
+      <!-- 所有闭合的图形,在svg中,默认都是天生充满并且画出来的 -->
+      <rect height="50" width="100" x="0" y="0" rx="10" ry="20"></rect>
+    </svg>
+  </body>
+</html>
+

svg 画圈,椭圆,直线

css
<style>
+polyline {
+    fill: transparent;
+    stroke: blueviolet;
+    stroke-width: 3px;
+}
+</style>
+
<style>
+polyline {
+    fill: transparent;
+    stroke: blueviolet;
+    stroke-width: 3px;
+}
+</style>
+
html
<svg width="500px" height="300px" style="border:1px solid">
+  <circle r="50" cx="50" cy="220"></circle>
+  <!--圆-->
+  <ellipse rx="100" ry="30" cx="400" cy="200"></ellipse>
+  <!-- 椭圆 -->
+  <polyline points="0 0, 50 50, 50 100,100 100,100 50"></polyline>
+  <!-- 曲线:默认填充 -->
+</svg>
+
<svg width="500px" height="300px" style="border:1px solid">
+  <circle r="50" cx="50" cy="220"></circle>
+  <!--圆-->
+  <ellipse rx="100" ry="30" cx="400" cy="200"></ellipse>
+  <!-- 椭圆 -->
+  <polyline points="0 0, 50 50, 50 100,100 100,100 50"></polyline>
+  <!-- 曲线:默认填充 -->
+</svg>
+

svg 画多边形和文本

html
<polygon points="0 0, 50 50, 50 100,100 100,100 50"> </polygon
+><!-- 与polylkine区别:多边形会自动首位相连 -->
+<text x="300" y="50">邓哥身体好</text>
+
<polygon points="0 0, 50 50, 50 100,100 100,100 50"> </polygon
+><!-- 与polylkine区别:多边形会自动首位相连 -->
+<text x="300" y="50">邓哥身体好</text>
+
css
polygon {
+  fill: transparent;
+  stroke: black;
+  stroke-width: 3px;
+}
+
+text {
+  stroke: blue;
+  stroke-width: 3px;
+}
+
polygon {
+  fill: transparent;
+  stroke: black;
+  stroke-width: 3px;
+}
+
+text {
+  stroke: blue;
+  stroke-width: 3px;
+}
+

SVG 透明度与线条样式

透明

css
stroke-opacity: 0.5; /* 边框半透明  */
+fill-opacity: 0.3; /* 填充半透明 */
+
stroke-opacity: 0.5; /* 边框半透明  */
+fill-opacity: 0.3; /* 填充半透明 */
+

线条样式

css
stroke-linecap: butt; /* (帽子)round square 都是额外加的长度*/
+
stroke-linecap: butt; /* (帽子)round square 都是额外加的长度*/
+
css
stroke-linejoin: ; /* 两个线相交的时候 bevel round miter */
+
stroke-linejoin: ; /* 两个线相交的时候 bevel round miter */
+

SVG 的 path 标签

html
<path d="M 100 100 L 200 100"></path>
+<path d="M 100 100 L 200 100 L 200 200"></path
+><!-- 默认有填充 -->
+<path d="M 100 100 L 200 100 l 100 100"></path>
+<!-- 以上:大写字母代表绝对位置,小写字母表示相对位置 -->
+<path d="M 100 100 H 200 V 200"></path
+><!-- H水平  V竖直 -->
+<path d="M 100 100 H 200 V 200 z"></path
+><!-- z表示闭合区间,不区分大小写 -->
+
<path d="M 100 100 L 200 100"></path>
+<path d="M 100 100 L 200 100 L 200 200"></path
+><!-- 默认有填充 -->
+<path d="M 100 100 L 200 100 l 100 100"></path>
+<!-- 以上:大写字母代表绝对位置,小写字母表示相对位置 -->
+<path d="M 100 100 H 200 V 200"></path
+><!-- H水平  V竖直 -->
+<path d="M 100 100 H 200 V 200 z"></path
+><!-- z表示闭合区间,不区分大小写 -->
+
css
path {
+  stroke: red;
+  fill: transparent;
+}
+
path {
+  stroke: red;
+  fill: transparent;
+}
+

path 画弧

html
<path d="M 100 100 A 100 50 0 1 1 150 200"></path>
+<!-- A代表圆弧指令,以M100 100为起点,150 200为终点 ,半径100,短半径50 ,旋转角度为0,1大圆弧,1顺时针 -->
+
<path d="M 100 100 A 100 50 0 1 1 150 200"></path>
+<!-- A代表圆弧指令,以M100 100为起点,150 200为终点 ,半径100,短半径50 ,旋转角度为0,1大圆弧,1顺时针 -->
+

svg 线性渐变

html
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+  </defs>
+  <rect x="100" y="100" height="100" width="200" style="fill:url(#bg1)"></rect>
+</svg>
+
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+  </defs>
+  <rect x="100" y="100" height="100" width="200" style="fill:url(#bg1)"></rect>
+</svg>
+

svg 高斯模糊

html
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+    <filter id="Gaussian">
+      <feGaussianBlur in="SourceGraphic" stdDeviation="20"></feGaussianBlur>
+    </filter>
+  </defs>
+  <rect
+    x="100"
+    y="100"
+    height="100"
+    width="200"
+    style="fill:url(#bg1);filter:url(#Gaussian)"
+  ></rect>
+</svg>
+
<svg width="500px" height="300px" style="border:1px solid">
+  <defs>
+    <!-- 定义一个渐变 -->
+    <linearGradient id="bg1" x1="0" y1="0" x2="0" y2="100%">
+      <stop offset="0%" style="stop-color:rgb(255,255,0)"></stop>
+      <stop offset="100%" style="stop-color:rgb(255,0,0)"></stop>
+    </linearGradient>
+    <filter id="Gaussian">
+      <feGaussianBlur in="SourceGraphic" stdDeviation="20"></feGaussianBlur>
+    </filter>
+  </defs>
+  <rect
+    x="100"
+    y="100"
+    height="100"
+    width="200"
+    style="fill:url(#bg1);filter:url(#Gaussian)"
+  ></rect>
+</svg>
+

SVG 虚线及简单动画

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 10px;
+        /* stroke-dasharray: 10px; */
+        /* stroke-dasharray: 10px 20px;1,2,3,依次取两个值(数组) */
+        /* stroke-dasharray: 10px 20px 30px;1,2,3,依次取两个值 */
+        /* stroke-dashoffset: 10px;偏移 */
+        /* stroke-dashoffset: 200px--->0;可以实现谈满又情空 */
+        stroke-dashoffset: 200px;
+        animation: move 2s linear infinite alternate-reverse;
+      }
+
+      @keyframes move {
+        0% {
+          stroke-dashoffset: 200px;
+        }
+
+        100% {
+          stroke-dashoffset: 0px;
+        }
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+    </svg>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .line1 {
+        stroke: black;
+        stroke-width: 10px;
+        /* stroke-dasharray: 10px; */
+        /* stroke-dasharray: 10px 20px;1,2,3,依次取两个值(数组) */
+        /* stroke-dasharray: 10px 20px 30px;1,2,3,依次取两个值 */
+        /* stroke-dashoffset: 10px;偏移 */
+        /* stroke-dashoffset: 200px--->0;可以实现谈满又情空 */
+        stroke-dashoffset: 200px;
+        animation: move 2s linear infinite alternate-reverse;
+      }
+
+      @keyframes move {
+        0% {
+          stroke-dashoffset: 200px;
+        }
+
+        100% {
+          stroke-dashoffset: 0px;
+        }
+      }
+    </style>
+  </head>
+
+  <body>
+    <svg width="500px" height="300px" style="border:1px solid">
+      <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+    </svg>
+  </body>
+</html>
+

svg 的 viewbox(比例尺)

html
<svg
+  width="500px"
+  height="300px"
+  viewbox="0,0,250,150"
+  style="border:1px solid"
+>
+  <!-- viewbox是宽高的一半 -->
+  <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+</svg>
+
<svg
+  width="500px"
+  height="300px"
+  viewbox="0,0,250,150"
+  style="border:1px solid"
+>
+  <!-- viewbox是宽高的一半 -->
+  <line x1="100" y1="100" x2="200" y2="100" class="line1"></line>
+</svg>
+

总结:SVG 开发中不太用

+ + + + + \ No newline at end of file diff --git a/html-css/drag.html b/html-css/drag.html new file mode 100644 index 00000000..b26c57c8 --- /dev/null +++ b/html-css/drag.html @@ -0,0 +1,350 @@ + + + + + + 拖拽 API | Sunny's blog + + + + + + + + +
Skip to content
On this page

拖拽 API

Drag 被拖拽元素

html
<div class="a" draggable="true"></div>
+<!-- 谷歌,safari可以,火狐,Ie不支持 -->
+
<div class="a" draggable="true"></div>
+<!-- 谷歌,safari可以,火狐,Ie不支持 -->
+

默认值 false

默认值为 true 的标签(默认带有拖拽功能的标签):a 标签和 img 标签

拖拽生命周期

  1. 拖拽开始,拖拽进行中,拖拽结束

  2. 组成:被拖拽的物体,目标区域 拖拽事件

js
var oDragDiv = document.getElementsByClassName("a")[0];
+oDragDiv.ondragstart = function (e) {
+  console.log(e);
+}; // 按下物体的瞬间不触发事件,只有拖动才会触发开始事件
+oDragDiv.ondrag = function (e) {
+  //移动事件
+  console.log(e);
+};
+oDragDiv.ondragend = function (e) {
+  console.log(e);
+};
+// 从而得出移动多少点
+
var oDragDiv = document.getElementsByClassName("a")[0];
+oDragDiv.ondragstart = function (e) {
+  console.log(e);
+}; // 按下物体的瞬间不触发事件,只有拖动才会触发开始事件
+oDragDiv.ondrag = function (e) {
+  //移动事件
+  console.log(e);
+};
+oDragDiv.ondragend = function (e) {
+  console.log(e);
+};
+// 从而得出移动多少点
+

小功能:.a

html
<div class="a" draggable="true"></div>
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  var beginX = 0;
+  var beginY = 0;
+  oDragDiv.ondragstart = function (e) {
+    beginX = e.clientX;
+    beginY = e.clientY;
+    console.log(e);
+  };
+  oDragDiv.ondragend = function (e) {
+    var x = e.clientX - beginX;
+    var y = e.clientY - beginY;
+    oDragDiv.style.left = oDragDiv.offsetLeft + x + "px";
+    oDragDiv.style.top = oDragDiv.offsetTop + y + "px";
+  };
+</script>
+
<div class="a" draggable="true"></div>
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  var beginX = 0;
+  var beginY = 0;
+  oDragDiv.ondragstart = function (e) {
+    beginX = e.clientX;
+    beginY = e.clientY;
+    console.log(e);
+  };
+  oDragDiv.ondragend = function (e) {
+    var x = e.clientX - beginX;
+    var y = e.clientY - beginY;
+    oDragDiv.style.left = oDragDiv.offsetLeft + x + "px";
+    oDragDiv.style.top = oDragDiv.offsetTop + y + "px";
+  };
+</script>
+

Drag 目标元素(目标区域)

css
.a {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  position: absolute;
+}
+.target {
+  width: 200px;
+  height: 200px;
+  border: 1px solid;
+  position: absolute;
+  left: 600px;
+}
+
.a {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  position: absolute;
+}
+.target {
+  width: 200px;
+  height: 200px;
+  border: 1px solid;
+  position: absolute;
+  left: 600px;
+}
+
html
<div class="a" draggable="true"></div>
+<div class="target"></div>
+
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  oDragDiv.ondragstart = function (e) {};
+  oDragDiv.ondrag = function (e) {
+    //只要移动就不停的触发
+  };
+  oDragDiv.ondragend = function (e) {};
+  var oDragTarget = document.getElementsByClassName("target")[0];
+  oDragTarget.ondragenter = function (e) {
+    //不是元素图形进入就触发的而是拖拽的鼠标进入才触发的
+    // console.log(e);
+  };
+  oDragTarget.ondragover = function (e) {
+    //类似于ondrag,只要在区域内移动,就不停的触发
+    // console.log(e);
+    // ondragover--回到原处 / 执行drop事件
+  };
+  oDragTarget.ondragleave = function (e) {
+    console.log(e);
+    // 离开就触发
+  };
+  oDragTarget.ondrop = function (e) {
+    console.log(e);
+    // 所有标签元素,当拖拽周期结束时,默认事件是回到原处,要想执行ondrop,必须在ondragover里面加上e.preventDefault();
+    // 事件是由行为触发,一个行为可以不只触发一个事件
+    // 抬起的时候,有ondragover默认回到原处的事件,只要阻止回到原处,就可以执行drop事件
+
+    // A -> B(阻止) -> C             想阻止c,只能在B上阻止  责任链模式
+  };
+</script>
+
<div class="a" draggable="true"></div>
+<div class="target"></div>
+
+<script>
+  var oDragDiv = document.getElementsByClassName("a")[0];
+  oDragDiv.ondragstart = function (e) {};
+  oDragDiv.ondrag = function (e) {
+    //只要移动就不停的触发
+  };
+  oDragDiv.ondragend = function (e) {};
+  var oDragTarget = document.getElementsByClassName("target")[0];
+  oDragTarget.ondragenter = function (e) {
+    //不是元素图形进入就触发的而是拖拽的鼠标进入才触发的
+    // console.log(e);
+  };
+  oDragTarget.ondragover = function (e) {
+    //类似于ondrag,只要在区域内移动,就不停的触发
+    // console.log(e);
+    // ondragover--回到原处 / 执行drop事件
+  };
+  oDragTarget.ondragleave = function (e) {
+    console.log(e);
+    // 离开就触发
+  };
+  oDragTarget.ondrop = function (e) {
+    console.log(e);
+    // 所有标签元素,当拖拽周期结束时,默认事件是回到原处,要想执行ondrop,必须在ondragover里面加上e.preventDefault();
+    // 事件是由行为触发,一个行为可以不只触发一个事件
+    // 抬起的时候,有ondragover默认回到原处的事件,只要阻止回到原处,就可以执行drop事件
+
+    // A -> B(阻止) -> C             想阻止c,只能在B上阻止  责任链模式
+  };
+</script>
+

拖拽 demo

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .box1 {
+        position: absolute;
+        width: 150px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      .box2 {
+        position: absolute;
+        width: 150px;
+        left: 300px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      li {
+        position: relative;
+        width: 100px;
+        height: 30px;
+        background: #abcdef;
+        margin: 10px auto 0px auto;
+        /* 居中,上下10px */
+        list-style: none;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="box1">
+      <ul>
+        <li></li>
+        <li></li>
+        <li></li>
+      </ul>
+    </div>
+    <div class="box2"></div>
+    <script>
+      var dragDom;
+      var liList = document.getElementsByTagName("li");
+      for (var i = 0; i < liList.length; i++) {
+        liList[i].setAttribute("draggable", true); //赋予class
+        liList[i].ondragstart = function (e) {
+          console.log(e.target);
+          dragDom = e.target;
+        };
+      }
+      var box2 = document.getElementsByClassName("box2")[0];
+      box2.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box2.ondrop = function (e) {
+        // console.log(dragDom);
+        box2.appendChild(dragDom);
+        dragDom = null;
+      };
+
+      // 拖回去
+      var box1 = document.getElementsByClassName("box1")[0];
+      box1.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box1.ondrop = function (e) {
+        // console.log(dragDom);
+        box1.appendChild(dragDom);
+        dragDom = null;
+      };
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .box1 {
+        position: absolute;
+        width: 150px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      .box2 {
+        position: absolute;
+        width: 150px;
+        left: 300px;
+        height: auto;
+        border: 1px solid;
+        padding-bottom: 10px;
+      }
+
+      li {
+        position: relative;
+        width: 100px;
+        height: 30px;
+        background: #abcdef;
+        margin: 10px auto 0px auto;
+        /* 居中,上下10px */
+        list-style: none;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="box1">
+      <ul>
+        <li></li>
+        <li></li>
+        <li></li>
+      </ul>
+    </div>
+    <div class="box2"></div>
+    <script>
+      var dragDom;
+      var liList = document.getElementsByTagName("li");
+      for (var i = 0; i < liList.length; i++) {
+        liList[i].setAttribute("draggable", true); //赋予class
+        liList[i].ondragstart = function (e) {
+          console.log(e.target);
+          dragDom = e.target;
+        };
+      }
+      var box2 = document.getElementsByClassName("box2")[0];
+      box2.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box2.ondrop = function (e) {
+        // console.log(dragDom);
+        box2.appendChild(dragDom);
+        dragDom = null;
+      };
+
+      // 拖回去
+      var box1 = document.getElementsByClassName("box1")[0];
+      box1.ondragover = function (e) {
+        e.preventDefault();
+      };
+      box1.ondrop = function (e) {
+        // console.log(dragDom);
+        box1.appendChild(dragDom);
+        dragDom = null;
+      };
+    </script>
+  </body>
+</html>
+

dataTransfer 补充属性

effectAllowed

js
oDragDiv.ondragstart = function (e) { e.dataTransfer.effectAllowed =
+"link";//指针是什么样子,只能在ondragstart里面设置 } /其他光标:link copy move
+copyMove linkMove all
+
oDragDiv.ondragstart = function (e) { e.dataTransfer.effectAllowed =
+"link";//指针是什么样子,只能在ondragstart里面设置 } /其他光标:link copy move
+copyMove linkMove all
+

dropEffect

js
oDragTarget.ondrop = function (e) { e.dataTransfer.dropEffect =
+"link";//放下时候的效果,只在drop里面设置 }
+
oDragTarget.ondrop = function (e) { e.dataTransfer.dropEffect =
+"link";//放下时候的效果,只在drop里面设置 }
+

试验不通过??

+ + + + + \ No newline at end of file diff --git a/html-css/flex.html b/html-css/flex.html new file mode 100644 index 00000000..55983799 --- /dev/null +++ b/html-css/flex.html @@ -0,0 +1,160 @@ + + + + + + 一文搞懂 flex:0,1,auto,none | Sunny's blog + + + + + + + + +
Skip to content
On this page

一文搞懂 flex:0,1,auto,none

flex 属性介绍

首先, flex 属性其实是一种简写,是 flex-grow , flex-shrink 和 flex-basis 的缩写形式。 默认值为 0 1 auto 。后两个属性可选。

flex-grow

flex-grow 属性定义项目的放大比例,默认为 0 ,即如果存在剩余空间,也不放大。 如果所有项目的 flex-grow 属性都为 1,则它们将等分剩余空间(如果有的话)。 如果一个项目的 flex-grow 属性为 2,其他项目都为 1,则前者占据的剩余空间将比其他项多一倍。

flex-shrink

flex-shrink 属性定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。 如果所有项目的 flex-shrink 属性都为 1,当空间不足时,都将等比例缩小。 如果一个项目的 flex-shrink 属性为 0,其他项目都为 1,则空间不足时,前者不缩小。

flex-basis

flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。 浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为 auto,即项目的本来大小。 它可以设为跟 width 或 height 属性一样的值(比如 350px),则项目将占据固定空间。

flex 缩写的等值

了解了三个属性各自的含义之后,可以看下三个属性对应的等值。

flex: initial

flex:initial 等同于设置 flex: 0 1 auto ,是 flex 属性的默认值。

举例,外容器是红色,内里元素蓝色边框,比较少,会有下图效果,剩余空间仍有保留。剩余空间有,但是因为 flex-grow 属性是 0,所以没有填补空白。

html
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+  }
+</style>
+
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+  }
+</style>
+

如果子项内容很多,由于 flex-shrink:1 ,因此,会缩小,表现效果就是文字换行,效果如下图所示。

适用场景

initial 表示 CSS 属性的初始值,通常用来还原已经设置的 CSS 属性。因此日常开发不会专门设置 flex:initial 声明。flex:initial 声明适用于下图所示的布局效果。

上图所示的布局效果常见于按钮、标题、小图标等小部件的排版布局,因为这些小部件的宽度都不会很宽,水平位置的控制多使用 justify-content 和 margin-left:auto/margin-right:auto 实现。

除了上图所示的布局效果外, flex:initial 声明还适用于一侧内容宽度固定,另外一侧内容宽度任意的两栏自适应布局场景,布局轮廓如图下图所示(点点点表示文本内容)。

此时,无需任何其他 Flex 布局相关的 CSS 设置,只需要容器元素设置 display:flex 即可。

总结下就是那些希望元素尺寸收缩,同时元素内容万一较多又能自动换行的场景可以不做任何 flex 属性设置。

flex:0 和 flex:none

flex:0 等同于设置 flex: 0 1 0% 。

html
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 0;
+  }
+</style>
+
<div class="container">
+  <div class="item">嘿嘿</div>
+  <div class="item">哈哈</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 0;
+  }
+</style>
+

flex:none 等同于设置 flex: 0 0 auto 。只是把 css 中的 flex 属性设置为 none ,不再展示代码。

对比看来可以看到 flex-0 时候会表现为最小内容宽度,会将高度撑高(当前没有设置高度,如果设置高度文字会超过设置的高度,如下图)flex-none 时候会表现为最大内容宽度,字数过多时候会超过容器宽度。

适用场景 flex-0

由于应用了 flex:0 的元素表现为最小内容宽度,因此,适合使用 flex:0 的场景并不多。

其中上图左侧部分的矩形表示一个图像,图像下方会有文字内容不定的描述信息,此时,左侧内容就适合设置 flex:0 ,这样,无论文字的内容如何设置,左侧内容的宽度都是图像的宽度。

适用场景 flex-none

flex-none 比 flex-0 的适用场景多,如内容文字固定不换行,宽度为内容宽度就适用该属性。

没设置 flex-none 代码如下:

html
<div class="aa">
+  <img src="a.png" />
+  <p>右侧按钮没有设置flex-none</p>
+  <button>按钮</button>
+</div>
+<style>
+  .aa {
+    width: 300px;
+    border: 1px solid #000;
+    display: flex;
+  }
+
+  .aa img {
+    width: 100px;
+    height: 100px;
+  }
+
+  .aa buttton {
+    height: 50px;
+    align-self: center;
+  }
+</style>
+
<div class="aa">
+  <img src="a.png" />
+  <p>右侧按钮没有设置flex-none</p>
+  <button>按钮</button>
+</div>
+<style>
+  .aa {
+    width: 300px;
+    border: 1px solid #000;
+    display: flex;
+  }
+
+  .aa img {
+    width: 100px;
+    height: 100px;
+  }
+
+  .aa buttton {
+    height: 50px;
+    align-self: center;
+  }
+</style>
+

flex:1 和 flex:auto

flex:1 时代码如下

html
<div class="container">
+  <div class="item">嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿</div>
+  <div class="item">哈哈</div>
+  <div class="item">呵呵</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 1;
+  }
+</style>
+
<div class="container">
+  <div class="item">嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿</div>
+  <div class="item">哈哈</div>
+  <div class="item">呵呵</div>
+</div>
+<style>
+  .container {
+    width: 200px;
+    display: flex;
+    border: 2px dashed crimson;
+  }
+
+  .container .item {
+    border: 2px solid blue;
+    flex: 1;
+  }
+</style>
+

虽然都是充分分配容器的尺寸,但是 flex:1 的尺寸表现更为内敛(优先牺牲自己的尺寸), flex:auto 的尺寸表现则更为霸道(优先扩展自己的尺寸)。

适合使用 flex:1 的场景

当希望元素充分利用剩余空间,同时不会侵占其他元素应有的宽度的时候,适合使用 flex:1 ,这样的场景在 Flex 布局中非常的多。

例如所有的等分列表,或者等比例列表都适合使用 flex:1 或者其他 flex 数值,适合的布局效果轮廓如下图所示。

适合使用 flex:auto 的场景

当希望元素充分利用剩余空间,但是各自的尺寸按照各自内容进行分配的时候,适合使用 flex:auto 。例如导航栏。整体设置为 200px,内部设置 flex:auto,会自动按照内容比例进行分配宽度。

+ + + + + \ No newline at end of file diff --git a/html-css/interview.html b/html-css/interview.html new file mode 100644 index 00000000..013a7e6a --- /dev/null +++ b/html-css/interview.html @@ -0,0 +1,2514 @@ + + + + + + HTML_CSS 面试题 | Sunny's blog + + + + + + + + +
Skip to content
On this page

HTML_CSS 面试题

1. 什么是 <!DOCTYPE>?是否需要在 HTML5 中使用?

它是 HTML 的文档声明,通过它告诉浏览器,使用哪一个 HTML 版本标准解析文档。

而文档声明有多种书写格式,对应不同的 HTML 版本,<!DOCTYPE> 这种书写是告诉浏览器,HTML5 的标准进行解析。

2. 什么是可替换元素,什么是非可替换元素,它们各自有什么特点?

可替换元素是指这样一种元素,它在页面中的大部分展现效果不由 CSS 决定。

比如 img 元素就是一个可替换元素,它在页面中显示出的效果主要取决于你连接的是什么图片,图片是什么它就展示什么,CSS** 虽然可以控制图片的尺寸位置,但永远无法控制图片本身**。

再比如,select** 元素也是一个典型的可替换元素**,它在页面上呈现的是用户操作系统上的下拉列表样式,因此,它的展现效果是由操作系统决定的。所以,同一个 select 元素,放到不同操作系统的电脑上会呈现不同的外观。

img、video、audio、大部分表单元素都属于可替换元素。 > 非可替换元素就是指的普通元素,它具体在页面上呈现什么,完全由 CSS 来决定。

3. srchref 的区别

srcsource 的缩写,它通常用于 img、video、audio、script 元素,通过 src 属性,可以指定外部资源的来源地址

hrefhyper reference 的缩写,意味「超引用」,它通常用于 a、link 元素,通过 href 属性,可以标识文档中引用的其他超文本。

4. 说说常用的 meta 标签

能够答出 meta 标签的意义,并且举出常规的 meta 标签使用方式

  1. meta 标签俗称描述网页数据的数据,比如网页本身的编码;
  2. 网页本身的作者,描述;
  3. 还有一些更高级的功能性特性,比如 dns 预解析,定义视口表现等
  4. 能结合 meta 提到一些 seo 相关的知识也可以有加分

    meta 标签提供关于 HTML 文档的元数据。元数据不会显示在页面上,但是对于机器是可读的。它可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。

    1. content ,设置或返回 meta 元素的 content 属性的值 。
    2. http-equiv,把 content 属性连接到一个 HTTP 头部。
    3. name,把 content 属性连接到某个名称。
html
<meta charset="UTF-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
<meta charset="UTF-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+

5. 说说对 html 语义化的理解

  • 用正确的标签做正确的事情!
  • html 语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;
  • 搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO
  • 便于阅读维护理解

6. label 的作用是什么?是怎么用的?

如果您在 label 元素内点击文本,就会触发此控件。 就是说,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上。

最常用 label 标签的地方,应该就是表单中选择性别的单选框了。

7. iframe 框架有那些优缺点?

  • iframe阻塞主页面的 Onload 事件;
  • iframe** 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。**
  • 使用 iframe 之前需要考虑这两个缺点。如果需要使用 iframe,最好是通过 javascript** 动态给 iframe 添加 src 属性值**,这样可以可以绕开以上两个问题。

8. HTMLXHTML 二者有什么区别,你觉得应该使用哪一个并说出理由。 不同于 XML

关于功能上的差别,主要是 XHTML 可兼容各大浏览器、手机以及 PDA,并且浏览器也能快速正确地编译网页。 XHTML** 的规则。** 下面列出了几条容易犯的错误,供理解。

  1. 所有标签都必须小写

XHTML 中,所有的标签都必须小写,不能大小写穿插其中,也不能全部都是大写。

  1. 标签必须成双成对

  2. 标签顺序必须正确

  3. 所有属性都必须使用双引号

9. HTML5form 如何关闭自动完成功能?

HTML 的输入框可以拥有自动完成的功能,当你往输入框输入内容的时候,浏览器会从你以前的同名输入框的历史记录中查找出类似的内容并列在输入框下面,这样就不用全部输入进去了,直接选择列表中的项目就可以了。

使用 autocomplete="off"(给不想要提示的 form 或某个 input 设置为 autocomplete=off。)

很多时候,需要对客户的资料进行保密,防止浏览器软件或者恶意插件获取到;

可以在 input 中加入 autocomplete=“off” 来关闭记录系统需要保密的情况下可以使用此参数

提示:autocomplete 属性有可能在 form 元素中是开启的,而在 input 元素中是关闭的。

10. titleh1 的区别、bstrong 的区别、iem 的区别?

  • title 属性表示网页的标题,h1 元素则表示层次明确的页面内容标题,对页面信息的抓取也有很大的影响
  • strong 是标明重点内容,有语气加强的含义,使用阅读设备阅读网络时:strong 会重读,而 b 是展示强调内容
  • i 内容展示为斜体,em 表示强调的文本

** 相似的还有如下的元素:** 自然样式标签:b、i、u、s、pre 语义样式标签:strong、em、ins、del、code

11. 请描述下 SEO 中的 TDK

SEO 中,所谓的 TDK 其实就是 title、description、keywords 这三个标签,title 标题标签,description 描述标签,keywords 关键词标签。

13. 什么是严格模式与混杂模式?

  • 严格模式:以浏览器支持的最高标准运行
  • 混杂模式:页面以宽松向下兼容的方式显示,模拟老式浏览器的行为

14. 对于 WEB 标准以及 W3C 的理解与认识问题

  1. 学习成本降低,只学习标准即可,否则将学习各个浏览器厂商的标准。
  2. 统一开发流程,用标准化工具开发(例如 VSCode、WebStorm、Sublime 等)再用标准化的浏览器测试,便于多人协作。
  3. 简化网站代码的维护。
  4. 跨平台,可方便迁移到不同设备中

15. 列举 IE 与其他浏览器不一样的特性?

  • IE 的排版引擎是 Trident (又称为 MSHTML
  • Trident 内核曾经几乎与 W3C 标准脱节
  • Trident 内核的大量 Bug 等安全性问题没有得到及时解决
  • JS 方面,有很多独立的方法,例如绑定事件的 attachEvent、创建事件的 createEventObject
  • CSS 方面,也有自己独有的处理方式,例如设置透明,低版本 IE 中使用滤镜的方式,盒模型也和 W3C 规定的盒模型不同

17. 页面可见性(Page VisibilityAPI 可以有哪些用途?

常用的 API 有三个,document.hidden 返回一个布尔值,如果是 true,表示页面可见,false 则表示页面隐藏。不同页面之间来回切换,会触发 visibilitychange 事件,还有一个 document.visibilityState,表示页面所处的状态。

19. div+css 的布局较 table 布局有什么优点?

  • 页面加载速度更快、结构化清晰、页面显示简洁。
  • 表现与结构相分离
  • 易于优化**(SEO)**搜索引擎更友好,排名更容易靠前。
  • ** 符合 W3C 标准。**

24. HTML 全局属性(global attribute)有哪些

所谓全局属性,就是指每个 HTML 元素都拥有的属性,大致有如下的属性:

  • class :为元素设置类标识
  • data-* : 为元素增加⾃定义属性
  • draggable : 设置元素是否可拖拽
  • id : 元素 id ,⽂档内唯⼀
  • lang : 元素内容的的语⾔
  • style : ⾏内 css 样式
  • title : 元素相关的建议信息

28.  iframe 的作用

优点

  • 重载页面时不需要重载整个页面,只需要重载页面中的一个框架页(减少了数据的传输,增加了网页下载速度)
  • 方便制作导航栏

缺点

  • 浏览器的后退按钮无效
  • 无法被一些搜索引擎索引到

目前框架的所有优点完全可以使用 Ajax 实现,因此已经没有必要使用 iframe 框架了。

29. imgtitlealt

  • alt:如果无法显示图像,浏览器将显示 alt 指定的内容
  • title:在鼠标移到元素上时显示 title 的内容
  1. 这些都是表面上的区别,alt 是 img 必要的属性,而 title 不是
  2. 网站 seo 优化,搜索引擎对图片意思的判断,主要靠 alt 属性。所以在图片 alt 属性中以简要文字说明,同时包含关键词,也是页面优化的一部分。条件允许的话,可以在 title 属性里,进一步对图片说明。

31. 行内元素和块级元素区别,有哪些,怎样转换?(顶呱呱)

块级元素:

  • 独占一行
  • 高度,行高以及外边距和内边距都可控制;
  • 宽度缺省是它的容器的 100%,除非设定一个宽度。
  • 它可以容纳内联元素和其他块元素

行内元素

  • 不换行
  • 高,行高及外边距和内边距不可改变;
  • 宽度就是它的文字或图片的宽度,不可改变
  • 内联元素只能容纳文本或者其他内联元素

对行内元素,需要注意如下:

  • 设置宽度 width 无效。
  • 设置高度 height 无效,可以通过 line-height 来设置。
  • 设置 margin 只有左右 margin 有效,上下无效。
  • 设置 padding 只有左右 padding 有效,上下则无效。注意元素范围是增大了,但是对元素周围的内容是没影响的。

通过 display 属性对行内元素和块级元素进行切换(主要看第 2、3、4 个值): html 中常见的块级元素:p、div、form、ul、ol、table > html 中常见的行内元素:a、img、span、button

34.  h5html5 区别

H5 是一个产品名词,包含了最新的 HTML5、CSS3、ES6 等新的技术来制作的**应用。 ** > HTML5 是一个技术名词,指代的就仅仅是第五代 HTML

35. form 表单上传文件时需要进行什么样的声明

html
enctype="multipart/form-data"
+
enctype="multipart/form-data"
+

37. 如何在一张图片上的某一个区域做到点击事件

可以使用图片热区技术。步骤如下: 1、插入图片,并设置好图像的有关参数,且在 <img> 标记中设置参数 usemap="#Map",以表示对图像地图(Map)的引用; 2、用 <map> 标记设定图像地图的作用区域,并取名为:Map; 3、分别用 <area> 标记针对相应位置划分出多个矩形作用区域,并设定好其链接参数 href

39. 什么是锚点?

锚点(anchor)是一种特殊连接,能定位到 HTML 文档中某个特定位置,通过 HTML 元素的 id 来设置锚点。

40. 图片与 span 元素混排图像下方会出现几像素的空隙的原因是什么?

参考答案: img 作为可替换元素,它没有自己的基线,如果与不可替换元素混合排列,其行盒底端与基线对齐。由于与基线对齐,图像下方就会出现几像素的空隙。

41. a 元素除了用于导航外,还可以有什么作用?

当然,a 元素最常见的两个应用就是做锚点和下载文件。发短信,打电话 锚点可以在点击时快速定位到一个页面的某一个位置,而下载的原理在于 a** 标签所对应的资源浏览器无法解析,于是浏览器会选择将其下载下来。**

1.   介绍下 BFC 及其应用

所谓 BFC,指的是一个独立的布局环境,BFC 内部的元素布局与外部互不影响。 触发 BFC 的方式有很多,常见的有:

  • 设置浮动
  • overflow 设置为 auto、scroll、hidden
  • positon 设置为 absolute、fixed
  • display: flow-root; 没有副作用,专门为 BFC

常见的 BFC 应用有:

  • 解决浮动元素令父元素高度坍塌的问题
  • 解决非浮动元素被浮动元素覆盖问题
  • 解决外边距垂直方向重合的问题

BFC block formatting Context 块级格式化上下文 新的渲染规则:BFC 包裹子元素 (浮动元素,margin 塌陷, margin 合并) BFC + 浮动实现多栏布局 BFC 兄弟元素,位置分明

css
.father {
+  width: 100px;
+  border: 1px solid;
+  float: left;
+  /*设置浮动之后,父级不能包裹子集了 height由子集撑开。怎么才能保住?
+  1. 清除浮动
+  2. 触发BFC
+  */
+  /*overflow: hidden;
+  position: absolute;
+  display: inline-block
+  display: flex; 只要display不是block
+  display: flow-root; 没有副作用,专门为BFC
+  */
+}
+.son {
+  width: 50px;
+  height: 50px;
+  background: #fac;
+}
+
.father {
+  width: 100px;
+  border: 1px solid;
+  float: left;
+  /*设置浮动之后,父级不能包裹子集了 height由子集撑开。怎么才能保住?
+  1. 清除浮动
+  2. 触发BFC
+  */
+  /*overflow: hidden;
+  position: absolute;
+  display: inline-block
+  display: flex; 只要display不是block
+  display: flow-root; 没有副作用,专门为BFC
+  */
+}
+.son {
+  width: 50px;
+  height: 50px;
+  background: #fac;
+}
+
css
.father {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  margin-top: 100px;
+  /*display: flow-root;*/
+}
+.son {
+  width: 50px;
+  height: 50px;
+  background: #fac;
+  margin-top: 200px;
+  /*margin-top: 100px;
+  margin:和父级margin-top比较,谁大,按谁;
+  */
+}
+
.father {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  margin-top: 100px;
+  /*display: flow-root;*/
+}
+.son {
+  width: 50px;
+  height: 50px;
+  background: #fac;
+  margin-top: 200px;
+  /*margin-top: 100px;
+  margin:和父级margin-top比较,谁大,按谁;
+  */
+}
+

BFC 多栏布局,左右固定,中间自适应;左侧固定,中间自适应

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <style>
+      .left,
+      .right {
+        width: 100px;
+        height: 100px;
+        background: #fac;
+        float: left;
+      }
+      .right {
+        float: right;
+      }
+      /*浮动了,脱离文档流*/
+      .center {
+        height: 150px;
+        background: #cfa;
+        overflow: hidden;
+        display: flow-root;
+      }
+    </style>
+    <div class="wrapper">
+      去掉任何一个实现两栏布局
+      <div class="left"></div>
+      <div class="right"></div>
+      <div class="center"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <style>
+      .left,
+      .right {
+        width: 100px;
+        height: 100px;
+        background: #fac;
+        float: left;
+      }
+      .right {
+        float: right;
+      }
+      /*浮动了,脱离文档流*/
+      .center {
+        height: 150px;
+        background: #cfa;
+        overflow: hidden;
+        display: flow-root;
+      }
+    </style>
+    <div class="wrapper">
+      去掉任何一个实现两栏布局
+      <div class="left"></div>
+      <div class="right"></div>
+      <div class="center"></div>
+    </div>
+  </body>
+</html>
+

2. 介绍下 BFC、IFC、GFCFFC

  • BFC:块级格式上下文,指的是一个独立的布局环境,BFC 内部的元素布局与外部互不影响。
  1. BFC 有隔离作用,内部元素不受外部元素影响,反之亦然。
  2. 一个元素只能存在于一个 BFC 中,如果能同时存在于两个 BFC 中,那么就违反了 BFC 的隔离原则。
  3. BFC 内的元素按正常流排列,元素间的间隙由元素外边距控制。
  4. BFC 中的内容不会与外面的浮动元素重叠。
  5. 计算 BFC 的高度需要包括 BFC 内的浮动子元素的高度。
  • IFC:行内格式化上下文,将一块区域以行内元素的形式来格式化。
  • GFC:网格布局格式化上下文,将一块区域以 grid 网格的形式来格式化
  • FFC:弹性格式化上下文,将一块区域以弹性盒的形式来格式化

3. flex 骰子

5. 分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景。

  • 结构: display:none: 会让元素完全从渲染树中消失,渲染的时候不占据任何空间, 不能点击, visibility: hidden:不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击 opacity: 0: 不会让元素从渲染树消失,**渲染元素继续占据空间,只是内容不可见,可以点击 **
  • 继承: display: none 和 opacity: 0:是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。 visibility: hidden:是继承属性,子孙节点消失由于继承了 hidden,通过设置 visibility: visible;可以让子孙节点显式。
  • 性能: displaynone : 修改元素会造成文档回流,读屏器不会读取 display: none 元素内容,性能消耗较大 visibility:hidden: 修改元素只会造成本元素的重绘,性能消耗较少读屏器读取 visibility: hidden 元素内容 opacity: 0 : 修改元素会造成重绘,性能消耗较少
  1. display: none (不占空间,不能点击)(场景,显示出原来这里不存在的结构)
  2. visibility: hidden(占据空间,不能点击)(场景:显示不会导致页面结构发生变动,不会撑开)
  3. opacity: 0(占据空间,可以点击)(场景:可以跟 transition 搭配)

6. 已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。

html
<img src="1.jpg" style="width:480px!important;”>
+
<img src="1.jpg" style="width:480px!important;”>
+

CSS 方法

  • max-width:300px; 覆盖其样式
  • transform: scale(0.625) 按比例缩放图片;
  • 利用 CSS 动画的样式优先级高于 _!important_** 的特性**

JS 方法

  • document.getElementsByTagName("img")[0].setAttribute("style","width:300px!important;")

7. 如何用 cssjs 实现多行文本溢出省略效果,考虑兼容性

CSS 实现方式 单行:

css
overflow: hidden;
+text-overflow: ellipsis;
+white-space: nowrap;
+
overflow: hidden;
+text-overflow: ellipsis;
+white-space: nowrap;
+

多行:

css
display: -webkit-box;
+-webkit-box-orient: vertical;
+-webkit-line-clamp: 3; //行数
+overflow: hidden;
+
display: -webkit-box;
+-webkit-box-orient: vertical;
+-webkit-line-clamp: 3; //行数
+overflow: hidden;
+

兼容: JS 实现方式:

  • 使用 split + 正则表达式将单词与单个文字切割出来存入 words
  • 加上 '...'
  • 判断 scrollHeight 与 clientHeight,超出的话就从 words 中 pop 一个出来
css
p {
+  position: relative;
+  line-height: 20px;
+  max-height: 40px;
+  overflow: hidden;
+}
+p::after {
+  content: "...";
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  padding-left: 40px;
+  background: -webkit-linear-gradient(left, transparent, #fff 55%);
+  background: -o-linear-gradient(right, transparent, #fff 55%);
+  background: -moz-linear-gradient(right, transparent, #fff 55%);
+  background: linear-gradient(to right, transparent, #fff 55%);
+}
+
p {
+  position: relative;
+  line-height: 20px;
+  max-height: 40px;
+  overflow: hidden;
+}
+p::after {
+  content: "...";
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  padding-left: 40px;
+  background: -webkit-linear-gradient(left, transparent, #fff 55%);
+  background: -o-linear-gradient(right, transparent, #fff 55%);
+  background: -moz-linear-gradient(right, transparent, #fff 55%);
+  background: linear-gradient(to right, transparent, #fff 55%);
+}
+

8. 居中为什么要使用 transform(为什么不使用 marginLeft/Top)(阿里)

transform 属于合成属性(composite property),对合成属性进行 transition/animation 动画将会创建一个合成层(composite layer),这使得被动画元素在一个独立的层中进行动画。通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧。

top/left 属于布局属性,该属性的变化会导致重排(reflow/relayout),所谓重排即指对这些节点以及受这些节点影响的其它节点,进行 CSS 计算->布局->重绘过程,浏览器需要为整个层进行重绘并重新上传到 GPU,造成了极大的性能开销。

10. 说出 space-betweenspace-around 的区别?(携程)

这个是 flex 布局的内容,其实就是一个边距的区别,按水平布局来说,space-between是两端对齐,在左右两侧没有边距,而space-around是每个 子项目左右方向的 margin 相等,所以两个 item 中间的间距会比较大。

11. CSS3transitionanimation 的属性分别有哪些

参考答案:

transition 过渡动画:

  • transition-property:指定过渡的 CSS 属性
  • transition-duration:指定过渡所需的完成时间
  • transition-timing-function:指定过渡函数
  • transition-delay:指定过渡的延迟时间

animation 关键帧动画:

  • animation-name:指定要绑定到选择器的关键帧的名称
  • animation-duration:动画指定需要多少秒或毫秒完成
  • animation-timing-function:设置动画将如何完成一个周期
  • animation-delay:设置动画在启动前的延迟间隔
  • animation-iteration-count:定义动画的播放次数
  • animation-direction:指定是否应该轮流反向播放动画
  • animation-fill-mode:规定当动画不播放时(当动画完成时,或当动画有一个延迟未开始播放时),要应用到元素的样式
  • animation-play-state:指定动画是否正在运行或已暂停
  • CSS 动画如何实现?

参考答案:

animation 属性,对元素某个或多个属性的变化进行控制,可以设置多个关键帧。属性包含了动画的名称、完成时间(以毫秒计算)、周期、间隔、播放次数、是否反复播放、不播放时应用的样式、动画暂停或运行。

它不需要触发任何事件就可以随着时间变化来改变元素的样式。

使用 CSS 做动画

  • [_@keyframes _]_ _ 规定动画。
  • animation 所有动画属性的简写属性。
  • animation-name 规定 [_@keyframes _] 动画的名称。
  • animation-duration 规定动画完成一个周期所花费的秒或毫秒。默认是 0。
  • animation-timing-function 规定动画的速度曲线。默认是 ease
  • animation-fill-mode 规定当动画不播放时(当动画完成时,或当动画有一个延迟未开始播放时),要应用到元素的样式。
  • animation-delay 规定动画何时开始。默认是 0
  • animation-iteration-count   规定动画被播放的次数。默认是 1
  • animation-direction 规定动画是否在下一周期逆向地播放。默认是 normal
  • animation-play-state 规定动画是否正在运行或暂停。默认是 running

-EOF-

12. 隐藏页面中的某个元素的方法有哪些?

  1. 隐藏类型

屏幕并不是唯一的输出机制,比如说屏幕上看不见的元素(隐藏的元素),其中一些依然能够被读屏软件阅读出来(因为读屏软件依赖于可访问性树来阐述)。为了消除它们之间的歧义,我们将其归为三大类:

  • 完全隐藏:元素从渲染树中消失,不占据空间。
  • 视觉上的隐藏:屏幕中不可见,占据空间。
  • 语义上的隐藏:读屏软件不可读,但正常占据空。

完全隐藏 (1) display 属性 (2) hidden 属性 HTML5 新增属性,相当于 display: none 视觉上的隐藏 (1) 设置 posoition 为 absolute 或 fixed,通过设置 top、left 等值,将其移出可视区域。 (2) 设置 position 为 relative,通过设置 top、left 等值,将其移出可视区域。 (3) 设置 margin 值,将其移出可视区域范围(可视区域占位)。

语义上隐藏 aria-hidden 属性 读屏软件不可读,占据空间,可见。

html
<div aria-hidden="true"></div>
+
<div aria-hidden="true"></div>
+

13. 层叠上下文

层叠上下文概念CSS2.1 规范中,每个盒模型的位置是三维的,分别是平面画布上的 X 轴,Y 轴以及表示层叠的 Z 轴。 一般情况下,元素在页面上沿 XY 轴平铺,我们察觉不到它们在 Z 轴上的层叠关系。而一旦元素发生堆叠,这时就能发现某个元素可能覆盖了另一个元素或者被另一个元素覆盖。 层叠上下文触发条件

  • HTML 中的根元素 HTML 本身就具有层叠上下文,称为“根层叠上下文”。
  • 普通元素设置 position 属性为非 static 值并设置 z-index 属性为具体数值,产生层叠上下文
  • CSS3 中的新属性也可以产生层叠上下文

层叠顺序 “层叠顺序”(stacking order)表示元素发生层叠时按照特定的顺序规则在 Z 轴上垂直显示。 说简单一点就是当元素处于同一层叠上下文内时如何进行层叠判断。 具体的层叠等级如下图:

image.png

15. 讲一下png8、png16、png32的区别,并简单讲讲 png 的压缩原理

PNG 图片主要有三个类型,分别为 PNG 8/ PNG 24 / PNG 32。

  • PNG 8:PNG 8 中的 8,其实指的是 8bits,相当于用 28(2 的 8 次方)大小来存储一张图片的颜色种类,28 等于 256,也就是说 PNG 8 能存储 256 种颜色,一张图片如果颜色种类很少,将它设置成 PNG 8 得图片类型是非常适合的。
  • PNG 24:PNG 24 中的 24,相当于 3 乘以 8 等于 24,就是用三个 8bits 分别去表示 R(红)、G(绿)、B(蓝)。R(0-255),G(0-255),B(0-255),可以表达 256 乘以 256 乘以 256=16777216 种颜色的图片,这样 PNG 24 就能比 PNG 8 表示色彩更丰富的图片。但是所占用的空间相对就更大了。
  • PNG 32:PNG 32 中的 32,相当于 PNG 24 加上 8bits 的透明颜色通道,就相当于 R(红)、G(绿)、B(蓝)、A(透明)。R(0255),G(0255),B(0255),A(0255)。比 PNG 24 多了一个 A(透明),也就是说 PNG 32 能表示跟 PNG 24 一样多的色彩,并且还支持 256 种透明的颜色,能表示更加丰富的图片颜色类型。

PNG 图片的压缩,分两个阶段:

  • 预解析(Prediction):这个阶段就是对 png 图片进行一个预处理,处理后让它更方便后续的压缩。
  • 压缩(Compression):执行 Deflate 压缩,该算法结合了 LZ77 算法和 Huffman 算法对图片进行编码。

16. 说说渐进增强和优雅降级

渐进增强相当于向上兼容,优雅降级相当于向下兼容。向下兼容指的是高版本支持低版本,或者说后期开发的版本能兼容早期开发的版本。

渐进增强:针对低版本浏览器进行页面构建,保证基本功能,再针对高级浏览器进行效果、交互等改进和追加功能,达到更好的用户体验。 优雅降级:一开始就构建完整的功能,再针对低版本浏览器进行兼容。 区别:优雅降级是从复杂的现状开始并试图减少用户体验的供给,而渐进增强则是从一个基础的、能够起到作用的版本开始再不断扩充,以适应未来环境的需要。 绝大多少的大公司都是采用渐进增强的方式,因为业务优先,提升用户体验永远不会排在最前面。

  • 例如新浪微博网站这样亿级用户的网站,前端的更新绝不可能追求某个特效而不考虑低版本用户是否可用。一定是确保低版本到高版本的可访问性再渐进增强。
  • 如果开发的是一面面向青少面的软件或网站,你明确这个群体的人总是喜欢尝试新鲜事物,喜欢炫酷的特效,喜欢把软件更新至最新版本,这种情况再考虑优雅降级。

17. 介绍下 positon 属性

position 属性主要用来定位,常见的属性值如下:

  • absolute 绝对定位,相对于 static 定位以外的第一个父元素进行定位。
  • relative 相对定位,相对于其自身正常位置进行定位。
  • fixed 固定定位,相对于浏览器窗口进行定位。

注意:fixed 元素一定是相对视口定位的吗?https://juejin.cn/post/6844904046663303181

  • static 默认值。没有定位,元素出现在正常的流中。
  • inherit 规定应该从父元素继承 position 属性的值。
  • sticky 粘性定位,当元素在容器中被滚动超过指定的偏移值时,元素在容器内固定在指定位置。

在屏幕范围(viewport)时该元素的位置并不受到定位影响(设置是 top、left 等属性无效),当该元素的位置将要移出偏移范围时,定位又会变成 fixed,根据设置的 left、top 等属性成固定位置的效果。

  • 该元素并不脱离文档流,仍然保留元素原本在文档流中的位置。
  • 元素固定的相对偏移是相对于离它最近的具有滚动框的祖先元素,如果祖先元素都不可以滚动,那么是相对于 viewport 来计算元素的偏移量

19. 如何实现一个自适应的正方形

方法 1:利用 CSS3 的 vw 单位 > vw 会把视口的宽度平均分为 100 份

css
.square {
+  width: 10vw;
+  height: 10vw; // 注意这里是vw
+  background: red;
+}
+
.square {
+  width: 10vw;
+  height: 10vw; // 注意这里是vw
+  background: red;
+}
+

方法 2:利用 margin 或者 padding 的百分比计算是参照父元素的 width 属性

.square {
+width: 10%;
+padding-bottom: 10%;
+height: 0; // 防止内容撑开多余的高度
+background: red;
+}
+
.square {
+width: 10%;
+padding-bottom: 10%;
+height: 0; // 防止内容撑开多余的高度
+background: red;
+}
+
  • 利用 vw 来实现:
css
.square {
+  width: 10%;
+  height: 10vw;
+  background: tomato;
+}
+复制代码
+
.square {
+  width: 10%;
+  height: 10vw;
+  background: tomato;
+}
+复制代码
+
  • 利用元素的 margin/padding 百分比是相对父元素 width 的性质来实现:
css
.square {
+  width: 20%;
+  height: 0;
+  padding-top: 20%;
+  background: orange;
+}
+复制代码
+
.square {
+  width: 20%;
+  height: 0;
+  padding-top: 20%;
+  background: orange;
+}
+复制代码
+
  • 利用子元素的 margin-top 的值来实现:
css
.square {
+  width: 30%;
+  overflow: hidden;
+  background: yellow;
+}
+.square::after {
+  content: '';
+  display: block;
+  margin-top: 100%;
+}
+复制代码
+
.square {
+  width: 30%;
+  overflow: hidden;
+  background: yellow;
+}
+.square::after {
+  content: '';
+  display: block;
+  margin-top: 100%;
+}
+复制代码
+

20. 如何实现三栏布局

三栏布局是很常见的一种页面布局方式。左右固定,中间自适应。实现方式有很多种方法。 第一种:flex

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .container {
+        display: flex;
+      }
+
+      .left {
+        flex-basis: 200px;
+        background: green;
+      }
+
+      .main {
+        flex: 1;
+        background: red;
+      }
+
+      .right {
+        flex-basis: 200px;
+        background: green;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="container">
+      <div class="left">left</div>
+      <div class="main">main</div>
+      <div class="right">right</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .container {
+        display: flex;
+      }
+
+      .left {
+        flex-basis: 200px;
+        background: green;
+      }
+
+      .main {
+        flex: 1;
+        background: red;
+      }
+
+      .right {
+        flex-basis: 200px;
+        background: green;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="container">
+      <div class="left">left</div>
+      <div class="main">main</div>
+      <div class="right">right</div>
+    </div>
+  </body>
+</html>
+

第二种:position + margin

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      body,
+      html {
+        padding: 0;
+        margin: 0;
+      }
+
+      .left,
+      .right {
+        position: absolute;
+        top: 0;
+        background: red;
+      }
+
+      .left {
+        left: 0;
+        width: 200px;
+      }
+
+      .right {
+        right: 0;
+        width: 200px;
+      }
+
+      .main {
+        margin: 0 200px;
+        background: green;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="container">
+      <div class="left">left</div>
+      <div class="right">right</div>
+      <div class="main">main</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      body,
+      html {
+        padding: 0;
+        margin: 0;
+      }
+
+      .left,
+      .right {
+        position: absolute;
+        top: 0;
+        background: red;
+      }
+
+      .left {
+        left: 0;
+        width: 200px;
+      }
+
+      .right {
+        right: 0;
+        width: 200px;
+      }
+
+      .main {
+        margin: 0 200px;
+        background: green;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="container">
+      <div class="left">left</div>
+      <div class="right">right</div>
+      <div class="main">main</div>
+    </div>
+  </body>
+</html>
+

第三种:float + margin

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      body,
+      html {
+        padding: 0;
+        margin: 0;
+      }
+
+      .left {
+        float: left;
+        width: 200px;
+        background: red;
+      }
+
+      .main {
+        margin: 0 200px;
+        background: green;
+      }
+
+      .right {
+        float: right;
+        width: 200px;
+        background: red;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="container">
+      <div class="left">left</div>
+      <div class="right">right</div>
+      <div class="main">main</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      body,
+      html {
+        padding: 0;
+        margin: 0;
+      }
+
+      .left {
+        float: left;
+        width: 200px;
+        background: red;
+      }
+
+      .main {
+        margin: 0 200px;
+        background: green;
+      }
+
+      .right {
+        float: right;
+        width: 200px;
+        background: red;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="container">
+      <div class="left">left</div>
+      <div class="right">right</div>
+      <div class="main">main</div>
+    </div>
+  </body>
+</html>
+

双飞翼布局

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <style>
+      .left,
+      .right {
+        width: 100px;
+        height: 100px;
+        background: #fac;
+        float: left;
+      }
+      .right {
+        float: right;
+      }
+      /*浮动了,脱离文档流*/
+      .center {
+        height: 150px;
+        background: #cfa;
+        overflow: hidden;
+        display: flow-root;
+      }
+    </style>
+    <div class="wrapper">
+      去掉任何一个实现两栏布局
+      <div class="left"></div>
+      <div class="right"></div>
+      <div class="center"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+  <body>
+    <style>
+      .left,
+      .right {
+        width: 100px;
+        height: 100px;
+        background: #fac;
+        float: left;
+      }
+      .right {
+        float: right;
+      }
+      /*浮动了,脱离文档流*/
+      .center {
+        height: 150px;
+        background: #cfa;
+        overflow: hidden;
+        display: flow-root;
+      }
+    </style>
+    <div class="wrapper">
+      去掉任何一个实现两栏布局
+      <div class="left"></div>
+      <div class="right"></div>
+      <div class="center"></div>
+    </div>
+  </body>
+</html>
+

圣杯布局

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: column;
+      }
+
+      .header,
+      .footer,
+      .left,
+      .right {
+        flex: 0 0 20%; /*不参与伸缩,占20%*/
+        border: 1px solid black;
+        box-sizing: border-box;
+      }
+
+      .contain {
+        flex: 1 1 auto;
+        display: flex;
+      }
+
+      .center {
+        flex: 1 1 auto;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="header">header</div>
+      <div class="contain">
+        <div class="left">left</div>
+        <div class="center">center</div>
+        <div class="right">right</div>
+      </div>
+      <div class="footer">footer</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+      }
+
+      .wrapper {
+        resize: both;
+        overflow: hidden;
+        width: 300px;
+        height: 300px;
+        border: 1px solid black;
+        display: flex;
+        flex-direction: column;
+      }
+
+      .header,
+      .footer,
+      .left,
+      .right {
+        flex: 0 0 20%; /*不参与伸缩,占20%*/
+        border: 1px solid black;
+        box-sizing: border-box;
+      }
+
+      .contain {
+        flex: 1 1 auto;
+        display: flex;
+      }
+
+      .center {
+        flex: 1 1 auto;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="wrapper">
+      <div class="header">header</div>
+      <div class="contain">
+        <div class="left">left</div>
+        <div class="center">center</div>
+        <div class="right">right</div>
+      </div>
+      <div class="footer">footer</div>
+    </div>
+  </body>
+</html>
+
  1. 从属关系区别

@import是 CSS 提供的语法规则,只有导入样式表的作用link是 HTML 提供的标签,不仅可以加载 CSS 文件,还可以定义 RSS、rel 连接属性等。 2. 加载顺序区别

加载页面时,link标签引入的 CSS 被同时加载@import引入的 CSS 将在页面加载完毕后被加载。 3. 兼容性区别

@import是 CSS2.1 才有的语法,故只可在 IE5+ 才能识别;link 标签作为 HTML 元素,不存在兼容性问题。 4. DOM 可控性区别

可以通过 JS 操作 DOM ,插入link标签来改变样式;由于 DOM 方法是基于文档的,无法使用@import的方式插入样式。

23. 清除浮动的方法

  • 给浮动元素父级设置高度
  • 父级同时浮动(需要给父级同级元素添加浮动)
  • 父级设置成 inline-block,其 margin: 0 auto 居中方式失效
  • 给父级添加 overflow:hidden 清除浮动方法
  • 万能清除法 after 伪类清浮动(现在主流方法,推荐使用)

使用 clear 属性清除浮动的原理?

使用 clear 属性清除浮动,其语法如下:

css
clear: none|left|right|both 复制代码;
+
clear: none|left|right|both 复制代码;
+

如果单看字面意思,clear:left 是“清除左浮动”,clear:right 是“清除右浮动”,实际上,这种解释是有问题的,因为浮动一直还在,并没有清除。 官方对 clear 属性解释:“元素盒子的边不能和前面的浮动元素相邻”,对元素设置 clear 属性是为了避免浮动元素对该元素的影响,而不是清除掉浮动。

还需要注意 clear 属性指的是元素盒子的边不能和前面的浮动元素相邻,注意这里“前面的”3 个字,也就是 clear 属性对“后面的”浮动元素是不闻不问的。考虑到 float 属性要么是 left,要么是 right,不可能同时存在,同时由于 clear 属性对“后面的”浮动元素不闻不问,因此,当 clear:left 有效的时候,clear:right 必定无效,也就是此时 clear:left 等同于设置 clear:both;同样地,clear:right 如果有效也是等同于设置 clear:both。由此可见,clear:left 和 clear:right 这两个声明就没有任何使用的价值,至少在 CSS 世界中是如此,直接使用 clear:both 吧。 clear 属性只有块级元素才有效的,而**::after 等伪元素默认都是内联**水平,这就是借助伪元素清除浮动影响时需要设置 display 属性值的原因。

对 requestAnimationframe 的理解

实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout 来实现,CSS3 中可以使用 transition 和 animation 来实现,HTML5 中的 canvas 也可以实现。除此之外,HTML5 提供一个专门用于请求动画的 API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。 MDN 对该方法的描述:

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

语法: window.requestAnimationFrame(callback);   其中,callback 是下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入 DOMHighResTimeStamp 参数,它表示 requestAnimationFrame() 开始去执行回调函数的时刻。该方法属于宏任务,所以会在执行完微任务之后再去执行。 取消动画: 使用 cancelAnimationFrame()来取消执行动画,该方法接收一个参数——requestAnimationFrame 默认返回的 id,只需要传入这个 id 就可以取消动画了。 优势:

  • CPU 节能:使用 SetTinterval 实现的动画,当页面被隐藏或最小化时,SetTinterval 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费 CPU 资源。而 RequestAnimationFrame 则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统走的 RequestAnimationFrame 也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。
  • 函数节流:在高频率事件( resize, scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,RequestAnimationFrame 可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销,一个刷新间隔内函数执行多次时没有意义的,因为多数显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来。
  • 减少 DOM 操作:requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒 60 帧。

setTimeout 执行动画的缺点:它通过设定间隔时间来不断改变图像位置,达到动画效果。但是容易出现卡顿、抖动的现象;原因是:

  • settimeout 任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;
  • settimeout 的固定时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧。

伪元素和伪类的区别和作用?

  • 伪元素:在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。它们只在外部显示可见,但不会在文档的源代码中找到它们,因此,称为“伪”元素。例如:
  • 伪类:将特殊的效果添加到特定选择器上。它是已有元素上添加类别的,不会产生新的元素。例如:

总结: 伪类是通过在元素选择器上加⼊伪类改变元素状态,⽽伪元素通过对元素的操作进⾏对元素的改变。

25. css resetnormalize.css 有什么区别?

  • 两者都是通过重置样式,保持浏览器样式的一致性
  • 前者几乎为所有标签添加了样式,后者保持了许多浏览器样式,保持尽可能的一致
  • 后者修复了常见的桌面端和移动端浏览器的 bug:包含了 HTML5 元素的显示设置、预格式化文字的 font-size 问题、在 IE9 中 SVG 的溢出、许多出现在各浏览器和操作系统中的与表单相关的 bug。
  • 前者中含有大段的继承链
  • 后者模块化,文档较前者来说丰富

27. 如何避免重绘或者重排?

  1. 集中改变样式 改变 class
css
// 判断是否是黑色系样式
+const theme = isDark ? 'dark' : 'light'
+
+// 根据判断来设置不同的class
+ele.setAttribute('className', theme)
+
// 判断是否是黑色系样式
+const theme = isDark ? 'dark' : 'light'
+
+// 根据判断来设置不同的class
+ele.setAttribute('className', theme)
+
  1. 使用 DocumentFragment

我们可以通过 createDocumentFragment 创建一个游离于 DOM 树之外的节点,然后在此节点上批量操作,最后插入 DOM 树中,因此只触发一次重排

javascript
var fragment = document.createDocumentFragment();
+
+for (let i = 0; i < 10; i++) {
+  let node = document.createElement("p");
+  node.innerHTML = i;
+  fragment.appendChild(node);
+}
+
+document.body.appendChild(fragment);
+
var fragment = document.createDocumentFragment();
+
+for (let i = 0; i < 10; i++) {
+  let node = document.createElement("p");
+  node.innerHTML = i;
+  fragment.appendChild(node);
+}
+
+document.body.appendChild(fragment);
+
  1. 提升为合成层

将元素提升为合成层有以下优点:

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint

提升合成层的最好方式是使用 CSS 的 will-change 属性:

javascript
#target {
+  will-change: transform;
+}
+
#target {
+  will-change: transform;
+}
+

28. 如何触发重排和重绘?

  • 添加、删除、更新 DOM 节点
  • 通过 display: none 隐藏一个 DOM 节点-触发重排和重绘
  • 通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化
  • 移动或者给页面中的 DOM 节点添加动画
  • 添加一个样式表,调整样式属性
  • 用户行为,例如调整窗口大小,改变字号,或者滚动。

29. 重绘与重排的区别?

  • 重排: 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,表现为重新生成布局,重新排列元素
  • 重绘: 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,表现为某些元素的外观被改变

30. 如何优化图片

  1. 对于很多装饰类图片,尽量不用图片,因为这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 雪碧图
  5. 选择正确的图片格式:
  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
  • 照片使用 JPEG

34. 什么是渐进式渲染(progressive rendering)?

渐进式渲染是用于提高网页性能(尤其是提高用户感知的加载速度),以尽快呈现页面的技术。 一些举例:

  • 图片懒加载——页面上的图片不会一次性全部加载。当用户滚动页面到图片部分时,JavaScript 将加载并显示图像。
  • 确定显示内容的优先级(分层次渲染)——为了尽快将页面呈现给用户,页面只包含基本的最少量的 CSS、脚本和内容,然后可以使用延迟加载脚本或监听DOMContentLoaded/load事件加载其他资源和内容。
  • 异步加载 HTML 片段——当页面通过后台渲染时,把 HTML 拆分,通过异步请求,分块发送给浏览器。

39. img 标签在页面中有 1px 的边框,怎么处理

css
img {
+  border: 0;
+}
+
img {
+  border: 0;
+}
+

40. 如何绝对居中(不用定位)

方法一: 使用 flex 弹性盒来绝对居中 方法二: 设置 margin-leftcalc(50% - 50px);

42. 我有 5 个 div 在一行,我要让 div 与 div 直接间距 10px 且最左最右两边的 div 据边框 10px,同时在我改变窗口大小时,这个 10px 不能变,div 根据窗口改变大小(长天星斗)

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+
+  <body>
+    <style>
+      * {
+        margin: 0;
+      }
+
+      .container {
+        display: flex;
+      }
+
+      .container > div {
+        outline: 1px solid;
+        margin: 0 5px;
+        flex-grow: 1;
+      }
+
+      .container > div:first-child {
+        margin-left: 10px;
+      }
+
+      .container > div:last-child {
+        margin-right: 10px;
+      }
+    </style>
+    <div class="container">
+      <div>1</div>
+      <div>2</div>
+      <div>3</div>
+      <div>4</div>
+      <div>5</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+  </head>
+
+  <body>
+    <style>
+      * {
+        margin: 0;
+      }
+
+      .container {
+        display: flex;
+      }
+
+      .container > div {
+        outline: 1px solid;
+        margin: 0 5px;
+        flex-grow: 1;
+      }
+
+      .container > div:first-child {
+        margin-left: 10px;
+      }
+
+      .container > div:last-child {
+        margin-right: 10px;
+      }
+    </style>
+    <div class="container">
+      <div>1</div>
+      <div>2</div>
+      <div>3</div>
+      <div>4</div>
+      <div>5</div>
+    </div>
+  </body>
+</html>
+

48. pxem 的区别

pxpixel 像素,是相对于屏幕分辨率而言的,是一个相对绝对单位,即在同一设备上每个设备像素所代表的物理长度是固定不变的(绝对性),但在不同设备间每个设备像素所代表的物理长度是可以变化的(相对性)。 > em 一个相对单位,不是一个固定的值,来源于纸张印刷业,在 web 领域它指代基准字号,浏览器默认渲染文字大小是 16px ,它会继承计算后的父级元素的 font-size。

49. div 之间的间隙是怎么产生的,应该怎么消除?

原因:浏览器解析的时候,会把回车换行符解析成一定的间隙,间隙的大小跟默认的字体大小设置有关。 解决:其父元素加上 font-size:0 的属性,但是字体需要额外处理

24. display:inline-block 什么时候会显示间隙?

  • 有空格时会有间隙,可以删除空格解决;
  • margin正值时,可以让margin使用负值解决;
  • 使用font-size时,可通过设置font-size:0letter-spacingword-spacing解决;

57. 你怎么处理页面兼容性问题?

  1. 统一标准模式
  2. 利用 CSS 重置技术初始化默认样式
  3. 针对不同浏览器采用不同的解决方案

63. 什么是继承?CSS 中哪些属性可以继承?哪些不可以继承?

  • 继承,指元素可以自动获得祖先元素的某些 CSS 属性。通常来说文本类的属性具有继承性。
  • 文本类的样式可以继承:例如 color、 font-size、 line-height、 font-family、 font-weight、 font-weight、 text-decoration、 letter-spacing、text-align 等等
  • display、 margin、 padding、 border、 background、 position、 float 等则不会被继承

65. CSS 的计算属性知道吗

calc( ) 函数,主要用于指定元素的长度,支持所有 CSS 长度单位,运算符前后都需要保留一个空格。 比如: width: calc(100% - 50px);

66. 为何 CSS 放在 HTML 头部?

为了**尽早让浏览器拿到 CSS 并且生成 **CSSOM,然后与 HTML 一次性生成最终的 RenderTree,渲染一次即可。如果放在 HTML 底部,会出现渲染卡顿的现象影响性能和用户体验。

67. background-size 有哪 4 种值类型?

  • length:设置背景图片高度和宽度。第一个值设置宽度,第二个值设置的高度。如果只给出一个值,第二个是设置为 auto(自动)
  • percentage:将计算相对于背景定位区域的百分比。第一个值设置宽度,第二个值设置的高度。如果只给出一个值,第二个是设置为“auto(自动)”
  • cover:此时会保持图像的纵横比并将图像缩放成将完全覆盖背景定位区域的最小大小。
  • contain:此时会保持图像的纵横比并将图像缩放成将适合背景定位区域的最大大小。

74. 有一个高度自适应的 div,里面有 2 个 div,一个高度 100px,希望另一个填满剩下的高度?(CSS 实现)

方法一:利用定位

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      html,
+      body {
+        height: 100%;
+        margin: 0px;
+        padding: 0px;
+      }
+
+      .main {
+        position: relative;
+        height: 100%;
+      }
+
+      .box1 {
+        height: 100px;
+        background-color: red;
+      }
+
+      .box2 {
+        position: absolute;
+        width: 100%;
+        top: 100px;
+        bottom: 0px;
+        background-color: blue;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="main">
+      <div class="box1"></div>
+      <div class="box2"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      html,
+      body {
+        height: 100%;
+        margin: 0px;
+        padding: 0px;
+      }
+
+      .main {
+        position: relative;
+        height: 100%;
+      }
+
+      .box1 {
+        height: 100px;
+        background-color: red;
+      }
+
+      .box2 {
+        position: absolute;
+        width: 100%;
+        top: 100px;
+        bottom: 0px;
+        background-color: blue;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="main">
+      <div class="box1"></div>
+      <div class="box2"></div>
+    </div>
+  </body>
+</html>
+

方法二:利用计算属性calc

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      html,
+      body {
+        height: 100%;
+        margin: 0px;
+        padding: 0px;
+      }
+
+      .main {
+        height: 100%;
+      }
+
+      .box1 {
+        height: 100px;
+        background-color: red;
+      }
+
+      .box2 {
+        height: calc(100% - 100px);
+        background-color: blue;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="main">
+      <div class="box1"></div>
+      <div class="box2"></div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      html,
+      body {
+        height: 100%;
+        margin: 0px;
+        padding: 0px;
+      }
+
+      .main {
+        height: 100%;
+      }
+
+      .box1 {
+        height: 100px;
+        background-color: red;
+      }
+
+      .box2 {
+        height: calc(100% - 100px);
+        background-color: blue;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div class="main">
+      <div class="box1"></div>
+      <div class="box2"></div>
+    </div>
+  </body>
+</html>
+

inline 和 block 区别(长宽,padding,margin)

77. 当 margin-top、padding-top 的值是百分比时,分别是如何计算的?

可以对元素的 margin 设置百分数,百分数是相对于父元素的 width 计算,不管是 margin-top/margin-bottom 还是 margin-left/margin-right。(padding 同理) 如果没有为元素声明 width,在这种情况下,元素框的总宽度包括外边距取决于父元素的 width,这样可能得到“流式”页面,即元素的外边距会扩大或缩小以适应父元素的实际大小。如果对这个文档设置样式,使其元素使用百分数外边距,当用户修改浏览窗口的宽度时,外边距会随之扩大或缩小。

81. 在 rem 自适应页面中使用 sprite 会出现背景图不随元素放大缩小的情况,如何解决?

backgroud-size 也换算为 rem 作为单位

98. 如何将大量可变化的图片显示到页面并不影响浏览器性能?

使用图片懒加载。将页面中未出现在可视区域的图片先不做加载,等滚动到可视区域后,再加载。

99. 图片与图片之间默认存在的间距如何去除?

  1. 将图片全部脱离文档流
  2. 将图片父元素设置文字大小为 0

102. 不使用 border 属性,使用其他属性模拟边框。

使用 outlinebox-shadow 都可以模拟出边框。

103. 阅读代码,计算 ul 的高度。

 <ul style="overflow: hidden;">
+   <li style="height: 100px; float: left;"></li>
+ </ul>
+
 <ul style="overflow: hidden;">
+   <li style="height: 100px; float: left;"></li>
+ </ul>
+

ul 的高度为 100px,虽然子级 li 浮动会造成父元素 ul 的高度塌陷,但父元素触发了 BFC,所以高度可以自动找回。

105. 如何使用媒体查询实现视口宽度大于 320px 小于 640pxdiv 元素宽度变成 30%?

css
@media screen and (min-width: 320px) and (max-width: 640px) {
+  div {
+    width: 30%;
+  }
+}
+
@media screen and (min-width: 320px) and (max-width: 640px) {
+  div {
+    width: 30%;
+  }
+}
+

106. 什么是 HTML 实体?

即对当前文档的编码方式不能包含的字符提供一种转义表示。例如对于 “>” 符号,它是 HTML 元素的一部分,如果要在文档中显示就需要进行转义为“>”。

109. 在定位属性 position 中,哪个值会脱离文档流?

absolute、fixed

画一条 0.5px 的线

  • 采用 transform: scale()的方式,该方法用来定义元素的 2D 缩放转换:
css
transform: scale(0.5, 0.5);
+
transform: scale(0.5, 0.5);
+
  • 采用 meta viewport 的方式
css
<meta name="viewport" content="width=device-width, initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5"/>
+
<meta name="viewport" content="width=device-width, initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5"/>
+

这样就能缩放到原来的 0.5 倍,如果是 1px 那么就会变成 0.5px。viewport 只针对于移动端,只在移动端上才能看到效果

6. 如何解决 1px 问题?

1px 问题指的是:在一些 Retina屏幕 的机型上,移动端页面的 1px 会变得很粗,呈现出不止 1px 的效果。原因很简单——CSS 中的 1px 并不能和移动设备上的 1px 划等号。它们之间的比例关系有一个专门的属性来描述:

html
window.devicePixelRatio = 设备的物理像素 / CSS像素。
+
window.devicePixelRatio = 设备的物理像素 / CSS像素。
+

打开 Chrome 浏览器,启动移动端调试模式,在控制台去输出这个 devicePixelRatio 的值。这里选中 iPhone6/7/8 这系列的机型,输出的结果就是 2: image.png 这就意味着设置的 1px CSS 像素,在这个设备上实际会用 2 个物理像素单元来进行渲染,所以实际看到的一定会比 1px 粗一些。 解决 1px 问题的三种思路:

思路一:直接写 0.5px

如果之前 1px 的样式这样写:

css
border: 1px solid #333;
+
border: 1px solid #333;
+

可以先在 JS 中拿到 window.devicePixelRatio 的值,然后把这个值通过 JSX 或者模板语法给到 CSS 的 data 里,达到这样的效果(这里用 JSX 语法做示范):

html
<div id="container" data-device="{{window.devicePixelRatio}}"></div>
+
<div id="container" data-device="{{window.devicePixelRatio}}"></div>
+

然后就可以在 CSS 中用属性选择器来命中 devicePixelRatio 为某一值的情况,比如说这里尝试命中 devicePixelRatio 为 2 的情况:

css
#container[data-device="2"] {
+  border: 0.5px solid #333;
+}
+
#container[data-device="2"] {
+  border: 0.5px solid #333;
+}
+

直接把 1px 改成 1/devicePixelRatio 后的值,这是目前为止最简单的一种方法。这种方法的缺陷在于兼容性不行,IOS 系统需要 8 及以上的版本,安卓系统则直接不兼容。

思路二:伪元素先放大后缩小

这个方法的可行性会更高,兼容性也更好。唯一的缺点是代码会变多。 思路是先放大、后缩小:在目标元素的后面追加一个 ::after 伪元素,让这个元素布局为 absolute 之后、整个伸展开铺在目标元素上,然后把它的宽和高都设置为目标元素的两倍,border 值设为 1px。接着借助 CSS 动画特效中的放缩能力,把整个伪元素缩小为原来的 50%。此时,伪元素的宽高刚好可以和原有的目标元素对齐,而 border 也缩小为了 1px 的二分之一,间接地实现了 0.5px 的效果。

css
#container[data-device="2"] {
+    position: relative;
+}
+#container[data-device="2"]::after{
+      position:absolute;
+      top: 0;
+      left: 0;
+      width: 200%;
+      height: 200%;
+      content:"";
+      transform: scale(0.5);
+      transform-origin: left top;
+      box-sizing: border-box;
+      border: 1px solid #333;
+    }
+}
+复制代码
+
#container[data-device="2"] {
+    position: relative;
+}
+#container[data-device="2"]::after{
+      position:absolute;
+      top: 0;
+      left: 0;
+      width: 200%;
+      height: 200%;
+      content:"";
+      transform: scale(0.5);
+      transform-origin: left top;
+      box-sizing: border-box;
+      border: 1px solid #333;
+    }
+}
+复制代码
+

思路三:viewport 缩放来解决

这个思路就是对 meta 标签里几个关键属性下手:

html
<meta
+  name="viewport"
+  content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no"
+/>
+
<meta
+  name="viewport"
+  content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no"
+/>
+

这里针对像素比为 2 的页面,把整个页面缩放为了原来的 1/2 大小。这样,本来占用 2 个物理像素的 1px 样式,现在占用的就是标准的一个物理像素。根据像素比的不同,这个缩放比例可以被计算为不同的值,用 js 代码实现如下:

javascript
const scale = 1 / window.devicePixelRatio;
+// 这里 metaEl 指的是 meta 标签对应的 Dom
+metaEl.setAttribute(
+  "content",
+  `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`
+);
+
const scale = 1 / window.devicePixelRatio;
+// 这里 metaEl 指的是 meta 标签对应的 Dom
+metaEl.setAttribute(
+  "content",
+  `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`
+);
+

这样解决了,但这样做的副作用也很大,整个页面被缩放了。这时 1px 已经被处理成物理像素大小,这样的大小在手机上显示边框很合适。但是,一些原本不需要被缩小的内容,比如文字、图片等,也被无差别缩小掉了。

118. 绝对定位和浮动有什么异同?

  1. 都会脱离文档流,改变元素盒子类型,将元素变为块级
  2. 都会创建 BFC
  3. 不同点在于对包含块的定义、兄弟元素间的影响、可摆放的位置、能否设置 z-index

120. 文本“强制换行”的属性是什么?

word-break: break-all;

121. 阅读代码,分析最终执行结果 div 的水平、垂直位置距离。

css
div {
+  width: 200px;
+  height: 100px;
+  padding: 10px;
+  transform: translate(50%, 50%);
+}
+
div {
+  width: 200px;
+  height: 100px;
+  padding: 10px;
+  transform: translate(50%, 50%);
+}
+

水平位移 110px 垂直位移 60px。水平方向参照的是元素的宽度,处置方向参照的是元素的高度。HTM_L 元素默认都以标准盒模型,当元素存在内边距,计算时还要包含内边距。即 _22050%12050%

122. 设置了元素的过渡后需要有触发条件才能看到效果,列举出可用的触发条件。

  1. :hover 、:checked 等伪类
  2. 媒体查询,当改变窗口尺寸触发
  3. js 触发,用脚本更改元素样式

123. 怎样把背景图附着在内容上?

background-attachment: fixed;

124. CSS 优化、提高性能的方法有哪些?

  1. 最好使用表示语义的名字。一个好的类名应该是描述他是什么而不是像什么
  2. 避免 !important,可以选择其他选择器
  3. 使用紧凑的语法
  4. 避免不必要的命名空间
  5. 尽可能的精简规则,你可以合并不同类里的重复规则

125. 网页中应该使用奇数还是偶数的字体?

偶数。偶数字号相对更容易与 web 设计的其他部分构成比例关系

126. marginpadding 分别适合什么场景使用?

  • 使用 margin:需要在 border 外侧添加空白、空白处不需要背景色、上下相连的两个盒子之间的空白,需要相互抵消时。
  • 使用 padding: 需要在 border 内侧添加空白、空白处需要背景色

127. 对于 line-height 是如何理解的?

  • 行高指一行文字的高度,两行文字间基线与基线之间的距离。在 CSS 中,起高度作用的是 heightline-height
  • 使用行高等于高的方式可以实现单行文字垂直居中
  • display 设置为 inline-block 可以实现多行文本居中

128. 如何让 Chrome 支持小于 12px 的文字?

可以配合 CSS3 中的 transform scale 属性来实现

129. 如果需要手动写动画,你认为最小时间间隔是多久?

多数显示器默认频率是 60Hz,即 1s 刷新 60 次,理论上最小间隔为 1/60*1000ms = 16.7s

131. style 标签写在 body 后面与 body 前面有什么区别?

页面加载自上而下,当然是先加载样式。写在 body 标签后由于浏览器以逐行方式对 HTML 文档进行解析,当解析到写在尾部的样式表(外联或写在 style 标签)会导致浏览器停止之前的渲染,等待加载且解析样式表完成之后重新渲染

132. 阐述一下 CSS Sprites

参考答案

将一个页面涉及到的所有图片都包含到一张大图中去,然后利用 CSSbackground-image、background- repeat、background-position 的组合进行背景定位。利用 CSS Sprites 能很好地减少网页的 http 请求,从而大大的提高页面的性能;还能减少图片的字节。

133. 写出背景色渐变的 CSS 代码

参考答案

线性渐变

  • direction:用角度值指定渐变的方向(或角度);
  • color-stop1,color-stop2,...:用于指定渐变的起止颜色

径向渐变

CSS 径向颜色渐变(Radial Gradients)跟线性渐变(linear gradients)不一样,它不是沿着一个方向渐变,而是以一个点为中心,向四周辐射渐变。

语法:

css
background: linear-gradient(direction, color-stop1, color-stop2, ...);
+
background: linear-gradient(direction, color-stop1, color-stop2, ...);
+
css
background-image: radial-gradient(
+  [<position> || <angle>],[<shape> || <size>],<stop>,<stop>,<stop>
+);
+
background-image: radial-gradient(
+  [<position> || <angle>],[<shape> || <size>],<stop>,<stop>,<stop>
+);
+

134. 写出添加下划线的 CSS 代码

参考答案

一般有两种方法:

  • 通过 CSS 下划线代码:text-decoration:underline 来设置文字下划线。
  • 通过设置 divborder 实现效果

135. 写出让对象顺时针旋转 90 度的 CSS 代码(最好附带动画效果)

参考答案

顺时针旋转可以使用 CSS3 新增的 transform 属性,属性值对应 rotate(90deg),如果要附带动画效果,那么可以添加 transition 过渡。下面是一段示例代码:

html
<div></div>
+
<div></div>
+
css
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  transition: all 1s;
+}
+div:hover {
+  transform: rotate(90deg);
+}
+
div {
+  width: 100px;
+  height: 100px;
+  background-color: red;
+  transition: all 1s;
+}
+div:hover {
+  transform: rotate(90deg);
+}
+

136. CSS 优先级顺序正确的是( )

A.  !important > class > id > tag

B.  !important > tag > class > id

C.  !important  > id > class > tag

D.  Class > !important > id > tag

参考答案

选 C

解析:

关于 CSS 选择器的优先级,具体可以参阅下图:

137. 如何产生带有正方形项目的列表

A. type:square

B. type:2

C. list-style-type:square

D. list-type:square

参考答案

C

解析:

CSS list-style-type 属性可以设置不同的列表样式

具体属性值可以参阅:https://www.w3school.com.cn/cssref/pr_list-style-type.asp

138. 手写一个三栏布局,要求:垂直三栏布局,所有两栏宽度固定,中间自适应

参考答案:

这是一道经典的面试题,实现的方式很多,这里列举两种。

方法一:flexbox 的解决方案

方法二:网格布局解决方案

html
<body>
+  <!-- flexbox解决方案 -->
+  <section class="layout flexbox">
+    <article class="left-center-right">
+      <div class="left"></div>
+      <div class="center">
+        <h1>flexbox的解决方案</h1>
+        <p>1.这是布局的中间部分</p>
+        <p>2.这是布局的中间部分</p>
+      </div>
+      <div class="right"></div>
+    </article>
+  </section>
+</body>
+
<body>
+  <!-- flexbox解决方案 -->
+  <section class="layout flexbox">
+    <article class="left-center-right">
+      <div class="left"></div>
+      <div class="center">
+        <h1>flexbox的解决方案</h1>
+        <p>1.这是布局的中间部分</p>
+        <p>2.这是布局的中间部分</p>
+      </div>
+      <div class="right"></div>
+    </article>
+  </section>
+</body>
+
css
<style>
+.layout.flexbox {
+margin-top: 140px;
+}
+
+.layout.flexbox .left-center-right {
+display: flex;
+}
+
+.layout.flexbox .left {
+width: 300px;
+background: red;
+}
+
+.layout.flexbox .center {
+flex: 1;
+background: yellow;
+}
+
+.layout.flexbox .right {
+width: 300px;
+background: blue;
+}
+</style>
+
<style>
+.layout.flexbox {
+margin-top: 140px;
+}
+
+.layout.flexbox .left-center-right {
+display: flex;
+}
+
+.layout.flexbox .left {
+width: 300px;
+background: red;
+}
+
+.layout.flexbox .center {
+flex: 1;
+background: yellow;
+}
+
+.layout.flexbox .right {
+width: 300px;
+background: blue;
+}
+</style>
+
html
<body>
+  <!-- 网格布局的解决方案     -->
+  <section class="layout grid">
+    <article class="left-center-right">
+      <div class="left"></div>
+      <div class="center">
+        <h1>网格布局的解决方案</h1>
+        <p>1.这是布局的中间部分</p>
+        <p>2.这是布局的中间部分</p>
+      </div>
+      <div class="right"></div>
+    </article>
+  </section>
+</body>
+
<body>
+  <!-- 网格布局的解决方案     -->
+  <section class="layout grid">
+    <article class="left-center-right">
+      <div class="left"></div>
+      <div class="center">
+        <h1>网格布局的解决方案</h1>
+        <p>1.这是布局的中间部分</p>
+        <p>2.这是布局的中间部分</p>
+      </div>
+      <div class="right"></div>
+    </article>
+  </section>
+</body>
+
css
<style>
+.layout.grid .left-center-right {
+display: grid;
+width: 100%;
+grid-template-rows: 100px;
+grid-template-columns: 300px auto 300px;
+}
+
+.layout.grid .left {
+background: red;
+}
+
+.layout.grid .center {
+background: yellow;
+}
+
+.layout.grid .right {
+background: blue;
+}
+</style>
+
<style>
+.layout.grid .left-center-right {
+display: grid;
+width: 100%;
+grid-template-rows: 100px;
+grid-template-columns: 300px auto 300px;
+}
+
+.layout.grid .left {
+background: red;
+}
+
+.layout.grid .center {
+background: yellow;
+}
+
+.layout.grid .right {
+background: blue;
+}
+</style>
+

css 如何做一个 loading 动画(关键帧、动画循环)

left 和 margin-left 哪性能好

left:脱离文档流

心跳

html
<html>
+  <head>
+    <title>demo</title>
+    <style type="text/css">
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      body {
+        width: 100vw;
+        height: 100vh;
+        justify-content: center;
+        align-items: center;
+        display: flex;
+      }
+      .love {
+        width: 200px;
+        height: 200px;
+        background: red;
+        position: relative;
+        transform: rotate(45deg);
+        animation: heartbit 1s infinite;
+      }
+      .love::after,
+      .love::before {
+        content: "";
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        border-radius: 50%;
+        background: red;
+      }
+      .love::after {
+        /*background: blue;*/
+        left: -100px;
+      }
+      .love::before {
+        /*background: blue;*/
+        top: -100px;
+      }
+
+      @keyframes heartbit {
+        0% {
+          width: 200px;
+          height: 200px;
+        }
+        20% {
+          width: 230px;
+          height: 230px;
+        }
+        100% {
+          width: 200px;
+          height: 200px;
+        }
+      }
+    </style>
+  </head>
+  <body>
+    <div class="love"></div>
+  </body>
+</html>
+
<html>
+  <head>
+    <title>demo</title>
+    <style type="text/css">
+      * {
+        padding: 0;
+        margin: 0;
+      }
+      body {
+        width: 100vw;
+        height: 100vh;
+        justify-content: center;
+        align-items: center;
+        display: flex;
+      }
+      .love {
+        width: 200px;
+        height: 200px;
+        background: red;
+        position: relative;
+        transform: rotate(45deg);
+        animation: heartbit 1s infinite;
+      }
+      .love::after,
+      .love::before {
+        content: "";
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        border-radius: 50%;
+        background: red;
+      }
+      .love::after {
+        /*background: blue;*/
+        left: -100px;
+      }
+      .love::before {
+        /*background: blue;*/
+        top: -100px;
+      }
+
+      @keyframes heartbit {
+        0% {
+          width: 200px;
+          height: 200px;
+        }
+        20% {
+          width: 230px;
+          height: 230px;
+        }
+        100% {
+          width: 200px;
+          height: 200px;
+        }
+      }
+    </style>
+  </head>
+  <body>
+    <div class="love"></div>
+  </body>
+</html>
+

如何写一个移动端 html 抖音界面和刷视频的功能实现 【手撕代码】

flex 骰子

html
<!DOCTYPE html>
+
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+    <title>骰子布局</title>
+
+    <style>
+      body {
+        display: flex;
+        justify-content: space-around;
+      }
+
+      body > div {
+        display: flex;
+
+        width: 100px;
+
+        height: 100px;
+
+        border-radius: 4px;
+
+        border: 2px solid red;
+
+        box-sizing: border-box;
+      }
+
+      p {
+        width: 15px;
+
+        height: 15px;
+
+        background-color: black;
+
+        border-radius: 50%;
+
+        margin: 2px;
+      }
+
+      .div1 {
+        justify-content: center;
+
+        align-items: center;
+      }
+
+      .div2 {
+        justify-content: space-around;
+
+        flex-direction: column;
+
+        align-items: center;
+      }
+
+      .div3 {
+        justify-content: space-around;
+
+        align-items: center;
+
+        padding: 10px;
+      }
+
+      .div3 p:first-child {
+        align-self: flex-start;
+      }
+
+      .div3 p:last-child {
+        align-self: flex-end;
+      }
+
+      .div4 {
+        justify-content: space-around;
+
+        flex-direction: column;
+
+        align-items: center;
+      }
+
+      .div4 div {
+        display: flex;
+
+        width: 100%;
+
+        justify-content: space-around;
+      }
+
+      .div4 p {
+        align-self: center;
+      }
+
+      .div8 {
+        flex-wrap: wrap;
+
+        padding: 2px;
+      }
+
+      .div8 div {
+        display: flex;
+
+        justify-content: space-between;
+
+        align-items: center;
+
+        flex-basis: 100%;
+      }
+    </style>
+  </head>
+
+  <body>
+    <!--骰子1-->
+
+    <div class="div1">
+      <p></p>
+    </div>
+
+    <!--骰子2-->
+
+    <div class="div2">
+      <p></p>
+
+      <p></p>
+    </div>
+
+    <!--骰子3-->
+
+    <div class="div3">
+      <p></p>
+
+      <p></p>
+
+      <p></p>
+    </div>
+
+    <!--骰子4-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子5-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子6-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子7-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子8-->
+
+    <div class="div8">
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子9-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+    <title>骰子布局</title>
+
+    <style>
+      body {
+        display: flex;
+        justify-content: space-around;
+      }
+
+      body > div {
+        display: flex;
+
+        width: 100px;
+
+        height: 100px;
+
+        border-radius: 4px;
+
+        border: 2px solid red;
+
+        box-sizing: border-box;
+      }
+
+      p {
+        width: 15px;
+
+        height: 15px;
+
+        background-color: black;
+
+        border-radius: 50%;
+
+        margin: 2px;
+      }
+
+      .div1 {
+        justify-content: center;
+
+        align-items: center;
+      }
+
+      .div2 {
+        justify-content: space-around;
+
+        flex-direction: column;
+
+        align-items: center;
+      }
+
+      .div3 {
+        justify-content: space-around;
+
+        align-items: center;
+
+        padding: 10px;
+      }
+
+      .div3 p:first-child {
+        align-self: flex-start;
+      }
+
+      .div3 p:last-child {
+        align-self: flex-end;
+      }
+
+      .div4 {
+        justify-content: space-around;
+
+        flex-direction: column;
+
+        align-items: center;
+      }
+
+      .div4 div {
+        display: flex;
+
+        width: 100%;
+
+        justify-content: space-around;
+      }
+
+      .div4 p {
+        align-self: center;
+      }
+
+      .div8 {
+        flex-wrap: wrap;
+
+        padding: 2px;
+      }
+
+      .div8 div {
+        display: flex;
+
+        justify-content: space-between;
+
+        align-items: center;
+
+        flex-basis: 100%;
+      }
+    </style>
+  </head>
+
+  <body>
+    <!--骰子1-->
+
+    <div class="div1">
+      <p></p>
+    </div>
+
+    <!--骰子2-->
+
+    <div class="div2">
+      <p></p>
+
+      <p></p>
+    </div>
+
+    <!--骰子3-->
+
+    <div class="div3">
+      <p></p>
+
+      <p></p>
+
+      <p></p>
+    </div>
+
+    <!--骰子4-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子5-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子6-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子7-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子8-->
+
+    <div class="div8">
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+
+    <!--骰子9-->
+
+    <div class="div4">
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+
+      <div>
+        <p></p>
+
+        <p></p>
+
+        <p></p>
+      </div>
+    </div>
+  </body>
+</html>
+

常见的 CSS 布局单位

三者的区别:

  • px 是固定的像素,一旦设置了就无法因为适应页面大小而改变。
  • em 和 rem 相对于 px 更具有灵活性,他们是相对长度单位,其长度不是固定的,更适用于响应式布局。
  • em 是相对于其父元素来设置字体大小,这样就会存在一个问题,进行任何元素设置,都有可能需要知道他父元素的大小。而 rem 是相对于根元素,这样就意味着,只需要在根元素确定一个参考值。

使用场景:

  • 对于只需要适配少部分移动设备,且分辨率对页面影响不大的,使用 px 即可 。
  • 对于需要适配各种移动设备,使用 rem,例如需要适配 iPhone 和 iPad 等分辨率差别比较挺大的设备。

(2)百分比%),当浏览器的宽度或者高度发生变化时,通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。一般认为子元素的百分比相对于直接父元素。 (3)em 和 rem相对于 px 更具灵活性,它们都是相对长度单位,它们之间的区别:em 相对于父元素,rem 相对于根元素

  • em: 文本相对长度单位。相对于当前对象内文本的字体尺寸。如果当前行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(默认 16px)。(相对父元素的字体大小倍数)。
  • rem: rem 是 CSS3 新增的一个相对单位,相对于根元素(html 元素)的 font-size 的倍数。作用:利用 rem 可以实现简单的响应式布局,可以利用 html 元素中字体的大小与屏幕间的比值来设置 font-size 的值,以此实现当屏幕分辨率变化时让元素也随之变化。

(4)vw/vh是与视图窗口有关的单位,vw 表示相对于视图窗口的宽度,vh 表示相对于视图窗口高度,除了 vw 和 vh 外,还有 vmin 和 vmax 两个相关的单位。

  • vw:相对于视窗的宽度,视窗宽度是 100vw;
  • vh:相对于视窗的高度,视窗高度是 100vh;

img 的 style 里面有 important 怎么覆盖掉

实现一个布局 1+(2,3,4(4 是在 3 的右上角)flex

移动端的点击事件的有延迟,时间是多久,为什么会有? 怎么解决这个 延时?

移动端点击有 300ms 的延迟是因为移动端会有双击缩放的这个操作,因此浏览器在 click 之 后要等待 300ms,看用户有没有下一次点击,来判断这次操作是不是双击。 有三种办法来解决这个问题: 1.通过 meta 标签禁用网页的缩放。 2.通过 meta 标签将网页的 viewport 设置为 ideal viewport。

一个 span 能不能改变长宽高。 --行元素不能改变宽高 假如 span 已经被 position 设置好了,能不能改变长宽高

可以改变的

实现内容多时页面滑动,内容少时保持占据一页

min-height

base64 优缺点

用 font-weight 和 b 标签有什么区别

seo

移动端 fastclick 原理是什么

grid 布局

判断元素是否到达可视区域

  • window.innerHeight 是浏览器可视区的高度;

  • document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离;

  • imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离);

  • 内容达到显示区域的:img.offsetTop 小于 window.innerHeight + document.body.scrollTop;

富文本编辑器,文本设置大小不同,怎么设置 line-height

translate 可以设置 x y,如果右边也有一个元素,右边元素会不会被挤掉。

图片尺寸不固定(可能比盒子大,小),盒子长宽固定,图片比盒子大,希望长宽达到 100%(等比缩放),比盒子小,希望垂直居中。两个 class,一个针对图片大,一个针对图片小,js 获取图片大小,更改 class

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .container {
+        width: 600px;
+        height: 500px;
+        margin: 40px auto;
+        border: 1px solid #000;
+        text-align: center;
+        line-height: 600px;
+        font-size: 0;
+      }
+      .container img {
+        max-width: 100%;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="container">
+      <img src="微信图片_20210709201030.png" alt="" />
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .container {
+        width: 600px;
+        height: 500px;
+        margin: 40px auto;
+        border: 1px solid #000;
+        text-align: center;
+        line-height: 600px;
+        font-size: 0;
+      }
+      .container img {
+        max-width: 100%;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="container">
+      <img src="微信图片_20210709201030.png" alt="" />
+    </div>
+  </body>
+</html>
+

rem 原理 ,rem 怎么计算出来的

rem 计算 基于跟元素做的转换。1rem=37.5px css3 中的 rem 单位,表示相对于网页根元素的字体大小。 设置 html 元素:font-size: 13.3333vw 从此后,所有需要弹性缩放的尺寸均要使用 rem 单位,具体的值为设计稿像素值/100 rem 例如:某张图片在设计稿中的宽度为 152px,则在页面中的宽度为 1.52rem

清除图片下方出现像素空白间隙

行级块元素:行级元素有默认的行高 Line-height:normal 含有默认的垂直方向的对齐方式 方法 1:vertical-align: bottom; 方法 2:父级 line-height: 1px; 方法 3:display:block

说一下 SEO 优化

(1)合理的 title、description、keywords (2)语义化的 HTML 代码,符合 W3C 规范:语义化代码让搜索引擎容易理解网页。 (3)重要内容 HTML 代码放在最前 (4)重要内容不要用 js 输出:爬虫不会执行 js 获取内容 (5)少用 iframe (6)非装饰性图片必须加 alt (7)提高网站速度:网站速度是搜索引擎排序的一个重要指标

浏览器乱码的原因是什么?如何解决?

产生乱码的原因:

  • 网页源代码是 gbk 的编码,而内容中的中文字是 utf-8 编码的,这样浏览器打开即会出现 html 乱码,反之也会出现乱码;
  • html 网页编码是 gbk,而程序从数据库中调出呈现是 utf-8 编码的内容也会造成编码乱码;
  • 浏览器不能自动检测网页编码,造成网页乱码。

解决办法:

  • 使用软件编辑 HTML 网页内容;
  • 如果网页设置编码是 gbk,而数据库储存数据编码格式是 UTF-8,此时需要程序查询数据库数据显示数据前进程序转码;
  • 如果浏览器浏览时候出现网页乱码,在浏览器中找到转换编码的菜单进行转换。

head 标签有什么作用,其中什么标签必不可少?

标签用于定义文档的头部,它是所有头部元素的容器。 中的元素可以引用脚本、指示浏览器在哪里找到样式表、提供元信息等。 文档的头部描述了文档的各种属性和信息,包括文档的标题、在 Web 中的位置以及和其他文档的关系等。绝大多数文档头部包含的数据都不会真正作为内容显示给读者。 下面这些标签可用在 head 部分:<base>, <link>, <meta>, <script>, <style>, <title>。 其中 <title> 定义文档的标题,它是 head 部分中唯一必需的元素。

对 line-height 的理解及其赋值方式

(1)line-height 的概念:

  • line-height 指一行文本的高度,包含了字间距,实际上是下一行基线到上一行基线距离;
  • 如果一个标签没有定义 height 属性,那么其最终表现的高度由 line-height 决定;
  • 一个容器没有设置高度,那么撑开容器高度的是 line-height,而不是容器内的文本内容;
  • 把 line-height 值设置为 height 一样大小的值可以实现单行文字的垂直居中;
  • line-height 和 height 都能撑开一个高度;

(2)line-height 的赋值方式:

  • 带单位:px 是固定值,而 em 会参考父元素 font-size 值计算自身的行高
  • 纯数字:会把比例传递给后代。例如,父级行高为 1.5,子元素字体为 18px,则子元素行高为 1.5 * 18 = 27px
  • 百分比:将计算后的值传递给后代

z-index 属性在什么情况下会失效

通常 z-index 的使用是在有两个重叠的标签,在一定的情况下控制其中一个在另一个的上方或者下方出现。z-index 值越大就越是在上层。z-index 元素的 position 属性需要是 relative,absolute 或是 fixed。

z-index 属性在下列情况下会失效:

  • 父元素 position 为 relative 时,子元素的 z-index 失效。解决:父元素 position 改为 absolute 或 static;
  • 元素没有设置 position 属性为非 static 属性。解决:设置该元素的 position 属性为 relative,absolute 或是 fixed 中的一种;
  • 元素在设置 z-index 的同时还设置了 float 浮动。解决:float 去除,改为 display:inline-block;

z-index 工作原理和适用范围

文档流 定位

CSS 环形进度条

https://www.cnblogs.com/jr1993/p/4677921.html

不考虑其它因素,下面哪种的渲染性能比较高?

css
.box a{
+}
+a{}
+阿里:浏览器查询从右往左
+2性能好,只查询a,1不但查询所有a,还要查询.box下的所有a
+
.box a{
+}
+a{}
+阿里:浏览器查询从右往左
+2性能好,只查询a,1不但查询所有a,还要查询.box下的所有a
+

script 标签中 defer 和 async 的区别

如果没有 defer 或 async 属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。 defer 和 async 属性都是去异步加载外部的 JS 脚本文件,它们都不会阻塞页面的解析,其区别如下:

  • 执行顺序: 多个带 async 属性的标签,不能保证加载的顺序;多个带 defer 属性的标签,按照加载顺序执行;
  • 脚本是否并行执行:_async 属性,表示_后续文档的加载和执行与 js 脚本的加载和执行是并行进行的,即异步执行;defer 属性,加载后续文档的过程和 js 脚本的加载(此时仅加载不执行)是并行进行的(异步),js 脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded 事件触发执行之前。

10. HTML5 的离线储存怎么使用,它的工作原理是什么

离线存储指的是:在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。

**原理:**HTML5 的离线存储是基于一个新建的 .appcache 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像 cookie 一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示

使用方法: (1)创建一个和 html 同名的 manifest 文件,然后在页面头部加入 manifest 属性:

html
<html lang="en" manifest="index.manifest">
+  复制代码
+</html>
+
<html lang="en" manifest="index.manifest">
+  复制代码
+</html>
+

(2)在 cache.manifest 文件中编写需要离线存储的资源:

html
CACHE MANIFEST #v0.11 CACHE: js/app.js css/style.css NETWORK: resourse/logo.png
+FALLBACK: / /offline.html 复制代码
+
CACHE MANIFEST #v0.11 CACHE: js/app.js css/style.css NETWORK: resourse/logo.png
+FALLBACK: / /offline.html 复制代码
+
  • CACHE: 表示需要离线存储的资源列表,由于包含 manifest 文件的页面将被自动离线存储,所以不需要把页面自身也列出来。
  • NETWORK: 表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。不过,如果在 CACHE 和 NETWORK 中有一个相同的资源,那么这个资源还是会被离线存储,也就是说 CACHE 的优先级更高。
  • FALLBACK: 表示如果访问第一个资源失败,那么就使用第二个资源来替换他,比如上面这个文件表示的就是如果访问根目录下任何一个资源失败了,那么就去访问 offline.html 。

(3)在离线状态时,操作 window.applicationCache 进行离线缓存的操作。

如何更新缓存:

(1)更新 manifest 文件

(2)通过 javascript 操作

(3)清除浏览器缓存

注意事项:

(1)浏览器对缓存数据的容量限制可能不太一样(某些浏览器设置的限制是每个站点 5MB)。

(2)如果 manifest 文件,或者内部列举的某一个文件不能正常下载,整个更新过程都将失败,浏览器继续全部使用老的缓存。

(3)引用 manifest 的 html 必须与 manifest 文件同源,在同一个域下。

(4)FALLBACK 中的资源必须和 manifest 文件同源。

(5)当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。

(6)站点中的其他页面即使没有设置 manifest 属性,请求的资源如果在缓存中也从缓存中访问。

(7)当 manifest 文件发生改变时,资源请求本身也会触发更新。

浏览器是如何对 HTML5 的离线储存资源进行管理和加载?

  • 在线的情况下,浏览器发现 html 头部有 manifest 属性,它会请求 manifest 文件,如果是第一次访问页面 ,那么浏览器就会根据 manifest 文件的内容下载相应的资源并且进行离线存储。如果已经访问过页面并且资源已经进行离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的 manifest 文件与旧的 manifest 文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,就会重新下载文件中的资源并进行离线存储。

  • 离线的情况下,浏览器会直接使用离线存储的资源。

  • Flex 布局原理

  • px, rem, em 的区别 网页的三层结构有哪些 -请简述媒体查询

  • flex:1 的 1 代表什么?闭包,作用域,内存?

html
<meta http-equiv="X-UA-Compatible" content="lE=edge,chrome=1" />`作用是什么?
+
<meta http-equiv="X-UA-Compatible" content="lE=edge,chrome=1" />`作用是什么?
+

项目中音频加载不出来时的优化处理 实现一个球一个白板,实现画笔的功能实现

  1. css 上下固定为 100px,中间为自适应高度
  2. Base64 在 html 中的缺点
  3. 500 张图片,如何实现预加载优化

已知如下代码,如何修改才能让图片宽度为 300px ? 注意下面代码不可修改

html
<img src="1.jpg" style="width:480px!important;”>
+答:max-width: 300pxtransform: scale(0.625,0.625)
+
<img src="1.jpg" style="width:480px!important;”>
+答:max-width: 300pxtransform: scale(0.625,0.625)
+
  • css 中的 position 取值,及区别(字节 2020 实习)
  • 盒模型  top:50%,margin-top:50%,相对什么计算的(字节 2020 实习)
  • 怎么求的盒子的总尺寸(字节 2020 实习)
  • translate 的优点(字节 2020 实习)
  • 实现一个球(字节 2020 校招)
  • 一个白板,实现画笔的功能(字节 2020 校招)
  • h5 新出的 api(字节 2020 校招)
  • 使用 cookie,怎么使用?(字节 2020 校招)
  • cookie 和 localStorage 区别,其中说到了 expires 过期时间(字节 2020 校招)
  • CSS 哪些属性可以加速 GPU?除了 absolute、fixed 呢?(字节 2020 校招)
  • 重绘重排,什么时候触发重排,transform 会触发重排么?为什么?(字节 2020 校招)
  • flex:1 表示什么?详细说下,详细问了 flex-baisit(字节 2020 校招)
  • css 左右两栏固定,中间自适应布局怎么做?(字节 2020 校招)
  • 回流重绘的概念和如何减少回流成本(字节 2020 校招)
  • 三列布局(字节 2020 校招)
  • css 实现宽高等比例(字节 2020 校招)
  • 说说 z-index 有什么需要注意的地方,z-index: 0 和 z-index:1 的区别,z-index 为 0 和 1 哪个元素在上面
  • 三栏布局,说出你能想到的所有方案
  • 垂直居中,写出你知道的所有方案
  • 两个 div,都给 margin:20px,这两个 div 的间距是多少?为什么会产生这种问题?怎么解决?(BFC)
  • css 画等边三角形(腾讯 2020 实习)
  • css 单位有哪些, em 如果父元素没有设置 font-size 呢,如果一直往上找也没有呢(腾讯 2020 实习)
  • 怎么做适配? (百度 2020 校招)
  • rem?flex?(百度 2020 校招)
  • h5 页面白屏(百度 2020 校招)
  • rem 和 px 的区别?(百度 2020 校招)
  • 1px 问题的解决?(百度 2020 校招)

CSS 自适应正方形

双重嵌套,外层 relative,内层 absolute

html
<div class="outer">
+  <div class="inner"></div>
+</div>
+
<div class="outer">
+  <div class="inner"></div>
+</div>
+
css
.outer {
+  padding-top: 50%;
+  width: 50%;
+  position: relative;
+}
+
+.inner {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  background: blue;
+}
+
.outer {
+  padding-top: 50%;
+  width: 50%;
+  position: relative;
+}
+
+.inner {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  background: blue;
+}
+

vw vh

css
.inner {
+  width: 10vw;
+  height: 10vw;
+  background: blue;
+}
+
.inner {
+  width: 10vw;
+  height: 10vw;
+  background: blue;
+}
+

css div 垂直水平居中,并完成 div 高度永远是宽度的一半(宽度可以不指定)

html
<div class="outer">
+  <div class="inner">
+    <div class="box">hello</div>
+  </div>
+</div>
+
<div class="outer">
+  <div class="inner">
+    <div class="box">hello</div>
+  </div>
+</div>
+
css
* {
+  margin: 0;
+  padding: 0;
+}
+
+html,
+body {
+  width: 100%;
+  height: 100%;
+}
+
+.outer {
+  width: 400px;
+  height: 100%;
+  background: blue;
+  margin: 0 auto;
+
+  display: flex;
+  align-items: center;
+}
+
+.inner {
+  position: relative;
+  width: 100%;
+  height: 0;
+  padding-bottom: 50%;
+  background: red;
+}
+
+.box {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
* {
+  margin: 0;
+  padding: 0;
+}
+
+html,
+body {
+  width: 100%;
+  height: 100%;
+}
+
+.outer {
+  width: 400px;
+  height: 100%;
+  background: blue;
+  margin: 0 auto;
+
+  display: flex;
+  align-items: center;
+}
+
+.inner {
+  position: relative;
+  width: 100%;
+  height: 0;
+  padding-bottom: 50%;
+  background: red;
+}
+
+.box {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+

padding 撑高画正方形

css
.outer {
+  width: 400px;
+  height: 600px;
+  background: blue;
+}
+
+.inner {
+  width: 100%;
+  height: 0;
+  padding-bottom: 100%;
+  background: red;
+}
+
.outer {
+  width: 400px;
+  height: 600px;
+  background: blue;
+}
+
+.inner {
+  width: 100%;
+  height: 0;
+  padding-bottom: 100%;
+  background: red;
+}
+
html
<div class="outer">
+  <div class="inner"></div>
+</div>
+
<div class="outer">
+  <div class="inner"></div>
+</div>
+

CSS flex: 1 有什么效果 多行超出自动省略号:Clamp.js css3 怎么创建逐帧动画

  • CSS 动画,transition 和 animation,哪一个性能更好

为什么 transform 可以减少回流

translate 的优点

  • 不会脱离文档流
  • 对 gpu 也比较友好

flex 原理 flex: 1 1 0; 的三个值分别指的是什么? 用 css 实现立方体,注意,是立方体。

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>立方体</title>
+  </head>
+  <style>
+    #cube {
+      width: 300px;
+      height: 300px;
+      font-size: 80px;
+      text-align: center;
+      line-height: 300px;
+      transform: rotateY(-30deg) rotateX(-35deg);
+      transform-style: preserve-3d;
+      margin: 300px auto;
+    }
+
+    #cube > div {
+      width: 300px;
+      height: 300px;
+      position: absolute;
+      left: 0;
+      top: 0;
+      opacity: 0.5;
+    }
+
+    #cube .flat1 {
+      background-color: red;
+      transform: translateY(-150px) rotateX(90deg);
+    }
+
+    #cube .flat2 {
+      background-color: orange;
+      transform: translateY(150px) rotateX(90deg);
+    }
+
+    #cube .flat3 {
+      background-color: yellow;
+      transform: translateX(-150px) rotateY(90deg);
+    }
+
+    #cube .flat4 {
+      background-color: green;
+      transform: translateX(150px) rotateY(90deg);
+    }
+
+    #cube .flat5 {
+      background-color: lightgreen;
+      transform: translateZ(150px);
+    }
+
+    #cube .flat6 {
+      background-color: blue;
+      transform: translateZ(-150px);
+    }
+  </style>
+
+  <body>
+    <div id="cube">
+      <div class="flat1">上</div>
+      <div class="flat2">下</div>
+      <div class="flat3">左</div>
+      <div class="flat4">右</div>
+      <div class="flat5">前</div>
+      <div class="flat6">后</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>立方体</title>
+  </head>
+  <style>
+    #cube {
+      width: 300px;
+      height: 300px;
+      font-size: 80px;
+      text-align: center;
+      line-height: 300px;
+      transform: rotateY(-30deg) rotateX(-35deg);
+      transform-style: preserve-3d;
+      margin: 300px auto;
+    }
+
+    #cube > div {
+      width: 300px;
+      height: 300px;
+      position: absolute;
+      left: 0;
+      top: 0;
+      opacity: 0.5;
+    }
+
+    #cube .flat1 {
+      background-color: red;
+      transform: translateY(-150px) rotateX(90deg);
+    }
+
+    #cube .flat2 {
+      background-color: orange;
+      transform: translateY(150px) rotateX(90deg);
+    }
+
+    #cube .flat3 {
+      background-color: yellow;
+      transform: translateX(-150px) rotateY(90deg);
+    }
+
+    #cube .flat4 {
+      background-color: green;
+      transform: translateX(150px) rotateY(90deg);
+    }
+
+    #cube .flat5 {
+      background-color: lightgreen;
+      transform: translateZ(150px);
+    }
+
+    #cube .flat6 {
+      background-color: blue;
+      transform: translateZ(-150px);
+    }
+  </style>
+
+  <body>
+    <div id="cube">
+      <div class="flat1">上</div>
+      <div class="flat2">下</div>
+      <div class="flat3">左</div>
+      <div class="flat4">右</div>
+      <div class="flat5">前</div>
+      <div class="flat6">后</div>
+    </div>
+  </body>
+</html>
+

rem 用过吗?做不同手机的适配怎么做?

  • scss 实现循环变换颜色

\7. 1px 像素

怎么优化 h5 的加载速度,怎么实现 h5 页面秒开?

https://juejin.cn/post/7065235287781146654

https://www.infoq.cn/article/2ETO4QYx82gd1LVfY56A

!important 什么时候会使用,什么情况下改不掉样式(内联) 移动端:

rem、em、px 如何实现 1px 在手机上?使用 transform: scale() + width/height * 2

rem:相对于根元素(html)的字体大小。

em:相对于父元素字体大小。浏览器默认字体尺寸 1em=16px。在 body 里设置 font-size:62.5%则全局 1em=10px。

px:相对于当前显示器屏幕分辨率。

如何在移动端实现 1 像素细线

rem,em 啥区别

em 和 rem 如何适配?有啥区别?还有其他适配的办法吗?

10.CSS 的尺寸单位,分别介绍一下;移动端适配可以用哪些单位?

如何实现 1px 在手机上?使用 transform: scale() + width/height * 2   移动端 1px 问题

rem,em。 设备像素占比

怎么实现移动端的布局?

说一下根结点的字体大小怎么确定

点透问题。为什么会有点透现象。

3.如何提升移动端用户的使用体验,让用户能更快的看到页面

第 1 个问题是 viewport 各个属性值的意义,以及如何实现不用 viewport 控制用户不能缩放,回答用 js 监听屏幕宽度。

em rem 区别,rem 缺点 css

移动端原生代码屏幕适配

移动端自适应有哪几种方法,优劣是什么

为什么设置 font-weight 是数字的时候作用会失效?因为用了一些特殊的字体,它们没有实现该粗细的字体

五星好评点几颗星亮几颗,用 css

z-index 在什么情况下会失效

一个页面有头部、内容、尾部。想出多种方案,如果内容超过浏览器显示长度则隐藏尾部

离线包怎么更新?怎么知道需要打开哪个离线包? .

transition 的触发条件

第一种方式如上, 使用伪类的方式触发, 包括hover,focus,checked等方式; 但是实际使用当中我们更多的是使用 JS 或者 Jquery 直接修改属性, 但是工作中发现这样不行.

JS 触发: 如果使用 JS 或者 Jquery 直接修改 CSS 属性并不会触发渐变, JS 的触发方式是toggleClass; 我的理解是必须元素发生什么改变使得它有了一些不同从而获取到一些新的属性, 对于伪类触发是这样, 对于 JS 触发方式应当是它的class发生改变以至于能够得到新的样式

对 css3 的剪切属性有了解吗

background-clip:背景图片从哪块开始截断,从哪块以外的部分都不显示背景图片,即从哪块结束

requestAnimationFrame

**window.requestAnimationFrame()** 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame

和 setInterval 的区别?

如何实现在图片被加载之前的占位符一个 image,宽高比 16:9

把一个矩形分成同样大小的正方形,最大边长是多少(用编程思维和数学思维)

float 和 position 一起用是什么效果

H5 页面中长列表在下拉过程中为什么会闪一下 ,如何解决?

查询 css 属性的时候会触发重排么 ?

flex 布局 flex 布局的原理,怎么理解主轴

回流和重绘机制 浏览器渲染机制是什么 我答:DOM、CSSOM -> 渲染树 -> 排列,布局 -> 绘制 追问:哪些操作会触发回流,位置和大小还有呢

BFC 会与 float 元素相互覆盖吗?为什么?举例说明

浏览器如何构建和渲染页面

浏览器渲染上减少重绘和重排的一些操作,提到了使用 opacity 和 z-index 新建图层等。还提到了 will-change,面试官说这个他不知道。

js 动画相比,优势https://zh.javascript.info/js-animation

  1. 一个盒子从中间开始,碰到最左边的边界往右移动,碰到最右边的边界往左移动,如此循环,问怎么做?
  2. 用过 canvas 吗,如果要实现一个一笔一画写汉字的效果,应该怎么做?
  3. 控制字体换行,大小写转换的属性? https://blog.csdn.net/qq_45799465/article/details/122667947 transform:translateZ(0)的作用 margin/padding 百分比如何计算 addEventListener 存在什么问题 https://blog.csdn.net/NewDayStudy/article/details/78656534https://blog.csdn.net/aa494661239/article/details/103404241?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control 1.3 removeEventListener 1.5 addEventListener 的第三个参数 前端路由 hash 和 history 模式的区别,以及 history 出现 404 的原因和解决方法
  • 回流/重绘简单介绍
  • 自问自答:如何避免这种情况,table 的绘制时长
  • 减少回流/重绘的触发 虚拟 DOM,统一绘制
  • 获取 offsettop 是否会触发回流和重绘(不会) last-childlast-of-type 的差别
html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .box p:last-child {
+        color: green;
+      }
+      .box1 p:last-of-type {
+        color: red;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="box">
+      <p>p1</p>
+      <p>p2</p>
+      123
+    </div>
+    p会不会被选中。
+
+    <div class="box1">
+      <p>11</p>
+      <p>22</p>
+      <i>5555</i>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      .box p:last-child {
+        color: green;
+      }
+      .box1 p:last-of-type {
+        color: red;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="box">
+      <p>p1</p>
+      <p>p2</p>
+      123
+    </div>
+    p会不会被选中。
+
+    <div class="box1">
+      <p>11</p>
+      <p>22</p>
+      <i>5555</i>
+    </div>
+  </body>
+</html>
+

为什么用 float 会影响性能 9.font-size:0 可以清除两个行内块盒子空隙的原理 web 里面性能优化,从样式方面说。

  1.
+
+  2.  移动端开发中,边框1px 问题,有的特别细有的特别粗。怎么解决?
+     -  如果要生成海报,并能保存到本地。应该如何实现?
+

transform 不准确 1. 先进行定位,再用 transform,相对于自身的位移,怎么实现垂直居中呢? 1. 父容器开启 absolute,左上角移位,偏一点,在使用 margin,-50%

边有 40 万人,另一边有 60 万人,这个村庄每天有 100 万通电话,假设每个人打给其他人电话的概率相同,问每天有多少通跨河电话 2-3-5 穷举

盒模型 top:50%,margin-top:50%,相对什么计算的

clientWidth,offsetWidth,scrollWidth 的区别

  • clientWidth 元素可视区域的宽度 不包含滚动条
  • offsetWidth 元素所占据的宽度 包含滚动条
  • scrollWidth 元素没有滚动条时的宽度 包含里面所有内容展开的宽度有一道关于 transform 的题,transform-origin

加载 JS 资源、CSS 资源对页面有什么影响吗(阻不阻塞):JS 资源,讲了 async 和 defer;CSS 资源是在啥时候解析的,会不会阻塞 DOM 结点的构建?(寄,我觉得是并行的)不会阻塞解析还是渲染?

CSS 哪些元素可以继承?

结合 media 媒体查询,将 html 的 font-size 设置为 (屏幕宽度/设计图宽度)*16px

行内元素: margin、padding 左右有效,上下无效 ,不能包含其他元素

为什么 css 要使用字面量。(从渲染流程解释,在计算样式的阶段可以减少标准化 css 样式表的时间)

为什么 transform 和 opacity 能够优化动画。(transform 生成的图层会直接交给合成线程,不影响主线程,不会发送回流和重绘)

  • 页面的回流和重绘知道吗?有什么区别?(回流必重绘,重绘不一定回流,回答了一下什么是回流,什么是重绘,这点之前看过文章,结合 dom tree 和 css rules 结合的 render tree 相关知识,_说的还是比较好的_)

  • 有什么好的办法优化页面渲染?(这个答得很烂,提了一些感觉不在点子上,说了一下少用 css 嵌套,css 继承的属性不用重复设置,合并 css 文件,图标类用 css spires?是不是这样拼的我也忘记了,只用加载一个大的图,大图中定位小的图标的一种技术)

    4.实现一个单行容器内:左边一行文字,右边一个 btn,文字边长过程中,不会把 btn 挤下去,而是文字超出省略

html
#wrapper { display: flex; } #text { white-space: nowrap; text-overflow:
+ellipsis; overflow: hidden; flex: 1; }
+
+<div id="wrapper">
+  <div id="text">
+    长文本长文dbsgvkjdabkajgbkjdalbsgkljfbkljadsbgkbadskljbgkljadsb
+    hewfoihngfewakjoh ;ewoia
+  </div>
+  <button>test</button>
+</div>
+
#wrapper { display: flex; } #text { white-space: nowrap; text-overflow:
+ellipsis; overflow: hidden; flex: 1; }
+
+<div id="wrapper">
+  <div id="text">
+    长文本长文dbsgvkjdabkajgbkjdalbsgkljfbkljadsbgkbadskljbgkljadsb
+    hewfoihngfewakjoh ;ewoia
+  </div>
+  <button>test</button>
+</div>
+

说说 block 元素和 in-line 元素? 二者的不同点和特征有哪些?

扩展:img 是行内元素吗?为什么可以设置宽高呢

img 确实是行内元素 但它也是置换元素 。置换元素就是浏览器根据元素的标签和属性,来决定元素的具体显示内容。例如浏览器会根据标签的 src 属性的值来读取图片信息并显示出来,而如果查看(X)HTML 代码,则看不到图片的实际内容;又例如根据标签的 type 属性来决定是显示输入框,还是单选按钮等。所以 img  input  select  textarea  button  label 等,他们被称为可置换元素(Replaced element)。他们区别一般 inline 元素是:这些元素拥有内在尺寸 内置宽高 他们可以设置 width/height 属性。他们的性质同设置了 display:inline-block 的元素一致。

scale(0.5) scale(2) scale(1) 分别是怎么样的 ,那 scale(-1)呢 scale(x) 当 x 为负值时,整个是颠倒过来的】

屏幕正中间有个元素 A, 随着屏幕宽度的增加, 始终需要满足以下条件:

  • A 元素垂直居中于屏幕***;
  • A 元素距离屏幕左右边距各 10px;
  • A 元素里面的文字”A”的 font-size:20px;水平垂直居中;
  • A 元素的高度始终是 A 元素宽度的 50%; (如果搞不定可以实现为 A 元素的高度固定为 200px;)

实现一个左右布局,左边固定 100PX,右边可伸缩,高度沾满整个屏幕。右侧正中间有个长方形,长方形长宽比 4:3,长是父元素的 50%

元素层叠顺序是如何计算的

百分比 translate 是根据什么值计算的

display:inline-block 元素和父元素上下存在间隙,产生原因及解决方案

css 文件加载的话会阻塞 dom 树渲染吗?会阻塞渲染树吗?

css 为什么要从右往左匹配;

absolution 计算时包括父元素 margin 吗?(把 margin 换成 padding)

css 性能优化

absolute 和 relative 对于重排重绘 那个性能好点

css3 transform 变换顺序考察

```css
+.box {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100px;
+  height: 100px;
+  border: 1px solid;
+}
+
+.box1 {
+  border: 1px solid red;
+  transform: translate(50px, 50px) rotate(45deg);
+}
+
+.box2 {
+  border: 1px solid blue;
+  transform: rotate(45deg) translate(50px, 50px);
+}
+
```css
+.box {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100px;
+  height: 100px;
+  border: 1px solid;
+}
+
+.box1 {
+  border: 1px solid red;
+  transform: translate(50px, 50px) rotate(45deg);
+}
+
+.box2 {
+  border: 1px solid blue;
+  transform: rotate(45deg) translate(50px, 50px);
+}
+

问 box1 和 box2 的位置一致么,如果不一致,那它们最终分别在什么地方? 不一致,box1 的中心点在(50, 50), box2 的中心点在 (50, 50√2) 原因是在旋转时轴也会随之旋转

css 单位的百分比

给一个 div 设置它父级 div 的宽度是 100px,然后再设置它的 padding-top 为 20%。 问现在的 div 有多高?如果父级元素定位是 absolute 呢? 现有 div 的高度等于自身高度+父级块的宽度*20%,如果父级元素定位是 absolute,结果不变; 当 margin/padding 取形式为百分比的值时,无论是 left/right,还是 top/bottom,都是以父元素的 width 为参照物的! 3 分: 能够答出 当 margin/padding 取形式为百分比的值时,无论是 left/right,还是 top/bottom,都是以父元素的 width 为参照物的! 3.5 分:这个计算规则发生在:bordersPlusPadding 计算的过程中 元素的高度可能由子元素撑开决定,如果子元素的 padding and margin 基于父元素的高度来判断的话 会陷入死循环

CSS3 新增伪类有那些?

  • p:first-of-type 选择属于其父元素的首个 元素的每个元素。
  • p:last-of-type 选择属于其父元素的最后元素的每个元素。
  • p:only-of-type 选择属于其父元素唯一的元素的每个元素。
  • p:only-child 选择属于其父元素的唯一子元素的每个元素。
  • p:nth-child(2) 选择属于其父元素的第二个子元素的每个元素。
  • ::after 在元素之前添加内容,也可以用来做清除浮动。
  • ::before 在元素之后添加内容
  • :enabled
  • :disabled 控制表单控件的禁用状态。
  • :checked         单选框或复选框被选中。

请问 span 标签设置宽高有效吗?设置 margin、padding 有效吗?

  1. span 由于是内联元素,所以设置宽高无效,当设置displya:inline-block;时生效;
  2. span 设置margin-left,margin-right是可以的,margin-top,margin-bottom无效;
  3. span 设置padding-left,padding-right是可以的,padding-top,padding-bottom无效;

弹性盒子中 flex: 0 1 auto 表示什么意思?

  1. flex-grow: 0; 增长比例,子项合计宽度小于容器宽度,需要根据每个子项设置的此属性比例对剩下的长度进行分配
  2. flex-shrink: 1; 回缩比例,子项合计宽度大于容器宽度,需要根据每个子项设置的此属性比例对多出的长度进行分配
  3. flex-basis: auto; 设置了宽度跟宽度走,没设置宽度跟内容实际宽度走

请简述响应式设计解决方案以及其应用

  1. media query
  2. 百分比
  3. flex
  4. rem
  5. js 动态计算

可继续追问 rem 实现 原理 和 1px 解决方案

inline-block 元素间隙处理

inline-block 水平呈现的元素间,换行显示或空格分隔的情况下会有间距,如何来解决呢?

一种答案,给父亲元素设置font-size:0 .par { font-size: 0; } 参考:http://www.zhangxinxu.com/wordpress/2012/04/inline-block-space-remove-去除间距/

line-height 取值

  • line-height:26px;
  • line-height:1.5;
  • line-height:150%;
  • line-height:1.5rem;
  • px/rem 单位取值 line-height: 26px 目的就是直接定义目标元素的行高为 26px 的高度。rem 同 px
  • % 百分比取值 line-height: 150% 一般用该方式定义目标元素的行高会配合 font-size: 14px 属性使用,因为用百分比当前元素的行高为 1.5 * 14px = 21px。且如果其层叠子元素没有定义 line-height 属性,不管有没有定义 font-size 属性,其层叠子元素行高均为 21px(与自身的 font-size 没有任何关系)。
  • 倍数取值 line-height:1.5 用该方式一般也是配合 font:14px 属性使用,但是对层叠子元素的影响效果并不同,如果层叠子元素没有定义 line-height 属性,但是定义了 font-size 属性,那这些层叠子元素的行高为继承过来的 line-height 倍数值乘以自身的 font-size 属性。

【CSS】1rem、1em、1vh、1px 这几个值的实际意义

rem rem 是全部的长度都相对于根元素元素。通常做法是给 html 元素设置一个字体大小,然后其他元素的长度单位就为 rem。 em 子元素字体大小的 em 是相对于父元素字体大小 元素的 width/height/padding/margin 用 em 的话是相对于该元素的 font-size vw/vh 全称是 Viewport Width 和 Viewport Height,视窗的宽度和高度,相当于 屏幕宽度和高度的 1%,不过,处理宽度的时候%单位更合适,处理高度的 话 vh 单位更好。 px px 像素(Pixel)。相对长度单位。像素 px 是相对于显示器屏幕分辨率而言的。 一般电脑的分辨率有{19201024}等不同的分辨率 19201024 前者是屏幕宽度总共有 1920 个像素,后者则是高度为 1024 个像素

css 百分比

css
<div class="father">
+<div class="son"></div>
+</div>
+
+* {
+margin: 0px;
+padding: 0px;
+}
+
+.father {
+position: relative;
+width: 300px;
+height: 500px;
+background-color: #ccc;
+padding: 10px 20px 30px 40px;
+margin: 10px 20px 30px 40px;
+}
+
+.son {
+position: relative;
+background-color: pink;
+top: 50%;
+width: 50%;
+height: 50%;
+padding: 50%;
+margin: 50%;
+}
+
+问son的width、height、padding、margin、 top分别多少px
+width:150px
+height:250px
+padding: 150px
+margin: 150px
+top: 250px
+
<div class="father">
+<div class="son"></div>
+</div>
+
+* {
+margin: 0px;
+padding: 0px;
+}
+
+.father {
+position: relative;
+width: 300px;
+height: 500px;
+background-color: #ccc;
+padding: 10px 20px 30px 40px;
+margin: 10px 20px 30px 40px;
+}
+
+.son {
+position: relative;
+background-color: pink;
+top: 50%;
+width: 50%;
+height: 50%;
+padding: 50%;
+margin: 50%;
+}
+
+问son的width、height、padding、margin、 top分别多少px
+width:150px
+height:250px
+padding: 150px
+margin: 150px
+top: 250px
+

CSS 中 px、em、rem 的区别

px:绝对单位,页面按精确像素显示; em:相对单位,基准点为父节点字体的大小; rem:相对单位,可理解为“root em”,相对根结点 html 的字体大小来计算,CSS3 新属性;

div 设置成 inline-block,有空隙是什么原因呢?如何解决

https://juejin.cn/post/6991762098111905806

css 中 z-index 属性考察

问:box1 在 box2 上面,还是 box2 在 box1 上面? 答案:box1 在 box2 上面 考察是否对z-index生效条件了解,概念: z-index 只对定位元素有效,这里的定位指:absolute、relative、fixed 和 inherit,其中 inherit 取决于父元素,如果父元素没有设置定位(absolute、relative、fixed)则 z-index 无效,注意低版本 IE 浏览器不支持这个值 除了给出答案,还了解 z-index 属性其他注意点,浏览器兼容性等 4. 4.0 分:

css3 GPU 加速

css3 中 2d 变化和 3d 变化有什么区别,性能对比如何,是否用到了 gpu 加速?

  1. 对于 translate 和 scale,2d 变换时有两条轴,对于 rotate,2d 变化时只有一条轴。3d 变化时,都存在相对 x、y、z 轴的变化。
  2. 2d 和 3d 变化都用到了 gpu,因为 gpu 擅长图像的变化操作
  3. 3d 变化性能更好,虽然 2d 和 3d 都用到了 gpu,但是 2d 变换的元素只有在动画过程中才会提到复合层(composite layer),而 3d 变换的元素一开始就被提到了符合层。

请问 HTML 中如何通过<script>加载 JAVAScript 脚本?如果同步阻塞加载,脚本过大影响页面渲染,如何优化?

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到

回答出浏览器同步加载,渲染引擎遇到 script 标签停止渲染,等待脚本加载执行完成,回答出脚本加载执行加载时机,通过(defer 或 async)异步加载方式优化防止渲染阻塞

如何 html 中开启和关闭 DNS 预读取

在文档中使用值为 http-equiv 的   标签:可以通过将 content 的参数设置为“on”来改变设置 然后通过使用 rel 属性值为 link type 中的 dns-prefetch 的   标签来对特定域名进行预读取: 总分 4 分,不知道不得分,知道通过 meta 值设置给 2 分, meta 写正确+1 分,link 写正确+1

要求:知道 dns 预读取和预读取的意义。如何开启 dns 预读取

请简述 transform 原理

transform 同时设置多个值,比如 translate(30px,0px) rotate(45deg)和 rotate(45deg) translate(30px,0px)效果是否相同,为什么?不相同 两个变换是有先后顺序的,是先转后移还是先移后转,比较容易比划出来 从原理上来说,变换最后是用矩阵乘向量来运算的,矩阵乘法不具有交换律,因此 M1_M2 和 M2_M1 的结果是不同的

答对了,原理也说出来了,为什么要用矩阵来实现变换也说得出来

如何解决 1px 问题

核心思路: 在 web 中,浏览器为我们提供了 window.devicePixelRatio 来帮助我们获取 dpr。在 css 中,可以使用媒体查询 min-device-pixel-ratio,区分 dpr 我们根据这个像素比,来算出他对应应该有的大小,但是暴露个非常大的兼容问题

解决过程会出现什么问题呢?

说说什么是视口吧(viewport)

viewport 即视窗、视口,用于显示网页部分的区域,在 PC 端视口即是浏览器窗口区域,在移动端,为了让页面展示更多的内容,视窗的宽度默认不为设备的宽度,在移动端视窗有三个概念:布局视窗、视觉视窗、理想视窗

  • 布局视窗:在浏览器窗口 css 的布局区域,布局视口的宽度限制 css 布局的宽。为了能在移动设备上正常显示那些为 pc 端浏览器设计的网站,移动设备上的浏览器都会把自己默认的 viewport 设为 980px 或其他值,一般都比移动端浏览器可视区域大很多,所以就会出现浏览器出现横向滚动条的情况
  • 视觉视窗:终端设备显示网页的区域
  • 理想视窗:针对当前设备最理想的展示页面的视窗,不会出现横向滚动条,页面刚好全部展现在视窗内,理想视窗也就是终端屏幕的宽度。

移动端视口配置怎么配的?

移动端适配有哪些方案知道吗?

1、rem 布局 2、vw、vh 布局 3、媒体查询响应式布局

说说媒体查询吧?

通过媒体查询,可以针对不同的屏幕进行单独设置,但是针对所有的屏幕尺寸做适配显然是不合理的,但是可以用来处理极端情况(例如 IPad 大屏设备)或做简单的适配(隐藏元素或改变元素位置)

说说 rem 适配吧

rem 是 CSS3 新增的一个相对单位,这个单位引起了广泛关注。这个单位与 em 有什么区别呢?区别在于使用 rem 为元素设定字体大小时,仍然是相对大小,但相对的只是 HTML 根元素。这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。目前,除了 IE8 及更早版本外,所有浏览器均已支持 rem。对于不支持它的浏览器,应对方法也很简单,就是多写一个绝对单位的声明。这些浏览器会忽略用 rem 设定的字体大小

rem 的具体适配方案知道吗?

flexible.js 适配:阿里早期开源的一个移动端适配解决方案

因为当年 viewport 在低版本安卓设备上还有兼容问题,而 vw,vh 还没能实现所有浏览器兼容,所以 flexible 方案用 rem 来模拟 vmin 来实现在不同设备等比缩放的“通用”方案,之所以说是通用方案,是因为他这个方案是根据设备大小去判断页面的展示空间大小即屏幕大小,然后根据屏幕大小去百分百还原设计稿,从而让人看到的效果(展示范围)是一样的,这样一来,苹果 5 和苹果 6p 屏幕如果你按照设计稿还原的话,字体大小实际上不一样,而人们在一样的距离上希望看到的大小其实是一样的,本质上,用户使用更大的屏幕,是想看到更多的内容,而不是更大的字

rem 的弊端知道吗

弊端之一:和根元素 font-size 值强耦合,系统字体放大或缩小时,会导致布局错乱 弊端之二:html 文件头部需插入一段 js 代码

说说 vw/vh 适配

vh、vw 方案即将视觉视口宽度 window.innerWidth 和视觉视口高度 window.innerHeight 等分为 100 份

vw 和 vh 有啥不足吗?

vw 和 vh 的兼容性: Android 4.4 之下和 iOS 8 以下的版本有一定的兼容性问题(但是目前这两版本已经很少有人使用了) rem 的兼容性:

+ + + + + \ No newline at end of file diff --git a/html-css/principle.html b/html-css/principle.html new file mode 100644 index 00000000..79f46b7c --- /dev/null +++ b/html-css/principle.html @@ -0,0 +1,50 @@ + + + + + + 显示器的成像原理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

显示器的成像原理

空间混色法 rgb 实质上并排排列(验证)

css
.wrapper {
+  display: flex;
+}
+.demo {
+  width: 1px;
+  height: 10px;
+}
+.demo:nth-of-type(2n) {
+  background-color: #f00;
+  /*蓝
+}
+  .demo:nth-of-type(2n+1){
+  background-color: #00f;
+  /*红*/
+}
+
.wrapper {
+  display: flex;
+}
+.demo {
+  width: 1px;
+  height: 10px;
+}
+.demo:nth-of-type(2n) {
+  background-color: #f00;
+  /*蓝
+}
+  .demo:nth-of-type(2n+1){
+  background-color: #00f;
+  /*红*/
+}
+

像素:--->红绿蓝像点----->空间混色

最小的单位:像点。像素由 3 个像点构成。

空间混色法应用

crt 显示屏

lcd 液晶屏

点距:crt 显示屏求点距的方法的意义,是几乎所有屏幕都通用的

像素的大小:点距

物理像素:设备出厂时,像素的大小

dpi:1 英寸所能容纳的像素点数

1 英寸= 2.54cm

dpi 打印机在一英寸屏幕里面可以打印多少墨点

ppi 一英寸所能容纳的像素点数(点距数)

参照像素

96dpi 一臂之遥的视角去看,显示出的具体大小

标杆 1/96*英寸

css 像素=逻辑像素

设备像素比 dpr = 物理像素/css 像素

衡量屏幕好不好:不看分辨率(分辨率:固定宽高下,展示的像素点数)

看的是 dpi

+ + + + + \ No newline at end of file diff --git a/html-css/selector.html b/html-css/selector.html new file mode 100644 index 00000000..d9cff16b --- /dev/null +++ b/html-css/selector.html @@ -0,0 +1,1336 @@ + + + + + + 选择器 | Sunny's blog + + + + + + + + +
Skip to content
On this page

选择器

1.关系型选择器模式(不常用)

E+F:下一个满足条件的兄弟元素节点

html
<div>div</div>
+<p>1</p>
+<p>2</p>
+<p>3</p>
+<p>4</p>
+
<div>div</div>
+<p>1</p>
+<p>2</p>
+<p>3</p>
+<p>4</p>
+
css
div + p {
+  /*选择div兄弟下一个兄弟节点,叫p*/
+  background-color: red;
+}
+
div + p {
+  /*选择div兄弟下一个兄弟节点,叫p*/
+  background-color: red;
+}
+

E~F:下一堆满足条件的兄弟元素节点

html
<div>div</div>
+<span class="demo">234</span>
+<p class="demo">1</p>
+<p>2</p>
+<p>3</p>
+
<div>div</div>
+<span class="demo">234</span>
+<p class="demo">1</p>
+<p>2</p>
+<p>3</p>
+
css
div ~ p {
+  background-color: green;
+}
+
div ~ p {
+  background-color: green;
+}
+

2.属性选择器(不常用)

复习属性选择器:

css
<div class="demo" data="a" > data</div > <div > </div > div[data] {
+  background-color: red;
+}
+
<div class="demo" data="a" > data</div > <div > </div > div[data] {
+  background-color: red;
+}
+

1.小破浪

属性名是 data,属性值里面有独立 a 的元素

html
<div data="a">1</div>
+<div data="b">2</div>
+<div data="a b">3</div>
+<div data="aa b c">4</div>
+
<div data="a">1</div>
+<div data="b">2</div>
+<div data="a b">3</div>
+<div data="aa b c">4</div>
+
css
div[data~="a"] {
+  background-color: red;
+}
+/*1,3变色*/
+
div[data~="a"] {
+  background-color: red;
+}
+/*1,3变色*/
+

2.小竖线

以 a 开头或者以 a-开头

css
div[class|="a"] {
+  background-color: red;
+}
+
div[class|="a"] {
+  background-color: red;
+}
+
html
<div class="a">1</div>
+<div class="a-test">2</div>
+<div class="b-test">3</div>
+
<div class="a">1</div>
+<div class="a-test">2</div>
+<div class="b-test">3</div>
+

3.^以...开头,$以...结尾,*存在

css
div[class$="a"] {
+  background-color: red;
+}
+
div[class$="a"] {
+  background-color: red;
+}
+
html
<div class="a">1</div>
+<div class="a-test">2</div>
+<div class="b-test">4</div>
+
<div class="a">1</div>
+<div class="a-test">2</div>
+<div class="b-test">4</div>
+

3.伪元素选择器(不常用)

before,after 一个两个冒号都可,但是接下来的两个必须两个冒号

1.placeholder

css
<input type="text" placeholder="请输入用户名" > input::placeholder {
+  color: green; /*只能改变字体颜色*/
+}
+
<input type="text" placeholder="请输入用户名" > input::placeholder {
+  color: green; /*只能改变字体颜色*/
+}
+

2.selection

html
<div>成哥很帅</div>
+<div>邓哥也很帅</div>
+
<div>成哥很帅</div>
+<div>邓哥也很帅</div>
+
css
div:nth-of-type(1)::selection {
+  /*只能用这三种属性*/
+  /*color*/
+  /*background-color: */
+  /*text-shadow: 阴影*/
+  text-shadow: 3px 5px black;
+  color: #fff;
+  background-color: #fcc;
+  /*实现选中变色*/
+}
+div:nth-of-type(2)::selection {
+  color: yellow;
+  background-color: green;
+}
+
div:nth-of-type(1)::selection {
+  /*只能用这三种属性*/
+  /*color*/
+  /*background-color: */
+  /*text-shadow: 阴影*/
+  text-shadow: 3px 5px black;
+  color: #fff;
+  background-color: #fcc;
+  /*实现选中变色*/
+}
+div:nth-of-type(2)::selection {
+  color: yellow;
+  background-color: green;
+}
+
css
user-select: none 不让选中;
+
user-select: none 不让选中;
+

DEMO

html
<!DOCTYPE html>
+<html lang="en">
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Document</title>
+  <style>
+    span::selection {
+      background-color: green;
+      color: yellow;
+    }
+  </style>
+  <body>
+    名下痴汉tid梦,也从大家的视线中消失了。<span>老</span>dengxu是一位非Two将打败过dengxu作为自己的主要战绩吹了很久。
+    后来dengxu海归追梦,也从大家的视线中消失了。<span>邓</span>dengxu是一位非常有实大家的视线中消失了。<span>虚</span>dengxu是一位非常有实
+    <span>弱</span>名下痴汉tidesof
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Document</title>
+  <style>
+    span::selection {
+      background-color: green;
+      color: yellow;
+    }
+  </style>
+  <body>
+    名下痴汉tid梦,也从大家的视线中消失了。<span>老</span>dengxu是一位非Two将打败过dengxu作为自己的主要战绩吹了很久。
+    后来dengxu海归追梦,也从大家的视线中消失了。<span>邓</span>dengxu是一位非常有实大家的视线中消失了。<span>虚</span>dengxu是一位非常有实
+    <span>弱</span>名下痴汉tidesof
+  </body>
+</html>
+

DEMO

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      p {
+        display: inline-block;
+        width: 200px;
+        border: 1px solid #000;
+      }
+      p::first-letter {
+        font-size: 30px;
+      }
+      p::first-line {
+        color: green;
+      }
+    </style>
+  </head>
+  <body>
+    <p>沙拉酱擦参考手册是空的充电口穿梭在考虑到开始做看大V南京市的计算机</p>
+    <input type="text" name="a" readonly /><span>dg</span>
+    <input type="text" name="a" read-write /><span>ds</span>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      p {
+        display: inline-block;
+        width: 200px;
+        border: 1px solid #000;
+      }
+      p::first-letter {
+        font-size: 30px;
+      }
+      p::first-line {
+        color: green;
+      }
+    </style>
+  </head>
+  <body>
+    <p>沙拉酱擦参考手册是空的充电口穿梭在考虑到开始做看大V南京市的计算机</p>
+    <input type="text" name="a" readonly /><span>dg</span>
+    <input type="text" name="a" read-write /><span>ds</span>
+  </body>
+</html>
+

4.伪类选择器

1.not(s)

案例

html
<div class="demo">1</div>
+<div class="demo">2</div>
+<div class="test">3</div>
+<div>4</div>
+
<div class="demo">1</div>
+<div class="demo">2</div>
+<div class="test">3</div>
+<div>4</div>
+
css
div:not([class]) {
+  background-color: red;
+}
+
div:not([class]) {
+  background-color: red;
+}
+

实际开发类似需求----移动端列表页:除了最后一个都要加上一条横线

css
*{
+    margin: 0;
+    padding: 0;
+}
+ul{
+    width: 300px;
+    border:1px solid #999;
+}
+li{
+    height: 50px;
+    margin: 0 5px;
+    /*border-bottom: 1px solid black;*/
+}
+li:not(:last-of-type){
+    border-bottom: 1px solid black;
+}
+<ul>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+</ul>
+
*{
+    margin: 0;
+    padding: 0;
+}
+ul{
+    width: 300px;
+    border:1px solid #999;
+}
+li{
+    height: 50px;
+    margin: 0 5px;
+    /*border-bottom: 1px solid black;*/
+}
+li:not(:last-of-type){
+    border-bottom: 1px solid black;
+}
+<ul>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+<li>content</li>
+</ul>
+

2.root 和 html 地位相等

区別:

1.root:根标签选择器   html xml

2.html:根标签

3.root 包含 html

用法:

css
:root{background-color}
+
:root{background-color}
+

3.target

含义

被标记成锚点之后--location.hash=×××

案例:可点击创造锚点

html
<a href="#box1">box1</a>
+<a href="#box2">box2</a>
+<div id="box1">1</div>
+<div id="box2">2</div>
+
<a href="#box1">box1</a>
+<a href="#box2">box2</a>
+<div id="box1">1</div>
+<div id="box2">2</div>
+
css
div:target {
+  border: 1px solid red;
+}
+
div:target {
+  border: 1px solid red;
+}
+

DEMO

html
<!DOCTYPE html>
+<html>
+    <head>
+        <title>finish js</title>
+        <style>
+            *{
+                padding: 0;
+                margin: 0;
+                list-style: none;
+            }
+            body{
+                height: 2000px;
+            }
+            a{
+                position: fixed;
+                top:0;
+            }
+            .item{
+                position: absolute;
+                top:1000px;
+                width: 100px;
+                height: 100px;
+                background: red;
+            }
+            #item1{
+                top:500px;
+            }
+            #item2{
+                top:1000px;
+            }
+            #item3{
+                top:1500px;
+            }
+            .item:target{
+                background: green;
+            }
+
+        </style>
+    </head>
+    <body>
+        <a href="#item1" style="left:200px">click1</a>
+        <a href="#item2" style="left:300px">click2</a>
+        <a href="#item3" style="left:400px">click3</a>
+        <div id="item1" class="item">1</div>
+        <div id="item2" class="item">2</div>
+        <div id="item3" class="item">3</div>
+    </body>
+</html
+
<!DOCTYPE html>
+<html>
+    <head>
+        <title>finish js</title>
+        <style>
+            *{
+                padding: 0;
+                margin: 0;
+                list-style: none;
+            }
+            body{
+                height: 2000px;
+            }
+            a{
+                position: fixed;
+                top:0;
+            }
+            .item{
+                position: absolute;
+                top:1000px;
+                width: 100px;
+                height: 100px;
+                background: red;
+            }
+            #item1{
+                top:500px;
+            }
+            #item2{
+                top:1000px;
+            }
+            #item3{
+                top:1500px;
+            }
+            .item:target{
+                background: green;
+            }
+
+        </style>
+    </head>
+    <body>
+        <a href="#item1" style="left:200px">click1</a>
+        <a href="#item2" style="left:300px">click2</a>
+        <a href="#item3" style="left:400px">click3</a>
+        <div id="item1" class="item">1</div>
+        <div id="item2" class="item">2</div>
+        <div id="item3" class="item">3</div>
+    </body>
+</html
+

小项目:三个 a 标签控制页面背景颜色

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      /*实现div=屏幕的高度	*/
+      :root,
+      body {
+        margin: 0;
+        padding: 0;
+        height: 100%;
+      }
+      #red,
+      #green,
+      #gray {
+        height: 100%;
+        width: 100%;
+      }
+      #red {
+        background-color: #f20;
+      }
+      #green {
+        background-color: green;
+      }
+      #gray {
+        background-color: gray;
+      }
+      div[id]:not(:target) {
+        display: none;
+      }
+      div.button-wrapper {
+        position: absolute;
+        width: 600px;
+        top: 400px;
+      }
+      div.button-wrapper a {
+        text-decoration: none;
+        color: #fff;
+        background-color: #fcc;
+        font-size: 30px;
+        border-radius: 3px;
+        margin: 0 10px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="button-wrapper">
+      <a href="#red" class="bgred">red</a>
+      <a href="#green" class="bggreen">green</a>
+      <a href="#gray" class="bggray">gray</a>
+    </div>
+    <div id="red"></div>
+    <div id="green"></div>
+    <div id="gray"></div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      /*实现div=屏幕的高度	*/
+      :root,
+      body {
+        margin: 0;
+        padding: 0;
+        height: 100%;
+      }
+      #red,
+      #green,
+      #gray {
+        height: 100%;
+        width: 100%;
+      }
+      #red {
+        background-color: #f20;
+      }
+      #green {
+        background-color: green;
+      }
+      #gray {
+        background-color: gray;
+      }
+      div[id]:not(:target) {
+        display: none;
+      }
+      div.button-wrapper {
+        position: absolute;
+        width: 600px;
+        top: 400px;
+      }
+      div.button-wrapper a {
+        text-decoration: none;
+        color: #fff;
+        background-color: #fcc;
+        font-size: 30px;
+        border-radius: 3px;
+        margin: 0 10px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="button-wrapper">
+      <a href="#red" class="bgred">red</a>
+      <a href="#green" class="bggreen">green</a>
+      <a href="#gray" class="bggray">gray</a>
+    </div>
+    <div id="red"></div>
+    <div id="green"></div>
+    <div id="gray"></div>
+  </body>
+</html>
+

4.其他伪类选择器(此部分都考虑其他元素的影响)

受其他元素的影响,父子级,不出常用

1.first-child

html
<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+
<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+
css
/*伪类选择器是被选择元素的状态*/
+p:first-child {
+  background-color: red;
+}
+
/*伪类选择器是被选择元素的状态*/
+p:first-child {
+  background-color: red;
+}
+
html
<p>1</p>
+<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+
<p>1</p>
+<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+

是不是 first-child,不只看是 p 里面的第一个,要看父级下面的第一个

html
<div>
+  <span>0</span>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+
<div>
+  <span>0</span>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+

2.last-child

html
<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+<p>4</p>
+<!-- last-child:只要是最后一个儿子,就可,即14 -->
+
<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+</div>
+<p>4</p>
+<!-- last-child:只要是最后一个儿子,就可,即14 -->
+

3.only-child

css
span:only-child {
+  background-color: aqua;
+}
+div:only-child {
+  background-color: blueviolet;
+}
+
span:only-child {
+  background-color: aqua;
+}
+div:only-child {
+  background-color: blueviolet;
+}
+
html
<div>
+  <span>0</span>
+  <p>1</p>
+</div>
+
<div>
+  <span>0</span>
+  <p>1</p>
+</div>
+

DEMO

html
nth-child(n)<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      /*实现div=屏幕的高度	*/
+      :root,
+      body {
+        margin: 0;
+        padding: 0;
+        height: 100%;
+      }
+      #red,
+      #green,
+      #gray {
+        height: 100%;
+        width: 100%;
+      }
+      #red {
+        background-color: #f20;
+      }
+      #green {
+        background-color: green;
+      }
+      #gray {
+        background-color: gray;
+      }
+      div[id]:not(:target) {
+        display: none;
+      }
+      div.button-wrapper {
+        position: absolute;
+        width: 600px;
+        top: 400px;
+      }
+      div.button-wrapper a {
+        text-decoration: none;
+        color: #fff;
+        background-color: #fcc;
+        font-size: 30px;
+        border-radius: 3px;
+        margin: 0 10px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="button-wrapper">
+      <a href="#red" class="bgred">red</a>
+      <a href="#green" class="bggreen">green</a>
+      <a href="#gray" class="bggray">gray</a>
+    </div>
+    <div id="red"></div>
+    <div id="green"></div>
+    <div id="gray"></div>
+  </body>
+</html>
+
nth-child(n)<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      /*实现div=屏幕的高度	*/
+      :root,
+      body {
+        margin: 0;
+        padding: 0;
+        height: 100%;
+      }
+      #red,
+      #green,
+      #gray {
+        height: 100%;
+        width: 100%;
+      }
+      #red {
+        background-color: #f20;
+      }
+      #green {
+        background-color: green;
+      }
+      #gray {
+        background-color: gray;
+      }
+      div[id]:not(:target) {
+        display: none;
+      }
+      div.button-wrapper {
+        position: absolute;
+        width: 600px;
+        top: 400px;
+      }
+      div.button-wrapper a {
+        text-decoration: none;
+        color: #fff;
+        background-color: #fcc;
+        font-size: 30px;
+        border-radius: 3px;
+        margin: 0 10px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="button-wrapper">
+      <a href="#red" class="bgred">red</a>
+      <a href="#green" class="bggreen">green</a>
+      <a href="#gray" class="bggray">gray</a>
+    </div>
+    <div id="red"></div>
+    <div id="green"></div>
+    <div id="gray"></div>
+  </body>
+</html>
+

4.nth-child()

CSS 从 1 开始,n 是从 0 开始   odd 奇数 even 偶数

css
p:nth-child(2n) {
+  background-color: blueviolet;
+}
+
p:nth-child(2n) {
+  background-color: blueviolet;
+}
+
html
<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+  <p>4</p>
+  <p>5</p>
+</div>
+
<div>
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+  <p>4</p>
+  <p>5</p>
+</div>
+

5.nth-last-child(n)

倒着数

5.其他伪类选择器(不受其他影响)

不受其他影响,常用

1.first-of-type

html
<ul>
+  <li>1</li>
+  <li>2</li>
+  <li>3</li>
+  <li>4</li>
+  <li>5</li>
+  <li>6</li>
+</ul>
+<li>1</li>
+
<ul>
+  <li>1</li>
+  <li>2</li>
+  <li>3</li>
+  <li>4</li>
+  <li>5</li>
+  <li>6</li>
+</ul>
+<li>1</li>
+
css
li:first-of-type {
+  background-color: aqua;
+}
+
li:first-of-type {
+  background-color: aqua;
+}
+

这一类型里面是的第一个

2.last-of-type

3.only-of-type

html
<div>
+  <span>0</span>
+  <p>1</p>
+  <!-- <p>2</p>不是特有的了 -->
+</div>
+
<div>
+  <span>0</span>
+  <p>1</p>
+  <!-- <p>2</p>不是特有的了 -->
+</div>
+
css
p:only-of-type {
+  background-color: aqua;
+}
+
p:only-of-type {
+  background-color: aqua;
+}
+

4.nth-of-type(n)常用

html
<div>
+  <span>0</span
+  ><!-- 注意这一行 -->
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+  <p>4</p>
+  <p>5</p>
+  <!-- <p>2</p>不是特有的了 -->
+</div>
+
<div>
+  <span>0</span
+  ><!-- 注意这一行 -->
+  <p>1</p>
+  <p>2</p>
+  <p>3</p>
+  <p>4</p>
+  <p>5</p>
+  <!-- <p>2</p>不是特有的了 -->
+</div>
+
css
p:nth-of-type(n+2){从第二个开始查
+    background-color: aqua;
+}
+
p:nth-of-type(n+2){从第二个开始查
+    background-color: aqua;
+}
+

5.nth-of-last-type(n)

与前一个相反

6.剩下的伪类选择器

1.empty:必须元素是空的,才叫 empty

html
<div><span>123</span></div>
+<div></div>
+<div>234</div>
+<div><!--sda--></div>
+<!--注释也算节点-->
+
<div><span>123</span></div>
+<div></div>
+<div>234</div>
+<div><!--sda--></div>
+<!--注释也算节点-->
+
css
div:empty {
+  border: 1px solid black;
+  height: 100px;
+}
+
div:empty {
+  border: 1px solid black;
+  height: 100px;
+}
+

2.checked

html
<label> 一个小惊喜<input type="checkbox" /><span></span> </label>
+
<label> 一个小惊喜<input type="checkbox" /><span></span> </label>
+
css
input:checked {
+  width: 200px;
+  height: 200px;
+}
+
input:checked {
+  width: 200px;
+  height: 200px;
+}
+

DEMO 处理简单交互

html
<style>
+  input:checked + span {
+    background-color: green;
+  }
+  input:checked + span::after {
+    content: "隔壁老王 电话xxx,请务必小心刑事";
+    color: #fff;
+  }
+</style>
+<label>
+  一个小惊喜
+  <input type="checkbox" />
+  <span></span>
+</label>
+
<style>
+  input:checked + span {
+    background-color: green;
+  }
+  input:checked + span::after {
+    content: "隔壁老王 电话xxx,请务必小心刑事";
+    color: #fff;
+  }
+</style>
+<label>
+  一个小惊喜
+  <input type="checkbox" />
+  <span></span>
+</label>
+

3.enable

html
<input type="text" /> <input type="text" disabled />
+
<input type="text" /> <input type="text" disabled />
+
css
input:enabled {
+  background: green;
+}
+input:disabled {
+  border: 1px solid #f20;
+  background-color: #fcc;
+  box-shadow: 1px 2px 3px #f20;
+}
+
input:enabled {
+  background: green;
+}
+input:disabled {
+  border: 1px solid #f20;
+  background-color: #fcc;
+  box-shadow: 1px 2px 3px #f20;
+}
+

4.disable

5.read-only

css
input:read-only{
+    color:chartreuse;
+}
+<input type="text">
+<input type="text" readonly value="你只能瞅着,干不了别的">
+
input:read-only{
+    color:chartreuse;
+}
+<input type="text">
+<input type="text" readonly value="你只能瞅着,干不了别的">
+

6.read-write

作业

选项卡

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+        list-style: none;
+      }
+      .wrapper {
+        position: relative;
+        width: 400px;
+        height: 400px;
+        border: 1px solid black;
+        text-align: center;
+        margin: 0 auto; /*居中*/
+      }
+      .wrapper input {
+        width: 30px;
+        height: 30px;
+      }
+      /*.wrapper div和.wrapper实现三个小圆点在一行展示,块级元素。*/
+      /*.wrapper div相对于.wrapper定位:top,left*/
+      .wrapper div {
+        position: absolute;
+        top: 30px;
+        left: 0;
+        width: 400px;
+        height: 370px;
+        display: none; /*先都是none,选中那个那个变成block*/
+        text-align: center;
+        line-height: 370px;
+        font-size: 30px;
+        color: #fff;
+      }
+      .wrapper div:nth-of-type(1) {
+        background-color: red;
+      }
+      .wrapper div:nth-of-type(2) {
+        background-color: green;
+      }
+      .wrapper div:nth-of-type(3) {
+        background-color: blue;
+      }
+      input:checked + div {
+        /*input:checked + div{     不要有空格,否则错误*/
+        display: block;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="wrapper">
+      <input type="radio" name="a" checked />
+      <div>a</div>
+      <input type="radio" name="a" />
+      <div>b</div>
+      <input type="radio" name="a" />
+      <div>c</div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Document</title>
+    <style>
+      * {
+        padding: 0;
+        margin: 0;
+        list-style: none;
+      }
+      .wrapper {
+        position: relative;
+        width: 400px;
+        height: 400px;
+        border: 1px solid black;
+        text-align: center;
+        margin: 0 auto; /*居中*/
+      }
+      .wrapper input {
+        width: 30px;
+        height: 30px;
+      }
+      /*.wrapper div和.wrapper实现三个小圆点在一行展示,块级元素。*/
+      /*.wrapper div相对于.wrapper定位:top,left*/
+      .wrapper div {
+        position: absolute;
+        top: 30px;
+        left: 0;
+        width: 400px;
+        height: 370px;
+        display: none; /*先都是none,选中那个那个变成block*/
+        text-align: center;
+        line-height: 370px;
+        font-size: 30px;
+        color: #fff;
+      }
+      .wrapper div:nth-of-type(1) {
+        background-color: red;
+      }
+      .wrapper div:nth-of-type(2) {
+        background-color: green;
+      }
+      .wrapper div:nth-of-type(3) {
+        background-color: blue;
+      }
+      input:checked + div {
+        /*input:checked + div{     不要有空格,否则错误*/
+        display: block;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="wrapper">
+      <input type="radio" name="a" checked />
+      <div>a</div>
+      <input type="radio" name="a" />
+      <div>b</div>
+      <input type="radio" name="a" />
+      <div>c</div>
+    </div>
+  </body>
+</html>
+

手风琴

html
<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <title>原生js手风琴特效</title>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+      ul,
+      li {
+        list-style: none;
+      }
+      .box {
+        width: 1050px;
+        height: 300px;
+        margin: 100px auto;
+        overflow: hidden;
+      }
+      .accordion li {
+        float: left;
+        width: 100px;
+        height: 300px;
+        color: #000;
+        font-size: 20px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="box">
+      <ul class="accordion">
+        <li style="background: #f99;">1</li>
+        <li style="background: #9ff;">2</li>
+        <li style="background: #f9f;">3</li>
+        <li style="background: #9f9;">4</li>
+        <li style="background: blue;">5</li>
+        <li style="width:450px;background: yellow;">6</li>
+      </ul>
+    </div>
+    <script>
+      function accordion() {
+        var oBox = document.querySelector(".box");
+        var accordion = oBox.querySelector(".accordion");
+        var oList = accordion.getElementsByTagName("li");
+        var i = 0;
+        var timer = null;
+        //console.log(oList.length);
+        for (var i = 0; i < oList.length; i++) {
+          oList[i].index = i;
+          oList[i].onmouseover = function () {
+            var index = this.index;
+            if (timer) {
+              clearInterval(timer);
+            }
+            timer = setInterval(function () {
+              var iWidth = oBox.offsetWidth; //盒子的总宽度
+              //console.log(iWidth); 1050
+              for (var i = 0; i < oList.length; i++) {
+                if (index != oList[i].index) {
+                  //设定速度
+                  var speed = (100 - oList[i].offsetWidth) / 5;
+                  speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
+                  oList[i].style.width = oList[i].offsetWidth + speed + "px";
+                  iWidth -= oList[i].offsetWidth;
+                }
+                oList[index].style.width = iWidth + "px";
+              }
+            }, 30);
+          };
+        }
+      }
+      accordion();
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <title>原生js手风琴特效</title>
+    <style>
+      * {
+        margin: 0;
+        padding: 0;
+      }
+      ul,
+      li {
+        list-style: none;
+      }
+      .box {
+        width: 1050px;
+        height: 300px;
+        margin: 100px auto;
+        overflow: hidden;
+      }
+      .accordion li {
+        float: left;
+        width: 100px;
+        height: 300px;
+        color: #000;
+        font-size: 20px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="box">
+      <ul class="accordion">
+        <li style="background: #f99;">1</li>
+        <li style="background: #9ff;">2</li>
+        <li style="background: #f9f;">3</li>
+        <li style="background: #9f9;">4</li>
+        <li style="background: blue;">5</li>
+        <li style="width:450px;background: yellow;">6</li>
+      </ul>
+    </div>
+    <script>
+      function accordion() {
+        var oBox = document.querySelector(".box");
+        var accordion = oBox.querySelector(".accordion");
+        var oList = accordion.getElementsByTagName("li");
+        var i = 0;
+        var timer = null;
+        //console.log(oList.length);
+        for (var i = 0; i < oList.length; i++) {
+          oList[i].index = i;
+          oList[i].onmouseover = function () {
+            var index = this.index;
+            if (timer) {
+              clearInterval(timer);
+            }
+            timer = setInterval(function () {
+              var iWidth = oBox.offsetWidth; //盒子的总宽度
+              //console.log(iWidth); 1050
+              for (var i = 0; i < oList.length; i++) {
+                if (index != oList[i].index) {
+                  //设定速度
+                  var speed = (100 - oList[i].offsetWidth) / 5;
+                  speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
+                  oList[i].style.width = oList[i].offsetWidth + speed + "px";
+                  iWidth -= oList[i].offsetWidth;
+                }
+                oList[index].style.width = iWidth + "px";
+              }
+            }, 30);
+          };
+        }
+      }
+      accordion();
+    </script>
+  </body>
+</html>
+
html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Document</title>
+    <style type="text/css">
+      * {
+        margin: 0;
+        padding: 0;
+        font-size: 12px;
+        list-style: none;
+      }
+
+      .menu {
+        margin: 50px auto;
+        width: 210px;
+        border: 1px solid #ccc;
+      }
+
+      .menu p {
+        height: 25px;
+        line-height: 25px;
+        background: #eee;
+        font-weight: bold;
+        border-bottom: 1px solid #ccc;
+        text-indent: 20px;
+        cursor: pointer;
+      }
+
+      .menu div ul {
+        display: none;
+      }
+
+      .menu li {
+        height: 24px;
+        line-height: 24px;
+        text-indent: 20px;
+      }
+    </style>
+    <script type="text/javascript">
+      window.onload = function () {
+        var menu = document.getElementById("menu");
+        var ps = menu.getElementsByTagName("p");
+        var uls = menu.getElementsByTagName("ul");
+        for (var i in ps) {
+          ps[i].id = i;
+          ps[i].onclick = function () {
+            var u = uls[this.id];
+            if (u.style.display == "block") {
+              u.style.display = "none";
+            } else {
+              u.style.display = "block";
+            }
+          };
+        }
+      };
+    </script>
+  </head>
+
+  <body>
+    <div class="menu" id="menu">
+      <div>
+        <p>Web前端</p>
+        <ul style="display:block">
+          <li>JavaScript</li>
+          <li>DIV+CSS</li>
+          <li>JQuary</li>
+        </ul>
+      </div>
+      <div>
+        <p>后台脚本</p>
+        <ul>
+          <li>PHP</li>
+          <li>ASP.net</li>
+          <li>JSP</li>
+        </ul>
+      </div>
+      <div>
+        <p>前端框架</p>
+        <ul>
+          <li>Extjs</li>
+          <li>Esspress</li>
+          <li>YUI</li>
+        </ul>
+      </div>
+    </div>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Document</title>
+    <style type="text/css">
+      * {
+        margin: 0;
+        padding: 0;
+        font-size: 12px;
+        list-style: none;
+      }
+
+      .menu {
+        margin: 50px auto;
+        width: 210px;
+        border: 1px solid #ccc;
+      }
+
+      .menu p {
+        height: 25px;
+        line-height: 25px;
+        background: #eee;
+        font-weight: bold;
+        border-bottom: 1px solid #ccc;
+        text-indent: 20px;
+        cursor: pointer;
+      }
+
+      .menu div ul {
+        display: none;
+      }
+
+      .menu li {
+        height: 24px;
+        line-height: 24px;
+        text-indent: 20px;
+      }
+    </style>
+    <script type="text/javascript">
+      window.onload = function () {
+        var menu = document.getElementById("menu");
+        var ps = menu.getElementsByTagName("p");
+        var uls = menu.getElementsByTagName("ul");
+        for (var i in ps) {
+          ps[i].id = i;
+          ps[i].onclick = function () {
+            var u = uls[this.id];
+            if (u.style.display == "block") {
+              u.style.display = "none";
+            } else {
+              u.style.display = "block";
+            }
+          };
+        }
+      };
+    </script>
+  </head>
+
+  <body>
+    <div class="menu" id="menu">
+      <div>
+        <p>Web前端</p>
+        <ul style="display:block">
+          <li>JavaScript</li>
+          <li>DIV+CSS</li>
+          <li>JQuary</li>
+        </ul>
+      </div>
+      <div>
+        <p>后台脚本</p>
+        <ul>
+          <li>PHP</li>
+          <li>ASP.net</li>
+          <li>JSP</li>
+        </ul>
+      </div>
+      <div>
+        <p>前端框架</p>
+        <ul>
+          <li>Extjs</li>
+          <li>Esspress</li>
+          <li>YUI</li>
+        </ul>
+      </div>
+    </div>
+  </body>
+</html>
+
+ + + + + \ No newline at end of file diff --git a/html-css/temop.html b/html-css/temop.html new file mode 100644 index 00000000..1e72239a --- /dev/null +++ b/html-css/temop.html @@ -0,0 +1,20 @@ + + + + + + Sunny's blog | Sunny's blog + + + + + + + + +
Skip to content
On this page
+ + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..afc50c19 --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + + Sunny's blog | Sunny's blog + + + + + + + + +
Skip to content

Sunny's blog

前端历险记

前端历险记
+ + + + + \ No newline at end of file diff --git "a/interview/\347\256\227\346\263\225\347\254\224\350\257\225.html" "b/interview/\347\256\227\346\263\225\347\254\224\350\257\225.html" new file mode 100644 index 00000000..eaf12179 --- /dev/null +++ "b/interview/\347\256\227\346\263\225\347\254\224\350\257\225.html" @@ -0,0 +1,306 @@ + + + + + + 算法笔试 | Sunny's blog + + + + + + + + +
Skip to content
On this page

算法笔试

练习地址:

https://www.nowcoder.com/test/question/93bc96f6d19f4795a4b893ee16e97654?pid=27976983&tid=55388436

A+B

javascript
var line
+while (line = readline()) {
+    var lines = line.split(' ');
+    var a = parseInt(lines[0]);
+    var b = parseInt(lines[1]);
+    print(a + b);
+}
+
var line
+while (line = readline()) {
+    var lines = line.split(' ');
+    var a = parseInt(lines[0]);
+    var b = parseInt(lines[1]);
+    print(a + b);
+}
+

2

javascript
let n = parseInt(readline())
+for (let i = 0; i < n; i++) {
+    let line = readline().split(' ')
+    let a = parseInt(line[0])
+    let b = parseInt(line[1])
+    console.log(a + b)
+}
+
let n = parseInt(readline())
+for (let i = 0; i < n; i++) {
+    let line = readline().split(' ')
+    let a = parseInt(line[0])
+    let b = parseInt(line[1])
+    console.log(a + b)
+}
+

3

javascript
while(line = readline()){
+    let arr = line.split(' ')
+    let a = parseInt(arr[0])
+    let b = parseInt(arr[1])
+    if(a!==0||b!==0){
+        console.log(a+b)
+    }
+}
+
while(line = readline()){
+    let arr = line.split(' ')
+    let a = parseInt(arr[0])
+    let b = parseInt(arr[1])
+    if(a!==0||b!==0){
+        console.log(a+b)
+    }
+}
+

4

javascript
while(true){
+     let arr = readline().split(' ').map(Number)
+     if(arr[0] === 0){
+         break
+     }
+    let ans = 0
+    for(let i=1; i<=arr[0]; i++){
+        ans += arr[i]
+    }
+    console.log(ans)
+}
+
while(true){
+     let arr = readline().split(' ').map(Number)
+     if(arr[0] === 0){
+         break
+     }
+    let ans = 0
+    for(let i=1; i<=arr[0]; i++){
+        ans += arr[i]
+    }
+    console.log(ans)
+}
+

5

javascript
let n = parseInt(readline())
+
+for(let i = 0; i < n; i++) {
+    const arr = readline().split(' ').map(Number)
+    let sum = 0
+    const [a, ...resp] = arr;
+    for(let i = 0; i < a; i++){
+        sum+=resp[i]
+    }
+    console.log(sum)
+}
+
let n = parseInt(readline())
+
+for(let i = 0; i < n; i++) {
+    const arr = readline().split(' ').map(Number)
+    let sum = 0
+    const [a, ...resp] = arr;
+    for(let i = 0; i < a; i++){
+        sum+=resp[i]
+    }
+    console.log(sum)
+}
+

6

javascript
while(line=readline()){
+    let sum = 0;
+    const [n, ...resp] = line.split(' ').map(Number)
+    for(let i = 0;i<n;i++){
+        sum+=resp[i]
+    }
+    console.log(sum)
+}
+
while(line=readline()){
+    let sum = 0;
+    const [n, ...resp] = line.split(' ').map(Number)
+    for(let i = 0;i<n;i++){
+        sum+=resp[i]
+    }
+    console.log(sum)
+}
+

7

javascript
let line;
+while(line=readline()){
+    const arr = line.split(' ').map(Number)
+    if(arr.every(i=>i===0)){
+        break
+    }
+    const res = arr.reduce((a, b)=>a+b, 0)
+    console.log(res)
+}
+
let line;
+while(line=readline()){
+    const arr = line.split(' ').map(Number)
+    if(arr.every(i=>i===0)){
+        break
+    }
+    const res = arr.reduce((a, b)=>a+b, 0)
+    console.log(res)
+}
+

字符串排序1

javascript
let n = parseInt(readline()) // 没啥用,只是拿出来占位
+let arr = readline().split(' ')
+arrSort = arr.sort()
+console.log(arrSort.join(' '))
+
let n = parseInt(readline()) // 没啥用,只是拿出来占位
+let arr = readline().split(' ')
+arrSort = arr.sort()
+console.log(arrSort.join(' '))
+

字符串排序2

javascript
while(line=readline()){
+    const arr = line.split(' ')
+    console.log(arr.sort().join(' '))
+}
+
while(line=readline()){
+    const arr = line.split(' ')
+    console.log(arr.sort().join(' '))
+}
+

字符串排序3

javascript
while (line = readline()) {
+    console.log(line.split(',').sort().join(','))
+}
+
while (line = readline()) {
+    console.log(line.split(',').sort().join(','))
+}
+

总结

  • 输入数组
    • 一定要记得转字符串到number
javascript
let arr = [];
+let line;
+while ((line = read_line()) != "") {
+    arr.push(line.split(' ').map(v=>parseInt(v)));
+}
+
let arr = [];
+let line;
+while ((line = read_line()) != "") {
+    arr.push(line.split(' ').map(v=>parseInt(v)));
+}
+

输入API

read_line()

  • 将读取至多1024个字符,当还未达到1024个时如果遇到回车或结束符,提前结束。
  • 读取多行最简单的办法是 while((line = read_line()) != '')

gets(n)

  • 将读取至多n个字符,当还未达到n个时如果遇到回车或结束符,会提前结束。
  • 回车符可能会包含在返回值中。
  • 如果字符串末尾不应该是回车符的话,

readInt()

读取(长)整数

readDouble()

读取浮点数

输出API

printsth(a, ...) 不加回车

输出不加回车的参数,空格分隔

输出加回车的参数,空格分隔

console.log(a, ...) 加回车

输出加回车的参数,空格分隔

示例

javascript
var a, b;
+var solveMeFirst = (a,b) => a+b;
+while((a=readInt())!=null && (b=readInt())!=null){
+    let c = solveMeFirst(a, b);
+    print(c);
+}
+var line;
+var solveMeFirst = (a,b) => a+b;
+while((line = read_line()) != ''){
+    let arr = line.split(' ');
+    let a = parseInt(arr[0]);
+    let b = parseInt(arr[1]);
+    let c = solveMeFirst(a, b);
+    print(c);
+}
+
var a, b;
+var solveMeFirst = (a,b) => a+b;
+while((a=readInt())!=null && (b=readInt())!=null){
+    let c = solveMeFirst(a, b);
+    print(c);
+}
+var line;
+var solveMeFirst = (a,b) => a+b;
+while((line = read_line()) != ''){
+    let arr = line.split(' ');
+    let a = parseInt(arr[0]);
+    let b = parseInt(arr[1]);
+    let c = solveMeFirst(a, b);
+    print(c);
+}
+

矩阵

情况一:输入的矩阵用空格分隔数字,没有[]和,

输入一个矩阵:每行以空格分隔。

3 2 3

1 6 5

7 8 9

javascript
// 输入
+let arr = [];
+let line;
+while ((line = read_line()) != "") {
+    arr.push(line.split(' ').map(v=>parseInt(v)));
+}
+// 调试时输出:使用自测数据按钮时调试用,正式提交时要删掉。
+for (let i=0; i<arr.length; i++) {
+    for (let j=0; j<arr[i].length; j++) {
+        printsth(arr[i][j], ' ');
+    }
+    print();
+}
+
// 输入
+let arr = [];
+let line;
+while ((line = read_line()) != "") {
+    arr.push(line.split(' ').map(v=>parseInt(v)));
+}
+// 调试时输出:使用自测数据按钮时调试用,正式提交时要删掉。
+for (let i=0; i<arr.length; i++) {
+    for (let j=0; j<arr[i].length; j++) {
+        printsth(arr[i][j], ' ');
+    }
+    print();
+}
+

情况二:输入的矩阵带有中括号和逗号

[[3,2,3],

[1,6,5],

[7,8,9]]

对于这种没有给定矩阵行列数的输入,而且还包含中括号和逗号的输入,我们也是只能按照字符串拆分来进行。

javascript
// 输入:
+let arr = [];
+let line;
+while ((line = read_line()) != "") {
+    // 字符串中replace四种:  ],	  [		]  空格
+    arr.push(line.replace(/\]\,/g, "").replace(/ /g, "").replace(/\[/g, "").replace(/\]/g, "").split(",").map(v=>parseInt(v)));
+}
+
+// 调试时输出:使用自测数据按钮时调试用,正式提交时要删掉。
+for (let i=0; i<arr.length; i++) {
+    for (let j=0; j<arr[i].length; j++) {
+        printsth(arr[i][j], ' ');
+    }
+    print();
+}
+
// 输入:
+let arr = [];
+let line;
+while ((line = read_line()) != "") {
+    // 字符串中replace四种:  ],	  [		]  空格
+    arr.push(line.replace(/\]\,/g, "").replace(/ /g, "").replace(/\[/g, "").replace(/\]/g, "").split(",").map(v=>parseInt(v)));
+}
+
+// 调试时输出:使用自测数据按钮时调试用,正式提交时要删掉。
+for (let i=0; i<arr.length; i++) {
+    for (let j=0; j<arr[i].length; j++) {
+        printsth(arr[i][j], ' ');
+    }
+    print();
+}
+

情况三:要求输出矩阵/数组带有中括号和逗号

javascript
let arr = [[1,2,3],[4,5,6]]
+console.log(arr.toString())
+// 输出是:1,2,3,4,5,6
+// 这样就错了,没有中括号
+
+console.log(JSON.stringify(arr));
+// 这样是对的,可以输出数组和矩阵
+
+// 输出矩阵,这样也是可以的
+printsth('[');
+for (let i=0; i<arr.length; i++) {
+    printsth('[');
+    printsth(arr[i].join(', '));
+    printsth(']');
+    if (i < arr.length - 1)
+        printsth(', ');
+}
+print(']');
+
let arr = [[1,2,3],[4,5,6]]
+console.log(arr.toString())
+// 输出是:1,2,3,4,5,6
+// 这样就错了,没有中括号
+
+console.log(JSON.stringify(arr));
+// 这样是对的,可以输出数组和矩阵
+
+// 输出矩阵,这样也是可以的
+printsth('[');
+for (let i=0; i<arr.length; i++) {
+    printsth('[');
+    printsth(arr[i].join(', '));
+    printsth(']');
+    if (i < arr.length - 1)
+        printsth(', ');
+}
+print(']');
+

JS读取超长字符串

由于read_line()只能读取1024个字符,所以如果题目中的用例涉及到长度大于1024字符串的,需要用到gets(n)这个函数。

超过1024个字符的情况下:

javascript
// 如果实际输入的字符长度为slen, slen < 10000,那么gets获取的字符串会再其后加一个回车符,令输入的字符串长度为slen + 1
+// 因此,假如字符串中不应该包含空白符的话,应该使用 .trim()
+// 注意,trim会将字符串头尾的回车、空格等空白符都删除。
+let line = gets(10000).trim();
+print(line.length);
+
// 如果实际输入的字符长度为slen, slen < 10000,那么gets获取的字符串会再其后加一个回车符,令输入的字符串长度为slen + 1
+// 因此,假如字符串中不应该包含空白符的话,应该使用 .trim()
+// 注意,trim会将字符串头尾的回车、空格等空白符都删除。
+let line = gets(10000).trim();
+print(line.length);
+

trim()

  • 方法用于删除字符串的头尾空白符,空白符包括:空格、制表符 tab、回车、换行符 等其他空白符等。
  • 不适用于 null, undefined, Number 类型。
+ + + + + \ No newline at end of file diff --git "a/interview/\351\235\242\350\257\225\345\256\230\357\274\232\344\275\240\350\277\230\346\234\211\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221\345\220\227.html" "b/interview/\351\235\242\350\257\225\345\256\230\357\274\232\344\275\240\350\277\230\346\234\211\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221\345\220\227.html" new file mode 100644 index 00000000..0f41516a --- /dev/null +++ "b/interview/\351\235\242\350\257\225\345\256\230\357\274\232\344\275\240\350\277\230\346\234\211\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221\345\220\227.html" @@ -0,0 +1,20 @@ + + + + + + 面试官:你还有问题要问我吗? | Sunny's blog + + + + + + + + +
Skip to content
On this page

面试官:你还有问题要问我吗?

下面列表里的问题对于参加技术面试的人来说可能有些用。

列表里的问题并不一定适用于某个特定的职位或者工作类型,也没有排序

最开始的时候这只是我自己的问题列表,但是慢慢地添加了一些我觉得可能让我对这家公司亮红牌的问题。

我也注意到被我面试的人提问我的问题太少了,感觉他们挺浪费机会的。

预期使用方式

  • 检查一下哪些问题你感兴趣
  • 检查一下哪些是你可以自己在网上找到答案的
  • 找不到的话就向面试官提问

绝对不要想把这个列表里的每个问题都问一遍。(尊重面试官的时间,而且你可以通过查找已经发布的答案来显示 你的主动性)

请记住事情总是灵活的,组织的结构调整也会经常发生。拥有一个 bug 追踪系统并不会保证高效处理 bug。 CI/CD (持续集成系统) 也不一定保证交付时间会很短。

职责

  • On-call (电话值班)的计划或者规定是什么?值班或者遇到问题加班时候有加班费吗?
  • 我的日常工作是什么?
  • 有给我设定的特定目标吗?
  • 团队里面初级和高级工程师的比例是多少?(有计划改变吗)
  • 入职培训 (onboarding) 会是什么样的?
  • 每个开发者有多大的自由来做出决定?
  • 在你看来,这个工作做到什么程度算成功?
  • 你期望我在最初的一个月 / 三个月能够完成什么?
  • 试用期结束的时候,你会怎么样衡量我的绩效?
  • 自己单独的开发活动和按部就班工作的比例大概是怎样的?
  • 一个典型的一天或者一周的工作是怎样安排的?
  • 对我的申请你有什么疑虑么?
  • 在这份工作上,我将会和谁紧密合作?
  • 我的直接上级他们的上级都是什么样的管理风格?(事无巨细还是着眼宏观)
  • 我在这个岗位上应该如何发展?会有哪些机会?
  • 每天预期 / 核心工作时间是多少小时?
  • 我入职的岗位是新增还是接替之前离职的同事?(是否有技术债需要还)?(zh)
  • 入职之后在哪个项目组,项目是新成立还是已有的?(zh)

技术

  • 公司常用的技术栈是什么?
  • 你们怎么使用源码控制系统?
  • 你们怎么测试代码?
  • 你们怎么追踪 bug?
  • 你们怎样监控项目?
  • 你们怎么集成和部署代码改动?是使用持续集成和持续部署吗 (CI/CD)?
  • 你们的基础设施搭建在版本管理系统里吗?或者是代码化的吗?
  • 从计划到完成一项任务的工作流是什么样的?
  • 你们如何准备故障恢复?
  • 有标准的开发环境吗?是强制的吗?
  • 你们需要花费多长时间来给产品搭建一个本地测试环境?(分钟 / 小时 / 天)
  • 你们需要花费多长时间来响应代码或者依赖中的安全问题?
  • 所有的开发者都可以使用他们电脑的本地管理员权限吗?
  • 介绍一下你们的技术原则或者展望。
  • 你们的代码有开发文档吗?有没有单独的供消费者阅读的文档?
  • 你们有更高层次的文档吗?比如说 ER 图,数据库范式
  • 你们使用静态代码分析吗?
  • 你们如何管理内部和外部的数字资产?
  • 你们如何管理依赖?
  • 公司是否有技术分享交流活动?有的话,多久一次呢?(zh)
  • 你们的数据库是怎么进行版本控制的?(zh)
  • 业务需求有没有文档记录?是如何记录的?(zh)

团队

  • 工作是怎么组织的?
  • 团队内 / 团队间的交流通常是怎样的?
  • 你们使用什么工具来做项目组织?你的实际体会是什么?
  • 如果遇到不同的意见怎样处理?
  • 谁来设定优先级 / 计划?
  • 如果团队没能赶上预期发布日期怎么办?
  • 每周都会开什么类型的会议?
  • 会有定期的和上级的一对一谈话吗?
  • 产品 / 服务的规划是什么样的?(n 周一发布 / 持续部署 / 多个发布流 / ...)
  • 生产环境发生事故了怎么办?是否有不批评人而分析问题的文化?
  • 有没有一些团队正在经历还尚待解决的挑战?
  • 你们如何跟踪进度?
  • 预期和目标是如何设定的?谁来设定?
  • Code Review 如何实施?
  • 给我介绍下团队里一个典型的 sprint
  • 你们如何平衡技术和商业目标?
  • 你们如何共享知识?
  • 团队有多大?
  • 公司技术团队的架构和人员组成?(zh)
  • 团队内开发、产品、运营哪一方是需求的主要提出方?哪一方更强势?(zh)

问未来的同事

  • 开发者倾向于从哪里学习?
  • 你对在这里工作最满意的地方是?
  • 最不满意的呢?
  • 如果可以的话,你想改变哪里?
  • 团队最老的成员在这里多久了?
  • 在小团队中,有没有出现成员性格互相冲突的情况?最后是如何解决的?

公司

  • 公司为什么在招人?(产品发展 / 新产品 / 波动...)
  • 有没有会议 / 旅行预算?使用的规定是什么?
  • 晋升流程是怎样的?要求 / 预期是怎样沟通的?
  • 绩效评估流程是怎样的?
  • 技术和管理两条职业路径是分开的吗?
  • 对于多元化招聘的现状或者观点是什么?
  • 有公司级别的学习资源吗?比如电子书订阅或者在线课程?
  • 有获取证书的预算吗?
  • 公司的成熟度如何?(早期寻找方向 / 有内容的工作 / 维护中 / ...)
  • 我可以为开源项目做贡献吗?是否需要审批?
  • 你认为公司未来五年或者十年会发展成什么样子?
  • 公司的大多数员工是如何看待整洁代码的?
  • 你上次注意到有人成长是什么时候?他们在哪方面成长了?
  • 在这里成功的定义是什么?如何衡量成功?
  • 有体育活动或者团建么?
  • 有内部的黑客马拉松活动吗?
  • 公司支持开源项目吗?
  • 有竞业限制或者保密协议需要签吗?
  • 你们认为公司文化中的空白是什么?
  • 能够跟我说一公司处于不良情况,以及如何处理的故事吗?
  • 您在这工作了多久了?您觉得体验如何?(zh)
  • 大家为什么会喜欢这里?(zh)
  • 公司的调薪制度是如何的?(zh)

社会问题

  • 你们关于多元化招聘什么看法?
  • 你们的公司文化如何?你认为有什么空白么?
  • 这里的工作生活平衡地怎么样?
  • 公司对气候变化有什么态度吗?

冲突

  • 不同的意见如何处理?
  • 如果被退回了会怎样?(“这个在预计的时间内做不完”)
  • 当团队有压力并且在超负荷工作的时候怎么处理?
  • 如果有人注意到了在流程或者技术等其他方面又改进的地方,怎么办?
  • 当管理层的预期和工程师的绩效之间有差距的时候如何处理?
  • 能给我讲一个公司深处有毒环境以及如何处理的故事吗?
  • 如果在公司内你的同事因涉嫌性侵犯他人而被调查,请问你会如何处理?
  • 假设我自己很不幸是在公司内被性侵的受害者,在公司内部有没有争取合法权益的渠道?

商业

  • 你们现在盈利吗?
  • 如果没有的话,还需要多久?
  • 公司的资金来源是什么?谁影响或者制定高层计划或方向?
  • 你们如何挣钱?
  • 什么阻止了你们挣更多的钱?
  • 公司未来一年的增长计划怎样?五年呢?
  • 你们认为什么是你们的竞争优势?
  • 你们的竞争优势是什么?
  • 公司未来的商业规划是怎样的?有上市的计划吗?(zh)

远程工作

  • 远程工作和办公室工作的比例是多少?
  • 公司提供硬件吗?更新计划如何?
  • 使用自己的硬件办公可以吗?现在有政策吗?
  • 额外的附件和家具可以通过公司购买吗?这方面是否有预算?
  • 有共享办公或者上网的预算吗?
  • 多久需要去一次办公室?
  • 公司的会议室是否一直是视频会议就绪的?

办公室布局

  • 办公室的布局如何?(开放的 / 小隔间 / 独立办公室)
  • 有没有支持 / 市场 / 或者其他需要大量打电话的团队在我的团队旁边办公?

终极问题

  • 该职位为何会空缺?
  • 公司如何保证人才不流失?
  • 这份工作 / 团队 / 公司最好和最坏的方面是?
  • 你最开始为什么选择了这家公司?
  • 你为什么留在这家公司?

待遇

  • 如果有奖金计划的话,奖金如何分配?
  • 如果有奖金计划的话,过去的几年里通常会发百分之多少的奖金?
  • 有五险一金(zh)/401k(us)或者其他退休养老金等福利吗?
  • 五险一金中,补充公积金一般交多少比例?/401k一般交多少比例?我可以自己选择这一比例吗?
  • 有什么医疗保险吗?如果有的话何时开始?
  • 有额外商业保险吗?例如人寿保险和额外的养老/医疗保险?
  • 更换工作地点,公司付费吗?

休假

  • 带薪休假时间有多久?
  • 病假和事假是分开的还是一起算?
  • 我可以提前使用假期时间吗?也就是说应休假期是负的?
  • 假期的更新策略是什么样的?也就是说未休的假期能否滚入下一周期
  • 照顾小孩的政策如何?
  • 无薪休假政策是什么样的?
  • 学术性休假政策是怎么样的?

参考链接

Find more inspiration for questions in:

https://github.com/viraptor/reverse-interview

翻译:

English

Korean

Portuguese

繁體中文

+ + + + + \ No newline at end of file diff --git a/js-c.png b/js-c.png new file mode 100644 index 00000000..c39b08d0 Binary files /dev/null and b/js-c.png differ diff --git a/js/2023-01-06-12-39-32.png b/js/2023-01-06-12-39-32.png new file mode 100644 index 00000000..159aa4d6 Binary files /dev/null and b/js/2023-01-06-12-39-32.png differ diff --git a/js/2023-01-06-12-40-13.png b/js/2023-01-06-12-40-13.png new file mode 100644 index 00000000..99834853 Binary files /dev/null and b/js/2023-01-06-12-40-13.png differ diff --git a/js/2023-01-06-12-41-12.png b/js/2023-01-06-12-41-12.png new file mode 100644 index 00000000..ac924d9c Binary files /dev/null and b/js/2023-01-06-12-41-12.png differ diff --git a/js/2023-01-06-14-09-28.png b/js/2023-01-06-14-09-28.png new file mode 100644 index 00000000..466d668d Binary files /dev/null and b/js/2023-01-06-14-09-28.png differ diff --git a/js/2023-01-06-14-09-39.png b/js/2023-01-06-14-09-39.png new file mode 100644 index 00000000..c873e987 Binary files /dev/null and b/js/2023-01-06-14-09-39.png differ diff --git a/js/2023-01-06-14-09-53.png b/js/2023-01-06-14-09-53.png new file mode 100644 index 00000000..a1534fb8 Binary files /dev/null and b/js/2023-01-06-14-09-53.png differ diff --git a/js/2023-01-06-14-10-02.png b/js/2023-01-06-14-10-02.png new file mode 100644 index 00000000..09425f53 Binary files /dev/null and b/js/2023-01-06-14-10-02.png differ diff --git "a/js/\344\273\243\347\220\206\344\270\216\345\217\215\345\260\204.html" "b/js/\344\273\243\347\220\206\344\270\216\345\217\215\345\260\204.html" new file mode 100644 index 00000000..748ba227 --- /dev/null +++ "b/js/\344\273\243\347\220\206\344\270\216\345\217\215\345\260\204.html" @@ -0,0 +1,980 @@ + + + + + + 代理与反射 | Sunny's blog + + + + + + + + +
Skip to content
On this page

代理与反射

属性描述符

Property Descriptor

Property Descriptor 属性描述符 是一个普通对象,用于描述一个属性的相关信息 通过Object.getOwnPropertyDescriptor(对象, 属性名)可以得到一个对象的某个属性的属性描述符

javascript
const desc = Object.getOwnPropertyDescriptor(obj, "a");
+console.log(desc);
+
const desc = Object.getOwnPropertyDescriptor(obj, "a");
+console.log(desc);
+
  • value:属性值
  • configurable:该属性的描述符是否可以修改
  • enumerable:该属性是否可以被枚举
  • writable:该属性是否可以被重新赋值

    Object.getOwnPropertyDescriptors(对象)可以得到某个对象的所有属性描述符

如果需要为某个对象添加属性时 或 修改属性时, 配置其属性描述符,可以使用下面的代码: Object.defineProperty(对象, 属性名, 描述符);

javascript
const obj = {
+  a: 1,
+  b: 2,
+};
+Object.defineProperty(obj, "a", {
+  value: 3,
+  configurable: false, //描述符不可以修改
+  enumerable: false, //不可枚举
+  writable: false, //不可写
+});
+console.log(obj);
+for (const prop in obj) {
+  console.log(prop); //只有b,没有a了
+}
+obj.a = 10; //不可写
+console.log(obj);
+const props = Object.keys(obj); //得到对象的所有属性
+console.log(props);
+const values = Object.values(obj); //得到对象的所有值
+console.log(values);
+
const obj = {
+  a: 1,
+  b: 2,
+};
+Object.defineProperty(obj, "a", {
+  value: 3,
+  configurable: false, //描述符不可以修改
+  enumerable: false, //不可枚举
+  writable: false, //不可写
+});
+console.log(obj);
+for (const prop in obj) {
+  console.log(prop); //只有b,没有a了
+}
+obj.a = 10; //不可写
+console.log(obj);
+const props = Object.keys(obj); //得到对象的所有属性
+console.log(props);
+const values = Object.values(obj); //得到对象的所有值
+console.log(values);
+

Object.defineProperties(对象, 多个属性的描述符)

javascript
Object.defineProperties(obj, {
+  a: {
+    value: 3,
+    configurable: false,
+    enumerable: false,
+    writable: false,
+  },
+});
+
Object.defineProperties(obj, {
+  a: {
+    value: 3,
+    configurable: false,
+    enumerable: false,
+    writable: false,
+  },
+});
+

存取器属性

属性描述符中,如果配置了 get 和 set 中的任何一个,则该属性,不再是一个普通属性,而变成了存取器属性。 get 和 set 配置均为函数,如果一个属性是存取器属性,则读取该属性时,会运行 get 方法,将 get 方法得到的返回值作为属性值;如果给该属性赋值,则会运行 set 方法。 属性已经不再内存里面了,而是 get 和 set

javascript
const obj = {
+  b: 2,
+};
+Object.defineProperty(obj, "a", {
+  get() {
+    console.log("运行了属性a的get函数");
+  },
+  set(val) {
+    console.log("运行了属性a的set函数", val);
+  },
+});
+// obj.a的时候运行get函数
+// obj.a赋值的时候运行set函数
+// 所以console.log(obj.a)//undefined,因为get函数没有返回结果
+obj.a = 1; //相当于set(20)
+console.log(obj.a); // 相当于console.log(get())
+// 运行set
+// 运行get
+// undefined
+
const obj = {
+  b: 2,
+};
+Object.defineProperty(obj, "a", {
+  get() {
+    console.log("运行了属性a的get函数");
+  },
+  set(val) {
+    console.log("运行了属性a的set函数", val);
+  },
+});
+// obj.a的时候运行get函数
+// obj.a赋值的时候运行set函数
+// 所以console.log(obj.a)//undefined,因为get函数没有返回结果
+obj.a = 1; //相当于set(20)
+console.log(obj.a); // 相当于console.log(get())
+// 运行set
+// 运行get
+// undefined
+
javascript
// 演示1
+obj.a = 20 + 10; // set(20 + 10) 相当于运行set()
+console.log(obj.a); // console.log(get()) 读取属性a,相当于运行get()
+// 演示2
+obj.a = obj.a + 1; // set(obj.a + 1) 相当于读取 set(get() + 1)  undefined+1=NaN
+console.log(obj.a); //读取属性一定运行get  undefined
+
// 演示1
+obj.a = 20 + 10; // set(20 + 10) 相当于运行set()
+console.log(obj.a); // console.log(get()) 读取属性a,相当于运行get()
+// 演示2
+obj.a = obj.a + 1; // set(obj.a + 1) 相当于读取 set(get() + 1)  undefined+1=NaN
+console.log(obj.a); //读取属性一定运行get  undefined
+
javascript
const obj = {
+  b: 2,
+};
+Object.defineProperty(obj, "a", {
+  get() {
+    console.log("运行了属性a的get函数");
+    return obj._a;
+    // 错误的思路:return obj.a
+    // 这里就成了无限递归,无限读取,导致浏览器卡死,同理set
+  },
+  set(val) {
+    console.log("运行了属性a的set函数", val);
+    obj._a = val;
+  },
+});
+obj.a = 10;
+console.log(obj.a);
+
const obj = {
+  b: 2,
+};
+Object.defineProperty(obj, "a", {
+  get() {
+    console.log("运行了属性a的get函数");
+    return obj._a;
+    // 错误的思路:return obj.a
+    // 这里就成了无限递归,无限读取,导致浏览器卡死,同理set
+  },
+  set(val) {
+    console.log("运行了属性a的set函数", val);
+    obj._a = val;
+  },
+});
+obj.a = 10;
+console.log(obj.a);
+

存取器属性最大的意义,在于可以控制属性的读取和赋值

javascript
obj = {
+  name: "adsf",
+};
+Object.defineProperty(obj, "age", {
+  get() {
+    return obj._age;
+  },
+  set(val) {
+    if (typeof val !== "number") {
+      throw new TypeError("年龄必须是一个数字");
+    }
+    if (val < 0) {
+      val = 0;
+    } else if (val > 200) {
+      val = 200;
+    }
+    obj._age = val;
+  },
+});
+obj.age = "Asdfasasdf";
+console.log(obj.age);
+
obj = {
+  name: "adsf",
+};
+Object.defineProperty(obj, "age", {
+  get() {
+    return obj._age;
+  },
+  set(val) {
+    if (typeof val !== "number") {
+      throw new TypeError("年龄必须是一个数字");
+    }
+    if (val < 0) {
+      val = 0;
+    } else if (val > 200) {
+      val = 200;
+    }
+    obj._age = val;
+  },
+});
+obj.age = "Asdfasasdf";
+console.log(obj.age);
+

应用:将对象的属性和元素关联

html
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <title>Document</title>
+  </head>
+
+  <body>
+    <p>
+      <span>姓名:</span>
+      <span id="name"></span>
+    </p>
+    <p>
+      <span>年龄:</span>
+      <span id="age"></span>
+    </p>
+    <script>
+      const spanName = document.getElementById("name");
+      const spanAge = document.getElementById("age");
+      const user = {};
+      Object.defineProperties(user, {
+        name: {
+          get() {
+            return spanName.innerText;
+            //获取页面窗口中对应的元素内容
+          },
+          set(val) {
+            spanName.innerText = val;
+          },
+        },
+        age: {
+          get() {
+            return +spanAge.innerText;
+          },
+          set(val) {
+            if (typeof val !== "number") {
+              throw new TypeError("年龄必须是一个数字");
+            }
+            if (val < 0) {
+              val = 0;
+            } else if (val > 200) {
+              val = 200;
+            }
+            spanAge.innerText = val;
+          },
+        },
+      });
+    </script>
+  </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <title>Document</title>
+  </head>
+
+  <body>
+    <p>
+      <span>姓名:</span>
+      <span id="name"></span>
+    </p>
+    <p>
+      <span>年龄:</span>
+      <span id="age"></span>
+    </p>
+    <script>
+      const spanName = document.getElementById("name");
+      const spanAge = document.getElementById("age");
+      const user = {};
+      Object.defineProperties(user, {
+        name: {
+          get() {
+            return spanName.innerText;
+            //获取页面窗口中对应的元素内容
+          },
+          set(val) {
+            spanName.innerText = val;
+          },
+        },
+        age: {
+          get() {
+            return +spanAge.innerText;
+          },
+          set(val) {
+            if (typeof val !== "number") {
+              throw new TypeError("年龄必须是一个数字");
+            }
+            if (val < 0) {
+              val = 0;
+            } else if (val > 200) {
+              val = 200;
+            }
+            spanAge.innerText = val;
+          },
+        },
+      });
+    </script>
+  </body>
+</html>
+

属性描述符应用

js
var obj = {
+  b: 2,
+};
+
+// 得到属性描述符
+// var desc = Object.getOwnPropertyDescriptor(obj, 'a');
+// console.log(desc);
+
+// 设置属性描述符
+Object.defineProperty(obj, "a", {
+  value: 10,
+  writable: false, // 不可重写
+  enumerable: false, // 不可遍历
+  configurable: false, // 不可修改描述符本身
+});
+// Object.defineProperty(obj, 'a', {
+//   writable: true,
+// });
+obj.a = "abc";
+console.log(obj.a);
+// for (var key in obj) {
+//   console.log(key);
+// }
+
+// var keys = Object.keys(obj);
+// console.log(keys);
+
+// console.log(obj);
+
var obj = {
+  b: 2,
+};
+
+// 得到属性描述符
+// var desc = Object.getOwnPropertyDescriptor(obj, 'a');
+// console.log(desc);
+
+// 设置属性描述符
+Object.defineProperty(obj, "a", {
+  value: 10,
+  writable: false, // 不可重写
+  enumerable: false, // 不可遍历
+  configurable: false, // 不可修改描述符本身
+});
+// Object.defineProperty(obj, 'a', {
+//   writable: true,
+// });
+obj.a = "abc";
+console.log(obj.a);
+// for (var key in obj) {
+//   console.log(key);
+// }
+
+// var keys = Object.keys(obj);
+// console.log(keys);
+
+// console.log(obj);
+
js
var obj = {};
+
+Object.defineProperty(obj, "a", {
+  get: function () {
+    return 123;
+  }, // 读取器 getter
+  set: function (val) {
+    throw new Error(
+      `兄弟,你正在给a这个属性重新赋值,你所赋的值是${val},但是,这个属性是不能复制,你再考虑考虑`
+    );
+  }, // 设置器 setter
+});
+
+console.log(obj.a);
+obj.a = "abx";
+// console.log(obj.a); // console.log(get())
+
var obj = {};
+
+Object.defineProperty(obj, "a", {
+  get: function () {
+    return 123;
+  }, // 读取器 getter
+  set: function (val) {
+    throw new Error(
+      `兄弟,你正在给a这个属性重新赋值,你所赋的值是${val},但是,这个属性是不能复制,你再考虑考虑`
+    );
+  }, // 设置器 setter
+});
+
+console.log(obj.a);
+obj.a = "abx";
+// console.log(obj.a); // console.log(get())
+

购物车描述

js
var aGoods = {
+  pic: ".",
+  title: "..",
+  desc: `...`,
+  sellNumber: 1,
+  favorRate: 2,
+  price: 3,
+};
+
+class UIGoods {
+  get totalPrice() {
+    return this.choose * this.data.price;
+  }
+
+  get isChoose() {
+    return this.choose > 0;
+  }
+
+  constructor(g) {
+    g = { ...g }; // 克隆一份
+    Object.freeze(g); // 冻结克隆对象
+    Object.defineProperty(this, "data", {
+      get: function () {
+        return g;
+      },
+      set: function () {
+        throw new Error("data 属性是只读的,不能重新赋值");
+      },
+      configurable: false,
+    });
+    var internalChooseValue = 0;
+    Object.defineProperty(this, "choose", {
+      configurable: false,
+      get: function () {
+        return internalChooseValue;
+      },
+      set: function (val) {
+        if (typeof val !== "number") {
+          throw new Error("choose属性必须是数字");
+        }
+        var temp = parseInt(val);
+        if (temp !== val) {
+          throw new Error("choose属性必须是整数");
+        }
+        if (val < 0) {
+          throw new Error("choose属性必须大于等于 0");
+        }
+        internalChooseValue = val;
+      },
+    });
+    this.a = 1;
+    Object.seal(this); // 密封自己 不能加属性了,其他属性还可以改
+  }
+}
+
+Object.freeze(UIGoods.prototype);
+
+var g = new UIGoods(aGoods);
+
+UIGoods.prototype.haha = "abc";
+// g.data.price = 100;
+
+console.log(g.haha);
+
+// 思考:能不能想到各种场景,提前预防
+
var aGoods = {
+  pic: ".",
+  title: "..",
+  desc: `...`,
+  sellNumber: 1,
+  favorRate: 2,
+  price: 3,
+};
+
+class UIGoods {
+  get totalPrice() {
+    return this.choose * this.data.price;
+  }
+
+  get isChoose() {
+    return this.choose > 0;
+  }
+
+  constructor(g) {
+    g = { ...g }; // 克隆一份
+    Object.freeze(g); // 冻结克隆对象
+    Object.defineProperty(this, "data", {
+      get: function () {
+        return g;
+      },
+      set: function () {
+        throw new Error("data 属性是只读的,不能重新赋值");
+      },
+      configurable: false,
+    });
+    var internalChooseValue = 0;
+    Object.defineProperty(this, "choose", {
+      configurable: false,
+      get: function () {
+        return internalChooseValue;
+      },
+      set: function (val) {
+        if (typeof val !== "number") {
+          throw new Error("choose属性必须是数字");
+        }
+        var temp = parseInt(val);
+        if (temp !== val) {
+          throw new Error("choose属性必须是整数");
+        }
+        if (val < 0) {
+          throw new Error("choose属性必须大于等于 0");
+        }
+        internalChooseValue = val;
+      },
+    });
+    this.a = 1;
+    Object.seal(this); // 密封自己 不能加属性了,其他属性还可以改
+  }
+}
+
+Object.freeze(UIGoods.prototype);
+
+var g = new UIGoods(aGoods);
+
+UIGoods.prototype.haha = "abc";
+// g.data.price = 100;
+
+console.log(g.haha);
+
+// 思考:能不能想到各种场景,提前预防
+

Reflect

Reflect 是什么?

Reflect 是一个内置的 JS 对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些 JS 底层功能 由于它类似于其他语言的反射,因此取名为 Reflect

它可以做什么?

使用 Reflect 可以实现诸如 属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中   等等功能

这些功能不是已经存在了吗?为什么还需要用 Reflect 实现一次?

有一个重要的理念,在 ES5 就被提出:减少魔法、让代码更加纯粹,这种理念很大程度上是受到函数式编程的影响

ES6 进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取出来,形成一个正常的 API,并高度聚合到某个对象中,于是,就造就了 Reflect 对象

因此,你可以看到 Reflect 对象中有很多的 API 都可以使用过去的某种语法或其他 API 实现。

它里面到底提供了哪些 API 呢?

  • Reflect.set(target, propertyKey, value): 设置对象 target 的属性 propertyKey 的值为 value,等同于给对象的属性赋值
javascript
const obj = {
+  a: 1,
+  b: 2,
+};
+// obj.a = 10;
+Reflect.set(obj, "a", 10); //给obj的a赋值为10
+console.log(Reflect.get(obj, "a"));
+
const obj = {
+  a: 1,
+  b: 2,
+};
+// obj.a = 10;
+Reflect.set(obj, "a", 10); //给obj的a赋值为10
+console.log(Reflect.get(obj, "a"));
+
  • Reflect.get(target, propertyKey): 读取对象 target 的属性 propertyKey,等同于读取对象的属性值
  • Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数,并绑定 this 和参数列表。等同于函数调用
  • Reflect.deleteProperty(target, propertyKey):删除一个对象的属性
  • Reflect.defineProperty(target, propertyKey, attributes):类似于 Object.defineProperty,不同的是如果配置出现问题,返回 false 而不是报错
  • Reflect.construct(target, argumentsList):用构造函数的方式创建一个对象
  • Reflect.has(target, propertyKey): 判断一个对象是否拥有一个属性
  • 其他 API
javascript
function method(a, b) {
+  console.log("method", a, b);
+}
+// method(3, 4);
+Reflect.apply(method, null, [3, 4]);
+
+const obj = {
+  a: 1,
+  b: 2,
+};
+// delete obj.a;
+Reflect.deleteProperty(obj, "a");
+console.log(obj);
+
+function Test(a, b) {
+  this.a = a;
+  this.b = b;
+}
+
+// const t = new Test(1, 3);
+const t = Reflect.construct(Test, [1, 3]);
+console.log(t);
+
function method(a, b) {
+  console.log("method", a, b);
+}
+// method(3, 4);
+Reflect.apply(method, null, [3, 4]);
+
+const obj = {
+  a: 1,
+  b: 2,
+};
+// delete obj.a;
+Reflect.deleteProperty(obj, "a");
+console.log(obj);
+
+function Test(a, b) {
+  this.a = a;
+  this.b = b;
+}
+
+// const t = new Test(1, 3);
+const t = Reflect.construct(Test, [1, 3]);
+console.log(t);
+
js
const obj = {
+  a: 1,
+  b: 2,
+};
+
+// console.log("a" in obj);
+console.log(Reflect.has(obj, "a"));
+
const obj = {
+  a: 1,
+  b: 2,
+};
+
+// console.log("a" in obj);
+console.log(Reflect.has(obj, "a"));
+

Proxy

提供了修改底层实现的方式

javascript
//代理一个目标对象
+//target:目标对象
+//handler:是一个普通对象,其中可以重写底层实现(可以重写反射里面所有的api)
+//返回一个代理对象
+new Proxy(target, handler);
+
//代理一个目标对象
+//target:目标对象
+//handler:是一个普通对象,其中可以重写底层实现(可以重写反射里面所有的api)
+//返回一个代理对象
+new Proxy(target, handler);
+
javascript
const obj = {
+  a: 1,
+  b: 2,
+};
+
+const proxy = new Proxy(obj, {
+  set(target, propertyKey, value) {
+    // console.log(target, propertyKey, value);
+    // target[propertyKey] = value;修改属性的值。重写了底层实现
+
+    // 等价于
+    Reflect.set(target, propertyKey, value);
+  },
+  get(target, propertyKey) {
+    if (Reflect.has(target, propertyKey)) {
+      return Reflect.get(target, propertyKey);
+    } else {
+      return -1;
+    }
+  },
+  has(target, propertyKey) {
+    return false; //代理说没有就没有
+  },
+});
+// console.log(proxy);
+// proxy.a = 10;
+// console.log(proxy.a);
+
+console.log(proxy.d); //运行get
+console.log("a" in proxy);
+
const obj = {
+  a: 1,
+  b: 2,
+};
+
+const proxy = new Proxy(obj, {
+  set(target, propertyKey, value) {
+    // console.log(target, propertyKey, value);
+    // target[propertyKey] = value;修改属性的值。重写了底层实现
+
+    // 等价于
+    Reflect.set(target, propertyKey, value);
+  },
+  get(target, propertyKey) {
+    if (Reflect.has(target, propertyKey)) {
+      return Reflect.get(target, propertyKey);
+    } else {
+      return -1;
+    }
+  },
+  has(target, propertyKey) {
+    return false; //代理说没有就没有
+  },
+});
+// console.log(proxy);
+// proxy.a = 10;
+// console.log(proxy.a);
+
+console.log(proxy.d); //运行get
+console.log("a" in proxy);
+

应用

观察者模式

有一个对象,是观察者,它用于观察另外一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事。

vue2

html
<div id="container"></div>
+<script>
+  //创建一个观察者
+  function observer(target) {
+    //通过观察目标元素的变化,把这些属性渲染到页面上
+    const div = document.getElementById("container");
+    const ob = {};
+    const props = Object.keys(target); // 拿到所有属性名
+    for (const prop of props) {
+      Object.defineProperty(ob, prop, {
+        get() {
+          // 获取的时候
+          return target[prop];
+        },
+        set(val) {
+          // 设置的时候
+          target[prop] = val;
+          render();
+        },
+        enumerable: true, //这两个属性默认不能被枚举
+      });
+    }
+    render();
+    function render() {
+      let html = "";
+      for (const prop of Object.keys(ob)) {
+        //拼接属性名属性值
+        html += `
+<p><span>${prop}:</span><span>${ob[prop]}</span></p>
+`;
+      }
+      div.innerHTML = html;
+    }
+    return ob;
+  }
+  const target = {
+    a: 1,
+    b: 2,
+  };
+  const obj = observer(target);
+</script>
+
<div id="container"></div>
+<script>
+  //创建一个观察者
+  function observer(target) {
+    //通过观察目标元素的变化,把这些属性渲染到页面上
+    const div = document.getElementById("container");
+    const ob = {};
+    const props = Object.keys(target); // 拿到所有属性名
+    for (const prop of props) {
+      Object.defineProperty(ob, prop, {
+        get() {
+          // 获取的时候
+          return target[prop];
+        },
+        set(val) {
+          // 设置的时候
+          target[prop] = val;
+          render();
+        },
+        enumerable: true, //这两个属性默认不能被枚举
+      });
+    }
+    render();
+    function render() {
+      let html = "";
+      for (const prop of Object.keys(ob)) {
+        //拼接属性名属性值
+        html += `
+<p><span>${prop}:</span><span>${ob[prop]}</span></p>
+`;
+      }
+      div.innerHTML = html;
+    }
+    return ob;
+  }
+  const target = {
+    a: 1,
+    b: 2,
+  };
+  const obj = observer(target);
+</script>
+

缺陷:搞出来了两个对象,占用了内存,代理不占用内存

vue3

html
<div id="container"></div>
+
+<script>
+  //创建一个观察者
+  function observer(target) {
+    const div = document.getElementById("container");
+    const proxy = new Proxy(target, {
+      set(target, prop, value) {
+        // 重写底层实现
+        Reflect.set(target, prop, value);
+        render();
+      },
+      get(target, prop) {
+        return Reflect.get(target, prop);
+      },
+    });
+    render();
+    function render() {
+      let html = "";
+      for (const prop of Object.keys(target)) {
+        html += `
+<p><span>${prop}:</span><span>${target[prop]}</span></p>
+`;
+      }
+      div.innerHTML = html;
+    }
+    return proxy;
+  }
+  const target = {
+    a: 1,
+    b: 2,
+  };
+  const obj = observer(target);
+</script>
+
<div id="container"></div>
+
+<script>
+  //创建一个观察者
+  function observer(target) {
+    const div = document.getElementById("container");
+    const proxy = new Proxy(target, {
+      set(target, prop, value) {
+        // 重写底层实现
+        Reflect.set(target, prop, value);
+        render();
+      },
+      get(target, prop) {
+        return Reflect.get(target, prop);
+      },
+    });
+    render();
+    function render() {
+      let html = "";
+      for (const prop of Object.keys(target)) {
+        html += `
+<p><span>${prop}:</span><span>${target[prop]}</span></p>
+`;
+      }
+      div.innerHTML = html;
+    }
+    return proxy;
+  }
+  const target = {
+    a: 1,
+    b: 2,
+  };
+  const obj = observer(target);
+</script>
+

偷懒的构造函数

恶心的类创建

javascript
class User(){
+  constructor(firstName, lastName, age) {
+    this.firstName = firstName;
+    this.lastName = lastName;
+    this.age = age;
+  }
+}
+
class User(){
+  constructor(firstName, lastName, age) {
+    this.firstName = firstName;
+    this.lastName = lastName;
+    this.age = age;
+  }
+}
+

自动赋值

javascript
class User {}
+function ConstructorProxy(Class, ...propNames) {
+  return new Proxy(Class, {
+    construct(target, argumentsList) {
+      const obj = Reflect.construct(target, argumentsList);
+      propNames.forEach((name, i) => {
+        obj[name] = argumentsList[i];
+      });
+      return obj;
+    },
+  });
+}
+const UserProxy = ConstructorProxy(User, "firstName", "lastName", "age");
+const obj = new UserProxy("付", "志强", 18);
+console.log(obj);
+class Monster {}
+const MonsterProxy = ConstructorProxy(
+  Monster,
+  "attack",
+  "defence",
+  "hp",
+  "rate",
+  "name"
+);
+const m = new MonsterProxy(10, 20, 100, 30, "怪物");
+console.log(m);
+
class User {}
+function ConstructorProxy(Class, ...propNames) {
+  return new Proxy(Class, {
+    construct(target, argumentsList) {
+      const obj = Reflect.construct(target, argumentsList);
+      propNames.forEach((name, i) => {
+        obj[name] = argumentsList[i];
+      });
+      return obj;
+    },
+  });
+}
+const UserProxy = ConstructorProxy(User, "firstName", "lastName", "age");
+const obj = new UserProxy("付", "志强", 18);
+console.log(obj);
+class Monster {}
+const MonsterProxy = ConstructorProxy(
+  Monster,
+  "attack",
+  "defence",
+  "hp",
+  "rate",
+  "name"
+);
+const m = new MonsterProxy(10, 20, 100, 30, "怪物");
+console.log(m);
+

可验证的函数参数

javascript
function sum(a, b) {
+  return a + b;
+}
+
+function validatorFunction(func, ...types) {
+  //并不占据内存空间,只是个代理
+  const proxy = new Proxy(func, {
+    apply(target, thisArgument, argumentsList) {
+      types.forEach((t, i) => {
+        // 进行验证
+        const arg = argumentsList[i];
+        if (typeof arg !== t) {
+          throw new TypeError(
+            `第${i + 1}个参数${argumentsList[i]}不满足类型${t}`
+          );
+        }
+      });
+      return Reflect.apply(target, thisArgument, argumentsList);
+    },
+  });
+  return proxy;
+}
+
+const sumProxy = validatorFunction(sum, "number", "number");
+console.log(sumProxy(1, 2));
+
function sum(a, b) {
+  return a + b;
+}
+
+function validatorFunction(func, ...types) {
+  //并不占据内存空间,只是个代理
+  const proxy = new Proxy(func, {
+    apply(target, thisArgument, argumentsList) {
+      types.forEach((t, i) => {
+        // 进行验证
+        const arg = argumentsList[i];
+        if (typeof arg !== t) {
+          throw new TypeError(
+            `第${i + 1}个参数${argumentsList[i]}不满足类型${t}`
+          );
+        }
+      });
+      return Reflect.apply(target, thisArgument, argumentsList);
+    },
+  });
+  return proxy;
+}
+
+const sumProxy = validatorFunction(sum, "number", "number");
+console.log(sumProxy(1, 2));
+

Proxy:如果没有我

javascript
function sum(a, b) {
+  return a + b;
+}
+
+function validatorFunction(func, ...types) {
+  return function (...argumentsList) {
+    // 新的函数占用内存
+    types.forEach((t, i) => {
+      const arg = argumentsList[i];
+      if (typeof arg !== t) {
+        throw new TypeError(
+          `第${i + 1}个参数${argumentsList[i]}不满足类型${t}`
+        );
+      }
+    });
+    return func(...argumentsList); //运行这个函数
+  };
+  return proxy;
+}
+
+const sumProxy = validatorFunction(sum, "number", "number");
+console.log(sumProxy(1, 2));
+
function sum(a, b) {
+  return a + b;
+}
+
+function validatorFunction(func, ...types) {
+  return function (...argumentsList) {
+    // 新的函数占用内存
+    types.forEach((t, i) => {
+      const arg = argumentsList[i];
+      if (typeof arg !== t) {
+        throw new TypeError(
+          `第${i + 1}个参数${argumentsList[i]}不满足类型${t}`
+        );
+      }
+    });
+    return func(...argumentsList); //运行这个函数
+  };
+  return proxy;
+}
+
+const sumProxy = validatorFunction(sum, "number", "number");
+console.log(sumProxy(1, 2));
+

其他 proxy 资料:

https://cloud.tencent.com/developer/article/1890562

https://juejin.cn/post/6844904101218631694

+ + + + + \ No newline at end of file diff --git "a/js/\345\274\202\346\255\245\345\244\204\347\220\206.html" "b/js/\345\274\202\346\255\245\345\244\204\347\220\206.html" new file mode 100644 index 00000000..1863ccda --- /dev/null +++ "b/js/\345\274\202\346\255\245\345\244\204\347\220\206.html" @@ -0,0 +1,2136 @@ + + + + + + 异步处理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

异步处理

事件循环 eventLoop

JS 运行的环境称之为宿主环境。JS语言不只运行在浏览器

执行栈:call stack(一个数据结构),用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。 JS 引擎永远执行的是执行栈的最顶部。

异步函数:某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。 浏览器宿主环境中包含 5 个线程:

  • JS 引擎:负责执行执行栈的最顶部代码
  • GUI 线程:负责渲染页面
  • 事件监听线程:负责监听各种事件
  • 计时线程:负责计时
  • 网络线程:负责网络通信

当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当 JS 引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行。 JS 引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。

JavaScript 运行机制详解:再谈Event Loop

事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:

  • 宏任务(队列):macroTask,计时器结束的回调、事件回调、http 回调等等绝大部分异步函数进入宏队列
  • 微任务(队列):MutationObserver,Promise 产生的回调进入微队列——vip

MutationObserver 用于监听某个 DOM 对象的变化 当执行栈清空时,JS 引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。

看下面一串代码

html
<script>
+  let count = 1;
+  const ul = document.getElementById("container");
+  document.getElementById("btn").onclick = function A() {
+    var li = document.createElement("li")
+    li.innerText = count++;
+    ul.appendChild(li);
+    console.log("添加了一个li")
+  }
+
+  //监听ul
+  const observer = new MutationObserver(function B() {
+    //当监听的dom元素发生变化时运行的回调函数
+    console.log("ul元素发生了变化")
+  })
+  //监听ul
+  observer.observe(ul, {  
+    attributes: true, //监听属性的变化
+    childList: true, //监听子元素的变化
+    subtree: true //监听子树的变化
+  })
+</script>
+
<script>
+  let count = 1;
+  const ul = document.getElementById("container");
+  document.getElementById("btn").onclick = function A() {
+    var li = document.createElement("li")
+    li.innerText = count++;
+    ul.appendChild(li);
+    console.log("添加了一个li")
+  }
+
+  //监听ul
+  const observer = new MutationObserver(function B() {
+    //当监听的dom元素发生变化时运行的回调函数
+    console.log("ul元素发生了变化")
+  })
+  //监听ul
+  observer.observe(ul, {  
+    attributes: true, //监听属性的变化
+    childList: true, //监听子元素的变化
+    subtree: true //监听子树的变化
+  })
+</script>
+

为什么先执行宏队列了?

点击按钮的时候还没有往ul里面加li,所以微队列也不执行

宏队列演示

html
<ul id="container">
+
+</ul>
+
+<button id="btn">点击</button>
+<script>
+  let count = 1;
+  const ul = document.getElementById("container");
+  document.getElementById("btn").onclick = function A() {
+      setTimeout(function C() {
+          console.log("添加了一个li")
+      }, 0); 
+      var li = document.createElement("li")
+      li.innerText = count++;
+      ul.appendChild(li);
+  }
+
+  //监听ul
+  const observer = new MutationObserver(function B() {
+      //当监听的dom元素发生变化时运行的回调函数
+      console.log("ul元素发生了变化")
+  })
+  //监听ul
+  observer.observe(ul, {	
+      attributes: true, //监听属性的变化
+      childList: true, //监听子元素的变化
+      subtree: true //监听子树的变化
+  })
+  //取消监听
+  // observer.disconnect();
+</script>
+
<ul id="container">
+
+</ul>
+
+<button id="btn">点击</button>
+<script>
+  let count = 1;
+  const ul = document.getElementById("container");
+  document.getElementById("btn").onclick = function A() {
+      setTimeout(function C() {
+          console.log("添加了一个li")
+      }, 0); 
+      var li = document.createElement("li")
+      li.innerText = count++;
+      ul.appendChild(li);
+  }
+
+  //监听ul
+  const observer = new MutationObserver(function B() {
+      //当监听的dom元素发生变化时运行的回调函数
+      console.log("ul元素发生了变化")
+  })
+  //监听ul
+  observer.observe(ul, {	
+      attributes: true, //监听属性的变化
+      childList: true, //监听子元素的变化
+      subtree: true //监听子树的变化
+  })
+  //取消监听
+  // observer.disconnect();
+</script>
+

事件和回调函数缺陷

我们习惯于使用传统的回调或事件处理来解决异步问题 事件:某个对象的属性是一个函数,当发生某一件事时,运行该函数

javascript
dom.onclick = function () {};
+
dom.onclick = function () {};
+

回调:运行某个函数以实现某个功能的时候,传入一个函数作为参数,当发生某件事的时候,会运行该函数。

javascript
dom.addEventListener("click", function () {});
+
dom.addEventListener("click", function () {});
+

本质上,事件和回调并没有本质的区别,只是把函数放置的位置不同而已。一直以来,该模式都运作良好。直到前端工程越来越复杂... 目前,该模式主要面临以下两个问题:

  1. 回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断的嵌套
  2. 异步之间的联系:某个异步操作要等待多个异步操作的结果,对这种联系的处理,会让代码的复杂度剧增
html
<p>
+    <button id="btn1">按钮1:给按钮2注册点击事件</button>
+    <button id="btn2">按钮2:给按钮3注册点击事件</button>
+    <button id="btn3">按钮3:点击后弹出hello</button>
+</p>
+<script>
+    const btn1 = document.getElementById("btn1"),
+          btn2 = document.getElementById("btn2"),
+          btn3 = document.getElementById("btn3");
+    btn1.addEventListener("click", function() {
+        //按钮1的其他事情
+        btn2.addEventListener("click", function() {
+            //按钮2的其他事情
+            btn3.addEventListener("click", function() {
+                alert("hello");
+            })
+        })
+    })
+</script>
+
<p>
+    <button id="btn1">按钮1:给按钮2注册点击事件</button>
+    <button id="btn2">按钮2:给按钮3注册点击事件</button>
+    <button id="btn3">按钮3:点击后弹出hello</button>
+</p>
+<script>
+    const btn1 = document.getElementById("btn1"),
+          btn2 = document.getElementById("btn2"),
+          btn3 = document.getElementById("btn3");
+    btn1.addEventListener("click", function() {
+        //按钮1的其他事情
+        btn2.addEventListener("click", function() {
+            //按钮2的其他事情
+            btn3.addEventListener("click", function() {
+                alert("hello");
+            })
+        })
+    })
+</script>
+

异步处理的通用模型

ES 官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景。 值得注意的是,为了兼容旧系统,ES6 并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的 API,使用该 API,会让异步处理更加的简洁优雅。 理解该 API,最重要的,是理解它的异步模型

  1. ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettledsettled
  • unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
  • settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转

事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。

  1. ES6 将事情划分为三种状态: pending、resolved、rejected
  • pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
  • resolved:已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
  • rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误

既然未决阶段有权力决定事情的走向,因此,未决阶段可以决定事情最终的状态!

我们将 把事情变为 resolved 状态的过程叫做:resolve,推向该状态时,可能会传递一些数据

我们将 把事情变为 rejected 状态的过程叫做:reject,推向该状态时,同样可能会传递一些数据,通常为错误信息

始终记住,无论是阶段,还是状态,是不可逆的!

  1. 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
  • resolved 状态:这是一个正常的已决状态,后续处理表示为 thenable
  • rejected 状态:这是一个非正常的已决状态,后续处理表示为 catchable

后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行

  1. 整件事称之为 Promise

理解上面的概念,对学习 Promise 至关重要!

Promise A+

Promise是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一

这套规范最早诞生于前端社区,规范名称为Promise A+

该规范出现后,立即得到了很多开发者的响应

Promise A+ 规定:

  1. 所有的异步场景,都可以看作是一个异步任务,每个异步任务,在JS中应该表现为一个对象,该对象称之为Promise对象,也叫做任务对象
  2. 每个任务对象,都应该有两个阶段、三个状态

根据常理,它们之间存在以下逻辑:

  • 任务总是从未决阶段变到已决阶段,无法逆行
  • 任务总是从挂起状态变到完成或失败状态,无法逆行
  • 时间不能倒流,历史不可改写,任务一旦完成或失败,状态就固定下来,永远无法改变
  1. 挂起->完成,称之为resolve挂起->失败称之为reject。任务完成时,可能有一个相关数据;任务失败时,可能有一个失败原因。

  1. 可以针对任务进行后续处理,针对完成状态的后续处理称之为onFulfilled,针对失败的后续处理称之为onRejected

ES6提供了一套API,实现了Promise A+规范

Promise的基本使用

javascript
const pro = new Promise((resolve, reject)=>{
+    // 未决阶段的处理
+    // 通过调用resolve函数将Promise推向已决阶段的resolved状态
+    // 通过调用reject函数将Promise推向已决阶段的rejected状态
+    // resolve和reject均可以传递最多一个参数,表示推向状态的数据
+  // 立即执行
+})
+
+pro.then(data=>{
+    //这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
+    //如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
+    //data为状态数据
+}, err=>{
+    //这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
+    //如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
+    //err为状态数据
+})
+
const pro = new Promise((resolve, reject)=>{
+    // 未决阶段的处理
+    // 通过调用resolve函数将Promise推向已决阶段的resolved状态
+    // 通过调用reject函数将Promise推向已决阶段的rejected状态
+    // resolve和reject均可以传递最多一个参数,表示推向状态的数据
+  // 立即执行
+})
+
+pro.then(data=>{
+    //这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
+    //如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
+    //data为状态数据
+}, err=>{
+    //这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
+    //如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
+    //err为状态数据
+})
+

细节

  • 未决阶段的处理函数是同步的,会立即执行
  • thenable和catchable函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列
  • pro.then可以只添加thenable函数,pro.catch可以单独添加catchable函数
  • 在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向rejected,并会被catchable捕获
  • 一旦状态推向了已决阶段,无法再对状态做任何更改
  • Promise并没有消除回调,只是让回调变得可控

创建 Promise

javascript
const pro = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    if (Math.random() < 0.1) {
+      resolve(true);
+    } else {
+      resolve(false);
+    }
+  }, 3000);
+})
+
const pro = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    if (Math.random() < 0.1) {
+      resolve(true);
+    } else {
+      resolve(false);
+    }
+  }, 3000);
+})
+

封装Ajax

javascript

+// 辅助函数,把传进来的对象拼接成url的字符串
+function toData(obj) {
+  if (obj === null) {
+    return obj;
+  }
+  let arr = [];
+  for (let i in obj) {
+    let str = i + "=" + obj[i];
+    arr.push(str);
+  }
+  return arr.join("&");
+}
+// 封装Ajax
+function ajax(obj) {
+  return new Promise((resolve, reject) => {
+    //指定提交方式的默认值
+    obj.type = obj.type || "get";
+    //设置是否异步,默认为true(异步)
+    obj.async = obj.async || true;
+    //设置数据的默认值
+    obj.data = obj.data || null;
+    // 根据不同的浏览器创建XHR对象
+    let xhr = null;
+    if (window.XMLHttpRequest) {
+      // 非IE浏览器
+      xhr = new XMLHttpRequest();
+    } else {
+      // IE浏览器
+      xhr = new ActiveXObject("Microsoft.XMLHTTP");
+    }
+    // 区分get和post,发送HTTP请求
+    if (obj.type === "post") {
+      xhr.open(obj.type, obj.url, obj.async);
+      xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+      let data = toData(obj.data);
+      xhr.send(data);
+    } else {
+      let url = obj.url + "?" + toData(obj.data);
+      xhr.open(obj.type, url, obj.async);
+      xhr.send();
+    }
+    // 接收返回过来的数据
+    xhr.onreadystatechange = function () {
+      if (xhr.readyState === 4) {
+        if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
+          resolve(JSON.parse(xhr.responseText))
+        } else {
+          reject(xhr.status)
+        }
+      }
+    }
+  })
+}
+
+const pro = Promise((resolve, reject) => {
+  ajax({
+    url: "./data/students.json?name=李华",
+    // success: function(data) {
+    // }
+    // 速写
+    success(data) {
+      resolve(data);
+    },
+    error(err) {
+      reject(error);
+    }
+  })
+})
+

+// 辅助函数,把传进来的对象拼接成url的字符串
+function toData(obj) {
+  if (obj === null) {
+    return obj;
+  }
+  let arr = [];
+  for (let i in obj) {
+    let str = i + "=" + obj[i];
+    arr.push(str);
+  }
+  return arr.join("&");
+}
+// 封装Ajax
+function ajax(obj) {
+  return new Promise((resolve, reject) => {
+    //指定提交方式的默认值
+    obj.type = obj.type || "get";
+    //设置是否异步,默认为true(异步)
+    obj.async = obj.async || true;
+    //设置数据的默认值
+    obj.data = obj.data || null;
+    // 根据不同的浏览器创建XHR对象
+    let xhr = null;
+    if (window.XMLHttpRequest) {
+      // 非IE浏览器
+      xhr = new XMLHttpRequest();
+    } else {
+      // IE浏览器
+      xhr = new ActiveXObject("Microsoft.XMLHTTP");
+    }
+    // 区分get和post,发送HTTP请求
+    if (obj.type === "post") {
+      xhr.open(obj.type, obj.url, obj.async);
+      xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+      let data = toData(obj.data);
+      xhr.send(data);
+    } else {
+      let url = obj.url + "?" + toData(obj.data);
+      xhr.open(obj.type, url, obj.async);
+      xhr.send();
+    }
+    // 接收返回过来的数据
+    xhr.onreadystatechange = function () {
+      if (xhr.readyState === 4) {
+        if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
+          resolve(JSON.parse(xhr.responseText))
+        } else {
+          reject(xhr.status)
+        }
+      }
+    }
+  })
+}
+
+const pro = Promise((resolve, reject) => {
+  ajax({
+    url: "./data/students.json?name=李华",
+    // success: function(data) {
+    // }
+    // 速写
+    success(data) {
+      resolve(data);
+    },
+    error(err) {
+      reject(error);
+    }
+  })
+})
+

如果当前的Promise已经是resolved状态,该函数会立即执行

javascript
const pro = new Promise((resolve, reject) => {
+    console.log('未决阶段')
+    resolve(123);
+})
+pro.then(data => {
+    //pro的状态是resloved
+    console.log(data);// 123
+})
+
const pro = new Promise((resolve, reject) => {
+    console.log('未决阶段')
+    resolve(123);
+})
+pro.then(data => {
+    //pro的状态是resloved
+    console.log(data);// 123
+})
+

如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行

javascript
const pro = new Promise((resolve, reject) => {
+    console.log('未决阶段')
+    setTimeout(() => {
+        resolve(123);
+    }, 3000)
+})
+pro.then(data => {
+    //pro的状态是pending
+    console.log(data);
+})
+// 可以注册多个then用来表示已决做什么
+
const pro = new Promise((resolve, reject) => {
+    console.log('未决阶段')
+    setTimeout(() => {
+        resolve(123);
+    }, 3000)
+})
+pro.then(data => {
+    //pro的状态是pending
+    console.log(data);
+})
+// 可以注册多个then用来表示已决做什么
+

catchable

javascript
const pro = new Promise((resolve, reject) => {
+    console.log('未决阶段')
+    setTimeout(() => {
+        if (Math.random() < 0.5) {
+            resolve(123)
+        } else {
+            reject(new Error('sanjs'));
+        }
+    }, 3000)
+})
+pro.then(data => {
+    console.log(data);
+}, err => {
+    console.log(err);
+})
+
const pro = new Promise((resolve, reject) => {
+    console.log('未决阶段')
+    setTimeout(() => {
+        if (Math.random() < 0.5) {
+            resolve(123)
+        } else {
+            reject(new Error('sanjs'));
+        }
+    }, 3000)
+})
+pro.then(data => {
+    console.log(data);
+}, err => {
+    console.log(err);
+})
+

封装初级演示1

javascript
function biaobai(god) {
+    return new Promise((resolve, reject) => {
+        console.log(`向${god}发出了请求`);
+        setTimeout(() => {
+            if (Math.random() < 0.1) {
+                resolve(true)// 一定resolve成功
+            } else {
+                //resolve
+                resolve(false);//  一定resolve成功
+            }
+        }, 3000);
+    })
+}
+//一定成功,失败指的是短信发不出去
+const pro = biaobai("女神1").then(result => {
+    console.log(result);
+})
+
function biaobai(god) {
+    return new Promise((resolve, reject) => {
+        console.log(`向${god}发出了请求`);
+        setTimeout(() => {
+            if (Math.random() < 0.1) {
+                resolve(true)// 一定resolve成功
+            } else {
+                //resolve
+                resolve(false);//  一定resolve成功
+            }
+        }, 3000);
+    })
+}
+//一定成功,失败指的是短信发不出去
+const pro = biaobai("女神1").then(result => {
+    console.log(result);
+})
+

演示2

html
<script>
+    // const pro = new Promise((resolve, reject) => {
+    //     console.log("未决阶段")
+    //     resolve(123);
+    // })
+    // pro.then(data => {
+    //     // pro的状态是resolved
+    //     console.log(data);
+    // })
+
+    const pro = new Promise((resolve, reject) => {
+        console.log("未决阶段")
+        setTimeout(() => {
+            resolve(123);
+        }, 3000);
+    })
+    pro.then(data => {
+        // pro的状态是pending
+        console.log(data);
+    })
+    pro.then(data => {
+        // pro的状态是pending
+        console.log(data);
+    })
+    pro.then(data => {
+        // pro的状态是pending
+        console.log(data);
+    })
+</script>
+
<script>
+    // const pro = new Promise((resolve, reject) => {
+    //     console.log("未决阶段")
+    //     resolve(123);
+    // })
+    // pro.then(data => {
+    //     // pro的状态是resolved
+    //     console.log(data);
+    // })
+
+    const pro = new Promise((resolve, reject) => {
+        console.log("未决阶段")
+        setTimeout(() => {
+            resolve(123);
+        }, 3000);
+    })
+    pro.then(data => {
+        // pro的状态是pending
+        console.log(data);
+    })
+    pro.then(data => {
+        // pro的状态是pending
+        console.log(data);
+    })
+    pro.then(data => {
+        // pro的状态是pending
+        console.log(data);
+    })
+</script>
+

演示4

html
<script>
+    const pro = new Promise((resolve, reject) => {
+        console.log("未决阶段")
+        setTimeout(() => {
+            if (Math.random < 0.5) {
+                resolve(123)
+            } else {
+                reject(new Error("asdfasdf"));
+            }
+        }, 3000);
+    })
+    pro.then(data => {
+        console.log(data);
+    }, err => {
+        console.log(err)
+    })
+</script>
+
<script>
+    const pro = new Promise((resolve, reject) => {
+        console.log("未决阶段")
+        setTimeout(() => {
+            if (Math.random < 0.5) {
+                resolve(123)
+            } else {
+                reject(new Error("asdfasdf"));
+            }
+        }, 3000);
+    })
+    pro.then(data => {
+        console.log(data);
+    }, err => {
+        console.log(err)
+    })
+</script>
+

演示5

html
<script>
+    const pro = new Promise((resolve, reject) => {
+        console.log("a")
+        resolve(1);
+        setTimeout(() => {
+            console.log("b")
+        }, 0);
+    })
+    //pro: resolved
+    pro.then(data => {
+        console.log(data)
+    })
+    pro.catch(err => {
+        console.log(err)
+    })
+    console.log("c")
+</script>
+
<script>
+    const pro = new Promise((resolve, reject) => {
+        console.log("a")
+        resolve(1);
+        setTimeout(() => {
+            console.log("b")
+        }, 0);
+    })
+    //pro: resolved
+    pro.then(data => {
+        console.log(data)
+    })
+    pro.catch(err => {
+        console.log(err)
+    })
+    console.log("c")
+</script>
+

在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向rejected,并会被catchable捕获

html
<script>
+    const pro = new Promise((resolve, reject) => {
+        throw new Error("123"); // 导致pro变成rejected
+    })
+    pro.then(data => {
+        console.log(data)
+    })
+    pro.catch(err => {
+        console.log(err)
+    })
+</script>
+
<script>
+    const pro = new Promise((resolve, reject) => {
+        throw new Error("123"); // 导致pro变成rejected
+    })
+    pro.then(data => {
+        console.log(data)
+    })
+    pro.catch(err => {
+        console.log(err)
+    })
+</script>
+

一旦状态推向了已决阶段,无法再对状态做任何更改。(未捕获的错误)

如果已经捕获了错误

javascript
const pro = new Promise((resolve, reject) => {
+    try {
+        throw new Error("abc");
+    } catch {
+    }
+    resolve(1); //错误被捕获,有效了
+    reject(2); //无效
+    resolve(3); //无效
+    reject(4); //无效
+})
+pro.then(data => {
+    console.log(data)
+})
+pro.catch(err => {
+    console.log(err)
+}
+
const pro = new Promise((resolve, reject) => {
+    try {
+        throw new Error("abc");
+    } catch {
+    }
+    resolve(1); //错误被捕获,有效了
+    reject(2); //无效
+    resolve(3); //无效
+    reject(4); //无效
+})
+pro.then(data => {
+    console.log(data)
+})
+pro.catch(err => {
+    console.log(err)
+}
+

未决阶段的处理函数是同步的,会立即执行thenable和catchable函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列

注意:thenable和catchable函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列

Promise的串联

当后续的Promise需要用到之前的Promise的处理结果时,需要Promise的串联 Promise对象中,无论是then方法还是catch方法,它们都具有返回值,返回的是一个全新的Promise对象,它的状态满足下面的规则:

  1. 如果当前的Promise是未决的,得到的新的Promise是挂起状态
  2. 如果当前的Promise是已决的,会运行响应的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中;如果后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中。
javascript
const pro = ajax({
+  url: "./data/students.json"
+})
+const pro2 = pro.then(resp => {// 这里面要运行完pro2才已决
+  // throw new Error('错误')// reject
+  for (let i = 0; i < resp.length; i++) {
+    if (resp[i].name === "李华") {
+      const cid = resp[i].classId;
+    }
+  }
+})
+console.log(pro2)
+
const pro = ajax({
+  url: "./data/students.json"
+})
+const pro2 = pro.then(resp => {// 这里面要运行完pro2才已决
+  // throw new Error('错误')// reject
+  for (let i = 0; i < resp.length; i++) {
+    if (resp[i].name === "李华") {
+      const cid = resp[i].classId;
+    }
+  }
+})
+console.log(pro2)
+

面试题:后续的Promise一定会等到前面的Promise有了后续处理结果后,才会变成已决状态

javascript
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+console.log(pro1);//fulfilled
+const pro2 = pro1.then(result => result * 2);
+// pro2是一个Promise对象
+console.log(pro2)//pending
+// 因为then是异步的,运行到打印pro2的时候函数还没调用(同步代码)
+
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+console.log(pro1);//fulfilled
+const pro2 = pro1.then(result => result * 2);
+// pro2是一个Promise对象
+console.log(pro2)//pending
+// 因为then是异步的,运行到打印pro2的时候函数还没调用(同步代码)
+

面试题

javascript
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+const pro2 = pro1.then(result => result * 2);//这里变成状态数据:2,pro2变成了已决
+pro2.then(result => console.log(result), err => console.log(err))
+
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+const pro2 = pro1.then(result => result * 2);//这里变成状态数据:2,pro2变成了已决
+pro2.then(result => console.log(result), err => console.log(err))
+

面试题

javascript
const pro1 = new Promise((resolve, reject) => {
+  throw 1;// 推向rejected,导致pro2的err运行
+})
+const pro2 = pro1.then(result => {
+  return result * 2;
+}, err => err * 3);// 3, 此时pro2已决了
+pro2.then(result => console.log(result * 2), err => console.log(err * 3))//这里主要看上述处理有没有错误,pro2没有错误,执行result
+结果6
+
const pro1 = new Promise((resolve, reject) => {
+  throw 1;// 推向rejected,导致pro2的err运行
+})
+const pro2 = pro1.then(result => {
+  return result * 2;
+}, err => err * 3);// 3, 此时pro2已决了
+pro2.then(result => console.log(result * 2), err => console.log(err * 3))//这里主要看上述处理有没有错误,pro2没有错误,执行result
+结果:6
+

变式

javascript
const pro1 = new Promise((resolve, reject) => {
+  throw 1;
+})
+const pro2 = pro1.then(result => {
+  return result * 2;
+}, err => {
+  throw err;
+});
+pro2.then(result => console.log(result * 2), err => console.log(err * 3))//3
+
const pro1 = new Promise((resolve, reject) => {
+  throw 1;
+})
+const pro2 = pro1.then(result => {
+  return result * 2;
+}, err => {
+  throw err;
+});
+pro2.then(result => console.log(result * 2), err => console.log(err * 3))//3
+

思考:then函数返回的Promise对象一开始一定是挂起状态:因为then的后序处理是异步的

javascript
const pro1 = new Promise((resolve, reject) => {
+  throw 1;
+})
+const pro2 = pro1.then(result => {// 这里是第一个调用then的,只看这里的处理函数
+  return result * 2;
+}, err => {
+  return err * 3;//3
+});
+const pro3 = pro1.catch(err => {// 这里得到的是一个新的Promise
+  throw err * 2;//1*2
+})
+pro2.then(result => console.log(result * 2), err => console.log(err * 3)) // 输出6,调用的是result
+pro3.then(result => console.log(result * 2), err => console.log(err * 3)) // 输出6,调用的是err
+
const pro1 = new Promise((resolve, reject) => {
+  throw 1;
+})
+const pro2 = pro1.then(result => {// 这里是第一个调用then的,只看这里的处理函数
+  return result * 2;
+}, err => {
+  return err * 3;//3
+});
+const pro3 = pro1.catch(err => {// 这里得到的是一个新的Promise
+  throw err * 2;//1*2
+})
+pro2.then(result => console.log(result * 2), err => console.log(err * 3)) // 输出6,调用的是result
+pro3.then(result => console.log(result * 2), err => console.log(err * 3)) // 输出6,调用的是err
+

如果前面的Promise的后续处理,返回的是一个Promise,则返回的新的Promise状态和后续处理返回的Promise状态保持一致。

javascript
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+const pro2 = new Promise((resolve, reject) => {
+  resolve(2)
+})
+const pro3 = pro1.then(result => {
+  return pro2;
+}).then(result => {
+  console.log(result)// 2
+})
+
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+const pro2 = new Promise((resolve, reject) => {
+  resolve(2)
+})
+const pro3 = pro1.then(result => {
+  return pro2;
+}).then(result => {
+  console.log(result)// 2
+})
+

变式

javascript
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+const pro2 = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    resolve(2)
+  }, 3000)
+})
+const pro3 = pro1.then(result => {
+  console.log("结果出来了,得到的是一个Promise")//一开始Pro1已决了,所以这个一开始就执行
+  return pro2;// Pro3要跟pro2状态保持一致,pro2还没运行出来,pro3得等着
+}).then(result => {
+  console.log(result)// 2;所以这里要等3s后才输出
+})
+
const pro1 = new Promise((resolve, reject) => {
+  resolve(1);
+})
+const pro2 = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    resolve(2)
+  }, 3000)
+})
+const pro3 = pro1.then(result => {
+  console.log("结果出来了,得到的是一个Promise")//一开始Pro1已决了,所以这个一开始就执行
+  return pro2;// Pro3要跟pro2状态保持一致,pro2还没运行出来,pro3得等着
+}).then(result => {
+  console.log(result)// 2;所以这里要等3s后才输出
+})
+

简化为链式编程:便于阅读

javascript
pro1.then(result => {
+  console.log("结果出来了,得到的是一个Promise")
+  return pro2;
+}).then(result => {
+  console.log(result)// 这只是打印结果,对后续有影响的是函数的返回值
+  // 不写,默认return undefined
+}).then(result => {
+  console.log(result)// undefined
+})
+
pro1.then(result => {
+  console.log("结果出来了,得到的是一个Promise")
+  return pro2;
+}).then(result => {
+  console.log(result)// 这只是打印结果,对后续有影响的是函数的返回值
+  // 不写,默认return undefined
+}).then(result => {
+  console.log(result)// undefined
+})
+

解决实战问题

javascript
//获取李华所在班级的老师的信息
+//1. 获取李华的班级id   Promise
+//2. 根据班级id获取李华所在班级的老师id   Promise
+//3. 根据老师的id查询老师信息   Promise
+const pro = ajax({
+  url: "./data/students.json"
+})
+pro.then(resp => {
+  for (let i = 0; i < resp.length; i++) {
+    if (resp[i].name === "李华") {
+      return resp[i].classId; //班级id
+    }
+  }
+}).then(cid => {
+  return ajax({
+    url: "./data/classes.json?cid=" + cid
+  }).then(cls => {
+    for (let i = 0; i < cls.length; i++) {
+      if (cls[i].id === cid) {
+        return cls[i].teacherId;
+      }
+    }
+  })
+}).then(tid => {
+  return ajax({
+    url: "./data/teachers.json"
+  }).then(ts => {
+    for (let i = 0; i < ts.length; i++) {
+      if (ts[i].id === tid) {
+        return ts[i];
+      }
+    }
+  })
+}).then(teacher => {
+  console.log(teacher);
+})
+
//获取李华所在班级的老师的信息
+//1. 获取李华的班级id   Promise
+//2. 根据班级id获取李华所在班级的老师id   Promise
+//3. 根据老师的id查询老师信息   Promise
+const pro = ajax({
+  url: "./data/students.json"
+})
+pro.then(resp => {
+  for (let i = 0; i < resp.length; i++) {
+    if (resp[i].name === "李华") {
+      return resp[i].classId; //班级id
+    }
+  }
+}).then(cid => {
+  return ajax({
+    url: "./data/classes.json?cid=" + cid
+  }).then(cls => {
+    for (let i = 0; i < cls.length; i++) {
+      if (cls[i].id === cid) {
+        return cls[i].teacherId;
+      }
+    }
+  })
+}).then(tid => {
+  return ajax({
+    url: "./data/teachers.json"
+  }).then(ts => {
+    for (let i = 0; i < ts.length; i++) {
+      if (ts[i].id === tid) {
+        return ts[i];
+      }
+    }
+  })
+}).then(teacher => {
+  console.log(teacher);
+})
+

实战案例

javascript
function biaobai(god) {
+  return new Promise(resolve => {
+    console.log(`向${god}发出了请求`);
+    setTimeout(() => {
+      if (Math.random() < 0.3) {
+        resolve(true)
+      } else {
+        resolve(false);
+      }
+    }, 500);
+  })
+}
+biaobai("女神1").then(resp => {
+  if (resp) {
+    console.log("女神1同意了")
+    return;
+  } else {
+    return biaobai("女神2");
+  }
+}).then(resp => {
+  if (resp === undefined) {
+    return;
+  } else if (resp) {
+    console.log("女神2同意了")
+    return;
+  } else {
+    return biaobai("女神3");
+  }
+}).then(resp => {
+  if (resp === undefined) {
+    return;
+  } else if (resp) {
+    console.log("女神3同意了")
+  } else {
+    console.log("都被拒绝了!");
+  }
+})
+// 切记:上一个处理的结果就是下一个处理的状态数据
+
function biaobai(god) {
+  return new Promise(resolve => {
+    console.log(`向${god}发出了请求`);
+    setTimeout(() => {
+      if (Math.random() < 0.3) {
+        resolve(true)
+      } else {
+        resolve(false);
+      }
+    }, 500);
+  })
+}
+biaobai("女神1").then(resp => {
+  if (resp) {
+    console.log("女神1同意了")
+    return;
+  } else {
+    return biaobai("女神2");
+  }
+}).then(resp => {
+  if (resp === undefined) {
+    return;
+  } else if (resp) {
+    console.log("女神2同意了")
+    return;
+  } else {
+    return biaobai("女神3");
+  }
+}).then(resp => {
+  if (resp === undefined) {
+    return;
+  } else if (resp) {
+    console.log("女神3同意了")
+  } else {
+    console.log("都被拒绝了!");
+  }
+})
+// 切记:上一个处理的结果就是下一个处理的状态数据
+

优化:增加了初始化

javascript
const gods = ["女神1", "女神2", "女神3", "女神4", "女神5"];
+let pro;
+for (let i = 0; i < gods.length; i++) {
+  if (i === 0) {
+    pro = biaobai(gods[i]);
+  }
+  pro = pro.then(resp => {
+    if (resp === undefined) {
+      return;
+    } else if (resp) {
+      console.log(`${gods[i]}同意了`)
+      return;
+    } else {
+      console.log(`${gods[i]}拒绝了`)
+      if (i < gods.length - 1) {
+        return biaobai(gods[i + 1]);
+      }
+    }
+  })
+}
+
const gods = ["女神1", "女神2", "女神3", "女神4", "女神5"];
+let pro;
+for (let i = 0; i < gods.length; i++) {
+  if (i === 0) {
+    pro = biaobai(gods[i]);
+  }
+  pro = pro.then(resp => {
+    if (resp === undefined) {
+      return;
+    } else if (resp) {
+      console.log(`${gods[i]}同意了`)
+      return;
+    } else {
+      console.log(`${gods[i]}拒绝了`)
+      if (i < gods.length - 1) {
+        return biaobai(gods[i + 1]);
+      }
+    }
+  })
+}
+

Promise的其他API

原型成员 (实例成员)

  • then:注册一个后续处理函数,当Promise为resolved状态时运行该函数
  • catch:注册一个后续处理函数,当Promise为rejected状态时运行该函数
  • finally:[ES2018]注册一个后续处理函数(无参),当Promise为已决时运行该函数

一个Promise不可能then,catch都执行

javascript
const pro = new Promise((resolve, reject) => {
+  reject(1);
+})
+pro.finally(() => console.log("finally1"))
+pro.finally(() => console.log("finally2"))
+pro.then(resp => console.log("then1", resp * 1));
+pro.then(resp => console.log("then2", resp * 2));
+pro.catch(resp => console.log("catch1", resp * 1));
+pro.catch(resp => console.log("catch2", resp * 2));
+
const pro = new Promise((resolve, reject) => {
+  reject(1);
+})
+pro.finally(() => console.log("finally1"))
+pro.finally(() => console.log("finally2"))
+pro.then(resp => console.log("then1", resp * 1));
+pro.then(resp => console.log("then2", resp * 2));
+pro.catch(resp => console.log("catch1", resp * 1));
+pro.catch(resp => console.log("catch2", resp * 2));
+

构造函数成员 (静态成员)

  • resolve(数据):该方法返回一个resolved状态的Promise,传递的数据作为状态数据
javascript
const pro = new Promise((resolve, reject) => {
+  resolve(1);
+})
+// 等效于:
+const pro = Promise.resolve(1);
+
const pro = new Promise((resolve, reject) => {
+  resolve(1);
+})
+// 等效于:
+const pro = Promise.resolve(1);
+

特殊情况:如果传递的数据是Promise,则直接返回传递的Promise对象

javascript
const p = new Promise((resolve, reject) => {
+  resolve(3);
+})
+// const pro = Promise.resolve(p);
+//等效于
+const pro = p;
+console.log(pro === p)//true
+
const p = new Promise((resolve, reject) => {
+  resolve(3);
+})
+// const pro = Promise.resolve(p);
+//等效于
+const pro = p;
+console.log(pro === p)//true
+
  • reject(数据):该方法返回一个rejected状态的Promise,传递的数据作为状态数据
javascript
const pro = new Promise((resolve, reject) => {
+  reject(1);
+})
+// 等效于:
+const pro = Promise.reject(1);
+
const pro = new Promise((resolve, reject) => {
+  reject(1);
+})
+// 等效于:
+const pro = Promise.reject(1);
+
  • all(iterable):这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。
javascript
function getRandom(min, max) {
+  return Math.floor(Math.random() * (max - min)) + min;
+}
+const proms = [];
+for (let i = 0; i < 10; i++) {
+  proms.push(new Promise((resolve, reject) => {
+    setTimeout(() => {
+      if (Math.random() < 0.5) {
+        console.log(i, "完成");
+        resolve(i);
+      } else {
+        console.log(i, "失败")
+        reject(i);
+      }
+    }, getRandom(1000, 5000));
+  }))
+}
+//等到所有的promise变成resolved状态后输出: 全部完成
+const pro = Promise.all(proms)// 把proms数组传进去,返回新的promise对象,必须等到所有promise都变成resolve后才变成resolve
+pro.then(datas => {
+  console.log("全部完成", datas);
+});
+pro.catch(err => {
+  console.log("有失败的", err);
+})
+console.log(proms);// 为什么这里直接输出promise数组?因为这是同步代码,同步创建的Promise,只是promise状态需要等待
+
function getRandom(min, max) {
+  return Math.floor(Math.random() * (max - min)) + min;
+}
+const proms = [];
+for (let i = 0; i < 10; i++) {
+  proms.push(new Promise((resolve, reject) => {
+    setTimeout(() => {
+      if (Math.random() < 0.5) {
+        console.log(i, "完成");
+        resolve(i);
+      } else {
+        console.log(i, "失败")
+        reject(i);
+      }
+    }, getRandom(1000, 5000));
+  }))
+}
+//等到所有的promise变成resolved状态后输出: 全部完成
+const pro = Promise.all(proms)// 把proms数组传进去,返回新的promise对象,必须等到所有promise都变成resolve后才变成resolve
+pro.then(datas => {
+  console.log("全部完成", datas);
+});
+pro.catch(err => {
+  console.log("有失败的", err);
+})
+console.log(proms);// 为什么这里直接输出promise数组?因为这是同步代码,同步创建的Promise,只是promise状态需要等待
+
  • race(iterable):当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象
javascript
function getRandom(min, max) {
+  return Math.floor(Math.random() * (max - min)) + min;
+}
+const proms = [];
+for (let i = 0; i < 10; i++) {
+  proms.push(new Promise((resolve, reject) => {
+    setTimeout(() => {
+      if (Math.random() < 0.5) {
+        console.log(i, "完成");
+        resolve(i);
+      } else {
+        console.log(i, "失败")
+        reject(i);
+      }
+    }, getRandom(1000, 5000));
+  }))
+}
+//等到所有的promise变成resolved状态后输出: 全部完成
+// const pro = Promise.all(proms)
+//  pro.then(data => {
+//    console.log("全部完成了", data);
+//  })
+//  pro.catch(err => {
+//    console.log("有人失败了", err);
+//  })
+const pro = Promise.race(proms)
+pro.then(data => {
+  console.log("有人完成了", data);
+})
+pro.catch(err => {
+  console.log("有人失败了", err);
+})
+console.log(proms);
+
function getRandom(min, max) {
+  return Math.floor(Math.random() * (max - min)) + min;
+}
+const proms = [];
+for (let i = 0; i < 10; i++) {
+  proms.push(new Promise((resolve, reject) => {
+    setTimeout(() => {
+      if (Math.random() < 0.5) {
+        console.log(i, "完成");
+        resolve(i);
+      } else {
+        console.log(i, "失败")
+        reject(i);
+      }
+    }, getRandom(1000, 5000));
+  }))
+}
+//等到所有的promise变成resolved状态后输出: 全部完成
+// const pro = Promise.all(proms)
+//  pro.then(data => {
+//    console.log("全部完成了", data);
+//  })
+//  pro.catch(err => {
+//    console.log("有人失败了", err);
+//  })
+const pro = Promise.race(proms)
+pro.then(data => {
+  console.log("有人完成了", data);
+})
+pro.catch(err => {
+  console.log("有人失败了", err);
+})
+console.log(proms);
+

应用

javascript
function biaobai(god) {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      if (Math.random() < 0.05) {
+        console.log(god, "同意")
+        resolve(true);
+      } else {
+        console.log(god, "拒绝")
+        resolve(false);
+      }
+    }, Math.floor(Math.random() * (3000 - 1000) + 1000));
+  })
+}
+const proms = [];
+let hasAgree = false; // 是否有女神同意
+for (let i = 1; i <= 20; i++) {
+  const pro = biaobai(`女神${i}`).then(resp => {
+    if (resp) {
+      if (hasAgree) {
+        console.log("发错了短信,很机智的拒绝了")
+      } else {
+        hasAgree = true;
+        console.log("很开心,终于成功了!");
+      }
+    }
+    return resp;
+  })
+  proms.push(pro);
+}
+Promise.all(proms).then(results => {
+  console.log("日志记录", results);
+})
+
function biaobai(god) {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      if (Math.random() < 0.05) {
+        console.log(god, "同意")
+        resolve(true);
+      } else {
+        console.log(god, "拒绝")
+        resolve(false);
+      }
+    }, Math.floor(Math.random() * (3000 - 1000) + 1000));
+  })
+}
+const proms = [];
+let hasAgree = false; // 是否有女神同意
+for (let i = 1; i <= 20; i++) {
+  const pro = biaobai(`女神${i}`).then(resp => {
+    if (resp) {
+      if (hasAgree) {
+        console.log("发错了短信,很机智的拒绝了")
+      } else {
+        hasAgree = true;
+        console.log("很开心,终于成功了!");
+      }
+    }
+    return resp;
+  })
+  proms.push(pro);
+}
+Promise.all(proms).then(results => {
+  console.log("日志记录", results);
+})
+
  • Promise.allSettled
javascript
Promise.allSettled([
+  Promise.resolve(1),
+  Promise.resolve(2),
+  Promise.reject('error')
+]).then(result=>{
+  /* 
+  result: [
+    {status: "fulfilled", value: 1},
+    {status: "fulfilled", value: 2},
+    {status: "rejected", reason: "error"}
+  ]
+	*/
+})
+
Promise.allSettled([
+  Promise.resolve(1),
+  Promise.resolve(2),
+  Promise.reject('error')
+]).then(result=>{
+  /* 
+  result: [
+    {status: "fulfilled", value: 1},
+    {status: "fulfilled", value: 2},
+    {status: "rejected", reason: "error"}
+  ]
+	*/
+})
+

新规范方法Promise.allSettled等待全部已决,不存在失败

javascript
Promise.allSettled(proms)
+  .then((result) => {
+  console.log('Promise.all resolved', result)
+})
+  .catch((reason) => {
+  console.log("Promise.all rejected", reason);
+});
+
Promise.allSettled(proms)
+  .then((result) => {
+  console.log('Promise.all resolved', result)
+})
+  .catch((reason) => {
+  console.log("Promise.all rejected", reason);
+});
+

应用

javascript
var proms = [// Promise数组
+  getTestPromise(true, 1),
+  getTestPromise(false, new Error("failed")),
+  getTestPromise(true, 3),
+  getTestPromise(false, new Error("failed")),
+];
+Promise.allSettled(proms)
+  .then((result) => {
+  var sum = result.reduce((a, b) => a + (b.value ?? 0), 0);// 这个0表示第一次的值
+                          console.log(result)
+  console.log(sum)
+})
+
var proms = [// Promise数组
+  getTestPromise(true, 1),
+  getTestPromise(false, new Error("failed")),
+  getTestPromise(true, 3),
+  getTestPromise(false, new Error("failed")),
+];
+Promise.allSettled(proms)
+  .then((result) => {
+  var sum = result.reduce((a, b) => a + (b.value ?? 0), 0);// 这个0表示第一次的值
+                          console.log(result)
+  console.log(sum)
+})
+
  • Promise.any

ES2021 将引入 Promise.any() 方法,一旦该方法从 promise 列表或数组中命中首个 resolve(成功) 的 promise ,就会短路并返回一个值。如果所有 promise 都被 reject ,该方法则将抛出一个聚合的错误信息 (在 Example 1b 里有所展示)。 其区别于 Promise.race() 之处在于,后者在某个 promise 率先 resolve 或 reject 后都会短路。 示例

javascript
var proms = [
+  getTestPromise(true, 1),
+  getTestPromise(false, new Error('failed')),
+  getTestPromise(true, 3),
+  getTestPromise(false, new Error('failed')),
+];
+Promise.any(proms)
+  .then(datas => {
+  console.log('Promise.all resolved', datas)
+})
+  .catch(reason => {
+  console.log("Promise.all rejected", reason);
+});
+
var proms = [
+  getTestPromise(true, 1),
+  getTestPromise(false, new Error('failed')),
+  getTestPromise(true, 3),
+  getTestPromise(false, new Error('failed')),
+];
+Promise.any(proms)
+  .then(datas => {
+  console.log('Promise.all resolved', datas)
+})
+  .catch(reason => {
+  console.log("Promise.all rejected", reason);
+});
+

一个成功,就返回成功的那个data 全部失败,就会返回一个对象,对象里面有一个数组,记录错误

async和await

async 和 await 是 ES2016 新增两个关键字,它们借鉴了 ES2015 中生成器在实际开发中的应用,目的是简化 Promise api 的使用,并非是替代 Promise。

async

目的是简化在函数的返回值中对Promise的创建 async 用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置,被修饰函数的返回结果一定是 Promise 对象。 存在setTimeout就不能用了

javascript
async function test(){
+    console.log(1);
+    return 2;// 完成时候的数据
+}
+//等效于
+function test(){
+    return new Promise((resolve, reject)=>{
+        console.log(1);
+        resolve(2);
+    })
+}
+
async function test(){
+    console.log(1);
+    return 2;// 完成时候的数据
+}
+//等效于
+function test(){
+    return new Promise((resolve, reject)=>{
+        console.log(1);
+        resolve(2);
+    })
+}
+

如果里面返回Promise就会返回这个Promise对象

javascript
async function test() {
+  return new Promise(resolve => {
+    resolve(1); 
+  })
+}
+
async function test() {
+  return new Promise(resolve => {
+    resolve(1); 
+  })
+}
+

await

await关键字必须出现在async函数中!!!! await用在某个表达式之前,如果表达式是一个Promise,则得到的是thenable中的状态数据。

javascript
async function test1(){
+    console.log(1);
+    return 2;
+}
+async function test2(){
+    const result = await test1();
+    console.log(result);
+}
+test2();
+
async function test1(){
+    console.log(1);
+    return 2;
+}
+async function test2(){
+    const result = await test1();
+    console.log(result);
+}
+test2();
+

等效于

javascript
function test1(){
+    return new Promise((resolve, reject)=>{
+        console.log(1);
+        resolve(2);
+    })
+}
+function test2(){
+    return new Promise((resolve, reject)=>{
+        test1().then(data => {
+            const result = data;
+            console.log(result);
+            resolve();
+        })
+    })
+}
+test2();
+
function test1(){
+    return new Promise((resolve, reject)=>{
+        console.log(1);
+        resolve(2);
+    })
+}
+function test2(){
+    return new Promise((resolve, reject)=>{
+        test1().then(data => {
+            const result = data;
+            console.log(result);
+            resolve();
+        })
+    })
+}
+test2();
+

如果await的表达式不是Promise,则会将其使用Promise.resolve包装后按照规则运行

javascript
async function test() {
+  const result = await 1;// 等同于 await Promise.resolve(1)
+  `await`也可以等待其他数据
+  console.log(result)
+}
+//等价于
+function test() {
+  return new Promise((resolve, reject) => {
+    Promise.resolve(1).then(data => {
+      const result = data;
+      console.log(result);
+      resolve();
+    })
+  })
+}
+test();
+console.log(123);
+// 先123 1 返回的是promise 不会阻塞
+
async function test() {
+  const result = await 1;// 等同于 await Promise.resolve(1)
+  `await`也可以等待其他数据
+  console.log(result)
+}
+//等价于
+function test() {
+  return new Promise((resolve, reject) => {
+    Promise.resolve(1).then(data => {
+      const result = data;
+      console.log(result);
+      resolve();
+    })
+  })
+}
+test();
+console.log(123);
+// 先123 1 返回的是promise 不会阻塞
+

演示4

javascript
async function getPromise() {
+  if (Math.random() < 0.5) {
+    return 1;
+  } else {
+    throw 2;
+  }
+}
+
+async function test() {
+  try {
+    const result = await getPromise();
+    console.log("正常状态", result)
+  } catch (err) {
+    console.log("错误状态", err);
+  }
+}
+
+test();
+
async function getPromise() {
+  if (Math.random() < 0.5) {
+    return 1;
+  } else {
+    throw 2;
+  }
+}
+
+async function test() {
+  try {
+    const result = await getPromise();
+    console.log("正常状态", result)
+  } catch (err) {
+    console.log("错误状态", err);
+  }
+}
+
+test();
+

改造计时器函数

javascript
function delay(duration) {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      resolve();
+    }, duration);
+  })
+}
+
+async function biaobai(god) {
+  console.log(`向${god}发出了表白短信`);
+  await delay(500);
+  return Math.random() < 0.3;
+}
+
function delay(duration) {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      resolve();
+    }, duration);
+  })
+}
+
+async function biaobai(god) {
+  console.log(`向${god}发出了表白短信`);
+  await delay(500);
+  return Math.random() < 0.3;
+}
+

再谈异步

单线程的主要优势是不需要考虑线程调度,降低了程序的复杂性

但在单线程中如果要处理需要等待的任务时,就必须要考虑阻塞的问题。

考虑下面的伪代码:

javascript
var dom = document.getElementById("name"); // 获取某个dom元素
+var name = syncConnect("http://server/getname"); // 以同步的方式向服务器获取名字
+dom.innerHTML = name;
+otherTask(); // 其他无关任务
+
var dom = document.getElementById("name"); // 获取某个dom元素
+var name = syncConnect("http://server/getname"); // 以同步的方式向服务器获取名字
+dom.innerHTML = name;
+otherTask(); // 其他无关任务
+

因此,JS引入异步来处理该问题

javascript
var dom = document.getElementById("name"); // 获取某个dom元素
+asyncConnect("http://server/getname", function callback(result){ //以异步的方式向服务器获取名字
+  dom.innerHTML = result;
+}); 
+otherTask(); // 其他无关任务
+
var dom = document.getElementById("name"); // 获取某个dom元素
+asyncConnect("http://server/getname", function callback(result){ //以异步的方式向服务器获取名字
+  dom.innerHTML = result;
+}); 
+otherTask(); // 其他无关任务
+

执行栈

要想执行必须有执行上下文 JS执行引擎只会执行栈顶端的东西

javascript
function A() {
+  console.log("A");// 函数调用,新建log上下文,入栈,执行完出栈
+  B();// 建立B的上下文
+}
+function B() {
+  console.log("B");// log上下文
+}
+A();// 创建A的上下文,入栈。JS执行引擎只会执行栈顶端的东西,所以不执行下一句,而是执行A
+console.log("global");
+// 答案:A B global
+
function A() {
+  console.log("A");// 函数调用,新建log上下文,入栈,执行完出栈
+  B();// 建立B的上下文
+}
+function B() {
+  console.log("B");// log上下文
+}
+A();// 创建A的上下文,入栈。JS执行引擎只会执行栈顶端的东西,所以不执行下一句,而是执行A
+console.log("global");
+// 答案:A B global
+

演练场

怎样理解JS的异步?

答案:事件发生、等待网络通信完成、等待计时结束等等。如果在执行线程上去等待,就浪费线程的宝贵执行时间,阻塞后续操作。更可怕的是,由于浏览器GUI线程和JS执行线程是互斥的,这就导致浏览器界面会因为JS的等待处于卡死状态。因此,JS通过异步来解决这个问题,当需要等待的时候,通知宿主的其他线程去做处理,执行线程则继续后续执行。当其他线程完成处理后,会发出通知,此时执行线程转而去执行事先定义好的回调函数即可。异步的方式充分了解放了执行线程,让执行线程可以毫无阻塞的运行,也就避免了浏览器宿主因为等待操作完成出现的卡死现象。

考点:GUI线程怎么渲染的? JS社区提出了Promise A+规范,希望把异步规范化,并消除回调地狱 再后来,ES6 官方标准中提出了 Promise API 来处理异步,它满足 Promise A+ 规范 由于异步处理变得标准了,就给ES官方提供了进一步改进的空间,于是在ES7中出现了新的语法async await,它更加完美的解决了异步处理问题

高仿setTimeout

javascript
function delay(duration) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, duration)
+  });
+}
+async function test() {
+  console.log(1);
+  await delay(1000);
+  console.log(2);
+  await delay(1000);
+  console.log(3)
+}
+test()
+
function delay(duration) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, duration)
+  });
+}
+async function test() {
+  console.log(1);
+  await delay(1000);
+  console.log(2);
+  await delay(1000);
+  console.log(3)
+}
+test()
+

高仿setInterval

javascript
function delay(duration) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, duration)
+  });
+}
+async function test() {
+  var count = 0;
+  while (true) {
+    await delay(1000);
+    console.log(count++);
+  }
+}
+test()
+
function delay(duration) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, duration)
+  });
+}
+async function test() {
+  var count = 0;
+  while (true) {
+    await delay(1000);
+    console.log(count++);
+  }
+}
+test()
+

如何理解JS的单线程?

我们之所以称JS为单线程的语言,是因为它的执行引擎只有一个线程,并且不会在执行期间开启新的线程。而并非浏览器是单线程的。

单线程的应用程序具有以下的特点:

  • 易于学习和理解:所有代码都是按照顺序从上到下执行的
  • 易于掌控程序:由于代码都按照顺序执行,不会出现中断,也没有共享资源的争夺问题,极大的降低了开发难度。
  • 更加合理的利用计算机资源:创建新的线程和销毁线程都会耗费额外的CPU和内存资源,没有良好的线程设计,将导致程序运行效率低下。而单线程的应用不受此影响

说一下 JS 异步解决方案发展历程

  • 回调函数(callback)

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队,等着,会拖延整个程序的执行。)

缺点:回调地狱,不能用try catch捕获错误,不能return

  • Promise

优点:解决了回调地狱的问题

缺点:无法取消Promise,错误需要通过回调函数来捕获

  • Generator

特点:可以控制函数的执行,可以配合co函数库使用

  • Async/await

优点:代码清晰,不用像Promise写一大堆then链,处理了回调地狱的问题

async较Generator的优势:

✅ 内置执行器。Generator函数的执行必须依靠执行器,而 Aysnc 函数自带执行器,调用方式跟普通函数的调用一样

✅ 更好的语义。async和await相较于*和yield 更加语义化

✅ 更广的适用性。yield命令后面只能是Thunk函数或Promise对象,async函数的await后面可以是Promise也可以是原始类型的值

✅ 返回值是Promise。async函数返回的是Promise对象,比Generator函数返回的Iterator对象方便,可以直接使用then()方法进行调用

缺点: await将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用await会导致性能上的降低。有传染性

Promise缺点

答案:无法取消

axios请求也是Promise,怎么取消请求的cancalToken

js
const a = await 1; // 有await和没有有啥不一样:await要等待状态。一定使得a=?异步执行
+console.log(999)
+// promise的await使代码具有传染性,不管await是什么,都会变成异步,好不好?
+// 关于API设计,想时异步时同步(stateState)还是永远异步(await)还是node的可以同步可以异步?
+
+
const a = await 1; // 有await和没有有啥不一样:await要等待状态。一定使得a=?异步执行
+console.log(999)
+// promise的await使代码具有传染性,不管await是什么,都会变成异步,好不好?
+// 关于API设计,想时异步时同步(stateState)还是永远异步(await)还是node的可以同步可以异步?
+
+

await后面接什么?

答案:Promise对象 await

catch,能不能模拟then效果

代码输出结果

下面的代码输出结果是 :

javascript
setTimeout(() => {
+  console.log(1)
+}, 0);
+
+var pro = new Promise(resolve => {
+  console.log(2);
+  resolve(3);
+  console.log(4)
+  reject(5);
+  console.log(6)
+})
+
+pro.then(data => {
+  console.log(data)
+}, err => {
+  console.log(err)
+})
+console.log(7)
+// 24731
+
setTimeout(() => {
+  console.log(1)
+}, 0);
+
+var pro = new Promise(resolve => {
+  console.log(2);
+  resolve(3);
+  console.log(4)
+  reject(5);
+  console.log(6)
+})
+
+pro.then(data => {
+  console.log(data)
+}, err => {
+  console.log(err)
+})
+console.log(7)
+// 24731
+

下面的代码输出结果是 :

javascript
const promise1 = new Promise((resolve, reject) => {
+	setTimeout(() => {
+    resolve()
+  }, 1000)
+})
+const promise2 = promise1.then(() => {//等着1
+  throw new Error("error");
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) 
+  console.log('promise2', promise2) 
+}, 2000)
+
const promise1 = new Promise((resolve, reject) => {
+	setTimeout(() => {
+    resolve()
+  }, 1000)
+})
+const promise2 = promise1.then(() => {//等着1
+  throw new Error("error");
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) 
+  console.log('promise2', promise2) 
+}, 2000)
+

变式

javascript
const promise1 = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    resolve()
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {//2取决于1的话,看处理没处理这个状态,处理了,看他处理过程,没有处理就漏下来
+  throw new Error("error");//此处成功了没有处理,状态和1一样。这里只处理了错误
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) 
+  console.log('promise2', promise2) //fulfilled
+}, 2000)
+
const promise1 = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    resolve()
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {//2取决于1的话,看处理没处理这个状态,处理了,看他处理过程,没有处理就漏下来
+  throw new Error("error");//此处成功了没有处理,状态和1一样。这里只处理了错误
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) 
+  console.log('promise2', promise2) //fulfilled
+}, 2000)
+

变式

javascript
const promise1 = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    reject();//promise是rejected
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {//处理了错误,处理的过程没报错
+	return 2;
+}).then(()=>{
+	//此处要执行
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) // rejected
+  console.log('promise2', promise2) // fulfiled
+}, 2000)
+
const promise1 = new Promise((resolve, reject) => {
+  setTimeout(() => {
+    reject();//promise是rejected
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {//处理了错误,处理的过程没报错
+	return 2;
+}).then(()=>{
+	//此处要执行
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) // rejected
+  console.log('promise2', promise2) // fulfiled
+}, 2000)
+

变式

javascript
const promise1 = new Promise((resolve, reject) => {
+  // 这里要是报错,发生在同步代码里面,返回rejected。
+  setTimeout(() => {
+    throw 1;//但是此处跑错是在异步代码里面。执行的此处的时候,已经没有proimise环境了
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {
+	return 2;
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) // pending
+  console.log('promise2', promise2) // pending
+}, 2000)
+
const promise1 = new Promise((resolve, reject) => {
+  // 这里要是报错,发生在同步代码里面,返回rejected。
+  setTimeout(() => {
+    throw 1;//但是此处跑错是在异步代码里面。执行的此处的时候,已经没有proimise环境了
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {
+	return 2;
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) // pending
+  console.log('promise2', promise2) // pending
+}, 2000)
+

下面代码的运行结果是

javascript
const promise = new Promise((resolve, reject) => {
+  resolve('success1')
+  reject('error')
+  resolve('success2')
+})
+
+promise
+  .then((res) => {
+    console.log('then: ', res) 
+  })
+  .catch((err) => {
+    console.log('catch: ', err)
+  })
+
const promise = new Promise((resolve, reject) => {
+  resolve('success1')
+  reject('error')
+  resolve('success2')
+})
+
+promise
+  .then((res) => {
+    console.log('then: ', res) 
+  })
+  .catch((err) => {
+    console.log('catch: ', err)
+  })
+

下面的代码输出结果是多少

javascript
Promise.resolve(2)
+  .then((res) => {
+    console.log(res)
+    return 2
+  })//返回一个新Promsie,状态为成功
+  .catch((err) => {
+    return 3
+  })
+  .then((res) => {
+    console.log(res) //2
+  })
+
Promise.resolve(2)
+  .then((res) => {
+    console.log(res)
+    return 2
+  })//返回一个新Promsie,状态为成功
+  .catch((err) => {
+    return 3
+  })
+  .then((res) => {
+    console.log(res) //2
+  })
+

下面的代码输出结果是多少

javascript
Promise.resolve()
+  .then(() => {
+    return new Error('error!!!')//这里是return 不是throw。没报错,只是返回了一个错误对象
+  })
+  .then((res) => {
+    console.log('then: ', res)
+  })
+  .catch((err) => {
+    console.log('catch: ', err)
+  })
+
Promise.resolve()
+  .then(() => {
+    return new Error('error!!!')//这里是return 不是throw。没报错,只是返回了一个错误对象
+  })
+  .then((res) => {
+    console.log('then: ', res)
+  })
+  .catch((err) => {
+    console.log('catch: ', err)
+  })
+

下面的代码输出结果是多少

javascript
Promise.resolve(1)	// promise  fullfilled   1
+  .then(2)//如果then里传递的不是函数,就当没执行就行
+  .then(Promise.resolve(3))//传递的是对象,then必须传函数,也是无效
+  .then(console.log)//函数  console.log(1)
+
Promise.resolve(1)	// promise  fullfilled   1
+  .then(2)//如果then里传递的不是函数,就当没执行就行
+  .then(Promise.resolve(3))//传递的是对象,then必须传函数,也是无效
+  .then(console.log)//函数  console.log(1)
+

下面代码的输出结果是什么

javascript
const promise = new Promise((resolve, reject) => {
+    console.log(1); 
+    resolve(); 
+    console.log(2);
+})
+
+promise.then(() => {
+    console.log(3);
+})
+
+console.log(4);
+
const promise = new Promise((resolve, reject) => {
+    console.log(1); 
+    resolve(); 
+    console.log(2);
+})
+
+promise.then(() => {
+    console.log(3);
+})
+
+console.log(4);
+

下面代码的输出结果是什么

javascript
const promise = new Promise((resolve, reject) => {
+    console.log(1); 
+    setTimeout(()=>{
+      console.log(2)
+      resolve(); 
+    	console.log(3);
+    })
+})
+
+promise.then(() => {
+    console.log(4);
+})
+
+console.log(5);
+
const promise = new Promise((resolve, reject) => {
+    console.log(1); 
+    setTimeout(()=>{
+      console.log(2)
+      resolve(); 
+    	console.log(3);
+    })
+})
+
+promise.then(() => {
+    console.log(4);
+})
+
+console.log(5);
+

下面代码的输出结果是什么

javascript
const promise1 = new Promise((resolve, reject) => {
+	setTimeout(() => {
+    resolve()
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {
+  return 2;
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) 
+  console.log('promise2', promise2) 
+}, 2000)
+
const promise1 = new Promise((resolve, reject) => {
+	setTimeout(() => {
+    resolve()
+  }, 1000)
+})
+const promise2 = promise1.catch(() => {
+  return 2;
+})
+
+console.log('promise1', promise1) 
+console.log('promise2', promise2) 
+
+setTimeout(() => {
+  console.log('promise1', promise1) 
+  console.log('promise2', promise2) 
+}, 2000)
+

下面代码的输出结果是什么

javascript
async function m(){
+  const n = await 1;
+  console.log(n);
+}
+
+m();
+console.log(2);
+
async function m(){
+  const n = await 1;
+  console.log(n);
+}
+
+m();
+console.log(2);
+

下面代码的输出结果是什么

javascript
async function m(){
+  const n = await 1;
+  console.log(n);
+}
+
+(async ()=>{
+  await m();
+  console.log(2);
+})();
+
+console.log(3);
+
async function m(){
+  const n = await 1;
+  console.log(n);
+}
+
+(async ()=>{
+  await m();
+  console.log(2);
+})();
+
+console.log(3);
+

下面代码的输出结果是什么

javascript
async function m1(){
+  return 1;
+}
+
+async function m2(){
+  const n = await m1();
+  console.log(n)
+  return 2;
+}
+
+async function m3(){
+  const n = m2();
+  console.log(n);
+  return 3;
+}
+
+m3().then(n=>{
+  console.log(n);
+});
+
+m3();
+
+console.log(4);
+
async function m1(){
+  return 1;
+}
+
+async function m2(){
+  const n = await m1();
+  console.log(n)
+  return 2;
+}
+
+async function m3(){
+  const n = m2();
+  console.log(n);
+  return 3;
+}
+
+m3().then(n=>{
+  console.log(n);
+});
+
+m3();
+
+console.log(4);
+

下面代码的输出结果是什么

javascript
Promise.resolve(1)	
+  .then(2)
+  .then(Promise.resolve(3))
+  .then(console.log)
+
Promise.resolve(1)	
+  .then(2)
+  .then(Promise.resolve(3))
+  .then(console.log)
+

下面代码的输出结果是什么

javascript
var a;
+var b = new Promise((resolve, reject) => {
+  console.log('promise1');
+  setTimeout(()=>{
+    resolve();
+  }, 1000);
+}).then(() => {
+  console.log('promise2');
+}).then(() => {
+  console.log('promise3');
+}).then(() => {
+  console.log('promise4');
+});
+
+a = new Promise(async (resolve, reject) => {
+  console.log(a);
+  await b;
+  console.log(a);
+  console.log('after1');
+  await a
+  resolve(true);
+  console.log('after2');
+});
+
+console.log('end');
+
var a;
+var b = new Promise((resolve, reject) => {
+  console.log('promise1');
+  setTimeout(()=>{
+    resolve();
+  }, 1000);
+}).then(() => {
+  console.log('promise2');
+}).then(() => {
+  console.log('promise3');
+}).then(() => {
+  console.log('promise4');
+});
+
+a = new Promise(async (resolve, reject) => {
+  console.log(a);
+  await b;
+  console.log(a);
+  console.log('after1');
+  await a
+  resolve(true);
+  console.log('after2');
+});
+
+console.log('end');
+

下面代码的输出结果是什么

javascript
async function async1() {
+    console.log('async1 start');
+    await async2();
+    console.log('async1 end');
+}
+async function async2() {
+	console.log('async2');
+}
+
+console.log('script start');
+
+setTimeout(function() {
+    console.log('setTimeout');
+}, 0)
+
+async1();
+
+new Promise(function(resolve) {
+    console.log('promise1');
+    resolve();
+}).then(function() {
+    console.log('promise2');
+});
+console.log('script end');
+
async function async1() {
+    console.log('async1 start');
+    await async2();
+    console.log('async1 end');
+}
+async function async2() {
+	console.log('async2');
+}
+
+console.log('script start');
+
+setTimeout(function() {
+    console.log('setTimeout');
+}, 0)
+
+async1();
+
+new Promise(function(resolve) {
+    console.log('promise1');
+    resolve();
+}).then(function() {
+    console.log('promise2');
+});
+console.log('script end');
+
+ + + + + \ No newline at end of file diff --git "a/js/\350\277\255\344\273\243\345\231\250\345\222\214\347\224\237\346\210\220\345\231\250.html" "b/js/\350\277\255\344\273\243\345\231\250\345\222\214\347\224\237\346\210\220\345\231\250.html" new file mode 100644 index 00000000..f757e1d7 --- /dev/null +++ "b/js/\350\277\255\344\273\243\345\231\250\345\222\214\347\224\237\346\210\220\345\231\250.html" @@ -0,0 +1,1248 @@ + + + + + + 迭代器和生成器 | Sunny's blog + + + + + + + + +
Skip to content
On this page

迭代器和生成器


WARNING

大量 code 警告

迭代器

背景知识

  1. 什么是迭代?

从一个数据集合中按照一定的顺序,不断取出数据的过程

  1. 迭代和遍历的区别?

迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完 遍历强调的是要把整个数据依次全部取出

  1. 迭代器

对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象

  1. 迭代模式

一种设计模式,用于统一迭代过程,并规范了迭代器规格:

  • 迭代器应该具有得到下一个数据的能力
  • 迭代器应该具有判断是否还有后续数据的能力

JS中的迭代器

JS规定,如果一个对象具有next方法,并且该方法返回一个对象,该对象的格式如下:

javascript
{value: , done: 是否迭代完成}
+
{value:, done: 是否迭代完成}
+

模板

javascript
const obj = {
+  next() {
+    return {
+      value:xxx,
+      done:xx
+    }
+  }
+}
+
const obj = {
+  next() {
+    return {
+      value:xxx,
+      done:xx
+    }
+  }
+}
+

则认为该对象obj是一个迭代器 含义:

  • next方法:用于得到下一个数据
  • 返回的对象
    • value:下一个数据的值
    • done:boolean,是否迭代完成

演示1

javascript
const arr = [1, 2, 3, 4, 5];
+//迭代数组arr
+const iterator = {
+  i: 0, //当前的数组下标
+  next() {
+    var result = {
+      value: arr[this.i],
+      done: this.i >= arr.length//下标越界了
+    }
+    this.i++;
+    return result;
+  }
+}
+//让迭代器不断的取出下一个数据,直到没有数据为止
+let data = iterator.next();
+while (!data.done) { //只要没有迭代完成,则取出数据
+  console.log(data.value)
+  //进行下一次迭代
+  data = iterator.next();
+}
+
+console.log("迭代完成")
+
const arr = [1, 2, 3, 4, 5];
+//迭代数组arr
+const iterator = {
+  i: 0, //当前的数组下标
+  next() {
+    var result = {
+      value: arr[this.i],
+      done: this.i >= arr.length//下标越界了
+    }
+    this.i++;
+    return result;
+  }
+}
+//让迭代器不断的取出下一个数据,直到没有数据为止
+let data = iterator.next();
+while (!data.done) { //只要没有迭代完成,则取出数据
+  console.log(data.value)
+  //进行下一次迭代
+  data = iterator.next();
+}
+
+console.log("迭代完成")
+

演示2

javascript
const arr1 = [1, 2, 3, 4, 5];
+const arr2 = [6, 7, 8, 9];
+// 迭代器创建函数  iterator creator
+function createIterator(arr) {
+  let i = 0;//当前的数组下标
+  return {
+    next() {
+      var result = {
+        value: arr[i],
+        done: i >= arr.length
+      }
+      i++;
+      return result;
+    }
+  }
+}
+const iter1 = createIterator(arr1);
+iter1.next()
+const iter2 = createIterator(arr2);
+iter2.next()
+
const arr1 = [1, 2, 3, 4, 5];
+const arr2 = [6, 7, 8, 9];
+// 迭代器创建函数  iterator creator
+function createIterator(arr) {
+  let i = 0;//当前的数组下标
+  return {
+    next() {
+      var result = {
+        value: arr[i],
+        done: i >= arr.length
+      }
+      i++;
+      return result;
+    }
+  }
+}
+const iter1 = createIterator(arr1);
+iter1.next()
+const iter2 = createIterator(arr2);
+iter2.next()
+

演示3:无限延伸功能

javascript
// 依次得到斐波拉契数列前面n位的值
+// 1 1 2 3 5 8 13 .....
+//创建一个斐波拉契数列的迭代器
+function createFeiboIterator() {
+  let prev1 = 1,
+      prev2 = 1, //当前位置的前1位和前2位
+      n = 1; //当前是第几位
+  return {
+    next() {
+      let value;
+      if (n <= 2) {
+        value = 1;
+      } else {
+        value = prev1 + prev2;
+      }
+      const result = {
+        value,
+        done: false//这个数列无限延伸
+      };
+      prev2 = prev1;
+      prev1 = result.value;
+      n++;
+      return result;
+    }
+  }
+}
+const iterator = createFeiboIterator();
+iterator.next()
+
// 依次得到斐波拉契数列前面n位的值
+// 1 1 2 3 5 8 13 .....
+//创建一个斐波拉契数列的迭代器
+function createFeiboIterator() {
+  let prev1 = 1,
+      prev2 = 1, //当前位置的前1位和前2位
+      n = 1; //当前是第几位
+  return {
+    next() {
+      let value;
+      if (n <= 2) {
+        value = 1;
+      } else {
+        value = prev1 + prev2;
+      }
+      const result = {
+        value,
+        done: false//这个数列无限延伸
+      };
+      prev2 = prev1;
+      prev1 = result.value;
+      n++;
+      return result;
+    }
+  }
+}
+const iterator = createFeiboIterator();
+iterator.next()
+

可迭代协议与for-of循环

可迭代协议

  • 迭代器(iterator):一个具有next方法的对象,next方法返回下一个数据并且能指示是否迭代完成
  • 迭代器创建函数(iterator creator):一个返回迭代器的函数

可迭代协议

ES6规定,如果一个对象具有知名符号属性Symbol.iterator,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable) 数组就是个可迭代对象

思考:如何知晓一个对象是否是可迭代的?答案:数组、类数组、自己写的对象都可以做成可迭代对象

思考:如何遍历一个可迭代对象?答案:for-of循环

for-of 循环

for-of 循环用于遍历可迭代对象,格式如下

javascript
//迭代完成后循环结束
+for(const item in iterable){
+    //iterable:可迭代对象
+    //item:每次迭代得到的数据
+}
+
//迭代完成后循环结束
+for(const item in iterable){
+    //iterable:可迭代对象
+    //item:每次迭代得到的数据
+}
+

for-of循环的原理

调用对象的[Symbol.iterator]方法,得到一个迭代器。不断调用next方法,只有返回的done为false,则将返回的value传递给变量,然后进入循环体执行一次。

演示:数组就是个可迭代对象

javascript
const arr = [5, 7, 2, 3, 6];
+// const iterator = arr[Symbol.iterator]();
+// let result = iterator.next();
+// while (!result.done) {
+//     const item = result.value; //取出数据
+//     console.log(item);
+//     //下一次迭代
+//     result = iterator.next();
+// }
+// 等价于
+for (const item of arr) {
+  console.log(item)
+}
+
const arr = [5, 7, 2, 3, 6];
+// const iterator = arr[Symbol.iterator]();
+// let result = iterator.next();
+// while (!result.done) {
+//     const item = result.value; //取出数据
+//     console.log(item);
+//     //下一次迭代
+//     result = iterator.next();
+// }
+// 等价于
+for (const item of arr) {
+  console.log(item)
+}
+

演示3

html
<div>1</div>
+<div>2</div>
+<div>3</div>
+<script>
+  const divs = document.querySelectorAll("div");
+  // const iterator = divs[Symbol.iterator]()
+  // let result = iterator.next();
+  // while (!result.done) {
+  //     const item = result.value; //取出数据
+  //     console.log(item);
+  //     //下一次迭代
+  //     result = iterator.next();
+  // }
+  // 等价于
+  for (const item of divs) {
+    console.log(item);
+  }
+</script>
+
<div>1</div>
+<div>2</div>
+<div>3</div>
+<script>
+  const divs = document.querySelectorAll("div");
+  // const iterator = divs[Symbol.iterator]()
+  // let result = iterator.next();
+  // while (!result.done) {
+  //     const item = result.value; //取出数据
+  //     console.log(item);
+  //     //下一次迭代
+  //     result = iterator.next();
+  // }
+  // 等价于
+  for (const item of divs) {
+    console.log(item);
+  }
+</script>
+

演示:自定义可迭代对象

javascript
//可迭代对象
+var obj = {
+  a: 1,
+  b: 2,
+  [Symbol.iterator]() {
+    const keys = Object.keys(this);//得到对象this里面所有属性名的一个数组
+    // console.log(keys)
+    let i = 0;
+    return {
+      next: () => {
+        const propName = keys[i];
+        const propValue = this[propName];
+        const result = {
+          value: {
+            propName,
+            propValue
+          },
+          done: i >= keys.length
+        }
+        i++;
+        return result;
+      }
+    }
+  }
+} 
+for (const item of obj) {
+  console.log(item); // {propName:"a", propValue:1}
+}
+
//可迭代对象
+var obj = {
+  a: 1,
+  b: 2,
+  [Symbol.iterator]() {
+    const keys = Object.keys(this);//得到对象this里面所有属性名的一个数组
+    // console.log(keys)
+    let i = 0;
+    return {
+      next: () => {
+        const propName = keys[i];
+        const propValue = this[propName];
+        const result = {
+          value: {
+            propName,
+            propValue
+          },
+          done: i >= keys.length
+        }
+        i++;
+        return result;
+      }
+    }
+  }
+} 
+for (const item of obj) {
+  console.log(item); // {propName:"a", propValue:1}
+}
+

展开运算符与可迭代对象

展开运算符可以作用于可迭代对象,这样,就可以轻松的将可迭代对象转换为数组。 演示

javascript
var obj = {
+  a: 1,
+  b: 2,
+  [Symbol.iterator]() {
+    const keys = Object.keys(this);
+    let i = 0;
+    return {
+      next: () => {
+        const propName = keys[i];
+        const propValue = this[propName];
+        const result = {
+          value: {
+            propName,
+            propValue
+          },
+          done: i >= keys.length
+        }
+        i++;
+        return result;
+      }
+    }
+  }
+}
+
+const arr = [...obj];
+console.log(arr);
+
+function test(a, b) {
+  console.log(a, b)
+}
+
+test(...obj);
+
var obj = {
+  a: 1,
+  b: 2,
+  [Symbol.iterator]() {
+    const keys = Object.keys(this);
+    let i = 0;
+    return {
+      next: () => {
+        const propName = keys[i];
+        const propValue = this[propName];
+        const result = {
+          value: {
+            propName,
+            propValue
+          },
+          done: i >= keys.length
+        }
+        i++;
+        return result;
+      }
+    }
+  }
+}
+
+const arr = [...obj];
+console.log(arr);
+
+function test(a, b) {
+  console.log(a, b)
+}
+
+test(...obj);
+

迭代器和可迭代协议,解决实际问题

解决副作用的 redux 中间件

  • redux-thunk:需要改动action,可接收action是一个函数

  • redux-promise:需要改动action,可接收action是一个promise对象,或action的payload是一个promise对象

以上两个中间件,会导致action或action创建函数不再纯净。

  • redux-saga将解决这样的问题,它不仅可以保持action、action创建函数、reducer的纯净,而且可以用模块化的方式解决副作用,并且功能非常强大。

redux-saga是建立在ES6的生成器基础上的,要熟练的使用saga,必须理解生成器。

要理解生成器,必须先理解迭代器和可迭代协议。

生成器

生成器 (Generator)

  1. 什么是生成器?

生成器是一个通过构造函数Generator创建的对象,生成器既是一个迭代器(有next方法),同时又是一个可迭代对象(有知名符号),可以用于for of循环

javascript
//伪代码
+
+var generator = new Generator();
+generator.next();//它具有next方法
+var iterator = generator[Symbol.iterator];//它也是一个可迭代对象
+for(const item of generator){
+    //由于它是一个可迭代对象,因此也可以使用for of循环
+}
+
//伪代码
+
+var generator = new Generator();
+generator.next();//它具有next方法
+var iterator = generator[Symbol.iterator];//它也是一个可迭代对象
+for(const item of generator){
+    //由于它是一个可迭代对象,因此也可以使用for of循环
+}
+

注意:Generator构造函数,不提供给开发者使用,仅作为JS引擎内部使用

  1. 如何创建生成器

生成器的创建,必须使用生成器函数(Generator Function)

  1. 如何书写一个生成器函数呢?
javascript
//这是一个生成器函数,该函数一定返回一个生成器
+function* method(){
+}
+
//这是一个生成器函数,该函数一定返回一个生成器
+function* method(){
+}
+
  1. 生成器函数内部是如何执行的?

第一次调用只是生成了生成器,并没有调用里面 生成器函数内部是为了给生成器的每次迭代提供的数据 每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置 yield是一个关键字,该关键字只能在生成器函数内部使用,表达“产生”一个迭代数据。

  1. 有哪些需要注意的细节?

1). 生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中 2). 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值 3). 第一次调用next方法时,传参没有任何意义 4). 在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号

  1. 生成器的其他API
  • return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
javascript
generator.return()//结束了,done为true
+generator.return(3)//value=3
+
generator.return()//结束了,done为true
+generator.return(3)//value=3
+
  • throw方法:调用该方法,可以在生成器中产生一个错误
javascript
generator.throw(new Error("sdbjjas"))	
+
generator.throw(new Error("sdbjjas"))	
+

演示

javascript
function* test() {
+  console.log("第1次运行")
+  yield 1;
+  console.log("第2次运行")
+  yield 2;
+  console.log("第3次运行")
+}
+
+const generator = test();
+
function* test() {
+  console.log("第1次运行")
+  yield 1;
+  console.log("第2次运行")
+  yield 2;
+  console.log("第3次运行")
+}
+
+const generator = test();
+

优化演示:数组的迭代

javascript
const arr1 = [1, 2, 3, 4, 5];
+const arr2 = [6, 7, 8, 9];
+
+// 迭代器创建函数  iterator creator
+function* createIterator(arr) {
+  for (const item of arr) {
+    yield item;
+  }
+}
+
+const iter1 = createIterator(arr1);
+const iter2 = createIterator(arr2);
+
const arr1 = [1, 2, 3, 4, 5];
+const arr2 = [6, 7, 8, 9];
+
+// 迭代器创建函数  iterator creator
+function* createIterator(arr) {
+  for (const item of arr) {
+    yield item;
+  }
+}
+
+const iter1 = createIterator(arr1);
+const iter2 = createIterator(arr2);
+

创建一个斐波拉契数列的迭代器

javascript
function* createFeiboIterator() {
+  let prev1 = 1,
+      prev2 = 1, //当前位置的前1位和前2位
+      n = 1; //当前是第几位
+  while (true) {
+    if (n <= 2) {
+      yield 1;
+    } else {
+      const newValue = prev1 + prev2
+      yield newValue;
+      prev2 = prev1;
+      prev1 = newValue;
+    }
+    n++;
+  }
+}
+
+const iterator = createFeiboIterator();
+
function* createFeiboIterator() {
+  let prev1 = 1,
+      prev2 = 1, //当前位置的前1位和前2位
+      n = 1; //当前是第几位
+  while (true) {
+    if (n <= 2) {
+      yield 1;
+    } else {
+      const newValue = prev1 + prev2
+      yield newValue;
+      prev2 = prev1;
+      prev1 = newValue;
+    }
+    n++;
+  }
+}
+
+const iterator = createFeiboIterator();
+

演示:生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中

javascript
function* test() {
+  console.log("第1次运行")
+  yield 1;
+  console.log("第2次运行")
+  yield 2;
+  console.log("第3次运行");
+  return 10;
+}
+
+const generator = test();
+
function* test() {
+  console.log("第1次运行")
+  yield 1;
+  console.log("第2次运行")
+  yield 2;
+  console.log("第3次运行");
+  return 10;
+}
+
+const generator = test();
+

演示:调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值

javascript
function* test() {
+  console.log("函数开始")
+  let info = yield 1;//这里info不是1,这里根据参数动态变化
+  console.log(info)
+  info = yield 2 + info;
+  console.log(info)
+}
+const generator = test();
+控制台
+generator.next()
+demo.js:2 函数开始
+{value: 1, done: false}
+generator.next(5)
+{value: 7, done: false}
+
function* test() {
+  console.log("函数开始")
+  let info = yield 1;//这里info不是1,这里根据参数动态变化
+  console.log(info)
+  info = yield 2 + info;
+  console.log(info)
+}
+const generator = test();
+控制台
+generator.next()
+demo.js:2 函数开始
+{value: 1, done: false}
+generator.next(5)
+{value: 7, done: false}
+

演示:在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号

javascript
function* t1(){
+  yield "a"
+  yield "b"
+}
+function* test() {
+  两种错误的调用方式
+  1. t1()// 调用生成器函数不会导致里面的代码执行
+  2. yield t1();// 将生成器对象给了yield执行
+  yield* t1();//相当于把t1里面代码直接copy过来了
+  yield 1;
+  yield 2;
+  yield 3;
+}
+const generator = test();
+
function* t1(){
+  yield "a"
+  yield "b"
+}
+function* test() {
+  两种错误的调用方式:
+  1. t1()// 调用生成器函数不会导致里面的代码执行
+  2. yield t1();// 将生成器对象给了yield执行
+  yield* t1();//相当于把t1里面代码直接copy过来了
+  yield 1;
+  yield 2;
+  yield 3;
+}
+const generator = test();
+

生成器

javascript
// generator.next === generator[Symbol.iterator]().next()
+//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
// generator.next === generator[Symbol.iterator]().next()
+//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
javascript
function* createArrayIterator(arr) {
+  for (let i = 0; i < arr.length; i++) {
+    const item = arr[i];
+    console.log(`第${i}次迭代`);
+    yield item;
+  }
+  console.log("函数结束");//迭代完成后运行
+}
+
+var generator = createArrayIterator([1, 2, 3, 4, 5, 6]);
+
function* createArrayIterator(arr) {
+  for (let i = 0; i < arr.length; i++) {
+    const item = arr[i];
+    console.log(`第${i}次迭代`);
+    yield item;
+  }
+  console.log("函数结束");//迭代完成后运行
+}
+
+var generator = createArrayIterator([1, 2, 3, 4, 5, 6]);
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  return;// 后面不会运行了
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  return;// 后面不会运行了
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
javascript
function asyncGetData() {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      resolve("成哥")
+    }, 2000);
+  })
+}
+
+function* task() {
+  console.log("开始获取数据....");
+  const data = yield asyncGetData()
+  console.log("获取到数据:", data);
+  const data2 = yield asyncGetData();
+  console.log("又获取到了数据:", data2);
+  const data3 = yield 1;
+  console.log("又获取到了数据:", data3)
+}
+
+/**
+         * 通用函数:运行一个生成器任务
+         */
+function run(generatorFunction) {
+  const generator = generatorFunction(); //得到一个生成器
+  next();
+
+  /**
+             * 封装了generator的next方法,进行下一次迭代
+             */
+  function next(nextValue) {
+    const result = generator.next(nextValue);
+    if (result.done) {
+      //迭代结束了
+      return; 
+    }
+    const value = result.value; //拿到迭代的数据
+    if (typeof value.then === "function") {
+      //迭代的数据是一个Promise
+      value.then(data => next(data));
+    } else {
+      next(result.value);
+    }
+  }
+}
+
+run(task);
+
function asyncGetData() {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      resolve("成哥")
+    }, 2000);
+  })
+}
+
+function* task() {
+  console.log("开始获取数据....");
+  const data = yield asyncGetData()
+  console.log("获取到数据:", data);
+  const data2 = yield asyncGetData();
+  console.log("又获取到了数据:", data2);
+  const data3 = yield 1;
+  console.log("又获取到了数据:", data3)
+}
+
+/**
+         * 通用函数:运行一个生成器任务
+         */
+function run(generatorFunction) {
+  const generator = generatorFunction(); //得到一个生成器
+  next();
+
+  /**
+             * 封装了generator的next方法,进行下一次迭代
+             */
+  function next(nextValue) {
+    const result = generator.next(nextValue);
+    if (result.done) {
+      //迭代结束了
+      return; 
+    }
+    const value = result.value; //拿到迭代的数据
+    if (typeof value.then === "function") {
+      //迭代的数据是一个Promise
+      value.then(data => next(data));
+    } else {
+      next(result.value);
+    }
+  }
+}
+
+run(task);
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值,还没有完成赋值 绝不是把1赋值给result,result是外部给他的
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+var result = generator.next(); //{value:1, done:false}
+while (!result.done) {
+  //有迭代的值
+  result = generator.next(result.value);//如果想把yield返回的值交给result
+}
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值,还没有完成赋值 绝不是把1赋值给result,result是外部给他的
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+var result = generator.next(); //{value:1, done:false}
+while (!result.done) {
+  //有迭代的值
+  result = generator.next(result.value);//如果想把yield返回的值交给result
+}
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  try {
+    console.log("生成器函数的函数体 - 开始");
+    let result = yield 1; //将1作为第一次的迭代的值.
+    console.log("生成器函数的函数体 - 运行1", result);
+    result = yield 2; //将2作为第二次迭代的值
+    console.log("生成器函数的函数体 - 运行2", result);
+    result = yield 3;
+    console.log("生成器函数的函数体 - 运行3", result);
+    return "结束";
+  } catch (err) {
+    console.log("报错了");
+    yield "Abc";
+  }
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  try {
+    console.log("生成器函数的函数体 - 开始");
+    let result = yield 1; //将1作为第一次的迭代的值.
+    console.log("生成器函数的函数体 - 运行1", result);
+    result = yield 2; //将2作为第二次迭代的值
+    console.log("生成器函数的函数体 - 运行2", result);
+    result = yield 3;
+    console.log("生成器函数的函数体 - 运行3", result);
+    return "结束";
+  } catch (err) {
+    console.log("报错了");
+    yield "Abc";
+  }
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
javascript
function* g2() {
+  console.log("g2-开始");
+  let result = yield "g1";
+  console.log("g2-运行1");
+  result = yield "g2";
+  return 123;
+}
+//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  result = yield* g2(); //result为g2函数的返回值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+
function* g2() {
+  console.log("g2-开始");
+  let result = yield "g1";
+  console.log("g2-运行1");
+  result = yield "g2";
+  return 123;
+}
+//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  result = yield* g2(); //result为g2函数的返回值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+

生成器函数特点

  1. 调用生成器函数,会返回一个生成器,而不是执行函数体(因为,生成器函数的函数体执行,收到生成器控制)
  2. 每当调用了生成器的next方法,生成器的函数体会从上一次yield的位置(或开始位置)运行到下一个yield
    • yield关键字只能在生成器内部使用,不可以在普通函数内部使用
    • 它表示暂停,并返回一个当前迭代的数据
    • 如果没有下一个yield,到了函数结束,则生成器的next方法得到的结果中的done为true
  3. yield关键字后面的表达式返回的数据,会作为当前迭代的数据
  4. 生成器函数的返回值,会作为迭代结束时的value
    • 但是,如果在结束过后,仍然反复调用next,则value为undefined
  5. 生成器调用next的时候,可以传递参数,该参数会作为生成器函数体上一次yield表达式的值。
    • 生成器第一次调用next函数时,传递参数没有任何意义
  6. 生成器带有一个throw方法,该方法与next的效果相同,唯一的区别在于:
    • next方法传递的参数会被返回成一个正常值
    • throw方法传递的参数是一个错误对象,会导致生成器函数内部发生一个错误。
  7. 生成器带有一个return方法,该方法会直接结束生成器函数
  8. 若需要在生成器内部调用其他生成器,注意:如果直接调用,得到的是一个生成器,如果加入*号调用,则进入其生成器内部执行。如果是yield* 函数()调用生成器函数,则该函数的返回结果,为该表达式的结果
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
javascript
function* createArrayIterator(arr) {
+  for (let i = 0; i < arr.length; i++) {
+    const item = arr[i];
+    console.log(`第${i}次迭代`)
+    yield item;
+  }
+  console.log("函数结束")
+}
+
+var generator = createArrayIterator([1, 2, 3, 4, 5, 6])
+
function* createArrayIterator(arr) {
+  for (let i = 0; i < arr.length; i++) {
+    const item = arr[i];
+    console.log(`第${i}次迭代`)
+    yield item;
+  }
+  console.log("函数结束")
+}
+
+var generator = createArrayIterator([1, 2, 3, 4, 5, 6])
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  return;
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  yield 1; // {value:1, done:false}
+  return;
+  console.log("生成器函数的函数体 - 运行1");
+  yield 2;
+  console.log("生成器函数的函数体 - 运行2");
+  yield 3;
+  console.log("生成器函数的函数体 - 运行3");
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+var result = generator.next(); //{value:1, done:false}
+while (!result.done) {
+  //有迭代的值
+  result = generator.next(result.value);
+}
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
+var result = generator.next(); //{value:1, done:false}
+while (!result.done) {
+  //有迭代的值
+  result = generator.next(result.value);
+}
+
javascript
function asyncGetData() {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      resolve("成哥")
+    }, 2000);
+  })
+}
+
+function* task() {
+  console.log("开始获取数据....");
+  const data = yield asyncGetData()
+  console.log("获取到数据:", data);
+  const data2 = yield asyncGetData();
+  console.log("又获取到了数据:", data2);
+  const data3 = yield 1;
+  console.log("又获取到了数据:", data3)
+}
+
+/**
+         * 通用函数:运行一个生成器任务
+         */
+function run(generatorFunction) {
+  const generator = generatorFunction(); //得到一个生成器
+  next();
+
+  /**
+             * 封装了generator的next方法,进行下一次迭代
+             */
+  function next(nextValue) {
+    const result = generator.next(nextValue);
+    if (result.done) {
+      //迭代结束了
+      return; 
+    }
+    const value = result.value; //拿到迭代的数据
+    if (typeof value.then === "function") {
+      //迭代的数据是一个Promise
+      value.then(data => next(data));
+    } else {
+      next(result.value);
+    }
+  }
+}
+
+run(task);
+
function asyncGetData() {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      resolve("成哥")
+    }, 2000);
+  })
+}
+
+function* task() {
+  console.log("开始获取数据....");
+  const data = yield asyncGetData()
+  console.log("获取到数据:", data);
+  const data2 = yield asyncGetData();
+  console.log("又获取到了数据:", data2);
+  const data3 = yield 1;
+  console.log("又获取到了数据:", data3)
+}
+
+/**
+         * 通用函数:运行一个生成器任务
+         */
+function run(generatorFunction) {
+  const generator = generatorFunction(); //得到一个生成器
+  next();
+
+  /**
+             * 封装了generator的next方法,进行下一次迭代
+             */
+  function next(nextValue) {
+    const result = generator.next(nextValue);
+    if (result.done) {
+      //迭代结束了
+      return; 
+    }
+    const value = result.value; //拿到迭代的数据
+    if (typeof value.then === "function") {
+      //迭代的数据是一个Promise
+      value.then(data => next(data));
+    } else {
+      next(result.value);
+    }
+  }
+}
+
+run(task);
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  try {
+    console.log("生成器函数的函数体 - 开始");
+    let result = yield 1; //将1作为第一次的迭代的值
+    console.log("生成器函数的函数体 - 运行1", result);
+    result = yield 2; //将2作为第二次迭代的值
+    console.log("生成器函数的函数体 - 运行2", result);
+    result = yield 3;
+    console.log("生成器函数的函数体 - 运行3", result);
+    return "结束";
+  } catch (err) {
+    console.log("报错了");
+    yield "Abc";
+  }
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  try {
+    console.log("生成器函数的函数体 - 开始");
+    let result = yield 1; //将1作为第一次的迭代的值
+    console.log("生成器函数的函数体 - 运行1", result);
+    result = yield 2; //将2作为第二次迭代的值
+    console.log("生成器函数的函数体 - 运行2", result);
+    result = yield 3;
+    console.log("生成器函数的函数体 - 运行3", result);
+    return "结束";
+  } catch (err) {
+    console.log("报错了");
+    yield "Abc";
+  }
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
javascript
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
javascript
function* g2() {
+  console.log("g2-开始");
+  let result = yield "g1";
+  console.log("g2-运行1");
+  result = yield "g2";
+  return 123;
+}
+//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  result = yield* g2(); //result为g2函数的返回值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+
function* g2() {
+  console.log("g2-开始");
+  let result = yield "g1";
+  console.log("g2-运行1");
+  result = yield "g2";
+  return 123;
+}
+//下面的函数是一个生成器函数,用于创建生成器
+function* createGenerator() {
+  console.log("生成器函数的函数体 - 开始");
+  let result = yield 1; //将1作为第一次的迭代的值
+  result = yield* g2(); //result为g2函数的返回值
+  console.log("生成器函数的函数体 - 运行1", result);
+  result = yield 2; //将2作为第二次迭代的值
+  console.log("生成器函数的函数体 - 运行2", result);
+  result = yield 3;
+  console.log("生成器函数的函数体 - 运行3", result);
+  return "结束";
+
+}
+
+var generator = createGenerator(); //调用后,一定得到一个生成器
+

生成器应用-异步任务控制

高仿await

javascript
function* task() {
+  const d = yield 1;
+  console.log(d)
+  // //d : 1
+  const resp = yield fetch("http://101.132.72.36:5100/api/local")
+  const result = yield resp.json();
+  console.log(result);
+}
+run(task)
+function run(generatorFunc) {
+  const generator = generatorFunc();
+  let result = generator.next(); //启动任务(开始迭代), 得到迭代数据
+  handleResult();
+  //对result进行处理
+  function handleResult() {
+    if (result.done) {
+      return; //迭代完成,不处理
+    }
+    //迭代没有完成,分为两种情况
+    //1. 迭代的数据是一个Promise
+    //2. 迭代的数据是其他数据
+    if (typeof result.value.then === "function") {//promise里面有个then方法
+      //1. 迭代的数据是一个Promise
+      //等待Promise完成后,再进行下一次迭代
+      result.value.then(data => {
+        result = generator.next(data)
+        handleResult();
+      })
+    } else {
+      //2. 迭代的数据是其他数据,直接进行下一次迭代
+      result = generator.next(result.value)
+      handleResult();
+    }
+  }
+}
+
function* task() {
+  const d = yield 1;
+  console.log(d)
+  // //d : 1
+  const resp = yield fetch("http://101.132.72.36:5100/api/local")
+  const result = yield resp.json();
+  console.log(result);
+}
+run(task)
+function run(generatorFunc) {
+  const generator = generatorFunc();
+  let result = generator.next(); //启动任务(开始迭代), 得到迭代数据
+  handleResult();
+  //对result进行处理
+  function handleResult() {
+    if (result.done) {
+      return; //迭代完成,不处理
+    }
+    //迭代没有完成,分为两种情况
+    //1. 迭代的数据是一个Promise
+    //2. 迭代的数据是其他数据
+    if (typeof result.value.then === "function") {//promise里面有个then方法
+      //1. 迭代的数据是一个Promise
+      //等待Promise完成后,再进行下一次迭代
+      result.value.then(data => {
+        result = generator.next(data)
+        handleResult();
+      })
+    } else {
+      //2. 迭代的数据是其他数据,直接进行下一次迭代
+      result = generator.next(result.value)
+      handleResult();
+    }
+  }
+}
+
+ + + + + \ No newline at end of file diff --git a/logo.jpeg b/logo.jpeg new file mode 100644 index 00000000..61d0eee4 Binary files /dev/null and b/logo.jpeg differ diff --git a/mini-any.png b/mini-any.png new file mode 100644 index 00000000..3d54ac41 Binary files /dev/null and b/mini-any.png differ diff --git a/react/2023-02-01-19-09-45.png b/react/2023-02-01-19-09-45.png new file mode 100644 index 00000000..5343741f Binary files /dev/null and b/react/2023-02-01-19-09-45.png differ diff --git a/react/2023-02-01-19-09-56.png b/react/2023-02-01-19-09-56.png new file mode 100644 index 00000000..90ece6cf Binary files /dev/null and b/react/2023-02-01-19-09-56.png differ diff --git a/react/2023-02-01-19-21-07.png b/react/2023-02-01-19-21-07.png new file mode 100644 index 00000000..c51f78a8 Binary files /dev/null and b/react/2023-02-01-19-21-07.png differ diff --git a/react/2023-02-01-19-21-23.png b/react/2023-02-01-19-21-23.png new file mode 100644 index 00000000..52cfb057 Binary files /dev/null and b/react/2023-02-01-19-21-23.png differ diff --git a/react/2023-02-01-19-21-38.png b/react/2023-02-01-19-21-38.png new file mode 100644 index 00000000..f040ae74 Binary files /dev/null and b/react/2023-02-01-19-21-38.png differ diff --git a/react/2023-02-01-19-21-46.png b/react/2023-02-01-19-21-46.png new file mode 100644 index 00000000..532ca8c9 Binary files /dev/null and b/react/2023-02-01-19-21-46.png differ diff --git a/react/2023-02-01-19-22-45.png b/react/2023-02-01-19-22-45.png new file mode 100644 index 00000000..80c1e236 Binary files /dev/null and b/react/2023-02-01-19-22-45.png differ diff --git a/react/2023-02-01-19-24-55.png b/react/2023-02-01-19-24-55.png new file mode 100644 index 00000000..4abb8615 Binary files /dev/null and b/react/2023-02-01-19-24-55.png differ diff --git a/react/2023-02-01-19-25-37.png b/react/2023-02-01-19-25-37.png new file mode 100644 index 00000000..0fb70ee6 Binary files /dev/null and b/react/2023-02-01-19-25-37.png differ diff --git a/react/2023-02-01-19-25-56.png b/react/2023-02-01-19-25-56.png new file mode 100644 index 00000000..f5df2ce2 Binary files /dev/null and b/react/2023-02-01-19-25-56.png differ diff --git a/react/2023-02-01-19-31-36.png b/react/2023-02-01-19-31-36.png new file mode 100644 index 00000000..f0c18557 Binary files /dev/null and b/react/2023-02-01-19-31-36.png differ diff --git a/react/2023-02-01-19-31-43.png b/react/2023-02-01-19-31-43.png new file mode 100644 index 00000000..3ad5eebb Binary files /dev/null and b/react/2023-02-01-19-31-43.png differ diff --git a/react/2023-02-01-19-31-50.png b/react/2023-02-01-19-31-50.png new file mode 100644 index 00000000..71a5ab13 Binary files /dev/null and b/react/2023-02-01-19-31-50.png differ diff --git a/react/2023-02-01-19-32-00.png b/react/2023-02-01-19-32-00.png new file mode 100644 index 00000000..17f44329 Binary files /dev/null and b/react/2023-02-01-19-32-00.png differ diff --git a/react/2023-02-01-19-32-06.png b/react/2023-02-01-19-32-06.png new file mode 100644 index 00000000..d8a8546f Binary files /dev/null and b/react/2023-02-01-19-32-06.png differ diff --git a/react/Fiber.html b/react/Fiber.html new file mode 100644 index 00000000..4948286c --- /dev/null +++ b/react/Fiber.html @@ -0,0 +1,20 @@ + + + + + + Fiber | Sunny's blog + + + + + + + + +
Skip to content
On this page

Fiber

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿

为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。

所以 React 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 分批延时对 DOM 进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。

核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

+ + + + + \ No newline at end of file diff --git a/react/ReactRouter.html b/react/ReactRouter.html new file mode 100644 index 00000000..a5d9ea90 --- /dev/null +++ b/react/ReactRouter.html @@ -0,0 +1,628 @@ + + + + + + ReactRouter | Sunny's blog + + + + + + + + +
Skip to content
On this page

ReactRouter

React Router 概述

站点

无论是使用 Vue,还是 React,开发的单页应用程序,可能只是该站点的一部分(某一个功能块) 一个单页应用里,可能会划分为多个页面(几乎完全不同的页面效果)(组件)

如果要在单页应用中完成组件的切换,需要实现下面两个功能:

  1. 根据不同的页面地址,展示不同的组件(核心)
  2. 完成无刷新的地址切换

我们把实现了以上两个功能的插件,称之为路由

React Router

  1. react-router:路由核心库,包含诸多和路由功能相关的核心代码
  2. react-router-dom:利用路由核心库,结合实际的页面,实现跟页面路由密切相关的功能

如果是在页面中实现路由,需要安装 react-router-dom 库

两种模式

路由:根据不同的页面地址,展示不同的组件

url 地址组成

例:https://www.react.com:443/news/1-2-1.html?a=1&b=2#abcdefg

  1. 协议名(schema):https
  2. 主机名(host):www.react.com
    1. ip 地址
    2. 预设值:localhost
    3. 域名
    4. 局域网中电脑名称
  3. 端口号(port):443
    1. 如果协议是 http,端口号是 80,则可以省略端口号
    2. 如果协议是 https,端口号是 443,则可以省略端口号
  4. 路径(path):/news/1-2-1.html
  5. 地址参数(search、query):?a=1&b=2
    1. 附带的数据
    2. 格式:属性名=属性值&属性名=属性值....
  6. 哈希(hash、锚点)
    1. 附带的数据

Hash Router 哈希路由

根据 url 地址中的哈希值来确定显示的组件

原因:hash 的变化,不会导致页面刷新 这种模式的兼容性最好

Borswer History Router 浏览器历史记录路由

之前存在的 api:history.forward(), history.back(), history.go() HTML5 出现后,新增了 History Api,从此以后,浏览器拥有了改变路径而不刷新页面的方式

History 表示浏览器的历史记录,它使用栈的方式存储。

image.png

  1. history.length:获取栈中数据量
  2. history.pushState:向当前历史记录栈中加入一条新的记录
    1. 参数 1:附加的数据,自定义的数据,可以是任何类型
    2. 参数 2:页面标题,目前大部分浏览器不支持
    3. 参数 3:新的地址
  3. history.replaceState:将当前指针指向的历史记录,替换为某个记录
    1. 参数 1:附加的数据,自定义的数据,可以是任何类型
    2. 参数 2:页面标题,目前大部分浏览器不支持
    3. 参数 3:新的地址

根据页面的路径决定渲染哪个组件,不是根据哈希了

路由组件

React-Router 为我们提供了两个重要组件

Router 组件

它本身不做任何展示,仅提供路由模式配置,另外,该组件会产生一个上下文,上下文中会提供一些实用的对象和方法,供其他相关组件使用

  1. HashRouter:该组件,使用 hash 模式匹配
  2. BrowserRouter:该组件,使用 BrowserHistory 模式匹配

通常情况下,Router 组件只有一个,将该组件包裹整个页面

Route 组件

根据不同的地址,展示不同的组件

重要属性:

  1. path:匹配的路径
    1. 默认情况下,不区分大小写,可以设置 sensitive 属性为 true,来区分大小写
    2. 默认情况下,只匹配初始目录,如果要精确匹配,配置 exact 属性为 true
    3. 如果不写 path,则会匹配任意路径
  2. component:匹配成功后要显示的组件
  3. children:
    1. 传递 React 元素,无论是否匹配,一定会显示 children,并且会忽略 component 属性
    2. 传递一个函数,该函数有多个参数,这些参数来自于上下文,该函数返回 react 元素,则一定会显示返回的元素,并且忽略 component 属性.

Route 组件可以写到任意的地方,只要保证它是 Router 组件的后代元素

Switch 组件

写到 Switch 组件中的 Route 组件,当匹配到第一个 Route 后,会立即停止匹配

由于 Switch 组件会循环所有子元素,然后让每个子元素去完成匹配,若匹配到,则渲染对应的组件,然后停止循环。因此,不能在 Switch 的子元素中使用除 Route 外的其他组件。

路由信息

Router 组件会创建一个上下文,并且,向上下文中注入一些信息

该上下文对开发者是隐藏的,Route 组件若匹配到了地址,则会将这些上下文中的信息作为属性传入对应的组件

history

它并不是 window.history 对象,我们利用该对象无刷新跳转地址

为什么没有直接使用 history 对象

  1. React-Router 中有两种模式:Hash、History,如果直接使用 window.history,只能支持一种模式。为了适配两种模式
  2. 当使用 windows.history.pushState 方法时,没有办法收到任何通知,将导致 React 无法知晓地址发生了变化,结果导致无法重新渲染组件
  • push:将某个新的地址入栈(历史记录栈)
    • 参数 1:新的地址
    • 参数 2:可选,附带的状态数据
  • replace:将某个新的地址替换掉当前栈中的地址
  • go: 与 window.history 一致
  • forward: 与 window.history 一致
  • back: 与 window.history 一致

location

与 history.location 完全一致,是同一个对象,但是,与 window.location 不同

location 对象中记录了当前地址的相关信息

我们通常使用第三方库query-string,用于解析地址栏中的数据

match

该对象中保存了,路由匹配的相关信息

  • isExact:事实上,当前的路径和路由配置的路径是否是精确匹配的。和 exact 写没写无关
  • params:获取路径规则中对应的数据

实际上,在书写 Route 组件的 path 属性时,可以书写一个string pattern(字符串正则)

react-router 使用了第三方库:Path-to-RegExp,该库的作用是,将一个字符串正则转换成一个真正的正则表达式。

向某个页面传递数据的方式:

  1. 使用 state:在 push 页面时,加入 state
  2. 利用 search:把数据填写到地址栏中的?后
  3. 利用 hash:把数据填写到 hash 后
  4. params:把数据填写到路径中

非路由组件获取路由信息

某些组件,并没有直接放到 Route 中,而是嵌套在其他普通组件中,因此,它的 props 中没有路由信息,如果这些组件需要获取到路由信息,可以使用下面两种方式:

  1. 将路由信息从父组件一层一层传递到子组件
  2. 使用 react-router 提供的高阶组件 withRouter,包装要使用的组件,该高阶组件会返回一个新组件,新组件将向提供的组件注入路由信息。

其他组件

已学习:

  • Router:BrowswerRouter、HashRouter
  • Route
  • Switch
  • 高阶函数:withRouter

生成一个无刷新跳转的 a 元素

  • to
    • 字符串:跳转的目标地址
    • 对象:
      • pathname:url 路径
      • search
      • hash
      • state:附加的状态信息
  • replace:bool,表示是否是替换当前地址,默认是 false,是 push 跳转
  • innerRef:可以将内部的 a 元素的 ref 附着在传递的对象或函数参数上
    • 函数
    • ref 对象

是一种特殊的 Link,Link 组件具备的功能,它都有

它具备的额外功能是:根据当前地址和链接地址,来决定该链接的样式

  • activeClassName: 匹配时使用的类名
  • activeStyle: 匹配时使用的内联样式
  • exact: 是否精确匹配
  • sensitive:匹配时是否区分大小写
  • strict:是否严格匹配最后一个斜杠

Redirect

重定向组件,当加载到该组件时,会自动跳转(无刷新)到另外一个地址

  • to:跳转的地址
    • 字符串
    • 对象
  • push: 默认为 false,表示跳转使用替换的方式,设置为 true 后,则使用 push 的方式跳转
  • from:当匹配到 from 地址规则时才进行跳转
  • exact: 是否精确匹配 from
  • sensitive:from 匹配时是否区分大小写
  • strict:from 是否严格匹配最后一个斜杠

vue-router 和 React router

vue-router 是一个静态的配置 react-router v4 之前 静态的配置 现在 react-router 是动态的组件,灵活了

嵌套路由

导航守卫

导航守卫:当离开一个页面,进入另一个页面时,触发的事件

history 对象

  • listen: 添加一个监听器,监听地址的变化,当地址发生变化时,会调用传递的函数
    • 参数:函数,运行时间点:发生在即将跳转到新页面时
      • 参数 1:location 对象,记录当前的地址信息
      • 参数 2:action,一个字符串,表示进入该地址的方式
        • POP:出栈 (指针移动)
          • 通过点击浏览器后退、前进
          • 调用 history.go
          • 调用 history.goBack
          • 调用 history.goForward
        • PUSH:入栈 (指针移动)
          • history.push
        • REPLACE:替换
          • history.replace
    • 返回结果:函数,可以调用该函数取消监听
  • block:设置一个阻塞,并同时设置阻塞消息,当页面发生跳转时,会进入阻塞,并将阻塞消息传递到路由根组件的 getUserConfirmation 方法。
    • 返回一个回调函数,用于取消阻塞器

路由根组件

  • getUserConfirmation
    • 参数:函数
      • 参数 1:阻塞消息
        • 字符串消息
        • 函数,函数的返回结果是一个字符串,用于表示阻塞消息
          • 参数 1:location 对象
          • 参数 2:action 值
      • 参数 2:回调函数,调用该函数并传递 true,则表示进入到新页面,否则,不做任何操作

常见应用 - 路由切换动画

第三方动画库:react-transition-group

CSSTransition:用于为内部的 DOM 元素添加类样式,通过 in 属性决定内部的 DOM 处于退出还是进入阶段。

滚动条复位

高阶组件

使用 useEffect

使用自定义的导航守卫

路由

React-Router 的实现原理是什么?

客户端路由实现的思想:

  • 基于 hash 的路由:通过监听 事件,感知 hash 的变化 hashchange
    • 改变 hash 可以直接通过 location.hash=xxx
  • 基于 H5 history 路由:
    • 改变 url 可以通过 history.pushState 和 resplaceState 等,会将 URL 压入堆栈,同时能够应用 history.go() 等 API
    • 监听 url 的变化可以通过自定义事件触发实现

react-router 实现的思想:

  • 基于 history 库来实现上述不同的客户端路由实现思想,并且能够保存历史记录等,磨平浏览器差异,上层无感知
  • 通过维护的列表,在每次 URL 发生变化的回收,通过配置的 路由路径,匹配到对应的 Component,并且 render

如何配置 React-Router 实现路由切换

(1)使用<Route> 组件

路由匹配是通过比较 <Route> 的 path 属性和当前地址的 pathname 来实现的。当一个 <Route> 匹配成功时,它将渲染其内容,当它不匹配时就会渲染 null。没有路径的 <Route> 将始终被匹配。

javascript
// when location = { pathname: '/about' }
+<Route path='/about' component={About}/> // renders <About/>
+<Route path='/contact' component={Contact}/> // renders null
+<Route component={Always}/> // renders <Always/>
+
// when location = { pathname: '/about' }
+<Route path='/about' component={About}/> // renders <About/>
+<Route path='/contact' component={Contact}/> // renders null
+<Route component={Always}/> // renders <Always/>
+

(2)Switch 和 Route

<Switch> 用于将 <Route> 分组。

javascript
<Switch>
+  <Route exact path="/" component={Home} />
+  <Route path="/about" component={About} />
+  <Route path="/contact" component={Contact} />
+</Switch>
+
<Switch>
+  <Route exact path="/" component={Home} />
+  <Route path="/about" component={About} />
+  <Route path="/contact" component={Contact} />
+</Switch>
+

<Switch> 不是分组 <Route> 所必须的,但他通常很有用。 一个 <Switch> 会遍历其所有的子 <Route>元素,并仅渲染与当前地址匹配的第一个元素。

**(3)使用 **<Link>、 <NavLink>、<Redirect>** 组件

<Link> 组件来在你的应用程序中创建链接。无论你在何处渲染一个<Link> ,都会在应用程序的 HTML 中渲染锚(<a>)。

javascript
<Link to="/">Home</Link>
+// <a href='/'>Home</a>
+
<Link to="/">Home</Link>
+// <a href='/'>Home</a>
+

是一种特殊类型的   当它的 to 属性与当前地址匹配时,可以将其定义为"活跃的"。

javascript
// location = { pathname: '/react' }
+<NavLink to="/react" activeClassName="hurray">
+  React
+</NavLink>;
+// <a href='/react' className='hurray'>React</a>
+
// location = { pathname: '/react' }
+<NavLink to="/react" activeClassName="hurray">
+  React
+</NavLink>;
+// <a href='/react' className='hurray'>React</a>
+

当我们想强制导航时,可以渲染一个<Redirect>,当一个<Redirect>渲染时,它将使用它的 to 属性进行定向。

React-Router 怎么设置重定向?

使用<Redirect>组件实现路由的重定向:

jsx
<Switch>
+  <Redirect from="/users/:id" to="/users/profile/:id" />
+  <Route path="/users/profile/:id" component={Profile} />
+</Switch>
+
<Switch>
+  <Redirect from="/users/:id" to="/users/profile/:id" />
+  <Route path="/users/profile/:id" component={Profile} />
+</Switch>
+

当请求 /users/:id 被重定向去 '/users/profile/:id'

  • 属性 from: string:需要匹配的将要被重定向路径。
  • 属性 to: string:重定向的 URL 字符串
  • 属性 to: object:重定向的 location 对象
  • 属性 push: bool:若为真,重定向操作将会把新地址加入到访问历史记录里面,并且无法回退到前面的页面。

从最终渲染的 DOM 来看,这两者都是链接,都是 标签,区别是 ∶ <Link>是 react-router 里实现路由跳转的链接,一般配合<Route> 使用,react-router 接管了其默认的链接跳转行为,区别于传统的页面跳转,<Link> 的“跳转”行为只会触发相匹配的<Route>对应的页面内容更新,而不会刷新整个页面。

<Link>做了 3 件事情:

  • 有 onclick 那就执行 onclick
  • click 的时候阻止 a 标签默认事件
  • 根据跳转 href(即是 to),用 history (web 前端路由两种方式之一,history & hash)跳转,此时只是链接变了,并没有刷新页面而<a>标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一 个页面(非锚点情况)。

a 标签默认事件禁掉之后做了什么才实现了跳转?

javascript
let domArr = document.getElementsByTagName('a')
+[...domArr].forEach(item=>{
+    item.addEventListener('click',function () {
+        location.href = this.href
+    })
+})
+
let domArr = document.getElementsByTagName('a')
+[...domArr].forEach(item=>{
+    item.addEventListener('click',function () {
+        location.href = this.href
+    })
+})
+

React-Router 如何获取 URL 的参数和历史对象?

(1)获取 URL 的参数

  • get 传值

路由配置还是普通的配置,如:'admin',传参方式如:'admin?id='1111''。通过this.props.location.search获取 url 获取到一个字符串'?id='1111' 可以用 url,qs,querystring,浏览器提供的 api URLSearchParams 对象或者自己封装的方法去解析出 id 的值。

  • 动态路由传值

路由需要配置成动态路由:如path='/admin/:id',传参方式,如'admin/111'。通过this.props.match.params.id 取得 url 中的动态路由 id 部分的值,除此之外还可以通过useParams(Hooks)来获取

  • 通过 query 或 state 传值

传参方式如:在 Link 组件的 to 属性中可以传递对象{pathname:'/admin',query:'111',state:'111'};。通过this.props.location.statethis.props.location.query来获取即可,传递的参数可以是对象、数组等,但是存在缺点就是只要刷新页面,参数就会丢失。

(2)获取历史对象

  • 如果 React >= 16.8 时可以使用 React Router 中提供的 Hooks
javascript
import { useHistory } from "react-router-dom";
+let history = useHistory();
+
import { useHistory } from "react-router-dom";
+let history = useHistory();
+

2.使用 this.props.history 获取历史对象

javascript
let history = this.props.history;
+
let history = this.props.history;
+

React-Router 4 怎样在路由变化时重新渲染同一个组件?

当路由变化时,即组件的 props 发生了变化,会调用 componentWillReceiveProps 等生命周期钩子。那需要做的只是: 当路由改变时,根据路由,也去请求数据:

javascript
class NewsList extends Component {
+  componentDidMount () {
+     this.fetchData(this.props.location);
+  }
+
+  fetchData(location) {
+    const type = location.pathname.replace('/', '') || 'top'
+    this.props.dispatch(fetchListData(type))
+  }
+  componentWillReceiveProps(nextProps) {
+     if (nextProps.location.pathname != this.props.location.pathname) {
+         this.fetchData(nextProps.location);
+     }
+  }
+  render () {
+    ...
+  }
+}
+
+
class NewsList extends Component {
+  componentDidMount () {
+     this.fetchData(this.props.location);
+  }
+
+  fetchData(location) {
+    const type = location.pathname.replace('/', '') || 'top'
+    this.props.dispatch(fetchListData(type))
+  }
+  componentWillReceiveProps(nextProps) {
+     if (nextProps.location.pathname != this.props.location.pathname) {
+         this.fetchData(nextProps.location);
+     }
+  }
+  render () {
+    ...
+  }
+}
+
+

利用生命周期 componentWillReceiveProps,进行重新 render 的预处理操作。

React-Router 的路由有几种模式?

React-Router 支持使用 hash(对应 HashRouter)和 browser(对应 BrowserRouter) 两种路由规则, react-router-dom 提供了 BrowserRouter 和 HashRouter 两个组件来实现应用的 UI 和 URL 同步:

(1)BrowserRouter

它使用 HTML5 提供的 history API(pushState、replaceState 和 popstate 事件)来保持 UI 和 URL 的同步。由此可以看出,BrowserRouter 是使用 HTML 5 的 history API 来控制路由跳转的:

javascript
<BrowserRouter
+  basename={string}
+  forceRefresh={bool}
+  getUserConfirmation={func}
+  keyLength={number}
+/>;
+
<BrowserRouter
+  basename={string}
+  forceRefresh={bool}
+  getUserConfirmation={func}
+  keyLength={number}
+/>;
+

其中的属性如下:

  • basename 所有路由的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠;
javascript
<BrowserRouter basename="/calendar">
+  <Link to="/today" />
+</BrowserRouter>;
+
<BrowserRouter basename="/calendar">
+  <Link to="/today" />
+</BrowserRouter>;
+

等同于

javascript
<a href="/calendar/today" />;
+
<a href="/calendar/today" />;
+
  • forceRefresh 如果为 true,在导航的过程中整个页面将会刷新。一般情况下,只有在不支持 HTML5 history API 的浏览器中使用此功能;
  • getUserConfirmation 用于确认导航的函数,默认使用 window.confirm。例如,当从 /a 导航至 /b 时,会使用默认的 confirm 函数弹出一个提示,用户点击确定后才进行导航,否则不做任何处理;
javascript
// 这是默认的确认函数
+const getConfirmation = (message, callback) => {
+  const allowTransition = window.confirm(message);
+  callback(allowTransition);
+};
+<BrowserRouter getUserConfirmation={getConfirmation} />;
+
// 这是默认的确认函数
+const getConfirmation = (message, callback) => {
+  const allowTransition = window.confirm(message);
+  callback(allowTransition);
+};
+<BrowserRouter getUserConfirmation={getConfirmation} />;
+

需要配合<Prompt> 一起使用。

  • KeyLength 用来设置 Location.Key 的长度。

(2)HashRouter

使用 URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。由此可以看出,HashRouter 是通过 URL 的 hash 属性来控制路由跳转的:

javascript
<HashRouter basename={string} getUserConfirmation={func} hashType={string} />;
+
<HashRouter basename={string} getUserConfirmation={func} hashType={string} />;
+

其参数如下

  • basename, getUserConfirmation 和 BrowserRouter 功能一样;
  • hashType window.location.hash 使用的 hash 类型,有如下几种:
    • slash - 后面跟一个斜杠,例如 #/ 和 #/sunshine/lollipops;
    • noslash - 后面没有斜杠,例如 # 和 #sunshine/lollipops;
    • hashbang - Google 风格的 ajax crawlable,例如 #!/ 和 #!/sunshine/lollipops。

React-Router 4 的 Switch 有什么用?

Switch 通常被用来包裹 Route,用于渲染与路径匹配的第一个子 <Route><Redirect>,它里面不能放其他元素。

假如不加 <Switch>

javascript
import { Route } from 'react-router-dom'
+
+<Route path="/" component={Home}></Route>
+<Route path="/login" component={Login}></Route>
+
+
import { Route } from 'react-router-dom'
+
+<Route path="/" component={Home}></Route>
+<Route path="/login" component={Login}></Route>
+
+

Route 组件的 path 属性用于匹配路径,因为需要匹配 /Home,匹配 /loginLogin,所以需要两个 Route,但是不能这么写。这样写的话,当 URL 的 path 为 “/login” 时,<Route path="/" /><Route path="/login" /> 都会被匹配,因此页面会展示 Home 和 Login 两个组件。这时就需要借助 <Switch> 来做到只显示一个匹配组件:

javascript
import { Switch, Route } from "react-router-dom";
+
+<Switch>
+  <Route path="/" component={Home}></Route>
+  <Route path="/login" component={Login}></Route>
+</Switch>;
+
import { Switch, Route } from "react-router-dom";
+
+<Switch>
+  <Route path="/" component={Home}></Route>
+  <Route path="/login" component={Login}></Route>
+</Switch>;
+

此时,再访问 “/login” 路径时,却只显示了 Home 组件。这是就用到了 exact 属性,它的作用就是精确匹配路径,经常与<Switch> 联合使用。只有当 URL 和该 <Route> 的 path 属性完全一致的情况下才能匹配上:

javascript
import { Switch, Route } from "react-router-dom";
+
+<Switch>
+  <Route exact path="/" component={Home}></Route>
+  <Route exact path="/login" component={Login}></Route>
+</Switch>;
+
import { Switch, Route } from "react-router-dom";
+
+<Switch>
+  <Route exact path="/" component={Home}></Route>
+  <Route exact path="/login" component={Login}></Route>
+</Switch>;
+

Redux

对 Redux 的理解,主要解决什么问题

React 是视图层框架。Redux 是一个用来管理数据状态和 UI 状态的 JavaScript 应用工具。随着 JavaScript 单页应用(SPA)开发日趋复杂, JavaScript 需要管理比任何时候都要多的 state(状态), Redux 就是降低管理难度的。(Redux 支持 React、Angular、jQuery 甚至纯 JavaScript)

在 React 中,UI 以组件的形式来搭建,组件之间可以嵌套组合。但 React 中组件间通信的数据流是单向的,顶层组件可以通过 props 属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄弟组件之间同样不能。这样简单的单向数据流支撑起了 React 中的数据可控性。

当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也将越来越不好管理。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当复杂。

Redux 提供了一个叫 store 的统一仓储库,组件通过 dispatch 将 state 直接传入 store,不用通过其他的组件。并且组件通过 subscribe 从 store 获取到 state 的改变。使用了 Redux,所有的组件都可以从 store 中获取到所需的 state,他们也能从 store 获取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。

主要解决的问题: 单纯的 Redux 只是一个状态机,是没有 UI 呈现的,react- redux 作用是将 Redux 的状态机和 React 的 UI 呈现绑定在一起,当你 dispatch action 改变 state 的时候,会自动更新页面。

Redux 原理及工作流程

(1)原理 Redux 源码主要分为以下几个模块文件

  • compose.js 提供从右到左进行函数式编程
  • createStore.js 提供作为生成唯一 store 的函数
  • combineReducers.js 提供合并多个 reducer 的函数,保证 store 的唯一性
  • bindActionCreators.js 可以让开发者在不直接接触 dispacth 的前提下进行更改 state 的操作
  • applyMiddleware.js 这个方法通过中间件来增强 dispatch 的功能
javascript
const actionTypes = {
+    ADD: 'ADD',
+    CHANGEINFO: 'CHANGEINFO',
+}
+
+const initState = {
+    info: '初始化',
+}
+
+export default function initReducer(state=initState, action) {
+    switch(action.type) {
+        case actionTypes.CHANGEINFO:
+            return {
+                ...state,
+                info: action.preload.info || '',
+            }
+        default:
+            return { ...state };
+    }
+}
+
+export default function createStore(reducer, initialState, middleFunc) {
+
+    if (initialState && typeof initialState === 'function') {
+        middleFunc = initialState;
+        initialState = undefined;
+    }
+
+    let currentState = initialState;
+
+    const listeners = [];
+
+    if (middleFunc && typeof middleFunc === 'function') {
+        // 封装dispatch
+        return middleFunc(createStore)(reducer, initialState);
+    }
+
+    const getState = () => {
+        return currentState;
+    }
+
+    const dispatch = (action) => {
+        currentState = reducer(currentState, action);
+
+        listeners.forEach(listener => {
+            listener();
+        })
+    }
+
+    const subscribe = (listener) => {
+        listeners.push(listener);
+    }
+
+    return {
+        getState,
+        dispatch,
+        subscribe
+    }
+}
+
+
const actionTypes = {
+    ADD: 'ADD',
+    CHANGEINFO: 'CHANGEINFO',
+}
+
+const initState = {
+    info: '初始化',
+}
+
+export default function initReducer(state=initState, action) {
+    switch(action.type) {
+        case actionTypes.CHANGEINFO:
+            return {
+                ...state,
+                info: action.preload.info || '',
+            }
+        default:
+            return { ...state };
+    }
+}
+
+export default function createStore(reducer, initialState, middleFunc) {
+
+    if (initialState && typeof initialState === 'function') {
+        middleFunc = initialState;
+        initialState = undefined;
+    }
+
+    let currentState = initialState;
+
+    const listeners = [];
+
+    if (middleFunc && typeof middleFunc === 'function') {
+        // 封装dispatch
+        return middleFunc(createStore)(reducer, initialState);
+    }
+
+    const getState = () => {
+        return currentState;
+    }
+
+    const dispatch = (action) => {
+        currentState = reducer(currentState, action);
+
+        listeners.forEach(listener => {
+            listener();
+        })
+    }
+
+    const subscribe = (listener) => {
+        listeners.push(listener);
+    }
+
+    return {
+        getState,
+        dispatch,
+        subscribe
+    }
+}
+
+

(2)工作流程

  • const store= createStore(fn)生成数据;
  • action: {type: Symble('action01), payload:'payload' }定义行为;
  • dispatch 发起 action:store.dispatch(doSomething('action001'));
  • reducer:处理 action,返回新的 state;

通俗点解释:

  • 首先,用户(通过 View)发出 Action,发出方式就用到了 dispatch 方法
  • 然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action,Reducer 会返回新的 State
  • State—旦有变化,Store 就会调用监听函数,来更新 View

以 store 为核心,可以把它看成数据存储中心,但是他要更改数据的时候不能直接修改,数据修改更新的角色由 Reducers 来担任,store 只做存储,中间人,当 Reducers 的更新完成以后会通过 store 的订阅来通知 react component,组件把新的状态重新获取渲染,组件中也能主动发送 action,创建 action 后这个动作是不会执行的,所以要 dispatch 这个 action,让 store 通过 reducers 去做更新 React Component 就是 react 的每个组件。

Redux 中异步的请求怎么处理

可以在 componentDidmount 中直接进⾏请求⽆须借助 redux。但是在⼀定规模的项⽬中,上述⽅法很难进⾏异步流的管理,通常情况下我们会借助 redux 的异步中间件进⾏异步处理。redux 异步流中间件其实有很多,当下主流的异步中间件有两种 redux-thunk、redux-saga。

(1)使用 react-thunk 中间件

redux-thunk优点:

  • 体积⼩: redux-thunk 的实现⽅式很简单,只有不到 20 ⾏代码
  • 使⽤简单: redux-thunk 没有引⼊像 redux-saga 或者 redux-observable 额外的范式,上⼿简单

redux-thunk缺陷:

  • 样板代码过多: 与 redux 本身⼀样,通常⼀个请求需要⼤量的代码,⽽且很多都是重复性质的
  • 耦合严重: 异步操作与 redux 的 action 偶合在⼀起,不⽅便管理
  • 功能孱弱: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装

使用步骤:

  • 配置中间件,在 store 的创建中配置
javascript
import { createStore, applyMiddleware, compose } from "redux";
+import reducer from "./reducer";
+import thunk from "redux-thunk";
+
+// 设置调试工具
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
+  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
+  : compose;
+// 设置中间件
+const enhancer = composeEnhancers(applyMiddleware(thunk));
+
+const store = createStore(reducer, enhancer);
+
+export default store;
+
import { createStore, applyMiddleware, compose } from "redux";
+import reducer from "./reducer";
+import thunk from "redux-thunk";
+
+// 设置调试工具
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
+  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
+  : compose;
+// 设置中间件
+const enhancer = composeEnhancers(applyMiddleware(thunk));
+
+const store = createStore(reducer, enhancer);
+
+export default store;
+
  • 添加一个返回函数的 actionCreator,将异步请求逻辑放在里面
javascript
/**
+  发送get请求,并生成相应action,更新store的函数
+  @param url {string} 请求地址
+  @param func {function} 真正需要生成的action对应的actionCreator
+  @return {function} 
+*/
+// dispatch为自动接收的store.dispatch函数
+export const getHttpAction = (url, func) => (dispatch) => {
+  axios.get(url).then(function (res) {
+    const action = func(res.data);
+    dispatch(action);
+  });
+};
+
/**
+  发送get请求,并生成相应action,更新store的函数
+  @param url {string} 请求地址
+  @param func {function} 真正需要生成的action对应的actionCreator
+  @return {function} 
+*/
+// dispatch为自动接收的store.dispatch函数
+export const getHttpAction = (url, func) => (dispatch) => {
+  axios.get(url).then(function (res) {
+    const action = func(res.data);
+    dispatch(action);
+  });
+};
+
  • 生成 action,并发送 action
javascript
componentDidMount(){
+    var action = getHttpAction('/getData', getInitTodoItemAction)
+    // 发送函数类型的action时,该action的函数体会自动执行
+    store.dispatch(action)
+}
+
+
componentDidMount(){
+    var action = getHttpAction('/getData', getInitTodoItemAction)
+    // 发送函数类型的action时,该action的函数体会自动执行
+    store.dispatch(action)
+}
+
+

(2)使用 redux-saga 中间件

redux-saga优点:

  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
  • action 摆脱 thunk function: dispatch 的参数依然是⼀个纯粹的 action (FSA),⽽不是充满 “⿊魔法” thunk function
  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
  • 功能强⼤: redux-saga 提供了⼤量的 Saga 辅助函数和 Effect 创建器供开发者使⽤,开发者⽆须封装或者简单封装即可使⽤
  • 灵活: redux-saga 可以将多个 Saga 可以串⾏/并⾏组合起来,形成⼀个⾮常实⽤的异步 flow
  • 易测试,提供了各种 case 的测试⽅案,包括 mock task,分⽀覆盖等等

redux-saga缺陷:

  • 额外的学习成本: redux-saga 不仅在使⽤难以理解的 generator function,⽽且有数⼗个 API,学习成本远超 redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与 redux-observable 不同,redux-observable 虽然也有额外学习成本但是背后是 rxjs 和⼀整套思想
  • 体积庞⼤: 体积略⼤,代码近 2000 ⾏,min 版 25KB 左右
  • 功能过剩: 实际上并发控制等功能很难⽤到,但是我们依然需要引⼊这些代码
  • ts ⽀持不友好: yield ⽆法返回 TS 类型

redux-saga 可以捕获 action,然后执行一个函数,那么可以把异步代码放在这个函数中,使用步骤如下:

  • 配置中间件
javascript
import { createStore, applyMiddleware, compose } from "redux";
+import reducer from "./reducer";
+import createSagaMiddleware from "redux-saga";
+import TodoListSaga from "./sagas";
+
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
+  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
+  : compose;
+const sagaMiddleware = createSagaMiddleware();
+
+const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
+
+const store = createStore(reducer, enhancer);
+sagaMiddleware.run(TodoListSaga);
+
+export default store;
+
import { createStore, applyMiddleware, compose } from "redux";
+import reducer from "./reducer";
+import createSagaMiddleware from "redux-saga";
+import TodoListSaga from "./sagas";
+
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
+  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
+  : compose;
+const sagaMiddleware = createSagaMiddleware();
+
+const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
+
+const store = createStore(reducer, enhancer);
+sagaMiddleware.run(TodoListSaga);
+
+export default store;
+
  • 将异步请求放在 sagas.js 中
javascript
import { takeEvery, put } from "redux-saga/effects";
+import { initTodoList } from "./actionCreator";
+import { GET_INIT_ITEM } from "./actionTypes";
+import axios from "axios";
+
+function* func() {
+  try {
+    // 可以获取异步返回数据
+    const res = yield axios.get("/getData");
+    const action = initTodoList(res.data);
+    // 将action发送到reducer
+    yield put(action);
+  } catch (e) {
+    console.log("网络请求失败");
+  }
+}
+
+function* mySaga() {
+  // 自动捕获GET_INIT_ITEM类型的action,并执行func
+  yield takeEvery(GET_INIT_ITEM, func);
+}
+
+export default mySaga;
+
import { takeEvery, put } from "redux-saga/effects";
+import { initTodoList } from "./actionCreator";
+import { GET_INIT_ITEM } from "./actionTypes";
+import axios from "axios";
+
+function* func() {
+  try {
+    // 可以获取异步返回数据
+    const res = yield axios.get("/getData");
+    const action = initTodoList(res.data);
+    // 将action发送到reducer
+    yield put(action);
+  } catch (e) {
+    console.log("网络请求失败");
+  }
+}
+
+function* mySaga() {
+  // 自动捕获GET_INIT_ITEM类型的action,并执行func
+  yield takeEvery(GET_INIT_ITEM, func);
+}
+
+export default mySaga;
+
  • 发送 action
javascript
componentDidMount(){
+  const action = getInitTodoItemAction()
+  store.dispatch(action)
+}
+
componentDidMount(){
+  const action = getInitTodoItemAction()
+  store.dispatch(action)
+}
+

Redux 怎么实现属性传递,介绍下原理

react-redux 数据传输 ∶ view-->action-->reducer-->store-->view。看下点击事件的数据是如何通过 redux 传到 view 上:

  • view 上的 AddClick 事件通过 mapDispatchToProps 把数据传到 action ---> click:()=>dispatch(ADD)
  • action 的 ADD 传到 reducer 上
  • reducer 传到 store 上 const store = createStore(reducer);
  • store 再通过 mapStateToProps 映射穿到 view 上 text:State.text

代码示例 ∶

jsx
import React from "react";
+import ReactDOM from "react-dom";
+import { createStore } from "redux";
+import { Provider, connect } from "react-redux";
+class App extends React.Component {
+  render() {
+    let { text, click, clickR } = this.props;
+    return (
+      <div>
+        <div>数据:已有人{text}</div>
+        <div onClick={click}>加人</div>
+        <div onClick={clickR}>减人</div>
+      </div>
+    );
+  }
+}
+const initialState = {
+  text: 5,
+};
+const reducer = function (state, action) {
+  switch (action.type) {
+    case "ADD":
+      return { text: state.text + 1 };
+    case "REMOVE":
+      return { text: state.text - 1 };
+    default:
+      return initialState;
+  }
+};
+
+let ADD = {
+  type: "ADD",
+};
+let Remove = {
+  type: "REMOVE",
+};
+
+const store = createStore(reducer);
+
+let mapStateToProps = function (state) {
+  return {
+    text: state.text,
+  };
+};
+
+let mapDispatchToProps = function (dispatch) {
+  return {
+    click: () => dispatch(ADD),
+    clickR: () => dispatch(Remove),
+  };
+};
+
+const App1 = connect(mapStateToProps, mapDispatchToProps)(App);
+
+ReactDOM.render(
+  <Provider store={store}>
+    <App1></App1>
+  </Provider>,
+  document.getElementById("root")
+);
+
import React from "react";
+import ReactDOM from "react-dom";
+import { createStore } from "redux";
+import { Provider, connect } from "react-redux";
+class App extends React.Component {
+  render() {
+    let { text, click, clickR } = this.props;
+    return (
+      <div>
+        <div>数据:已有人{text}</div>
+        <div onClick={click}>加人</div>
+        <div onClick={clickR}>减人</div>
+      </div>
+    );
+  }
+}
+const initialState = {
+  text: 5,
+};
+const reducer = function (state, action) {
+  switch (action.type) {
+    case "ADD":
+      return { text: state.text + 1 };
+    case "REMOVE":
+      return { text: state.text - 1 };
+    default:
+      return initialState;
+  }
+};
+
+let ADD = {
+  type: "ADD",
+};
+let Remove = {
+  type: "REMOVE",
+};
+
+const store = createStore(reducer);
+
+let mapStateToProps = function (state) {
+  return {
+    text: state.text,
+  };
+};
+
+let mapDispatchToProps = function (dispatch) {
+  return {
+    click: () => dispatch(ADD),
+    clickR: () => dispatch(Remove),
+  };
+};
+
+const App1 = connect(mapStateToProps, mapDispatchToProps)(App);
+
+ReactDOM.render(
+  <Provider store={store}>
+    <App1></App1>
+  </Provider>,
+  document.getElementById("root")
+);
+

Redux 中间件是什么?接受几个参数?柯里化函数两端的参数具体是什么?

Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等。

applyMiddleware中可以看出 ∶

  • redux 中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数。
  • 柯里化函数两端一个是 middewares,一个是 store.dispatch

Redux 请求中间件如何处理并发

使用 redux-Saga redux-saga 是一个管理 redux 应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将 react 中的同步操作与异步操作区分开来,以便于后期的管理与维护。 redux-saga 如何处理并发:

  • takeEvery

可以让多个 saga 任务并行被 fork 执行。

javascript
import { fork, take } from "redux-saga/effects";
+
+const takeEvery = (pattern, saga, ...args) =>
+  fork(function* () {
+    while (true) {
+      const action = yield take(pattern);
+      yield fork(saga, ...args.concat(action));
+    }
+  });
+
import { fork, take } from "redux-saga/effects";
+
+const takeEvery = (pattern, saga, ...args) =>
+  fork(function* () {
+    while (true) {
+      const action = yield take(pattern);
+      yield fork(saga, ...args.concat(action));
+    }
+  });
+
  • takeLatest

takeLatest 不允许多个 saga 任务并行地执行。一旦接收到新的发起的 action,它就会取消前面所有 fork 过的任务(如果这些任务还在执行的话)。 在处理 AJAX 请求的时候,如果只希望获取最后那个请求的响应, takeLatest 就会非常有用。

javascript
import { cancel, fork, take } from "redux-saga/effects";
+
+const takeLatest = (pattern, saga, ...args) =>
+  fork(function* () {
+    let lastTask;
+    while (true) {
+      const action = yield take(pattern);
+      if (lastTask) {
+        yield cancel(lastTask); // 如果任务已经结束,则 cancel 为空操作
+      }
+      lastTask = yield fork(saga, ...args.concat(action));
+    }
+  });
+
import { cancel, fork, take } from "redux-saga/effects";
+
+const takeLatest = (pattern, saga, ...args) =>
+  fork(function* () {
+    let lastTask;
+    while (true) {
+      const action = yield take(pattern);
+      if (lastTask) {
+        yield cancel(lastTask); // 如果任务已经结束,则 cancel 为空操作
+      }
+      lastTask = yield fork(saga, ...args.concat(action));
+    }
+  });
+

Redux 状态管理器和变量挂载到 window 中有什么区别

两者都是存储数据以供后期使用。但是 Redux 状态更改可回溯——Time travel,数据多了的时候可以很清晰的知道改动在哪里发生,完整的提供了一套状态管理模式。

随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。 如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。

这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 可以称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 视图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux 就是为了帮你解决这个问题。

mobox 和 redux 有什么区别?

(1)共同点

  • 为了解决状态管理混乱,无法有效同步的问题统一维护管理应用状态;
  • 某一状态只有一个可信数据来源(通常命名为 store,指状态容器);
  • 操作更新状态方式统一,并且可控(通常以 action 方式提供更新状态的途径);
  • 支持将 store 与 React 组件连接,如 react-redux,mobx- react;

(2)区别 Redux 更多的是遵循 Flux 模式的一种实现,是一个 JavaScript 库,它关注点主要是以下几方面 ∶

  • Action∶ 一个 JavaScript 对象,描述动作相关信息,主要包含 type 属性和 payload 属性 ∶
o type∶ action 类型; o payload∶ 负载数据;
+
o type∶ action 类型; o payload∶ 负载数据;
+
  • Reducer∶ 定义应用状态如何响应不同动作(action),如何更新状态;
  • Store∶ 管理 action 和 reducer 及其关系的对象,主要提供以下功能 ∶
o 维护应用状态并支持访问状态(getState());
+o 支持监听action的分发,更新状态(dispatch(action));
+o 支持订阅store的变更(subscribe(listener));
+
o 维护应用状态并支持访问状态(getState());
+o 支持监听action的分发,更新状态(dispatch(action));
+o 支持订阅store的变更(subscribe(listener));
+
  • 异步流 ∶ 由于 Redux 所有对 store 状态的变更,都应该通过 action 触发,异步任务(通常都是业务或获取数据任务)也不例外,而为了不将业务或数据相关的任务混入 React 组件中,就需要使用其他框架配合管理异步任务流程,如 redux-thunk,redux-saga 等;

Mobx 是一个透明函数响应式编程的状态管理库,它使得状态管理简单可伸缩 ∶

  • Action∶ 定义改变状态的动作函数,包括如何变更状态;
  • Store∶ 集中管理模块状态(State)和动作(action)
  • Derivation(衍生)∶ 从应用状态中派生而出,且没有任何其他影响的数据

对比总结:

  • redux 将数据保存在单一的 store 中,mobx 将数据保存在分散的多个 store 中
  • redux 使用 plain object 保存数据,需要手动处理变化后的操作;mobx 适用 observable 保存数据,数据变化后自动处理响应的操作
  • redux 使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数;mobx 中的状态是可变的,可以直接对其进行修改
  • mobx 相对来说比较简单,在其中有很多的抽象,mobx 更多的使用面向对象的编程思维;redux 会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
  • mobx 中有更多的抽象和封装,调试会比较困难,同时结果也难以预测;而 redux 提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易

Redux 和 Vuex 有什么区别,它们的共同思想

(1)Redux 和 Vuex 区别

  • Vuex 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里改变 state 值即可
  • Vuex 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的 State 即可
  • Vuex 数据流的顺序是 ∶View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自动渲染)

通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;

(2)共同思想

  • 单—的数据源
  • 变化可以预测

本质上 ∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案。

Redux 中间件是怎么拿到 store 和 action? 然后怎么处理?

redux 中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个 middleware 接受 2 个参数, Store 的 getState 函数和 dispatch 函数,分别获得 store 和 action,最终返回一个函数。该函数会被传入 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是({ getState,dispatch })=> next => action。

Redux 中的 connect 有什么作用

connect 负责连接 React 和 Redux

(1)获取 state

connect 通过 context 获取 Provider 中的 store,通过store.getState() 获取整个 store tree 上所有 state

(2)包装原组件

将 state 和 action 通过 props 的方式传入到原组件内部 wrapWithConnect 返回—个 ReactComponent 对 象 Connect,Connect 重 新 render 外部传入的原组件 WrappedComponent ,并把 connect 中传入的 mapStateToProps,mapDispatchToProps 与组件上原有的 props 合并后,通过属性的方式传给 WrappedComponent

(3)监听 store tree 变化

connect 缓存了 store tree 中 state 的状态,通过当前 state 状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发 Connect 及其子组件的重新渲染

+ + + + + \ No newline at end of file diff --git a/react/Redux.html b/react/Redux.html new file mode 100644 index 00000000..abcf71a0 --- /dev/null +++ b/react/Redux.html @@ -0,0 +1,58 @@ + + + + + + Redux | Sunny's blog + + + + + + + + +
Skip to content
On this page

Redux

Redux 核心概念

action  reducer  store

MVC

它是一个 UI 的解决方案,用于降低 UI,以及 UI 关联的数据的复杂度。

传统的服务器端的 MVC

环境:

  1. 服务端需要响应一个完整的 HTML
  2. 该 HTML 中包含页面需要的数据
  3. 浏览器仅承担渲染页面的作用

以上的这种方式叫做服务端渲染,即服务器端将完整的页面组装好之后,一起发送给客户端。

服务器端需要处理 UI 中要用到的数据,并且要将数据嵌入到页面中,最终生成一个完整的 HTML 页面响应。

为了降低处理这个过程的复杂度,出现了 MVC 模式。

Controller: 处理请求,组装这次请求需要的数据 Model:需要用于 UI 渲染的数据模型 View:视图,用于将模型组装到界面中

前后端分离前端 MVC 模式的困难

React 解决了   数据 -> 视图   的问题

解决了 MVC 的 V

  1. 前端的 controller 要比服务器复杂很多,因为前端中的 controller 处理的是用户的操作,而用户的操作场景是复杂的。
  2. 对于那些组件化的框架(比如 vue、react),它们使用的是单向数据流。若需要共享数据,则必须将数据提升到顶层组件,然后数据再一层一层传递,极其繁琐。 虽然可以使用上下文来提供共享数据,但对数据的操作难以监控,容易导致调试错误的困难,以及数据还原的困难。并且,若开发一个大中型项目,共享的数据很多,会导致上下文中的数据变得非常复杂。

比如,上下文中有如下格式的数据:

javascript
value = {
+  users: [{}, {}, {}],
+  addUser: function (u) {},
+  deleteUser: function (u) {},
+  updateUser: function (u) {},
+  //  这些方法都可能改变数据,发生错误难以调试
+};
+
value = {
+  users: [{}, {}, {}],
+  addUser: function (u) {},
+  deleteUser: function (u) {},
+  updateUser: function (u) {},
+  //  这些方法都可能改变数据,发生错误难以调试
+};
+

前端需要一个独立的数据解决方案

独立:可不一定是 react,根本就没关系

Flux

Facebook 提出的数据解决方案,它的最大历史意义,在于它引入了 action 的概念

action 是一个普通的对象,用于描述要干什么。action 是触发数据变化的唯一原因

store 表示数据仓库,用于存储共享数据。还可以根据不同的 action 更改仓库中的数据

示例:

javascript
var loginAction = {
+  type: "login",
+  payload: {
+    loginId: "admin",
+    loginPwd: "123123",
+  },
+};
+
+var deleteAction = {
+  type: "delete",
+  payload: 1, // 用户id为1
+};
+
var loginAction = {
+  type: "login",
+  payload: {
+    loginId: "admin",
+    loginPwd: "123123",
+  },
+};
+
+var deleteAction = {
+  type: "delete",
+  payload: 1, // 用户id为1
+};
+

Redux

在 Flux 基础上,引入了 reducer 的概念

reducer:处理器,用于根据 action 来处理数据,处理后的数据会被仓库重新保存。

Action

  1. action 是一个 plain-object(平面对象)
    1. 它的proto指向 Object.prototype
  2. 通常,使用 payload 属性表示附加数据(没有强制要求)
  3. action 中必须有 type 属性,该属性用于描述操作的类型
    1. 但是,没有对 type 的类型做出要求
  4. 在大型项目,由于操作类型非常多,为了避免硬编码(hard code),会将 action 的类型存放到一个或一些单独的文件中(样板代码)。

  1. 为了方面传递 action,通常会使用 action 创建函数(action creator)来创建 action

  2. action 创建函数应为无副作用的纯函数

    1. 不能以任何形式改动参数
    2. 不可以有异步
    3. 不可以对外部环境中的数据造成影响
  3. 为了方便利用 action 创建函数来分发(触发)action,redux 提供了一个函数bindActionCreators,该函数用于增强 action 创建函数的功能,使它不仅可以创建 action,并且创建后会自动完成分发。

Reducer

Reducer 是用于改变数据的函数

  1. 一个数据仓库,有且仅有一个 reducer,并且通常情况下,一个工程只有一个仓库,因此,一个系统,只有一个 reducer
  2. 为了方便管理,通常会将 reducer 放到单独的文件中。
  3. reducer 被调用的时机
    1. 通过 store.dispatch,分发了一个 action,此时,会调用 reducer
    2. 当创建一个 store 的时候,会调用一次 reducer
      1. 可以利用这一点,用 reducer 初始化状态
      2. 创建仓库时,不传递任何默认状态
      3. 将 reducer 的参数 state 设置一个默认值。创建仓库不写默认值,传递 reducer 的时候传递默认值
  4. reducer 内部通常使用 switch 来判断 type 值
  5. reducer 必须是一个没有副作用的纯函数
    1. 为什么需要纯函数
      1. 纯函数有利于测试和调式
      2. 有利于还原数据
      3. 有利于将来和 react 结合时的优化
    2. 具体要求
      1. 不能改变参数,因此若要让状态变化,必须得到一个新的状态
      2. 不能有异步
      3. 不能对外部环境造成影响
  6. 由于在大中型项目中,操作比较复杂,数据结构也比较复杂,因此,需要对 reducer 进行细分。
    1. redux 提供了方法,可以帮助我们更加方便的合并 reducer
    2. combineReducers: 合并 reducer,得到一个新的 reducer,该新的 reducer 管理一个对象,该对象中的每一个属性交给对应的 reducer 管理。

Store

Store:用于保存数据

通过 createStore 方法创建的对象。

该对象的成员:

  • dispatch:分发一个 action
  • getState:得到仓库中当前的状态
  • replaceReducer:替换掉当前的 reducer
  • subscribe:注册一个监听器,监听器是一个无参函数,该分发一个 action 之后,会运行注册的监听器。该函数会返回一个函数,用于取消监听。可以注册多个监听器

createStore

返回一个对象:

  • dispatch:分发一个 action
  • getState:得到仓库中当前的状态
  • subscribe:注册一个监听器,监听器是一个无参函数,该分发一个 action 之后,会运行注册的监听器。该函数会返回一个函数,用于取消监听

bindActionCreators

combineReducers

组装 reducers,返回一个 reducer,数据使用一个对象表示,对象的属性名与传递的参数对象保持一致

Redux 中间件(Middleware)

中间件:类似于插件,可以在不影响原本功能、并且不改动原本代码的基础上,对其功能进行增强。在 Redux 中,中间件主要用于增强 dispatch 函数。

实现 Redux 中间件的基本原理,是更改仓库中的 dispatch 函数。

Redux 中间件书写:

  • 中间件本身是一个函数,该函数接收一个 store 参数,表示创建的仓库,该仓库并非一个完整的仓库对象,仅包含 getState,dispatch。该函数运行的时间,是在仓库创建之后运行。
    • 由于创建仓库后需要自动运行设置的中间件函数,因此,需要在创建仓库时,告诉仓库有哪些中间件
    • 需要调用 applyMiddleware 函数,将函数的返回结果作为 createStore 的第二或第三个参数。
  • 中间件函数必须返回一个 dispatch 创建函数
  • applyMiddleware 函数,用于记录有哪些中间件,它会返回一个函数
    • 该函数用于记录创建仓库的方法,然后又返回一个函数

redux-actions

不维护了:https://github.com/redux-utilities/redux-actions#looking-for-maintainers

该库用于简化 action-types、action-creator 以及 reducer 官网文档:https://redux-actions.js.org/

createAction(s)

createAction

该函数用于帮助你创建一个 action 创建函数(action creator)

createActions

该函数用于帮助你创建多个 action 创建函数

handleAction(s)

handleAction

简化针对单个 action 类型的 reducer 处理,当它匹配到对应的 action 类型后,会执行对应的函数

handleActions

简化针对多个 action 类型的 reducre 处理

combineActions

配合 createActions 和 handleActions 两个函数,用于处理多个 action-type 对应同一个 reducer 处理函数。

+ + + + + \ No newline at end of file diff --git a/react/component-communication.html b/react/component-communication.html new file mode 100644 index 00000000..6d4a6e5d --- /dev/null +++ b/react/component-communication.html @@ -0,0 +1,20 @@ + + + + + + React 组件通信 | Sunny's blog + + + + + + + + +
Skip to content
On this page

React 组件通信

1. 父子组件的通信方式?

父组件向子组件通信:父组件通过 props 向子组件传递需要的信息。

子组件向父组件通信:: props+回调的方式。

2. 跨级组件的通信方式?

父组件向子组件的子组件通信,向更深层子组件通信:

  • 使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了复杂度,并且这些 props 并不是中间组件自己需要的。
  • 使用 context,context 相当于一个大容器,可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。

3. 非嵌套关系组件的通信方式?

即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。

  • 可以使用自定义事件通信(发布订阅模式)
  • 可以通过 redux 等进行全局状态管理
  • 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点, 结合父子间通信方式进行通信。
+ + + + + \ No newline at end of file diff --git a/react/context.html b/react/context.html new file mode 100644 index 00000000..b9e25983 --- /dev/null +++ b/react/context.html @@ -0,0 +1,818 @@ + + + + + + Context 上下文 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Context 上下文

上下文:Context,表示做某一些事情的环境 React 中的上下文特点:

  1. 当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享
  2. 如果某个组件依赖了上下文,会导致该组件不再纯粹(纯粹指的是外部数据仅来源于属性 props)
  3. 一般情况下,用于第三方组件(通用组件)

旧的 API

创建上下文

只有类组件才可以创建上下文

  1. 给类组件书写静态属性 childContextTypes,使用该属性对上下文中的数据类型进行约束
  2. 添加实例方法 getChildContext,该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次 render 之后运行。

使用上下文中的数据

要求:如果要使用上下文中的数据,组件必须有一个静态属性 contextTypes,该属性描述了需要获取的上下文中的数据类型。

  1. 可以在组件的构造函数中,通过第二个参数,获取上下文数据。但是由于构造函数只会运行一次,后面上下文数据改变了,不会更新
  2. 从组件的 context 属性中获取
  3. 在函数组件中,通过第二个参数,获取上下文数据。数据并不会流动异常,只是调用了父组件的函数而已

    创建上下文只能是类组件,获取上下文可以是类组件或函数组件

jsx
import React, { Component } from "react";
+import PropTypes from "prop-types";
+const types = {
+  a: PropTypes.number,
+  b: PropTypes.string.isRequired,
+};
+function ChildA(props, context) {
+  // 在函数组件中,通过第二个参数,获取上下文数据
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <h2>
+        a{context.a} ; b{context.b}
+      </h2>
+      <ChildB />
+    </div>
+  );
+}
+ChildA.contextTypes = types; //前提条件contextTypes
+
+class ChildB extends React.Component {
+  static contextTypes = types;
+  // 方法2:从组件的context属性中获取
+  constructor(props, context) {
+    super(props, context);
+    console.log(this.context);
+  }
+  // 如果不写构造函数,会自动运行super
+  render() {
+    return (
+      <p>
+        ChildB 来自于上下文的数据 a:{this.context.a}, b:{this.context.b}
+      </p>
+    );
+  }
+}
+export default class OldContext extends Component {
+  /**
+   * 约束上下文中数据的类型
+   */
+  static childContextTypes = types;
+  /**
+   *
+   * @returns 得到上下文数据
+   */
+  getChildContext() {
+    console.log("获取上下文中的数据");
+    return {
+      a: 123,
+      b: "aaa",
+    };
+  }
+  render() {
+    return (
+      <div>
+        <ChildA />
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+import PropTypes from "prop-types";
+const types = {
+  a: PropTypes.number,
+  b: PropTypes.string.isRequired,
+};
+function ChildA(props, context) {
+  // 在函数组件中,通过第二个参数,获取上下文数据
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <h2>
+        a{context.a} ; b{context.b}
+      </h2>
+      <ChildB />
+    </div>
+  );
+}
+ChildA.contextTypes = types; //前提条件contextTypes
+
+class ChildB extends React.Component {
+  static contextTypes = types;
+  // 方法2:从组件的context属性中获取
+  constructor(props, context) {
+    super(props, context);
+    console.log(this.context);
+  }
+  // 如果不写构造函数,会自动运行super
+  render() {
+    return (
+      <p>
+        ChildB 来自于上下文的数据 a:{this.context.a}, b:{this.context.b}
+      </p>
+    );
+  }
+}
+export default class OldContext extends Component {
+  /**
+   * 约束上下文中数据的类型
+   */
+  static childContextTypes = types;
+  /**
+   *
+   * @returns 得到上下文数据
+   */
+  getChildContext() {
+    console.log("获取上下文中的数据");
+    return {
+      a: 123,
+      b: "aaa",
+    };
+  }
+  render() {
+    return (
+      <div>
+        <ChildA />
+      </div>
+    );
+  }
+}
+

上下文的数据变化

上下文中的数据不可以直接变化,最终都是通过状态改变

jsx
export default class OldContext extends Component {
+  /**
+   * 约束上下文中数据的类型
+   */
+  static childContextTypes = types;
+  state = {
+    a: 123,
+    b: "abc",
+  };
+  /**
+   *
+   * @returns 得到上下文数据
+   */
+  getChildContext() {
+    console.log("获取新的上下文");
+    return {
+      a: this.state.a, //来自状态了
+      b: this.state.b,
+    };
+  }
+  render() {
+    return (
+      <div>
+        <ChildA />
+        <button
+          onClick={() => {
+            this.setState({
+              //每次改变状态都会重新运行getChildContext。会导致重新渲染
+              a: this.state.a + 1,
+            });
+          }}
+        >
+          a+1
+        </button>
+      </div>
+    );
+  }
+}
+
export default class OldContext extends Component {
+  /**
+   * 约束上下文中数据的类型
+   */
+  static childContextTypes = types;
+  state = {
+    a: 123,
+    b: "abc",
+  };
+  /**
+   *
+   * @returns 得到上下文数据
+   */
+  getChildContext() {
+    console.log("获取新的上下文");
+    return {
+      a: this.state.a, //来自状态了
+      b: this.state.b,
+    };
+  }
+  render() {
+    return (
+      <div>
+        <ChildA />
+        <button
+          onClick={() => {
+            this.setState({
+              //每次改变状态都会重新运行getChildContext。会导致重新渲染
+              a: this.state.a + 1,
+            });
+          }}
+        >
+          a+1
+        </button>
+      </div>
+    );
+  }
+}
+

在上下文中加入一个处理函数,可以用于后代组件更改上下文的数据

jsx
import React, { Component } from "react";
+import PropTypes from "prop-types";
+const types = {
+  a: PropTypes.number,
+  b: PropTypes.string.isRequired,
+  onChangeA: PropTypes.func,
+};
+function ChildA(props, context) {
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <h2>
+        a{context.a} ; b{context.b}
+      </h2>
+      <ChildB />
+    </div>
+  );
+}
+ChildA.contextTypes = types; //前提条件contextTypes
+
+class ChildB extends React.Component {
+  static contextTypes = types;
+  constructor(props, context) {
+    super(props, context);
+    console.log(this.context);
+  }
+  render() {
+    return (
+      <p>
+        ChildB 来自于上下文的数据 a:{this.context.a}, b:{this.context.b}
+        <button
+          onClick={() => {
+            this.context.onChangeA(this.context.a + 2);
+          }}
+        >
+          子组件按钮a+2
+        </button>
+      </p>
+    );
+  }
+}
+export default class OldContext extends Component {
+  static childContextTypes = types;
+  state = {
+    a: 123,
+    b: "abc",
+  };
+  getChildContext() {
+    return {
+      a: this.state.a,
+      b: this.state.b,
+      onChangeA: (newA) => {
+        //这里要用箭头函数,否则报错
+        this.setState({
+          a: newA,
+        });
+      },
+    };
+  }
+  render() {
+    return (
+      <div>
+        <ChildA />
+        <button
+          onClick={() => {
+            this.setState({
+              a: this.state.a + 1,
+            });
+          }}
+        >
+          a+1
+        </button>
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+import PropTypes from "prop-types";
+const types = {
+  a: PropTypes.number,
+  b: PropTypes.string.isRequired,
+  onChangeA: PropTypes.func,
+};
+function ChildA(props, context) {
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <h2>
+        a{context.a} ; b{context.b}
+      </h2>
+      <ChildB />
+    </div>
+  );
+}
+ChildA.contextTypes = types; //前提条件contextTypes
+
+class ChildB extends React.Component {
+  static contextTypes = types;
+  constructor(props, context) {
+    super(props, context);
+    console.log(this.context);
+  }
+  render() {
+    return (
+      <p>
+        ChildB 来自于上下文的数据 a:{this.context.a}, b:{this.context.b}
+        <button
+          onClick={() => {
+            this.context.onChangeA(this.context.a + 2);
+          }}
+        >
+          子组件按钮a+2
+        </button>
+      </p>
+    );
+  }
+}
+export default class OldContext extends Component {
+  static childContextTypes = types;
+  state = {
+    a: 123,
+    b: "abc",
+  };
+  getChildContext() {
+    return {
+      a: this.state.a,
+      b: this.state.b,
+      onChangeA: (newA) => {
+        //这里要用箭头函数,否则报错
+        this.setState({
+          a: newA,
+        });
+      },
+    };
+  }
+  render() {
+    return (
+      <div>
+        <ChildA />
+        <button
+          onClick={() => {
+            this.setState({
+              a: this.state.a + 1,
+            });
+          }}
+        >
+          a+1
+        </button>
+      </div>
+    );
+  }
+}
+

新版 API

旧版 API 存在严重的效率问题,并且容易导致滥用

创建上下文

上下文是一个独立于组件的对象,该对象通过 React.createContext(默认值)创建

返回的是一个包含两个属性的对象

  1. Provider 属性:生产者。一个组件,该组件会创建一个上下文,该组件有一个 value 属性,通过该属性,可以为其数据赋值
    1. 同一个 Provider,不要用到多个组件中,如果需要在其他组件中使用该数据,应该考虑将数据提升到更高的层次

使用类组件获取上下文

jsx
import React, { Component } from "react";
+
+const ctx = React.createContext(); //里面可以填默认值
+
+// console.log(ctx)
+
+function ChildA(props) {
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <ChildB />
+    </div>
+  );
+}
+
+class ChildB extends React.Component {
+  static contextType = ctx;
+  render() {
+    return (
+      <div>
+        ChildB,来自上下文的数据:a:{this.context.a},b:{this.context.b}
+        <button
+          onClick={() => {
+            this.context.changeA(this.context.a + 2);
+          }}
+        >
+          后代组件的按钮,点击a+2
+        </button>
+      </div>
+    );
+  }
+}
+export default class NewContext extends Component {
+  state = {
+    a: 0,
+    b: "abc",
+    changeA: (newA) => {
+      this.setState({
+        a: newA,
+      });
+    },
+  };
+  /* render() {
+        const Provider = ctx.Provider;//可以把ctx.Provider直接当组件用<ctx.Provider></ctx.Provider>
+        return (
+            <Provider value={this.state}>
+                <div></div>
+            </Provider>
+        )
+    } */
+  render() {
+    return (
+      <ctx.Provider value={this.state}>
+        <div>
+          <ChildA />
+          <button
+            onClick={() => {
+              this.setState({
+                a: this.state.a + 1,
+              });
+            }}
+          >
+            父组件的按钮a+1
+          </button>
+        </div>
+      </ctx.Provider>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+const ctx = React.createContext(); //里面可以填默认值
+
+// console.log(ctx)
+
+function ChildA(props) {
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <ChildB />
+    </div>
+  );
+}
+
+class ChildB extends React.Component {
+  static contextType = ctx;
+  render() {
+    return (
+      <div>
+        ChildB,来自上下文的数据:a:{this.context.a},b:{this.context.b}
+        <button
+          onClick={() => {
+            this.context.changeA(this.context.a + 2);
+          }}
+        >
+          后代组件的按钮,点击a+2
+        </button>
+      </div>
+    );
+  }
+}
+export default class NewContext extends Component {
+  state = {
+    a: 0,
+    b: "abc",
+    changeA: (newA) => {
+      this.setState({
+        a: newA,
+      });
+    },
+  };
+  /* render() {
+        const Provider = ctx.Provider;//可以把ctx.Provider直接当组件用<ctx.Provider></ctx.Provider>
+        return (
+            <Provider value={this.state}>
+                <div></div>
+            </Provider>
+        )
+    } */
+  render() {
+    return (
+      <ctx.Provider value={this.state}>
+        <div>
+          <ChildA />
+          <button
+            onClick={() => {
+              this.setState({
+                a: this.state.a + 1,
+              });
+            }}
+          >
+            父组件的按钮a+1
+          </button>
+        </div>
+      </ctx.Provider>
+    );
+  }
+}
+
  1. Consumer 属性:后续讲解

使用上下文中的数据

  1. 在类组件中,直接使用 this.context 获取上下文数据
    1. 要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象
  2. 在函数组件中,需要使用 Consumer 来获取上下文数据
    1. Consumer 是一个组件
    2. 它的子节点,是一个函数(它的 props.children 需要传递一个函数)
    3. 不需要写静态属性

      使用函数组件获取上下文

jsx
const ctx = React.createContext();
+function ChildA(props) {
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <h2>
+        {/* 
+                写法1
+               <ctx.Consumer>
+                  这里已经明确使用哪个上下文了,不用写静态属性了
+                    {value => <>{value.a},{value.b}</>}
+                </ctx.Consumer> */}
+        {/* 写法2 */}
+        <ctx.Consumer
+          children={(value) => (
+            <>
+              {value.a},{value.b}
+            </>
+          )}
+        ></ctx.Consumer>
+      </h2>
+      <ChildB />
+    </div>
+  );
+}
+
const ctx = React.createContext();
+function ChildA(props) {
+  return (
+    <div>
+      <h1>ChildA</h1>
+      <h2>
+        {/* 
+                写法1
+               <ctx.Consumer>
+                  这里已经明确使用哪个上下文了,不用写静态属性了
+                    {value => <>{value.a},{value.b}</>}
+                </ctx.Consumer> */}
+        {/* 写法2 */}
+        <ctx.Consumer
+          children={(value) => (
+            <>
+              {value.a},{value.b}
+            </>
+          )}
+        ></ctx.Consumer>
+      </h2>
+      <ChildB />
+    </div>
+  );
+}
+

类组件中,也可以通过这个方法获取。比较常用,不需要 contextType 了

javascript
class ChildB extends Component {
+  render() {
+    return (
+      <ctx.Consumer>
+        {(value) => {
+          <p>
+            来自上下文的数据:{value.a}
+            <button onClick={value.changeA(value.a + 2)}>
+              后代组件按钮,点击a+2
+            </button>
+          </p>;
+        }}
+      </ctx.Consumer>
+    );
+  }
+}
+
class ChildB extends Component {
+  render() {
+    return (
+      <ctx.Consumer>
+        {(value) => {
+          <p>
+            来自上下文的数据:{value.a}
+            <button onClick={value.changeA(value.a + 2)}>
+              后代组件按钮,点击a+2
+            </button>
+          </p>;
+        }}
+      </ctx.Consumer>
+    );
+  }
+}
+

创建多个,互不干扰

jsx
import React, { Component } from "react";
+
+const ctx1 = React.createContext();
+const ctx2 = React.createContext();
+
+function ChildA(props) {
+  return (
+    <ctx2.Provider
+      value={{
+        a: 789,
+        c: "hello",
+      }}
+    >
+      <div>
+        <h1>ChildA</h1>
+        <h2>
+          <ctx1.Consumer>
+            {(value) => (
+              <>
+                {value.a}{value.b}
+              </>
+            )}
+          </ctx1.Consumer>
+        </h2>
+        <ChildB />
+      </div>
+    </ctx2.Provider>
+  );
+}
+
+class ChildB extends React.Component {
+  render() {
+    return (
+      <ctx1.Consumer>
+        {(value) => (
+          <>
+            <p>
+              ChildB,来自于上下文的数据:a: {value.a}, b:{value.b}
+              <button
+                onClick={() => {
+                  value.changeA(value.a + 2);
+                }}
+              >
+                后代组件的按钮,点击a+2
+              </button>
+            </p>
+            <p>
+              <ctx2.Consumer>
+                {(val) => (
+                  <>
+                    来自于ctx2的数据: a: {val.a}, c:{val.c}
+                  </>
+                )}
+              </ctx2.Consumer>
+            </p>
+          </>
+        )}
+      </ctx1.Consumer>
+    );
+  }
+}
+
+export default class NewContext extends Component {
+  state = {
+    a: 0,
+    b: "abc",
+    changeA: (newA) => {
+      this.setState({
+        a: newA,
+      });
+    },
+  };
+  render() {
+    return (
+      <ctx1.Provider value={this.state}>
+        <div>
+          <ChildA />
+
+          <button
+            onClick={() => {
+              this.setState({
+                a: this.state.a + 1,
+              });
+            }}
+          >
+            父组件的按钮,a加1
+          </button>
+        </div>
+      </ctx1.Provider>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+const ctx1 = React.createContext();
+const ctx2 = React.createContext();
+
+function ChildA(props) {
+  return (
+    <ctx2.Provider
+      value={{
+        a: 789,
+        c: "hello",
+      }}
+    >
+      <div>
+        <h1>ChildA</h1>
+        <h2>
+          <ctx1.Consumer>
+            {(value) => (
+              <>
+                {value.a},{value.b}
+              </>
+            )}
+          </ctx1.Consumer>
+        </h2>
+        <ChildB />
+      </div>
+    </ctx2.Provider>
+  );
+}
+
+class ChildB extends React.Component {
+  render() {
+    return (
+      <ctx1.Consumer>
+        {(value) => (
+          <>
+            <p>
+              ChildB,来自于上下文的数据:a: {value.a}, b:{value.b}
+              <button
+                onClick={() => {
+                  value.changeA(value.a + 2);
+                }}
+              >
+                后代组件的按钮,点击a+2
+              </button>
+            </p>
+            <p>
+              <ctx2.Consumer>
+                {(val) => (
+                  <>
+                    来自于ctx2的数据: a: {val.a}, c:{val.c}
+                  </>
+                )}
+              </ctx2.Consumer>
+            </p>
+          </>
+        )}
+      </ctx1.Consumer>
+    );
+  }
+}
+
+export default class NewContext extends Component {
+  state = {
+    a: 0,
+    b: "abc",
+    changeA: (newA) => {
+      this.setState({
+        a: newA,
+      });
+    },
+  };
+  render() {
+    return (
+      <ctx1.Provider value={this.state}>
+        <div>
+          <ChildA />
+
+          <button
+            onClick={() => {
+              this.setState({
+                a: this.state.a + 1,
+              });
+            }}
+          >
+            父组件的按钮,a加1
+          </button>
+        </div>
+      </ctx1.Provider>
+    );
+  }
+}
+

注意细节

如果,上下文提供者(Context.Provider)中的 value 属性发生变化(Object.is 比较),会导致该上下文提供的所有后代元素全部重新渲染,无论该子元素是否有优化(无论 shouldComponentUpdate 函数返回什么结果)

上下文的应用场景

编写一套组件(有多个组件),这些组件之间需要相互配合才能最终完成功能

比如,我们要开发一套表单组件,使用方式如下

jsx
render(){
+  return (
+    <Form onSubmit={datas=>{
+        console.log(datas); //获取表单中的所有数据(对象)
+        /*
+                {
+                    loginId:xxxx,
+                    loginPwd:xxxx
+                }
+            */
+      }}>
+      <div>
+        账号: <Form.Input name="loginId" />
+      </div>
+      <div>
+        密码: <Form.Input name="loginPwd" type="password" />
+      </div>
+      <div>
+        <Form.Button>提交</Form.Button>
+      </div>
+    </Form>
+  );
+}
+
render(){
+  return (
+    <Form onSubmit={datas=>{
+        console.log(datas); //获取表单中的所有数据(对象)
+        /*
+                {
+                    loginId:xxxx,
+                    loginPwd:xxxx
+                }
+            */
+      }}>
+      <div>
+        账号: <Form.Input name="loginId" />
+      </div>
+      <div>
+        密码: <Form.Input name="loginPwd" type="password" />
+      </div>
+      <div>
+        <Form.Button>提交</Form.Button>
+      </div>
+    </Form>
+  );
+}
+

对 React context 的理解

在 React 中,数据传递一般使用 props 传递数据,维持单向数据流,这样可以让组件之间的关系变得简单且可预测,但是单项数据流在某些场景中并不适用。单纯一对的父子组件传递并无问题,但要是组件之间层层依赖深入,props 就需要层层传递显然,这样做太繁琐了。

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

可以把 context 当做是特定一个组件树内共享的 store,用来做数据传递。简单说就是,当你不想在组件树中通过逐层传递 props 或者 state 的方式来传递数据时,可以使用 Context 来实现跨层级的组件数据传递。

JS 的代码块在执行期间,会创建一个相应的作用域链,这个作用域链记录着运行时 JS 代码块执行期间所能访问的活动对象,包括变量和函数,JS 程序通过作用域链访问到代码块内部或者外部的变量和函数。

假如以 JS 的作用域链作为类比,React 组件提供的 Context 对象其实就好比一个提供给子组件访问的作用域,而 Context 对象的属性可以看成作用域上的活动对象。由于组件 的 Context 由其父节点链上所有组件通 过 getChildContext()返回的 Context 对象组合而成,所以,组件通过 Context 是可以访问到其父组件链上所有节点组件提供的 Context 的属性。

为什么 React 并不推荐优先考虑使用 Context?

  • Context 目前还处于实验阶段,可能会在后面的发行版本中有很大的变化,事实上这种情况已经发生了,所以为了避免给今后升级带来大的影响和麻烦,不建议在 app 中使用 context。
  • 尽管不建议在 app 中使用 context,但是独有组件而言,由于影响范围小于 app,如果可以做到高内聚,不破坏组件树之间的依赖关系,可以考虑使用 context
  • 对于组件之间的数据通信或者状态管理,有效使用 props 或者 state 解决,然后再考虑使用第三方的成熟库进行解决,以上的方法都不是最佳的方案的时候,在考虑 context。
  • context 的更新需要通过 setState()触发,但是这并不是很可靠的,Context 支持跨组件的访问,但是如果中间的子组件通过一些方法不影响更新,比如 shouldComponentUpdate() 返回 false 那么不能保证 Context 的更新一定可以使用 Context 的子组件,因此,Context 的可靠性需要关注
+ + + + + \ No newline at end of file diff --git a/react/dva.html b/react/dva.html new file mode 100644 index 00000000..5ca766cc --- /dev/null +++ b/react/dva.html @@ -0,0 +1,20 @@ + + + + + + dva | Sunny's blog + + + + + + + + +
Skip to content
On this page

dva

DANGER

已经不维护了,仅供学习

官方网站:https://dvajs.com dva 不仅仅是一个第三方库,更是一个框架,它主要整合了 redux 的相关内容,让我们处理数据更加容易,实际上,dva 依赖了很多:react、react-router、redux、redux-saga、react-redux、connected-react-router 等。

dva 的使用

  1. dva 默认导出一个函数,通过调用该函数,可以得到一个 dva 对象
  2. dva 对象.router:路由方法,传入一个函数,该函数返回一个 React 节点,将来,应用程序启动后,会自动渲染该节点。
  3. dva 对象.start: 该方法用于启动 dva 应用程序,可以认为启动的就是 react 程序,该函数传入一个选择器,用于选中页面中的某个 dom 元素,react 会将内容渲染到该元素内部。
  4. dva 对象.model: 该方法用于定义一个模型,该模型可以理解为 redux 的 action、reducer、redux-saga 副作用处理的整合,整合成一个对象,将该对象传入 model 方法即可。
  5. namespace:命名空间,该属性是一个字符串,字符串的值,会被作为仓库中的属性保存
  6. state:该模型的默认状态
  7. reducers: 该属性配置为一个对象,对象中的每个方法就是一个 reducer,dva 约定,方法的名字,就是匹配的 action 类型
  8. effects: 处理副作用,底层是使用 redux-saga 实现的,该属性配置为一个对象,对象中的每隔方法均处理一个副作用,方法的名字,就是匹配的 action 类型。
    1. 函数的参数 1:action
    2. 参数 2:封装好的 saga/effects 对象
  9. subscriptions:配置为一个对象,该对象中可以写任意数量任意名称的属性,每个属性是一个函数,这些函数会在模型加入到仓库中后立即运行。
  10. 在 dva 中同步路由到仓库
  11. 在调用 dva 函数时,配置 history 对象
  12. 使用 ConnectedRouter 提供路由上下文
  13. 配置:
  14. history:同步到仓库的 history 对象
  15. initialState:创建 redux 仓库时,使用的默认状态
  16. onError: 当仓库的运行发生错误的时候,运行的函数
  17. onAction: 可以配置 redux 中间件
    1. 传入一个中间件对象
    2. 传入一个中间件数组
  18. onStateChange: 当仓库中的状态发生变化时运行的函数
  19. onReducer:对模型中的 reducer 的进一步封装
  20. onEffect:类似于对模型中的 effect 的进一步封装
  21. extraReducers:用于配置额外的 reducer,它是一个对象,对象的每一个属性是一个方法,每个方法就是一个需要合并的 reducer,方法名即属性名。
  22. extraEnhancers: 它是用于封装 createStore 函数的,dva 会将原来的仓库创建函数作为参数传递,返回一个新的用于创建仓库的函数。函数必须放置到数组中。

dva 插件

通过dva对象.use(插件),来使用插件,插件本质上就是一个对象,该对象与配置对象相同,dva 会在启动时,将传递的插件对象混合到配置中。

dva-loading

该插件会在仓库中加入一个状态,名称为 loading,它是一个对象,其中有以下属性

  • global:全局是否正在处理副作用(加载),只要有任何一个模型在处理副作用,则该属性为 true
  • models:一个对象,对象中的属性名以及属性的值,表示哪个对应的模型是否在处理副作用中(加载中)
  • effects:一个对象,对象中的属性名以及属性的值,表示是哪个 action 触发了副作用
+ + + + + \ No newline at end of file diff --git a/react/event.html b/react/event.html new file mode 100644 index 00000000..f2b84fea --- /dev/null +++ b/react/event.html @@ -0,0 +1,22 @@ + + + + + + React 事件机制 | Sunny's blog + + + + + + + + +
Skip to content
On this page

React 事件机制

React 事件机制

jsx
<div onClick={this.handleClick.bind(this)}>点我</div>
+
<div onClick={this.handleClick.bind(this)}>点我</div>
+

React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。

除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。

JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。

另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault

实现合成事件的目的如下:

  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

React 的事件和普通的 HTML 事件有什么不同?

区别:

  • 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
  • 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:

  • 兼容所有浏览器,更好的跨平台;
  • 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
  • 方便 react 统一管理和事务机制。

事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到 document 上合成事件才会执行。

React 组件中怎么做事件代理?它的原理是什么?

React 基于 Virtual DOM 实现了一个 SyntheticEvent 层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合 W3C 标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。

在 React 底层,主要对合成事件做了两件事:

  • 事件委派: React 会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
  • 自动绑定: React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。
+ + + + + \ No newline at end of file diff --git a/react/hooks.html b/react/hooks.html new file mode 100644 index 00000000..6df5def4 --- /dev/null +++ b/react/hooks.html @@ -0,0 +1,310 @@ + + + + + + Hooks | Sunny's blog + + + + + + + + +
Skip to content
On this page

HOOK 简介

组件:无状态组件(函数组件)、类组件 类组件中的麻烦:

  1. this 指向问题
  2. 繁琐的生命周期
  3. 其他问题

HOOK 专门用于增强函数组件的功能(HOOK 在类组件中是不能使用的),使之理论上可以成为类组件的替代品 官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件 HOOK(钩子)本质上是一个函数(命名上总是以use开头),该函数可以挂载任何功能 HOOK 种类:

  1. useState 解决状态
  2. useEffect 解决生命周期函数
  3. 其他...

不同 HOOK 能解决某一方面的功能

State Hook

State Hook 是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态

useState

  • 函数有一个参数,这个参数的值表示状态的默认值
  • 函数的返回值是一个数组,该数组一定包含两项
    • 第一项:当前状态的值
    • 第二项:改变状态的函数

一个函数组件中可以有多个状态,这种做法非常有利于横向切分关注点。 函数组件的写法

jsx
import React, { useState } from "react";
+// 函数组件的写法
+export default function App() {
+  // const arr = useState(0); // 不填默认undefined。使用一个状态,该状态默认值是0
+  // const n = arr[0]; //得到状态的值
+  // const setN = arr[1]; //得到一个函数,改函数用于改变状态
+  // 解构语法简化:
+  const [n, setN] = useState(0);
+  return (
+    <div>
+      <button
+        onClick={() => {
+          setN(n - 1);
+        }}
+      >
+        -
+      </button>
+      <span>{n}</span>
+      <button
+        onClick={() => {
+          setN(n + 1);
+        }}
+      >
+        +
+      </button>
+    </div>
+  );
+}
+
import React, { useState } from "react";
+// 函数组件的写法
+export default function App() {
+  // const arr = useState(0); // 不填默认undefined。使用一个状态,该状态默认值是0
+  // const n = arr[0]; //得到状态的值
+  // const setN = arr[1]; //得到一个函数,改函数用于改变状态
+  // 解构语法简化:
+  const [n, setN] = useState(0);
+  return (
+    <div>
+      <button
+        onClick={() => {
+          setN(n - 1);
+        }}
+      >
+        -
+      </button>
+      <span>{n}</span>
+      <button
+        onClick={() => {
+          setN(n + 1);
+        }}
+      >
+        +
+      </button>
+    </div>
+  );
+}
+

原理

注意的细节

  1. useState 最好写到函数的起始位置,便于阅读
  2. useState 严禁出现在代码块(判断、循环)中
  3. useState 返回的函数(数组的第二项),引用不变(节约内存空间)
  4. 使用函数改变数据,若数据和之前的数据完全相等(使用 Object.is 比较),不会导致重新渲染,以达到优化效率的目的。
  5. 使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。不要直接改变对象。setState 是用混合。

    应该横切开来,写第二个状态。如果的确需要在一起,就用展开运算符

jsx
export default function App() {
+  const [data, setData] = useState({
+    x: 1,
+    y: 2,
+  });
+  return (
+    <p>
+      x:{data.x},y:{data.y}
+      <button
+        onClick={() => {
+          setData({
+            ...data,
+            x: data.x + 1,
+          });
+        }}
+      >
+        x+1
+      </button>
+    </p>
+  );
+}
+
export default function App() {
+  const [data, setData] = useState({
+    x: 1,
+    y: 2,
+  });
+  return (
+    <p>
+      x:{data.x},y:{data.y}
+      <button
+        onClick={() => {
+          setData({
+            ...data,
+            x: data.x + 1,
+          });
+        }}
+      >
+        x+1
+      </button>
+    </p>
+  );
+}
+
  1. 如果要实现强制刷新组件

类组件:使用 forceUpdate 函数。不运行 shouldComponentUpdate 函数组件:使用一个空对象的 useState

jsx
import React, { useState } from "react";
+export default function App() {
+  console.log("App render");
+  const [, forceUpdate] = useState({});
+  return (
+    <div>
+      <p>
+        <button
+          onClick={() => {
+            forceUpdate({});
+          }}
+        >
+          强制刷新
+        </button>
+      </p>
+    </div>
+  );
+}
+
import React, { useState } from "react";
+export default function App() {
+  console.log("App render");
+  const [, forceUpdate] = useState({});
+  return (
+    <div>
+      <p>
+        <button
+          onClick={() => {
+            forceUpdate({});
+          }}
+        >
+          强制刷新
+        </button>
+      </p>
+    </div>
+  );
+}
+
  1. 如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象
  2. 和类组件的状态一样,函数组件中改变状态可能是异步的(在 DOM 事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。
jsx
import React, { useState } from "react";
+export default function App() {
+  console.log("render"); //两次改变合并成一个,只运行一次
+  const [n, setN] = useState(0);
+  return (
+    <div>
+      <span>{n}</span>
+      <button
+        onClick={() => {
+          // setN(n + 1); // 不会立即改变,事件运行完成后一起改变
+          // setN(n + 1); // 此时n仍是0
+          setN((prevN) => prevN + 1); // 传入的函数,在事件完成后统一运行
+          setN((prevN) => prevN + 1);
+        }}
+      >
+        +
+      </button>
+    </div>
+  );
+}
+
import React, { useState } from "react";
+export default function App() {
+  console.log("render"); //两次改变合并成一个,只运行一次
+  const [n, setN] = useState(0);
+  return (
+    <div>
+      <span>{n}</span>
+      <button
+        onClick={() => {
+          // setN(n + 1); // 不会立即改变,事件运行完成后一起改变
+          // setN(n + 1); // 此时n仍是0
+          setN((prevN) => prevN + 1); // 传入的函数,在事件完成后统一运行
+          setN((prevN) => prevN + 1);
+        }}
+      >
+        +
+      </button>
+    </div>
+  );
+}
+

Effect Hook

副作用:生命周期 componentDidMount,componentDidUpdate,componentWillUnmount 才能有副作用,其他都不能,因为都可能调用两遍,服务端渲染可能出问题

Effect Hook:用于在函数组件中处理副作用 副作用:

  1. ajax 请求
  2. 计时器
  3. 其他异步操作
  4. 更改真实 DOM 对象
  5. 本地存储
  6. 其他会对外部产生影响的操作

函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数

jsx
export default function EffectHook() {
+  const [n, setN] = useState(0);
+  useEffect(() => {
+    document.title = n;
+  });
+  return (
+    <div>
+      <button onClick={() => setN(n + 1)}>+</button>
+      {n}
+    </div>
+  );
+}
+
export default function EffectHook() {
+  const [n, setN] = useState(0);
+  useEffect(() => {
+    document.title = n;
+  });
+  return (
+    <div>
+      <button onClick={() => setN(n + 1)}>+</button>
+      {n}
+    </div>
+  );
+}
+

细节

  1. 副作用函数的运行时间点,是在页面完成真实的 UI 渲染之后。因此它的执行是异步的,并且不会阻塞浏览器 。所以会有些延迟

与类组件中 componentDidMount 和 componentDidUpdate 的区别 componentDidMount 和 componentDidUpdate,更改了真实 DOM,但是用户还没有看到 UI 更新,同步的。 useEffect 中的副作用函数,更改了真实 DOM,并且用户已经看到了 UI 更新,异步的。

  1. 每个函数组件中,可以多次使用 useEffect,但不要放入判断或循环等代码块中。

  2. useEffect 中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数

    1. 该函数运行时间点,在每次运行副作用函数之前
    2. 首次渲染组件不会运行
    3. 组件被销毁时一定会运行 window.timer => null
  3. useEffect 函数,可以传递第二个参数

    1. 第二个参数是一个数组
    2. 数组中记录该副作用的依赖数据
    3. 当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
    4. 所以,当传递了依赖数据之后,如果数据没有发生变化
      1. 副作用函数仅在第一次渲染后运行
      2. 清理函数仅在卸载组件后运行
  4. 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。

  5. 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。

自定义 Hook

State Hook: useState Effect Hook:useEffect

自定义 Hook:将一些常用的、跨越多个组件的 Hook 功能,抽离出去形成一个函数,该函数就是自定义 Hook,自定义 Hook,由于其内部需要使用 Hook 功能,所以它本身也需要按照 Hook 的规则实现:

  1. 函数名必须以 use 开头
  2. 调用自定义 Hook 函数时,应该放到顶层

例如:

  1. 很多组件都需要在第一次加载完成后,获取所有学生数据
  2. 很多组件都需要在第一次加载完成后,启动一个计时器,然后在组件销毁时卸载

使用 Hook 的时候,如果没有严格按照 Hook 的规则进行,eslint 的一个插件(eslint-plugin-react-hooks)会报出警告

Reducer Hook

Flux:Facebook 出品的一个数据流框架

  1. 规定了数据是单向流动的
  2. 数据存储在数据仓库中(目前,可以认为 state 就是一个存储数据的仓库)
  3. action 是改变数据的唯一原因(本质上就是一个对象,action 有两个属性)
    1. type:字符串,动作的类型
    2. payload:任意类型,动作发生后的附加信息
    3. 例如,如果是添加一个学生,action 可以描述为:
      1. { type:"addStudent", payload: {学生对象的各种信息} }
    4. 例如,如果要删除一个学生,action 可以描述为:
      1. { type:"deleteStudent", payload: 学生id }
  4. 具体改变数据的是一个函数,该函数叫做reducer
    1. 该函数接收两个参数
      1. state:表示当前数据仓库中的数据
      2. action:描述了如何去改变数据,以及改变数据的一些附加信息
    2. 该函数必须有一个返回结果,用于表示数据仓库变化之后的数据
      1. Flux 要求,对象是不可变的,如果返回对象,必须创建新的对象
    3. reducer 必须是纯函数,不能有任何副作用
  5. 如果要触发 reducer,不可以直接调用,而是应该调用一个辅助函数dispatch
    1. 该函数仅接收一个参数:action
    2. 该函数会间接去调用 reducer,以达到改变数据的目的

Context Hook

用于获取上下文数据

Callback Hook

函数名:useCallback

用于得到一个固定引用值的函数,通常用它进行性能优化

useCallback:

该函数有两个参数:

  1. 函数,useCallback 会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
  2. 数组,记录依赖项

该函数返回:引用相对固定的函数地址

Memo Hook

用于保持一些比较稳定的数据,通常用于性能优化

如果 React 元素本身的引用没有发生变化,一定不会重新渲染

Ref Hook

useRef 函数:

  1. 一个参数:默认值
  2. 返回一个固定的对象{current: 值}

可以做到每一个组件有一个唯一地址

ImperativeHandle Hook

函数:useImperativeHandleHook

LayoutEffect Hook

useEffect:浏览器渲染完成后,用户看到新的渲染结果之后

useLayoutEffectHook:完成了DOM改动,但还没有呈现给用户运行

应该尽量使用 useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用 useLayoutEffectHook。使用上和 useEffect 没有区别

DebugValue Hook

useDebugValue:用于将自定义 Hook 的关联数据显示到调试栏

如果创建的自定义 Hook 通用性比较高,可以选择使用 useDebugValue 方便调试

Hooks

对 React Hook 的理解,它的实现原理是什么

(1)类组件: 所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。内部预置了相当多的“现成的东西”等着我们去调度/定制,state 和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,只需要继承一个 React.Component 即可。使得类组件内部的逻辑难以实现拆分和复用。

(2)函数组件:函数组件就是以函数的形态存在的 React 组件。

类组件和函数组件之间,是面向对象和函数式编程这两套不同的设计思想之间的差异。而函数组件更加契合 React 框架的设计理念

React 组件本身的定位就是函数,一个输入数据、输出 UI 的函数。作为开发者,我们编写的是声明式的代码,而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这就意味着从原则上来讲,React 的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。函数组件就真正地将数据和渲染绑定到了一起。函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式。

(1)在组件之间复用状态逻辑很难

React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)解决此类问题可以使用 render props 和 高阶组件。但是这类方案需要重新组织组件结构,这可能会很麻烦,并且会使代码难以理解。由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。

可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使我们在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

(2)复杂组件变得难以理解

在组件中,每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

(3)难以理解的 class

为什么 useState 要使用数组而不是对象

  • 如果 useState 返回的是数组,那么使用者可以对数组中的元素命名,代码看起来也比较干净
  • 如果 useState 返回的是对象,在解构对象的时候必须要和 useState 内部实现返回的对象同名,想要使用多次的话,必须得设置别名才能使用返回值

下面来看看如果 useState 返回对象的情况:

javascript
// 第一次使用
+const { state, setState } = useState(false);
+// 第二次使用
+const { state: counter, setState: setCounter } = useState(0);
+
// 第一次使用
+const { state, setState } = useState(false);
+// 第二次使用
+const { state: counter, setState: setCounter } = useState(0);
+

这里可以看到,返回对象的使用方式还是挺麻烦的,更何况实际项目中会使用的更频繁。 *总结:__useState 返回的是 array 而不是 object 的原因就是为了__降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了

React Hook 的使用限制

React Hooks 的限制主要有两条:

  • 不要在循环、条件或嵌套函数中调用 Hook;
  • 在 React 的函数组件中调用 Hook。

那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。

  • 组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、render props 及状态管理框架。
  • 复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
  • 人和机器都很容易混淆类。常见的有 this 的问题,但在 React 团队中还有类难以优化的问题,希望在编译优化层面做出一些改进。

这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。

那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。

这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。

useEffect 与 useLayoutEffect 的区别

(1)共同点

  • 运用效果: useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。
  • 使用方式: useEffect 与 useLayoutEffect 两者底层的函数签名是完全一致的,都是调用的 mountEffectImpl 方法,在使用上也没什么差异,基本可以直接替换。

(2)不同点

  • 使用场景: useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
  • 使用效果: useEffect 是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变 DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect 是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变 DOM 后渲染),不会产生闪烁。useLayoutEffect 总是比 useEffect 先执行。

在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。

在平时开发中需要注意的问题和原因

(1)不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 Hook

这是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。

(2)使用 useState 时候,使用 push,pop,splice 等直接更改数组对象的坑

使用 push 直接更改数组无法获取到新值,应该采用析构方式,但是在 class 里面不会有这个问题。

(3)useState 设置状态的时候,只有第一次生效,后期需要更新状态,必须通过 useEffect

TableDeail 是一个公共组件,在调用它的父组件里面,我们通过 set 改变 columns 的值,以为传递给 TableDeail 的 columns 是最新的值,所以 tabColumn 每次也是最新的值,但是实际 tabColumn 是最开始的值,不会随着 columns 的更新而更新

(4)善用 useCallback

父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用 useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。

(5)不要滥用 useContext

可以使用基于 useContext 封装的状态管理工具。

React Hooks 和生命周期的关系?

函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。 但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useStateuseEffect()useLayoutEffect()

即:Hooks 组件(使用了 Hooks 的函数组件)有生命周期,而函数组件(未使用 Hooks 的函数组件)是没有生命周期的

下面是具体的 class 与 Hooks 的生命周期对应关系

  • constructor:函数组件不需要构造函数,可以通过调用 **useState 来初始化 state**。如果计算的代价比较昂贵,也可以传一个函数给 useState
javascript
const [num, UpdateNum] = useState(0);
+
const [num, UpdateNum] = useState(0);
+
  • getDerivedStateFromProps:一般情况下,我们不需要使用它,可以在渲染过程中更新 state,以达到实现 getDerivedStateFromProps 的目的。
javascript
function ScrollView({ row }) {
+  let [isScrollingDown, setIsScrollingDown] = useState(false);
+  let [prevRow, setPrevRow] = useState(null);
+  if (row !== prevRow) {
+    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
+    setIsScrollingDown(prevRow !== null && row > prevRow);
+    setPrevRow(row);
+  }
+  return `Scrolling down: ${isScrollingDown}`;
+}
+
function ScrollView({ row }) {
+  let [isScrollingDown, setIsScrollingDown] = useState(false);
+  let [prevRow, setPrevRow] = useState(null);
+  if (row !== prevRow) {
+    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
+    setIsScrollingDown(prevRow !== null && row > prevRow);
+    setPrevRow(row);
+  }
+  return `Scrolling down: ${isScrollingDown}`;
+}
+

React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。

  • shouldComponentUpdate:可以用 React.memo 包裹一个组件来对它的 props 进行浅比较
javascript
const Button = React.memo((props) => {  // 具体的组件});
+
+
const Button = React.memo((props) => {  // 具体的组件});
+
+

注意:React.memo 等效于PureComponent,它只浅比较 props。这里也可以使用useMemo` 优化每一个节点。

  • render:这是函数组件体本身。
  • componentDidMount, componentDidUpdateuseLayoutEffect 与它们两的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffectuseEffect 可以表达所有这些的组合。
javascript
// componentDidMount
+useEffect(() => {
+  // 需要在 componentDidMount 执行的内容
+}, []);
+useEffect(() => {
+  // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
+  document.title = `You clicked ${count} times`;
+  return () => {
+    // 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)
+    // 以及 componentWillUnmount 执行的内容
+  }; // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关
+}, [count]); // 仅在 count 更改时更新
+
// componentDidMount
+useEffect(() => {
+  // 需要在 componentDidMount 执行的内容
+}, []);
+useEffect(() => {
+  // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
+  document.title = `You clicked ${count} times`;
+  return () => {
+    // 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)
+    // 以及 componentWillUnmount 执行的内容
+  }; // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关
+}, [count]); // 仅在 count 更改时更新
+

请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 ,因此会使得额外操作很方便

  • componentWillUnmount:相当于 useEffect里面返回的 cleanup 函数
javascript
// componentDidMount/componentWillUnmount
+useEffect(() => {
+  // 需要在 componentDidMount 执行的内容
+  return function cleanup() {
+    // 需要在 componentWillUnmount 执行的内容
+  };
+}, []);
+
// componentDidMount/componentWillUnmount
+useEffect(() => {
+  // 需要在 componentDidMount 执行的内容
+  return function cleanup() {
+    // 需要在 componentWillUnmount 执行的内容
+  };
+}, []);
+
  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。
    class 组件Hooks 组件
    constructoruseState
    getDerivedStateFromPropsuseState 里面 update 函数
    shouldComponentUpdateuseMemo
    render函数本身
    componentDidMountuseEffect
    componentDidUpdateuseEffect
    componentWillUnmountuseEffect 里面返回的函数
    componentDidCatch
    getDerivedStateFromError

虚拟 DOM

对虚拟 DOM 的理解?

Virtual Dom 是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改 DOM 的重绘重排次数,提高渲染性能。

虚拟 DOM 是对 DOM 的抽象,这个对象是更加轻量级的对 DOM 的描述。它设计的最初目的,就是更好的跨平台,比如 node.js 就没有 DOM,如果想实现 SSR,那么一个方式就是借助虚拟 dom,因为虚拟 dom 本身是 js 对象。 在代码渲染到页面之前,vue 或者 react 会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实 dom 结构,最终渲染到页面。在每次数据发生变化前,虚拟 dom 都会缓存一份,变化之时,现在的虚拟 dom 会与缓存的虚拟 dom 进行比较。在 vue 或者 react 内部封装了 diff 算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。

另外现代前端框架的一个基本要求就是无须手动操作 DOM,一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动 DOM 操作可以大大提高开发效率。

为什么要用 Virtual DOM:

(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能

下面对比一下修改 DOM 时真实 DOM 操作和 Virtual DOM 的过程,来看一下它们重排重绘的性能消耗 ∶

  • 真实 DOM∶ 生成 HTML 字符串+ 重建所有的 DOM 元素
  • Virtual DOM∶ 生成 vNode + DOMDiff +必要的 DOM 更新

Virtual DOM 的更新 DOM 的准备工作耗费更多的时间,也就是 JS 层面,相比于更多的 DOM 操作它的消费是极其便宜的。尤雨溪在社区论坛中说道 ∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。 (2)跨平台 Virtual DOM 本质上是 JavaScript 的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp 等。

React diff 算法的原理是什么?

实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新。

具体的流程如下:

  • 真实的 DOM 首先会映射为虚拟 DOM;
  • 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
  • 根据 patch 去更新真实的 DOM,反馈到用户的界面上。 一个简单的例子:
javascript
import React from "react";
+export default class ExampleComponent extends React.Component {
+  render() {
+    if (this.props.isVisible) {
+      return <div className="visible">visbile</div>;
+    }
+    return <div className="hidden">hidden</div>;
+  }
+}
+
import React from "react";
+export default class ExampleComponent extends React.Component {
+  render() {
+    if (this.props.isVisible) {
+      return <div className="visible">visbile</div>;
+    }
+    return <div className="hidden">hidden</div>;
+  }
+}
+

这里,首先假定 ExampleComponent 可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM 操作是这样的,React 会创建一个 div 节点。

javascript
<div class="visible">visbile</div>
+
<div class="visible">visbile</div>
+

当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerText 为 hidden。这样一个生成补丁、更新差异的过程统称为 diff 算法。

diff 算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:

策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)

这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。

策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)

在组件比对的过程中:

  • 如果组件是同一类型则进行树比对;
  • 如果不是则直接放入补丁中。

只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、PureComponent 及 React.memo 可以提高性能的原因。

策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)

元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。

React key

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。

在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。

注意事项:

  • key 值一定要和具体的元素—一对应;
  • 尽量不要用数组的 index 去作为 key;
  • 不要在 render 的时候用随机数或者其他操作给元素加上不稳定的 key,这样造成的性能开销比不加 key 的情况下更糟糕。

虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么

虚拟 DOM 相对原生的 DOM 不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,虚拟 DOM 也会比 innerHTML 插入慢。它能保证性能下限,在真实 DOM 操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。

在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。

React 与 Vue 的 diff 算法

diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。

React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。

  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。
  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。

以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。

Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。

+ + + + + \ No newline at end of file diff --git a/react/index.html b/react/index.html new file mode 100644 index 00000000..daff4f2c --- /dev/null +++ b/react/index.html @@ -0,0 +1,936 @@ + + + + + + React OnePage | Sunny's blog + + + + + + + + +
Skip to content
On this page

React OnePage

React 是由Facebook研发的、用于解决 UI 复杂度的开源JavaScript 库,目前由 React 联合社区维护。

它不是框架,只是为了解决 UI 复杂度而诞生的一个库,并不是 MVVM MVC

React 的特点

  • 轻量:React 的开发版所有源码(包含注释)仅 3000 多行
  • 原生:所有的 React 的代码都是用原生 JS 书写而成的,不依赖其他任何库
  • 易扩展:React 对代码的封装程度较低,也没有过多的使用魔法,所以 React 中的很多功能都可以扩展。
  • 不依赖宿主环境:React 只依赖原生 JS 语言,不依赖任何其他东西,包括运行环境。因此,它可以被轻松的移植到浏览器、桌面应用、移动端。
  • 渐近式:React 并非框架,对整个工程没有强制约束力。这对与那些已存在的工程,可以逐步的将其改造为 React,而不需要全盘重写。
  • 单向数据流:所有的数据自顶而下的流动
  • 用 JS 代码声明界面
  • 组件化

Hello World

html
<!-- React的核心库,与宿主环境无关 -->
+<script
+  crossorigin
+  src="https://unpkg.com/react@16/umd/react.development.js"
+></script>
+<!-- 依赖核心库,将核心的功能与页面结合 -->
+<script
+  crossorigin
+  src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"
+></script>
+crossorigin跨域。script本身可以跨域,为何还要加呢?报错会显示详细信息
+
<!-- React的核心库,与宿主环境无关 -->
+<script
+  crossorigin
+  src="https://unpkg.com/react@16/umd/react.development.js"
+></script>
+<!-- 依赖核心库,将核心的功能与页面结合 -->
+<script
+  crossorigin
+  src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"
+></script>
+crossorigin跨域。script本身可以跨域,为何还要加呢?报错会显示详细信息
+

React.createElement

创建一个React 元素,称作虚拟 DOM,本质上是一个对象

  1. 参数 1:元素类型,如果是字符串,一个普通的 HTML 元素
  2. 参数 2:元素的属性,一个对象
  3. 后续参数:元素的子节点

最原始的写法:

javascript
// 创建一个span元素
+var span = React.createElement("span", {}, "一个span元素");
+// 创建一个h1元素
+创建一个H1元素;
+var h1 = React.createElement(
+  "h1",
+  {
+    title: "第一个React元素",
+  },
+  "Hello",
+  "World",
+  span
+);
+ReactDOM.render(h1, document.getElementById("root"));
+
// 创建一个span元素
+var span = React.createElement("span", {}, "一个span元素");
+// 创建一个h1元素
+创建一个H1元素;
+var h1 = React.createElement(
+  "h1",
+  {
+    title: "第一个React元素",
+  },
+  "Hello",
+  "World",
+  span
+);
+ReactDOM.render(h1, document.getElementById("root"));
+

React 本质:createElement 创建一个对象,把这些对象形成一种结构,最终交给 ReactDOM 渲染到页面上

JSX

JS 的扩展语法,需要使用 babel 进行转义。

html
<!-- 编译JSX -->
+<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
+<script type="text/babel">
+  // 创建一个span元素
+  var span = <span>一个span元素</span>;
+  // 创建一个H1元素
+  var h1 = (
+    <h1 title="第一个React元素">
+      Hello World <span>一个span元素</span>
+    </h1>
+  );
+  console.log(h1);
+  // 等价于直接写    最终都会转成React.createElement来执行
+  // ReactDOM.render(<h1 title="第一个React元素">Hello World <span>一个span元素</span></h1>, document.getElementById("root"));
+</script>
+
<!-- 编译JSX -->
+<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
+<script type="text/babel">
+  // 创建一个span元素
+  var span = <span>一个span元素</span>;
+  // 创建一个H1元素
+  var h1 = (
+    <h1 title="第一个React元素">
+      Hello World <span>一个span元素</span>
+    </h1>
+  );
+  console.log(h1);
+  // 等价于直接写    最终都会转成React.createElement来执行
+  // ReactDOM.render(<h1 title="第一个React元素">Hello World <span>一个span元素</span></h1>, document.getElementById("root"));
+</script>
+

emmet 配置:emmet 语法书写代码

json
"javascript": "javascriptreact"
+
"javascript": "javascriptreact"
+

ESLint:代码风格检查:合适的时候提出警告

ES7 React/Redux/GraphQL/React-Native snippets:快速代码编写

React Developer Tools

什么是 JSX

  • Facebook 起草的 JS 扩展语法

  • 本质是一个 JS 对象,会被 babel 编译,最终会被转换为 React.createElement

  • 每个 JSX 表达式,有且仅有一个根节点。原因:最终 React.createElement。

    • React.Fragment 简写为 <></>
    • 每个 JSX 元素必须结束(XML 规范)
  • 在 JSX 中嵌入表达式

  • 将表达式作为内容的一部分

    • null、undefined、false 不会显示
    • 普通对象,不可以作为子元素
    • 可以放置 React 元素对象
    • 数组:遍历数组,把数组的每一个元素当成子元素加进来,undefined null false 不会算,放普通对象会出错
  • 将表达式作为元素属性

  • 属性使用小驼峰命名法

  • 防止注入攻击

    • 自动编码
    • dangerouslySetInnerHTML
javascript
import React from "react";
+import ReactDOM from "react-dom";
+const a = 1234,
+  b = 2345;
+
+const div = (
+  <h1>
+    {a} * {b} = {a * b};
+  </h1>
+);
+// 底层实现上
+// const div = React.createElement("div",{},`${a}*${b} = ${a*b}`)
+ReactDOM.render(div, document.getElementById("root"));
+
import React from "react";
+import ReactDOM from "react-dom";
+const a = 1234,
+  b = 2345;
+
+const div = (
+  <h1>
+    {a} * {b} = {a * b};
+  </h1>
+);
+// 底层实现上
+// const div = React.createElement("div",{},`${a}*${b} = ${a*b}`)
+ReactDOM.render(div, document.getElementById("root"));
+

元素的不可变性

  • 虽然 JSX 元素是一个对象,但是该对象中的所有属性不可更改
javascript
div.props.num = 2;
+div.props.title = "sac";
+// 不能重新设置的原因:Object.freeze了
+
div.props.num = 2;
+div.props.title = "sac";
+// 不能重新设置的原因:Object.freeze了
+
  • 如果确实需要更改元素的属性,需要重新创建 JSX 元素
javascript
import React from "react";
+import ReactDOM from "react-dom";
+let num = 0;
+setInterval(() => {
+  num++;
+  const div = <div title="asa">{num}</div>;
+  ReactDOM.render(div, document.getElementById("root"));
+}, 1000);
+
import React from "react";
+import ReactDOM from "react-dom";
+let num = 0;
+setInterval(() => {
+  num++;
+  const div = <div title="asa">{num}</div>;
+  ReactDOM.render(div, document.getElementById("root"));
+}, 1000);
+

DOM 优化:效率很高,只变动 div 的内容

  1. 函数组件

返回一个 React 元素

  1. 类组件

必须继承 React.Component 必须提供 render 函数,用于渲染组件

组件的属性

  1. 对于函数组件,属性会作为一个对象的属性,传递给函数的参数
  2. 对于类组件,属性会作为一个对象的属性,传递给构造函数的参数

注意:组件的属性,应该使用小驼峰命名法

组件无法改变自身的属性。(只读)

之前学习的 React 元素,本质上,就是一个组件(内置组件) (都只读)

React 中的哲学:数据属于谁,谁才有权力改动(父组件也无权改)

React 中的数据,自顶而下流动

组件状态

组件状态:组件可以自行维护的数据

组件状态仅在类组件中有效

状态(state),本质上是类组件的一个属性,是一个对象

状态初始化

方法 1

javascript
class Demo {
+  constructor(props) {
+    super(props);
+    this.state = {
+      left: this.props.left,
+    };
+  }
+}
+
class Demo {
+  constructor(props) {
+    super(props);
+    this.state = {
+      left: this.props.left,
+    };
+  }
+}
+

方法 2

javascript
state = {
+  left: this.props.number,
+};
+
state = {
+  left: this.props.number,
+};
+

状态的变化

不能直接改变状态:因为 React 无法监控到状态发生了变化 必须使用 this.setState({})改变状态 一旦调用了 this.setState,会导致当前组件重新渲染

组件中的数据

  1. props:该数据是由组件的使用者传递的数据,所有权不属于组件自身,因此组件无法改变该数据
  2. state:该数据是由组件自身创建的,所有权属于组件自身,因此组件有权改变该数据。只有组件自身可以改

事件

在 React 中,组件的事件,本质上就是一个属性 如果没有特殊处理,在事件处理函数中,this 指向 undefined

  1. 使用 bind 函数,绑定 this
  2. 使用箭头函数

深入认识 setState

setState,它对状态的改变,可能是异步的

如果改变状态的代码处于某个 HTML 元素的事件中,则其是异步的,否则是同步

如果遇到某个事件中,需要同步调用多次,需要使用函数的方式得到最新状态

最佳实践:

  1. 把所有的 setState 当作是异步的
  2. 永远不要信任 setState 调用之后的状态
  3. 如果要使用改变之后的状态,需要使用回调函数(setState 的第二个参数)
  4. 如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态(setState 的第一个函数)

React 会对异步的 setState 进行优化,将多次 setState 进行合并(将多次状态改变完成后,再统一对 state 进行改变,然后触发 render)

生命周期

生命周期:组件从诞生到销毁会经历一系列的过程,该过程就叫做生命周期。React 在组件的生命周期中提供了一系列的钩子函数(类似于事件),可以让开发者在函数中注入代码,这些代码会在适当的时候运行。 生命周期仅存在于类组件中,函数组件每次调用都是重新运行函数,旧的组件即刻被销毁

旧版生命周期

React < 16.0.0

  1. constructor
    1. 同一个组件对象只会创建一次
    2. 不能在第一次挂载到页面之前,调用 setState,为了避免问题,构造函数中严禁使用 setState
  • 因为 setState 会导致重新渲染,在挂载之前,没必要重新渲染
  1. componentWillMount

    1. 正常情况下,和构造函数一样,它只会运行一次
    2. 可以使用 setState,但是为了避免 bug,不允许使用,因为在某些特殊情况下,该函数可能被调用多次
  2. render

    1. 返回一个虚拟 DOM,会被挂载到虚拟 DOM 树中,最终渲染到页面的真实 DOM 中
    2. render 可能不只运行一次,只要需要重新渲染,就会重新运行
    3. 严禁使用 setState,因为可能会导致无限递归渲染
  3. componentDidMount

    1. 只会执行一次
    2. 可以使用 setState
    3. 通常情况下,会将网络请求、启动计时器等一开始需要的操作,书写到该函数中
  4. 组件进入活跃状态

  5. componentWillReceiveProps

    1. 即将接收新的属性值
    2. 参数为新的属性对象
    3. 该函数可能会导致一些 bug,所以不推荐使用
  6. shouldComponentUpdate

    1. 指示 React 是否要重新渲染该组件,通过返回 true 和 false 来指定
    2. 默认情况下,会直接返回 true
    3. 属性直接被赋值了,不一定要值变化
  7. componentWillUpdate

    1. 组件即将被重新渲染
  8. componentDidUpdate

    1. 往往在该函数中使用 dom 操作,改变元素
  9. componentWillUnmount

  10. 通常在该函数中销毁一些组件依赖的资源,比如计时器

新版生命周期

React >= 16.0.0

React 官方认为,某个数据的来源必须是单一的。要么来自属性,要么来自状态。

  1. getDerivedStateFromProps

    1. 通过参数可以获取新的属性和状态
    2. 该函数是静态的
    3. 该函数的返回值会覆盖掉组件状态
    4. 该函数几乎是没有什么用
  2. getSnapshotBeforeUpdate

    1. 运行时间:真实的 DOM 构建完成,但还未实际渲染到页面中。
    2. 在该函数中,通常用于实现一些附加的 dom 操作
    3. 该函数的返回值,会作为 componentDidUpdate 的第三个参数
    4. 配合 componentDidUpdate 使用

传递元素内容

内置组件:div、h1、p

html
<div>asdfafasfafasdfasdf</div>
+
<div>asdfafasfafasdfasdf</div>
+

index.js

jsx
ReactDOM.render(<New html={<h1>sss</h1>} />, document.getElementById("root"));
+
ReactDOM.render(<New html={<h1>sss</h1>} />, document.getElementById("root"));
+

new.js

jsx
export default function New(props) {
+  return <div className="comp">{props.html}</div>;
+}
+
export default function New(props) {
+  return <div className="comp">{props.html}</div>;
+}
+

如果给自定义组件传递元素内容,则 React 会将元素内容作为 children 属性传递过去。 传递多个:

jsx
export default function Comp(props) {
+  return (
+    <div>
+      {props.children}
+      {props.content1}
+      {props.content2}
+    </div>
+  );
+}
+
+function Test() {
+  return (
+    <div>
+      <Comp content1={<h1>demo</h1>} content2={<h2>demo</h2>}>
+        <div>children</div>
+      </Comp>
+    </div>
+  );
+}
+
export default function Comp(props) {
+  return (
+    <div>
+      {props.children}
+      {props.content1}
+      {props.content2}
+    </div>
+  );
+}
+
+function Test() {
+  return (
+    <div>
+      <Comp content1={<h1>demo</h1>} content2={<h2>demo</h2>}>
+        <div>children</div>
+      </Comp>
+    </div>
+  );
+}
+

表单

受控组件和非受控组件

受控组件:组件的使用者,有能力完全控制该组件的行为和内容。通常情况下,受控组件往往没有自身的状态,其内容完全收到属性的控制。

非受控组件:组件的使用者,没有能力控制该组件的行为和内容,组件的行为和内容完全自行控制。

表单组件,默认情况下是非受控组件,一旦设置了表单组件的 value 属性,则其变为受控组件(单选和多选框需要设置 checked)

属性默认值 和 类型检查

属性默认值

之前是用混入解决的:Object.assign({},defaultProps,props) 通过一个静态属性defaultProps告知 react 属性默认值 函数本身的属性:函数也是对象 混合完成时间:

  1. 函数组件:调用函数之前就完成了
  2. 类组件:运行构造函数之前完成
javascript
// 函数组件
+
+export default function Comp(props) {
+    console.log(props)// 已经完成了混合
+    return (
+        <div>Comp</div>
+    )
+}
+Comp.defaultProps = {
+    a: 1,
+    b: 2
+}
+
+/// 类组件
+export default class Comp extends Component {
+    // static defaultProps = {
+    //     a: 1,
+    //     b: 2
+    // }
+    render() {
+        return (
+            <div>index</div>
+        )
+    }
+}
+// 类的本质也是函数
+Comp.defaultProps = {
+    a: 1
+}
+
+
// 函数组件
+
+export default function Comp(props) {
+    console.log(props)// 已经完成了混合
+    return (
+        <div>Comp</div>
+    )
+}
+Comp.defaultProps = {
+    a: 1,
+    b: 2
+}
+
+/// 类组件
+export default class Comp extends Component {
+    // static defaultProps = {
+    //     a: 1,
+    //     b: 2
+    // }
+    render() {
+        return (
+            <div>index</div>
+        )
+    }
+}
+// 类的本质也是函数
+Comp.defaultProps = {
+    a: 1
+}
+
+

属性类型检查

使用库:prop-types 对组件使用静态属性propTypes告知 react 如何检查属性 如果不按照类型传递属性的话,报警告,不影响代码执行。(只在开发阶段报警告) 可以不传递,但是一旦传递,必须正确

javascript
PropTypes.any//任意类型  通常用在列出所有属性,方便阅读;可以设置必填
+PropTypes.array//数组类型
+PropTypes.bool//布尔类型
+PropTypes.func//函数类型  事件
+PropTypes.number//数字类型
+PropTypes.object//对象类型
+PropTypes.string//字符串类型
+PropTypes.symbol//符号类型
+
+PropTypes.node
+//任何可以被渲染的内容,字符串、数字、React元素。如果传递null undefined,没有报错,因为没有进行非空验证
+PropTypes.element//react元素
+PropTypes.elementType//react元素类型
+PropTypes.instanceOf(构造函数)://必须是指定构造函数的实例
+PropTypes.oneOf([xxx, xxx])://枚举
+PropTypes.oneOfType([xxx, xxx]);  //属性类型必须是数组中的其中一个
+PropTypes.arrayOf(PropTypes.XXX)://必须是某一类型组成的数组
+PropTypes.objectOf(PropTypes.XXX)://对象由某一类型的值组成
+PropTypes.shape(对象): //属性必须是对象,并且满足指定的对象要求
+PropTypes.exact({...})://和shape一样,只是exact要求对象必须精确匹配传递的数据
+
+//自定义属性检查,如果有错误,返回错误对象即可
+属性: function(props, propName, componentName) {
+}
+export default class Comp extends Component {
+    static propTypes = {
+        score: function (props, propName, componentName) {
+            const val = props[propName];
+            // 必填
+            if (val === undefined || val === null) return new Error('Invalid')
+            // 必须是数字
+            if (typeof val !== 'number') return new Error('Invalid')
+        }
+    }
+    render() {
+        return (
+            <div>
+                {this.props.score}
+            </div>
+        )
+    }
+}
+
PropTypes.any://任意类型  通常用在列出所有属性,方便阅读;可以设置必填
+PropTypes.array://数组类型
+PropTypes.bool://布尔类型
+PropTypes.func://函数类型  事件
+PropTypes.number://数字类型
+PropTypes.object://对象类型
+PropTypes.string://字符串类型
+PropTypes.symbol://符号类型
+
+PropTypes.node:
+//任何可以被渲染的内容,字符串、数字、React元素。如果传递null undefined,没有报错,因为没有进行非空验证
+PropTypes.element://react元素
+PropTypes.elementType://react元素类型
+PropTypes.instanceOf(构造函数)://必须是指定构造函数的实例
+PropTypes.oneOf([xxx, xxx])://枚举
+PropTypes.oneOfType([xxx, xxx]);  //属性类型必须是数组中的其中一个
+PropTypes.arrayOf(PropTypes.XXX)://必须是某一类型组成的数组
+PropTypes.objectOf(PropTypes.XXX)://对象由某一类型的值组成
+PropTypes.shape(对象): //属性必须是对象,并且满足指定的对象要求
+PropTypes.exact({...})://和shape一样,只是exact要求对象必须精确匹配传递的数据
+
+//自定义属性检查,如果有错误,返回错误对象即可
+属性: function(props, propName, componentName) {
+}
+export default class Comp extends Component {
+    static propTypes = {
+        score: function (props, propName, componentName) {
+            const val = props[propName];
+            // 必填
+            if (val === undefined || val === null) return new Error('Invalid')
+            // 必须是数字
+            if (typeof val !== 'number') return new Error('Invalid')
+        }
+    }
+    render() {
+        return (
+            <div>
+                {this.props.score}
+            </div>
+        )
+    }
+}
+

HOC 高阶组件

HOF:Higher-Order Function, 高阶函数,以函数作为参数,并返回一个函数

HOC: Higher-Order Component, 高阶组件,以组件作为参数,并返回一个组件

通常,可以利用 HOC 实现横切关注点。

举例:20 个组件,每个组件在创建组件和销毁组件时,需要作日志记录 20 个组件,它们需要显示一些内容,得到的数据结构完全一致

注意

  1. 不要在 render 中使用高阶组件。在 render 内部的话重新创建,失去状态,浪费效率。在 render 外部则使用的是同一个类。
  2. 不要在高阶组件内部更改传入的组件(防止混乱)

ref

场景:希望直接使用 dom 元素中的某个方法,或者希望直接使用自定义组件中的某个方法

  1. ref 作用于内置的 html 组件,得到的将是真实的 dom 对象
  2. ref 作用于类组件,得到的将是类的实例
  3. ref 不能作用于函数组件
jsx
import React, { Component } from "react";
+
+/**
+ * 类组件
+ */
+class A extends Component {
+  method() {
+    console.log("调用了A组件的方法");
+  }
+  render() {
+    return <h1>组件A</h1>;
+  }
+}
+
+/**
+ * 内置html组件
+ */
+export default class Comp extends Component {
+  handleClick = () => {
+    // console.log(this)
+    this.refs.txt.focus();
+    this.refs.compA.method();
+  };
+  render() {
+    return (
+      <div>
+        <input type="text" ref="txt" />
+        <A ref="compA" />
+        <button onClick={this.handleClick}>聚焦</button>
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+/**
+ * 类组件
+ */
+class A extends Component {
+  method() {
+    console.log("调用了A组件的方法");
+  }
+  render() {
+    return <h1>组件A</h1>;
+  }
+}
+
+/**
+ * 内置html组件
+ */
+export default class Comp extends Component {
+  handleClick = () => {
+    // console.log(this)
+    this.refs.txt.focus();
+    this.refs.compA.method();
+  };
+  render() {
+    return (
+      <div>
+        <input type="text" ref="txt" />
+        <A ref="compA" />
+        <button onClick={this.handleClick}>聚焦</button>
+      </div>
+    );
+  }
+}
+

以上:ref 不再推荐使用字符串赋值,字符串赋值的方式将来可能会被移出。(效率低) 目前,ref 推荐使用对象或者是函数 对象 通过 React.createRef 函数创建

jsx
import React, { Component } from "react";
+
+export default class Comp extends Component {
+  constructor(props) {
+    super(props);
+    /* 
+            方法2:这样创建对象也可以
+          this.txt = {
+            current: null
+        } */
+    // 方法1 createRef
+    this.txt = React.createRef();
+    console.log(this.txt); //{current: null}
+    // 第一次渲染的时候会给current赋值
+    // current: input
+  }
+  handleClick = () => {
+    // console.log(this)
+    this.txt.current.focus();
+  };
+  render() {
+    return (
+      <div>
+        <input ref={this.txt} type="txt" />
+        <button onClick={this.handleClick}>聚焦</button>
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+export default class Comp extends Component {
+  constructor(props) {
+    super(props);
+    /* 
+            方法2:这样创建对象也可以
+          this.txt = {
+            current: null
+        } */
+    // 方法1 createRef
+    this.txt = React.createRef();
+    console.log(this.txt); //{current: null}
+    // 第一次渲染的时候会给current赋值
+    // current: input
+  }
+  handleClick = () => {
+    // console.log(this)
+    this.txt.current.focus();
+  };
+  render() {
+    return (
+      <div>
+        <input ref={this.txt} type="txt" />
+        <button onClick={this.handleClick}>聚焦</button>
+      </div>
+    );
+  }
+}
+

函数 函数的调用时间:

  1. componentDidMount 的时候会调用该函数
    1. 在 componentDidMount 事件中可以使用 ref
  2. 如果 ref 的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在 componentDidUpdate 之前
    1. 旧的函数被调用时,传递 null
    2. 新的函数被调用时,传递对象
  3. 如果 ref 所在的组件被卸载,会调用函数

谨慎使用 ref

能够使用属性和状态进行控制,就不要使用 ref。

  1. 调用真实的 DOM 对象中的方法
  2. 某个时候需要调用类组件的方法

Ref 转发

forwardRef forwardRef 方法:

  1. 参数,传递的是函数组件,不能是类组件,并且,函数组件需要有第二个参数来得到 ref
  2. 返回值,返回一个新的组件
jsx
import React, { Component } from "react";
+
+function A(props, ref) {
+  // console.log(props, ref)
+  /*  return <h1>组件A
+         <span>{props.word}</span>
+     </h1> */
+  return (
+    <h1 ref={ref}>
+      组件A
+      <span>{props.word}</span>
+    </h1>
+  );
+}
+// 传递函数组件A,得到一个新组件NewA
+const NewA = React.forwardRef(A); //一旦经过了这个操作,ref代表的就由函数组件自己控制了
+
+export default class App extends Component {
+  ARef = React.createRef();
+  // componentDidMount() {
+  //     console.log(this.ARef)//null  啥都得不到,让我自己去控制。依靠第二个参数了
+  // }
+
+  render() {
+    return (
+      <div>
+        {/* <A ref={this.ARef} /> */}
+        {/* 木得:this.ARef.current:h1即组件内部的根元素 */}
+        <NewA ref={this.ARef} word="aa" />
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+function A(props, ref) {
+  // console.log(props, ref)
+  /*  return <h1>组件A
+         <span>{props.word}</span>
+     </h1> */
+  return (
+    <h1 ref={ref}>
+      组件A
+      <span>{props.word}</span>
+    </h1>
+  );
+}
+// 传递函数组件A,得到一个新组件NewA
+const NewA = React.forwardRef(A); //一旦经过了这个操作,ref代表的就由函数组件自己控制了
+
+export default class App extends Component {
+  ARef = React.createRef();
+  // componentDidMount() {
+  //     console.log(this.ARef)//null  啥都得不到,让我自己去控制。依靠第二个参数了
+  // }
+
+  render() {
+    return (
+      <div>
+        {/* <A ref={this.ARef} /> */}
+        {/* 木得:this.ARef.current:h1即组件内部的根元素 */}
+        <NewA ref={this.ARef} word="aa" />
+      </div>
+    );
+  }
+}
+

类组件咋办呢?

方法 1

jsx
import React, { Component } from "react";
+
+/**
+ * ref:就是一个对象
+ * ref:{
+ *  current:null
+ * }
+ */
+class A extends React.Component {
+  render() {
+    return (
+      <h1 ref={this.props.ref1}>
+        组件A
+        <span>{this.props.words}</span>
+      </h1>
+    );
+  }
+}
+export default class App extends Component {
+  ARef = React.createRef();
+  componentDidMount() {
+    console.log(this.ARef);
+  }
+  render() {
+    return (
+      <div>
+        <A ref1={this.ARef} words="aa" />
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+/**
+ * ref:就是一个对象
+ * ref:{
+ *  current:null
+ * }
+ */
+class A extends React.Component {
+  render() {
+    return (
+      <h1 ref={this.props.ref1}>
+        组件A
+        <span>{this.props.words}</span>
+      </h1>
+    );
+  }
+}
+export default class App extends Component {
+  ARef = React.createRef();
+  componentDidMount() {
+    console.log(this.ARef);
+  }
+  render() {
+    return (
+      <div>
+        <A ref1={this.ARef} words="aa" />
+      </div>
+    );
+  }
+}
+

方法 2

jsx
import React, { Component } from "react";
+
+class A extends React.Component {
+  render() {
+    return (
+      <h1 ref={this.props.abc}>
+        组件A
+        <span>{this.props.words}</span>
+      </h1>
+    );
+  }
+}
+/**
+ * 函数包装一下
+ */
+const NewA = React.forwardRef((props, ref) => {
+  return <A {...props} abc={ref} />;
+});
+export default class App extends Component {
+  ARef = React.createRef();
+  componentDidMount() {
+    console.log(this.ARef);
+  }
+
+  render() {
+    return (
+      <div>
+        <NewA ref={this.ARef} words="aa" />
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+class A extends React.Component {
+  render() {
+    return (
+      <h1 ref={this.props.abc}>
+        组件A
+        <span>{this.props.words}</span>
+      </h1>
+    );
+  }
+}
+/**
+ * 函数包装一下
+ */
+const NewA = React.forwardRef((props, ref) => {
+  return <A {...props} abc={ref} />;
+});
+export default class App extends Component {
+  ARef = React.createRef();
+  componentDidMount() {
+    console.log(this.ARef);
+  }
+
+  render() {
+    return (
+      <div>
+        <NewA ref={this.ARef} words="aa" />
+      </div>
+    );
+  }
+}
+

高阶组件

jsx
import React from "react";
+import { A } from "./components/Comp";
+import withLog from "./HOC/withLog";
+let AComp = withLog(A);
+
+export default class App extends React.Component {
+  myRef = React.createRef();
+  componentDidMount() {
+    console.log(this.myRef); //logwrapper
+  }
+  render() {
+    return (
+      <div>
+        {/* ref:代表了日志记录了,但是我想代表A */}
+        <AComp ref={this.myRef} />
+      </div>
+    );
+  }
+}
+
import React from "react";
+import { A } from "./components/Comp";
+import withLog from "./HOC/withLog";
+let AComp = withLog(A);
+
+export default class App extends React.Component {
+  myRef = React.createRef();
+  componentDidMount() {
+    console.log(this.myRef); //logwrapper
+  }
+  render() {
+    return (
+      <div>
+        {/* ref:代表了日志记录了,但是我想代表A */}
+        <AComp ref={this.myRef} />
+      </div>
+    );
+  }
+}
+

改装 withLog.js

javascript
import React from "react";
+
+/**
+ * 高阶组件
+ * @param {*} comp 组件
+ */
+export default function withLog(Comp) {
+  class LogWrapper extends React.Component {
+    componentDidMount() {
+      console.log(`日志:组件${Comp.name}被创建了!${Date.now()}`);
+    }
+    componentWillUnmount() {
+      console.log(`日志:组件${Comp.name}被销毁了!${Date.now()}`);
+    }
+    render() {
+      //正常的属性
+      //forwardRef代表要转发的ref  {current:null}
+      const { forwardRef, ...rest } = this.props;
+      return (
+        <>
+          <Comp ref={forwardRef} {...rest} /> 最终,current指向他
+        </>
+      );
+    }
+  }
+
+  return React.forwardRef((props, ref) => {
+    return <LogWrapper {...props} forwardRef={ref} />;
+  });
+}
+
import React from "react";
+
+/**
+ * 高阶组件
+ * @param {*} comp 组件
+ */
+export default function withLog(Comp) {
+  class LogWrapper extends React.Component {
+    componentDidMount() {
+      console.log(`日志:组件${Comp.name}被创建了!${Date.now()}`);
+    }
+    componentWillUnmount() {
+      console.log(`日志:组件${Comp.name}被销毁了!${Date.now()}`);
+    }
+    render() {
+      //正常的属性
+      //forwardRef代表要转发的ref  {current:null}
+      const { forwardRef, ...rest } = this.props;
+      return (
+        <>
+          <Comp ref={forwardRef} {...rest} /> 最终,current指向他
+        </>
+      );
+    }
+  }
+
+  return React.forwardRef((props, ref) => {
+    return <LogWrapper {...props} forwardRef={ref} />;
+  });
+}
+

PureComponent

纯组件:用于避免不必要的渲染(运行 render),从而提高效率

优化:如果一个组件的属性和状态,都没有发生变化,该组件的重新渲染是没有必要的

PureComponent 是一个组件,如果某个组件继承自该组件,则该组件的 shouldComponentUpdate 会进行优化,对属性和状态进行钱比较。相等就不会重新渲染

注意场景:改动之前的数组,地址不会变化,浅比较会认为没发生变化。所以尽量不改动原数组,应该创建新数组

  1. 为了效率,尽量用纯组件
  2. 不要改变之前的状态,永远是创建新的状态覆盖之前的状态(Immutable 不可变对象)
jsx
// 加入要改变对象的某个值,不要改变原对象
+obj:{
+	...this.state.obj,
+    b:500
+}
+// 或者
+
+Object.assign({},this.state.obj, {b:500})
+
// 加入要改变对象的某个值,不要改变原对象
+obj:{
+	...this.state.obj,
+    b:500
+}
+// 或者
+
+Object.assign({},this.state.obj, {b:500})
+
  1. 有一个第三方库,Immutable.js 专门用于制作不可变对象

函数组件没有生命周期,每次需要重新调用函数,要防止重新调用函数,使用 React.memo 函数制作纯组件

jsx
export default React.memo(Task); //高阶组件  套了一层类组件
+
export default React.memo(Task); //高阶组件  套了一层类组件
+

实现原理

jsx
function memo(FuncComp) {
+  return class Memo extends PureComponent {
+    render() {
+      // return <FuncComp  {...this.props}/>
+      return <>{FuncComp(this.props)}</>;
+    }
+  };
+}
+
function memo(FuncComp) {
+  return class Memo extends PureComponent {
+    render() {
+      // return <FuncComp  {...this.props}/>
+      return <>{FuncComp(this.props)}</>;
+    }
+  };
+}
+

render props

Render Props – React

https://www.bilibili.com/video/BV13k4y117UG/?spm_id_from=333.788.recommend_more_video.1&vd_source=5ca956a1f37d0ed72cd1c453c15a3c03

有时候,某些组件的各种功能及其处理逻辑几乎完全相同,只是显示的界面不一样,建议下面的方式认选其一来解决重复代码的问题(横切关注点)

  1. render props
    1. 某个组件,需要某个属性
    2. 该属性是一个函数,函数的返回值用于渲染
    3. 函数的参数会传递为需要的数据
    4. 注意纯组件的属性(尽量避免每次传递的 render props 的地址不一致,应该把函数提出来)
    5. 通常该属性的名字叫做 render
  2. HOC

Portals

插槽:将一个 React 元素渲染到指定的 DOM 容器中 ReactDOM.createPortal(React 元素, 真实的 DOM 容器),该函数返回一个 React 元素

jsx
import React from "react";
+import ReactDOM from "react-dom";
+function ChildA() {
+  return ReactDOM.createPortal(
+    <div className="child-a">
+      <h1>ChildA</h1>
+      <ChildB />
+    </div>,
+    document.querySelector(".modal")
+  );
+}
+function ChildB() {
+  return (
+    <div className="child-b">
+      <h1>ChildB</h1>
+    </div>
+  );
+}
+export default function App() {
+  return (
+    <div className="app">
+      <h1>App</h1>
+      <ChildA />
+    </div>
+  );
+}
+
import React from "react";
+import ReactDOM from "react-dom";
+function ChildA() {
+  return ReactDOM.createPortal(
+    <div className="child-a">
+      <h1>ChildA</h1>
+      <ChildB />
+    </div>,
+    document.querySelector(".modal")
+  );
+}
+function ChildB() {
+  return (
+    <div className="child-b">
+      <h1>ChildB</h1>
+    </div>
+  );
+}
+export default function App() {
+  return (
+    <div className="app">
+      <h1>App</h1>
+      <ChildA />
+    </div>
+  );
+}
+

现象:真实的 DOM 结构变成了代码那样,但是组件结构变化。说明 React 虚拟 DOM 树和真实 DOM 树可以有差异 注意事件冒泡

  1. React 中的事件是包装过的
  2. 它的事件冒泡是根据虚拟 DOM 树(组件结构)来冒泡的,与真实的 DOM 树无关。
jsx
import React from "react";
+import ReactDOM from "react-dom";
+function ChildA() {
+  return ReactDOM.createPortal(
+    <div
+      className="child-a"
+      style={{
+        marginTop: 200,
+      }}
+    >
+      <h1>ChildA</h1>
+      <ChildB />
+    </div>,
+    document.querySelector(".modal")
+  );
+}
+function ChildB() {
+  return (
+    <div className="child-b">
+      <h1>ChildB</h1>
+    </div>
+  );
+}
+export default function App() {
+  return (
+    <div
+      className="app"
+      onClick={(e) => {
+        console.log("App被点击了", e.target);
+      }}
+    >
+      <h1>App</h1>
+      <ChildA />
+    </div>
+  );
+}
+
import React from "react";
+import ReactDOM from "react-dom";
+function ChildA() {
+  return ReactDOM.createPortal(
+    <div
+      className="child-a"
+      style={{
+        marginTop: 200,
+      }}
+    >
+      <h1>ChildA</h1>
+      <ChildB />
+    </div>,
+    document.querySelector(".modal")
+  );
+}
+function ChildB() {
+  return (
+    <div className="child-b">
+      <h1>ChildB</h1>
+    </div>
+  );
+}
+export default function App() {
+  return (
+    <div
+      className="app"
+      onClick={(e) => {
+        console.log("App被点击了", e.target);
+      }}
+    >
+      <h1>App</h1>
+      <ChildA />
+    </div>
+  );
+}
+

错误边界

默认情况下,若一个组件在渲染期间(render)发生错误,会导致整个组件树全部被卸载

错误边界:是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播

让某个组件捕获错误

  1. 编写生命周期函数 getDerivedStateFromError
    1. 静态函数
    2. 运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
    3. 注意:只有子组件发生错误,才会运行该函数。自己发生错误处理不了
    4. 该函数返回一个对象,React 会将该对象的属性覆盖掉当前组件的 state
    5. 参数:错误对象
    6. 通常,该函数用于改变状态
  2. 编写生命周期函数 componentDidCatch
    1. 实例方法
    2. 运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
    3. 通常,该函数用于记录错误消息

细节

某些错误,错误边界组件无法捕获

  1. 自身的错误
  2. 异步的错误
  3. 事件中的错误

这些错误,需要用 try catch 处理

总结:仅处理渲染子组件期间的同步错误

React 中的事件

这里的事件:React 内置的 DOM 组件中的事件

  1. 给 document 注册事件
  2. 几乎所有的元素的事件处理,均在 document 的事件中处理
    1. 一些不冒泡的事件,是直接在元素上监听
    2. 一些 document 上面没有的事件,直接在元素上监听
  3. 在 document 的事件处理,React 会根据虚拟 DOM 树的完成事件函数的调用
  4. React 的事件参数,并非真实的 DOM 事件参数,是 React 合成的一个对象,该对象类似于真实 DOM 的事件参数
    1. stopPropagation,阻止事件在虚拟 DOM 树中冒泡
    2. nativeEvent,可以得到真实的 DOM 事件对象
    3. 为了提高执行效率,React 使用事件对象池来处理事件对象

注意事项

  1. 如果给真实的 DOM 注册事件,阻止了事件冒泡,则会导致 react 的相应事件无法触发
  2. 如果给真实的 DOM 注册事件,事件会先于 React 事件运行
  3. 通过 React 的事件中阻止事件冒泡,无法阻止真实的 DOM 事件冒泡
  4. 可以通过 nativeEvent.stopImmediatePropagation(),阻止 document 上剩余事件的执行
  5. 在事件处理程序中,不要异步的使用事件对象,如果一定要使用,需要调用 persist 函数
+ + + + + \ No newline at end of file diff --git a/react/lifecycle.html b/react/lifecycle.html new file mode 100644 index 00000000..c23d2258 --- /dev/null +++ b/react/lifecycle.html @@ -0,0 +1,286 @@ + + + + + + 生命周期 | Sunny's blog + + + + + + + + +
Skip to content
On this page

生命周期

componentWillReceiveProps

该方法当props发生变化时执行,初始化render时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用this.setState()来更新你的组件状态,旧的属性还是可以通过this.props来获取,这里调用更新状态是安全的,并不会触发额外的render调用。

使用好处: 在这个生命周期中,可以在子组件的 render 函数执行前获取新的 props,从而更新子组件自己的 state。 可以将数据请求放在这里进行执行,需要传的参数则从 componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。

componentWillReceiveProps 在初始化 render 的时候不会执行,它会在 Component 接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染。

React 通常将组件生命周期分为三个阶段:

  • 装载阶段(Mount),组件第一次在 DOM 树中被渲染的过程;
  • 更新过程(Update),组件状态发生变化,重新更新渲染的过程;
  • 卸载过程(Unmount),组件从 DOM 树中被移除的过程;

组件挂载阶段

挂载阶段组件被创建,然后组件实例插入到 DOM 中,完成组件的第一次渲染,该过程只会发生一次,在此阶段会依次调用以下这些方法:

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount
(1)constructor

组件的构造函数,第一个被执行,若没有显式定义它,会有一个默认的构造函数,但是若显式定义了构造函数,我们必须在构造函数中执行 super(props),否则无法在构造函数中拿到 this。

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数Constructor

constructor 中通常只做两件事:

  • 初始化组件的 state
  • 给事件处理方法绑定 this
javascript
constructor(props) {
+  super(props);
+  // 不要在构造函数中调用 setState,可以直接给 state 设置初始值
+  this.state = { counter: 0 }
+  this.handleClick = this.handleClick.bind(this)
+}
+
constructor(props) {
+  super(props);
+  // 不要在构造函数中调用 setState,可以直接给 state 设置初始值
+  this.state = { counter: 0 }
+  this.handleClick = this.handleClick.bind(this)
+}
+
(2)getDerivedStateFromProps
javascript
static getDerivedStateFromProps(props, state)
+
static getDerivedStateFromProps(props, state)
+

这是个静态方法,所以不能在这个函数里使用 this,有两个参数 propsstate,分别指接收到的新参数和当前组件的 state 对象,这个函数会返回一个对象用来更新当前的 state 对象,如果不需要更新可以返回 null

该函数会在装载时,接收到新的 props 或者调用了 setStateforceUpdate 时被调用。如当接收到新的属性想修改 state ,就可以使用。

javascript
// 当 props.counter 变化时,赋值给 state
+class App extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      counter: 0,
+    };
+  }
+  static getDerivedStateFromProps(props, state) {
+    if (props.counter !== state.counter) {
+      return {
+        counter: props.counter,
+      };
+    }
+    return null;
+  }
+
+  handleClick = () => {
+    this.setState({
+      counter: this.state.counter + 1,
+    });
+  };
+  render() {
+    return (
+      <div>
+        <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
+      </div>
+    );
+  }
+}
+
// 当 props.counter 变化时,赋值给 state
+class App extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      counter: 0,
+    };
+  }
+  static getDerivedStateFromProps(props, state) {
+    if (props.counter !== state.counter) {
+      return {
+        counter: props.counter,
+      };
+    }
+    return null;
+  }
+
+  handleClick = () => {
+    this.setState({
+      counter: this.state.counter + 1,
+    });
+  };
+  render() {
+    return (
+      <div>
+        <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
+      </div>
+    );
+  }
+}
+

现在可以显式传入 counter ,但是这里有个问题,如果想要通过点击实现 state.counter 的增加,但这时会发现值不会发生任何变化,一直保持 props 传进来的值。这是由于在 React 16.4^ 的版本中 setStateforceUpdate 也会触发这个生命周期,所以当组件内部 state 变化后,就会重新走这个方法,同时会把 state 值赋值为 props 的值。因此需要多加一个字段来记录之前的 props 值,这样就会解决上述问题。具体如下:

javascript
// 这里只列出需要变化的地方
+class App extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      // 增加一个 preCounter 来记录之前的 props 传来的值
+      preCounter: 0,
+      counter: 0,
+    };
+  }
+  static getDerivedStateFromProps(props, state) {
+    // 跟 state.preCounter 进行比较
+    if (props.counter !== state.preCounter) {
+      return {
+        counter: props.counter,
+        preCounter: props.counter,
+      };
+    }
+    return null;
+  }
+  handleClick = () => {
+    this.setState({
+      counter: this.state.counter + 1,
+    });
+  };
+  render() {
+    return (
+      <div>
+        <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
+      </div>
+    );
+  }
+}
+
// 这里只列出需要变化的地方
+class App extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      // 增加一个 preCounter 来记录之前的 props 传来的值
+      preCounter: 0,
+      counter: 0,
+    };
+  }
+  static getDerivedStateFromProps(props, state) {
+    // 跟 state.preCounter 进行比较
+    if (props.counter !== state.preCounter) {
+      return {
+        counter: props.counter,
+        preCounter: props.counter,
+      };
+    }
+    return null;
+  }
+  handleClick = () => {
+    this.setState({
+      counter: this.state.counter + 1,
+    });
+  };
+  render() {
+    return (
+      <div>
+        <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
+      </div>
+    );
+  }
+}
+
(3)render

render 是 React 中最核心的方法,一个组件中必须要有这个方法,它会根据状态 state 和属性 props 渲染组件。这个函数只做一件事,就是返回需要渲染的内容,所以不要在这个函数内做其他业务逻辑,通常调用该方法会返回以下类型中一个:

  • React 元素:这里包括原生的 DOM 以及 React 组件;
  • 数组和 Fragment(片段):可以返回多个元素;
  • Portals(插槽):可以将子元素渲染到不同的 DOM 子树种;
  • 字符串和数字:被渲染成 DOM 中的 text 节点;
  • 布尔值或 null:不渲染任何内容。
(4)componentDidMount()

componentDidMount()会在组件挂载后(插入 DOM 树中)立即调。该阶段通常进行以下操作:

  • 执行依赖于 DOM 的操作;
  • 发送网络请求;
  • 添加订阅消息(会在 componentWillUnmount 取消订阅);

如果在 componentDidMount 中调用 setState ,就会触发一次额外的渲染,多调用了一次 render 函数,由于它是在浏览器刷新屏幕前执行的,所以用户对此是没有感知的,但是我应当避免这样使用,这样会带来一定的性能问题,尽量是在 constructor 中初始化 state 对象。

在组件装载之后,将计数数字变为 1:

javascript
class App extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      counter: 0,
+    };
+  }
+  componentDidMount() {
+    this.setState({
+      counter: 1,
+    });
+  }
+  render() {
+    return <div className="counter">counter值: {this.state.counter}</div>;
+  }
+}
+
class App extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      counter: 0,
+    };
+  }
+  componentDidMount() {
+    this.setState({
+      counter: 1,
+    });
+  }
+  render() {
+    return <div className="counter">counter值: {this.state.counter}</div>;
+  }
+}
+

组件更新阶段

当组件的 props 改变了,或组件内部调用了 setState/forceUpdate,会触发更新重新渲染,这个过程可能会发生多次。这个阶段会依次调用下面这些方法:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate
(1)shouldComponentUpdate
javascript
shouldComponentUpdate(nextProps, nextState);
+
shouldComponentUpdate(nextProps, nextState);
+

在说这个生命周期函数之前,来看两个问题:

  • setState 函数在任何情况下都会导致组件重新渲染吗?例如下面这种情况:
javascript
this.setState({ number: this.state.number });
+
this.setState({ number: this.state.number });
+
  • 如果没有调用 setState,props 值也没有变化,是不是组件就不会重新渲染?

第一个问题答案是 ,第二个问题如果是父组件重新渲染时,不管传入的 props 有没有变化,都会引起子组件的重新渲染。

那么有没有什么方法解决在这两个场景下不让组件重新渲染进而提升性能呢?这个时候 shouldComponentUpdate 登场了,这个生命周期函数是用来提升速度的,它是在重新渲染组件开始前触发的,默认返回 true,可以比较 this.propsnextPropsthis.statenextState 值是否变化,来确认返回 true 或者 false。当返回 false 时,组件的更新过程停止,后续的 rendercomponentDidUpdate 也不会被调用。

注意: 添加 shouldComponentUpdate 方法时,不建议使用深度相等检查(如使用 JSON.stringify()),因为深比较效率很低,可能会比重新渲染组件效率还低。而且该方法维护比较困难,建议使用该方法会产生明显的性能提升时使用。

(2)getSnapshotBeforeUpdate
javascript
getSnapshotBeforeUpdate(prevProps, prevState);
+
getSnapshotBeforeUpdate(prevProps, prevState);
+

这个方法在 render 之后,componentDidUpdate 之前调用,有两个参数 prevPropsprevState,表示更新之前的 propsstate,这个函数必须要和 componentDidUpdate 一起使用,并且要有一个返回值,默认是 null,这个返回值作为第三个参数传给 componentDidUpdate

(3)componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。 该阶段通常进行以下操作:

  • 当组件更新后,对 DOM 进行操作;
  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
javascript
componentDidUpdate(prevProps, prevState, snapshot){}
+
+
componentDidUpdate(prevProps, prevState, snapshot){}
+
+

该方法有三个参数:

  • prevProps: 更新前的 props
  • prevState: 更新前的 state
  • snapshot: getSnapshotBeforeUpdate()生命周期的返回值

组件卸载阶段

卸载阶段只有一个生命周期函数,componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作:

  • 清除 timer,取消网络请求或清除
  • 取消在 componentDidMount() 中创建的订阅等;

这个生命周期在一个组件被卸载和销毁之前被调用,因此你不应该再这个方法中使用 setState,因为组件一旦被卸载,就不会再装载,也就不会重新渲染。

错误处理阶段

componentDidCatch(error, info),此生命周期在后代组件抛出错误后被调用。 它接收两个参数 ∶

  • error:抛出的错误。
  • info:带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息

React 常见的生命周期如下:

React 常见生命周期的过程大致如下:

  • 挂载阶段,首先执行 constructor 构造方法,来创建组件
  • 创建完成之后,就会执行 render 方法,该方法会返回需要渲染的内容
  • 随后,React 会将需要渲染的内容挂载到 DOM 树上
  • 挂载完成之后就会执行 componentDidMount 生命周期函数
  • 如果我们给组件创建一个 props(用于组件通信)、调用 setState(更改 state 中的数据)、调用 forceUpdate(强制更新组件)时,都会重新调用 render 函数
  • render 函数重新执行之后,就会重新进行 DOM 树的挂载
  • 挂载完成之后就会执行 componentDidUpdate 生命周期函数
  • 当移除组件时,就会执行 componentWillUnmount 生命周期函数

React 主要生命周期总结:

  1. getDefaultProps:这个函数会在组件创建之前被调用一次(有且仅有一次),它被用来初始化组件的 Props;
  2. getInitialState:用于初始化组件的 state 值;
  3. componentWillMount:在组件创建后、render 之前,会走到 componentWillMount 阶段。这个阶段我个人一直没用过、非常鸡肋。后来 React 官方已经不推荐大家在 componentWillMount 里做任何事情、到现在 React16 直接废弃了这个生命周期,足见其鸡肋程度了;
  4. render:这是所有生命周期中唯一一个你必须要实现的方法。一般来说需要返回一个 jsx 元素,这时 React 会根据 props 和 state 来把组件渲染到界面上;不过有时,你可能不想渲染任何东西,这种情况下让它返回 null 或者 false 即可;
  5. componentDidMount:会在组件挂载后(插入 DOM 树中后)立即调用,标志着组件挂载完成。一些操作如果依赖获取到 DOM 节点信息,我们就会放在这个阶段来做。此外,这还是 React 官方推荐的发起 ajax 请求的时机。该方法和 componentWillMount 一样,有且仅有一次调用。

React 废弃了哪些生命周期?

被废弃的三个函数都是在 render 之前,因为 fber 的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次。另外的一个原因则是,React 想约束使用者,好的框架能够让人不得已写出容易维护和扩展的代码,这一点又是从何谈起,可以从新增加以及即将废弃的生命周期分析入手

1) componentWillMount

首先这个函数的功能完全可以使用 componentDidMount 和 constructor 来代替,异步获取的数据的情况上面已经说明了,而如果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在 constructor 中执行,除此之外,如果在 willMount 中订阅事件,但在服务端这并不会执行 willUnMount 事件,也就是说服务端会导致内存泄漏所以 componentWilIMount 完全可以不使用,但使用者有时候难免因为各 种各样的情况在 componentWilMount 中做一些操作,那么 React 为了约束开发者,干脆就抛掉了这个 API

2) componentWillReceiveProps

在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWilReceiveProps 中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的时,根据传入的某个值,直接定位到某个 Tab。为了解决这些问题,React 引入了第一个新的生命周期:getDerivedStateFromProps。它有以下的优点 ∶

  • getDSFP 是静态方法,在这里不能使用 this,也就是一个纯函数,开发者不能写出副作用的代码
  • 开发者只能通过 prevState 而不是 prevProps 来做对比,保证了 state 和 props 之间的简单关系以及不需要处理第一次渲染时 prevProps 为空的情况
  • 基于第一点,将状态变化(setState)和昂贵操作(tabChange)区分开,更加便于 render 和 commit 阶段操作或者说优化。

3) componentWillUpdate

与 componentWillReceiveProps 类似,许多开发者也会在 componentWillUpdate 中根据 props 的变化去触发一些回调 。 但不论是 componentWilReceiveProps 还 是 componentWilUpdate,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount 类 似, componentDidUpdate 也不存在这样的问题,一次更新中 componentDidUpdate 只会被调用一次,所以将原先写在 componentWillUpdate 中 的 回 调 迁 移 至 componentDidUpdate 就可以解决这个问题。

另外一种情况则是需要获取 DOM 元素状态,但是由于在 fber 中,render 可打断,可能在 wilMount 中获取到的元素状态很可能与实际需要的不同,这个通常可以使用第二个新增的生命函数的解决 getSnapshotBeforeUpdate(prevProps, prevState)

4) getSnapshotBeforeUpdate(prevProps, prevState)

返回的值作为 componentDidUpdate 的第三个参数。与 willMount 不同的是,getSnapshotBeforeUpdate 会在最终确定的 render 执行之前执行,也就是能保证其获取到的元素状态与 didUpdate 中获取到的元素状态相同。官方参考代码:

javascript
class ScrollingList extends React.Component {
+  constructor(props) {
+    super(props);
+    this.listRef = React.createRef();
+  }
+
+  getSnapshotBeforeUpdate(prevProps, prevState) {
+    // 我们是否在 list 中添加新的 items ?
+    // 捕获滚动位置以便我们稍后调整滚动位置。
+    if (prevProps.list.length < this.props.list.length) {
+      const list = this.listRef.current;
+      return list.scrollHeight - list.scrollTop;
+    }
+    return null;
+  }
+
+  componentDidUpdate(prevProps, prevState, snapshot) {
+    // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
+    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
+    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
+    if (snapshot !== null) {
+      const list = this.listRef.current;
+      list.scrollTop = list.scrollHeight - snapshot;
+    }
+  }
+
+  render() {
+    return <div ref={this.listRef}>{/* ...contents... */}</div>;
+  }
+}
+
class ScrollingList extends React.Component {
+  constructor(props) {
+    super(props);
+    this.listRef = React.createRef();
+  }
+
+  getSnapshotBeforeUpdate(prevProps, prevState) {
+    // 我们是否在 list 中添加新的 items ?
+    // 捕获滚动位置以便我们稍后调整滚动位置。
+    if (prevProps.list.length < this.props.list.length) {
+      const list = this.listRef.current;
+      return list.scrollHeight - list.scrollTop;
+    }
+    return null;
+  }
+
+  componentDidUpdate(prevProps, prevState, snapshot) {
+    // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
+    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
+    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
+    if (snapshot !== null) {
+      const list = this.listRef.current;
+      list.scrollTop = list.scrollHeight - snapshot;
+    }
+  }
+
+  render() {
+    return <div ref={this.listRef}>{/* ...contents... */}</div>;
+  }
+}
+

React 16.X 中 props 改变后在哪个生命周期中处理

在 getDerivedStateFromProps 中进行处理。

这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。

两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。

需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:

javascript
static getDerivedStateFromProps(nextProps, prevState) {
+    const {type} = nextProps;
+    // 当传入的type发生变化的时候,更新state
+    if (type !== prevState.type) {
+        return {
+            type,
+        };
+    }
+    // 否则,对于state不进行任何操作
+    return null;
+}
+
+
static getDerivedStateFromProps(nextProps, prevState) {
+    const {type} = nextProps;
+    // 当传入的type发生变化的时候,更新state
+    if (type !== prevState.type) {
+        return {
+            type,
+        };
+    }
+    // 否则,对于state不进行任何操作
+    return null;
+}
+
+

state 和 props 触发更新的生命周期分别有什么区别?

state 更新流程:

这个过程当中涉及的函数:

  1. shouldComponentUpdate: 当组件的 state 或 props 发生改变时,都会首先触发这个生命周期函数。它会接收两个参数:nextProps, nextState——它们分别代表传入的新 props 和新的 state 值。拿到这两个值之后,我们就可以通过一些对比逻辑来决定是否有 re-render(重渲染)的必要了。如果该函数的返回值为 false,则生命周期终止,反之继续;

注意:此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()

  1. componentWillUpdate:当组件的 state 或 props 发生改变时,会在渲染之前调用 componentWillUpdate。componentWillUpdate 是 React16 废弃的三个生命周期之一。过去,我们可能希望能在这个阶段去收集一些必要的信息(比如更新前的 DOM 信息等等),现在我们完全可以在 React16 的 getSnapshotBeforeUpdate 中去做这些事;
  2. componentDidUpdate:componentDidUpdate() 会在 UI 更新后会被立即调用。它接收 prevProps(上一次的 props 值)作为入参,也就是说在此处我们仍然可以进行 props 值对比(再次说明 componentWillUpdate 确实鸡肋哈)。

props 更新流程:

相对于 state 更新,props 更新后唯一的区别是增加了对 componentWillReceiveProps 的调用。关于 componentWillReceiveProps,需要知道这些事情:

  • componentWillReceiveProps:它在 Component 接受到新的 props 时被触发。componentWillReceiveProps 会接收一个名为 nextProps 的参数(对应新的 props 值)。该生命周期是 React16 废弃掉的三个生命周期之一。在它被废弃前,可以用它来比较 this.props 和 nextProps 来重新 setState。在 React16 中,用一个类似的新生命周期 getDerivedStateFromProps 来代替它。

React 中发起网络请求应该在哪个生命周期中进行?为什么?

对于异步请求,最好放在 componentDidMount 中去操作,对于同步的状态改变,可以放在 componentWillMount 中,一般用的比较少。

如果认为在 componentWillMount 里发起请求能提早获得结果,这种想法其实是错误的,通常 componentWillMount 比 componentDidMount 早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。

react 的生命周期: constructor() -> componentWillMount() -> render() -> componentDidMount()

上面这些方法的调用是有次序的,由上而下依次调用。

  • constructor 被调用是在组件准备要挂载的最开始,此时组件尚未挂载到网页上。
  • componentWillMount 方法的调用在 constructor 之后,在 render 之前,在这方法里的代码调用 setState 方法不会触发重新 render,所以它一般不会用来作加载数据之用。
  • componentDidMount 方法中的代码,是在组件已经完全挂载到网页上才会调用被执行,所以可以保证数据的加载。此外,在这方法中调用 setState 方法,会触发重新渲染。所以,官方设计这个方法就是用来加载外部数据用的,或处理其他的副作用代码。与组件上的数据无关的加载,也可以在 constructor 里做,但 constructor 是做组件 state 初绐化工作,并不是做加载数据这工作的,constructor 里也不能 setState,还有加载的时间太长或者出错,页面就无法加载出来。所以有副作用的代码都会集中在 componentDidMount 方法里。

总结:

  • 跟服务器端渲染(同构)有关系,如果在 componentWillMount 里面获取数据,fetch data 会执行两次,一次在服务器端一次在客户端。在 componentDidMount 中可以解决这个问题,componentWillMount 同样也会 render 两次。
  • 在 componentWillMount 中 fetch data,数据一定在 render 后才能到达,如果忘记了设置初始状态,用户体验不好。
  • react16.0 以后,componentWillMount 可能会被执行多次。

React 16 中新生命周期有哪些

关于 React16 开始应用的新生命周期:

可以看出,React16 自上而下地对生命周期做了另一种维度的解读:

  • Render 阶段:用于计算一些必要的状态信息。这个阶段可能会被 React 暂停,这一点和 React16 引入的 Fiber 架构(我们后面会重点讲解)是有关的;
  • Pre-commit 阶段:所谓“commit”,这里指的是“更新真正的 DOM 节点”这个动作。所谓 Pre-commit,就是说我在这个阶段其实还并没有去更新真实的 DOM,不过 DOM 信息已经是可以读取的了;
  • Commit 阶段:在这一步,React 会完成真实 DOM 的更新工作。Commit 阶段,我们可以拿到真实 DOM(包括 refs)。

与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、“卸载”这三个广义的划分方式。它们分别对应到:

  • 挂载过程:
    • constructor
    • getDerivedStateFromProps
    • render
    • componentDidMount
  • 更新过程:
    • getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  • 卸载过程:
    • componentWillUnmount
+ + + + + \ No newline at end of file diff --git a/react/react-interview.html b/react/react-interview.html new file mode 100644 index 00000000..b5b5e683 --- /dev/null +++ b/react/react-interview.html @@ -0,0 +1,1450 @@ + + + + + + 那些年,被问烂了的 React 面试题 | Sunny's blog + + + + + + + + +
Skip to content
On this page

那些年,被问烂了的 React 面试题

React 组件命名推荐的方式是哪个?

通过引用而不是使用来命名组件 displayName。

使用 displayName 命名组件:

javascript
export default React.createClass({  displayName: 'TodoApp',  // ...})
+
export default React.createClass({  displayName: 'TodoApp',  // ...})
+

React 推荐的方法:

javascript
export default class TodoApp extends React.Component {  // ...}
+
export default class TodoApp extends React.Component {  // ...}
+

react 最新版本解决了什么问题,增加了哪些东西

React 16.x 的三大新特性 Time Slicing、Suspense、 hooks

  • Time Slicing(解决 CPU 速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然保持有良好的性能
  • Suspense (解决网络 IO 问题) 和 lazy 配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从 react 出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T 异步但是同步的写法,这是最好的解决异步问题的方式
  • 提供了一个内置函数 componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件。

(1)React16.8 加入 hooks,让 React 函数式组件更加灵活,hooks 之前,React 存在很多问题:

  • 在组件间复用状态逻辑很难
  • 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。
  • class 组件的 this 指向问题
  • 难以记忆的生命周期

hooks 很好的解决了上述问题,hooks 提供了很多方法

  • useState 返回有状态值,以及更新这个状态值的函数
  • useEffect 接受包含命令式,可能有副作用代码的函数。
  • useContext 接受上下文对象(从 React.createContext 返回的值)并返回当前上下文值,
  • useReducer useState 的替代方案。接受类型为 (state,action)=> newState 的 reducer,并返回与 dispatch 方法配对的当前状态。
  • useCalLback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯的一个记忆函数 o useRef 返回一个可变的 ref 对象,其 Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。
  • useImperativeMethods 自定义使用 ref 时公开给父组件的实例值
  • useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发
  • useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染

(2)React16.9

  • 重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
  • 废弃 javascrip:形式的 URL。以 javascript:开头的 URL 非常容易遭受攻击,造成安全漏洞。
  • 废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。
  • act()也支持异步函数,并且你可以在调用它时使用 await。
  • 使用 <React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回归可能会很方便

(3)React16.13.0

  • 支持在渲染期间调用 setState,但仅适用于同一组件
  • 可检测冲突的样式规则并记录警告
  • 废弃 unstable_createPortal,使用 CreatePortal
  • 将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。

react 实现一个全局的 dialog

React 数据持久化有什么实践吗?

封装数据持久化组件:

javascript
let storage = {
+  // 增加
+  set(key, value) {
+    localStorage.setItem(key, JSON.stringify(value));
+  },
+  // 获取
+  get(key) {
+    return JSON.parse(localStorage.getItem(key));
+  },
+  // 删除
+  remove(key) {
+    localStorage.removeItem(key);
+  },
+};
+export default Storage;
+
let storage = {
+  // 增加
+  set(key, value) {
+    localStorage.setItem(key, JSON.stringify(value));
+  },
+  // 获取
+  get(key) {
+    return JSON.parse(localStorage.getItem(key));
+  },
+  // 删除
+  remove(key) {
+    localStorage.removeItem(key);
+  },
+};
+export default Storage;
+

在 React 项目中,通过 redux 存储全局数据时,会有一个问题,如果用户刷新了网页,那么通过 redux 存储的全局数据就会被全部清空,比如登录信息等。这时就会有全局数据持久化存储的需求。首先想到的就是 localStorage,localStorage 是没有时间限制的数据存储,可以通过它来实现数据的持久化存储。

但是在已经使用 redux 来管理和存储全局数据的基础上,再去使用 localStorage 来读写数据,这样不仅是工作量巨大,还容易出错。那么有没有结合 redux 来达到持久数据存储功能的框架呢?当然,它就是redux-persist。redux-persist 会将 redux 的 store 中的数据缓存到浏览器的 localStorage 中。其使用步骤如下:

(1)首先要安装 redux-persist:

javascript
npm i redux-persist
+
npm i redux-persist
+

(2)对于 reducer 和 action 的处理不变,只需修改 store 的生成代码,修改如下:

javascript
import { createStore } from "redux";
+import reducers from "../reducers/index";
+import { persistStore, persistReducer } from "redux-persist";
+import storage from "redux-persist/lib/storage";
+import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";
+const persistConfig = {
+  key: "root",
+  storage: storage,
+  stateReconciler: autoMergeLevel2, // 查看 'Merge Process' 部分的具体情况
+};
+const myPersistReducer = persistReducer(persistConfig, reducers);
+const store = createStore(myPersistReducer);
+export const persistor = persistStore(store);
+export default store;
+
import { createStore } from "redux";
+import reducers from "../reducers/index";
+import { persistStore, persistReducer } from "redux-persist";
+import storage from "redux-persist/lib/storage";
+import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";
+const persistConfig = {
+  key: "root",
+  storage: storage,
+  stateReconciler: autoMergeLevel2, // 查看 'Merge Process' 部分的具体情况
+};
+const myPersistReducer = persistReducer(persistConfig, reducers);
+const store = createStore(myPersistReducer);
+export const persistor = persistStore(store);
+export default store;
+

(3)在 index.js 中,将 PersistGate 标签作为网页内容的父标签:

javascript
import React from "react";
+import ReactDOM from "react-dom";
+import { Provider } from "react-redux";
+import store from "./redux/store/store";
+import { persistor } from "./redux/store/store";
+import { PersistGate } from "redux-persist/lib/integration/react";
+ReactDOM.render(
+  <Provider store={store}>
+    <PersistGate loading={null} persistor={persistor}>
+      {/*网页内容*/}
+    </PersistGate>
+  </Provider>,
+  document.getElementById("root")
+);
+
import React from "react";
+import ReactDOM from "react-dom";
+import { Provider } from "react-redux";
+import store from "./redux/store/store";
+import { persistor } from "./redux/store/store";
+import { PersistGate } from "redux-persist/lib/integration/react";
+ReactDOM.render(
+  <Provider store={store}>
+    <PersistGate loading={null} persistor={persistor}>
+      {/*网页内容*/}
+    </PersistGate>
+  </Provider>,
+  document.getElementById("root")
+);
+

这就完成了通过 redux-persist 实现 React 持久化本地数据存储的简单应用。

对 React 和 Vue 的理解,它们的异同

相似之处:

  • 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库
  • 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板。
  • 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能
  • 都有 props 的概念,允许组件间的数据传递
  • 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性

不同之处:

1)数据流

Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流

2)虚拟 DOM

Vue2.x 开始引入"Virtual DOM",消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。

  • Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
  • 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。

3)组件化

React 与 Vue 最大的不同是模板的编写。

  • Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。
  • React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。

具体来讲:React 中 render 函数是支持闭包特性的,所以我们 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 完组件之后,还需要在 components 中再声明下。

4)监听数据变化的实现原理不同

  • Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
  • React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。

5)高阶组件

react 可以通过高阶组件(Higher Order Components-- HOC)来扩展,而 vue 需要通过 mixins 来扩展。

原因高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不采用 HOC 来实现。

6)构建工具

两者都有自己的构建工具

  • React ==> Create React APP
  • Vue ==> vue-cli

7)跨平台

  • React ==> React Native
  • Vue ==> Weex

React 理念

(1)编写简单直观的代码

React 最大的价值不是高性能的虚拟 DOM、封装的事件机制、服务器端渲染,而是声明式的直观的编码方式。react 文档第一条就是声明式,React 使创建交互式 UI 变得轻而易举。为应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。 以声明式编写 UI,可以让代码更加可靠,且方便调试。

(2)简化可复用的组件

React 框架里面使用了简化的组件模型,但更彻底地使用了组件化的概念。React 将整个 UI 上的每一个功能模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成更大的组件。React 的组件具有如下的特性 ∶

  • 可组合:简单组件可以组合为复杂的组件
  • 可重用:每个组件都是独立的,可以被多个组件使用
  • 可维护:和组件相关的逻辑和 UI 都封装在了组件的内部,方便维护
  • 可测试:因为组件的独立性,测试组件就变得方便很多。

(3) Virtual DOM

真实页面对应一个 DOM 树。在传统页面的开发模式中,每次需要更新页面时,都要手动操作 DOM 来进行更新。 DOM 操作非常昂贵。在前端开发中,性能消耗最大的就是 DOM 操作,而且这部分代码会让整体项目的代码变得难 以维护。React 把真实 DOM 树转换成 JavaScript 对象树,也就是 Virtual DOM,每次数据更新后,重新计算 Virtual DOM,并和上一次生成的 Virtual DOM 做对比,对发生变化的部分做批量更新。React 也提供了直观的 shouldComponentUpdate 生命周期回调,来减少数据变化后不必要的 Virtual DOM 对比过程,以保证性能。

(4)函数式编程

React 把过去不断重复构建 UI 的过程抽象成了组件,且在给定参数的情况下约定渲染对应的 UI 界面。React 能充分利用很多函数式方法去减少冗余代码。此外,由于它本身就是简单函数,所以易于测试。

(5)一次学习,随处编写

无论现在正在使用什么技术栈,都可以随时引入 React 来开发新特性,而不需要重写现有代码。

React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。因为 React 组件可以映射为对应的原生控件。在输出的时候,是输出 Web DOM,还是 Android 控件,还是 iOS 控件,就由平台本身决定了。所以,react 很方便和其他平台集成

React 中 props.children 和 React.Children 的区别

在 React 中,当涉及组件嵌套,在父组件中使用props.children把所有子组件显示出来。如下:

如果想把父组件中的属性传给所有的子组件,需要使用React.Children方法。

jsx
// App
+import React from "react";
+import RadioOption from "./Comp";
+//父组件用,props是指父组件的props
+function renderChildren(props) {
+  return React.Children.map(props.children, (child) => {
+    if (child.type === RadioOption)
+      return React.cloneElement(child, {
+        //把父组件的props.name赋值给每个子组件
+        name: props.name,
+      });
+    else return child;
+  });
+}
+//父组件
+function RadioGroup(props) {
+  return <div>{renderChildren(props)}</div>;
+}
+function App() {
+  return (
+    <RadioGroup name="hello">
+      <RadioOption label="选项一" value="1" />
+      <RadioOption label="选项二" value="2" />
+      <RadioOption label="选项三" value="3" />
+    </RadioGroup>
+  );
+}
+export { App };
+
// App
+import React from "react";
+import RadioOption from "./Comp";
+//父组件用,props是指父组件的props
+function renderChildren(props) {
+  return React.Children.map(props.children, (child) => {
+    if (child.type === RadioOption)
+      return React.cloneElement(child, {
+        //把父组件的props.name赋值给每个子组件
+        name: props.name,
+      });
+    else return child;
+  });
+}
+//父组件
+function RadioGroup(props) {
+  return <div>{renderChildren(props)}</div>;
+}
+function App() {
+  return (
+    <RadioGroup name="hello">
+      <RadioOption label="选项一" value="1" />
+      <RadioOption label="选项二" value="2" />
+      <RadioOption label="选项三" value="3" />
+    </RadioGroup>
+  );
+}
+export { App };
+
jsx
// Comp
+function RadioOption(props) {
+  return (
+    <label>
+      <input type="radio" value={props.value} name={props.name} />
+      {props.label}
+    </label>
+  );
+}
+
+export default RadioOption;
+
// Comp
+function RadioOption(props) {
+  return (
+    <label>
+      <input type="radio" value={props.value} name={props.name} />
+      {props.label}
+    </label>
+  );
+}
+
+export default RadioOption;
+

以上,React.Children.map让我们对父组件的所有子组件又更灵活的控制。

React 的严格模式如何使用,有什么用处?

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。 可以为应用程序的任何部分启用严格模式。例如:

jsx
import React from "react";
+function ExampleApplication() {
+  return (
+    <div>
+      <Header />
+      <React.StrictMode>
+        <div>
+          <ComponentOne />
+          <ComponentTwo />
+        </div>
+      </React.StrictMode>
+      <Footer />
+    </div>
+  );
+}
+
import React from "react";
+function ExampleApplication() {
+  return (
+    <div>
+      <Header />
+      <React.StrictMode>
+        <div>
+          <ComponentOne />
+          <ComponentTwo />
+        </div>
+      </React.StrictMode>
+      <Footer />
+    </div>
+  );
+}
+

在上述的示例中,不会对 HeaderFooter 组件运行严格模式检查。但是,ComponentOneComponentTwo 以及它们的所有后代元素都将进行检查。

StrictMode 目前有助于:

  • 识别不安全的生命周期
  • 关于使用过时字符串 ref API 的警告
  • 关于使用废弃的 findDOMNode 方法的警告
  • 检测意外的副作用
  • 检测过时的 context API

在 React 中页面重新加载时怎样保留数据?

这个问题就设计到了数据持久化, 主要的实现方式有以下几种:

  • Redux: 将页面的数据存储在 redux 中,在重新加载页面时,获取 Redux 中的数据;
  • data.js: 使用 webpack 构建的项目,可以建一个文件,data.js,将数据保存 data.js 中,跳转页面后获取;
  • sessionStorge: 在进入选择地址页面之前,componentWillUnMount 的时候,将数据存储到 sessionStorage 中,每次进入页面判断 sessionStorage 中有没有存储的那个值,有,则读取渲染数据;没有,则说明数据是初始化的状态。返回或进入除了选择地址以外的页面,清掉存储的 sessionStorage,保证下次进入是初始化的数据
  • history API: History API 的 pushState 函数可以给历史记录关联一个任意的可序列化 state,所以可以在路由 push 的时候将当前页面的一些信息存到 state 中,下次返回到这个页面的时候就能从 state 里面取出离开前的数据重新渲染。react-router 直接可以支持。这个方法适合一些需要临时存储的场景。

同时引用这三个库 react.js、react-dom.js 和 babel.js 它们都有什么作用?

  • react:包含 react 所必须的核心代码
  • react-dom:react 渲染在不同平台所需要的核心代码
  • babel:将 jsx 转换成 React 代码的工具

React 必须使用 JSX 吗?

React 并不强制要求使用 JSX。当不想在构建环境中配置有关 JSX 编译时,不在 React 中使用 JSX 会更加方便;每个 JSX 元素只是调用 React.createElement(component, props, ...children) 的语法糖。因此,使用 JSX 可以完成的任何事情都可以通过纯 JavaScript 完成。

为什么使用 jsx 的组件中没有看到使用 react 却需要引入 react?

本质上来说 JSX 是React.createElement(component, props, ...children)方法的语法糖。在 React 17 之前,如果使用了 JSX,其实就是在使用 React, babel 会把组件转换为 CreateElement 形式。在 React 17 之后,就不再需要引入,因为 babel 已经可以帮我们自动引入 react。

在 React 中怎么使用 async/await?

async/await 是 ES7 标准中的新特性。如果是使用 React 官方的脚手架创建的项目,就可以直接使用。如果是在自己搭建的 webpack 配置的项目中使用,可能会遇到 regeneratorRuntime is not defined 的异常错误。那么我们就需要引入 babel,并在 babel 中配置使用 async/await。可以利用 babel 的 transform-async-to-module-method 插件来转换其成为浏览器支持的语法,虽然没有性能的提升,但对于代码编写体验要更好。

React.Children.map 和 js 的 map 有什么区别?

JavaScript 中的 map 不会对为 null 或者 undefined 的数据进行处理,而 React.Children.map 中的 map 可以处理 React.Children 为 null 或者 undefined 的情况。

对 React SSR 的理解

服务端渲染是数据与模版组成的 html,即 HTML = 数据 + 模版。将组件或页面通过服务器生成 html 字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。页面没使用服务渲染,当请求页面时,返回的 body 里为空,之后执行 js 将 html 结构注入到 body 里,结合 css 显示出来;

SSR 的优势:

  • 对 SEO 友好
  • 所有的模版、图片等资源都存在服务器端
  • 一个 html 返回所有数据
  • 减少 HTTP 请求
  • 响应快、用户体验好、首屏渲染快

1)更利于 SEO

不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本使用了 React 或者其它 MVVM 框架之后,页面大多数 DOM 元素都是在客户端根据 js 动态生成,可供爬虫抓取分析的内容大大减少。另外,浏览器爬虫不会等待我们的数据完成之后再去抓取页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行 JavaScript 脚本的最终 HTML,网络爬中就可以抓取到完整页面的信息。

2)更利于首屏渲染

首屏的渲染是 node 发送过来的 html 字符串,并不依赖于 js 文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。

SSR 的局限:

1)服务端压力较大

本来是通过客户端完成渲染,现在统一到服务端 node 服务去做。尤其是高并发访问的情况,会大量占用服务端 CPU 资源;

2)开发条件受限

在服务端渲染中,只会执行到 componentDidMount 之前的生命周期钩子,因此项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;

3)学习成本相对较高 除了对 webpack、MVVM 框架要熟悉,还需要掌握 node、 Koa2 等相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。

时间耗时比较:

1)数据请求

由服务端请求首屏数据,而不是客户端请求首屏数据,这是"快"的一个主要原因。服务端在内网进行请求,数据响应速度快。客户端在不同网络环境进行数据请求,且外网 http 请求开销大,导致时间差

为什么 React 要用 JSX?

JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引入更多的标准。

而 JSX 更像是一种语法糖,通过类似 XML 的描述方式,描写函数对象。

通过对比,可以清晰地发现,代码变得更为简洁,而且代码结构层次更为清晰。

因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在手写一棵结构树。而XML 在树结构的描述上天生具有可读性强的优势。

但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运行的时候,会使用 Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。

总结: JSX 是一个 JavaScript 的语法扩展,结构类似 XML。JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。

React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。

HOC 相比 mixins 有什么优点?

HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如:

  • 隐含了一些依赖,比如我在组件中写了某个 state 并且在 mixin 中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去 mixin 中查找依赖
  • 多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名真的是一件麻烦事。。
  • 雪球效应,虽然我一个组件还是使用着同一个 mixin,但是一个 mixin 会被多个组件使用,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本

HOC 解决了这些问题,并且它们达成的效果也是一致的,同时也更加的政治正确(毕竟更加函数式了)。

React 中的高阶组件运用了什么设计模式?

使用了装饰模式,高阶组件的运用:

javascript
function withWindowWidth(BaseComponent) {
+  class DerivedClass extends React.Component {
+    state = {
+      windowWidth: window.innerWidth,
+    };
+    onResize = () => {
+      this.setState({
+        windowWidth: window.innerWidth,
+      });
+    };
+    componentDidMount() {
+      window.addEventListener("resize", this.onResize);
+    }
+    componentWillUnmount() {
+      window.removeEventListener("resize", this.onResize);
+    }
+    render() {
+      return <BaseComponent {...this.props} {...this.state} />;
+    }
+  }
+  return DerivedClass;
+}
+const MyComponent = (props) => {
+  return <div>Window width is: {props.windowWidth}</div>;
+};
+export default withWindowWidth(MyComponent);
+
function withWindowWidth(BaseComponent) {
+  class DerivedClass extends React.Component {
+    state = {
+      windowWidth: window.innerWidth,
+    };
+    onResize = () => {
+      this.setState({
+        windowWidth: window.innerWidth,
+      });
+    };
+    componentDidMount() {
+      window.addEventListener("resize", this.onResize);
+    }
+    componentWillUnmount() {
+      window.removeEventListener("resize", this.onResize);
+    }
+    render() {
+      return <BaseComponent {...this.props} {...this.state} />;
+    }
+  }
+  return DerivedClass;
+}
+const MyComponent = (props) => {
+  return <div>Window width is: {props.windowWidth}</div>;
+};
+export default withWindowWidth(MyComponent);
+

装饰模式的特点是不需要改变 被装饰对象 本身,而只是在外面套一个外壳接口。JavaScript 目前已经有了原生装饰器的提案,其用法如下:

javascript
@testable
+class MyTestableClass {}
+
@testable
+class MyTestableClass {}
+

React.memo 作用及实现

javascript
作用为函数组件提供类似PureComponent的功能调用方式
+React.memo(Component, [isEqual])
+其中isEqual方法的返回值与shouldComponentUpdate返回值相反
+// 3种实现方式
+// 函数式组件
+function memo(Component, isEqual) {
+  function MemoComponent(props) {
+    const prevProps = React.useRef(props)
+    React.useEffect(() => {
+      prevProps.current = props
+    })
+    return React.useMemo(() => <Component {...props} />, [
+      isEqual(prevProps.current, props),
+    ])
+  }
+  return MemoComponent
+}
+// 使用PureComponent
+function memo(Component, isEqual) {
+  return class MemoComponent extends React.PureComponent {
+    render() {
+      return <Component {...this.props} />
+    }
+  }
+}
+
+// 使用shouldComponentUpdate
+function memo(Component, isEqual) {
+  return class MemoComponent extends React.Component {
+    shouldComponentUpdate(prevProps) {
+      return !isEqual(prevProps, this.props)
+    }
+    render() {
+      return <Component {...this.props} />
+    }
+  }
+}
+
作用:为函数组件提供类似PureComponent的功能,调用方式
+React.memo(Component, [isEqual])
+其中isEqual方法的返回值与shouldComponentUpdate返回值相反
+// 3种实现方式
+// 函数式组件
+function memo(Component, isEqual) {
+  function MemoComponent(props) {
+    const prevProps = React.useRef(props)
+    React.useEffect(() => {
+      prevProps.current = props
+    })
+    return React.useMemo(() => <Component {...props} />, [
+      isEqual(prevProps.current, props),
+    ])
+  }
+  return MemoComponent
+}
+// 使用PureComponent
+function memo(Component, isEqual) {
+  return class MemoComponent extends React.PureComponent {
+    render() {
+      return <Component {...this.props} />
+    }
+  }
+}
+
+// 使用shouldComponentUpdate
+function memo(Component, isEqual) {
+  return class MemoComponent extends React.Component {
+    shouldComponentUpdate(prevProps) {
+      return !isEqual(prevProps, this.props)
+    }
+    render() {
+      return <Component {...this.props} />
+    }
+  }
+}
+

React 有哪些代码复用的方式?举一些实践的例子?

高阶组件,RenderProps,Hooks 等

https://juejin.cn/post/6941546135827775525

Redux 解决了什么问题?实现原理是什么?中间件的原理?

https://juejin.cn/post/6844903857135304718#heading-40

react 受控组件

在 Ant Design 中,对 Input 输入框进行操作,如果是改变 defaultValue 会发现毫无作用。 这是因为 React 的 form 表单组件中的 defaultValue 一经传递值后,后续改变 defaultValue 都将不起作用,被忽略了。 具体来说这是一种 React 非受控组件,其状态是在 input 的 React 内部控制,不受调用者控制。 所以受控组件就是可以被 React 状态控制的组件。双向数据绑定就是受控组件,你可以为 form 中某个输入框添加 value 属性,然后控制它的一个改变。而非受控组件就是没有添加 value 属性的组件,你并不能对它的固定值进行操作。

React 组件在多次 render 中, hooks 的调用顺序为什么要保持一致?

Hooks 通过链表存储,需要顺序访问 https://juejin.cn/post/6944863057000529933

请比较 React hooks 与 React class component 性能的区别

javascript
function Counter() {
+  const [count, setCount] = useState(0);
+
+  useEffect(() => {
+    setTimeout(() => {
+      console.log(`You clicked ${count} times`);
+    }, 3000);
+  });
+
+ return (
+    <div>
+      <p>You clicked {count} times</p>
+      <button onClick={() => setCount(count + 1)}>
+        Click me
+      </button>
+    </div>
+  );
+}
+
+class Counter extends Component {
+  state = {
+    count: 0
+  };
+  componentDidMount() {
+    setTimeout(() => {
+      console.log(`You clicked ${this.state.count} times`);
+    }, 3000);
+  }
+
+  componentDidUpdate() {
+    setTimeout(() => {
+      console.log(`You clicked ${this.state.count} times`);
+    }, 3000);
+  }
+
+  render() {
+    return (
+      <div>
+        <p>You clicked {this.state.count} times</p>
+        <button onClick={() => this.setState({
+          count: this.state.count + 1
+        })}>
+          Click me
+        </button>
+      </div>
+    )
+  }
+}
+
+1. 连续点击 Click me 5fc和class component 分别输出啥为什么
+2. 如何修改class 让其输出 12345
+3. 如何修改fc让其输出 5次5
+
+1. fc: 1,2,3,4,5 class: 5,5,5,5,5  (每次的count值)
+2. componentDidUpdate() {
+    const count = this.state.count;
+    setTimeout(() => {
+      console.log(`You clicked ${count} times`);
+    }, 3000);
+  }
+3.  const latestCount = useRef(count);
+    useEffect(() => {
+      latestCount.current = count;
+      setTimeout(() => {
+        console.log(`You clicked ${latestCount.current} times`);
+      }, 3000);
+    });
+
function Counter() {
+  const [count, setCount] = useState(0);
+
+  useEffect(() => {
+    setTimeout(() => {
+      console.log(`You clicked ${count} times`);
+    }, 3000);
+  });
+
+ return (
+    <div>
+      <p>You clicked {count} times</p>
+      <button onClick={() => setCount(count + 1)}>
+        Click me
+      </button>
+    </div>
+  );
+}
+
+class Counter extends Component {
+  state = {
+    count: 0
+  };
+  componentDidMount() {
+    setTimeout(() => {
+      console.log(`You clicked ${this.state.count} times`);
+    }, 3000);
+  }
+
+  componentDidUpdate() {
+    setTimeout(() => {
+      console.log(`You clicked ${this.state.count} times`);
+    }, 3000);
+  }
+
+  render() {
+    return (
+      <div>
+        <p>You clicked {this.state.count} times</p>
+        <button onClick={() => this.setState({
+          count: this.state.count + 1
+        })}>
+          Click me
+        </button>
+      </div>
+    )
+  }
+}
+
+1. 连续点击 Click me 5下, fc和class component 分别输出啥? 为什么?
+2. 如何修改class 让其输出 12345
+3. 如何修改fc让其输出 5次5?
+
+1. fc: 1,2,3,4,5 class: 5,5,5,5,5  (每次的count值)
+2. componentDidUpdate() {
+    const count = this.state.count;
+    setTimeout(() => {
+      console.log(`You clicked ${count} times`);
+    }, 3000);
+  }
+3.  const latestCount = useRef(count);
+    useEffect(() => {
+      latestCount.current = count;
+      setTimeout(() => {
+        console.log(`You clicked ${latestCount.current} times`);
+      }, 3000);
+    });
+

请介绍 react diff 算法和策略

react 的 diff 算法和策略了解多少,为什么 react 的 diff 性能好,遵循什么样的策略可以把 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题

React 分别对 tree diff、component diff 以及 element diff 做了算法优化, 做了一些假设

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分

tree diff:React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较 component diff:

a.如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。

b.如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。

c.对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff element diff: 允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,减少增加和删除 详见:https://zhuanlan.zhihu.com/p/20346379

要求:专门看过相关的介绍并可以入手在减少节点移动和减少重复渲染上做过努力,对 diff 算法有较深入的代码层面的了解,可得 3.5 分

使用过哪些 Hooks,解决了什么问题?如何实现自定义 Hooks?Hooks 的实现原理?

https://juejin.cn/post/6844903857135304718#heading-50

React 原理题: 多次调用 setState 会执行几次?我说只会更新一次,其中会有一个异步更新去重队列。(面试官又提起兴趣了)

React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代

这三者是目前 react 解决代码复用的主要方式:

  • 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
  • render props 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
  • 通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。

(1)HOC 官方解释 ∶

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

简言之,HOC 是一种组件的设计模式,HOC 接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。

javascript
// hoc的定义
+function withSubscription(WrappedComponent, selectData) {
+  return class extends React.Component {
+    constructor(props) {
+      super(props);
+      this.state = {
+        data: selectData(DataSource, props)
+      };
+    }
+    // 一些通用的逻辑处理
+    render() {
+      // ... 并使用新数据渲染被包装的组件!
+      return <WrappedComponent data={this.state.data} {...this.props} />;
+    }
+  };
+
+// 使用
+const BlogPostWithSubscription = withSubscription(BlogPost,
+  (DataSource, props) => DataSource.getBlogPost(props.id));
+
+
// hoc的定义
+function withSubscription(WrappedComponent, selectData) {
+  return class extends React.Component {
+    constructor(props) {
+      super(props);
+      this.state = {
+        data: selectData(DataSource, props)
+      };
+    }
+    // 一些通用的逻辑处理
+    render() {
+      // ... 并使用新数据渲染被包装的组件!
+      return <WrappedComponent data={this.state.data} {...this.props} />;
+    }
+  };
+
+// 使用
+const BlogPostWithSubscription = withSubscription(BlogPost,
+  (DataSource, props) => DataSource.getBlogPost(props.id));
+
+

HOC 的优缺点 ∶

  • 优点 ∶ 逻辑复用、不影响被包裹组件的内部逻辑。
  • 缺点 ∶ hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖

(2)Render props 官方解释 ∶

"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

具有 render prop 的组件接受一个返回 React 元素的函数,将 render 的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。

javascript
// DataProvider组件内部的渲染逻辑如下
+class DataProvider extends React.Components {
+  state = {
+    name: "Tom",
+  };
+
+  render() {
+    return (
+      <div>
+        <p>共享数据组件自己内部的渲染逻辑</p>
+        {this.props.render(this.state)}
+      </div>
+    );
+  }
+}
+
+// 调用方式
+<DataProvider render={(data) => <h1>Hello {data.name}</h1>} />;
+
// DataProvider组件内部的渲染逻辑如下
+class DataProvider extends React.Components {
+  state = {
+    name: "Tom",
+  };
+
+  render() {
+    return (
+      <div>
+        <p>共享数据组件自己内部的渲染逻辑</p>
+        {this.props.render(this.state)}
+      </div>
+    );
+  }
+}
+
+// 调用方式
+<DataProvider render={(data) => <h1>Hello {data.name}</h1>} />;
+

由此可以看到,render props 的优缺点也很明显 ∶

  • 优点:数据共享、代码复用,将组件内的 state 作为 props 传递给调用者,将渲染逻辑交给调用者。
  • 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅

(3)Hooks 官方解释 ∶

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义 hook,可以复用代码逻辑。

javascript
// 自定义一个获取订阅数据的hook
+function useSubscription() {
+  const data = DataSource.getComments();
+  return [data];
+}
+//
+function CommentList(props) {
+  const {data} = props;
+  const [subData] = useSubscription();
+    ...
+}
+// 使用
+<CommentList data='hello' />
+
+
// 自定义一个获取订阅数据的hook
+function useSubscription() {
+  const data = DataSource.getComments();
+  return [data];
+}
+//
+function CommentList(props) {
+  const {data} = props;
+  const [subData] = useSubscription();
+    ...
+}
+// 使用
+<CommentList data='hello' />
+
+

以上可以看出,hook 解决了 hoc 的 prop 覆盖的问题,同时使用的方式解决了 render props 的嵌套地狱的问题。hook 的优点如下 ∶

  • 使用直观;
  • 解决 hoc 的 prop 重名问题;
  • 解决 render props 因共享数据 而出现嵌套地狱的问题;
  • 能在 return 之外使用数据的问题。

需要注意的是:hook 只能在组件顶层使用,不可在分支语句中使用。

总结 ∶ Hoc、render props 和 hook 都是为了解决代码复用的问题,但是 hoc 和 render props 都有特定的使用场景和明显的缺点。hook 是 react16.8 更新的新的 API,让组件逻辑复用更简洁明了,同时也解决了 hoc 和 render props 的一些缺点。

React.Component 和 React.PureComponent 的区别

PureComponent 表示一个纯组件,可以用来优化 React 程序,减少 render 函数执行的次数,从而提高组件的性能。

在 React 中,当 prop 或者 state 发生变化时,可以通过在 shouldComponentUpdate 生命周期函数中执行 return false 来阻止页面的更新,从而减少不必要的 render 执行。React.PureComponent 会自动执行 shouldComponentUpdate。

不过,pureComponent 中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候 render 是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent 一般会用在一些纯展示组件上。

使用 pureComponent 的好处:当组件更新时,如果组件的 props 或者 state 都没有改变,render 函数就不会触发。省去虚拟 DOM 的生成和对比过程,达到提升性能的目的。这是因为 react 自动做了一层浅比较。

Component, Element, Instance 之间有什么区别和联系?

  • 元素: 一个元素element是一个普通对象(plain object),描述了对于一个 DOM 节点或者其他组件component,你想让它在屏幕上呈现成什么样子。元素element可以在它的属性props中包含其他元素(译注:用于形成元素树)。创建一个 React 元素element成本很低。元素element创建之后是不可变的。
  • 组件: 一个组件component可以通过多种方式声明。可以是带有一个render()方法的类,简单点也可以定义为一个函数。这两种情况下,它都把属性props作为输入,把返回的一棵元素树作为输出。
  • 实例: 一个实例instance是你在所写的组件类component class中使用关键字this所指向的东西(译注:组件实例)。它用来存储本地状态和响应生命周期事件很有用。

函数式组件(Functional component)根本没有实例instance。类组件(Class component)有实例instance,但是永远也不需要直接创建一个组件的实例,因为 React 帮我们做了这些。

React.createClass 和 extends Component 的区别有哪些?

React.createClass 和 extends Component 的 bai 区别主要在于:

(1)语法区别

  • createClass 本质上是一个工厂函数,extends 的方式更加接近最新的 ES6 规范的 class 写法。两种方式在语法上的差别主要体现在方法的定义和静态属性的声明上。
  • createClass 方式的方法定义使用逗号,隔开,因为 creatClass 本质上是一个函数,传递给它的是一个 Object;而 class 的方式定义方法时务必谨记不要使用逗号隔开,这是 ES6 class 的语法规范。

(2)propType 和 getDefaultProps

  • React.createClass:通过 proTypes 对象和 getDefaultProps()方法来设置和获取 props.
  • React.Component:通过设置两个属性 propTypes 和 defaultProps

(3)状态的区别

  • React.createClass:通过 getInitialState()方法返回一个包含初始值的对象
  • React.Component:通过 constructor 设置初始状态

(4)this 区别

  • React.createClass:会正确绑定 this
  • React.Component:由于使用了 ES6,这里会有些微不同,属性并不会自动绑定到 React 类的实例上。

(5)Mixins

  • React.createClass:使用 React.createClass 的话,可以在创建组件时添加一个叫做 mixins 的属性,并将可供混合的类的集合以数组的形式赋给 mixins。
  • 如果使用 ES6 的方式来创建组件,那么 React mixins 的特性将不能被使用了。

React 高阶组件是什么,和普通组件有什么区别,适用什么场景

官方解释 ∶

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由 react 自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。

javascript
// hoc的定义
+function withSubscription(WrappedComponent, selectData) {
+  return class extends React.Component {
+    constructor(props) {
+      super(props);
+      this.state = {
+        data: selectData(DataSource, props)
+      };
+    }
+    // 一些通用的逻辑处理
+    render() {
+      // ... 并使用新数据渲染被包装的组件!
+      return <WrappedComponent data={this.state.data} {...this.props} />;
+    }
+  };
+
+// 使用
+const BlogPostWithSubscription = withSubscription(BlogPost,
+  (DataSource, props) => DataSource.getBlogPost(props.id));
+
+
// hoc的定义
+function withSubscription(WrappedComponent, selectData) {
+  return class extends React.Component {
+    constructor(props) {
+      super(props);
+      this.state = {
+        data: selectData(DataSource, props)
+      };
+    }
+    // 一些通用的逻辑处理
+    render() {
+      // ... 并使用新数据渲染被包装的组件!
+      return <WrappedComponent data={this.state.data} {...this.props} />;
+    }
+  };
+
+// 使用
+const BlogPostWithSubscription = withSubscription(BlogPost,
+  (DataSource, props) => DataSource.getBlogPost(props.id));
+
+

1)HOC 的优缺点

  • 优点 ∶ 逻辑复用、不影响被包裹组件的内部逻辑。
  • 缺点 ∶hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖

2)适用场景

  • 代码复用,逻辑抽象
  • 渲染劫持
  • State 抽象和更改
  • Props 更改

3)具体应用例子

  • 权限控制: 利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别和 页面元素级别
javascript
// HOC.js
+function withAdminAuth(WrappedComponent) {
+    return class extends React.Component {
+        state = {
+            isAdmin: false,
+        }
+        async UNSAFE_componentWillMount() {
+            const currentRole = await getCurrentUserRole();
+            this.setState({
+                isAdmin: currentRole === 'Admin',
+            });
+        }
+        render() {
+            if (this.state.isAdmin) {
+                return <WrappedComponent {...this.props} />;
+            } else {
+                return (<div>您没有权限查看该页面,请联系管理员!</div>);
+            }
+        }
+    };
+}
+
+// pages/page-a.js
+class PageA extends React.Component {
+    constructor(props) {
+        super(props);
+        // something here...
+    }
+    UNSAFE_componentWillMount() {
+        // fetching data
+    }
+    render() {
+        // render page with data
+    }
+}
+export default withAdminAuth(PageA);
+
+
+// pages/page-b.js
+class PageB extends React.Component {
+    constructor(props) {
+        super(props);
+    // something here...
+        }
+    UNSAFE_componentWillMount() {
+    // fetching data
+    }
+    render() {
+    // render page with data
+    }
+}
+export default withAdminAuth(PageB);
+
+
// HOC.js
+function withAdminAuth(WrappedComponent) {
+    return class extends React.Component {
+        state = {
+            isAdmin: false,
+        }
+        async UNSAFE_componentWillMount() {
+            const currentRole = await getCurrentUserRole();
+            this.setState({
+                isAdmin: currentRole === 'Admin',
+            });
+        }
+        render() {
+            if (this.state.isAdmin) {
+                return <WrappedComponent {...this.props} />;
+            } else {
+                return (<div>您没有权限查看该页面,请联系管理员!</div>);
+            }
+        }
+    };
+}
+
+// pages/page-a.js
+class PageA extends React.Component {
+    constructor(props) {
+        super(props);
+        // something here...
+    }
+    UNSAFE_componentWillMount() {
+        // fetching data
+    }
+    render() {
+        // render page with data
+    }
+}
+export default withAdminAuth(PageA);
+
+
+// pages/page-b.js
+class PageB extends React.Component {
+    constructor(props) {
+        super(props);
+    // something here...
+        }
+    UNSAFE_componentWillMount() {
+    // fetching data
+    }
+    render() {
+    // render page with data
+    }
+}
+export default withAdminAuth(PageB);
+
+
  • 组件渲染性能追踪: 借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录 ∶
javascript
class Home extends React.Component {
+  render() {
+    return <h1>Hello World.</h1>;
+  }
+}
+function withTiming(WrappedComponent) {
+  return class extends WrappedComponent {
+    constructor(props) {
+      super(props);
+      this.start = 0;
+      this.end = 0;
+    }
+    UNSAFE_componentWillMount() {
+      super.componentWillMount && super.componentWillMount();
+      this.start = Date.now();
+    }
+    componentDidMount() {
+      super.componentDidMount && super.componentDidMount();
+      this.end = Date.now();
+      console.log(
+        `${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`
+      );
+    }
+    render() {
+      return super.render();
+    }
+  };
+}
+
+export default withTiming(Home);
+
class Home extends React.Component {
+  render() {
+    return <h1>Hello World.</h1>;
+  }
+}
+function withTiming(WrappedComponent) {
+  return class extends WrappedComponent {
+    constructor(props) {
+      super(props);
+      this.start = 0;
+      this.end = 0;
+    }
+    UNSAFE_componentWillMount() {
+      super.componentWillMount && super.componentWillMount();
+      this.start = Date.now();
+    }
+    componentDidMount() {
+      super.componentDidMount && super.componentDidMount();
+      this.end = Date.now();
+      console.log(
+        `${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`
+      );
+    }
+    render() {
+      return super.render();
+    }
+  };
+}
+
+export default withTiming(Home);
+

注意:withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。

  • 页面复用
javascript
const withFetching = fetching => WrappedComponent => {
+    return class extends React.Component {
+        state = {
+            data: [],
+        }
+        async UNSAFE_componentWillMount() {
+            const data = await fetching();
+            this.setState({
+                data,
+            });
+        }
+        render() {
+            return <WrappedComponent data={this.state.data} {...this.props} />;
+        }
+    }
+}
+
+// pages/page-a.js
+export default withFetching(fetching('science-fiction'))(MovieList);
+// pages/page-b.js
+export default withFetching(fetching('action'))(MovieList);
+// pages/page-other.js
+export default withFetching(fetching('some-other-type'))(MovieList);
+
+
const withFetching = fetching => WrappedComponent => {
+    return class extends React.Component {
+        state = {
+            data: [],
+        }
+        async UNSAFE_componentWillMount() {
+            const data = await fetching();
+            this.setState({
+                data,
+            });
+        }
+        render() {
+            return <WrappedComponent data={this.state.data} {...this.props} />;
+        }
+    }
+}
+
+// pages/page-a.js
+export default withFetching(fetching('science-fiction'))(MovieList);
+// pages/page-b.js
+export default withFetching(fetching('action'))(MovieList);
+// pages/page-other.js
+export default withFetching(fetching('some-other-type'))(MovieList);
+
+

哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?

(1)哪些方法会触发 react 重新渲染?

  • setState()方法被调用

setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。

javascript
class App extends React.Component {
+  state = {
+    a: 1,
+  };
+
+  render() {
+    console.log("render");
+    return (
+      <React.Fragement>
+        <p>{this.state.a}</p>
+        <button
+          onClick={() => {
+            this.setState({ a: 1 }); // 这里并没有改变 a 的值
+          }}
+        >
+          Click me
+        </button>
+        <button onClick={() => this.setState(null)}>setState null</button>
+        <Child />
+      </React.Fragement>
+    );
+  }
+}
+
class App extends React.Component {
+  state = {
+    a: 1,
+  };
+
+  render() {
+    console.log("render");
+    return (
+      <React.Fragement>
+        <p>{this.state.a}</p>
+        <button
+          onClick={() => {
+            this.setState({ a: 1 }); // 这里并没有改变 a 的值
+          }}
+        >
+          Click me
+        </button>
+        <button onClick={() => this.setState(null)}>setState null</button>
+        <Child />
+      </React.Fragement>
+    );
+  }
+}
+
  • 父组件重新渲染

只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render

(2)重新渲染 render 会做些什么?

  • 会对新旧 VNode 进行对比,也就是我们所说的 Diff 算法。
  • 对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面
  • 遍历差异对象,根据差异的类型,根据对应对规则更新 VNode

React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能.

React 声明组件有哪几种方法,有什么不同?

React 声明组件的三种方式:

  • 函数式定义的无状态组件
  • ES5 原生方式React.createClass定义的组件
  • ES6 形式的extends React.Component定义的组件

(1)无状态函数式组件 它是为了创建纯展示组件,这种组件只负责根据传入的 props 来展示,不涉及到 state 状态的操作 组件不会被实例化,整体渲染性能得到提升,不能访问 this 对象,不能访问生命周期的方法

(2)ES5 原生方式 React.createClass // RFC React.createClass 会自绑定函数方法,导致不必要的性能开销,增加代码过时的可能性。

(3)E6 继承形式 React.Component // RCC 目前极为推荐的创建有状态组件的方式,最终会取代 React.createClass 形式;相对于 React.createClass 可以更好实现代码复用。

无状态组件相对于于后者的区别: 与无状态组件相比,React.createClass 和 React.Component 都是创建有状态的组件,这些组件是要被实例化的,并且可以访问组件的生命周期方法。

React.createClass 与 React.Component 区别:

① 函数 this 自绑定

  • React.createClass 创建的组件,其每一个成员函数的 this 都有 React 自动绑定,函数中的 this 会被正确设置。
  • React.Component 创建的组件,其成员函数不会自动绑定 this,需要开发者手动绑定,否则 this 不能获取当前组件实例对象。

② 组件属性类型 propTypes 及其默认 props 属性 defaultProps 配置不同

  • React.createClass 在创建组件时,有关组件 props 的属性类型及组件默认的属性会作为组件实例的属性来配置,其中 defaultProps 是使用 getDefaultProps 的方法来获取默认组件属性的
  • React.Component 在创建组件时配置这两个对应信息时,他们是作为组件类的属性,不是组件实例的属性,也就是所谓的类的静态属性来配置的。

③ 组件初始状态 state 的配置不同

  • React.createClass 创建的组件,其状态 state 是通过 getInitialState 方法来配置组件相关的状态;
  • React.Component 创建的组件,其状态 state 是在 constructor 中像初始化组件属性一样声明的。

对有状态组件和无状态组件的理解及使用场景

(1)有状态组件

特点:

  • 是类组件
  • 有继承
  • 可以使用 this
  • 可以使用 react 的生命周期
  • 使用较多,容易频繁触发生命周期钩子函数,影响性能
  • 内部使用 state,维护自身状态的变化,有状态组件根据外部组件传入的 props 和自身的 state 进行渲染。

使用场景:

  • 需要使用到状态的。
  • 需要使用状态操作组件的(无状态组件的也可以实现新版本 react hooks 也可实现)

总结: 类组件可以维护自身的状态变量,即组件的 state ,类组件还有不同的生命周期方法,可以让开发者能够在组件的不同阶段(挂载、更新、卸载),对组件做更多的控制。类组件则既可以充当无状态组件,也可以充当有状态组件。当一个类组件不需要管理自身状态时,也可称为无状态组件。

(2)无状态组件 特点:

  • 不依赖自身的状态 state
  • 可以是类组件或者函数组件。
  • 可以完全避免使用 this 关键字。(由于使用的是箭头函数事件无需绑定)
  • 有更高的性能。当不需要使用生命周期钩子时,应该首先使用无状态函数组件
  • 组件内部不维护 state ,只根据外部组件传入的 props 进行渲染的组件,当 props 改变时,组件重新渲染。

使用场景:

  • 组件不需要管理 state,纯展示

优点:

  • 简化代码、专注于 render
  • 组件不需要被实例化,无生命周期,提升性能。 输出(渲染)只取决于输入(属性),无副作用
  • 视图和数据的解耦分离

缺点:

  • 无法使用 ref
  • 无生命周期方法
  • 无法控制组件的重渲染,因为无法使用 shouldComponentUpdate 方法,当组件接受到新的属性时则会重渲染

总结: 组件内部状态且与外部无关的组件,可以考虑用状态组件,这样状态树就不会过于复杂,易于理解和管理。当一个组件不需要管理自身状态时,也就是无状态组件,应该优先设计为函数组件。比如自定义的 <Button/><Input /> 等组件。

对 React 中 Fragment 的理解,它的使用场景是什么?

在 React 中,组件返回的元素只能有一个根元素。为了不添加多余的 DOM 节点,我们可以使用 Fragment 标签来包裹所有的元素,Fragment 标签不会渲染出任何元素。React 官方对 Fragment 的解释:

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

javascript
import React, { Component, Fragment } from 'react'
+
+// 一般形式
+render() {
+  return (
+    <React.Fragment>
+      <ChildA />
+      <ChildB />
+      <ChildC />
+    </React.Fragment>
+  );
+}
+// 也可以写成以下形式
+render() {
+  return (
+    <>
+      <ChildA />
+      <ChildB />
+      <ChildC />
+    </>
+  );
+}
+
+
import React, { Component, Fragment } from 'react'
+
+// 一般形式
+render() {
+  return (
+    <React.Fragment>
+      <ChildA />
+      <ChildB />
+      <ChildC />
+    </React.Fragment>
+  );
+}
+// 也可以写成以下形式
+render() {
+  return (
+    <>
+      <ChildA />
+      <ChildB />
+      <ChildC />
+    </>
+  );
+}
+
+

React 如何获取组件对应的 DOM 元素?

可以用 ref 来获取某个子节点的实例,然后通过当前 class 组件实例的一些特定属性来直接获取子节点实例。ref 有三种实现方法:

  • 字符串格式:字符串格式,这是 React16 版本之前用得最多的,例如:<p ref="info">span</p>
  • 函数格式:ref 对应一个方法,该方法有一个参数,也就是对应的节点实例,例如:<p ref={ele => this.info = ele}></p>
  • createRef 方法:React 16 提供的一个 API,使用 React.createRef()来实现

React 中可以在 render 访问 refs 吗?为什么?

javascript
<>
+  <span id="name" ref={this.spanRef}>
+    {this.state.title}
+  </span>
+  <span>{this.spanRef.current ? "有值" : "无值"}</span>
+</>;
+
<>
+  <span id="name" ref={this.spanRef}>
+    {this.state.title}
+  </span>
+  <span>{this.spanRef.current ? "有值" : "无值"}</span>
+</>;
+

不可以,render 阶段 DOM 还没有生成,无法获取 DOM。DOM 的获取需要在 pre-commit 阶段和 commit 阶段:

对 React 的插槽(Portals)的理解,如何使用,有哪些使用场景

React 官方对 Portals 的定义:

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案

Portals 是 React 16 提供的官方解决方案,使得组件可以脱离父组件层级挂载在 DOM 树的任何位置。通俗来讲,就是我们 render 一个组件,但这个组件的 DOM 结构并不在本组件内。

Portals 语法如下:

javascript
ReactDOM.createPortal(child, container);
+
ReactDOM.createPortal(child, container);
+
  • 第一个参数 child 是可渲染的 React 子项,比如元素,字符串或者片段等;
  • 第二个参数 container 是一个 DOM 元素。

一般情况下,组件的 render 函数返回的元素会被挂载在它的父级组件上:

javascript
import DemoComponent from './DemoComponent';
+render() {
+  // DemoComponent元素会被挂载在id为parent的div的元素上
+  return (
+    <div id="parent">
+        <DemoComponent />
+    </div>
+  );
+}
+
+
import DemoComponent from './DemoComponent';
+render() {
+  // DemoComponent元素会被挂载在id为parent的div的元素上
+  return (
+    <div id="parent">
+        <DemoComponent />
+    </div>
+  );
+}
+
+

然而,有些元素需要被挂载在更高层级的位置。最典型的应用场景:当父组件具有overflow: hidden或者z-index的样式设置时,组件有可能被其他元素遮挡,这时就可以考虑要不要使用 Portal 使组件的挂载脱离父组件。例如:对话框,模态窗。

javascript
import DemoComponent from './DemoComponent';
+render() {
+  // DemoComponent元素会被挂载在id为parent的div的元素上
+  return (
+    <div id="parent">
+        <DemoComponent />
+    </div>
+  );
+}
+
+
import DemoComponent from './DemoComponent';
+render() {
+  // DemoComponent元素会被挂载在id为parent的div的元素上
+  return (
+    <div id="parent">
+        <DemoComponent />
+    </div>
+  );
+}
+
+

对 React-Intl 的理解,它的工作原理?

React-intl 是雅虎的语言国际化开源项目 FormatJS 的一部分,通过其提供的组件和 API 可以与 ReactJS 绑定。

React-intl 提供了两种使用方法,一种是引用 React 组件,另一种是直接调取 API,官方更加推荐在 React 项目中使用前者,只有在无法使用 React 组件的地方,才应该调用框架提供的 API。它提供了一系列的 React 组件,包括数字格式化、字符串格式化、日期格式化等。

在 React-intl 中,可以配置不同的语言包,他的工作原理就是根据需要,在语言包之间进行切换。

React 中什么是受控组件和非控组件?

(1)受控组件 在使用表单来收集用户输入时,例如<input><select><textearea>等元素都要绑定一个 change 事件,当表单的状态发生变化,就会触发 onChange 事件,更新组件的 state。这种组件在 React 中被称为受控组件,在受控组件中,组件渲染出的状态与它的 value 或 checked 属性相对应,react 通过这种方式消除了组件的局部状态,使整个状态可控。react 官方推荐使用受控表单组件。

受控组件更新 state 的流程:

  • 可以通过初始 state 中设置表单的默认值
  • 每当表单的值发生变化时,调用 onChange 事件处理器
  • 事件处理器通过事件对象 e 拿到改变后的状态,并更新组件的 state
  • 一旦通过 setState 方法更新 state,就会触发视图的重新渲染,完成表单组件的更新

受控组件缺陷: 表单元素的值都是由 React 组件进行管理,当有多个输入框,或者多个这种组件时,如果想同时获取到全部的值就必须每个都要编写事件处理函数,这会让代码看着很臃肿,所以为了解决这种情况,出现了非受控组件。

(2)非受控组件 如果一个表单组件没有 value props(单选和复选按钮对应的是 checked props)时,就可以称为非受控组件。在非受控组件中,可以使用一个 ref 来从 DOM 获得表单值。而不是为每个状态更新编写一个事件处理程序。

React 官方的解释:

要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用 ref 来从 DOM 节点中获取表单数据。 因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

例如,下面的代码在非受控组件中接收单个属性:

javascript
class NameForm extends React.Component {
+  constructor(props) {
+    super(props);
+    this.handleSubmit = this.handleSubmit.bind(this);
+  }
+  handleSubmit(event) {
+    alert("A name was submitted: " + this.input.value);
+    event.preventDefault();
+  }
+  render() {
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <label>
+          Name:
+          <input type="text" ref={(input) => (this.input = input)} />
+        </label>
+        <input type="submit" value="Submit" />
+      </form>
+    );
+  }
+}
+
class NameForm extends React.Component {
+  constructor(props) {
+    super(props);
+    this.handleSubmit = this.handleSubmit.bind(this);
+  }
+  handleSubmit(event) {
+    alert("A name was submitted: " + this.input.value);
+    event.preventDefault();
+  }
+  render() {
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <label>
+          Name:
+          <input type="text" ref={(input) => (this.input = input)} />
+        </label>
+        <input type="submit" value="Submit" />
+      </form>
+    );
+  }
+}
+

总结: 页面中所有输入类的 DOM 如果是现用现取的称为非受控组件,而通过 setState 将输入的值维护到了 state 中,需要时再从 state 中取出,这里的数据就受到了 state 的控制,称为受控组件。

React 中 refs 的作用是什么?有哪些应用场景?

Refs 提供了一种方式,用于访问在 render 方法中创建的 React 元素或 DOM 节点。Refs 应该谨慎使用,如下场景使用 Refs 比较适合:

  • 处理焦点、文本选择或者媒体的控制
  • 触发必要的动画
  • 集成第三方 DOM 库

Refs 是使用 React.createRef() 方法创建的,他通过 ref 属性附加到 React 元素上。要在整个组件中使用 Refs,需要将 ref 在构造函数中分配给其实例属性:

javascript
class MyComponent extends React.Component {
+  constructor(props) {
+    super(props);
+    this.myRef = React.createRef();
+  }
+  render() {
+    return <div ref={this.myRef} />;
+  }
+}
+
class MyComponent extends React.Component {
+  constructor(props) {
+    super(props);
+    this.myRef = React.createRef();
+  }
+  render() {
+    return <div ref={this.myRef} />;
+  }
+}
+

由于函数组件没有实例,因此不能在函数组件上直接使用 ref

javascript
function MyFunctionalComponent() {
+  return <input />;
+}
+class Parent extends React.Component {
+  constructor(props) {
+    super(props);
+    this.textInput = React.createRef();
+  }
+  render() {
+    // 这将不会工作!
+    return <MyFunctionalComponent ref={this.textInput} />;
+  }
+}
+
function MyFunctionalComponent() {
+  return <input />;
+}
+class Parent extends React.Component {
+  constructor(props) {
+    super(props);
+    this.textInput = React.createRef();
+  }
+  render() {
+    // 这将不会工作!
+    return <MyFunctionalComponent ref={this.textInput} />;
+  }
+}
+

但可以通过闭合的帮助在函数组件内部进行使用 Refs:

javascript
function CustomTextInput(props) {
+  // 这里必须声明 textInput,这样 ref 回调才可以引用它
+  let textInput = null;
+  function handleClick() {
+    textInput.focus();
+  }
+  return (
+    <div>
+      <input
+        type="text"
+        ref={(input) => {
+          textInput = input;
+        }}
+      />
+      <input type="button" value="Focus the text input" onClick={handleClick} />
+    </div>
+  );
+}
+
function CustomTextInput(props) {
+  // 这里必须声明 textInput,这样 ref 回调才可以引用它
+  let textInput = null;
+  function handleClick() {
+    textInput.focus();
+  }
+  return (
+    <div>
+      <input
+        type="text"
+        ref={(input) => {
+          textInput = input;
+        }}
+      />
+      <input type="button" value="Focus the text input" onClick={handleClick} />
+    </div>
+  );
+}
+

注意:

  • 不应该过度的使用 Refs
  • 的返回值取决于节点的类型:
    • ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为他的 current 属性以创建 ref
    • ref 属性被用于一个自定义的类组件时,ref 对象将接收该组件已挂载的实例作为他的 current
  • 当在父组件中需要访问子组件中的 ref 时可使用传递 Refs 或回调 Refs。

React 组件的构造函数有什么作用?它是必须的吗?

构造函数主要用于两个目的:

  • 通过将对象分配给 this.state 来初始化本地状态
  • 将事件处理程序方法绑定到实例上

所以,当在 React class 中需要设置 state 的初始值或者绑定事件时,需要加上构造函数,官方 Demo:

javascript
class LikeButton extends React.Component {
+  constructor() {
+    super();
+    this.state = {
+      liked: false,
+    };
+    this.handleClick = this.handleClick.bind(this);
+  }
+  handleClick() {
+    this.setState({ liked: !this.state.liked });
+  }
+  render() {
+    const text = this.state.liked ? "liked" : "haven't liked";
+    return (
+      <div onClick={this.handleClick}>You {text} this. Click to toggle.</div>
+    );
+  }
+}
+ReactDOM.render(<LikeButton />, document.getElementById("example"));
+
class LikeButton extends React.Component {
+  constructor() {
+    super();
+    this.state = {
+      liked: false,
+    };
+    this.handleClick = this.handleClick.bind(this);
+  }
+  handleClick() {
+    this.setState({ liked: !this.state.liked });
+  }
+  render() {
+    const text = this.state.liked ? "liked" : "haven't liked";
+    return (
+      <div onClick={this.handleClick}>You {text} this. Click to toggle.</div>
+    );
+  }
+}
+ReactDOM.render(<LikeButton />, document.getElementById("example"));
+

构造函数用来新建父类的 this 对象;子类必须在 constructor 方法中调用 super 方法;否则新建实例时会报错;因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法;子类就得不到 this 对象。

注意:

  • constructor () 必须配上 super(), 如果要在 constructor 内部使用 this.props 就要 传入 props , 否则不用
  • JavaScript 中的 bind 每次都会返回一个新的函数, 为了性能等考虑, 尽量在 constructor 中绑定事件

React.forwardRef 是什么?它有什么作用?

React.forwardRef 会创建一个 React 组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

  • 转发 refs 到 DOM 组件
  • 在高阶组件中转发 refs

类组件与函数组件有什么异同?

相同点: 组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。

我们甚至可以将一个类组件改写成函数组件,或者把函数组件改写成一个类组件(虽然并不推荐这种重构行为)。从使用者的角度而言,很难从使用体验上区分两者,而且在现代浏览器中,闭包和类的性能只在极端场景下才会有明显的差别。所以,基本可认为两者作为组件是完全一致的。

不同点:

  • 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
  • 之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。
  • 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。
  • 从上手程度而言,类组件更容易上手,从未来趋势上看,由于 React Hooks 的推出,函数组件成了社区未来主推的方案。
  • 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。

React setState 调用的原理

  • 首先调用了setState 入口函数,入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去;
javascript
ReactComponent.prototype.setState = function (partialState, callback) {
+  this.updater.enqueueSetState(this, partialState);
+  if (callback) {
+    this.updater.enqueueCallback(this, callback, "setState");
+  }
+};
+
ReactComponent.prototype.setState = function (partialState, callback) {
+  this.updater.enqueueSetState(this, partialState);
+  if (callback) {
+    this.updater.enqueueCallback(this, callback, "setState");
+  }
+};
+
  • enqueueSetState 方法将新的 state 放进组件的状态队列里,并调用 enqueueUpdate 来处理将要更新的实例对象;
javascript
enqueueSetState: function (publicInstance, partialState) {
+  // 根据 this 拿到对应的组件实例
+  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
+  // 这个 queue 对应的就是一个组件实例的 state 数组
+  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
+  queue.push(partialState);
+  //  enqueueUpdate 用来处理当前的组件实例
+  enqueueUpdate(internalInstance);
+}
+
+
enqueueSetState: function (publicInstance, partialState) {
+  // 根据 this 拿到对应的组件实例
+  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
+  // 这个 queue 对应的就是一个组件实例的 state 数组
+  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
+  queue.push(partialState);
+  //  enqueueUpdate 用来处理当前的组件实例
+  enqueueUpdate(internalInstance);
+}
+
+
  • enqueueUpdate 方法中引出了一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates 属性直接决定了当下是要走更新流程,还是应该排队等待;如果轮到执行,就调用 batchedUpdates 方法来直接发起更新流程。由此可以推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。
javascript
function enqueueUpdate(component) {
+  ensureInjected();
+  // 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
+  if (!batchingStrategy.isBatchingUpdates) {
+    // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
+    batchingStrategy.batchedUpdates(enqueueUpdate, component);
+    return;
+  }
+  // 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
+  dirtyComponents.push(component);
+  if (component._updateBatchNumber == null) {
+    component._updateBatchNumber = updateBatchNumber + 1;
+  }
+}
+
function enqueueUpdate(component) {
+  ensureInjected();
+  // 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
+  if (!batchingStrategy.isBatchingUpdates) {
+    // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
+    batchingStrategy.batchedUpdates(enqueueUpdate, component);
+    return;
+  }
+  // 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
+  dirtyComponents.push(component);
+  if (component._updateBatchNumber == null) {
+    component._updateBatchNumber = updateBatchNumber + 1;
+  }
+}
+

注意:batchingStrategy 对象可以理解为“锁管理器”。这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。

React setState 调用之后发生了什么?是同步还是异步?

(1)React 中 setState 后发生了什么

在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。

在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

如果在短时间内频繁 setState。React 会将 state 的改变压入栈中,在合适的时机,批量更新 state 和视图,达到提高性能的效果。

(2)setState 是同步还是异步的

假如所有 setState 是同步的,意味着每执行一次 setState 时(有可能一个同步代码中,多次 setState),都重新 vnode diff + dom 修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个 setState 合并成一次组件更新。所以默认是异步的,但是在一些情况下是同步的。

setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。

  • 异步: 在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
  • 同步: 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。

一般认为,做异步设计是为了性能优化、减少渲染次数:

  • setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了state,但是还没有执行render函数,那么stateprops不能保持同步。stateprops不能保持一致性,会在开发中产生很多的问题;

React 中的 setState 批量更新的过程是什么?

调用 setState 时,组件的 state 并不会立即改变, setState 只是把要修改的 state 放入一个队列, React 会优化真正的执行时机,并出于性能原因,会将 React 事件处理程序中的多次React 事件处理程序中的多次 setState 的状态修改合并成一次状态修改。 最终更新只产生一次组件及其子组件的重新渲染,这对于大型应用程序中的性能提升至关重要。

javascript
this.setState({
+  count: this.state.count + 1    ===>    入队,[count+1的任务]
+});
+this.setState({
+  count: this.state.count + 1    ===>    入队,[count+1的任务count+1的任务]
+});
+
+                                         合并 state,[count+1的任务]
+
+                                         执行 count+1的任务
+
+
this.setState({
+  count: this.state.count + 1    ===>    入队,[count+1的任务]
+});
+this.setState({
+  count: this.state.count + 1    ===>    入队,[count+1的任务,count+1的任务]
+});
+
+                                         合并 state,[count+1的任务]
+
+                                         执行 count+1的任务
+
+

需要注意的是,只要同步代码还在执行,“攒起来”这个动作就不会停止。(注:这里之所以多次 +1 最终只有一次生效,是因为在同一个方法中多次 setState 的合并动作不是单纯地将更新累加。比如这里对于相同属性的设置,React 只会为其保留最后一次的更新)。

React 中有使用过 getDefaultProps 吗?它有什么作用?

通过实现组件的 getDefaultProps,对属性设置默认值(ES5 的写法):

javascript
var ShowTitle = React.createClass({
+  getDefaultProps: function () {
+    return {
+      title: "React",
+    };
+  },
+  render: function () {
+    return <h1>{this.props.title}</h1>;
+  },
+});
+
var ShowTitle = React.createClass({
+  getDefaultProps: function () {
+    return {
+      title: "React",
+    };
+  },
+  render: function () {
+    return <h1>{this.props.title}</h1>;
+  },
+});
+

React 中 setState 的第二个参数作用是什么?

setState 的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate 生命周期内执行。通常建议使用 componentDidUpdate 来代替此方式。在这个回调函数中你可以拿到更新后 state 的值:

javascript
this.setState({
+    key1: newState1,
+    key2: newState2,
+    ...
+}, callback) // 第二个参数是 state 更新完成后的回调函数
+
+
this.setState({
+    key1: newState1,
+    key2: newState2,
+    ...
+}, callback) // 第二个参数是 state 更新完成后的回调函数
+
+

React 中的 setState 和 replaceState 的区别是什么?

(1)setState() setState()用于设置状态对象,其语法如下:

javascript
setState(object nextState[, function callback])
+
+
setState(object nextState[, function callback])
+
+
  • nextState,将要设置的新状态,该状态会和当前的 state 合并
  • callback,可选参数,回调函数。该函数会在 setState 设置成功,且组件重新渲染后调用。

合并 nextState 和当前 state,并重新渲染组件。setState 是 React 事件处理函数中和请求回调函数中触发 UI 更新的主要方法。

(2)replaceState() replaceState()方法与 setState()类似,但是方法只会保留 nextState 中状态,原 state 不在 nextState 中的状态都会被删除。其语法如下:

javascript
replaceState(object nextState[, function callback])
+
+
replaceState(object nextState[, function callback])
+
+
  • nextState,将要设置的新状态,该状态会替换当前的 state。
  • callback,可选参数,回调函数。该函数会在 replaceState 设置成功,且组件重新渲染后调用。

总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而 replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。

在 React 中组件的 this.state 和 setState 有什么区别?

this.state 通常是用来初始化 state 的,this.setState 是用来修改 state 值的。如果初始化了 state 之后再使用 this.state,之前的 state 会被覆盖掉,如果使用 this.setState,只会替换掉相应的 state 值。所以,如果想要修改 state 的值,就需要使用 setState,而不能直接修改 state,直接修改 state 之后页面是不会更新的。

state 是怎么注入到组件的,从 reducer 到组件经历了什么样的过程

通过 connect 和 mapStateToProps 将 state 注入到组件中:

javascript
import { connect } from "react-redux";
+import { setVisibilityFilter } from "@/reducers/Todo/actions";
+import Link from "@/containers/Todo/components/Link";
+
+const mapStateToProps = (state, ownProps) => ({
+  active: ownProps.filter === state.visibilityFilter,
+});
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+  setFilter: () => {
+    dispatch(setVisibilityFilter(ownProps.filter));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Link);
+
import { connect } from "react-redux";
+import { setVisibilityFilter } from "@/reducers/Todo/actions";
+import Link from "@/containers/Todo/components/Link";
+
+const mapStateToProps = (state, ownProps) => ({
+  active: ownProps.filter === state.visibilityFilter,
+});
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+  setFilter: () => {
+    dispatch(setVisibilityFilter(ownProps.filter));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Link);
+

上面代码中,active 就是注入到 Link 组件中的状态。 mapStateToProps(state,ownProps)中带有两个参数,含义是 ∶

  • state-store 管理的全局状态对象,所有都组件状态数据都存储在该对象中。
  • ownProps 组件通过 props 传入的参数。

reducer 到组件经历的过程:

  • reducer 对 action 对象处理,更新组件状态,并将新的状态值返回 store。
  • 通过 connect(mapStateToProps,mapDispatchToProps)(Component)对组件 Component 进行升级,此时将状态值从 store 取出并作为 props 参数传递到组件。

高阶组件实现源码 ∶

javascript
import React from "react";
+import PropTypes from "prop-types";
+
+// 高阶组件 contect
+export const connect =
+  (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
+    class Connect extends React.Component {
+      // 通过对context调用获取store
+      static contextTypes = {
+        store: PropTypes.object,
+      };
+
+      constructor() {
+        super();
+        this.state = {
+          allProps: {},
+        };
+      }
+
+      // 第一遍需初始化所有组件初始状态
+      componentWillMount() {
+        const store = this.context.store;
+        this._updateProps();
+        store.subscribe(() => this._updateProps()); // 加入_updateProps()至store里的监听事件列表
+      }
+
+      // 执行action后更新props,使组件可以更新至最新状态(类似于setState)
+      _updateProps() {
+        const store = this.context.store;
+        let stateProps = mapStateToProps
+          ? mapStateToProps(store.getState(), this.props)
+          : {}; // 防止 mapStateToProps 没有传入
+        let dispatchProps = mapDispatchToProps
+          ? mapDispatchToProps(store.dispatch, this.props)
+          : {
+              dispatch: store.dispatch,
+            }; // 防止 mapDispatchToProps 没有传入
+        this.setState({
+          allProps: {
+            ...stateProps,
+            ...dispatchProps,
+            ...this.props,
+          },
+        });
+      }
+
+      render() {
+        return <WrappedComponent {...this.state.allProps} />;
+      }
+    }
+    return Connect;
+  };
+
import React from "react";
+import PropTypes from "prop-types";
+
+// 高阶组件 contect
+export const connect =
+  (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
+    class Connect extends React.Component {
+      // 通过对context调用获取store
+      static contextTypes = {
+        store: PropTypes.object,
+      };
+
+      constructor() {
+        super();
+        this.state = {
+          allProps: {},
+        };
+      }
+
+      // 第一遍需初始化所有组件初始状态
+      componentWillMount() {
+        const store = this.context.store;
+        this._updateProps();
+        store.subscribe(() => this._updateProps()); // 加入_updateProps()至store里的监听事件列表
+      }
+
+      // 执行action后更新props,使组件可以更新至最新状态(类似于setState)
+      _updateProps() {
+        const store = this.context.store;
+        let stateProps = mapStateToProps
+          ? mapStateToProps(store.getState(), this.props)
+          : {}; // 防止 mapStateToProps 没有传入
+        let dispatchProps = mapDispatchToProps
+          ? mapDispatchToProps(store.dispatch, this.props)
+          : {
+              dispatch: store.dispatch,
+            }; // 防止 mapDispatchToProps 没有传入
+        this.setState({
+          allProps: {
+            ...stateProps,
+            ...dispatchProps,
+            ...this.props,
+          },
+        });
+      }
+
+      render() {
+        return <WrappedComponent {...this.state.allProps} />;
+      }
+    }
+    return Connect;
+  };
+

React 组件的 state 和 props 有什么区别?

(1)props

props 是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会改变。

(2)state

state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。

(3)区别

  • props 是传递给组件的(类似于函数的形参),而 state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。
  • props 是不可修改的,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
  • state 是在组件中创建的,一般在 constructor 中初始化 state。state 是多变的、可以修改,每次 setState 都异步更新的。

React 中的 props 为什么是只读的?

this.props是组件之间沟通的一个接口,原则上来讲,它只能从父组件流向子组件。React 具有浓重的函数式编程的思想。

提到函数式编程就要提一个概念:纯函数。它有几个特点:

  • 给定相同的输入,总是返回相同的输出。
  • 过程没有副作用。
  • 不依赖外部状态。

this.props就是汲取了纯函数的思想。props 的不可以变性就保证的相同的输入,页面显示的内容是一样的,并且不会产生副作用

在 React 中组件的 props 改变时更新组件的有哪些方法?

在一个组件传入的 props 更新时重新渲染该组件常用的方法是在componentWillReceiveProps中将新的 props 更新到组件的 state 中(这种 state 被成为派生状态(Derived State)),从而实现重新渲染。React 16.3 中还引入了一个新的钩子函数getDerivedStateFromProps来专门实现这一需求。

(1)componentWillReceiveProps(已废弃)

在 react 的 componentWillReceiveProps(nextProps)生命周期中,可以在子组件的 render 函数执行前,通过 this.props 获取旧的属性,通过 nextProps 获取新的 props,对比两次 props 是否相同,从而更新子组件自己的 state。

这样的好处是,可以将数据请求放在这里进行执行,需要传的参数则从 componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。

(2)getDerivedStateFromProps(16.3 引入)

这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。

两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。

需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:

javascript
static getDerivedStateFromProps(nextProps, prevState) {
+    const {type} = nextProps;
+    // 当传入的type发生变化的时候,更新state
+    if (type !== prevState.type) {
+        return {
+            type,
+        };
+    }
+    // 否则,对于state不进行任何操作
+    return null;
+}
+
+
static getDerivedStateFromProps(nextProps, prevState) {
+    const {type} = nextProps;
+    // 当传入的type发生变化的时候,更新state
+    if (type !== prevState.type) {
+        return {
+            type,
+        };
+    }
+    // 否则,对于state不进行任何操作
+    return null;
+}
+
+

React 中怎么检验 props?验证 props 的目的是什么?

React为我们提供了PropTypes以供验证使用。当我们向Props传入的数据无效(向 Props 传入的数据类型和验证的数据类型不符)就会在控制台发出警告信息。它可以避免随着应用越来越复杂从而出现的问题。并且,它还可以让程序变得更易读。

javascript
import PropTypes from "prop-types";
+
+class Greeting extends React.Component {
+  render() {
+    return <h1>Hello, {this.props.name}</h1>;
+  }
+}
+
+Greeting.propTypes = {
+  name: PropTypes.string,
+};
+
import PropTypes from "prop-types";
+
+class Greeting extends React.Component {
+  render() {
+    return <h1>Hello, {this.props.name}</h1>;
+  }
+}
+
+Greeting.propTypes = {
+  name: PropTypes.string,
+};
+

当然,如果项目汇中使用了 TypeScript,那么就可以不用 PropTypes 来校验,而使用 TypeScript 定义接口来校验 props。

+ + + + + \ No newline at end of file diff --git a/react/react-redux-router.html b/react/react-redux-router.html new file mode 100644 index 00000000..88ebf5a8 --- /dev/null +++ b/react/react-redux-router.html @@ -0,0 +1,20 @@ + + + + + + react-redux | Sunny's blog + + + + + + + + +
Skip to content
On this page

react-redux

  • React: 组件化的 UI 界面处理方案
  • React-Router: 根据地址匹配路由,最终渲染不同的组件
  • Redux:处理数据以及数据变化的方案(主要用于处理共享数据)

如果一个组件,仅用于渲染一个 UI 界面,而没有状态(通常是一个函数组件),该组件叫做展示组件 如果一个组件,仅用于提供数据,没有任何属于自己的 UI 界面,则该组件叫做容器组件,容器组件纯粹是为了给其他组件提供数据。

react-redux 库:链接 redux 和 react

  • Provider 组件:没有任何 UI 界面,该组件的作用,是将 redux 的仓库放到一个上下文中。
  • connect:高阶组件,用于链接仓库和组件的
    • 细节一:如果对返回的容器组件加上额外的属性,则这些属性会直接传递到展示组件
    • 第一个参数:mapStateToProps:
      • 参数 1:整个仓库的状态
      • 参数 2:使用者传递的属性对象
    • 第二个参数:
      • 情况 1:传递一个函数 mapDispatchToProps
        • 参数 1:dispatch 函数
        • 参数 2:使用者传递的属性对象
        • 函数返回的对象会作为属性传递到展示组件中(作为事件处理函数存在)
      • 情况 2:传递一个对象,对象的每个属性是一个 action 创建函数,当事件触发时,会自动的 dispatch 函数返回的 action
    • 细节二:如果不传递第二个参数,通过 connect 连接的组件,会自动得到一个属性:dispatch,使得组件有能力自行触发 action,但是,不推荐这样做。

知识

  1. chrome 插件:redux-devtools
  2. 使用 npm 安装第三方库:redux-devtools-extension

redux 和 router 的结合(connected-react-router)

希望把路由信息放进仓库统一管理的时候才需要这个库

用于将 redux 和 react-router 进行结合

本质上,router 中的某些数据可能会跟数据仓库中的数据进行联动

该组件会将下面的路由数据和仓库保持同步

  1. action:它不是 redux 的 action,它表示当前路由跳转的方式(PUSH、POP、REPLACE)
  2. location:它记录了当前的地址信息

该库中的内容:

connectRouter

这是一个函数,调用它,会返回一个用于管理仓库中路由信息的 reducer,该函数需要传递一个参数,参数是一个 history 对象。该对象,可以使用第三方库 history 得到。

routerMiddleware

该函数会返回一个 redux 中间件,用于拦截一些特殊的 action

ConnectedRouter

这是一个组件,用于向上下文提供一个 history 对象和其他的路由信息(与 react-router 提供的信息一致)

之所以需要新制作一个组件,是因为该库必须保证整个过程使用的是同一个 history 对象

一些 action 创建函数

  • push
  • replace
+ + + + + \ No newline at end of file diff --git a/react/render.html b/react/render.html new file mode 100644 index 00000000..6b6a1289 --- /dev/null +++ b/react/render.html @@ -0,0 +1,248 @@ + + + + + + 渲染原理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

渲染原理

渲染:生成用于显示的对象,以及将这些对象形成真实的 DOM 对象

  • React 元素:React Element,通过 React.createElement 创建(语法糖:JSX)
    • 例如:
    • <div><h1>标题</h1></div>
    • <App />
  • React 节点:专门用于渲染到 UI 界面的对象,React 会通过 React 元素,创建 React 节点,ReactDOM 一定是通过 React 节点来进行渲染的
  • 节点类型:
    • React DOM 节点:创建该节点的 React 元素类型是一个字符串
    • React 组件节点:创建该节点的 React 元素类型是一个函数或是一个类
    • React 文本节点:由字符串、数字创建的
    • React 空节点:由 null、undefined、false、true
    • React 数组节点:该节点由一个数组创建
  • 真实 DOM:通过 document.createElement 创建的 dom 元素

首次渲染(新节点渲染)

  1. 通过参数的值创建节点
  2. 根据不同的节点,做不同的事情
    1. 文本节点:通过 document.createTextNode 创建真实的文本节点
    2. 空节点:什么都不做,但是存在会占位
    3. 数组节点:遍历数组,将数组每一项递归创建节点(回到第 1 步进行反复操作,直到遍历结束)
    4. DOM 节点:通过 document.createElement 创建真实的 DOM 对象,然后立即设置该真实 DOM 元素的各种属性,然后遍历对应 React 元素的 children 属性,递归操作(回到第 1 步进行反复操作,直到遍历结束)
    5. 组件节点
      1. 函数组件:调用函数(该函数必须返回一个可以生成节点的内容),将该函数的返回结果递归生成节点(回到第 1 步进行反复操作,直到遍历结束)
      2. 类组件:
        1. 创建该类的实例
        2. 立即调用对象的生命周期方法:static getDerivedStateFromProps
        3. 运行该对象的 render 方法,拿到节点对象(将该节点递归操作,回到第 1 步进行反复操作)
        4. 将该组件的 componentDidMount 加入到执行队列(先进先出,先进先执行),当整个虚拟 DOM 树全部构建完毕,并且将真实的 DOM 对象加入到容器中后,执行该队列
  3. 生成出虚拟 DOM 树之后,将该树保存起来,以便后续使用
  4. 将之前生成的真实的 DOM 对象,加入到容器中。
javascript
const app = (
+  <div className="assaf">
+    <h1>
+      标题
+      {["abc", null, <p>段落</p>]}
+    </h1>
+    <p>{undefined}</p>
+  </div>
+);
+ReactDOM.render(app, document.getElementById("root"));
+
const app = (
+  <div className="assaf">
+    <h1>
+      标题
+      {["abc", null, <p>段落</p>]}
+    </h1>
+    <p>{undefined}</p>
+  </div>
+);
+ReactDOM.render(app, document.getElementById("root"));
+

以上代码生成的虚拟 DOM 树:

javascript
function Comp1(props) {
+  return <h1>Comp1 {props.n}</h1>;
+}
+
+function App(props) {
+  return (
+    <div>
+      <Comp1 n={5} />
+    </div>
+  );
+}
+
+const app = <App />;
+ReactDOM.render(app, document.getElementById("root"));
+
function Comp1(props) {
+  return <h1>Comp1 {props.n}</h1>;
+}
+
+function App(props) {
+  return (
+    <div>
+      <Comp1 n={5} />
+    </div>
+  );
+}
+
+const app = <App />;
+ReactDOM.render(app, document.getElementById("root"));
+

以上代码生成的虚拟 DOM 树:

javascript
class Comp1 extends React.Component {
+  render() {
+    return <h1>Comp1</h1>;
+  }
+}
+
+class App extends React.Component {
+  render() {
+    return (
+      <div>
+        <Comp1 />
+      </div>
+    );
+  }
+}
+
+const app = <App />;
+ReactDOM.render(app, document.getElementById("root"));
+
class Comp1 extends React.Component {
+  render() {
+    return <h1>Comp1</h1>;
+  }
+}
+
+class App extends React.Component {
+  render() {
+    return (
+      <div>
+        <Comp1 />
+      </div>
+    );
+  }
+}
+
+const app = <App />;
+ReactDOM.render(app, document.getElementById("root"));
+

以上代码生成的虚拟 DOM 树:

面试题:

jsx
import React, { Component } from "react";
+
+class Comp1 extends React.Component {
+  state = {};
+  constructor(props) {
+    super(props);
+    console.log(4, "Comp1 constructor");
+  }
+  static getDerivedStateFromProps(prop, state) {
+    console.log(5, "Comp1 getDerivedStateFromProps");
+    return null;
+  }
+  render() {
+    console.log(6, "Comp1 render");
+    return (
+      <div>
+        <h1>Comp</h1>
+      </div>
+    );
+  }
+}
+
+export default class appp extends Component {
+  state = {};
+  constructor(props) {
+    super(props);
+    // 先创建实例,创建实例对象(运行constructor)
+    console.log(1, "App constructor");
+  }
+  static getDerivedStateFromProps(prop, state) {
+    console.log(2, "App getDerivedStateFromProps");
+    return null;
+  }
+  render() {
+    // console.log(3, "App render");
+    return (
+      <div>
+        <Comp1 />
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+
+class Comp1 extends React.Component {
+  state = {};
+  constructor(props) {
+    super(props);
+    console.log(4, "Comp1 constructor");
+  }
+  static getDerivedStateFromProps(prop, state) {
+    console.log(5, "Comp1 getDerivedStateFromProps");
+    return null;
+  }
+  render() {
+    console.log(6, "Comp1 render");
+    return (
+      <div>
+        <h1>Comp</h1>
+      </div>
+    );
+  }
+}
+
+export default class appp extends Component {
+  state = {};
+  constructor(props) {
+    super(props);
+    // 先创建实例,创建实例对象(运行constructor)
+    console.log(1, "App constructor");
+  }
+  static getDerivedStateFromProps(prop, state) {
+    console.log(2, "App getDerivedStateFromProps");
+    return null;
+  }
+  render() {
+    // console.log(3, "App render");
+    return (
+      <div>
+        <Comp1 />
+      </div>
+    );
+  }
+}
+

面试题 2

jsx
import React, { Component } from "react";
+class Comp1 extends React.Component {
+  state = {};
+  componentDidMount() {
+    console.log("b", "Comp componentDidMount");
+  }
+  render() {
+    return (
+      <div>
+        <h1>Comp</h1>
+      </div>
+    );
+  }
+}
+export default class appp extends Component {
+  state = {};
+  componentDidMount() {
+    console.log("a", "App componentDidMount");
+  }
+  render() {
+    // 先b后a原因(子组件先运行,在运行父组件)
+    // 先运行render,要进行递归操作(第三步:找类组件,找到Comp1,运行render。Comp1先加入队列)
+    // 处理完App的第三步,在运行第四布(加入队列)
+    return (
+      <div>
+        <Comp1 />
+      </div>
+    );
+  }
+}
+
import React, { Component } from "react";
+class Comp1 extends React.Component {
+  state = {};
+  componentDidMount() {
+    console.log("b", "Comp componentDidMount");
+  }
+  render() {
+    return (
+      <div>
+        <h1>Comp</h1>
+      </div>
+    );
+  }
+}
+export default class appp extends Component {
+  state = {};
+  componentDidMount() {
+    console.log("a", "App componentDidMount");
+  }
+  render() {
+    // 先b后a原因(子组件先运行,在运行父组件)
+    // 先运行render,要进行递归操作(第三步:找类组件,找到Comp1,运行render。Comp1先加入队列)
+    // 处理完App的第三步,在运行第四布(加入队列)
+    return (
+      <div>
+        <Comp1 />
+      </div>
+    );
+  }
+}
+

面试题 3:div 包住两个 App,再问执行顺序

队列:Comp1 App Comp1 App 左边执行完在执行右边而已(递归)

为什么不能写对象

对象可以构成 React 元素,但是没法构建节点,节点需要渲染,{a:1,b:2} 没法渲染

更新节点

更新的场景:

  1. 重新调用 ReactDOM.render,触发根节点更新
  2. 在类组件的实例对象中调用 setState,会导致该实例所在的节点更新

节点的更新

  • 如果调用的是 ReactDOM.render,进入根节点的对比(diff)更新
  • 如果调用的是 setState
      1. 运行生命周期函数,static getDerivedStateFromProps
      1. 运行 shouldComponentUpdate,如果该函数返回 false,终止当前流程
      1. 运行 render,得到一个新的节点,进入该新的节点的对比更新
      1. 将生命周期函数 getSnapshotBeforeUpdate 加入执行队列,以待将来执行
      1. 将生命周期函数 componentDidUpdate 加入执行队列,以待将来执行

以上两点的后续步骤:

  1. 更新虚拟 DOM 树
  2. 完成真实的 DOM 更新
  3. 依次调用执行队列中的 componentDidMount
  4. 依次调用执行队列中的 getSnapshotBeforeUpdate
  5. 依次调用执行队列中的 componentDidUpdate

对比更新

将新产生的节点,对比之前虚拟 DOM 中的节点,发现差异,完成更新

问题:对比之前 DOM 树中哪个节点

React 为了提高对比效率,做出以下假设

  1. 假设节点不会出现层次的移动(对比时,直接找到旧树中对应位置的节点进行对比)
  2. 不同的节点类型会生成不同的结构
    1. 相同的节点类型:节点本身类型相同,如果是由 React 元素生成,type 值还必须一致
    2. 其他的,都属于不相同的节点类型
  3. 多个兄弟通过唯一标识(key)来确定对比的新节点

key 值的作用:用于通过旧节点,寻找对应的新节点,如果某个旧节点有 key 值,则其更新时,会寻找相同层级中的相同 key 值的节点,进行对比。

key 值应该在一个范围内唯一(兄弟节点中),并且应该保持稳定

找到了对比的目标

判断节点类型是否一致

  • 一致

根据不同的节点类型,做不同的事情

空节点:不做任何事情

DOM 节点

  1. 直接重用之前的真实 DOM 对象
  2. 将其属性的变化记录下来,以待将来统一完成更新(现在不会真正的变化)
  3. 遍历该新的 React 元素的子元素,递归对比更新

文本节点

  1. 直接重用之前的真实 DOM 对象
  2. 将新的文本变化记录下来,将来统一完成更新

组件节点

函数组件:重新调用函数,得到一个节点对象,进入递归对比更新

类组件

  1. 重用之前的实例
  2. 调用生命周期方法 getDerivedStateFromProps
  3. 调用生命周期方法 shouldComponentUpdate,若该方法返回 false,终止
  4. 运行 render,得到新的节点对象,进入递归对比更新
  5. 将该对象的 getSnapshotBeforeUpdate 加入队列
  6. 将该对象的 componentDidUpdate 加入队列

数组节点:遍历数组进行递归对比更新

  • 不一致

整体上,卸载旧的节点,全新创建新的节点

创建新节点

进入新节点的挂载流程

卸载旧节点

  1. 文本节点、DOM 节点、数组节点、空节点、函数组件节点:直接放弃该节点,如果节点有子节点,递归卸载节点
  2. 类组件节点
    1. 直接放弃该节点
    2. 调用该节点的 componentWillUnMount 函数
    3. 递归卸载子节点

没有找到对比的目标

新的 DOM 树中有节点被删除

新的 DOM 树中有节点添加

  • 创建新加入的节点
  • 卸载多余的旧节点
+ + + + + \ No newline at end of file diff --git a/react/transition.html b/react/transition.html new file mode 100644 index 00000000..f3df1b32 --- /dev/null +++ b/react/transition.html @@ -0,0 +1,20 @@ + + + + + + React 动画 | Sunny's blog + + + + + + + + +
Skip to content
On this page

React 动画

React 动画库:react-transition-group 文档在 npm 搜索https://reactcommunity.org/react-transition-group/

React 动画 - CSSTransition

当进入时,发生:

  1. 为 CSSTransition 内部的 DOM 根元素(后续统一称之为 DOM 元素)添加样式 enter
  2. 在一下帧(enter 样式已经完全应用到了元素),立即为该元素添加样式 enter-active
  3. 当 timeout 结束后,去掉之前的样式,添加样式 enter-done

当退出时,发生:

  1. 为 CSSTransition 内部的 DOM 根元素(后续统一称之为 DOM 元素)添加样式 exit
  2. 在一下帧(exit 样式已经完全应用到了元素),立即为该元素添加样式 exit-active
  3. 当 timeout 结束后,去掉之前的样式,添加样式 exit-done

设置classNames属性,可以指定类样式的名称

  1. 字符串:为类样式添加前缀
  2. 对象:为每个类样式指定具体的名称(非前缀)

关于首次渲染时的类样式,appear、apear-active、apear-done,它和 enter 的唯一区别在于完成时,会同时加入 apear-done 和 enter-done

还可以与 Animate.css 联用

React 动画 - SwitchTransition

和 CSSTransition 的区别:用于有秩序的切换内部组件

默认情况下:out-in 先退出后进入

  1. 当 key 值改变时,会将之前的 DOM 根元素添加退出样式(exit,exit-active)
  2. 退出完成后,将该 DOM 元素移除
  3. 重新渲染内部 DOM 元素
  4. 为新渲染的 DOM 根元素添加进入样式(enter, enter-active, enter-done)

in-out:

  1. 重新渲染内部 DOM 元素,保留之前的元素
  2. 为新渲染的 DOM 根元素添加进入样式(enter, enter-active, enter-done)
  3. 将之前的 DOM 根元素添加退出样式(exit,exit-active)
  4. 退出完成后,将该 DOM 元素移除

该库寻找 dom 元素的方式,是使用已经过时的 API:findDomNode,该方法可以找到某个组件下的 DOM 根元素,先保留,创建新的之后在删除

React 动画 - TransitionGroup

该组件的 children,接收多个 Transition 或 CSSTransition 组件,该组件用于根据这些子组件的 key 值,控制他们的进入和退出状态

+ + + + + \ No newline at end of file diff --git a/react/umi.html b/react/umi.html new file mode 100644 index 00000000..7edef932 --- /dev/null +++ b/react/umi.html @@ -0,0 +1,20 @@ + + + + + + umijs 简介 | Sunny's blog + + + + + + + + +
Skip to content
On this page

umijs 简介

官网:https://umijs.org/

umijs, nextjs(ssr 服务端渲染),antd,antd-pro(antd+umijs)

  • 插件化
  • 开箱即用
  • 约定式路由

全局安装 umi

提供了一个命令行工具:umi,通过该命令可以对 umi 工程进行操作

umi 还可以使用对应的脚手架

  • dev: 使用开发模式启动工程
  • build:打包

约定式路由

umi 对路由的处理,主要通过两种方式:

  1. 约定式:使用约定好的文件夹和文件,来代表页面,umi 会根据开发者书写的页面,生成路由配置。
  2. 配置式:直接书写路由配置文件

路由匹配

  • umi 约定,工程中的 pages 文件夹中存放的是页面。如果工程包含 src 目录,则 src/pages 是页面文件夹。

  • umi 约定,页面的文件名,以及页面的文件路径,是该页面匹配的路由

  • umi 约定,如果页面的文件名是 index(不写 index 才能访问,写了反而不能访问了),则可以省略文件名(首页)(注意避免文件名和当前目录中的文件夹名称相同)

  • umi 约定,如果 src/layout 目录存在,则该目录中的 index.js 表示的是全局的通用布局,布局中的 children 则会添加具体的页面。

  • umi 约定,如果 pages 文件夹中包含_layout.js,则 layout.js 所在的目录以及其所有的子目录中的页面,共用该布局。

  • 404 约定,umi 约定,pages/404.js,表示 404 页面,如果路由无匹配,则会渲染该页面。该约定在开发模式中无效,只有部署后生效。

  • 使用$名称,会产生动态路由

路由跳转

  • 跳转链接: 导入umi/linkumi/navlink
  • 代码跳转: 导入umi/router

导入模块时,@表示 src 目录

路由信息的获取

所有的页面、布局组件,都会通过属性 props,收到下面的属性

  • match:等同于 react-router 的 match
  • history:等同于 react-router 的 history(history.location.query 被封装成了一个对象,使用的是 query-string 库进行的封装)
  • location:等同于 react-router 的 location(location.query 被封装成了一个对象,使用的是 query-string 库进行的封装)
  • route:对应的是路由配置

如果需要在普通组件中获取路由信息,则需要使用 withRouter 封装,可以通过umi/withRouter导入

配置式路由

当使用了路由配置后,约定式路由全部失效。

两种方式书写 umi 配置:

  1. 使用根目录下的文件.umirc.js
  2. 使用根目录下的文件config/config.js

进行路由配置时,每个配置就是一个匹配规则,并且,每个配置是一个对象,对象中的某些属性,会直接形成 Route 组件的属性

注意:

  • component 配置项,需要填写页面组件的路径,路径相对于 pages 文件夹
  • 如果配置项没有 exact,则会自动添加 exact 为 true
  • 每一个路由配置,可以添加任何属性
  • Routes 属性是一个数组,数组的每一项是一个组件路径,路径相对于项目根目录,当匹配到路由后,会转而渲染该属性指定的组件,并会将 component 组件作为 children 放到匹配的组件中

路由配置中的信息,同样可以放到约定式路由中,方式是,为约定式路由添加第一个文档注释(注释的格式的 YAML),需要将注释放到最开始的位置

YAML 格式

  • 键值对,冒号后需要加上空格
  • 如果某个属性有多个键或多个值,需要进行缩进(空格,不能 tab)

使用 dva

官方插件集 umi-plugin-react 文档:https://umijs.org/zh/plugin/umi-plugin-react.html

dva 插件和 umi 整合后,将模型分为两种:

  1. 全局模型:所有页面通用,工程一开始启动后,模型就会挂载到仓库
  2. 局部模型:只能被某些页面使用,访问具体的页面时才会挂载到仓库

定义全局模型

src/models目录下定义的 js 文件都会被看作是全局模型,默认情况下,模型的命名空间和文件名一致。

定义局部模型

局部模型定义在 pages 文件夹或其子文件夹中,在哪个文件夹定义的模型,会被该文件夹中的所有页面以及子页面、以及该文件夹的祖先文件夹中的页面所共享。

局部模型的定义和全局模型的约定类似,需要创建一个 models 文件夹

使用样式

解决两个问题:

  1. 保证类样式名称的唯一性:css-module
  2. 样式代码的重复:less 或 sass

局部样式和全局样式

底层使用了 webpack 的加载器:css-loader(内部包含了 css-module 的功能)

css 文件 -> css-module -> 对象

  1. 某个组件特有的样式,不与其他组件共享,通常,将该样式文件与组件放置在同一个目录(非强制性)(要保证类样式名称唯一)
  2. 如果某些样式可能被某些组件共享,这样的样式,通常放到 assets/css 文件夹中。(要保证类样式名称唯一)
  3. 全局样式,名称一定唯一,不需要 css-module 处理。umijs 约定,src/global.css 样式,是全局样式,不会交给 css-module 处理。

less

less 代码 -> less-loader -> css 代码 -> css-module -> 对象

代理和数据模拟

代理

代理用于解决跨域问题

配置.umirc.js中的 proxy,配置方式和 devServer 中的 proxy 配置相同

数据模拟

用于解决前后端协同开发的问题

数据模拟可以让前端开发者在开发时,无视后端接口是否真正完成,因为使用的是模拟的数据

umijs 约定:

  1. mock 文件夹中的文件
  2. src/pages 文件夹中的_mock.js 文件

以上两种 JS 文件,均会被 umijs 读取,并作为数据模拟的配置

可以自行发挥,添加模拟数据,通常,我们会和 mockjs 配合。

配置

额外的约定文件

  • src/pages/document.ejs: 页面模板文件
  • src/global.js:在 umi 最开始启动时运行的 js 文件
  • src/app.js:作运行时配置的代码
    • patchRoutes: 函数,该函数会在 umi 读取完所有静态路由配置后执行
    • dva
      • config: 相当于 new dva(配置)
      • plugins: 相当于 dva.use(插件)
  • .env: 配置环境变量,这些变量会在 umi 编译期间发挥作用
    • UMI_ENV:umi 的环境变量值,可以是任意值,该值会影响到.umirc.js
    • PORT
    • MOCK

umirc 配置

umi 配置

书写在.umirc.js 文件中的配置

  • plugins:配置 umijs 的插件
  • routes:配置路由(会导致约定式路由失效)
  • history:history 对象模式(默认是 browser)
  • outputPath:使用 umi build 后,打包的目录名称,默认./dist
  • base: 相当于之前 BrowserRouter 中的 basename
  • publicPath: 指定静态资源所在的目录
  • exportStatic: 开启该配置后,会打包成多个静态页面,每个页面对应一个路由,开启多静态页面应用的前提条件是:没有动态路由

webpack 配置

umi 脚手架

create-umi

+ + + + + \ No newline at end of file diff --git a/react/utils.html b/react/utils.html new file mode 100644 index 00000000..ad1c6163 --- /dev/null +++ b/react/utils.html @@ -0,0 +1,20 @@ + + + + + + 工具 | Sunny's blog + + + + + + + + +
Skip to content
On this page

工具

严格模式

StrictMode(React.StrictMode),本质是一个组件,该组件不进行 UI 渲染(React.Fragment <> </>),它的作用是,在渲染内部组件时,发现不合适的代码。

  • 识别不安全的生命周期
  • 关于使用过时字符串 ref API 的警告
  • 关于使用废弃的 findDOMNode 方法的警告
  • 检测意外的副作用
    • React 要求,副作用代码仅出现在以下生命周期函数中
    • ComponentDidMount
    • ComponentDidUpdate
    • ComponentWillUnMount

副作用:一个函数中,做了一些会影响函数外部数据的事情,例如:

  1. 异步处理
  2. 改变参数值
  3. setState
  4. 本地存储
  5. 改变函数外部的变量

相反的,如果一个函数没有副作用,则可以认为该函数是一个纯函数

在严格模式下,虽然不能监控到具体的副作用代码,但它会将不能具有副作用的函数调用两遍,以便发现问题。(这种情况,仅在开发模式下有效)

  • 检测过时的 context API

Profiler

性能分析工具

分析某一次或多次提交(更新),涉及到的组件的渲染时间

火焰图:得到某一次提交,每个组件总的渲染时间以及自身的渲染时间

排序图:得到某一次提交,每个组件自身渲染时间的排序

组件图:某一个组件,在多次提交中,自身渲染花费的时间

+ + + + + \ No newline at end of file diff --git a/ts/2023-02-07-11-12-59.png b/ts/2023-02-07-11-12-59.png new file mode 100644 index 00000000..2c77fc8b Binary files /dev/null and b/ts/2023-02-07-11-12-59.png differ diff --git a/ts/2023-02-07-11-14-40.png b/ts/2023-02-07-11-14-40.png new file mode 100644 index 00000000..85014e02 Binary files /dev/null and b/ts/2023-02-07-11-14-40.png differ diff --git a/ts/2023-02-07-11-14-45.png b/ts/2023-02-07-11-14-45.png new file mode 100644 index 00000000..2f3faa02 Binary files /dev/null and b/ts/2023-02-07-11-14-45.png differ diff --git a/ts/2023-02-07-11-14-51.png b/ts/2023-02-07-11-14-51.png new file mode 100644 index 00000000..8b1e7f17 Binary files /dev/null and b/ts/2023-02-07-11-14-51.png differ diff --git a/ts/2023-02-07-11-15-52.png b/ts/2023-02-07-11-15-52.png new file mode 100644 index 00000000..f54acde6 Binary files /dev/null and b/ts/2023-02-07-11-15-52.png differ diff --git a/ts/2023-02-07-11-16-05.png b/ts/2023-02-07-11-16-05.png new file mode 100644 index 00000000..b597ae96 Binary files /dev/null and b/ts/2023-02-07-11-16-05.png differ diff --git a/ts/2023-02-07-11-16-13.png b/ts/2023-02-07-11-16-13.png new file mode 100644 index 00000000..011d42a6 Binary files /dev/null and b/ts/2023-02-07-11-16-13.png differ diff --git a/ts/2023-02-07-11-16-20.png b/ts/2023-02-07-11-16-20.png new file mode 100644 index 00000000..ba7de096 Binary files /dev/null and b/ts/2023-02-07-11-16-20.png differ diff --git a/ts/2023-02-07-11-17-25.png b/ts/2023-02-07-11-17-25.png new file mode 100644 index 00000000..9497cb54 Binary files /dev/null and b/ts/2023-02-07-11-17-25.png differ diff --git a/ts/2023-02-07-11-18-39.png b/ts/2023-02-07-11-18-39.png new file mode 100644 index 00000000..5244aefc Binary files /dev/null and b/ts/2023-02-07-11-18-39.png differ diff --git a/ts/2023-02-07-11-20-12.png b/ts/2023-02-07-11-20-12.png new file mode 100644 index 00000000..c1388bb9 Binary files /dev/null and b/ts/2023-02-07-11-20-12.png differ diff --git a/ts/2023-02-07-11-20-27.png b/ts/2023-02-07-11-20-27.png new file mode 100644 index 00000000..c206019e Binary files /dev/null and b/ts/2023-02-07-11-20-27.png differ diff --git a/ts/2023-02-07-11-20-35.png b/ts/2023-02-07-11-20-35.png new file mode 100644 index 00000000..7c1ecec4 Binary files /dev/null and b/ts/2023-02-07-11-20-35.png differ diff --git a/ts/2023-02-07-11-21-01.png b/ts/2023-02-07-11-21-01.png new file mode 100644 index 00000000..3f7f8ec4 Binary files /dev/null and b/ts/2023-02-07-11-21-01.png differ diff --git a/ts/2023-02-07-11-21-11.png b/ts/2023-02-07-11-21-11.png new file mode 100644 index 00000000..84031a7e Binary files /dev/null and b/ts/2023-02-07-11-21-11.png differ diff --git a/ts/2024-01-27-11-37-14.png b/ts/2024-01-27-11-37-14.png new file mode 100644 index 00000000..b04b990b Binary files /dev/null and b/ts/2024-01-27-11-37-14.png differ diff --git a/ts/TypeScript-onePage.html b/ts/TypeScript-onePage.html new file mode 100644 index 00000000..0f086a81 --- /dev/null +++ b/ts/TypeScript-onePage.html @@ -0,0 +1,2214 @@ + + + + + + TypeScript onePage | Sunny's blog + + + + + + + + +
Skip to content
On this page

TypeScript onePage

为什么学习 TS

  • 获得更好的开发体验
  • 解决 JS 中难以解决的问题
javascript
function getUserName() {
+  if (Math.random() < 0.5) {
+    return "sunny-117";
+  }
+  return 404;
+}
+let myname = getUserName(); //名字可能错了,类型也可能错误
+myname = myname
+  .split(" ")
+  .filter((it) => it) //过滤空的
+  .map((it) => it[0].toUpperCase() + it.substr(1))
+  .join(" ");
+
function getUserName() {
+  if (Math.random() < 0.5) {
+    return "sunny-117";
+  }
+  return 404;
+}
+let myname = getUserName(); //名字可能错了,类型也可能错误
+myname = myname
+  .split(" ")
+  .filter((it) => it) //过滤空的
+  .map((it) => it[0].toUpperCase() + it.substr(1))
+  .join(" ");
+

JS 的问题

  • 使用了不存在的变量,函数或成员
  • 把一个不确定的类型当做一个确定的类型处理
  • 在使用 undefined null 的成员

JS 原罪

  • JS 语言本身特性决定了改语言无法适应大型复杂项目
  • 弱类型:某个变量,可以随时更换类型
  • 解释性:错误的发生时间,是在运行时

前端开发中,大部分时间都在排错

TypeScript

TS 是 JS 的超集,是一个可选的,静态的类型系统

类型系统:对代码中所有的标识符(变量,函数,参数,返回值)进行类型检查

静态的:无论浏览器环境,还是 node,无法直接识别 TS 代码

babel:es6--->es5 tsc:ts---->es tsc:ts 编译器

静态:类型检查发生的时间,在编译的时候,而非运行时

TS 不参与任何运行时候的类型检查,运行的是 JS 代码

TS 常识

额外的惊喜

有了类型检查,增强了面向对象的开发

JS 中, 也有类和对象,JS 支持面向对象开发。没有类型检查,很多面向对象的场景实现起来有诸多问题

使用 TS 后,可以编写出完善的面向对象代码。

在 node 中搭建 TS 开发环境

默认情况 TS 会做出假设:

  • 假设当前执行环境是 DOM
  • 如果使用代码中没有使用模块化语句,便认为改代码全局执行
  • 编译的目标代码是 ES3

有两种方式更改以上假设:

  • 使用 tsc 命令时加上选项参数
  • 使用 TS 配置文件,更改编译选项

TS 配置文件

  1. tsconfig.json
  2. tsc --init 生成配置文件

使用配置文件后,使用 tsc 进行编译时,不能跟上文件名,如果跟上的话,会忽略配置文件

直接 tsc 即可编译

javascript
{
+  "compilerOptions": {//编译选项
+    "target":"es2016",//配置编译目标代码的版本标准
+    "module":"commonjs",//配置编译目标使用的模块化标准
+    "lib":["es2016"],//这里没有node环境可以配置,但是去掉了浏览器环境,
+    // 也不知道这是node环境,所以导致console也没有了,这里必须要安装第三大库
+    "outDir":"./dist/"//dist里面放编译结果
+  },
+  "include":["./src"]//要编译的文件夹。默认整个工程
+}
+
{
+  "compilerOptions": {//编译选项
+    "target":"es2016",//配置编译目标代码的版本标准
+    "module":"commonjs",//配置编译目标使用的模块化标准
+    "lib":["es2016"],//这里没有node环境可以配置,但是去掉了浏览器环境,
+    // 也不知道这是node环境,所以导致console也没有了,这里必须要安装第三大库
+    "outDir":"./dist/"//dist里面放编译结果
+  },
+  "include":["./src"]//要编译的文件夹。默认整个工程
+}
+

第三方库:@types/node

@types 是一个 ts 官方的类型库 其中包含了很多对 JS 代码的类型描述。(第三方库 axios,lodash,mock 等是 JS 写的,没有类型检查,我需要类型检查,就去 types 里面找有没有对应的类型库)

jQuery:js 写的,没有类型检查 安装@types/jquery,为 jquery 库添加类型定义

cnpm i -D @types/node 开发依赖,运行时候不需要,所以-D

json
{
+  "compilerOptions": {
+    //...
+  },
+  "includes": ["./src"], //标识要编译的文件夹,默认所有
+  "files": ["./src/index.ts"] // 编译文件夹里面的某个文件
+}
+
{
+  "compilerOptions": {
+    //...
+  },
+  "includes": ["./src"], //标识要编译的文件夹,默认所有
+  "files": ["./src/index.ts"] // 编译文件夹里面的某个文件
+}
+

编译和运行流程:

得先 tsc 编译 ts 在 node index.js 运行 js 嫌麻烦,所以衍生出用第三方库简化流程

第三方库简化编译运行流程

ts-node:编译完没有 dist 目录,没有 js 文件,直接运行 js

cnpm i -g ts-node 安装后,运行用 ts-node src/index.ts,编译完直接执行,没有 dist 文件

监控代码变化,变化后再次编译运行

nodemon: 用于检测文件变化

cnpm i -g nodemon

执行命令:nodemon --exec ts-node .\src\index.ts

可以把这个命令弄到 package.json 里面

json
{
+  "scripts": {
+    "dev": "nodemon --exec ts-node ./src/index.ts"
+  }
+}
+
{
+  "scripts": {
+    "dev": "nodemon --exec ts-node ./src/index.ts"
+  }
+}
+

两个细节

  • nodemon 检测的范围太广,我只想让他检测 ts
javascript
 "dev":"nodemon -e ts --exec ts-node ./src/index.ts"//表示检测文件的拓展名ts
+
 "dev":"nodemon -e ts --exec ts-node ./src/index.ts"//表示检测文件的拓展名ts
+
  • src 文件夹之外的 ts 文件
javascript
"dev":"nodemon --watch src -e ts --exec ts-node ./src/index.ts"//只监控文件夹src的
+
"dev":"nodemon --watch src -e ts --exec ts-node ./src/index.ts"//只监控文件夹src的
+

最终可以使用 tsc 完成最后的打包

基本类型约束

TS 是一个可选的静态的类型系统

如何进行类型约束

仅需要在 变量、函数的参数、函数的返回值位置加上:类型

javascript
function sum(a: number, b: number): number {
+  return a + b;
+}
+console.log(sum(3, 4));
+
function sum(a: number, b: number): number {
+  return a + b;
+}
+console.log(sum(3, 4));
+

ts 在很多场景中可以完成类型推导

any: 表示任意类型,对该类型,ts 不进行类型检查

小技巧,如何区分数字字符串和数字,关键看怎么读? 如果按照数字的方式朗读,则为数字;否则,为字符串。

源代码和编译结果的差异

编译结果中没有类型约束信息

基本类型

  • number:数字
  • string:字符串
  • boolean:布尔
  • 数组
  • object: 对象
  • null 和 undefined

boolean

javascript
function isOdd(n: number): boolean {
+  return n % 2 === 0;
+}
+console.log(isOdd(3));
+
function isOdd(n: number): boolean {
+  return n % 2 === 0;
+}
+console.log(isOdd(3));
+

数组

javascript
let num: number[]; //num必须是数组,数组里每一项都是number
+num = [1, 2];
+// 或语法糖
+let num: Array<number> = [3, 4, 5];
+
let num: number[]; //num必须是数组,数组里每一项都是number
+num = [1, 2];
+// 或语法糖
+let num: Array<number> = [3, 4, 5];
+

object: 对象

javascript
let u: object;
+u = {
+  name: "abc",
+  age: 19,
+  // 不能约束对象里面的东西
+};
+
let u: object;
+u = {
+  name: "abc",
+  age: 19,
+  // 不能约束对象里面的东西
+};
+

应用场景

javascript
// 传入一个对象,打印对象所有属性值
+function printValues(obj: object) {
+  const vals = Object.values(obj); //返回的是一个any类型的数组(不知道数组每一项是什么的数组)
+  vals.forEach((v) => {
+    console.log(v);
+  });
+}
+printValues({
+  name: "abc",
+  age: 19,
+});
+
// 传入一个对象,打印对象所有属性值
+function printValues(obj: object) {
+  const vals = Object.values(obj); //返回的是一个any类型的数组(不知道数组每一项是什么的数组)
+  vals.forEach((v) => {
+    console.log(v);
+  });
+}
+printValues({
+  name: "abc",
+  age: 19,
+});
+

null 和 undefined 是所有其他类型的子类型,它们可以赋值给其他类型

javascript
let m: string = null; //隐患产生了
+let n: string = undefined; //隐患产生了
+n.toLocaleLowerCase(); //报错
+
let m: string = null; //隐患产生了
+let n: string = undefined; //隐患产生了
+n.toLocaleLowerCase(); //报错
+

通过在 tsconfig.json 里面添加strictNullChecks:true,可以获得更严格的空类型检查,null 和 undefined 只能赋值给自身。

其他常用类型

  • 联合类型:多种类型任选其一

配合类型保护进行判断

typescript
let name: string | undefined; // 联合类型
+// name.的时候没有提示了
+if (typeof name === "string") {
+  // name.  确定为string出现提示了
+}
+//即类型保护
+
let name: string | undefined; // 联合类型
+// name.的时候没有提示了
+if (typeof name === "string") {
+  // name.  确定为string出现提示了
+}
+//即类型保护
+

类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof 可以触发基本类型保护。

  • void 类型:通常用于约束函数的返回值,表示该函数没有任何返回

  • never 类型:通常用于约束函数的返回值,表示该函数永远不可能结束

js
function test(): never {
+  throw new Error("123");
+  console.log("此处代码无法访问");
+}
+
function test(): never {
+  throw new Error("123");
+  console.log("此处代码无法访问");
+}
+
js
function test(): never {
+  while (true) {}
+}
+
function test(): never {
+  while (true) {}
+}
+
  • 字面量类型:使用一个值进行约束

字符串 数组 对象

  • 元祖类型(Tuple): 一个固定长度的数组,并且数组中每一项的类型确定

  • any 类型: any 类型可以绕过类型检查,因此,any 类型的数据可以赋值给任意类型

类型别名

对已知的一些类型定义名称

type 类型名 = ...
+
type 类型名 = ...
+
typescript
type Gender = "男" | "女";
+type User = {
+  name: string;
+  age: number;
+  gender: Gender;
+};
+let u: User;
+
+u = {
+  name: "ads",
+  gender: "男",
+  age: 34,
+};
+
+function getUsers(g: Gender): User[] {
+  return [];
+}
+getUsers("女");
+
type Gender = "男" | "女";
+type User = {
+  name: string;
+  age: number;
+  gender: Gender;
+};
+let u: User;
+
+u = {
+  name: "ads",
+  gender: "男",
+  age: 34,
+};
+
+function getUsers(g: Gender): User[] {
+  return [];
+}
+getUsers("女");
+

函数的相关约束

函数重载:在函数实现之前,对函数调用的多种情况进行声明

typescript
function combine(a: number, b: number): number;
+function combine(a: string, b: string): string;
+function combine(a: number | string, b: number | string): number | string {
+  //两个数字,相乘返回;
+  //两个字符串,拼接返回
+  if (typeof a === "number" && typeof b === "number") {
+    return a * b;
+  } else if (typeof a === "string" && typeof b === "string") {
+    return a + b;
+  }
+  throw new Error("a和b必须是相同类型");
+}
+const result1 = combine(1, 2);
+const result2 = combine("1", "2");
+
function combine(a: number, b: number): number;
+function combine(a: string, b: string): string;
+function combine(a: number | string, b: number | string): number | string {
+  //两个数字,相乘返回;
+  //两个字符串,拼接返回
+  if (typeof a === "number" && typeof b === "number") {
+    return a * b;
+  } else if (typeof a === "string" && typeof b === "string") {
+    return a + b;
+  }
+  throw new Error("a和b必须是相同类型");
+}
+const result1 = combine(1, 2);
+const result2 = combine("1", "2");
+

可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。

默认参数(一旦传递了默认参数,就默认可选)

扩展类型-枚举

扩展类型:类型别名、枚举、接口、类

枚举通常用于约束某个变量的取值范围。

字面量和联合类型配合使用,也可以达到同样的目标。

字面量类型的问题

  • 在类型约束位置,会产生重复代码。可以使用类型别名解决该问题。
javascript
type Gender = "男" | "女";
+let gender: Gender;
+gender = "男";
+function searchGender(g: Gender) {}
+
type Gender = "男" | "女";
+let gender: Gender;
+gender = "男";
+function searchGender(g: Gender) {}
+
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
javascript
type Gender = "帅哥" | "美女"; //把男女改为帅哥美女
+
type Gender = "帅哥" | "美女"; //把男女改为帅哥美女
+
  • 字面量类型不会进入到编译结果。

枚举

如何定义一个枚举:

enum 枚举名{
+    枚举字段1 = 值1,
+    枚举字段2 = 值2,
+    ...
+}
+
enum 枚举名{
+    枚举字段1 = 值1,
+    枚举字段2 = 值2,
+    ...
+}
+

逻辑名称和真实名称区分开来

javascript
enum Gender{
+    male = "男",
+    female = "女"
+}
+let gender =  Gender.male;
+console.log(gender)
+
enum Gender{
+    male = "男",
+    female = "女"
+}
+let gender =  Gender.male;
+console.log(gender)
+

枚举会出现在编译结果中,编译结果中表现为对象。

typescript
enum Gender {
+  male = "男",
+  female = "女",
+}
+let gender = Gender.male;
+
+function printGenders() {
+  const vals = Object.values(Gender);
+  vals.forEach((v) => console.log(v));
+}
+printGenders();
+
enum Gender {
+  male = "男",
+  female = "女",
+}
+let gender = Gender.male;
+
+function printGenders() {
+  const vals = Object.values(Gender);
+  vals.forEach((v) => console.log(v));
+}
+printGenders();
+

枚举的规则:

  • 枚举的字段值可以是字符串或数字
  • 数字枚举的值会自动自增
typescript
enum Level {
+  level1 = 1, //不赋值就是0,后面依次递增
+  level2,
+  level3,
+}
+let l: Level = Level.level1;
+l = Level.level2;
+console.log(l); //2
+
enum Level {
+  level1 = 1, //不赋值就是0,后面依次递增
+  level2,
+  level3,
+}
+let l: Level = Level.level1;
+l = Level.level2;
+console.log(l); //2
+
  • 被数字枚举约束的变量,可以直接赋值为数字
  • 数字枚举的编译结果 和 字符串枚举有差异

最佳实践:

  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值(逻辑含义与真实区分开)

扩展:位枚举(枚举的位运算)

针对的数字枚举

位运算:两个数字换算成 2 进制后进行的计算

typescript
enum Permission {
+  // 通过2进制位上的标识确定权限
+  Read = 1, //2^0   0001
+  Write = 2, //2^1  0010
+  Create = 4, //2^2 0100
+  Delete = 8, //2^3 1000
+}
+//3 == 0011 可读,可写
+// 1. 如何组合权限:    |:或运算
+// let p1 = Permission.Read | Permission.Write;
+let p2: Permission = Permission.Read | Permission.Write;
+
+// 2. 如何判断是否拥有某个权限
+
+function hasPermission(target: Permission, per: Permission) {
+  //target里面包不包含per
+  return (target & per) === per; //且运算
+}
+// 判断变量p2是否拥有可读权限
+hasPermission(p2, Permission.Read);
+
+// 3. 如何删除某个权限
+
+p2 = p2 ^ Permission.Write; //异或(两个位置相同取0不同取1)
+
enum Permission {
+  // 通过2进制位上的标识确定权限
+  Read = 1, //2^0   0001
+  Write = 2, //2^1  0010
+  Create = 4, //2^2 0100
+  Delete = 8, //2^3 1000
+}
+//3 == 0011 可读,可写
+// 1. 如何组合权限:    |:或运算
+// let p1 = Permission.Read | Permission.Write;
+let p2: Permission = Permission.Read | Permission.Write;
+
+// 2. 如何判断是否拥有某个权限
+
+function hasPermission(target: Permission, per: Permission) {
+  //target里面包不包含per
+  return (target & per) === per; //且运算
+}
+// 判断变量p2是否拥有可读权限
+hasPermission(p2, Permission.Read);
+
+// 3. 如何删除某个权限
+
+p2 = p2 ^ Permission.Write; //异或(两个位置相同取0不同取1)
+

模块化

相关配置:

配置名称含义
module设置编译结果中使用的模块化标准
moduleResolution设置解析模块的模式
noImplicitUseStrict编译结果中不包含"use strict"
removeComments编译结果移除注释
noEmitOnError错误时不生成编译结果
esModuleInterop启用 es 模块化交互非 es 模块导出

前端领域中的模块化标准:ES6、commonjs、amd、umd、system、esnext

TS 中如何书写模块化语句 编译结果??

TS 中如何书写模块化语句

TS 中,导入和导出模块,统一使用 ES6 的模块化标准

如果不写导入,可以快速修复

必须是声明导出方式,如果使用默认导出,就没有智能导入了

typescript
export default {
+  name: "kevin",
+  sum(a: number, b: number) {
+    return a + b;
+  },
+};
+
export default {
+  name: "kevin",
+  sum(a: number, b: number) {
+    return a + b;
+  },
+};
+

导入的时候不要添加.ts,因为编译后是 js 了,但是编译结果没有 js 文件

编译结果中的模块化

可配置:在配置文件中设置

json
 "module":"commonjs",
+
 "module":"commonjs",
+

如果不希望注释也在编译的结果中:在配置文件里面写上

json
{
+  "removeComments": true
+}
+
{
+  "removeComments": true
+}
+

TS 中的模块化在编译结果中:

  • 如果编译结果的模块化标准是 ES6: 没有区别
  • 如果编译结果的模块化标准是 commonjs:导出的声明会变成 exports 的属性,默认的导出会变成 exports 的 default 属性;导入:把整个对象拿到,依次取属性

解决默认导入的错误

方案 1

方案 2 方案 3

"esModuleInterop": true

如何在 TS 中书写 commonjs 模块化代码(一般不会遇到此问题)

如果使用之前的 commonjs 的话,就失去类型检查功能了 可以这样导入(条件:必须经过 esModuleInterop 的配置):import myModule from './myModule',就有类型检查了

使用以下语法就可以解决:

导出:export = xxx

导入:import xxx = require("xxx")

模块解析

模块解析:应该从什么位置寻找模块

TS 中,有两种模块解析策略

  • classic:经典(过时)
  • node:node 解析策略(唯一的变化,是将 js 替换为 ts)
    • 相对路径require("./xxx") 先找当前路径有没有,没有的话看package.json的main.ts,没有就这个文件夹下的index.ts
    • 非相对模块require("xxx") 当前文件夹有没有node-modules,在node-modules下有没有改模块

为了防止出错,强行 node 解析策略:加上配置:"moduleResolution":"node"

接口和类型兼容性

扩展类型-接口

接口:inteface

扩展类型:类型别名、枚举、接口、类

TypeScript 的接口:用于约束类、对象、函数的契约(标准)

契约(标准)的形式:

  • API 文档,弱标准
  • 代码约束,强标准

和类型别名一样,接口,不出现在编译结果中

  1. 接口约束对象
  2. 接口约束函数

对象

typescript
interface User {
+  name: string;
+  age: number;
+}
+// 和类型别名的区别:约束类
+/* type User = {
+    name:string,
+    age:number
+} */
+let u: User = {
+  name: "abc",
+  age: 19,
+};
+
interface User {
+  name: string;
+  age: number;
+}
+// 和类型别名的区别:约束类
+/* type User = {
+    name:string,
+    age:number
+} */
+let u: User = {
+  name: "abc",
+  age: 19,
+};
+

函数

typescript
interface User {
+  name: string;
+  age: number;
+  // 书写方式1    sayHello:() => void
+  // 书写方式2    sayHello():void
+  // 和类型别名一样,接口,不出现在编译结果中,所以不能写函数实现
+  sayHello(): void;
+}
+let u: User = {
+  name: "abc",
+  age: 19,
+  sayHello() {
+    console.log("asda");
+  },
+};
+
interface User {
+  name: string;
+  age: number;
+  // 书写方式1    sayHello:() => void
+  // 书写方式2    sayHello():void
+  // 和类型别名一样,接口,不出现在编译结果中,所以不能写函数实现
+  sayHello(): void;
+}
+let u: User = {
+  name: "abc",
+  age: 19,
+  sayHello() {
+    console.log("asda");
+  },
+};
+

类型别名约束函数

typescript
type Condition = (n: number) => boolean;
+
+function sum(numbers: number[], callBack: Condition) {
+  let s = 0;
+  numbers.forEach((n) => {
+    if (callBack(n)) {
+      s += n;
+    }
+  });
+  return s;
+}
+const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
+console.log(result);
+
type Condition = (n: number) => boolean;
+
+function sum(numbers: number[], callBack: Condition) {
+  let s = 0;
+  numbers.forEach((n) => {
+    if (callBack(n)) {
+      s += n;
+    }
+  });
+  return s;
+}
+const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
+console.log(result);
+

接口约束函数

typescript
interface Condition {
+  (n: number): boolean;
+}
+
+// 可以这样写
+/* type Condition = {//定界符
+    (n:number):boolean
+} */
+
+function sum(numbers: number[], callBack: Condition) {
+  let s = 0;
+  numbers.forEach((n) => {
+    if (callBack(n)) {
+      s += n;
+    }
+  });
+  return s;
+}
+const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
+console.log(result);
+
interface Condition {
+  (n: number): boolean;
+}
+
+// 可以这样写
+/* type Condition = {//定界符
+    (n:number):boolean
+} */
+
+function sum(numbers: number[], callBack: Condition) {
+  let s = 0;
+  numbers.forEach((n) => {
+    if (callBack(n)) {
+      s += n;
+    }
+  });
+  return s;
+}
+const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
+console.log(result);
+

接口可以继承

typescript
interface A {
+  T1: string;
+}
+interface B extends A {
+  //接口B里面有A里面所有成员
+  T2: number;
+}
+let u: B = {
+  T2: 33,
+  T1: "ab",
+};
+
interface A {
+  T1: string;
+}
+interface B extends A {
+  //接口B里面有A里面所有成员
+  T2: number;
+}
+let u: B = {
+  T2: 33,
+  T1: "ab",
+};
+

可以通过接口之间的继承,实现多种接口的组合

typescript
interface A {
+  T1: string;
+}
+interface B {
+  T2: number;
+}
+interface C extends A, B {
+  T3: boolean;
+}
+let u: C = {
+  T2: 33,
+  T1: "abc",
+  T3: true,
+};
+
interface A {
+  T1: string;
+}
+interface B {
+  T2: number;
+}
+interface C extends A, B {
+  T3: boolean;
+}
+let u: C = {
+  T2: 33,
+  T1: "abc",
+  T3: true,
+};
+

使用类型别名可以实现类似的组合效果,需要通过&,它叫做交叉类型

typescript
type A = {
+  T1: string;
+};
+type B = {
+  T2: number;
+};
+type C = {
+  T3: boolean;
+} & A &
+  B;
+
+let u: C = {
+  T2: 123,
+  T1: "abc",
+  T3: true,
+};
+
type A = {
+  T1: string;
+};
+type B = {
+  T2: number;
+};
+type C = {
+  T3: boolean;
+} & A &
+  B;
+
+let u: C = {
+  T2: 123,
+  T1: "abc",
+  T3: true,
+};
+

它们的区别:

  • 子接口不能覆盖父接口的成员
  • 交叉类型会把相同成员的类型进行交叉 不是覆盖 无法正常赋值了

readonly

只读修饰符,修饰的目标是只读

只读修饰符不在编译结果中

类型兼容性

B->A,如果能完成赋值,则 B 和 A 类型兼容

鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可

  • 基本类型:完全匹配
  • 对象类型:鸭子辨型法

类型断言

typescript
interface Duck {
+  sound: "嘎嘎嘎";
+  swin(): void;
+}
+let person = {
+  name: "伪装成鸭子的人",
+  age: 11,
+  //sound: "嘎嘎嘎",这里需要类型断言,因为这里的sound:嘎嘎嘎是string,但是要求sound是嘎嘎嘎字面量
+  sound: "嘎嘎嘎" as "嘎嘎嘎",
+  swin() {
+    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
+  },
+};
+let duck: Duck = person; //这个结构满足鸭子的特征,所以可以赋值了
+
interface Duck {
+  sound: "嘎嘎嘎";
+  swin(): void;
+}
+let person = {
+  name: "伪装成鸭子的人",
+  age: 11,
+  //sound: "嘎嘎嘎",这里需要类型断言,因为这里的sound:嘎嘎嘎是string,但是要求sound是嘎嘎嘎字面量
+  sound: "嘎嘎嘎" as "嘎嘎嘎",
+  swin() {
+    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
+  },
+};
+let duck: Duck = person; //这个结构满足鸭子的特征,所以可以赋值了
+

解决的问题:假设有个函数,用于得到服务器某个接口的返回结果,是一个用户对象。对象有很多属性,但是实际需要的属性很少,就可以用鸭子辨型法。

当直接使用对象字面量赋值的时候,会进行更加严格的判断

  • 函数类型

一切无比自然

参数:传递给目标函数的参数可以少,但不可以多

typescript
interface Condition {
+  (n: number, i: number): boolean;
+}
+
+function sum(numbers: number[], callBack: Condition) {
+  let s = 0;
+  for (let i = 0; i < numbers.length; i++) {
+    const n = numbers[i];
+    if (callBack(n, i)) {
+      s += n;
+    }
+  }
+  return s;
+}
+// const result = sum([3, 4, 5, 6, 7], n => n % 2 !== 0);
+// const result = sum([3, 4, 5, 6, 7], (n, i) => i % 2 !== 0);
+// console.log(result);
+
interface Condition {
+  (n: number, i: number): boolean;
+}
+
+function sum(numbers: number[], callBack: Condition) {
+  let s = 0;
+  for (let i = 0; i < numbers.length; i++) {
+    const n = numbers[i];
+    if (callBack(n, i)) {
+      s += n;
+    }
+  }
+  return s;
+}
+// const result = sum([3, 4, 5, 6, 7], n => n % 2 !== 0);
+// const result = sum([3, 4, 5, 6, 7], (n, i) => i % 2 !== 0);
+// console.log(result);
+

返回值:要求返回必须返回;不要求返回,你随意;

忠告:不要死记硬背,理解 TS,舒服安全逻辑地写代码

TS 中的类

面向对象思想

基础部分,学习类的时候,仅讨论新增的语法部分。

属性

js
class User {
+  constructor(name: string) {
+    this.name = name; // 报错
+  }
+  // TS不允许动态添加属性
+  // const obj={}
+  // obj.name ='aaa' 报错
+}
+
class User {
+  constructor(name: string) {
+    this.name = name; // 报错
+  }
+  // TS不允许动态添加属性
+  // const obj={}
+  // obj.name ='aaa' 报错
+}
+

使用属性列表来描述类中的属性

属性的初始化检查配置

strictPropertyInitialization:true

属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

默认:性别男

方法 1:构造函数中

typescript
class User {
+  name: string;
+  age: number;
+  gender: "男" | "女";
+  constructor(name: string, age: number, gender: "男" | "女" = "男") {
+    this.name = name;
+    this.age = age;
+    this.gender = gender;
+  }
+}
+const u = new User("abc", 19);
+
class User {
+  name: string;
+  age: number;
+  gender: "男" | "女";
+  constructor(name: string, age: number, gender: "男" | "女" = "男") {
+    this.name = name;
+    this.age = age;
+    this.gender = gender;
+  }
+}
+const u = new User("abc", 19);
+

方法 2:属性默认值

typescript
class User {
+  name: string;
+  age: number;
+  gender: "男" | "女" = "男";
+  constructor(name: string, age: number) {
+    this.name = name;
+    this.age = age;
+  }
+}
+const u = new User("abc", 1);
+u.gender = "女"; //可改
+
class User {
+  name: string;
+  age: number;
+  gender: "男" | "女" = "男";
+  constructor(name: string, age: number) {
+    this.name = name;
+    this.age = age;
+  }
+}
+const u = new User("abc", 1);
+u.gender = "女"; //可改
+

属性可以修饰为可选的

js
pid: string;
+// ...
+pid: string | undefined;
+
pid: string;
+// ...
+pid: string | undefined;
+

属性可以修饰为只读的

typescript
class User {
+  readonly id: number;
+  name: string;
+  age: number;
+  gender: "男" | "女" = "男";
+  pid?: string;
+  constructor(name: string, age: number) {
+    this.id = Math.random();
+    this.name = name;
+    this.age = age;
+  }
+}
+
class User {
+  readonly id: number;
+  name: string;
+  age: number;
+  gender: "男" | "女" = "男";
+  pid?: string;
+  constructor(name: string, age: number) {
+    this.id = Math.random();
+    this.name = name;
+    this.age = age;
+  }
+}
+

不希望外部使用

typescript
class User {
+  name: string;
+  age: number;
+  gender: "男" | "女" = "男";
+  publishNumber: number = 3; //每天一个可以发布多少文章
+  curNumber: number = 0; //当前可以发布的文章数
+  constructor(name: string, age: number) {
+    this.name = name;
+    this.age = age;
+  }
+}
+const u = new User("abc", 1);
+u.publishNumber; //不希望外部使用
+u.curNumber; //不希望外部使用
+// JS用Symble
+
class User {
+  name: string;
+  age: number;
+  gender: "男" | "女" = "男";
+  publishNumber: number = 3; //每天一个可以发布多少文章
+  curNumber: number = 0; //当前可以发布的文章数
+  constructor(name: string, age: number) {
+    this.name = name;
+    this.age = age;
+  }
+}
+const u = new User("abc", 1);
+u.publishNumber; //不希望外部使用
+u.curNumber; //不希望外部使用
+// JS用Symble
+

使用访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:见下文

属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写。 加上的是修饰符(public private ...)

访问器

作用:用于控制属性的读取和赋值

方式 1:类似 java es6 方式 2:类似 c#

typescript
class User {
+  constructor(public name: string, public _age: number) {}
+  set age(value: number) {
+    if (value < 0) {
+      this._age = 0;
+    } else if (value > 200) {
+      this._age = 200;
+    } else {
+      this._age = value;
+    }
+  }
+  get age() {
+    return Math.floor(this._age);
+  }
+}
+
class User {
+  constructor(public name: string, public _age: number) {}
+  set age(value: number) {
+    if (value < 0) {
+      this._age = 0;
+    } else if (value > 200) {
+      this._age = 200;
+    } else {
+      this._age = value;
+    }
+  }
+  get age() {
+    return Math.floor(this._age);
+  }
+}
+

泛型

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)

typescript
function take(arr: any[], n: number): any[] {
+  if (n > arr.length) {
+    return arr;
+  }
+  const newArr: any[] = [];
+  for (let i = 0; i < n; i++) {
+    newArr.push(arr[i]);
+  }
+  return newArr;
+}
+const newArr = take(["2", 3, 4, 5, 1], 3);
+console.log(newArr);
+// any应该相同
+
function take(arr: any[], n: number): any[] {
+  if (n > arr.length) {
+    return arr;
+  }
+  const newArr: any[] = [];
+  for (let i = 0; i < n; i++) {
+    newArr.push(arr[i]);
+  }
+  return newArr;
+}
+const newArr = take(["2", 3, 4, 5, 1], 3);
+console.log(newArr);
+// any应该相同
+

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS 会智能的根据传递的参数,推导出泛型的具体类型

如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值(不能完成推导,又没有传递就使用默认值)

在函数中使用泛型

在函数名之后写上<泛型名称>

typescript
function take<T>(arr: T[], n: number): T[] {
+  //T依附于函数
+  if (n > arr.length) {
+    return arr;
+  }
+  const newArr: T[] = [];
+  for (let i = 0; i < n; i++) {
+    newArr.push(arr[i]);
+  }
+  return newArr;
+}
+take<number>([1, 2], 2); //调用的时候才知道T的类型
+
function take<T>(arr: T[], n: number): T[] {
+  //T依附于函数
+  if (n > arr.length) {
+    return arr;
+  }
+  const newArr: T[] = [];
+  for (let i = 0; i < n; i++) {
+    newArr.push(arr[i]);
+  }
+  return newArr;
+}
+take<number>([1, 2], 2); //调用的时候才知道T的类型
+

如果不写泛型名称,会自动推导

如何在类型别名、接口、类中使用泛型

直接在名称后写上<泛型名称>

typescript
// 回调函数:判断数组中的某一项是否满足条件
+// type callback = (n: number, i: number) => boolean;
+type callback<T> = (n: T, i: number) => boolean;
+// 封装一个filter
+function filter<T>(arr: T[], callback: callback<T>): T[] {
+  const newArr: T[] = [];
+  arr.forEach((n, i) => {
+    if (callback(n, i)) {
+      newArr.push(n);
+    }
+  });
+  return newArr;
+}
+
+const arr = [3, 4, 5, 2, 1, 6];
+console.log(filter(arr, (n) => n % 2 !== 0));
+
+// 接口
+interface callback<T> {
+  (n: T, i: number): boolean;
+}
+
// 回调函数:判断数组中的某一项是否满足条件
+// type callback = (n: number, i: number) => boolean;
+type callback<T> = (n: T, i: number) => boolean;
+// 封装一个filter
+function filter<T>(arr: T[], callback: callback<T>): T[] {
+  const newArr: T[] = [];
+  arr.forEach((n, i) => {
+    if (callback(n, i)) {
+      newArr.push(n);
+    }
+  });
+  return newArr;
+}
+
+const arr = [3, 4, 5, 2, 1, 6];
+console.log(filter(arr, (n) => n % 2 !== 0));
+
+// 接口
+interface callback<T> {
+  (n: T, i: number): boolean;
+}
+

typescript
export class ArrayHelper<T> {
+  constructor(private arr: T[]) {}
+  take(n: number): T[] {
+    if (n > this.arr.length) {
+      return this.arr;
+    }
+    const newArr: T[] = [];
+    for (let i = 0; i < n; i++) {
+      newArr.push(this.arr[i]);
+    }
+    return newArr;
+  }
+  shuffle() {
+    for (let i = 0; i < this.arr.length; i++) {
+      const targetIndex = this.getRandom(0, this.arr.length);
+      const temp = this.arr[i];
+      this.arr[i] = this.arr[targetIndex];
+      this.arr[targetIndex] = temp;
+    }
+  }
+  private getRandom(min: number, max: number) {
+    const dec = max - min;
+    return Math.floor(Math.random() * dec + max);
+  }
+}
+const helper = new ArrayHelper([1, 2]); //自动类型推导
+
+helper.take(); //结果是number类型
+
export class ArrayHelper<T> {
+  constructor(private arr: T[]) {}
+  take(n: number): T[] {
+    if (n > this.arr.length) {
+      return this.arr;
+    }
+    const newArr: T[] = [];
+    for (let i = 0; i < n; i++) {
+      newArr.push(this.arr[i]);
+    }
+    return newArr;
+  }
+  shuffle() {
+    for (let i = 0; i < this.arr.length; i++) {
+      const targetIndex = this.getRandom(0, this.arr.length);
+      const temp = this.arr[i];
+      this.arr[i] = this.arr[targetIndex];
+      this.arr[targetIndex] = temp;
+    }
+  }
+  private getRandom(min: number, max: number) {
+    const dec = max - min;
+    return Math.floor(Math.random() * dec + max);
+  }
+}
+const helper = new ArrayHelper([1, 2]); //自动类型推导
+
+helper.take(); //结果是number类型
+

泛型约束

泛型约束,用于现实泛型的取值

typescript
interface hasNameProperty {
+  //nameToUpperCase函数只能传递满足此接口的类型
+  name: string;
+}
+/**
+ * 将某个对象name的属性的每个单词的首字母大写,并将该对象返回
+ */
+function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
+  obj.name = obj.name
+    .split(" ")
+    .map((s) => s[0].toUpperCase() + s.substr(1))
+    .join(" ");
+  return obj;
+}
+const o = {
+  name: "kevin",
+  age: 19,
+};
+const newO = nameToUpperCase(o);
+console.log(newO.name);
+
interface hasNameProperty {
+  //nameToUpperCase函数只能传递满足此接口的类型
+  name: string;
+}
+/**
+ * 将某个对象name的属性的每个单词的首字母大写,并将该对象返回
+ */
+function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
+  obj.name = obj.name
+    .split(" ")
+    .map((s) => s[0].toUpperCase() + s.substr(1))
+    .join(" ");
+  return obj;
+}
+const o = {
+  name: "kevin",
+  age: 19,
+};
+const newO = nameToUpperCase(o);
+console.log(newO.name);
+

多泛型

写函数或类,依赖多种类型

typescript
// 将两个数组混合[1,2] ['a','b'] === [1,'a',2,'b']
+function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
+  if (arr1.length !== arr2.length) {
+    throw new Error("长度不等");
+  }
+  let result: (T | K)[] = [];
+  for (let i = 0; i < arr2.length; i++) {
+    result.push(arr1[i]);
+    result.push(arr2[i]);
+  }
+  return result;
+}
+
+const result = mixinArray([1, 2, 3], ["a", "b", "c"]);
+result.forEach((it) => console.log(it));
+
// 将两个数组混合[1,2] ['a','b'] === [1,'a',2,'b']
+function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
+  if (arr1.length !== arr2.length) {
+    throw new Error("长度不等");
+  }
+  let result: (T | K)[] = [];
+  for (let i = 0; i < arr2.length; i++) {
+    result.push(arr1[i]);
+    result.push(arr2[i]);
+  }
+  return result;
+}
+
+const result = mixinArray([1, 2, 3], ["a", "b", "c"]);
+result.forEach((it) => console.log(it));
+

深入理解类和接口

面向对象概述

为什么要讲面向对象

  1. TS 为前端面向对象开发带来了契机

JS 语言没有类型检查,如果使用面向对象的方式开发,会产生大量的接口,而大量的接口会导致调用复杂度剧增,这种复杂度必须通过严格的类型检查来避免错误,尽管可以使用注释或文档或记忆力,但是它们没有强约束力。

TS 带来了完整的类型系统,因此开发复杂程序时,无论接口数量有多少,都可以获得完整的类型检查,并且这种检查是据有强约束力的。

  1. 面向对象中有许多非常成熟的模式,能处理复杂问题

在过去的很多年中,在大型应用或复杂领域,面向对象已经积累了非常多的经验。

基于 ts 的 nextjs 相当于前端的 java spring

什么是面向对象

面向对象:Oriented(基于) Object(事物),简称 OO。

是一种编程思想,它提出一切以类对切入点思考问题。

其他编程思想:面向过程(模块化)、函数式编程

学开发最重要最难的是什么?思维

面向过程:以功能流程为思考切入点,不太适合大型应用

函数式编程:以数学运算为思考切入点

面向对象:以划分类为思考切入点。类是最小的功能单元

类:可以产生对象的模板。

如何学习

  1. TS 中的 OOP (面向对象编程,Oriented Object Programing)
  2. 小游戏练习

理解 -> 想法 -> 实践 -> 理解 -> ....

类的继承

继承的作用

继承可以描述类与类之间的关系

坦克、玩家坦克、敌方坦克 玩家坦克是坦克,敌方坦克是坦克

如果 A 和 B 都是类,并且可以描述为 A 是 B,则 A 和 B 形成继承关系:

  • B 是父类,A 是子类
  • B 派生 A,A 继承自 B
  • B 是 A 的基类,A 是 B 的派生类

如果 A 继承自 B,则 A 中自动拥有 B 中的所有成员

成员的重写

重写(override):子类中覆盖父类的成员

子类成员不能改变父类成员的类型

无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配。

注意 this 关键字:在继承关系中,this 的指向是动态——调用方法时,根据具体的调用者确定 this 指向

super 关键字:在子类的方法中,可以使用super 关键字读取父类成员

类型匹配

鸭子辨型法 子类的对象,始终可以赋值给父类 let p:Tank = new EnemyTank()

面向对象中,这种现象,叫做里氏替换原则

如果需要判断一个数据的具体子类类型,可以使用 instanceof

protected 修饰符

readonly:只读修饰符

访问权限修饰符:private public protected

protected: 受保护的成员,只能在自身和子类中访问

单根性和传递性

单根性:每个类最多只能拥有一个父类

传递性:如果 A 是 B 的父类,并且 B 是 C 的父类,则,可以认为 A 也是 C 的父类

抽象类

为什么需要抽象类

有时,某个类只表示一个抽象概念,主要用于提取子类共有的成员,而不能直接创建它的对象。该类可以作为抽象类。

给类前面加上abstract,表示该类是一个抽象类,不可以创建一个抽象类的对象。

抽象成员

父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或实现是什么,因此,需要有一种强约束,让继承该类的子类,必须要实现该成员。

抽象类中,可以有抽象成员,这些抽象成员必须在子类中实现**。抽象成员必须出现在抽象类中。**

typescript
abstract class Chess {
+  x: number = 0;
+  y: number = 0;
+  abstract readonly name: string;
+}
+class Horse extends Chess {
+  // 方式1:快速修复
+  // readonly name:string = '马'
+  // 方式2
+  // readonly name: string
+  // constructor() {
+  //     super()
+  //     this.name = '跑'
+  // }
+  // 方式3:访问器
+  get name() {
+    return "兵";
+  } //没写set,本身就是只读的
+}
+
abstract class Chess {
+  x: number = 0;
+  y: number = 0;
+  abstract readonly name: string;
+}
+class Horse extends Chess {
+  // 方式1:快速修复
+  // readonly name:string = '马'
+  // 方式2
+  // readonly name: string
+  // constructor() {
+  //     super()
+  //     this.name = '跑'
+  // }
+  // 方式3:访问器
+  get name() {
+    return "兵";
+  } //没写set,本身就是只读的
+}
+
typescript
abstract class Chess {
+  x: number = 0;
+  y: number = 0;
+  abstract readonly name: string;
+  abstract move(targetX: number, targetY: number): boolean;
+}
+class Horse extends Chess {
+  move(targetX: number, targetY: number): boolean {
+    this.x = targetX;
+    this.y = targetY;
+    console.log("移动成功");
+    return true;
+  }
+  get name() {
+    return "兵";
+  }
+}
+
abstract class Chess {
+  x: number = 0;
+  y: number = 0;
+  abstract readonly name: string;
+  abstract move(targetX: number, targetY: number): boolean;
+}
+class Horse extends Chess {
+  move(targetX: number, targetY: number): boolean {
+    this.x = targetX;
+    this.y = targetY;
+    console.log("移动成功");
+    return true;
+  }
+  get name() {
+    return "兵";
+  }
+}
+

设计模式 - 模板模式

设计模式:面对一些常见的功能场景,有一些固定的、经过多年实践的成熟方法,这些方法称之为设计模式。

模板模式:有些方法,所有的子类实现的流程完全一致,只是流程中的某个步骤的具体实现不一致,可以将该方法提取到父类,在父类中完成整个流程的实现,遇到实现不一致的方法时,将该方法做成抽象方法。

typescript
abstract class Chess {
+  x: number = 0;
+  y: number = 0;
+
+  abstract readonly name: string;
+
+  move(targetX: number, targetY: number): boolean {
+    console.log("1. 边界判断");
+    console.log("2. 目标位置是否有己方棋子");
+    //3. 规则判断
+    if (this.rule(targetX, targetY)) {
+      this.x = targetX;
+      this.y = targetY;
+      console.log(`${this.name}移动成功`);
+      return true;
+    }
+    return false;
+  }
+
+  protected abstract rule(targetX: number, targetY: number): boolean;
+}
+
+class Horse extends Chess {
+  protected rule(targetX: number, targetY: number): boolean {
+    return true;
+  }
+
+  readonly name: string = "马";
+}
+
+class Pao extends Chess {
+  protected rule(targetX: number, targetY: number): boolean {
+    return false;
+  }
+
+  readonly name: string;
+
+  constructor() {
+    super();
+    this.name = "炮";
+  }
+}
+
+class Soldier extends Chess {
+  protected rule(targetX: number, targetY: number): boolean {
+    return true;
+  }
+
+  get name() {
+    return "兵";
+  }
+}
+class King extends Chess {
+  name: string = "将";
+
+  protected rule(targetX: number, targetY: number): boolean {
+    throw new Error("Method not implemented.");
+  }
+}
+
abstract class Chess {
+  x: number = 0;
+  y: number = 0;
+
+  abstract readonly name: string;
+
+  move(targetX: number, targetY: number): boolean {
+    console.log("1. 边界判断");
+    console.log("2. 目标位置是否有己方棋子");
+    //3. 规则判断
+    if (this.rule(targetX, targetY)) {
+      this.x = targetX;
+      this.y = targetY;
+      console.log(`${this.name}移动成功`);
+      return true;
+    }
+    return false;
+  }
+
+  protected abstract rule(targetX: number, targetY: number): boolean;
+}
+
+class Horse extends Chess {
+  protected rule(targetX: number, targetY: number): boolean {
+    return true;
+  }
+
+  readonly name: string = "马";
+}
+
+class Pao extends Chess {
+  protected rule(targetX: number, targetY: number): boolean {
+    return false;
+  }
+
+  readonly name: string;
+
+  constructor() {
+    super();
+    this.name = "炮";
+  }
+}
+
+class Soldier extends Chess {
+  protected rule(targetX: number, targetY: number): boolean {
+    return true;
+  }
+
+  get name() {
+    return "兵";
+  }
+}
+class King extends Chess {
+  name: string = "将";
+
+  protected rule(targetX: number, targetY: number): boolean {
+    throw new Error("Method not implemented.");
+  }
+}
+

静态成员

什么是静态成员

静态成员是指,附着在类上的成员(属于某个构造函数的成员)

使用 static 修饰的成员,是静态成员

实例成员:对象成员,属于某个类的对象

静态成员:非实例成员,属于某个类

typescript
class User {
+  constructor(
+    public loginId: string,
+    public loginPwd: string,
+    public name: string,
+    public age: number
+  ) {}
+  static login(loginId: string, loginPwd: string): User | undefined {
+    return undefined;
+  }
+}
+
class User {
+  constructor(
+    public loginId: string,
+    public loginPwd: string,
+    public name: string,
+    public age: number
+  ) {}
+  static login(loginId: string, loginPwd: string): User | undefined {
+    return undefined;
+  }
+}
+

静态方法中的 this

实例方法中的 this 指向的是当前对象

而静态方法中的 this 指向的是当前类

typescript
class User {
+  static users: User[] = [];
+
+  constructor(
+    public loginId: string,
+    public loginPwd: string,
+    public name: string,
+    public age: number
+  ) {
+    //需要将新建的用户加入到数组中
+    User.users.push(this);
+  }
+
+  sayHello() {
+    console.log(
+      `大家好,我叫${this.name},今年${this.age}岁了,我的账号是${this.loginId}`
+    );
+  }
+
+  static login(loginId: string, loginPwd: string): User | undefined {
+    return this.users.find(
+      (u) => u.loginId === loginId && u.loginPwd === loginPwd
+    );
+  }
+}
+
+new User("u1", "123", "王富贵", 11);
+new User("u2", "123", "坤坤", 18);
+new User("u3", "123", "旺财", 22);
+
+const result = User.login("u3", "123");
+if (result) {
+  result.sayHello();
+} else {
+  console.log("登录失败,账号或密码不正确");
+}
+
class User {
+  static users: User[] = [];
+
+  constructor(
+    public loginId: string,
+    public loginPwd: string,
+    public name: string,
+    public age: number
+  ) {
+    //需要将新建的用户加入到数组中
+    User.users.push(this);
+  }
+
+  sayHello() {
+    console.log(
+      `大家好,我叫${this.name},今年${this.age}岁了,我的账号是${this.loginId}`
+    );
+  }
+
+  static login(loginId: string, loginPwd: string): User | undefined {
+    return this.users.find(
+      (u) => u.loginId === loginId && u.loginPwd === loginPwd
+    );
+  }
+}
+
+new User("u1", "123", "王富贵", 11);
+new User("u2", "123", "坤坤", 18);
+new User("u3", "123", "旺财", 22);
+
+const result = User.login("u3", "123");
+if (result) {
+  result.sayHello();
+} else {
+  console.log("登录失败,账号或密码不正确");
+}
+

设计模式 - 单例模式

单例模式:某些类的对象,在系统中最多只能有一个,为了避免开发者造成随意创建多个类对象的错误,可以使用单例模式进行强约束。

typescript
class Board {
+  width: number = 500;
+  height: number = 700;
+
+  init() {
+    console.log("初始化棋盘");
+  }
+
+  private constructor() {} //构造函数私有,只能在内部创建对象
+
+  private static _board;
+
+  static createBoard(): Board {
+    if (this._board) {
+      return this._board;
+    }
+    this._board = new Board();
+    return this._board;
+  }
+}
+
+const b1 = Board.createBoard(); //静态方法创建
+const b2 = Board.createBoard();
+console.log(b1 === b2);
+
class Board {
+  width: number = 500;
+  height: number = 700;
+
+  init() {
+    console.log("初始化棋盘");
+  }
+
+  private constructor() {} //构造函数私有,只能在内部创建对象
+
+  private static _board;
+
+  static createBoard(): Board {
+    if (this._board) {
+      return this._board;
+    }
+    this._board = new Board();
+    return this._board;
+  }
+}
+
+const b1 = Board.createBoard(); //静态方法创建
+const b2 = Board.createBoard();
+console.log(b1 === b2);
+

再谈接口

接口用于约束类、对象、函数,是一个类型契约。 有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗,这些动物都具有共同的特征:名字、年龄、种类名称,还包含一个共同的方法:打招呼,它们各自有各自的技能,技能是可以通过训练改变的。狮子和老虎能进行火圈表演,猴子能进行平衡表演,狗能进行智慧表演 马戏团中有以下常见的技能:

  • 火圈表演:单火圈、双火圈
  • 平衡表演:独木桥、走钢丝
  • 智慧表演:算术题、跳舞

不适用接口实现时:

  • 对能力(成员函数)没有强约束力
  • 容易将类型和能力耦合在一起

系统中缺少对能力的定义 —— 接口 面向对象领域中的接口的语义:表达了某个类是否拥有某种能力 某个类具有某种能力,其实,就是实现了某种接口 类型保护函数:通过调用该函数,会触发 TS 的类型保护,该函数必须返回 boolean 接口和类型别名的最大区别:接口可以被类实现,而类型别名不可以

接口可以继承类,表示该类的所有成员都在接口中。

索引器

对象[值],使用成员表达式

在 TS 中,默认情况下,不对索引器(成员表达式)做严格的类型检查

使用配置noImplicitAny开启对隐式 any 的检查。

隐式 any:TS 根据实际情况推导出的 any 类型

typescript
class User {
+  [prop: string]: any; //属性名是字符串,类型是any就行
+  constructor(public name: string, public age: number) {}
+  sayHello() {}
+}
+const u = new User("aa", 22);
+u.pid = "samoia";
+
class User {
+  [prop: string]: any; //属性名是字符串,类型是any就行
+  constructor(public name: string, public age: number) {}
+  sayHello() {}
+}
+const u = new User("aa", 22);
+u.pid = "samoia";
+

在索引器中,键的类型可以是字符串,也可以是数字

typescript
class MyArray {
+  [index: number]: string;
+  0 = "asdas";
+  1 = "sdas";
+  2 = "asds";
+}
+const my = new MyArray();
+// my[0]
+// my[5] = 'a'
+
class MyArray {
+  [index: number]: string;
+  0 = "asdas";
+  1 = "sdas";
+  2 = "asds";
+}
+const my = new MyArray();
+// my[0]
+// my[5] = 'a'
+

在类中,索引器书写的位置应该是所有成员之前

TS 中索引器的作用

  • 在严格的检查下,可以实现为类动态增加成员
  • 可以实现动态的操作类成员

在 JS 中,所有的成员名本质上,都是字符串,如果使用数字作为成员名,会自动转换为字符串。

在 TS 中,如果某个类中使用了两种类型的索引器,要求两种索引器的值类型必须匹配

typescript
class A {
+  [prop: number]: string;
+  [prop: string]: string;
+}
+const a = new A();
+a[0] = "as";
+a["scads"] = "asa";
+
class A {
+  [prop: number]: string;
+  [prop: string]: string;
+}
+const a = new A();
+a[0] = "as";
+a["scads"] = "asa";
+

this 指向约束

https://yehudakatz.com/2011/08/10/understanding-javascript-function-invocation-and-this/

在 JS 中 this 指向的几种情况

明确:大部分时候,this 的指向取决于函数的调用方式

  • 如果直接调用函数(全局调用),this 指向全局对象或 undefined (启用严格模式)
  • 如果使用对象.方法调用,this 指向对象本身
  • 如果是 dom 事件的处理函数,this 指向事件处理对象

特殊情况:

  • 箭头函数,this 在函数声明时确定指向,指向函数位置的 this
  • 使用 bind、apply、call 手动绑定 this 对象

TS 中的 this

配置noImplicitThis为 true,表示不允许 this隐式的指向 any

在 TS 中,允许在书写函数时,手动声明该函数中 this 的指向,将 this 作为函数的第一个参数,该参数只用于约束 this,并不是真正的参数,也不会出现在编译结果中。

typescript
// interface IUser {
+//     name: string,
+//     age: number,
+//     sayHello(this: IUser): void
+// }
+
+// const u: IUser = {
+//     name: "ssf",
+//     age: 33,
+//     sayHello() {
+//         console.log(this.name, this.age)
+//     }
+// }
+// const say = u.sayHello;
+
+class User {
+  constructor(public name: string, public age: number) {}
+
+  sayHello() {
+    // 类里面的this使用了严格模式,全局调用this指向undefined
+    // this指向用户对象本身
+    console.log(this, this.name, this.age);
+  }
+}
+
// interface IUser {
+//     name: string,
+//     age: number,
+//     sayHello(this: IUser): void
+// }
+
+// const u: IUser = {
+//     name: "ssf",
+//     age: 33,
+//     sayHello() {
+//         console.log(this.name, this.age)
+//     }
+// }
+// const say = u.sayHello;
+
+class User {
+  constructor(public name: string, public age: number) {}
+
+  sayHello() {
+    // 类里面的this使用了严格模式,全局调用this指向undefined
+    // this指向用户对象本身
+    console.log(this, this.name, this.age);
+  }
+}
+

装饰器

概述

面向对象的概念(java:注解,c#:特征),decorator angular 大量使用,react 中也会用到 目前 JS 支持装饰器,目前处于建议征集的第二阶段

解决的问题

装饰器,能够带来额外的信息量,可以达到分离关注点的目的。

  • 信息书写位置的问题
  • 重复代码的问题

上述两个问题产生的根源:某些信息,在定义时,能够附加的信息量有限。

装饰器的作用:为某些属性、类、参数、方法提供元数据信息(metadata)

元数据:描述数据的数据

装饰器的本质

在 JS 中,装饰器是一个函数。(装饰器是要参与运行的

装饰器可以修饰:

  • 成员(属性+方法)
  • 参数
typescript
class User {
+  // @require
+  // @range(3, 5)
+  // @description('账号')
+  loginId: string; //必须是3-5个字符
+  loginPwd: string; //必须是6-12位字符
+  age: number; //必须是数字0-100
+  gender: "男" | "女";
+}
+class Article {
+  title: string;
+}
+/**
+ * 统一的验证函数
+ * @param obj
+ */
+function validate(obj: object) {
+  for (const key in obj) {
+    const val = (obj as any)[key];
+    // 缺少该属性的验证规则
+  }
+}
+
class User {
+  // @require
+  // @range(3, 5)
+  // @description('账号')
+  loginId: string; //必须是3-5个字符
+  loginPwd: string; //必须是6-12位字符
+  age: number; //必须是数字0-100
+  gender: "男" | "女";
+}
+class Article {
+  title: string;
+}
+/**
+ * 统一的验证函数
+ * @param obj
+ */
+function validate(obj: object) {
+  for (const key in obj) {
+    const val = (obj as any)[key];
+    // 缺少该属性的验证规则
+  }
+}
+

类装饰器

类装饰器的本质是一个函数,该函数接收一个参数,表示类本身(构造函数本身)

使用装饰器**@得到一个函数**

在 TS 中,如何约束一个变量为类

  • Function
  • **new (参数)=>object**

在 TS 中要使用装饰器,需要开启experimentalDecorators

装饰器函数的运行时间:在类定义后直接运行

类装饰器可以具有的返回值

  • void:仅运行函数
  • 返回一个新的类:会将新的类替换掉装饰目标
typescript
function test(target: new () => object) {
+  return class B {
+    // 	不建议这样做
+  };
+}
+@test
+class A {}
+const a = new A();
+console.log(a); //B{}
+
function test(target: new () => object) {
+  return class B {
+    // 	不建议这样做
+  };
+}
+@test
+class A {}
+const a = new A();
+console.log(a); //B{}
+
typescript
function test(target: new () => object) {
+  return class B extends target {
+    // 不建议这样做
+  };
+}
+@test
+class A {
+  prop1: string; //失去了类型检查
+}
+const a = new A();
+console.log(a); //B{}
+
function test(target: new () => object) {
+  return class B extends target {
+    // 不建议这样做
+  };
+}
+@test
+class A {
+  prop1: string; //失去了类型检查
+}
+const a = new A();
+console.log(a); //B{}
+
typescript
function test(target: new (...args: any[]) => object) {}
+@test
+class A {
+  prop1: string;
+  constructor(public prop2: string, public prop3: string) {} //默认不能有参数,加上剩余参数能有参数了
+}
+
function test(target: new (...args: any[]) => object) {}
+@test
+class A {
+  prop1: string;
+  constructor(public prop2: string, public prop3: string) {} //默认不能有参数,加上剩余参数能有参数了
+}
+
typescript
function test(str: string) {
+  return function (target: new (...args: any[]) => object) {};
+}
+@test("这是一个类") //要满足返回一个函数
+class A {
+  prop1: string;
+}
+
function test(str: string) {
+  return function (target: new (...args: any[]) => object) {};
+}
+@test("这是一个类") //要满足返回一个函数
+class A {
+  prop1: string;
+}
+

多个装饰器的情况:会按照后加入先调用的顺序进行调用。

typescript
type constructor = new (...args: any[]) => object;
+function d1(target: constructor) {
+  console.log("d1");
+}
+function d2(target: constructor) {
+  console.log("d2");
+}
+@d1
+@d2
+//先输出d2 后 d1
+class A {
+  prop1: string;
+}
+
type constructor = new (...args: any[]) => object;
+function d1(target: constructor) {
+  console.log("d1");
+}
+function d2(target: constructor) {
+  console.log("d2");
+}
+@d1
+@d2
+//先输出d2 后 d1
+class A {
+  prop1: string;
+}
+

面试题

typescript
type constructor = new (...args: any[]) => object;
+function d1() {
+  console.log("d1");
+  return function (target: constructor) {
+    console.log("d1 decorator");
+  };
+}
+function d2() {
+  console.log("d2");
+  return function (target: constructor) {
+    console.log("d2 decorator");
+  };
+}
+@d1() //先运行d1 d2函数,得到的d1 d2装饰器,装饰器从下到上运行
+@d2()
+class A {
+  prop1: string;
+}
+
type constructor = new (...args: any[]) => object;
+function d1() {
+  console.log("d1");
+  return function (target: constructor) {
+    console.log("d1 decorator");
+  };
+}
+function d2() {
+  console.log("d2");
+  return function (target: constructor) {
+    console.log("d2 decorator");
+  };
+}
+@d1() //先运行d1 d2函数,得到的d1 d2装饰器,装饰器从下到上运行
+@d2()
+class A {
+  prop1: string;
+}
+

成员装饰器

  • 属性

属性装饰器也是一个函数,该函数需要两个参数:

  1. 如果是静态属性,则为类本身;如果是实例属性,则为类的原型
  2. 固定为一个字符串,表示属性名

实例属性

typescript
type constructor = new (...args: any[]) => object;
+function d(target: any, key: string) {
+  // console.log(target === A.prototype, key)
+  if (!target.__props) {
+    target.__props = [];
+  }
+  target.__props.push(key);
+}
+class A {
+  @d
+  prop1: string;
+  @d
+  prop2: string;
+}
+// console.log((A.prototype as any).__props)
+const a = new A();
+console.log((a as any).__props);
+
type constructor = new (...args: any[]) => object;
+function d(target: any, key: string) {
+  // console.log(target === A.prototype, key)
+  if (!target.__props) {
+    target.__props = [];
+  }
+  target.__props.push(key);
+}
+class A {
+  @d
+  prop1: string;
+  @d
+  prop2: string;
+}
+// console.log((A.prototype as any).__props)
+const a = new A();
+console.log((a as any).__props);
+

静态属性

typescript
function d(target: any, key: string) {
+  console.log(target, key);
+}
+class A {
+  @d
+  prop1: string;
+  @d
+  static prop2: string;
+}
+
function d(target: any, key: string) {
+  console.log(target, key);
+}
+class A {
+  @d
+  prop1: string;
+  @d
+  static prop2: string;
+}
+
  • 方法

方法装饰器也是一个函数,该函数需要三个参数:

  1. 如果是静态方法,则为类本身;如果是实例方法,则为类的原型;
  2. 固定为一个字符串,表示方法名
  3. 属性描述对象
typescript
function d() {
+  return function (target: any, key: string, descriptor: PropertyDescriptor) {
+    // console.log(target, key, descriptor)
+    descriptor.enumerable = true; //可以更改属性描述符
+  };
+}
+class A {
+  @d()
+  method1() {}
+}
+
function d() {
+  return function (target: any, key: string, descriptor: PropertyDescriptor) {
+    // console.log(target, key, descriptor)
+    descriptor.enumerable = true; //可以更改属性描述符
+  };
+}
+class A {
+  @d()
+  method1() {}
+}
+

可以有多个装饰器修饰

练习:类和属性的描述装饰器

reflect-metadata 库

该库的作用:保存元数据

typescript
import "reflect-metadata";
+@Reflect.metadata("a", "一个类")
+class A {
+  @Reflect.metadata("prop", "一个属性")
+  prop1: string;
+}
+const obj = new A();
+console.log(Reflect.getMetadata("a", A));
+console.log(Reflect.getMetadata("prop", obj, "prop1"));
+
import "reflect-metadata";
+@Reflect.metadata("a", "一个类")
+class A {
+  @Reflect.metadata("prop", "一个属性")
+  prop1: string;
+}
+const obj = new A();
+console.log(Reflect.getMetadata("a", A));
+console.log(Reflect.getMetadata("prop", obj, "prop1"));
+

class-validator 和 class-transformer 库

typescript
import "reflect-metadata";
+import {
+  IsNotEmpty,
+  validate,
+  MinLength,
+  MaxLength,
+  Min,
+  Max,
+} from "class-validator";
+
+class RegUser {
+  @IsNotEmpty({ message: "账号不可以为空" })
+  @MinLength(5, { message: "账号必须至少有5个字符" })
+  @MaxLength(12, { message: "账号最多12个字符" })
+  loginId: string;
+
+  loginPwd: string;
+
+  @Min(0, { message: "年龄的最小值是0" })
+  @Max(100, { message: "年龄的最大值是100" })
+  age: number;
+  gender: "男" | "女";
+}
+
+const post = new RegUser();
+post.loginId = "22";
+post.age = -1;
+
+validate(post).then((errors) => {
+  console.log(errors);
+});
+
import "reflect-metadata";
+import {
+  IsNotEmpty,
+  validate,
+  MinLength,
+  MaxLength,
+  Min,
+  Max,
+} from "class-validator";
+
+class RegUser {
+  @IsNotEmpty({ message: "账号不可以为空" })
+  @MinLength(5, { message: "账号必须至少有5个字符" })
+  @MaxLength(12, { message: "账号最多12个字符" })
+  loginId: string;
+
+  loginPwd: string;
+
+  @Min(0, { message: "年龄的最小值是0" })
+  @Max(100, { message: "年龄的最大值是100" })
+  age: number;
+  gender: "男" | "女";
+}
+
+const post = new RegUser();
+post.loginId = "22";
+post.age = -1;
+
+validate(post).then((errors) => {
+  console.log(errors);
+});
+
typescript
import "reflect-metadata";
+import { plainToClass, Type } from "class-transformer";
+import axios from "axios";
+
+class User {
+  id: number;
+  firstName: string;
+  lastName: string;
+
+  @Type(() => Number) //告诉他age是数字
+  age: number;
+
+  getName() {
+    return this.firstName + " " + this.lastName;
+  }
+
+  isAdult() {
+    return this.age > 36 && this.age < 60;
+  }
+}
+
+axios
+  .get("https://api.myjson.com/bins/1b59tw")
+  .then((resp) => resp.data)
+  .then((users) => {
+    const us = plainToClass(User, users);
+    // console.log(us.getName(), us.isAdult())
+    for (const u of us) {
+      console.log(typeof u.age, u.age);
+    }
+  });
+
import "reflect-metadata";
+import { plainToClass, Type } from "class-transformer";
+import axios from "axios";
+
+class User {
+  id: number;
+  firstName: string;
+  lastName: string;
+
+  @Type(() => Number) //告诉他age是数字
+  age: number;
+
+  getName() {
+    return this.firstName + " " + this.lastName;
+  }
+
+  isAdult() {
+    return this.age > 36 && this.age < 60;
+  }
+}
+
+axios
+  .get("https://api.myjson.com/bins/1b59tw")
+  .then((resp) => resp.data)
+  .then((users) => {
+    const us = plainToClass(User, users);
+    // console.log(us.getName(), us.isAdult())
+    for (const u of us) {
+      console.log(typeof u.age, u.age);
+    }
+  });
+

补充

  • 参数装饰器

依赖注入、依赖倒置

要求函数有三个参数:

  1. 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
  2. 方法名称
  3. 在参数列表中的索引
  • 关于 TS 自动注入的元数据

如果安装了reflect-metadata,并且导入了该库,并且在某个成员上添加了元数据,并且启用了emitDecoratorMetadata

则 TS 在编译结果中,会将约束的类型,作为元数据加入到相应位置

这样一来,TS 的类型检查(约束)将有机会在运行时进行。

  • AOP(aspect oriented programming)

编程方式,属于面向对象开发。

将一些在业务中共同出现的功能块,横向切分,已达到分离关注点的目的。

typescript
class RegUser {
+  loginId: string;
+
+  loginPwd: string;
+  // @规则
+  age: number;
+  // @规则
+  pid: string;
+
+  email: string;
+
+  /**
+   * 将用户保存到数据库
+   */
+  save() {
+    // 验证抽离出去了
+    if (validate(this)) {
+      //通过后保存数据库
+    }
+  }
+}
+
class RegUser {
+  loginId: string;
+
+  loginPwd: string;
+  // @规则
+  age: number;
+  // @规则
+  pid: string;
+
+  email: string;
+
+  /**
+   * 将用户保存到数据库
+   */
+  save() {
+    // 验证抽离出去了
+    if (validate(this)) {
+      //通过后保存数据库
+    }
+  }
+}
+

类型演算

根据已知的信息,计算出新的类型

三个关键字

  • typeof

TS 中的 typeof,书写的位置在类型约束的位置上。

表示:获取某个数据的类型

typescript
const a: string = "asnd";
+let b: typeof a = "ad";
+console.log(typeof b);
+
const a: string = "asnd";
+let b: typeof a = "ad";
+console.log(typeof b);
+

当 typeof 作用于类的时候,得到的类型,是该类的构造函数

typescript
// 当想要约束参数为构造函数的时候:
+class User {
+  loginId: string;
+  loginPwd: string;
+}
+// 方法1
+// function createUser(cls: new () => User): User {
+//     return new cls()
+// }
+// 方法2
+function createUser(cls: typeof User): User {
+  return new cls();
+}
+const u = createUser(User);
+
// 当想要约束参数为构造函数的时候:
+class User {
+  loginId: string;
+  loginPwd: string;
+}
+// 方法1
+// function createUser(cls: new () => User): User {
+//     return new cls()
+// }
+// 方法2
+function createUser(cls: typeof User): User {
+  return new cls();
+}
+const u = createUser(User);
+
  • keyof

作用于类、接口、类型别名,用于获取其他类型中的所有成员名组成的联合类型

typescript
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+// function printUserProperty(obj: User, prop: string) {
+//     console.log(obj[prop])//报错,因为不确定prop是loginId,loginPwd,age之一
+// }
+function printUserProperty(obj: User, prop: "loginId" | "loginPwd" | "age") {
+  console.log(obj[prop]);
+}
+const u: User = {
+  loginId: "sada",
+  loginPwd: "sss",
+  age: 22,
+};
+printUserProperty(u, "age");
+
+// 场景:某个类型应该是某个类型所有字段中的一个
+
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+// function printUserProperty(obj: User, prop: string) {
+//     console.log(obj[prop])//报错,因为不确定prop是loginId,loginPwd,age之一
+// }
+function printUserProperty(obj: User, prop: "loginId" | "loginPwd" | "age") {
+  console.log(obj[prop]);
+}
+const u: User = {
+  loginId: "sada",
+  loginPwd: "sss",
+  age: 22,
+};
+printUserProperty(u, "age");
+
+// 场景:某个类型应该是某个类型所有字段中的一个
+

应用 keyof

typescript
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+function printUserProperty(obj: User, prop: keyof User) {
+  console.log(obj[prop]);
+}
+const u: User = {
+  loginId: "sada",
+  loginPwd: "sss",
+  age: 22,
+};
+printUserProperty(u, "age");
+
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+function printUserProperty(obj: User, prop: keyof User) {
+  console.log(obj[prop]);
+}
+const u: User = {
+  loginId: "sada",
+  loginPwd: "sss",
+  age: 22,
+};
+printUserProperty(u, "age");
+
  • in

该关键字往往和 keyof 联用,限制某个索引类型的取值范围。

typescript
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+type Obj = {
+  // [p:string]:string//太宽泛,什么属性都可以加
+
+  [p in "loginId" | "loginPwd" | "age"]: string; //  []叫索引器
+  /**
+   * 等价于
+   * loginId:string
+   * loginPwd:string
+   * age:string
+   */
+};
+const u: Obj = {
+  age: "1",
+  loginId: "a",
+  loginPwd: "aa",
+};
+// u.abc = '123'
+// 只能加"loginId" | "loginPwd" | "age"中的一个
+u.age = "1";
+
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+type Obj = {
+  // [p:string]:string//太宽泛,什么属性都可以加
+
+  [p in "loginId" | "loginPwd" | "age"]: string; //  []叫索引器
+  /**
+   * 等价于
+   * loginId:string
+   * loginPwd:string
+   * age:string
+   */
+};
+const u: Obj = {
+  age: "1",
+  loginId: "a",
+  loginPwd: "aa",
+};
+// u.abc = '123'
+// 只能加"loginId" | "loginPwd" | "age"中的一个
+u.age = "1";
+

用 keyof 简化

typescript
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+// 类型演算:将User所有类型变成字符串,得到一个新类型
+type Obj = {
+  [p in keyof User]: string;
+};
+const u: Obj = {
+  age: "1",
+  loginId: "a",
+  loginPwd: "aa",
+};
+u.age = "1";
+
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+// 类型演算:将User所有类型变成字符串,得到一个新类型
+type Obj = {
+  [p in keyof User]: string;
+};
+const u: Obj = {
+  age: "1",
+  loginId: "a",
+  loginPwd: "aa",
+};
+u.age = "1";
+
typescript
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+type Obj = {
+  [p in keyof User]: User[p]; //不改变User里面的的类型,直接取出来附上
+};
+const u: Obj = {
+  age: 1,
+  loginId: "a",
+  loginPwd: "aa",
+};
+u.age = 1;
+
interface User {
+  loginId: string;
+  loginPwd: string;
+  age: number;
+}
+type Obj = {
+  [p in keyof User]: User[p]; //不改变User里面的的类型,直接取出来附上
+};
+const u: Obj = {
+  age: 1,
+  loginId: "a",
+  loginPwd: "aa",
+};
+u.age = 1;
+
typescript
type Obj = {
+  readonly [p in keyof User]: User[p]; //类型不变,全部变成只读
+};
+
type Obj = {
+  readonly [p in keyof User]: User[p]; //类型不变,全部变成只读
+};
+

配合泛型:强大了

typescript
import { type } from "os";
+
+interface User {
+  loginId: string;
+  loginpwd: string;
+}
+
+interface Article {
+  title: string;
+  publishDate: Date;
+}
+//将User的所有属性值类型变成字符串,得到一个新类型
+type String<T> = {
+  [p in keyof T]: string;
+};
+
+type Readonly<T> = {
+  readonly [p in keyof T]: T[p];
+};
+
+type Partial<T> = {
+  [p in keyof T]?: T[p];
+};
+
+const u: String<Article> = {
+  title: "Sfsdf",
+  publishDate: "sdf",
+};
+
import { type } from "os";
+
+interface User {
+  loginId: string;
+  loginpwd: string;
+}
+
+interface Article {
+  title: string;
+  publishDate: Date;
+}
+//将User的所有属性值类型变成字符串,得到一个新类型
+type String<T> = {
+  [p in keyof T]: string;
+};
+
+type Readonly<T> = {
+  readonly [p in keyof T]: T[p];
+};
+
+type Partial<T> = {
+  [p in keyof T]?: T[p];
+};
+
+const u: String<Article> = {
+  title: "Sfsdf",
+  publishDate: "sdf",
+};
+

TS 中预设的类型演算

typescript
Partial<T>; // 将类型T中的成员变为可选
+
+Required<T>; // 将类型T中的成员变为必填
+
+Readonly<T>; // 将类型T中的成员变为只读
+
+Exclude<T, U>; // 从T中剔除可以赋值给U的类型。
+
+Extract<T, U>; // 提取T中可以赋值给U的类型。
+
+NonNullable<T>; // 从T中剔除null和undefined。
+
+ReturnType<T>; // 获取函数返回值类型。
+
+InstanceType<T>; // 获取构造函数类型的实例类型。
+
Partial<T>; // 将类型T中的成员变为可选
+
+Required<T>; // 将类型T中的成员变为必填
+
+Readonly<T>; // 将类型T中的成员变为只读
+
+Exclude<T, U>; // 从T中剔除可以赋值给U的类型。
+
+Extract<T, U>; // 提取T中可以赋值给U的类型。
+
+NonNullable<T>; // 从T中剔除null和undefined。
+
+ReturnType<T>; // 获取函数返回值类型。
+
+InstanceType<T>; // 获取构造函数类型的实例类型。
+

用法:

typescript
interface User {
+  age: number;
+  name: string;
+}
+let u: Partial<User>;
+u = {
+  age: 23,
+};
+
interface User {
+  age: number;
+  name: string;
+}
+let u: Partial<User>;
+u = {
+  age: 23,
+};
+
typescript
// let u: Exclude<"a" | "b" | "c" | "d", "b" | "c">
+
+type T = "男" | "女" | null | undefined;
+type NEWT = Exclude<T, null | undefined>;
+
+type func = () => number;
+type returnType = ReturnType<func>; //传入的是函数类型
+
+function sum(a: number, b: number) {
+  return a + b;
+}
+let a: ReturnType<typeof sum>; //传入的是函数类型
+
// let u: Exclude<"a" | "b" | "c" | "d", "b" | "c">
+
+type T = "男" | "女" | null | undefined;
+type NEWT = Exclude<T, null | undefined>;
+
+type func = () => number;
+type returnType = ReturnType<func>; //传入的是函数类型
+
+function sum(a: number, b: number) {
+  return a + b;
+}
+let a: ReturnType<typeof sum>; //传入的是函数类型
+

声明文件

概述、编写、发布

概述

  1. 什么是声明文件?

.d.ts结尾的文件

  1. 声明文件有什么作用?

为 JS 代码提供类型声明

  1. 声明文件的位置
  • 放置到 tsconfig.json 配置中包含的目录中
  • 放置到 node_modules/@types 文件夹中
  • 手动配置
  • 与 JS 代码所在目录相同,并且文件名也相同的文件。用 ts 代码书写的工程发布之后的格式。

编写声明文件

手动编写   自动生成

  • 自动生成

工程是使用 ts 开发的,发布(编译)之后,是 js 文件,发布的是 js 文件。

如果发布的文件,需要其他开发者使用,可以使用声明文件,来描述发布结果中的类型。

配置tsconfig.json中的declaration:true即可

  • 手动编写
  1. 对已有库,它是使用 js 书写而成,并且更改该库的代码为 ts 成本较高,可以手动编写声明文件
  2. 对一些第三方库,它们使用 js 书写而成,并且这些第三方库没有提供声明文件,可以手动编写声明文件。

全局声明

声明一些全局的对象、属性、变量

namespace: 表示命名空间,可以将其认为是一个对象,命名空间中的内容,必须通过命名空间.成员名访问

模块声明

三斜线指令

在一个声明文件中,包含另一个声明文件

发布

  1. 当前工程使用 ts 开发

编译完成后,将编译结果所在文件夹直接发布到 npm 上即可

  1. 为其他第三方库开发的声明文件

发布到@types/**中。

1) 进入 github 的开源项目:https://github.com/DefinitelyTyped/DefinitelyTyped

2) fork 到自己的开源库中

3) 从自己的开源库中克隆到本地

4) 本地新建分支(例如:mylodash4.3),在新分支中进行声明文件的开发

在types目录中新建文件夹,在新的文件夹中开发声明文件
+
在types目录中新建文件夹,在新的文件夹中开发声明文件
+

5) push 分支到你的开源库

6) 到官方的开源库中,提交 pull request

7) 等待官方管理员审核(1 天)

审核通过之后,会将你的分支代码合并到主分支,然后发布到 npm。

之后,就可以通过命令npm install @types/你发布的库名

+ + + + + \ No newline at end of file diff --git a/vue/2023-02-01-13-16-09.png b/vue/2023-02-01-13-16-09.png new file mode 100644 index 00000000..4e5aef02 Binary files /dev/null and b/vue/2023-02-01-13-16-09.png differ diff --git a/vue/2023-02-01-13-18-13.png b/vue/2023-02-01-13-18-13.png new file mode 100644 index 00000000..b025700e Binary files /dev/null and b/vue/2023-02-01-13-18-13.png differ diff --git a/vue/2023-02-01-13-21-47.png b/vue/2023-02-01-13-21-47.png new file mode 100644 index 00000000..b5071d9c Binary files /dev/null and b/vue/2023-02-01-13-21-47.png differ diff --git a/vue/2023-02-01-13-27-42.png b/vue/2023-02-01-13-27-42.png new file mode 100644 index 00000000..803c8bf8 Binary files /dev/null and b/vue/2023-02-01-13-27-42.png differ diff --git a/vue/2023-02-01-13-46-04.png b/vue/2023-02-01-13-46-04.png new file mode 100644 index 00000000..d31de935 Binary files /dev/null and b/vue/2023-02-01-13-46-04.png differ diff --git a/vue/2023-02-01-13-48-01.png b/vue/2023-02-01-13-48-01.png new file mode 100644 index 00000000..8137d513 Binary files /dev/null and b/vue/2023-02-01-13-48-01.png differ diff --git a/vue/2023-02-01-13-49-33.png b/vue/2023-02-01-13-49-33.png new file mode 100644 index 00000000..1873a55d Binary files /dev/null and b/vue/2023-02-01-13-49-33.png differ diff --git a/vue/2023-02-01-13-52-47.png b/vue/2023-02-01-13-52-47.png new file mode 100644 index 00000000..6603a700 Binary files /dev/null and b/vue/2023-02-01-13-52-47.png differ diff --git a/vue/2023-02-01-13-55-15.png b/vue/2023-02-01-13-55-15.png new file mode 100644 index 00000000..5fd77c39 Binary files /dev/null and b/vue/2023-02-01-13-55-15.png differ diff --git a/vue/2023-02-01-13-56-31.png b/vue/2023-02-01-13-56-31.png new file mode 100644 index 00000000..51e6805d Binary files /dev/null and b/vue/2023-02-01-13-56-31.png differ diff --git a/vue/2023-02-01-13-58-47.png b/vue/2023-02-01-13-58-47.png new file mode 100644 index 00000000..93af0274 Binary files /dev/null and b/vue/2023-02-01-13-58-47.png differ diff --git a/vue/2023-02-01-14-07-17.png b/vue/2023-02-01-14-07-17.png new file mode 100644 index 00000000..910b25d9 Binary files /dev/null and b/vue/2023-02-01-14-07-17.png differ diff --git a/vue/2023-02-01-14-11-21.png b/vue/2023-02-01-14-11-21.png new file mode 100644 index 00000000..86832efd Binary files /dev/null and b/vue/2023-02-01-14-11-21.png differ diff --git a/vue/2023-02-01-14-11-32.png b/vue/2023-02-01-14-11-32.png new file mode 100644 index 00000000..1e3706b9 Binary files /dev/null and b/vue/2023-02-01-14-11-32.png differ diff --git a/vue/2023-02-01-14-11-46.png b/vue/2023-02-01-14-11-46.png new file mode 100644 index 00000000..10336e41 Binary files /dev/null and b/vue/2023-02-01-14-11-46.png differ diff --git a/vue/2023-02-01-14-22-04.png b/vue/2023-02-01-14-22-04.png new file mode 100644 index 00000000..228de123 Binary files /dev/null and b/vue/2023-02-01-14-22-04.png differ diff --git a/vue/2023-02-01-14-26-02.png b/vue/2023-02-01-14-26-02.png new file mode 100644 index 00000000..779cec88 Binary files /dev/null and b/vue/2023-02-01-14-26-02.png differ diff --git a/vue/2023-02-01-14-30-58.png b/vue/2023-02-01-14-30-58.png new file mode 100644 index 00000000..4c42d40b Binary files /dev/null and b/vue/2023-02-01-14-30-58.png differ diff --git a/vue/2023-02-01-15-24-12.png b/vue/2023-02-01-15-24-12.png new file mode 100644 index 00000000..9a02308e Binary files /dev/null and b/vue/2023-02-01-15-24-12.png differ diff --git a/vue/2023-02-01-15-24-21.png b/vue/2023-02-01-15-24-21.png new file mode 100644 index 00000000..e206adaf Binary files /dev/null and b/vue/2023-02-01-15-24-21.png differ diff --git a/vue/2023-02-01-15-24-49.png b/vue/2023-02-01-15-24-49.png new file mode 100644 index 00000000..c38a046f Binary files /dev/null and b/vue/2023-02-01-15-24-49.png differ diff --git a/vue/2023-02-01-15-25-03.png b/vue/2023-02-01-15-25-03.png new file mode 100644 index 00000000..67b5965d Binary files /dev/null and b/vue/2023-02-01-15-25-03.png differ diff --git a/vue/2023-02-01-15-25-57.png b/vue/2023-02-01-15-25-57.png new file mode 100644 index 00000000..a42edb6c Binary files /dev/null and b/vue/2023-02-01-15-25-57.png differ diff --git a/vue/2023-02-01-15-26-20.png b/vue/2023-02-01-15-26-20.png new file mode 100644 index 00000000..1f66243b Binary files /dev/null and b/vue/2023-02-01-15-26-20.png differ diff --git a/vue/2023-02-01-15-26-32.png b/vue/2023-02-01-15-26-32.png new file mode 100644 index 00000000..be3b8c08 Binary files /dev/null and b/vue/2023-02-01-15-26-32.png differ diff --git a/vue/2023-02-01-15-26-38.png b/vue/2023-02-01-15-26-38.png new file mode 100644 index 00000000..c3471085 Binary files /dev/null and b/vue/2023-02-01-15-26-38.png differ diff --git a/vue/2023-02-01-15-26-45.png b/vue/2023-02-01-15-26-45.png new file mode 100644 index 00000000..4a61a21f Binary files /dev/null and b/vue/2023-02-01-15-26-45.png differ diff --git a/vue/2023-02-01-15-27-05.png b/vue/2023-02-01-15-27-05.png new file mode 100644 index 00000000..b71ec3ad Binary files /dev/null and b/vue/2023-02-01-15-27-05.png differ diff --git a/vue/SSR.html b/vue/SSR.html new file mode 100644 index 00000000..d7b11cf1 --- /dev/null +++ b/vue/SSR.html @@ -0,0 +1,20 @@ + + + + + + SSR | Sunny's blog + + + + + + + + +
Skip to content
On this page

SSR

SPA

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

Vue SSR 的实现原理

  • app.js 作为客户端与服务端的公用入口,导出 Vue 根实例,供客户端 entry 与服务端 entry 使用。客户端 entry 主要作用挂载到 DOM 上,服务端 entry 除了创建和返回实例,还需要进行路由匹配与数据预获取。
  • webpack 为客服端打包一个 ClientBundle,为服务端打包一个 ServerBundle
  • 服务器接收请求时,会根据 url,加载相应组件,获取和解析异步数据,创建一个读取 Server BundleBundleRenderer,然后生成 html 发送给客户端。
  • 客户端混合,客户端收到从服务端传来的 DOM 与自己的生成的 DOM 进行对比,把不相同的 DOM 激活,使其可以能够响应后续变化,这个过程称为客户端激活(也就是转换为单页应用)。为确保混合成功,客户 端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到 store 里,这样,在客户端挂载到 DOM 之前,可以直接从 store 里取数据。首屏的动态数据通过 window.INITIAL_STATE 发送到客户端
  • VueSSR 的原理,主要就是通过 vue-server-rendererVue 的组件输出成一个完整 HTML,输出到客户端,到达客户端后重新展开为一个单页应用。
+ + + + + \ No newline at end of file diff --git a/vue/challages.html b/vue/challages.html new file mode 100644 index 00000000..3dfb4fd4 --- /dev/null +++ b/vue/challages.html @@ -0,0 +1,502 @@ + + + + + + vue 手写篇 | Sunny's blog + + + + + + + + +
Skip to content
On this page

vue 手写篇

实现一个迷你版的reactivity

js
let activeEffect = undefined;
+const targetMap = new WeakMap();
+
+const effect = function (fn) {
+    activeEffect = fn;
+    fn();
+}
+
+const track = function (target, key) {
+    const dep = targetMap.get(target) || new Map();
+    const funcs = dep.get(key) || new Set();
+    funcs.add(activeEffect);
+    dep.set(key, funcs);
+    targetMap.set(target, dep);
+}
+
+const trigger = function (target, key) {
+    const dep = targetMap.get(target);
+    if (dep && dep.get(key)) {
+        dep.get(key).forEach(fn => fn());
+    }
+}
+
+const reactive = function (target) {
+    return new Proxy(target, {
+        get(target, key, reciever) {
+            track(target, key);
+            return Reflect.get(target, key, reciever);
+        },
+        set(target, key, value, reciever) {
+            Reflect.set(target, key, value, reciever);
+            trigger(target, key);
+        }
+    });
+}
+
+const person = {
+    firstName: 'Lu',
+    lastName: 'xun',
+    get name() {
+        return this.firstName + this.lastName;
+    }
+}
+
+const proxy = reactive(person);
+
+effect(() => {
+    console.log('effect1', proxy.name)
+});
+effect(() => {
+    console.log('effect2', proxy.name)
+});
+
+proxy.firstName = 'Zhang';
+
let activeEffect = undefined;
+const targetMap = new WeakMap();
+
+const effect = function (fn) {
+    activeEffect = fn;
+    fn();
+}
+
+const track = function (target, key) {
+    const dep = targetMap.get(target) || new Map();
+    const funcs = dep.get(key) || new Set();
+    funcs.add(activeEffect);
+    dep.set(key, funcs);
+    targetMap.set(target, dep);
+}
+
+const trigger = function (target, key) {
+    const dep = targetMap.get(target);
+    if (dep && dep.get(key)) {
+        dep.get(key).forEach(fn => fn());
+    }
+}
+
+const reactive = function (target) {
+    return new Proxy(target, {
+        get(target, key, reciever) {
+            track(target, key);
+            return Reflect.get(target, key, reciever);
+        },
+        set(target, key, value, reciever) {
+            Reflect.set(target, key, value, reciever);
+            trigger(target, key);
+        }
+    });
+}
+
+const person = {
+    firstName: 'Lu',
+    lastName: 'xun',
+    get name() {
+        return this.firstName + this.lastName;
+    }
+}
+
+const proxy = reactive(person);
+
+effect(() => {
+    console.log('effect1', proxy.name)
+});
+effect(() => {
+    console.log('effect2', proxy.name)
+});
+
+proxy.firstName = 'Zhang';
+

实现一个observer方法——要监听的对象属性可配置

js
// 实现一个observer方法——要监听的对象属性可配置
+// https://juejin.cn/post/7114588899661316126
+function observer(obj, path, cb) {
+    path.forEach((key) => {
+        let value = "";
+        if (key) {
+            const _key = key.split(".");
+            if (_key.length > 1) {
+                let tmp = obj;
+                // 循环,为了取到属性值,不断地赋值给 tmp
+                _key.forEach((k) => {
+                    // 递归,对每一个层级都进行劫持,要不然无法监听到深层级的属性值变化
+                    observer(tmp, [k], cb);
+                    tmp = tmp[k];
+                });
+                value = tmp;
+            } else {
+                value = obj[key];
+            }
+
+            Object.defineProperty(obj, key, {
+                get() {
+                    return value;
+                },
+                set(newV) {
+                    if (newV !== value) {
+                        cb && cb(newV, value);
+                        value = newV;
+                    }
+                },
+            });
+        }
+    });
+}
+
+var o = {
+    a: 1,
+    b: 2,
+    c: {
+        x: 1,
+        y: 2,
+    },
+};
+
+observer(o, ['a', "c.x"], (v, prev) => {
+    console.log("newV=", v, " oldV=", prev);
+});
+
+o.a = 2; // 2, 1
+o.b = 3; // 不打印
+o.c.x = 3; // 3, 1
+o.c.y = 3; // 没有没监听,不打印
+
// 实现一个observer方法——要监听的对象属性可配置
+// https://juejin.cn/post/7114588899661316126
+function observer(obj, path, cb) {
+    path.forEach((key) => {
+        let value = "";
+        if (key) {
+            const _key = key.split(".");
+            if (_key.length > 1) {
+                let tmp = obj;
+                // 循环,为了取到属性值,不断地赋值给 tmp
+                _key.forEach((k) => {
+                    // 递归,对每一个层级都进行劫持,要不然无法监听到深层级的属性值变化
+                    observer(tmp, [k], cb);
+                    tmp = tmp[k];
+                });
+                value = tmp;
+            } else {
+                value = obj[key];
+            }
+
+            Object.defineProperty(obj, key, {
+                get() {
+                    return value;
+                },
+                set(newV) {
+                    if (newV !== value) {
+                        cb && cb(newV, value);
+                        value = newV;
+                    }
+                },
+            });
+        }
+    });
+}
+
+var o = {
+    a: 1,
+    b: 2,
+    c: {
+        x: 1,
+        y: 2,
+    },
+};
+
+observer(o, ['a', "c.x"], (v, prev) => {
+    console.log("newV=", v, " oldV=", prev);
+});
+
+o.a = 2; // 2, 1
+o.b = 3; // 不打印
+o.c.x = 3; // 3, 1
+o.c.y = 3; // 没有没监听,不打印
+

手写vue的patch方法

js
//简单实现vue的更新方法,递归实现
+function update(obj, key, value) {
+    if (typeof obj !== 'object' || obj === null) {
+        return;
+    }
+    // 递归遍历对象属性
+    for (let prop in obj) {
+        if (obj.hasOwnProperty(prop)) {
+            if (typeof obj[prop] === 'object') {
+                update(obj[prop], key, value); // 递归调用更新方法
+            } else if (prop === key) {
+                obj[prop] = value; // 更新属性值
+            }
+        }
+    }
+}
+
+// 示例用法
+const data = {
+    name: 'John',
+    age: 30,
+    address: {
+        city: 'New York',
+        state: 'NY',
+        country: 'USA'
+    }
+};
+
+console.log('Before update:', data);
+update(data, 'city', 'San Francisco');
+console.log('After update:', data);
+
//简单实现vue的更新方法,递归实现
+function update(obj, key, value) {
+    if (typeof obj !== 'object' || obj === null) {
+        return;
+    }
+    // 递归遍历对象属性
+    for (let prop in obj) {
+        if (obj.hasOwnProperty(prop)) {
+            if (typeof obj[prop] === 'object') {
+                update(obj[prop], key, value); // 递归调用更新方法
+            } else if (prop === key) {
+                obj[prop] = value; // 更新属性值
+            }
+        }
+    }
+}
+
+// 示例用法
+const data = {
+    name: 'John',
+    age: 30,
+    address: {
+        city: 'New York',
+        state: 'NY',
+        country: 'USA'
+    }
+};
+
+console.log('Before update:', data);
+update(data, 'city', 'San Francisco');
+console.log('After update:', data);
+

模板字符串解析

js
let template = "我是{{name}},年龄{{age}},性别{{sex}},子姓名:{{nest.subName}}";
+let data = {
+    name: "姓名1",
+    age: 18,
+    nest: {
+        subName: '姓名1-1'
+    }
+};
+
+function render(template, data) {
+    return template.replace(/\{\{(.+?)\}\}/g, function (match, key) {
+        const keys = key.split('.'); // Split the key by dot to handle nested properties
+        let value = data;
+
+        // Traverse the nested properties
+        for (const k of keys) {
+            value = value[k];
+            if (value === undefined) {
+                break; // Stop if any intermediate property is undefined
+            }
+        }
+
+        return value !== undefined ? value : '';
+    });
+}
+
+console.log(render(template, data));
+// Output: 我是姓名1,年龄18,性别,子姓名:姓名1-1
+
+
let template = "我是{{name}},年龄{{age}},性别{{sex}},子姓名:{{nest.subName}}";
+let data = {
+    name: "姓名1",
+    age: 18,
+    nest: {
+        subName: '姓名1-1'
+    }
+};
+
+function render(template, data) {
+    return template.replace(/\{\{(.+?)\}\}/g, function (match, key) {
+        const keys = key.split('.'); // Split the key by dot to handle nested properties
+        let value = data;
+
+        // Traverse the nested properties
+        for (const k of keys) {
+            value = value[k];
+            if (value === undefined) {
+                break; // Stop if any intermediate property is undefined
+            }
+        }
+
+        return value !== undefined ? value : '';
+    });
+}
+
+console.log(render(template, data));
+// Output: 我是姓名1,年龄18,性别,子姓名:姓名1-1
+
+

vue2发布订阅响应式实现

js
function isObject(val) {
+  return val !== null && !Array.isArray(val) && typeof val === "object";
+}
+
+function observe(obj) {
+  if (!isObject(obj)) {
+    return;
+  }
+  Object.keys(obj).forEach((key) => {
+    var dep = new Dep();
+    // 重新定义属性
+    var internalValue = obj[key]; // 缓存该属性的值
+    observe(internalValue); // 递归监听该属性
+    Object.defineProperty(obj, key, {
+      get() {
+        dep.depend(); // 看一下,是哪个函数用到了我这个属性,将该函数记录下来
+        return internalValue;
+      },
+      set(val) {
+        observe(val);
+        internalValue = val;
+        dep.notify(); // 通知所有用到我这个属性的函数,全部重新运行
+      },
+    });
+  });
+}
+
+function Dep() {
+  this.subscribes = new Set(); // 用于记录依赖
+}
+
+Dep.prototype.depend = function () {
+  if (activeUpdate) {
+    this.subscribes.add(activeUpdate);
+  }
+};
+
+Dep.prototype.notify = function () {
+  this.subscribes.forEach((fn) => fn());
+};
+
+var activeUpdate = null; // 当前正在收集依赖的函数
+
+function autorun(fn) {
+  function updateWrapper() {// 防止后面的依赖收集不到,所以谈一层函数 
+    activeUpdate = updateWrapper;
+    fn(); // 该函数的运行期间,activeUpdate一定有值
+    activeUpdate = null;
+  }
+  updateWrapper();
+}
+
+var state = {
+  name: "monica",
+  age: 18,
+  addr: {
+    province: "黑龙江",
+    city: "哈尔滨",
+  },
+};
+
+observe(state);
+
+autorun(() => {
+  state.age;
+  state.name
+  console.log('autorun')
+});
+
+state.age = 19;
+state.name = "sunny-117";
+// state.addr.province = "四川";
+// state.addr.city = "成都";
+
+// 设置没有的属性,删除属性监控不到,所以要用$set,$delete
+
function isObject(val) {
+  return val !== null && !Array.isArray(val) && typeof val === "object";
+}
+
+function observe(obj) {
+  if (!isObject(obj)) {
+    return;
+  }
+  Object.keys(obj).forEach((key) => {
+    var dep = new Dep();
+    // 重新定义属性
+    var internalValue = obj[key]; // 缓存该属性的值
+    observe(internalValue); // 递归监听该属性
+    Object.defineProperty(obj, key, {
+      get() {
+        dep.depend(); // 看一下,是哪个函数用到了我这个属性,将该函数记录下来
+        return internalValue;
+      },
+      set(val) {
+        observe(val);
+        internalValue = val;
+        dep.notify(); // 通知所有用到我这个属性的函数,全部重新运行
+      },
+    });
+  });
+}
+
+function Dep() {
+  this.subscribes = new Set(); // 用于记录依赖
+}
+
+Dep.prototype.depend = function () {
+  if (activeUpdate) {
+    this.subscribes.add(activeUpdate);
+  }
+};
+
+Dep.prototype.notify = function () {
+  this.subscribes.forEach((fn) => fn());
+};
+
+var activeUpdate = null; // 当前正在收集依赖的函数
+
+function autorun(fn) {
+  function updateWrapper() {// 防止后面的依赖收集不到,所以谈一层函数 
+    activeUpdate = updateWrapper;
+    fn(); // 该函数的运行期间,activeUpdate一定有值
+    activeUpdate = null;
+  }
+  updateWrapper();
+}
+
+var state = {
+  name: "monica",
+  age: 18,
+  addr: {
+    province: "黑龙江",
+    city: "哈尔滨",
+  },
+};
+
+observe(state);
+
+autorun(() => {
+  state.age;
+  state.name
+  console.log('autorun')
+});
+
+state.age = 19;
+state.name = "sunny-117";
+// state.addr.province = "四川";
+// state.addr.city = "成都";
+
+// 设置没有的属性,删除属性监控不到,所以要用$set,$delete
+
+ + + + + \ No newline at end of file diff --git a/vue/component-communication.html b/vue/component-communication.html new file mode 100644 index 00000000..f55fd00f --- /dev/null +++ b/vue/component-communication.html @@ -0,0 +1,406 @@ + + + + + + Vuejs 组件通信概览 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vuejs 组件通信概览

父子组件通信

绝大部分vue本身提供的通信方式,都是父子组件通信

prop

最常见的组件通信方式之一,由父组件传递到子组件

event

最常见的组件通信方式之一,当子组件发生了某些事,可以通过event通知父组件

styleclass

父组件可以向子组件传递styleclass,它们会合并到子组件的根元素中

示例

父组件

vue
<template>
+  <div id="app">
+    <HelloWorld
+      style="color:red"
+      class="hello"
+      msg="Welcome to Your Vue.js App"
+    />
+  </div>
+</template>
+
+<script>
+import HelloWorld from "./components/HelloWorld.vue";
+
+export default {
+  components: {
+    HelloWorld,
+  },
+};
+</script>
+
<template>
+  <div id="app">
+    <HelloWorld
+      style="color:red"
+      class="hello"
+      msg="Welcome to Your Vue.js App"
+    />
+  </div>
+</template>
+
+<script>
+import HelloWorld from "./components/HelloWorld.vue";
+
+export default {
+  components: {
+    HelloWorld,
+  },
+};
+</script>
+

子组件

vue
<template>
+  <div class="world" style="text-align:center">
+    <h1>{{ msg }}</h1>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "HelloWorld",
+  props: {
+    msg: String,
+  },
+};
+</script>
+
<template>
+  <div class="world" style="text-align:center">
+    <h1>{{ msg }}</h1>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "HelloWorld",
+  props: {
+    msg: String,
+  },
+};
+</script>
+

渲染结果:

html
<div id="app">
+  <div class="hello world" style="color:red; text-aling:center">
+    <h1>Welcome to Your Vue.js App</h1>
+  </div>
+</div>
+
<div id="app">
+  <div class="hello world" style="color:red; text-aling:center">
+    <h1>Welcome to Your Vue.js App</h1>
+  </div>
+</div>
+

attribute

如果父组件传递了一些属性到子组件,但子组件并没有声明这些属性,则它们称之为attribute,这些属性会直接附着在子组件的根元素上

不包括styleclass,它们会被特殊处理

示例

父组件

vue
<template>
+  <div id="app">
+    <!-- 除 msg(声明过的) 外,其他均为 attribute -->
+    <HelloWorld data-a="1" data-b="2" msg="Welcome to Your Vue.js App" />
+  </div>
+</template>
+
+<script>
+import HelloWorld from "./components/HelloWorld.vue";
+
+export default {
+  components: {
+    HelloWorld,
+  },
+};
+</script>
+
<template>
+  <div id="app">
+    <!-- 除 msg(声明过的) 外,其他均为 attribute -->
+    <HelloWorld data-a="1" data-b="2" msg="Welcome to Your Vue.js App" />
+  </div>
+</template>
+
+<script>
+import HelloWorld from "./components/HelloWorld.vue";
+
+export default {
+  components: {
+    HelloWorld,
+  },
+};
+</script>
+

子组件

vue
<template>
+  <div>
+    <h1>{{ msg }}</h1>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "HelloWorld",
+  props: {
+    msg: String,
+  },
+  created() {
+    console.log(this.$attrs); // 得到: { "data-a": "1", "data-b": "2" }
+  },
+};
+</script>
+
<template>
+  <div>
+    <h1>{{ msg }}</h1>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "HelloWorld",
+  props: {
+    msg: String,
+  },
+  created() {
+    console.log(this.$attrs); // 得到: { "data-a": "1", "data-b": "2" }
+  },
+};
+</script>
+

渲染结果:

html
<div id="app">
+  <div data-a="1" data-b="2">
+    <h1>Welcome to Your Vue.js App</h1>
+  </div>
+</div>
+
<div id="app">
+  <div data-a="1" data-b="2">
+    <h1>Welcome to Your Vue.js App</h1>
+  </div>
+</div>
+

子组件可以通过inheritAttrs: false配置,禁止将attribute附着在子组件的根元素上,但不影响通过$attrs获取

natvie修饰符

在注册事件时,父组件可以使用native修饰符,将事件注册到子组件的根元素上

示例

父组件

vue
<template>
+  <div id="app">
+    <HelloWorld @click.native="handleClick" />
+  </div>
+</template>
+
+<script>
+import HelloWorld from "./components/HelloWorld.vue";
+
+export default {
+  components: {
+    HelloWorld,
+  },
+  methods: {
+    handleClick() {
+      console.log(1);
+    },
+  },
+};
+</script>
+
<template>
+  <div id="app">
+    <HelloWorld @click.native="handleClick" />
+  </div>
+</template>
+
+<script>
+import HelloWorld from "./components/HelloWorld.vue";
+
+export default {
+  components: {
+    HelloWorld,
+  },
+  methods: {
+    handleClick() {
+      console.log(1);
+    },
+  },
+};
+</script>
+

子组件

vue
<template>
+  <div>
+    <h1>Hello World</h1>
+  </div>
+</template>
+
<template>
+  <div>
+    <h1>Hello World</h1>
+  </div>
+</template>
+

渲染结果

html
<div id="app">
+  <!-- 点击该 div,会输出 1 -->
+  <div>
+    <h1>Hello World</h1>
+  </div>
+</div>
+
<div id="app">
+  <!-- 点击该 div,会输出 1 -->
+  <div>
+    <h1>Hello World</h1>
+  </div>
+</div>
+

$listeners

子组件可以通过$listeners获取父组件传递过来的所有事件处理函数

v-model

详见:探索 v-model 原理

sync修饰符

v-model的作用类似,用于双向绑定,不同点在于v-model只能针对一个数据进行双向绑定,而sync修饰符没有限制 实现原理

vue
<input v-model="loginId" />
+// 相当于
+<input :value="loginId" @input="loginId = $event.target.value" />
+
<input v-model="loginId" />
+// 相当于
+<input :value="loginId" @input="loginId = $event.target.value" />
+

示例

子组件

vue
<template>
+  <div>
+    <p>
+      <button @click="$emit(`update:num1`, num1 - 1)">-</button>
+      {{ num1 }}
+      <button @click="$emit(`update:num1`, num1 + 1)">+</button>
+    </p>
+    <p>
+      <button @click="$emit(`update:num2`, num2 - 1)">-</button>
+      {{ num2 }}
+      <button @click="$emit(`update:num2`, num2 + 1)">+</button>
+    </p>
+  </div>
+</template>
+
+<script>
+export default {
+  props: ["num1", "num2"], //父组件的东西,自己不能改
+};
+</script>
+
<template>
+  <div>
+    <p>
+      <button @click="$emit(`update:num1`, num1 - 1)">-</button>
+      {{ num1 }}
+      <button @click="$emit(`update:num1`, num1 + 1)">+</button>
+    </p>
+    <p>
+      <button @click="$emit(`update:num2`, num2 - 1)">-</button>
+      {{ num2 }}
+      <button @click="$emit(`update:num2`, num2 + 1)">+</button>
+    </p>
+  </div>
+</template>
+
+<script>
+export default {
+  props: ["num1", "num2"], //父组件的东西,自己不能改
+};
+</script>
+

sync并没有改变这种模式,只是语法糖

父组件

vue
<template>
+  <div id="app">
+    <Numbers :num1.sync="n1" :num2.sync="n2" />
+    <!-- 等同于 -->
+    <Numbers :num1="n1" @update:num1="n1 = $event"//将传过来的$event赋值给n1
+    :num2="n2" @update:num2="n2 = $event" />
+  </div>
+</template>
+<script>
+import Numbers from "./components/Numbers.vue";
+
+export default {
+  components: {
+    Numbers,
+  },
+  data() {
+    return {
+      n1: 0,
+      n2: 0,
+    };
+  },
+};
+</script>
+
<template>
+  <div id="app">
+    <Numbers :num1.sync="n1" :num2.sync="n2" />
+    <!-- 等同于 -->
+    <Numbers :num1="n1" @update:num1="n1 = $event"//将传过来的$event赋值给n1
+    :num2="n2" @update:num2="n2 = $event" />
+  </div>
+</template>
+<script>
+import Numbers from "./components/Numbers.vue";
+
+export default {
+  components: {
+    Numbers,
+  },
+  data() {
+    return {
+      n1: 0,
+      n2: 0,
+    };
+  },
+};
+</script>
+

$parent$children

在组件内部,可以通过$parent$children属性,分别得到当前组件的父组件和子组件实例

$slots$scopedSlots

详见 TODO

ref

父组件可以通过ref获取到子组件的实例

跨组件通信

Provide提供Inject获取

示例

javascript
// 父级组件提供 'foo'
+var Provider = {
+  provide: {
+    foo: "bar",
+  },
+  // ...
+};
+
+// 组件注入 'foo'
+var Child = {
+  inject: ["foo"], //我要拿到祖先组件提供的foo数据
+  created() {
+    console.log(this.foo); // => "bar"
+  },
+  // ...
+};
+
// 父级组件提供 'foo'
+var Provider = {
+  provide: {
+    foo: "bar",
+  },
+  // ...
+};
+
+// 组件注入 'foo'
+var Child = {
+  inject: ["foo"], //我要拿到祖先组件提供的foo数据
+  created() {
+    console.log(this.foo); // => "bar"
+  },
+  // ...
+};
+

router

如果一个组件改变了地址栏,所有监听地址栏的组件都会做出相应反应

最常见的场景就是通过点击router-link组件改变了地址,router-view组件就渲染其他内容

vuex

适用于大型项目的数据仓库

Pinia

您将喜欢使用的 Vue 存储库

store模式

适用于中小型项目的数据仓库

javascript
// store.js
+const store = {
+  loginUser: ...,
+  setting: ...
+}
+// AB共同使用store
+// compA
+const compA = {
+  data(){
+    return {
+      loginUser: store.loginUser
+    }
+  }
+}
+
+// compB
+const compB = {
+  data(){
+    return {
+      setting: store.setting,
+      loginUser: store.loginUser
+    }
+  }
+}
+
// store.js
+const store = {
+  loginUser: ...,
+  setting: ...
+}
+// AB共同使用store
+// compA
+const compA = {
+  data(){
+    return {
+      loginUser: store.loginUser
+    }
+  }
+}
+
+// compB
+const compB = {
+  data(){
+    return {
+      setting: store.setting,
+      loginUser: store.loginUser
+    }
+  }
+}
+

缺点:无法跟踪数据的变化,如果组件数变得复杂,任何组件都有权改动他,仓库数据出了问题,难以判断那个步骤出现问题。

eventbus

组件通知事件总线发生了某件事,事件总线通知其他监听该事件的所有组件运行某个函数

$emit$listeners通信的异同

相同点:均可实现子组件向父组件传递消息

差异点:

  • $emit更加符合单向数据流,子组件仅发出通知,由父组件监听做出改变;而$listeners则是在子组件中直接使用了父组件的方法。
  • 调试工具可以监听到子组件$emit的事件,但无法监听到$listeners中的方法调用。(想想为什么)
  • 由于$listeners中可以获得传递过来的方法,因此调用方法可以得到其返回值。但$emit仅仅是向父组件发出通知,无法知晓父组件处理的结果
+ + + + + \ No newline at end of file diff --git a/vue/computed.html b/vue/computed.html new file mode 100644 index 00000000..6a65dd70 --- /dev/null +++ b/vue/computed.html @@ -0,0 +1,20 @@ + + + + + + computed | Sunny's blog + + + + + + + + +
Skip to content
On this page

computed

Vue2 中 computed 源码解读

methods

vue 对 methods 的处理比较简单,只需要遍历 methods 配置中的每个属性,将其对应的函数使用 bind 绑定当前组件实例后复制其引用到组件实例中即可

computed

当组件实例触发生命周期函数beforeCreate后,它会做一系列事情,其中就包括对 computed 的处理

它会遍历 computed 配置中的所有属性,为每一个属性创建一个 Watcher 对象,并传入一个函数,该函数的本质其实就是 computed 配置中的 getter,这样一来,getter 运行过程中就会收集依赖

但是和渲染函数不同,为计算属性创建的 Watcher 不会立即执行,因为要考虑到该计算属性是否会被渲染函数使用,如果没有使用,就不会得到执行。因此,在创建 Watcher 的时候,它使用了 lazy 配置,lazy 配置可以让 Watcher 不会立即执行。

收到lazy的影响,Watcher 内部会保存两个关键属性来实现缓存,一个是value,一个是dirty

value属性用于保存 Watcher 运行的结果,受lazy的影响,该值在最开始是undefined

dirty属性用于指示当前的value是否已经过时了,即是否为脏值,受lazy的影响,该值在最开始是true

Watcher创建好后,vue 会使用代理模式,将计算属性挂载到组件实例中

当读取计算属性时,vue 检查其对应的 Watcher 是否是脏值,如果是,则运行函数,计算依赖,并得到对应的值,保存在 Watcher 的 value 中,然后设置 dirty 为 false,然后返回。

如果 dirty 为 false,则直接返回 watcher 的 value,即为缓存的原理 巧妙的是,在依赖收集时,被依赖的数据不仅会收集到计算属性的 Watcher,还会收集到组件的 Watcher

当计算属性的依赖变化时,会先触发计算属性的 Watcher执行,此时,它只需设置**dirty**为 true 即可,不做任何处理。

由于依赖同时会收集到组件的 Watcher,因此组件会重新渲染,而重新渲染时又读取到了计算属性,由于计算属性目前已为 dirty,因此会重新运行 getter 进行运算

而对于计算属性的 setter,则极其简单,当设置计算属性时,直接运行 setter 即可

Vue3 中 computed 源码解读

TODO

常见问题

面试官:computed 和 methods 有什么区别?

我:

  1. 在使用时,computed 当做属性使用,而 methods 则当做方法调用
  2. computed 可以具有 getter 和 setter,因此可以赋值,而 methods 不行
  3. computed 无法接收多个参数,而 methods 可以
  4. computed 具有缓存,而 methods 没有

面试官:回去等通知吧!

更深入的回答:↑

watch 与 computed 的区别是什么?

  1. 都是观察数据变化的(相同)
  2. 计算属性将会混入到 vue 的实例中,所以需要监听自定义变量;watch 监听 data 、props 里面数据的变化;
  3. computed 有缓存,它依赖的值变了才会重新计算,watch 没有;
  4. watch 支持异步,computed 不支持;
  5. watch 是一对多(监听某一个值变化,执行对应操作);computed 是多对一(监听属性依赖于其他属性)
  6. watch 监听函数接收两个参数,第一个是最新值,第二个是输入之前的值;
  7. computed 属性是函数时,都有 get 和 set 方法,默认走 get 方法,get 必须有返回值(return)

watch 的 参数:deep:深度监听;immediate :组件加载立即触发回调函数执行

对于 Computed:

  • 它支持缓存,只有依赖的数据发生了变化,才会重新计算
  • 不支持异步,当 Computed 中有异步操作时,无法监听数据的变化
  • computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 声明过,或者父组件传递过来的 props 中的数据进行计算的。
  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
  • 如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法。

对于 Watch:

  • 它不支持缓存,数据变化时,它就会触发相应的操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
  • 当一个属性发生变化时,就需要执行相应的操作
  • 监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
    • immediate:组件加载立即触发回调函数
    • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep 无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch。 总结:

  • computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
  • watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

运用场景:

  • 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
  • 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

Vue 中要获取当前时间你会放到 computed 还是 methods 里?

放在 computed 里面。因为 computed 只有在它的相关依赖发生改变时才会重新求值。相比而言,方法只要发生重新渲染,methods 调用总会执行所有函数。

+ + + + + \ No newline at end of file diff --git a/vue/diff.html b/vue/diff.html new file mode 100644 index 00000000..86860d36 --- /dev/null +++ b/vue/diff.html @@ -0,0 +1,212 @@ + + + + + + Vuejs diff 算法 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vuejs diff 算法

diff的时机

当组件创建时,以及依赖的属性或数据变化时,会运行一个函数,该函数会做两件事:

  • 运行_render生成一棵新的虚拟 dom 树(vnode tree)
  • 运行_update,传入虚拟 dom 树的根节点,对新旧两棵树进行对比,最终完成对真实 dom 的更新

核心代码如下:

javascript
// vue构造函数
+function Vue() {
+  // ... 其他代码
+  var updateComponent = () => {
+    this._update(this._render());
+  };
+  new Watcher(updateComponent);
+  // ... 其他代码
+}
+
// vue构造函数
+function Vue() {
+  // ... 其他代码
+  var updateComponent = () => {
+    this._update(this._render());
+  };
+  new Watcher(updateComponent);
+  // ... 其他代码
+}
+

diff就发生在_update函数的运行过程中

_update函数在干什么

_update函数接收到一个vnode参数,这就是生成的虚拟 dom 树 同时,_update函数通过当前组件的_vnode属性,拿到的虚拟 dom 树 _update函数首先会给组件的_vnode属性重新赋值,让它指向新树

javascript
function update(vnode) {
+  // vnode:新
+  // this._vnode:旧
+  var oldVnode = this._vnode;
+  this._vnode = vnode; //虚拟dom其实在这一步就已经更新了,所以对比的木得是更新真实DOM
+}
+
function update(vnode) {
+  // vnode:新
+  // this._vnode:旧
+  var oldVnode = this._vnode;
+  this._vnode = vnode; //虚拟dom其实在这一步就已经更新了,所以对比的木得是更新真实DOM
+}
+

然后会判断旧树是否存在:

不存在

说明这是第一次加载组件,于是通过内部的patch函数,直接遍历新树,为每个节点生成真实 DOM,挂载到每个节点的elm属性上

javascript
function update(vnode) {
+  // vnode: 新
+  // this._vnode: 旧
+  var oldVnode = this._vnode;
+  this._vnode = vnode;
+  // 对比的目的:更新真实dom
+  if (!oldVnode) {
+    this.__patch__(this.$el, vnode); //el:元素位置
+  }
+}
+
function update(vnode) {
+  // vnode: 新
+  // this._vnode: 旧
+  var oldVnode = this._vnode;
+  this._vnode = vnode;
+  // 对比的目的:更新真实dom
+  if (!oldVnode) {
+    this.__patch__(this.$el, vnode); //el:元素位置
+  }
+}
+

存在

说明之前已经渲染过该组件,于是通过内部的patch函数,对新旧两棵树进行对比,以达到下面两个目标:

  • 完成对所有真实 dom 的最小化处理
  • 让新树的节点对应合适的真实 dom

patch函数的对比流程

术语解释:

  1. 相同」:是指两个虚拟节点的标签类型、key值均相同,但input元素还要看type属性
javascript
/**
+ * 什么叫「相同」是指两个虚拟节点的标签类型、`key`值均相同,但`input`元素还要看`type`属性
+ *
+ * <h1>asdfdf</h1>        <h1>asdfasfdf</h1>    相同
+ *
+ * <h1 key="1">adsfasdf</h1>   <h1 key="2">fdgdf</h1> 不同
+ *
+ * <input type="text" />    <input type="radio" /> 不同
+ *
+ * abc        bcd  相同
+ *
+ * {
+ *  tag: undefined,
+ *  key: undefined,
+ *  text: "abc"
+ * }
+ *
+ * {
+ *  tag: undefined,
+ *  key: undefined,
+ *  text: "bcd"
+ * }
+ */
+
+// 这里的判断相同使用到的是sameVnode函数:源码
+function sameVnode(a, b) {
+  return (
+    a.key === b.key &&
+    ((a.tag === b.tag &&
+      a.isComment === b.isComment &&
+      isDef(a.data) === isDef(b.data) &&
+      sameInputType(a, b)) ||
+      (isTrue(a.isAsyncPlaceholder) &&
+        a.asyncFactory === b.asyncFactory &&
+        isUndef(b.asyncFactory.error)))
+  );
+}
+
/**
+ * 什么叫「相同」是指两个虚拟节点的标签类型、`key`值均相同,但`input`元素还要看`type`属性
+ *
+ * <h1>asdfdf</h1>        <h1>asdfasfdf</h1>    相同
+ *
+ * <h1 key="1">adsfasdf</h1>   <h1 key="2">fdgdf</h1> 不同
+ *
+ * <input type="text" />    <input type="radio" /> 不同
+ *
+ * abc        bcd  相同
+ *
+ * {
+ *  tag: undefined,
+ *  key: undefined,
+ *  text: "abc"
+ * }
+ *
+ * {
+ *  tag: undefined,
+ *  key: undefined,
+ *  text: "bcd"
+ * }
+ */
+
+// 这里的判断相同使用到的是sameVnode函数:源码
+function sameVnode(a, b) {
+  return (
+    a.key === b.key &&
+    ((a.tag === b.tag &&
+      a.isComment === b.isComment &&
+      isDef(a.data) === isDef(b.data) &&
+      sameInputType(a, b)) ||
+      (isTrue(a.isAsyncPlaceholder) &&
+        a.asyncFactory === b.asyncFactory &&
+        isUndef(b.asyncFactory.error)))
+  );
+}
+
  1. 新建元素」:是指根据一个虚拟节点提供的信息,创建一个真实 dom 元素,同时挂载到虚拟节点的elm属性上
  2. 销毁元素」:是指:vnode.elm.remove()
  3. 更新」:是指对两个虚拟节点进行对比更新,它仅发生在两个虚拟节点「相同」的情况下。具体过程稍后描述。
  4. 对比子节点」:是指对两个虚拟节点的子节点进行对比,具体过程稍后描述(深度优先)

详细流程:

根节点比较

patch函数首先对根节点进行比较

如果两个节点:

  • 「相同」,进入「更新」流程
    1. 将旧节点的真实 dom 赋值到新节点:newVnode.elm = oldVnode.elm
    2. 对比新节点和旧节点的属性,有变化的更新到真实 dom 中
    3. 当前两个节点处理完毕,开始「对比子节点」
  • 不「相同」 (直接看成旧树不存在
    1. 新节点递归「新建元素」
    2. 旧节点「销毁元素」

「对比子节点」

在「对比子节点」时,vue 一切的出发点,都是为了:

  • 尽量啥也别做
  • 不行的话,尽量仅改动元素属性
  • 还不行的话,尽量移动元素,而不是删除和创建元素
  • 还不行的话,删除和创建元素

流程

  • 对比旧树和新树的头指针,一样就进入更新流程(递归)新旧相连,对比有没有属性变化,对比子节点(递归)

  • 两个头指针往后移动,如果不一样,就比较尾指针,尾指针一样,递归

  • 两个尾指针往前移动,再次比较头指针,还是不一样,尾指针也不一样,就比较旧树的头和新树的尾,一样的话,连接,复用真实都 dom,更新属性,再把真实 dom 的位置移动到旧树的尾指针后

  • 新指针的尾指针往前移动,旧指针的头指针往后移动,头头不同,尾尾不同,两边的头尾不同,则以新树的头为基准,看一下在旧树里面存不存在,存在则复用,真实 dom 的位置调到前面。

  • 继续,头头不同,尾尾不同,头尾相同,同理交换,交换后把真实 dom 移动到头指针前面。

  • 继续,都不相同了,找 8 在旧树里面存不存在,不存在就新建。继续移动,头指针>尾指针,循环结束。

  • 销毁旧树剩下的对应的真实 dom

对开发的影响:

加 key

为什么要 key?如果不加,会把所有子元素直接改动,浪费效率;如果加上,变成了指针,dom 移动(没有改动真实 dom 内部),对真实 dom 几乎没有改动

html
<div id="app" style="width: 500px; margin: 0 auto; line-height: 3">
+  <div>
+    <a href="" @click.prevent="accoutLogin=true">账号登录</a>
+    <span>|</span>
+    <a href="" @click.prevent="accoutLogin=false">手机号登录</a>
+  </div>
+  <!-- 根据accoutLogin是否显示,如果没有key,默认key:undefined,新旧树的div没变,进入里面对比,里面也相同,会导致文本框里的内容不消失 -->
+  <div v-if="accoutLogin" key="1">
+    <label>账号</label>
+    <input type="text" />
+  </div>
+  <div v-else key="2">
+    <label>手机号</label>
+    <input type="text" />
+  </div>
+</div>
+
<div id="app" style="width: 500px; margin: 0 auto; line-height: 3">
+  <div>
+    <a href="" @click.prevent="accoutLogin=true">账号登录</a>
+    <span>|</span>
+    <a href="" @click.prevent="accoutLogin=false">手机号登录</a>
+  </div>
+  <!-- 根据accoutLogin是否显示,如果没有key,默认key:undefined,新旧树的div没变,进入里面对比,里面也相同,会导致文本框里的内容不消失 -->
+  <div v-if="accoutLogin" key="1">
+    <label>账号</label>
+    <input type="text" />
+  </div>
+  <div v-else key="2">
+    <label>手机号</label>
+    <input type="text" />
+  </div>
+</div>
+

为什么不用 index 作为 key

vue
<template>
+  <div id="app">
+    <ul>
+      <li v-for="item in list" :key="item.index">{{ item }}</li>
+    </ul>
+    <button @click="add">添加</button>
+  </div>
+</template>
+<script setup>
+import { ref } from "vue";
+// 我们发现添加操作导致的整个列表的重新渲染,按道理来说,Diff 算法会复用后面的三项,
+// 因为它们只是位置发生了变化,内容并没有改变。但是我们回过头来发现,
+// 我们在前面添加了一项,导致后面三项的 index 变化,从而导致 key 值发生变化。Diff 算法失效了
+const list = ref(["html", "css", "js"]);
+const add = () => {
+  list.value.unshift("阳阳羊");
+};
+</script>
+
<template>
+  <div id="app">
+    <ul>
+      <li v-for="item in list" :key="item.index">{{ item }}</li>
+    </ul>
+    <button @click="add">添加</button>
+  </div>
+</template>
+<script setup>
+import { ref } from "vue";
+// 我们发现添加操作导致的整个列表的重新渲染,按道理来说,Diff 算法会复用后面的三项,
+// 因为它们只是位置发生了变化,内容并没有改变。但是我们回过头来发现,
+// 我们在前面添加了一项,导致后面三项的 index 变化,从而导致 key 值发生变化。Diff 算法失效了
+const list = ref(["html", "css", "js"]);
+const add = () => {
+  list.value.unshift("阳阳羊");
+};
+</script>
+

解决:只要不是索引即可,比如,直接使用 item。这样,key 就是永远不变的,更新前后都是一样的,并且又由于节点的内容本来就没变,所以 Diff 算法完美生效,只需将新节点添加到真实 DOM 就行了。

总结

当组件创建和更新时,vue 均会执行内部的 update 函数,该函数使用 render 函数生成的虚拟 dom 树,将新旧两树进行对比,找到差异点,最终更新到真实 dom

对比差异的过程叫 diff,vue 在内部通过一个叫 patch 的函数完成该过程

在对比时,vue 采用深度优先、同层比较的方式进行比对。

在判断两个节点是否相同时,vue 是通过虚拟节点的 key 和 tag来进行判断的

具体来说

  • 首先对根节点进行对比,如果相同则将旧节点关联的真实 dom 的引用挂到新节点上,然后根据需要更新属性到真实 dom

  • 然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实 dom,同时挂到对应虚拟节点上,然后移除掉旧的 dom。

  • 在对比其子节点数组时,vue 对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实 dom,尽量少的销毁和创建真实 dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实 dom 到合适的位置。

这样一直递归的遍历下去,直到整棵树完成对比。

注意:

  • 在给 vue 中的元素设置 key 值时可以使用 Mathrandom 方法么?

random 是生成随机数,有一定概率多个 item 会生成相同的值,不能保证唯一。

如果是根据数据来生成 item,数据具有 id 属性,那么就可以使用 id 来作为 key

如果不是根据数据生成 item,那么最好的方式就是使用时间戳来作为 key。或者使用诸如 uuid 之类的库来生成唯一的 id

  • 为什么不建议用 index 作为 key?

使用 index 作为 key 和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列, 导致 Vue 会复用错误的旧子节点,做很多额外的工作。

对比 Vue3

简单来说,diff 算法有以下过程

  • 同级比较,再比较子节点
  • 先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)
  • 比较都有子节点的情况(核心 diff)
  • 递归比较子节点

正常 Diff 两个树的时间复杂度是 O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 VueDiff 进行了优化,从O(n^3) -> O(n),只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。

Vue2 的核心 Diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 ReactDiff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x 借鉴了 ivi 算法和 inferno 算法

在创建 VNode 时就确定其类型,以及在 mount/patch 的过程中采用位运算来判断一个 VNode 的类型,在这个基础之上再配合核心的 Diff 算法,使得性能上较 Vue2.x 有了提升。该算法中还运用了动态规划的思想求解最长递归子序列。

+ + + + + \ No newline at end of file diff --git a/vue/directive.html b/vue/directive.html new file mode 100644 index 00000000..2ec2d0e1 --- /dev/null +++ b/vue/directive.html @@ -0,0 +1,20 @@ + + + + + + Vue 指令篇 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vue 指令篇

描述下 Vue 自定义指令

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。 一般需要对 DOM 元素进行底层操作时使用,尽量只用来操作 DOM 展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定 v-model 的值也不会同步更新;如必须修改可以在自定义指令中使用 keydown 事件,在 vue 组件中使用 change 事件,回调中修改 vue 数据;

(1)自定义指令基本内容

  • 全局定义:Vue.directive("focus",{})

  • 局部定义:directives:{focus:

  • 钩子函数:指令定义对象提供钩子函数

    • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
    • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
    • ComponentUpdate:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
    • unbind:只调用一次,指令与元素解绑时调用。
  • 钩子函数参数

    • el:绑定元素
    • bing: 指令核心对象,描述指令全部信息属性
    • name
    • value
    • oldValue
    • expression
    • arg
    • modifers
    • vnode   虚拟节点
    • oldVnode:上一个虚拟节点(更新钩子函数中才有用)

(2)使用场景

  • 普通 DOM 元素进行底层操作的时候,可以使用自定义指令
  • 自定义指令是用来操作 DOM 的。尽管 Vue 推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM 操作,并且是可复用的。

(3)使用案例 初级应用:

  • 鼠标聚焦
  • 下拉菜单
  • 相对时间转换
  • 滚动动画

高级应用:

  • 自定义指令实现图片懒加载
  • 自定义指令集成第三方插件
+ + + + + \ No newline at end of file diff --git a/vue/interviewer.html b/vue/interviewer.html new file mode 100644 index 00000000..90c9acbe --- /dev/null +++ b/vue/interviewer.html @@ -0,0 +1,20 @@ + + + + + + 今天我是面试官 | Sunny's blog + + + + + + + + +
Skip to content
On this page

今天我是面试官

vue 中 key 的功能是什么?

  • 用于虚拟节点树之间,diff 时更好地比较 如果不设置 key,或者用 index 作为 key 会有什么问题?

为什么用 vuex,不用 event bus? 这两者有什么区别?

vue router 的实现原理

  • Location   href, path
  • 知道多页面应用中,如何区分前端路由和后端路由?
  • 路由哈希模式和 history 模式的区别 -
  • 把 nextTick 中的回调放到事件循环的微任务队列中。
  1. vue 发送请求在 created 生命周期钩子中,尽量避免 DOM 渲染挂载后再发送请求,更新数据,更改 DOM;
  2. vue router 组件懒加载;
  3. index.html 文件控制在 14kb 之内,这是基于 TCP 的慢开始规则,第一个响应包的大小就是 14kb,应该包含浏览器开始渲染页面所需的所有内容,或者至少包含页面模板(第一次渲染所需的 CSS 和 HTML)

以头条为例,采用组件化方式设计一个信息流?

  • 组件功能抽象能力
  • 组件设计思路
  • 功能划分:多种文章显示格式,刷新提示,评论/媒体/时间等信息显示,refresh/loadmore 功能,dislike 功能,动态广告
  • 卡片形式抽象:单图、多图、无图、视频、ugc、刷新条等

要求:有清晰思路,良好的信息流组件设计模式,可扩展性强并给出核心功能的实现方式

请简述什么是双向数据绑定和单向数据流,以及它们的区别

双向数据绑定

双向数据绑定意味着 UI 动态地绑定到模型数据,这样当 UI 改变时,模型数据就随之变化,反之亦然。

单向数据流

单向数据流意味着 model 是唯一来源。UI 触发消息的变化,将用户行为标记为 model。只有 model 具有访问更改应用程序状态的权限。其效果是数据总是朝一个方向流动,这使得理解起来更容易。

二者有什么优缺点

单向数据流是确定性的,数据流动方向可以跟踪,流动单一,追查问题的时候可以跟快捷。缺点就是写起来不太方便。要使 UI 发生变更就必须创建各种 action 来维护对应的 state 双向绑定,优点是使用方便,值和 UI 双绑定,但是由于各种数据相互依赖相互绑定,导致数据问题的源头难以被跟踪到,子组件修改父组件,兄弟组件互相修改有有违设计原则

可以介绍一下模板引擎的原理,比如实现类似 html 这种模板将其中变量替换为对应值的方式。

介绍下你所理解的 MVVM 框架,如 Angular、React、Vue 都解决了什么问题?

要求:能够从每个框架的生态系统,甚至结合之前的项目及不同的业务特点,给出框架的优劣

Vue 框架中组件消息通信方式

父子之间层级过多时,当父子组件之间层级不多的时候,父组件可以一层层的向子组件传递数据或者子组件一层层向父组件发送消息,代码上没有太难维护的地方。可是,一旦父子组件之间层级变多后,传递一个数据或者发送一个消息就变得麻烦。这块如果了解开源的 Element 组件库,就会知道其实现方式:构造一个函数自动向上/向下查询父亲节点,以[组件名, 消息名, 参数]三元组进行消息传递,降低长链传播成本;

具体实现参考:https://github.com/ElemeFE/element/blob/dev/src/mixins/emitter.js

如何理解虚拟 DOM?

对虚拟 dom 和 diff 算法中的一些细节理解

https://github.com/livoras/blog/issues/13

要求:写出 diff 算法的核心部分

+ + + + + \ No newline at end of file diff --git a/vue/keep-alive-LRU.html b/vue/keep-alive-LRU.html new file mode 100644 index 00000000..a3d5e021 --- /dev/null +++ b/vue/keep-alive-LRU.html @@ -0,0 +1,30 @@ + + + + + + keep-alive 与 LRU 算法 | Sunny's blog + + + + + + + + +
Skip to content
On this page

keep-alive 与 LRU 算法

keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态。

keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。

受 keep-alive 的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后

在具体的实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象

key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值

cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM

在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。

当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素。

javascript
// keep-alive 内部的声明周期函数
+created () {
+    this.cache = Object.create(null)
+    this.keys = []
+}
+
// keep-alive 内部的声明周期函数
+created () {
+    this.cache = Object.create(null)
+    this.keys = []
+}
+

keep-alive 中的生命周期哪些

keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。

当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。

LRU 缓存算法

TODO

+ + + + + \ No newline at end of file diff --git a/vue/lifecycle.html b/vue/lifecycle.html new file mode 100644 index 00000000..0c725e5f --- /dev/null +++ b/vue/lifecycle.html @@ -0,0 +1,72 @@ + + + + + + Vue 生命周期 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vue 生命周期

创建 vue 实例和创建组件的流程基本一致

  1. 首先做一些初始化的操作,主要是设置一些私有属性到实例中
  2. 运行生命周期钩子函数beforeCreate
  3. 进入注入流程:处理属性、computed、methods、data、provide、inject,最后使用代理模式将它们挂载到实例中

data 为例:

javascript
function Vue(options) {
+  var data = options.data();
+  observe(data); // 变成响应式数据
+  var methods = options.methods; //直接赋值
+  Object.defineProperty(this, "a", {
+    get() {
+      return data.a;
+    },
+    set(val) {
+      data.a = val;
+    },
+  });
+
+  Object.entries(methods).forEach(([methodName, fn]) => {
+    this[methodName] = fn.bind(this); //拿到每一个methods
+    // bind绑定,使得在vue里,this始终指向vue实例
+  });
+
+  var updateComponent = () => {
+    this._update(this._render());
+  };
+
+  new Watcher(updateComponent);
+}
+
+new Vue(vnode.componentOptions);
+
function Vue(options) {
+  var data = options.data();
+  observe(data); // 变成响应式数据
+  var methods = options.methods; //直接赋值
+  Object.defineProperty(this, "a", {
+    get() {
+      return data.a;
+    },
+    set(val) {
+      data.a = val;
+    },
+  });
+
+  Object.entries(methods).forEach(([methodName, fn]) => {
+    this[methodName] = fn.bind(this); //拿到每一个methods
+    // bind绑定,使得在vue里,this始终指向vue实例
+  });
+
+  var updateComponent = () => {
+    this._update(this._render());
+  };
+
+  new Watcher(updateComponent);
+}
+
+new Vue(vnode.componentOptions);
+
  1. 运行生命周期钩子函数created
  2. 渲染:生成render函数:如果有配置,直接使用配置的render,如果没有,使用运行时编译器,把模板编译为render
  3. 运行生命周期钩子函数beforeMount
  4. 创建一个Watcher,传入一个函数updateComponent,该函数会运行render,把得到的vnode再传入_update函数执行。 在执行render函数的过程中,会收集所有依赖,将来依赖变化时会重新运行updateComponent函数 在执行_update函数的过程中,触发patch函数,由于目前没有旧树,因此直接为当前的虚拟 dom 树的每一个普通节点生成 elm 属性,即真实 dom。 如果遇到创建一个组件的 vnode,则会进入组件实例化流程,该流程和创建 vue 实例流程基本相同,递归,最终会把创建好的组件实例挂载 vnode 的componentInstance属性中,以便复用。
  5. 运行生命周期钩子函数mounted

Vue 父子组件挂载顺序

重渲染?

  1. 数据变化后,所有依赖该数据的Watcher均会重新运行,这里仅考虑updateComponent函数对应的Watcher
  2. Watcher会被调度器放到nextTick中运行,也就是微队列中,这样是为了避免多个依赖的数据同时改变后被多次执行
  3. 运行生命周期钩子函数beforeUpdate
  4. updateComponent函数重新执行
  • 在执行render函数的过程中,会去掉之前的依赖,重新收集所有依赖,将来依赖变化时会重新运行updateComponent函数
  • 在执行_update函数的过程中,触发patch函数。
  • 新旧两棵树进行对比。
  • 普通html节点的对比会导致真实节点被创建、删除、移动、更新
  • 组件节点的对比会导致组件被创建、删除、移动、更新
  • 当新组件需要创建时,进入实例化流程
  • 当旧组件需要删除时,会调用旧组件的$destroy方法删除组件,该方法会先触发生命周期钩子函数beforeDestroy,然后递归调用子组件的$destroy方法,然后触发生命周期钩子函数- destroyed
  • 当组件属性更新时,相当于组件的updateComponent函数被重新触发执行,进入重渲染流程,和本节相同。
  1. 运行生命周期钩子函数updated

Vue 父子组件重新渲染顺序

总体流程

它可以总共分为 8 个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

  • beforeCreate:是 new Vue( ) 之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。

  • created:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 DOM 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 DOM

  • beforeMount:发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 DOM 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated

  • mounted:在挂载完成后发生,在当前阶段,真实的 DOM 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点,使用 $refs 属性对 DOM 进行操作。

注意:

  • created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。
  • mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作。
  • beforeUpdate:发生在更新之前,也就是响应式数据发生更新,虚拟 DOM 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

  • updated:发生在更新完成之后,当前阶段组件 DOM 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

  • beforeDestroy:发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

  • destroyed:发生在实例销毁之后,这个时候只剩下了 DOM 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

  • activated keep-alive 专属,组件被激活时调用

  • deactivated keep-alive 专属,组件被销毁时调用

常见问题

接口请求一般放在哪个生命周期中?

接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间
  • SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于代码的一致性
  • created 是在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。如果在 mounted 钩子函数中请求数据可能导致页面闪屏问题

Vue 子组件和父组件执行顺序

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed
+ + + + + \ No newline at end of file diff --git a/vue/nextTick.html b/vue/nextTick.html new file mode 100644 index 00000000..48e8bb4e --- /dev/null +++ b/vue/nextTick.html @@ -0,0 +1,22 @@ + + + + + + $nextTick 工作原理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

$nextTick 工作原理

DANGER

写作中

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

作用:vue 更新 DOM 是异步更新的,数据变化,DOM 的更新不会马上完成,nextTick 的回调是在下次 DOM 更新循环结束之后执行的延迟回调。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout 的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理

nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因 ∶

  • 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
  • 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要

Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作 DOM。有时候,可能遇到这样的情况,DOM1 的数据发生了变化,而 DOM2 需要从 DOM1 中获取数据,那这时就会发现 DOM2 的视图并没有更新,这时就需要用到了 nextTick 了。

由于 Vue 的 DOM 操作是异步的,所以,在上面的情况中,就要将 DOM2 获取数据的操作写在$nextTick 中。

vue
this.$nextTick(() => { // 获取数据的操作...})
+
this.$nextTick(() => { // 获取数据的操作...})
+

所以,在以下情况下,会用到 nextTick:

  • 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的 DOM 结构的时候,这个操作就需要方法在 nextTick()的回调函数中。
  • 在 vue 生命周期中,如果在 created()钩子进行 DOM 操作,也一定要放在 nextTick()的回调函数中。

因为在 created()钩子函数中,页面的 DOM 还未渲染,这时候也没办法操作 DOM,所以,此时如果想要操作 DOM,必须将操作的代码放在 nextTick()的回调函数中。 nextTick:可以做什么不可以做什么? nextTick:里面调用 update 会是什么情况? 如果我循环更新 dom 节点并且执行它,会有什么结果? 循环调用的话 nextTick:里面有容错机制吗?

实现原理:nextTick 主要使用了宏任务和微任务。根据执行环境分别尝试采用

  • Promise:可以将函数延迟到当前函数调用栈最末端
  • MutationObserver :是 H5 新加的一个功能,其功能是监听 DOM 节点的变动,在所有 DOM 变动完成后,执行回调函数
  • setImmediate:用于中断长时间运行的操作,并在浏览器完成其他操作(如事件和显示更新)后立即运行回调函数
  • 如果以上都不行则采用 setTimeout 把函数延迟到 DOM 更新之后再使用

原因是宏任务消耗大于微任务,优先使用微任务,最后使用消耗最大的宏任务。

参考资料

https://juejin.cn/post/6844903557372575752

+ + + + + \ No newline at end of file diff --git a/vue/reactive.html b/vue/reactive.html new file mode 100644 index 00000000..062dc9a0 --- /dev/null +++ b/vue/reactive.html @@ -0,0 +1,724 @@ + + + + + + Vuejs 数据响应原理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vuejs 数据响应原理

Vue2

https://cn.vuejs.org/v2/guide/reactivity.html

通过 Object.defineProperty 遍历对象的每一个属性,把数据变成 getter,setter。读取属性 getter, 更改属性 setter。形成了响应式数据。组件 render 函数会生成虚拟 DOM 树,影响到界面。怎么让响应式数据和虚拟 dom 连接起来呢?render 运行的时候用到了响应式数据,于是收集了依赖,数据 变化,会通知 watchwatch 会重新运行 render 函数

响应式数据的最终目标,是当对象本身或对象属性发生变化时,将会运行一些函数,最常见的就是 render 函数。 在具体实现上,vue2 用到了几个核心模块

  1. Observer
  2. Dep
  3. Watcher
  4. Scheduler

Observer

Observer 要实现的目标非常简单,就是把一个普通的对象转换为响应式的对象

为了实现这一点,Observer 把对象的每个属性通过 Object.defineProperty 转换为带有 getter 和 setter 的属性,这样一来,当访问或设置属性时,vue 就有机会做一些别的事情。

Observer 是 vue 内部的构造器,我们可以通过 Vue 提供的静态方法 Vue.observable( object )间接的使用该功能。

javascript
var obj = {
+  a: 1,
+  c: {
+    d: 3,
+  },
+  f: [
+    {
+      a: 1,
+      b: 2,
+    },
+    3,
+  ],
+};
+Vue.observable(obj); //递归遍历
+
var obj = {
+  a: 1,
+  c: {
+    d: 3,
+  },
+  f: [
+    {
+      a: 1,
+      b: 2,
+    },
+    3,
+  ],
+};
+Vue.observable(obj); //递归遍历
+

在组件生命周期中,数据响应式发生在 beforeCreate 之后,created 之前。

具体实现上,它会递归遍历对象的所有属性,以完成深度的属性转换。

由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此 vue 提供了$set$delete 两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。

对于数组,vue 会更改它的隐式原型,之所以这样做,是因为 vue 需要监听那些可能改变数组内容的方法

所以如果直接给数组的某一项(下标)直接赋值,监控不到

总之,Observer 的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被 vue 感知到。

Dep

这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠 Dep 来解决。 Dep 的含义是 Dependency,表示依赖的意思。 Vue 会为响应式对象中的每个属性、对象本身、数组本身创建一个 Dep 实例,

js
const obj = {
+  //dep
+  a: 1, //dep
+  n: [1, 2, 3], //dep
+  d: {
+    //dep
+    p: 1, //dep
+  },
+};
+
const obj = {
+  //dep
+  a: 1, //dep
+  n: [1, 2, 3], //dep
+  d: {
+    //dep
+    p: 1, //dep
+  },
+};
+

每个 Dep 实例都有能力做以下两件事:

  • 记录依赖:是谁在用我

  • 派发更新:我变了,我要通知那些用到我的人

当读取响应式对象的某个属性时,它会进行依赖收集:有人用到了我

当改变某个属性时,它会派发更新:那些用我的人听好了,我变了

举个例子:

Watcher

这里又出现一个问题,就是 Dep 如何知道是谁在用我?要解决这个问题,需要依靠另一个东西,就是 Watcher。

当某个函数执行的过程中,用到了响应式数据,响应式数据是无法知道是哪个函数在用自己的 vue 通过一种巧妙的办法来解决这个问题 我们不要直接执行函数,而是把函数交给一个叫做 watcher 的东西去执行,watcher 是一个对象,每个这样的函数执行时都应该创建一个 watcher,通过 watcher 去执行 watcher 会设置一个全局变量,让全局变量记录当前负责执行的 watcher 等于自己,然后再去执行函数,在函数的执行过程中,如果发生了依赖记录 dep.depend(),那么 Dep 就会把这个全局变量记录下来,表示有一个 watcher 用到了我这个属性

javascript
window.currentWatcher = this; //接下来执行我
+render(); // -->get(){dep.depend()}//通过全局变量来收集
+window.currentWatcher = null;
+
window.currentWatcher = this; //接下来执行我
+render(); // -->get(){dep.depend()}//通过全局变量来收集
+window.currentWatcher = null;
+

当 Dep 进行派发更新时,它会通知之前记录的所有 watcher:我变了

每一个 vue 组件实例,都至少对应一个 watcher,该 watcher 中记录了该组件的 render 函数。 watcher 首先会把 render 函数运行一次以收集依赖,于是那些在 render 中用到的响应式数据就会记录这个 watcher。数据变化时,dep 就会通知该 watcher,而 watcher 将重新运行 render 函数,从而让界面重新渲染同时重新记录当前的依赖。

Scheduler

现在还剩下最后一个问题,就是 Dep 通知 watcher 之后,如果 watcher 执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下

试想,如果一个交给 watcher 的函数,它里面用到了属性 a、b、c、d,那么 a、b、c、d 属性都会记录依赖,于是下面的代码将触发 4 次更新:

javascript
state.a = "new data";
+state.b = "new data";
+state.c = "new data";
+state.d = "new data";
+
state.a = "new data";
+state.b = "new data";
+state.c = "new data";
+state.d = "new data";
+

这样显然是不合适的,因此,watcher 收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给一个叫调度器的东西

调度器维护一个执行队列,该队列同一个 watcher 仅会存在一次,队列中的 watcher 不是立即执行,它会通过一个叫做 nextTick 的工具方法,把这些需要执行的 watcher 放入到事件循环的微队列中,nextTick 的具体做法是通过 Promise 完成的

nextTick 通过 this.$nextTick 暴露给开发者

也就是说,当响应式数据变化时,render 函数的执行是异步的,并且在微队列中

总体流程

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

  1. 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化

  2. compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

  3. Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

    ① 在自身实例化时往属性订阅器(dep)里面添加自己

    ② 自身必须有一个 update()方法

    ③ 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。

  4. MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

Vue3 数据响应原理

Vue 3.0 中采用了 Proxy,抛弃了 Object.defineProperty 方法。

究其原因,主要是以下几点:

  • Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
  • Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象。
  • Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
  • Proxy 有多达 13 种拦截方法
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化

注意:

  • Proxy 只会代理对象的第一层,那么 Vue3 又是怎样处理这个问题的呢?

判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。

  • 监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢?

我们可以判断 key 是否为当前被代理对象 target 自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行 trigger

@vue/reactivity api

获取响应式数据

API传入返回备注
reactiveplain-object对象代理深度代理对象中的所有成员
readonlyplain-object or proxy对象代理只能读取代理对象中的成员,不可修改
refany{ value: ... }对 value 的访问是响应式的
如果给 value 的值是一个对象,
则会通过reactive函数进行代理
如果已经是代理,则直接使用代理
computedfunction{ value: ... }当读取 value 值时,
根据情况决定是否要运行函数
javascript
// 想把{ a: 1, b: 2 }变成响应式
+import { reactive, readonly, ref, computed } from "vue";
+// 1. reactive
+const state = reactive({ a: 1, b: 2 });
+// window.state = state;
+// 2. readonly
+// 只读,不能set
+/*
+const imState = readonly({ a: 1, b: 2 });
+window.imState = imState;
+*/
+/*
+const imState = readonly(state);//代理套代理
+window.imState = imState;
+// imState -> state -> {a:3,b:2}
+
+// 3. ref
+const count = ref(0);//如果里面是对象,就会调用reactive;普通值就ref
+console.log(count);
+const count = ref(state);//已经是代理,就返回这个代理
+console.log(count.value===state)
+// 4. computed
+*/
+/* const sum = computed(() => {
+    console.log("computed");
+    return state.a + state.b;
+})
+console.log(sum.value);//当这句话运行的时候就会输出computed,但是只允许一次(有缓存)
+当依赖数据a,b变了,就重新运行
+ */
+
// 想把{ a: 1, b: 2 }变成响应式
+import { reactive, readonly, ref, computed } from "vue";
+// 1. reactive
+const state = reactive({ a: 1, b: 2 });
+// window.state = state;
+// 2. readonly
+// 只读,不能set
+/*
+const imState = readonly({ a: 1, b: 2 });
+window.imState = imState;
+*/
+/*
+const imState = readonly(state);//代理套代理
+window.imState = imState;
+// imState -> state -> {a:3,b:2}
+
+// 3. ref
+const count = ref(0);//如果里面是对象,就会调用reactive;普通值就ref
+console.log(count);
+const count = ref(state);//已经是代理,就返回这个代理
+console.log(count.value===state)
+// 4. computed
+*/
+/* const sum = computed(() => {
+    console.log("computed");
+    return state.a + state.b;
+})
+console.log(sum.value);//当这句话运行的时候就会输出computed,但是只允许一次(有缓存)
+当依赖数据a,b变了,就重新运行
+ */
+

应用:

  • 如果想要让一个对象变为响应式数据,可以使用reactiveref
  • 如果想要让一个对象的所有属性只读,使用readonly
  • 如果想要让一个非对象数据变为响应式数据,使用ref
  • 如果想要根据已知的响应式数据得到一个新的响应式数据,使用computed
  • 总结:在 vue3 中,两种数据响应式格式:ref object 和 proxy

笔试题 1:下面的代码输出结果是什么?

javascript
import { reactive, readonly, ref, computed } from "vue";
+
+const state = reactive({
+  firstName: "Xu Ming",
+  lastName: "Deng",
+});
+const fullName = computed(() => {
+  console.log("changed");
+  return `${state.lastName}, ${state.firstName}`;
+});
+console.log("state ready");
+console.log("fullname is", fullName.value);
+console.log("fullname is", fullName.value); //计算属性有缓存
+const imState = readonly(state);
+console.log(imState === state); //false
+
+const stateRef = ref(state);
+console.log(stateRef.value === state); //如果已经是代理,则直接使用代理
+
+state.firstName = "Cheng";
+state.lastName = "Ji"; //改了数据,计算属性还是不允许,得等到用到.value的时候才运行
+
+console.log(imState.firstName, imState.lastName);
+console.log("fullname is", fullName.value);
+console.log("fullname is", fullName.value);
+
+const imState2 = readonly(stateRef);
+console.log(imState2.value === stateRef.value); //代理有区别,一个可改一个不可改
+
import { reactive, readonly, ref, computed } from "vue";
+
+const state = reactive({
+  firstName: "Xu Ming",
+  lastName: "Deng",
+});
+const fullName = computed(() => {
+  console.log("changed");
+  return `${state.lastName}, ${state.firstName}`;
+});
+console.log("state ready");
+console.log("fullname is", fullName.value);
+console.log("fullname is", fullName.value); //计算属性有缓存
+const imState = readonly(state);
+console.log(imState === state); //false
+
+const stateRef = ref(state);
+console.log(stateRef.value === state); //如果已经是代理,则直接使用代理
+
+state.firstName = "Cheng";
+state.lastName = "Ji"; //改了数据,计算属性还是不允许,得等到用到.value的时候才运行
+
+console.log(imState.firstName, imState.lastName);
+console.log("fullname is", fullName.value);
+console.log("fullname is", fullName.value);
+
+const imState2 = readonly(stateRef);
+console.log(imState2.value === stateRef.value); //代理有区别,一个可改一个不可改
+

笔试题 2:按照下面的要求完成函数

javascript
function useUser() {
+  // 在这里补全函数
+  return {
+    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
+    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
+    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
+  };
+}
+
function useUser() {
+  // 在这里补全函数
+  return {
+    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
+    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
+    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
+  };
+}
+

答案

javascript
import { readonly, reactive } from "vue";
+
+function useUser() {
+  // 在这里补全函数
+  const userOrigin = reactive({}); //原始的可以改
+  const user = readonly(userOrigin); //只读了
+  const setUserName = (name) => {
+    //运用reactive巧妙的避开了只读不能改
+    userOrigin.name = name; //通过原始来改
+  };
+  const setUserAge = (age) => {
+    userOrigin.age = age;
+  };
+  return {
+    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
+    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
+    // 难点:只读了,咋改
+    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
+  };
+}
+
+const { user, setUserName, setUserAge } = useUser();
+
+console.log(user);
+setUserName("monica");
+setUserAge(18);
+console.log(user);
+
import { readonly, reactive } from "vue";
+
+function useUser() {
+  // 在这里补全函数
+  const userOrigin = reactive({}); //原始的可以改
+  const user = readonly(userOrigin); //只读了
+  const setUserName = (name) => {
+    //运用reactive巧妙的避开了只读不能改
+    userOrigin.name = name; //通过原始来改
+  };
+  const setUserAge = (age) => {
+    userOrigin.age = age;
+  };
+  return {
+    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
+    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
+    // 难点:只读了,咋改
+    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
+  };
+}
+
+const { user, setUserName, setUserAge } = useUser();
+
+console.log(user);
+setUserName("monica");
+setUserAge(18);
+console.log(user);
+

笔试题 3:响应式防抖

javascript
function useDebounce(obj, duration) {
+  // 在这里补全函数
+  return {
+    value, // 这里是一个只读对象,响应式数据,默认值为参数值
+    setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
+  };
+}
+
function useDebounce(obj, duration) {
+  // 在这里补全函数
+  return {
+    value, // 这里是一个只读对象,响应式数据,默认值为参数值
+    setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
+  };
+}
+

答案

javascript
import { reactive, readonly } from "vue";
+
+function useDebounce(obj, duration) {
+  // 在这里补全函数
+  const valueOrigin = reactive(obj);
+  const value = readonly(valueOrigin);
+  let timer = null;
+  const setValue = (newValue) => {
+    clearTimeout(timer);
+    timer = setTimeout(() => {
+      console.log("值改变了");
+      Object.entries(newValue).forEach(([k, v]) => {
+        valueOrigin[k] = v;
+      });
+    }, duration);
+  };
+  return {
+    value, // 这里是一个只读对象,响应式数据,默认值为参数值
+    setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
+  };
+}
+
+const { value, setValue } = useDebounce({ a: 1, b: 2 }, 5000);
+
+window.value = value;
+window.setValue = setValue;
+
import { reactive, readonly } from "vue";
+
+function useDebounce(obj, duration) {
+  // 在这里补全函数
+  const valueOrigin = reactive(obj);
+  const value = readonly(valueOrigin);
+  let timer = null;
+  const setValue = (newValue) => {
+    clearTimeout(timer);
+    timer = setTimeout(() => {
+      console.log("值改变了");
+      Object.entries(newValue).forEach(([k, v]) => {
+        valueOrigin[k] = v;
+      });
+    }, duration);
+  };
+  return {
+    value, // 这里是一个只读对象,响应式数据,默认值为参数值
+    setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
+  };
+}
+
+const { value, setValue } = useDebounce({ a: 1, b: 2 }, 5000);
+
+window.value = value;
+window.setValue = setValue;
+

监听数据变化

watchEffect

自动收集依赖,依赖改变时候自动收集

javascript
const stop = watchEffect(() => {
+  // 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
+});
+
+// 通过调用stop函数,会停止监听
+stop(); // 停止监听
+
const stop = watchEffect(() => {
+  // 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
+});
+
+// 通过调用stop函数,会停止监听
+stop(); // 停止监听
+
javascript
import { reactive, ref, watchEffect } from "vue";
+
+const state = reactive({ a: 1, b: 2 });
+const count = ref(0);
+
+watchEffect(() => {
+  console.log(state.a, count.value); //都是响应式的
+}); //watchEffect马上执行一次
+// state.b++;//不变,因为不依赖b,不运行get
+state.a++;
+state.a++;
+state.a++;
+state.a++;
+state.a++;
+count.value++;
+count.value++;
+count.value++;
+count.value++;
+// 异步的,会进入微队列,数据改变完成后才运行,所以只会运行一次 6 4
+
import { reactive, ref, watchEffect } from "vue";
+
+const state = reactive({ a: 1, b: 2 });
+const count = ref(0);
+
+watchEffect(() => {
+  console.log(state.a, count.value); //都是响应式的
+}); //watchEffect马上执行一次
+// state.b++;//不变,因为不依赖b,不运行get
+state.a++;
+state.a++;
+state.a++;
+state.a++;
+state.a++;
+count.value++;
+count.value++;
+count.value++;
+count.value++;
+// 异步的,会进入微队列,数据改变完成后才运行,所以只会运行一次 6 4
+

watch

javascript
import { reactive, ref, watch } from "vue";
+
+const state = reactive({ a: 1, b: 2 });
+const count = ref(0);
+
+// eg1
+// watch(state.a, (newValue, oldValue) => {//不会依赖,直接就把state.a读出来了
+//   console.log('new', newValue, 'old', oldValue)
+// })
+// eg2
+// watch(() => state.a, (newValue, oldValue) => {//函数是在watch里面调用,收集依赖
+//   console.log('new', newValue, 'old', oldValue)
+// })
+// eg3
+// watch([() => count.value], (newValue, oldValue) => {
+//   console.log('new', newValue, 'old', oldValue)
+// })
+// eg4
+// watch(count, (newValue, oldValue) => {//count可以直接这样写,因为count是对象。不能.value,如果.value就相当于给了0
+//   console.log('new', newValue, 'old', oldValue)
+// })
+watch([() => state.a, count], () => {
+  console.log("变化了");
+});
+
+count.value++;
+state.a++;
+
import { reactive, ref, watch } from "vue";
+
+const state = reactive({ a: 1, b: 2 });
+const count = ref(0);
+
+// eg1
+// watch(state.a, (newValue, oldValue) => {//不会依赖,直接就把state.a读出来了
+//   console.log('new', newValue, 'old', oldValue)
+// })
+// eg2
+// watch(() => state.a, (newValue, oldValue) => {//函数是在watch里面调用,收集依赖
+//   console.log('new', newValue, 'old', oldValue)
+// })
+// eg3
+// watch([() => count.value], (newValue, oldValue) => {
+//   console.log('new', newValue, 'old', oldValue)
+// })
+// eg4
+// watch(count, (newValue, oldValue) => {//count可以直接这样写,因为count是对象。不能.value,如果.value就相当于给了0
+//   console.log('new', newValue, 'old', oldValue)
+// })
+watch([() => state.a, count], () => {
+  console.log("变化了");
+});
+
+count.value++;
+state.a++;
+
javascript
// 等效于vue2的$watch
+// 不同于watchEffect  不会立即执行
+// 监听单个数据的变化
+const state = reactive({ count: 0 });
+watch(
+  () => state.count,
+  (newValue, oldValue) => {
+    // ...
+  },
+  options
+);
+
+const countRef = ref(0);
+watch(
+  countRef,
+  (newValue, oldValue) => {
+    // ...
+  },
+  options
+);
+
+// 监听多个数据的变化
+watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
+  // ...
+});
+
// 等效于vue2的$watch
+// 不同于watchEffect  不会立即执行
+// 监听单个数据的变化
+const state = reactive({ count: 0 });
+watch(
+  () => state.count,
+  (newValue, oldValue) => {
+    // ...
+  },
+  options
+);
+
+const countRef = ref(0);
+watch(
+  countRef,
+  (newValue, oldValue) => {
+    // ...
+  },
+  options
+);
+
+// 监听多个数据的变化
+watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
+  // ...
+});
+

注意:无论是watchEffect还是watch,当依赖项变化时,回调函数的运行都是异步的(微队列)

应用:除非遇到下面的场景,否则均建议选择watchEffect

  • 不希望回调函数一开始就执行
  • 数据改变时,需要参考旧值
  • 需要监控一些回调函数中不会用到的数据
javascript
import { reactive, ref, watch } from "vue";
+
+const state = reactive({ a: 1, b: 2 });
+const count = ref(0);
+
+watch([() => state.a, count], () => {
+  //第二个参数:数据变化的时候运行回调函数
+  console.log("变化了");
+}); //不同于watchEffect,一开始不允许,要加上一个配置:{immediate:true}才会直接运行
+count.value++;
+state.a++;
+
import { reactive, ref, watch } from "vue";
+
+const state = reactive({ a: 1, b: 2 });
+const count = ref(0);
+
+watch([() => state.a, count], () => {
+  //第二个参数:数据变化的时候运行回调函数
+  console.log("变化了");
+}); //不同于watchEffect,一开始不允许,要加上一个配置:{immediate:true}才会直接运行
+count.value++;
+state.a++;
+

笔试题: 下面的代码输出结果是什么?

javascript
import { reactive, watchEffect, watch } from "vue";
+const state = reactive({
+  count: 0,
+});
+watchEffect(() => {
+  //立即执行
+  console.log("watchEffect", state.count); //自动收集依赖
+});
+watch(
+  //不会立即执行
+  () => state.count, //手动告诉他收集的依赖是state.count
+  (count, oldCount) => {
+    console.log("watch", count, oldCount);
+  }
+);
+console.log("start");
+setTimeout(() => {
+  //宏队列
+  console.log("time out");
+  state.count++;
+  state.count++; //这次为准
+});
+state.count++;
+state.count++; //这两个++导致了watchEffect,watch检测到了变化,进入了微队列
+
+console.log("end");
+
import { reactive, watchEffect, watch } from "vue";
+const state = reactive({
+  count: 0,
+});
+watchEffect(() => {
+  //立即执行
+  console.log("watchEffect", state.count); //自动收集依赖
+});
+watch(
+  //不会立即执行
+  () => state.count, //手动告诉他收集的依赖是state.count
+  (count, oldCount) => {
+    console.log("watch", count, oldCount);
+  }
+);
+console.log("start");
+setTimeout(() => {
+  //宏队列
+  console.log("time out");
+  state.count++;
+  state.count++; //这次为准
+});
+state.count++;
+state.count++; //这两个++导致了watchEffect,watch检测到了变化,进入了微队列
+
+console.log("end");
+

判断

API含义
isProxy判断某个数据是否是由reactivereadonly
isReactive判断某个数据是否是通过reactive创建的.详细:https://v3.vuejs.org/api/basic-reactivity.html#isreactive
isReadonly判断某个数据是否是通过readonly创建的
isRef判断某个数据是否是一个ref对象

转换

unref 等同于:isRef(val) ? val.value : val 应用:

javascript
function useNewTodo(todos) {
+  todos = unref(todos);
+  // ...
+}
+
function useNewTodo(todos) {
+  todos = unref(todos);
+  // ...
+}
+

toRef 得到一个响应式对象某个属性的 ref 格式

javascript
const state = reactive({
+  foo: 1,
+  bar: 2,
+});
+
+const fooRef = toRef(state, "foo"); // fooRef: {value: ...}
+
+fooRef.value++;
+console.log(state.foo); // 2
+
+state.foo++;
+console.log(fooRef.value); // 3
+
const state = reactive({
+  foo: 1,
+  bar: 2,
+});
+
+const fooRef = toRef(state, "foo"); // fooRef: {value: ...}
+
+fooRef.value++;
+console.log(state.foo); // 2
+
+state.foo++;
+console.log(fooRef.value); // 3
+

toRefs 把一个响应式对象的所有属性转换为 ref 格式,然后包装到一个plain-object中返回

javascript
const state = reactive({
+  foo: 1,
+  bar: 2,
+});
+
+const stateAsRefs = toRefs(state);
+/*
+stateAsRefs: not a proxy
+{
+  foo: { value: ... },
+  bar: { value: ... }
+}
+*/
+
const state = reactive({
+  foo: 1,
+  bar: 2,
+});
+
+const stateAsRefs = toRefs(state);
+/*
+stateAsRefs: not a proxy
+{
+  foo: { value: ... },
+  bar: { value: ... }
+}
+*/
+

应用:

javascript
setup(){
+  const state1 = reactive({a:1, b:2});
+  const state2 = reactive({c:3, d:4});
+  return {
+    ...state1, // lost reactivity 展开剂运算符让他失去了响应式
+    ...state2 // lost reactivity
+  }
+}
+
+setup(){
+  const state1 = reactive({a:1, b:2});
+  const state2 = reactive({c:3, d:4});
+  return {
+    ...toRefs(state1), // reactivity
+    ...toRefs(state2) // reactivity
+  }
+}
+// composition function
+function usePos(){
+  const pos = reactive({x:0, y:0});
+  return pos;
+}
+
+setup(){
+  const {x, y} = usePos(); // lost reactivity
+  const {x, y} = toRefs(usePos()); // reactivity
+}
+
setup(){
+  const state1 = reactive({a:1, b:2});
+  const state2 = reactive({c:3, d:4});
+  return {
+    ...state1, // lost reactivity 展开剂运算符让他失去了响应式
+    ...state2 // lost reactivity
+  }
+}
+
+setup(){
+  const state1 = reactive({a:1, b:2});
+  const state2 = reactive({c:3, d:4});
+  return {
+    ...toRefs(state1), // reactivity
+    ...toRefs(state2) // reactivity
+  }
+}
+// composition function
+function usePos(){
+  const pos = reactive({x:0, y:0});
+  return pos;
+}
+
+setup(){
+  const {x, y} = usePos(); // lost reactivity
+  const {x, y} = toRefs(usePos()); // reactivity
+}
+

降低心智负担

所有的composition function均以ref的结果返回,以保证setup函数的返回结果中不包含reactivereadonly直接产生的数据

javascript
function usePos(){
+  const pos = reactive({ x:0, y:0 });
+  return toRefs(pos); //  {x: refObj, y: refObj}
+}
+function useBooks(){
+  const books = ref([]);
+  return {
+    books // books is refObj
+  }
+}
+function useLoginUser(){
+  const user = readonly({
+    isLogin: false,
+    loginId: null
+  });
+  return toRefs(user); // { isLogin: refObj, loginId: refObj }  all ref is readonly
+}
+
+setup(){
+  // 在setup函数中,尽量保证解构、展开出来的所有响应式数据均是ref
+  return {
+    ...usePos(),
+    ...useBooks(),
+    ...useLoginUser()
+  }
+}
+
function usePos(){
+  const pos = reactive({ x:0, y:0 });
+  return toRefs(pos); //  {x: refObj, y: refObj}
+}
+function useBooks(){
+  const books = ref([]);
+  return {
+    books // books is refObj
+  }
+}
+function useLoginUser(){
+  const user = readonly({
+    isLogin: false,
+    loginId: null
+  });
+  return toRefs(user); // { isLogin: refObj, loginId: refObj }  all ref is readonly
+}
+
+setup(){
+  // 在setup函数中,尽量保证解构、展开出来的所有响应式数据均是ref
+  return {
+    ...usePos(),
+    ...useBooks(),
+    ...useLoginUser()
+  }
+}
+
+ + + + + \ No newline at end of file diff --git a/vue/slot.html b/vue/slot.html new file mode 100644 index 00000000..75bad77f --- /dev/null +++ b/vue/slot.html @@ -0,0 +1,20 @@ + + + + + + 一篇精通 slot | Sunny's blog + + + + + + + + +
Skip to content
On this page

一篇精通 slot

DANGER

写作中

slot 是什么?有什么作用?

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。

  • 默认插槽:又名匿名查抄,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
  • 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。
  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。本质是子组件可以通过插槽的位置绑定一些数据,让父组件插槽位置可以用这个数据。

实现原理

当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在 vm.slot 中,默认插槽为 um. slot.default,具名插槽为 vm.slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用 slot 中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

+ + + + + \ No newline at end of file diff --git a/vue/v-model.html b/vue/v-model.html new file mode 100644 index 00000000..14184987 --- /dev/null +++ b/vue/v-model.html @@ -0,0 +1,48 @@ + + + + + + 探索 v-model 原理 | Sunny's blog + + + + + + + + +
Skip to content
On this page

探索 v-model 原理

v-model即可以作用于表单元素,又可作用于自定义组件,无论是哪一种情况,它都是一个语法糖,最终会生成一个属性和一个事件

当其作用于表单元素时vue根据作用的表单元素类型而生成合适的属性和事件。例如,作用于普通文本框的时候,它会生成value属性和input事件,而当其作用于单选框或多选框时,它会生成checked属性和change事件。

v-model也可作用于自定义组件,当其作用于自定义组件时,默认情况下,它会生成一个value属性和input事件。

html
<Comp v-model="data" />
+<!-- 等效于 -->
+<Comp :value="data" @input="data=$event" />
+
<Comp v-model="data" />
+<!-- 等效于 -->
+<Comp :value="data" @input="data=$event" />
+

开发者可以通过组件的model配置来改变生成的属性和事件

javascript
// Comp
+const Comp = {
+  model: {
+    prop: "number", // 默认为 value
+    event: "change", // 默认为 input
+  },
+  // ...
+};
+
// Comp
+const Comp = {
+  model: {
+    prop: "number", // 默认为 value
+    event: "change", // 默认为 input
+  },
+  // ...
+};
+
html
<Comp v-model="data" />
+<!-- 等效于 -->
+<Comp :number="data" @change="data=$event" />
+
<Comp v-model="data" />
+<!-- 等效于 -->
+<Comp :number="data" @change="data=$event" />
+

总结

首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者 Watcher 看是否需要更新。

因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅者 Watcher 之间进行统一管理的。

接着,我们还需要有一个指令解析器 Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

因此接下去我们执行以下 3 个步骤,实现数据的双向绑定:

  1. 实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

  2. 实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

  3. 实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

+ + + + + \ No newline at end of file diff --git a/vue/vdom.html b/vue/vdom.html new file mode 100644 index 00000000..6bbf1862 --- /dev/null +++ b/vue/vdom.html @@ -0,0 +1,74 @@ + + + + + + 虚拟 DOM | Sunny's blog + + + + + + + + +
Skip to content
On this page

虚拟 DOM

什么是虚拟 dom?

虚拟 dom 本质上就是一个普通的 JS 对象,用于描述视图的界面结构 在 vue 中,每个组件都有一个render函数,每个render函数都会返回一个虚拟 dom 树,这也就意味着每个组件都对应一棵虚拟 DOM 树

查看虚拟 DOM:

javascript
mounted() {
+  console.log(this._vnode);
+},
+
mounted() {
+  console.log(this._vnode);
+},
+

vdom 结构:

javascript
var vnode = {
+  tag: "h1",
+  children: [{ tag: undefined, text: "第一个vue应用:Hello World" }],
+};
+
var vnode = {
+  tag: "h1",
+  children: [{ tag: undefined, text: "第一个vue应用:Hello World" }],
+};
+

上面的对象描述了:有一个标签名为 h1 的节点,它有一个子节点,该子节点是一个文本,内容为第一个 vue 应用:Hello World

为什么需要虚拟 dom?

  • 解决框架自身的效率问题(权衡)
  • 抽象层次和表达力

vue中,渲染视图会调用render函数,这种渲染不仅发生在组件创建时,同时发生在视图依赖的数据更新时。如果在渲染时,直接使用真实DOM,由于真实DOM的创建、更新、插入等操作会带来大量的性能损耗,从而就会极大的降低渲染效率。 因此,vue在渲染时,使用虚拟 dom 来替代真实 dom,主要为解决渲染效率的问题。

对比 JS 对象和真实 DOM 对象

javascript
var times = 10000000;
+console.time(`js object`);
+for (var i = 0; i < times; i++) {
+  var obj = {};
+}
+console.timeEnd("js object");
+console.time(`dom object`);
+for (var i = 0; i < times; i++) {
+  var obj = document.createElement("div");
+}
+console.timeEnd("dom object");
+
var times = 10000000;
+console.time(`js object`);
+for (var i = 0; i < times; i++) {
+  var obj = {};
+}
+console.timeEnd("js object");
+console.time(`dom object`);
+for (var i = 0; i < times; i++) {
+  var obj = document.createElement("div");
+}
+console.timeEnd("dom object");
+

虚拟 dom 是如何转换为真实 dom 的?

在一个组件实例首次被渲染时,它先生成虚拟 dom 树,然后根据虚拟 dom 树创建真实 dom,并把真实 dom 挂载到页面中合适的位置,此时,每个虚拟 dom 便会对应一个真实的 dom。这时候虚拟 dom 多一个创建虚拟 dom 树的过程,所以效率比真实 dom 低。

如果一个组件受响应式数据变化的影响,需要重新渲染时,它仍然会重新调用 render 函数,创建出一个新的虚拟 dom 树,用新树和旧树对比,通过对比,vue 会找到最小更新量,然后更新必要的虚拟 dom 节点,最后,这些更新过的虚拟节点,会去修改它们对应的真实 dom

实际直接使用新树,抛弃旧树,只更新必要的真实 dom 这样一来,就保证了对真实 dom 达到最小的改动。

虚拟 DOM 树会最终生成为真实的 DOM 树,这个过程叫渲染 当数据变化后,将引发重新渲染,vue 会比较新旧两棵 vnode tree,找出差异,然后仅把差异部分应用到真实 dom tree 中。对比的是对象,效率很高

可见,在 vue 中,要得到最终的界面,必须要生成一个 vnode tree

渲染的本质: render 生成虚拟 DOM

模板和虚拟 dom 的关系

vue 框架中有一个compile模块,它主要负责将模板(实际上是字符串)转换为render函数,而render函数调用后将得到虚拟 dom。

编译的过程分两步:

a. 将模板字符串转换成为AST抽象语法树

b. 将AST转换为render函数

AST 是什么?

一句话概括:使用 js 树形结构描述原始代码

  • 如果使用传统的引入方式(script 的 src),则编译时间发生在组件第一次加载时,这称之为运行时编译。

  • 如果是在vue-cli的默认配置下,编译发生在打包时,这称之为模板预编译。(打包的时候编译完成)

  • 编译是一个极其耗费性能的操作,预编译可以有效的提高运行时的性能,而且,由于运行的时候已不需要编译,vue-cli在打包时会排除掉vue中的compile模块,以减少打包体积

javascript
//vue config.js
+module.export = {
+  runtimeCompiler: true, //打包的时候要不要包含运行时候编译,默认false,不建议使用true
+};
+
//vue config.js
+module.export = {
+  runtimeCompiler: true, //打包的时候要不要包含运行时候编译,默认false,不建议使用true
+};
+

模板的存在,仅仅是为了让开发人员更加方便的书写界面代码

vue 最终运行的时候,最终需要的是 render 函数,而不是模板,因此,模板中的各种语法,在虚拟 dom 中都是不存在的,它们都会变成虚拟 dom 的配置

总之:模板会编译成 render 方法,最终总是 render()

注意:虚拟节点树必须是单根的,所以模板必须单根(v2)

模版预编译

vue-cli进行打包时,会直接把组件中的模板转换为render函数,这叫做模板预编译 这样做的好处在于:

  1. 运行时就不再需要编译模板了,提高了运行效率
  2. 打包结果中不再需要 vue 的编译代码,减少了打包体积

易混淆:vue-cli 打包存在预编译,发现有模板,会覆盖 render;

在 vue 中,如果有模板和 render,则 render 优先

挂载

将生成的真实 DOM 树,放置到某个元素位置,称之为挂载 挂载的方式:

  1. 通过el:"css选择器"进行配置
  2. 通过vue实例.$mount("css选择器")进行配置

总结:完整流程

Vue template 到 render 的过程

vue 的模版编译过程主要如下:template -> ast -> render 函数 vue 在模版编译版本的码中会执行 compileToFunctions 将 template 转化为 render 函数:

javascript
// 将模板编译为render函数
+const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
+
// 将模板编译为render函数
+const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
+

CompileToFunctions 中的主要逻辑如下 ∶ (1)调用 parse 方法将 template 转化为 ast(抽象语法树)

javascript
constast = parse(template.trim(), options);
+
constast = parse(template.trim(), options);
+
  • parse 的目标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。
  • 解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造 AST 树的目的。

AST 元素节点总共三种类型:type 为 1 表示普通元素、2 为表达式、3 为纯文本 (2)对静态节点做优化

javascript
optimize(ast, options);
+
optimize(ast, options);
+

这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化

深度遍历 AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的 DOM 永远不会改变,这对运行时模板更新起到了极大的优化作用。

(3)生成代码

javascript
const code = generate(ast, options);
+
const code = generate(ast, options);
+

generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(render) 生成 render 函数。

+ + + + + \ No newline at end of file diff --git a/vue/vs.html b/vue/vs.html new file mode 100644 index 00000000..a78726a9 --- /dev/null +++ b/vue/vs.html @@ -0,0 +1,20 @@ + + + + + + Vue 和 React 的核心区别 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vue 和 React 的核心区别

VueAngular 以及 React 的区别是什么?

关于 Vue 和其他框架的不同,官方专门写了一篇文档,从性能、体积、灵活性等多个方面来进行了说明。 详细可以参阅:https://cn.vuejs.org/v2/guide/comparison.html

Composition API 与 React Hook 很像,区别是什么

从 React Hook 的实现角度看,React Hook 是根据 useState 调用的顺序来确定下一次重渲染时的 state 是来源于哪个 useState,所以出现了以下限制

  • 不能在循环、条件、嵌套函数中调用 Hook
  • 必须确保总是在你的 React 函数的顶层调用 Hook
  • useEffect、useMemo 等函数必须手动确定依赖关系

而 Composition API 是基于 Vue 的响应式系统实现的,与 React Hook 的相比

  • 声明在 setup 函数内,一次组件实例化只调用一次 setup,而 React Hook 每次重渲染都需要调用 Hook,使得 React 的 GC 比 Vue 更有压力,性能也相对于 Vue 来说也较慢
  • Compositon API 的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用
  • 响应式系统自动实现了依赖收集,进而组件的部分的性能优化由 Vue 内部自己完成,而 React Hook 需要手动传入依赖,而且必须必须保证依赖的顺序,让 useEffect、useMemo 等函数正确的捕获依赖变量,否则会由于依赖不正确使得组件性能下降。

虽然 Compositon API 看起来比 React Hook 好用,但是其设计思想也是借鉴 React Hook 的。

Vue 和 React 区别

定位相同:处理 UI 层的,vue 提倡渐进式处理,react 没有 vue 推崇模版写法,react 是 all in js jsx, vue 支持 jsx, react 不支持 vue 的 template react hooks, vue3 借鉴了,都有 hoos 风格的 api UI 更新策略:react 传入一个新数据,不能修改旧数据,vue 会根据两次数据渲染 dom 的 diff 更新 UI,数据变化,就会计划更新 UI,都会延迟更新 vue 文化:全部封装好;React 推崇第三方库结合 vue 有 keep-alive, react 没有,重新渲染,需要自己实现 vue css scoped;react 需要第三方:css modules/style-component

相似之处:

  • 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;
  • 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;
  • 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能;
  • 都有 props 的概念,允许组件间的数据传递;
  • 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。

不同之处 :

1)数据流 Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流 2)虚拟 DOM Vue2.x 开始引入"Virtual DOM",消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。

  • Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
  • 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。

3)组件化 React 与 Vue 最大的不同是模板的编写。

  • Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。
  • React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。

具体来讲:React 中 render 函数是支持闭包特性的,所以 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 4)监听数据变化的实现原理不同

  • Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
  • React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。

5)高阶组件 react 可以通过高阶组件(HOC)来扩展,而 Vue 需要通过 mixins 来扩展。 高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不能采用 HOC 来实现。 6)构建工具 两者都有自己的构建工具:

  • React ==> Create React APP
  • Vue ==> vue-cli

7)跨平台

  • React ==> React Native
  • Vue ==> Weex

Vue 的优点

  • 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;
  • 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
  • 双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;
  • 组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;
  • 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
  • 虚拟 DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;
  • 运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。
+ + + + + \ No newline at end of file diff --git a/vue/vue-cli.html b/vue/vue-cli.html new file mode 100644 index 00000000..75c224ee --- /dev/null +++ b/vue/vue-cli.html @@ -0,0 +1,20 @@ + + + + + + vue-cli 到底帮我们做了什么 | Sunny's blog + + + + + + + + +
Skip to content
On this page

vue-cli 到底帮我们做了什么

vue-cli 中的工程化

  1. vue.js:vue-cli 工程的核心,主要特点是双向数据绑定和组件系统。
  2. vue-router:vue 官方推荐使用的路由框架。
  3. vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护 vue 组件间共用的一些 变量 和 方法。
  4. axios(或者 fetch、ajax):用于发起 GET 、或 POST 等 http 请求,基于 Promise 设计。
  5. vux 等:一个专为 vue 设计的移动端 UI 组件库。
  6. webpack:模块加载和 vue-cli 工程打包器。
  7. eslint:代码规范工具

vue-cli 工程常用的 npm 命令有哪些?

下载 node_modules 资源包的命令:npm install

启动 vue-cli 开发环境的 npm 命令:npm run dev

vue-cli 生成 生产环境部署资源 的 npm 命令:npm run build

用于查看 vue-cli 生产环境部署资源文件大小的 npm 命令:npm run build --report

+ + + + + \ No newline at end of file diff --git a/vue/vue-compile.html b/vue/vue-compile.html new file mode 100644 index 00000000..2df33b58 --- /dev/null +++ b/vue/vue-compile.html @@ -0,0 +1,20 @@ + + + + + + Vue 编译器为什么如此强大 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vue 编译器为什么如此强大

说一下 vue 模版编译的原理是什么

简单说,Vue 的编译过程就是将 template 转化为 render 函数的过程。会经历以下阶段:

  • 生成 AST
  • 优化
  • codegen

首先解析模版,生成 AST 语法树(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。

Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最后一步是将优化后的 AST 树转换为可执行的代码。

说一下 Vue complier 的实现原理是什么样的?

在使用 vue 的时候,我们有两种方式来创建我们的 HTML 页面,第一种情况,也是大多情况下,我们会使用模板 template 的方式,因为这更易读易懂也是官方推荐的方法;第二种情况是使用 render 函数来生成 HTML,它比 template 更接近最终结果。

complier 的主要作用是解析模板,生成渲染模板的 render, 而 render 的作用主要是为了生成 VNode

complier 主要分为 3 大块:

  • parse:接受 template 原始模板,按着模板的节点和数据生成对应的 ast
  • optimize:遍历 ast 的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,通过 diff 减少去对比这部分 DOM,提升性能
  • generate 把前两步生成完善的 ast,组成 render 字符串,然后将 render 字符串通过 new Function 的方式转换成渲染函数

Vue 模版编译原理

vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。

  • 解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。
  • 优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能。
  • 生成阶段:将最终的 AST 转化为 render 函数字符串。

源码实现

TODO

+ + + + + \ No newline at end of file diff --git a/vue/vue-interview.html b/vue/vue-interview.html new file mode 100644 index 00000000..9e1691f0 --- /dev/null +++ b/vue/vue-interview.html @@ -0,0 +1,326 @@ + + + + + + 那些年,被问烂了的 Vuejs 面试题 | Sunny's blog + + + + + + + + +
Skip to content
On this page

那些年,被问烂了的 Vuejs 面试题

说一下 v-ifv-show 的区别

  • 共同点:都是动态显示 DOM 元素
  • 区别点:
    • 手段 v-if 是动态的向 DOM 树内添加或者删除 DOM 元素 v-show 是通过设置 DOM 元素的 display 样式属性控制显隐
    • 编译过程 v-if   切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件 v-show 只是简单的基于 css 切换
    • 编译条件 v-if   是惰性的,如果初始条件为假,则什么也不做。只有在条件第一次变为真时才开始局部编译 v-show 是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且 DOM 元素保留
    • 性能消耗 v-if   有更高的切换消耗 v-show 有更高的初始渲染消耗
    • 使用场景 v-if   适合运营条件不大可能改变 v-show 适合频繁切换

如何让 CSS 值在当前的组件中起作用

vue 文件中的 style 标签上,有一个特殊的属性:scoped。当一个 style 标签拥有 scoped 属性时,它的 CSS 样式就只能作用于当前的组件,也就是说,该样式只能适用于当前组件元素。通过该属性,可以使得组件之间的样式不互相污染。如果一个项目中的所有 style 标签全部加上了 scoped,相当于实现了样式的模块化。

scoped 的实现原理

vue 中的 scoped 属性的效果主要通过 PostCSS 转译实现的。PostCSS 给一个组件中的所有 DOM 添加了一个独一无二的动态属性,然后,给 CSS 选择器额外添加一个对应的属性选择器来选择该组件中 DOM,这种做法使得样式只作用于含有该属性的 DOM,即组件内部 DOM

例如:

转译前

javascript
<template>
+  <div class="example">hi</div>
+</template>
+
+<style scoped>
+.example {
+  color: red;
+}
+</style>
+
<template>
+  <div class="example">hi</div>
+</template>
+
+<style scoped>
+.example {
+  color: red;
+}
+</style>
+

转译后:

javascript
<template>
+  <div class="example" data-v-5558831a>hi</div>
+</template>
+
+<style>
+.example[data-v-5558831a] {
+  color: red;
+}
+</style>
+
<template>
+  <div class="example" data-v-5558831a>hi</div>
+</template>
+
+<style>
+.example[data-v-5558831a] {
+  color: red;
+}
+</style>
+

MVVM的优缺点?

优点:

  • 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定不同的"View"上,当 View 变化的时候 Model 不可以不变,当 Model 变化的时候 View 也可以不变。你可以把⼀些视图逻辑放在⼀个 ViewModel ⾥⾯,让很多 view 重⽤这段视图逻辑
  • 提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代码
  • ⾃动更新 dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动 dom 中解放

缺点:

  • Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的
  • ⼀个⼤的模块中 model 也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存
  • 对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼。

MVVM、MVC、MVP 的区别

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。

在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。

(1)MVC

MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。

(2)MVVM

MVVM 分为 Model、View、ViewModel:

  • Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
  • View 代表 UI 视图,负责数据的展示;
  • ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;

Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。

这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作 DOM。

(3)MVP

MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的 Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。

谈一谈对 MVVM 的理解?

  • MVVMModel-View-ViewModel 的缩写。MVVM 是一种设计思想。
  • Model 层代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑;
  • View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来,View 是一个同步 ViewModel 的对象
  • MVVM 架构下,ViewModel 之间并没有直接的联系,而是通过 ViewModel 进行交互, ModelViewModel 之间的交互是双向的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。
  • ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而 ViewModel 之间的 同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作 DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

Vue 中如何进行组件的使用?Vue 如何实现全局组件的注册?

要使用组件,首先需要使用 import 来引入组件,然后在 components 属性中注册组件,之后就可以在模板中使用组件了。

可以使用 Vue.component 方法来实现全局组件的注册。

Vue 组件的 data 为什么必须是函数

组件中的 data 写成一个函数,数据以函数返回值形式定义。这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果。

vue 如何快速定位那个组件出现性能问题的

timeline ⼯具。 通过 timeline 来查看每个函数的调⽤时常,定位出哪个函数的问题,从⽽能判断哪个组件出了问题。

scoped 是如何实现样式穿透的?

首先说一下什么场景下需要 scoped 样式穿透。

在很多项目中,会出现这么一种情况,即:引用了第三方组件,需要在组件中局部修改第三方组件的样式,而又不想去除 scoped 属性造成组件之间的样式污染。此时只能通过特殊的方式,穿透 scoped

有三种常用的方法来实现样式穿透。

方法一

使用 ::v-deep 操作符( >>> 的别名)

如果希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,可以使用 >>> 操作符:

上述代码将会编译成:

后面的类名没有 data 属性,所以能选到子组件里面的类名。

有些像 Sass 之类的预处理器无法正确解析 >>>,所以需要使用 ::v-deep 操作符来代替。

方法二

定义一个含有 scoped 属性的 style 标签之外,再定义一个不含有 scoped 属性的 style 标签,即在一个 vue 组件中定义一个全局的 style 标签,一个含有作用域的 style 标签:

此时,我们只需要将修改第三方样式的 css 写在第一个 style 中即可。

方法三

上面的方法一需要单独书写一个不含有 scoped 属性的 style 标签,可能会造成全局样式的污染。

更推荐的方式是在组件的外层 DOM 上添加唯一的 class 来区分不同组件,在书写样式时就可以正常针对针对这部分 DOM 书写样式。

javascript
<style scoped>.a >>> .b {/* ... */}</style>
+
<style scoped>.a >>> .b {/* ... */}</style>
+
javascript
.a[data-v-f3f3eg9] .b { /* ... */ }
+
.a[data-v-f3f3eg9] .b { /* ... */ }
+
javascript
<style>
+/* global styles */
+</style>
+
+<style scoped>
+/* local styles */
+</style>
+
<style>
+/* global styles */
+</style>
+
+<style scoped>
+/* local styles */
+</style>
+

说一下 ref 的作用是什么?

ref 的作用是被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。其特点是:

  • 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素
  • 如果用在子组件上,引用就指向组件实例

所以常见的使用场景有:

  1. 基本用法,本页面获取 DOM 元素
  2. 获取子组件中的 data
  3. 调用子组件中的方法

说一下你知道的 vue 修饰符都有哪些?

vue 中修饰符可以分为 3 类:

  • 事件修饰符
  • 按键修饰符
  • 表单修饰符

事件修饰符

在事件处理程序中调用 event.preventDefaultevent.stopPropagation 方法是非常常见的需求。尽管可以在 methods 中轻松实现这点,但更好的方式是:methods 只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,vuev-on 提供了事件修饰符。通过由点 . 表示的指令后缀来调用修饰符。

常见的事件修饰符如下:

  • .stop:阻止冒泡。
  • .prevent:阻止默认事件。
  • .capture:使用事件捕获模式。
  • .self:只在当前元素本身触发。
  • .once:只触发一次。
  • .passive:默认行为将会立即触发。

按键修饰符

除了事件修饰符以外,在 vue 中还提供了有鼠标修饰符,键值修饰符,系统修饰符等功能。

  • .left:左键
  • .right:右键
  • .middle:滚轮
  • .enter:回车
  • .tab:制表键
  • .delete:捕获 “删除” 和 “退格” 键
  • .esc:返回
  • .space:空格
  • .up:上
  • .down:下
  • .left:左
  • .right:右
  • .ctrlctrl
  • .altalt
  • .shiftshift
  • .metameta

表单修饰符

vue 同样也为表单控件也提供了修饰符,常见的有 .lazy.number.trim

  • .lazy:在文本框失去焦点时才会渲染
  • .number:将文本框中所输入的内容转换为 number 类型
  • .trim:可以自动过滤输入首尾的空格

Vue.extendVue.component 的区别是什么?

Vue.extend 用于创建一个基于 Vue 构造函数的“子类”,其参数应为一个包含组件选项的对象。

Vue.component 用来注册全局组件。

移动端如何实现一个比较友好的 header 组件

Header 一般分为左、中、右三个部分,分为三个区域来设计,中间为主标题,每个页面的标题肯定不同,所以可以通过 vue props的方式做成可配置对外进行暴露,左侧大部分页面可能都是回退按钮,但是样式和内容不尽相同,右侧一般都是具有功能性的操作按钮,所以左右两侧可以通过 vue slot 插槽的方式对外暴露以实现多样化,同时也可以提供 default slot 默认插槽来统一页面风格。

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 监测差异 ?

现代前端框架有两种方式侦测变化,一种是 pull,一种是 push

pull

其代表为 React,我们可以回忆一下 React 是如何侦测到变化的。

我们通常会用 setState API 显式更新,然后 React 会进行一层层的 Virtual Dom Diff 操作找出差异,然后 PatchDOM 上,React 从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的 Diff 操作查找「哪发生变化了」,另外一个代表就是 Angular 的脏检查操作。

push

Vue 的响应式系统则是 push 的代表,当 Vue 程序初始化的时候就会对数据 data 进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此 Vue 是一开始就知道是「在哪发生变化了」

但是这又会产生一个问题,通常绑定一个数据就需要一个 Watcher,一但我们的绑定细粒度过高就会产生大量的 Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此 Vue 的设计是选择中等细粒度的方案,在组件级别进行 push 侦测的方式,也就是那套响应式系统。

通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行 Virtual Dom Diff 获取更加具体的差异,而 Virtual Dom Diff 则是 pull 操作,Vuepush + pull 结合的方式进行变化侦测的。

Vue 为什么没有类似于 ReactshouldComponentUpdate 的生命周期?

根本原因是 VueReact 的变化侦测方式有所不同

Reactpull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual Dom Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能。

Vuepull+push 的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push 的阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期。

说一下你对 vue 事件绑定原理的理解?

vue 中的事件绑定是有两种,一种是原生的事件绑定,另一种是组件的事件绑定。

原生的事件绑定在普通元素上是通过 @click _ 进行绑定,在组件上是通过 @click.native 进行绑定,组件中的 _nativeOn_ 是等价于 on 的。组件的事件绑定的 @click 是 vue 中自定义的 $on 方法来实现的,必须有 $emit 才可以触发。

原生事件绑定原理

在 runtime 下的 patch.js 中 createPatchFunction 执行了之后再赋值给 patch。

createPatchFunction 方法有两个参数,分别是 nodeOps 存放操作 dom 节点的方法和 modules,modules 是有两个数组拼接起来的,modules 拼接完的数组中有一个元素就是 events,事件添加就发生在这里。

events 元素关联的就是 events.js 文件,在 events 中有一个 updateDOMListeners 方法,在 events 文件的结尾导出了一个对象,然后对象有一个属性叫做 create,这个属性关联的就是 updateDOMListeners 方法。

在执行 createPatchFunction 方法时,就会将这两个参数传入,在 createPatchFunction 方法中接收了一个参数 backend,在该方法中一开始进行 backend 的解构,就是上面的 nodeOps 和 modules 参数,解构完之后进入 for 循环。

在 createPatchFunction 开头定义了一个 cbs 对象。for 循环遍历一个叫 hooks 的数组。hooks 是文件一开头定义的一个数组,其中包括有 create,for 循环就是在 cbs 上定义一系列和 hooks 元素相同的属性,然后键值是一个数组,然后数组内容是 modules 里面的一些内容。这时就把 events 文件中导出来的 create 属性放在了 cbs 上。

当我们进入首次渲染的时候,会执行到 patch 函数里面的 createElm 方法,这个方法中就会调用 invokeCreateHooks 函数,用来处理事件系统,这里就是真正准备进行原生事件绑定的入口。invokeCreateHooks 方法中,遍历了 cbs.create 数组里面的内容。然后把 cbs.create 里面的函数全部都执行一次,在 cbs.create 其中一个函数就是 updateDOMListeners。

updateDOMListeners 就是用来添加事件的方法,在这方法中会根据 vnode 判断是否有定义一个点击事件。如果没有点击事件就 return。有的话就继续执行,给 on 进行赋值,然后进行一些赋值操作,将 vnode.elm 赋值给 target,elm 这个属性就是指向 vnode 所对应的真实 dom 节点,这里就是把我们要绑定事件的 dom 结点进行缓存,接下来执行 updateListeners 方法。在接下来执行 updateListeners 方法中调用了一个 add 的方法,然后在 app 方法中通过原生 addEventListener 把事件绑定到 dom 上。

组件事件绑定原理

在组件实例初始化会调用 initMixin 方法中的 Vue.prototype._init,在 init 函数中,会通过 initInternalComponent 方法初始化组件信息,将自定义的组件事件放到_parentListeners 上,下来就会调用 initEvents 来初始化组件事件,在 initEvents 中会实例上添加一个 _event 对象,用于保存自定义事件,然后获取到 父组件给 子组件绑定的自定义事件,也就是刚才在初始化组件信息的时候将自定义的组件事件放在了_parentListeners 上,这时候 vm.$options._parentListeners 就是自定义的事件。

最后进行判断,如果有自定义的组件事件就执行 updateComponentListeners 方法进行事件绑定,在 updateComponentListeners 方法中会调用 updateListeners 方法,并传传一个 add 方法进行执行,这个 add 方法里就是$on 方法。

deleteVue.delete 删除数组的区别是什么?

delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。 Vue.delete 是直接将元素从数组中完全删除,改变了数组其他元素的键值。

v-on 可以实现监听多个方法么?

可以监听多个方法。关于监听多个方法提供了几种不同的写法:

示例代码如下:

html
写法一:
+<div v-on="{ 事件类型: 事件处理函数, 事件类型: 事件处理函数 }"></div>
+写法二:
+<div @事件类型="“事件处理函数”" @事件类型="“事件处理函数”"></div>
+写法三:在一个事件里面书写多个事件处理函数
+<div @事件类型="“事件处理函数1,事件处理函数2”"></div>
+写法四:在事件处理函数内部调用其他的函数
+
写法一:
+<div v-on="{ 事件类型: 事件处理函数, 事件类型: 事件处理函数 }"></div>
+写法二:
+<div @事件类型="“事件处理函数”" @事件类型="“事件处理函数”"></div>
+写法三:在一个事件里面书写多个事件处理函数
+<div @事件类型="“事件处理函数1,事件处理函数2”"></div>
+写法四:在事件处理函数内部调用其他的函数
+
html
<template>
+  <div>
+    <!-- v-on在vue2.x中测试,以下两种均可-->
+    <button v-on="{ mouseenter: onEnter, mouseleave: onLeave }">
+      鼠标进来1
+    </button>
+    <button @mouseenter="onEnter" @mouseleave="onLeave">鼠标进来2</button>
+
+    <!-- 一个事件绑定多个函数,按顺序执行,这里分隔函数可以用逗号也可以用分号-->
+    <button @click="a(), b()">点我ab</button>
+    <button @click="one()">点我onetwothree</button>
+  </div>
+</template>
+<script>
+  export default {
+    methods: {
+      //这里是es6对象里函数写法
+      a() {
+        console.log("a");
+      },
+      b() {
+        console.log("b");
+      },
+      one() {
+        console.log("one");
+        this.two();
+        this.three();
+      },
+      two() {
+        console.log("two");
+      },
+      three() {
+        console.log("three");
+      },
+      onEnter() {
+        console.log("mouse enter");
+      },
+      onLeave() {
+        console.log("mouse leave");
+      },
+    },
+  };
+</script>
+
<template>
+  <div>
+    <!-- v-on在vue2.x中测试,以下两种均可-->
+    <button v-on="{ mouseenter: onEnter, mouseleave: onLeave }">
+      鼠标进来1
+    </button>
+    <button @mouseenter="onEnter" @mouseleave="onLeave">鼠标进来2</button>
+
+    <!-- 一个事件绑定多个函数,按顺序执行,这里分隔函数可以用逗号也可以用分号-->
+    <button @click="a(), b()">点我ab</button>
+    <button @click="one()">点我onetwothree</button>
+  </div>
+</template>
+<script>
+  export default {
+    methods: {
+      //这里是es6对象里函数写法
+      a() {
+        console.log("a");
+      },
+      b() {
+        console.log("b");
+      },
+      one() {
+        console.log("one");
+        this.two();
+        this.three();
+      },
+      two() {
+        console.log("two");
+      },
+      three() {
+        console.log("three");
+      },
+      onEnter() {
+        console.log("mouse enter");
+      },
+      onLeave() {
+        console.log("mouse leave");
+      },
+    },
+  };
+</script>
+

vue 的数据为什么频繁变化但只会更新一次?

这是因为 vueDOM 更新是一个异步操作,在数据更新后会首先被 set 钩子监听到,但是不会马上执行 DOM 更新,而是在下一轮循环中执行更新。

具体实现是 vue 中实现了一个 queue 队列用于存放本次事件循环中的所有 watcher 更新,并且同一个 watcher 的更新只会被推入队列一次,并在本轮事件循环的微任务执行结束后执行此更新(UI Render 阶段),这就是 DOM 只会更新一次的原因。

这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,vue 刷新队列并执行实际 (已去重的) 工作。vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver   和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

插槽与作用域插槽的区别是什么?

插槽的作用是子组件提供了可替换模板,父组件可以更换模板的内容。

作用域插槽给了子组件将数据返给父组件的能力,子组件一样可以复用,同时父组件也可以重新组织内容和样式。

vue 中相同逻辑如何进行抽离?

可以使用 vue 里面的混入(mixin)技术。混入(mixin)提供了一种非常灵活的方式,来将 vue 中相同的业务逻辑进行抽离。

例如:

  • data 中有很多是公用数据
  • 引用封装好的组件也都是一样的
  • methods、watch、computed 中也都有大量的重复代码

当然这个时候可以将所有的代码重复去写来实现功能,但是我们并不不推荐使用这种方式,无论是工作量、工作效率和后期维护来说都是不建议的,这个时候 mixin 就可以大展身手了。

一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。说白了就是给每个生命周期,函数等等中间加入一些公共逻辑。

混入技术特点

  • 当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
  • 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
  • 值为对象的选项,例如 methods、componentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

说一说自定义指令有哪些生命周期?

自定义指令的生命周期,有 5 个事件钩子,可以设置指令在某一个事件发生时的具体行为:

  • bind: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。
  • inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
  • update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新(详细的钩子函数参数见下)。
  • componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
  • unbind: 只调用一次, 指令与元素解绑时调用。

钩子函数的参数 (包括 el,binding,vnode,oldVnode)

  • el: 指令所绑定的元素,可以用来直接操作 DOM 。
  • binding: 一个对象,包含以下属性:name: 指令名、value: 指令的绑定值、oldValue: 指令绑定的前一个值、expression: 绑定值的字符串形式、arg: 传给指令的参数、modifiers: 一个包含修饰符的对象。
  • vnode: Vue 编译生成的虚拟节点。
  • oldVnode: 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

vue 为什么采用异步渲染

因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染;所以为了性能考虑,Vue 会在本轮数据更新后,再去异步更新视图。

异步渲染的原理:

  1. 调用 notify( ) 方法,通知 watcher 进行更新操作
  2. 依次调用 watcher 的 update 方法
  3. 对 watcher 进行去重操作(通过 id)放到队列里
  4. 执行完后异步清空这个队列,nextTick(flushSchedulerQueue)进行批量更新操作

组件中写 name 选项有哪些好处

  1. 可以通过名字找到对应的组件( 递归组件:组件自身调用自身 )
  2. 可以通过 name 属性实现缓存功能(keep-alive
  3. 可以通过 name 来识别组件(跨级组件通信时非常重要)
  4. 使用 vue-devtools 调试工具里显示的组见名称是由 vue 中组件 name 决定的

过滤器的作用,如何实现一个过滤器

根据过滤器的名称,过滤器是用来过滤数据的,在 Vue 中使用 filters 来过滤数据,filters 不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。

使用场景:

  • 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。
  • 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用 fliters 过滤器来处理数据。

过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 * 和 v-bind*** 表达式** 中,然后放在操作符“ | ”后面进行指示。

例如,在显示金额,给商品价格添加单位:

vue
<li>商品价格:{{item.price | filterPrice}}</li>
+filters: { filterPrice (price) { return price ? ('¥' + price) : '--' } }
+
<li>商品价格:{{item.price | filterPrice}}</li>
+filters: { filterPrice (price) { return price ? ('¥' + price) : '--' } }
+

如何保存页面的当前的状态

既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:

  • 前组件会被卸载
  • 前组件不会被卸载

那么可以按照这两种情况分别得到以下方法:

组件会被卸载:

(1)将状态存储在 LocalStorage / SessionStorage

只需要在组件即将被销毁的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。

比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。 优点:

  • 兼容性好,不需要额外库或工具。
  • 简单快捷,基本可以满足大部分需求。

缺点:

  • 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷贝的缺点)
  • 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象

(2)路由传值

通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。 在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。 优点:

  • 简单快捷,不会污染 LocalStorage / SessionStorage。
  • 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)

缺点:

  • 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。

组件不会被卸载:

(1)单页面渲染

要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。

优点:

  • 代码量少
  • 不需要考虑状态传递过程中的错误

缺点:

  • 增加 A 组件维护成本
  • 需要传入额外的 prop 到 B 组件
  • 无法利用路由定位页面

除此之外,在 Vue 中,还可以是用 keep-alive 来缓存页面,当组件在 keep-alive 内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行 被包裹在 keep-alive 中的组件的状态将会被保留:

vue
<keep-alive>
+	<router-view v-if="$route.meta.keepAlive"></router-view>
+</kepp-alive>
+
<keep-alive>
+	<router-view v-if="$route.meta.keepAlive"></router-view>
+</kepp-alive>
+

router.js

javascript
{
+  path: '/',
+  name: 'xxx',
+  component: ()=>import('../src/views/xxx.vue'),
+  meta:{
+    keepAlive: true // 需要被缓存
+  }
+},
+
{
+  path: '/',
+  name: 'xxx',
+  component: ()=>import('../src/views/xxx.vue'),
+  meta:{
+    keepAlive: true // 需要被缓存
+  }
+},
+

常见的事件修饰符及其作用

  • .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;
  • .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
  • .capture :与事件冒泡的方向相反,事件捕获由外到内;
  • .self :只会触发自己范围内的事件,不包含子元素;
  • .once :只会触发一次。

v-if、v-show、v-html 的原理

  • v-if 会调用 addIfCondition 方法,生成 vnode 的时候会忽略对应节点,render 的时候就不会渲染;
  • v-show 会生成 vnode,render 的时候也会渲染成真实节点,只是在 render 过程中会在节点的属性中修改 show 属性值,也就是常说的 display;
  • v-html 会先移除节点下的所有节点,调用 html 方法,通过 addProp 添加 innerHTML 属性,归根结底还是设置 innerHTML 为 v-html 的值。

v-if 能够控制是否生成 vnode,也就间接控制了是否生成对应的 dom。当 v-if 为 true 时,会生成对应的 vnode,并生成对应的 dom 元素;当其为 false 时,不会生成对应的 vnode,自然不会生成任何的 dom 元素。 v-show 始终会生成 vnode,也就间接导致了始终生成 dom。它只是控制 dom 的 display 属性,当 v-show 为 true 时,不做任何处理;当其为 false 时,生成的 dom 的 display 属性为 none。 使用 v-if 可以有效的减少树的节点和渲染量,但也会导致树的不稳定;而使用 v-show 可以保持树的稳定,但不能减少树的节点和渲染量。 因此,在实际开发中,显示状态变化频繁的情况下应该使用 v-show,以保持树的稳定;显示状态变化较少时应该使用 v-if,以减少树的节点和渲染量。

data 为什么是一个函数而不是对象

JavaScript 中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。 而在 Vue 中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。 所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的 data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

Vue 中封装的数组方法有哪些,其如何实现页面更新

在 Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。 push() pop() shift() unshift () splice() sort() reverse() 那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:

javascript
// 缓存数组原型
+const arrayProto = Array.prototype;
+// 实现 arrayMethods.__proto__ === Array.prototype
+export const arrayMethods = Object.create(arrayProto);
+// 需要进行功能拓展的方法
+const methodsToPatch = [
+  "push",
+  "pop",
+  "shift",
+  "unshift",
+  "splice",
+  "sort",
+  "reverse",
+];
+
+/**
+ * Intercept mutating methods and emit events
+ */
+methodsToPatch.forEach(function (method) {
+  // 缓存原生数组方法
+  const original = arrayProto[method];
+  def(arrayMethods, method, function mutator(...args) {
+    // 执行并缓存原生数组功能
+    const result = original.apply(this, args);
+    // 响应式处理
+    const ob = this.__ob__;
+    let inserted;
+    switch (method) {
+      // push、unshift会新增索引,所以要手动observer
+      case "push":
+      case "unshift":
+        inserted = args;
+        break;
+      // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
+      case "splice":
+        inserted = args.slice(2);
+        break;
+    }
+    //
+    if (inserted) ob.observeArray(inserted); // 获取插入的值,并设置响应式监听
+    // notify change
+    ob.dep.notify(); // 通知依赖更新
+    // 返回原生数组方法的执行结果
+    return result;
+  });
+});
+
// 缓存数组原型
+const arrayProto = Array.prototype;
+// 实现 arrayMethods.__proto__ === Array.prototype
+export const arrayMethods = Object.create(arrayProto);
+// 需要进行功能拓展的方法
+const methodsToPatch = [
+  "push",
+  "pop",
+  "shift",
+  "unshift",
+  "splice",
+  "sort",
+  "reverse",
+];
+
+/**
+ * Intercept mutating methods and emit events
+ */
+methodsToPatch.forEach(function (method) {
+  // 缓存原生数组方法
+  const original = arrayProto[method];
+  def(arrayMethods, method, function mutator(...args) {
+    // 执行并缓存原生数组功能
+    const result = original.apply(this, args);
+    // 响应式处理
+    const ob = this.__ob__;
+    let inserted;
+    switch (method) {
+      // push、unshift会新增索引,所以要手动observer
+      case "push":
+      case "unshift":
+        inserted = args;
+        break;
+      // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
+      case "splice":
+        inserted = args.slice(2);
+        break;
+    }
+    //
+    if (inserted) ob.observeArray(inserted); // 获取插入的值,并设置响应式监听
+    // notify change
+    ob.dep.notify(); // 通知依赖更新
+    // 返回原生数组方法的执行结果
+    return result;
+  });
+});
+

简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过 target proto == arrayMethods 来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。

Vue 单页应用与多页应用的区别

概念:

  • SPA 单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次 js、css 等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
  • MPA 多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载 js、css 等相关资源。多页应用跳转,需要整页资源刷新。

Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。 如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。

简述 mixin、extends 的覆盖逻辑

(1)mixin 和 extends mixin 和 extends 均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。

  • mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
  • extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数。

(2)mergeOptions 的执行过程

  • 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
  • 对未合并的选项,进行判断
javascript
if (!child._base) {
+  if (child.extends) {
+    parent = mergeOptions(parent, child.extends, vm);
+  }
+  if (child.mixins) {
+    for (let i = 0, l = child.mixins.length; i < l; i++) {
+      parent = mergeOptions(parent, child.mixins[i], vm);
+    }
+  }
+}
+
if (!child._base) {
+  if (child.extends) {
+    parent = mergeOptions(parent, child.extends, vm);
+  }
+  if (child.mixins) {
+    for (let i = 0, l = child.mixins.length; i < l; i++) {
+      parent = mergeOptions(parent, child.mixins[i], vm);
+    }
+  }
+}
+
  • 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
  • 返回合并结果 options。

assets 和 static 的区别

相同点: assets 和 static 两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点

不相同点: assets 中存放的静态资源文件在项目打包时,也就是运行 npm run build 时会将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static 文件中跟着 index.html 一同上传至服务器。static 中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件提交较大点。在服务器中就会占据更大的空间。

建议: 将项目中 template 需要的样式文件 js 文件等都可以放置在 assets 中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如 iconfoont.css 等文件可以放置在 static 中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。

delete 和 Vue.delete 删除数组的区别

  • delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
  • Vue.delete 直接删除了数组 改变了数组的键值。

什么是 mixin ?

  • Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
  • 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
  • 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

template 和 jsx 的有什么分别?

对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用 vue-loader 编译.vue 文件,内部依赖的 vue-template-compiler 模块,在 webpack 构建过程中,将 template 预编译成 render 函数。与 react 类似,在添加了 jsx 的语法糖解析器 babel-plugin-transform-vue-jsx 之后,就可以直接手写 render 函数。 所以,template 和 jsx 的都是 render 的一种表现形式,不同的是:JSX 相对于 template 而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。

vue 初始化页面闪动问题

使用 vue 开发时,在 vue 初始化之前,由于 div 是不归 vue 管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。 首先:在 css 里加上以下代码:

javascript
[v-cloak] {    display: none;}
+
[v-cloak] {    display: none;}
+

如果没有彻底解决问题,则在根元素加上 style="display: none;" :style="{display: 'block'}"

extend 有什么作用

这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。

javascript
// 创建组件构造器
+let Component = Vue.extend({ template: "<div>test</div>" });
+// 挂载到 #app 上new Component().$mount('#app')// 除了上面的方式,还可以用来扩展已有的组件let SuperComponent = Vue.extend(Component)new SuperComponent({    created() {        console.log(1)    }})
+new SuperComponent().$mount("#app");
+
// 创建组件构造器
+let Component = Vue.extend({ template: "<div>test</div>" });
+// 挂载到 #app 上new Component().$mount('#app')// 除了上面的方式,还可以用来扩展已有的组件let SuperComponent = Vue.extend(Component)new SuperComponent({    created() {        console.log(1)    }})
+new SuperComponent().$mount("#app");
+

mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。

javascript
Vue.mixin({
+  beforeCreate() {        // ...逻辑        // 这种方式会影响到每个组件的 beforeCreate 钩子函数    }})
+
Vue.mixin({
+  beforeCreate() {        // ...逻辑        // 这种方式会影响到每个组件的 beforeCreate 钩子函数    }})
+

虽然文档不建议在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。

mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。 另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

内置组件 Transition

官网详细文档:https://cn.vuejs.org/v2/guide/transitions.html

时机

Transition组件会监控slot唯一根元素的出现和消失,并会在其出现和消失时应用过渡效果 Transition不生成任何元素,只是为了生成过渡效果 具体的监听内容是:

  • 它会对新旧两个虚拟节点进行对比,如果旧节点被销毁,则应用消失效果,如果新节点是新增的,则应用进入效果
  • 如果不是上述情况,则它会对比新旧节点,观察其v-show是否变化,true->false应用消失效果,false->true应用进入效果

流程

类名规则:

  1. 如果transition上没有定义name,则类名为v-xxxx
  2. 如果transition上定义了name,则类名为${name}-xxxx
  3. 如果指定了类名,直接使用指定的类名

指定类名见:自定义过渡类名

1. 进入效果

2. 消失效果

过渡组

Transision可以监控其内部的单个 dom 元素的出现和消失,并为其附加样式

如果要监控一个 dom 列表,就需要使用TransitionGroup组件

它会对列表的新增元素应用进入效果,删除元素应用消失效果,对被移动的元素应用v-move样式

被移动的元素之所以能够实现过渡效果,是因为TransisionGroup内部使用了 Flip 过渡方案

+ + + + + \ No newline at end of file diff --git a/vue/vue-router.html b/vue/vue-router.html new file mode 100644 index 00000000..418f5a03 --- /dev/null +++ b/vue/vue-router.html @@ -0,0 +1,312 @@ + + + + + + VueRouter 路由从使用到源码 | Sunny's blog + + + + + + + + +
Skip to content
On this page

VueRouter 路由从使用到源码

源码实现

先看我写的 mini-router,后续会补充文章

Vue 的路由实现原理

解释 hash 模式和 history 模式的实现原理

# 后面 hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新页面;通过监听 hashchange 事件可以知道 hash 发生了哪些变化,然后根据 hash 变化来实现更新页面部分内容的操作。

history 模式的实现,主要是 HTML5 标准发布的两个 APIpushStatereplaceState,这两个 API 可以在改变 URL,但是不会发送请求。这样就可以监听 url 变化来实现更新页面部分内容的操作。

两种模式的区别:

  • 首先是在 URL 的展示上,hash 模式有“#”,history 模式没有
  • 刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由
  • 在兼容性上,hash 可以支持低版本浏览器和 IE

router 和route 的区别

$route 对象表示当前的路由信息,包含了当前 URL 解析得到的信息。包含当前的路径,参数,query 对象等。

  • $route.path:字符串,对应当前路由的路径,总是解析为绝对路径,如 "/foo/bar"。
  • $route.params: 一个 key/value 对象,包含了 动态片段 和 全匹配片段,如果没有路由参数,就是一个空对象。
  • route.query.user == 1,如果没有查询参数,则是个空对象。
  • $route.hash:当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。
  • $route.fullPath:完成解析后的 URL,包含查询参数和 hash 的完整路径。
  • $route.matched:数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
  • $route.name:当前路径名字
  • $route.meta:路由元信息

$route 对象出现在多个地方:

  • 组件内的 this.$routeroute watcher 回调(监测变化处理)
  • router.match(location) 的返回值
  • scrollBehavior 方法的参数
  • 导航钩子的参数,例如 router.beforeEach 导航守卫的钩子函数中,tofrom 都是这个路由信息对象。

$router 对象是全局路由的实例,是 router 构造方法的实例。

$router 对象常用的方法有:

  • push:向 history 栈添加一个新的记录
  • go:页面路由跳转前进或者后退
  • replace:替换当前的页面,不会向 history 栈添加一个新的记录

vueRouter 有哪几种导航守卫?

  • 全局前置/钩子:beforeEach、beforeR-esolve、afterEach
  • 路由独享的守卫:beforeEnter
  • 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

解释一下 vueRouter 的完整的导航解析流程是什么

一次完整的导航解析流程如下:

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

如何监听 pushstatereplacestate 的变化呢?

History.replaceStatepushState 不会触发 popstate 事件,所以我们可以通过在方法中创建一个新的全局事件来实现  pushstatereplacestate 变化的监听。

具体做法为:

这样就创建了 2 个全新的事件,事件名为 pushStatereplaceState,我们就可以在全局监听:

这样就可以监听到 pushStatereplaceState 行为。

javascript
var _wr = function (type) {
+  var orig = history[type];
+  return function () {
+    var rv = orig.apply(this, arguments);
+    var e = new Event(type);
+    e.arguments = arguments;
+    window.dispatchEvent(e);
+    return rv;
+  };
+};
+history.pushState = _wr("pushState");
+history.replaceState = _wr("replaceState");
+
var _wr = function (type) {
+  var orig = history[type];
+  return function () {
+    var rv = orig.apply(this, arguments);
+    var e = new Event(type);
+    e.arguments = arguments;
+    window.dispatchEvent(e);
+    return rv;
+  };
+};
+history.pushState = _wr("pushState");
+history.replaceState = _wr("replaceState");
+
javascript
window.addEventListener("replaceState", function (e) {
+  console.log("THEY DID IT AGAIN! replaceState 111111");
+});
+window.addEventListener("pushState", function (e) {
+  console.log("THEY DID IT AGAIN! pushState 2222222");
+});
+
window.addEventListener("replaceState", function (e) {
+  console.log("THEY DID IT AGAIN! replaceState 111111");
+});
+window.addEventListener("pushState", function (e) {
+  console.log("THEY DID IT AGAIN! pushState 2222222");
+});
+

路由模式

路由模式决定了:

  1. 路由从哪里获取访问路径
  2. 路由如何改变访问路径

vue-router提供了三种路由模式:

  1. hash:默认值。路由从浏览器地址栏中的 hash 部分获取路径,改变路径也是改变的 hash 部分。该模式兼容性最好。
  2. history:路由从浏览器地址栏的location.pathname中获取路径,改变路径使用的 H5 的history api。该模式可以让地址栏最友好,但是需要浏览器支持history api
  3. abstract:路由从内存中获取路径,改变路径也只是改动内存中的值。这种模式通常应用到非浏览器环境中。

每一次刷新页面

请求 inde.html 请求各种.js 请求各种.css 执行 Js 创建 vue 应用 渲染全部组件树 挂载到指定的 div 中

不刷新

执行一段 JS 代码:切换某个区域的组件 以上,所以推荐单页面应用

Vue-Router 的懒加载如何实现

非懒加载:

javascript
import List from "@/components/list.vue";
+const router = new VueRouter({
+  routes: [{ path: "/list", component: List }],
+});
+
import List from "@/components/list.vue";
+const router = new VueRouter({
+  routes: [{ path: "/list", component: List }],
+});
+

(1)方案一(常用):使用箭头函数+import 动态加载

javascript
const List = () => import("@/components/list.vue");
+const router = new VueRouter({
+  routes: [{ path: "/list", component: List }],
+});
+
const List = () => import("@/components/list.vue");
+const router = new VueRouter({
+  routes: [{ path: "/list", component: List }],
+});
+

(2)方案二:使用箭头函数+require 动态加载

javascript
const router = new Router({
+  routes: [
+    {
+      path: "/list",
+      component: (resolve) => require(["@/components/list"], resolve),
+    },
+  ],
+});
+
const router = new Router({
+  routes: [
+    {
+      path: "/list",
+      component: (resolve) => require(["@/components/list"], resolve),
+    },
+  ],
+});
+

(3)方案三:使用 webpack 的 require.ensure 技术,也可以实现按需加载。 这种情况下,多个路由指定相同的 chunkName,会合并打包成一个 js 文件。

javascript
// r就是resolve
+const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
+// 路由也是正常的写法  这种是官方推荐的写的 按模块划分懒加载
+const router = new Router({
+  routes: [
+  {
+    path: '/list',
+    component: List,
+    name: 'list'
+  }
+ ]
+}))
+
// r就是resolve
+const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
+// 路由也是正常的写法  这种是官方推荐的写的 按模块划分懒加载
+const router = new Router({
+  routes: [
+  {
+    path: '/list',
+    component: List,
+    name: 'list'
+  }
+ ]
+}))
+

如何获取页面的 hash 变化

(1)监听$route 的变化

javascript
// 监听,当路由发生变化的时候执行
+watch: {
+  $route: {
+    handler: function(val, oldVal){
+      console.log(val);
+    },
+    // 深度观察监听
+    deep: true
+  }
+},
+
+
// 监听,当路由发生变化的时候执行
+watch: {
+  $route: {
+    handler: function(val, oldVal){
+      console.log(val);
+    },
+    // 深度观察监听
+    deep: true
+  }
+},
+
+

(2)window.location.hash 读取#值 window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。

$route 和$router 的区别

  • $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
  • $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。

如何定义动态路由?如何获取传过来的动态参数?

(1)param 方式

  • 配置路由格式:/router/:id
  • 传递的方式:在 path 后面跟上对应的值
  • 传递后形成的路径:/router/123

1)路由定义

javascript
//在APP.vue中
+<router-link :to="'/user/'+userId" replace>用户</router-link>
+
+//在index.js
+{
+   path: '/user/:userid',
+   component: User,
+},
+
+
//在APP.vue中
+<router-link :to="'/user/'+userId" replace>用户</router-link>
+
+//在index.js
+{
+   path: '/user/:userid',
+   component: User,
+},
+
+

2)路由跳转

javascript
// 方法1:
+<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
+
+// 方法2:
+this.$router.push({name:'users',params:{uname:wade}})
+
+// 方法3:
+this.$router.push('/user/' + wade)
+
// 方法1:
+<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
+
+// 方法2:
+this.$router.push({name:'users',params:{uname:wade}})
+
+// 方法3:
+this.$router.push('/user/' + wade)
+

3)参数获取 通过 $route.params.userid 获取传递的值

(2)query 方式

  • 配置路由格式:/router,也就是普通配置
  • 传递的方式:对象中使用 query 的 key 作为传递方式
  • 传递后形成的路径:/route?id=123

1)路由定义

javascript
//方式1:直接在router-link 标签上以对象的形式
+<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
+
+// 方式2:写成按钮以点击事件形式
+<button @click='profileClick'>我的</button>
+
+profileClick(){
+  this.$router.push({
+    path: "/profile",
+    query: {
+        name: "kobi",
+        age: "28",
+        height: 198
+    }
+  });
+}
+
+
//方式1:直接在router-link 标签上以对象的形式
+<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
+
+// 方式2:写成按钮以点击事件形式
+<button @click='profileClick'>我的</button>
+
+profileClick(){
+  this.$router.push({
+    path: "/profile",
+    query: {
+        name: "kobi",
+        age: "28",
+        height: 198
+    }
+  });
+}
+
+

2)跳转方法

javascript
// 方法1:
+<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
+
+// 方法2:
+this.$router.push({ name: 'users', query:{ uname:james }})
+
+// 方法3:
+<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
+
+// 方法4:
+this.$router.push({ path: '/user', query:{ uname:james }})
+
+// 方法5:
+this.$router.push('/user?uname=' + jsmes)
+
+
// 方法1:
+<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
+
+// 方法2:
+this.$router.push({ name: 'users', query:{ uname:james }})
+
+// 方法3:
+<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
+
+// 方法4:
+this.$router.push({ path: '/user', query:{ uname:james }})
+
+// 方法5:
+this.$router.push('/user?uname=' + jsmes)
+
+

3)获取参数

javascript
通过$route.query 获取传递的值
+
通过$route.query 获取传递的值
+

Vue-router 路由钩子在生命周期的体现

一、Vue-Router 导航守卫

有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。 为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的

  1. 全局路由钩子

vue-router 全局有三个路由钩子;

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后

具体使用 ∶

  • beforeEach(判断是否登录了,没登录就跳转到登录页)
javascript
router.beforeEach((to, from, next) => {
+  let ifInfo = Vue.prototype.$common.getSession("userData"); // 判断是否登录的存储信息
+  if (!ifInfo) {
+    // sessionStorage里没有储存user信息
+    if (to.path == "/") {
+      //如果是登录页面路径,就直接next()
+      next();
+    } else {
+      //不然就跳转到登录
+      Message.warning("请重新登录!");
+      window.location.href = Vue.prototype.$loginUrl;
+    }
+  } else {
+    return next();
+  }
+});
+
router.beforeEach((to, from, next) => {
+  let ifInfo = Vue.prototype.$common.getSession("userData"); // 判断是否登录的存储信息
+  if (!ifInfo) {
+    // sessionStorage里没有储存user信息
+    if (to.path == "/") {
+      //如果是登录页面路径,就直接next()
+      next();
+    } else {
+      //不然就跳转到登录
+      Message.warning("请重新登录!");
+      window.location.href = Vue.prototype.$loginUrl;
+    }
+  } else {
+    return next();
+  }
+});
+
  • afterEach (跳转之后滚动条回到顶部)
javascript
router.afterEach((to, from) => {
+  // 跳转之后滚动条回到顶部
+  window.scrollTo(0, 0);
+});
+
router.afterEach((to, from) => {
+  // 跳转之后滚动条回到顶部
+  window.scrollTo(0, 0);
+});
+
  1. 单个路由独享钩子

beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数 ∶ to、from、next

javascript
export default [
+  {
+    path: "/",
+    name: "login",
+    component: login,
+    beforeEnter: (to, from, next) => {
+      console.log("即将进入登录页面");
+      next();
+    },
+  },
+];
+
export default [
+  {
+    path: "/",
+    name: "login",
+    component: login,
+    beforeEnter: (to, from, next) => {
+      console.log("即将进入登录页面");
+      next();
+    },
+  },
+];
+
  1. 组件内钩子

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave

这三个钩子都有三个参数 ∶to、from、next

  • beforeRouteEnter∶ 进入组件前触发
  • beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径 foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的 foa 组件,这个钩子在这种情况下就会被调用
  • beforeRouteLeave∶ 离开组件被调用

注意点,beforeRouteEnter 组件内还访问不到 this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next 来访问,例如:

javascript
beforeRouteEnter(to, from, next) {
+    next(target => {
+        if (from.path == '/classProcess') {
+            target.isFromProcess = true
+        }
+    })
+}
+
+
beforeRouteEnter(to, from, next) {
+    next(target => {
+        if (from.path == '/classProcess') {
+            target.isFromProcess = true
+        }
+    })
+}
+
+

二、Vue 路由钩子在生命周期函数的体现

  1. 完整的路由导航解析流程(不包括其他生命周期)
  • 触发进入其他路由。
  • 调用要离开路由的组件守卫 beforeRouteLeave
  • 调用局前置守卫 ∶ beforeEach
  • 在重用的组件里调用 beforeRouteUpdate
  • 调用路由独享守卫 beforeEnter。
  • 解析异步路由组件。
  • 在将要进入的路由组件中调用 beforeRouteEnter
  • 调用全局解析守卫 beforeResolve
  • 导航被确认。
  • 调用全局后置钩子的 afterEach 钩子。
  • 触发 DOM 更新(mounted)。
  • 执行 beforeRouteEnter 守卫中传给 next 的回调函数
  1. 触发钩子的完整顺序

路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从 a 组件离开,第一次进入 b 组件 ∶

  • beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
  • beforeEach:路由全局前置守卫,可用于登录验证、全局路由 loading 等。
  • beforeEnter:路由独享守卫
  • beforeRouteEnter:路由组件的组件进入路由前钩子。
  • beforeResolve:路由全局解析守卫
  • afterEach:路由全局后置钩子
  • beforeCreate:组件生命周期,不能访问 tAis。
  • created;组件生命周期,可以访问 tAis,不能访问 dom。
  • beforeMount:组件生命周期
  • deactivated:离开缓存组件 a,或者触发 a 的 beforeDestroy 和 destroyed 组件销毁钩子。
  • mounted:访问/操作 dom。
  • activated:进入缓存组件,进入 a 的嵌套子组件(如果有的话)。
  • 执行 beforeRouteEnter 回调函数 next。
  1. 导航行为被触发到导航完成的整个过程
  • 导航行为被触发,此时导航未被确认。
  • 在失活的组件里调用离开守卫 beforeRouteLeave。
  • 调用全局的 beforeEach 守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  • 在路由配置里调用 beforeEnteY。
  • 解析异步路由组件(如果有)。
  • 在被激活的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
  • 触发 DOM 更新。
  • 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
  • 导航完成

Vue-router 跳转和 location.href 有什么区别

  • 使用 location.href= /url来跳转,简单方便,但是刷新了页面;
  • 使用 history.pushState( /url ) ,无刷新页面,静态跳转;
  • 引进 router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为 vue-router 就是用了 history.pushState() ,尤其是在 history 模式下。

params 和 query 的区别

用法:query 要用 path 来引入,params 要用 name 来引入,接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name

url 地址显示:query 更加类似于 ajax 中 get 传参,params 则类似于 post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示

注意:query 刷新不会丢失 query 里面的数据 params 刷新会丢失 params 里面的数据。

Vue-router 导航守卫有哪些

  • 全局前置/钩子:beforeEach、beforeResolve、afterEach
  • 路由独享的守卫:beforeEnter
  • 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
+ + + + + \ No newline at end of file diff --git a/vue/vue3-onepage.html b/vue/vue3-onepage.html new file mode 100644 index 00000000..2567d708 --- /dev/null +++ b/vue/vue3-onepage.html @@ -0,0 +1,790 @@ + + + + + + Vue3 新特性 TODO: 性能提升 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vue3 新特性 TODO: 性能提升

createApp

javascript
// vue2
+const app = new Vue(options);
+// 使用插件:Vue.use()
+app.$mount("#app");
+// vue3
+// 不存在构造函数Vue
+const app = createApp(App);
+// 使用插件:app.use()
+app.mount("#app");
+// 简写:createApp(App).mount("#app");
+// App是根组件,返回一个vue应用
+
// vue2
+const app = new Vue(options);
+// 使用插件:Vue.use()
+app.$mount("#app");
+// vue3
+// 不存在构造函数Vue
+const app = createApp(App);
+// 使用插件:app.use()
+app.mount("#app");
+// 简写:createApp(App).mount("#app");
+// App是根组件,返回一个vue应用
+

this 变化:vue3 的组件实例代理

composition api 组合 api

vue2 配置式的,data,computed,props,methods,即 option api

vue3 对 ref 的特殊处理

去掉了 Vue 构造函数

可以更好的 tree shaking

在过去,如果遇到一个页面有多个vue应用时,往往会遇到一些问题

html
<!-- vue2 -->
+<div id="app1"></div>
+<div id="app2"></div>
+<script>
+    Vue.use(...); // 此代码会影响所有的vue应用
+    Vue.mixin(...); // 此代码会影响所有的vue应用
+    Vue.component(...); // 此代码会影响所有的vue应用
+  // 可能只是第一个需要用插件,第二个不用,之前毫无办法
+  	new Vue({
+      // 配置
+    }).$mount("#app1")
+
+    new Vue({
+      // 配置
+    }).$mount("#app2")
+</script>
+
<!-- vue2 -->
+<div id="app1"></div>
+<div id="app2"></div>
+<script>
+    Vue.use(...); // 此代码会影响所有的vue应用
+    Vue.mixin(...); // 此代码会影响所有的vue应用
+    Vue.component(...); // 此代码会影响所有的vue应用
+  // 可能只是第一个需要用插件,第二个不用,之前毫无办法
+  	new Vue({
+      // 配置
+    }).$mount("#app1")
+
+    new Vue({
+      // 配置
+    }).$mount("#app2")
+</script>
+

vue3中,去掉了Vue构造函数,转而使用createApp创建vue应用

html
<!-- vue3 -->
+<div id="app1"></div>
+<div id="app2"></div>
+<script>
+  去了vue实例里面      链式编程因为每次都返回还是实例
+  createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
+   createApp(根组件).mount("#app2")
+</script>
+
<!-- vue3 -->
+<div id="app1"></div>
+<div id="app2"></div>
+<script>
+  去了vue实例里面      链式编程,因为每次都返回还是实例
+  createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
+   createApp(根组件).mount("#app2")
+</script>
+

更多 vue 应用的 api:https://v3.vu ejs.org/api/application-api.html

组件实例中的 API

vue3中,组件实例是一个Proxy,它仅提供了下列成员,功能和vue2一样

属性:https://v3.vuejs.org/api/instance-properties.html

方法:https://v3.vuejs.org/api/instance-methods.html

只能调用列表里面的属性和方法,私有的和其他的不可以调用

对比数据响应式

vue2 和 vue3 均在相同的生命周期(beforeCreate 后,created 之前)完成数据响应式,但做法不一样

TIP

为什么 vue3 中去掉了 vue 构造函数?

vue2 的全局构造函数带来了诸多问题:

  1. 调用构造函数的静态方法会对所有 vue 应用生效,不利于隔离不同应用
  2. vue2 的构造函数集成了太多功能,不利于 tree shaking,vue3 把这些功能使用普通函数导出,能够充分利用 tree shaking 优化打包体积
  3. vue2 没有把组件实例和 vue 应用两个概念区分开,在 vue2 中,通过 new Vue 创建的对象,既是一个 vue 应用,同时又是一个特殊的 vue 组件。vue3 中,把两个概念区别开来,通过 createApp 创建的对象,是一个 vue 应用,它内部提供的方法是针对整个应用的,而不再是一个特殊的组件。

TIP

谈谈你对 vue3 数据响应式的理解

vue3 不再使用 Object.defineProperty 的方式定义完成数据响应式,而是使用 Proxy。除了 Proxy 本身效率比 Object.defineProperty 更高之外,由于不必递归遍历所有属性,而是直接得到一个 Proxy。所以在 vue3 中,对数据的访问是动态的,当访问某个属性的时候,再动态的获取和设置,这就极大的提升了在组件初始阶段的效率。(想用那个再读那个),如果访问一个对象,则再返回一个 proxy。同时,由于 Proxy 可以监控到成员的新增和删除,因此,在 vue3 中新增成员、删除成员、索引访问等均可以触发重新渲染,而这些在 vue2 中是难以做到的。

vue2 的响应式是使用 Object.defineProperty 完成的,它会对原始对象有侵入。 在创建响应式阶段,会递归遍历原始对象的所有属性,当对象属性较多、较深时,对效率的影响颇为严重。不仅如此,由于遍历属性仅在最开始完成,因此在这儿之后无法响应属性的新增和删除。 在收集依赖时,vue2 采取的是构造函数的办法,构造函数是一个整体,不利于 tree shaking。

vue3 的响应式是使用 Proxy 完成的,它不会侵入原始对象,而是返回一个代理对象,通过操作代理对象完成响应式。 由于使用了代理对象,因此并不需要遍历原始对象的属性,只需在读取属性时动态的决定要不要继续返回一个代理,这种按需加载的模式可以完全无视对象属性的数量和深度,达到更高的执行效率。 由于 ES6 的 Proxy 可以代理更加底层的操作,因此对属性的新增、删除都可以完美响应。 在收集依赖时,vue3 采取的是普通函数的做法,利用高效率的 WeakMap 实现依赖记录,这利于 tree shaking,从而降低打包体积。

v-model

vue2比较让人诟病的一点就是提供了两种双向绑定:v-model.sync,在vue3中,去掉了.sync修饰符,只需要使用v-model进行双向绑定即可。

为了让v-model更好的针对多个属性进行双向绑定,vue3作出了以下修改

  • 当对自定义组件使用v-model指令时,绑定的属性名由原来的value变为modelValue,事件名由原来的input变为update:modelValue
html
<!-- vue2 -->
+<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
+value绑定一个数据,input的时候给这个数据重新赋值
+<!-- 简写为 -->
+<ChildComponent v-model="pageTitle" />
+
+<!-- vue3 -->
+<ChildComponent
+  :modelValue="pageTitle"
+  @update:modelValue="pageTitle = $event"
+/>
+<!-- 简写为 -->
+<ChildComponent v-model="pageTitle" />
+
<!-- vue2 -->
+<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
+value绑定一个数据,input的时候给这个数据重新赋值
+<!-- 简写为 -->
+<ChildComponent v-model="pageTitle" />
+
+<!-- vue3 -->
+<ChildComponent
+  :modelValue="pageTitle"
+  @update:modelValue="pageTitle = $event"
+/>
+<!-- 简写为 -->
+<ChildComponent v-model="pageTitle" />
+
  • 去掉了.sync修饰符,它原本的功能由v-model的参数替代
html
<!-- vue2 -->
+<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
+<!-- 简写为 -->
+<ChildComponent :title.sync="pageTitle" />
+
+<!-- vue3 -->
+<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
+<!-- 简写为 -->
+<ChildComponent v-model:title="pageTitle" />
+
<!-- vue2 -->
+<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
+<!-- 简写为 -->
+<ChildComponent :title.sync="pageTitle" />
+
+<!-- vue3 -->
+<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
+<!-- 简写为 -->
+<ChildComponent v-model:title="pageTitle" />
+
  • model配置被移除
  • 允许自定义v-model修饰符 vue2 无此功能

举例:trim

v-if v-for

v-if 的优先级 现在高于 v-for

key

  • 当使用<template>进行v-for循环时,需要把key值放到<template>中,而不是它的子元素中
  • 当使用v-if v-else-if v-else分支的时候,不再需要指定key值,因为vue3会自动给予每个分支一个唯一的key 即便要手工给予key值,也必须给予每个分支唯一的key不能因为要重用分支而给予相同的 key

Fragment

vue3现在允许组件出现多个根节点

组件的变化

Teleport

asyncComponent 异步组件

CompositionApi

composition api 相比于 option api 有哪些优势?

不同于 reactivity api,composition api 提供的函数很多是与组件深度绑定的,不能脱离组件而存在。

setup

javascript
// component
+export default {
+  setup(props, context) {
+    // 该函数在组件属性被赋值后立即执行,早于所有生命周期钩子函数,只运行一次
+    // props 是一个对象,包含了所有的组件属性值
+    // context 是一个对象,提供了组件所需的上下文信息
+  },
+};
+
// component
+export default {
+  setup(props, context) {
+    // 该函数在组件属性被赋值后立即执行,早于所有生命周期钩子函数,只运行一次
+    // props 是一个对象,包含了所有的组件属性值
+    // context 是一个对象,提供了组件所需的上下文信息
+  },
+};
+

context 对象的成员

成员类型说明
attrs对象vue2this.$attrs
slots对象vue2this.$slots
emit方法vue2this.$emit

生命周期函数

vue2 option apivue3 option apivue 3 composition api
beforeCreatebeforeCreate不再需要,代码可直接置于 setup 中
createdcreated不再需要,代码可直接置于 setup 中
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroy改 beforeUnmountonBeforeUnmount
destroyed改 unmountedonUnmounted
errorCapturederrorCapturedonErrorCaptured
-新 renderTrackedonRenderTracked
-新 renderTriggeredonRenderTriggered

beforeCreate 和 created 去除?在这两个后,已经有了数据响应式,而在 composition api 中,响应式已经被抽离出去(见上节) 新增钩子函数说明:

钩子函数参数执行时机
renderTrackedDebuggerEvent渲染 vdom 收集到的每一次依赖时
renderTriggeredDebuggerEvent某个依赖变化导致组件重新渲染时

DebuggerEvent:

  • target: 跟踪或触发渲染的对象
  • key: 跟踪或触发渲染的属性
  • type: 跟踪或触发渲染的方式

TIP

composition api 相比于 option api 有哪些优势?

  1. 为了更好的逻辑复用(粒度更细了,组件间相同的功能逻辑复用)和代码组织
  2. 更好的类型推导(利于 TypeScript 类型推导)

有了 composition api,配合 reactivity api,可以在组件内部进行更加细粒度的控制,使得组件中不同的功能高度聚合,提升了代码的可维护性。对于不同组件的相同功能,也能够更好的复用。 相比于 option api,composition api 中没有了指向奇怪的 this,所有的 api 变得更加函数式,这有利于和类型推断系统比如 TS 深度配合。

https://vuejs.org/guide/built-ins/suspense.html

共享数据

vuex 方案

安装vuex@4.x

两个重要变动:

  • 去掉了构造函数Vuex,而使用createStore创建仓库
  • 为了配合composition api,新增useStore函数获得仓库对象

global state

由于vue3的响应式系统本身可以脱离组件而存在,因此可以充分利用这一点,轻松制造多个全局响应式数据

javascript
// store/useLoginUser 提供当前登录用户的共享数据
+// 以下代码仅供参考
+import { reactive, readonly } from "vue";
+import * as userServ from "../api/user"; // 导入api模块
+// 创建默认的全局单例响应式数据,仅供该模块内部使用
+const state = reactive({ user: null, loading: false });
+// 对外暴露的数据是只读的,不能直接修改
+// 也可以进一步使用toRefs进行封装,从而避免解构或展开后响应式丢失
+export const loginUserStore = readonly(state);
+
+// 登录
+export async function login(loginId, loginPwd) {
+  state.loading = true;
+  const user = await userServ.login(loginId, loginPwd);
+  state.loginUser = user;
+  state.loading = false;
+}
+// 退出
+export async function loginOut() {
+  state.loading = true;
+  await userServ.loginOut();
+  state.loading = false;
+  state.loginUser = null;
+}
+// 恢复登录状态
+export async function whoAmI() {
+  state.loading = true;
+  const user = await userServ.whoAmI();
+  state.loading = false;
+  state.loginUser = user;
+}
+
// store/useLoginUser 提供当前登录用户的共享数据
+// 以下代码仅供参考
+import { reactive, readonly } from "vue";
+import * as userServ from "../api/user"; // 导入api模块
+// 创建默认的全局单例响应式数据,仅供该模块内部使用
+const state = reactive({ user: null, loading: false });
+// 对外暴露的数据是只读的,不能直接修改
+// 也可以进一步使用toRefs进行封装,从而避免解构或展开后响应式丢失
+export const loginUserStore = readonly(state);
+
+// 登录
+export async function login(loginId, loginPwd) {
+  state.loading = true;
+  const user = await userServ.login(loginId, loginPwd);
+  state.loginUser = user;
+  state.loading = false;
+}
+// 退出
+export async function loginOut() {
+  state.loading = true;
+  await userServ.loginOut();
+  state.loading = false;
+  state.loginUser = null;
+}
+// 恢复登录状态
+export async function whoAmI() {
+  state.loading = true;
+  const user = await userServ.whoAmI();
+  state.loading = false;
+  state.loginUser = user;
+}
+

Provide&Inject

vue2中,提供了provideinject配置,可以让开发者在高层组件中注入数据,然后在后代组件中使用

除了兼容vue2的配置式注入,vue3composition api中添加了provideinject方法,可以在setup函数中注入和使用数据

考虑到有些数据需要在整个 vue 应用中使用,vue3还在应用实例中加入了provide方法,用于提供整个应用的共享数据

javascript
creaetApp(App).provide("foo", ref(1)).provide("bar", ref(2)).mount("#app");
+
creaetApp(App).provide("foo", ref(1)).provide("bar", ref(2)).mount("#app");
+

因此,我们可以利用这一点,在整个 vue 应用中提供共享数据

javascript
// store/useLoginUser 提供当前登录用户的共享数据
+// 以下代码仅供参考
+import { readonly, reactive, inject } from "vue";
+const key = Symbol(); // Provide的key
+
+// 在传入的vue应用实例中提供数据
+export function provideStore(app) {
+  // 创建默认的响应式数据
+  const state = reactive({ user: null, loading: false });
+  // 登录
+  async function login(loginId, loginPwd) {
+    state.loading = true;
+    const user = await userServ.login(loginId, loginPwd);
+    state.loginUser = user;
+    state.loading = false;
+  }
+  // 退出
+  async function loginOut() {
+    state.loading = true;
+    await userServ.loginOut();
+    state.loading = false;
+    state.loginUser = null;
+  }
+  // 恢复登录状态
+  async function whoAmI() {
+    state.loading = true;
+    const user = await userServ.whoAmI();
+    state.loading = false;
+    state.loginUser = user;
+  }
+  // 提供全局数据
+  app.provide(key, {
+    state: readonly(state), // 对外只读
+    login,
+    loginOut,
+    whoAmI,
+  });
+}
+
+export function useStore(defaultValue = null) {
+  return inject(key, defaultValue);
+}
+
+// store/index
+// 应用所有store
+import { provideStore as provideLoginUserStore } from "./useLoginUser";
+// 继续导入其他共享数据模块...
+// import { provideStore as provideNewsStore } from "./useNews"
+
+// 提供统一的数据注入接口
+export default function provideStore(app) {
+  provideLoginUserStore(app);
+  // 继续注入其他共享数据
+  // provideNewsStore(app);
+}
+
+// main.js
+import { createApp } from "vue";
+import provideStore from "./store";
+const app = createApp(App);
+provideStore(app);
+app.mount("#app");
+
// store/useLoginUser 提供当前登录用户的共享数据
+// 以下代码仅供参考
+import { readonly, reactive, inject } from "vue";
+const key = Symbol(); // Provide的key
+
+// 在传入的vue应用实例中提供数据
+export function provideStore(app) {
+  // 创建默认的响应式数据
+  const state = reactive({ user: null, loading: false });
+  // 登录
+  async function login(loginId, loginPwd) {
+    state.loading = true;
+    const user = await userServ.login(loginId, loginPwd);
+    state.loginUser = user;
+    state.loading = false;
+  }
+  // 退出
+  async function loginOut() {
+    state.loading = true;
+    await userServ.loginOut();
+    state.loading = false;
+    state.loginUser = null;
+  }
+  // 恢复登录状态
+  async function whoAmI() {
+    state.loading = true;
+    const user = await userServ.whoAmI();
+    state.loading = false;
+    state.loginUser = user;
+  }
+  // 提供全局数据
+  app.provide(key, {
+    state: readonly(state), // 对外只读
+    login,
+    loginOut,
+    whoAmI,
+  });
+}
+
+export function useStore(defaultValue = null) {
+  return inject(key, defaultValue);
+}
+
+// store/index
+// 应用所有store
+import { provideStore as provideLoginUserStore } from "./useLoginUser";
+// 继续导入其他共享数据模块...
+// import { provideStore as provideNewsStore } from "./useNews"
+
+// 提供统一的数据注入接口
+export default function provideStore(app) {
+  provideLoginUserStore(app);
+  // 继续注入其他共享数据
+  // provideNewsStore(app);
+}
+
+// main.js
+import { createApp } from "vue";
+import provideStore from "./store";
+const app = createApp(App);
+provideStore(app);
+app.mount("#app");
+

对比

vuexglobal stateProvide&Inject
组件数据共享
可否脱离组件❌ 和 vue 应用/组件深入绑定
调试工具
状态树自行决定自行决定
量级

script setup

script setup的提案文档:https://github.com/vuejs/rfcs/blob/script-setup-2/active-rfcs/0000-script-setup.md

vue
<template>
+  <hello-world></hello-world>
+  <p>the number is {{ count }}</p>
+  <button @click="increase">click to increase</button>
+</template>
+
+<script>
+import HelloWorld from "./HelloWorld.vue";
+import { ref } from "vue";
+export default {
+  setup() {
+    const count = ref(0);
+    const increase = () => {
+      count.value++;
+    };
+    return {
+      HelloWorld,
+      count,
+      increase,
+    };
+  },
+};
+</script>
+
<template>
+  <hello-world></hello-world>
+  <p>the number is {{ count }}</p>
+  <button @click="increase">click to increase</button>
+</template>
+
+<script>
+import HelloWorld from "./HelloWorld.vue";
+import { ref } from "vue";
+export default {
+  setup() {
+    const count = ref(0);
+    const increase = () => {
+      count.value++;
+    };
+    return {
+      HelloWorld,
+      count,
+      increase,
+    };
+  },
+};
+</script>
+

当我们使用composition api时,往往整个组件对象的配置中只有一个setup

因此,vue3SFC中使用script setup来简化代码书写

vue
<template>
+  <hello-world></hello-world>
+  <p>the number is {{ count }}</p>
+  <button @click="increase">click to increase</button>
+</template>
+
+<script setup>
+import HelloWorld from "./HelloWorld.vue";
+import { ref } from "vue";
+const count = ref(0);
+const increase = () => {
+  count.value++;
+};
+</script>
+
<template>
+  <hello-world></hello-world>
+  <p>the number is {{ count }}</p>
+  <button @click="increase">click to increase</button>
+</template>
+
+<script setup>
+import HelloWorld from "./HelloWorld.vue";
+import { ref } from "vue";
+const count = ref(0);
+const increase = () => {
+  count.value++;
+};
+</script>
+

script setup中,所有的顶级绑定都会被使用setup的返回值,顶级绑定包括:

  • import导入
  • 顶级变量

如何使用组件属性

vue
<template>
+  <h1>Hello {{ props.title }}</h1>
+</template>
+
+<script setup>
+import { defineProps } from "vue";
+const props = defineProps(["title"]);
+</script>
+
<template>
+  <h1>Hello {{ props.title }}</h1>
+</template>
+
+<script setup>
+import { defineProps } from "vue";
+const props = defineProps(["title"]);
+</script>
+

编译结果

vue
<template>
+  <h1>Hello {{ props.title }}</h1>
+</template>
+
+<script>
+export default {
+  props: ["title"],
+  setup() {},
+};
+</script>
+
<template>
+  <h1>Hello {{ props.title }}</h1>
+</template>
+
+<script>
+export default {
+  props: ["title"],
+  setup() {},
+};
+</script>
+

如何使用组件事件

vue
<template>
+  <button @click="handleClick">Click Me</button>
+</template>
+
+<script setup>
+import { defineEmit } from "vue";
+const emit = defineEmit(["click"]);
+const handleClick = () => {
+  emit("click");
+};
+</script>
+
<template>
+  <button @click="handleClick">Click Me</button>
+</template>
+
+<script setup>
+import { defineEmit } from "vue";
+const emit = defineEmit(["click"]);
+const handleClick = () => {
+  emit("click");
+};
+</script>
+

编译结果:

vue
<template>
+  <button @click="handleClick">Click Me</button>
+</template>
+
+<script>
+export default {
+  emits: ["click"],
+  setup(props, { emit }) {
+    const handleClick = () => {
+      emit("click");
+    };
+    return {
+      handleClick,
+    };
+  },
+};
+</script>
+
<template>
+  <button @click="handleClick">Click Me</button>
+</template>
+
+<script>
+export default {
+  emits: ["click"],
+  setup(props, { emit }) {
+    const handleClick = () => {
+      emit("click");
+    };
+    return {
+      handleClick,
+    };
+  },
+};
+</script>
+

ref sugar

ref语法糖目前仍在提案阶段,具有一定的争议

ref sugar提案地址:https://github.com/vuejs/rfcs/blob/ref-sugar/active-rfcs/0000-ref-sugar.md

vue
<template>
+  <!-- 受模板上下文代理的影响,在模板中直接当做原始值使用 -->
+  <h1>Hello, {{ fullName }}</h1>
+  <p>设置姓名</p>
+  <p>
+    姓:
+    <input type="text" v-model="firstName" />
+  </p>
+  <p>
+    名:
+    <input type="text" v-model="lastName" />
+  </p>
+</template>
+
+<script>
+import { ref, computed } from "vue";
+export default {
+  setup() {
+    const firstName = ref("邓"); // firstName 是一个对象
+    const lastName = ref("旭明"); // lastName 是一个对象
+    // 在 setup 函数中,则必须把 ref 当做对象使用
+    const fullName = computed(() => firstName.value + lastName.value);
+    return {
+      firstName,
+      lastName,
+      fullName,
+    };
+  },
+};
+</script>
+
<template>
+  <!-- 受模板上下文代理的影响,在模板中直接当做原始值使用 -->
+  <h1>Hello, {{ fullName }}</h1>
+  <p>设置姓名</p>
+  <p>
+    姓:
+    <input type="text" v-model="firstName" />
+  </p>
+  <p>
+    名:
+    <input type="text" v-model="lastName" />
+  </p>
+</template>
+
+<script>
+import { ref, computed } from "vue";
+export default {
+  setup() {
+    const firstName = ref("邓"); // firstName 是一个对象
+    const lastName = ref("旭明"); // lastName 是一个对象
+    // 在 setup 函数中,则必须把 ref 当做对象使用
+    const fullName = computed(() => firstName.value + lastName.value);
+    return {
+      firstName,
+      lastName,
+      fullName,
+    };
+  },
+};
+</script>
+

使用ref时,主要有以下心智负担:

  • 模板和setup中对待ref不一致
  • refcomputedreactive带来的响应数据结构不一致

为解决以上问题,vue3SFC中提出ref sugar方案,它是在script setup方案上进一步的演化

上述代码可以用ref sugar简化为

vue
<template>
+  <!-- 使用ref标记的数据直接使用即可 -->
+  <h1>Hello, {{ fullName }}</h1>
+  <p>设置姓名</p>
+  <p>
+    姓:
+    <input type="text" v-model="firstName" />
+  </p>
+  <p>
+    名:
+    <input type="text" v-model="lastName" />
+  </p>
+</template>
+
+<script setup>
+import { computed } from "vue";
+ref: firstName = "邓"; // 这是一个ref
+ref: lastName = "旭明"; // 这是一个ref
+// 使用ref标记的数据直接使用即可
+const fullName = computed(() => firstName + lastName);
+</script>
+
<template>
+  <!-- 使用ref标记的数据直接使用即可 -->
+  <h1>Hello, {{ fullName }}</h1>
+  <p>设置姓名</p>
+  <p>
+    姓:
+    <input type="text" v-model="firstName" />
+  </p>
+  <p>
+    名:
+    <input type="text" v-model="lastName" />
+  </p>
+</template>
+
+<script setup>
+import { computed } from "vue";
+ref: firstName = "邓"; // 这是一个ref
+ref: lastName = "旭明"; // 这是一个ref
+// 使用ref标记的数据直接使用即可
+const fullName = computed(() => firstName + lastName);
+</script>
+

上述js部分代码会被编译为

vue
<script setup>
+import { computed, ref } from "vue";
+const firstName = "邓";
+const lastName = "旭明";
+const fullName = computed(() => firstName.value + lastName.value);
+</script>
+
<script setup>
+import { computed, ref } from "vue";
+const firstName = "邓";
+const lastName = "旭明";
+const fullName = computed(() => firstName.value + lastName.value);
+</script>
+

争议

  • ref sugar带来了新的js语义,并且同TypeScript语义冲突
  • ref sugar仅在script setup中有效,出了SFC环境就会失效,心智负担不减反增
  • ref sugar虽然降低了初学者的使用成本,但却增加了理解成本
  • 当使用TypeScript的时候,可以从编辑器中获得智能提示,可以很轻松的辨别一个数据是否是ref,根本没必要解决这个问题,否则会让事情变得越来越复杂。

Suspence

UserName组件的setup函数是异步的,只有等到它的setup函数加载完成后才能够显示模板

vue
<template>
+  <h1>Hello {{ name }}</h1>
+</template>
+
+<script>
+function delay(duration = 1000) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, duration);
+  });
+}
+export default {
+  async setup() {
+    await delay();
+    return {
+      name: "邓哥",
+    };
+  },
+};
+</script>
+
<template>
+  <h1>Hello {{ name }}</h1>
+</template>
+
+<script>
+function delay(duration = 1000) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, duration);
+  });
+}
+export default {
+  async setup() {
+    await delay();
+    return {
+      name: "邓哥",
+    };
+  },
+};
+</script>
+

App组件使用了UserName组件,并使用了Suspense组件等待UserName组件渲染完成。

vue
<template>
+  <Suspense>
+    <template #default>
+      <UserName />
+    </template>
+    <template #fallback>
+      <h1>Loading...</h1>
+    </template>
+  </Suspense>
+</template>
+
+<script>
+import UserName from "./UserName.vue";
+export default {
+  components: {
+    UserName,
+  },
+};
+</script>
+
<template>
+  <Suspense>
+    <template #default>
+      <UserName />
+    </template>
+    <template #fallback>
+      <h1>Loading...</h1>
+    </template>
+  </Suspense>
+</template>
+
+<script>
+import UserName from "./UserName.vue";
+export default {
+  components: {
+    UserName,
+  },
+};
+</script>
+

TypeScript

vue3TypeScript支持良好,无论使用vue-cli还是vite,都可以达到开箱即用的效果。

使用vite的时候,由于没有针对.vue文件的类型定义,需要手动定义,以避免编辑器报错

自定义渲染器

vue3有着清晰的模块结构,特别是它将dom操作进行了分离,使得vue3的核心模块与dom操作脱钩,这样一来,就非常便于我们替换成其他的渲染模式。

hook 范式

vue
<template>
+  <h1>count:{{ countRef }}</h1>
+  <p>
+    <button @click="decrease">decrease</button>
+    <button @click="increase">increase</button>
+  </p>
+</template>
+
+<script>
+import { ref } from "vue";
+
+function useCount() {
+  let countRef = ref(0); //具有响应式了
+  // console.log(countRef);
+  const increase = () => {
+    countRef.value++;
+  };
+  const decrease = () => {
+    countRef.value--;
+  };
+  return {
+    countRef,
+    increase,
+    decrease,
+  };
+}
+
+export default {
+  setup() {
+    // console.log("所有生命周期钩子函数之前自动调用");
+    // console.log(this); // this -> undefined
+
+    // setup中,count是一个对象
+    // 实例代理中,count是一个count.value
+
+    //1. 新增
+
+    //2. 修改
+
+    //3. 删除
+    return {
+      ...useCount(),
+    };
+  },
+};
+</script>
+
<template>
+  <h1>count:{{ countRef }}</h1>
+  <p>
+    <button @click="decrease">decrease</button>
+    <button @click="increase">increase</button>
+  </p>
+</template>
+
+<script>
+import { ref } from "vue";
+
+function useCount() {
+  let countRef = ref(0); //具有响应式了
+  // console.log(countRef);
+  const increase = () => {
+    countRef.value++;
+  };
+  const decrease = () => {
+    countRef.value--;
+  };
+  return {
+    countRef,
+    increase,
+    decrease,
+  };
+}
+
+export default {
+  setup() {
+    // console.log("所有生命周期钩子函数之前自动调用");
+    // console.log(this); // this -> undefined
+
+    // setup中,count是一个对象
+    // 实例代理中,count是一个count.value
+
+    //1. 新增
+
+    //2. 修改
+
+    //3. 删除
+    return {
+      ...useCount(),
+    };
+  },
+};
+</script>
+

说一下 vue3.0 是如何变得更快的?

优化 Diff 算法

相比 Vue 2Vue 3 采用了更加优化的渲染策略。去掉不必要的虚拟 DOM 树遍历和属性比较,因为这在更新期间往往会产生最大的性能开销。

这里有三个主要的优化:

  • 首先,在 DOM 树级别。

在没有动态改变节点结构的模板指令(例如 v-ifv-for)的情况下,节点结构保持完全静态。

当更新节点时,不再需要递归遍历 DOM 树。所有的动态绑定部分将在一个平面数组中跟踪。这种优化通过将需要执行的树遍历量减少一个数量级来规避虚拟 DOM 的大部分开销。

  • 其次,编译器积极地检测模板中的静态节点、子树甚至数据对象,并在生成的代码中将它们提升到渲染函数之外。这样可以避免在每次渲染时重新创建这些对象,从而大大提高内存使用率并减少垃圾回收的频率。
  • 第三,在元素级别。

编译器还根据需要执行的更新类型,为每个具有动态绑定的元素生成一个优化标志。

例如,具有动态类绑定和许多静态属性的元素将收到一个标志,提示只需要进行类检查。运行时将获取这些提示并采用专用的快速路径。

综合起来,这些技术大大改进了渲染更新基准,Vue 3.0 有时占用的 CPU 时间不到 Vue 2 的十分之一。

体积变小

重写后的 Vue 支持了 tree-shaking,像修剪树叶一样把不需要的东西给修剪掉,使 Vue 3.0 的体积更小。

需要的模块才会打入到包里,优化后的 Vue 3.0 的打包体积只有原来的一半(13kb)。哪怕把所有的功能都引入进来也只有 23kb,依然比 Vue 2.x 更小。像 keep-alive、transition 甚至 v-for 等功能都可以按需引入。

并且 Vue 3.0 优化了打包方法,使得打包后的 bundle 的体积也更小。

官方所给出的一份惊艳的数据:打包大小减少 41%,初次渲染快 55%,更新快 133%,内存使用减少 54%

说一说相比 vue3.x 对比 vue2.x 变化

  1. 源码组织方式变化:使用 TS 重写
  2. 支持 Composition API:基于函数的 API,更加灵活组织组件逻辑(vue2 用的是 options api)
  3. 响应式系统提升:Vue3 中响应式数据原理改成 proxy,可监听动态新增删除属性,以及数组变化
  4. 编译优化:vue2 通过标记静态根节点优化 diff,Vue3 标记和提升所有静态根节点,diff 的时候只需要对比动态节点内容
  5. 打包体积优化:移除了一些不常用的 api(inline-template、filter)
  6. 生命周期的变化:使用 setup 代替了之前的 beforeCreate 和 created
  7. Vue3 的 template 模板支持多个根标签
  8. Vuex 状态管理:创建实例的方式改变,Vue2 为 new Store , Vue3 为 createStore
  9. Route 获取页面实例与路由信息:vue2 通过 this 获取 router 实例,vue3 通过使用 getCurrentInstance/ userRoute 和 userRouter 方法获取当前组件实例
  10. Props 的使用变化:vue2 通过 this 获取 props 里面的内容,vue3 直接通过 props
  11. 父子组件传值:vue3 在向父组件传回数据时,如使用的自定义名称,如 backData,则需要在 emits 中定义一下
+ + + + + \ No newline at end of file diff --git a/vue/vuex.html b/vue/vuex.html new file mode 100644 index 00000000..140b9753 --- /dev/null +++ b/vue/vuex.html @@ -0,0 +1,80 @@ + + + + + + Vuex 思想和源码 | Sunny's blog + + + + + + + + +
Skip to content
On this page

Vuex 思想和源码

源码实现

先看我写的 mini-vuex,后续会补充文章

可以结合 mini-pinia 一起食用

Vuex 的原理

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。

  • Vuex 为 Vue Components 建立起了一个完整的生态圈,包括开发中的 API 调用一环。

    (1)核心流程中的主要功能:

  • Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;

  • 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;

  • 然后 Mutations 就去改变(Mutate)State 中的数据;

  • 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。

(2)各模块在核心流程中的主要功能:

  • Vue Components∶ Vue 组件。HTML 页面上,负责接收用户操作等交互行为,执行 dispatch 方法触发对应 action 进行回应。
  • dispatch∶ 操作行为触发方法,是唯一能执行 action 的方法。
  • actions∶ 操作行为处理模块。负责处理 Vue Components 接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台 API 请求的操作就在这个模块中进行,包括触发其他 action 以及提交 mutation 的操作。该模块提供了 Promise 的封装,以支持 action 的链式触发。
  • commit∶ 状态改变提交操作方法。对 mutation 进行提交,是唯一能执行 mutation 的方法。
  • mutations∶ 状态改变操作方法。是 Vuex 修改 state 的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些 hook 暴露出来,以进行 state 的监控等。
  • state∶ 页面状态管理容器对象。集中存储 Vuecomponents 中 data 对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用 Vue 的细粒度数据响应机制来进行高效的状态更新。
  • getters∶ state 对象读取方法。图中没有单独列出该模块,应该被包含在了 render 中,Vue Components 通过该方法读取全局 state 对象。

Vuex 中 action 和 mutation 的区别

mutation 中的操作是一系列的同步函数,用于修改 state 中的变量的的状态。当使用 vuex 时需要通过 commit 来提交需要操作的内容。mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

javascript
const store = new Vuex.Store({
+  state: {
+    count: 1,
+  },
+  mutations: {
+    increment(state) {
+      state.count++; // 变更状态
+    },
+  },
+});
+
const store = new Vuex.Store({
+  state: {
+    count: 1,
+  },
+  mutations: {
+    increment(state) {
+      state.count++; // 变更状态
+    },
+  },
+});
+

当触发一个类型为 increment 的 mutation 时,需要调用此函数:

javascript
store.commit("increment");
+
store.commit("increment");
+

而 Action 类似于 mutation,不同点在于:

  • Action 可以包含任意异步操作。
  • Action 提交的是 mutation,而不是直接变更状态。
javascript
const store = new Vuex.Store({
+  state: {
+    count: 0,
+  },
+  mutations: {
+    increment(state) {
+      state.count++;
+    },
+  },
+  actions: {
+    increment(context) {
+      context.commit("increment");
+    },
+  },
+});
+
const store = new Vuex.Store({
+  state: {
+    count: 0,
+  },
+  mutations: {
+    increment(state) {
+      state.count++;
+    },
+  },
+  actions: {
+    increment(context) {
+      context.commit("increment");
+    },
+  },
+});
+

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。 所以,两者的不同点如下:

  • Mutation 专注于修改 State,理论上是修改 State 的唯一途径;Action 业务代码、异步请求。
  • Mutation:必须同步执行;Action:可以异步,但不能直接操作 State。
  • 在视图更新时,先触发 actions,actions 再触发 mutation
  • mutation 的参数是 state,它包含 store 中的数据;store 的参数是 context,它是 state 的父级,包含 state、getters

Vuex 和 localStorage 的区别

(1)最重要的区别

  • vuex 存储在内存中
  • localstorage 则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要 JSON 的 stringify 和 parse 方法进行处理。 读取内存比读取硬盘速度要快

(2)应用场景

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex 用于组件之间的传值。
  • localstorage 是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用 。
  • Vuex 能做到数据的响应式,localstorage 不能

(3)永久性 刷新页面时 vuex 存储的值会丢失,localstorage 不会。

注意: 对于不变的数据确实可以用 localstorage 可以代替 vuex,但是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件改变了该数据源,希望另一个组件响应该变化时,localstorage 无法做到,原因就是区别

Redux 和 Vuex 有什么区别,它们的共同思想

(1)Redux 和 Vuex 区别

  • Vuex 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里改变 state 值即可
  • Vuex 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的 State 即可
  • Vuex 数据流的顺序是 ∶View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自动渲染)

通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;

(2)共同思想

  • 单—的数据源
  • 变化可以预测

本质上:redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案; 形式上:vuex 借鉴了 redux,将 store 作为全局的数据中心,进行 mode 管理;

为什么要用 Vuex 或者 Redux

由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。

所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。

另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。

Vuex 和单纯的全局对象有什么区别?

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。

为什么 Vuex 的 mutation 中不能做异步操作?

  • Vuex 中所有的状态更新的唯一途径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。
  • 每个 mutation 执行完成后都会对应到一个新的状态变更,这样 devtools 就可以打个快照存下来,然后就可以实现 time-travel 了。如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。

Vuex 的严格模式是什么,有什么作用,如何开启?

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

在 Vuex.Store 构造器选项中开启,如下

const store = new Vuex.Store({
+    strict:true,
+})
+
+
const store = new Vuex.Store({
+    strict:true,
+})
+
+

如何在组件中批量使用 Vuex

使用 mapGetters 辅助函数, 利用对象展开运算符将 getter 混入 computed 对象中 使用 mapMutations 辅助函数,在组件中这么使用

+ + + + + \ No newline at end of file