Compare commits

..
12 Commits
12 changed files with 1726 additions and 4 deletions

No files matched your search

+3 -1
View File
@@ -1 +1,3 @@
node_modules
node_modules
test.*
.vscode
+7
View File
@@ -1,5 +1,12 @@
# 与星网项目相关
* 写项目时注意的:
- 使用如loading这样可能可以复用的函数之前,记得去hook里面看看有没有,没必要什么都去自己实现。
- 静态资源使用css去展示,不要import进去使用,能复用就复用,不能复用的话用tinypng去压缩一下。
- 组件能不用就不要用,不然会造成一定的性能消耗(其实感觉也不是很多但是这样做就对了)。
- 接口请求写在生命周期里,不要用闭包游离出去。
---
* 如何去添加一个新的页面(addTabPage)
可参考以下部分添加内容(部分文件、文件夹自行创建):
frame\frame.config.js
+227
View File
@@ -0,0 +1,227 @@
# 与 css 相关,多学习 css
## css inset
- 同时设置上下左右等多个属性值
- inset : top | right| bottom | left
- 使用例:
```css
.className {
inset: 4px 8px; /* 上下 | 左右 */
inset: 5px 15px 10px; /* 上 | 左右 | 下 */
inset: 2.4em 3em 3em 3em; /* 上 | 右 | 下 | 左 */
}
```
## css 过渡 transition
- 有 transition-property、transition-duration、transition-timing-function 和 transition-delay 这几个属性,不过一般使用时应该都是用 transition 合起来写。
- 使用例:
```css
.className {
/* property name | duration | timing function | delay */
transition: margin-right 4s ease-in-out 1s;
/* Apply to 2 properties */
transition: margin-right 4s, color 1s;
}
```
## css 动画 滚动条驱动
- animation-timeline
- 有时间再写
## css 动画
- 分为“定义”与“使用”两个阶段。
- “定义”阶段的 keyword 为@keyframes,例:
```scss
// 每个时间点填对应时间点的style
@keyframes animationName {
0% {
border-radius: 1px;
}
50% {
border-radius: 12px;
}
100% {
border-radius: 123px;
}
}
```
- “使用”阶段的 keyword 为 animation,使用 animation 绑定动画,例
```scss
div {
animation: animationName 5s;
}
```
- animation 有许多属性
```scss
div {
// 绑定的动画名
animation-name: animationName;
// 动画执行总时长
animation-duration: 5s;
// 速度曲线。默认是 "ease"。
animation-timing-function: linear;
animation-delay: 2s;
// 播放次数,默认1
animation-iteration-count: infinite;
// 是否在下一个周期逆向播放
animation-direction: alternate;
// 是否正在播放()
animation-play-state: running;
// 简写
animation: animationName 5s linear 2s infinite alternate;
// 多个动画之间使用逗号进行隔开
animation: a1..., a2...;
}
```
## before 与 after
- 伪元素,通过 content 来指定内容,before 就是在前面,after 就是在后面(哇好有道理呢)
- 用途一般是加个 icon 啥的,content 可以为 url(...)
- 不能应用在替换元素上,比如 `<img>`
- 伪元素不支持嵌套,也就是说伪元素的父元素不可以为伪元素
使用例:
```css
a {
color: #0000ff;
text-decoration: none;
}
a::before {
content: '🔗';
}
```
- 使用 inherit 使子元素拥有与父元素相同的特定属性值
使用例:
```css
div {
position: relative;
background: url('your-image-url.jpg');
}
div::before {
content: '';
position: absolute;
top: 0;
left: 0;
background: inherit;
z-index: -1;
}
```
## !important 相关
- 不到万不得已不要用,是不好的写法
- 如果你的 important 不够 important,那可以写 class 的时候多写点前缀,例:
```scss
// 一般
.ant-slider-rail {
background-color: rgb(141, 93, 56) !important;
}
// 优先级特别高
.slider .ant-slider .ant-slider-rail {
background-color: rgb(141, 93, 56) !important;
}
```
## xx[a=xxx]的使用
- 这玩意叫属性选择器
- 案例:
```css
a {
color: blue;
}
/* 以 "#" 开头的页面内部链接 */
a[href^='#'] {
background-color: gold;
}
/* 包含 "example" 的链接 */
a[href*='example'] {
background-color: silver;
}
/* 包含 "insensitive" 的链接,不区分大小写 */
a[href*='insensitive' i] {
color: cyan;
}
/* 包含 "cAsE" 的链接,区分大小写 */
a[href*='cAsE' s] {
color: pink;
}
/* 以 ".org" 结尾的链接 */
a[href$='.org'] {
color: red;
}
/* 以 "https" 开始,".org" 结尾的链接 */
a[href^='https'][href$='.org'] {
color: green;
}
```
- 属性不一定要是 href 什么的,自己定也没关系。
- 案例:
```css
/* 将所有包含 `lang` 属性的 <div> 元素的字重设为 bold */
div[lang] {
font-weight: bold;
}
/* 将所有语言为美式英语的 <div> 元素的文本颜色设为蓝色 */
div[lang~='en-us'] {
color: blue;
}
/* 将所有语言为葡萄牙语的 <div> 元素的文本颜色设为绿色 */
div[lang='pt'] {
color: green;
}
```
## grid
```css
.container {
display: grid;
grid-template-columns: repeat(2, 100px 20px 80px);
/* repeat 配合auto-fill/auto-fit 配合minmax 实现自适应布局 */
grid-template-columns: 1fr 1fr minmax(100px, 1fr);
grid-row-gap: 20px;
grid-column-gap: 20px;
/* 先行后列,尽可能填满 */
grid-auto-flow: row dense;
}
.item-1 {
/* 边框所对应的网格线 */
/* grid-column属性是grid-column-start和grid-column-end的合并简写形式,grid-row属性是grid-row-start属性和grid-row-end的合并简写形式。 */
grid-column-start: 2;
grid-column-end: 4;
grid-column: 2/4;
grid-row: 1/2;
}
```
## point-event
当为none时,所有点击事件无效
##
+101
View File
@@ -0,0 +1,101 @@
# 杂谈 记录一些碎碎念
---
npx 用来临时使用一个库(如 rollup)
---
强缓存和协商缓存的 header 参数
强缓存:
Expires:过期时间,如果设置了时间,则浏览器会在设置的时间内直接读取缓存,不再请求
Cache-Control:当值设为 max-age=300 时,则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存。
cache-control:除了该字段外,还有下面几个比较常用的设置值:
(1) max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;
(2) s-maxage:和 max-age 是一样的,不过它只针对代理服务器缓存而言;
(3)public:指示响应可被任何缓存区缓存;
(4)private:只能针对个人用户,而不能被代理服务器缓存;
(5)no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收到 请求,然后判断资源是否变更,是则返回新内容,否则返回 304,未变更。这个很容易让人产生误解,使人误 以为是响应不被缓存。实际上 Cache-Control: no-cache 是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。
(6)no-store:禁止一切缓存(这个才是响应不被缓存的意思)。
---
mvvm:
vm: 关注绑定器,将数据和视图绑定,自动更新,低耦合。model仅关注数据更新
---
垃圾回收相关:
window.gc: 手动调用垃圾回收
现代主要使用标记清除法。
进入执行环境的时候标记上。
离开环境的时候直接回收。
常见的内存泄漏:
全局变量,解决方式: 全局变量
定时器未清除:记得及时清除
闭包: 解决方式: 及时设置为null
dom元素删除后未清除: 解决方式: 及时设置为null
栈(Stack)
特点:栈是一种后进先出(LIFO, Last In First Out)的数据结构。
存储内容:用于存储简单的数据类型(如数字、字符串、布尔值)和函数调用上下文。
内存分配和释放:内存分配和释放非常快速,直接在栈顶操作。函数调用结束后,栈帧自动释放。
作用:主要用于管理函数调用和局部变量。
堆(Heap)
特点:堆是一种无序的数据结构,用于动态分配内存。
存储内容:用于存储复杂的数据类型(如对象、数组)。
内存分配和释放:内存分配比较灵活,但管理和回收内存需要垃圾回收机制(GC, Garbage Collection)来处理。
作用:用于存储需要动态分配的较大数据块和复杂数据结构。
---
babel相关:
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
---
es6几个主要的:
1. let, const
2. 箭头函数
3. class
4. 模板字符串
5. 解构
6. 函数传参默认值&rest
---
http响应头请求头
请求头:
content-type: 请求的数据的类型
Authorization: 身份验证,如JWT这样的
UA: 浏览器环境
响应头:
set-cookie
content-length
cache-control
跨域cookie需要服务器进行设置
---
SEO优化
1. 大量使用语义化标签
2. 保证关键词覆盖率,title之类,配置meta信息
3. 制定一个站点地图,放在“robot.txt”中
4. 结构化数据
+192
View File
@@ -0,0 +1,192 @@
# 以下是关于 ts(以及任何关于 ts 的不限于语法,技巧之类的学习心得)
## any unknown void never
- any 表示任意类型。这导致不会对类型进行任何检查,用太多不太好就是了。
- unknown 表示位置类型。看作 any 的安全版本。可以赋值为任何类型,但在使用之前必须进行类型检查或类型断言。
- void 代表没有返回值。
- never 代表永远不会有任何返回值,如死循环或抛出异常。
## 接口 interface
- 基本可以认为是声明 object 类型用的东西
## 泛型 <T>
- 实际上可以理解为是一种类型的变量
- 如:
```ts
function identity<T>(arg: T): T {
return arg
}
// 使用泛型函数
let result = identity<string>('Hello')
console.log(result) // 输出: Hello
let numberResult = identity<number>(42)
console.log(numberResult) // 输出: 42
// 基本语法
interface Pair<T, U> {
first: T
second: U
}
// 使用泛型接口
let pair: Pair<string, number> = { first: 'hello', second: 42 }
console.log(pair) // 输出: { first: 'hello', second: 42 }
```
## 箭头函数与普通函数的区别
- 箭头函数没有 yield
- 无 prototype
- 没有 this 和 arguments
## 使用 Object.freeze()
- Object.freeze() 可以冻结一个对象,使其不能添加、删除或修改属性。
```ts
const obj = { key: 'value' };
Object.freeze(obj);
obj.key = 'new value'; // 无效
obj.newKey = 'new value'; // 无效
delete obj.key; // 无效
console.log(obj);
```
## 使用 Object.defineProperty()使对象某一 key 其不可变
- Object.defineProperty() 可以定义对象属性,并使其不可变。
```ts
const obj = {};
Object.defineProperty(obj, 'key', {
value: 'value',
writable: false,
configurable: false // 不可配置的
});
obj.key = 'new value'; // 无效
delete obj.key; // 无效
console.log(obj); // 输出: { key: 'value' }
```
## Object.seal()。
- Object.seal() 可以让对象不能添加或删除属性,但允许修改现有属性的值。
```ts
const obj = { key: 'value' };
Object.seal(obj);
obj.key = 'new value'; // 有效
obj.newKey = 'new value'; // 无效
delete obj.key; // 无效
console.log(obj);
```
## 判断是否为数组
- Array.isArray()
- instanceof Array
- obj.**proto** === [].**proto**
## 是否为 NaN
- Number.isNaN()
## obj 可迭代
- 实现了迭代器协议(拥有 Symbol.iterator 方法)实现了迭代器协议(拥有 Symbol.iterator 方法)
```ts
const iterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
}
```
## promise新的
- Promise.race(iterable) 接受一个可迭代对象,返回第一个解决或拒绝的 Promise 的结果。
- Promise.allSettled(iterable) 接受一个可迭代对象,返回一个 Promise,所有 Promise 无论解决或拒绝都会返回结果。
- Promise.any(iterable) 接受一个可迭代对象,返回第一个成功解决的 Promise 的结果。如果所有 Promise 都被拒绝,则返回一个拒绝的 Promise。
## deepReadonly
```ts
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}
```
## callable的对象
```ts
const callableObject = {
[Symbol('callable')](...args) {
console.log('Object called with args:', ...args);
// 执行一些操作并返回结果
return args.reduce((sum, val) => sum + val, 0);
}
}
```
## 异步迭代
```ts
const asyncIterable = {
async *[Symbol.asyncIterator]() {
yield await new Promise(resolve => setTimeout(() => resolve(1), 1000));
yield await new Promise(resolve => setTimeout(() => resolve(2), 1000));
yield await new Promise(resolve => setTimeout(() => resolve(3), 1000));
}
};
(async function() {
for await (const value of asyncIterable) {
console.log(value); // 输出: 1, 2, 3,每个值之间间隔1秒
}
})
```
---
二进制/文件处理
blob: 一种类文件对象,只能使用fileReader进行异步读取,读取为arrayBuffer
arraybuffer: 一种通用的二进制数据缓冲区,需要通过视图(如uint8array)进行修改
---
关于闭包,有逻辑的回答:
闭包(closure)指有权访问另一个函数作用域中变量的函数。 ----- JavaScript 高级程序设计
闭包有什么用?
延伸变量作用域范围,读取函数内部的变量
让这些变量的值始终保持在内存中
简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量。
---
weakMap
key必须为一个obj
可以用于给一个obj附加一些生命周期相同的数据内容(如缓存)
+66 -3
View File
@@ -1,8 +1,19 @@
# 与vue项目相关
# 与vue项目相关,其实也不止vue,js之类的都算其中.
---
# 路由返回拦截hook
# 子子组件之间的通信可以通过事件总线(eventBus)实现
---
# doucument.createElement出来的东西不及时销毁就会一直游离在外面不被释放,积累下来很不好。(似乎是错误的)
---
# 组件的设计
组件设计可以分成业务组件,展示组件,展示组件只负责以对应的形式展示对应的数据,对于业务的处理(如增删查改),则需要通过emit或其他方法交给业务组件处理
---
# 路由返回拦截hook(路由守卫)
* 当点击返回键(浏览器或者是页面上的路由返回功能)时触发,形式如下:
```ts
import { onBeforeRouteLeave } from 'vue-router'
@@ -17,7 +28,7 @@ onBeforeRouteLeave((to, from, next) => {
next(true)
})
```
* 当next(false时,取消路由原有的返回功能)
* 当next===false时,取消路由原有的返回功能
---
## 具名作用域插槽
* 具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到
@@ -72,4 +83,56 @@ example:
</div>
</template>
</el-popover>
```
# 路由相关:
* 路由守卫
- - beforeEach 全局前置守卫
作用:
主要用于在路由跳转前进行权限验证、页面加载状态的设置等操作。例如,判断用户是否已经登录,如果没有登录且要访问需要登录才能访问的页面,就可以将用户重定向到登录页面。
它可以对每一次路由跳转进行全局拦截,是控制路由访问权限的重要手段。
- - afterEach全局后置守卫
作用:
它在路由跳转完成后被调用,主要用于记录页面访问历史、修改页面标题或者进行一些不需要阻塞路由跳转的操作。
例如,根据不同的路由来更新文档的标题,或者发送页面访问统计数据到服务器。
- - 全局解析守卫(beforeResolve)的触发时机
全局解析守卫beforeResolve在路由导航被触发后,在所有组件内守卫和异步路由组件被解析之前调用。它是在beforeEach之后,但是在组件实例创建和beforeRouteEnter等组件内守卫之前执行。这意味着它处于路由导航过程的一个中间阶段,此时路由已经被匹配,但组件还没有完全解析和进入。
主要作用
数据预获取:
可以用于获取组件渲染所需的数据。与beforeEach不同的是,beforeResolve更专注于确保在组件真正开始渲染之前,所有必要的数据都已经获取并准备好。例如,对于一个包含多个嵌套组件的复杂页面,其中每个组件都需要从服务器获取数据,beforeResolve可以协调这些数据获取操作,确保所有数据都准备好后再进行组件渲染。
处理异步操作的集中点:
它为处理整个路由导航过程中的异步操作提供了一个集中的位置。在大型应用中,可能会有多个异步操作需要在路由跳转时完成,如加载第三方脚本、初始化插件等。beforeResolve可以用于确保这些异步操作都顺利完成后再继续路由解析过程。
- - 组件路由守卫
```vue
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
// 与 beforeRouteLeave 相同,无法访问 `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// 取消导航并停留在同一页面上
if (!answer) return false
})
const userData = ref()
// 与 beforeRouteUpdate 相同,无法访问 `this`
onBeforeRouteUpdate(async (to, from) => {
//仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
</script>
```
+34
View File
@@ -0,0 +1,34 @@
# 常用的命令
## 初始化项目
```shell
cargo new hello_world
```
## 添加依赖
```shell
cargo add xxx
```
## 检查项目
```shell
cargo check
```
## 运行项目
```shell
cargo run
```
## 编译项目
```shell
cargo build
```
## 发布项目 (编译时间更长,但是会做一些优化)
```shell
cargo build --release
```
+547
View File
@@ -0,0 +1,547 @@
# Rust 语法学习
## 用例 1:猜数游戏
生成一个随机 1-100 的数并让玩家猜测
```rust
// rust默认隐式引入prelude
// 除了prelude以外都要显式引入
use rand::random;
fn main() {
// 使用常量定义一个偏置量
// 常量在编译时就已经确定,所以不能通过如CONST XX = XX()来初始化
// let声明的不可变变量虽然不可变,但是在编译时默认为未知的。
const BIAS: u8 = 1;
println!("guess number game");
// let: 声明变量,默认不可变
// mut:声明变量可变
// String::new(),冒号表示这是一个String类的关联函数
// 关联函数就是类似于静态方法
let secret_number = random::<u8>() + BIAS;
println!("secret number is {}", secret_number);
// mut使得变量可变
let mut user_guess: u8 = 0;
let mut user_input = String::new();
while user_guess != secret_number {
// 清空之前的输入
user_input.clear();
// io::stdin():标准输入
// read_line:读取一行
// &:取地址符号,表示引用,默认不可变
// &mut:声明可变引用
std::io::stdin().read_line(&mut user_input).expect("read fail");
// 将输入的字符串转换为数字
// user_guess = user_input.trim().parse().expect("parse fail");
// 使用 match 表达式判断用户输入的是否为数字
// 比使用if else更简洁
user_guess = match user_input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("please input a number");
continue;
}
};
// 判断用户输入的数字与随机数的大小关系
// if user_guess > secret_number {
// println!("user number is too large");
// } else if user_guess < secret_number {
// println!("user number is too small");
// } else {
// println!("user number is correct");
// break;
// }
// 使用 match 表达式判断用户输入的数字与随机数的大小关系
// 比使用if else更简洁
match user_guess.cmp(&secret_number) {
std::cmp::Ordering::Greater => println!("user number is too large"),
std::cmp::Ordering::Less => println!("user number is too small"),
std::cmp::Ordering::Equal => {
println!("user number is correct");
break;
}
}
println!("user number is {}", user_input);
}
}
```
## 特性: shadow
在同一个作用域内,可以重复声明同名的变量,会覆盖之前的变量
```rust
fn main() {
let x = 5;
// 重复声明同名的变量,会覆盖之前的变量
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x); // 12
}
```
## 类型
### 基本类型
#### 整数类型
- 整数:i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
- 整数类型范围:
- 有符号:-2^n+1 ~ 2^(n-1)-1
- 无符号:0 ~ 2^n-1
- isize, usize:取决于运行程序的计算机的架构
- 表示
- hex: 0x12345678
- oct: 0o12345678
- bin: 0b1010_1010
- Byte: b'A'
#### 浮点数类型
- 浮点数:f32, f64, 默认是 f64
- 表示
- 科学计数法:1.23e3
- 小数点:1.23
#### 布尔类型
- 布尔类型:bool
- 表示
- true, false
#### 字符类型
- 字符类型:char
- 表示
- 'a', 'A', '1', '中'
- 单引号表示
- 占用 4 字节
### 复合类型
#### 元组类型(Tuple)
- 可以将多个类型的多个值放在一个类型里
- 长度是固定的,一旦声明就无法改变
- 声明例: let tup: (i32, f64, u8) = (500, 6.4, 1);
- 访问例 1: let (x, y, z) = tup;
- 访问例 2: let x = tup.0;
#### 数组类型(Array)
- 可以将多个类型的多个值放在一个类型里
- 长度也是固定的,一旦声明就无法改变
- 想要使用有固定数量的元素时使用数组更有好处
- 声明例: let arr: [i32; 5] = [1, 2, 3, 4, 5];
- 声明例 2: let arr = [3; 5]; // 表示数组有 5 个 3
- 访问例: let x = arr[0];
- 没有 Vector 灵活
## 函数
函数名小写,单词间使用下划线进行分割
例子:
```rust
fn f(x: i32, y: i32) -> i32 {
// 这里不加分号,因为是表达式,会返回值
// 函数默认使用最后一个表达式作为返回值
x + y
}
fn f2(x: i32, y: i32) {
let z = {
x + y + 1
};
z
}
```
## 控制流
### if 表达式
- 如果使用多于一个的 if-else,最好使用 match 对代码进行重构
- if 表达式是一个表达式,所以可以将其赋值给一个变量
```rust
fn main() {
let x = 5;
if x < 5 {
println!("x is less than 5");
} else if x > 5 {
println!("x is greater than 5");
} else {
println!("x is 5");
}
// 使用 match 对代码进行重构
let x = 5;
match x.cmp(&5) {
std::cmp::Ordering::Less => println!("x is less than 5"),
std::cmp::Ordering::Greater => println!("x is greater than 5"),
std::cmp::Ordering::Equal => println!("x is 5"),
}
let y = if x < 5 {
1
} else {
2
};
}
```
### 循环
```rust
fn main() {
let mut x = 0;
while x < 5 {
println!("x is {}", x);
x += 1;
}
x = 0;
// 使用 .. 表示一个范围,不包含右边的数
// 实际的类型是std::ops::Range<{integer}>
// 可以通过(1..5).rev() 表示一个倒序的范围
for x in 0..5 {
println!("x is {}", x);
}
const A: [i32; 5] = [1, 2, 3, 4, 5];
// iter 方法返回一个迭代器,迭代器是一个可以遍历集合的对象
for x in A.iter() {
println!("x is {}", x);
}
x = 0;
loop {
println!("x is {}", x);
x += 1;
if x > 5 {
break;
}
}
// loop 表达式是一个表达式,所以可以将其赋值给一个变量
let result = loop {
if x > 5 {
break x * 2;
}
x += 1;
};
}
```
## 所有权
- 内存管理分为两个部分:栈和堆
- 所有权是 Rust 最独特的特性之一
- 在 Rust 中,每个值都有一个变量,这个变量被称为所有者
- 每个值在任意时刻有且只有一个所有者
- 当所有者离开 scope(作用域),这个值将被删除
### 作用域
例:
```rust
fn main() {
{
let x = 5;
println!("x is {}", x);
}
// x 已经离开作用域
}
```
### copy trait
- 赋值要么发生移动,要么发生复制
- Rust 中的基本类型都实现了 copy trait
- 如果一个类型实现了 copy trait,则旧的变量在赋值后仍然可以使用
- 否则就像下面的 String 一样,赋值以后旧的变量会失效
- 内容全为已实现 copy 的 tuple 也是可 copy 的
### 所有权和函数
- rust 函数传参与赋值操作类似
- 所以可能发生移动,使得一些变量失效
- 返回值时所有权也很发生移动或复制
- 一个变量的所有权总是遵循同样的模式
- 把一个值赋给其他变量的时候会发生移动
- 当一个包含 heap 数据的变量离开作用域时。这个值就会被 drop 函数删除
- 除非其所有权发生了转移
例:
```rust
fn main() {
let s = String::from("hello");
takes_ownership(s);
// 这里会报错,因为s已经失效了
// println!("{}", s);
let x = 5;
makes_copy(x);
// 这里不会报错,因为x仍然可以使用
println!("{}", x);
let s1 = generates_string();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
println!("{}", s1);
// 这里会报错,因为s2已经失效了
println!("{}", s2);
println!("{}", s3);
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
fn generates_string() -> String {
let s = String::from("hello");
s
}
fn makes_copy(x: i32) {
println!("{}", x);
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
```
## 引用和借用
- 引用是一个指向某个值的指针
- 引用不会获取到值的所有权
- 引用的变量默认是不可变的
- 引用的变量可以通过 mut 关键字声明为可变的
- 特定的作用域内,一个变量只能有一个可变引用
- 不可同时拥有可变引用与不可变引用
- 不可变引用可以有多个
- rust 在编译时会防止悬空指针的出现
例
```rust
fn main() {
let s1 = String::from("hello");
// 这里的s2是一个引用,指向s1
let len = calculate_length(&s1);
change_string(&mut s1);
println!("{}", s2);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
fn change_string(s: &mut String) {
s.push_str(", world");
}
```
## slice 切片
- slice 是一个引用,指向一个字符串/数组的一部分
- 定义函数时使用字符串切片(&str)来代替字符串引用会使我们的 api 更加通用且不会损失任何功能
```rust
fn main() {
let s = String::from("hello world");
let wordIndex = first_word(&s);
let hello = &s[0..5];
let world = &s[6..11];
// 这里会报错,因为clear会导致可变,但是 s已被不可变引用
s.clear();
let a: [u8; 5] = [1, 2, 3, 4, 5];
let slice = &a[0..2];
}
fn first_word(s: &str) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[i..];
}
}
&s[..]
}
```
## String
- 相比于使用双引号创建的字符串,使用 String 类型可以动态地分配内存
- String 是一个可变的字符串类型
- String 是一个堆上的字符串,而不是栈上的字符串
- 底层实现上:
- String 是一个结构体,包含一个指向堆上的字符串的指针和一个长度,一个容量
- 结构体储存在栈上
- 真实的字符串储存在堆上
```rust
fn main() {
// 在heap上分配内存了
let mut s = String::from("hello");
// 和js类似,s1和s实际上是同一个字符串
// 但是因为一些所有权的问题,所以在s1生效以后,s就失效了
let s1 = s;
// 克隆消耗资源,但是原来的不会失效了
let s2 = s1.clone();
// 这样会报错,因为s已经失效了
s.push_str(", world");
s1.push_str(", world");
println!("s is {}", s1);
}
```
## struct 结构体
- 结构体是一个自定义的类型
- 一旦结构体是可变的,那么结构体的所有字段就是可变的
- 只要 struct 是有效的,则里面的引用也都是有效的
```rust
struct User {
name: String,
age: u8,
id: u32,
}
// tuple struct 元组结构体
struct Color(i32, i32, i32)
// 这样可以将结构体打印出来
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// impl关键字用于为结构体实现接口或者关联函数或者方法
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// 关联函数,不需要实例化就可以调用,通常用于创建结构体
// 类似于静态方法
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let id: u32 = 1;
let user = User {
name: String::from("hello"),
age: 18,
id, // 如果字段名和变量名相同,可以省略变量名
};
let user2 = User {
name: String::from("world"),
id: 2,
..user //struct更新语法
};
println!("user is {}", user.name);
let color = Color(255, 0, 0);
let rect = Rectangle {
width: 10,
height: 20,
};
let rect2 = Rectangle {
width: 5,
height: 10,
};
println!("rect is {:#?}", rect);
println!("rect area is {}", rect.area());
println!("rect can hold rect2? {}", rect.can_hold(&rect2));
}
```
## 枚举
- 枚举是一个自定义的类型
例
```rust
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6((String::from("::1")));
}
```
## Option
- Option 是一个枚举类型,表示一个值可能不存在
- 包含在预导入库中,可以直接使用
```rust
enum Option<T> {
Some(T),
None,
}
```
- 如 let x: Option<i32> = None;
- 通过unwrap方法可以获取到Some的值
## match
- match可以提供一种匹配
```rust
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}
fn main() {
let coin = Coin::Quarter(UsState::Alaska);
println!("value_in_cents is {}", value_in_cents(coin));
}
```
+200
View File
@@ -1,6 +1,206 @@
## 记录在星网的每日目标
# 2024.8.16
## 工作目标
* 初步完成创作者中心的作品分析页面基本布局。 (100%)
* 完成今日todo后抓紧时间继续重写作品创编组件。 (21%)
## 学习目标
* 继续看front-design中play-song的实现,准备接下来的工作 (好多啊)
# 2024.8.15
## 工作目标
* 按导师要求继续重写作品创编组件 (20%)
## 学习目标
* 看下antd的具体使用方法,准备明天的工作。 (100%)
* 复习react的基础语法,准备明天的工作。 (100%)
* 继续看front-design中play-song的实现,准备接下来的工作 (好多啊)
* 继续看front-design中play-creation-simple-play的实现,准备接下来的工作 (好多啊)
# 2024.8.14
## 工作目标
* 按导师要求重写作品创编组件 (20%)
## 学习目标
* 继续看front-design中play-song的实现,准备接下来的工作 (好多啊)
* 继续看front-design中play-creation-simple-play的实现,准备接下来的工作 (好多啊)
# 2024.8.13
## 工作目标
* 完善之前做的作品创编组件 (60%)
* 移植作品创编工具组件到作品创编组件中 (不能工作)
* 修复作品创编工具组件不能工作的问题 (定位到bug)
* 整理bug目录交给导师 (整理了12个)
# 2024.8.12
## 工作目标
* 完善之前做的作品创编组件 (50%)
* 移植选择音色的下拉框到作品创编组件
## 学习目标
* 继续看front-design中play-song的实现,看里面关于如何切换midi音色的实现
* 了解sheetMusicBusiness是如何初始化的
# 2024.8.09
## 学习目标
* 继续看front-design中play-song的实现,准备接下来的工作 (好多啊)
# 2024.8.08
## 工作目标
* 给那个播放组件添加一个声部演奏的功能并完善使其正常工作 (通过验收^_^)
# 2024.8.07
## 工作目标
* 给那个播放组件添加一个声部演奏的功能 (还在熟悉组件中,功能上尚未实现)
## 学习目标
* 看front-design中play-song的实现,准备接下来的工作
* 整理导师教的内容
# 2024.8.06
## 工作目标
* 保证昨天做的表单功能正常 (接口已和后端给的一致,后端还未实现,后续有待检测)
## 学习目标
* 看front-design中play-song的实现,准备接下来的工作
# 2024.8.05
## 工作目标
* 给星网后台的一个上传音乐的表单里面添加一个上传声部的内容。 (样式实现,具体功能未实现)
* 在上传音乐的表单内内嵌一个自定义的上传队列显示组件。 (样式实现,功能不完整)
## 学习目标
* 看下这个项目是怎么写的 (antDesignVue的风格,还以为是react吓死了)
# 2024.8.02
## 工作目标
* 根据设计稿完善歌曲创编界面的大致demo。 (除了进度条其他的都差不多ok了)
## 学习目标
* 继续看front-design这个组件里面的各个部分的实现以照猫画虎完成工作。
# 2024.8.01
## 工作目标
* 根据设计稿做出歌曲创编界面的大致demo。 (功能基本完整)
## 学习目标
* 看front-design这个组件里面的各个部分的实现以照猫画虎完成工作。
# 2024.7.31
## 工作目标
* 修测试提出的bug (今天没收到问题,估计我的部分测试完成了吧)
## 学习目标
* 看css实现滚动驱动动画
* 看tsx语法以便接入周四周五的工作 (还好前几周空余的时间看了react的一些基础,虽然和vue的tsc语法差距较大,不过好在也有所帮助)
# 2024.7.30
## 工作目标
* 修测试提出的bug (今天没收到问题)
## 学习目标
* 看css实现滚动驱动动画。
* 等导师说明任务。
# 2024.7.29
## 工作目标
* 修测试提出的bug (今天没收到问题)
## 学习目标
* 学cordova打包apk文件 (打包出来没法发送http请求,已玉玉)
* 再复习了下ts的一些内容
# 2024.7.26
## 工作目标
* 修各种各样之前留下的bug (给的问题都解决了)
## 学习目标
* 看node.js打包成单个可执行文件的几种不同方法 (除了pkg其他的打包出来都有问题)
# 2024.7.25
## 工作目标
* 台风天电脑在公司
## 学习目标
* 看node.js相关的内容
# 2024.7.24
## 工作目标
* 根据合作伙伴的反馈修改样式。 (100%)
## 学习目标
* 学css (还是跟着网上的范例写,主要是看动画方向和过渡方向)
# 2024.7.23
## 工作目标
* 完善福宝世界风格的singplay界面样式 (100%)
* 完善福宝世界风格的singplayback界面样式 (100%)
## 学习目标
* 学css (根据网上的范例做了一些css动画的demo)
# 2024.7.22
## 工作目标
* 部分完善福宝世界风格的singplay界面样式 (除了评分和没切图的部分都做好了)
* 剩下的等待他们给切图 (等)
## 学习目标
* 看css语法 (看了before和after)
# 2024.7.19
## 工作目标
* 继续完善活动详情页面和活动列表页面 (100%)
* 给活动页面添加异常处理 (100%)
* 活动详情的奖项一栏可能还要修改 (100%)
* 按照导师意见修改以下:
1. 图片压缩 tinypng (1)
2. 标题背景都要放在外面 跟之前一样 (1)
3. 二维码少2个 ,上下文件间距 鼠标hove有显示 (1)
4. index.vue 图片用css 主要修改 放下午做 (1)
5. useElLoading 已经有方法了 (1)
6. 有必要适用el-image吗? (1)
7. 二维码函数冗余 document.createElement('div') 会游离 (0.5)
8. window.ResizeObserver 看不懂 (已删,确实没啥用,奇怪了之前明明有问题的)
9. 接口 为啥不是在什么周期请求,然后详情为啥不在自己组件内部请求 (1)
10. 无关的代码片段删掉 (1)
11. 后端给什么api就用什么api (1)
## 学习目标
* 继续看react(其实想看kotlin) (还没看)(不要发tower上)
---
# 2024.7.18
## 工作目标
* 继续完善活动详情页面和活动列表页面 (100%)
* 给活动详情页面对接接口 (100%)
## 学习目标
* 粗略看下react的语法 (还没看反向数据流)(不要发tower上)
---
# 2024.7.17
## 工作目标
* 完善活动详情页面和活动列表页面 (100%)
* 给活动列表页面对接接口 (100%)
---
# 2024.7.16
## 工作目标
* 初步完成活动详情页面 (100%)
* 修改二维码组件,使其支持图标 (100%)
## 学习目标
* 看qrcodejs2实现 (中间一大段看不懂,粗略的实现过程了解了)
---
# 2024.7.15
## 工作目标
* 初步完成活动展示页面 (100%)
## 学习目标
---
# 2024.7.12
## 工作目标
* 完善回放界面以及相关组件组成 (100%)
* 完成作品保存功能相关逻辑修改 (100%)
## 学习目标
* 看活动页面设计 (100%)
---
# 2024.7.11
## 工作目标
* 按需求继续重新设计回放界面以及相关组件组成 (100%)
## 学习目标
* null
---
# 2024.7.10
## 工作目标
* 重新设计回放界面以及相关组件组成 (100%)
## 学习目标
* null
---
# 2024.7.10
## 工作目标
* 根据teach-webview完成回放界面 (100%)
+263
View File
@@ -0,0 +1,263 @@
# getUserMedia (获取设备音视频输入)
navigator.mediaDevices.getUserMedia(constraints?: MediaStreamConstraints)
获取用户设备上的输入,比如摄像头,麦克风,返回一个 mediaStream 对象
注意需要通过用户主动操作(如 click)唤起才行,否则会报错。
```ts
const getUserMedia = async (constraints?: MediaStreamConstraints) => {
if (!navigator.mediaDevices.getUserMedia) {
console.log('getUserMedia not supported');
return;
}
mediaStream.value = await navigator.mediaDevices.getUserMedia(constraints);
console.log(mediaStream.value, videoRef.value)
const track = mediaStream.value.getVideoTracks()[0];
console.log(track, track.getSettings())
videoRef.value && (videoRef.value.srcObject = mediaStream.value)
videoRef.value && (videoRef.value.play())
}
```
# getDisplayMedia (获取屏幕音视频输入)
navigator.mediaDevices.getDisplayMedia(constraints?: MediaStreamConstraints)
获取用户屏幕上的音视频,返回一个 mediaStream 对象
注意需要通过用户主动操作(如 click)唤起才行,否则会报错。
```ts
const getDesktopStream = async () => {
if (!navigator.mediaDevices.getDisplayMedia) {
console.log('getDisplayMedia not supported');
return;
}
mediaStream.value = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true,
});
console.log(mediaStream.value, videoRef.value)
videoRef.value && (videoRef.value.srcObject = mediaStream.value)
videoRef.value && (videoRef.value.play())
}
```
# mediaStream
mediaStream 主要表示一系列的音视频轨道
常用:
```ts
mediaStream.addTrack(track);
const tracks = mediaStream.getTracks();
const videoTrack = mediaStream.getVideoTracks()[0];
const audioTrack = mediaStream.getAudioTracks()[0];
```
# mediaStreamTrack
mediaStreamTrack 表示一个音视频轨道
常用属性:
```ts
mediaStreamTrack.id;// 轨道的唯一标识符
mediaStreamTrack.kind;// 轨道的类型,如 video、audio、text 等
mediaStreamTrack.label;// 轨道的标签, 如摄像头名称
mediaStreamTrack.enabled;// 轨道是否可用, false表示不可用, 可能是黑屏等
mediaStreamTrack.muted;// 轨道是否静音
mediaStreamTrack.getSettings();// 获取轨道的设置, 如分辨率、帧率、比特率等
mediaStreamTrack.getConstraints();// 获取轨道的约束,可能是由用户设置的
```
# mediaNegotiate 媒体协商
媒体协商用来交换双端的信息,如可接受的音视频编码等。通过信令服务器交换 sdp 信息。
sdp = Session Description Protocol,webRTC 中分为会话描述和媒体描述
具体流程如下:
1. 客户端 A 通过 getUserMedia 获取音视频输入
2. 客户端 A 通过 createOffer 创建一个 offer
3. 客户端 A 通过 setLocalDescription 设置本地描述
4. 客户端 A 通过 signalingChannel 发送 offer 给客户端 B
5. 客户端 B 通过 setRemoteDescription 设置远程描述
6. 客户端 B 通过 createAnswer 创建一个 answer
7. 客户端 B 通过 setLocalDescription 设置本地描述
8. 客户端 B 通过 signalingChannel 发送 answer 给客户端 A
9. 客户端 A 通过 setRemoteDescription 设置远程描述
10. 客户端 A 和客户端 B 可以开始通话了
代码实现如下:
```ts
// 设置ICE候选,在媒体协商之前
const configuration = {
iceServers: [
{
urls: 'stun:stun.example.com:3478'
},
{
urls: 'turn:turn.example.com:3478',
username: 'your_username',
credential: 'your_password'
}
]
};
// 假设已经有一个信令服务器
const signalingChannel = {...};
const mediaNegotiateA = async () => {
const peerConnection = new RTCPeerConnection(configuration);
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
stream.getTracks().forEach(track => {
peerConnection.addTrack(track, stream);
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
console.log('offer', offer);
// 发送offer给B
signalingChannel.send(JSON.stringify(offer));
// 接收B的answer
const answer = await signalingChannel.receive();
console.log('answer', answer);
await peerConnection.setRemoteDescription(answer);
}
const mediaNegotiateB = async () => {
const peerConnection = new RTCPeerConnection(configuration);
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
stream.getTracks().forEach(track => {
peerConnection.addTrack(track, stream);
});
const offer = await signalingChannel.receive();
console.log('offer', offer);
await peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
console.log('answer', answer);
// 发送answer给A
signalingChannel.send(JSON.stringify(answer));
}
```
信令服务器只需要负责中继传递 offer 和 answer 即可。
可以使用如 ws 的方式自行实现。
# webRTC 网络穿越
## ICE
交互式连接创建(Interactive Connectivity Establishment,ICE)是一个允许你的浏览器和对端浏览器建立连接的协议框架。在实际的网络当中,有很多原因能导致简单的从 A 端到 B 端直连不能如愿完成。这需要绕过阻止建立连接的防火墙,给你的设备分配一个唯一可见的地址(通常情况下我们的大部分设备没有一个固定的公网地址),如果路由器不允许主机直连,还得通过一台服务器转发数据。
ICE中有控制方(发送offer的)和被控制方,最终地址选择由控制方决定。
具体工作流程如下:
1. 收集候选地址
1. 主机网卡实际地址host
2. 服务器反射候选者srflx(STUN返回的binding success response中的地址,即最外层NAT地址)
3. 中继候选者relay(TURN服务器的地址)
4. 对端反射候选者prflx(对端STUN返回的binding success response中的地址,无法主动获取)
2. 交换候选项
3. 开始连通性检测(一方发送binding request,一方返回binding response)
4. 选择地址从host到srflx/prflx到relay
## NAT 穿透
NAT类型:
完全圆锥型:外部主机可以通过映射后的公网 IP 地址和端口号,主动向内部主机发送数据包。
一旦内部主机的某个私有 IP 地址和端口号被映射到一个公网 IP 地址和端口号,任何外部主机都可以使用这个公网地址和端口号向内部主机发送数据。
受限圆锥型:外部主机只有先收到内部主机发送的数据包后,才能向内部主机发送数据。
具体来说,当内部主机通过 NAT 设备向外部某个主机发送数据包后,NAT 设备会记录这个外部主机的 IP 地址。之后,只有这个被记录的外部主机才能使用 NAT 转换后的公网 IP 地址和端口号向内部主机发送数据。
端口受限圆锥型:比起受限圆锥型,增加了外部主机的端口限制。
对称型:内部主机每次向不同的外部主机发送数据包时,NAT 设备都会为其分配不同的公网 IP 地址和端口号组合。且具有端口受限型的所有特征。
## STUN
STUN(Session Traversal Utilities for NAT,翻译为用于NAT的会话穿越工具)是一种用于 NAT 穿越的协议。它可以帮助 NAT 设备自动发现映射后的公网 IP 地址和端口号。
STUN的工作流程如下:
1. NAT探测阶段:STUN客户端向服务端发送一个binding请求,STUN服务端返回一个binding响应,其中携带MAPPED-ADDRESS、XOR-MAPPED-ADDRESS和RESPONSE-ORIGIN等属性。STUN客户端通过比较各个属性,确定其中是否存在NAT。
2. 打洞阶段:STUN客户端向STUN服务端请求其他STUN客户端的信息,通过交换NAT映射地址尝试实现P2P连接。客户端交替发送绑定请求,在各自的NAT设备上建立会话表项,当两端NAT都建立会话表项的时候,就可以建立P2P连接。
## TURN
TURN(Traversal Using Relays around NAT,翻译为围绕NAT使用中继穿越)是一种用于 NAT 穿越的协议。它可以帮助 NAT 设备通过中继服务器转发数据包,从而实现 NAT 穿越。
## 时机:
设置 ICE 候选人应该在进行媒体协商后进行。
# RTCPeerConnection
表示本地端和远程对等端之间的 WebRTC 连接。它提供了创建远程对等端连接、维护和监视连接,以及在连接不再需要时关闭连接的方法。
主要功能:
```ts
const configuration = {
// 设置ICE服务器
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'turn:your-turn-server-url',
username: 'your-username',
credential: 'your-password'
}
],
// ICE传输策略,可选值有'relay', 'public' 和'all',relay就是只中继
iceTransportPolicy: 'all',
// 当指定当远程对等点与 SDP BUNDLE 标准不兼容时,应如何处理候选的协商。
// balanced
// ICE 代理最初为每一种内容类型(音频、视频、数据通道)创建一个 RTCDtlsTransport。如果远程端点无法感知 BUNDLE,那么每一个 DTLS 传输用于处理一种数据类型的通信。
// max-compat
// ICE 代理最初为每个媒体轨道创建一个 RTCDtlsTransport,对数据通道则创建一个单独的传输。如果远程端点无法感知 BUNDLE,那么对于所有的内容都会协商一个单独的 DTLS 传输。
// max-bundle
// ICE 代理最初仅创建一个 RTCDtlsTransport 来承载所有的 RTCPeerConnection 的数据。如果远程端点无法感知 BUNDLE,那么仅会协商一个轨道而忽略其余的轨道。
bundlePolicy: 'max-bundle',
};
const pc = new RTCPeerConnection(configuration);
// 添加本地音视频流
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
// 设置完本地SDP后会触发
pc.onicecandidate = (event) => {
// 发送ICE候选给对端
signalingChannel.send(JSON.stringify(event.candidate));
};
// 设置远端SDP后会触发
pc.ontrack = (event) => {
// 接收对端的音视频流
videoRef.value.srcObject = event.streams[0];
};
```
+86
View File
@@ -0,0 +1,86 @@
```ts
import {
defineComponent,
watch,
onMounted,
onBeforeUnmount,
h,
getCurrentInstance,
type PropType,
} from 'vue-demi'
import ResizeObserver from './ResizeObserver'
import type { IOptions } from './types'
// const component = {
// name: 'Home',
// props:{
// data: String,
// },
// setup(props, context){
// // props.data
// // context.attrs context.slots context.emit
// }
// }
export default defineComponent({
name: 'ResizeObserverComponent',
props: {
target: [String, Object] as PropType<string | HTMLElement>,
disabled: Boolean as PropType<boolean>,
immediate: Boolean as PropType<boolean>,
delay: {
type: Number as PropType<number>,
default: 150
}
},
setup(props, { slots }) {
let ro: ResizeObserver | null = null
watch(() => props.disabled, (val) => {
if (val) {
ro?.disconnect()
} else {
ro?.observe()
}
})
onMounted(() => {
// const { target, disabled, limiter, wait } = toRefs(props)
// 拿到当前组件的实例(注意已挂载)
const vm = getCurrentInstance()?.proxy
// 先用$el
let $el: HTMLElement = vm?.$el as HTMLElement
if (props.target) {
if (typeof props.target === 'string') {
$el = document.querySelector(props.target) as HTMLElement
} else if (props.target instanceof HTMLElement) {
$el = props.target
}
}
const options: IOptions = {
delay: props.delay,
immediate: props.immediate,
resize: (data, elem) => {
vm?.$emit('resize', data, elem)
}
}
ro = new ResizeObserver($el, options)
})
// 销毁 ResizeObserver实例
onBeforeUnmount(() => {
if (ro) {
ro.destroy()
}
})
// 如果有单个子元素则直接渲染子元素
const defaultContent = slots.default?.()
return () => (
(defaultContent && defaultContent.length === 1)
? defaultContent[0]
: h('div', defaultContent)
)
}
})
```
View File
Whitespace-only changes.