读完这篇,Vue3 用到飞起~

在过去的一年新项目开发中,我都采用了 Vue 3+TypeScript 的技术栈。从去年刚了解Vue3和TypsScript 时的懵懵懂懂;到开发时使用时的别扭,随时想转过头去写Vue2;再到后来熟悉和适应它们,而到现在我已经离不开它们了。回头看这一年来的使用经历,我觉得有必要沉淀一下,因为在这当中有很多的疑惑是一直困扰我的,相信大家也会有类似的问题。

下面是内容会结合官方文档进行展开,对于 Vue 3的一些原理性的东西这里不会去做分析。

前排提示:内容很干,请备好茶水,读完一定会有收获。

目标人群:使用Vue3 、TypeScript开发过项目的开发者。

响应式 API⭐️

在 Vue 3 的 响应式 API 中有两对 API,它们的功能相近,看起来能取代对方。而对于它们的选择,每个开发者可能都会有自己的想法。

ref 还是 reactive?

响应式基础是捕获值的改变,Vue 3 使用了 Proxy 实现了值的代理,使值的读写能够进行被监听,进而触发副作用。 Vue 3基于这个原理,提供了两个 API 来获取响应式对象,那么它们有什么区别,而我们应该选择哪个呢?

javascript 复制代码
// 类型  
function ref<T>(value: T): Ref<UnwrapRef<T>>  
interface Ref<T> {  
  value: T  
}  
// 示例  
const count = ref(0)  
console.log(count.value) // 0  
count.value++  
console.log(count.value) // 1  
// 类型  
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>  
// 示例  
const obj = reactive({ count: 0 })  
obj.count++

这两个API 相同点就是功能相同,都能将值转换为一个可修改的响应式对象。ref接受任意的一个值,返回的响应式对象需要通过 .value访问;reactive一个接受一个对象,返回的响应式对象能够直接访问。它们的主要区别有两点:

  1. 入参类型不同。

  2. 返回的响应式对象使用方式不同,ref 返回的响应式对象使用较为繁琐。

  3. ref能作为组件引用。

就第一点区别来说,ref的能力更加强大,非对象类型的数据只能用 ref,而第二点区别来说, reactive 的返回值使用起来更加方便。

如果让我来选择的话,我会选择只用 ref 。 对我而言, ref 需要通过 value 属性访问到对象反而是优点,它会明确的告诉我,当前正在用的对象是一个响应式对象而不是普通对象,而且computed属性也是徐彤通过value属性进行访问的,使用 ref 行为更为统一。更何况 ref 的适用面更广。(至于说会忘记 使用 value属性来访问,麻烦开启TS检查。)

Vue 3 实验提供响应式语法糖,这样由编译器来为ref对象的访问添加 .value 。不过现在已经废弃了,原因大家猜!

watch 还是 watchEffect?

Vue 3提供了一个新的API -- watchEffect ,用来进行丰富依赖追踪。那么这个新的API 它和watch有什么区别呢?什么时候用watch什么时候用watchEffect呢?

watch接受一个数据源和回调函数,数据源发生变化会触发回调函数;而 watchEffect 接受一个回调函数,不需要指定数据源,自动并收集回调函数里的所有依赖,同时创建监听器会立即执行一次回调。所以它们的区别主要有两点:

  1. 依赖收集方式不同。

  2. 回调是否会立即执行。

watch的功能比 watchEffect更加强大:

• 懒执行副作用;

• 更加明确是应该由哪个状态触发侦听器重新执行;

• 可以访问所侦听状态的前一个值和当前值。

js 复制代码
HTML  
<template>  
  {{ refValue }}  
</template>  
  
<script setup lang="ts">  
import { ref, watch, watchEffect } from "vue";  
const refValue = ref(0);  
watch(refValue, (value) => {  
  console.log("watch", value);  
});  
watchEffect(async () => {  
  console.log("watchEffect", refValue.value);  
});  
refValue.value = 1;  
</script>

上述代码会输出:

Bash watchEffect 0 watch 1 watchEffect 1

watchEffect 的依赖关系可能不会那么明确。我们需要通读代码,通过检查有没有读取响应式数据的值,判断它是否会被监听。例如,下面这段代码,refValue1会被当做依赖收集,而refValue2不是回调函数的依赖。

js 复制代码
<template>  
  {{ refValue1 }}  
</template>  
  
<script setup lang="ts">  
import { nextTick, ref, watchEffect } from "vue";  
const refValue1 = ref(1);  
const refValue2 = ref(0);  
watchEffect(() => {  
  refValue2.value = refValue1.value * 2;  
  refvalue2.value = obj.a  
});  
</script>

虽然合理的使用watchEffect,能够让代码更加简洁。但是 watchEffect 依赖关系是根据代码推断出来的,对于Reviewer 需要读完整段代码才能确定完整的依赖关系,所以不太建议使用 watchEffect。

函数的两种声明模式⭐️

通过函数声明可以限制函数的入参类型,确定函数的返回类型。 Vue 3的API都提供了两种声明方式:运行时声明或是类型声明,对应的,我们会有两种用法。

运行时声明:函数从参数推到出它的类型。

类型声明:泛型,在函数定义是传入一个类型作为返回类型

可以这样理解 运行时声明,函数根据传入的参数类型推断返回值类型;而类型声明,则由开发者指定泛型,编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。两者在运行效率上没有优劣之分。更多是写法和智能提示的区别,运行时声明不需要指定类型,写法接近JavaScript,能够提供有限的智能提示;类型声明语义化更好、表达更明确,而且编辑器能提供更加完整的智能提示,所以建议使用类型声明。

下面将通过 defineProps 和defineEmits 两个API来介绍这两种类型声明使用的区别。

defineProps

js 复制代码
<script setup lang="ts">  
// 运行时声明 1  
const props = defineProps({  
  foo: { type: String, required: true },  
  bar: Number  
})  
// 运行时声明 2  
const props = defineProps({  
  foo: '',  
  bar: 0  
})  
// 运行不声明 1  
const props = defineProps(['foo', 'bar'])  
// 类型声明  
const props = defineProps<{  
  foo: string  
  bar?: number  
}>()  
</script>

defineProps的用法可以说是非常多了,足足支持有四种完全不一样的写法。其中 运行不声明 1 的写法是完全获取不到类型提示的,所以非常不推荐。运行时声明 2 的写法可能能满足大部分的情景,它能根据传入值的实参类型推断出期望的类型,但是不能做复杂的类型判断。

运行时声明 1 写法是上一种写法的进阶版,它需要开发者提供一个校验选项。这种写法能够提供完整的类型校验,并提供智能提示。

但是有 TypeScript 这样的类型系统为什么不用呢?与 运行时声明 1 相比 类型声明 的写法符合 TypeScript 对类型的定义,没有学习成本,属性的定义也可以抽取成 interfece 进行复用。

综上,对于 defineProps ,推荐使用 类型声明 的写法。

defineProps的类型写法的唯一缺点就是函数不能直接设置默认值,需要借助withDefatult 编译器宏来设置默认值。Vue 也在尝试用新的语法糖来简化写法(响应式语法糖)。

js 复制代码
interface Props {  
  msg?: string  
  labels?: string[]  
}  
const props = withDefaults(defineProps<Props>(), {  
  msg: 'hello',  
  labels: () => ['one', 'two']  
})
事实上defineProps类型声明最终会被编译成defineProps运行时声明 1。

defineEmits

<script setup> 中,defineEmits API可以有两种写法。

js 复制代码
<script setup lang="ts">  
// 运行时  
const emits = defineEmits(['change', 'update'])  
  
// 基于类型  
const emits = defineEmits<{  
  (e: 'change', id: number): void  
  (e: 'update', value: string): void  
}>()  
</script>

defineEmits 比起defineProps就写法就少很多了。结论也显而易见。

运行时写法,入参是事件名,这样定义出来的emits不可能得到提示。而基于类型的写法,需要指明调用时的参数类型,这样在父组件接受事件时,就有足够的提示来推约束处理函数。

所以,推荐使用 类型声明 的写法。

js 复制代码
<template>  
  <li>  
    <span>{{ todoItem.id }}</span>   
    <span>{{ todoItem.message }}</span>   
    <span>{{ todoItem.status }}</span>   
    <button @click="emits('itemDone', todoItem.id)" v-if="todoItem.status === 'pending'" >  
      done  
    </button>  
  </li>  
</template>  
<script setup lang="ts">  
defineProps<{  
  todoItem: {  
    id: number;  
    status: string;  
    message: string;  
  };  
}>();  
const emits = defineEmits<{  
  (e: "itemDone", id: number): void;  
}>();  
</script>

其他

Vue中几乎所有API都支持两种声明模式,并不是所有的API都应该使用类型声明的方式。运行时声明能够简化我们的代码,如果类型可以通过入参明确确定,指定泛型反而是画蛇添足,所以这就需要去分析到时什么时候该用类型声明。

举个例子, toRef这个API,它接受一个reactive 对象和它的一个属性的key,返回类型就是该属性的类型。在这个API中,返回的参数就很明确,且就是和入参强相关,这种情况用运行时声明就没什么问题。

总结了一下,在下列情况下使用类型声明 :

  1. 入参的类型不定,需要约束的。例如 ref ,可以传任意类型作为参数,而当希望它只为某种类型时,就可以提供一个类型来声明。

  2. 需要类型提示的。例如 defineEmits ,使用类型声明能够获得类型提示。

js 复制代码
interface Props {  
  msg?: string;  
  labels?: string[];  
}  
const props = withDefaults(defineProps<Props>(), {  
  msg: "hello",  
  labels: () => ["one", "two"],  
});  
const emits = defineEmits<{  
  (e: "change", id: number): void;  
  (e: "update", value: string): void;  
}>();  
const number = ref<number>(0);  
const numberPlusOne = computed<number>(() => {  
  return number.value + 1;  
});  
interface State {  
  foo: number;  
  bar: number;  
}  
const state = reactive<State>({  
  foo: 1,  
  bar: 2,  
});  
const fooRef = toRef(state, "foo");  
const fooRefs = toRefs(state);  
defineExponse({  
    number  
})

<script setup>

在script块中,我们通过指定setup参数,Vue编译时会自动把这个块编译成setup函数,并将里面定义的函数和属性暴露给模版,不需要手动写返回。

它与setup函数相比,具有更多优势:

• 更少的样板内容,更简洁的代码。

• 能够使用纯 TypeScript 声明 props 和自定义事件。

• 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。

• 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。

所以,选择<script setup>是毋庸置疑的。

setup 异步?

如果我们的一个组件需要在一个异步操作得到响应后才能进行渲染,那我们应该怎么写?

js 复制代码
<template>  
  <span v-if="preLoaded"> suspense: {{ showValue }} </span>  
</template>  
  
<script setup lang="ts">  
import { ref } from "vue";  
const showValue = ref<number>();  
const preLoaded = ref(false);  
async function asyncFun() {  
  await new Promise((resolve: (res: number) => void) => {  
    console.log("async start", Date.now());  
    setTimeout(resolve, 2000, Math.random());  
  }).then((res) => {  
    preLoaded.value = true;  
    showValue.value = res;  
    console.log("async res" + res, Date.now());  
  });  
}  
asyncFun();  
console.log("under asyncFun()");  
</script>

如果在下面还有逻辑是依赖于某个异步的结果的,那么我们就得继续在这个async函数中加代码。而如果直接在setup函数里写await,页面是不会渲染的。

能否直接让setup函数就支持await呢?Vue 3提供了suspense抽象组件:

js 复制代码
<template>  
  <span v-if="preLoaded"> suspense: {{ showValue }} </span>  
</template>  
  
<script setup lang="ts">  
...  
await asyncFun();  
...  
</script>  
  
<script setup lang="ts">  
import SuspenseCompVue from "./components/SuspenseComp.vue";  
</script>  
  
<template>  
  <header>  
      <suspense>  
        <suspense-comp-vue />  
      </suspense>  
  </header>  
</template>

借助 suspense 抽象组件,我们能够让 setup 函数能够阻塞组件的渲染,进行异步操作。

定义组件的name

在vue2的选项式模式中,可以给自定义组件设置一个name属性,它在以下情况下会被使用到:

  1. 组件模板递归地调用自身。(写递归组件时)

  2. 便于调试。(vue-devtools)

  3. keep-alive时,include和exclude 是通过name来匹配的。

Vue 3 的 <setup script> 可以有个同级的 <scrpit>,在这里可以像vue2 一样导出一个对象,它最终会和 <setup script> 返回做合并。

js 复制代码
<script>  
// 普通 <script>, 在模块作用域下执行 (仅一次)  
runSideEffectOnce()  
  
// 声明额外的选项  
export default {  
  name: 'myName',  
  customOptions: {}  
}  
</script>  
  
<script setup>  
/* eslint-disable import/first */  
// 在 setup() 作用域中执行 (对每个实例皆如此)  
</script>

cn.vuejs.org/api/sfc-scr...

inheritAttrs属性用于控制是否启用默认的组件 attribute 透传行为,默认开启。也可以通过这样的方式来进行关闭inheritAttrs的行为。

js 复制代码
<script>  
export default {  
  inheritAttrs: false  
}  
</script>  
  
<script setup>  
  
</script>

自定义组件

defineExponse

我们在ref调用子组件的方法时,希望IDE能够提示组件导出的方法有哪些入参以及返回值是什么,可以这样写。

js 复制代码
    
// app.vue  
<script setup lang="ts">  
import { ref } from "vue";  
import InputValidateVue from "./components/InputValidate.vue";  
const inputValidateRef = ref<InstanceType<typeof InputValidateVue> | null>(null);  
</script>  
  
<template>  
    <InputValidateVue ref="inputValidateRef" />  
</template>
js 复制代码
// inputvalidate.vue  
<template>  
  <input type="text" v-model="inputStr" />  
</template>  
  
<script setup lang="ts">  
import { ref } from "vue";  
  
const inputStr = ref("");  
  
function validate() {  
  const inputStrVal = +inputStr.value;  
  if (Number.isInteger(inputStrVal)) {  
    return true;  
  }  
  return false;  
}  
  
function getInputVal() {  
  return inputStr.value;  
}  
  
defineExpose({  
  validate,  
  getInputVal,  
});  
</script>

在父组件中定义给子组件的 ref 时,指定 InstanceType<typeof InputValidateVue> | null 这样的一个类型,编辑器便能提示 子组件实例呢内置指令和通过 defineExponse 导出的属性和方法。

InstanceType 是 TypeScript提供的一个预设类型,它会返回指定的类的实例类型。而在setup里定义ref 时,组件是还没挂载的,所以还需要指定一个 联合类型 null。www.typescriptlang.org/docs/handbo...

v-model 的使用

v-model是vue提供的一个语法糖,它既能作用于普通表单元素上,又可以作用在组件上。

Vue 2和Vue 3在组件上的v-model是很大区别的:

• 破坏性更新:在自定义组件上使用v-model 时,默认绑定的属性名和事件名变了。

○ prop: value -> modelValue

○ event: input -> update:modelValue

• 破坏性更新:v-bind的 .sync写法和组件的model选项改为用v-model

• 新特性: 同个组件可以绑定多个 v-model

• 新特性: 支持自定义修饰符

可以通过例子看他们的区别。

• 自定义组件上使用v-model

js 复制代码
<!-- vue2 -->  
<ChildComponent v-model="pageTitle" />  
<!-- === -->  
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />  
  
<!-- Vue 3 -->  
<ChildComponent v-model="pageTitle" />  
<!-- would be shorthand for: -->  
<ChildComponent  
  :modelValue="pageTitle"  
  @update:modelValue="pageTitle = $event"  
/>

• v-bind的.sync 写法和 组件的model选项改为用v-model

js 复制代码
<!-- vue2 -->  
<ChildComponent :title.sync="pageTitle" />  
  
<!-- Vue 3 -->  
<ChildComponent v-model:title="pageTitle" />

• 同个组件可以绑定多个 v-model

• 支持自定义修饰符

父组件会将自定义的修饰符通过名为 arg + "Modifiers"属性传到子组件,子组件能够判断属性中是否包含自定义修饰符做对应的操作。

js 复制代码
<script setup>  
const props = defineProps({  
  modelValue: String,  
  modelModifiers: { default: () => ({}) }  
})  
defineEmits(['update:modelValue'])  
console.log(props.modelModifiers) // { capitalize: true }  
</script>  
<template>  
  <input  
    type="text"  
    :value="modelValue"  
    @input="$emit('update:modelValue', $event.target.value)"  
  />  
</template>
Vue 3把v-bind的.sync 写法删除了,所以现在在vue中进行双向绑定只有v-model一种写法。

最佳实践 表单绑定:

有一个表单数据obj,它有 a~d 4个属性,其中 a 属性是不能改变的,b~c属性由 组件A来修改,d属性由组件B来修改,f 属性计算依赖a~c属性。

js 复制代码
<template>  
  <ModelAVue v-if="obj" v-model:b="obj.b" v-model:c="obj.c" /> //只对组件能修改的做绑定  
  <ModelBVue v-if="obj" v-model:d="obj.d" :obj="obj" /> //obj只支持查看  
</template>  
<script lang="ts" setup>  
import ModelAVue from "./ModelA.vue";  
import ModelBVue from "./ModelB.vue";  
import { ref } from "vue";  
const obj = ref<{  
  a: string;  
  b: string;  
  c: string;  
  d: string;  
}>();  
</script>  
// ModelA.vue  
<template>  
  {{ c }}  
</template>  
<script lang="ts" setup>  
defineProps<{  
  b: string;  
  c: string;  
}>();  
defineEmits<{  
  (e: "update:b", data: string): void;  
  (e: "update:c", data: string): void;  
}>();  
</script>  
// ModelB.vue  
<template>  
  {{ d }}  
</template>  
<script lang="ts" setup>  
defineProps<{  
  obj: {  
    a: string;  
    b: string;  
    c: string;  
  };  
  d: string;  
}>();  
defineEmits<{  
  (e: "update:d", data: string): void;  
}>();  
</script>

其他

组合式函数⭐️

Vue 3最显著的特点是,支持组合式(Composition)API。同时,Vue 3也鼓励我们使用组合式函数来抽象一些通用逻辑。

官方文档是这样定义的:"组合式函数"(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。需要注意的是,组合式函数和一般的工具函数是有区别的,组合式函数需要带状态,而工具函数是不带状态的。

约定

命名:组合式函数约定用驼峰命名法命名,并以"use"作为开头。

输入参数:没有要求,可以使ref也可以是普通参数。

返回值:一般是包含多个ref的普通非响应式对象

在日常开发中,我们可能需要在 dom元素上添加原生事件监听器,那么,在改dom元素被卸载时,我们需要回收掉,可以写出如下代码:

js 复制代码
<template>  
  <div id="specialDiv" ref="specialDiv">Click Me</div>  
</template>  
<script lang="ts" setup>  
import { onMounted, onUnmounted, ref } from "vue";  
const specialDiv = ref<HTMLDivElement>();  
function specialDivClickHandler(event: Event) {  
  console.log("specialDiv been click", event.timeStamp);  
}  
function specialDivMouseOverHandle(event: Event) {  
  console.log("specialDiv been mouse over", event.timeStamp);  
}  
onMounted(() => {  
  if (specialDiv.value) {  
    specialDiv.value.addEventListener("click", specialDivClickHandler);  
    specialDiv.value.addEventListener("mouseover", specialDivMouseOverHandle);  
  }  
});  
onUnmounted(() => {  
  if (specialDiv.value) {  
    specialDiv.value.removeEventListener("click", specialDivClickHandler);  
    specialDiv.value.removeEventListener(  
      "mouseover",  
      specialDivMouseOverHandle  
    );  
  }  
});  
</script>

每新增一个原生事件监听器,便要多重复写一次添加和卸载逻辑,能否让代码能自动在回收原生监听器呢?

可以通过组合式函数抽象回收的公共逻辑:

js 复制代码
// useEventListener.ts  
import { onUnmounted } from "vue";  
  
export function useEventListener(  
  target: HTMLDivElement,  
  type: string,  
  listener: (event: Event) => void  
) {  
  target.addEventListener(type, listener);  
  onUnmounted(() => {  
    target.removeEventListener(type, listener);  
  });  
}
js 复制代码
<template>  
  <div id="specialDiv" ref="specialDiv">Click Me</div>  
</template>  
<script lang="ts" setup>  
import { onMounted, ref } from "vue";  
import { useEventListener } from "./useEventListener";  
const specialDiv = ref<HTMLDivElement>();  
  
onMounted(() => {  
  if (specialDiv.value) {  
    useEventListener(specialDiv.value, "click", function (event: Event) {  
      console.log("specialDiv been click", event.timeStamp);  
    });  
    useEventListener(specialDiv.value, "mouseover", function (event: Event) {  
      console.log("specialDiv been mouse over", event.timeStamp);  
    });  
  }  
});  
</script>

vueuse ,这个仓库里实现了很多有用的组合式函数,可以学习或者copy到项目中使用。

在开发的过程中,我们应该多思考、多做提取,减少重复编码,提高开发效率。对于不带状态的逻辑,提取为普通函数,带状态的,提取为组合式函数。

多根节点和class⭐️

在Vue 2编码中,我们在写template时会觉得很繁琐,除了用template标签包裹,还得额外定义一个标签来套住所写的内容,否则编译器会报错。

js 复制代码
<template>  
<div class="header"></div>  
<div class="body"></div>  
<div class="footer"></div>  
</template>  
  
warn:  
 - Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

而在 Vue 3的 template 里可以直接写多个标签。

js 复制代码
<template>  
  <li v-for="item of list" :key="item">{{ item }}</li>  
  <button @click="handleClick">Click</button>  
</template>  
<script lang="ts" setup>  
defineProps<{  
  list: number[];  
}>();  
function handleClick() {  
  console.log("clicked");  
}  
</script>
js 复制代码
<script setup lang="ts">  
import MultiRootVue from "./components/MultiRoot.vue";  
</script>  
  
<template>  
  <header>  
    <ul>  
      <MultiRootVue :list="[1, 2, 3, 4]"  class="wrapper" />  
    </div>  
  </header>  
</template>  
  
  
<style>  
.wrapper {  
  color: red;  
}  
</style>

还是报了warn,同时 wrapper 的样式也没有生效。这是因为 class 属性是一个 "非props"属性,如果不做特殊处理的话,它会被当成原生属性透传到子组件的唯一根元素上,而 Vue 3 的多根template让编译器不知道该将这些属性挂载在哪个元素上,所以直接就进行告警。

"透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class style id 。当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。

所以,我们该如何将这些属性进行显性绑定呢?Vue 3提供了模版变量 $attrs 和 API useAttrs() 帮助我们获到当前所有透传的属性。只要将这些属性显性绑定到能接受这些属性的元素上就能消除警告。

如果绑定的元素是自定义组件,还是不能消费掉这个属性,那么还是会告警。

js 复制代码
<template>  
  <li :class="$attrs.class" v-for="item of list" :key="item">{{ item }}</li>  
</template>
js 复制代码
<template>  
  <li v-bind="$attrs" v-for="item of list" :key="item">{{ item }}</li>  
</template>  
// **v-bind不指定属性则会解构对象并绑定所有的属性**

我们也可以通过设置inheritAttrs属性来关闭透传行为来消除警告,但是这样无异于掩耳盗铃,因为属性还是被丢弃了。

js 复制代码
export default {  
  inheritAttrs: false  
}

对于多根节点,我们需要注意透传属性要做进行显性绑定。 有时我们会发现在父组件使用v-show控制子组件的显示隐藏不生效,而 v-if 却能隐藏元素,这就和透传行为相关。

或许Vue未来可以做点优化,多根节点的父节点存在透传属性时,批量设置透传属性。

总结

Vue 3相比于Vue 2的变动是非常巨大的。推出组合式API、全面拥抱TypeScript、setup函数,甚至对于相同的问题提供了几套开发选型来选择,像选项式API和组合式API、watch和watchEffect、运行时声明和类型声明;当然还有许多附加的新的 API。

这些改变或颠覆了让开发者看花了眼,也产生了巨大的压力和畏难心理,所以会有人道Vue 2 就已经足够了。曾经我也是这样认为的,但是一眨眼,抬头望去早已变了天。

希望以上总总会对你有帮助。

当然,上述只是我一些浅略的拙见,如有疏漏之处烦请见谅。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax