22.Vue Vapor 组件 props 的实现

前言

我们在前一篇中已经实现了 Vue Vapor 的组件渲染,普通 Vue3 组件的渲染是通过运行组件的 render 函数生成组件的虚拟DOM,然后再通过渲染器把组件的虚拟DOM 渲染到父级节点元素上,而 Vue Vapor 组件也是通过运行组件的 render 函数进行渲染的,但 Vue Vapor 组件的 render 函数返回的不是虚拟DOM,而是真实DOM,然后把组件的真实DOM 挂载到父级节点元素上。这就是 Vue Vapor 组件和 Vue 虚拟DOM 版本的组件的区别之一。

我们知道在 Vue 中父子组件的通讯方式之一就是通过 props 进行通讯,父组件可以通过 props 传递数据给子组件,然后子组件内部就可以根据这些 props 数据实现各种各样的功能了。相信很多好学的同学或多或少都有了解过 Vue3 或者 Vue2 中的 props 的实现原理,简单来说就是父组件传递的 props 是一个对象会保存在子组件的实例对象的 props 属性上,所以子组件的 props 数据本质上是父组件的数据,所以当父组件相关的 props 响应式数据发生变化后,就会重新更新,同时会创建一个新的 props,这样在比对新老子组件的虚拟DOM 时,就会先去比较子组件的新老 props 是否一样,然后决定是否更新子组件。

根据我们前面提到的 Vue Vapor 为了兼容 Vue.js 3.x 所以在组件 props 上的 API 设计也是一致的,但它的底层实现原理又有什么变化呢?带着这些疑问,在这一篇,让我们一起探讨 Vue Vapor 组件的 props 的实现原理吧。且纵向比较和虚拟DOM 版本的 Vue3 组件的 props 的实现原理有什么区别。

组件 props 的本质

通过组件,我们可以将一个很大很复杂的页面拆分成多个部分,然后每个部分又可以拆分成若干个小部分,这些部分都可以是一个个单独的组件,通过这些组件,就可以像堆积木那样把一个复杂页面进行堆砌出来。同时这些组件可能功能需求都是相同的,比如说弹框、日历、表单,那么就不需要每个部分都重新写一套功能相同的组件代码,那么我们只需要写一个组件,然后在需要的地方进行引用即可,然后根据不同的数据展示不同的内容和功能,那么这些组件需要接收相关的数据,其中一种的接收方式就是通过 props。所以组件 props 的本质就是父子组件间的一个通讯渠道

为了可以更直观地理解,我们假如有一个 AuthorInfo 的组件,定义如下:

html 复制代码
<template>
    <dl>
        <dt>昵称:{{nickname}}</dt>
        <dd>简介:{{info}}</dd>
    <dl>
</template>

使用方式如下:

html 复制代码
<author-info :nickname="nickname" :info="info">

很明显我们这个 AuthorInfo 组件可以根据不同的数据展示不同的内容。那么 Vue Vapor 内部又是如何初始化和更新 props 的呢?

为了简化我们的逻辑,我们在 src 目录定义以下一个子组件:

javascript 复制代码
import { ref, template, effect } from '../runtime-vapor/src'
const ChildComponent = {
    setup(props) {
        return (() => {
            const _tmpl$ = template('<div id="child"></div>')
            // 真正进行创建模板内容的地方
            const el = _tmpl$()
            effect(() => {
                el.textContent = props.count
            }) 
            return el
        })()
    }
}

export default ChildComponent

我们知道在 Vue3 中当 setup 方法返回一个函数的时候,这个函数就会被当作这个组件的 render 函数,我们为了方便测试,就直接在 setup 方法中返回一个渲染函数。同时我们知道 setup 方法的第一个参数就是 props,那么这个 props 是怎么来的呢?

根据我们前面所学的知识,我们定义一个父组件,并且在父组件中调用我们上面定义的子组件:

javascript 复制代码
import { ref, template, insert } from "../runtime-vapor/src"
import { createComponent } from "../runtime-vapor/src/apiCreateComponent"
import ChildComponent from "./childComponent"
const App = {
    setup() {
        const count = ref(0)
        return { count } 
    },
    render(_ctx) {
        // 生成创建 button 标签的函数
        const _tmpl$ = template('<button id="parent"></button>')
        // 真正进行创建模板内容的地方
        const el = _tmpl$()
        el.addEventListener('click', () => {
            _ctx.count++
        })
        const n1 = createComponent(ChildComponent, {
            count: _ctx.count
        })
        insert(n1, el)
        return el
    }
}

export default App

我们在父组件调用子组件是通过 createComponent 方法调用的,并且设计它的第二个参数便是 props。

所以我们的 createComponent 函数修改如下:

diff 复制代码
export function createComponent(
  comp,
+  rawProps
) {
  const instance = createComponentInstance(
    comp,
+    rawProps
  )
  setupComponent(instance)

  return instance
}

接着我们去修改 createComponentInstance 方法

diff 复制代码
export const createComponentInstance = (
  component,
+  rawProps
) => {
  const instance = {
    block: null,
    container: null, // set on mount
    component,
+    props: {}, // 父组件传递进来的 props 数据最终存储的地方
  }
+  // 初始化 props
+  initProps(instance, rawProps)
  return instance
}

接着我们在 runtime-vapor/src 目录下创建一个 componentProps.js 文件用来处理 props 相关的逻辑。我们目前只做一件事,就是把父组件传递过来的 props 设置到组件实例对象上的 props 属性上。代码如下:

javascript 复制代码
export function initProps(instance, rawProps) {
  instance.props = rawProps || {}
}

我们现在已经把父组件传递的 props 设置到子组件的实例对象上了,我们又知道组件的 setup 方法的第一个参数便是 props,所以我们只需要在执行 setup 方法的时候把父组件传递过来的 props 传递过去即可。同时我们 setup 方法中返回生成的 DOM 节点,所以我们还需要进行兼容这种情况的迭代修改。根据我们前面了解的,setup 方法是在 setupComponent 方法中执行的,所以我们对 setupComponent 方法的修改如下:

javascript 复制代码
export function setupComponent(instance) {
  const { component } = instance
  // 判断是状态组件还是函数组件
  const setupFn =
      typeof component === 'function' ? component : component.setup
  // 获取 setup 方法的执行结果,并且把 props 作为 setup 方法的第一个参数传递进去
  const stateOrNode = setupFn && setupFn(instance.props)
  let block
  if (stateOrNode && stateOrNode instanceof Node) {
    // setup 方法返回的也可能是 DOM 节点,所以如果是 DOM 节点则直接把 DOM 节点赋值给组件实例对象上的 block 属性
    block = stateOrNode
  } else {
    // 执行 render 函数获取 DOM 结果
    block = component.render(proxyRefs(stateOrNode))
  }
  instance.block = block
}

至此我们在父组件通过 props 传递数据给子组件的功能就实现了,渲染结果如下:

到这里我们再进行总结一下组件 props 的本质,我们在上面开头就抛出了一个观点,props 的本质就是父子组件间的一个通讯渠道,这个表述还是比较宏观的,我们从代码组织的角度来看,所谓的组件 props 就是在一个函数内创建的变量传递到另一个函数内。

组件 props 的更新

我们上述的代码中,我们实现了把父组件的数据通过 props 传递到子组件中并实现了渲染,但我们点击按钮的时候,却还不能实现更新,接下来我们就来实现一下组件的更新吧。

组件的 props 是在父组件中赋值的,在普通 Vue3 项目中当在父组件中读取响应式变量并赋值给子组件的 props 时,就会触发父组件响应式变量的 getter,继而触发父组件的进行依赖收集,这个所谓依赖就是父组件的 render 函数组成的副作用函数,当相应的响应式变量发生变化的时候,就会触发父组件的 render 函数重新执行,在执行的过程中,又会生成新的子组件的 props,然后在虚拟DOM Difff 的时候就会对比子组件的新老 props 是否相同,如果不相同则重新渲染子组件,并且更新组件的 props、slots 等内容。这是普通存在虚拟DOM 的 Vue 组件更新的原理,很明显在无虚拟DOM 中则不再是这样更新的了,首先是在 Vue Vapor 中的组件 render 函数只在初始化的时候执行一次,就不会再执行第二次了。

我们在上述的子组件中是通过以下的方式读取 props 的:

javascript 复制代码
effect(() => {
    el.textContent = props.count
}) 

那么我们只需要在读取 props.count 的时候触发父级组件对应性响应式数据进行依赖收集即可,这样将来对应的响应式数据发生变化了,我们上述 effect 中的副作用函数也会重新执行,这样我们就实现了 props 的更新了,而且是精准更新。

其实我们只要做到读取 props.count 的时候就是在读取父级组件对应的响应式数据就可以,那么我们通过 Object.defineProperty 监听每一个 props 属性然后进行代理即可。我们在初始化 props 的时候做以下修改:

javascript 复制代码
export function initProps(instance, rawProps) {
  const props = {}
  if (rawProps) {
    for(const key in rawProps) {
      const valueGetter = rawProps[key]
      Object.defineProperty(props, key, {
        get() {
          return valueGetter()
        }
      })
    }
  }
  instance.props = props
}

这样在父组件传递过来的 props 的属性需要设置成一个函数,修改如下:

diff 复制代码
const n1 = createComponent(ChildComponent, {
-   count: _ctx.count
+   count: () => _ctx.count
})

经过我们上述的功能迭代,我们实现了 Vue Vapor 组件的 props 更新。

组件 props 配置的实现

我们一般设计一个组件的时候,都会设置该组件的 props 的数据类型,例如我们上面子组件在父组件调用它的时候需要给它传递一个 count 的参数,那么我们为了组件的健壮性会为 count 指定数据类型,有点像 TypeScript 中的类型的概念。

我们上述的子组件中的 count 是 Number 类型,所以我们可以设置它的 props 类型为 Number,这样在开发阶段就可以监控传递的类型是否是 Number,从而提高程序的健壮性。设置如下:

javascript 复制代码
const ChildComponent = {
    props: {
        count: Number
    }
    setup(props) {
        // 省略...
    }
}

我们还可以为它设置必填项:

javascript 复制代码
const ChildComponent = {
    props: {
        count: {
            type: Number,
            required: true
        }
    }
    setup(props) {
        // 省略...
    }
}

我们可以看到用户可以非常灵活地配置 props,所以为了方便框架后续统一对用户输入的 props 进行校验是否合规,我们就需要对用户配置的 props 进行标准化处理。props 标准化处理是在创建组件实例对象的时候进行的,也就是在 createComponentInstance 中,代码位置如下:

javascript 复制代码
export const createComponentInstance = (
  component,
  rawProps
) => {
  const instance = {
    // 省略...
    propsOptions: normalizePropsOptions(component),
  }
  // 省略...
}

我们接着在 runtime-vapor\src\componentProps.js 文件中创建 normalizePropsOptions 函数,代码如下:

javascript 复制代码
export function normalizePropsOptions(comp) {
  const raw = comp.props
  // props 标准化后的存储对象
  const normalized = {}
  // 是否需要转换
  const needCastKeys = []

  for (const key in raw) {
    // 判断属性名称是否合法
    if (validatePropName(key)) {
      const opt = raw[key]
      // 主要针对 props 配置为数组和原生类型进行标准化配置,我们设置的 props: { count: Number } 中的 Number 其实就是一个原生的类型构造函数
      normalized[key] = Array.isArray(opt) || typeof opt === 'function' ? { type: opt } : raw[key]
    }
  }

  return [normalized, needCastKeys]
}

所谓 props 配置的标准化主要针对 props 配置为数组和原生类型进行标准化配置,我们设置的 props: { count: Number } 中的 Number 其实就是一个原生的类型构造函数,其次在标准化设置之前我们还要检查一下用户设置的 props 属性名称是否合法,因为在 Vue 内部也使用 $ 开头作为方法和属性,所以为了防止冲突,也禁止使用 $ 开头的 prop。检查 props 属性名称的 validatePropName 方法设置如下:

javascript 复制代码
// 因为在 Vue 内部也使用 $ 开头作为方法和属性,所以为了防止冲突,也禁止使用 $ 开头的 prop
function validatePropName(key) {
  if (key[0] !== '$') {
    return true
  } else if (__DEV__) {
    // 开发环境会进行警告
    console.warn(`Invalid prop name: "${key}" is a reserved property.`)
  }
  return false
}

验证 props

我们经过上述 props 配置的标准化设置好,我们就可以在 props 的初始化的时候进行验证 props 的合法性了。

javascript 复制代码
// 模拟开发环境变量
const __DEV__ = true
export function initProps(instance, rawProps) {
  const [options] = instance.propsOptions
  // 省略...
  
  // 开发环境进行检查 props 的数据是否合规
  if (__DEV__) {
    validateProps(rawProps, props, options || {})
  }

  instance.props = props
}

验证是在开发环境的下进行的,我们来看看 validateProps 的实现:

javascript 复制代码
function validateProps(
  rawProps,
  props,
  options,
) {
  const presentKeys = []
  // 收集传递过来的 props 属性然后在后面判断是否存在 props 配置中
  rawProps && presentKeys.push(...Object.keys(rawProps))

  for (const key in options) {
    const opt = options[key]
    if (opt != null)
      validateProp(
        key,
        props[key],
        opt,
        props,
        !presentKeys.some(k => k === key), // 判断传过来的 prop 是否存在配置中
      )
  }  
}

具体每一个 prop 的验证是在 validateProp 中进行的,所以我们接下来实现 validateProp,代码如下:

javascript 复制代码
function validateProp(
  name,
  value,
  option,
  props,
  isAbsent
) {
  const { required, validator } = option
  // 必填项校验
  if (required && isAbsent) {
    console.warn('Missing required prop: "' + name + '"')
    return
  }

  // 自定义校验器校验
  if (validator && !validator(value, props)) {
    console.warn('Invalid prop: custom validator check failed for prop "' + name + '".')
  }
}

接着我们测试一下当父组件不传递 count 数据会不会打印警报。

javascript 复制代码
const n1 = createComponent(ChildComponent, {})

我们可以看到如期打印了我们期望的警报,说明我们的写的功能是正确的。

props 配置默认值的实现

我们一般设计一个组件的 prop 的时候,除了设置它的类型、是否必传之外,还会设置默认值,这样当用户调用组件的时候没传相关 prop 的时候,就读取默认值。

javascript 复制代码
const ChildComponent = {
    props: {
        count: {
            type: Number,
            default: 520 // 设置默认值,还有可能是一个函数 () => 520
        }
    },
    // 省略...
}

值得注意的时候,默认值 default 还有可能是一个函数。

首先我们需要在标准化 props 配置的时候进行标记哪些 props 有默认值的,需要转换的。我们对 normalizePropsOptions 函数迭代如下:

diff 复制代码
export function normalizePropsOptions(comp) {
  // 省略...
  for (const key in raw) {
    // 判断属性名称是否合法
    if (validatePropName(key)) {
      const opt = raw[key]
      // 主要针对 props 配置为数组和原生类型进行标准化配置,我们设置的 props: { count: Number } 中的 Number 其实就是一个原生的类型构造函数
-      normalized[key] = Array.isArray(opt) || typeof opt === 'function' ? { type: opt } : raw[key]
+      const prop = (normalized[key] = Array.isArray(opt) || typeof opt === 'function' ? { type: opt } : raw[key])
+      if (prop) {
+        // 如果存在默认值就需要转换
+        if (Object.prototype.hasOwnProperty.call(prop, 'default')) {
+          needCastKeys.push(key)
+        }
+      }
    }
  }
  return [normalized, needCastKeys]
}

我们将有默认值的 key 保存到 needCastKeys 中跟标准化后的 props 的变量 normalized 一起存储到组件实例 instance.propsOptions 上,方便后续调用。

接着在 props 初始化的时候进行判断用户是否有给组件传递 prop,如果用户没有传递相关的 prop 则需要去看组件的 props 的配置项是否存在默认值,如果存在则把默认值赋值给 prop。相关操作在 initProps 函数中,迭代代码如下:

javascript 复制代码
export function initProps(instance, rawProps) {
  const props = {}
  const [options, needCastKeys] = instance.propsOptions
  if (options) {
    // 循环组件配置中 props
    for(const key in options) {
      // 读取用户传递过来的 props
      const valueGetter = rawProps[key]
      let value
      // 如果用户没传相关 prop 
      if (valueGetter === undefined) {
        // 检查是否存在默认值需要转换
        const needCast = needCastKeys && needCastKeys.includes(key)
        // 需要转换
        if (needCast) {
          const opt = options[key]
          // 存在配置项
          if (opt != null) {
            // 默认值必须是自身的属性
            const hasDefault = Object.prototype.hasOwnProperty.call(opt, 'default')
            // 如果存在默认值
            if (hasDefault) {
              const defaultValue = opt.default
              // 如果默认值是函数,且类型不是函数
              if (opt.type !== Function && typeof defaultValue === 'function') {
                // 默认值时函数需要把执行的结果返回再进行赋值,并且默认值中的函数不能访问组件实例 this 的,所以在执行的时候需要把里面的 this 通过 call 方法指向 null
                value = defaultValue.call(null, props)
              } else {
                // 默认值不是函数,直接赋值
                value = defaultValue
              }
            }
          }
        }
      }
      Object.defineProperty(props, key, {
        get() {
          // 如果用户没传递 prop 就读取默认值
          return valueGetter === undefined ? value : valueGetter()
        }
      })
    }
  }
  // 开发环境进行检查 props 的数据是否合规
  if (__DEV__) {
    validateProps(rawProps, props, options || {})
  }

  instance.props = props
}

首先我们要循环组件配置中的 props,然后读取用户传递过来的 props,如果用户没传递相关 props,那么我们就要检查是否存在默认值配置,判断是否存在默认值的时候要注意默认值必须是自身的属性,如果存在默认值那么还要判断默认是不是函数,如果是函数且配置类型不是函数就要把执行的结果返回再进行赋值,并且默认值中的函数不能访问组件实例 this 的,所以在执行的时候需要把里面的 this 通过 call 方法指向 null,如果不是函数就直接赋值。最后在读取 prop 属性时触发 getter 的时候,在 getter 里面判断用户如果没有传递 prop 就读取默认值。

接下来我们设置在父组件调用子组件的时候,不传递 count:

javascript 复制代码
const n1 = createComponent(ChildComponent, {
    // count: () => _ctx.count
})

最后我们测试结果如下:

我们可以看到当我们没传 count 值的时候,子组件就读取了 props 配置中的默认值。

重构 initProps

我们上一小节中在 initProps 中实现的读取 props 的默认值的那一坨代码,其实职责是很不清晰的,也不利于后续的维护,所以我们需要对它进行重构。软件应该是"自描述"的,代码除了给机器看之外,也要给人看。我们希望写的代码更易读,让代码可以更好地表达自己的意图。

我们可以通过提炼函数,通过函数名称来知道我们的程序的业务结构,而提炼函数这个方法是《重构》这本书中介绍的一种代码重构手段。我们将使用设置每个 prop 值的代码进行提炼为一个函数并命名为 registerProp,函数代码如下:

javascript 复制代码
function registerProp(
  instance,
  props,
  key,
  getter
) {
  // 如果已经存在的就不再处理
  if (key in props) return
  const [options, needCastKeys] = instance.propsOptions
  let value
  // 如果父组件没传相关 prop 
  if (getter === undefined) {
    // 检查是否存在默认值需要转换
    const needCast = needCastKeys && needCastKeys.includes(key)
    // 需要转换
    if (needCast) {
      const opt = options[key]
      // 存在配置项
      if (opt != null) {
        // 默认值必须是自身的属性
        const hasDefault = Object.prototype.hasOwnProperty.call(opt, 'default')
        // 如果存在默认值
        if (hasDefault) {
          const defaultValue = opt.default
          // 如果默认值是函数,且类型不是函数
          if (opt.type !== Function && typeof defaultValue === 'function') {
            // 默认值时函数需要把执行的结果返回再进行赋值,并且默认值中的函数不能访问组件实例 this 的,所以在执行的时候需要把里面的 this 通过 call 方法指向 null
            value = defaultValue.call(null, props)
          } else {
            // 默认值不是函数,直接赋值
            value = defaultValue
          }
        }
      }
    }
  }
  Object.defineProperty(props, key, {
    get() {
      return getter === undefined ? value : getter()
    }
  })
}

接着 initProps 函数修改如下:

javascript 复制代码
export function initProps(instance, rawProps) {
  instance.rawProps = rawProps
  const props = {}
  const [options] = instance.propsOptions
  if (options) {
    for(const key in options) {
      registerProp(instance, props, key, rawProps[key])
    }
  }
  // 开发环境进行检查 props 的数据是否合规
  if (__DEV__) {
    validateProps(rawProps, props, options || {})
  }

  instance.props = props
}

我们可以看到 initProps 函数清爽了很多,但 registerProp 函数目前还是不方便阅读,我们还需要继续迭代,我们把获取 props 默认值也提炼成一个函数 resolvePropValue,代码如下:

javascript 复制代码
function resolvePropValue(options, props, key, value) {
  const opt = options[key]
  // 存在配置项
  if (opt != null) {
    // 默认值必须是自身的属性
    const hasDefault = Object.prototype.hasOwnProperty.call(opt, 'default')
    // 如果存在默认值
    if (hasDefault) {
      const defaultValue = opt.default
      // 如果默认值是函数,且类型不是函数
      if (opt.type !== Function && typeof defaultValue === 'function') {
        // 默认值时函数需要把执行的结果返回再进行赋值,并且默认值中的函数不能访问组件实例 this 的,所以在执行的时候需要把里面的 this 通过 call 方法指向 null
        value = defaultValue.call(null, props)
      } else {
        // 默认值不是函数,直接赋值
        value = defaultValue
      }
    }
  }
  return value
}

这样 registerProp 中读取默认值则迭代如下:

javascript 复制代码
function registerProp(
  instance,
  props,
  key,
  getter
) {
  // 如果已经存在的就不再处理
  if (key in props) return
  const [options, needCastKeys] = instance.propsOptions
  let value
  // 如果父组件没传相关 prop 
  if (getter === undefined) {
    // 检查是否存在默认值需要转换
    const needCast = needCastKeys && needCastKeys.includes(key)
    // 需要转换
    if (needCast) {
      value = resolvePropValue(options, props, key, getter ? getter() : undefined)
    }
  }
  Object.defineProperty(props, key, {
    get() {
      return getter === undefined ? value : getter()
    }
  })
}

我们还可以继续对 Object.defineProperty 中的劫持函数 getter 进行迭代:

javascript 复制代码
function registerProp(
  instance,
  props,
  key,
  getter
) {
  // 如果已经存在的就不再处理
  if (key in props) return
  const [options, needCastKeys] = instance.propsOptions
  // 检查是否存在默认值需要转换
  const needCast = needCastKeys && needCastKeys.includes(key) 
  const get = needCast ? () => resolvePropValue(options, props, key, getter ? getter() : undefined) : getter 
  Object.defineProperty(props, key, {
    get
  })
}

接着我们还需要对 resolvePropValue 中判断是否存在默认值的时候进行修改,修改成如果存在默认值并且 value 是 undefined 才能读取默认值,代码修改如下:

diff 复制代码
function resolvePropValue(options, props, key, value) {
  const opt = options[key]
  // 存在配置项
  if (opt != null) {
    // 默认值必须是自身的属性
    const hasDefault = Object.prototype.hasOwnProperty.call(opt, 'default')
+    // 如果存在默认值并且 value 是 undefined 才能读取默认值
-    if (hasDefault) {
+    if (hasDefault && value === undefined) {
      // 省略...
    }
  }
  return value
}

总结

Vue Vapor 的组件 props 本质是父子组件间的数据通道,但实现上摒弃了虚拟 DOM 的 diff 机制。初始化时,将父组件传入的响应式数据包装为带有 getter 拦截的属性,子组件通过 props.xxx 直接访问父组件变量,从而在副作用中建立精准依赖。更新时,父组件数据变化直接触发子组件对应副作用函数重执行,实现定点 DOM 更新,无需整组件重渲染。

配置层面完整兼容 Vue3 的 props 声明,支持类型校验、必填标记、自定义验证及默认值(含函数式默认值),开发环境下提供友好警告。与 Vue3 相比,Vapor 以"函数包裹"形式传递响应式数据(如 count: () => _ctx.count),虽写法略异,但换来了更高的更新效率和更清晰的响应式链路,是性能与 API 一致性之间的巧妙平衡。

相关推荐
lichenyang4531 小时前
从 has.showToast 看 ASCF 的 API 调用链路
前端
张就是我1065923 小时前
DOMPurify 的一个漏洞:你以为 {} 是空的?
前端
浮生望3 小时前
JS字符串与回文算法:从包装类到双指针的面试进阶之路
javascript·算法
疯狂的魔鬼3 小时前
一套 Schema 驱动四视图:记 useCrudSchemas 的设计与实践
前端·javascript·typescript
风骏时光牛马3 小时前
大模型开发工具高频故障与实操问题汇总代码案例大全
前端
没落英雄3 小时前
2. 让 Agent 能读写文件、执行命令 —— LocalShellBackend 实战
前端·人工智能·架构
白雾茫茫丶3 小时前
探索 Nuxt.js 全栈能力:用 Better-Auth 打造类型安全的 RBAC 权限系统
前端·vue.js·nuxt.js
奇奇怪怪的3 小时前
检索增强——混合检索、Re-rank 与 Query 优化
前端
user62229864925813 小时前
React 常用技术知识全景:从组件到 Hooks 的系统理解
前端