--- url: 'https://zhaobc.site/about.md' description: 关于赵斌 --- # 关于我 ## 个人介绍 网络用名:`zhbchwin`,`UJN-Bin`。~~`FuckDoctors`(确实痛恨无良医生,请见谅,请忽略)~~ 真实名称`Zhao Binchen`,所以才用了此域名[zhaobc.site](https://www.zhaobc.site)。 ## 兴趣爱好 没啥兴趣爱好,喜欢瞎折腾(ㄒoㄒ) ## 联系方式 * 邮件: * GitHub: [FuckDoctors](https://github.com/FuckDoctors) --- --- url: 'https://zhaobc.site/links.md' description: 链接 --- # 链接 ## 工具 ## 友情链接 --- --- url: 'https://zhaobc.site/notes/index.md' --- # 笔记 ## 初衷 > 纸上得来终觉浅,绝知此事要躬行。 有些事情看过,感觉自己知道,但是要用的时候又不记得了,所以,学习不能只看,还要动手,加深印象。 本仓库的目的是记录学习的东西,边学边动手,照着学习资料手抄一下,不用全抄,记录重点内容即可。 大体分为前端和后端,主要有一下内容。(感觉会以前端为主,因为感兴趣) 注:内容可能随时更新。 ## [前端](frontend/) 主要是基础的 JavaScript, CSS3,TypeScript 以及前端框架,比如 Vue。 目录如下: * JavaScript * [JavaScript 基础教程](frontend/js/) * CSS3 * 待定 * [TypeScript](frontend/ts/) * TypeScript 官方资料 * [Vue](frontend/vue/) * Vue 官方资料 ## [后端](backend/) 主要是 Java,.Net 以及后台框架,比如 Spring,SpringBoot 等。 目录结构如下: * [Java](backend/java/) * 待定 * .Net * 待定 * [Spring](backend/spring/) * 待定 * SpringBoot * 待定 * [Python](backend/python/) * [Python 官方教程](https://docs.python.org/zh-cn/3/tutorial/index.html) * [Python 100 天从新手到大师](https://github.com/jackfrued/Python-100-Days) ## [其他](other/) 其他内容 --- --- url: 'https://zhaobc.site/notes/backend/index.md' --- # 后端笔记 记录后端相关的学习笔记 --- --- url: 'https://zhaobc.site/notes/backend/java/index.md' --- # Java 笔记 Java 相关的学习笔记。 --- --- url: 'https://zhaobc.site/notes/backend/java/code-snippets/dbunit.md' --- # DBUnit 相关代码片段 ## 根据不同连接创建不同的数据库连接 ```java private IDatabaseConnection createDatabaseConnection(Connection conn, String schema) { IDatabaseConnection dbUnitConn = new DatabaseConnection(conn, schema); DatabaseConfig config = dbUnitConn.getConfig(); config.setProperty(DatabaseConfig.PROPERTY_ESCAPE_PATTERN, "\"?\""); config.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FILEDS, true); String dbName = conn.getMetaData().getDatabaseProductName().toLowerCase(); if (dbName.contains("oracle")) { // 注意这里没有使用 OracleDataTypeFactory, 而是自己的 MyOracleDataTypeFactory config.setProperty(DatabaseConfig.PROPERTY_DATETYPE_FACTORY, new MyOracleDataTypeFactory()); } else if (dbName.contains("mysql")) { config.setProperty(DatabaseConfig.PROPERTY_DATETYPE_FACTORY, new MySqlDataTypeFactory()); config.setProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER, new MySqlMetadataHandler()); } } ``` ## 自定义数据类型 如果想做特殊处理的话,自定义 `DataType`, 然后注册到 `DataTypeFactory`。 比如,对于 Oracle 数据库的 `CHAR` 类型,预期值跟实际值比较时,忽略空格后比较。 可以自定义 DataType 来实现。 ### 自定义字符串数据类型 ```java public class StringDataTypeIgnoreSpaceDataType extends StringDataType { public StringDataTypeIgnoreSpaceDataType(String name, int sqlType) { super(name, sqlType); } @Override protected int compareNonNulls(Object value1, Object value2) throws TypeCastException { String val1 = (String) value1; String val2 = (String) value2; return val1.strip().compareTo(val2.strip()); } } ``` ### 注册到数据类型工厂 ```java public class MyOracleDataTypeFactory extends OracleDataTypeFactory { // 第二个参数的 sqlType 跟原来的 sqlType 保持一致 public static final DataType MY_CHAR = new StringDataTypeIgnoreSpaceDataType("CHAR", 1); public static final DataType MY_NCHAR = new StringDataTypeIgnoreSpaceDataType("NCHAR", -15); @Override public DataType createDataType(int sqlType, String sqlTypeName) throws DataTypeException { if ("CHAR".equals(sqlTypeName)) { return MY_CHAR; } else { return super.createDataType(sqlType, sqlTypeName); } } } ``` --- --- url: 'https://zhaobc.site/notes/backend/java/code-snippets/group-by.md' --- # Group by 使用 ## 多个字段的 Group By ```java list.stream() .filter(item -> item.getDate() <= curDate) .collect(Collectors.groupingBy(item -> item.getClass() + "_" + item.getName(), Collectors.maxBy(Comparator.comparing(ADto::getScore))) .values() .stream() .collect(Collectors.toList()) .stream() .filter(item -> item.isPresent()) .map(item -> item.get()) .collect(Collectors.toList()); ``` ```java Map> groupedMap = list.stream() .collect(Collectors.groupingBy(a -> a.getClass() + "," + a.getName() + "," + a.getType()), LinkedHashMap::new, Collectors.toList())); ``` --- --- url: 'https://zhaobc.site/notes/backend/java/code-snippets/mockito.md' --- # Mockito 相关代码片段 ## 参数验证 JUnit 测试时,对应 Mocked 方法,我们可以返回自己想要的返回值或异常,但是有时希望验证一下,我们调用时传的参数是否正确。 此时,可以使用 ArgumentCaptor 来收集参数,进而做验证。 示例: ```java ArgumentCaptor argCaptor = ArgumentCaptor.forClass(TestIn.class); // 调用 (注意,这里指定了类型,不指定的话有些时候不能正确执行,比如, dao.find(any()) 就不知实际该匹配哪个,可能返回 null) testService.doMethod(any(TestIn.class)).thenReturn(1); // 参数收集 verify(testService).doMethod(argCaptor.captor()); // 参数校验 assertEquals("0", argCaptor.getValue().getInArg()); ``` 多次调用时的验证: ```java ArgumentCaptor argCaptor = ArgumentCaptor.forClass(TestIn.class); // 参数收集 verify(testService, times(2)).doMethod(argCaptor.captor()); List inValues = argCaptor.getAllValues(); // 参数校验 // 第一次调用的参数验证 assertEquals("0", inValues.get(0).getInArg()); // 第二次调用的参数验证 assertEquals("1", inValues.get(1).getInArg()); ``` ## 编程式返回期待值 ```java when(testService.doMethod(any())).thenAnswer(inv -> { TestIn in = inv.getArgument(0, TestIn.class); TestOut out = new TestOut(); out.setA(in.getA()); if ("".equals(in.getB())) { out.setOb("1"); } return out; }); when(testService.doMethod2(eq("1"), any())).thenReturn("0"); ``` ## 参数为对象时,返回 null (无法正确匹配) 当参数为对象类型时,为了能区分不太的参数,返回不同的内容,需要自定义参数匹配来实现。 单纯的用 `any(InParam.class)` 是无法实现的。 自定义参数匹配示例: ```java public class MessageMatcher implements ArgumentMatcher { private Message left; // constructors @Override public boolean matches(Message right) { return left.getFrom().equals(right.getFrom()) && left.getTo().equals(right.getTo()) && left.getText().equals(right.getText()) && right.getDate() != null && right.getId() != null; } } ``` 使用: ```java // 业务代码 MessageDTO messageDTO = new MessageDTO(); messageDTO.setFrom("me"); messageDTO.setTo("you"); messageDTO.setText("Hello, you!"); messageController.createMessage(messageDTO); // JUnit 代码 Message message = new Message(); message.setFrom("me"); message.setTo("you"); message.setText("Hello, you!"); verify(messageService, times(1)).deliverMessage(argThat(new MessageMatcher(message))); ``` 关于自定义参数匹配,可以参考这篇文章: [Mockito ArgumentMatchers](https://www.baeldung.com/mockito-argument-matchers)。 --- --- url: 'https://zhaobc.site/notes/backend/java/code-snippets/sort.md' --- # 排序 ## 多个字段的排序 ```java Comparator comparator = Comparator .comparing(ADto::getClass, Comparator.nullsLast(Comparator.naturalOrder())) .thenComparing(ADto::getScore, Comparator.nullsLast(Comparator.reverseOrder())) .thenComparing(ADto::getName, Comparator.nullsLast(String::compareTo)); list.sort(comparator); ``` ## 按语言排序 ```java Comparator comparator = Comparator .comparing(ADto::getClass, Comparator.nullsLast(Comparator.naturalOrder())) .thenComparing(ADto::getScore, Comparator.nullsLast(Comparator.reverseOrder())) .thenComparing(ADto::getName, Comparator.nullsLast(Collator.getInstance(Locale.SIMPLIFIED_CHINESE))); list.sort(comparator); ``` --- --- url: 'https://zhaobc.site/notes/backend/python/index.md' --- # Python --- --- url: 'https://zhaobc.site/notes/backend/python/python-100-days/index.md' --- # Python - 100 天从新手到大师 本笔记以 [Python - 100 天从新手到大师](https://github.com/jackfrued/Python-100-Days) 为学习教程,记录一些学习笔记。 --- --- url: 'https://zhaobc.site/notes/backend/python/python-100-days/day-01-15/index.md' --- # Day 01-15 --- --- url: >- https://zhaobc.site/notes/backend/python/python-100-days/day-01-15/01.初识Python.md --- # 01.初识 Python ## 安装 Python 解析器 以下以 centos 为例: ```shell # 安装所需依赖 yum -y install wget gcc zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel # 下载源代码并解压 wget https://www.python.org/ftp/python/3.7.6/Python-3.7.6.tar.xz xz -d Python-3.7.6.tar.xz tar -xvf Python-3.7.6.tar # 编译安装 cd Python-3.7.6 ./configure --prefix=/usr/local/python37 --enable-optimizations make && make install ``` ## 查看 Python 版本 ```shell python --version # 或 python3 --version ``` ::: warning Unix 系统中,为了不与同时安装的 Python 2.x 冲突,Python 3.x 解释器默认安装的执行文件名不是 python。 ::: 可以进入 python 交互环境查看版本号: ```python import sys print(sys.version_info) print(sys.version) ``` ## 调用参数 解释器读取命令行参数,把脚本名与其他参数转换为字符串列表,存到 `sys` 模块的 `argv` 变量中。 执行 `import sys`, 可以导入这个模块,并访问该列表。 该列表至少有一个元素,未指定参数时,`sys.argv[0]` 是一个空字符串。 给定脚本名是 `-` 时,`sys.argv[0]` 是 `-`。 使用 `-c command` 时,`sys.argv[0]` 是 `-c`。 如果使用选项 `-m module` 时,`sys.argv[0]` 就是包含目录的模块全名。 解释器不处理 `-c command` 或 `-m module` 之后的选项,而是直接留在 `sys.argv` 中,由命令或模块来处理。 ### `-c command` 执行 *command* 中的 Python 代码。*command* 可以是一条语句,也可以是用换行符分割的多条语句。 使用此项时,`sys.argv` 的首个元素为 `-c`,并会把当前目录加入至 `sys.path` 开头。 ### `-m module-name` 在 `sys.path` 中搜索指定的模块,并以 `__main__` 模块执行其内容。 该参数是 *模块名*,请勿输入文件扩展名(`.py`)。模块名应为有效的绝对 Python 模块名。 如果给出此选项,`sys.argv` 的首个元素将为模块文件的完整路径 (在定位模块文件期间,首个元素将设为 "-m")。 与 `-c` 选项一样,当前目录将被加入 `sys.path`的开头。 ## 编写 Python 源代码 编写 python 源代码,并使用 `.py` 作为文件后缀。 例: ```python # hello.py print('hello, world!') ``` ## 源文件的字符编码 默认情况下,Python 源码文件的编码是 UTF-8。 如果不使用默认编码时,则要声明文件的编码,文件的 *第一行* 要写成特殊的注释,如下: ```python # -*- coding: encoding -*- ``` 其中,*encoding* 就是 Python 支持的编码,比如:`gb2313`。 第一行也有例外,源代码以 `shebang` 开头时,编码声明则要写在第二行: ```python #!/usr/bin/env python3 # -*- coding: gb2313 -*- ``` 详情:[源文件的字符编码](https://docs.python.org/zh-cn/3/tutorial/interpreter.html#source-code-encoding) ## 运行 Python ```shell python hello.py # 或 python3 hello.py ``` ## 代码注释 1. 单行注释 以#和空格开头的部分 2. 多行注释 3 个引号开头,3 个引号结尾 例: ```python """ 这里可以写多行注释 这是第二行注释 """ # 这是单行注释 print('hello, world!') ``` ## 开发工具 * PyCharm * VS Code ## 示例 ```python """ 这里可以写多行注释,比如: Version: 0.1 Author: zhaobc """ print('hello, world!') print("hello, world!") print('hello', 'world') print('hello', 'world', sep=',', end='!') print('goodbye, world', end='!') ``` 试一试: ```shell python day-01-15/day-01/hello.py ``` --- --- url: 'https://zhaobc.site/notes/backend/python/python-100-days/day-01-15/02.语言元素.md' --- # 02.语言元素 ## 变量和类型 常用的数据类型: * 整型 Python 2.x 中有 `int` 和 `long`,Python 3.x 中只有 `int`。 支持二进制(`0b100`,十进制 4),八进制(`0o100`,十进制 64),十六进制(`0x100`,十进制 256) * 浮点型 浮点数除了支持数学写法,如 `123.456`,还支持科学计数法,如 `1.23456e2` * 字符串型 以单引号或双引号括起来的任意文本,比如 `'hello'` 或 `"hello"` * 布尔型 布尔值只有 `True` 和 `False`,注意大小写。 * 复数型 形如 `3+5j`,跟数学上的复数一样,只不过是虚部的 `i` 换成了 `j`。 ## 变量命名 * 硬性规则 * 变量由字母,数字和下划线构成,数字不能开头。 * 大小写敏感 * 不要跟关键字,保留字冲突 * PEP 8 要求 * 小写字母拼写,下划线连接 * 受保护的实例属性用单个下划线开头 * 私有的实例属性用两个下划线开头 ## 变量的使用 在 Python 中可以使用 `type` 函数对变量的类型进行检查。 ```python a = 100 b = 12.345 c = 1 + 5j d = 'hello, world' e = True print(type(a)) # print(type(b)) # print(type(c)) # print(type(d)) # print(type(e)) # ``` 试一试: ```shell python day-01-15/day-02/var-type.py ``` 可以使用 Python 中的内置函数对变量类型进行变换。 * `int()` 将一个数值或字符串转换成正数,可以指定进制。 * `float()` 将一个字符串转换成浮点数。 * `str()` 将指定的对象转换成字符串形式,可以指定编码。 * `chr()` 将整数转换成该编码对应的字符串(一个字符)。 * `ord()` 将字符串(一个字符)转换成对应的编码(整数) 下面的代码通过键盘输入两个整数来实现对两个整数的算术运算。 ```python a = int(input('a = ')) b = int(input('b = ')) print('%d + %d = %d' % (a, b, a + b)) print('%d - %d = %d' % (a, b, a - b)) print('%d * %d = %d' % (a, b, a * b)) print('%d / %d = %f' % (a, b, a / b)) print('%d // %d = %d' % (a, b, a // b)) print('%d %% %d = %d' % (a, b, a % b)) print('%d ** %d = %f' % (a, b, a ** b)) ``` 试一试: ```shell python day-01-15/day-02/calc.py ``` ## 运算符 Python 支持多种运算符,下表大致按照优先级从高到低的顺序列出了所有的运算符。 | 运算符 | 描述 | | ----------------- | ------------------------------ | | `[]` `[:]` | 下标,切片 | | `**` | 指数 | | `~` `+` `-` | 按位取反,正负号 | | `*` `/` `%` `//` | 乘,除,摸,整除 | | `+` `-` | 加,减 | | `>>` `<<` | 右移,左移 | | `&` | 按位与 | | `^` `\|` | 按位异或,按位或 | | `<=` `<` `>` `>=` | 小于等于,小于,大于,大于等于 | | `==` `!=` | 等于,不等于 | | `is` `is not` | 身份运算符 | | `in` `not in` | 成员运算符 | | `not` `or` `and` | 逻辑运算符 | | `+=` `-=` 等 | `a +=b` 等价于 `a = a + b` | ## 练习 ### 华氏温度转换为摄氏温度 ::: tip 华氏温度到摄氏温度的转换公式为:$C=(F - 32) \div 1.8$。 ::: ```python ''' 华氏温度转换为摄氏温度 ''' f = float(input('请输入华氏温度:')) c = (f - 12) / 1.8 print('%.1f华氏度 = %.1f摄氏度' % (f, c)) ``` 在使用 `print` 函数输出时,也可以对字符串内容进行格式化处理。 上面的 `print` 函数中的字符串 `%.1f` 是一个占位符,稍后会由一个 `float` 变量替换它。 同理, `%s` 由字符串替换,`%d` 用整数变量替换。 除了这种格式化字符串的方式外,还可以用下面的方式来格式化字符串,其中 `{f:.1f}` 和 `{c:.1f}` 可以先看成 `{f}` 和 `{c}`,表示输出时会用变量 `f` 和 `c` 的值会替换掉这两个占位符,后面的 `%.1f` 表示这是一个浮点数,小数点后保留 1 位有效数字。 ```python print(f'{f:.1f}华氏度 = {c:.1f}摄氏度') ``` 试一试: ```shell python day-01-15/day-02/deg-converter.py ``` ### 输入年份判断是不是闰年 ```python """ 输入年份判断是不是闰年 """ year = int(input("请输入年份:")) is_leap = year % 4 == 0 and year % 100 != 0 \ or year % 400 == 0 print(is_leap) ``` 试一试: ```shell python day-01-15/day-02/is-leap.py ``` --- --- url: 'https://zhaobc.site/notes/backend/python/python-100-days/day-01-15/03.分支结构.md' --- # 03.分支结构 ## `if` 分支 `if` 结构由 `if`, `elif` 和 `else` 关键字。 Python 中没有用括号来构造代码块,而是使用了**缩进的方式表示代码块**。 如果 `if` 条件成立时需要执行多条语句,只要多条语句保持相同的缩进即可。 缩进可以是任意数量的空格,但通常是 4 个空格。 如果要构造多个分支,可以使用 `if...elif...else...`。分支结构可以嵌套。 ### 分段函数求值 $$ f(x)=\begin{cases} 3x - 5,\quad x > 1 \\ x,\quad -1 \leq x \leq 1 \\ 5x + 3, \quad x < -1 \end{cases} $$ ```python """ 分段函数求值 3x - 5 (x > 1) f(x) = x + 2 (-1 <= x <= 1) 5x + 3 (x < -1) """ x = float(input('x = ')) if x > 1: y = 3 * x -5 elif x >= -1: y = x + 2 else: y = 5 * x + 3 print('f(%.2f) = %.2f' % (x, y)) print(f'f({x:.2f}) = {y:.2f}') ``` 试一试: ```shell python day-01-15/day-03/if-demo.py ``` ### 三角形 判断输入的边长能否构成三角形,如果能则计算出三角形的周长和面积。 海伦公式[^海伦公式]:$S=\sqrt{p(p - a)(p - b)(p - a)}$ ```python """ 判断输入的边长能否构成三角形,如果能则计算出三角形的周长和面积 """ a = float(input('a = ')) b = float(input('b = ')) c = float(input('c = ')) if a + b > c and a + c > b and b + c > a: print('周长:%.2f' % (a + b + c)) p = (a + b + c) / 2 area = (p * (p - a) * (p - b) * (p - c)) ** 0.05 print(f'面积:{area:.2f}') else: print('不能构成三角形') ``` 试一试: ```shell python day-01-15/day-03/if-demo-triangle.py ``` [^海伦公式]: 海伦公式又译作希伦公式、海龙公式、希罗公式、海伦-秦九韶公式。它是利用三角形的三条边的边长直接求三角形面积的公式。表达式为:S=√p\(p-a\)\(p-b\)\(p-c\),它的特点是形式漂亮,便于记忆。 ``` 引用: ``` --- --- url: 'https://zhaobc.site/notes/backend/python/python-100-days/day-01-15/04.循环结构.md' --- # 04.循环结构 Python 中循环有两种,`for-in` 和 `while`。 ## `for-in` 循环 ```python """ 用for循环实现1~100求和 """ sum=0 for x in range(101): sum += x print(sum) ``` `range` 用法非常灵活,下面给出了一个例子: * `range(101)` : 产生 0 到 100 范围的整数,需要注意的是取不到 101。 * `range(1, 101)` : 产生 1 到 100 范围的整数,相当于前面是闭区间,后面是开区间。 * `range(1, 101, 2)` : 产生 1 到 100 范围的奇数,步长是 2。 * `range(100, 0, -2)` : 产生 100 到 0 的偶数,步长是 -2。 知道了这一点,我们可以用下面的代码来实现 1~100 之间的偶数求和。 ```python """ 用for循环实现1~100之间的偶数求和 """ sum=0 for x in range(2, 101, 2): sum += x print(sum) ``` ## `while` 循环 下面我们通过一个“猜数字”的小游戏来看看如何使用 while 循环。 猜数字游戏的规则是:计算机出一个 1 到 100 之间的随机数,玩家输入自己猜的数字,计算机给出对应的提示信息(大一点、小一点或猜对了), 如果玩家猜中了数字,计算机提示用户一共猜了多少次,游戏结束,否则游戏继续。 ```python """ 猜数字游戏 """ import random answer = random.randint(1, 100) counter = 0 while True: counter += 1 number = int(input('请输入:')) if number < answer: print('大一点') elif number > answer: print('小一点') else: print('恭喜你猜对了!') break print('你一共猜了%d次' % counter) if counter > 7: print('需要加油哦~') ``` 试一试: ```shell python day-01-15/day-04/while-demo.py ``` ## 练习 下面的例子演示了如何通过嵌套的循环来输出一个九九乘法表。 ```python """ 输出乘法口诀表(九九表) """ for i in range(1, 10): for j in range(1, i + 1): print('%d * %d = %d' % (i, j, i * j), end='\t') print() ``` 试一试: ::: playground#python 九九乘法表 @file main.py ```python """ 输出乘法口诀表(九九表) """ for i in range(1, 10): for j in range(1, i + 1): print('%d * %d = %d' % (i, j, i * j), end='\t') print() ``` ::: --- --- url: 'https://zhaobc.site/notes/backend/python/tutorial/index.md' --- # Python 教程 本笔记以 [Python 官方教程](https://docs.python.org/zh-cn/3/tutorial/index.html) 为学习教程,记录一些学习笔记。 部分代码在 [replit](https://replit.com/@zhaobin/python-tutorial) 上,不过由于它已不能分享可编辑,可运行的示例,所以不再使用。 --- --- url: 'https://zhaobc.site/notes/backend/python/tutorial/3.introduction.md' --- # 3. 速览 ## 3.1 Python 用作计算器 ### 3.1.1 数字 除了 int 和 float,Python 还支持其他数字类型,例如 Decimal 或 Fraction。 ### 3.1.2 字符串 用单引号('……')或双引号("……")标注的结果相同,可交替使用,例 `"doesn't"`,`'"Yes," they said.'`。 反斜杠 `\` 用于转义,例 `'doesn\'t'`。 字符串字面值可以包含多行。 一种实现方式是使用三重引号:`"""..."""` 或 `'''...'''`。 字符串中将自动包括行结束符,但也可以在换行的地方添加一个 `\` 来避免此情况(避免换行,就是加 `\` 后不换行)。 参见以下示例: ```python print("""\ Usage: thingy [OPTIONS] -h Display this usage message -H Hostname to connect to """) ``` 输出如下(请注意开始的换行符没有被包括在内): ```txt Usage: thingy [OPTIONS] -h Display this usage message -H Hostname to connect to ``` 字符串可以用 `+` 拼接,也可以用 `*` 重复,相邻的两个或多个 字符串字面值 (引号标注的字符)会自动合并。 拼接分隔开的长字符串时,这个功能特别实用。 ```python print(3 * 'un' + 'ium') # 'unununium' print('Py' 'thon') # 'Python' print('Put several strings within parentheses ' 'to have them joined together.') ``` 字符串支持 索引 (下标访问),第一个字符的索引是 0。单字符没有专用的类型,就是长度为一的字符串。 索引还支持负数,用负数索引时,从右边开始计数,注意,-0 和 0 一样,因此,负数索引从 -1 开始。 ```python word = "python" print(word[0]) # p print(word[-1]) # n ``` 除了索引,字符串还支持 切片。索引可以提取单个字符,切片 则提取子字符串,*前闭后开*。 切片索引的默认值很有用;省略开始索引时,默认值为 0,省略结束索引时,默认为到字符串的结尾。 负数代表从右边开始数。 输出结果包含切片开始,但不包含切片结束。因此,`s[:i] + s[i:]` 总是等于 `s`。 索引越界会报错,但是,切片会自动处理越界索引。 ```python print(word[0:2]) # py print(word[2:5]) # tho print(word[:2]) # py print(word[4:]) # on print(word[-2:]) # on print(word[42]) # IndexError: string index out of range print(word[4:42]) # on print(word[42:]) # '' ``` Python 字符串不能修改,是 immutable 的。因此,为字符串中某个索引位置赋值会报错。 ```python word[0] = 'J' # TypeError: 'str' object does not support item assignment word[2:] = 'py' # TypeError: 'str' object does not support item assignment ``` 其他补充: * `len()` 返回字符串长度 * `str.format()` 格式化字符串 示例:`"The sum of 1 + 2 is {0}".format(1+2)`,详情。[str.format()](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format) * [printf 风格的字符串格式化](https://docs.python.org/zh-cn/3/library/stdtypes.html#old-string-formatting) 试一试: ::: playground#python 字符串 @file main.py ```python print("""\ Usage: thingy [OPTIONS] -h Display this usage message -H Hostname to connect to """) print(3 * 'un' + 'ium') # 'unununium' print('Py' 'thon') # 'Python' print('Put several strings within parentheses ' 'to have them joined together.') word = "python" print(word[0]) # p print(word[-1]) # n print(word[0:2]) # py print(word[2:5]) # tho print(word[:2]) # py print(word[4:]) # on print(word[-2:]) # on #print(word[42]) # IndexError: string index out of range print(word[4:42]) # on print(word[42:]) # '' ``` ::: ### 3.1.3 列表 列表 ,是用方括号标注,逗号分隔的一组值。列表 可以包含不同类型的元素,但一般情况下,各个元素的类型相同。 和字符串(及其他内置 sequence 类型)一样,列表也支持索引和切片。 切片操作返回包含请求元素的新列表。 列表还支持合并操作。 ```python squares = [1, 4, 9, 16, 25] print(squares[0]) # 1 print(squares[-1]) # 25 print(squares[-3:]) # [9, 16, 25] 返回新的列表 print(squares[:]) # [1, 4, 9, 16, 25] 返回列表的浅拷贝 print(squares + [36, 49, 64]) # [1, 4, 9, 16, 25, 36, 49, 64] ``` 与 immutable 字符串不同, 列表是 mutable 类型,其内容可以改变。 `append()` 方法 可以在列表结尾添加新元素。 为切片赋值可以改变列表大小,甚至清空整个列表。 内置函数 `len()` 也支持列表。 还可以嵌套列表(创建包含其他列表的列表)。 ```python cubes = [1, 8, 27, 65, 125] # something's wrong here cubes[3] = 64 cubes.append(6 ** 3) print(cubes) # [1, 8, 27, 64, 125, 216] letters = ['a', 'b', 'c', 'd', 'e', 'f'] letters[2:5] = ['C', 'D', 'E'] print(letters) # ['a', 'b', 'C', 'D', 'E', 'f'] letters[2:5] = [] # 删除 2:5 print(letters) # ['a', 'b', 'f'] ``` 试一试: ::: playground#python 列表 @file main.py ```python cubes = [1, 8, 27, 65, 125] # something's wrong here cubes[3] = 64 cubes.append(6 ** 3) print(cubes) # [1, 8, 27, 64, 125, 216] letters = ['a', 'b', 'c', 'd', 'e', 'f'] letters[2:5] = ['C', 'D', 'E'] print(letters) # ['a', 'b', 'C', 'D', 'E', 'f'] letters[2:5] = [] # 删除 2:5 print(letters) # ['a', 'b', 'f'] ``` ::: ## 3.2 走向编程的第一步 打印斐波那契数列: ```python """ Fibonacci series """ a, b = 0, 1 while a < 10: # print(a) print(a, sep = ', ', end = '\n') # print 默认带换回,使用 end 改用自己指定的字符 a, b = b, a + b ``` 试一试: ::: playground#python 打印斐波那契数列 @file main.py ```python """ Fibonacci series """ a, b = 0, 1 while a < 10: # print(a) print(a, sep = ', ', end = '\n') # print 默认带换回,使用 end 改用自己指定的字符 a, b = b, a + b ``` ::: 本例引入的新功能: * 第一行中的 *多重赋值*:变量 `a` 和 `b` 同时获得新值 `0` 和 `1`,最后一行又用了一次多重赋值。 * `while` 循环,循环体缩进。 --- --- url: 'https://zhaobc.site/notes/backend/python/tutorial/4.controlflow.md' --- # 4. 流程控制 ## 4.1 if 语句 `if` 语句包含零个或多个 `elif` 子句及可选的 `else` 子句。 ::: playground#python if 语句 @file main.py ```python x = int(input('Please enter an integer: ')) if x < 0: x = 0 print('Negative changed to zero') elif x == 0: print('Zero') elif x == 1: print('Single') else: print('More') ``` ::: 如果要把一个值与多个常量进行比较,或者检查特定类型或属性,`match` 语句更实用。 ## 4.2 for 语句 Python 的 for 语句不迭代算术递增数值,或是给予用户定义迭代步骤和暂停条件的能力(如 C),而是迭代列表或字符串等任意序列,元素的迭代顺序与在序列中出现的顺序一致。 ```python words = ['cat', 'window', 'test'] for w in words: print(w, len(w)) ``` 遍历集合时修改集合的内容,会很容易生成错误的结果。因此不能直接进行循环,而是应遍历该集合的副本或创建新的集合: ```python users = {'Hans': 'active', 'hello': 'inactive', '赵斌': 'active'} for user, status in users.copy().items(): if status == 'inactive': del users[user] active_users = {} for user, status in users.items(): if status == 'active': active_users[user] = status print('\nactive users:') for user in active_users: print(user) ``` 试一试: ::: playground#python for 语句 @file main.py ```python words = ['cat', 'window', 'test'] for w in words: print(w, len(w)) users = {'Hans': 'active', 'hello': 'inactive', '赵斌': 'active'} for user, status in users.copy().items(): if status == 'inactive': del users[user] active_users = {} for user, status in users.items(): if status == 'active': active_users[user] = status print('\nactive users:') for user in active_users: print(user) ``` ::: ## 4.3 range 函数 内置函数 `range()` 常用于遍历数字序列。 range 可以不从 0 开始,还可以按指定幅度递增(递增幅度称为 '步进',支持负数) ```python for i in range(5): print(i) sum(range(5)) # 0 + 1 + 2 + 3 + 4 list(range(5, 10)) # [5, 6, 7, 8, 9] list(range(0, 10, 3)) # [0, 3, 6, 9] list(range(-10, -100, -30)) # [-10, -40, -70] ``` `range()` 和 `len()` 组合在一起,可以按索引迭代序列: ```python a = ['Mary', 'had', 'a', 'little', 'lamb'] for i in range(len(a)): print(i, a[i]) ``` 试一试: ::: playground#python range 函数 @file main.py ```python for i in range(5): print(i) print(sum(range(5))) # 0 + 1 + 2 + 3 + 4 print(list(range(5, 10))) # [5, 6, 7, 8, 9] print(list(range(0, 10, 3))) # [0, 3, 6, 9] print(list(range(-10, -100, -30))) # [-10, -40, -70] a = ['Mary', 'had', 'a', 'little', 'lamb'] for i in range(len(a)): print(i, a[i]) ``` ::: ## 4.4 循环中的 break、continue 语句及 else 子句 break 语句和 C 中的类似,用于跳出最近的 for 或 while 循环。 循环语句支持 else 子句;for 循环中,可迭代对象中的元素全部循环完毕,或 while 循环的条件为假时,执行该子句;break 语句终止循环时,不执行该子句。 请看下面这个查找素数的循环示例: ```python for n in range(2, 10): for x in range(2, n): if n % x == 0: print(n, 'equals', x, '*', n // x) break else: # loop fell through without finding a factor print(n, 'is a prime number') ``` (没错,这段代码就是这么写。仔细看:else 子句属于 for 循环,不属于 if 语句。) 与 `if` 语句相比,循环的 `else` 子句更像 `try` 的 `else` 子句: `try` 的 `else` 子句在未触发异常时执行,循环的 `else` 子句则在未运行 `break` 时执行。`try` 语句和异常详见 异常的处理。 试一试: ::: playground#python else @file main.py ```python for n in range(2, 10): for x in range(2, n): if n % x == 0: print(n, 'equals', x, '*', n // x) break else: # loop fell through without finding a factor print(n, 'is a prime number') ``` ::: ## 4.5 pass pass 语句不执行任何动作。语法上需要一个语句,但程序毋需执行任何动作时,可以使用该语句。 这常用于创建一个最小的类: ```python class MyEmptyClass: pass ``` pass 还可用作函数或条件语句体的占位符,让你保持在更抽象的层次进行思考。pass 会被默默地忽略: ```python def initlog(*args): pass # Remember to implement this! ``` ## 4.6 match 语句 `match` 语句接受一个表达式并把它的值与一个或多个 `case` 块给出的一系列模式进行比较。 这表面上像 C、Java 或 JavaScript(以及许多其他程序设计语言)中的 switch 语句,但其实它更像 Rust 或 Haskell 中的模式匹配。 **只有第一个匹配的模式会被执行**,并且它还可以提取值的组成部分(序列的元素或对象的属性)赋给变量。 最简单的形式是将一个主语值与一个或多个字面值进行比较: ```python def http_error(status): match status: case 400: return "Bad request" case 404: return "Not found" case 418: return "I'm a teapot" case 401 | 403: # 你可以使用 | (“ or ”)在一个模式中组合几个字面值: return "Not allowed" case _: return "Something's wrong with the internet" ``` 注意最后一个代码块:“变量名” `_` 被作为 *通配符* 并必定会匹配成功。如果没有 case 匹配成功,则不会执行任何分支。 试一试: ::: playground#python match 语句 @file main.py ```python def http_error(status): match status: case 400: return "Bad request" case 404: return "Not found" case 418: return "I'm a teapot" case 401 | 403: # 你可以使用 | (“ or ”)在一个模式中组合几个字面值: return "Not allowed" case _: return "Something's wrong with the internet" print(http_error(400)) print(http_error(401)) print(http_error(500)) ``` ::: 形如解包赋值的模式,可用于绑定变量: ```python # point is an (x, y) tuple match point: case (0, 0): print("Origin") case (0, y): print(f"Y={y}") case (x, 0): print(f"X={x}") case (x, y): print(f"X={x}, Y={y}") case _: raise ValueError("Not a point") ``` ```python from enum import Enum class Color(Enum): RED = 'red' GREEN = 'green' BLUE = 'blue' color = Color(input("Enter your choice of 'red', 'green' or 'blue': ")) match color: case COLOR.RED: print("I see red!") case COLOR.GREEN: print("Grass is green.") case COLOR.BLUE: print("I'm feeling the blues :(") ``` 试一试: ## 4.7 定义函数 下列代码创建一个可以输出限定数值内的斐波那契数列函数: ```python def fib(n): # write Fibonacci series up to n """Print a Fibonacci series up to n.""" a, b = 0, 1 while a < n: print(a, end = " ") a,b = b, a + b print() # Now call the function we just defined: fib(10) ``` *定义* 函数使用关键字 `def`,后跟函数名与括号内的形参列表。函数语句从下一行开始,并且必须缩进。 函数内的第一条语句是字符串时,该字符串就是文档字符串,也称为 docstring,详见 [文档字符串](https://docs.python.org/zh-cn/3/tutorial/controlflow.html#tut-docstrings)。 `fib` 不返回值,因此,其他语言不把它当作函数,而是当作过程。事实上,没有 return 语句的函数也返回值,只不过这个值比较是 `None` (是一个内置名称)。一般来说,解释器不会输出单独的返回值 None ,如需查看该值,可以使用 `print()`: ```python print(fib(0)) # None ``` ::: playground#python 定义函数 @file main.py ```python def fib(n): # write Fibonacci series up to n """Print a Fibonacci series up to n.""" a, b = 0, 1 while a < n: print(a, end = " ") a,b = b, a + b print() # Now call the function we just defined: print(fib(10)) print(fib(0)) ``` ::: ## 4.8 函数详解 函数定义支持可变数量的参数。这里列出三种可以组合使用的形式。 ### 4.8.1 默认参数 为参数指定默认值是非常有用的方式。调用函数时,可以使用比定义时更少的参数,例如: ```python def ask_ok(prompt, retries=4, reminder="Please try again!"): while True: ok = input(prompt) if ok in ("y", "ye", "yes"): return True if ok in ("N", "no", "nop", "nope"): return False retries = retries - 1 if retries < 0: raise ValueError("invalid user response") print(reminder) ``` 该函数可以用以下方式调用: * 只给出必选参数 `ask_ok('Do you really want to quit?')` * 给出一个可选参数 `ask_ok('OK to overwrite the file?', 2)` * 给出所有参数 `ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')` 本例还使用了关键字 `in` ,用于确认序列中是否包含某个值。 ::: warning 重要警告: 默认值只计算一次。默认值为列表、字典或类实例等可变对象时,会产生与该规则不同的结果。 ::: 例如,下面的函数会累积后续调用时传递的参数: ```python i = 5 def f(arg=i): print(arg) i = 6 f() # 输出: 5 def f(a, L=[]): L.append(a) return L print(f(1)) print(f(2)) print(f(3)) # 输出结果: # [1] # [1, 2] # [1, 2, 3] ``` 不想在后续调用之间共享默认值时,应以如下方式编写函数: ```python def f(a, L=None): if L is None: L = [] L.append(a) return L ``` ### 4.8.2 关键字参数 `kwarh=value` 形式的[关键字参数](https://docs.python.org/zh-cn/3/glossary.html#term-keyword-argument)也可以用于调用参数。函数示例如下: ```python def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'): print("-- This parrot wouldn't", action, end = ' ') print("if you put", voltage, "volts through it.") print("-- Lovely plumage, the", type) print("-- It's", state, "!") ``` 该函数接受一个必选参数(voltage)和三个可选参数(state, action 和 type)。该函数可用下列方式调用: ```python parrot(1000) # 1 positional argument (位置参数) parrot(voltage=1000) # 1 keyword argument (关键字参数) parrot(voltage=1000000, action='VOOOOOM') # 2 keyword arguments (关键字参数) parrot(action='VOOOOOM', voltage=1000000) # 2 keyword arguments (关键字参数) parrot('a million', 'bereft of life', 'jump') # 3 positional arguments (位置参数) parrot('a thousand', state='pushing up the daisies') # 1 positional, 1 keyword (位置参数,关键字参数) ``` 以下调用函数的方式都无效: ```python parrot() # required argument missing parrot(voltage=5.0, 'dead') # non-keyword argument after a keyword argument parrot(110, voltage=220) # duplicate value for the same argument parrot(actor='John Cleese') # unknown keyword argument ``` * 函数调用时,关键字参数必须跟在位置参数后面。 * 所有传递的关键字参数都必须匹配一个函数接受的参数,关键字参数的顺序并不重要。 ::: playground#python 关键字参数 @file main.py ```python def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'): print("-- This parrot wouldn't", action, end = ' ') print("if you put", voltage, "volts through it.") print("-- Lovely plumage, the", type) print("-- It's", state, "!") parrot(1000) # 1 positional argument (位置参数) parrot(voltage=1000) # 1 keyword argument (关键字参数) parrot(voltage=1000000, action='VOOOOOM') # 2 keyword arguments (关键字参数) parrot(action='VOOOOOM', voltage=1000000) # 2 keyword arguments (关键字参数) parrot('a million', 'bereft of life', 'jump') # 3 positional arguments (位置参数) parrot('a thousand', state='pushing up the daisies') # 1 positional, 1 keyword (位置参数,关键字参数) ``` ::: 默认情况下,参数可以按位置或显式关键字传递给 Python 函数。为了让代码易读、高效,最好限制参数的传递方式,这样,开发者只需查看函数定义,即可确定参数项是仅按位置、按位置或关键字,还是仅按关键字传递。 函数定义如下: ```txt def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): ----------- ---------- ---------- | | | | Positional or keyword | | - Keyword only -- Positional only ``` ```txt def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): ----------- ---------- ---------- | | | | 位置或关键字 | | - 仅限关键字 -- 仅限位置 ``` `/` 和 `*` 是可选的。这些符号表明形参如何把参数值传递给函数:位置、位置或关键字、关键字。关键字形参也叫作命名形参。 #### 4.8.3.1 位置或关键字参数 函数定义中未使用 `/` 和 `*` 时,参数可以按位置或关键字传递给函数。 #### 4.8.3.2 仅位置参数 此处再介绍一些细节,特定形参可以标记为 仅限位置。仅限位置 时,形参的顺序很重要,且这些形参不能用关键字传递。仅限位置形参应放在 `/` (正斜杠)前。`/` 用于在逻辑上分割仅限位置形参与其它形参。如果函数定义中没有 /,则表示没有仅限位置形参。 / 后可以是 位置或关键字 或 仅限关键字 形参。 #### 4.8.3.3 仅限关键字参数 把形参标记为 仅限关键字,表明必须以关键字参数形式传递该形参,应在参数列表中第一个 仅限关键字 形参前添加 `*`。 ### 4.8.4 任意实参列表 调用函数时,使用任意数量的实参是最少见的选项。这些实参包含在元组中(详见 元组和序列 )。在可变数量的实参之前,可能有若干个普通参数: ```python def write_multiple_items(file, separator, *args): file.write(separator.join(args)) ``` variadic 参数用于采集传递给函数的所有剩余参数,因此,它们通常在形参列表的末尾。`*args` 形参后的任何形式参数只能是仅限关键字参数,即只能用作关键字参数,不能用作位置参数: ```python def concat(*args, sep="/"): return sep.join(args) concat("earth", "mars", "venus") # 'earth/mars/venus' concat("earth", "mars", "venus", sep=".") # 'earth.mars.venus' ``` ::: playground#python 任意实参列表 @file main.py ```python def concat(*args, sep="/"): return sep.join(args) print(concat("earth", "mars", "venus")) # 'earth/mars/venus' print(concat("earth", "mars", "venus", sep=".")) # 'earth.mars.venus' ``` ::: ### 4.8.5 解包实参列表 函数调用要求独立的位置参数,但实参在列表或元组里时,要执行相反的操作。 例如,内置的 range() 函数要求独立的 start 和 stop 实参。 如果这些参数不是独立的,则要在调用函数时,用 `*` 操作符把实参从列表或元组解包出来: ```python list(range(3, 6)) # 两个参数的正常调用 # [3, 4, 5] args = [3, 6] list(range(*args)) # 从一个数组解包出参数的调用 # [3, 4, 5] ``` 同样,字典可以用 `**` 操作符传递关键字参数: ```python def parrot(voltage, state='a stiff', action='voom'): print("-- This parrot wouldn't", action, end=' ') print("if you put", voltage, "volts through it.", end=' ') print("E's", state, "!") d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"} parrot(**d) # This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised ! ``` ## 4.9 Lambda 表达式 lambda 关键字用于 *创建小巧的匿名函数*。lambda a, b: a+b 函数返回两个参数的和。Lambda 函数可用于任何需要 *函数对象* 的地方。 在语法上,匿名函数只能是单个表达式。在语义上,它只是常规函数定义的语法糖。与嵌套函数定义一样,lambda 函数可以引用包含作用域中的变量: ```python def make_incrementor(n): return lambda x: x + n f = make_incrementor(42) f(0) # 42 f(1) # 43 ``` 上例用 lambda 表达式返回函数。还可以把匿名函数用作传递的实参: ```python pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')] pairs.sort(key=lambda pair: pair[1]) pairs # [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')] ``` ## 4.10 文档字符串 以下是文档字符串内容和格式的约定。 * 第一行应为对象用途的简短摘要。 这一行应以大写字母开头,以句点结尾。 * 文档字符串为多行时,第二行应为空白行,在视觉上将摘要与其余描述分开。 后面的行可包含若干段落,描述对象的调用约定、副作用等。 * 文档字符串第一行 之后 的第一个非空行决定了整个文档字符串的缩进量。 第一行通常与字符串开头的引号相邻,其缩进在字符串中并不明显,因此,不能用第一行的缩进。 然后,删除字符串中所有行开头处与此缩进“等价”的空白符。 不能有比此缩进更少的行,但如果出现了缩进更少的行,应删除这些行的所有前导空白符。 ```python def my_function(): """Do nothing, but document it. No, really, it doesn't do anything. """ pass print(my_function.__doc__) #Do nothing, but document it. # # No, really, it doesn't do anything. ``` ## 4.11 函数注解 函数注解 是可选的用户自定义函数类型的元数据完整信息。 标注 以字典的形式存放在函数的 `__annotations__` 属性中而对函数的其他部分没有影响。 下面的示例有一个必须的参数、一个可选的关键字参数以及返回值都带有相应的标注: ```python def f(ham: str, eggs: str = 'eggs') -> str: print("Annotations:", f.__annotations__) print("Arguments:", ham, eggs) return ham + ' and ' + eggs f('spam') #Annotations: {'ham': , 'return': , 'eggs': } #Arguments: spam eggs #'spam and eggs' ``` --- --- url: 'https://zhaobc.site/notes/backend/python/tutorial/5.datastructures.md' --- # 5. 数据结构 ## 5.1 列表详解 列表数据类型支持很多方法,列表对象的所有方法所示如下: * `list.append(x)` * `list.extend(iterable)` 通过添加来自 `iterable` 的所有项来扩展列表。 * `list.insert(i, x)` * `list.remove(x)` * `list.pop([i])` * `list.clear()` * `list.index(x[, start[, end]])` * `list.count(x)` * `list.sort(*, key=None, reverse=False)` 就地排序列表中的元素 * `list.reverse()` * `list.copy()` 返回列表的浅拷贝。类似于 `a[:]`。 `insert`, `remove` 或 `sort` 等仅修改列表的方法都不会打印返回值 -- 它们返回默认值 `None`。 这是适用于 Python 中所有可变数据结构的设计原则。 ### 5.1.3 列表推导式 列表推导式创建列表的方式更简洁。常见的用法为,对序列或可迭代对象中的每个元素应用某种操作,用生成的结果创建新的列表;或用满足特定条件的元素创建子序列。 例如,创建平方值的列表: ```python squares = [] for x in range(10): squares.append(x**2) print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] print(x) ``` 注意,这段代码创建(或覆盖)变量 `x`,该变量在循环结束后仍然存在。 下述方法可以无副作用地计算平方列表: :::playground#python 无副作用地计算平方列表 @file main.py ```python squares = list(map(lambda x: x**2, range(10))) # 或等价于 squares = [x**2 for x in range(10)] print(squares) ``` @file sample1.py ```python squares = [] for x in range(10): squares.append(x**2) print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] print(x) ``` ::: 上面这种写法更简洁、易读。 列表推导式的方括号内包含以下内容:一个表达式,后面为一个 `for` 子句,然后,是零个或多个 `for` 或 `if` 子句。 结果是由表达式依据 `for` 和 `if` 子句求值计算而得出一个新列表。 举例来说,以下列表推导式将两个列表中不相等的元素组合起来: ```python [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y] # [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)] ``` 等价于: ```python combs = [] for x in [1, 2, 3]: for y in [3, 1, 4]: if x != y: combs.append((x, y)) print(combs) ``` 表达式是元组(例如上例的 `(x, y)`)时,必须加上括号。 ```python vec = [-4, -2, 0, 2, 4] # 过滤列表以排除负数 [x for x in vec if x >= 0] # [0, 2, 4] # 对所有元素应用一个函数 [abs(x) for x in vec] # [4, 2, 0, 2, 4] # 创建一个包含 (数字, 平方) 2 元组的列表 # 元组必须加圆括号,否则会引发错误 [(x, x**) for x in range(6)] # [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)] # 使用两个 'for' 来展平嵌套的列表 vec = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] [num for elem in vec for num in elem] # [1, 2, 3, 4, 5, 6, 7, 8, 9] ``` 列表推导式可以使用复杂的表达式和嵌套函数: ```python from math import pi [str(round(pi, i)) for i in range(1, 6)] ``` ### 5.1.4 嵌套的列表推导式 列表推导式中的初始表达式可以是任何表达式,甚至可以是另一个列表推导式。 下面这个 3x4 矩阵,由 3 个长度为 4 的列表组成,列表推导式可以转置行列: ```python matrix = [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12] ] # 下面的列表推导式可以转置行列: [[row[i] for row in matrix] for i in range(4)] # [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]] #实际应用中,最好用内置函数替代复杂的流程语句。此时,zip() 函数更好用: list(zip(*matrix)) # [(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)] ``` `zip()` 在多个迭代器上并行迭代,从每个迭代器返回一个数据项组成元组。 ## 5.2 del 语句 可以按索引而不是按值从一个列表移除条目:即使用 `del` 语句。这不同于返回一个值的 pop() 方法。 `del` 语句还可被用来从列表移除切片或清空整个列表(之前我们通过将一个空列表赋值给切片实现此功能)。 ```python a = [-1, 1, 66.5, 333, 333, 1234.5] del a[0] a # [1, 66.5, 333, 333, 1234.5] del a[2:4] a # [1, 66.25, 1234.5] del a[:] a # [] ``` `del` 也可以用来删除整个变量: ```python del a a # NameError: name 'a' is not defined ``` 此后,再引用 `a` 就会报错(直到为它赋与另一个值)。 ## 5.3 元祖和序列 列表和字符串有很多共性,例如,索引和切片操作。这两种数据类型是 *序列* (参见 [序列类型 --- list, tuple, range](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#typesseq)) 本节介绍另一种标准序列类型:*元组*。 元组由多个用逗号隔开的值组成,例如: ```python t = 12345, 54321, 'Hello!' t[0] # 12345 t # (12345, 54321, 'Hello!') # 元组可以嵌套: u = t, (1, 2, 3, 4, 5) u # ((12345, 54321, 'Hello!'), (1, 2, 3, 4, 5)) # 元组是不可变对象: t[0] = 88888 # TypeError: 'tuple' object does not support item assignment # 但它们可以包含可变对象: v = ([1, 2, 3], [3, 2, 1]) v # ([1, 2, 3], [3, 2, 1]) ``` 输出时,元组都要由圆括号标注,这样才能正确地解释嵌套元组。 输入时,圆括号可有可无,不过经常是必须的(如果元组是更大的表达式的一部分)。 不允许为元组中的单个元素赋值,当然,可以创建含列表等可变对象的元组。 虽然,元组与列表很像,但使用场景不同,用途也不同。元组是 `immutable` (不可变的),一般可包含异质元素序列,通过解包(见本节下文)或索引访问(如果是 `namedtuples`,可以属性访问)。 列表是 `mutable` (可变的),列表元素一般为同质类型,可迭代访问。 构造 0 个或 1 个元素的元组比较特殊:为了适应这种情况,对句法有一些额外的改变。用一对空圆括号就可以创建空元组; 只有一个元素的元组可以通过在这个元素后添加逗号来构建(圆括号里只有一个值的话不够明确)。丑陋,但是有效。例如: ```python empty = () singleton = 'hello', # <-- 注意末尾的逗号 len(empty) # 0 len(singleton) # 1 singleton # ('hello',) ``` 语句 `t = 12345, 54321, 'hello!'` 是 *元组打包* 的例子:值 `12345`, `54321` 和 `'hello!'` 一起被打包进元组。 逆操作也可以: ```python x, y, z = t x # 12345 y # 54321 z # 'Hello!' ``` 称之为 *序列解包* 也是妥妥的,适用于右侧的任何序列。 序列解包时,左侧变量与右侧序列元素的数量应相等。注意,多重赋值其实只是元组打包和序列解包的组合。 ## 5.4 集合 集合是由 *不重复* 元素组成的 *无序* 容器。 基本用法包括成员检测、消除重复元素。集合对象支持合集、交集、差集、对称差分等数学运算。 创建集合用 *花括号* 或 `set()` 函数。注意,创建空集合只能用 `set()`,不能用 {},{} 创建的是空字典。 以下是一些简单的示例: ```python basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'} print(basket) # {'apple', 'pear', 'orange', 'banana'} 'orange' in basket # 快速成员检测 # True 'crabgrass' in basket # False # 演示针对两个单词中独有字母进行集合运算 a = set('abracadabra') b = set('alacazam') a # {'r', 'c', 'd', 'b', 'a'} # a 中独有的字母 a - b # {'b', 'd', 'r'} # 存在于 a 中,但不存在于 b 中的字母 a | b # {'z', 'r', 'c', 'd', 'b', 'a', 'l', 'm'} # 存在于 a 或 b 中或两者中皆有的字母 a & b # {'c', 'a'} # 同时存在于 a 和 b 的字母 a ^ b # {'z', 'd', 'b', 'l', 'm', 'r'} # 存在与 a 或 b 中但非两者中皆有的字母 ``` 与 列表推导式 类似,集合也支持推导式: ```python a = { x for x in 'abracadabra' if x not in 'abc' } a # {'d', 'r'} ``` ## 5.5 字典 字典在其他编程语言中可能称为“联合内存”或“联合数组”。 与以连续整数为索引的序列不同,字典是以 *键* 来索引的,键可以是任何 **不可变** 类型;字符串和数字总是可以作为键。 可以把字典理解为 *键值对* 的集合,但字典的键必须是唯一的。花括号 `{}` 用于创建空字典。 另一种初始化字典的方式是,在花括号里输入逗号分隔的键值对,这也是字典的输出方式。 用 `del` 可以删除键值对。通过不存在的键提取值,则会报错。 对字典执行 `list(d)` 操作,返回该字典中所有键的列表,按插入次序排列(如需排序,请使用 `sorted(d)`)。检查字典里是否存在某个键,使用关键字 `in`。 以下是一些字典的简单示例: ```python tel = {'jack': 4098, 'sape': 4139} tel['guido'] = 4127 tel # {'jack': 4098, 'sape': 4139, 'guido': 4127} tel['jack'] # 4098 del tel['sape'] tel['irv']= 4127 # {'jack': 4098, 'guido': 4127, 'irv': 4127} list(tel) # ['jack', 'guido', 'irv'] sorted(tel) # ['guido', 'irv', 'jack'] 'guido' in tel # True 'jack' not in tel # False ``` `dict()` 构筑函数可以直接用键值对序列创建字典: ```python dict([('sape', 4139), ('guido', 4127), ('jack', 4098)]) # {'sape': 4139, 'guido': 4127, 'jack': 4098} ``` 字典推导式可以用任意键值表达式创建字典: ```python {x: x**2 for x in (2, 4, 6)} # {2: 4, 4: 16, 6: 36} ``` 关键字是比较简单的字符串时,直接用关键字参数指定键值对更便捷: ```python dict(sape=4139, guido=4127, jack=4098) # {'sape': 4139, 'guido': 4127, 'jack': 4098} ``` ## 5.6 循环的技巧 当对字典执行循环时,可以使用 `items()` 方法同时提取键及其对应的值。 ```python knights = {'gallahad': 'the pure', 'robin': 'the brave'} for k, v in knights.items(): print(k, v) # gallahad the pure # robin the brave ``` 在序列中循环时,用 `enumerate()` 函数可以同时取出位置索引和对应的值: ```python for i, v in enumerate(['tic', 'tac', 'toe']): print(i, v) # 0 tic # 1 tac # 2 toe ``` 同时循环两个或多个序列时,用 `zip()` 函数可以将其内的元素一一匹配: ```python questions = ['name', 'quest', 'favorite color'] answers = ['lancelot', 'the holy grail', 'blue'] for q, a in zip(questions, answers): print('What is your {0}? It is {1}.'.format(q, a)) # What is your name? It is lancelot. # What is your quest? It is the holy grail. # What is your favorite color? It is blue. ``` 为了逆向对序列进行循环,可以求出欲循环的正向序列,然后调用 `reversed()` 函数: ```python for i in reversed(range(1, 10, 2)): print(id) # 9 # 7 # 5 # 3 # 1 ``` 按指定顺序循环序列,可以用 `sorted()` 函数,在不改动原序列的基础上,返回一个重新的序列: ```python basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] for i in sorted(basket): print(i) # apple # apple # banana # orange # orange # pear ``` 使用 `set()` 去除序列中的重复元素。使用 `sorted()` 加 `set()` 则按排序后的顺序,循环遍历序列中的唯一元素: ```python basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] for f in sorted(set(basket)): print(f) # apple # banana # orange # pear ``` 一般来说,在循环中修改列表的内容时,**创建新列表** 比较简单,且安全: ```python import math raw_data = [56.2, float('NaN'), 51.7, 55.3, 52.5, float('NaN'), 47.8] filtered_data = [] for value in raw_data: if not math.isnan(value): filtered_data.append(value) filtered_data # [56.2, 51.7, 55.3, 52.5, 47.8] ``` ## 5.7 深入条件控制 `while` 和 `if` 条件句不只可以进行比较,还可以使用任意运算符。 比较运算符 `in` 和 `not in` 用于执行确定一个值是否存在(或不存在)于某个容器中的成员检测。 运算符 `is` 和 `is not` 用于比较两个对象是否是同一个对象。 所有比较运算符的优先级都一样,且低于任何数值运算符。 比较操作支持链式操作。例如,`a < b == c` 校验 `a` 是否小于 `b`,且 `b` 是否等于 `c`。 比较操作可以用布尔运算符 `and` 和 `or` 组合,并且,比较操作(或其他布尔运算)的结果都可以用 `not` 取反。 这些操作符的优先级低于比较操作符;`not` 的优先级最高, `or` 的优先级最低,因此,`A and not B or C` 等价于 `(A and (not B)) or C`。 与其他运算符操作一样,此处也可以用圆括号表示想要的组合。 布尔运算符 `and` 和 `or` 是所谓的 *短路* 运算符:其参数从左至右求值,一旦可以确定结果,求值就会停止。 注意,Python 与 C 不同,在表达式内部赋值必须显式使用 [海象运算符](https://docs.python.org/zh-cn/3.13/faq/design.html#why-can-t-i-use-an-assignment-in-an-expression) `:=`。 ## 5.8 序列和其他类型的比较 序列对象可以与相同序列类型的其他对象比较。这种比较使用 *字典式* 顺序: 首先,比较前两个对应元素,如果不相等,则可确定比较结果; 如果相等,则比较之后的两个元素,以此类推,直到其中一个序列结束。 如果要比较的两个元素本身是相同类型的序列,则递归地执行字典式顺序比较。 如果两个序列中所有的对应元素都相等,则两个序列相等。 如果一个序列是另一个的初始子序列,则较短的序列可被视为较小(较少)的序列。 对于字符串来说,字典式顺序使用 Unicode 码位序号排序单个字符。 ```python (1, 2, 3) < (1, 2, 4) [1, 2, 3] < [1, 2, 4] 'ABC' < 'C' < 'Pascal' < 'Python' (1, 2, 3, 4) < (1, 2, 4) (1, 2) < (1, 2, -1) (1, 2, 3) == (1.0, 2.0, 3.0) (1, 2, ('aa', 'ab')) < (1, 2, ('abc', 'a'), 4) ``` 注意,当比较不同类型的对象时,只要待比较的对象提供了合适的比较方法,就可以使用 `<` 和 `>` 进行比较。 例如,混合的数字类型通过数字值进行比较,所以,`0` 等于 `0.0`,等等。 如果没有提供合适的比较方法,解释器不会随便给出一个比较结果,而是引发 `TypeError` 异常。 --- --- url: 'https://zhaobc.site/notes/backend/python/tutorial/6.modules.md' --- # 6. 模块 模块是包含 Python 定义和语句的文件。其文件名是模块名加后缀名 `.py` 。在模块内部,通过全局变量 `__name__` 可以获取模块名(即字符串)。 ```python title="fibo.py" # 斐波那契数列模块 def fib(n): """Write Fibonacci series up to n.""" a, b = 0, 1 while a < n: print(a, end=' ') a, b = b, a + b print() def fib2(n): """Return Fibonacci series up to n.""" result = [] a, b = 0, 1 while a < n: result.append(a) a, b = b, a + b return result ``` 现在,进入 Python 解释器,用以下命令导入该模块: ```python import fibo ``` 此操作不会直接把 `fibo` 中定义的函数名称添加到当前 [namespace](https://docs.python.org/zh-cn/3.13/glossary.html#term-namespace) 中(请参阅 Python [作用域和命名空间](https://docs.python.org/zh-cn/3.13/tutorial/classes.html#tut-scopes) 了解详情); 它只是将模块名称 fibo 添加到那里。 使用该模块名称你可以访问其中的函数: ```python fibo.fib(1000) # 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 fibo.fib2(100) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] fibo.__name__ # 'fibo' ``` 如果经常使用某个函数,可以把它赋值给局部变量: ```python fib = fibo.fib fib(500) # 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 ``` 试一试: :::playground#python 模块示例 @file main.py ```python import fibo fibo.fib(1000) print(fibo.fib2(100)) print(fibo.__name__) ``` @file fibo.py ```python # 斐波那契数列模块 def fib(n): """Write Fibonacci series up to n.""" a, b = 0, 1 while a < n: print(a, end=' ') a, b = b, a + b print() def fib2(n): """Return Fibonacci series up to n.""" result = [] a, b = 0, 1 while a < n: result.append(a) a, b = b, a + b return result ``` ::: ## 6.1. 模块详解 模块包含可执行语句及函数定义。这些语句用于初始化模块,且仅在 `import` 语句 第一次 遇到模块名时执行。 文件作为脚本运行时,也会执行这些语句。 每个模块都有自己的私有命名空间,它会被用作模块中定义的所有函数的全局命名空间。 因此,模块作者可以在模块内使用全局变量而不必担心与用户的全局变量发生意外冲突。 另一方面,如果您知道自己在做什么,您可以使用与引用模块函数相同的语法去访问一个模块的全局变量,即 `modname.itemname` 。 模块可以导入其他模块。根据惯例可以将所有 `import` 语句都放在模块(或者也可以说是脚本)的开头但这并非强制要求。 如果被放置于一个模块的最高层级,则被导入的模块名称会被添加到该模块的全局命名空间。 还有一种 import 语句的变化形式可以将来自某个模块的名称直接导入到导入方模块的命名空间中。例如: ```python from fibo import fib, fib2 fib(500) # 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 ``` 这条语句不会将所导入的模块的名称引入到局部命名空间中(因此在本示例中,`fibo` 将是未定义的名称)。 模块名后使用 `as` 时,直接把 `as` 后的名称与导入模块绑定。 ```python import fibo as fib fib.fib(500) # 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 ``` `from` 中也可以使用这种方式,效果类似: ```python from fibo import fib as fibonacci fibonacci(500) # 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 ``` :::warning 为了保证运行效率,每次解释器会话只导入一次模块。 如果更改了模块内容,必须重启解释器; 仅交互测试一个模块时,也可以使用 [`importlib.reload()`](https://docs.python.org/zh-cn/3.13/library/importlib.html#importlib.reload),例如 `import importlib; importlib.reload(modulename)`。 ::: ### 6.1.1. 以脚本方式执行模块 可以用以下方式运行 Python 模块: ```shell python fibo.py ``` 这项操作将执行模块里的代码,和导入模块一样,但会把 `__name__` 赋值为 `"__main__"`。 也就是把下列代码添加到模块末尾: ```python if __name__ == "__main__": import sys fib(int(sys.argv[1])) ``` 这个文件既能被用作脚本,又能被用作一个可供导入的模块,因为解析命令行参数的那两行代码只有在模块作为“main”文件执行时才会运行: ```shell python fibo.py 50 # 0 1 1 2 3 5 8 13 21 34 ``` 当这个模块被导入到其它模块时,那两行代码不运行。 这常用于为模块提供一个便捷的用户接口,或用于测试(把模块作为执行测试套件的脚本运行)。 ### 6.1.2. 模块搜索路径 当导入一个名为 `spam` 的模块时,解释器首先会搜索具有该名称的内置模块。这些模块的名称在 [sys.builtin\_module\_names](https://docs.python.org/zh-cn/3.13/library/sys.html#sys.builtin_module_names) 中列出。 如果未找到,它将在变量 [sys.path](https://docs.python.org/zh-cn/3.13/library/sys.html#sys.path) 所给出的目录列表中搜索名为 `spam.py` 的文件。 [sys.path](https://docs.python.org/zh-cn/3.13/library/sys.html#sys.path) 是从这些位置初始化的: * 被命令行直接运行的脚本所在的目录(或未指定文件时的当前目录)。 * [PYTHONPATH](https://docs.python.org/zh-cn/3.13/using/cmdline.html#envvar-PYTHONPATH) (目录列表,与 `shell` 变量 `PATH` 的语法一样)。 * 依赖安装的默认值(按照惯例包括一个 `site-packages` 目录,由 `site` 模块处理)。 更多细节请参阅 [`sys.path` 模块搜索路径的初始化](https://docs.python.org/zh-cn/3.13/library/sys_path_init.html#sys-path-init)。 初始化后,Python 程序可以更改 `sys.path`。脚本所在的目录先于标准库所在的路径被搜索。 这意味着,脚本所在的目录如果有和标准库同名的文件,那么加载的是该目录里的,而不是标准库的。 这一般是一个错误,除非这样的替换是你有意为之。详见 [标准模块](https://docs.python.org/zh-cn/3.13/tutorial/modules.html#tut-standardmodules)。 ### 6.1.3. 已编译的 Python 文件 为了快速加载模块,Python 把模块的编译版本缓存在 `__pycache__` 目录中,文件名为 `module.version.pyc`,`version` 对编译文件格式进行编码,一般是 Python 的版本号。 例如,CPython 的 3.3 发行版中,`spam.py` 的编译版本缓存为 `__pycache__/spam.cpython-33.pyc`。这种命名惯例让不同 Python 版本编译的模块可以共存。 Python 在两种情况下不检查缓存。 1. 从命令行直接载入的模块,每次都会重新编译,且不储存编译结果; 2. 没有源模块,就不会检查缓存。 为了让一个库能以隐藏源代码的形式分发(通过将所有源代码变为编译后的版本),编译后的模块必须放在源目录而非缓存目录中,并且源目录绝不能包含同名的未编译的源模块。 给专业人士的一些小建议: * 在 Python 命令中使用 [`-O`](https://docs.python.org/zh-cn/3.13/using/cmdline.html#cmdoption-O) 或 [`-OO`](https://docs.python.org/zh-cn/3.13/using/cmdline.html#cmdoption-OO) 开关,可以减小编译模块的大小。`-O` 去除断言语句,`-OO` 去除断言语句和 `__doc__` 字符串。 有些程序可能依赖于这些内容,因此,没有十足的把握,不要使用这两个选项。“优化过的”模块带有 `opt-` 标签,并且文件通常会一小些。将来的发行版或许会改进优化的效果。 * 从 `.pyc` 文件读取的程序不比从 `.py` 读取的执行速度快,`.pyc` 文件只是加载速度更快。 * [`compileall`](https://docs.python.org/zh-cn/3.13/library/compileall.html#module-compileall) 模块可以为一个目录下的所有模块创建 `.pyc` 文件。 ## 6.2. 标准模块 Python 自带一个标准模块的库,它在 Python 库参考(此处以下称为"库参考" )里另外描述。 一些模块是内嵌到解释器里面的, 它们给一些虽并非语言核心但却内嵌的操作提供接口,要么是为了效率,要么是给操作系统基础操作例如系统调入提供接口。 一个特别值得注意的模块 [`sys`](https://docs.python.org/zh-cn/3.13/library/sys.html#module-sys),它被内嵌到每一个 Python 解释器中。`sys.ps1` 和 `sys.ps2` 变量定义了一些字符,它们可以用作主提示符和辅助提示符: ```python import sys sys.ps1 # '>>> ' sys.ps2 # '... ' sys.ps1 = 'C> ' # C> print('Yuck!') # Yuck! # C> ``` 只有解释器用于交互模式时,才定义这两个变量。 变量 `sys.path` 是字符串列表,用于确定解释器的模块搜索路径。该变量以环境变量 `PYTHONPATH` 提取的默认路径进行初始化,如未设置 `PYTHONPATH`,则使用内置的默认路径。 可以用标准列表操作修改该变量: ```python import sys sys.path.append('/ufs/guido/lib/python') ``` ## 6.3. `dir()` 函数 内置函数 [`dir()`](https://docs.python.org/zh-cn/3.13/library/functions.html#dir) 用于查找模块定义的名称。返回结果是经过排序的字符串列表: ```python import fibo, sys dir(fibo) # ['__name__', 'fib', 'fib2'] ``` 没有参数时,`dir()` 列出当前已定义的名称: ```python a = [1, 2, 3, 4, 5] import fibo fib = fibo.fib dir() # ['__builtins__', '__name__', 'a', 'fib', 'fibo', 'sys'] dir(sys) # ['__breakpointhook__', '__displayhook__', '__doc__', '__excepthook__', ...] ``` 注意它列出所有类型的名称:变量,模块,函数,……。 `dir()` 不会列出内置函数和变量的名称。这些内容的定义在标准模块 [`builtins`](https://docs.python.org/zh-cn/3.13/library/builtins.html#module-builtins) 中: ```python import builtins dir(builtins) # ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', ...] ``` ## 6.4. 包 包是通过使用“带点号模块名”来构造 Python 模块命名空间的一种方式。 例如,模块名 `A.B` 表示名为 `A` 的包中名为 `B` 的子模块。 就像使用模块可以让不同模块的作者不必担心彼此的全局变量名一样,使用带点号模块名也可以让 NumPy 或 Pillow 等多模块包的作者也不必担心彼此的模块名冲突。 假设要为统一处理声音文件与声音数据设计一个模块集(“包”)。 声音文件的格式很多(通常以扩展名来识别,例如:`.wav`,`.aiff`,`.au`),因此,为了不同文件格式之间的转换,需要创建和维护一个不断增长的模块集合。 为了实现对声音数据的不同处理(例如,混声、添加回声、均衡器功能、创造人工立体声效果),还要编写无穷无尽的模块流。 下面这个分级文件树展示了这个包的架构: ```txt sound/ 最高层级的包 __init__.py 初始化 sound 包 formats/ 用于文件格式转换的子包 __init__.py wavread.py wavwrite.py aiffread.py aiffwrite.py auread.py auwrite.py ... effects/ 用于音效的子包 __init__.py echo.py surround.py reverse.py ... filters/ 用于过滤器的子包 __init__.py equalizer.py vocoder.py karaoke.py ... ``` 导入包时,Python 搜索 `sys.path` 里的目录,查找包的子目录。 需要有 `__init__.py` 文件才能让 Python 将包含该文件的目录当作包来处理(除非使用 [namespace package](https://docs.python.org/zh-cn/3.13/glossary.html#term-namespace-package),这是一个相对高级的特性)。 这可以防止重名的目录如 `string` 在无意中屏蔽后继出现在模块搜索路径中的有效模块。 在最简单的情况下,`__init__.py` 可以只是一个空文件,但它也可以执行包的初始化代码或设置 `__all__` 变量,这将在稍后详细描述。 还可以从包中导入单个模块,例如: ```python import sound.effects.echo ``` 这将加载子模块 `sound.effects.echo`。 它必须通过其全名来引用。 ```python sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)\ ``` 另一种导入子模块的方法是 : ```python from sound.effects import echo ``` 这也会加载子模块 echo,并使其不必加包前缀,因此可按如下方式使用: ```python echo.echofilter(input, output, delay=0.7, atten=4) ``` Import 语句的另一种变体是直接导入所需的函数或变量: ```python from sound.effects.echo import echofilter ``` 同样,这将加载子模块 echo,但这使其函数 echofilter() 直接可用: ```python echofilter(input, output, delay=0.7, atten=4) ``` 注意,使用 `from package import item` 时,`item` 可以是包的子模块(或子包),也可以是包中定义的函数、类或变量等其他名称。 `import` 语句首先测试包中是否定义了 `item`;如果未在包中定义,则假定 `item` 是模块,并尝试加载。如果找不到 `item`,则触发 `ImportError` 异常。 相反,使用 `import item.subitem.subsubitem` 句法时,除最后一项外,每个 `item` 都必须是包;最后一项可以是模块或包,但不能是上一项中定义的类、函数或变量 ### 6.4.1. 从包中导入 `*` 使用 `from sound.effects import *` 时会发生什么?你可能希望它会查找并导入包的所有子模块,但事实并非如此。因为这将花费很长的时间,并且可能会产生你不想要的副作用。 唯一的解决办法是提供包的显式索引。 `import` 语句使用如下惯例:如果包的 `__init__.py` 代码定义了列表 `__all__`,运行 `from package import *` 时,它就是被导入的模块名列表。 发布包的新版本时,包的作者应更新此列表。 如果包的作者认为没有必要在包中执行导入 `*` 操作,也可以不提供此列表。 例如,`sound/effects/__init__.py` 文件可以包含以下代码: ```python __all__ = ["echo", "surround", "reverse"] ``` 这意味着 `from sound.effects import *` 将导入 `sound.effects` 包的三个命名子模块。 请注意子模块可能会受到本地定义名称的影响。 例如,如果你在 `sound/effects/__init__.py` 文件中添加了一个 `reverse` 函数,`from sound.effects import *` 将只导入 `echo` 和 `surround` 这两个子模块,但 **不会** 导入 `reverse` 子模块,因为它被本地定义的 `reverse` 函数所遮挡: ```python __all__ = [ "echo", # 指向 'echo.py' 文件 "surround", # 指向 'surround.py' 文件 "reverse", # !!! 现在指向 'reverse' 函数 !!! ] def reverse(msg: str): # <-- 此名称将覆盖 'reverse.py' 子模块 return msg[::-1] # 针对 'from sound.effects import *' 的情况 ``` 如果没有定义 `__all__`,`from sound.effects import *` 语句 **不会** 把包 `sound.effects` 中的所有子模块都导入到当前命名空间;它只是确保包 `sound.effects` 已被导入(可能还会运行 `__init__.py` 中的任何初始化代码),然后再导入包中定义的任何名称。 虽然,可以把模块设计为用 `import *` 时只导出遵循指定模式的名称,但仍不提倡在生产代码中使用这种做法。 记住,使用 `from package import specific_submodule` 没有任何问题! 实际上,除了导入模块使用不同包的同名子模块之外,这种方式是推荐用法。 ### 6.4.2. 相对导入 当包由多个子包构成(如示例中的 `sound` 包)时,可以使用绝对导入来引用同级包的子模块。 例如,如果 `sound.filters.vocoder` 模块需要使用 `sound.effects` 包中的 `echo` 模块,它可以使用 `from sound.effects import echo`。 你还可以编写相对导入代码,即使用 `from module import name` 形式的 `import` 语句。 这些导入使用前导点号来表示相对导入所涉及的当前包和上级包。 例如对于 `surround` 模块,可以使用: ```python from . import echo from .. import formats from ..filters import equalizer ``` Note that relative imports are based on the name of the current module's package. Since the main module does not have a package, modules intended for use as the main module of a Python application must always use absolute imports. ### 6.4.3. 多目录中的包 包还支持一个特殊的属性,[`__path__`](https://docs.python.org/zh-cn/3.13/reference/datamodel.html#module.__path__) 。 在执行该文件中的代码之前,它被初始化为字符串的 [sequence](https://docs.python.org/zh-cn/3.13/glossary.html#term-sequence),其中包含包的 `__init__.py` 的目录名称。 这个变量可以修改;修改后会影响今后对模块和包中包含的子包的搜索。 这个功能虽然不常用,但可用于扩展包中的模块集。 --- --- url: 'https://zhaobc.site/notes/backend/python/tutorial/7.input-output.md' --- # 7. 输入输出 ## 7.1. 更复杂的输出格式 对输出格式的控制不只是打印空格分隔的值,还需要更多方式。格式化输出包括以下几种方法。 * 使用 [格式化字符串字面值](https://docs.python.org/zh-cn/3.13/tutorial/inputoutput.html#tut-f-strings) ,要在字符串开头的引号/三引号前添加 `f` 或 `F` 。 在这种字符串中,可以在 `{` 和 `}` 字符之间输入引用的变量,或字面值的 Python 表达式。 ```python year = 2016 event = 'Referendum' f'Results of the {year} {event}' # 'Results of the 2016 Referendum' ``` * 字符串的 [`str.format()`](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.format) 方法需要更多手动操作。 你仍将使用 `{` 和 `}` 来标记变量将被替换的位置并且可以提供详细的格式化指令,但你还需要提供待格式化的信息。 下面的代码块中有两个格式化变量的例子: ```python yes_votes = 42_572_654 total_votes = 85_705_149 percentage = yes_votes / total_votes '{:-9} YES votes {:2.2%}'.format(yes_votes, percentage) # ' 42572654 YES votes 49.67%' ``` 请注意Notice how the `yes_votes` 填充了空格并且只为负数添加了负号。 这个例子还打印了 `percentage` 乘以 100 的结果,保留 2 个数位并带有一个百分号 (请参阅 [格式规格迷你语言](https://docs.python.org/zh-cn/3.13/library/string.html#formatspec) 了解详情)。 * 最后,还可以用字符串切片和合并操作完成字符串处理操作,创建任何排版布局。字符串类型还支持将字符串按给定列宽进行填充,这些方法也很有用。 如果不需要花哨的输出,只想快速显示变量进行调试,可以用 [`repr()`](https://docs.python.org/zh-cn/3.13/library/functions.html#repr) 或 [`str()`](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str) 函数把值转化为字符串。 `str()` 函数返回供人阅读的值,`repr()` 则生成适于解释器读取的值(如果没有等效的语法,则强制执行 `SyntaxError`)。 对于没有支持供人阅读展示结果的对象, `str()` 返回与 `repr()` 相同的值。一般情况下,数字、列表或字典等结构的值,使用这两个函数输出的表现形式是一样的。字符串有两种不同的表现形式。 示例如下: ```python s = 'Hello, world.' str(s) # 'Hello, world.' repr(s) # "'Hello, world.'" str(1/7) # '0.14285714285714285' repr(1/7) # '0.14285714285714285' # The repr() of a string adds string quotes and backslashes: hello = 'hello, world\n' hellos = repr(hello) print(hellos) # 'hello, world\n' x = 10 * 3.25 y = 200 * 200 # The argument to repr() may be any Python object: repr((x, y, ('spam', 'eggs'))) # "(32.5, 40000, ('spam', 'eggs'))" ``` `string` 模块包含 [`Template`](https://docs.python.org/zh-cn/3.13/library/string.html#string.Template) 类,提供了将值替换为字符串的另一种方法。该类使用 `$x` 占位符,并用字典的值进行替换,但对格式控制的支持比较有限。 ### 7.1.1. 格式化字符串字面值 [格式化字符串字面值](https://docs.python.org/zh-cn/3.13/reference/lexical_analysis.html#f-strings) (简称为 f-字符串)在字符串前加前缀 `f` 或 `F`,通过 `{expression}` 表达式,把 Python 表达式的值添加到字符串内。 格式说明符是可选的,写在表达式后面,可以更好地控制格式化值的方式。下例将 pi 舍入到小数点后三位: ```python import math print(f'The value of pi is approximately {math.pi:.3f}') # The value of pi is approximately 3.142. ``` 在 `':'` 后传递整数,为该字段设置最小字符宽度,常用于列对齐: ```python table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 7678} for name, phone in table.items(): print(f'{name:10} ==> {phone:10d}') # Sjoerd ==> 4127 # Jack ==> 4098 # Dcab ==> 7678 ``` 还有一些修饰符可以在格式化前转换值。 `'!a'` 应用 `ascii()` ,`'!s'` 应用 `str()`,`'!r'` 应用 `repr()`: ```python animals = 'eels' print(f'My hovercraft is full of {animals}.') # My hovercraft is full of eels. print(f'My hovercraft is full of {animals!r}.') # My hovercraft is full of 'eels'. ``` `=` 说明符可被用于将一个表达式扩展为表达式文本、**等号再加表达式求值结果** 的形式。 ```python bugs = 'roaches' count = 13 area = 'living room' print(f'Debugging {bugs=} {count=} {area=}') # Debugging bugs='roaches' count=13 area='living room' ``` 请参阅 [自说明型表达式](https://docs.python.org/zh-cn/3.13/whatsnew/3.8.html#bpo-36817-whatsnew) 以了解 `=` 说明符的更多信息。 有关这些格式说明的详情,请查看针对 [格式规格迷你语言](https://docs.python.org/zh-cn/3.13/library/string.html#formatspec) 的参考指南。 ### 7.1.2. 字符串 `format()` 方法 [`str.format()`](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.format) 方法的基本用法如下所示: ```python print('We are the {} who say "{}!"'.format('knights', 'Ni')) # We are the knights who say "Ni!" ``` 花括号及之内的字符(称为格式字段)被替换为传递给 `str.format()` 方法的对象。花括号中的数字表示传递给 `str.format()` 方法的对象所在的位置。 ```python print('{0} and {1}'.format('spam', 'eggs')) # spam and eggs print('{1} and {0}'.format('spam', 'eggs')) # eggs and spam ``` `str.format()` 方法中使用关键字参数名引用值。 ```python print('This {food} is {adjective}.'.format(food='spam', adjective='absolutely horrible')) # This spam is absolutely horrible. ``` 位置参数和关键字参数可以任意组合: ```python print('The story of {0}, {1} and {other}.'.format('Bill', 'Manfred', other='Georg')) # The story of Bill, Manfred and Georg. ``` 如果不想分拆较长的格式字符串,最好按名称引用变量进行格式化,不要按位置。这项操作可以通过传递字典,并用方括号 `'[]'` 访问键来完成。 ```python table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678} print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; Dcab: {0[Dcab]:d}'.format(table)) # Jack: 4098; Sjoerd: 4127; Dcab: 8637678 ``` zhaobc 注:因为 `format` 只有一个参数,而且是字典,所以 `0[Jack]` 代表第一个参数取 `Jack` 键。 而 `0[Jack]:d` 的 `:d` 代表使用十进制整数格式化。 这也可以通过将 table 字典作为采用 `**` 标记的关键字参数传入来实现。 ```python table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678} print('Jack: {Jack:d}; Sojoerd: {Sojerd:d}; Dcab: {Dcab:d}'.format(**tab)) # Jack: 4098; Sjoerd: 4127; Dcab: 8637678 ``` 与内置函数 [`vars()`](https://docs.python.org/zh-cn/3.13/library/functions.html#vars) 一同使用时这种方式非常实用,它将返回一个包含所有局部变量的字典: ```python vars() # {'__name__': '__main__', '__doc__': None, '__package__': '_pyrepl', '__loader__': ... } table = {k: str(v) for k, v in vars().items()} message = " ".join([f'{k}: ' + '{' + k + '};' for k in table.keys()]) print(message.format(**table)) # __name__: __main__; __doc__: None; __package__: _pyrepl; __loader__: ... ``` 举个例子,以下几行代码将产生一组整齐的数据列,包含给定的整数及其平方与立方: ```python for x in range(1, 11): print('{0:2d} {1:3d} {2:4d}'.format(x, x*x, x*x*x)) # 1 1 1 # 2 4 8 # 3 9 27 # 4 16 64 # 5 25 125 # 6 36 216 # 7 49 343 # 8 64 512 # 9 81 729 # 10 100 1000 ``` [`str.format()`](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.format) 进行字符串格式化的完整概述详见 [格式字符串语法](https://docs.python.org/zh-cn/3.13/library/string.html#formatstrings) 。 ### 7.1.3. 手动格式化字符串 下面是使用手动格式化方式实现的同一个平方和立方的表: ```python for x in range(1, 11): print(repr(x).rjust(2), repr(x*x).rjust(3), end=' ') print(repr(x*x*x).rjust(4)) # 1 1 1 # 2 4 8 # 3 9 27 # 4 16 64 # 5 25 125 # 6 36 216 # 7 49 343 # 8 64 512 # 9 81 729 # 10 100 1000 ``` (注意,每列之间的空格是通过使用 `print()` 添加的:它总在其参数间添加空格。) 字符串对象的 [`str.rjust()`](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.rjust) 方法通过在左侧填充空格,对给定宽度字段中的字符串进行右对齐。 同类方法还有 `str.ljust()` 和 `str.center()` 。这些方法不写入任何内容,只返回一个新字符串,如果输入的字符串太长,它们不会截断字符串,而是原样返回; 如果真的想截断字符串,可以使用 `x.ljust(n)[:n]` 这样的切片操作 。 另一种方法是 [`str.zfill()`](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.zfill) ,该方法在数字字符串左边填充零,且能识别正负号: ```python '12'.zfill(5) # '00012' '-3.14'.zfill(7) # '-003.14' '3.14159265359'.zfill(5) # '3.14159265359' ``` ### 7.1.4. 旧式字符串格式化方法 `%` 运算符 (求余) 也可被用于字符串格式化。 给定 `format % values` (其中 format 是一个字符串),则 format 中的 `%` 转换占位符将以 values 中的零个或多个元素来替换。 此操作通常称为字符串插值。 ```python import math print('The value of pi is approximately %5.3f.' % math.pi) # The value of pi is approximately 3.142. ``` [`printf` 风格的字符串格式化](https://docs.python.org/zh-cn/3.13/library/stdtypes.html#old-string-formatting) 小节介绍更多相关内容。 ## 7.2. 读写文件 [`open()`](https://docs.python.org/zh-cn/3.13/library/functions.html#open) 返回一个 [file object](https://docs.python.org/zh-cn/3.13/glossary.html#term-file-object) ,最常使用的是两个位置参数和一个关键字参数:`open(filename, mode, encoding=None)` ```python f = open('workfile', 'w', encoding="utf-8") ``` 第一个实参是文件名字符串。 第二个实参是包含描述文件使用方式字符的字符串。 mode 的值包括 'r' ,表示文件只能读取; 'w' 表示只能写入(现有同名文件会被覆盖); 'a' 表示打开文件并追加内容,任何写入的数据会自动添加到文件末尾。 'r+' 表示打开文件进行读写。 mode 实参是可选的,省略时的默认值为 'r'。 通常情况下,文件是以 text mode 打开的,也就是说,你从文件中读写字符串,这些字符串是以特定的 encoding 编码的。 如果没有指定 encoding ,默认的是与平台有关的(见 `open()` )。 在模式后面加上一个 'b' ,可以用 binary mode 打开文件。 二进制模式的数据是以 bytes 对象的形式读写的。在二进制模式下打开文件时,你不能指定 encoding 。 在文本模式下读取文件时,默认把平台特定的行结束符(Unix 上为 `\n`, Windows 上为 `\r\n`)转换为 `\n`。 在文本模式下写入数据时,默认把 `\n` 转换回平台特定结束符。 这种操作方式在后台修改文件数据对文本文件来说没有问题,但会破坏 JPEG 或 EXE 等二进制文件中的数据。注意,在读写此类文件时,一定要使用二进制模式。 在处理文件对象时,最好使用 [`with`](https://docs.python.org/zh-cn/3.13/reference/compound_stmts.html#with) 关键字。 优点是,子句体结束后,文件会正确关闭,即便触发异常也可以。 而且,使用 `with` 相比等效的 [`try`](https://docs.python.org/zh-cn/3.13/reference/compound_stmts.html#try)-[`finally`](https://docs.python.org/zh-cn/3.13/reference/compound_stmts.html#finally) 代码块要简短得多: ```python with open('workfile', encoding="utf-8") as f: read_data = f.read() # 们可以检测文件是否已被自动关闭。 f.closed # True ``` 如果没有使用 `with` 关键字,则应调用 `f.close()` 关闭文件,即可释放文件占用的系统资源。 :::warning 调用 `f.write()` 时,未使用 `with` 关键字,或未调用 `f.close()`,即使程序正常退出,也 **可能** 导致 `f.write()` 的参数没有完全写入磁盘。 ::: 通过 `with` 语句,或调用 `f.close()` 关闭文件对象后,再次使用该文件对象将会失败。 ### 7.2.1. 文件对象方法 `f.read(size)` 可用于读取文件内容,它会读取一些数据,并返回字符串(文本模式),或字节串对象(在二进制模式下)。 `size` 是可选的数值参数。省略 `size` 或 `size` 为负数时,读取并返回整个文件的内容;文件大小是内存的两倍时,会出现问题。 `size` 取其他值时,读取并返回最多 `size` 个字符(文本模式)或 `size` 个字节(二进制模式)。 如已到达文件末尾,`f.read()` 返回空字符串('')。 `f.readline()` 从文件中读取单行数据;字符串末尾保留换行符(`\n`),只有在文件不以换行符结尾时,文件的最后一行才会省略换行符。 这种方式让返回值清晰明确;只要 `f.readline()` 返回空字符串,就表示已经到达了文件末尾,空行使用 `'\n'` 表示,该字符串只包含一个换行符。 从文件中读取多行时,可以用循环遍历整个文件对象。这种操作能高效利用内存,快速,且代码简单: ```python for line in f: print(line, end='') ``` 如需以列表形式读取文件中的所有行,可以用 `list(f)` 或 `f.readlines()`。 `f.write(string)` 把 string 的内容写入文件,并返回写入的字符数。 写入其他类型的对象前,要先把它们转化为字符串(文本模式)或字节对象(二进制模式): ```python value = ('the answer', 42) s = str(value) # # 将元组转换为字符串 # "('the answer', 42)" f.write(s) # 18 ``` `f.tell()` 返回整数,给出文件对象在文件中的当前位置,表示为二进制模式下时从文件开始的字节数,以及文本模式下的意义不明的数字。 `f.seek(offset, whence)` 可以改变文件对象的位置。通过向参考点添加 `offset` 计算位置;参考点由 `whence` 参数指定。 `whence` 值为 0 时,表示从文件开头计算,1 表示使用当前文件位置,2 表示使用文件末尾作为参考点。 省略 `whence` 时,其默认值为 0,即使用文件开头作为参考点。 ```python f = open('workfile', 'rb+') f.write(b'0123456789abcdef') # 16 f.seek(5) # 定位到文件中的第 6 个字节 # b'5' f.seek(-3, 2) # 定位到倒数第 3 个字节 # 13 f.read(1) # b'd' ``` 在文本文件(模式字符串未使用 `b` 时打开的文件)中,只允许相对于文件开头搜索(使用 `seek(0, 2)` 搜索到文件末尾是个例外),唯一有效的 `offset` 值是能从 `f.tell()` 中返回的,或 0。其他 offset 值都会产生未定义的行为。 文件对象还有一些额外的方法,如使用频率较低的 [`isatty()`](https://docs.python.org/zh-cn/3.13/library/io.html#io.IOBase.isatty) 和 [`truncate()`](https://docs.python.org/zh-cn/3.13/library/io.html#io.IOBase.truncate) 等;有关文件对象的完整指南请查阅标准库参考。 ## 7.2.2. 使用 [json](https://docs.python.org/zh-cn/3.13/library/json.html#module-json) 保存结构化数据 字符串可以很容易地写入文件或从文件中读取。当你想要保存嵌套列表和字典等更复杂的数据类型时,手动执行解析和序列化操作将会变得非常复杂。 只需一行简单的代码即可查看某个对象的 JSON 字符串表现形式: ```python import json x = [1, 'simple', 'list'] json.dumps(x) # '[1, "simple", "list"]' ``` [`dumps()`](https://docs.python.org/zh-cn/3.13/library/json.html#json.dumps) 函数还有一个变体, [`dump()`](https://docs.python.org/zh-cn/3.13/library/json.html#json.dump) ,它只将对象序列化为 [text file](https://docs.python.org/zh-cn/3.13/glossary.html#term-text-file) 。因此,如果 f 是 text file 对象,可以这样做: ```python json.dump(x, f) ``` 要再次解码对象,如果 f 是已打开、供读取的 [binary file](https://docs.python.org/zh-cn/3.13/glossary.html#term-binary-file) 或 text file 对象: ```python x = json.load(f) ``` :::note JSON文件必须以 `UTF-8` 编码。当打开JSON文件作为一个 text file 用于读写时,使用 `encoding="utf-8"` 。 ::: --- --- url: 'https://zhaobc.site/notes/backend/python/tutorial/8.errors.md' --- # 8. 错误和异常 ## 8.1. 语法错误 语法错误又称解析错误。 解析器会重复出错的行并显示指向检测到错误的位置的小箭头。 请注意这并不一定是需要被修复的位置。 ## 8.2. 异常 即使语句或表达式使用了正确的语法,执行时仍可能触发错误。执行时检测到的错误称为 *异常* 。 [内置异常](https://docs.python.org/zh-cn/3.13/library/exceptions.html#bltin-exceptions) 列出了内置异常及其含义。 ## 8.3. 异常的处理 可以编写程序处理选定的异常。下例会要求用户一直输入内容,直到输入有效的整数,但允许用户中断程序(使用 Control-C 或操作系统支持的其他操作); 注意,用户中断程序会触发 [`KeyboardInterrupt`](https://docs.python.org/zh-cn/3.13/library/exceptions.html#KeyboardInterrupt) 异常。 :::playground#python 输入异常处理 @file main.py ```python while True: try: x = int(input("Please enter a number: ")) break except ValueError: print("Oops! That was no valid number. Try again...") ``` ::: [`try`](https://docs.python.org/zh-cn/3.13/reference/compound_stmts.html#try) 语句的工作原理如下: * 首先,执行 `try` 子句 (`try` 和 [`except`](https://docs.python.org/zh-cn/3.13/reference/compound_stmts.html#except) 关键字之间的(多行)语句)。 * 如果没有触发异常,则跳过 `except` 子句,`try` 语句执行完毕。 * 如果在执行 `try` 子句时发生了异常,则跳过该子句中剩下的部分。 如果异常的类型与 `except` 关键字后指定的异常相匹配,则会执行 `except` 子句,然后跳到 `try/except` 代码块之后继续执行。 * 如果发生的异常与 `except` 子句 中指定的异常不匹配,则它会被传递到外层的 `try` 语句中;如果没有找到处理器,则它是一个 未处理异常 且执行将停止并输出一条错误消息。 `try` 语句可以有多个 `except` 子句 来为不同的异常指定处理程序。但最多只有一个处理程序会被执行。 处理程序只处理对应的 `try` 子句 中发生的异常,而不处理同一 `try` 语句内其他处理程序中的异常。 `except` 子句 可以用带圆括号的元组来指定多个异常,例如: ```python except (RuntimeError, TypeError, NameError): pass ``` 一个 `except` 子句中的类匹配的异常将是该类本身的实例或其所派生的类的实例(但反过来则不可以 --- 列出派生类的 `except` 子句 不会匹配其基类的实例)。 例如,下面的代码将依次打印 B, C, D: ```python class B(Exception): pass class C(B): pass class D(C): pass for cls in [B, C, D]: try: raise cls() except D: print("D") except C: print("C") except B: print("B") ``` 请注意如果颠倒 `except` 子句 的顺序(把 `except B` 放在最前),则会输出 `B`, `B`, `B` --- 即触发了第一个匹配的 `except` 子句。 发生异常时,它可能具有关联值,即异常 *参数* 。是否需要参数,以及参数的类型取决于异常的类型。 `except` 子句可能会在异常名称后面指定一个变量。 这个变量将被绑定到异常实例,该实例通常会有一个存储参数的 `args` 属性。 为了方便起见,内置异常类型定义了 [`__str__()`](https://docs.python.org/zh-cn/3.13/reference/datamodel.html#object.__str__) 来打印所有参数而不必显式地访问 `.args`。 :::playground#python 异常参数 @file main.py ```python try: raise Exception('spam', 'eggs') except Exception as inst: print(type(inst)) # 异常的类型 print(inst.args) # 参数保存在 .args 中 print(inst) # __str__ 允许 args 被直接打印 # 但可能在异常子类中被覆盖 x, y = inst .args # 解包 args print('x = ', x) print('y = ', y) # # ('spam', 'eggs') # ('spam', 'eggs') # x = spam # y = eggs ``` ::: 未处理异常的 `__str__()` 输出会被打印为该异常消息的最后部分 ('detail')。 [BaseException](https://docs.python.org/zh-cn/3.13/library/exceptions.html#BaseException) 是所有异常的共同基类。它的一个子类, [Exception](https://docs.python.org/zh-cn/3.13/library/exceptions.html#Exception) ,是所有非致命异常的基类。 不是 `Exception` 的子类的异常通常不被处理,因为它们被用来指示程序应该终止。 它们包括由 `sys.exit()` 引发的 `SystemExit` ,以及当用户希望中断程序时引发的 `KeyboardInterrupt` 。 `Exception` 可以被用作通配符,捕获(几乎)一切。然而,好的做法是,尽可能具体地说明我们打算处理的异常类型,并允许任何意外的异常传播下去。 处理 `Exception` 最常见的模式是打印或记录异常,然后重新提出(允许调用者也处理异常): :::playground#python 重新提出异常 @file main.py ```python import sys try: f = open('myfile.txt') s = f.readline() i = int(s.strip()) except OSError as err: print("OS error: ", err) except ValueError: print("Could not convert data to an integer.") except Exception as err: print(f"Unexpected {err=}, {type(err)=}") raise ``` @file myfile.txt ```txt test ``` ::: `try` ... `except` 语句具有可选的 `else` 子句,该子句如果存在,它必须放在所有 `except` 子句 之后。 它适用于 `try` 子句 没有引发异常但又必须要执行的代码。例如: ```python for arg in sys.argv[1:]: try: f = open(arg, 'r') except OSError: print('cannot open', arg) else: print(arg, 'has', len(f.readlines()), 'lines') f.close() ``` 使用 `else` 子句比向 `try` 子句添加额外的代码要好,可以避免意外捕获非 `try` ... `except` 语句保护的代码触发的异常。 ## 8.4. 触发异常 `raise` 语句支持强制触发指定的异常。 `raise` 唯一的参数就是要触发的异常。这个参数必须是异常实例或异常类(派生自 `BaseException` 类,例如 `Exception` 或其子类)。 如果传递的是异常类,将通过调用没有参数的构造函数来隐式实例化。 ```python raise NameError('Hi There') raise ValueError # raise Value() 的简化 ``` ## 8.5. 异常链 如果一个未处理的异常发生在 `except` 部分内,它将会有被处理的异常附加到它上面,并包括在错误信息中。 为了表明一个异常是另一个异常的直接后果, raise 语句允许一个可选的 from 子句: ```python # exc 必须为异常类实例或 None raise RuntimeError from exc ``` ```python def func(): raise ConnectionError try: func() except ConnectionError as exc: raise RuntimeError('Failed to open database') from exc # Traceback (most recent call last): # File "", line 2, in # func() # ~~~~^^ # File "", line 2, in func # ConnectionError # # The above exception was the direct cause of the following exception: # # Traceback (most recent call last): # File "", line 4, in # raise RuntimeError('Failed to open database') from exc # RuntimeError: Failed to open database ``` `from None` 表达禁用自动异常链。 ```python try: open('database.sqlite') except OSError: raise RuntimeError from None # Traceback (most recent call last): # File "", line 4, in # raise RuntimeError from None # RuntimeError ``` 异常链机制详见 [内置异常](https://docs.python.org/zh-cn/3.13/library/exceptions.html#bltin-exceptions)。 ## 8.6. 用户自定义异常 程序可以通过创建新的异常类命名自己的异常(Python 类的内容详见 类)。不论是以直接还是间接的方式,异常都应从 `Exception` 类派生。 大多数异常命名都以 “Error” 结尾,类似标准异常的命名。 ## 8.7. 定义清理操作 ```python try: raise KeyboardInterrupt finally: print('Goodbye, world!') ``` 如果存在 `finally` 子句,则 `finally` 子句是 `try` 语句结束前执行的最后一项任务。不论 `try` 语句是否触发异常,都会执行 `finally` 子句。 * 如果执行 `try` 子句期间触发了某个异常,则某个 `except` 子句应处理该异常。如果该异常没有 `except` 子句处理,在 `finally` 子句执行后会被重新触发。 * `except` 或 `else` 子句执行期间也会触发异常。同样,该异常会在 `finally` 子句执行之后被重新触发。 * 如果 `finally` 子句中包含 `break`、`continue` 或 `return` 等语句,异常将不会被重新引发。 * 如果执行 `try` 语句时遇到 `break`,、`continue` 或 `return` 语句,则 `finally` 子句在执行 `break`、`continue` 或 `return` 语句之前执行。 * 如果 `finally` 子句中包含 `return` 语句,则返回值来自 `finally` 子句的某个 `return` 语句的返回值,而不是来自 `try` 子句的 `return` 语句的返回值。 ## 8.8. 预定义的清理操作 某些对象定义了不需要该对象时要执行的标准清理操作。无论使用该对象的操作是否成功,都会执行清理操作。 [`with`](https://docs.python.org/zh-cn/3.13/reference/compound_stmts.html#with) 语句支持以及时、正确的清理的方式使用文件对象: ```python with open('myfile.txt') as f: for line in f: print(line, end="") ``` 语句执行完毕后,即使在处理行时遇到问题,都会关闭文件 `f`。 ## 8.9. 引发和处理多个不相关的异常 在有些情况下,有必要报告几个已经发生的异常。这通常是在并发框架中当几个任务并行失败时的情况,但也有其他的用例,有时需要是继续执行并收集多个错误而不是引发第一个异常。 内置的 [`ExceptionGroup`](https://docs.python.org/zh-cn/3.13/library/exceptions.html#ExceptionGroup) 打包了一个异常实例的列表,这样它们就可以一起被引发。它本身就是一个异常,所以它可以像其他异常一样被捕获。 ```python def f(): excs = [OSError('error 1'), SystemError('error 2')] raise ExceptionGroup('there were problems', excs) f() # + Exception Group Traceback (most recent call last): # | File "", line 1, in # | f() # | ~^^ # | File "", line 3, in f # | raise ExceptionGroup('there were problems', excs) # | ExceptionGroup: there were problems (2 sub-exceptions) # +-+---------------- 1 ---------------- # | OSError: error 1 # +---------------- 2 ---------------- # | SystemError: error 2 # +------------------------------------ try: f() except Exception as e: print(f'caught {type(e)}: e') # caught : e ``` 通过使用 `except*` 代替 `except` ,我们可以有选择地只处理组中符合某种类型的异常。 在下面的例子中,显示了一个嵌套的异常组,每个 `except*` 子句都从组中提取了某种类型的异常,而让所有其他的异常传播到其他子句,并最终被重新引发。 ```python def f(): raise ExceptionGroup( "group1", [ OSError(1), SystemError(2), ExceptionGroup( "group2", [ OSError(3), RecursionError(4) ] ) ] ) try: f() except* OSError as e: print("There were OSErrors") except* SystemError as e: print("There were SystemErrors") # There were OSErrors # There were SystemErrors # + Exception Group Traceback (most recent call last): # | File "", line 2, in # | f() # | ~^^ # | File "", line 2, in f # | raise ExceptionGroup( # | ...<12 lines>... # | ) # | ExceptionGroup: group1 (1 sub-exception) # +-+---------------- 1 ---------------- # | ExceptionGroup: group2 (1 sub-exception) # +-+---------------- 1 ---------------- # | RecursionError: 4 # +------------------------------------ ``` 注意,嵌套在一个异常组中的异常必须是实例,而不是类型。这是因为在实践中,这些异常通常是那些已经被程序提出并捕获的异常,其模式如下: ```python excs = [] for test in tests: try: test.run() except Exception as e: excs.append(e) if excs: raise ExceptionGroup('Test Failures', excs) ``` ## 8.10. 用注释细化异常情况 当一个异常被创建以引发时,它通常被初始化为描述所发生错误的信息。在有些情况下,在异常被捕获后添加信息是很有用的。 为了这个目的,异常有一个 `add_note(note)` 方法接受一个字符串,并将其添加到异常的注释列表。 标准的回溯在异常之后按照它们被添加的顺序呈现包括所有的注释。 ```python try: raise TypeError('bad type') except Exception as e: e.add_note('Add some information') e.add_note('Add some more information') raise # Traceback (most recent call last): # File "", line 2, in # raise TypeError('bad type') # TypeError: bad type # Add some information # Add some more information ``` --- --- url: 'https://zhaobc.site/notes/backend/spring/index.md' --- # Spring 笔记 Spring 相关的学习笔记 --- --- url: 'https://zhaobc.site/notes/frontend/index.md' --- # 前端笔记 记录前端相关的学习笔记。 --- --- url: 'https://zhaobc.site/notes/frontend/js/index.md' --- # JavaScript 笔记 --- --- url: 'https://zhaobc.site/notes/frontend/js/bosn/index.md' --- # JavaScript 深入浅出 ## 前言 原视频链接:[慕课网上 Bosn 老师的《JavaScript 深入浅出》](https://www.imooc.com/learn/277) ## 目录 * [第 1 章 数据类型](./chapter01.md) * [第 2 章 表达式和运算符](./chapter02.md) * [第 3 章 语句](./chapter03.md) * [第 4 章 对象](./chapter04.md) * [第 5 章 数组](./chapter05.md) * 第 6 章 函数和作用域(函数、this) * 第 7 章 函数和作用域(闭包、作用域) * 第 8 章 OOP(上) * 第 9 章 OOP(下) * 第 10 章 正则与模式匹配 --- --- url: 'https://zhaobc.site/notes/frontend/js/bosn/chapter01.md' --- # 第 1 章 数据类型 ## 1-1 深入浅出课程介绍 略 ## 1-2 六种数据类型 原始类型(基本类型): * `number` * `string` * `boolean` * `null` * `undefined` 除此之外,JavaScript 还有一种对象类型(复合类型)`object`。 `object` 包含以下类型: * `Function` * `Array` * `Date` * ... ## 1-3 隐式转换 关于`+`和`-`: `+`隐式转为字符串,`-`隐式转换为数字。 ```js '37' - 7 // 30 '37' + 7 // 377 ``` 因此可以巧用`+`和`-`转换数据类型,比如使用`- 0`(减 0),转换为数字,使用`+ ''`(加空字符串)转换为字符串。 因此可以巧用`+`和`-`转换数据类型,比如使用`- 0`(减 0),转换为数字,使用`+ ''`(加空字符串)转换为字符串。 ## 1-4 包装对象 [1-2 六种数据类型](#_1-2-六种数据类型) 中介绍的 5 种基本类型和一种对象类型,其中 5 中基本类型都有其对应的包装类型。 当把一个基本类型作为对象使用时,JavaScript 会尝试将基本类型转换为包装对象,想定于 new 了一个临时对象,对象的值为基本类型的值。 使用基本对象访问对象的方法后,这个临时对象会被销毁掉,所以再次访问对象的方法时就会变成 undefined。 如下图: ![图片1](./images/1-4.figure01.png) ::: warning 图片名中不能出现`#`,否则会会有问题: 1. Markdown 插入图片的方式,不能显示图片 2. HTML 插入图片的方式会报错。 ::: ## 1-5 类型检查 JavaScript 中检查类型的方法有很多,比如: * `typeof` * `instanceof` * `Object.prototype.toString` * `constructor` 注意:使用 textlint 时,报`TypeError: entry.split is not a function`,所以上面加了引号。 * duck type ### `typeof` `typeof`返回一个字符串,它非常适合函数对象和基本类型的判断。 比如: | 命令 | 结果 | | -------------------: | :---------- | | typeof 100 | "number" | | typeof true | "boolean" | | typeof function() {} | "function" | | typeof Function | "function" | | typeof undefined | "undefined" | | typeof null | "object" | | typeof new Object() | "object" | | typeof \[1, 2] | "object" | | typeof NaN | "number" | ### `instanceof` `instanceof`常用于对象类型的判断,它是基于`原型链`的形式去判断的操作符。 它期望左操作树是一个对象,如果不是对象而是基本类型的话,直接返回 false。 | 命令 | 结果 | | ----------------------: | :---- | | 1 instanceof Number | false | | true instanceof Boolean | false | 它期望右操作树是一个函数对象,或函数构造器,如果不是的话就会跑出一个 TypeError 异常。 | 命令 | 结果 | | ----------------------------: | :---- | | \[1, 2] instanceof Array | true | | new Object() instanceof Array | false | `instanceof`的大概原理就是它会判断左边的操作树的对象的原型链上,是否有右边的构造函数的 prototype 属性。 例子: ::: normal-demo instanceof 示例 ```js function Person() {} function Student() {} Student.prototype = new Person() Student.prototype.constructor = Student var bosn = new Student() console.log(bosn instanceof Student) var one = new Person() console.log(one instanceof Person) console.log(one instanceof Student) console.log(bosn instanceof Person) ``` ::: 打开开发者工具看结果,或者打开 Codepen 在线调试。 ::: tip Caution! 不同 window 或 iframe 间的对象类型检查不能使用 instanceof! ::: ### `Object.prototype.toString` | 命令 | 结果 | | --------------------------------------------: | :------------------- | | Object.prototype.toString.apply(\[]) | "\[object Array]" | | Object.prototype.toString.apply(function(){}) | "\[object Function]" | | Object.prototype.toString.apply(null) | "\[object Null]" | | Object.prototype.toString.apply(undefined) | "\[object Undefined]" | ::: tip IE6/7/8 Object.prototype.toString.apply(null)返回"\[object Object]" ::: ### 类型检查小结 * `typeof` 适合基本类型及 function 检测,遇到 null 失效。 * `Object.prototype.toString` 通过 Object.prototype.toString 拿到,适合内置对象和基本类型,遇到 null 和 undefined 失效(IE6/7/8 等返回\[object Object])。 * `instanceof` 适合自定义对象,也可以用来检测原生对象,在不同 iframe 和 window 间检测时失效。 --- --- url: 'https://zhaobc.site/notes/frontend/js/bosn/chapter02.md' --- # 第 2 章 表达式和运算符 ## 2-1 表达式 ### 原始表达式 * 常量、直接量 * 关键字 * 变量 原始表达式和原始表达式可以复合成复合表达式,比如`10 * 20`就是两个原始表达式通过运算符构成了复合表达式。 ### 数组、对象的初始化表达式 比如: | 表达式 | 等价表达式 | | -----------: | :------------------------------------------- | | \[1, 2] | new Array(1, 2) | | \[1, , , 4] | \[1, undefined, undefined, 4] | | {x: 1, y: 2} | var o = new Object();o.x = 1; o.y = 2; | ### 函数表达式 把函数赋给一个变量,或者,把函数用括号阔起来直接调用,这样的就是函数表达式。 比如: ```js var f = function () {} ;(function () { console.log('Hello World!') })() ``` ### 属性访问表达式 如下: ```js var o = { x: 1 } o.x o['x'] ``` ### 调用表达式 比如:`func();`。 ### 对象创建表达式 比如: ```js new Func(1, 2) new Object() // 无参数时,括号可以省略。 ``` ## 2-2 运算符 ### 常见的运算符 * 一元运算符 比如:`+num`,`+`可以将字符串转换成数字,`-`可以将字符串转换成数字;也可以正数变负数,负数变正数。 * 二元运算符 比如:`a + b` * 三元运算符 比如:`c ? a : b`,它会将`c`强制转换为布尔型去比较。 除此之外,还有其他运算符: ![运算符](./images/2-2.figure01.png) ### 逗号运算符`,` `var val = (1, 2, 3); // val = 3` 逗号运算符是从左往右依次计算表达式的值,最后取最右边的值。 ### `delete`运算符 ```js var obj = { x: 1 } obj.x // 1 delete obj.x obj.x // undefined ``` 后来新增了一个`Object.defineProperty`,可以给对象新增属性,或者修改已经存在的属性,并返回改对象。但不是说有的属性都能`delete`,只有定义的时候,`configurable: true`的才可以`delete`。 例: ```js var obj = {} Object.defineProperty(obj, 'x', { configurable: false, value: 1, }) delete obj.x // false obj.x // 1 ``` ### `in`运算符 使用`in`运算符来判断一个对象里有没有特定的`key`。 ```js window.x = 1 'x' in window // true ``` ### instanceof, typeof 运算符 `instanceof`判断对象的类型,基于原型链判断。 `typeof`返回一个字符串,一般用来判断原始类型或者函数对象。 ```js {} instanceof Object; // true typeof 100 === 'number'; // true ``` ### `new`运算符 可以通过`new`来创建一个构造器的实例。 ```js function Foo() {} Foo.prototype.x = 1 var obj = new Foo() obj.x // 1 obj.hasOwnProperty('x') // false obj.__proto__.hasOwnProperty('x') // true ``` 可以通过`hasOwnProperty`来判断一个属性到底属于这个对象上的,还是这个对象原型链上的。 可以通过`__proto__`拿到这个对象的原型。 ### `this`运算符 `this`比较复杂,后面会详细再说。 ```js this // window(浏览器) var obj = { func: function () { return this }, } obj.func() // obj ``` 注:上面的`obj.func`,在 chrome 78.0.3904.108 中,返回`obj.func`对象。 注:可以使用`apply`改变`this`指向。 ### `void`运算符 无论`void`后面是多少,都会返回`undefined`。 ```js void 0 // undefined void 0 // undefined ``` --- --- url: 'https://zhaobc.site/notes/frontend/js/bosn/chapter03.md' --- # 第 3 章 语句 ## 3-1 block 语句、var 语句 ### 块 block 块语句常用于组合 0~多个语句。块语句用一对花括号定义。 语句: ```txt { 语句1; 语句2; ... 语句N; } ``` 请注意:没有块级作用域。 ### 定义 var ::: tip 使用一个`var`定义多个变量时,后面的变量会被隐式的创建为全局变量。 ::: 比如: ```js function foo() { var a = (b = 1) } foo() console.log(typeof a) // undifined console.log(typeof b) // number ``` 所以,如果一行里面定义多个变量时,应用逗号分割。 ## 3-2 try-catch ```js try { throw 'test' } catch (ex) { console.log(ex) // test } finally { console.log('finally') } ``` try 后面跟着 catch 或者 finally. ```js try { try { throw new Error('oops') } finally { console.log('finally') } } catch (ex) { console.error('outer', ex.message) } ``` 结果为: ```md finally outer oops ``` ## 3-3 函数、swith、循环 ### function 函数声明: ```js function fd() { // do sth. return true } ``` 函数表达式: ```js var fe = function () { // do sth. } ``` 两者比较重要的一个区别:函数声明会被预先处理,也叫函数前置,所以在函数声明的前面调用也是可以的,但是函数表达式却不可以。 也可通过`new Function`来定义,具体区别稍后讨论。 ### for...in ```js var p var obj = { x: 1, y: 2 } for (p in obj) { // do sth. } ``` ::: tip 1. 顺序不确定 2. enumerable 为 false 时不会出现 3. for in 对象属性时受原型链影响 ::: ### switch ```js var val = 2 switch (val) { case 1: case 2: case 3: console.log(123) break case 4: case 5: console.log(45) break default: console.log(0) } // 123 ``` ### 循环 ```js while (isTrue) { // do sth. } do { // do sth. } while (isTrue) var i for (i = 0; i < n; i++) { // do sth. } ``` ### with with 可以修改作用域。 ```js with ({ x: 1 }) { console.log(x) } with (document.forms[0]) { console.log(name.value) } ``` JavaScript 中,已经不建议使用 with. * 让 JS 引擎优化困难 * 可读性差 * 可被变量定义代替 * 严格模式下被禁用 ## 3-4 严格模式 严格模式是一种特殊的执行模式,它修复了部分语言上的不足,提供更强的错误检查,并增强安全性。 如何使用: ```js // 限制某个方法,在方法的第一句 function func() { 'use strict' // 好处是,如果旧的浏览器不支持严格模式,这句话会被作为一个普通的字符串 } // 也可以在.js文件的开头加 ;('use strict') function func() {} ``` 坑:'use strict'不一定非要在第一句,如果前面是'a','b'也是可以的,但是`var a; var b;`这种是不行的。 ### 不允许使用 with,SyntaxError ### 不允许未声明的变量被赋值 ```js !(function () { 'use strict' x = 1 console.log(window.x) })() // ReferenceError ``` 如果不加`'use strict'`,`x` 会是一个全局变量。 ### arguments 变为参数的静态副本 ```js !(function (a) { arguments[0] = 100 console.log(a) })(1) // 参数为1时,由于内部修改了,所以结果为100 // 但是,参数不传时,a为undefined,单机原来的参数不会被改,还是undefined ``` 如果加上`'use strict'`的话,arguments 只是参数的副本,所以不会影响原来的参数,所以结果为 1. 如果参数是一个`object`的话,由于 js 是 call by sharing,所以,修改了对象的属性的值,会保持下来。 ### delete 参数、函数名报错 ```js !(function (a) { console.log(delete a) })(1) // false !(function (a) { 'use strict' delete a })(a) // SyntaxError ``` ### delete 不可配置的报错 ```js !(function (a) { var obj = {} Object.defineProperty(obj, 'a', { configurable: false }) console.log(delete obj.a) })(1) // false !(function (a) { 'use strict' var obj = {} Object.defineProperty(obj, 'a', { configurable: false }) console.log(delete obj.a) })(1) // TypeError ``` ### 对象字面量重复属性名报错 ```js !(function () { var obj = { x: 1, x: 2 } console.log(obj.x) })() // 2 !(function () { 'use strict' var obj = { x: 1, x: 2 } console.log(obj.x) })() // SyntaxError ``` ### 禁止八进制字面量 ```js !(function () { console.log(0123) })() // 83 !(function () { 'use strict' console.log(0123) })() // SyntaxError ``` ### eval, arguments 变为关键字,不能作为变量、函数名 ```js !(function () { function eval() {} console.log(eval) })() // function eval() {} !(function () { 'use strict' function eval() {} console.log(eval) })() // SyntaxError ``` ### eval 独立作用域 ```js !(function () { eval('var evalVal = 2;') console.log(typeof evalVal) })() // number !(function () { 'use strict' eval('var evalVal = 2;') console.log(typeof evalVal) })() // undefined ``` ### 其他 !['use strict'](./images/3-1.use-strict.png) 严格模式是一种特殊的运行模式,它修复了部分语言上的不足,提供更强的错误检查,并增强安全性。 --- --- url: 'https://zhaobc.site/notes/frontend/js/bosn/chapter04.md' --- # 第 4 章 对象概述 ## 4 - 1 对象概述 ### 概述 对象中包含一系列属性,这些属性是**无序**的。 每个属性都有一个**字符串 key**和对应的 value。 ```js var obj = {} obj[1] = 1 obj['1'] = 2 obj // Object {1: 2} // 上面可以看错,不管是数字型的1作为属性,还是字符串型的'1'作为属性,他们指向同一个属性。 obj[{}] = false obj[{ x: 1 }] = true obj // Object {1:2, [object Object]: true} // 同样,这里不管是空对象,还是属性x为1的对象作为key,都是把它们toString()之后作为key,他们最终指向同一个属性。 ``` ### 对象结构 对象的每个属性都有一些便签,比如: * writable * enumerable * configurable * value * get/set * `__proto__` * 等等 图示: ![对象构造](./images/4-1.structure.png) ## 4 - 2 创建对象、原型链 ### 对象创建-字面量 `var obj1 = {x: 1, y: 2};` ### 对象创建-new/原型链 使用 new 构造对象的好处,对象的原型会指向构造器的 prototype 属性。 当访问一个对象的属性时,先看该对象有没有这个属性,如果有就使用该对象的该属性值,如果没有,则向上查找该对象的原型链上有没有这个属性。 当查找到最后的 prototype 为 null 时还没有查找到,就返回 undefined. 注意`in`和`hasOwnProperty`的区别。 原型链示意图: ![原型链示意图](./images/4-2.prototype-01.png) 但是,当给对象的属性赋值的时候,不会通过原型链向上查找。 也就是说,当给对象的属性赋值时,会看这个对象上有没有这个属性,如果有,直接修改属性的值; 如果没有,直接在这个对象上新加这个属性,而不会修改对象原型链上的值。 示意图: ![示意图](./images/4-2.prototype-02.png) 如果给一个对象的属性赋值为 undefined 的话,这个属性还是存在的,只不过值为 undefined。 所以,如果访问一个对象的属性时返回 undefined,不一定是对象没有这个属性,可能是有这个属性,只不过值为 undefined。 此时,可以通过`hasOwnProperty`和`in`来区分。 如果所示: ![属性赋值undefined](./images/4-2.prototype-03.png) 那么,如果要删掉对象的属性的话,使用`delete obj.z`去删除,删除后就可以通过`obj.z`访问到原型链上的值了。 但是,如果继续`delete obj.z`的话,是不起作用的。 也就是说,delete 只会删掉对象上的属性,而原型链上的属性不会受影响。 这样的话,就通过原型链实现了继承,对象可以访问原型链上的属性,但是有不会修改原型链上的属性。 ### 对象创建-`Object.create` 除了用原型之外,还可以通过`Object.create`这个内置函数来创建对象。 这个方法的作用是,创建一个对象,并且让这个对象的原型执行这个方法的参数。 如果所示: ![Object.create](./images/4-2.prototype-04.png) 使用 obj.x 可以访问,但是这个 x 是 obj 的原型链上的,所以,hasOwnProperty 返回 false。 并不是所有对象的原型链末端都是 Object.prototype,也就是说不是说有的对象都有 toString 方法。 比如上图所示,使用`Object.create(null)`创建的对象,它的原型直接就是 null 了,所以这个对象就不会 包含任何方法。 ## 4 - 3 属性操作 ### 属性读写 使用`obj.x`或`obj['y']`形式。 使用`for...in`遍历属性时,会把原型链上的东西遍历出来,并且他的顺序时不确定的。 ```js var p for (p in obj) { console.log(obj[p]) } ``` ### 属性读写-异常 使用`obj.x = y`时,如果 x 属性存在就赋为 y,如果不存在就新建 x 属性,并赋值。 ### 属性删除 使用`delete obj.x`或`delete obj[x]`形式删除,删除之后 obj.x 就变为 undefined。 如果去删除不存在的属性,js 不去做任何事情,但是仍然返回 true,所以,返回 true 并不代表它删除成功了,而是说这个对象上不存在这个属性了。 但是,有些属性是不允许删除的,这时会返回 false,比如: ```js delete Object.prototype // false ``` 每个属性都有自己的一些权限配置,我们可以通过下面的方式获取: ```js var descriptor = Object.getOwnPropertyDescriptor(Object, 'prototype') descriptor.configurable // false ``` 除此之外,使用 var 定义的全局变量或局部变量,仍然不能被删除,虽然它不是对象的属性。比如: ```js var globalVal = 1 delete globalVal // false; ;(function () { var localVal = 1 return delete localVal })() // false // 同理,函数也一样 function fd() {} delete fd // false ;(function () { function fd() {} return delete fd })() // false // 注意,不使用var定义的这种可以删除。 ohNo = 1 window.ohNo // 1 delete ohNo // true // 还有中情况,eval里定义的变量是可以删除的。 eval('var a = 1;') delete a // true ``` ### 属性检测 可以使用 in 操作符来检测,它能访问原型链上的属性,可以用 hasOwnProperty 来检测是否是原型链上的属性。 但是,使用 in 遍历时,不是每个属性都会输出,可以使用`obj.propertyIsEnumerable('x')`的形式来判断属性是否可以被枚举。 我们可以使用 defineProperty 来自定义属性,并且可以设为不可枚举。 ```js Object.defineProperty(cat, 'price', { enumerable: false, value: 1000 }) cat.propertyIsEnumerable('price') // false cat.hasOwnProperty('price') // true ``` 我们使用对象字面量或者`new`创建的对象,或者赋值创建的属性,它的属性都是可读可写,也是可以枚举,也可以 delete 掉。 而使用 defineProperty 创建的属性,如果不指定的话,它的标签都是 false 的,比如上面的 enumerable 不写也可以,默认 false。 补充,`if (a != undefined)`,等价于`if (a !== undefined || a !== null)`,不使用严格等于时,undefined 跟 null 当成一个东西。 ### 属性枚举 可以使用 propertyIsEnumerable 或者 hasOwnProperty 来枚举。 注意,使用`var obj = Object.create(xxx)`创建出来的对象,它的原型指向后面的参数,比如这个 obj 的原型指向 xxx。 ## 4-4 get/set 方法 ### getter/setter 使用 Getter/Setter 的使用,是使用 get/set 关键字,加上字段名,紧接着跟着方法体。 Getter/Setter 跟其他属性之间,依然使用逗号隔开。 ```js var man = { name: 'Bosn', weibo: '@Boson', get age() { return new Date().getFullYear() - 1988 }, set age(val) { console.log("Age can't be set to " + val) }, } console.log(man.age) // 27 man.age = 100 // Age can't be set to 100 console.log(man.age) // 27 ``` 一个稍微复杂的例子: ```js var man = { name: 'Bosn', weibo: '@Boson', $age: null, // 这里使用$,是为了不想把这个变量暴露给外面。注:实际man.$age还是可以访问的 get age() { if (this.$age == undefined) { // 这里没有使用严格等于,所以,这里实现了null和undefined判断 return new Date().getFullYear() - 1988 } else { return this.$age } }, set age(val) { val = +val // 这里使用一元操作符+,可以将字符串转为数值。注:+不会将负值转为正值,+(-10)为-10 if (!isNaN(val) && val > 0 && val < 150) { this.$age = +val } else { throw new Error('Incorrect val = ' + val) } }, } console.log(man.age) // 27 man.age = 100 console.log(man.age) // 100 man.age = 'abc' // error: Incorrect val = NaN 注:这里是NaN,而不是'abc',因为在val = +val中就出错了。+'abc'为NaN // 如果去掉val = +val,执行man.age = 'abc'时,返回:Incorrect val = abc man.age = -10 // error: Incorrect val = -10 ``` ### get/set 与原型链 如图所示: ![get/set与原型链](./images/4-4-get-set-prototype.png) `obj.z; // 1` 右上部分,为 foo.prototype 定义了一个 z 的 get 方法,使用 obj.z,由于 obj 中没有 z,所以向上查找原型链,得到原型上的 get 方法,所以返回 1。 ```js obj.z = 10 obj.z // still 1 ``` 给 z 赋值时,由于 obj 上没有 z,按之前的讲解,应该会在 obj 上创建一个 z 属性,但是这里是失败了,因为仍然返回 1。 原因是,当 obj 上没有这个属性时,并且 obj 的原型链上有对应的 get/set 方法时,当对属性赋值时,它会走原型链上对应的 get/set 方法,而不会做对当前对象创建新属性的方式这样的处理。 那么,如果实现对 obj 上这样的属性进行修改呢。我们可以通过`Object.DefineProperty`来添加新属性。 如右下所示,使用`configurable: true`来指定属性可以被修改,这里没有指定 writebale 和 enumerable,所以默认是 false。 ## 4 - 5 属性标签 ### 查看属性标签 查看一个对象上的属性有哪些属性标签使用`Object.getOwnPropertyDescriptor(obj, prop)`,如下所示: ```js Object.getOwnPropertyDescriptor({ pro: true }, 'pro') // Object {value: true, writable: true, enumerable: true, configurable: true} // value: 属性的值 // writable: 属性是否可修改,默认为true // enumerable: 属性是否可以被遍历,可被枚举(for..in或者Object.keys('xx')),默认为true // configurable: 是表示这些属性标签是否可以再被修改,另外,也表示是否可以通过delete去删除这个属性,默认为true Object.getOwnPropertyDescriptor({ pro: true }, 'a') // undefined // 如果去获取一个不存在的属性的属性标签,将返回undefined // 定义属性使用Object.defineProperty // 第一个参数为对象, // 第二个参数为属性名 // 第三个参数为属性标签对象 var person = {} Object.defineProperty(person, 'name', { configurable: false, writable: false, enumerable: true, value: 'Bosn Ma', }) // 运行结果 person.name // Bosn Ma person.name = 1 person.name // still Bosn Ma // 上面的name没有赋值成功,因为writable为false delete person.name // false // delete也不成功,因为configurable为false ``` 一次性定义多个属性时,使用`Object.defineProperties(obj, properties)`,注意这里是复数形式。 第一个参数为对象,第二个参数是一个属性对象,这个对象的 key 是对象的属性,值为属性描述。 ```js Object.defineProperties(person, { title: { value: 'fe', enumerable: true }, corp: { value: 'BABA', enumerable: true }, salary: { value: 50000, enumerable: true, writable: true }, }) Object.getOwnPropertyDescriptor(person, 'salary') // Object {value: 50000, writable: true, enumerable: true, configurable: false} // 使用defineProperty定义时,没有明确知道的标签,默认为false Object.getOwnPropertyDescriptor(person, 'corp') // Object {value: 'BABA', writable: false, enumerable: true, configurable: false} ``` 属性的标签是可以重复设置的,我们可以再次使用 defineProperty 去修改对应属性的标签。 不同的标签是做不同的事情的,如下所示: ![属性标签](./images/4-5-properties.png) ::: tip 如果 configurable 为 true,即使其他标签为 false 也是可以被修改的,因为可以使用 defineProperty 再次定义为 true。 ::: ## 4 - 6 对象标签,对象序列化 ### 对象标签 * \[\[proto]] * \[\[class]] * \[\[extensible]] ### 原型标签**proto** ![原型链](./images/4-6-proto.png) ### class 标签 没有直接的方式获取 class 标签,可以通过间接的方式`Object.prototype.toString`获取。 ```js var toString = Object.prototype.toString function getType(o) { return toString.call(o).slice(8, -1) // 从第9位截取,-1代表从后面数1位,即不包含最后一位。 } toString(null) // [object Null] getType(null) // Null getType(undefined) // Undefined getType(1) // Number getType(new Number(1)) // Number typeof new Number(1) // object getType(true) // Boolean getType(new Boolean(true)) // Boolean ``` ### extensible 标签 --- --- url: 'https://zhaobc.site/notes/frontend/js/bosn/chapter05.md' --- # 第 5 章 数组 --- --- url: 'https://zhaobc.site/notes/frontend/js/code-snippets/group-by.md' --- # Javascript Group By ## Object 数组分组 下面的代码是用来按数组对象的指定属性来排序。 ### 代码 ```ts export const groupBy = (arr: any[], key: string): any[] | null => { if (arr) { const grouped = arr.reduce((group: any, obj: any) => { const data = obj[key] group[data] = group[data] ?? [] group[data].push(obj) return group }, {}) return grouped } return null } ``` ### 示例 ::: playground#ts 对象数组分组 @file index.ts ```ts const groupBy = (arr: any[], key: string): any[] | null => { if (arr) { const grouped = arr.reduce((group: any, obj: any) => { const data = obj[key] group[data] = group[data] ?? [] group[data].push(obj) return group }, {}) return grouped } return null } const arr = [ { name: 'zhaobc', commit: 'test1', }, { name: 'zhaobc2', commit: 'test1', }, { name: 'zhaobc', commit: 'test2', }, ] console.log(arr) console.log(groupBy(arr, 'name')) ``` ::: --- --- url: 'https://zhaobc.site/notes/frontend/js/code-snippets/reduce.md' --- # reduce 使用 ## 使用 reduce 切分字符串然后重新拼接 业务上有一个需求,后台传过来一个字符串,里面有括号 `(` `)` 和顿号 `、`,但是在前台展示时,要切分之后给每段文字加链接。 另外,字符串不确认内容,最多包含包含 4 段文字。 示例: 文字 1、(文字 2、文字 3)、文字 4 分隔完上面的文字后,就可以对 `(`, `)`, `、`以外的文字加链接了。 ### 代码 ```js function reduceText(val) { if (val === null || val.length === 0) { return [] } const arr = val.split('、').reduce((pre, cur) => { if (cur.includes('(') && cur.includes(')')) { // 同时包含左右括号 let tmp = cur.split('(') pre.push(tmp[0]) pre.push('(') tmp = tmp[1].split(')') pre.push(tmp[0]) pre.push(')') pre.push(tmp[1]) } else if (cur.includes('(')) { const tmp = cur.split('(') pre.push(tmp[0]) pre.push('(') pre.push(tmp[1]) } else if (cur.includes(')')) { const tmp = cur.split(')') pre.push(tmp[0]) pre.push(')') pre.push(tmp[1]) } else { pre.push(cur) } // 按原来的顺序,把顿号也加上 pre.push('、') return pre }, []) // 删除最后一个顿号 arr.splice(arr.length - 1, 1) // 删除空白元素 return arr.filter(item => item.length !== 0) } ``` ### 示例 ::: normal-demo 使用 reduce 切分字符串然后重新拼接 ```js function reduceText(val) { if (val === null || val.length === 0) { return [] } const arr = val.split('、').reduce((pre, cur) => { if (cur.includes('(') && cur.includes(')')) { // 同时包含左右括号 let tmp = cur.split('(') pre.push(tmp[0]) pre.push('(') tmp = tmp[1].split(')') pre.push(tmp[0]) pre.push(')') pre.push(tmp[1]) } else if (cur.includes('(')) { const tmp = cur.split('(') pre.push(tmp[0]) pre.push('(') pre.push(tmp[1]) } else if (cur.includes(')')) { const tmp = cur.split(')') pre.push(tmp[0]) pre.push(')') pre.push(tmp[1]) } else { pre.push(cur) } // 按原来的顺序,把顿号也加上 pre.push('、') return pre }, []) // 删除最后一个顿号 arr.splice(arr.length - 1, 1) // 删除空白元素 return arr.filter(item => item.length !== 0) } console.log(reduceText('文字1')) // ["文字1"] console.log(reduceText('文字1、文字2')) // ["文字1","、","文字2"] console.log(reduceText('文字1、(文字2)')) // ["文字1","、","(","文字2",")"] console.log(reduceText('文字1、(文字2、文字3)')) // ["文字1","、","(","文字2","、","文字3",")"] console.log(reduceText('文字1、(文字2、文字3)、文字4')) // ["文字1","、","(","文字2","、","文字3",")","、","文字4"] ``` ::: --- --- url: 'https://zhaobc.site/notes/frontend/ts/index.md' --- # TypeScript 笔记 TypeScript 学习笔记 --- --- url: 'https://zhaobc.site/notes/frontend/ts/handbook/index.md' --- # Handbook [The TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) [Source Code](https://github.com/microsoft/TypeScript-Website/tree/v2/packages/documentation/copy/en/handbook-v2) --- --- url: 'https://zhaobc.site/notes/frontend/ts/handbook/more_on_function.md' --- # More on Functions ## Function Type Expressions The simplest way to describe a function is with a *function type expression*. These types are syntactically similar to arrow functions: ```ts twoslash function greeter(fn: (a: string) => void) { fn('Hello World') } function printToConsole(s: string) { console.log(s) } greeter(printToConsole) ``` The syntax `(a: string) => void` means "a function with one parameter, named `a`, of type `string`, that doesn't have a return value". Just like with function declarations, if a parameter type isn't specified, it's implicitly `any`. > Note that the parameter name is **required**. The function type `(string) => void` means "a function with a parameter named `string` of type `any`"! Of course, we can use a type alias to name a function type: ```ts twoslash type GreetFunction = (a: string) => void function greeter(fn: GreetFunction) { // ... } ``` ## Call Signatures In JavaScript, functions can have properties in addition to being callable. However, the function type expression syntax doesn't allow for declaring properties. If we want to describe something callable with properties, we can write a *call signature* in an object type: ```ts twoslash type DescribableFunction = { description: string (someArg: number): boolean } function doSomething(fn: DescribableFunction) { console.log(fn.description + ' returned ', fn(6)) } function myFunc(someArg: number) { return someArg > 3 } myFunc.description = 'default description: is greater than 3' doSomething(myFunc) ``` Note that the syntax is slightly different compared to a function type expression - use `:` between the parameter list and the return type rather than `=>`. ## Construct Signatures JavaScript functions can also be invoked with the `new` operator. TypeScript refers to these as *constructors* because they usually create a new object. You can write a *construct signature* by adding the `new` keyword in front of a call signature: ```ts twoslash type SomeObject = any // ---cut--- type SomeConstructor = { new (s: string): SomeObject } function fn(ctor: SomeConstructor) { return new ctor('hello') } ``` Some objects, like JavaScript's `Date` object, can be called with or without `new`. You can combine call and construct signatures in the same type arbitrarily: ```ts twoslash interface CallConstructor { (n?: number): string new (s: string): Date } ``` ## Generic Functions It's common to write a function where the types of the input relate to the type of the output, or where the types of two inputs are related in some way. In TypeScript, *generics* are used when we want to describe a correspondence between two values. We do this by declaring a *type parameter* in the function signature: ```ts twoslash function firstElement(arr: Type[]): Type | undefined { return arr[0] } ``` By adding a type parameter `Type` to this function and using it in two places, we've created a link between the input of the function (the array) and the output (the return value). Now when we call it, a more specific type comes out: ```ts twoslash declare function firstElement(arr: Type[]): Type | undefined // ---cut--- // s is of type 'string' const s = firstElement(['a', 'b', 'c']) // n is of type 'number' const n = firstElement([1, 2, 3]) // u is of type undefined const u = firstElement([]) ``` ### Inference Note that we didn't have to specify `Type` in this sample. The type was *inferred* - chosen automatically - by TypeScript. ```ts twoslash // prettier-ignore function map(arr: Input[], func: (arr: Input) => Output): Output[] { return arr.map(func) } // Parameter 'n' is of type 'string' // 'parsed' is of type 'number[]' const parsed = map(['1', '2', '3'], n => parseInt(n)) ``` Note that in this example, TypeScript could infer both the type of the `Input` type parameter (from the given `string` array), as well as the `Output` type parameter based on the return value of the function expression (`number`). ### Constraints We've written some generic functions that can work on *any* kind of value. Sometimes we want to relate two values, but can only operate on a certain subset of values. In this case, we can use a *constraint* to limit the kinds of types that a type parameter can accept. Let's write a function that returns the longer of two values. To do this, we need a `length` property that's a number. We *constrain* the type parameter to that type by writing an `extends` clause: ```ts twoslash // @errors: 2345 2322 function longest(a: Type, b: Type) { if (a.length >= b.length) { return a } else { return b } } // longerArray is of type 'number[]' const longerArray = longest([1, 2], [1, 2, 3]) // longerString is of type 'alice' | 'bob' const longerString = longest('alice', 'bob') // Error! Numbers don't have a 'length' property const notOK = longest(10, 100) ``` Because we constrained `Type` to `{ length: number }`, we were allowed to access the `.length` property of the `a` and `b` parameters. ### Working with Constrained Values Here's a common error when working with generic constraints: ```ts twoslash // @errors: 2322 function minimumLength( obj: Type, minimum: number ): Type { if (obj.length >= minimum) { return obj } else { return { length: minimum } } } ``` It might look like this function is OK - `Type` is constrained to `{ length: number }`, and the function either returns `Type` or a value matching that constraint. The problem is that the function promises to return the *same* kind of object as was passed in, not just *some* object matching the constraint. ### Specifying Type Arguments TypeScript can usually infer the intended type arguments in a generic call, but not always. ```ts twoslash function combine(arr1: Type[], arr2: Type[]): Type[] { return arr1.concat(arr2) } ``` Normally it would be an error to call this function with mismatched arrays: ```ts twoslash // @errors: 2322 declare function combine(arr1: Type[], arr2: Type[]): Type[] // ---cut--- const arr = combine([1, 2, 3], ['hello']) ``` If you intended to do this, however, you could manually specify `Type`: ```ts twoslash declare function combine(arr1: Type[], arr2: Type[]): Type[] // ---cut--- const arr = combine([1, 2, 3], ['hello']) // ^^^^^^^^^^^^^^^ ``` ### Guidelines for Writing Good Generic Functions Writing generic functions is fun, and it can be easy to get carried away with type parameters. Having too many type parameters or using constraints where they aren't needed can make inference less successful, frustrating callers of your function. #### Push Type Parameters Down Here are two ways of writing a function that appear similar: ```ts twoslash function firstElement1(arr: Type[]) { return arr[0] } function firstElement2(arr: Type) { return arr[0] } // a: number (good) const a = firstElement1([1, 2, 3]) // b: any (bad) const b = firstElement2([1, 2, 3]) ``` These might seem identical at first glance, but `firstElement1` is a much better way to write this function. Its inferred return type is `Type`, but `firstElement2`'s inferred return type is `any` because TypeScript has to resolve the `arr[0]` expression using the constraint type, rather than "waiting" to resolve the element during a call. > **Rule**: When possible, use the type parameter itself rather than constraining it #### Use Fewer Type Parameters > **Rule**: Always use as few type parameters as possible #### Type Parameters Should Appear Twice > **Rule**: If a type parameter only appears in one location, strongly reconsider if you actually need it ## Optional Parameters We can model this in TypeScript by marking the parameter as *optional* with `?`: ```ts twoslash function f(x?: number) { // ... } f() // OK f(10) // OK ``` Although the parameter is specified as type `number`, the `x` parameter will actually have the type `number | undefined` because unspecified parameters in JavaScript get the value `undefined`. You can also provide a parameter *default*: ```ts twoslash function f(x = 10) { // ... } ``` Now in the body of `f`, `x` will have type `number` because any `undefined` argument will be replaced with `10`. Note that when a parameter is optional, callers can always pass `undefined`, as this simply simulates a "missing" argument: ### Optional Parameters in Callbacks ```ts twoslash function myForEach(arr: any[], callback: (arg: any, index?: number) => void) { for (let i = 0; i < arr.length; i++) { callback(arr[i], i) } } ``` What people usually intend when writing `index?` as an optional parameter is that they want both of these calls to be legal: ```ts twoslash declare function myForEach( arr: any[], callback: (arg: any, index?: number) => void ): void // ---cut--- myForEach([1, 2, 3], a => console.log(a)) myForEach([1, 2, 3], (a, i) => console.log(a, i)) ``` What this *actually* means is that *`callback` might get invoked with one argument*. TypeScript will enforce this meaning and issue errors that aren't really possible: ```ts twoslash // @errors: 2532 18048 declare function myForEach( arr: any[], callback: (arg: any, index?: number) => void ): void // ---cut--- myForEach([1, 2, 3], (a, i) => { console.log(i.toFixed()) }) ``` In JavaScript, if you call a function with more arguments than there are parameters, the extra arguments are simply ignored. TypeScript behaves the same way. Functions with fewer parameters (of the same types) can always take the place of functions with more parameters. > **Rule**: When writing a function type for a callback, *never* write an optional parameter unless you intend to *call* the function without passing that argument ## Function Overloads Some JavaScript functions can be called in a variety of argument counts and types. In TypeScript, we can specify a function that can be called in different ways by writing *overload signatures*. ```ts twoslash // @errors: 2575 function makeDate(timestamp: number): Date function makeDate(m: number, d: number, y: number): Date function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { if (d !== undefined && y !== undefined) { return new Date(y, mOrTimestamp, d) } else { return new Date(mOrTimestamp) } } const d1 = makeDate(12345678) const d2 = makeDate(5, 5, 5) const d3 = makeDate(1, 3) ``` In this example, we wrote two overloads: one accepting one argument, and another accepting three arguments. These first two signatures are called the *overload signatures*. Then, we wrote a function implementation with a compatible signature. Functions have an *implementation* signature, but this signature can't be called directly. Even though we wrote a function with two optional parameters after the required one, it can't be called with two parameters! ### Overload Signatures and the Implementation Signature Again, the signature used to write the function body can't be "seen" from the outside. > The signature of the *implementation* is not visible from the outside. > When writing an overloaded function, you should always have *two* or more signatures above the implementation of the function. The implementation signature must also be *compatible* with the overload signatures. ### Writing Good Overloads Like generics, there are a few guidelines you should follow when using function overloads. Following these principles will make your function easier to call, easier to understand, and easier to implement. > Always prefer parameters with union types instead of overloads when possible This function is fine; we can invoke it with strings or arrays. However, we can't invoke it with a value that might be a string *or* an array, because TypeScript can only resolve a function call to a single overload: ```ts twoslash // @errors: 2769 declare function len(s: string): number declare function len(arr: any[]): number // ---cut--- len('') // OK len([0]) // OK len(Math.random() > 0.5 ? 'hello' : [0]) ``` Because both overloads have the same argument count and same return type, we can instead write a non-overloaded version of the function: ```ts twoslash function len(x: any[] | string) { return x.length } ``` This is much better! Callers can invoke this with either sort of value, and as an added bonus, we don't have to figure out a correct implementation signature. ## Other Types to Know About ### `void` `void` represents the return value of functions which don't return a value. It's the inferred type any time a function doesn't have any `return` statements, or doesn't return any explicit value from those return statements: ```ts twoslash // The inferred return type is void function noop() { return } ``` In JavaScript, a function that doesn't return any value will implicitly return the value `undefined`. However, `void` and `undefined` are not the same thing in TypeScript. > `void` is not the same as `undefined`. ### `object` The special type `object` refers to any value that isn't a primitive (`string`, `number`, `bigint`, `boolean`, `symbol`, `null`, or `undefined`). This is different from the *empty object type* `{ }`, and also different from the global type `Object`. > `object` is not `Object`. **Always** use `object`! ### `unknown` The `unknown` type represents *any* value. This is similar to the `any` type, but is safer because it's not legal to do anything with an `unknown` value: ```ts twoslash // @errors: 2571 18046 function f1(a: any) { a.b() // OK } function f2(a: unknown) { a.b() // NG } ``` ```ts twoslash declare const someRandomString: string // ---cut--- function safeParse(s: string): unknown { return JSON.parse(s) } // Need to be careful with 'obj'! const obj = safeParse(someRandomString) ``` ### `never` Some functions *never* return a value: ```ts twoslash function fail(msg: string): never { throw new Error(msg) } ``` The `never` type represents values which are *never* observed. In a return type, this means that the function throws an exception or terminates execution of the program. `never` also appears when TypeScript determines there's nothing left in a union. ```ts twoslash function fn(x: string | number) { if (typeof x === 'string') { // do something } else if (typeof x === 'number') { // do something else } else { console.log(x) // has type 'never'! // ^? } } ``` ### `Function` The global type `Function` describes properties like `bind`, `call`, `apply`, and others present on all function values in JavaScript. It also has the special property that values of type `Function` can always be called; these calls return `any`: ```ts twoslash function doSomething(f: Function) { return f(1, 2, 3) } ``` ## Rest Parameters and Arguments ::: tip Background Reading: [Rest Parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) [Spread Syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) ::: ### Rest Parameters In addition to using optional parameters or overloads to make functions that can accept a variety of fixed argument counts, we can also define functions that take an *unbounded* number of arguments using *rest parameters*. A rest parameter appears after all other parameters, and uses the `...` syntax: ```ts twoslash function multiply(n: number, ...m: number[]) { return m.map(x => n * x) } // 'a' gets value [10, 20, 30, 40] const a = multiply(10, 1, 2, 3, 4) ``` In TypeScript, the type annotation on these parameters is implicitly `any[]` instead of `any`, and any type annotation given must be of the form `Array` or `T[]`, or a tuple type (which we'll learn about later). ### Rest Arguments Conversely, we can *provide* a variable number of arguments from an iterable object (for example, an array) using the spread syntax. For example, the `push` method of arrays takes any number of arguments: ```ts twoslash const arr1 = [1, 2, 3] const arr2 = [4, 5, 6] arr1.push(...arr2) ``` Note that in general, TypeScript does not assume that arrays are immutable. This can lead to some surprising behavior: ```ts twoslash // @errors: 2556 // Inferred type is number[] -- "an array with zero or more numbers", // not specifically two numbers const args = [8, 5] const angle = Math.atan2(...args) ``` The best fix for this situation depends a bit on your code, but in general a `const` context is the most straightforward solution: ```ts twoslash // Inferred as 2-length tuple const args = [8, 5] as const // OK const angle = Math.atan2(...args) ``` ## Parameter Destructuring ::: tip Background Reading: [Destructuring Assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) [Spread Syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) ::: You can use parameter destructuring to conveniently unpack objects provided as an argument into one or more local variables in the function body. In JavaScript, it looks like this: ```js function sum({ a, b, c }) { console.log(a + b + c) } sum({ a: 10, b: 3, c: 9 }) ``` The type annotation for the object goes after the destructuring syntax: ```ts twoslash function sum({ a, b, c }: { a: number; b: number; c: number }) { console.log(a + b + c) } ``` This can look a bit verbose, but you can use a named type here as well: ```ts twoslash type ABC = { a: number; b: number; c: number } function sum({ a, b, c }: ABC) { console.log(a + b + c) } ``` ## Assignability of Functions ### Return type `void` The `void` return type for functions can produce some unusual, but expected behavior. Contextual typing with a return type of `void` does **not** force functions to **not** return something. Another way to say this is a contextual function type with a `void` return type (`type voidFunc = () => void`), when implemented, can return *any* other value, but it will be ignored. Thus, the following implementations of the type `() => void` are valid: ```ts twoslash type voidFunc = () => void const f1: voidFunc = () => { return true } const f2: voidFunc = () => true const f3: voidFunc = function () { return true } ``` And when the return value of one of these functions is assigned to another variable, it will retain the type of `void`: ```ts twoslash type voidFunc = () => void const f1: voidFunc = () => { return true } const f2: voidFunc = () => true const f3: voidFunc = function () { return true } // ---cut--- const v1 = f1() const v2 = f2() const v3 = f3() ``` This behavior exists so that the following code is valid even though `Array.prototype.push` returns a number and the `Array.prototype.forEach` method expects a function with a return type of `void`. ```ts twoslash const src = [1, 2, 3] const dst = [0] src.forEach(el => dst.push(el)) ``` There is one other special case to be aware of, when a literal function definition has a `void` return type, that function must **not** return anything. ```ts twoslash function f2(): void { // @ts-expect-error return true } const f3 = function (): void { // @ts-expect-error return true } ``` --- --- url: 'https://zhaobc.site/notes/frontend/ts/handbook/narrowing.md' --- # Narrowing Imagine we have a function called `padLeft`. ```ts twoslash function padLeft(padding: number | string, input: string): string { throw new Error('Not implemented yet!') } ``` If `padding` is a `number`, it will treat that as the number of spaces we want to prepend to `input`. If `padding` is a `string`, it should just prepend `padding` to `input`. Let's try to implement the logic for when `padLeft` is passed a `number` for `padding`. ```ts twoslash function padLeft(padding: number | string, input: string): string { if (typeof padding === 'number') { return ' '.repeat(padding) + input // ^? } return padding + input // ^? } ``` ## `typeof` type guards JavaScript supports a `typeof` operator which can give very basic information about the type of values we have at runtime. TypeScript expects this to return a certain set of strings: * `"string"` * `"number"` * `"bigint"` * `"boolean"` * `"symbol"` * `"undefined"` * `"object"` * `"function"` In TypeScript, checking against the value returned by `typeof` is a *type guard*. `typeof` doesn't return the string `null`, `typeof null` is actually `"object"`! ```ts twoslash // @errors: 18047 function printAll(strs: string | string[] | null) { if (typeof strs === 'object') { for (const s of strs) { console.log(s) } } else if (typeof strs === 'string') { console.log(strs) } else { // do nothing } } ``` In the `printAll` function, we try to check if `strs` is an object to see if it's an array type, But it turns out that in JavaScript, `typeof null` is actually `"object"`! ## Truthiness narrowing Values like * `0` * `NaN` * `''` (the empty string) * `0n` (the `bigint` version of zero) * `null` * `undefined` all coerce to `false`, and other values get coerced to `true`. You can always coerce values to `boolean`s by running them through the `Boolean` function, or by using the shorter double-Boolean negation. It's fairly popular to leverage this behavior, especially for guarding against values like `null` or `undefined`. ```ts twoslash // @errors: 2872 // both of these result in 'true' Boolean('hello') !!'world' ``` ### Equality narrowing TypeScript also uses `switch` statements and equality checks like `===`, `!==`, `==`, and `!=` to narrow types. For example: ```ts twoslash function example(x: string | number, y: string | boolean) { if (x === y) { // We can now call any 'string' method on 'x' or 'y'. console.log(x.toUpperCase()) // ^? console.log(y.toUpperCase()) // ^? } else { console.log(x) // ^? console.log(y) // ^? } } ``` Since `string` is the only common type that both `x` and `y` could take on, TypeScript knows that `x` and `y` must be `string`s in the first branch. JavaScript's looser equality checks with `==` and `!=` also get narrowed correctly. If you're unfamiliar, checking whether something `== null` actually not only checks whether it is specifically the value `null` - it also checks whether it's potentially `undefined`. The same applies to `== undefined`: it checks whether a value is either `null` or `undefined`. ```ts twoslash interface Container { value: number | null | undefined } function multiplyValue(container: Container, factor: number) { // Remove both 'null' and 'undefined' from the type. if (container.value != null) { console.log(container.value) // ^? // Now we can safely multiply 'container.value'. container.value *= factor } } ``` ### The `in` operator narrowing TypeScript takes this into account as a way to narrow down potential types. ```ts twoslash type Fish = { swim: () => void } type Bird = { fly: () => void } function move(animal: Fish | Bird) { if ('swim' in animal) { return animal.swim() } return animal.fly() } ``` To reiterate, optional properties will exist in both sides for narrowing. For example, a human could both swim and fly (with the right equipment) and thus should show up in both sides of the `in` check: ```ts twoslash type Fish = { swim: () => void } type Bird = { fly: () => void } type Human = { swim: () => void fly: () => void } function move(animal: Fish | Bird | Human) { if ('swim' in animal) { return animal.swim() // ^? } return animal.fly() // ^? } ``` ### `instanceof` narrowing ```ts twoslash function logValue(x: Date | string) { if (x instanceof Date) { console.log(x.toUTCString()) } else { console.log(x.toUpperCase()) } } ``` ### Assignments When we assign to any variable, TypeScript looks at the right side of the assignment and narrows the left side appropriately. ### Control flow analysis TypeScript narrows within specific branches. ### Using type predicates Sometimes you want more direct control over how types change throughout your code. To define a user-defined type guard, we simply need to define a function whose return type is a type *predicate*: ```ts twoslash type Fish = { swim: () => void } type Bird = { fly: () => void } // ---cut--- function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined } ``` `pet is Fish` is our type predicate in this example. A predicate takes the form `parameterName is Type`, where `parameterName` must be the name of a parameter from the current function signature. Any time `isFish` is called with some variable, TypeScript will *narrow* that variable to that specific type if the original type is compatible. ```ts twoslash type Fish = { swim: () => void } type Bird = { fly: () => void } declare function getSmallPet(): Fish | Bird function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined } // ---cut--- // Both calls to 'swim' and 'fly' are now okay. let pet = getSmallPet() if (isFish(pet)) { pet.swim() } else { pet.fly() } ``` Notice that TypeScript not only knows that `pet` is a `Fish` in the `if` branch; it also knows that in the `else` branch, you don’t have a `Fish`, so you must have a `Bird`. You may use the type guard `isFish` to filter an array of `Fish | Bird` and obtain an array of `Fish`: ```ts twoslash type Fish = { name: string swim: () => void } type Bird = { name: string fly: () => void } declare function getSmallPet(): Fish | Bird function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined } // ---cut--- const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()] const underWater1: Fish[] = zoo.filter(isFish) // or, equivalently const underWater2: Fish[] = zoo.filter(isFish) as Fish[] // The predicate may need repeating for more complex examples const underWater3: Fish[] = zoo.filter((pet): pet is Fish => { if (pet.name === 'sharkey') { return false } return isFish(pet) }) ``` In addition, classes can use `this is Type` to narrow their type. ## Discriminated unions ```ts twoslash interface Shape { kind: 'circle' | 'square' radius?: number sideLength?: number } ``` Notice we're using a union of string literal types: `"circle"` and `"square"` to tell us whether we should treat the shape as a circle or square respectively. By using `"circle" | "square"` instead of `string`, we can avoid misspelling issues. ```ts twoslash // @errors: 2367 interface Shape { kind: 'circle' | 'square' radius?: number sideLength?: number } // ---cut--- function handleShape(shape: Shape) { // oops! if (shape.kind === 'rect') { // ... } } ``` We can write a `getArea` function that applies the right logic based on if it's dealing with a circle or square. We'll first try dealing with circles. ```ts twoslash // @errors: 2532 18048 interface Shape { kind: 'circle' | 'square' radius?: number sideLength?: number } // ---cut--- function getArea(shape: Shape) { return Math.PI * shape.radius ** 2 } ``` Under `strictNullChecks` that gives us an error - which is appropriate since `radius` might not be defined. But what if we perform the appropriate checks on the kind property? ```ts twoslash // @errors: 2532 18048 interface Shape { kind: 'circle' | 'square' radius?: number sideLength?: number } // ---cut--- function getArea(shape: Shape) { if (shape.kind === 'circle') { return Math.PI * shape.radius ** 2 } } ``` Hmm, TypeScript still doesn't know what to do here. We've hit a point where we know more about our values than the type checker does. We could try to use a non-null assertion (a `!` after `shape.radius`) to say that radius is definitely present. ```ts twoslash interface Shape { kind: 'circle' | 'square' radius?: number sideLength?: number } // ---cut--- function getArea(shape: Shape) { if (shape.kind === 'circle') { return Math.PI * shape.radius! ** 2 } } ``` But this doesn't feel ideal. We had to shout a bit at the type-checker with those non-null assertions (`!`) to convince it that `shape.radius` was defined, but those assertions are error-prone if we start to move code around. ```ts twoslash interface Circle { kind: 'circle' radius: number } interface Square { kind: 'square' sideLength: number } type Shape = Circle | Square ``` Here, we've properly separated `Shape` out into two types with different values for the `kind` property, but `radius` and `sideLength` are declared as required properties in their respective types. Let's see what happens here when we try to access the `radius` of a `Shape`. ```ts twoslash // @errors: 2339 interface Circle { kind: 'circle' radius: number } interface Square { kind: 'square' sideLength: number } type Shape = Circle | Square // ---cut--- function getArea(shape: Shape) { return Math.PI * shape.radius ** 2 } ``` Like with our first definition of Shape, this is still an error. because TypeScript couldn't tell whether the property was present. But what if we tried checking the kind property again? ```ts twoslash interface Circle { kind: 'circle' radius: number } interface Square { kind: 'square' sideLength: number } type Shape = Circle | Square // ---cut--- function getArea(shape: Shape) { if (shape.kind === 'circle') { return Math.PI * shape.radius ** 2 // ^? } } ``` That got rid of the error! When every type in a union contains a common property with literal types, TypeScript considers that to be a *discriminated union*, and can narrow out the members of the union. In this case, `kind` was that common property (which is what's considered a *discriminant* property of `Shape`). Checking whether the `kind` property was `"circle"` got rid of every type in `Shape` that didn't have a `kind` property with the type `"circle"`. That narrowed shape down to the type `Circle`. ## The `never` type When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a `never` type to represent a state which shouldn't exist. ## Exhaustiveness checking The `never` type is assignable to every type; however, no type is assignable to `never` (except `never` itself). This means you can use narrowing and rely on `never` turning up to do exhaustive checking in a `switch` statement. ```ts twoslash interface Circle { kind: 'circle' radius: number } interface Square { kind: 'square' sideLength: number } type Shape = Circle | Square // ---cut--- function getArea(shape: Shape) { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2 case 'square': return shape.sideLength ** 2 default: const _exhaustiveCheck: never = shape return _exhaustiveCheck } } ``` Adding a new member to the Shape union, will cause a TypeScript error: ```ts twoslash // @errors: 2322 interface Circle { kind: 'circle' radius: number } interface Square { kind: 'square' sideLength: number } // ---cut--- interface Triangle { kind: 'triangle' sideLength: number } type Shape = Circle | Square | Triangle function getArea(shape: Shape) { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2 case 'square': return shape.sideLength ** 2 default: const _exhaustiveCheck: never = shape return _exhaustiveCheck } } ``` --- --- url: 'https://zhaobc.site/notes/frontend/ts/handbook/types.md' --- # Types ## The primitives: `string`, `number`, and `boolean` JavaScript has three very commonly used primitives: `string`, `number`, and `boolean`. `number`: JavaScript does not have a special runtime value for integers, so there’s no equivalent to int or float - everything is simply number ::: tip The type names `String`, `Number`, and `Boolean` (starting with *capital letters*) are legal, but refer to some special *built-in types* that will very rarely appear in your code. *Always* use `string`, `number`, or `boolean` for types. ::: ## Arrays To specify the type of an array, you can use the syntax `number[]`. this syntax works for any type (e.g. `string[]` is an array of strings, and so on). You may also see this written as `Array`, which means the same thing ## any TypeScript also has a special type, `any`, that you can use whenever you don’t want a particular value to cause typechecking errors. When a value is of type `any`, you can access any properties of it (which will in turn be of type any), call it like a function, assign it to (or from) a value of any type, or pretty much anything else that’s syntactically legal: ```ts let obj: any = { x: 0 } // None of the following lines of code will throw compiler errors. // Using `any` disables all further type checking, and it is assumed // you know the environment better than TypeScript. obj.foo() obj() obj.bar = 100 obj = 'hello' const n: number = obj ``` ### `noImplicitAny` When you don’t specify a type, and TypeScript can’t infer it from context, the compiler will typically default to `any`. You usually want to avoid this, though, because any isn’t type-checked. Use the compiler flag `noImplicitAny` to flag any implicit any as an error. ## Type Annotations on Variables When you declare a variable using `const`, `var`, or `let`, you can optionally add a type annotation to explicitly specify the type of the variable. In most cases, though, this isn’t needed. Wherever possible, TypeScript tries to automatically *infer* the types in your code. ```ts const myName: string = 'Alice' // No type annotation needed -- 'myName' inferred as type 'string' const myName2 = 'Alice' ``` ## Functions ### Parameter Type Annotations When you declare a function, you can add type annotations after each parameter to declare what types of parameters the function accepts. Parameter type annotations go after the parameter name: ```ts twoslash function greet(name: string) { console.log('Hello, ' + name.toUpperCase() + '!!') } ``` When a parameter has a type annotation, arguments to that function will be checked: ```ts twoslash declare function greet(name: string): void // ---cut--- // @errors: 2345 // Would be a runtime error if executed! greet(42) ``` ### Return Type Annotations You can also add return type annotations. Return type annotations appear after the parameter list: ```ts twoslash function getFavoriteNumber(): number { // ^^^^^^ return 26 } ``` Much like variable type annotations, you usually don't need a return type annotation because TypeScript will infer the function's return type based on its `return` statements. ### Functions Which Return Promises If you want to annotate the return type of a function which returns a promise, you should use the `Promise` type: ```ts twoslash async function getFavoriteNumber(): Promise { return 26 } ``` ### Anonymous Functions When a function appears in a place where TypeScript can determine how it’s going to be called, the parameters of that function are automatically given types. This process is called *contextual typing* because the *context* that the function occurred within informs what type it should have. ```ts twoslash const names = ['Alice', 'Bob', 'Eve'] // Contextual typing for function - parameter s inferred to have type string names.forEach(function (s) { console.log(s.toUpperCase()) }) // Contextual typing also applies to arrow functions names.forEach(s => { console.log(s.toUpperCase()) }) ``` ## Object Types Apart from primitives, the most common sort of type you’ll encounter is an object type. This refers to any JavaScript value with properties, which is almost all of them! To define an object type, we simply list its properties and their types. ```ts twoslash // The parameter's type annotation is an object type function printCoord(pt: { x: number; y: number }) { console.log("The coordinate's x value is " + pt.x) console.log("The coordinate's y value is " + pt.y) } printCoord({ x: 3, y: 7 }) ``` ### Optional Properties Object types can also specify that some or all of their properties are optional. To do this, add a `?` after the property name: In JavaScript, if you access a property that doesn't exist, you'll get the value `undefined` rather than a runtime error. Because of this, when you *read* from an optional property, you'll have to *check* for `undefined` before using it. ```ts twoslash // @errors: 18048 function printName(obj: { first: string; last?: string }) { // Error - might crash if 'obj.last' wasn't provided! console.log(obj.last.toUpperCase()) if (obj.last !== undefined) { // OK console.log(obj.last.toUpperCase()) } // A safe alternative using modern JavaScript syntax: console.log(obj.last?.toUpperCase()) } ``` ## Union Types TypeScript’s type system allows you to build new types out of existing ones using a large variety of operators. Now that we know how to write a few types, it’s time to start combining them in interesting ways. ### Defining a Union Type A union type is a type formed from two or more other types, representing values that may be any one of those types. ```ts twoslash // @errors: 2345 function printId(id: number | string) { console.log('Your ID is: ' + id) } // OK printId(101) // OK printId('202') // Error printId({ myId: 1234 }) ``` ### Working with Union Types It's easy to *provide* a value matching a union type - simply provide a type matching any of the union's members. TypeScript will only allow an operation if it is valid for *every* member of the union. For example, if you have the union `string | number`, you can't use methods that are only available on `string`: ```ts twoslash // @errors: 2339 function printId(id: number | string) { console.log(id.toUpperCase()) } ``` The solution is to *narrow* the union with code, the same as you would in JavaScript without type annotations. *Narrowing* occurs when TypeScript can deduce a more specific type for a value based on the structure of the code. ```ts twoslash function printId(id: number | string) { if (typeof id === 'string') { // In this branch, id is of type 'string' console.log(id.toUpperCase()) } else { // Here, id is of type 'number' console.log(id) } } ``` Another example is to use a function like `Array.isArray`: ```ts twoslash function welcomePeople(x: string[] | string) { if (Array.isArray(x)) { // Here: 'x' is 'string[]' console.log('Hello, ' + x.join(' and ')) } else { // Here: 'x' is 'string' console.log('Welcome lone traveler ' + x) } } ``` If every member in a union has a property in common, you can use that property without narrowing: ```ts twoslash // Return type is inferred as number[] | string function getFirstThree(x: number[] | string) { return x.slice(0, 3) } ``` ## Type Aliases It's common to want to use the same type more than once and refer to it by a single name. A *type alias* is exactly that - a *name* for any *type*. The syntax for a type alias is: ```ts twoslash {1} type Point = { x: number y: number } function printCoord(pt: Point) { console.log("The coordinate's x value is " + pt.x) console.log("The coordinate's y value is " + pt.y) } printCoord({ x: 100, y: 200 }) ``` You can actually use a type alias to give a name to any type at all, not just an object type. For example, a type alias can name a union type: ```ts twoslash type ID = number | string ``` When you use the alias, it's exactly as if you had written the aliased type. ```ts twoslash type UserInputSanitizedString = string declare function getInput(): string declare function sanitize(str: string): string // ---cut--- function sanitizeInput(str: string): UserInputSanitizedString { return sanitize(str) } // Create a sanitized input let userInput = sanitizeInput(getInput()) // Can still be re-assigned with a 'string' though userInput = 'new input' ``` ## Interfaces An *interface declaration* is another way to name an object type: ```ts twoslash {1} interface Point { x: number y: number } function printCoord(pt: Point) { console.log("The coordinate's x value is " + pt.x) console.log("The coordinate's y value is " + pt.y) } printCoord({ x: 100, y: 200 }) ``` ### Differences Between Type Aliases and Interfaces Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an `interface` are available in `type`, the key distinction is that a *type cannot be re-opened to add new properties* vs *an interface which is always extendable*. ::: tabs @tab Interface#interface Extending an interface ```ts twoslash {5} declare function getBear(): Bear // ---cut--- interface Animal { name: string } interface Bear extends Animal { honey: boolean } const bear = getBear() bear.name bear.honey ``` Adding new fields to an existing interface ```ts interface Window { title: string } interface Window { ts: TypeScriptAPI } const src = 'const a = "Hello World"' window.ts.transpileModule(src, {}) ``` @tab Type#type Extending a type via intersections ```ts twoslash {5} declare function getBear(): Bear // ---cut--- type Animal = { name: string } type Bear = Animal & { honey: boolean } const bear = getBear() bear.name bear.honey ``` A type cannot be changed after being created ```ts interface Window { title: string } // Error: Duplicate identifier 'Window'. interface Window { ts: TypeScriptAPI } ``` ::: ## Type Assertions Sometimes you will have information about the type of a value that TypeScript can’t know about. In this situation, you can use a *type assertion* to specify a more specific type: ```ts twoslash const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement ``` You can also use the angle-bracket syntax (except if the code is in a `.tsx` file), which is equivalent: ```ts twoslash const myCanvas = document.getElementById('main_canvas') ``` TypeScript only allows type assertions which convert to a *more specific* or *less specific* version of a type. This rule prevents "impossible" coercions like: ```ts twoslash // @errors: 2352 const x = 'hello' as number ``` Sometimes this rule can be too conservative and will disallow more complex coercions that might be valid. If this happens, you can use two assertions, first to `any` (or `unknown`, which we'll introduce later), then to the desired type: ```ts twoslash declare const expr: any type T = { a: 1; b: 2; c: 3 } // ---cut--- const a = expr as any as T ``` ## Literal Types Both `var` and `let` allow for changing what is held inside the variable, and `const` does not. This is reflected in how TypeScript creates types for literals. ```ts twoslash let changingString = 'Hello World' changingString = 'zhaobc' // Because `changingString` can represent any possible string, that // is how TypeScript describes it in the type system changingString // ^? const constantString = 'Hello World' // Because `constantString` can only represent 1 possible string, it // has a literal type representation constantString // ^? ``` By themselves, literal types aren't very valuable: ```ts twoslash // @errors: 2322 let x: 'hello' = 'hello' // OK x = 'hello' // NG x = 'zhaobc' ``` It's not much use to have a variable that can only have one value! But by *combining* literals into unions, you can express a much more useful concept - for example, functions that only accept a certain set of known values: ```ts twoslash // @errors: 2345 function printText(s: string, alignment: 'left' | 'center' | 'right') { // ... } printText('Hello', 'left') printText('World', 'centre') ``` Numeric literal types work the same way: ```ts twoslash function compare(a: string, b: string): -1 | 0 | 1 { return a === b ? 0 : a > b ? 1 : -1 } ``` Of course, you can combine these with non-literal types: ```ts twoslash // @errors: 2345 interface Options { width: number | string } function configure(x: Options | 'auto') { // ... } configure({ width: 100 }) configure('auto') configure('automatic') ``` ### Literal Inference When you initialize a variable with an object, TypeScript assumes that the properties of that object might change values later. For example, if you wrote code like this: ```ts twoslash declare const someCondition = true // ---cut--- const obj = { counter: 0, // ^? } if (someCondition) { obj.counter = 1 } ``` TypeScript *doesn’t* assume the assignment of `1` to a field which previously had `0` is an error. Another way of saying this is that `obj.counter` must have the type `number`, not `0`, because types are used to determine both reading and writing behavior. The same applies to strings: ```ts twoslash // @errors: 2345 declare function handleRequest(url: string, method: 'GET' | 'POST'): void const req = { url: 'http://example.com', method: 'GET', } handleRequest(req.url, req.method) ``` In the above example `req.method` is inferred to be `string`, not `"GET"`. Because code can be evaluated between the creation of `req` and the call of `handleRequest` which could assign a new string like `"GUESS"` to `req.method`, TypeScript considers this code to have an error. There are two ways to work around this. 1. You can change the inference by adding a type assertion in either location: ```ts twoslash declare function handleRequest(url: string, method: 'GET' | 'POST'): void // ---cut--- // Change 1: const req = { url: 'https://example.com', method: 'GET' as 'GET' } // Change 2 handleRequest(req.url, req.method as 'GET') ``` Change 1 means "I intend for `req.method` to always have the *literal type* `"GET"`", preventing the possible assignment of `"GUESS"` to that field after. Change 2 means "I know for other reasons that `req.method` has the value `"GET"`". 2. You can use `as const` to convert the entire object to be type literals: ```ts twoslash declare function handleRequest(url: string, method: 'GET' | 'POST'): void // ---cut--- const req = { url: 'https://example.com', method: 'GET' } as const handleRequest(req.url, req.method) ``` The `as const` suffix acts like `const` but for the type system, ensuring that all properties are assigned the literal type instead of a more general version like `string` or `number`. ## `null` and `undefined` JavaScript has two primitive values used to signal absent or uninitialized value: `null` and `undefined`. TypeScript has two corresponding *types* by the same names. How these types behave depends on whether you have the \[`strictNullChecks`] option on. ### `strictNullChecks` off With \[`strictNullChecks`] *off*, values that might be `null` or `undefined` can still be accessed normally, and the values `null` and `undefined` can be assigned to a property of any type. The lack of checking for these values tends to be a major source of bugs; we always recommend people turn [`strictNullChecks`](/tsconfig#strictNullChecks) on if it's practical to do so in their codebase. ### `strictNullChecks` on With \[`strictNullChecks`] *on*, when a value is `null` or `undefined`, you will need to test for those values before using methods or properties on that value. Just like checking for `undefined` before using an optional property, we can use *narrowing* to check for values that might be `null`: ```ts twoslash function doSomething(x: string | null) { if (x === null) { // do nothing } else { console.log(x.toUpperCase()) } } ``` ### Non-null Assertion Operator (Postfix `!`) TypeScript also has a special syntax for removing `null` and `undefined` from a type without doing any explicit checking. Writing `!` after any expression is effectively a type assertion that the value isn't `null` or `undefined`: ```ts twoslash function liveDangerously(x?: number | null) { // No error console.log(x!.toFixed()) } ``` Just like other type assertions, this doesn't change the runtime behavior of your code, so it's important to only use `!` when you know that the value *can't* be `null` or `undefined`. ## Enums Enums allow a developer to define a set of named constants. ```ts twoslash enum Direction { Up, Down, Left, Right, } ``` ## Less Common Primitives ### `bigint` From ES2020 onwards, there is a primitive in JavaScript used for very large integers, `BigInt`: ```ts twoslash // @target: es2020 // Creating a bigint via the BigInt function const oneHundred: bigint = BigInt(100) // Creating a BigInt via the literal syntax const anotherHundred: bigint = 100n ``` ### `symbol` There is a primitive in JavaScript used to create a globally unique reference via the function `Symbol()`: ```ts twoslash // @errors: 2367 const firstName = Symbol('name') const secondName = Symbol('name') if (firstName === secondName) { // Can't even happen } ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/index.md' --- # Vue 笔记 Vue 学习笔记 * [Vue2 学习笔记](https://github.com/FuckDoctors/hello-vue-2.0) 只是照着官方指南敲了敲代码 * Vue3 学习笔记 只是照着官方指南敲了敲代码 * [第一部分](https://github.com/FuckDoctors/hello-vue3) * [第二部分](./vue3/README.md) * [Vue.js 设计与实现](./vue3-book/) 本书源代码: * [Pinia 学习笔记](./pinia/) 只是照着官方指南敲了敲代码 --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/index.md' --- # Pinia 官方资料笔记 --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/core-concepts/index.md' --- # Core Concepts --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/core-concepts/actions.md' --- # Actions Actions are the equivalent of methods in components. They can be defined with the actions property in `defineStore()` and they are perfect to define **business logic**. Like getters, actions get access to the whole store instance through `this` with full typing (and autocompletion ✨) support. Unlike getters, `actions` **can be asynchronous**, you can await inside of actions any API call or even other actions! ```js import { defineStore } from 'pinia' import { mande } from 'mande' import { useAuthStore } from './auth-store' const api = mande('/api/users') export const useUsers = defineStore('users', { state: () => ({ userData: null, preferences: null, }), actions: { // async async registerUser(login, password) { this.userData = await api.post({ login, password }) }, async fetchUserPreferences() { // use another store const auth = useAuthStore() if (auth.isAuthenticated) { this.preferences = await fetchPreferences() } }, }, }) ``` If you would prefer not to use Composition API at all, you can use the `mapActions()` helper to map actions properties as methods in your component: ```js import { mapActions } from 'pinia' import { useCounterStore } from '../stores/counterStore' export default { methods: { // gives access to this.increment() inside the component // same as calling from store.increment() ...mapActions(useCounterStore, ['increment']) // same as above but registers it as this.myOwnName() ...mapActions(useCounterStore, { myOwnName: 'doubleCounter' }), }, } ``` ## Subscribing to actions It is possible to observe actions and their outcome with `store.$onAction()`. The callback passed to it is executed **before** the action itself. `after` handle promises and allows you to execute a function after the action resolves. In a similar way, onError allows you execute a function if the action throws or rejects. Here is an example that logs before running actions and after they resolve/reject. ```js const unsubscribe = someStore.$onAction( ({ name, // name of the action store, // store instance, same as 'someStore' args, // array of parameters passed to the action after, // hook after the action returns or resolves onError, // hook if the action throws or reject }) => { // a shared variable for this specific action call const startTime = Date.now() // this will trigger before an action on `store` is executed console.log(`Start "${name}" with params [${args.join(', ')}].`) // this will trigger if the action succeeds and after it has fully run. // it waits for any returned promised after(result => { console.log( `Finished "${name}" after ${ Date.now() - startTime }ms.\nResult: ${result}` ) }) // this will trigger if the action throws or returns a promise that rejects onError(error => { console.warn( `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.` ) }) } ) // manually remove the listener unsubscribe() ``` By default, action subscriptions are bound to the component where they are added (if the store is inside a component's `setup()`). Meaning, they will be automatically removed when the component is unmounted. If you want to keep them after the component is unmounted, pass `true` as the second argument to detach the action subscription from the current component: ```js export default { setup() { const someStore = useSomeStore() // this subscription will be kept after the component is unmounted someStore.$onAction(callback, true) }, } ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/core-concepts/define-store.md' --- # Defining a Store A store is defined using `defineStore()` and that it requires a **unique** name, passed as the first argument: ```js import { defineStore } from 'pinia' // useStore could be anything like useUser, useCart // the first argument is a unique id of the store across your application export const useStore = defineStore('main', { // other options }) ``` This *name*, also referred as id, is necessary and is used by Pinia to connect the store to the devtools. Naming the returned function use... is a convention across composables to make its usage idiomatic. ## Using a store We are *defining* a store because the store won't be created until `useStore()` is called inside of `setup()`: ```js import { useStore } from '@/store/counter' export default { setup() { const store = useStore() return { // you can return the whole store instance to use it in the template store, } }, } ``` You can define as many stores as you want and **you should define each store in a different file** to get the most out of pinia (like automatically allow your bundle to code split and TypeScript inference). Note that store is an object wrapped with `reactive`, meaning there is no need to write `.value` after getters but, like `props` in setup, **we cannot destructure it**: ```js export default defineComponent({ setup() { const store = userStore() // ❌ This won't work because it breaks reactivity // it's the same as destructuring from `props` const { name, doubleCount } = store name // "eduardo" doubleCount // 2 return { // will always be "eduardo" name, // will always be 2 doubleCount, // this one will be reactive doubleValue: computed(() => store.doubleCount), } }, }) ``` ::: playground#vue Destructure example @file App.vue ```vue ``` @file store.js ```js import { defineStore } from 'pinia' export const useStore = defineStore('test', { state: () => ({ name: 'zhaobc', count: 0, }), getters: { doubleCount: state => state.count * 2, }, actions: { increment() { this.count++ }, }, }) ``` @setting ```json { "service": "https://play.zhaobc.site" } ``` ::: In order to extract properties from the store while keeping its reactivity, you need to use `storeToRefs()`. It will create refs for every reactive property. Note you can destructure actions directly from the store as they are bound to the store itself too: ```js import { storeToRefs } from 'pinia' export default defineComponent({ setup() { const store = useStore() // `name` and `doubleCount` are reactive refs // This will also create refs for properties added by plugins // but skip any action or non reactive (non ref/reactive) property const { name, doubleCount } = storeToRefs(store) // the increment action can be just extracted const { increment } = store return { name, doubleCount, increment, } }, }) ``` ::: playground#vue Destructure example using storeToRefs @file App.vue ```vue ``` @file store.js ```js import { defineStore } from 'pinia' export const useStore = defineStore('test', { state: () => ({ name: 'zhaobc', count: 0, }), getters: { doubleCount: state => state.count * 2, }, actions: { increment() { this.count++ }, }, }) ``` @setting ```json { "service": "https://play.zhaobc.site" } ``` ::: --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/core-concepts/getters.md' --- # Getters Getters are exactly the equivalent of computed values for the state of a Store. They can be defined with the getters property in `defineStore()`. They receive the state as the first parameter to encourage the usage of arrow function: we can get access to the whole store instance through `this` when defining a regular function, but it is necessary to define the type of the return type (in TypeScript). ```js export const useStore = defineStore('main', { state: () => ({ counter: 0, }), getters: { // Passing arguments to getters doubleCount(state) { return state.counter * 2 }, // the return **must** be explicitly set doublePlusOne(): number { // autocompletion and typings for the whole store // Accessing other getters return this.doubleCount + 1 }, }, }) ``` To use another store getters, you can directly use it inside of the getter: ```js import { useOtherStore } from './other-store' export const useStore = defineStore('main', { state: () => ({ // ... }), getters: { otherGetter(state) { const otherStore = useOtherStore() return state.localData + otherStore.data }, }, }) ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/core-concepts/plugins.md' --- # Plugins Pinia stores can be fully extended thanks to a low level API. Here is a list of things you can do: * Add new properties to stores * Add new options when defining stores * Add new methods to stores * Wrap existing methods * Change or even cancel actions * Implement side effects like Local Storage * Apply only to specific stores Plugins are added to the pinia instance with `pinia.use()`. The simplest example is adding a static property to all stores by returning an object: ```js import { createPinia } from 'pinia' // add a property named `secret` to every store that is created after this plugin is installed // this could be a different file function SecretPiniaPlugin() { return { secret: 'the cake is a lie', } } const pinia = createPinia() // give the plugin to pinia pinia.use(SecretPiniaPlugin) // in another file const store = useStore() store.secret // 'the cake is a lie' ``` This is useful to add global objects like the router, modal, or toast managers. ## Introduction A Pinia plugin is a function that optionally returns properties to be added to a store. It takes one optional argument, a context: ```js export function myPiniaPlugin(context) { context.pinia // the pinia created with `createPinia` context.app // the current app created with `createApp` (Vue3 only) context.store // the store the plugin is augmenting context.options // the options object defining the store passed to `defineStore()` // ... } pinia.use(myPiniaPlugin) ``` Plugins are only applied to stores **created after** `pinia` is passed to the app, otherwise they won't be applied. ## Augmenting(扩大,增大,增强) a Store You can add properties to every store by simply returning an object of them in a plugin: ```js pinia.use(() => ({ hello: 'world', })) ``` You can also set the property directly on the store but if possible `use the return version` so they can be automatically tracked by devtools: ```js pinia.use(({ store }) => { store.hello = 'world' }) ``` Note that every store is wrapped with `reactive`, **automatically unwrapping** any `Ref` (`ref()`, `computed()`, ...) it contains: ```js const sharedRef = ref('shared') pinia.use(({ store }) => { // each store has its individual `hello` property store.hello = ref('secret') // it gets automatically unwrapped store.hello // 'secret' // all stores are sharing the value `shared` property store.shared = sharedRef store.shared // 'shared' }) ``` This is why you can access all computed properties without .value and why they are reactive. ## Adding new state If you want to add new state properties to a store or properties that are meant to be used during hydration, you will have to add it in two places: * On the `store` so you can access it with `store.myState` * On `store.$state` so it can be used in devtools and, be serialized during SSR. On top of that, you will certainly have to use a `ref()` (or other reactive API) in order to share the value across different accesses: ```js import { toRef, ref } from 'vue' pinia.use(({ store }) => { // to correctlly handle SSR, we need to make sure we are not overriding an existing value if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) { // hasError is defined within the plugin, so each store has their individual state property const hasError = ref(false) // setting the variable on `$state`, allows it be serialized during SSR store.$state.hasError = hasError } // we need to transfer the ref from the state to the store, // this way both accesses: store.hasError and store.$state.hasError // will work and share the same variable store.hasError = toRef(store.$state, 'hasError') // in this case it's better not to return `hasError` since it // will be displayed in the `state` section in the devtools // anyway and if we return it, devtools will display it twice. }) ``` ## Adding new external properties When adding external properties, class instances that come from other libraries, or simply things that are not reactive, you should wrap the object with `markRaw()` before passing it to pinia. ```js import { markRaw } from 'vue' import { router } from './router' pinia.use(store => { store.router = markRaw(router) }) ``` ## Calling `$subscribe` inside plugins You can use [`store.$subscribe`](https://pinia.vuejs.org/core-concepts/state.html#subscribing-to-the-state) and [`store.$onAction`](https://pinia.vuejs.org/core-concepts/actions.html#subscribing-to-actions) inside plugins too: ```js pinia.use(({ store }) => { store.$subscribe(() => { // react to store changes }) store.$onAction(() => { // react to store anctions }) }) ``` ## Adding new options It is possible to create new options when defining stores to later on consume them from plugins. For example, you could create a `debounce` option that allows you to debounce any action: ```js const useSearchStore = defineStore('search', { actions: { searchContacts() { // ... } } // this will be read by a plugin later on debounce: { // debounce the action searchContacts by 300ms searchContacts: 300 } }) ``` The plugin can then read that option to wrap actions and replace the original ones: ```js // use any debounce library import debounce from 'loadash/debounce' pinia.use(({ options, store }) => { if (options.debounce) { // we are overriding the actions with new ones return Object.keys(options.debounce).reduce((debounceActions, action) => { debounceActions[action] = debounce( store[action], options.debounce[action] ) }) } }) ``` Note that custom options are passed as the 3rd argument when using the setup syntax: ```js defineStore( 'search', () => { // ... }, { // this will be read by a plugin later on debounce: { searchContacts: 300, }, } ) ``` ## TypeScript ### A Pinia plugin can be typed as follows ```ts import { PiniaPluginContext } from 'pinia' export function myPiniaPlugin(context: PiniaPluginContext) { // ... } ``` ### Typing new store properties When adding new properties to stores, you should also extend the `PiniaCustomProperties` interface. ```ts import 'pinia' declare module 'pinia' { export interface PiniaCustomProperties { // by using a setter we can allow string and refs set hello(value: string | Ref) get hello(): string // you can define simpler value too simpleNumber: number } } ``` It can then be written and read safely: ```ts pinia.use(({ store }) => { store.hello = 'hola' store.hello = ref('hola') store.simpleNumber = Math.random() // @ts-expect-error: we haven't typed this correctlly sotre.simpleNumber = ref }) ``` `PiniaCustomProperties` is a generic type that allows you to reference properties of a store. ```ts pinia.use(({ options }) => ({ $options options })) ``` We can properly type this by using the 4 generic types of `PiniaCustomProperties:` ```ts import 'pinia' declare module 'pinia' { export interface PiniaCustomProperties s options? A } } } ``` ### Typing new state When adding new state properties (to both, the `store` and `store.$state`), you need to add the type to `PiniaCustomStateProperties` instead. Differently from `PiniaCustomProperties`, it only receives the State generic: ```ts import 'pinia' declare module 'pinia' { export interface DefineCustomStateProperties { hello: string } } ``` ### Typing new creation options When creating new options for `defineStore()`, you should extend the `DefineStoreOptionsBase`. Differently from `PiniaCustomProperties`, it only exposes two generics: the State and the Store type, allowing you to limit what can be defined. For example, you can use the names of the actions: ```ts import 'pinia' define module 'pinia' { export interface DefineStoreOptionsBase { // allow defining a number of ms for any of the actions debounce?: Partial, number>> } } ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/core-concepts/state.md' --- # State The state is, most of the time, the central part of your store. In Pinia the state is **defined as a function that returns the initial state**. ```js import { defineStore } from 'pinia' const useStore = defineStore('storeId', { // arrow function recommended for full type inference state: () => { return { // all these properties will have their type inferred automatically counter: 0, name: 'Eduardo', isAdmin: true, } }, }) ``` ::: tip If you are using Vue 2, the data you create in state follows the same rules as the data in a Vue instance, ie the state object must be plain and you need to call `Vue.set()` when adding new properties to it. See also: [Vue#data](https://v2.vuejs.org/v2/api/#data). ::: ## Accessing the `state` By default, you can directly read and write to the state by accessing it through the `store` instance: ```js const store = useStore() store.counter++ ``` ## Resetting the state You can *reset* the state to its initial value by calling the `$reset()` method on the store: ```js const store = useStore() store.$reset() ``` ## Usage with the Options API ```js // src/stores/counterStore.js import { defineStore } from 'pinia' const useCounterStore = defineStore('counterStore', { state: () => ({ counter: 0, }), }) ``` If you are not using the Composition API, and you are using `computed`, `methods`, ..., you can use the `mapState()` helper to map state properties as readonly computed properties: ```js import { mapState } from 'pinia' import { useCounterStore } from '@/stores/counterStore' export default { computed: { // gives access to this.counter inside the component // same as reading from store.counter ...mapState(useCounterStore, ['counter']), // same as above but registers is as this.myOwnName ...mapState(useCounterStore, { myOwnName: 'counter', // you can also write a function that gets access to the store double: store => store.counter * 2, // it can have access to `this` but it won't be typed correctlly magicValue(store) { return store.someGetter + this.counter + this.double }, }), }, } ``` ### Modifiable state If you want to be able to write to these state properties (e.g. if you have a form), you can use `mapWritableState()` instead. Note you cannot pass a function like with `mapState()`: ```js import { mapWritableState } from 'pinia' import { useCounterStore } from '../stores/counterStore' export default { computed: { // gives access to this.counter inside the component and allows setting it // this.counter++ // same as reading from store.counter ...mapWritableState(useCounterStore, ['counter']) // same as above but registers it as this.myOwnName ...mapWritableState(useCounterStore, { myOwnName: 'counter', }), }, } ``` ::: tip You don't need `mapWritableState()` for collections like arrays unless you are replacing the whole array with cartItems = \[], `mapState()` still allows you to call methods on your collections. ::: ::: playground#vue Usage with the Options API @file App.vue ```vue ``` @file counterStore.js ```js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { doubleCount: state => state.count * 2, }, actions: { increment() { this.count++ }, }, }) ``` @setting ```json { "service": "https://play.zhaobc.site" } ``` ::: ## Mutating the state Apart from directly mutating the store with store.counter++, you can also call the `$patch` method. It allows you to apply multiple changes at the same time with a partial state object: ```js store.$patch({ counter: store.counter + 1, name: 'zhaobc', }) ``` However, some mutations are really hard or costly to apply with this syntax: any collection modification (e.g. pushing, removing, splicing an element from an array) requires you to create a new collection. Because of this, the `$patch` method also accepts a function to group this kind of mutations that are difficult to apply with a patch object: ```js cartStore.$patch(state => { state.items.push({ name: 'shoes', quantity: 1, }) state.hasChanged = true }) ``` ## Replacing the state You can replace the whole state of a store by setting its `$state` property to a new object: ```js store.$state = { counter: 666, name: 'zhaobc', } ``` You can also replace the whole state of your application by changing the state of the pinia instance: ```js pinia.state.value = {} ``` ## Subscribing to the state You can watch the state and its changes through the `$subscribe()` method of a store, similar to Vuex's subscribe method. The advantage of using `$subscribe()` over a regular `watch()` is that subscriptions will trigger only **once** after patches (e.g. when using the function version from above). ```js cartStore.$subscribe((mutation, state) => { // import { MutationType } from 'pinia' mutation.type // 'direct' | 'patch object' | 'patch function' mutation.storeId // cart mutation.payload // patch object passed to cartStore.$patch // persist the whole state to the local storage whenever it changes localStorage.setItem('cart', JSON.stringify(state)) }) ``` By default, state subscriptions are bound to the component where they are added (if the store is inside a component's `setup()`). Meaning, they will be automatically removed when the component is unmounted. If you want to keep them after the component is unmounted, pass `{ detached: true }` as the second argument to detach the state subscription from the current component: ```js export default { setup() { const someState = useSomeStore() // this subscription will be kept after the component is unmounted someStore.$subscribe(callback, { detached: true }) }, } ``` You can watch the whole state on the pinia instance: ```js watch( pinia.state, state => { // persist the whole state to the local storage whenever it changes localStorage.setItem('piniaState', JSON.stringify(state)) }, { deep: true, } ) ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/intro/index.md' --- # Intro --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/intro/get-started.md' --- # Get Started ## Installation Create a pinia (the root store) and pass it to the app: ```js import { createPinia } from 'pinia' app.use(createPinia()) ``` If you are using Vue 2, you also need to install a plugin and inject the created pinia at the root of the app: ```js import { PiniaVuePlugin, createPinia } from 'pinia' Vue.use(PiniaVuePlugin) const pinia = createPinia() new Vue({ el: '#app', // other options // ... // note the same `pinia` instance can be used across multiple Vue apps on the same page pinia, }) ``` ## What is a Store? A Store (like Pinia) is an entity holding state and business logic that isn't bound to your Component tree. In other words, **it hosts global state**. It's a bit like a component that is always there and that everybody can read off and write to. It has **three concepts**, the **state**, **getters** and **actions** and it's safe to assume these concepts are the equivalent of **data**, **computed** and **methods** in components. ## When should I use a Store A store should contain data that can be accessed throughout your application. This includes data that is used in many places, e.g. User information that is displayed in the navbar, as well as data that needs to be preserved through pages, e.g. a very complicated multi-step form. Not all applications need access to a global state, but if yours need one, Pinia will make your life easier. --- --- url: 'https://zhaobc.site/notes/frontend/vue/pinia/intro/intro.md' --- # Introduction Pinia (pronounced /piːnjʌ/, like "peenya" in English) is a store library for Vue, it allows you to share a state across components/pages. * Works for both Vue 2 and Vue 3 * Optional composition API * The same API for SSR. * TypeScript support * Hot module replacement * Plugins ## Basic example Create a store: ```js // stores/counter.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => { return { count: 0, } }, // could also be defined as // state: () => ({ count: 0 }) actions: { increment() { this.count++ }, }, }) ``` And then you use it in a component: ```js import { useCounterStore } from '@/stores/counter' export default { setup() { const counter = useCounterStore() counter.count++ // with autocompletion counter.$patch({ count: counter.count + 1 }) // or using a action instead counter.increment() }, } ``` ::: playground#vue Basic example @file App.vue ```vue ``` @file counterStore.js ```js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => { return { count: 0, } }, actions: { increment() { this.count++ }, }, }) ``` @setting ```json { "service": "https://play.zhaobc.site" } ``` ::: You can even use a function (similar to a component `setup()`) to define a Store for more advanced use cases: ```js export const useCounterStore = defineStore('counter', () => { const count = ref(0) function increment() { count.value++ } return { count, increment, } }) ``` ::: playground#vue setup example @file App.vue ```vue ``` @file counterStore.js ```js import { ref } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', () => { const count = ref(0) function increment() { count.value++ } return { count, increment, } }) ``` @setting ```json { "service": "https://play.zhaobc.site" } ``` ::: If you are still not into `setup()` and Composition API, don't worry, Pinia also support a similar set of map helpers like Vuex. You define stores the same way but then use `mapStores()`, `mapState()`, or `mapActions()`: ```js const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { double: state => state.count * 2, }, actions: { increment() { this.count++ }, }, }) const useUserStore = defineStore('user', { // ... }) export default { computed: { // other computed properties // ... // gives access to this.counterStore and this.userStore ...mapStores(useCounterStore, useUserStore), // gives read access to this.count and this.double ...mapState(useCounterStore, ['count', 'double']), }, methods: { // gives access to this.increment() ...mapActions(useCounterStore, ['increment']), }, } ``` ::: playground#vue Option API example @file App.vue ```vue ``` @file counterStore.js ```js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { double: state => state.count * 2, }, actions: { increment() { this.count++ }, }, }) ``` @file userStore.js ```js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ name: 'zhaobc', age: 18, isLogin: false, roles: [], }), getters: { isAdmin: state => state.roles.includes('admin'), }, actions: { login() { this.isLogin = true }, }, }) ``` @setting ```json { "service": "https://play.zhaobc.site" } ``` ::: ## A more realistic example Here is a more complete example of the API you will be using with Pinia with types even in JavaScript. ```js import { defineStore } from 'pinia' export const todos = defineStore('todos', { state: () => ({ /** @type {{ text: string, id: number, isFinished: boolean }[]} */ todos: [], /** @type {'all' | 'finished' | 'unfinished'} */ filter: 'all', // type will be automatically inferred to number nextId: 0, }), getters: { finishedTodos(state) { // autocompletion return state.todos.filter(todo => todo.isFinished) }, unfinishedTodos(state) { return state.todos.filter(todo => !todo.isFinished) }, /** * @returns {{ text: string, id: number, isFinished: boolean }[]} */ filteredTodos(state) { if (this.filter === 'finished') { // call other getters with autocompletion return this.finishedTodos } else if (this.filter === 'unfinished') { return this.unfinishedTodos } return this.todos }, }, actions: { // any amount of arguments, return a promise or not addTodo(text) { // you can directly mutate the state this.todos.push({ text, id: this.nextId++, isFinished: false, }) }, }, }) ``` ## Comparison with Vuex Compared to Vuex, Pinia provides a simpler API with less ceremony, offers Composition-API-style APIs, and most importantly, has solid type inference support when used with TypeScript. ### Comparison with Vuex 3.x/4.x > Vuex 3.x is Vuex for Vue 2 while Vuex 4.x is for Vue 3 Pinia API is very different from Vuex ≤4, namely: * Mutations no longer exist * TypeScript support * No more magic strings to inject * No need to dynamically add stores, they are all dynamic by default * No more nested structuring of modules, Pinia offers a flat structuring by design * No namespaced modules. Given the flat architecture of stores, "namespacing" stores is inherent to how they are defined and you could say all stores are namespaced. For more detailed instructions on how to convert an existing Vuex ≤4 project to use Pinia, see the [Migration from Vuex Guide](https://pinia.vuejs.org/cookbook/migration-vuex.html). --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3-book/index.md' --- # Vue.js 设计与实现 ## 前言 记录 Vue.js 设计与实现的部分学习笔记。 ## 第一篇 框架设计概览 * [第 1 章 权衡的艺术](./chapter01.md) * [第 2 章 框架设计的核心要素](./chapter02.md) * [第 3 章 Vue.js 3 的设计思路](./chapter03.md) ## 第二篇 响应系统 * [第 4 章 响应系统的作用与实现](./chapter04.md) --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3-book/chapter01.md' --- # 第 1 章 权衡的艺术 ## 1.1 命令式和声明式 从范式上来看,视图层框架通常分为命令式和声明式。 * 命令式**关注过程** * 声明式**关注结果** 早年流行的 jQuery 是典型的命令式框架,Vue.js 是声明式框架。 例如,我们把下面的这段话翻译成对应的代码: ```txt - 获取 id 为 app 的 div 标签 - 它的文本内容为 hello world - 为其绑定点击事件 - 当点击时弹出提示:ok ``` jQuery 代码: ```js $('#app') .text('hello world') .on('click', () => { alert('ok') }) ``` 原生 JavaScript 代码: ```js const div = document.querySelector('#app') div.innerText = 'hello world' div.addEventListener('click', () => { alert('ok') }) ``` 以上代码本身是在描述“做事的过程”。 Vue.js 代码: ```vue
hello world
``` 可以看到,我们提供的是一个“结果”,至于如何实现这个“结果”,我们并不关心。实现这个“结果”,是 Vue.js 帮我们实现的,它内部是**命令式**的,而暴露给用户的却更加**声明式**。 ## 1.2 性能与可维护性的权衡 命令式和声明式各有优缺点,在框架设计方面,则体现在性能与可维护性的权衡。这里先抛出一个结论:**声明式代码的性能不优于命令式代码的性能**。 **毕竟框架本省就是封装了命令式代码才实现了面向用户的声明式**。 ## 1.3 虚拟 DOM 的性能到底如何 性能:**声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗** 如果我们能够**最小化找出差异的性能消耗**,就可以让声明式代码的性能无限接近命令式的性能。 涉及 DOM 的运算要远比 JavaScript 层面的计算性能差。 虚拟 DOM 在创建页面时的性能:**创建 JavaScript 对象的计算量 + 创建真实 DOM 的计算量** 虚拟 DOM 在更新页面时的性能:**创建新的 JavaScript 对象 + Diff + 渲染 HTML 字符串** ## 1.4 运行时和编译时 设计框架的三种选择: * 纯运行时 * 运行时 + 编译时 * 编译时 设计一个运行时框架,为它提供一个 Render 函数,Render 函数根据用户提供的树形结构的数据对象,渲染成 DOM 元素。 规定树形结构的数据对象如下: ```js const obj = { tag: 'div', children: [ { tag: 'span', children: 'hello world', }, ], } ``` 每个对象有两个属性: * tag 代表标签名称 * children 即可以是一个数据组(代表子节点),也可以直接是一个文本(代表文本节点) Render 函数如下: ```js function Render(obj, root) { const el = document.createElement(obj.tag) if (typeof obj.children === 'string') { const text = document.createTextNode(obj.children) el.appendChild(text) } else if (obj.children) { // 数组,递归调用Render,使用el作为root参数 obj.children.forEach(child => Render(child, el)) } // 将元素添加到root root.appendChild(el) } ``` 使用 ```js Render(obj, document.body) ``` 下面内容为代码演示结果 :::normal-demo 运行时 Render 函数示例 ```html
``` ```js const obj = { tag: 'div', children: [ { tag: 'span', children: 'hello world', }, ], } function Render(obj, root) { const el = window.document.createElement(obj.tag) if (typeof obj.children === 'string') { const text = window.document.createTextNode(obj.children) el.appendChild(text) } else if (obj.children) { // 数组,递归调用Render,使用el作为root参数 obj.children.forEach(child => Render(child, el)) } // 将元素添加到root root.appendChild(el) } Render(obj, window.document.querySelector('#code-demo-render-basic')) ``` ::: ::: warning 本主题的代码演示采用了 `ShadowRoot` 方式,将代码演示和文档的主 DOM 树分开渲染。 所以,代码中的 `document` 会被定义为 演示代码块的一个 `ShadowRoot` \[^shadowroot], `document.createElement` 会报错,没法直接显示代码演示的结果。 本例直接使用了 `window.document` 来强制使用文档的主 DOM , 为了能看到输出结果,在代码演示的外面,加了一个 id 为 `code-demo-render-basic` 的 `div`。 ::: 上面的 Render 函数虽然可以根据树形结构数据渲染内容,但是手写树形结构数据太麻烦,而且不直观,能够用 HTML 标签的方式来描述就好了。 为了满足这个需求,考虑加入编译手段,把 HTML 标签编译成树形结构就可以继续使用 Render 函数了。 于是,可以考虑写一个 Compiler 函数,配合 Render 函数使用。这样我们的框架就编程了**运行时 + 编译时**。 ## 1.5 总结 * 讨论了声明式和命令式的区别 * 讨论了虚拟 DOM 的性能 声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗 * 介绍了运行时和编译时,并且可以看出 Vue.js 3 是一个编译时 + 运行时的框架 \[^shadowroot]: Shadow DOM API 的 [ShadowRoot](https://developer.mozilla.org/zh-CN/docs/Web/API/ShadowRoot) 接口是一个 DOM 子树的根节点, 它与文档的主 DOM 树分开渲染。 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3-book/chapter02.md' --- # 第 2 章 框架设计的核心要素 ## 2.1 提升用户的开发体验 在框架设计和开发过程中,提供友好的警告信息至关重要。 Vue.js 3 中为了方便的在控制台输出 `ref` 数据,提供了自定义的 formatter,在 `initCustomFormatter` 函数中。 在 Chrome 中,打开 DevTools 的设置,勾选 "Console" -> "Enable custom formatters" 开启。 ## 2.2 控制框架代码的体积 框架的大小也是衡量框架的标准之一。 Vue.js 3 的源代码中,每个 `warn` 函数的调用都会配合 `__DEV__` 常量的检查,例如: ```js if (__DEV__ && !res) { warn( `Failed to mount app: mount target selector "${container}"` returned null. ) } ``` Vue.js 使用 rollup.js 对项目进行构建,这里的 `__DEV__` 常量实际上是通过 rollup.js 的插件配置来预定义的,其功能类似于 webpack 中的 DefinePlugin 插件。 针对不同的环境,比如开发环境和生产环境,把 `__DEV__` 替换成 `true` 和 `false` 来控制这块代码的执行与否。 当 `__DEV__` 为 `false` 时,这段代码永远都不会执行,被认为是 dead code, 它不会出现在最终产物中,在构建资源的时候就会被移除。 这样,我们就做到了**在开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积。**。 ## 2.3 框架要做到良好的 Tree-Shaking 简单来说,Tree-Shaking 指的是消除那些永远不会执行的代码,也就是排除 dead code。 想要实现 Tree-Shaking ,必须满足一个条件,即模块必须是 ESM(ES Module) ,因为 Tree-Shaking 依赖 ESM 的静态结构。 使用 rollup 打包 esm: ```js npx rollup input.js -f esm -o bundle.js ``` 这句命令的意思是,以 input.js 文件为入口,输出 ESM,输出的文件叫 bundle.js。 Tree-Shaking 的第二个关键点 —— 副作用。如果一个函数调用会产生副作用,那么就不能将其移除。 简单地说,副作用就是,当调用函数的时候会对外部产生影响,例如修改了全局变量。 而到底会不会产生副作用,只有代码真正运行的时候才会知道。 JavaScript 本身是动态语言,静态地分析 JavaScript 代码很困难, 因此,像 rollup.js 这类的工具会提供一个机制,让我们能明确的告诉 rollup.js: “放心吧,这段代码不会产生副作用,你可以移除它。” 如下所示: ```js import { foo } from './utils' /*#__PURE__*/ foo() ``` 注意注释代码 `/*#__PURE__*/`,其作用就是告诉 rollup.js,对应 foo 函数的调用不会产生副作用,你可以放心地对其进行 Tree-Shaking。 因此,在编写框架时,合理使用`/*#__PURE__*/`注释,可以做到更好的 Tree-Shaking,Vue.js 3 中大量使用了该注释。 例如: ```js export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS) ``` 这样编写代码也不会造成很大的心智负担,因为通常产生副作用的代码都是模块内的顶级调用。 什么是顶级调用?如下所示: ```js foo() // 顶级调用 function bar() { foo() // 函数内调用 } ``` 只要 bar 没有被调用,自然不会产生副作用。 ## 2.4 框架应该输出怎样的构建产物 在 HTML 中直接使用时,需要输出一种叫 IIFE 格式的资源。 ```html ``` IIFE 的全称是 Immediately Invoked Function Expression,即“立即调用的函数表达式”,例如: ```js ;(function () { // ... })() ``` 实际上,vue.global.js 文件就是 IIFE 格式的资源,它的代码结构如下所示: ```js var Vue = (function (exports) { // ... exports.createApp = createApp // ... return exports })({}) ``` 这样当我们使用 \ ``` 除了可以直接使用\ ``` 将被编译为: ```js export default { data() { /* ... */ }, methods: { handler: () => { /* ... */ }, }, render() { return h('div', { onClick: handler }, 'click me') }, } ``` 无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的。 ## 3.5 Vue.js 是各个模块组成的有机整体 Vue.js 中**编译器**和**渲染器**会配合工作,以提升性能。 比如,编译器有分析动态内容的能力,能识别哪些是静态属性,哪些是动态属性,生成代码时附带这些信息,供渲染器使用,比如 `patchFlags`。 ## 3.6 总结 * Vue.js 是声明式框架,可以用模板方式描述,也可以用渲染函数(虚拟 DOM)来描述 * 渲染器的作用是把虚拟 DOM 对象渲染为真实 DOM 元素 * 组件的本质是一组虚拟 DOM 的封装,它可以是一个返回虚拟 DOM 的函数,也可以是一个对象 * Vue.js 的模板 会被编译器编译为渲染函数 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3-book/chapter04.md' --- # 第 4 章 响应系统的作用与实现 响应系统是 Vue.js 的重要组成部分,Vue.js 3 采用 Proxy 实现响应式数据。 ## 4.1 响应式数据与副作用函数 副作用函数是指会产生副作用的函数。副作用函数的执行会直接或间接影响其他的变量或其他函数的执行。 如下所示: ```js const state = { text: null } const obj = { text: 'hello world' } function effect() { state.text = obj.text document.body.innerText = obj.text } ``` effect 执行会读取 obj,并设置 state,以及更改 DOM 元素,这个就是副作用。 但是,obj.text 改了之后,并不会自动执行 effect 函数。 ## 4.2 响应式数据的基本实现 观察可以看出: * 副作用函数 effect 执行时,会出发 obj.text 的**读取**操作 * 修改 obj.text 时,会触发**设置**操作 如果我们能拦截一个对象的读取和设置操作,那么就能做一些额外的操作。 在 ES2015 之前只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,可以使用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。 ::: note Proxy 示例 ::: normal-demo Proxy 示例 ```html
``` ```js // 存储副作用函数的桶 const bucket = new Set() // 原始数据 const data = { text: 'hello world' } // 对原始数据的代理 const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 effect 添加到副作用函数桶中 bucket.add(effect) // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶中取出来并执行 bucket.forEach(fn => fn()) // 返回 true 代表 设置成功 return true }, }) // 副作用函数 function effect() { window.document.querySelector('#effect-proxy-demo').innerText = obj.text } // 执行副作用函数,触发读取 effect() function changeText() { obj.text = 'hello vue3' } ``` ::: 上述示例,可以达到响应式的结果。 ## 4.3 设计一个完善的响应式系统 从上一节中看出,一个响应式系统的工作流如下: * 当**读取**操作发生时,将副作用收集到桶中 * 当**设置**操作发生时,从桶中取出副作用函数并执行 上节中的副作用函数 effect 硬编码,不合适,我们要做的是,哪怕副作用是匿名函数也能够被正确的收集。 为了实现这一点,我们需要提供一个用来注册副作用函数的机制,如下所示: ::: note 改进后的示例 ::: normal-demo 改进后的示例 ```html
``` ```js // 用一个全局变量存储被注册的副作用函数 let activeEffect // effect 函数用于注册副作用函数 function effect(fn) { // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect activeEffect = fn // 执行副作用函数 fn() } // 存储副作用函数的桶 const bucket = new Set() // 原始数据 const data = { text: 'hello world' } // 对原始数据的代理 const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将 activeEffect 中存储的副作用函数收集到桶中 if (activeEffect) { // 新增 bucket.add(activeEffect) } // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶中取出来并执行 bucket.forEach(fn => fn()) // 返回 true 代表 设置成功 return true }, }) // 使用 effect 注册副作用函数 effect(() => { // 匿名副作用函数 console.log('effect run') // 会打印 2 次,注册时立即执行了一次,后面更改 obj.notExist 时会再执行一次 window.document.querySelector('#effect-proxy-demo2').innerText = obj.text }) function changeText() { obj.notExist = 'hello vue3' } ``` ::: 上面代码可以看出,匿名副作用函数内部读取了 obj.text 的值,于是匿名函数与字段 obj.text 之间建立了响应联系。 但是,点击 change text 时,在匿名副作用内并没有读取 obj.notExist 属性的值,所以,理论上字段 obj.notExist 并没有与副作用建立响应联系。 因此,点击按钮时,不应该出发匿名副作用,这是不正确的,为了解决这个问题,我们应该重新设计桶。 在上例中,我们**没有在副作用函数与被操作的目标字段之间建立明确的联系**。当读取属性时,无论读取的是哪一个属性,都会把副作用函数收集到桶中; 当设置属性时,也都会把桶中的副作用函数取出来并执行。解决办法只需要在副作用与被操作的字段直接建立联系。 Set 类型的桶,不能实现这个目的,需要使用 WeakMap 代替 Set 作为桶的数据结构。 ::: note WeakMap 类型的桶 ::: normal-demo WeakMap 类型的桶 ```html
``` ```js // 元素数据 const data = { ok: true, text: 'hello world', } // 存储副作用的桶 const bucket = new WeakMap() let activeEffect const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 添加到桶中 track(target, key) // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶中取出并执行 trigger(target, key) return true }, }) // 在 get 拦截函数内调用 track 函数追踪变化 function track(target, key) { // 没有 activeEffect 直接返回 if (!activeEffect) return // 根据 target 从桶中取得 depsMap,它也是一个 Map 类型,key: effects let depsMap = bucket.get(target) // 如果不存在 depsMap,则新建一个 Map 与 target 关联 if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型 // 里面存储着所有与当前 key 关联的副作用函数:effects let deps = depsMap.get(key) // 如果 deps 不存在,则同样新建一个 Set 与 key 关联 if (!deps) { depsMap.set(key, (deps = new Set())) } // 最后将当前激活的副作用添加到桶中 deps.add(activeEffect) } // 在 set 拦截函数内调用 trigger 函数出发变化 function trigger(target, key) { // 根据 target 从桶中取得 depsMap,它是 key: effects const depsMap = bucket.get(target) if (!depsMap) return // 根据 key 取得所有副作用函数 const effects = depsMap.get(key) // 执行副作用函数 effects && effects.forEach(fn => fn()) } // effect 函数用于注册副作用函数 function effect(fn) { // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect activeEffect = fn // 执行副作用函数 fn() } // 使用 effect 注册副作用函数 effect(() => { // 匿名副作用函数 console.log('effect run - weekmap') // 会打印 2 次,注册时立即执行了一次,后面更改 obj.text 时会再执行一次,obj.notExist 不执行 window.document.querySelector('#effect-proxy-weakmap').innerText = obj.text }) function changeText() { obj.text = 'hello vue3' obj.notExist = 'hello vue3' } ``` ::: 从上述代码中可以看出构建数据的方式,分别使用了 WeakMap, Map 和 Set: * WeakMap 是 target: Map 键值对 * Map 是 target.key: effects (副作用) 键值对 那么,WeakMap 跟 Map 有什么区别呢? WeakMap 对 key 是弱引用,WeakMap 的 key 是不可枚举的,不影响垃圾回收器的工作。 参考资料:[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) ::: normal-demo Map 和 WeakMap 请打开控制台查看: ```js const map = new Map() const weakmap = new WeakMap() ;(function () { const foo = { foo: 1 } const bar = { bar: 2 } map.set(foo, 1) weakmap.set(bar, 2) })() // 可以打印出 foo,说明 foo 没有被回收 console.log('map.keys', map.keys().next().value) // WeakMap 无法获取 key,也就无法获取对象 bar console.log('weakmap', weakmap) ``` ::: ## 4.4 分支切换与 cleanup 什么是分支定义?先看下面的代码: ```js const data = { ok: true, text: 'hello world' } const obj = new Proxy(data, { /* ... */ }) effect(() => { document.body.innerText = obj.ok ? obj.text : 'not' }) ``` 上面的三元表达式中,当字段 obj.ok 发送变化时,代码执行的分支就会跟着变化,这就是分支切换。 分支切换可能会产生遗留的副作用函数。上面的代码中,会触发 obj.ok 和 obj.text 的读取操作,所以会收集它们俩对应的副作用函数。 当 obj.ok 修改为 false 时,会触发副作用函数重新执行后,由于此时字段 obj.text 不会被读取,只会执行 obj.ok 的读取操作。 所以,理想情况下,副作用函数不应该被字段 obj.text 所对应的依赖集合收集。 遗留的副作用会导致不必要的更新。 但是上例中,obj.ok 改为 false 时,无论 obj.text 如何变,document.body.innerText 的值始终是 'not' 。 所以,最好的结果是,无论 obj.text 如何变,都不需要重新执行副作用函数。 解决这个问题的思路很简单,就是每次副作用执行时,我们可以先把它从所有与之关联的依赖集合中删除。 当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数。 要将一个副作用函数从所有与之关联的依赖集合中移除,需要明确有哪些依赖集合中包含它,因此,我们要重新设计副作用函数。 ```js // 用一个全局变量存储被注册的副作用函数 let activeEffect function effect(fn) { const effectFn = () => { // 调用 cleanup 函数完成清除工作 cleanup(effectFn) // 当 effectFn 执行时,将其设为当前激活的副作用函数 activeEffect = effectFn fn() } // effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } function track(target, key) { // 没有 activeEffect 直接返回 if (!activeEffect) return // 根据 target 从桶中取得 depsMap,它也是一个 Map 类型,key: effects let depsMap = bucket.get(target) // 如果不存在 depsMap,则新建一个 Map 与 target 关联 if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型 // 里面存储着所有与当前 key 关联的副作用函数:effects let deps = depsMap.get(key) // 如果 deps 不存在,则同样新建一个 Set 与 key 关联 if (!deps) { depsMap.set(key, (deps = new Set())) } // 把当前激活的副作用函数添加到依赖集合 deps 中 deps.add(activeEffect) // deps 就是一个与当前副作用函数存在联系的依赖集合 // 将其添加到 activeEffect.deps 数组中 activeEffect.deps.push(deps) } function cleanup(effectFn) { // 遍历 effectFn 的 deps 数组 for (let i = 0; i < effectFn.deps.length; i++) { // deps 是依赖集合 const deps = effectFn.deps[i] // 将 effectFn 从依赖集合中移除 deps.delete(effectFn) } // 最后需要重置 effectFn.deps 数组 effectFn.deps.length = 0 } function trigger(target, key) { const depsMap = bucket.get(target) if (!depsMap) { return } const effects = depsMap.get(key) // effects && effects.forEach(fn => fn()) // 删除,这个会导致死循环 // 构造一个新的集合 effectToRun 然后变量它,用来遍历删除,避免死循环 const effectToRun = new Set(effects) effectToRun.forEach(effectFn => effectFn()) } ``` 在 trigger 中我们遍历 effects 集合,它是一个 Set 集合,当执行副作用函数时,会调用 cleanup 进行清除,实际上是从 effects 中将当前副作用函数剔除。 但是副作用函数的执行会导致其重新被收集,此时对于 effects 的遍历仍在进行,会引起死循环。 剪短的代码来表达: ```js const set = new Set([1]) set.forEach(item => { set.delete(1) set.add(1) console.log('遍历中') }) ``` 语言规范中对此有明确的说明:在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合, 如果此时 forEach 遍历没有结束,那么该值会重新被访问。 因此,上面的代码会无限循环。解决办法也很简单,构造领一个 Set 集合并遍历它: ```js const set = new Set([1]) const newSet = new Set(set) newSet.forEach(item => { set.delete(1) set.add(1) console.log('遍历中') }) ``` ::: note 分支切换与 cleanup demo 运行结果 ::: normal-demo 分支切换与 cleanup demo ```html
Change OK ``` ```js // 存储副作用的桶 const bucket = new WeakMap() // 用一个全局变量存储被注册的副作用函数 let activeEffect function effect(fn) { const effectFn = () => { // 调用 cleanup 函数完成清除工作 cleanup(effectFn) // 当 effectFn 执行时,将其设为当前激活的副作用函数 activeEffect = effectFn fn() } // effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } function track(target, key) { // 没有 activeEffect 直接返回 if (!activeEffect) return // 根据 target 从桶中取得 depsMap,它也是一个 Map 类型,key: effects let depsMap = bucket.get(target) // 如果不存在 depsMap,则新建一个 Map 与 target 关联 if (!depsMap) { bucket.set(target, (depsMap = new Map())) } // 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型 // 里面存储着所有与当前 key 关联的副作用函数:effects let deps = depsMap.get(key) // 如果 deps 不存在,则同样新建一个 Set 与 key 关联 if (!deps) { depsMap.set(key, (deps = new Set())) } // 把当前激活的副作用函数添加到依赖集合 deps 中 deps.add(activeEffect) // deps 就是一个与当前副作用函数存在联系的依赖集合 // 将其添加到 activeEffect.deps 数组中 activeEffect.deps.push(deps) } function cleanup(effectFn) { // 遍历 effectFn 的 deps 数组 for (let i = 0; i < effectFn.deps.length; i++) { // deps 是依赖集合 const deps = effectFn.deps[i] // 将 effectFn 从依赖集合中移除 deps.delete(effectFn) } // 最后需要重置 effectFn.deps 数组 effectFn.deps.length = 0 } function trigger(target, key) { const depsMap = bucket.get(target) if (!depsMap) { return } const effects = depsMap.get(key) // effects && effects.forEach(fn => fn()) // 删除,这个会导致死循环 // 构造一个新的集合 effectToRun 然后变量它,用来遍历删除,避免死循环 const effectToRun = new Set(effects) effectToRun.forEach(effectFn => effectFn()) } // 元素数据 const data = { ok: true, text: 'hello world', } const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { // 将副作用函数 添加到桶中 track(target, key) // 返回属性值 return target[key] }, // 拦截设置操作 set(target, key, newVal) { // 设置属性值 target[key] = newVal // 把副作用函数从桶中取出并执行 trigger(target, key) return true }, }) // 使用 effect 注册副作用函数 effect(() => { // 匿名副作用函数 console.log('effect run - branch-cleanup') window.document.querySelector('#effect-branch-cleanup').innerText = obj.ok ? obj.text : 'not' }) function changeText() { obj.text = 'hello vue3' obj.notExist = 'hello vue3' } function changeOk(val) { obj.ok = val } ``` ::: ## 4.5 嵌套的 effect 与 effect 栈 effect 是可以发生嵌套的,例如: ```js effect(function effectFn1() { effect(function effectFn2() { /* ... */ }) }) ``` effectFn1 里嵌套了 effectFn2,什么场景会有呢?比如,Foo 组件有 effect,Foo 组件里调用了 Bar 组件, Bar 里有 effect 的话,就会发送 effect 嵌套。 但是,前面的代码中,全局变量 activeEffect 只能存储一个,有嵌套时不能正确的恢复外层的副作用函数。 为了解决这个问题,我们需要一个副作用栈 effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作用函数。 代码如下: ```js // 用一个全局变量存储被注册的副作用函数 let activeEffect // effect 栈 let effectStack = [] function effect(fn) { const effectFn = () => { // 调用 cleanup 函数完成清除工作 cleanup(effectFn) // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect activeEffect = effectFn // 在调用副作用函数之前,将当前副作用函数压入栈中 effectStack.push(effectFn) fn() // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈, // 并把 activeEffect 还原为之前的值 effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } // effectFn.deps 用来存储所有与该副作用函数相关联的依赖集合 effectFn.deps = [] // 执行副作用函数 effectFn() } ``` ## 4.6 避免无限递归循环 当有如下的代码时,会发生无限递归循环: ```js const data = { foo: 1 } const obj = new Proxy(data, { /* ... */ }) effect(() => obj.foo++) // 上句相当于 effect(() => { obj.foo = obj.foo + 1 }) ``` 在副作用中,既读取 obj.foo,又设置 obj.foo,读取会触发 track 操作,将副作用函数放入桶中;设置会触发 trigger,从桶中取出副作用函数并执行。 但问题是该副作用函数正在执行中,还没执行完毕,就要开始下一次的执行。这将会导致无限递归调用自己,产生栈溢出。 通过分析发现,读取和设置操作是在同一个副作用函数内进行的。不管是 track 收集的副作用函数,还是 trigger 执行的副作用函数都是 activeEffect。 基于此,我们可以加个条件,**如果 trigger 执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行**,如下所示: ```js function trigger(target, key) { const depsMap = bucket.get(target) if (!depsMap) { return } const effects = depsMap.get(key) // 构造一个新的集合 effectToRun 然后变量它,用来遍历删除,避免死循环 const effectsToRun = new Set() effects && effects.forEach(effectFn => { if (effectFn !== activeEffectFn) { // 如果 trigger 执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行 effectsToRun.add(effectFn) } }) effectsToRun.forEach(effectFn => effectFn()) } ``` ## 4.7 调度执行 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/index.md' --- # Vue3 官方资料笔记 Vue3 官方资料的学习,分成了两个部分,第一部分在另一个工程里 [hello-vue3](https://github.com/FuckDoctors/hello-vue3),后面的在本站内记录。 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/basic/index.md' --- # 基础 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/basic/reactivity-fundamentals.md' --- # 响应式基础 ## 声明响应式状态 我们可以使用 reactive() 函数创建一个响应式对象或数组: ::: vue-playground reactive 示例 @file App.vue ```vue ``` ::: ### ` ``` ::: ### DOM 更新时机 当你更改响应式状态后,DOM 会自动更新。然而,你得注意 DOM 的更新并不是同步的。 相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改,每个组件都只更新一次。 若要等待一个状态改变后的 DOM 更新完成,你可以使用 `nextTick()` 这个全局 API。 ### 深层响应性 在 Vue 中,状态都是默认深层响应式的。 你也可以直接创建一个浅层响应式对象 `shallowReactive`。它们仅在顶层具有响应性,一般仅在某些特殊场景中需要。 ### reactive() 的局限性 `reactive()` API 有两条限制: 1. 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。 2. 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失。 同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性。 ## 用 `ref()` 定义响应式变量 `reactive()` 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。 为此,Vue 提供了一个 `ref()` 方法来允许我们创建可以使用任何值类型的响应式 `ref`。 `ref()` 将传入参数的值包装为一个带 `.value` 属性的 `ref` 对象: ::: vue-playground ref 示例 @file App.vue ```vue ``` ::: ```js const count = ref(0) console.log(count) // { value: 0 } console.log(count.value) // 0 count.value++ console.log(count.value) // 1 ``` 和响应式对象的属性类似,`ref` 的 `.value` 属性也是响应式的。 同时,当值为对象类型时,会用 `reactive()` 自动转换它的 `.value`。 一个包含对象类型值的 `ref` 可以响应式地替换整个对象: ```js const objectRef = ref({ count: 0 }) // 这是响应式的替换 objectRef.value = { count: 1 } ``` `ref` 被传递给函数或是从一般对象上被解构时,不会丢失响应性: ```js const obj = { foo: ref(1), bar: ref(2), } // 该函数接收一个 ref // 需要通过 .value 取值 // 但它会保持响应性 callSomeFunction(obj.foo) // 仍然是响应式的 const { foo, bar } = obj ``` 简言之,`ref()` 让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。 这个功能很重要,因为它经常用于将逻辑提取到 **组合函数** 中。 ### ref 在模板中的解包 当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 `.value`。 ### ref 在响应式对象中的解包 当一个 `ref` 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样: ```js const count = ref(0) const state = reactive({ count }) console.log(state.count) // 0 state.count = 1 console.log(count.value) // 1 ``` 如果将一个新的 `ref` 赋值给一个关联了已有 `ref` 的属性,那么它会替换掉旧的 `ref`: ```js const otherCount = ref(2) state.count = otherCount console.log(state.count) // 2 // 原始 ref 现在已经和 state.count 失去联系 console.log(count.value) // 1 ``` 只有当嵌套在一个深层响应式对象内时,才会发生 `ref` 解包。当其作为浅层响应式对象 `shallowReactive` 的属性被访问时不会解包。 ### 数组和集合类型的 ref 解包 跟响应式对象不同,当 `ref` 作为响应式数组或像 `Map` 这种原生集合类型的元素被访问时,不会进行解包。 ```js const books = reactive([ref('Vue 3 Guide')]) // 这里需要 .value console.log(books[0].value) const map = reactive(new Map([['count', ref(0)]])) // 这里需要 .value console.log(map.get('count').value) ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/basic/template-syntax.md' --- # 模板语法 ## 文本插值 ```vue Message: {{ msg }} ``` ## 原始 HTML ```vue

Using v-html directive:

``` ## Attribute 绑定 想要响应式地绑定一个 attribute,应该使用 v-bind 指令: ```vue
``` 因为 v-bind 非常常用,我们提供了特定的简写语法: ```vue
``` 动态绑定多个值: 如果你有像这样的一个包含多个 attribute 的 JavaScript 对象: ```js const objectOfAttrs = { id: 'container', class: 'wrapper', } ``` 通过不带参数的 `v-bind`,你可以将它们绑定到单个元素上: ```vue
``` ## 指令 Directives 指令是带有 `v-` 前缀的特殊 attribute。 ### 参数 Arguments 某些指令会需要一个“参数”,在指令名后通过一个冒号(`:`)隔开做标识。 例如用 `v-bind` 指令来响应式地更新一个 HTML attribute: ```vue ... ... ``` 这里 `href` 就是一个参数,它告诉 `v-bind` 指令将表达式 `url` 的值绑定到元素的 `href` attribute 上。 另一个例子是 `v-on` 指令,它将监听 DOM 事件: ```vue ... ... ``` ### 动态参数 同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号(`[]`)内: ```vue ... ... ``` ### 修饰符 Modifiers 修饰符是以点(`.`)开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。 例如 `.prevent` 修饰符会告知 `v-on` 指令对触发的事件调用 `event.preventDefault()`: ```vue
...
``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/built-ins/index.md' --- # 内置组件 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/built-ins/keep-alive.md' --- # KeepAlive `` 是一个内置组件,使我们可以在动态切换多个组件时视情况缓存组件实例。 ## 基本用法 在组件基础章节中,我们已经介绍了动态组件的用法,即使用特殊的 `` 元素: ```vue ``` 默认情况下,一个活跃的组件实例会在切走后被卸载。这会导致它丢失其中所有已变化的状态。 我们是的确想要组件能在非活跃状态时保留它们的状态。要解决这个问题,我们可以用内置的 `` 组件将这些动态组件包装起来: ```vue ``` ::: playground#vue KeepAlive 基本用法 @file App.vue ```vue ``` @file CompA.vue ```vue ``` @file CompB.vue ```vue ``` ::: ## 包含 / 排除 默认情况下,`` 会缓存内部的任何组件实例。但我们可以通过 `include` 和 `exclude` prop 来定制该行为。 这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组: ```vue ``` ::: warning 它会根据组件的 `name` 选项进行匹配,所以组件如果想要条件性地被 `KeepAlive` 缓存,就必须显式声明一个 `name` 选项。 ::: ## 最大缓存实例数 我们可以通过传入 `max` prop 来限制可被缓存的最大组件实例数。 `` 的行为在指定了 `max` 后类似一个 LRU 缓存: 如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。 ```vue ``` ## 缓存实例的生命周期 当一个组件实例从 DOM 上移除但因为被 `` 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。 当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。 一个持续存在的组件可以通过 `onActivated()` 和 `onDeactivated()` 注册相应的两个状态的生命周期钩子: ```vue ``` 请注意: * `onActivated` 在组件挂载时也会调用,并且 `onDeactivated` 在组件卸载时也会调用。 * 这两个钩子不仅适用于 `` 缓存的根组件,也适用于缓存树中的后代组件。 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/built-ins/suspense.md' --- # Suspense `` 是一个内置组件,用来在组件树中编排异步依赖。它可以在等待组件树下的多个嵌套异步依赖项解析完成时,呈现加载状态。 ## 异步依赖 有了 `` 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。 `` 可以等待的异步依赖有两种: 1. 带有异步 `setup()` 钩子的组件。这也包含了使用 ` ``` ### 异步组件 异步组件默认就是\*\*"suspensible"\*\*的。这意味着如果组件关系链上有一个 ``,那么这个异步组件就会被当作这个 `` 的一个异步依赖。 在这种情况下,加载状态是由 `` 控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略。 异步组件也可以通过在选项中指定 `suspensible: false` 表明不用 `Suspense` 控制,并让组件始终自己控制其加载状态。 ## 加载中状态 `` 组件有两个插槽:`#default` 和 `#fallback`。 两个插槽都只允许一个直接子节点。在可能的时候都将显示默认槽中的节点。否则将显示后备槽中的节点。 ```vue ``` 在初始渲染时,`` 将在内存中渲染其默认的插槽内容。 如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。 当所有遇到的异步依赖都完成后,`` 会进入完成状态,并将展示出默认插槽的内容。 如果在初次渲染时没有遇到异步依赖,`` 会直接进入完成状态。 进入完成状态后,只有当默认插槽的根节点被**替换**时,`` 才会回到挂起状态。组件树中新的更深层次的异步依赖**不会**造成 `` 回退到挂起状态。 发生回退时,后备内容不会立即展示出来。相反,`` 在等待新内容和异步依赖完成时,会展示之前 `#default` 插槽的内容。 这个行为可以通过一个 `timeout` prop 进行配置: 在等待渲染新内容耗时超过 `timeout` 之后,`` 将会切换为展示后备内容。若 `timeout` 值为 `0` 将导致在替换默认内容时立即显示后备内容。 ## 事件 除了 `pending` 事件之外,`` 组件还有 `resolve` 和 `fallback` 事件。 `pending` 事件是在进入挂起状态时触发。`resolve` 事件是在 `default` 插槽完成获取新内容时触发。`fallback` 事件则是在 `fallback` 插槽展示时触发。 例如,可以使用这些事件在加载新组件时在之前的 DOM 最上层显示一个加载指示器。 ## 错误处理 `` 组件自身目前还不提供错误处理,不过你可以使用 `errorCaptured` 选项或者 `onErrorCaptured()` 钩子,在使用到 `` 的父组件中捕获和处理异步错误。 ## 和其他组件结合 我们常常会将 `` 和 ``、`` 等组件结合。要保证这些组件都能正常工作,嵌套的顺序非常重要。 另外,这些组件都通常与 Vue Router 中的 `` 组件结合使用。 ```vue ``` Vue Router 使用动态导入对[懒加载组件](https://next.router.vuejs.org/guide/advanced/lazy-loading.html)进行了内置支持。 这些与异步组件不同,目前他们不会触发 ``。但是,它们仍然可以有异步组件作为后代,这些组件可以照常触发 ``。 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/built-ins/teleport.md' --- # Teleport 传送门 `` 是一个内置组件,使我们可以将一个组件的一部分模板“传送”到该组件的 DOM 层次结构之外的 DOM 节点中。 ## 基本使用 为 `` 指定的目标 `to` 期望接收一个 CSS 选择器字符串或者一个真实的 DOM 节点。这里我们其实就是让 Vue 去“传送这部分模板片段到 `body` 标签下”。 ```vue ``` 你可以点击下面这个按钮,然后通过浏览器的开发者工具,在 `` 标签下找到模态框元素: ::: tip `` 挂载时,传送门的 to 目标必须是已经存在于 DOM 之中。 理想情况下,这应该是整个 Vue 应用程序之外的一个元素。 如果目标是由 Vue 呈现的另一个元素,你需要确保在 `` 之前挂载该元素。 ::: ## 搭配组件使用 `` 只改变了渲染的 DOM 结构,它不会影响组件间的逻辑关系。 也就是说,如果 `` 包含了一个组件,那么该组件始终和这个使用了 `` 的组件保持逻辑上的父子关系。 传入的 props 和触发的事件也会照常工作。 这也意味着来自父组件的注入也会按预期工作,子组件将在 Vue Devtools 中嵌套在父级组件下面,而不是放在实际内容移动到的地方。 ## 禁用传送门 在某些场景中,你们可能需要视情况禁用 ``。 举个例子,我们想要在桌面端将一个组件当做浮层来渲染,但在移动端则当作行内组件。 可以对 `` 动态地传入一个 `disabled` prop 来处理这两种不同情况。 ```vue ``` ## 同一目标上多个传送门 一个常见的应用场景就是写一个可重用的 `` 组件,可能同时存在多个实例。 对于此类场景,多个 `` 组件可以将其内容挂载在同一个目标元素上, 而顺序就是简单的顺次追加,后挂载的将排在目标元素下更后面的位置上。 ```vue
A
B
``` 渲染结果: ```html
A
B
``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/built-ins/transition-group.md' --- # TransitionGroup `` 是一个内置组件,设计用于呈现一个列表中的元素或组件的插入、移除和顺序改变的动画效果。 ## 和 `` 的区别 `` 支持和 `` 基本相同的 prop、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别: * 默认情况下,它不会渲染一个包装器元素。但你可以通过传入 `tag` 属性来指定一个元素作为包装器元素来渲染。 * 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。 * 其中的元素**总是必须**有一个独一无二的 `key` attribute。 * CSS 过渡 class 会被应用在其中的**每一个元素**上,**而不是**这个组的容器上。 ## 进入 / 离开过渡 这里是 `` 对一个 `v-for` 列表应用进入 / 离开过渡的示例: ```vue
  • {{ item }}
  • ``` ```css .list-enter-active, .list-leave-active { transition: all 0.5s ease; } .list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); } ``` ::: playground#vue 简单列表过渡 @file App.vue ```vue ``` @import ```json { "imports": { "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js" } } ``` ::: ## 交错的列表过渡 如果通过 data attribute 用 JavaScript 来执行过渡时,那么我们也可以实现列表中的交错过渡。 首先,我们把某一项的索引作为 DOM 元素上的一个 data attribute 呈现出来。 ```vue {11}
  • {{ item.msg }}
  • ``` 接着,在 JavaScript 钩子中,我们基于这个 data attribute 对该元素执行一个延迟动画: ```js {5} function onEnter(el, done) { gsap.to(el, { opacity: 1, height: '1.6em', delay: el.dataset.index * 0.15, onComplete: done, }) } ``` ::: playground#vue 交错的列表过渡 @file App.vue ```vue ``` @import ```json { "imports": { "gsap": "https://unpkg.com/gsap?module" } } ``` ::: --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/built-ins/transition.md' --- # Transition Vue 提供了两个内置组件,可以帮助你制作基于状态变化的过渡和动画: * `Transition` 会在一个元素或组件进入和离开 DOM 时应用动画。 * `TransitionGroup` 会在一个元素或组件被插入到 `v-for` 列表中,或是被移动或从其中移除时应用动画。 除了这两个组件,我们也可以通过其他技术手段来应用动画,比如切换 CSS class 或用状态绑定样式来驱动动画。 ## `Transition` 组件 它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发: * 由 `v-if` 所带来的条件渲染 * 由 `v-show` 所带来的条件显示 * 由特殊元素 `` 切换的动态组件 最基本的示例: ::: playground#vue transistion 示例 @file App.vue ```vue ``` ::: ::: tip `` 仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。 ::: 当一个 `Transition` 组件中的元素被插入或移除时,会发生下面这些事情: 1. Vue 会自动检查目标元素是否应用了 CSS 过度或动画。如果是,则一些 CSS 过度 class 会在适当的时机被添加或移除。 2. 如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。 3. 如果没有探测到 CSS 过度或动画,没有提供 JavaScript 钩子,那么 DOM 的插入,删除操作将在浏览器的下一个动画帧上进行。 ## 基于 CSS 的过渡 ### CSS 过渡 class 一共有 6 个应用于进入与离开过渡效果的 CSS class。 ```mermaid stateDiagram-v2 direction LR state Enter { direction LR Opacity#58; 0

    v#45;enter#45;from --> Opacity#58; 1

    v#45;enter#45;to : v#45;enter#45;active } state Leave { direction LR Opacity#58; 1

    v#45;leave#45;from --> Opacity#58; 0

    v#45;leave#45;to : v#45;leave#45;active } ``` ::: tip mermaid escapse Numbers given are base 10, so `#` can be encoded as `#35;`. It is also supported to use HTML character names. Because semicolons can be used instead of line breaks to define the markup, you need to use `#59;` to include a semicolon in message text. 冒号 `:` = `#58;`, 空格 ` `,换行 `
    `,减号 `-` = `#45;` ::: 1. `v-enter-from`:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。 2. `v-enter-active`:进入动画的生效状态。应用于整个进入动画阶段。 在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。 3. `v-enter-to`:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 `v-enter-from` 被移除的同时),在过渡或动画完成之后移除。 4. `v-leave-from`:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。 5. `v-leave-active`:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。 这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。 6. `v-leave-to`:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 `v-leave-from` 被移除的同时),在过渡或动画完成之后移除。 `v-enter-active` 和 `v-leave-active` 给我们提供了为进入和离开动画指定不同速度曲线的能力。 ### 为过渡命名 可以通过一个 `name` 属性来声明一种过渡: ```vue ... ``` 对于一个已命名的过渡,它的过渡相关 class 会以其名字而不是 `v` 作为前缀。 比如,上方例子中被应用的 class 将会是 `fade-enter-active` 而不是 `v-enter-active`。这个 "fade" 过渡的 class 应该是这样: ```css .fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } ``` ### CSS 的 transition `` 一般都会搭配[原生 CSS 过渡](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions)一起使用,这个 transition CSS 属性是一个简写形式,使我们可以一次定义一个过渡的各个方面,包括需要执行动画的属性、持续时间和[速度曲线](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function)。 下面是一个更高级的例子,它使用了不同的持续时间和速度曲线来过渡多个 property: ::: playground#vue CSS Transition @file App.vue ```vue ``` ::: ### CSS 的 animation [原生 CSS 动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)和 CSS trasition 的应用方式基本上是相同的,只有一点不同,那就是 `*-enter-from` 不是在元素插入后立即移除,而是在一个 `animationend` 事件触发时被移除。 对于大多数的 CSS 动画,我们可以简单地在 `*-enter-active` 和 `*-leave-active` class 下声明它们。下面是一个示例: ::: playground#vue CSS animation @file App.vue ```vue ``` ::: ### 自定义过渡 class 你也可以向 `` 传递以下的 props 来指定自定义的过渡 class: * `enter-from-class` * `enter-active-class` * `enter-to-class` * `leave-from-class` * `leave-active-class` * `leave-to-class` 你传入的这些 class 会覆盖相应阶段的默认 class 名。这个功能在你想要在 Vue 的动画机制下集成其他的第三方 CSS 动画库时非常有用,比如 [Animate.css](https://daneden.github.io/animate.css/): ::: playground#vue 自定义过渡 class @file App.vue ```vue ``` ::: ### 同时使用 transition 和 animation Vue 需要附加事件侦听器,以便知道过渡何时结束。可以是 `transitionend` 或 `animationend`,这取决于你所应用的 CSS 规则。 如果你仅仅使用二者的其中之一,Vue 可以自动探测到正确的类型。 然而在某些场景中,你或许想要在同一个元素上同时使用它们两个。 举个例子,Vue 触发了一个 CSS 动画,同时鼠标悬停触发另一个 CSS 过渡。 此时你需要显式地传入 type prop 来声明,告诉 Vue 需要关心哪种类型,传入的值是 `animation` 或 `transition`: ```vue ... ``` ### 深层级过渡与显示过渡时间 尽管过渡 class 仅能应用在 `` 的直接子元素上,我们还是可以使用深层级的 CSS 选择器,使深层级的元素发生过渡。 ```vue
    Hello
    ``` ```css /* 应用于嵌套元素的规则 */ .nested-enter-active .inner, .nested-leave-active .inner { transition: all 0.3s ease-in-out; } .nested-enter-from .inner, .nested-leave-to .inner { transform: translateX(30px); opacity: 0; } ``` 我们甚至可以在嵌套元素上添加一个过渡延迟,这会创建一个交错进入动画序列: ```css {3} /* 延迟嵌套元素的进入以获得交错效果 */ .nested-enter-active .inner { transition-delay: 0.25s; } ``` 然而,这会带来一个小问题。默认情况下,`` 组件会通过监听过渡根元素上的**第一个** `transitionend` 或者 `animationend` 事件来尝试自动判断过渡何时结束。 而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成。 在这种情况下,你可以通过向 `` 组件传入 `duration` 属性来显式指定过渡的持续时间 (以毫秒为单位)。总持续时间应该匹配延迟加上内部元素的过渡持续时间: ```vue ... ``` 如果有必要的话,你也可以用对象的形式传入,分开指定进入和离开所需的时间: ```vue ... ``` ::: playground#vue 深层级过渡与显式过渡时间 @file App.vue ```vue ``` ::: ### 性能考量 你可能注意到我们上面例子中展示的动画所用到的属性大多是 `transform` 和 `opacity` 之类的。用这些属性制作动画非常高效,因为: 1. 他们在动画过程中不会影响到 DOM 结构,因此每一个动画帧都不会触发昂贵的 CSS 布局重新计算。 2. 大多数的现代浏览器都可以在执行 `transform` 动画时利用 GPU 进行硬件加速。 相比之下,像 `height` 或者 `margin` 这样的属性会触发 CSS 布局变动,因此执行它们的动画效果更昂贵,需要谨慎使用。 我们可以在 [CSS-Triggers](https://csstriggers.com/) 这类的网站查询哪些属性会在执行动画时触发 CSS 布局变动。 ### JavaScript 钩子 你可以通过监听 `` 组件事件的方式在过渡过程中挂上钩子函数: ```vue ``` ```js // 在元素被插入到 DOM 之前被调用 // 用这个来设置元素的 "enter-from" 状态 function onBeforeEnter(el) {}, // 在元素被插入到 DOM 之后的下一帧被调用 // 用这个来开始进入动画 function onEnter(el, done) { // 调用回调函数 done 表示过渡结束 // 如果与 CSS 结合使用,则这个回调是可选参数 done() } // 当进入过渡完成时调用。 function onAfterEnter(el) {} function onEnterCancelled(el) {} // 在 leave 钩子之前调用 // 大多数时候,你应该只会用到 leave 钩子 function onBeforeLeave(el) {} // 在离开过渡开始时调用 // 用这个来开始离开动画 function onLeave(el, done) { // 调用回调函数 done 表示过渡结束 // 如果与 CSS 结合使用,则这个回调是可选参数 done() } // 在离开过渡完成、 // 且元素已从 DOM 中移除时调用 function onAfterLeave(el) {} // 仅在 v-show 过渡中可用 function leaveCancelled(el) {} ``` 这些钩子可以与 CSS 过渡或动画结合使用,也可以单独使用。 在使用仅由 JavaScript 执行的动画时,最好是添加一个 `:css="false"` 属性。 这显式地向 Vue 表明跳过对 CSS 过渡的自动探测。除了性能稍好一些之外,还可以防止 CSS 规则意外地干扰过渡。 ```vue {3} ... ``` 在有了 `:css="false"` 后,我们就自己全权负责控制什么时候过渡结束了。 这种情况下对于 `@enter` 和 `@leave` 钩子来说,回调函数 `done` 就是必须的。否则,钩子将被同步调用,过渡将立即完成。 可以使用 [GreenSock](https://greensock.com/), [Anime.js](https://animejs.com/), 或者 [Motion One](https://motion.dev/) 等库来执行动画。 ::: playground#vue JavaScript 钩子 @file App.vue ```vue ``` @import ```json { "imports": { "gsap": "https://unpkg.com/gsap?module" } } ``` ::: ### 可重用过渡 得益于 Vue 的组件系统,过渡是可以被重用的。要创建一个可被重用的过渡,我们需要为 `` 组件创建一个包装组件,并向内传入插槽内容: ```vue ``` 现在 `MyTransition` 可以在导入后像内置组件那样使用了: ```vue
    hello
    ``` ### 出现时过渡 如果你想在某个节点初次渲染时应用一个过渡效果,你可以添加 `appear` attribute: ```vue ``` ### 元素间过渡 除了通过 `v-if` / `v-show` 切换一个元素,我们也可以通过 `v-if` / `v-else` / `v-else-if` 在几个组件间进行切换过: ```vue ``` ### 过渡模式 在之前的例子中,进入和离开的元素都是在同时开始动画的,并且我们必须将它们设为 `position: absolute` 以避免二者同时存在时出现的布局问题。 然而,在某些场景中这可能不是个好的方案,或者并不能符合行为预期。 我们可能想要先执行离开动画,然后在其完成**之后**再执行元素的进入动画。 手动编排这样的动画是非常复杂的,好在我们可以通过向 `` 传入一个 `mode` prop 来实现这个行为: ```vue ``` ### 组件间过渡 `` 也可以用在动态组件之间: ```vue ``` ::: playground#vue 组件间过渡 & 动态过渡 @file App.vue ```vue ``` @file CompA.vue ```vue ``` @file CompB.vue ```vue ``` ::: --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/extra/index.md' --- # 进阶主题 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/extra/animation.md' --- # 动画技巧 ## 基于 CSS class 的动画 对于那些不是正在进入或离开 DOM 的元素,我们可以通过给它们动态添加 CSS class 来触发动画: ::: vue-playground 基于 CSS class 的动画 @file App.vue ```vue ``` ::: ## 状态驱动的动画 有些过渡效果可以通过动态地插值来实现,例如,在交互时动态地绑定样式到元素,以这个例子为例: ::: vue-playground 状态驱动的动画 @file App.vue ```vue ``` ::: ## 带侦听器的动画 在一些动画创意里,我们可以根据一些数字状态,使用侦听器将任何东西做成动画。例如,我们可以将数字本身变成动画: ::: vue-playground 带侦听器的动画 @file App.vue ```vue ``` @import ```json { "imports": { "gsap": "https://unpkg.com/gsap?module" } } ``` ::: --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/extra/composition-api.md' --- # 组合式 API FAQ ## 什么是组合式 API? 组合式 API 是一系列 API 的集合,使我们可以使用函数而不是声明式选项的方式来书写 Vue 组件。 它包含以下 API: * [响应式 API](https://vuejs.org/api/reactivity-core.html): 例如 `ref()` 和 `reactive()`,使我们可以直接创建响应式状态、计算属性和侦听器。 * [生命周期钩子](https://vuejs.org/api/composition-api-lifecycle.html): 例如,`onMounted()` 和 `onUnmounted()`, 使我们可以在组件的各个生命周期阶段添加逻辑。 * [依赖注入](https://vuejs.org/api/composition-api-dependency-injection.html): 例如 `privide()` 和 `inject()`,使我们可以在使用响应性 API 时,利用 Vue 的依赖注入系统。 组合式 API 是 Vue 3 的内置功能,而要在 Vue 2 中使用,则需要使用 [`@vue/composition-api`](https://github.com/vuejs/composition-api)。 虽然,响应式 API 是基于函数的组合,但是,组合式 API 并不是函数式编程。 组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程更强调数据不可变。 ## 为什么要有组合式 API? * 更好的逻辑复用 * 更灵活的代码组织 * 更好的类型推导 * 更小的打包体积 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/extra/reactivity-transform.md' --- # 响应性语法糖 响应性语法糖目前是一个实验性功能,默认是禁用的,需要显式选择使用。 Vue 的响应性语法糖是一个编译时的转换过程,使我们可以像这样书写代码: ## ref vs. 响应式变量 ::: playground#vue 响应性语法糖 @file App.vue ```vue ``` @setting ```json { "showCompileOutput": true } ``` ::: 这里的这个 `$ref()` 方法是一个编译时的宏命令:它不是一个真实的、在运行时会调用的方法。 而是用作 Vue 编译器的标记,表明最终的 `count` 变量需要是一个响应式变量。 每一个会返回 `ref` 的响应性 API 都有一个相对应的、以 `$` 为前缀的宏函数。包括以下这些 API: * `ref` -> `$ref` * `computed` -> `$computed` * `shallowRef` -> `$shalloRef` * `customRef` -> `$customRef` * `toRef` -> `$toRef` 当启用响应性语法糖时,这些宏函数都是全局可用的、无需手动导入。 但如果你想让它更明显,你也可以选择从 `vue/macros` 中引入它们: ```js import { $ref } from 'vue/macros' const count = $ref(0) ``` ## 通过 `$()` 结构 我们常常会让一个组合函数返回一个含数个 `ref` 的对象,然后解构得到这些 `ref`。对于这种场景,响应性语法糖提供了一个 `$()` 宏: ::: playground#vue 通过 `$()` 结构 @file App.vue ```vue ``` @import ```json { "imports": { "@vueuse/core": "https://unpkg.com/@vueuse/core/dist/index.esm.js" } } ``` @setting ```json { "showCompileOutput": true } ``` ::: ## 响应式 props 解构 现在的 ` ``` @setting ```json { "showCompileOutput": true } ``` ::: ## 保持在函数间的响应性 虽然响应式变量使我们可以不再受 `.value` 的困扰,但它也使得我们在函数间传递响应式变量时可能造成**响应性丢失**的问题。 这可能在以下两种场景中出现: ### 以参数形式传入函数 ```js function trackChange(x: Ref) { watch(x, x => { console.log('x 改变了!') }) } const count = $ref(0) trackChange(count) // 无效! ``` 上面的例子不会正常工作,因为代码被编译成了这样: ```js const count = ref(0) trackChange(count.value) ``` 这里的 `count.value` 是以一个 `number` 类型值的形式传入,然而 trackChange 期望接收的是一个真正的 `ref`。 要解决这个问题,可以在将 count 作为参数传入之前,用 `$$()` 包装: ```diff let count = $ref(0) - trackChange(count) + trackChange($$(count)) ``` 上面的代码将被编译成: ```js import { ref } from 'vue' const count = ref(0) trackChange(count) ``` 我们可以看到,`$$()` 的效果就像是一个转义标识:`$$()` 中的响应式变量不会追加上 `.value`。 ### 作为函数返回值 如果将响应式变量直接放在返回值表达式中会丢失掉响应性: ```js function useMouse() { const x = $ref(0) const y = $ref(0) // 监听 mousemove 事件 // 不起效! return { x, y, } } ``` 上面的语句将被翻译为: ```js return { x: x.value, y: y.value, } ``` 为了保持响应性,我们需要返回的是真正的 `ref`,而不是返回时 `ref` 内的值。 我们还是可以使用 `$$()` 来解决这个问题。 在这个例子中,`$$()` 可以直接用在要返回的对象上, `$$()` 调用时任何对响应式变量的引用都会保留为对相应 `ref` 的引用: ```js function useMouse() { const x = $ref(0) const y = $ref(0) // 监听 mousemove 事件 // 修改后起效 return $$({ x, y, }) } ``` ### 在已解构的 prop 上使用 `$$()` `$$()` 适用于已解构的 prop,因为它们也是响应式的变量。编译器会高效地通过 `toRef` 来做转换: ```js const { count } = defineProps<{ count: number }>() passAsRef($$(count)) ``` 编译结果为: ```js setup(props) { const __props_count = toRef(props, 'count') passAsRef(__props_count) } ``` ## 显式启用 响应性语法糖目前默认是关闭状态,需要你显式选择[启用](https://vuejs.org/guide/extras/reactivity-transform.html#explicit-opt-in)。 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/extra/render-and-jsx.md' --- # 渲染函数 & JSX 在绝大多数情况下,Vue 推荐使用模板语法来搭建 HTML。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力。这时渲染函数就派上用场了。 ## 基本用法 ### 创建 VNodes Vue 提供了一个 `h()` 函数用于创建 vnodes: ```js import { h } from 'vue' const vnode = h( 'div', // type { id: 'foo', class: 'bar' }, // props [ // children ] ) ``` `h()` 函数的使用非常灵活: ```js // 除了类型必填以外,其他的参数都是可选的 h('div') h('div', { id: 'foo' }) // attribute 和 property 都能在 props 中书写 // Vue 会自动将他们分配到正确的位置 h('div', { class: 'bar', innerHTML: 'hello' }) // props modifiers such as .prop and .attr can be added // with '.' and `^' prefixes respectively h('div', { '.name': 'some-name', '^width': '100' }) // 类与样式可以像在模板中一样 // 用数组或对象的形式书写 h('div', { class: [foo, { bar }], style: { color: 'red' } }) // 事件监听器应以 onXxx 的形式书写 h('div', { onClick: () => {} }) // children 可以是一个字符串 h('div', { id: 'foo' }, 'hello') // 没有 props 时可以省略不写 h('div', 'hello') h('div', [h('span', 'hello')]) // children 数组可以同时包含 vnodes 与字符串 h('div', ['hello', h('span', 'hello')]) ``` ### 声明渲染函数 当组合式 API 与模板一起使用时,`setup()` 钩子的返回值是用于暴露数据给模板。然而当我们使用渲染函数时,可以直接把渲染函数返回: ```js import { h, ref } from 'vue' export default { props: { // props }, setup(props) { const count = ref(0) // 返回渲染函数 return () => h('div', props.msg + count.value) }, } ``` 除了返回一个 `vnode`,你还可以返回字符串或数组: ```js import { h } from 'vue' export default { setup() { // 使用数组返回多个根节点 return () => [h('div'), h('div'), h('div')] }, } ``` ::: tip 请确保返回的是一个**函数而**不是一个值!setup() 函数在每个组件中只会被调用一次,而返回的渲染函数将会被调用多次。 ::: ### Vnodes 必须唯一 ```js function render() { const p = h('p', 'hi') return h('div', [ // 啊哦,重复的 vnodes 是无效的 p, p, ]) } ``` 如果你真的非常想在页面上渲染多个重复的元素或者组件,你可以使用一个工厂函数来做这件事。 比如下面的这个渲染函数就可以完美渲染出 20 个相同的段落: ```js function render() { return h( 'div', Array.from({ length: 20 }).map(() => { return h('p', 'hi') }) ) } ``` ## JSX / TSX [JSX](https://facebook.github.io/jsx/) 是 JavaScript 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码: ```jsx const vnode =
    hello
    ``` 在 JSX 表达式中,使用大括号来嵌入动态值: ```js const vnode = (
    Hello, {username}
    ) ``` ### 渲染函数案例 #### `v-if` 模板: ```vue
    yes
    no
    ``` 渲染函数 / JSX 语法: ```js h('div', [ok.value ? h('div', 'yes') : h('span', 'no')]) ``` ```jsx
    {ok.value ?
    yes
    : no}
    ``` #### `v-for` 模板: ```vue
    • {{ text }}
    ``` 等价于使用如下渲染函数 / JSX 语法: ```js h( 'ul', items.value.map(({ id, text }) => { return h('li', { key: id }, text) }) ) ``` ```jsx
      {items.value.map(({ id, text }) => { return
    • {text}
    • })}
    ``` #### `v-on` 以 `on` 开头,并跟着大写字母的 `props` 会被当作事件监听器。比如,`onClick` 与模板中的 `@click` 等价。 ```js h( 'button', { onClick(event) { // ... }, }, 'click me' ) ``` ```jsx ``` #### 事件修饰符 对于 \`\`.passive`、`.capture`和`.once\` 事件修饰符,可以使用驼峰写法将他们拼接在事件名后面: 实例: ```js h('input', { onClickCapture() { // 捕捉模式中的监听器 }, onKeyupOnce() { // 只触发一次 }, onMouseoverOnceCapture() { // 单次 + 捕捉 }, }) ``` ```jsx {}} onKeyupOnce={() => {}} onMouseoverOnceCapture={() => {}} /> ``` 对于事件和按键修饰符,可以使用 `withModifiers` 函数: ```js import { withModifiers } from 'vue' h('div', { onClick: withModifiers(() -> {}, ['self']) }) ``` ```jsx
    {}, ['self'])}>
    ``` ### 组件 在给组件创建 `vnode` 时,传递给 `h()` 函数的第一个参数应当是组件的定义。 这意味着使用渲染函数时不再需要注册组件了 —— 可以直接使用导入的组件: ```js import Foo from './Foo.vue' import Bar from './Bar.jsx' function render() { return h('div', [h(Foo), h(Bar)]) } ``` ```jsx function render() { return (
    ) } ``` 不管是什么类型的文件,只要从中导入的是有效的 Vue 组件,`h` 就能正常运作。 动态组件在渲染函数中也可直接使用: ```js import Foo from './Foo.vue' import Bar from './Bar.jsx' function render() { return ok.value ? h(Foo) : h(Bar) } ``` ```jsx function render() { return ok.value ? : } ``` 如果一个组件是用名字注册的,不能直接导入 (例如,由一个库全局注册),可以使用 `resolveComponent()` 来解决这个问题。 ### 渲染插槽 在渲染函数中,插槽可以通过 `setup()` 的上下文来访问。每个 `slots` 对象中的插槽都是一个返回 `vnodes` 数组的函数: ```js export default { props: ['message'], setup(props, { slots }) { return () => [ // 默认插槽 //
    h('div', slots.default()), // 具名插槽 //
    h( 'div', slots.footer({ text: props.message, }) ), ] }, } ``` 等价 JSX 语法: ```jsx // 默认插槽
    {slots.default()}
    // 具名插槽
    {slots.footer({ text: message })}
    ``` ### 传递插槽 我们需要传递一个插槽函数或者是一个包含插槽函数的对象而非是数组, 插槽函数的返回值同一个正常的渲染函数的返回值一样——并且在子组件中被访问时总是会被转化为一个 vnodes 数组。 ```js // 单个默认插槽 h(MyComponent, () => 'hello') // 具名插槽 // 注意 null 是必须的 // 以避免 slot 对象被当成 props 处理 h(MyComponent, null, { default: () => 'default slot', foo: () => h('div', 'foo'), bar: () => [h('span', 'one'), h('span', 'two')], }) ``` 等价 JSX 语法: ```jsx // 默认插槽 { () => 'hello' } // 具名插槽 {{ default: () => 'default slot', foo: () =>
    foo
    , bar: () => [ one, two ] }}
    ``` 插槽以函数的形式传递使得它们可以被子组件懒调用。这能确保它被注册为子组件的依赖关系,而不是父组件。这使得更新更加准确及有效。 ### 内置组件 诸如 `KeepAlive`、`Transition`、`TransitionGroup`、`Teleport` 和 `Suspense` 等内置组件在渲染函数中必须导入才能使用: ```js import { KeepAlive, Teleport, Transition, TransitionGroup, h } from 'vue' export default { setup() { return () => h(Transition, { mode: 'out-in' } /* ... */) }, } ``` ### `v-model` `v-model` 指令扩展为 `modelValue` 和 `onUpdate:modelValue` 在模板编译过程中,我们必须自己提供这些 props: ```js export default { props: ['modelValue'], emits: ['update:modelValue'], setup(props, { emit }) { return () => h(SomeComponent, { modelValue: props.modelValue, 'onUpdate:modelValue': value => emit('update:modelValue', value), }) }, } ``` ### 自定义指令 可以使用 `withDirectives` 将自定义指令应用于 VNode: ```js import { h, withDirectives } from 'vue' // 自定义指令 const pin = { mounted() { // ... }, updated() { // ... }, } //
    const vnode = withDirectives(h('div'), [[pin, 200, 'top', { animate: true }]]) ``` 当一个指令是以名称注册并且不能被直接导入时,可以使用 `resolveDirective` 函数来解决这个问题。 ### 函数式组件 函数式组件是自身没有任何状态的组件的另一种形式。它们在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。 我们使用的是一个简单函数,而不是一个选项对象,来创建函数式组件。该函数实际上就是该组件的 `render` 函数。 函数式组件的签名与 `setup()` 钩子相同: ```js function MyComponent(props, { slots, emit, attrs }) { // ... } ``` 大多数常规组件的配置选项在函数式组件中都不可用。然而我们还是可以把 `props` 和 `emits` 作为 property 加入,以达到定义它们的目的: ```js MyComponent.props = ['value'] MyComponent.emits = ['click'] ``` 如果这个 `props` 选项没有被定义,那么被传入函数的 `props` 对象就会像 `attrs` 一样会包含所有 attribute。 除非指定了 `props` 选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。 函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 `h`,它将会被当作一个函数式组件来对待。 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/reusability/index.md' --- # 可重用性 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/reusability/composables.md' --- # 组合式函数 ## 什么是“组合式函数”? 在 Vue 应用的概念中,“组合式函数”是一个利用 Vue 组合式 API 来封装和复用**有状态逻辑**的函数。 当构建前端应用时,我们常常需要复用公共任务的逻辑,可以封装为**无状态的逻辑**。有状态逻辑负责管理会随时间而变化的状态。 ## 鼠标跟踪器示例 如果我们想在多个组件中复用这个相同的逻辑,我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中。 ::: playground#vue 鼠标跟踪器示例 @file App.vue ```vue ``` @file mouse.js ```js import { onMounted, onUnmounted, ref } from 'vue' // 按照惯例,组合式函数名以 use 开头 export function useMouse() { // 被组合式封装和管理的状态 const x = ref(0) const y = ref(0) // 组合式函数可以随时改变其管理的状态 const update = event => { x.value = event.pageX y.value = event.pageY } // a composable can also hook into its owner component's // lifecycle to setup and teardown side effects. onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) // expose managed state as return value return { x, y } } ``` ::: 如你所见,核心逻辑一点都没有被改变,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。 然而更酷的一点是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。 这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。 实际上,这正是我们决定将实现了这一设计模式的 API 集合命名为组合式 API 的原因。 举个例子,我们可以将添加和清除 DOM 事件监听器的逻辑放入一个组合式函数中: ::: playground#vue 鼠标跟踪器示例 嵌套调用 @file App.vue ```vue ``` @file mouse.js ```js import { onMounted, onUnmounted, ref } from 'vue' import { useEventListener } from './event.js' // 按照惯例,组合式函数名以 use 开头 export function useMouse() { // 被组合式封装和管理的状态 const x = ref(0) const y = ref(0) useEventListener(window, 'mousemove', event => { x.value = event.pageX y.value = event.pageY }) // expose managed state as return value return { x, y } } ``` @file event.js ```js import { onMounted, onUnmounted } from 'vue' export function useEventListener(target, event, callback) { onMounted(() => target.addEventListener(event, callback)) onUnmounted(() => target.removeEventListener(event, callback)) } ``` ::: ## 异步状态示例 `useMouse()` 组合式函数没有接收任何参数,因此让我们再来看一个需要接收一个参数的组合式函数示例。 在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。 ::: playground#vue 异步状态示例 @file App.vue ```vue ``` @file useFetch.js ```js import { isRef, ref, unref, watchEffect } from 'vue' export function useFetch(url) { const data = ref(null) const error = ref(null) function doFetch() { // reset state before fetching.. data.value = null error.value = null // unref() unwraps potential refs fetch(unref(url)) .then(res => res.json()) .then(json => (data.value = json)) .error(err => (error.value = err)) } if (isRef(url)) { // setup reactive re-fetch if input URL is a ref // 若输入的 URL 是一个 ref,那么启动一个响应式的请求 watchEffect(doFetch) } else { // otherwise, just fetch once // and avoid the overhead of a watcher // 避免监听器的额外开销 doFetch() } return { data, error, retry: doFetch } } ``` ::: 这个版本的 useFetch() 现在同时可以接收静态的 URL 字符串和 URL 字符串的 ref。 当通过 isRef() 检测到 URL 是一个动态 ref 时,它会使用 watchEffect() 启动一个响应式的 effect。 该 effect 会立刻执行一次,并在此过程中将 URL 的 ref 作为依赖进行跟踪。 当 URL 的 ref 发生改变时,数据就会被重置,并重新请求。 ## 约定和最佳实践 ### 命名 组合式函数约定用驼峰命名法命名,并以 `use` 作为开头。 ### 输入参数 尽管其响应性不依赖 ref,组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用,你最好在处理输入参数时兼容 ref 而不只是原始的值。unref() 工具函数会对此非常有帮助: ```js import { unref } fron 'vue' function useFeature(maybeRef) { // 若 maybeRef 确实是一个 Ref,则返回它的 value // 否则,maybeRef 原样返回 const value = unref(maybeRef) } ``` 如果你的组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用 `watch()` 显示地监听次 ref, 或者在 `watchEffect()` 中调用 `unref()` 来正确地追踪。 ### 返回值 你可能已经注意到了,我们一直在组合式函数中使用 ref() 而不是 reactive()。我们推荐的约定是组合式函数始终返回 `ref` 对象,这样该函数在组件中解构之后仍可以保持响应性: ```js // x 和 y 是两个 ref 对象 const { x, y } = useMouse() ``` 从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。 如果你更希望以对象 `property` 的形式从组合式函数中返回状态,你可以将要返回的对象用 `reactive()` 包装,这样其中的 ref 会被自动解包,例如: ```js const mouse = reactive(useMouse()) // mouse.x 链接到了原来的 x ref console.log(mouse.x) ``` ```vue Mouse position is at: {{ mouse.x }}, {{ mouse.y }} ``` ### 副作用 在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则: * 如果你在一个应用中使用了服务器端渲染 (SSR),请确保在后置加载的声明钩子上执行 DOM 相关的副作用,例如:onMounted()。 这些钩子仅会在浏览器中使用,因此可以确保能访问到 DOM。 * **确保在 onUnmounted() 时清理副作用。** 举个例子,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted() 中被移除 (就像我们在 useMouse() 示例中看到的一样)。 当然也可以像之前的 useEventListener() 示例那样,使用一个组合式函数来自动帮你做这些事。 ### 使用限制 组合式函数在 ` ``` ### 在选项式 API 中使用组合式函数 如果你正在使用选项式 API,组合式函数必须在 `setup()` 中调用。且其返回的绑定必须在 `setup()` 中**返回**,以便暴露给 this 及其模板: ```js import { useMouse } from './mouse.js' import { useFetch } from './fetch.js' export default { setup() { const { x, y } = useMouse() const { data, error } = useFetch('xx') return { x, y, data, error } }, mounted() { // setup 中暴露的 property,可以通过 this 访问 console.log(this.x) }, } ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/reusability/custom-directives.md' --- # 自定义指令 ## 介绍 除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令。 我们已经介绍了两种在 Vue 中重用代码的方式:**组件**和**组合式函数**。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。另一方面,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。 一个自定义指令被定义为一个包含类似于组件的生命周期钩子的对象。钩子接收指令绑定到的元素。下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它将被聚焦: ::: playground#vue v-focus 指令 @file App.vue ```vue ``` ::: ::: playground#vue 组合式 API v-focus 指令 @file App.vue ```vue ``` ::: 和组件类似,自定义指令在模板中使用前必须先注册。在上面的例子中,我们使用 directives 选项完成了指令的局部注册。 将一个自定义指令全局注册到应用层级也是一种通用的做法: ```js const app = createApp({}) // 全局注册,使 v-focus 在所有组件中都可用 app.directive('focus', { // ... }) ``` ::: tip 只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。应该尽可能地通过像 `v-bind` 这样的内置指令以使用声明式的模板,因为这更高效,且对服务端渲染也更友好。 ::: ## 指令钩子 一个指令的定义对象可以提供几种钩子函数 (都是可选的): ```js const myDirective = { // called before bound element's attributes // or event listeners are applied created(el, binding, vnode, preVnode) { // see below for details on arguments }, // called right before the element is inserted into the DOM. beforeCreated() {}, // called when the bound element's parent component // and all its children are mounted. mounted() {}, // called before the parent component is updated beforeUpdate() {}, // called before the parent component is updated // all of its children have updated updated() {}, // called before the parent component is unmounted beforeUnmounted() {}, // called when the parent component is unmounted unmounted() {}, } ``` ### 钩子参数 指令的钩子会传递以下几种参数: * `el`: 指令绑定到的元素。这可以用于直接操作 DOM。 * `binding`: 一个对象,包含以下 property。 * `value`: 传递给指令的值。例如在 `v-my-directive="1 + 1"` 中,值是 2。 * `oldValue`: 之前的值,仅在 `beforeUpdate` 和 `updated` 中可用。无论值是否更改,它都可用。 * `arg`: 传递给指令的参数(如果有的话)。例如在 `v-my-directive:foo` 中,参数是 `"foo"`。 * `modifiers`:一个包含修饰符的对象 (如果有的话)。例如在 `v-my-directive.foo.bar` 中,修饰符对象是 `{ foo: true, bar: true }`。 * `instance`:使用该指令的组件实例 * `dir`:指令的定义对象。 * `vnode`: 代表绑定元素的底层 VNode。 * `preVnode`: 之前的渲染中代表指令所绑定元素的 VNode。仅在 `beforeUpdate` 和 `updated` 钩子中可用。 举个例子,像下面这样使用指令: ```vue
    ``` `binding` 参数会是一个这样的对象: ```js { arg: 'foo', modifiers: { bar: true }, value: // baz 的值 oldValue: // 上一次更新时 baz 的值 } ``` 和内置指令类似,自定义指令的参数也可以是动态的: ```vue
    ``` 里指令的参数会基于组件状态 `arg` 属性响应式地更新。 ::: note 除了 `el` 外,你应该将这些参数都视为只读的,并一律不更改它们。若你需要在不同的钩子间共享信息,推荐通过元素的 [dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) 实现 ::: ### 简化形式 对于自定义指令来说,需要在 `mounted` 和 `updated` 上实现相同的行为、又并不关心其他钩子的情况很常见。此时我们可以将指令定义成一个下面这样的函数: ```vue
    ``` ```js app.directive('color', (el, binding) => { // this will be called for both `mounted` and `updated` el.style.color = binding.value }) ``` ### 对象字面量 如果你的指令需要多个值,你可以向它传递一个 JavaScript 对象字面量。请记住,指令也可以接收任何合法的 JavaScript 表达式。 ```vue
    ``` ```js app.directive('demo', (el, binding) => { console.log(binding.value.color) // "white" console.log(binding.value.text) // "hello" }) ``` ### 在组件上使用 当在组件上使用自定义指令时,它会始终应用于组件的根节点,和[透传 attributes](https://staging-cn.vuejs.org/guide/components/attrs.html) 类似。 ```vue ``` ```vue
    My Component content
    ``` 需要注意的是组件可能含有多个根节点。当应用到一个多根组件时,指令将会被忽略且抛出一个警告。和 attribute 不同,指令不能通过 `v-bind="$attrs"` 来传递给一个不同的元素。总而言之,**不推荐**在组件上使用自定义指令。 --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/reusability/plugins.md' --- # 插件 ## 介绍 插件是一种能为 Vue 添加全局功能的工具代码。我们会这样安装一个插件: ```js import { createApp } from 'vue' const app = createApp({}) app.use('myPlugin', { // 可选的选项 }) ``` 它可以是一个拥有 `install()` 方法的对象,或者就简单地只是一个函数,它自己就是安装函数。安装函数接收应用实例和传递给 `app.use()` 的额外选项: ```js const myPlugin = { install(app, options) { // 配置此应用 }, } ``` 插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种: 1. 通过 `app.component()` 和 `app.directive()` 注册一到多个全局组件或自定义指令。 2. 通过 `app.provide()` 使一个资源可被注入进整个应用 3. 向 `app.config.globalProperties` 中添加一些全局实例属性或方法 4. 一个可能上述 3 种都包含的功能库(例如 vue-router) ### 编写一个插件 为了更好地理解如何构建 Vue.js 插件,我们可以试着写一个简单的 i18n (国际化 (Internationalization) 的缩写) 插件。 我们想让整个应用程序有一个按 key 名翻译文本内容的函数,因此我们将它暴露在 `app.config.globalProperties` 上。这个函数接收一个以 . 作为分隔符的 key 字符串,用来在用户提供的翻译字典中查找对应语言的文本。 ```js // plugins/i18n.js export default { install: (app, options) => { // 注入一个全局可用的 $translate() 方法 app.config.globalProperties.$translate = key => { // 获取 `options` 对象的深层属性 // 使用 `key` 作为索引 return key.split('.').reduce((o, i) => { if (o) { return o[i] } }, options) } }, } ``` 该插件希望用户在使用该插件时通过选项传入一个翻译字典对象,所以应该这样使用: ```js import i18nPlugin from './plugins/i18n.js' app.use(i18nPlugin, { greetings: { hello: 'Bonjour!', }, }) ``` ```vue

    {{ $translate('greetings.hello') }}

    ``` ### 插件中的 provide / inject 在插件中,我们可以通过 provide 来为插件用户供给一些内容。举个例子,我们可以将 options 参数提供给整个应用,以便各个组件都能使用这个翻译字典对象。 ```js {13} // plugins/i18n.js export default { install: (app, options) => { // 注入一个全局可用的 $translate() 方法 app.config.globalProperties.$translate = key => { // 获取 `options` 对象的深层属性 // 使用 `key` 作为索引 return key.split('.').reduce((o, i) => { if (o) { return o[i] } }, options) } app.provide('i18n', options) }, } ``` 现在,插件用户就可以在他们的组件中以 i18n 为 key 注入并访问插件的选项对象了。 ```js ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/typescript/index.md' --- # TypeScript --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/typescript/composition-api.md' --- # TypeScript 与组合式 API ## 为组件的 props 标注类型 ## 使用 ` ``` 这被称之为“运行时声明”,因为传递给 `defineProps()` 的参数会作为运行时的 `props` 选项使用。 然而,通过泛型参数来定义 `props` 的类型通常更直接: ```vue ``` 这被称之为“基于类型的声明”。编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。在这种场景下,我们第二个例子中编译出的运行时选项和第一个是完全一致的。 基于类型的声明或者运行时声明可以择一使用,但是不能同时使用。 我们也可以将 props 的类型移入一个单独的接口中: ```vue ``` 这同样适用于 Props 从另一个源文件中导入的情况。该功能要求 TypeScript 作为 Vue 的一个 peer dependency。 ```vue ``` ## Props 解构默认值 当使用基于类型的声明时,我们失去了为 `props` 声明默认值的能力。这可以通过 `withDefaults` 编译器宏解决: ```ts export interface Props { msg?: string labels?: string[] } const props = withDefaults(defineProps(), { msg: 'hello', labels: () => ['one', 'two'], }) ``` 这将被编译为等效的运行时 props default 选项。此外,`withDefaults` 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。 ::: tip 请注意,可变引用类型 (如数组或对象) 的默认值应封装在函数中,以避免被意外修改或产生外部副作用。这样可以确保每个组件实例都能获得属于自己的默认值副本。 ::: ### 非 ` ``` 对于运行时声明,我们可以使用 `PropType` 工具类型: ```ts import type { PropType } from 'vue' const props = defineProps({ book: Object as PropType, }) ``` 其工作方式与直接指定 props 选项基本相同: ```ts import { defineComponent } from 'vue' import type { PropType } from 'vue' export default defineComponent({ props: { book: Object as PropType, }, }) ``` ## 为组件的 emits 标注类型 在 ` ``` ```vue ``` ````vue ```vue ```` 类型参数可以是以下的一种: 1. 一个可调用的函数类型,但是写作一个包含调用签名的类型字面量。它将被用作返回的 emit 函数的类型。 2. 一个类型字面量,其中键是事件名称,值是数组或元组类型,表示事件的附加接受参数。上面的示例使用了具名元组,因此每个参数都可以有一个显式的名称。 若没有使用 ` ``` ::: warning 为了严格的类型安全,有必要在访问 `el.value` 时使用可选链或类型守卫。这是因为直到组件被挂载前,这个 `ref` 的值都是初始的 `null`,并且在由于 `v-if` 的行为将引用的元素卸载时也可以被设置为 `null`。 ::: ## 为组件模板引用标注类型 有时,你可能需要为一个子组件添加一个模板引用,以便调用它公开的方法。举例来说,我们有一个 `MyModal` 子组件,它有一个打开模态框的方法: ```vue ``` 为了获取 `MyModal` 的类型,我们首先需要通过 `typeof` 得到其类型,再使用 TypeScript 内置的 `InstanceType` 工具类型来获取其实例类型: ```vue ``` 如果组件的具体类型无法获得,或者你并不关心组件的具体类型,那么可以使用 `ComponentPublicInstance`。这只会包含所有组件都共享的属性,比如 `$el`。 ```vue ``` --- --- url: 'https://zhaobc.site/notes/frontend/vue/vue3/typescript/overview.md' --- # 搭配 TypeScript 使用 Vue Vue 本身就是用 TypeScript 编写的,所有的 Vue 官方库都自带了类型声明文件,开箱即用。 ## 配置 tsconfig.json 通过 create-vue 搭建的项目包含了预先配置好的 tsconfig.json。其底层配置抽象于 @vue/tsconfig 包中。 手动配置 tsconfig.json 时,请留意以下选项: * `compilerOptions.isolatedModules` 应当设置为 `true`,因为 Vite 使用 esbuild 来转译 TypeScript,并受限于单文件转译的限制。 * 如果你正在使用选项式 API,需要将 `compilerOptions.strict` 设置为 `true` (或者至少开启 `compilerOptions.noImplicitThis`,它是 `strict` 模式的一部分),才可以获得对组件选项中 `this` 的类型检查。否则 `this` 会被认为是 `any`。 * 如果你在构建工具中配置了路径解析别名,例如 `@/\*` 这个别名被默认配置在了 `create-vue` 项目中,你需要通过 `compilerOptions.paths` 选项为 TypeScript 再配置一遍。 如果你打算在 Vue 中使用 TSX,请将 `compilerOptions.jsx` 设置为 `"preserve"`,并将 `compilerOptions.jsxImportSource` 设置为 `"vue"`。 ## 常见使用说明 ### `defineComponent()` 为了让 TypeScript 正确地推导出组件选项内的类型,我们需要通过 `defineComponent()` 这个全局 API 来定义组件: ```ts import { defineComponent } from 'vue' export default defineComponent({ props: { name: String, msg: { type: String, required: true, }, }, data() { return { count: 1, } }, mounted() { this.name // string | undefined this.msg // string this.count // number }, }) ``` 当没有结合 ` ``` `lang="ts"` 也可以用于 ` ``` ### 模板中的 TypeScript 在使用了 ` ``` 可以使用内联类型强制转换解决此问题: ```vue {6} ``` --- --- url: 'https://zhaobc.site/notes/other/index.md' --- # 其他内容 --- --- url: 'https://zhaobc.site/notes/other/demo/index.md' --- # 测试示例 ## ECharts 示例 ::: echarts echarts demo ```json { "xAxis": { "type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] }, "yAxis": { "type": "value" }, "series": [ { "data": [150, 230, 224, 218, 135, 147, 260], "type": "line", "smooth": true } ] } ``` ::: ## Playground 示例 ::: playground#vue Playground demo @file App.vue ```vue ``` @file Comp.vue ```vue ``` @import ```json { "imports": { "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js" } } ``` ::: ::: vue-playground Vue Playground demo @file App.vue ```vue ``` @file Comp.vue ```vue ``` @import ```json { "imports": { "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js" } } ``` @setting ```json { "service": "https://play.zhaobc.site" } ``` ::: ## Element-Plus 示例 ::: playground#vue Element-Plus demo @file App.vue ```vue ``` @import import\_map.json ```json { "imports": { "a": "b" } } ``` @setting ```json { "service": "https://element-plus.run/" } ``` ::: ## Replit Demo 已废弃。 ```md ``` ## Flippy Card --- --- url: 'https://zhaobc.site/notes/other/dev/regex.md' --- # 正则表达式 ## 查找不含指定字符串的行 ```txt ^(?!指定字符串).*$ ``` 可视化 @startregex title 查找不含指定字符串的行 ^(?!指定字符串).\*$ @endregex ::: warning 直接书写 plantuml 会被替换成一个图片,直接在 `[vp-content]` 下面。 主题默认使用 `plugin-photo-swipe` 来浏览图片,它默认的选择器不能选择 `[vp-content]` 直接儿子元素,所以,需要在 plantuml 外面包一层 `div` 才可以。 另外,`
    ` 与 plantuml 内容直接需要空一行,不然 plantuml 不能正确渲染。 ::: 需求:查找 MyBatis 的 sqlmap 中未指定 `jdbcType` 的参数。 ```txt // 查找以 # 或 $ 开头,且不含 jdbcType 或包含no 或 offset 或 limit 的行 ^.*[#\$]\{(?!.*jdbcType)(?!*.no)(?!offset)(?!limit).*$ ``` --- --- url: 'https://zhaobc.site/notes/other/linux/index.md' --- # Linux 相关内容 记录一些 linux 相关的东西。 --- --- url: 'https://zhaobc.site/notes/other/linux/certs.md' --- # SSL 证书 生成 Apache 所需的 SSL 证书。 ```shell # 生成CA认证机构的证书密钥key # 需要设置密码,输入两次 openssl genrsa -des3 -out ca.key 2048 # 比如 123456 # 去除密钥里的密码(可选) # 这里需要再输入一次原来设的密码 openssl rsa -in ca.key -out ca.key # 用私钥ca.key生成CA认证机构的证书ca.crt # 其实就是相当于用私钥生成公钥,再把公钥包装成证书 openssl req -new -x509 -key ca.key -out ca.crt -days 3650 # 这个证书ca.crt有的又称为"根证书",因为可以用来认证其他证书 # 生成自己网站的密钥server.key openssl genrsa -des3 -out server.key 2048 openssl rsa -in server.key -out server.key # 比如 123456 # 生成自己网站证书的请求文件 # 如果找外面的CA机构认证,也是发个请求文件给他们 # 这个私钥就包含在请求文件中了,认证机构要用它来生成网站的公钥,然后包装成一个证书 openssl req -new -key server.key -out server.csr # 使用虚拟的CA认证机构的证书ca.crt,来对自己网站的证书请求文件server.csr进行处理,生成签名后的证书server.crt # 注意设置序列号和有效期(一般都设1年,这里设了10年) openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt -days 3650 ``` --- --- url: 'https://zhaobc.site/notes/other/linux/gitlab-pages.md' --- # Gitlab Pages 配置 ## Gitlab 配置 `/etc/gitlab/gitlab.rb` : ```ruby ##! Define to enable GitLab Pages #pages_external_url 'http://192.168.201.16' pages_external_url 'http://dev.bhh' gitlab_pages['enable'] = true ``` 重新配置 gitlab : ```shell gitlab-ctl reconfigure ``` ## Gitlab Pages 域名访问 `gitlab-pages.conf` 如下: ```conf server { listen *:80; server_name ~^(?.*)\.dev\.bhh$; server_tokens off; ## Don't show the nginx version number, a security best practice ## Disable symlink traversal disable_symlinks on; ## Real IP Module Config ## http://nginx.org/en/docs/http/ngx_http_realip_module.html ## Individual nginx logs for this GitLab vhost access_log /var/log/gitlab/nginx/gitlab_pages_access.log gitlab_access; error_log /var/log/gitlab/nginx/gitlab_pages_error.log; # Pass everything to pages daemon location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto http; # Prevent NGINX from caching pages in response to the pages `Cache-Control` # header. # # Browsers already respect this directive and Pages can handle the request # volume without help from NGINX. # # If this changes in the future, ensure `proxy_cache_key` is set to a value # like `$scheme$host$request_uri`, as the default value does not take the # Pages hostname into account, leading to incorrect responses being served. # # See https://gitlab.com/gitlab-org/gitlab-pages/issues/73 proxy_cache off; proxy_pass http://localhost:8090; } # Define custom error pages error_page 403 /403.html; error_page 404 /404.html; } ``` ## Gitlab Pages IP 访问 Gitlab Pages 的 nginx 配置文件在 `/var/opt/gitlab/nginx/conf` 下面。 添加一个新的配置文件,名为 `gitlab-pages-ip.conf`,内容如下: ```conf server { listen 8088; server_name 192.168.201.16; server_tokens off; ## Don't show the nginx version number, a security best practice ## Disable symlink traversal disable_symlinks on; ## Real IP Module Config ## http://nginx.org/en/docs/http/ngx_http_realip_module.html ## Individual nginx logs for this GitLab vhost access_log /var/log/gitlab/nginx/gitlab_pages_ip_access.log gitlab_access; error_log /var/log/gitlab/nginx/gitlab_pages_ip_error.log; # Pass everything to pages daemon location / { root /var/opt/gitlab/gitlab-rails/shared/pages; index index.html; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto http; # Prevent NGINX from caching pages in response to the pages `Cache-Control` # header. # # Browsers already respect this directive and Pages can handle the request # volume without help from NGINX. # # If this changes in the future, ensure `proxy_cache_key` is set to a value # like `$scheme$host$request_uri`, as the default value does not take the # Pages hostname into account, leading to incorrect responses being served. # # See https://gitlab.com/gitlab-org/gitlab-pages/issues/73 #proxy_cache off; #proxy_pass http://localhost:8090; } # location ~* ^/(.+)/(.+)/ { # root /var/opt/gitlab/gitlab-rails/shared/pages/$1/$2/public; # index index.html; # # proxy_cache off; # } # location ^~ /bbx/wiki/ { # root /var/opt/gitlab/gitlab-rails/shared/pages/bbx/wiki/public; # index index.html; # # proxy_cache off; # } location ^~ /wiki { #root /var/opt/gitlab/gitlab-rails/shared/pages/bbx/wiki/public; alias /var/opt/gitlab/gitlab-rails/shared/pages/bbx/wiki/public; index index.html; #proxy_cache off; } # Define custom error pages error_page 403 /403.html; error_page 404 /404.html; } ``` ::: warning 为了便捷访问 Gitlab Pages,不想多加 `public` 路径,所以自己重写了 `location`。 并且为了能同时使用域名,并且保持域名访问和 IP 访问的 `base` 一致,重写了 `location`。 nginx 中的 `root` 和 `alias` 不同,使用 `root` 时,会再拼上 `location`,会导致加载有问题,所以改用 `alias`。 ::: IP 访问地址:`http://192.168.201.16:8088/bbx/wiki/public/`,需要带 `public` 路径。 ## Gitlab Pages Nginx 重启 ```shell gitlab-ctl restart nginx ``` ## 开放端口 ```shell firewall-cmd --zone=public --add-port=8088/tcp --permanent systemctl restart firewalld ``` --- --- url: 'https://zhaobc.site/notes/other/linux/misc.md' --- # 杂项 ## 更改语言 ```shell # centos # change locale sed "s/LANG=\"en_US.UTF-8\"/LANG=\"ja_JP.UTF-8\"/" -i /etc/locale.conf localedef -i ja_JP -f UTF-8 ja_JP.UTF-8 # setup env cat << EOF >> /etc/profile export LANG=ja_JP.UTF-8 export LC_ALL=ja_JP.UTF-8 export LANGUAGE=ja_JP.UTF-8 EOF ``` ## 更改主机名 ```shell hostnamectl set-hostname Xxx ``` 注意:主机名带 `.` 的话,会忽略 `.` 之后的内容。 比如 `201.23` 会变为 `201`。 ## 开发防火墙端口 ### 查看端口 ```shell firewall-cmd --list-ports ``` ### 添加端口 ```shell firewall-cmd --zone=public --add-port=80/tcp --permanent ``` ### 重启防火墙 ```shell firewall-cmd --reload ``` --- --- url: 'https://zhaobc.site/notes/other/tools/git.md' --- # git 小提示 设置用户: ```shell git config user.email "413853119@qq.com" git config user.name "ZhaoBin" ``` 修改最后一次 commit 的用户信息: ```shell git commit --amend --author="ZhaoBin <413853119@qq.com>" --no-edit ``` 最近几次提交变基: ```shell # 最近3次提交变基 git rebase -i HEAD~3 ``` 遍历文件获取第一次提交的信息: ```shell echo "file,filename,hash,date,author,email,comment" > commit.txt git ls-files -r --name-only "$branch" | while read file; do # 截取最后一个 "/" 后面的文件名 filename="${file##*/}" # 输出第一次提交信息 log=$(git log --reverse --pretty=format:"%h,%ad,%an,%ae,%s" --date=format:%Y/%m/%d -- "$file" | head -n 1) echo "$file,$filename,$log" >> commit.txt done ``` --- --- url: 'https://zhaobc.site/posts/dev/coep-coop-corp-corp-cors.md' --- # 跨源隔离和跨域资源共享 在开发 [Python Playground](https://play-py.zhaobc.site) 时遇到了跨域资源共享的问题,尤其是把它嵌入到本站时,导致了一些功能无法正常工作。 ## 相关术语 在解决这些过程中,遇到了一些专业术语,这里简单记录一下。 * [`Cross-Origin-Opener-Policy` (*COOP*)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy) * [`Cross-Origin-Embedder-Policy` (*COEP*)](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy) * [`Cross-Origin-Resource-Policy` (*CORP*)](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy) * [Cross-Origin Resource Sharing (*CORS*)](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/CORS) * [`Content-Security-Policy` (*CSP*)](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/CSP) ## SharedArrayBuffer 相关 Python Playground 中用到了 [`SharedArrayBuffer`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer),它需要一定的[安全需求](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#%E5%AE%89%E5%85%A8%E9%9C%80%E6%B1%82)。 MDN 中对 [`SharedArrayBuffer`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) 有详细的说明。 ::: tip 对于顶级文档,需要设置两个标头来实现你网站的跨源隔离: `Cross-Origin-Opener-Policy` 设置为 `same-origin`(来保护你的源站点免受攻击) `Cross-Origin-Embedder-Policy` 设置为 `require-corp` 或 `credentialless`(保护受害者免受你的源站点的影响) 为了验证跨源隔离是否生效,你可以测试窗口和 worker 上下文中的 `Window.crossOriginIsolated` 或 `WorkerGlobalScope.crossOriginIsolated` 属性。 嵌套文档和专用 worker 线程也需要将 `Cross-Origin-Embedder-Policy` 标头设置为同样的值。 对于同源嵌套文档和子资源,不需要进行任何其他更改。 同站(但跨源)嵌套文档和子资源需要将 `Cross-Origin-Resource-Policy` 标头设置为 `same-site`。 而它们的跨源(和跨站点)的对应部分也需要将同样的标头设置为 `cross-origin`。 请注意,将 `Cross-Origin-Resource-Policy` 标头设置为除 `same-origin` 之外的任何值,都会使资源暴露于潜在的攻击中,比如幽灵漏洞。 ::: ::: warning `credentialless` 目前还有[浏览器兼容性问题](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9%E6%80%A7)。 Safari 和多数移动端浏览器目前不支持 `credentialless`。 ::: ## 相关设置 基于以上信息,将主站(也就是本站)的 headers 做了一下设置。 * 正常 PC 浏览器 ```txt Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: credentialless Cross-Origin-Resource-Policy: cross-origin ``` * 移动端浏览器 ```txt Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Resource-Policy: cross-origin ``` * Safari ```txt Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Resource-Policy: cross-origin ``` `vite` 中设置如下 : ```js export default defineConfig({ plugins: [ vue(), // 在 server.headers 中设置貌似不起作用,在 plugins 中可以起作用。 // Support Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy on dev server // https://github.com/vitejs/vite/issues/3909#issuecomment-934044912 { name: 'configure-response-headers', configureServer: server => { server.middlewares.use((_req, res, next) => { // Cross-Origin-Embedder-Policy 浏览器兼容性 // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9%E6%80%A7 const userAgent = _req.headers['user-agent']?.toLowerCase() || '' if (/safari/i.test(userAgent) && !/chrome/i.test(userAgent)) { // safari res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp') } else { res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless') } res.setHeader('Cross-Origin-Opener-Policy', 'same-origin') res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin') next() }) }, }, ], server: { headers: { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Resource-Policy': 'cross-origin', }, }, }) ``` `vercel.json` 设置: ::: warning [Invalid route source pattern](https://vercel.com/docs/errors/error-list#invalid-route-source-pattern) The `source` property follows the syntax from [path-to-regexp](https://github.com/pillarjs/path-to-regexp), not the `RegExp` syntax. ::: ```json { "headers": [ { "source": "/(.*)", "headers": [ { "key": "Cross-Origin-Embedder-Policy", "value": "credentialless" }, { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, { "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" } ] }, { "source": "/(.*)", "has": [ { "type": "header", "key": "User-Agent", "value": { "re": ".*(iPhone|iPad|iPod|iOS|Android).*" } } ], "headers": [ { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }, { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, { "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" } ] }, { "source": "/(.*)", "has": [ { "type": "header", "key": "User-Agent", "value": { "re": "^(?=.*Safari)(?!.*Chrome).*$" } } ], "headers": [ { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }, { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, { "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" } ] } } ``` ## 副作用 * 设置了 `COEP` 后,会阻塞 `CORS` 导致引入的外部资源无法加载,需要单独指定 `crossorigin`。 尤其是 外部的 CDN资源,CSS, 图片,视频,等资源。 ```html ``` 但是,一些外部资源动态追加的资源无法控制,导致无法加载。比如,百度统计, clarity 统计。 Safari 浏览器会无法加载 CDN 资源,导致图标无法正常显示。 哪位朋友知道如何解决的话,忘不吝赐教。 * 设置了 `COEP` 后,`iframes` 会无法加载。需要指定 `sandbox="allow-scripts allow-same-origin" credentialless` 属性。 --- --- url: 'https://zhaobc.site/posts/dev/docker-jitsi.md' --- # Jitsi 会议系统升级 ## 简介 [Jitsi Meet](https://github.com/jitsi/jitsi-meet) 是一款开源的 Web 会议系统。 ## 背景 2019 年时在公司内部搞了一个 Jitsi 会议系统,为了省事,都是搭建 Docker 版的。 少人数时使用下来还可以,后来就一直使用了下来,中间也升级过几次。 最近有同事反应,不能多人在线了,超过2人后,前面的人会自动被踢下线,奇怪的是之前都是可以的,2022 年 8 月 升级后,一直没这个问题。 后来猜测可能是最近浏览器升级导致这个问题。 Jitsi 的版本也确实老了,想着升级一下,看看新版能不能解决这个问题。 ## 升级 Docker 版搭建和升级都比较简单,直接下载官方配置好的 yaml 文件,直接执行 `docker compose` 启动即可。 但是往往想象很美好,现实很残酷。。。 这里简单记录一下遇到的坑。 ### 准备 参照官方的指南: [Self-Hosting Guide - Docker](https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker) ### 修改 `.env` 配置 修改 `.env` 里的 以下配置: * `PUBLIC_URL` * `JVB_ADVERTISE_IPS` * `ETHERPAD_PUBLIC_URL` * `WHITEBOARD_COLLAB_SERVER_PUBLIC_URL` ### 问题点 #### 无法进入会议 按照之前的设置,`PUBLIC_URL` 配置了服务器的 IP,启动都没问题,但是无法参加会议。 浏览器控制台有 SSL 的错误,原来以为是证书的问题,自己生成了证书,客户端也安装了证书,还是不行。 查阅了一些资料,貌似别人都没问题,最后 `PUBLIC_URL` 改用域名的方式就没问题了。。 #### 白板无法同步 一开始看到 nginx 里配置了 `/socket.io/` 的转发,以为 `WHITEBOARD_COLLAB_SERVER_PUBLIC_URL` 配置要加 `/socket.io/`。 但是这样反而会导致白板无法同步,每个人各自显示各自写的白板。 F12 网络请求里 `/socket.io/` 的消息里有 `invalid namespace`。 白板的 `WHITEBOARD_COLLAB_SERVER_PUBLIC_URL` 不要加 `/socket.io/`,去掉就能协作了。 参考资料:[记socket.io实现websocket长连接,解决 invalid namespace ERROR](https://blog.csdn.net/cheendf/article/details/132276345) > `socket.io` 如果没有在 options 选项中指定 websocket 建立连接进行 handshake 的路径,则默认为 `/socket.io/` > 客户端进行连接时 URL只需要写 `ip:port` 或 `域名` 就可以了。 ## 配置文件示例 实际用到的配置文件如下: ```shell # # Basic configuration options # # Directory where all configuration will be stored CONFIG=~/.jitsi-meet-cfg # Exposed HTTP port HTTP_PORT=8000 # Exposed HTTPS port HTTPS_PORT=8443 # System time zone #TZ=UTC TZ=Asia/Shanghai # add by zhaobc XMPP_DOMAIN=meet.jitsi XMPP_SERVER=xmpp.meet.jitsi XMPP_AUTH_DOMAIN=auth.meet.jitsi XMPP_BOSH_URL_BASE=http://xmpp.meet.jitsi:5280 XMPP_MUC_DOMAIN=muc.meet.jitsi XMPP_CROSS_DOMAIN=true ENABLE_COLIBRI_WEBSOCKET=1 ENABLE_XMPP_WEBSOCKET=1 ENABLE_HSTS=0 # Public URL for the web service (required) PUBLIC_URL=https://meet.example.com:8443 # Media IP addresses to advertise by the JVB # This setting deprecates DOCKER_HOST_ADDRESS, and supports a comma separated list of IPs # See the "Running behind NAT or on a LAN environment" section in the Handbook: # https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker#running-behind-nat-or-on-a-lan-environment JVB_ADVERTISE_IPS=192.168.1.1,1.2.3.4 JVB_DISABLE_STUN=true # # Etherpad integration (for document sharing) # # Set the etherpad-lite URL in the docker local network (uncomment to enable) ETHERPAD_URL_BASE=http://etherpad.meet.jitsi:9001 # Set etherpad-lite public URL, including /p/ pad path fragment (uncomment to enable) ETHERPAD_PUBLIC_URL=https://meet.example.com:8443/etherpad/p/ # # Whiteboard integration # # Set the excalidraw-backend URL in the docker local network (uncomment to enable) WHITEBOARD_COLLAB_SERVER_URL_BASE=http://whiteboard.meet.jitsi # Set the excalidraw-backend public URL (uncomment to enable) WHITEBOARD_COLLAB_SERVER_PUBLIC_URL=https://meet.example.com:8443/ # # Authentication configuration (see handbook for details) # # Enable authentication #ENABLE_AUTH=1 # Enable guest access ENABLE_GUESTS=1 # # Docker Compose options # # Container restart policy #RESTART_POLICY=unless-stopped RESTART_POLICY=always # Jitsi image version (useful for local development) #JITSI_IMAGE_VERSION=latest ``` ## 启动 ```shell docker compose -f docker-compose.yml -f etherpad.yml -f whiteboard.yml up -d ``` ## 停止 ```shell docker compose -f docker-compose.yml -f etherpad.yml -f whiteboard.yml down ``` --- --- url: 'https://zhaobc.site/posts/dev/git-submodule.md' --- # git submodule 使用 ## 为什么 本仓库代码是公开的,但是有时候又想保存一些个人私有资料,又希望跟本站一起发布。 Hope 主题已具备加密访问功能,剩下的就是怎么保存私有的 MD 文件了。 此时想到了 git submodule,将私有仓库作为本仓库的一个文件夹,然后通过 git submodule 关联过来, 这时候再跟本站一起发布应该可以达到想要的效果。 ## 使用 ### 事先设置 先设置 `submodule.recurse`,以便使用 `git clone`, `git pull` 时同步更新子模块。 ```shell git config --global submodule.recurse true ``` ### 添加子模块 ::: warning 私有仓库作为子模块的话,本地使用的话,建议使用 `SSH` 协议,不然更新起来比较麻烦,得使用 `PAT`。 ::: 比如,本站想把一个私有仓库 `https://github.com/FuckDoctors/private-notes`,放到 `docs/posts/private` 下面。 先进到 `docs/posts` 下面,然后执行下面的命令。 ```shell git submodule add -b main git@github.com:FuckDoctors/private-notes.git private ``` 上面 `-b main` 是指定了 `main` 分支。 小插曲,一开始不知怎么回事,`git submodule add` 老不成功,即使使用了 `-f` 也不行,`git submodule deinit` 也不行。 后来手动删除了 `.git/modules` 文件夹,然后重新执行 `git submodule add` 就可以了。 执行成功后,会自动生成一个 `.gitmodules` 文件,大概长下面这样。 ```txt [submodule "docs/posts/private"] path = docs/posts/private url = git@github.com:FuckDoctors/private-notes.git branch = main ``` 同时,`docs/posts` 下面会多一个 `private` 文件夹。 ### 更新一个子模块 进到子模块目录 `docs/posts/private` ,然后执行下面的命令。 ```shell git submodule update --remote ``` ### 更新所有子模块 ```shell git submodule foreach git pull # 或 git submodule update --recursive --remote ``` ### 删除子模块 ```shell git submodule deinit [] git rm [] rm -rf .git/modules/[] ``` ### 列出子模块 ```shell git submodule ``` ### 列出子模块状态 ```shell git submodule status ``` ### 更改子模块的 url 新版 git 可以直接使用下面的命令: ```shell git submodule set-url ``` 旧版的话,需要删除子模块,然后重新添加。 ## 包含私有子模块发布 当仓库中含有私有的子模块时,需要指定一个只读的 `PAT`,然后使用 `HTTPS` 方式来访问。 ### Github Action [actions/checkout](https://github.com/actions/checkout) 本身就支持子模块。 ```yaml - uses: actions/checkout@v4 with: # Personal access token (PAT) used to fetch the repository. The PAT is configured # with the local git config, which enables your scripts to run authenticated git # commands. The post-job step removes the PAT. # # We recommend using a service account with the least permissions necessary. Also # when generating a new PAT, select the least scopes necessary. # # [Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) # # Default: ${{ github.token }} token: '' # Whether to checkout submodules: `true` to checkout submodules or `recursive` to # recursively checkout submodules. # # When the `ssh-key` input is not provided, SSH URLs beginning with # `git@github.com:` are converted to HTTPS. # # Default: false submodules: true ``` ### Vercel Vercel 貌似暂不支持私有子模块。[Build Features for Customizing Deployments](https://vercel.com/docs/builds/build-features#git-submodules) > On Vercel, you can deploy Git submodules with a Git provider as long as the submodule is publicly accessible through the HTTP protocol. Git submodules that are private or requested over SSH will fail during the Build step. However, you can reference private repositories formatted as npm packages in your package.json file dependencies. 可以在 Vercel Checkout 完了之后,更改子模块的原来的 `SSH` 协议的 url 为 带有 `PAT` 的 `HTTPS`,或者删除子模块重新创建。 可以参考这篇文章:[How to Deploy a Project with Private Git Submodules to Vercel](https://www.ytyng.com/en/blog/how-to-deploy-project-to-vercel-includes-private-submodules) ## 参考资料 * [Github Actions: submodule 下公私有仓库授权和通信](https://zhuanlan.zhihu.com/p/408319831) * [git-submodule](https://git-scm.com/docs/git-submodule) * [Using Git Submodules for Private Content](https://www.taniarascia.com/git-submodules-private-content/) * [How to use private Git submodules](https://docs.readthedocs.com/platform/stable/guides/private-submodules.html) --- --- url: 'https://zhaobc.site/posts/dev/hello-deepseek.md' --- # DeepSeek 初体验 春节期间,DeepSeek 真是大火,手头正好有个需求,我就体验了一下。 **它真的是很强大**。 这里仅是简单记录一下,它生成的代码,更多关于 DeepSeek 的使用,大家可以去官网查看。 ## 需求 我需要一个分割 DOM 树的功能,根据指定的节点,将 DOM 树分割成两个 DOM 树。 一开始试了几个 DeepSeek 和 豆包 AI,都没达到想要的效果。 随后,重新提了自己的需求,将需求更明确化之后,试了一下 DeepSeek,它经过 5 分钟的推理之后,给出了一个非常不错的答案。 需求描述如下: > 帮我用javascript写一段按指定节点分割dom树的代码,分割后的dom树依旧内容完整。dom树可以是多层嵌套的。指定节点之前的dom树内容到该节点结束,自动补全剩余的dom树结束符。指定节点之后的dom树内容以该节点开始,自动补全之前的dom树开始符。 ## 生成结果 以下是实现按指定节点分割DOM树的JavaScript代码,返回分割后的两个完整DOM结构: 注:原来是以整个 document 来处理的,稍微改了一下,接受指定的 root。 ```javascript // function splitDOMAtNode(originalNode) { function splitDOMAtNode(root, originalNode) { // 深克隆整个DOM树并建立原始节点映射 function cloneTreeWithMap(root) { const nodeMap = new Map() function clone(node) { const cloned = node.cloneNode(false) nodeMap.set(node, cloned) Array.from(node.childNodes).forEach(child => { cloned.appendChild(clone(child)) }) return cloned } return { clonedTree: clone(root), nodeMap } } // 修剪后续兄弟节点 function pruneSiblingsAfter(node) { let current = node while (current.parentNode) { const parent = current.parentNode const siblings = Array.from(parent.childNodes) const idx = siblings.indexOf(current) // 移除当前节点之后的所有节点 for (let i = siblings.length - 1; i > idx; i--) { parent.removeChild(siblings[i]) } current = parent } } // 修剪前置兄弟节点 function pruneSiblingsBefore(node) { let current = node while (current.parentNode) { const parent = current.parentNode const siblings = Array.from(parent.childNodes) const idx = siblings.indexOf(current) // 移除当前节点之前的所有节点 for (let i = 0; i < idx; i++) { parent.removeChild(siblings[i]) } current = parent } } // // 获取文档根节点 // const docRoot = originalNode.ownerDocument.documentElement; // 处理前半部分 // const front = cloneTreeWithMap(docRoot); const front = cloneTreeWithMap(root) const frontNode = front.nodeMap.get(originalNode) pruneSiblingsAfter(frontNode) // 处理后半部分 // const back = cloneTreeWithMap(docRoot); const back = cloneTreeWithMap(root) const backNode = back.nodeMap.get(originalNode) pruneSiblingsBefore(backNode) return { frontFragment: front.clonedTree, backFragment: back.clonedTree, } } // 使用示例 const targetNode = document.getElementById('split-target') const { frontFragment, backFragment } = splitDOMAtNode(targetNode) // 获取HTML字符串 console.log('前半部分:\n', frontFragment.outerHTML) console.log('后半部分:\n', backFragment.outerHTML) ``` ### 实现原理 1. **深度克隆**:使用递归方式克隆整个DOM树,并建立原始节点到克隆节点的映射关系 2. **节点修剪**: * 前半部分:从目标节点向上遍历,逐层移除当前节点之后的所有兄弟节点 * 后半部分:从目标节点向上遍历,逐层移除当前节点之前的所有兄弟节点 3. **保持结构完整**:通过逐层处理父节点,确保生成的DOM片段保持完整的树形结构 ### 使用说明 1. 传入需要分割的目标节点 2. 返回包含两个属性的对象: * `frontFragment`:包含目标节点及其之前内容的新DOM树 * `backFragment`:包含目标节点及其之后内容的新DOM树 3. 可通过`outerHTML`属性获取完整HTML字符串 ### 特点 * 保持原始DOM结构完整性 * 自动处理多层嵌套结构 * 不修改原始DOM树 * 返回标准DOM节点,可直接插入文档 > 注意:该方法会克隆整个文档,适用于中小型页面。对于超大DOM结构建议指定子树进行克隆以提升性能。 ## 推理过程 由于推理过程较长,将其折叠起来,感兴趣的可以查看。 ::: details 点击查看推理过程 ```txt 好的,我现在需要帮用户写一段JavaScript代码,按指定节点分割DOM树,分割后的两部分要保持内容完整。这个问题看起来有点挑战性,我需要仔细分析用户的需求。 首先,用户希望将一个DOM树在指定节点处分割成两部分。分割后的前部分应该包含指定节点之前的所有内容,并且在该节点结束,自动补全剩下的结束标签。后部分则是从指定节点开始,补全之前的开始标签。这听起来像是要生成两个新的HTML片段,每个片段在结构上都是完整的,但分别以指定节点为结尾和开头。 接下来,我需要理解如何实现这个功能。可能需要遍历DOM树,分别处理分割点之前和之后的节点。对于前部分,需要从根节点开始,一直到指定节点,然后确保所有未闭合的标签都被正确闭合。对于后部分,可能需要从指定节点开始,向上补全所有父节点的开始标签,使得这个片段可以独立存在。 首先,处理前部分。当分割点在某个节点时,前部分需要包含该节点及其之前的所有兄弟节点,同时保证父节点的结构完整。比如,如果指定节点是某个div的第三个子节点,那么前部分应该包含该div的前三个子节点,并且闭合该div以及所有祖先节点。但用户可能希望前部分在指定节点处结束,所以可能需要克隆指定节点及其之前的兄弟节点,然后补全所有未闭合的标签。 然后是后部分,需要包含指定节点及其之后的兄弟节点,同时补全父节点的开始标签。例如,如果指定节点在某个div中,那么后部分的div应该从该节点开始,同时前面的父节点结构需要补全,但可能只需要从该节点所在层级往上补全。 但是,直接操作DOM树可能会有问题,因为DOM是动态的,修改会影响页面。所以可能需要先克隆节点,然后在副本上进行操作,避免影响实际页面。 可能的步骤是: 1. 找到指定节点,获取其父节点和所有兄弟节点。 2. 分割兄弟节点为前部分(直到指定节点)和后部分(从指定节点开始)。 3. 对于前部分,克隆父节点,将前部分兄弟节点添加到克隆的父节点中,然后递归处理所有祖先节点,确保结构完整。 4. 对于后部分,同样克隆父节点,将后部分兄弟节点添加进去,然后补全父节点的结构,可能需要向上构建父节点的开始部分。 不过,如何自动补全开始和结束标签呢?可能需要从指定节点向上遍历,克隆父节点结构,但只保留必要的子节点。例如,前部分需要父节点结构,但只包含到指定节点为止,并确保每个父节点都被正确闭合。后部分需要父节点结构,但只包含从指定节点开始的部分,并在前面补全父节点的开始标签。 或者,可以考虑生成HTML字符串,而不是操作实际的DOM节点。这样可以通过字符串操作来补全标签。例如,对于前部分,遍历从根到指定节点的路径,生成开始标签,直到指定节点,然后生成结束标签。但这样可能不太准确,因为DOM结构可能复杂,嵌套层次多。 或者,可以分别构建两个文档片段,前片段包含到指定节点为止的结构,后片段包含从指定节点开始的结构,同时补全必要的父节点。 可能需要一个函数来克隆指定节点及其祖先的结构,只保留必要的子节点。例如,对于前部分,克隆每个祖先节点,但只保留到指定节点为止的兄弟节点。对于后部分,克隆祖先节点,但只保留从指定节点开始的兄弟节点。 举个例子,假设指定节点是某个div中的第三个p标签。前部分需要生成一个包含该div的克隆,其中只有前三个p标签,然后闭合该div及其所有祖先。后部分则生成一个包含该div的克隆,其中从第三个p开始,同时这些克隆的div需要被包裹在祖先节点的结构中,但祖先的开始标签需要存在,而结束标签在适当的位置。 这似乎需要递归地处理每个父节点,克隆它们,并调整子节点列表为分割后的部分。例如,对于前部分: function cloneAncestors(node, includeSiblingsBefore) { if (!node.parentNode) return null; const parentClone = node.parentNode.cloneNode(false); // 浅克隆,不包含子节点 const siblings = Array.from(node.parentNode.childNodes); const index = siblings.indexOf(node); const nodesToInclude = includeSiblingsBefore ? siblings.slice(0, index + 1) : siblings.slice(index); nodesToInclude.forEach(child => { if (child === node && includeSiblingsBefore) { parentClone.appendChild(child.cloneNode(true)); // 可能需要深度克隆? } else { // 对于其他子节点,可能需要克隆并根据情况处理 // 这里可能需要递归处理,如果是前部分,只克隆前面的兄弟节点 // 但这样可能变得复杂,因为每个子节点可能还有自己的子节点 } }); const higherAncestor = cloneAncestors(node.parentNode, includeSiblingsBefore); if (higherAncestor) { higherAncestor.appendChild(parentClone); } else { return parentClone; } } 这可能比较复杂,因为每个祖先节点都需要被克隆,并且其子节点需要被调整为分割后的部分。比如,父节点克隆后,子节点只包括到指定节点为止的前兄弟节点,或者从指定节点开始的后兄弟节点。 或者,对于前部分,从指定节点向上遍历,每个父节点克隆一个副本,该副本的子节点只包含到指定节点及其之前的兄弟节点。例如: function buildFrontFragment(node) { let current = node; let fragment = null; while (current.parentNode) { const parentClone = current.parentNode.cloneNode(false); const siblings = Array.from(current.parentNode.childNodes); const index = siblings.indexOf(current); const precedingSiblings = siblings.slice(0, index + 1); precedingSiblings.forEach(sibling => { if (sibling === current) { parentClone.appendChild(sibling.cloneNode(true)); } else { parentClone.appendChild(sibling.cloneNode(true)); // 或者是否需要处理嵌套? } }); if (fragment) { parentClone.appendChild(fragment); } fragment = parentClone; current = current.parentNode; } return fragment; } 这可能生成一个从根到指定节点的结构,其中每个父节点只包含到当前节点为止的子节点。但这样的话,可能结构会被正确补全。例如,如果原结构是 body > div > p,指定节点是p,那么前部分的fragment会是body克隆,包含div克隆,div克隆包含p克隆。这样,当序列化为HTML时,会自动生成正确的闭合标签。 同样,后部分需要从指定节点开始,向上构建父节点,每个父节点克隆后只包含从指定节点开始的子节点,并且这些父节点被正确包裹。例如,同样使用类似的方法: function buildBackFragment(node) { let current = node; let fragment = null; while (current.parentNode) { const parentClone = current.parentNode.cloneNode(false); const siblings = Array.from(current.parentNode.childNodes); const index = siblings.indexOf(current); const followingSiblings = siblings.slice(index); followingSiblings.forEach(sibling => { if (sibling === current) { parentClone.appendChild(sibling.cloneNode(true)); } else { parentClone.appendChild(sibling.cloneNode(true)); } }); if (fragment) { parentClone.appendChild(fragment); } fragment = parentClone; current = current.parentNode; } return fragment; } 这样,后部分的fragment会包含从指定节点开始的所有后续兄弟节点,并且每个父节点都被克隆,只包含这些子节点。当序列化为HTML时,父节点的开始标签会被保留,而结束标签在最后。 但这样生成的fragment是否正确呢?例如,原结构是:

    1

    2

    3

    4

    如果指定节点是id为target的p,前部分应该生成:

    1

    2

    3

    后部分应该生成:

    3

    4

    但按照上面的buildBackFragment函数,生成的div克隆会包含target和后续的p,但父结构可能更高层级的节点也被克隆。例如,如果div在body中,那么后部分的fragment会是body克隆包含div克隆,而div克隆包含target和后续的p。这样当序列化时,会有完整的结构,但用户可能需要后部分从target开始,即可能需要div从target开始,而不是整个body结构。 可能用户希望分割后的两个部分各自都是完整的HTML结构。比如,前部分可能是一个完整的HTML,包含到target为止的所有内容,后部分则是另一个完整HTML,从target开始的内容。但这可能不太现实,因为一个HTML只能有一个html根元素。或者用户可能希望得到两个DOM片段,每个片段在结构上是自洽的,可以独立存在。 或者,用户可能希望分割后的两个部分在DOM结构上各自闭合,比如前部分包含必要的父节点直到target,并闭合所有标签;后部分从target开始,并且有父节点的开始标签,以便结构正确。 例如,假设原结构是: body > div.container > div.main > p#target 前部分应该生成: body > div.container > div.main > p#target 后部分生成: p#target 的父结构是 div.main > div.container > body,但后部分需要从p#target开始,所以结构应该是: (但body不能嵌套在div里,这显然有问题) 这说明直接克隆父结构可能会导致不正确的嵌套,尤其是当父元素是像body这样的顶层元素时。 因此,可能需要另一种方法:生成两个HTML片段,前部分包含到target为止的所有祖先,每个祖先节点只包含到target及其之前的兄弟节点,并闭合所有标签。后部分则包含target及其之后的兄弟节点,并在前面补全祖先的开始标签,但如何补全这些开始标签? 或者,可以考虑将前部分和后部分作为两个文档片段,其中前部分的结构是原始结构的一部分,直到target,并且自动闭合所有父标签;后部分的结构则是从target开始,并且前面自动打开父标签,直到根。 但如何实现这一点呢? 另一种思路是,前部分可以通过将target之后的所有兄弟节点删除,然后取其父节点的outerHTML。但是这样会修改原始DOM,不可行。所以需要使用克隆的方式。 例如,前部分的处理步骤: 1. 克隆整个文档的body元素(或者指定的根元素)。 2. 在克隆中,找到对应的target节点。 3. 从target的父节点开始,删除target之后的所有兄弟节点。 4. 递归向上处理所有父节点,确保每个父节点只保留到当前处理节点的位置。 类似地,后部分的处理: 1. 克隆整个文档的body元素。 2. 找到对应的target节点。 3. 删除target之前的所有兄弟节点。 4. 递归向上处理父节点,确保父节点只保留从当前节点开始的部分。 但这样可能需要修改克隆的DOM结构,确保每个父节点只保留必要的子节点。 例如,实现前部分的克隆: function cloneAndPruneAfter(node, clonedNode) { let current = clonedNode; let original = node; while (current.parentNode) { const originalParent = original.parentNode; const clonedParent = current.parentNode; const siblings = Array.from(originalParent.childNodes); const index = siblings.indexOf(original); // 在克隆的父节点中,删除index之后的所有子节点 const clonedSiblings = Array.from(clonedParent.childNodes); for (let i = clonedSiblings.length - 1; i > index; i--) { clonedParent.removeChild(clonedSiblings[i]); } current = clonedParent; original = originalParent; } return current; // 返回根克隆节点 } 然后,使用这个函数来处理克隆的节点。例如: const originalRoot = document.documentElement; // 假设整个文档 const clonedRoot = originalRoot.cloneNode(true); const clonedTarget = clonedRoot.querySelector('#target'); // 需要找到对应的克隆节点 cloneAndPruneAfter(node, clonedTarget); 这样,clonedRoot将只包含到target为止的结构。然后,可以将clonedRoot的outerHTML作为前部分的HTML。 同理,后部分需要克隆整个结构,然后删除target之前的兄弟节点: function cloneAndPruneBefore(node, clonedNode) { let current = clonedNode; let original = node; while (current.parentNode) { const originalParent = original.parentNode; const clonedParent = current.parentNode; const siblings = Array.from(originalParent.childNodes); const index = siblings.indexOf(original); // 删除index之前的所有子节点 const clonedSiblings = Array.from(clonedParent.childNodes); for (let i = 0; i < index; i++) { clonedParent.removeChild(clonedSiblings[i]); } current = clonedParent; original = originalParent; } return current; } 但这里的问题是,当克隆整个文档时,如何找到对应的克隆节点?因为如果用户传入的node是原始文档中的节点,克隆后的节点在克隆树中的位置可能不同,需要找到对应的克隆节点。这可能需要一个映射,或者通过选择器重新查询,但这可能不可靠,尤其是当有多个相同选择器的情况下。 这似乎是一个挑战。例如,如果用户传入一个节点,要找到它在克隆树中的对应节点,可能需要遍历原始树和克隆树同步进行,或者给节点添加临时ID,然后在克隆树中查找。 或者,可以递归克隆,并在克隆时记录原始节点到克隆节点的映射。例如: function cloneNodeWithMap(node, map) { const clone = node.cloneNode(false); // 浅克隆 map.set(node, clone); if (node.childNodes) { node.childNodes.forEach(child => { const childClone = cloneNodeWithMap(child, map); clone.appendChild(childClone); }); } return clone; } const map = new Map(); const clonedRoot = cloneNodeWithMap(originalRoot, map); const clonedTarget = map.get(originalTarget); 这样,map会保存原始节点到克隆节点的映射,可以快速找到克隆后的目标节点。 然后,对前部分和后部分进行处理: 处理前部分: cloneAndPruneAfter(clonedTarget, clonedRoot); const frontHtml = clonedRoot.outerHTML; 处理后部分: cloneAndPruneBefore(clonedTarget, clonedRoot); const backHtml = clonedRoot.outerHTML; 这可能是一个可行的方法。但需要注意,这样的操作可能会影响其他部分,比如事件监听器、属性等,但用户可能只需要结构,不需要事件等。 总结步骤: 1. 克隆整个DOM树(或指定的根节点),并建立原始节点到克隆节点的映射。 2. 找到克隆后的目标节点。 3. 对于前部分,从目标节点向上遍历,删除每个父节点中目标节点之后的所有兄弟节点。 4. 对于后部分,同样向上遍历,删除每个父节点中目标节点之前的所有兄弟节点。 5. 将处理后的克隆树的outerHTML输出,得到前部分和后部分的HTML字符串。 但用户可能需要的是将分割后的两部分作为DOM节点,而不仅仅是HTML字符串。或者用户可能需要返回两个文档片段。 但问题描述中提到“分割后的dom树依旧内容完整”,所以可能需要生成两个DOM片段,每个都是有效的结构。 现在,我需要考虑如何实现这个功能,并处理可能出现的边缘情况,比如目标节点是根节点、没有父节点、或处于深层嵌套结构中。 可能的代码结构: function splitDOMAtNode(node) { // 创建原始到克隆的映射 const map = new Map(); function cloneTree(originalNode) { const clone = originalNode.cloneNode(false); map.set(originalNode, clone); Array.from(originalNode.childNodes).forEach(child => { clone.appendChild(cloneTree(child)); }); return clone; } const clonedRoot = cloneTree(node.ownerDocument.documentElement); const clonedNode = map.get(node); // 前部分处理:删除目标节点之后的所有兄弟节点及其子树 function pruneAfter(clonedNode) { let current = clonedNode; while (current.parentNode) { const parent = current.parentNode; const siblings = Array.from(parent.childNodes); const index = siblings.indexOf(current); for (let i = siblings.length - 1; i > index; i--) { parent.removeChild(siblings[i]); } current = parent; } } // 后部分处理:删除目标节点之前的所有兄弟节点及其子树 function pruneBefore(clonedNode) { let current = clonedNode; while (current.parentNode) { const parent = current.parentNode; const siblings = Array.from(parent.childNodes); const index = siblings.indexOf(current); for (let i = 0; i < index; i++) { parent.removeChild(siblings[i]); } current = parent; } } // 创建前部分和后部分的克隆 const frontRoot = clonedRoot.cloneNode(true); const frontClonedNode = map.get(node); // 这里可能有错误,因为frontRoot是新的克隆,之前的map可能不适用 // 这里发现一个问题,前面的cloneTree返回的是原始文档的克隆,但如果我们再次克隆frontRoot,那么需要重新建立映射,否则无法找到对应的clonedNode。 // 因此,正确的方法应该是分别克隆两次,一次用于前部分,一次用于后部分。 // 重新设计: // 创建前部分的克隆树 const frontClonedRoot = cloneTree(node.ownerDocument.documentElement); const frontClonedNode = map.get(node); pruneAfter(frontClonedNode); // 创建后部分的克隆树 const backClonedRoot = cloneTree(node.ownerDocument.documentElement); const backClonedNode = map.get(node); pruneBefore(backClonedNode); return { front: frontClonedRoot, back: backClonedRoot }; } 但这里的问题是,cloneTree函数在第二次调用时会覆盖map中的值,导致错误。因此,需要在每次克隆时使用新的map。 修改后的代码: function splitDOMAtNode(originalNode) { function cloneTreeWithMap(root) { const map = new Map(); function clone(node) { const cloned = node.cloneNode(false); map.set(node, cloned); Array.from(node.childNodes).forEach(child => { cloned.appendChild(clone(child)); }); return cloned; } const clonedRoot = clone(root); return { clonedRoot, map }; } const root = originalNode.ownerDocument.documentElement; // 克隆前部分 const { clonedRoot: frontRoot, map: frontMap } = cloneTreeWithMap(root); const frontClonedNode = frontMap.get(originalNode); pruneAfter(frontClonedNode); // 克隆后部分 const { clonedRoot: backRoot, map: backMap } = cloneTreeWithMap(root); const backClonedNode = backMap.get(originalNode); pruneBefore(backClonedNode); return { front: frontRoot, back: backRoot }; } function pruneAfter(clonedNode) { let current = clonedNode; while (current.parentNode) { const parent = current.parentNode; const siblings = Array.from(parent.childNodes); const index = siblings.indexOf(current); for (let i = siblings.length - 1; i > index; i--) { parent.removeChild(siblings[i]); } current = parent; } } function pruneBefore(clonedNode) { let current = clonedNode; while (current.parentNode) { const parent = current.parentNode; const siblings = Array.from(parent.childNodes); const index = siblings.indexOf(current); for (let i = 0; i < index; i++) { parent.removeChild(siblings[i]); } current = parent; } } 这样,前部分和后部分各自独立克隆整个文档,并分别进行处理。然后返回处理后的克隆根节点。 测试一下,假设原结构如下:

    1

    2

    3

    调用splitDOMAtNode(target)后,frontRoot应该包含:

    1

    2

    而backRoot应该包含:

    2

    3

    这样,两部分的结构都是完整的,闭合正确。 但实际测试中,pruneAfter函数会从目标节点开始,向上遍历每个父节点,删除后面的兄弟节点。例如,在front克隆中,处理div.container时,删除target之后的p,即第三个p。然后在body和html层级,由于它们没有其他子节点在div之后,所以不会有变化。因此,生成的frontRoot结构正确。 同样,back克隆中,处理div.container时,删除target之前的p,即第一个p。然后向上遍历父节点,body和html中没有其他子节点在div之前,因此结构正确。 但这种方法是否适用于更复杂的嵌套结构?比如,如果目标节点在一个列表中的某个深层位置,父节点可能有多个层级,每个层级都需要修剪。 例如:
    • A
    • B
      • C
      • D
      • E
    • F
    处理后的前部分应该包含到id为target的li为止:
    • A
    • B
      • C
      • D
    后部分应该从target开始:
    • B
      • D
      • E
    • F
    但根据pruneAfter的处理,当处理到li#target的父ul时,会删除其后的li。然后向上处理li B的父ul,删除其后的li F。再向上处理div,删除其后可能存在的兄弟节点?如果div是唯一的子节点,则不会有变化。最终前部分的结构正确。 而pruneBefore在处理后部分时,会删除target之前的li C,然后在父ul中,删除前面的兄弟节点。同样,父li B的ul中,删除前面的li A,然后在div的ul中,删除前面的兄弟节点。可能结构会是:
    • B
      • D
      • E
    • F
    这符合预期。 因此,这个方法是可行的。 现在,如何将这个逻辑转化为JavaScript代码,并处理可能的边缘情况? 需要注意的问题: 1. 如何克隆整个文档,包括html、head、body等。 2. 如何处理目标节点在根节点的情况。 3. 处理文本节点和其他类型的节点。 在代码中,cloneTreeWithMap函数会克隆传入的根节点,这里假设是document.documentElement,即html元素。然后递归克隆所有子节点。 pruneAfter和pruneBefore函数通过遍历父节点,删除不需要的兄弟节点。 测试时,需要确保目标节点存在于克隆树中,并且正确找到对应的克隆节点。 现在,编写代码: function splitDOMAtNode(originalNode) { // 辅助函数:克隆树并建立映射 function cloneTreeWithMap(root) { const map = new Map(); // 保存原始节点到克隆节点的映射 function clone(node) { const cloned = node.cloneNode(false); // 浅克隆,不克隆子节点 map.set(node, cloned); // 递归克隆子节点 Array.from(node.childNodes).forEach(child => { cloned.appendChild(clone(child)); }); return cloned; } const clonedRoot = clone(root); return { clonedRoot, map }; } // 修剪函数:删除目标节点之后的所有兄弟节点 function pruneAfter(clonedNode) { let current = clonedNode; while (current.parentNode) { const parent = current.parentNode; const siblings = Array.from(parent.childNodes); const index = siblings.indexOf(current); // 删除当前节点之后的所有兄弟节点 for (let i = siblings.length - 1; i > index; i--) { parent.removeChild(siblings[i]); } current = parent; // 向上处理父节点 } } // 修剪函数:删除目标节点之前的所有兄弟节点 function pruneBefore(clonedNode) { let current = clonedNode; while (current.parentNode) { const parent = current.parentNode; const siblings = Array.from(parent.childNodes); const index = siblings.indexOf(current); // 删除当前节点之前的所有兄弟节点 for (let i = 0; i < index; i++) { parent.removeChild(siblings[i]); } current = parent; // 向上处理父节点 } } // 获取原始文档的根节点 const root = originalNode.ownerDocument.documentElement; // 克隆并处理前部分 const { clonedRoot: frontRoot, map: frontMap } = cloneTreeWithMap(root); const frontClonedNode = frontMap.get(originalNode); if (!frontClonedNode) { throw new Error('Target node not found in cloned tree'); } pruneAfter(frontClonedNode); // 克隆并处理后部分 const { clonedRoot: backRoot, map: backMap } = cloneTreeWithMap(root); const backClonedNode = backMap.get(originalNode); if (!backClonedNode) { throw new Error('Target node not found in cloned tree'); } pruneBefore(backClonedNode); return { front: frontRoot, back: backRoot }; } 这个函数返回包含前部分和后部分克隆根节点的对象。用户可以通过outerHTML属性获取它们的HTML字符串,或者进一步操作。 例如: const target = document.getElementById('target'); const { front, back } = splitDOMAtNode(target); console.log(front.outerHTML); // 前部分的HTML console.log(back.outerHTML); // 后部分的HTML 需要注意,这种方法会克隆整个文档,包括head和body,这在某些情况下可能不是用户需要的。用户可能希望只处理某个子树,而不是整个文档。因此,可能需要修改函数,允许用户指定根节点,默认为document.documentElement。 改进后的函数可以接受一个root参数: function splitDOMAtNode(originalNode, root = originalNode.ownerDocument.documentElement) { // ...其余代码同前,使用传入的root作为克隆的根 } 这样用户可以选择分割某个子树,而不是整个文档。 另外,需要考虑原始Node是否在传入的root子树中,否则会导致映射失败。 但为了简化,假设用户传入的originalNode在root子树中。 综上所述,最终的代码应该能够处理大部分情况,生成正确分割的两个DOM树。 ``` ::: --- --- url: 'https://zhaobc.site/posts/dev/oh-my-posh.md' --- # oh-my-posh 自定义主题配置 ## 简介 macOS 上可以使用 [oh-my-zsh](https://ohmyz.sh/) 来美化你的命令行提示。 windows 上可以使用 [oh-my-posh](https://ohmyposh.dev/) 来美化你的 PowerShell 提示。 ::: note windows 下推荐使用 [Windows Terminal](https://learn.microsoft.com/zh-cn/windows/terminal/install) 配合 [PowerShell](https://learn.microsoft.com/zh-cn/powershell/scripting/install/installing-powershell-on-windows) 来使用。 ::: Oh My Posh 是一个开源的跨平台命令行提示工具,它可以让你的 PowerShell 显示漂亮的提示信息。 各自官网都有详细的安装和使用说明,本文不再赘述。 ## 自定义主题配置 oh-my-posh 提供了丰富的主题配置,你可以在 [主题](https://ohmyposh.dev/docs/themes) 中找到你喜欢的主题。 当然,你也可以自定义主题。 下面就是本人定制后的效果,备份一下,方便以后使用。 ### 效果 ![oh-my-posh](./oh-my-posh/oh-my-posh-1.png) ![oh-my-posh](./oh-my-posh/oh-my-posh-2.png) ### 配置文件 点击这里下载配置文件:[zhaobc.omp.json](./oh-my-posh/zhaobc.omp.json) ```json ``` --- --- url: 'https://zhaobc.site/posts/dev/python-playground.md' --- # 在线 Python Playground 简单记录一下近期抽空做的简单的 [Python Playground](https://play-py.zhaobc.site)。 Python Playground 主要是为了配合[笔记](../../notes/backend/python/)中的示例,基本保持跟 [Vue Playground](https://play.vuejs.org) 一样的逻辑。 大概思想是将 markdown 中的示例代码直接转为 Playground 中的代码,并可预览执行结果。 ::: details 变更日志 2025-08-03 * 增加查看和下载文件功能 * 重命名环境变量文件名 `env` => `.env` 2025-08-02 * 修复 scrollToView 导致整个页面滚动的问题 2025-08-01 * 增加关于对话框 * 增加 scrollToView,保持输出内容始终可见 2025-07-30 * 增加嵌入式模式时 **点击加载** 功能 2025-07-17 * 增加中断功能 2025-07-14 * 增加输入功能 2025-07-05 * 增加多文件 / 模块功能 ::: ## 示例 以下是一些简单的示例。 ### Hello world :::: preview 示例 :::playground#python 示例 @file main.py ```python import os print('Hello python playground!') print('env: ', os.environ) ``` @file .env ```shell a=b a1=b1 ``` @file requirements.txt ```txt pandas numpy ``` @settings ```json { "autoRun": true } ``` ::: :::: 上面的 `@settings` 用来传一些参数,比如 `autoRun` 自动运行。 :::warning 为了减少不必要的资源消耗,提高加载速度,嵌入式 (iframe方式) playground 默认不自动加载 pyodide 资源。 设置了 `autoLoad` 或 `autoRun` 时,才自动加载 pyodide 资源。 ::: ### Matplotlib 绘图 ::: tip 需加载相关依赖较慢,请耐心等待。 ::: :::: preview Matplotlib 绘图 :::playground#python Matplotlib @file main.py ```python import matplotlib.pyplot as plt import numpy as np xpoints = np.array([0, 6]) ypoints = np.array([0, 100]) plt.plot(xpoints, ypoints) plt.show() ``` @settings ```json { "autoRun": true, "outputMode": "preview" } ``` ::: :::: `settings` 里是自动运行和显示预览。 ### Plotly 图表 ::: tip 需加载相关依赖较慢,请耐心等待。 ::: :::: preview Plotly 图表 :::playground#python Plotly @file main.py ```python import plotly.graph_objects as go # 创建示例数据 x = [1, 2, 3, 4, 5] y = [10, 11, 12, 13, 14] # 创建折线图 fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=y, mode='lines+markers', name='数据线')) # 更新布局 fig.update_layout( title='互动折线图', xaxis_title='X 轴', yaxis_title='Y 轴', hovermode='closest' ) # 显示图表 fig.show() ``` @settings ```json { "autoRun": true, "outputMode": "preview" } ``` ::: :::: ### 多文件示例 :::: preview 多文件示例 :::playground#python 多文件示例 @file main.py ```python from test import print_func print('Hello python playground!') print_func('This message is from test.py') ``` @file test.py ```python def print_func(args): print(args) ``` ::: :::: ### 控制台输入示例 ::: warning 请注意 `CONSOLE` 标签中光标闪烁的地方! 按 `Enter` 键,或输入框失去焦点后会自动执行。 ::: :::: preview 控制台输入示例 :::playground#python 控制台输入示例 @file main.py ```python inp = input('请输入一些内容:') print('这是你输入的内容:', inp) ``` ::: :::: ## 开发背景 为了学习 Python (很遗憾没坚持下来。。) 希望能达到能及时看到代码执行结果的目的,希望加人一个 Python Playground。 最初调查后,发现 [Replit](https://replit.com/) 可达到效果,并且为主题封装了一个简易的 `Replit` 组件。 但是,好景不长,后续 Replit 不再提过可编辑,可运行的 Python 环境。 所以,换个方向,使用其他方式来达到效果,同时期望使用 Vue Playground 的方式,可传递代码,实用性强。 ## 开发过程 开发之前先搜了几个在线 Python 环境,但是不太符合要求,只好自己尝试开发了。 * [cliprun](https://cliprun.com/) 简洁易用,但是跟我自己的需求不符,不能传入代码。 * [Play Python](https://play-python.asyncmove.com/) 一个相对完善在线 Python 环境,而且跟 Vue Playground 类似,可以传入代码。 但,遗憾的是,尝试集成到主题时未成功。 * [toyai/python-playground](https://github.com/toyai/python-playground) 开源,跟 Vue Playground 类似,但是部署需要单独的服务器,而且 Python 版本有点旧。 鉴于以上种种原因,打算自己开发一个 Python Playground, 需要满足以下要求: * 像 Vue Playground 一样,可以传入代码 * 无需单独的服务器部署 * 基于 [Pyodide](https://pyodide.org/) 开发 * 集成 Python LSP, 提高用户体验 ### 第一步 搭框架 下载 [vue-repl](https://github.com/vuejs/repl),基于它进行魔改。 工作内容: * 去除 Vue, TypeScript, Import Map 等相关处理 * 去除 CodeMirror,仅保留 MonacoEditor * 新增 Python 语言及语法高亮 ### 第二步 集成 Pyodide 集成 Pyodide, 以便能运行 Python 代码 工作内容: * 编写 PyodideWorker,完成各种通信 由于能力水平有限,直接使用了 [cliprun](https://cliprun.com/) 的 PyodideWorker,然后稍微修改了一下。 在此感谢 [cliprun](https://cliprun.com/) 的出色工作! 也正是因为使用了 [cliprun](https://cliprun.com/) 的代码,所以可能涉及版权问题,不能随意公开代码,分发代码。 当然,自己水平也有限,也不太好意思公开代码。 ### 第三步 同步文件系统 Vue Playground 中,可以支持多个 `.vue`, `.js` 等文件,这次的 Play Playground 也期望能支持多文件。 为此,需要使用 Pyodide 的文件系统。 工作内容: * 修改 `store.ts` 文件,文件增删改时,使用 PyodideWorker,完成 Pyodide 文件系统的同步操作 ### 第四步 集成 Python LSP 通过上面的几步,已经可以完成 Python 代码执行,但是无法像 Vue Playground 那样有代码提示,代码校验。 为了解决这个问题,需要集成 Python LSP (Language Server Protocol)。 LSP Server 又有点犯难,期望直接在浏览器中能用的那种,不需要单独的服务器来部署。 最终选择了 Pyright 来做 LSP,主要使用了 [Pyright Playground](https://github.com/erictraut/pyright-playground/) 的代码。 工作内容: * 下载 [Pyright Playground](https://github.com/erictraut/pyright-playground/) 代码,然后将 server 端作为 LSP Server * 摘取 [Pyright Playground](https://github.com/erictraut/pyright-playground/) client 端部分代码,集成到 MonacoEditor 中 * 稍微修改 server 端代码,使其适配 Windows 环境 至此,本地开发中可以使用 Python LSP了,能完成代码提示,代码校验等。 ### 第五步 部署到 Vercel 由于本人博客主要部署在 Vercel 上,所以本次的 Python Playground 也打算继续部署到 Vercel 上(主要是穷,没钱买服务器 -\_-!)。 由于 [Pyright Playground](https://github.com/erictraut/pyright-playground/) 中的 server 端使用 express 作为 Server 来处理 API,本地没问题。 但是 Vercel 虽然也支持 express,但是试验中发现并不好用,估计是 express + typescript 的问题吧。。。 为了解决部署到 Vercel 中无法使用 LSP 的问题,打算改写 server 端,使用 Vercel Functions 来处理 API。 但是 Vercel Functions 的资料感觉很少,没看到怎么处理复杂 API 路径和参数的问题,后来偶然看到了一篇文章介绍了路径和参数的例子才知道怎么处理。 在此表示感谢,但是忘记记录地址了。。。 大概按以下的逻辑处理: ```txt api // vercel 要求,必须放到根目录下的 api 文件夹 │ hello.ts │ session.ts // 处理 /api/session 请求 │ status.ts │ └─session │ [sid].ts // 处理 /api/session/:sid 请求 │ └─[sid] completion.ts // 处理 /api/session/:sid/completion 请求 diagnostics.ts hover.ts ``` 最后,还需要调整 server 端的 API 处理,原来的 Pyright 中有 session 的管理, 最初创建一个 seesion,后续处理使用其 sessionId 接着处理,已有该 session 的话,直接使用,这样效率高。 但是,Vercel 中,不能执行长时间的请求,也不能使用全局变量,这导致了 session 管理失效,不能正常使用。 对策,Vercel 环境中,找不到原来的 session 时就新建一个,其实相当于每次都新建 session,这也导致了部署后,代码提示会相对比较慢。 ## 注意点 开发中不熟悉 Pyodide, 也不熟悉 MonoEditor, Web Worker 所以遇到了不少问题,这里简单记录一下。 ### SharedArrayBuffer 使用问题 * SharedArrayBuffer is not defined 在处理 `stdin` 中,用到了 SharedArrayBuffer,但是使用上有些限制,需要配置 `headers`。 MDN 文档:[安全需求](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements) 本地开发时,需要在 `vite.config.ts` 中配置一下。 ```ts title="vite.config.ts" server: { // SharedArrayBuffer is not defined headers: { 'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Opener-Policy': 'same-origin', //'Cross-Origin-Resource-Policy': 'same-site', // 嵌入方式仅在本站内使用 'Cross-Origin-Resource-Policy': 'cross-origin', // 嵌入方式仅在本站内使用 }, }, ``` 服务器端也需要做相应配置。 由于本人使用 vercel 部署,所以需要配置一下 `vercel.json`。 ```json title="vercel.json" { "headers": [ { "source": "/(.*)", "headers": [ { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }, { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, { "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" } ] } ] } ``` * `stdin` 处理 python `input` 在 python 中处理 `input` 时用到了两个输入流,一个是用来做控制的,一个是用来处理数据的。 由于 `input` 会有用户交互,所以用到了 `Atomics.wait` 来做中断,等待用户输入完后再继续处理。 Pyodide 文档: [设置中断](https://pyodide.org/en/stable/usage/keyboard-interrupts.html#setting-up-interrupts) python repl 中有以下代码。 ```js inputBuffer = new SharedArrayBuffer(4) inputTextBuffer = new SharedArrayBuffer(1024) inputView = new Int32Array(inputBuffer) inputTextView = new Uint8Array(inputTextBuffer) Atomics.store(inputView, 0, 0) Atomics.wait(inputView, 0, 0) ``` 前台处理中有以下代码。 ```js // 返回 Uint8Array const encoder = new TextEncoder() const inputData = encoder.encode(el.value) const inputTextBuff = new Uint8Array(buff) // buff 是 repl 创建的 SharedArrayBuffer(1024) // 将用户数据放到 inputTextBuff 中 for (let i = 0; i < inputData.length; i++) { inputTextBuff[i] = inputData[i] } inputTextBuff[inputData.length] = 0 // 使用 Atomics 恢复程序 const inputView = new Int32Array(inputBuff) // inputBuff 是 repl 创建的 SharedArrayBuffer Atomics.store(inputView, 0, 1) Atomics.notify(inputView, 0) ``` repl 和 前台的这两个输入流的类型必须一致。 ### 跨域设置问题 为了使用 `SharedArrayBuffer` 主站设置了以下 headers: * `Cross-Origin-Opener-Policy` (*COOP*) * `Cross-Origin-Embedder-Policy` (*COEP*) * `Cross-Origin-Resource-Policy` (*CORP*) ```txt Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Resource-Policy: same-site ``` 其中,设置 `Cross-Origin-Resource-Policy` 是为了嵌入的 `iframe` 里的 Python Playground,能正常使用。 除此之外,还设置了 `iframe` 的属性 `allow="cross-origin-isolated"`。 由于设置了上面 header 会导致主站的一些 `js`, `css` 等加载有问题,需要加上 `crossorigin` 属性。 [避免 COEP 阻塞 CORS](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy#%E9%81%BF%E5%85%8D_cors_%E9%98%BB%E5%A1%9E_coep) ::: warning 更新:2022 年 5 月 Cross-Origin-Embedder-Policy: credentialless 已发布,自 Chrome 96 起便可在 Chrome 中使用。 该值允许浏览器通过发送不包含凭据(例如 Cookie)的请求来加载不使用跨源资源政策 (CORP) 的跨源资源。这有助于开发者更轻松地采用跨源隔离。 [使用 COEP 无凭据加载不含 CORP 标头的跨源资源](https://developer.chrome.google.cn/blog/coep-credentialless-origin-trial?hl=zh-cn) 您也可以使用以下两个标头来启用跨源隔离: ```txt Cross-Origin-Embedder-Policy: credentialless Cross-Origin-Opener-Policy: same-origin ``` 您可以在以下演示中尝试各种标头选项: ::: clarity 设置: * `Content-Security-Policy` (*CSP*) * [Clarity Content Security Policy](https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-csp) * [CSP:default-src](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/default-src#%E8%AF%AD%E6%B3%95) ```txt Content-Security-Policy: default-src 'self' https://*.clarity.ms https://c.bing.com 'unsafe-inline'; ``` ::: warning 除了 clarity 外,可能用到了其他的外部资源,比如百度统计, CDN 等,如果设置了 CSP,内容不全的话,反而会阻塞外部资源。 ::: 参考: * [关于启用跨域隔离的指南](https://web.developers.google.cn/articles/cross-origin-isolation-guide?hl=zh-cn) * [使用 COEP 无凭据加载不含 CORP 标头的跨源资源](https://developer.chrome.google.cn/blog/coep-credentialless-origin-trial?hl=zh-cn) ## 类似功能 其他网站已提供现成的功能,可以直接使用。本次开发中也借鉴了它们,再次表示感谢。 * [Vue Playground](https://play.vuejs.org) * [cliprun](https://cliprun.com/) Online Python Compiler, IDE, and Interpreter * [Play Python](https://play-python.asyncmove.com/) Run code, collaborate in real-time. * [Pyodide](https://pyodide.org/) Python distribution for the browser and Node.js based on WebAssembly --- --- url: 'https://zhaobc.site/posts/dev/remove-git-commit-history.md' --- # git 中如何删除指定 commit 之前的所有 commit 履历 在使用 Git 进行版本控制时,有时候我们需要删除指定 commit 之前的所有历史。这可能是因为我们需要重新开始一个新的项目,或者因为我们发现了一些错误,需要修复历史记录。 或者,forked 了一个项目,不想保留它原历史记录,需要重新开始一个新的项目。 在 Git 中删除指定 commit 之前的所有历史,可以通过将该 commit 设为新的根提交来实现。以下是具体步骤: :::tip 本文方法由 DeepSeek 生成,这里仅是为了记录一下,方便自己查阅。 ::: 在Git中删除指定commit之前的所有历史,可以通过将该commit设为新的根提交来实现。以下是具体步骤: *** ## **方法一:使用 `git rebase`(适用于线性历史)** 1. **找到目标commit的哈希值**\ 使用 `git log` 确定你要保留的最早commit的哈希(例如 `abc123`)。 2. **启动交互式rebase** ```bash git rebase -i --root ``` * 这会从仓库的初始提交开始编辑历史。 3. **编辑提交列表**\ 在打开的编辑器中: * **删除目标commit(`abc123`)之前的所有行**,只保留 `abc123` 及其之后的commit。 * 保存并退出编辑器。 4. **解决可能的冲突**\ 如果发生冲突,按提示解决后继续: ```bash git rebase --continue ``` *** ## **方法二:使用 `git filter-branch`(适用于复杂历史)** 1. **将目标commit设为根提交** ```bash git filter-branch --parent-filter ' if [ $GIT_COMMIT = <目标commit哈希> ]; then echo "" else cat fi ' -- --all ``` * 替换 `<目标commit哈希>` 为实际的哈希值(如 `abc123`)。 * 此命令将目标commit的父提交设为空,使其成为新的根提交。 2. **清理仓库** ```bash git reset --hard git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d git reflog expire --expire=now --all git gc --aggressive --prune=now ``` *** ## **方法三:使用孤儿分支(最彻底,但需手动操作)** 1. **创建孤儿分支** ```bash git checkout --orphan new-branch <目标commit哈希> ``` * 这会基于目标commit创建一个无历史的新分支。 2. **提交当前状态** ```bash git commit -m "新的根提交" ``` * 将当前文件状态作为新分支的第一个提交。 3. **删除旧分支并重命名** ```bash git branch -D main # 删除旧分支(如main) git branch -m main # 重命名新分支为main ``` 4. **强制推送到远程** ```bash git push -f origin main ``` *** ## **注意事项** * **备份仓库**:操作前务必备份,历史重写不可逆。 * **协作影响**:强制推送(`git push -f`)会覆盖远程历史,需通知所有协作者重新克隆仓库。 * **敏感信息**:若历史中包含敏感数据,需彻底清除(推荐使用 `git filter-repo`)。 选择适合你场景的方法,谨慎操作! 本人使用的是方法二,经验证可满足需求。 --- --- url: 'https://zhaobc.site/posts/dev/umami-docker.md' --- # 内网部署 umami 内网部署了一个 WiKi,为了简便,使用的 VuePress + [Hope 主题](https://theme-hope.vuejs.press/)。 估计是内部试用版的原因,基本上没人看。更主要是除了我自己,也没人往上放东西。。。 不过,本着网站功能齐全的目标,想者加一个统计功能,顺带看看是不是真的只有我一个人在用。。。 ## 安装 本着能省则省的原则,首先看了下是否有 docker 版。挺好,有 docker 版,遂采用了 docker 版。 docker compose 文件参考了这里:[使用 Docker 部署 Umami 网站访问统计系统](https://homulilly.com/post/use-docker-deploy-umami.html) docker 镜像下载折腾了不少时间(没可用的镜像),这里略去不表。。。 启动完了后,看到了自己映射的端口,同时也开放了该端口,但仍然不能访问。 然后使用 `docker logs` 查看了一下,虽然最后显示成功,但是中间有个下载出错了。 由于是内网环境,该服务器没法联网。遂上网搜了一下有无类似 `SCSS` 之类的设置 url 的方式。 找到了一个解决方案:[解决 prisma 国内部署失败](https://blog.bg7zag.com/2852) 遂按照其方式,改造了一下 compose 文件。 ```yaml{15-18} services: umami: image: umami-software/umami:postgresql-latest ports: - 11030:3000 environment: # 使用自己的密码替换 DATABASE_URL: postgresql://umami:@db:5432/umami DATABASE_TYPE: postgresql # 生成一段随机字符串替换 # openssl rand -base64 32 APP_SECRET: NVHYhP3wYaMM14d8hGFfR3aCCmsSi2+cQ/LKLfV67V0= # 下载 prisma 引擎失败 # https://blog.bg7zag.com/2852 PRISMA_QUERY_ENGINE_LIBRARY: /tmp/prisma/libquery_engine.so.node # PRISMA_QUERY_ENGINE_BINARY: /tmp/prisma/query-engine PRISMA_SCHEMA_ENGINE_BINARY: /tmp/prisma/schema-engine # PRISMA_FMT_BINARY: /tmp/prisma/prisma-fmt volumes: - ./prisma/:/tmp/prisma depends_on: db: condition: service_healthy init: true restart: always healthcheck: test: [CMD-SHELL, 'curl http://localhost:3000/api/heartbeat'] interval: 5s timeout: 5s retries: 5 db: image: postgres:16-alpine environment: POSTGRES_DB: umami POSTGRES_USER: umami # 与前面生成的 一致 POSTGRES_PASSWORD: volumes: # 数据文件保存在 docker-compose.yaml 所在目?下的 data 文件? - ./data:/var/lib/postgresql/data restart: always healthcheck: test: [CMD-SHELL, 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'] interval: 5s timeout: 5s retries: 5 ``` 由于下载链接不固定,所以实际做法是每次只让一个变量有效,然后得到它的链接手动下载下来,放到 volumes 下面。 重启多次看效果,哪里出错了改哪里。 经试验,仅需要两个变量即可。其中,`schema-engine` 需要加上执行权限。 ## 问题 使用过程中,发现浏览量特别大,而实际并没有那么多。起初不知道什么问题,后来发现浏览量是根据 hash 统计的,同一个页面里 hash 变了的话,浏览量就会增加。 由于部署的 vuepress 并不是单页应用,所以不需要按 hash 统计浏览量。 有两种解决方式,一个是修改 [环境变量](https://umami.is/docs/environment-variables) `REMOVE_TRAILING_SLASH = 1`。 另一个是修改 [跟踪配置](https://umami.is/docs/tracker-configuration) `data-exclude-hash="true"`。 --- --- url: 'https://zhaobc.site/posts/devops/run-algolia-docsearch-from-code.md' --- # 运行自己的 algolia 使用 Algolia DocSearch 可以实现全文检索,对于搜索信息特别方便。 参考 Hope 提供的[使用 @vuepress/docsearch](https://vuepress-theme-hope.github.io/v2/zh/guide/feature/search.html#%E4%BD%BF%E7%94%A8-vuepress-docsearch)。 提交 DocSearch 申请可能需要等待一段时间(我提交申请到最终下来花了 4 天),在申请下来之前,也体验下效果,自己使用旧版的方式爬取了内容。 申请下来之后,使用 Crawler 更方便,所以之前的旧方式就不再用了。为了保留记录,写成一篇文章。 旧方式使用 Github Actions 做定期爬虫: ```yaml name: Algolia Docsearch Action on: push: branches: - main paths: - 'docs/algolia-config-hope.json' # 当配置文件有变时,执行爬虫程序 schedule: - cron: '0 18 * * *' # Runs at 18:00 UTC every day, 02:00 UTC+8 jobs: algolia: runs-on: ubuntu-latest name: algolia-crawler steps: - uses: actions/checkout@v2 - name: crawling # the `uses' attribute must be a path, a Docker image, or owner/repo@ref uses: darrenjennings/algolia-docsearch-action@master with: algolia_api_key: ${{ secrets.ALGOLIA_API_KEY }} algolia_application_id: ${{ secrets.ALGOLIA_APPLICATION_ID }} # needs to be inside $GITHUB_WORKSPACE from actions/checkout step file: docs/algolia-config-hope.json # 配置文件 ``` --- --- url: 'https://zhaobc.site/posts/devops/skip-ci-for-vercel.md' --- # 取消 Vercel 发布 在 Github Actions 中,我们可以选择性地 取消 workflow 的执行,比如 在 git message 中 填写 \[skip ci] 或者 \[ci skip] 即可达到目的。 但是,在 vercel 中,这种方法不起作用。 搜索了一下资料,发现 vercel 中也可以自定义来实现。 * [How do I use the "Ignored Build Step" field on Vercel?](https://vercel.com/support/articles/how-do-i-use-the-ignored-build-step-field-on-vercel) * [Skip deploys for commits that have \[skip-ci\] in the name](https://github.com/vercel/vercel/discussions/5765) ```bash bash -c 'echo "$VERCEL_GIT_COMMIT_MESSAGE" | grep -iE "\[skip ci\]|\[ci skip\]"' # 单独忽略 docs-ci # bash -c 'echo "$VERCEL_GIT_COMMIT_MESSAGE" | grep -iE "\[skip (docs-)?ci\]|\[(docs-)?ci skip\]"' ``` 也可以按照上面写的方式,写一个脚本来做。下面是为了留下记录,所以写了一个比较繁琐的脚本: ```bash #!/bin/bash # How do I use the "Ignored Build Step" field on Vercel? # https://vercel.com/support/articles/how-do-i-use-the-ignored-build-step-field-on-vercel # Skip deploys for commits that have [skip-ci] in the name # https://github.com/vercel/vercel/discussions/5765 # 可以在"Ignored Build Step"中直接使用: bash -c 'echo "$VERCEL_GIT_COMMIT_MESSAGE" | grep -iE "\[skip ci\]|\[ci skip\]"' echo "VERCEL_GIT_COMMIT_REF: $VERCEL_GIT_COMMIT_REF" echo "VERCEL_GIT_COMMIT_MESSAGE: $VERCEL_GIT_COMMIT_MESSAGE" # grep通过返回一个状态值来说明搜索的状态, # 如果模板搜索成功,则返回0,如果搜索不成功,则返回1, # 如果搜索的文件不存在,则返回2。 # # 过滤message的第一行是否包含[skip ci]或者[ci skip] # echo "$VERCEL_GIT_COMMIT_MESSAGE" | head -n 1 | grep -iE "\[skip ci\]|\[ci skip\]" echo "$VERCEL_GIT_COMMIT_MESSAGE" | grep -iE "\[skip ci\]|\[ci skip\]" # echo "$VERCEL_GIT_COMMIT_MESSAGE" | grep -iE "\[skip (docs-)?ci\]|\[(docs-)?ci skip\]" if [[ "$?" == "0" ]] ; then # Don't build echo "🛑 - Build cancelled" exit 0; else # Proceed with the build echo "✅ - Build can proceed" exit 1; fi ``` --- --- url: 'https://zhaobc.site/posts/devops/vuepress-articels-sort.md' --- # VuePress 文章排序问题 ## 现象 开发模式下,文章排序没问题,CI(Github Actions/Vercel) 发布后文章排序不对,并且时间线也不对。 ## 调查 查看 `vuepress-theme-hope` 后,发现默认按 `frontmatter.date` 和 git 创建时间排序。 `posts` 下的文章都指定了 `date`,没有问题;而 `notes` 下的笔记都没有指定 `date`,想定按 git 创建日期显示,但是貌似按 git 最后更新时间,或者发布时间显示了。 从而推断是 CI 上的问题。 ## 对策 由于本工程采用了两个 CI 同时发布,所以需要解决两条路。 * Github Action 发布 Github Pages * Vercel CI 发布到 Vercel ### Github Actions 查看 Github Actions 发现 `actions/checkout` 默认只取最后一次履历,指定 `fetch-depth: 0` 可以解决该问题。 ### Vercel Vercel 控制面板上没有地方指定 `git clone` 相关内容,查阅文档后,发现 Vercel 默认执行 `git clone --depth=10 (...)`。 > During this time, Vercel performs a "shallow clone" on your Git repository using the command `git clone --depth=10 (...)` and fetches ten levels of git commit history. This means that only the latest ten commits are pulled and not the entire repository history. 但是官方文档里没提到如何更改这个设置。。。 一番搜索后,发现了两种方案: [How to unshallow repo?](https://github.com/vercel/vercel/discussions/5737) 1. 使用 Github Action 的 `deploy-to-vercel-action` 发布到 Vercel 2. 通过 Vercel 的环境变量 `VERCEL_DEEP_CLONE: true` 第一种感觉有点怪怪的感觉,第二种更简单也更符合我的需求,所以采用了第二种方案。 --- --- url: 'https://zhaobc.site/posts/docker/command.md' --- # Docker 常用命令 官方文档: ## docker info Display system-wide information ## docker inspect Return low-level information on Docker objects ```bash docker inspect [OPTIONS] NAME|ID [NAME|ID...] ``` ## docker ps 查看运行的 docker 容器 ## docker images 查看 docker 镜像 ## docker rm Remove one or more containers ```bash docker rm [OPTIONS] CONTAINER [CONTAINER...] ``` ```bash # 删除所有已经停止的容器 docker rm $(docker ps -a -q) ``` ## docker rmi Remove one or more images ```bash docker rmi [OPTIONS] IMAGE [IMAGE...] ``` ## docker start Start one or more stopped containers ```bash docker start [OPTIONS] CONTAINER [CONTAINER...] ``` ## docker stop Stop one or more running containers ```bash docker stop [OPTIONS] CONTAINER [CONTAINER...] ``` ## docker restart Restart one or more containers ```bash docker restart [OPTIONS] CONTAINER [CONTAINER...] ``` ## docker port List port mappings or a specific mapping for the container ```bash docker port CONTAINER [PRIVATE_PORT[/PROTO]] ``` ## docker run Run a command in a new container ```bash docker run [OPTIONS] IMAGE [COMMAND] [ARG...] ``` ## docker exec Run a command in a running container ```bash docker exec [OPTIONS] CONTAINER COMMAND [ARG...] ``` 进入 docker 容器 ```bash docker exec -it container_name /bin/bash docker exec -it ubuntu_bash bash ``` 执行 docker 容器里的命令 ```bash docker exec -ti my_container sh -c "echo a && echo b" # 解压文件 docker exec -it container_name tar -vxf a.tar # 使用docker容器的root用户执行 docker exec -u root ``` ## docker cp Copy files/folders between a container and the local filesystem ```bash docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH ``` ## docker commit Create a new image from a container’s changes ```bash docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] ``` ## docker build Build an image from a Dockerfile ```bash docker build [OPTIONS] PATH | URL | - ``` --- --- url: 'https://zhaobc.site/posts/docker/deploy-waline.md' --- # Waline 部署 [Waline](https://waline.js.org/) 是一款简洁、安全的评论系统。 ## 独立部署 本次使用 Docker 独立部署,以便方便内网使用。 ## 部署脚本 ```yml # docker-compose.yml version: '3' services: waline: container_name: waline image: lizheming/waline:latest restart: always links: - postgresql depends_on: - postgresql ports: - 8360:8360 volumes: - /srv/waline/data:/app/data environment: TZ: 'Asia/Shanghai' SITE_NAME: 'Site Name' SITE_URL: 'http://www.zhaobc.site' #SECURE_DOMAINS: 'example.com' AUTHOR_EMAIL: 'zhbchwin@163.com' PG_DB: waline PG_USER: waline PG_PASSWORD: xxxxxx PG_HOST: postgresql PG_PORT: 5432 PG_PREFIX: 'wl_' PG_SSL: false postgresql: container_name: postgres image: postgres:latest restart: always ports: - 5460:5432 volumes: - /srv/waline/db/pgdata:/var/lib/postgresql/data - /srv/waline/db/initdb:/docker-entrypoint-initdb.d environment: POSTGRES_USER: waline POSTGRES_PASSWORD: xxxxxx POSTGRES_DB: waline ``` --- --- url: 'https://zhaobc.site/posts/edu/chazi.md' --- # 查字 ::: info 查询汉字拼音,笔画,笔顺等信息。 打印模式下可以生成字帖。 ::: ## 使用 ## 更多 --- --- url: 'https://zhaobc.site/posts/edu/learn-hanzi.md' --- # 学习汉字 ## 背景 家有小学生,为了让这位小学生有事做,准备给他布置点作业,文库搜到了一个别人整理的小学一年级下的汉字生词,觉得挺好。 但是,下载要收费。。。遂自己尝试做了个简单的学习汉字的小程序,放这里给孩他妈看一下效果。 ## 功能 原来想用 [`hanzi-writer`](https://hanziwriter.org/) 实现,但是偶然发现 [`cnchar`](https://theajack.github.io/cnchar/) 更符合要求,所以用 [`cnchar`](https://theajack.github.io/cnchar/) 实现了,在此感谢~ 展示汉字的拼音,部首,结构,笔画,以及生成练字帖功能。 同时,可以展示汉字发音,书写笔画,以及汉字临摹功能。 另外,也可以打印出来,方便孩子学习。 ::: warning 网页查看跟打印是两种风格,可查看网页打印预览。 * 网页是为了展示发音,笔顺动画,大字体显示笔顺 * 打印模式是为了方便练字 ::: :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 ::: ## 使用方法 封装了一个简单的 `Hanzi` 组件,使用方式如下: ```vue ``` ## 属性 `zi` 是必须属性,其他都是非必需属性。 其他属性未指定时,使用 `cnchar` 字库里默认的内容显示,如果有不符合自己的需求时,可传入自己指定的属性用于显示。 ### `zi` 用于指定单个汉字。 ### `pinyin` 用于指定拼音。 ### `bushou` 用于指定汉字的部首。 ### `jiegou` 用于指定汉字的结构。 ### `bihuashu` 用于指定汉字的笔画数。 ### `bihua` 它是一个数组,用于指定汉字的笔画。 ### `zuci` 它是一个数组,用于指定汉字的组词。 ### `chengyu` 它是一个数组,用于指定汉字的成语。 ## 简单效果 ## 查字 --- --- url: 'https://zhaobc.site/posts/edu/xiaoxue-yuwen-1-xia-hanzi-kapian.md' --- # 一年级语文下册识字卡片 :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 卡片设计参考自己购买的实体卡片。 ::: ## 待补充 欢迎有需要的人补充(评论或邮件)~~~ 格式如下: 为了更好的配合教材,请按 `方式 2` 补充,自动生成的拼音(多音字),组词,成语之类的不是特别好。 更多详细信息,请参考 [学习汉字](./learn-hanzi.md)。 ```vue ``` --- --- url: 'https://zhaobc.site/posts/edu/xiaoxue-yuwen-1-xia.md' --- # 一年级语文下册 :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 点击打印可作为练字帖使用,打印时建议缩放 70%。 ::: ## 1.春夏秋冬 ## 2.姓氏歌 ## 3.小青蛙 ## 4.猜字谜 ## 5.园地一 ## 待补充 欢迎有需要的人补充(评论或邮件)~~~ 格式如下: 为了更好的配合教材,请按 `方式 2` 补充,自动生成的拼音(多音字),组词,成语之类的不是特别好。 更多详细信息,请参考 [学习汉字](./learn-hanzi.md)。 ```vue ``` --- --- url: 'https://zhaobc.site/posts/edu/xiaoxue-yuwen-2-shang-2.md' --- # 二年级语文上册 (二) :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 点击打印可作为练字帖使用,打印时建议缩放 70%。 ::: ## 1.场景歌 ## 2. 树之歌 ## 3. 拍手歌 ## 4. 田家四季歌 ## 4. 曹冲称象 ## 5. 玲玲的画 ## 6. 一封信 ## 7. 妈妈睡了 ## 待补充 欢迎有需要的人补充(评论或邮件)~~~ 格式如下: 为了更好的配合教材,请按 `方式 2` 补充,自动生成的拼音(多音字),组词,成语之类的不是特别好。 更多详细信息,请参考 [学习汉字](./learn-hanzi.md)。 ```vue ``` --- --- url: 'https://zhaobc.site/posts/edu/xiaoxue-yuwen-2-shang-3.md' --- # 二年级语文上册 (三) :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 点击打印可作为练字帖使用,打印时建议缩放 70%。 ::: ## 8. 古诗二首 ## 9. 黄山奇石 ## 待补充 欢迎有需要的人补充(评论或邮件)~~~ 格式如下: 为了更好的配合教材,请按 `方式 2` 补充,自动生成的拼音(多音字),组词,成语之类的不是特别好。 更多详细信息,请参考 [学习汉字](./learn-hanzi.md)。 ```vue ``` --- --- url: 'https://zhaobc.site/posts/edu/xiaoxue-yuwen-2-shang-hanzi-kapian.md' --- # 二年级语文上册 卡片 :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 卡片设计参考自己购买的实体卡片。 ::: ## 1.小蝌蚪找妈妈 ## 2. 我是什么 ## 3. 植物妈妈有办法 ## 待补充 欢迎有需要的人补充(评论或邮件)~~~ 格式如下: 为了更好的配合教材,请按 `方式 2` 补充,自动生成的拼音(多音字),组词,成语之类的不是特别好。 更多详细信息,请参考 [学习汉字](./learn-hanzi.md)。 ```vue ``` --- --- url: 'https://zhaobc.site/posts/edu/xiaoxue-yuwen-2-shang-kapian-2.md' --- # 二年级语文上册卡片 (二) :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 点击打印可作为练字帖使用,打印时建议缩放 70%。 ::: ## 1.场景歌 ## 2. 树之歌 ## 3. 拍手歌 ## 4. 田家四季歌 ## 4. 曹冲称象 ## 5. 玲玲的画 ## 6. 一封信 ## 7. 妈妈睡了 ## 待补充 欢迎有需要的人补充(评论或邮件)~~~ 格式如下: 为了更好的配合教材,请按 `方式 2` 补充,自动生成的拼音(多音字),组词,成语之类的不是特别好。 更多详细信息,请参考 [学习汉字](./learn-hanzi.md)。 ```vue ``` --- --- url: 'https://zhaobc.site/posts/edu/xiaoxue-yuwen-2-shang.md' --- # 二年级语文上册 :::info 为了发音迅速,优先使用浏览器自带的特性实现,推荐使用最新的 Edge 浏览器。 点击打印可作为练字帖使用,打印时建议缩放 70%。 ::: ## 1.小蝌蚪找妈妈 ## 2. 我是什么 ## 3. 植物妈妈有办法 ## 语文园地一 ## 待补充 欢迎有需要的人补充(评论或邮件)~~~ 格式如下: 为了更好的配合教材,请按 `方式 2` 补充,自动生成的拼音(多音字),组词,成语之类的不是特别好。 更多详细信息,请参考 [学习汉字](./learn-hanzi.md)。 ```vue ``` --- --- url: 'https://zhaobc.site/posts/home/tv.md' --- # 客厅电视尺寸推荐 本文内容引用自中国电子视像行业协会发布的 T/CVIA-56-2016 《客厅平板电视主流尺寸推荐规范》。 ## 人眼最小分辨角 通常将人眼的最小分辨角设为 1 弧分,即 1/3400。 ## 人眼视度 好的视觉临感:垂直视角为 20 度,水平视角为 36 度。 ## 平板电视最佳观看距离 平板电视的最佳观看距离是指在此观看距离上,屏幕一个像素对人眼的张角正好不大于人眼的最小分辨角,不会产生颗粒感。 同时观看视角约在 36 度附近,有强临场感。 视角太大,会因频繁转动眼球造成疲劳感。 * 观看者正对屏幕,且双眼与电视屏幕中心在同一水平高度 * 视角约在 36 度附近,有强临场感 平板电视机的最佳观看距离可表示为: 颗粒感观看距离: $Lopl= \frac{3400}{Rv} \times H$ 上式中,`Rv` 为屏幕垂直方向分辨率,4K 屏幕为 2160,8K 屏幕为 4320;`H` 为屏幕有效显示区域高度,单位为米。 临场感观看距离: $Lop2=H \times \frac{8}{9 \times \tan (18°)}$ (屏幕宽高比为 16:9) 推荐观看距离: $Lop1\5.0 | **98**, **110**, 130 及以上 | | 注:粗体为最优推荐尺寸。 --- --- url: 'https://zhaobc.site/posts/n1/upgrade-omv.md' --- # 更新 OpenMediaVault 很久之前折腾过的 N1 盒子,一直默默运行着,用处不大所以一直没再折腾,最近孩子听课,在家看孩子,想看电视时,拔掉 USB 运行原来的盒子系统,用来做电视盒子。 摸了几次 N1 盒子后才想着是不是该升级一下了,于是又开始了折腾。。 ## 简介 之前折腾 newifi-d3 时逛恩山论坛,看到有人折腾 N1 盒子,看着能运行 linux 系统,还能做 NAS 所以从 PDD 上买了一个,同时也从海鲜市场上买了一个移动硬盘,被骗了一个相当老的影片,运行时间长,速度还慢。。 经过折腾,安装了 armbian, docker, nextcloud,理论上可做下载机,小型 NAS,但是没怎么用,偶尔用来做 win 和 macOS 的中转站。 安装 armbian 的过程已经不记得了,网上可搜到很多。 ## 遇到的问题和解决办法 ### omv 源 omv 比较慢,可以更换清华源: 但是使用 omv-env 命令时报错了,后来手动改的 /etc/apt/source.list 文件。 但是配置完了之后执行 apt update 和 apt upgrade 时报错,导致后面一直升级不成功,再后来又可以 使用 omv-env 命令了,重做了一遍,注意要做签名验证。 过程中遇到的错误: Package openmediavault is not configured yet. 解决办法: 1. 清空或删除 /var/lib/dpkg/info/ 2. 执行 apt clean apt autreomove 3. 重新执行 apt upgrade ### Wifi 网络不可用 升级完 omv 后,登录进去提示要重启系统,悲催的是重启完后连不上 N1 了,插拔了几次电源后也不行。 以为是卡在启动了,然后给 N1 插上键盘,插上显示器看了一下,启动正常,可以本机登录,但有个错误写着网络有问题。 看路由器上也没显示 N1 连接 Wifi,执行 armbian-config 看了一下,发现居然没有了 Wifi 选项,试了 nmtui 添加 Wifi 但是还是不行。 按网上资料准备手动填写 /etc/network/interfaces 时,发现上面写着不要手动修改, omv 会覆盖掉,于是准备进 omv 设置网络。 接着给 N1 插上 网线,然后看到 路由器上 N1 上线了,根据 IP 进入了 omv,找到 网络,发现又 WiFi 配置,但是不能点应用,于是修改了下有线网络,点击应用后,稍等一会终于连上 WifFi 了。 ### 升级 PHP 升级了一些软件后发现 nextcloud 连不上了,重定向后的地址不对,于是想着干脆升级一下得了。 查询了一下看到官方推荐 PHP8 ,看着现在的 PHP7.3 就想着升级一下。 根据下面的资料,准备升级 PHP: * [Install PHP 8.1 In Debian 9, 10, and Debian 11](https://www.linuxandubuntu.com/home/install-php-8-1-in-debian-9-10-and-debian-11) * [Install PHP 8 on Debian 10/Debian 9 – How to do it](https://bobcares.com/blog/install-php-8-on-debian-10/) ```bash sudo apt install apt-transport-https lsb-release ca-certificates wget -y sudo wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg # wget -qO – https://packages.sury.org/php/apt.gpg | sudo apt-key add – sudo sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' sudo apt update sudo apt upgrade -y ``` 又报错了: Package php7.3-common is not configured yet. 解决: ```bash sudo rm /var/lib/dpkg/info/php* sudo apt -f install # Then again for libapache2-mod-php sudo rm /var/lib/dpkg/info/libapache2-mod-php* sudo apt -f install ``` 终于升级成功,但是 php -v 还是 7.3 ,发现两个版本共存了。 ```bash ls -la /usr/bin | grep php ``` ### 切换 PHP 版本 查看 php 又多个,执行 php8.1 -v 是没问题的,需要修改默认的版本,默认的版本 /etc/alternatives/php 根据以下资料做修改: * [update-alternatives——linux 软件版本管理命令](https://cloud.tencent.com/developer/article/1532283) * [How to Switch between Multiple PHP Version on Ubuntu](https://tecadmin.net/switch-between-multiple-php-version-on-ubuntu/) * [Ubuntu 下轻松实现 PHP 多版本共存](https://www.mf8.biz/ubuntu-multip-php/) ```bash update-alternatives --install /usr/bin/php php /usr/bin/php8.1 200 update-alternatives --install /usr/bin/php-cgi php-cgi /usr/bin/php-cgi8.1 200 update-alternatives --install /usr/bin/phar phar /usr/bin/phar8.1 200 update-alternatives --install /usr/bin/phar.phar phar.phar /usr/bin/phar8.1.phar 200 # 查看 update-alternatives --display php # 配置 update-alternatives --config php ``` 设置好上面的 update-alternatives 后,查看 php -v 已经切换为 php8.1。 切换 apache 的 php: ```bash sudo a2dismod php7.3 sudo a2enmod php8.1 sudo service apache2 restart ``` ## 升级 NextCloud ### command line ```bash sudo -u www-data php /var/www/html/nextcloud/updater/updater.phar ``` ### Installer 使用 setup-nextcloud.php,放到 /var/www/html 下,并修改 http server 权限: ```bash chown -R www-data:www-data /var/www/html/ ``` 报找不到相应的 php module,但是这些 module 已经安装了。。 /etc/php/8.1/mods-available 下 比 /etc/php/7.3/mods-available 下少很多。 接着需要配置 php.ini,php.ini 有多处: * /etc/php/7.3/apache2/php.ini * /etc/php/7.3/fpm/php.ini * /etc/php/7.3/cgi/php.ini * /etc/php/7.3/cli/php.ini 但 php8.1 中没有 php.ini,上面的 7.3 的 php.ini 中,虽然有写明 extension=xxx.so 这样,但是都是注释掉的状态,extension\_dir 也没有设置。 8.1 默认 extension\_dr: ```bash php -i | grep -i extension_dir ``` 启用哪些模块,可以在 php.ini 中定义,也可以使用命令: [How to enable or disable PHP Modules on Ubuntu](https://tecadmin.net/enable-disable-php-modules-ubuntu/) * phpenmod – Used to enable modules in PHP * phpdismod – Used to disable modules in PHP * phpquery – Used to view status of modules of PHP ```bash ### Syntax phpenmod MODULE_NAME ### Enable mbstring php module phpenmod mbstring ### Syntax phpenmod -v ### Enable module for specific php version phpenmod -v 5.6 mbstring phpenmod -v 7.4 mbstring ### Syntax phpenmod -s ### Enable module for specific SAPI phpenmod -s cli mbstring phpenmod -s fpm mbstring phpenmod -s apache2 mbstring ``` 查看 7.3 开启的模块: php7.3 -v,然后根据需要开启 php8.1 的模块 使用 phpenmod 前,需要先在 /etc/php/8.1/mods-available 下建 对应的 .ini 文件。 为了省事,把 /etc/php/7.3/mods-available/\* 复制到 /etc/php/8.1/mods-available 下。 然后重启 apache2, systemctl restart apache2. --- --- url: 'https://zhaobc.site/posts/other/columns-layout-demo.md' --- # 台湾问题与新时代中国统一事业 ## 前言 {.span-all .center} 解决台湾问题、实现祖国完全统一,是全体中华儿女的共同愿望,是实现中华民族伟大复兴的必然要求,是中国共产党矢志不渝的历史任务。中国共产党、中国政府和中国人民为此进行了长期不懈的努力。 中共十八大以来,中国特色社会主义进入新时代。在以习近平同志为核心的中共中央坚强领导下,中国共产党和中国政府积极推进对台工作理论和实践创新,牢牢把握两岸关系主导权和主动权,有力维护台海和平稳定,扎实推进祖国统一进程。但一个时期以来,台湾民进党当局加紧进行“台独”分裂活动,一些外部势力极力搞“以台制华”,企图阻挡中国实现完全统一和中华民族迈向伟大复兴。 中国共产党团结带领全国各族人民长期奋斗,如期全面建成小康社会、实现第一个百年奋斗目标,开启全面建设社会主义现代化国家、向第二个百年奋斗目标进军新征程。中华民族迎来了从站起来、富起来到强起来的伟大飞跃,实现中华民族伟大复兴进入了不可逆转的历史进程。这是中国统一大业新的历史方位。 中国政府于 1993 年 8 月、2000 年 2 月分别发表了《台湾问题与中国的统一》、《一个中国的原则与台湾问题》白皮书,全面系统阐述了解决台湾问题的基本方针和有关政策。为进一步重申台湾是中国的一部分的事实和现状,展现中国共产党和中国人民追求祖国统一的坚定意志和坚强决心,阐述中国共产党和中国政府在新时代推进实现祖国统一的立场和政策,特发布本白皮书。 ## 台湾是中国的一部分不容置疑也不容改变 {.span-all .center} 台湾自古属于中国的历史经纬清晰、法理事实清楚。不断有新的考古发现和研究证明海峡两岸深厚的历史和文化联系。大量的史书和文献记载了中国人民早期开发台湾的情景。公元 230 年,三国时期吴人沈莹所著《临海水土志》留下了关于台湾最早的记述。隋朝政府曾三次派兵到时称“流求”的台湾。宋元以后,中国历代中央政府开始在澎湖、台湾设治,实施行政管辖。1624 年,荷兰殖民者侵占台湾南部。1662 年,民族英雄郑成功驱逐荷兰殖民者收复台湾。清朝政府逐步在台湾扩增行政机构,1684 年设立台湾府,隶属福建省管辖;1885 年改设台湾为行省,是当时中国第 20 个行省。 1894 年 7 月,日本发动侵略中国的甲午战争,次年 4 月迫使战败的清朝政府割让台湾及澎湖列岛。抗日战争时期,中国共产党人明确提出收复台湾的主张。1937 年 5 月 15 日,毛泽东同志会见美国记者尼姆·韦尔斯时表示:“中国的抗战是要求得最后的胜利,这个胜利的范围,不限于山海关,不限于东北,还要包括台湾的解放。” 1941 年 12 月 9 日,中国政府发布对日宣战布告,宣告“所有一切条约、协定、合同,有涉及中日间之关系者,一律废止”,并宣布将收回台湾、澎湖列岛。1943 年 12 月 1 日,中美英三国政府发表《开罗宣言》宣布,三国之宗旨在使日本所窃取于中国之领土,例如东北、台湾、澎湖列岛等,归还中国。1945 年 7 月 26 日,中美英三国共同签署、后来苏联参加的《波茨坦公告》,重申“开罗宣言之条件必将实施”。同年 9 月,日本签署《日本投降条款》,承诺“忠诚履行波茨坦公告各项规定之义务”。10 月 25 日,中国政府宣告“恢复对台湾行使主权”,并在台北举行“中国战区台湾省受降仪式”。由此,通过一系列具有国际法律效力的文件,中国从法律和事实上收复了台湾。 1949 年 10 月 1 日,中华人民共和国中央人民政府宣告成立,取代中华民国政府成为代表全中国的唯一合法政府。这是在中国这一国际法主体没有发生变化情况下的政权更替,中国的主权和固有领土疆域没有改变,中华人民共和国政府理所当然地完全享有和行使中国的主权,其中包括对台湾的主权。由于中国内战延续和外部势力干涉,海峡两岸陷入长期政治对立的特殊状态,但中国的主权和领土从未分割也决不允许分割,台湾是中国领土的一部分的地位从未改变也决不允许改变。 1971 年 10 月,第 26 届联合国大会通过第 2758 号决议,决定:“恢复中华人民共和国的一切权利,承认她的政府的代表为中国在联合国组织的唯一合法代表并立即把蒋介石的代表从它在联合国组织及其所属一切机构中所非法占据的席位上驱逐出去。”这一决议不仅从政治上、法律上和程序上彻底解决了包括台湾在内全中国在联合国的代表权问题,而且明确了中国在联合国的席位只有一个,不存在“两个中国”、“一中一台”的问题。随后,联合国相关专门机构以正式决议等方式,恢复中华人民共和国享有的合法席位,驱逐台湾当局的“代表”,如 1972 年 5 月第 25 届世界卫生大会通过第 25.1 号决议。联合国秘书处法律事务办公室官方法律意见明确指出,“台湾作为中国的一个省没有独立地位”,“台湾当局不享有任何形式的政府地位”。实践中,联合国对台湾使用的称谓是“台湾,中国的省(Taiwan,Province of China)”\[^注1]。 联大第 2758 号决议是体现一个中国原则的政治文件,国际实践充分证实其法律效力,不容曲解。台湾没有任何根据、理由或权利参加联合国及其他只有主权国家才能参加的国际组织。近年来,以美国为首的个别国家一些势力与“台独”分裂势力沆瀣一气,妄称该决议没有处理“台湾的代表权问题”,炒作非法无效的“旧金山和约”(注 2),无视《开罗宣言》、《波茨坦公告》在内的一系列国际法律文件,再度鼓吹“台湾地位未定”,宣称支持台湾“有意义地参与联合国体系”,其实质是企图改变台湾是中国的一部分的地位,制造“两个中国”、“一中一台”,实现其“以台制华”的政治目的。这些行径歪曲联大第 2758 号决议,违反国际法,严重背弃有关国家对中国作出的政治承诺,侵犯中国的主权和尊严,践踏国际关系基本准则。对此,中国政府已经表明了反对和谴责的严正立场。 一个中国原则是国际社会的普遍共识,是遵守国际关系基本准则的应有之义。目前,全世界有包括美国在内的 181 个国家,在一个中国原则的基础上与中国建立了外交关系。1978 年 12 月发表的《中美建交公报》声明:“美利坚合众国政府承认中国的立场,即只有一个中国,台湾是中国的一部分”;“美利坚合众国承认中华人民共和国政府是中国的唯一合法政府。在此范围内,美国人民将同台湾人民保持文化、商务和其他非官方关系”。 1982 年 12 月,中华人民共和国第五届全国人民代表大会第五次会议通过《中华人民共和国宪法》,规定:“台湾是中华人民共和国的神圣领土的一部分。完成统一祖国的大业是包括台湾同胞在内的全中国人民的神圣职责。”2005 年 3 月,第十届全国人民代表大会第三次会议通过《反分裂国家法》,规定:“世界上只有一个中国,大陆和台湾同属一个中国,中国的主权和领土完整不容分割。维护国家主权和领土完整是包括台湾同胞在内的全中国人民的共同义务。”“台湾是中国的一部分。国家绝不允许‘台独’分裂势力以任何名义、任何方式把台湾从中国分裂出去。”2015 年 7 月,第十二届全国人民代表大会常务委员会第十五次会议通过《中华人民共和国国家安全法》,规定:“中国的主权和领土完整不容侵犯和分割。维护国家主权、统一和领土完整是包括港澳同胞和台湾同胞在内的全中国人民的共同义务。” 世界上只有一个中国,台湾是中国的一部分的历史事实和法理事实不容置疑,台湾从来不是一个国家而是中国的一部分的地位不容改变。任何歪曲事实、否定和挑战一个中国原则的行径都将以失败告终。 \[^注1]: 详见《联合国司法年鉴 2010》(United Nations Juridical Yearbook 2010)第 516 页。 --- --- url: 'https://zhaobc.site/posts/router/router-settings.md' --- # 路由器折腾笔记 ## 刷机 路由器型号为 newifi3-d2,咸鱼上『拣』的垃圾,当然不是免费的,为了 d2 版本挑拣的矿机。 到手后,刷了 breed,然后刷了 padavan,本人小白,所以选了 padavan。 之前很早的时候,newifi1 用过 openwrt,主要看中的是多拨,后来升级带宽,感觉够用了,而且感觉多拨也没怎么提升,padavan 功能整合的挺好就刷了 padavan,所以就一直沿用下来了。 (多拨没提升,估计是我网线的事,原来埋的是百兆线,padavan 上没多拨也是个遗憾吧) ## padavan 外网访问 路由器刷了 padavan 后感觉功能很强大,但是基本没啥用,但是总想搞事情。 逛恩山论坛后,看到大家在刷翻墙,奈何自己没服务器,只能作罢。还看到大家在搞外网访问,虽然自己没啥用,但是感觉大家在搞,我也想搞搞,而且自己也正好有域名。 但是,之前买的 3 年的域名到期了,看着续费也不便宜(其实是穷。。。),自己不怎么用,所以就不续费了。新注册反而更便宜,那还是新注册个吧,反正之前的域名也没啥用。。。 鉴于上次买的域名时间短,这次想搞个时间长的,省得续费还贵,一次性搞了 10 年,对比了一下虽然腾讯域名比阿里域名便宜一点,但是鉴于腾讯的也不能用 Q 币(正好有闲置),而且之前阿里上的域名也都用过,所以还是选择了阿里。 上面废话了一通,这次回归正题,按以下几点实现外部访问。 1. 自己有域名后,可以配合 padavan 的`Aliddns`功能解析到域名下。 * 配置阿里域名,获取 Access Key 的相关信息,使用`RAM 用户`,配置了`管理云解析(DNS)的权限`和`管理域名服务的权限`。 * 使用`Aliddns`配置。[配置链接](http://192.168.123.1/Advanced_Extensions_aliddns.asp):`http://192.168.123.1/Advanced_Extensions_aliddns.asp` 2. 确保开启了路由的`HTTP Web 服务` * [配置链接](http://192.168.123.1/Advanced_Services_Content.asp):`http://192.168.123.1/Advanced_Services_Content.asp` * 开启 HTTP & HTTPS 服务,如下所示: ![开启HTTP & HTTPS](./images/http-https-ssh.png) * 此处同时开启了 ssh 服务 3. 使用 frp 实现内网穿透 * [配置链接](http://192.168.123.1/Advanced_Extensions_frp.asp):`http://192.168.123.1/Advanced_Extensions_frp.asp` * 配置脚本`frp_scripts` 为了简便,这里只说明修改部分。 ```bash # ==========客户端配置:========== [common] server_addr = 自己的域名 server_port = 7000 token = 自己设置一个token,跟服务器端一致即可 [web] type = http local_port = 80 # 跟上面HTTP Web服务的的端口保持一致 remote_port = 自己设置一个端口 # 这里注意一下,一般运营上会屏蔽80端口,使用其他端口。 custom_domains = 自己的域名 [https] type = https local_port = 443 # 跟上面HTTPS Web服务的的端口保持一致 remote_port = https的端口 # 自己配置一个https的端口 custom_domains = 自己的域名 [ssh] type = tcp local_port = 22 remote_port = ssh的端口 # ==========服务端配置:========== [common] bind_port = 7000 dashboard_port = 7500 # dashboard 用户名密码,默认都为 admin dashboard_user = admin dashboard_pwd = admin # 通过自定义域名访问部署于内网的 web 服务 # https://github.com/fatedier/frp/blob/master/README_zh.md#%E9%80%9A%E8%BF%87%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9F%9F%E5%90%8D%E8%AE%BF%E9%97%AE%E9%83%A8%E7%BD%B2%E4%BA%8E%E5%86%85%E7%BD%91%E7%9A%84-web-%E6%9C%8D%E5%8A%A1 vhost_http_port = 供外网访问的http端口,跟上面的http的remote_port保持一致 vhost_https_port = 供外网访问的https端口,跟上面的https的remote_port保持一致 token = 跟客户端的token保持一致 ``` * 特别说明 * http 的`remote_port`遇到过一个坑,最初设置的 8080 能正常范围,后来不知啥时候不能访问了,一开始以为配置错了,各种调整依然不行,后来发现设为别的端口就可以,估计是 8080 也被运营商封掉了。。。 * https 的 443 也被运行商封掉了,换用其他端口。 4. 配置端口转发 * 为了定制更多端口转发,我在`外部网络 (WAN) - 端口转发 (UPnP)`中自己设的 * 也可以在`防火墙 - 通用设置`中开启 5. SSL 证书 需要启用 https 的话,需要申请免费或购买收费证书,得到正数后,将证书内容填到下面两项内。 位置:`系统管理 - 服务` 配置项: * Server Certificate (required) * Server Private Key (required) 不出意外,就可以外网访问了,如有意外,先检查配置,再网络搜索。 ## 开启 IPv6 外网访问 运营上已支持 IPv6,路由里也自动获得了 IPv6。 1. 默认配置: ![IPv6配置](./images/ipv6-settings.png) 2. Aliddns 的`顶级域名3[IPv6]`配置上,会自动加上 AAAA 记录。 3. `自定义设置 - 脚本 - 在防火墙规则启动后执行:`里加上 IPv6 的防火墙策略 ```bash ### 所需要的防火墙策略 ip6tables -F ip6tables -P INPUT ACCEPT ip6tables -P FORWARD ACCEPT ip6tables -P OUTPUT ACCEPT ``` 4. 在 frp 里配置上 IPv6 的节点。 跟 IPv4 一样,需要注意的是,http/https 可以共用同一个端口,tcp 不能共用同一个端口,需要单独起一个端口供 IPv6 使用,同时端口转发里也加上这个端口。 --- --- url: 'https://zhaobc.site/posts/theme/auto-articles.md' --- # 自动显示当前路径下的文章列表 起因是为了方便管理文章,将文章按目录存放,但是这样存放后,地址栏和导航栏会自动多一层路径。 如果启用了 `AutoCatalog` 功能的话,会自动显示目录,不开启的话,显示会有问题。 `AutoCatalog` 显示的目录不太符合文章的风格,想做成显示文章列表的样子,于是就写了 `AutoArticles` 功能。 同时,验证了一下自定义布局的功能。 ## 自定义布局 为了在文章目录下显示文章列表,默认的布局无法实现,因此需要自定义布局。 自定义布局,需要在 `client.ts` 中加入自己的布局。 ```ts {9} import { defineClientConfig } from '@vuepress/client' import AutoArticleListLayout from './theme/layouts/AutoArticleListLayout' import AutoArticleList from './theme/components/AutoArticleList' export default defineClientConfig({ // You can override or add layouts here layouts: { AutoArticleListLayout, }, enhance: ({ app, router, siteData }) => { app.component('AutoArticleList', AutoArticleList) }, }) ``` `AutoArticleListLayout` 是自定义布局,用于显示文章列表。 代码如下: ```ts import { type VNode, defineComponent, h } from 'vue' import { usePageData } from '@vuepress/client' import CommonWrapper from '@theme-hope/components/CommonWrapper' import SkipLink from '@theme-hope/components/SkipLink' import FadeSlideY from '@theme-hope/components/transitions/FadeSlideY' import AutoArticleList from '../components/AutoArticleList' export default defineComponent({ name: 'AutoArticleListLayout', setup() { const page = usePageData() return (): VNode[] => [ h(SkipLink), h( CommonWrapper, { noSidebar: true, }, { default: () => h(FadeSlideY, () => h(AutoArticleList)), } ), ] }, }) ``` ## 文章列表 除了布局外,还需要一个文章列表的组件 `AutoArticleList`。 它的功能是选出当前路径下的文字,然后显示文章列表。 代码如下: ```ts import { type VNode, computed, defineComponent, h, ref } from 'vue' import BreadCrumb from '@theme-hope/components/BreadCrumb' import MarkdownContent from '@theme-hope/components/MarkdownContent' import DropTransition from '@theme-hope/components/transitions/DropTransition' import ArticleList from '@theme-hope/modules/blog/components/ArticleList' import { useArticles } from '@theme-hope/modules/blog/composables/index' import '../styles/auto-article-list.scss' import { useRouter } from 'vue-router' import { usePageData } from '@vuepress/client' export default defineComponent({ name: 'AutoArticleList', setup() { const articles = useArticles() const router = useRouter() const page = usePageData() const currRoute = router.currentRoute const currArticles = computed(() => { return articles.value.items.filter( // 需要去掉本身的路径,不然多渲染一个空的列表 item => item.path.startsWith(currRoute.value.path) && item.path !== currRoute.value.path ) }) return (): VNode => h('div', { class: 'page blog' }, [ h('div', { class: 'blog-page-wrapper' }, [ h( 'main', { class: 'auto-article-list', id: 'auto-article-list-main-content', }, [ h(BreadCrumb), h(DropTransition, { appear: true, delay: 0.24 }, () => h(ArticleList, { items: currArticles.value }) ), ] ), ]), h(DropTransition, { appear: true, delay: 0.28 }, () => h(MarkdownContent) ), ]) }, }) ``` ## 使用 为了让路径导航 `BreadCrumb` 能正常渲染出来最后一层,文件夹目录需要加一个 `README.md`,并指定 `title`。 同时,为了让自定义布局生效,需要指定自己的布局。 而且,文字列表页不是文章,需要设置不进行索引。 示例: ```yml --- title: 主题 layout: AutoArticleListLayout index: false article: false --- ``` --- --- url: 'https://zhaobc.site/posts/theme/column-layout.md' --- # 文章分栏布局 如果想要文章分两栏来展示的话,可以借助 CSS 的 [columns](https://developer.mozilla.org/zh-CN/docs/Web/CSS/columns) 来完成。 为了方便直接使用,做了一个 css 文件。 ::: code-tabs @tab column-layout.scss ```scss @use '@sass-palette/hope-config'; .column-layout { &.two-col { [vp-content] { columns: 2; } } .span-all { column-span: all; &.center { text-align: center; } } .divider { column-span: all; position: relative; display: block; height: 1px; margin: 1rem; border-bottom: 1px var(--vp-c-border) solid; transition: border-bottom-color var(--vp-t-color); &::before { content: '·'; position: absolute; left: 50%; transform: translate(-50%) translateY(-50%); height: 1rem; line-height: 1rem; color: var(--vp-c-text); background: var(--vp-c-bg); width: 2rem; text-align: center; transition: color, background var(--vp-t-color); } } .span-none { column-span: none; } @media screen and (max-width: hope-config.$mobile) { &.two-col { [vp-content] { columns: unset; } } } } ``` ::: 使用时,需要在 md 文件的 frontmatter 中指定 `containerClass`。 ```md --- containerClass: 'column-layout two-col' --- ``` `.span-all` 用于跨所有列,同时,使用 `attrs` 功能实现标题的跨列和居中示例: ```md
    xxx
    ## 前言 {.span-all .center} ``` 适当使用分割线来分段,以避免阅读不便。 ```md
    ``` 文章示例:[点这里](../other/columns-layout-demo.md) --- --- url: 'https://zhaobc.site/posts/theme/customize-theme.md' --- # 自定义主题 本站使用功能强大的 [vuepress-theme-hope](https://vuepress-theme-hope.github.io/v2/zh) 主题,主题简洁,markdown 插件功能丰富。 同时,`vuepress-theme-hope` 提供了完善的文档,以及 QQ 交流群,作者 [Mr.Hope](https://github.com/Mister-Hope) 更新频率高,问题反馈处理也很及时。 使用 `vuepress-theme-hope` 时,发现了一些小问题,自己重写了一点 `css`,根据文档可以替换组件,或者继承主题实现自己的主题。 最开始修改过的导航栏跟博主信息的间距问题,文章置顶图标,已经提过 `PR` 不需要再自定义了。 下面介绍一下自定义的部分。 ## 自定义侧边栏文字 ::: warning 已不再使用自定义的侧边栏,使用 `vuepress-theme-hope` 主题默认的侧边栏,这里仅保留记录。 原因:懒,升级主题时不用自己维护修改了。 ::: 侧边栏可以使用主题的自动生成功能直接生成,但是目录下的 `README.md` 生成时,目录菜单跟 `README.md` 的侧边栏文字显示是一致的,感官上有些重复。 另外,记录笔记时,想生成那种跟书本一样,`README.md` 生成**前言**或**目录**的样子。所以自己重写了侧边栏生成的处理。 做法是重写 `theme/node/sidebar.ts`,在生成 `README.md` 的 `title` 时读取自定义的 `frontmatter.sidebarText`。 由于主题提供的可替换组件里没有这个组件,为了让主题读取自己自定义的处理,使用了 `onPrepared` 钩子函数。 ## 增加自己的 markdown 插件 ::: warning `echarts`, `playground` 插件已经提交 PR,不需要自己实现,使用 `vuepress-theme-hope` 主题内置即可,这里仅保留记录。 ::: `vuepress-theme-hope` 的 `md-enhance` 已经提供了很多开箱即用的插件,如果有不满足的话,可以自己编写。 本站增加了下面的插件: * [echarts](./echarts-plugin.md) 主题内置了 `chart` 图表,使用 `chart.js` 做图表,但是国内 `echarts` 使用的还是挺多的,刚好用过一点,所以仿照 `chart`,编写了一个 `echarts` 图表。 * [playground](./playground-plugin-v3.md) 主题内置的 `demo` 挺好的,对于记录笔记,展示代码片段很方便。 但是,只能使用 `export default { // ... }` 形式,不支持 `script setup` 语法糖, 于是借鉴了 `Vue3` 官方 Playground 以及 Element Plus 的代码示例简单做了一个 markdown 的 `playground`。 --- --- url: 'https://zhaobc.site/posts/theme/echarts-plugin.md' --- # ECharts 插件 让你的 VuePress 站点的 Markdown 文件支持 [ECharts](https://echarts.apache.org/zh/index.html) 图表。 使用本插件时,需要自己安装 `echarts` 依赖,本插件会将 `echarts` 加到 `optimizeDeps` 中。 另外,本插件使用了 `vueuse`,用来做宽度自适应。 ::: warning 本插件已经提交 PR,不需要自己实现,使用 `vuepress-theme-hope` 主题内置即可,这里仅保留记录。 ::: ## 配置 ```ts {4} // customConfig.ts export const customConfig: CustomConfig = { mdEnhance: { echarts: true, }, } ``` ## 格式 ````md ::: echarts 标题 ```json { // 此处为图表配置 } ``` ::: ```` ## 案例 ### 折线图 ::: echarts ECharts 折线图 ```json { "xAxis": { "type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] }, "yAxis": { "type": "value" }, "series": [ { "data": [150, 230, 224, 218, 135, 147, 260], "type": "line", "smooth": true } ] } ``` ::: :::: details 代码 ````md ::: echarts ECharts 折线图 ```json { "xAxis": { "type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] }, "yAxis": { "type": "value" }, "series": [ { "data": [150, 230, 224, 218, 135, 147, 260], "type": "line", "smooth": true } ] } ``` ::: ```` :::: ### 柱状图 ::: echarts ECharts 柱状图 ```json { "xAxis": { "type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] }, "yAxis": { "type": "value" }, "series": [ { "data": [120, 200, 150, 80, 70, 110, 130], "type": "bar", "showBackground": true, "backgroundStyle": { "color": "rgba(180, 180, 180, 0.2)" } } ] } ``` ::: :::: details 代码 ````md ::: echarts ECharts 柱状图 ```json { "xAxis": { "type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] }, "yAxis": { "type": "value" }, "series": [ { "data": [120, 200, 150, 80, 70, 110, 130], "type": "bar", "showBackground": true, "backgroundStyle": { "color": "rgba(180, 180, 180, 0.2)" } } ] } ``` ::: ```` :::: ### 饼图 ::: echarts ECharts 饼图 ```json { "title": { "text": "Referer of a Website", "subtext": "Fake Data", "left": "center" }, "tooltip": { "trigger": "item" }, "legend": { "orient": "vertical", "left": "left" }, "series": [ { "name": "Access From", "type": "pie", "radius": "50%", "data": [ { "value": 1048, "name": "Search Engine" }, { "value": 735, "name": "Direct" }, { "value": 580, "name": "Email" }, { "value": 484, "name": "Union Ads" }, { "value": 300, "name": "Video Ads" } ], "emphasis": { "itemStyle": { "shadowBlur": 10, "shadowOffsetX": 0, "shadowColor": "rgba(0, 0, 0, 0.5)" } } } ] } ``` ::: :::: details 代码 ````md ::: echarts ECharts 饼图 ```json { "title": { "text": "Referer of a Website", "subtext": "Fake Data", "left": "center" }, "tooltip": { "trigger": "item" }, "legend": { "orient": "vertical", "left": "left" }, "series": [ { "name": "Access From", "type": "pie", "radius": "50%", "data": [ { "value": 1048, "name": "Search Engine" }, { "value": 735, "name": "Direct" }, { "value": 580, "name": "Email" }, { "value": 484, "name": "Union Ads" }, { "value": 300, "name": "Video Ads" } ], "emphasis": { "itemStyle": { "shadowBlur": 10, "shadowOffsetX": 0, "shadowColor": "rgba(0, 0, 0, 0.5)" } } } ] } ``` ::: ```` :::: ## 文档 相关详情,详见 [ECharts](https://echarts.apache.org/zh/option.html) 文档. --- --- url: 'https://zhaobc.site/posts/theme/gitlab-contributors-plugin.md' --- # Gitlab 贡献者插件 本插件用来显示 gitlab 作为版本管理的 vuepress-theme-hope 项目的贡献者。 ## 初衷 公司领导想提高效率,征集经验集,工具集以方便大家使用,于是就用 vuepress-theme-hope 搭了个简单的知识库。 再配合 waline 加上了评论功能。 由于是内网环境,所以在公司的 gitlab 服务器上建了一个 git 仓库。 为了鼓励大家积极踊跃地分享,写了个 `GitlabContributors` 组件,在主页上显示贡献者头像。 后来,又在每个页面上写了一个当前页的贡献者组件 `PageGitlabContributors`。 再后来,又想加上多语言支持,然后就干脆做了个插件使用。 ## 功能 * 根据 gitlab 项目 id,显示该项目所有的贡献者头像 * 根据每个页面的贡献者,显示 gitlab 头像 ### 显示项目所有的贡献者 查阅了下 Gitlab API,发现可以通过项目 id 获取贡献者,但是默认是按页返回贡献者,而不是一次性返回全部贡献者,而且每页最多返回 100 条数据。 使用 Gitlab API 时,有时会需要 `PRIVATE_ACCESS_TOKEN`,需要自己生成一下。 拿到项目的所有贡献者后,再获取相应贡献者的头像,如果不是 Gitlab 系统里的用户的话,则显示贡献者名称。 ### 显示当前页面的所有的贡献者 vuepress 内置了 git 插件,可以显示当前页的所有贡献者,然后再根据 email 去匹配 gitlab 用户,从而显示贡献者头像。 ## 实现 目录结构: ```txt └─gitlab-contributors ├─client │ │ config.ts │ │ │ ├─components │ │ GitlabContributors.vue │ │ PageGitlabContributors.vue │ │ │ ├─composables │ │ gitlab.ts │ │ index.ts │ │ utils.ts │ │ │ └─styles │ contributors.scss │ ├─node │ index.ts │ locales.ts │ options.ts │ plugins.ts │ └─shared index.ts locales.ts ``` ### 组件 ::: code-tabs @tab GitlabContributors.vue ```vue ``` @tab PageGitlabContributors.vue ```vue ``` ::: ### 组合函数 ::: code-tabs @tab gitlab.ts ```ts import { ComputedRef } from 'vue' import { type GitContributor } from '@vuepress/plugin-git' import { useContributors as useGitContributors } from 'vuepress-theme-hope/modules/info/composables/index' import { type RequiredLocaleConfig } from 'vuepress-shared/client' import { ContributorLocaleData } from '../../shared' import { groupBy } from './utils' declare const CONTRIBUTOR_PROJECT_ID: string declare const CONTRIBUTOR_HOST: string declare const CONTRIBUTOR_API: string declare const CONTRIBUTOR_ACCESS_TOKEN: string declare const CONTRIBUTOR_PAGE_COUNT: number declare const COPYRIGHT_LOCALES: RequiredLocaleConfig export const GITLAB_PROJECT_ID = CONTRIBUTOR_PROJECT_ID export const GITLAB_HOST = CONTRIBUTOR_HOST export const GITLAB_API = `${GITLAB_HOST}/${CONTRIBUTOR_API}` // gitlab 单页最多返回 100 个用户,即使设了 99999 const MAX_USER_COUNT = CONTRIBUTOR_PAGE_COUNT || 100 const PERSONAL_ACCESS_TOKEN = CONTRIBUTOR_ACCESS_TOKEN export interface Contributor { id?: number username?: string name: string email: string avatar_url?: string web_url?: string commits: number } /** * 获取 Gitlab 所有用户 * @returns Gitlab users */ export const useUsers = (page: number, per_page: number) => { // https://docs.gitlab.com/ee/api/users.html#list-users const p = page || 1 const maxCount = per_page || MAX_USER_COUNT return fetch(`${GITLAB_API}/users?page=${p}&per_page=${maxCount}`, { method: 'GET', headers: { 'PRIVATE-TOKEN': PERSONAL_ACCESS_TOKEN, }, }) } /** * 获取全部的用户 * @returns users */ export const useAllUsers = async () => { let users = [] as any[] let startPage = 1 // 按页获取,然后拼接起来 let result: any[] do { result = await (await useUsers(startPage, MAX_USER_COUNT)).json() users = users.concat(result) startPage++ } while (result.length === MAX_USER_COUNT) return users } /** * 获取指定项目的贡献者 * @param id project id * @returns 指定项目的贡献者 */ export const useRepositoryContributors = (id: string) => { // https://docs.gitlab.com/ee/api/repositories.html#contributors return fetch( `${GITLAB_API}/projects/${id}/repository/contributors?order_by=commits&sort=desc`, { method: 'GET', } ) } /** * 获取库的提交 * @param projectId project id * @param path The file path */ export const useRepositoryCommits = (projectId: string, path: string) => { // https://docs.gitlab.com/ee/api/commits.html#list-repository-commits let url = `${GITLAB_API}/projects/${projectId}/repository/commits` if (path) { url = `${url}?path=${encodeURIComponent(path)}` } return fetch(url, { method: 'GET', }) } /** * 获取贡献者 * @param projectId project id * @param path The file path */ export const useContributors = (projectId: string, path: string) => { return new Promise((resolve, reject) => { useAllUsers() .then(users => { const contributors: Contributor[] = [] if (path) { useRepositoryCommits(projectId, path) .then(res => res.json()) .then(commits => { const groupedCommits = groupBy(commits, 'committer_email') Object.keys(groupedCommits).forEach(k => { const tmpContri: Contributor = { name: groupedCommits[k][0].committer_name, username: groupedCommits[k][0].committer_name, email: k, commits: groupedCommits[k].length, } const user = users.find( u => u.username === groupedCommits[k][0].committer_name && u.email === k ) if (user) { tmpContri.name = user.name tmpContri.avatar_url = user.avatar_url tmpContri.web_url = user.web_url } contributors.push(tmpContri) }) return resolve(contributors.sort((a, b) => b.commits - a.commits)) }) .catch(err => reject(err)) } else { useRepositoryContributors(projectId) .then(res => res.json()) .then(contris => { contris.forEach(c => { const tmpContri: Contributor = { name: c.name, username: c.name, email: c.email, commits: c.commits, } const user = users.find( u => u.username === c.name && u.email === c.email ) if (user) { tmpContri.name = user.name tmpContri.avatar_url = user.avatar_url tmpContri.web_url = user.web_url } contributors.push(tmpContri) }) return resolve(contributors) }) .catch(err => reject(err)) } }) .catch(err => { reject(err) }) }) } /** * 获取当前页的贡献者 */ export const usePageContributors = () => { return new Promise((resolve, reject) => { const gitContributors = useGitContributors() as ComputedRef< null | GitContributor[] > if (gitContributors.value) { useAllUsers() .then(users => { const contributors: Contributor[] = [] gitContributors .value!.sort((a, b) => b.commits - a.commits) .forEach(c => { const tmpContri: Contributor = { name: c.name, username: c.name, email: c.email, commits: c.commits, } const user = users.find(u => u.email === c.email) if (user) { tmpContri.name = user.name tmpContri.avatar_url = user.avatar_url tmpContri.web_url = user.web_url } contributors.push(tmpContri) }) return resolve(contributors) }) .catch(err => reject(err)) } else { return resolve(null) } }) } ``` @tab utils.ts ```ts export const groupBy = (arr: any[], key: string) => { if (arr) { const grouped = arr.reduce((group: any, obj: any) => { const data = obj[key] group[data] = group[data] ?? [] group[data].push(obj) return group }, {}) return grouped } return null } ``` ::: ### 样式 只是将默认的 contributors 隐藏掉。 ```css .vp-page-meta .contributors { display: none; } ``` ### 插件 ::: code-tabs @tab options.ts ```ts import { type LocaleConfig, type Page } from '@vuepress/core' import { ContributorLocaleData } from '../shared/locales' export interface ContributorOptions { projectId: string host: string apiBase: string accessToken: string pageCount?: number locales?: LocaleConfig } ``` @tab plugins.ts ```ts import { type Page, type PluginFunction } from '@vuepress/core' import { type PageHeader, usePageData } from '@vuepress/client' import { getDirname, path } from '@vuepress/utils' import { getLocales, inferLocalePath } from 'vuepress-shared/node' import { PLUGIN_NAME } from '../shared' import { contributorLocales } from './locales' import { type ContributorOptions } from './options' const __dirname = getDirname(import.meta.url) export const contributorPlugin = (options: ContributorOptions): PluginFunction => app => { const locales = getLocales({ app, name: PLUGIN_NAME, default: contributorLocales, config: options.locales, }) return { name: PLUGIN_NAME, define: (): Record => ({ CONTRIBUTOR_PROJECT_ID: options.projectId, CONTRIBUTOR_HOST: options.host, CONTRIBUTOR_API: options.apiBase, CONTRIBUTOR_ACCESS_TOKEN: options.accessToken, CONTRIBUTOR_PAGE_COUNT: options.pageCount || 100, CONTRIBUTOR_LOCALES: locales, }), extendsPage: (page: Page): void => { const langPath = inferLocalePath(page.lang) const headerTitle = locales[langPath] ? locales[langPath].title : locales['/'].title if (page.headers) { page.headers.push({ level: 2, title: headerTitle, slug: 'page-contributors', link: '#page-contributors', children: [], }) } }, clientConfigFile: path.resolve(__dirname, '../client/config.ts'), } } ``` @tab locales.ts ```ts import { ContributorLocaleConfig } from '../shared/locales' export const contributorLocales: ContributorLocaleConfig = { '/zh/': { title: '贡献者', loadingText: '努力加载中...', errorText: '加载失败,请稍后重试。', }, '/en/': { title: 'Contributors', loadingText: 'Loading...', errorText: 'Error occurred, please retry it later.', }, '/ja/': { title: '貢献者', loadingText: '処理中...', errorText: 'エラーが発生しました。リトライしてください。', }, } ``` ::: --- --- url: 'https://zhaobc.site/posts/theme/playground-plugin-v2.md' --- # Playground 插件 v2 让你的 VuePress 站点中的 Markdown 文件支持 [Vue SFC Playground](https://sfc.vuejs.org/) 类似的代码案例。 同时支持外置和内置两种模式: * 外置模式,通过嵌入`iframe`的形式来引入 playground. * 内置模式,通过`@vue/repl`直接渲染出来 playground. 比 [v1](./playground-plugin.md) 更强大。 ::: warning `playground` 插件已经提交 PR,不需要自己实现,使用 `vuepress-theme-hope` 主题内置即可,这里仅保留记录。 ::: ## 配置 ::: code-tabs#config @tab TS ```ts {8} // .vuepress/config.ts import { mdEnhance } from 'vuepress-plugin-md-enhance' export default { plugins: [ mdEnhance({ // 配置你的 playground playground: true, // 使用默认配置 }), ], } ``` @tab JS ```js {8} // .vuepress/config.js const { mdEnhance } = require('vuepress-plugin-md-enhance') module.exports = { plugins: [ mdEnhance({ // 配置你的 playground playground: true, // 使用默认配置 }), ], } ``` ::: 你也可以使用 `PlaygroundOptions` 自定义你的 playground 配置: ::: code-tabs#config @tab TS ```ts {8-21} // .vuepress/config.ts import { mdEnhance } from 'vuepress-plugin-md-enhance' export default { plugins: [ mdEnhance({ // 配置你的 playground playground: { mode: 'external', // 使用外置模式 external: { base: 'https://sfc.vuejs.org/', // 使用 vue sfc playground. defaultImportsMap: 'import-map.json', }, internal: { defaultImportsMap: 'import-map.json', showCode: false, // 不显示代码 showCompileOutput: false, // 不显示 js, css, ssr 面板 showImportMap: true, // 显示 import map clearConsole: false, // 不清空控制台 }, }, }), ], } ``` @tab JS ```js {8-21} // .vuepress/config.js const { mdEnhance } = require('vuepress-plugin-md-enhance') module.exports = { plugins: [ mdEnhance({ // 配置你的 playground playground: { mode: 'external', // 使用外置模式 external: { base: 'https://sfc.vuejs.org/', // 使用 vue sfc playground. defaultImportsMap: 'import-map.json', }, internal: { defaultImportsMap: 'import-map.json', showCode: false, // 不显示代码 showCompileOutput: false, // 不显示 js, css, ssr 面板 showImportMap: true, // 显示 import map clearConsole: false, // 不清空控制台 }, }, }), ], } ``` ::: ## 案例 ### 外置模式 #### 基本用法 ````md :::: playground 基础用法 ::: file App.vue ```vue ``` ::: :::: ```` #### 高级用法 本示例向你展示如何自定义你的 playground。 * 使用你自己的 playground * 使用你自己的 import map * 应用额外的配置到你的 playground ````md :::: playground 高级用法 ::: file App.vue ```vue ``` ::: file Comp.vue ```vue ``` ::: ::: imports import-map.json ```json { "imports": { "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js" } } ``` ::: ::: settings ```json { "mode": "external", "external": { "base": "https://vue-sfc-playground.vercel.app/", "options": { "showOutput": "true" } } } ``` ::: :::: ```` ### 内置模式 #### 内置模式 基本用法 ````md :::: playground 基础用法 ::: file App.vue ```vue ``` ::: ::: settings ```json { "mode": "internal" } ``` ::: :::: ```` #### 内置模式 高级用法 显示 playground 的 JS, CSS, SSR 面板,并显示代码编辑器。 ````md :::: playground 高级用法 ::: file App.vue ```vue ``` ::: ::: settings ```json { "mode": "internal", "internal": { "showCode": "true", "showCompileOutput": "true" } } ``` ::: :::: ```` ## Markdown 插件代码 ```ts // playground.ts import { hash } from '@vuepress/utils' import type { PluginSimple } from 'markdown-it' import type { default as Token } from 'markdown-it/lib/token' import type { PlaygroundFiles } from '../../shared/playground' import { IMPORT_MAP_KEY } from '../../shared/playground' import { container } from './container' const extensions = ['html', 'js', 'ts', 'vue', 'jsx', 'tsx', 'json'] // export const playground: PluginSimple = (md) => { export const playground = (defaultImportMap?: string): PluginSimple => md => container(md, { name: 'playground', openRender: (tokens: Token[], index: number): string => { const { info } = tokens[index] const title = info .trimStart() // 'playground' length .slice(10) .trim() const hashKey = `${index}-${title}` const key = `playground-${hash(hashKey)}` const codeConfigs: PlaygroundFiles = {} let settings: string | null = null let configKey: string | null = null let isSettings = false for (let i = index; i < tokens.length; i++) { // console.log(i, tokens[i]) const { type, content, info } = tokens[i] if (type === 'container_playground_close') { break } if (type === 'container_file_open') { const fileName = info .trimStart() // 'file' length .slice(4) .trim() if (!fileName || fileName.length === 0) { continue } configKey = fileName } else if (type === 'container_imports_open') { const fileName = info .trimStart() // 'imports' length .slice(7) .trim() if (fileName && fileName.length > 0) { configKey = fileName } else { configKey = defaultImportMap || IMPORT_MAP_KEY } } else if (type === 'container_settings_open') { isSettings = true } else if (type === 'inline') { continue } if (!content) { continue } if (isSettings) { if (type === 'fence' && info === 'json') { settings = content.replace(/^\s+|\s+$/g, '').replace(/\/+$/, '') } } else { if (type === 'fence' && extensions.includes(info) && configKey) { codeConfigs[configKey] = { lang: info, content, } } } // set to an unexisit token type tokens[i].type = 'playground_empty' // hide token tokens[i].hidden = true } const config = encodeURIComponent(JSON.stringify(codeConfigs)) const settingString = settings ? encodeURIComponent(settings) : encodeURIComponent('{}') return `` }, closeRender: () => '', }) const getPlugin = (name: string, component: string): PluginSimple => md => container(md, { name, openRender: (tokens: Token[], index: number): string => { const { info } = tokens[index] const title = info.trimStart().slice(name.length).trim() let config = '' let lang = '' for (let i = index; i < tokens.length; i++) { const { type, content, info } = tokens[i] if (type === `container_${name}_close`) { break } if (!content) { continue } if (type === 'fence' && extensions.includes(info)) { lang = info config = encodeURIComponent(content) // break; } // set to an unexisit token type tokens[i].type = `${name}_empty` // hide token tokens[i].hidden = true } return `<${component} id="${name}-${hash( `${name}${index}${title}${config}` )}" ${title ? ` title="${encodeURIComponent(title)}"` : ''}${ config ? ` config="${config}"` : '' } ${lang ? ` lang="${lang}"` : ''}>` }, closeRender: () => ``, }) export const playFile: PluginSimple = getPlugin('file', 'PlayFile') export const playSettings: PluginSimple = getPlugin('settings', 'PlaySettings') export const playImports: PluginSimple = getPlugin('imports', 'PlayImports') ``` --- --- url: 'https://zhaobc.site/posts/theme/playground-plugin-v3.md' --- # Playground 插件 v3 让你的 VuePress 站点中的 Markdown 文件支持 [Vue SFC Playground](https://sfc.vuejs.org/) 类似的代码案例。 与 [v2](./playground-plugin-v2.md) 相比,重写了 Markdown 插件的逻辑,减少 container 嵌套。 使用 `@file`, `@imports`, `@settings` 来代替原来的 `::: file`, `::: imports`, `::: settings`, 其他未变。 ::: warning `playground` 插件已经提交 PR,不需要自己实现,使用 `vuepress-theme-hope` 主题内置即可,这里仅保留记录。 ::: ## 配置 与 [v2](./playground-plugin-v2.md) 相同。 ::: code-tabs#config @tab TS ```ts {8} // .vuepress/config.ts import { mdEnhance } from 'vuepress-plugin-md-enhance' export default { plugins: [ mdEnhance({ // 配置你的 playground playground: true, // 使用默认配置 }), ], } ``` @tab JS ```js {8} // .vuepress/config.js const { mdEnhance } = require('vuepress-plugin-md-enhance') module.exports = { plugins: [ mdEnhance({ // 配置你的 playground playground: true, // 使用默认配置 }), ], } ``` ::: 你也可以使用 `PlaygroundOptions` 自定义你的 playground 配置: ::: code-tabs#config @tab TS ```ts {8-21} // .vuepress/config.ts import { mdEnhance } from 'vuepress-plugin-md-enhance' export default { plugins: [ mdEnhance({ // 配置你的 playground playground: { mode: 'external', // 使用外置模式 external: { base: 'https://sfc.vuejs.org/', // 使用 vue sfc playground. defaultImportsMap: 'import-map.json', }, internal: { defaultImportsMap: 'import-map.json', showCode: false, // 不显示代码 showCompileOutput: false, // 不显示 js, css, ssr 面板 showImportMap: true, // 显示 import map clearConsole: false, // 不清空控制台 }, }, }), ], } ``` @tab JS ```js {8-21} // .vuepress/config.js const { mdEnhance } = require('vuepress-plugin-md-enhance') module.exports = { plugins: [ mdEnhance({ // 配置你的 playground playground: { mode: 'external', // 使用外置模式 external: { base: 'https://sfc.vuejs.org/', // 使用 vue sfc playground. defaultImportsMap: 'import-map.json', }, internal: { defaultImportsMap: 'import-map.json', showCode: false, // 不显示代码 showCompileOutput: false, // 不显示 js, css, ssr 面板 showImportMap: true, // 显示 import map clearConsole: false, // 不清空控制台 }, }, }), ], } ``` ::: ## 案例 ### 外置模式 #### 基本用法 ````md ::: playground 基础用法 @file App.vue ```vue ``` ::: ```` #### 高级用法 本示例向你展示如何自定义你的 playground。 * 使用你自己的 playground * 使用你自己的 import map * 应用额外的配置到你的 playground ````md ::: playground 高级用法 @file App.vue ```vue ``` @file Comp.vue ```vue ``` @imports import-map.json ```json { "imports": { "lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js" } } ``` @settings ```json { "mode": "external", "external": { "base": "https://vue-sfc-playground.vercel.app/", "options": { "showOutput": "true" } } } ``` ::: ```` ### 内置模式 #### 内置模式 基本用法 ````md ::: playground 基础用法 @file App.vue ```vue ``` @settings ```json { "mode": "internal" } ``` ::: ```` #### 内置模式 高级用法 显示 playground 的 `JS`, `CSS`, `SSR` 面板,并显示代码编辑器。 Playground 的 `key` 是自动产生的。它是基于标题计算的。 你也可以自己指定它,使用 `playground#customId` 形式。 ````md ::: playground#customId 高级用法 @file App.vue ```vue ``` @settings ```json { "mode": "internal", "internal": { "showCode": "true", "showCompileOutput": "true" } } ``` ::: ```` --- --- url: 'https://zhaobc.site/posts/theme/playground-plugin.md' --- # Playground 插件 让你的 VuePress 站点中的 Markdown 文件支持 [Vue SFC Playground](https://sfc.vuejs.org/) 类似的代码案例。 ::: warning `playground` 插件已经提交 PR,不需要自己实现,使用 `vuepress-theme-hope` 主题内置即可,这里仅保留记录。 ::: ## 配置 ```ts {4-10} // cunstomConfig.ts export const customConfig: CustomConfig = { mdEnhance: { // playground: true, playground: { base: 'https://vue-sfc-playground.vercel.app/', option: { showOutput: true, }, }, }, } ``` * `playground` 设为 `true` 时,启用默认的 Playground 配置,即 Vue3 官方的 Playground。 * `playground` 设为 对象时,则使用配置项指定的内容来显示 Playground。 * 另外,`playground` 还支持个别设置 `base` 和 `imports`,稍后为你介绍。 ## 语法 \~~为了简单方便,你只需配合 `CodeGroup` 和 `CodeGroupItem` 即可。 每一个 `CodeGroupItem` 为一个 `.vue` 文件。~~ 为了简单方便,你只需配合 `code-tabs` 和 `@tab` 即可。 每一个 `@tab` 为一个 `.vue` 或 `.js` 文件。 使用 `imports` 和 `settings` 可为每个 Playground 自定义配置。 使用方法如下: ### 使用主题默认配置 ````md :::: playground Playground demo ::: code-tabs @tab App.vue ```vue ``` @tab Comp.vue ```vue ``` ::: :::: ```` ### 使用个别配置 使用个别配置的 `base` 和 `imports`,以 Element Plus 为例。 ````md :::: playground Customize playground demo ::: code-tabs @tab App.vue ```vue ``` @tab Comp.vue ```vue ``` ::: ::: imports import_map.json ```json { "imports": { "vue": "https://sfc.vuejs.org/vue.runtime.esm-browser.js" } } ``` ::: ::: settings ```json { "base": "https://element-plus.run/" } ``` ::: :::: ```` ## 案例 ### 使用主题默认配置的案例 ````md :::: playground Playground demo ::: code-tabs @tab App.vue ```vue ``` @tab Comp.vue ```vue ``` ::: :::: ```` ### 使用个别配置的案例 使用个别配置的 `base` 和 `imports`。 ````md :::: playground Element-Plus demo ::: code-tabs @tab App.vue ```vue ``` ::: ::: imports import-map.json ```json { "imports": { "a": "b" } } ``` ::: ::: settings ```json { "base": "https://element-plus.run/" } ``` ::: :::: ```` --- --- url: 'https://zhaobc.site/posts/theme/sandpack-plugin.md' --- # Sandpack 插件 让你的 VuePress 站点中的 Markdown 文件支持 [Sandpack](https://sandpack.codesandbox.io/) 类似的代码案例。 ## 缘由 手头的项目刚刚收尾,看知乎一个 React 和 Vue 撕逼问题时,里面提到了最新的 React 的一些新特性,也附上了 React 新的官网 [react.dev](https://react.dev) . 出于好奇点了进去,然后看到了官网的可交互示例,觉得不错,然后看了下源代码,发现是使用的 `sandpack-react`。 看了下 sandpack [官网](https://sandpack.codesandbox.io/),感觉功能挺强大的,集成到 vuepress 里做代码演示很不错。 然后搜了下关键字 `sandpack vuepress`,找到了 [`vitepress-plugin-sandpack`](https://github.com/jerrywu001/vitepress-plugin-sandpack),进而找到了 `sandpack-vue3`,再次表示感谢。 您也可以直接使用 [`vitepress-plugin-sandpack`](https://github.com/jerrywu001/vitepress-plugin-sandpack)。 :::warning 截止到目前(2023/9/5),代码提及到 `feat/sandpck` 分支,暂未合并到 `main` 分支,功能和配置可能会有变动。 ::: ## 配置 ```ts {4} // cunstomConfig.ts export const customConfig: CustomConfig = { mdEnhance: { sandpack: true, }, } ``` ## 使用 要使用交互演示,你应该使用一个名为 `sandpack#template` 的容器。 在其中,你可以使用 3 个指令: * `@file FullPathFile` 紧跟文件的代码块,同时也支持文件选项,例如:`@file FullPathFile [active readOnly hidden]` * `@options` 紧跟一个自定义 "options" 的 javascript 代码块 * `@setup` 紧跟一个自定义 "customSetup" 的 javascript 代码块 ## 示例 ````md ::: sandpack#vue 带自定义设置的 Vue 示例 @file /src/App.vue ```vue ``` @file /src/Comp.vue ```vue ``` @options ```js { activeFile: "/src/Comp.vue", } ``` @setup ```js { dependencies: { "@vueuse/core": "latest", "@vueuse/shared": "latest", "vue-demi": "latest", } } ``` ::: ```` 更多内容详见 Hope 网站介绍,本文仅做一个简单记录。 --- --- url: 'https://zhaobc.site/posts/theme/twoslash.md' --- # 引入 twoslash 插件 ::: warning Hope 主题已经支持了 twoslash 功能,本文可以忽略,此处只是记录一下。 ::: 看 `vue-macros` 官网时,看到比较有意思的代码提示功能,才知道是用了 `twoslash` 功能。 它是用的 `@shikijs/vitepress-twoslash` 插件,但是本主题是 `vuepress` 而不是 `vitepress`,有点遗憾。 不过,看了下 `@shikijs/vitepress-twoslash` 的代码,貌似跟 `vitepress` 没有太大关系,随尝试在 `vuepress` 里导入一下试试。 ## 配置 具体参考[这里](https://shiki.style/packages/vitepress)。 ### 安装依赖 ```shell pnpm add -D @shikijs/vitepress-twoslash ``` ### 修改配置 `theme-hope` 主题配置: ```ts {10} import { transformerTwoslash } from '@shikijs/vitepress-twoslash' export default hopeTheme({ plugins: { shiki: { themes: { light: 'one-light', dark: 'one-dark-pro', }, transformers: [transformerTwoslash()], }, }, }) ``` `vuepress` 主题配置: ```ts // .vuepress/config.ts import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' import '@shikijs/vitepress-twoslash/style.css' export default defineClientConfig({ enhance: ({ app }) => { // twoslash app.use(TwoslashFloatingVue) }, }) ``` ### 其他修改 不幸的是,上述配置后,未能达到想定的效果。 `v-menu` 被原样输出到 html 中,而没有被渲染为 vue 组件。 原本以为是 `app.use(TwoslashFloatingVue)` 未起作用,后来各自调试后发现不是。 参考了 `vuepress-theme-plume` 后,发现需要处理一下 markdown 的 `v-pre`。 如下所示: ```ts // .vuepress/theme/index.ts export default (options: ThemeOptions): Theme => ({ extendsMarkdownOptions: (options: MarkdownOptions, app: App) => { // 注入 floating-vue 后,需要关闭 代码块 的 v-pre 配置 if ((options as any).vPre !== false) { const vPre = isPlainObject((options as any).vPre) ? (options as any).vPre : { block: true } if (vPre.block) { ;(options as any).vPre ??= {} ;(options as any).vPre.block = false } } }, }) ``` 但是,这样做会导致有些页面出错,没有 `v-pre` 会导致编译出错。 最后查看了 vuepress 的 `v-pre` 后,发现可以使用 `:no-v-pre` 来避免这个问题。 同时,为了适配 `vuepress` , 将 `@shikijs/vitepress-twoslash` 里的代码复制了一份,然后修改了一点里的组件和 css。 ## 使用方法 使用方法:在需要启用 `twoslash` 的地方,同时设置 `:no-v-pre` 和 `twoslash`。 如下所示: ````md ```ts:no-v-pre twoslash console.log('hello') // ^? ``` ```` ## 示例 ### TypeScript 示例 ```ts:no-v-pre twoslash console.log('hello') // ^? ``` ### Vue 单文件组件示例 ```vue:no-v-pre twoslash ``` --- --- url: 'https://zhaobc.site/slides/demo.md' --- # 幻灯片页 @slidestart ## 幻灯片演示 一个简单的幻灯片演示与各种小贴士。 > 作者 Mr.Hope. 请滚动鼠标滚轮进入下一页 *** ## 标注幻灯片 [👇](#/1/1) \-- ## 标注幻灯片 使用 `---` 标注水平幻灯片 在水平幻灯片中使用 `--` 分割垂直幻灯片 使用 `` 在幻灯片上添加属性 使用 `` 在前一个 HTML 元素上添加属性 *** ## Markdown 你可以在幻灯片中使用 Markdown 语法的各种标记. \-- ## Markdown 你可以在幻灯片中使用 Markdown 语法的各种标记. ### 这是一个 H3 标题默认会自动转换为大写。 这是一个有着 **粗体**, *斜体*, ~~删除线~~ 文字并包含 [一个链接](https://mrhope.site) 的段落,并且它会自动换行。所以你无需担心它的长度。 \-- ## Markdown 你可以在幻灯片中使用 Markdown 语法的各种标记. 列表默认为 `inline-block` * 项目 * 项目 * 项目 1. 项目 1 2. 项目 2 3. 项目 3 \-- ## Markdown 你可以在幻灯片中使用 Markdown 语法的各种标记. 在你启用 `highlight` 插件后,代码块会自动高亮。 ```js const a = 1 ``` \-- ## Markdown 你可以在幻灯片中使用 Markdown 语法的各种标记. 在你启用 `math` 插件后,你也可以使用 TEX 格式使用数学公式。 $$ J(\theta\_0,\theta\_1) = \sum\_{i=0} $$ \-- ## Markdown 你可以在幻灯片中使用 Markdown 语法的各种标记. ⚠**请注意**: 表格和分割线,以及所有不在 Markdown 标准语法中的内容均不受支持。 *** ## 布局 \-- ## 布局 👆 `r-fit-text` class 会让文字在不超出幻灯片范围的情况下尽可能大。 \-- ## 布局 ![Logo](/logo.svg) 👆 `r-stretch` class 帮助你控制注入图片或视频的大小,使它们填充满幻灯片垂直方向上的剩余空间。 \-- ## 布局 ### 背景 你可以通过向特定幻灯片添加 `data-background` 属性自定义幻灯片背景. *** ## 动画片段 \-- ## 动画片段 动画片段用于高亮或显隐幻灯片中的元素。 你需要在元素上添加 `fragment` 和动画 class。 \-- ## 动画片段 ### 动画 class * `fade-in` - `fade-out` * `fade-up` - `fade-down` * `fade-left` - `fade-right` * `fade-in-then-out` - `fade-in-then-semi-out` \-- ## 动画片段 ### 动画 class * `grow` - `shrink` * `strike` - `highlight-red` * `highlight-green` - `highlight-blue` * `highlight-current-red` - `highlight-current-green` * `highlight-current-blue` \-- ## 动画片段 ### 多个动画片段 你可以按照顺序包裹一个 HTML 元素使其拥有多个动画片段 \-- ## 动画片段 ### 顺序 你可以使用 `data-fragment-index` 属性改变元素的动画顺序。 不同元素可以有相同的动画顺序。 * 最后显示 - 第二个显示 * 第一个显示 - 第二个显示 *** ## 渐变 \-- ## 渐变 Transition 可以通过配置中的 `transition` 选项全局设置,也可以通过在特定幻灯片添加 `data-transition` 属性局部设置. 可能的值: * none * fade * slide - convex - concave - zoom \-- ## 渐变 ### 过渡动画 你可以在相邻的幻灯片上添加 `data-auto-animate` 使相同的 HTML 元素产生过渡动画效果。 *** ## 功能 \-- ## 功能 ### 代码 通过启用 `highlight` 插件,你可以对代码块进行高亮。 你可以使用 `[a-b|c-d]` 语法来分布高亮特定行。 ```js [1-2|3|4] let a = 1 let b = 2 let c = x => 1 + 2 + x c(3) ``` \-- ## 功能 ### 预览模式 按下 `Esc` 或 `O` 即可在幻灯片获得焦点时进入预览模式。 \-- ## 功能 ### 全屏模式 按下 `F` 或 `F11` 即可在幻灯片获得焦点时进入全屏模式。 \-- ## 功能 ### 缩放 按下 `alt` (Linux 上使用 `ctrl`) 的同时点击幻灯片的任何元素,即可以向此元素进行放大。 再次点击即可缩小。 *** ## 结束 @slideend