《Vuejs设计与实现》第 18 章(同构渲染)(上) - 详解

news/2025/10/22 11:09:42/文章来源:https://www.cnblogs.com/ljbguanli/p/19157494

《Vuejs设计与实现》第 18 章(同构渲染)(上) - 详解

目录

18.1 CSR、SSR 以及同构渲染

18.2 将虚拟 DOM 渲染为 HTML 字符串

18.3 将组件渲染为 HTML 字符串


Vue.js 可以用于构建客户端应用程序,组件的代码在浏览器中运行,并输出 DOM 元素。
同时,Vue.js 还可以在 Node.js 环境中运行,它可以将同样的组件渲染为字符串并发送给浏览器。
这实际上描述了 Vue.js 的两种渲染方式,即客户端渲染(client-side rendering,CSR),以及服务端渲染(server-side rendering,SSR)。
另外,Vue.js 作为现代前端框架,不仅能够独立地进行 CSR 或 SSR,还能够将两者结合,形成所谓的同构渲染(isomorphicrendering)。
本章,我们将讨论 CSR、SSR 以及同构渲染之间的异同,以及 Vue.js 同构渲染的实现机制。

18.1 CSR、SSR 以及同构渲染

服务端渲染并不是一项新技术,也不是一个新概念。
在 Web 2.0 之前,网站主要负责提供各种各样的内容,通常是一些新闻站点、个人博客、小说站点等。这些站点主要强调内容本身,而不强调与用户之间具有高强度的交互。
当时的站点基本采用传统的服务端渲染技术来实现。例如,比较流行的 PHP/JSP 等技术。下面给出服务端渲染的工作流程图:

image.png

  1. 用户通过浏览器请求站点。
  2. 服务器请求 API 获取数据。
  3. 接口返回数据给服务器。
  4. 服务器根据模板和获取的数据拼接出最终的 HTML 字符串。
  5. 服务器将 HTML 字符串发送给浏览器,浏览器解析 HTML 内容并渲染。

当用户再次通过超链接进行页面跳转,会重复上述 5 个步骤。
传统的服务端渲染的用户体验非常差,任何一个微小的操作都可能导致页面刷新。
后来以 AJAX 为代表,催生了 Web 2.0。在这个阶段,大量的 SPA(single-page application)诞生,也就是接下来我们要介绍的 CSR 技术。
与 SSR 在服务端完成模板和数据的融合不同,CSR 是在浏览器中完成模板与数据的融合,并渲染出最终的 HTML 页面。CSR 工作流程图:

image.png


客户端向服务器或 CDN 发送请求,获取静态的 HTML 页面。
注意,此时获取的 HTML 页面通常是空页面。在 HTML 页面中,会包含 <style><link>和 <script> 等标签。例如:

My App
<script src="/dist/app.js"></script>

是一个包含 <link rel="stylesheet"> 与 <script> 标签的空 HTML 页面。
浏览器在得到该页面后,不会渲染出任何内容,所以从用户的视角看,此时页面处于“白屏”阶段。
解析 HTML 内容。通过 <link rel="stylesheet"> 和 <script> 等标签加载引用的资源。
因为页面的渲染任务是由 JavaScript 来完成的,所以当 JavaScript 被解释和执行后,才会渲染出页面内容,即“白屏”结束。
但初始渲染出来的内容通常是一个“骨架”,因为还没有请求 API 获取数据。
客户端再通过 AJAX 技术请求 API 获取数据,一旦接口返回数据,客户端就会完成动态内容的渲染,并呈现完整的页面。
当用户再次通过点击“跳转”到其他页面时,浏览器并不会真正的进行跳转动作,即不会进行刷新,而是通过前端路由的方式动态地渲染页面,这对用户的交互体验会非常友好。
但很明显的是,与 SSR 相比,CSR 会产生所谓的“白屏”问题。并且它对 SEO(搜索引擎优化)也不友好。
下图从多个方面比较了 SSR 与 CSR:

image.png


可以看到,无论是 SSR 还是 CSR,都不可以作为“银弹”,我们需要从项目的实际需求出发,决定到底采用哪一个。例如你的项目非常需要 SEO,那么就应该采用 SSR。
那么,我们能否融合 SSR 与 CSR 两者的优点于一身呢?答案是“可以的”,这就是接下来我们要讨论的同构渲染。
同构渲染分为首次渲染(即首次访问或刷新页面)以及非首次渲染。下图是同构渲染首次渲染的工作流程:

image.png


实际上,同构渲染中的首次渲染与 SSR 的工作流程是一致的。
当首次访问或者刷新页面时,整个页面的内容是在服务端完成渲染的,浏览器最终得到的是渲染好的 HTML 页面。
但是该页面是纯静态的,这意味着用户还不能与页面进行任何交互,因为整个应用程序的脚本还没有加载和执行。
另外,该静态的 HTML 页面中也会包含 <link><script> 等标签。
同构渲染所产生的 HTML 页面会包含当前页面所需要的初始化数据。而 SSR 不会。
服务器通过 API 请求的数据会被序列化为字符串,并拼接到静态的 HTML 字符串中,最后一并发送给浏览器。这么做实际上是为了后续的激活操作,后文讲解。
假设浏览器已经接收到初次渲染的静态 HTML 页面,接下来浏览器会解析并渲染该页面。
在解析过程中,浏览器会发现 HTML 代码中存在 <link> 和 <script> 标签,于是会从 CDN 或服务器获取相应的资源,这一步与 CSR 一致。
当 JavaScript 资源加载完毕后,会进行激活操作,这里的激活就是我们在 Vue.js 中常说的 “hydration”。激活包含两部分工作内容。

  • Vue.js 在当前页面已经渲染的 DOM 元素以及 Vue.js 组件所渲染的虚拟 DOM 之间建立联系。
  • Vue.js 从 HTML 页面中提取由服务端序列化后发送过来的数据,用以初始化整个 Vue.js 应用程序。

激活完成后,整个应用程序已经完全被 Vue.js 接管为 CSR 应用程序了。
后续操作都会按照 CSR 应用程序的流程来执行。当然,如果刷新页面,仍然会进行服务端渲染,然后再进行激活,如此往复。
下图对比了 SSR、CSR 和同构渲染的优劣:

image.png


可以看到,同构渲染除了也需要部分服务端资源外,其他方面的表现都非常棒。
由于同构渲染方案在首次渲染时和浏览器刷新时仍然需要服务端完成渲染工作,所以也需要部分服务端资源。
但相比所有页面跳转都需要服务端完成渲染来说,同构渲染所占用的服务端资源相对少一些。
注意理论上同构渲染无法提升可交互时间(TTI)。还是需要像 CSR 那样等待 JavaScript 资源加载完成,并且客户端激活完成后,才能响应用户操作。
同构渲染的“同构”一词的含义是,同样一套代码既可以在服务端运行,也可以在客户端运行。
例如,我们用 Vue.js 编写一个组件,该组件既可以在服务端运行,被渲染为 HTML 字符串;也可以在客户端运行,就像普通的 CSR 应用程序一样。

18.2 将虚拟 DOM 渲染为 HTML 字符串

既然“同构”指的是,同样的代码既能在服务端运行,也能在客户端运行,我们来说说如何在服务端将虚拟 DOM 渲染为 HTML 字符串。
给出如下虚拟节点对象,它用来描述一个普通的 div 标签:

const ElementVNode = {type: 'div',props: {id: 'foo',},children: [{ type: 'p', children: 'hello' }],
}

为了将虚拟节点 ElementVNode 渲染为字符串,我们需要实现 renderElementVNode 函数。
该函数接收用来描述普通标签的虚拟节点作为参数,并返回渲染后的 HTML 字符串:

function renderElementVNode(vnode) {// 返回渲染后的结果,即 HTML 字符串
}

在不考虑任何边界条件的情况下,实现 renderElementVNode 非常简单,如下所示:

function renderElementVNode(vnode) {// 取出标签名称 tag 和标签属性 props,以及标签的子节点const { type: tag, props, children } = vnode// 开始标签的头部let ret = `<${tag}`// 处理标签属性if (props) {for (const k in props) {// 以 key="value" 的形式拼接字符串ret += ` ${k}="${props[k]}"`}}// 开始标签的闭合ret += `>`// 处理子节点// 如果子节点的类型是字符串,则是文本内容,直接拼接if (typeof children === 'string') {ret += children} else if (Array.isArray(children)) {// 如果子节点的类型是数组,则递归地调用 renderElementVNode 完成渲染children.forEach(child => {ret += renderElementVNode(child)})}// 结束标签ret += ``// 返回拼接好的 HTML 字符串return ret
}

接着,我们可以调用 renderElementVNode 函数完成对 ElementVNode 的渲染:

console.log(renderElementVNode(ElementVNode)) // 

hello

可以看到,输出结果是我们所期望的 HTML 字符串。实际上,将一个普通标签类型的虚拟节点渲染为 HTML 字符串,本质上是字符串的拼接。
不过,上面给出的 renderElementVNode 函数的实现仅仅用来展示将虚拟 DOM 渲染为 HTML 字符串的核心原理,并不满足生产要求,因为它存在以下几点缺陷:

  • renderElementVNode 函数在渲染标签类型的虚拟节点时,还需要考虑该节点是否是自闭合标签。
  • 对于属性(props)的处理会比较复杂,要考虑属性名称是否合法,还要对属性值进行 HTML 转义。
  • 子节点的类型多种多样,可能是任意类型的虚拟节点,如 Fragment、组件、函数式组件、文本等,这些都需要处理。
  • 标签的文本子节点也需要进行 HTML 转义。

上述这些问题都属于边界条件,接下来我们逐个处理。首先处理自闭合标签,它的术语叫作 void element,它的完整列表如下:

const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'

对于 void element,由于它无须闭合标签,所以在为此类标签生成 HTML 字符串时,无须为其生成对应的闭合标签,如下面的代码所示:

const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split(',')
function renderElementVNode2(vnode) {const { type: tag, props, children } = vnode// 判断是否是 void elementconst isVoidElement = VOID_TAGS.includes(tag)let ret = `<${tag}`if (props) {for (const k in props) {ret += ` ${k}="${props[k]}"`}}// 如果是 void element,则自闭合ret += isVoidElement ? `/>` : `>`// 如果是 void element,则直接返回结果,无须处理 children,因为 void element 没有 childrenif (isVoidElement) return retif (typeof children === 'string') {ret += children} else {children.forEach(child => {ret += renderElementVNode2(child)})}ret += ``return ret
}

接下来,我们需要更严谨地处理 HTML 属性。处理属性需要考虑多个方面,首先是对 boolean attribute 的处理。
所谓 boolean attribute,并不是说这类属性的值是布尔类型,而是指,如果这类指令存在,则代表 true,否则代表 false。
例如 <input/> 标签的 checked 属性和 disabled 属性:


当渲染 boolean attribute 时,通常无须渲染它的属性值。
另外一点需要考虑的是安全问题,WHATWG 规范的 13.1.2.3 节中明确定义了属性名称的组成。
属性名称必须由一个或多个非以下字符组成。

  • 控制字符集(control character)的码点范围是:[0x01, 0x1f] 和 [0x7f,0x9f]。
  • U+0020 (SPACE)、U+0022 (")、U+0027 (')、U+003E (>)、U+002F (/)以及 U+003D (=)。
  • noncharacters,这里的 noncharacters 代表 Unicode 永久保留的码点,这些码点在 Unicode 内部使用,它的取值范围是:[0xFDD0, 0xFDEF],还包括:0xFFFE、0xFFFF、0x1FFFE、0x1FFFF、0x2FFFE、0x2FFFF、0x3FFFE、0x3FFFF、0x4FFFE、0x4FFFF、0x5FFFE、0x5FFFF、0x6FFFE、0x6FFFF、0x7FFFE、0x7FFFF、0x8FFFE、0x8FFFF、0x9FFFE、0x9FFFF、0xAFFFE、0xAFFFF、0xBFFFE、0xBFFFF、0xCFFFE、0xCFFFF、0xDFFFE、0xDFFFF、0xEFFFE、0xEFFFF、0xFFFFE、0xFFFFF、0x10FFFE、0x10FFFF。

考虑到 Vue.js 的模板编译器在编译过程中已经对 noncharacters 以及控制字符集进行了处理,所以我们只需要小范围处理即可,任何不满足上述条件的属性名称都是不安全且不合法的。
另外,在虚拟节点中的 props 对象中,通常会包含仅用于组件运行时逻辑的相关属性。
例如,key 属性仅用于虚拟 DOM 的 Diff 算法,ref 属性仅用于实现 template ref 的功能等。在进行服务端渲染时,应该忽略这些属性。
除此之外,服务端渲染也无须考虑事件绑定。因此,也应该忽略 props 对象中的事件处理函数。
更加严谨的属性处理方案如下:

function renderElementVNode(vnode) {const { type: tag, props, children } = vnodeconst isVoidElement = VOID_TAGS.includes(tag)let ret = `<${tag}`if (props) {// 调用 renderAttrs 函数进行严谨处理ret += renderAttrs(props)}ret += isVoidElement ? `/>` : `>`if (isVoidElement) return retif (typeof children === 'string') {ret += children} else {children.forEach(child => {ret += renderElementVNode(child)})}ret += ``return ret
}

对应 renderAttrs 函数对 props 处理,具体实现如下:

// 应该忽略的属性
const shouldIgnoreProp = ['key', 'ref']
function renderAttrs(props) {let ret = ''for (const key in props) {if (// 检测属性名称,如果是事件或应该被忽略的属性,则忽略它shouldIgnoreProp.includes(key) ||/^on[^a-z]/.test(key)) {continue}const value = props[key]// 调用 renderDynamicAttr 完成属性的渲染ret += renderDynamicAttr(key, value)}return ret
}

renderDynamicAttr 函数的实现如下:、

// 用来判断属性是否是 boolean attribute
const isBooleanAttr = key =>(`itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly` +`,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +`loop,open,required,reversed,scoped,seamless,` +`checked,muted,multiple,selected`).split(',').includes(key)
// 用来判断属性名称是否合法且安全
const isSSRSafeAttrName = key => !/[>/="'\u0009\u000a\u000c\u0020]/.test(key)
function renderDynamicAttr(key, value) {if (isBooleanAttr(key)) {// 对于 boolean attribute,如果值为 false,则什么都不需要渲染,否则只需要渲染 key 即可return value === false ? `` : ` ${key}`} else if (isSSRSafeAttrName(key)) {// 对于其他安全的属性,执行完整的渲染,// 注意:对于属性值,我们需要对它执行 HTML 转义操作return value === '' ? ` ${key}` : ` ${key}="${escapeHtml(value)}"`} else {// 跳过不安全的属性,并打印警告信息console.warn(`[@vue/server-renderer] Skipped rendering unsafe attribute name: ${key}`)return ``}
}

这样我们就实现了对普通元素类型的虚拟节点的渲染。
实际上,在 Vue.js中,由于 class 和 style 这两个属性可以使用多种合法的数据结构来表示,例如 class 的值可以是字符串、对象、数组,所以理论上我们还需要考虑这些情况。
不过原理都是相通的,对于使用不同数据结构表示的 class 或 style,我们只需要将不同类型的数据结构序列化成字符串表示即可。
另外,观察上面代码中的 renderDynamicAttr 函数的实现能够发现,在处理属性值时,我们调用了 escapeHtml 对其进行转义处理,这对于防御 XSS 攻击至关重要。HTML 转义指的是将特殊字符转换为对应的 HTML 实体。其转换规则很简单。

  • 如果该字符串作为普通内容被拼接,则应该对以下字符进行转义。
    • 将字符 & 转义为实体 &。
    • 将字符 < 转义为实体 <。
    • 将字符 > 转义为实体 >。
  • 如果该字符串作为属性值被拼接,那么除了上述三个字符应该被转义之外,还应该转义下面两个字符。
    • 将字符 " 转义为实体 "。
    • 将字符 ' 转义为实体 '。

具体实现如下:

const escapeRE = /["'&<>]/
function escapeHtml(string) {const str = '' + stringconst match = escapeRE.exec(str)if (!match) {return str}let html = ''let escapedlet indexlet lastIndex = 0for (index = match.index; index < str.length; index++) {switch (str.charCodeAt(index)) {case 34: // "escaped = '"'breakcase 38: // &escaped = '&'breakcase 39: // 'escaped = '''breakcase 60: // <escaped = '<'breakcase 62: // >escaped = '>'breakdefault:continue}if (lastIndex !== index) {html += str.substring(lastIndex, index)}lastIndex = index + 1html += escaped}return lastIndex !== index ? html + str.substring(lastIndex, index) : html
}

原理很简单,只需要在给定字符串中查找需要转义的字符,然后将其替换为对应的 HTML 实体即可。

18.3 将组件渲染为 HTML 字符串

在上节,我们讨论了如何将普通标签类型的虚拟节点渲染为 HTML 字符串。
本节,我们将在此基础上,讨论如何将组件类型的虚拟节点渲染为 HTML 字符串。
假设我们有如下组件,以及用来描述组件的虚拟节点:

// 组件
const MyComponent = {setup() {return () => {// 该组件渲染一个 div 标签return {type: 'div',children: 'hello',}}},
}
// 用来描述组件的 VNode 对象
const CompVNode = {type: MyComponent,
}

我们将实现 renderComponentVNode 函数,并用它把组件类型的虚拟节点渲染为 HTML 字符串:

  • subTree 本身可能是任意类型的虚拟节点,包括组件类型。因此,我们不能直接使用 renderElementVNode 来渲染它。
  • 执行 setup 函数时,也应该提供 setupContext 对象。而执行渲染函数 render 时,也应该将其 this 指向 renderContext 对象。实际上,在组件的初始化和渲染方面,其完整流程与第 13 章讲解的客户端的渲染流程一致。例如,也需要初始化 data,也需要得到 setup 函数的执行结果,并检查 setup 函数的返回值是函数还是 setupState 等。

对于第一个问题,我们可以通过封装通用函数来解决,如下所示:

function renderVNode(vnode) {const type = typeof vnode.typeif (type === 'string') {return renderElementVNode(vnode)} else if (type === 'object' || type === 'function') {return renderComponentVNode(vnode)} else if (vnode.type === Text) {// 处理文本...} else if (vnode.type === Fragment) {// 处理片段...} else {// 其他 VNode 类型}
}

有了 renderVNode 后,我们就可以在 renderComponentVNode 中使用它来渲染 subTree 了:

function renderComponentVNode(vnode) {let {type: { setup },} = vnodeconst render = setup()const subTree = render()// 使用 renderVNode 完成对 subTree 的渲染return renderVNode(subTree)
}

第二个问题则涉及组件的初始化流程。我们先回顾一下组件在客户端渲染时的整体流程:

image.png


在进行服务端渲染时,组件的初始化流程与客户端渲染时组件的初始化流程基本一致,但有两个重要的区别:

  • 服务端渲染的是应用的当前快照,它不存在数据变更后重新渲染的情况。因此,所有数据在服务端都无须是响应式的。利用这一点,我们可以减少服务端渲染过程中创建响应式数据对象的开销。
  • 服务端渲染只需要获取组件要渲染的 subTree 即可,无须调用渲染器完成真实 DOM 的创建。因此,在服务端渲染时,可以忽略“设置 render effect 完成渲染”这一步。

下图给出了服务端渲染时初始化组件的流程:

image.png


可以看到,只需要对客户端初始化组件的逻辑稍作调整,即可实现组件在服务端的渲染。
另外,由于组件在服务端渲染时,不需要渲染真实 DOM 元素,所以无须创建并执行 render effect。
这意味着,组件的 beforeMount 以及 mounted 钩子不会被触发。
而且,由于服务端渲染不存在数据变更后的重新渲染逻辑,所以 beforeUpdate 和 updated 钩子也不会在服务端执行。完整的实现如下:

function renderComponentVNode(vnode) {const isFunctional = typeof vnode.type === 'function'let componentOptions = vnode.typeif (isFunctional) {componentOptions = {render: vnode.type,props: vnode.type.props,}}let { render, data, setup, beforeCreate, created, props: propsOption } = componentOptionsbeforeCreate && beforeCreate()// 无须使用 reactive() 创建 data 的响应式版本const state = data ? data() : nullconst [props, attrs] = resolveProps(propsOption, vnode.props)const slots = vnode.children || {}const instance = {state,props, // props 无须 shallowReactiveisMounted: false,subTree: null,slots,mounted: [],keepAliveCtx: null,}function emit(event, ...payload) {const eventName = `on${event[0].toUpperCase() + event.slice(1)}`const handler = instance.props[eventName]if (handler) {handler(...payload)} else {console.error('事件不存在')}}// setuplet setupState = nullif (setup) {const setupContext = { attrs, emit, slots }const prevInstance = setCurrentInstance(instance)const setupResult = setup(shallowReadonly(instance.props), setupContext)setCurrentInstance(prevInstance)if (typeof setupResult === 'function') {if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')render = setupResult} else {setupState = setupContext}}vnode.component = instanceconst renderContext = new Proxy(instance, {get(t, k, r) {const { state, props, slots } = tif (k === '$slots') return slotsif (state && k in state) {return state[k]} else if (k in props) {return props[k]} else if (setupState && k in setupState) {return setupState[k]} else {console.error('不存在')}},set(t, k, v, r) {const { state, props } = tif (state && k in state) {state[k] = v} else if (k in props) {props[k] = v} else if (setupState && k in setupState) {setupState[k] = v} else {console.error('不存在')}},})created && created.call(renderContext)const subTree = render.call(renderContext, renderContext)return renderVNode(subTree)
}

观察上面的代码可以发现,该实现与客户端渲染的逻辑基本一致。
这段代码与第 13 章给出的关于组件渲染的代码也非常相似。
唯一的区别在于,在服务端渲染时,无须使用 reactive 函数为 data 数据创建响应式版本,并且 props 数据也无须是浅响应的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/943193.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

配置git

1 从项目中去掉现有的 Git 信息,并重新建立新的 Git 仓库 删除原有的 Git 信息:打开命令行工具,进入项目所在的目录。 执行以下命令以删除 .git 文件夹,这将移除所有的 Git 版本控制信息:对于 Linux 和 macOS: rm…

0253-CLAP-统计参数出现次数

环境Time 2022-12-02 WSL-Ubuntu 22.04 CLAP 4.0.29前言 说明 参考:https://docs.rs/clap/latest/clap/index.html 目标 统计参数出现的次数。 Cargo.toml [package] edition = "2021" name = "game&q…

什么情况下有必要使用抽象基类ABC?

在面向对象编程中,抽象基类(Abstract Base Class,简称ABC) 是一种不能被实例化的特殊类,其主要作用是定义一组子类必须实现的接口(方法或属性),从而强制子类遵循统一的规范。 抽象基类的核心特点:不能实例化:…

实用指南:TensorFlow2 Python深度学习 - 深度学习概述

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

HTTP/2协议漏洞引发史上最大DDoS攻击——Rapid Reset技术深度解析

本文深度解析基于HTTP/2协议CVE-2023-44487漏洞的Rapid Reset DDoS攻击技术细节。该攻击峰值达每秒3.98亿请求,仅用2万台机器就打破历史记录,文章还探讨了TCP连接终止等防御方案。史上最大DDoS攻击:Rapid Reset技术…

因果机器学习模型实战测试与比较

本文通过实际案例对比传统机器学习模型与专门设计的因果机器学习模型在效果评估上的差异,探讨了因果ML如何弥补预测性模型的局限性,并介绍了PyWhy和因果森林等工具的应用场景。因果机器学习模型实战测试与比较 因果机…

Berry.Live:开箱即用的.NET直播流媒体服务器

🚀 Berry.Live:开箱即用的.NET直播流媒体服务器想要快速搭建自己的直播平台?厌倦了复杂的流媒体服务器配置?Berry.Live 为你提供了一个简单、强大、开源的解决方案!🎯 什么是 Berry.Live? Berry.Live 是一个基…

Vscode误删文件如何恢复(二)?

如果是刚刚删除的,那么可以打开Source Control, 看到changes里面有刚刚删除的文件,拓宽视界窗口,可以看到文件后面有三个图标,选中第二个,即Discard Changes, 弹出提示框,询问你是否恢复该文件,点击Restore F…

01-C程序设计语言-第2版-第1章导言笔记

一、入门 1、编写的第一个程序:打印出“hello, world”点击查看代码 #include <stdio.h> //包含标准库信息 int main() //定义名为main函数,没有参数值 {printf("hello, world\n"); //显示字符re…

0252-CLAP-标记类型的参数

环境Time 2022-12-02 WSL-Ubuntu 22.04 CLAP 4.0.29前言 说明 参考:https://docs.rs/clap/latest/clap/index.html 目标 使用标记类型的参数。 Cargo.toml [package] edition = "2021" name = "game&q…

中国企业DevOps工具链选型标准深度解析:云原生与开源生态的博弈

中国企业DevOps工具链选型标准深度解析:云原生与开源生态的博弈 在数字化转型浪潮席卷各行各业之际,DevOps工具链的选择已成为中国企业技术战略中的关键决策。随着国内企业对于自主可控需求的日益增长,DevOps工具的…

AI智能外呼系统的工作原理解析

在很多企业看来,AI智能外呼系统已经成为销售线索跟进、客户回访、通知提醒等环节中不可或缺的工具。但在真正投入使用前,企业往往会产生疑问:AI外呼系统究竟是怎么“智能”的?它与传统自动拨号器或人工外呼有何不同…

HTTP状态码全览

HTTP状态码是用于表示HTTP请求消息的处理状态的代码。它们被分为五大类,每类都有不同的含义。以下是一些常见的HTTP状态码及其含义:1xx(信息性状态码):接收的请求正在处理100 Continue:服务器已收到请求头且客户…

免费白嫖Claude 4小技巧

免费白嫖Claude 4小技巧Posted on 2025-10-22 10:59 且行且思 阅读(0) 评论(0) 收藏 举报Kiro与Amazon Q:免费用上Claude 4的两种官方姿势免费白嫖Claude 4的两种官方小技巧!!想免费体验强大的 Claude 4 模型吗…

在PySide6/PyQt6的开发框架中,增加对表格多种格式录入的处理,以及主从表的数据显示和保存操作。

在PySide6/PyQt6的开发框架中,增加对表格多种格式录入的处理,以及主从表的数据显示和保存操作。在PySide6/PyQt6的开发框架中, 为了方便对表格数据的快速录入,有时候包括多种录入的类型,包括文本框、数字格式(整数…

笔记本电脑如何连接打印机?安装指南分享给你!

无论是处理居家办公文件,还是打印孩子的学习资料,打印机都是我们不可缺少的小伙伴!但许多用户在将笔记本电脑与打印机连接时,常会遇到物理连接出错、驱动不匹配、系统频繁报错等各种问题。本文将从连接方式到驱动安…

技术团队负责人咨询AI数智化升级改造路径

技术团队负责人咨询AI数智化升级改造路径一个客户咨询: 公司是做传统软件开发的 ERP、CRM、MES系统这类,目前发展遇到瓶颈,此类传统软件客户订单锐减,公司现金流紧张,观望到同行都在转AI-ERP,AI-MES,AI-CRM系统…

2025 年麦克风厂家最新推荐榜单:覆盖娱乐 / 演出 / 直播 / 会议多场景,精选技术领先口碑优良品牌助力采购

引言 随着音频设备应用场景不断拓展,麦克风已成为 KTV、舞台演出、直播、会议等场景的核心设备,但其市场现状却给用户带来诸多困扰。当前市场品牌繁杂,部分产品缺乏核心技术,无线传输不稳定、音频保真度低,且不少…

2025 年胶条厂家最新推荐排行榜:聚焦密封 / 系统门窗 / 环保领域,森特达领衔优质品牌榜单EPDM/硫化焊接/门窗复合/门窗幕墙胶条厂家推荐

引言 当前密封胶条市场需求旺盛,但产品质量参差不齐,劣质产品抗老化差、密封性能不足等问题频发,严重影响建筑节能、汽车密封等终端场景使用效果,采购商难以精准筛选优质品牌。为解决这一痛点,帮助下游企业及采购…

深入解析:智能物流管理|基于springboot+vue的智能物流管理系统

深入解析:智能物流管理|基于springboot+vue的智能物流管理系统2025-10-22 10:55 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !impo…