React 的 KeepAlive 实战指南:深度解析组件缓存机制

news2024/9/9 0:11:25

Vue 的 Keep-Alive 组件是用于缓存组件的高阶组件,可以有效地提高应用性能。它能够使组件在切换时仍能保留原有的状态信息,并且有专门的生命周期方便去做额外的处理。该组件在很多场景非常有用,比如:

· tabs 缓存页面

· 分步表单

· 路由缓存

在 Vue 中,通过 KeepAlive 包裹内的组件会自动缓存下来, 其中只能有一个直接子组件。

<KeepAlive>
  // <component 语法相当于 React的{showA ? <A /> : <B />}
   <component :is="showA ? 'A' : 'B'">
</KeepAlive>

可惜的是 React 官方目前并没有对外正式提供的 KeepAlive 组件,但是我们可以参考 Vue 的使用方式与 API 设计,实现一套 React 版本的 KeepAlive。

下文将为大家详细介绍三种不同的实现方式。

Style 隐藏法

Style 隐藏法是最简单方便的方式,直接使用 display: none 来代替组件的销毁。

封装一个 StyleKeepAlive 组件,传入的 showComponentName 属性表示当前要展示的组件名,同时 children 组件都需要定义下组件名 name。

const StyleKeepAlive: React.FC<any> = ({children, showComponentName}) => {
    return (
        <>
            {React.Children.map(children, (child) => (
                <div
                    style={{
                        display: child.props.name === showComponentName ? "block" : "none",
                    }}
                >
                    {child}
                </div>
            ))}
        </>
    );
}

// 使用
<StyleKeepAlive showComponentName={counterName}>
      <Counter name="A" />
      <Counter name="B" />
</StyleKeepAlive>

假如就这样写,勉强能实现要求,但会带来以下问题:

· 第一次挂载时每个子组件都会渲染一遍

· 父组件 render ,会导致子组件 render ,即使该组件目前是隐藏状态

· 对实际 dom 结构具有侵入式,如会为每个子组件包一层 div 用来控制 display 样式

file

我们研究下antd的Tabs 组件,其 TabPane 也是通过 display 来控制显隐的, 动态设置.ant-tabs-tabpane-hidden 类来切换。

可是它并没有一次性就把所有 TabPane 渲染出来,active 过一次后再通过类名来做控制显隐,且切换 tab后,除了第一次挂载会 render ,后续切换 tab 都不会 rerender 。

file

为了实现与 Tabs 一样的效果,我们稍加改造 StyleKeepAlive 组件, 对传入的 children 包裹一层 ShouldRender 组件,该组件实现初次挂载时只渲染当前激活的子组件, 且只有在组件激活时才会进行 rerender 。

const ShouldRender = ({ children, visible }: any) => {
    // 是否已经挂载
    const renderedRef = useRef(false);
    // 缓存子组件,避免不必要的渲染
    const childRef = useRef();

    if (visible) {
        renderedRef.current = true;
        childRef.current = children();
    } 

    if (!renderedRef.current) return null;

    return (
        <div
            style={{
                display: visible ? "block" : "none",
            }}
        >
            {childRef.current}
        </div>
    );
};

const StyleKeepAlive: React.FC<any> = ({children, showComponentName}) => {
    return (
        <>
            {React.Children.map(children, (child) => {
                const visible = child.props.name === showComponentName;
                return (
                    <ShouldRender visible={visible}>
                       {() => child}
                    </ShouldRender>
                );
            })}
        </>
    );
}

再来看看效果,我们实现了懒加载,但与antd 的 Tabs 不同的是, 父组件 render 时,我们对隐藏的子组件不会再进行 render , 这样能很大程度的减少性能影响。

file

这种方式虽然通过很简易的代码就实现了我们需要的 KeepAlive 功能,但其仍需要保留 dom 元素,在某些大数据场景下可能存在性能问题,并且以下面这种使用方法,会使开发者感觉到它是一次性渲染所有子组件。

<StyleKeepAlive showComponentName={componentName}>
      <Counter name="A" />
      <Counter name="B" />
</StyleKeepAlive>

// API可改写成这种形式更加直观, 且name也不再需要传
<StyleKeepAlive active={isActive}>
      <Counter />
</StyleKeepAlive>
<StyleKeepAlive active={isActive}>
      <Counter />
</StyleKeepAlive>

Suspense 法

Suspense 内部使用了 OffScreen 组件,这是一个类似于 KeepAlive 的组件,如下图所示,Suspense 的 children 会通过 OffScreen 包裹一层,因为 fallback 组件和 children 组件可能会多次进行切换。

file

既然 Offscreen 可以看成 React 内部的 KeepAlive 组件,那我们下面深入研究下它的特性。

由于Offscreen 目前还是unstable状态,我们安装试验性版本的 react 和 react-dom 可以去尝试这个组件。

pnpm add react@experimental react-dom@experimental

在组件中导入,注意:Offscreen 在今年某个版本后统一更名为了 Activity 。更名后其实更能体现出 KeepAlive 激活与失活的状态特性。

import { unstable_Activity as Offscreen } from "react";

Offscreen组件的使用方式也很简单,只有一个参数 mode: “visible” | ”hidden”。

<Offscreen mode={counterName === "A" ? "visible" : "hidden"}>
    <Counter name="A" />
</Offscreen>
<Offscreen mode={counterName === "B" ? "visible" : "hidden"}>
    <Counter name="B" />
</Offscreen>

我们再看看实际的页面效果:

file

第一次组件挂载时,竟然把应该隐藏的组件也渲染出来了,而且也是通过样式来控制显式隐藏的。

这乍看上去是不合理的,我们期望初次挂载时不要渲染失活的组件,否则类似于 Tabs 搭配数据请求的场景就不太适合了,我们不应该一次性请求所有 Tabs 中的数据。

但先别急,我们看看useEffect的执行情况,子组件中加入以下代码debug:

console.log(`${name} rendered`)

useEffect(() => {
    console.log(`${name} mounted`)
    return () => {
        console.log(`${name} unmounted`)
    }
}, [])

file

我们可以观察到,只有激活的组件A执行了 useEffect ,失活的组件B只是进行了一次pre-render 。

切换一次组件后,A组件卸载了,但是它最后又render了一次, 这是因为父组件中的 counterName更新了,导致子组件更新 。

file

我们得出结论:

通过 Offscreen 包裹的组件, useEffect 在每次激活时都会执行一次,且每次父组件更新都会导致其进行render。

虽然激活才会调用 useEffect 的机制解决了副作用会全部执行的问题,但对失活组件的pre-render 是否会造成性能影响?

进行下性能测试,对比使用常规 display 去实现的方法, 其中LongList 渲染20000条数据,且每条数据渲染依赖于参数 value, value 为受控组件控制,那么当我们在父组件进行输入时,是否会有卡顿呢?

const StyleKeepAliveNoPerf: React.FC<any> = ({children, showComponentName}) => {
    return (
        <>
            {React.Children.map(children, (child) => (
                <div
                    style={{
                        display: child.props.name === showComponentName ? "block" : "none",
                    }}
                >
                    {child}
                </div>
            ))}
        </>
    );
}

const LongList = ({value}: any) => {
    const [list] = useState(new Array(20000).fill(0))

    return (
        <ul style={{ height: 500, overflow: "auto" }}>
            {list.map((_, index) => (
                <li key={index}>{value}: {index}</li>
            ))}
        </ul>
    );
}

const PerformanceTest = () => {
    const [activeComponent, setActiveComponent] = useState('A');
    const [value, setValue] = useState('');

    return (
        <div className="card">
            <p>
                <button
                    onClick={() =>
                        setActiveComponent((val) => (val === "A" ? "B" : "A"))
                    }
                >
                    Toggle Counter
                </button>
            </p>
            <p>
                受控组件:
                <Input
                    value={value}
                    onChange={(e) => setValue(e.target.value)}
                />
            </p>
            <div>
                {/* 1. 直接使用display进行keep-alive */}
                <StyleKeepAliveNoPerf showComponentName={activeComponent}>
                    <Counter name="A" />
                    <LongList value={value} name="B" />
                </StyleKeepAliveNoPerf>

                {/* 2. 使用Offscreen */}
                <Offscreen mode={activeComponent === 'A' ? 'visible' : 'hidden'}>
                    <Counter name="A" />
                </Offscreen>
                <Offscreen mode={activeComponent === 'B' ? 'visible' : 'hidden'}>
                    <LongList value={value}/>
                </Offscreen>
            </div>
        </div>
    );
}

● 使用 StyleKeepAliveNoPerf

file

● 使用 Offscreen

file

我们可以看到,使用Offscreen 下几乎没有任何性能影响,且查看dom树,即使失活的LongList组件也照样被渲染出来了。

file

这样看来,使用 Offscreen 不但不会有性能影响,还有 pre-render 带来的某种意义上的性能提升。

这得益于React的 concurrent 模式,高优先级的组件会打断低优先级的组件的更新,用户输入事件拥有着最高的优先级,而 Offscreen 组件在失活时拥有着最低的优先级。如下为 Lane 模型中的优先级:

file

我们再与优化过的 StyleKeepAlive 组件比较,该组件对失活的组件不会进行 render,所以在进行输入时也非常流畅,但当我们切换组件渲染 LongList 时,出现了明细的卡顿掉帧,毕竟需要重新 render 一个长列表。而 Offscreen 在进行组件切换时就显得非常流畅了,只有 dispaly 改变时产生的重排导致的短暂卡顿感。

因此我们得出结论,使用Offscreen优于第一种Style方案。

由于该组件还是 unstable 的,我们无法直接在项目中使用,所以我们需要利用已经正式发布的 Suspense 去实现 Offscreen 版的 KeepAlive 。

Suspense 需要让子组件内部 throw 一个 Promise 错误来进行 children 与 fallback 间切换,那么我们只需要在激活时渲染 children , 失活时 throw Promise ,就能快速的实现 KeepAlive 。

const Wrapper = ({children, active}: any) => {
    const resolveRef = useRef();

    if (active) {
        resolveRef.current && resolveRef.current();
        resolveRef.current = null;
    } else {
        throw new Promise((resolve) => {
           resolveRef.current = resolve;
        })
    }

    return children;
}

const OffscreenKeepAlive = ({children, active}: any) => {
    return <Suspense>
        <Wrapper active={active}>
            {children}
        </Wrapper>
    </Suspense>
}

我们来看看实际效果。

初次渲染情况:

file

切换组件后渲染情况:

file

这与直接使用 Offscreen 的效果并不一致。

· 初次渲染只会渲染当前激活的组件,这是因为 Suspense 会在 render 时就抛出错误,那么当然不能把未激活的组件也 render 了

· 切换组件后,A组件的 useEffect 没有触发unmount , 也就是说,进行激活状态切换不会再去重新执行 useEffect

· 切换组件后,A组件失活,但没有进行render ,也就是说不会对失活的组件再进行渲染,也就是说没有了 pre-render 的特性

这样一来,虽然实现了 KeepAlive 功能,能够实现与我们的 StyleKeepAlive 完全一致的效果,但丢失了 Offscreen 激活/失活的生命周期,pre-render 预渲染等优点。

接下来,我们为其添加生命周期,由于失活的组件会直接被 throw 出去,子组件中的 useEffect 卸载函数不会被执行,我们需要把两个生命周期函数 useActiveEffect、useDeactiveEffect 中的回调注册给上层组件才能实现, 通过 context 传递注册函数。

const KeepAliveContext = React.createContext<{
    registerActiveEffect: (effectCallback) => void;
    registerDeactiveEffect: (effectCallback) => void;
}>({
    registerActiveEffect: () => void 0,
    registerDeactiveEffect: () => void 0,
});

export const useActiveEffect = (callback) => {
  const { registerActiveEffect } = useContext(KeepAliveContext);

  useEffect(() => {
    registerActiveEffect?.(callback);
  }, []);
};

export const useDeactiveEffect = (callback) => {
  const { registerDeactiveEffect } = useContext(KeepAliveContext);

  useEffect(() => {
    registerDeactiveEffect?.(callback);
  }, []);
};

我们在上层组件 KeepAlive 中对 effects 进行保存,并监听 active 状态的变化,以执行对应的生命周期函数。

const KeepAlive: React.FC<KeepAliveProps> = ({ active, children }) => {
  const activeEffects = useRef([]);
  const deactiveEffects = useRef([]);

  const registerActiveEffect = (callback) => {
    activeEffects.current.push(() => {
      callback();
    });
  };

  const registerDeactiveEffect = (callback) => {
    deactiveEffects.current.push(() => {
      callback();
    });
  };

  useEffect(() => {
    if (active) {
      activeEffects.current.forEach((effect) => {
        effect();
      });
    } else {
      deactiveEffects.current.forEach((effect) => {
        effect();
      });
    }
  }, [active]);

  return (
    <KeepAliveContext.Provider value={{ registerActiveEffect, registerDeactiveEffect }}>
      <Suspense fallback={null}>
        <Wrapper active={active}>{children}</Wrapper>
      </Suspense>
    </KeepAliveContext.Provider>
  );
};

至此,我们实现了一个相对比较完美的基于 Suspense 的 KeepAlive 组件。

DOM 移动法

由于组件的状态保存的一个前提是该组件必须存在于 React组件树 中,也就是说必须把这个组件 render 出来,但 render 并不是意味着这个组件会存在于DOM树中,如 createPortal 能把某个组件渲染到任意一个DOM节点上,甚至是内存中的DOM节点。

那么要实现 KeepAlive ,我们可以让这个组件一直存在于 React组件树 中,但不让其存在于 DOM树中。

社区中两个 KeepAlive 实现使用最多的库都使用了该方法,react-keep-alive, react-activation ,下面以 react-activation 最简单实现为例。完整实现见 react-activation:https://github.com/CJY0208/react-activation/

file

具体实现如下:

· 在某个不会被销毁的父组件(比如根组件)上创建一个 state 用来保存所有需要 KeepAlive 的 children ,并通过 id 标识

· KeepAlive 组件会在首次挂载时将 children 传递给父组件

· 父组件接收到 children,保存至 state 触发重新渲染,在父组件渲染所有KeepAlive children,得到真实DOM节点,将DOM节点移动至实际需要渲染的位置

· KeepAlive 组件失活时,组件销毁,DOM节点也销毁,但 children 是保存在父组件渲染的,所以状态得以保存

· KeepAlive 再次激活时,父组件拿到缓存的 children,重新渲染一编,完成状态切换

import { Component, createContext } from 'react'

const KeepAliveContext = createContext({});

const withScope = WrappedComponent => props => (
  <KeepAliveContext.Consumer>{keep => <WrappedComponent {...props} keep={keep} />}</KeepAliveContext.Consumer>
)

export class AliveScope extends Component<any> {
  nodes = {};
  state = {};

  keep = (id, children) => {
    return new Promise((resolve) =>
      this.setState(
        {
          [id]: { id, children },
        },
        () => resolve(this.nodes[id])
      )
    );
  };

  render() {
    return (
      <KeepAliveContext.Provider value={this.keep}>
        {this.props.children}
        <div className='keepers-store'>
          {Object.values(this.state).map(({ id, children }: any) => (
        <div
          key={id}
          ref={(node) => {
            this.nodes[id] = node;
          }}
          >
          {children}
        </div>
      ))}
        </div>

      </KeepAliveContext.Provider>
    );
  }
}

class ActivationKeepAlive extends Component {
  constructor(props) {
    super(props)
  }

  placeholder: HTMLElement | null = null;

  componentDidMount(): void {
    this.init(this.props)
  }

  init = async ({ id, children, keep }) => {
    // keep用于向父组件传递最新的children,并返回该children对应的DOM节点
    const realContent = await keep(id, children)
    // appendChild为剪切操作
    this.placeholder?.appendChild(realContent)
  }

  // 只渲染占位元素,不渲染children
  render() {
    return (
      <div
        className='keep-placeholder'
        ref={node => {
          this.placeholder = node
        }}
        />
    )
  }
}

export default withScope(ActivationKeepAlive)

  // 使用
<AliveScope>
  {counterName === "A" && (
    <ActivationKeepAlive id="A">
      <Counter name="A" />
    </ActivationKeepAlive>
  )}
  {counterName === "B" && (
  <ActivationKeepAlive id="B">
    <Counter name="B" />
  </ActivationKeepAlive>
  )}
</AliveScope>

组件树如下,渲染在了 AliveScope 下,而非 ActivationKeepAlive 下。

file

虽然这种方法理论性可行,但实际上会有很多事情要处理,比如事件流会乱掉,父组件更新渲染也会有问题,因为children 实际渲染在 AliveScope 上, 要让 AliveScope 重新渲染才会使 children 重新渲染。

在 react-activation 中,也还有部分问题有待解决,如果使用 createPortal 方案,也只是 AliveScope 中免去了移动 DOM 的操作(隐藏时渲染在空标签下,显示时渲染在占位节点下)。

《行业指标体系白皮书》下载地址:https://www.dtstack.com/resources/1057?src=szsm

《数栈产品白皮书》下载地址:https://www.dtstack.com/resources/1004?src=szsm

《数据治理行业实践白皮书》下载地址:https://www.dtstack.com/resources/1001?src=szsm

想了解或咨询更多有关大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=szcsdn

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

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

相关文章

C++类与对象-六大成员函数

默认成员函数就是用户没有显式实现&#xff0c;编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个空类编译器会默认⽣成8个默认成员函数。本文只介绍其中6个&#xff0c;C11增加两个函数见后续博客。 目录 一、构造函数 1.1 概念 1.2 特性 1.3 使用举例 1.4 初始化列表 1…

旷野之间32 - OpenAI 拉开了人工智能竞赛的序幕,而Meta 将会赢得胜利

他们通过故事做到了这一点&#xff08;Snapchat 是第一个&#xff09;他们用 Reels 实现了这个功能&#xff08;TikTok 是第一个实现这个功能的&#xff09;他们正在利用人工智能来实现这一点。 在人工智能竞赛开始时&#xff0c;Meta 的人工智能平台的表现并没有什么特别值得…

Java面试八股之@Qualifier的作用

Qualifier的作用 Qualifier 是 Spring 框架中的一个非常有用的注解&#xff0c;它主要用于解决在依赖注入过程中出现的歧义问题。当 Spring 容器中有多个相同类型的 Bean 时&#xff0c;Qualifier 可以帮助指明应该使用哪一个具体的 Bean 进行注入。 Qualifier 的作用&#x…

【error】AttributeError: module ‘cv2.dnn‘ has no attribute ‘DictValue‘(库冲突)

conda list conda remove opencv pip uninstall opencv-python conda list pip 同时卸载两个库 pip uninstall opencv-contrib-python opencv-python 没有and 直接写库名 module ‘cv2.dnn‘ has no attribute ‘DictValue‘解决办法_module cv2.dnn has no attribute d…

spark 3.0.0源码环境搭建

环境 Spark版本&#xff1a;3.0.0 java版本&#xff1a;1.8 scala版本&#xff1a;2.12.19 Maven版本&#xff1a;3.8.1 编译spark 将spark-3.0.0的源码导入到idea中 执行mvn clean package -Phive -Phive-thriftserver -Pyarn -DskipTests 执行sparksql示例类SparkSQLExam…

人工智能如何推动工业数字化转型?

随着科技的浪潮汹涌向前&#xff0c;人工智能&#xff08;AI&#xff09;正日益成为推动工业数字化发展的核心引擎。其强大的影响力不仅为工业生产注入了智能化、自动化的新活力&#xff0c;更在优化资源配置、提升生产效率以及实现个性化制造等关键领域展现出了无与伦比的潜力…

2020真题-架构师案例(五)

问题1&#xff08;13分&#xff09; 针对该系统的功能&#xff0c;孪工建议采用管道-过滤器&#xff08;pipe and filter&#xff09;的架构风格&#xff0c;而王工则建议采用仓库&#xff08;reposilory&#xff09;架构风格。满指出该系统更适合采用哪种架构风格&#xff0c…

数据库练习5

建立两个表:goods(商品表)、orders(订单表) 在商品表中导入商品记录并查看数据 建立触发器&#xff0c;订单表中增加订单数量后&#xff0c;商品表商品数量同步减少对应的商品订单出数量,并测试 下订单前查询数据库 下订单后查询数据库 建立触发器&#xff0c;实现功能:客户取消…

Matlab编程资源库(17)符号对象

一、建立符号对象 1 &#xff0e;建立符号变量和符号常量 MATLAB 提供了两个建立符号对象的函数&#xff1a; sym 和 syms &#xff0c;两个函数的用法不同。 (1) sym 函数 sym 函数用来建立单个符号量&#xff0c;一般调用格式为&#xff1a; 符号量名 sym( 符号字符串 )…

第一代iPad Mini完美降级8.4.1

文章目录 写在前面准备工作iOS 9.3.5版本越狱踩坑记录正确步骤越狱后设置 写在最后 写在前面 前几天打扫房间&#xff0c;不小心翻出来了10年前的第一代iPad Mini&#xff0c;版本升级到9.3.5之后&#xff0c;基本上算是报废了&#xff0c;运行啥都卡成狗&#xff1b;但是买不…

R语言 爬取数据+简单清洗

小小练习。见代码注释 # 加载必要的包 library(rvest) library(dplyr) library(tidyr)# 指定网页URL url <- "https://research.un.org/en/unmembers/scmembers"# 读取网页内容 webpage <- read_html(url)# 提取所有表格节点 table_nodes <- html_nodes(web…

聊聊基于Alink库的特征工程方法

独热编码 OneHotEncoder 是用于将类别型特征转换为独热编码的类。独热编码是一种常用的特征编码方式&#xff0c;特别适用于处理类别型特征&#xff0c;将其转换为数值型特征。 对于每个类别型特征&#xff0c;OneHotEncoder 将其编码成一个长度为类别数量的向量。 每个类别对…

使用 Python 进行马尔可夫链职业路径建模

欢迎来到雲闪世界。从职业角度来说&#xff0c;我是个非常奇怪的人&#xff1a;我在一家初创公司担任软件/机器学习工程师&#xff0c;拥有物理学硕士学位&#xff0c;即将为航空航天和机械工程博士学位论文答辩。在我不断变化的职业生涯中&#xff0c;有两件事始终不变&#x…

Java9-21的开发相关新特性总结

目录 下载地址 Java 21(LTS) 概述 变动说明 1、JEP 441: Switch 的模式匹配&#xff08;正式特性&#xff09; 功能进化 Switch 模式匹配 类型标签 null标签 守卫标签 使用enum常量作值 语法总结 2、JEP 440&#xff1a;Record模式&#xff08;正式特性&#xff09…

GPU的shader分支跳转性能总结

引言&#xff1a; 如下的&#xff08;一&#xff09;与&#xff08;二&#xff09;分别属于uniform branch与宏定义&#xff0c;&#xff08;一&#xff09;至始至终是一个固定的值&#xff0c;分支只执行一条而不是既有执行condition ture 也有执行condition false 的情况&am…

基于CentOS Stream 9平台安装MySQL Community Server 9.0.1 Innovation

1. 安装之前 1.1 查看系统版本 cat /etc/redhat-releaseCentOS Stream release 9 1.2 查看cpu架构 lscpu架构&#xff1a; x86_64 CPU 运行模式&#xff1a; 32-bit, 64-bit 2. 官网下载 https://dev.mysql.com/downloads/mysql/ 要多看看 官方9.0文档&#xff1a;https://d…

前端面试基础题(微信公众号:前端面试成长之路)

BFC、IFC、GFC、FFC CSS2.1中只有BFC和IFC, CSS3中才有GFC和FFC。 到底什么是BFC、IFC、GFC和FFC Whats FC&#xff1f; 一定不是KFC&#xff0c;FC的全称是&#xff1a;Formatting Contexts&#xff0c;是W3C CSS2.1规范中的一个概念。它是页面中的一块渲染区域&#xff0c;并…

【C++进阶学习】第九弹——哈希的原理与实现——开放寻址法的讲解

前言&#xff1a; 在前面&#xff0c;我们已经学习了很多存储机构&#xff0c;包括线性存储、树性存储等&#xff0c;并学习了多种拓展结构&#xff0c;效率也越来越高&#xff0c;但是是否有一种存储结构可以在大部分问题中都一次找到目标值呢&#xff1f;哈希可能能实现 目录…

npm国内淘宝镜像registry镜像过期

我们在使用npm的时候会遇到淘宝镜像安装过期的问题 首先了解 npm 淘宝镜像是一个 npm 注册表的镜像&#xff0c;用于加速 npm 包的下载。 一、如何设置&#xff1f; 如何设置淘宝镜像&#xff1f;淘宝镜像已经从 registry.npm.taobao.org 切换到了 registry.npmmirror.com n…

【书生大模型实战营(暑假场)】入门任务一 Linux+InternStudio 关卡

入门任务一 LinuxInternStudio 关卡 参考&#xff1a; 教程任务 1 闯关任务 1.1 基于 VScode 的 SSH 链接 感谢官方教程的清晰指引&#xff0c;基于VS code 实现 SSH 的链接并不困难&#xff0c;完成公钥配之后&#xff0c;可以实现快速一键链接&#xff0c;链接后效果如下…