前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >天天用 antd 的 Form 组件?自己手写一个吧

天天用 antd 的 Form 组件?自己手写一个吧

作者头像
神说要有光zxg
发布于 2024-04-10 11:13:00
发布于 2024-04-10 11:13:00
37300
代码可运行
举报
运行总次数:0
代码可运行

大家写中后台系统的时候,应该都用过 Ant Design 的 Form 组件:

用 Form.Item 包裹 Input、Checkbox 等表单项,可以定义 rules,也就是每个表单项的校验规则。

外层 Form 定义 initialValues 初始值,onFinish 当提交时的回调,onFinishFailed 当提交有错误时的回调。

Form 组件每天都在用,那它是怎么实现的呢?

其实原理不复杂。

每个表单项都有 value 和 onChange 参数,我们只要在 Item 组件里给 children 传入这俩参数,把值收集到全局的 Store 里。

这样在 Store 里就存储了所有表单项的值,在 submit 时就可以取出来传入 onFinish 回调。

并且,还可以用 async-validator 对表单项做校验,如果有错误,就把错误收集起来传入 onFinishFailed 回调。

那这些 Item 是怎么拿到 Store 来同步表单值的呢?

用 Context。

在 Form 里保存 Store 到 Context,然后在 Item 里取出 Context 的 Store 来,同步表单值到 Store。

我们来写下试试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npx create-vite

安装依赖,改下 main.tsx

然后创建 Form/FormContext.ts

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { createContext } from 'react';

export interface FormContextProps {
  values?: Record<string, any>;
  setValues?: (values: Record<string, any>) => void;
  onValueChange?: (key: string, value: any) => void;
  validateRegister?: (name:string, cb: Function) => void;
}

export default createContext<FormContextProps>({})

在 context 里保存 values 也就是 Store 的值。

然后添加 setValues 来修改 values

onValueChange 监听 value 变化

validateRegister 用来注册表单项的校验规则,也就是 rules 指定的那些。

然后写下 Form 组件 Form/Form.tsx

参数传入初始值 initialValues、点击提交的回调 onFinish、点击提交有错误时的回调 onFinishFailed。

这里的 Record<string,any> 是 ts 的类型,任意的对象的意思。

用 useState 保存 values,用 useRef 保存 errors 和 validator

为什么不都用 useState 呢?

因为修改 state 调用 setState 的时候会触发重新渲染。

而 ref 的值保存在 current 属性上,修改它不会触发重新渲染。

errors、validator 这种就是不需要触发重新渲染的数据。

然后 onValueChange 的时候就是修改 values 的值。

submit 的时候调用 onFinish,传入 values,再调用所有 validator 对值做校验,如果有错误,调用 onFinishFailed 回调:

然后把这些方法保存到 context 中,并且给原生 form 元素添加 onSubmit 的处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import React, { CSSProperties, useState, useRef, FormEvent, ReactNode } from 'react';
import classNames from 'classnames';
import FormContext from './FormContext';

export interface FormProps extends React.HTMLAttributes<HTMLFormElement> {
    className?: string;
    style?: CSSProperties;
    onFinish?: (values: Record<string, any>) => void;
    onFinishFailed?: (errors: Record<string, any>) => void;
    initialValues?: Record<string, any>;
    children?: ReactNode
}

const Form = (props: FormProps) => {
    const { 
        className, 
        style,
        children, 
        onFinish,
        onFinishFailed,
        initialValues,
        ...others 
    } = props;

    const [values, setValues] = useState<Record<string, any>>(initialValues || {});

    const validatorMap = useRef(new Map<string, Function>());

    const errors = useRef<Record<string, any>>({});

    const onValueChange = (key: string, value: any) => {
        values[key] = value;
    }

    const handleSubmit = (e: FormEvent) => {
        e.preventDefault();

        for (let [key, callbackFunc] of validatorMap.current) {
            if (typeof callbackFunc === 'function') {
                errors.current[key] = callbackFunc();
            }
        }

        const errorList = Object.keys(errors.current).map(key => {
                return errors.current[key]
        }).filter(Boolean);

        if (errorList.length) {
            onFinishFailed?.(errors.current);
        } else {
            onFinish?.(values);
        }
    }

    const handleValidateRegister = (name: string, cb: Function) => {
        validatorMap.current.set(name, cb);
    }

    const cls = classNames('ant-form', className);

    return (
        <FormContext.Provider
            value={{
                onValueChange,
                values,
                setValues: (v) => setValues(v),
                validateRegister: handleValidateRegister
            }}
        >
            <form {...others} className={cls} style={style} onSubmit={handleSubmit}>{children}</form>
        </FormContext.Provider>
    );
}

export default Form;

这里用到了 classnames 包要安装下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm install --save classnames

接下来添加 Form/Item.tsx,也就是包装表单项用的组件:

首先是参数,可以传入 label、name、valuePropName、rules 等:

valuePropName 默认是 value,当 checkbox 等表单项就要取 checked 属性了:

这里 children 类型为 ReactElement 而不是 ReactNode。

因为 ReactNode 除了包含 ReactElement 外,还有 string、number 等:

而作为 Form.Item 组件的 children,只能是 ReactElement。

然后实现下 Item 组件:

如果没有传入 name 参数,那就直接返回 children。

比如这种就不需要包装:

创建两个 state,分别存储表单值 value 和 error。

从 context 中读取对应 name 的 values 的值,同步设置 value:

然后 React.cloneElement 复制 chilren,额外传入 value、onChange 等参数:

onChange 回调里设置 value,并且修改 context 里的 values 的值:

这里的 getValueFromEvent 是根据表单项类型来获取 value:

然后是校验 rules,这个是用 async-validator 这个包:

在 context 注册 name 对应的 validator 函数:

然后 Item 组件渲染 label、children、error

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import React, { ReactNode, CSSProperties, useState, useContext, ReactElement, useEffect, PropsWithChildren, ChangeEvent } from 'react';
import classNames from 'classnames';
import Schema, { Rules } from 'async-validator';

import FormContext from './FormContext';

export interface ItemProps{
    className?: string;
    style?: CSSProperties;
    label?: ReactNode;
    name?: string;
    valuePropName?: string;
    rules?: Array<Record<string, any>>;
    children?: ReactElement
}

const getValueFromEvent = (e: ChangeEvent<HTMLInputElement>) => {
    const { target } = e;
    if (target.type === 'checkbox') {
        return target.checked;
    } else if (target.type === 'radio') {
        return target.value;
    }

    return target.value;
}

const Item = (props: ItemProps) => {
    const { 
        className,
        label,
        children,
        style,
        name,
        valuePropName,
        rules,
    } = props;

    if(!name) {
        return children;
    }

    const [value, setValue] = useState<string | number | boolean>();
    const [error, setError] = useState('');

    const { onValueChange, values, validateRegister } = useContext(FormContext);

    useEffect(() => {
        if (value !== values?.[name]) {
            setValue(values?.[name]);
        }
    }, [values, values?.[name]])

    const handleValidate = (value: any) => {
        let errorMsg = null;
        if (Array.isArray(rules) && rules.length) {
            const validator = new Schema({
                [name]: rules.map(rule => {
                    return {
                        type: 'string',
                        ...rule
                    }
                })
            });

            validator.validate({ [name]:value }, (errors) => {
                if (errors) {
                    if (errors?.length) {
                        setError(errors[0].message!);
                        errorMsg = errors[0].message;
                    }
                } else {
                    setError('');
                    errorMsg = null;
                }
            });

        }

        return errorMsg;
    }

    useEffect(() => {
        validateRegister?.(name, () => handleValidate(value));
    }, [value]);

    const propsName: Record<string, any> = {};
    if (valuePropName) {
        propsName[valuePropName] = value;
    } else {
        propsName.value = value;
    }

    const childEle = React.Children.toArray(children).length > 1 ? children: React.cloneElement(children!, {
        ...propsName,
        onChange: (e: ChangeEvent<HTMLInputElement>) => {
            const value = getValueFromEvent(e);
            setValue(value);
            onValueChange?.(name, value);

            handleValidate(value);
        }
    });

    const cls = classNames('ant-form-item', className);

    return (
        <div className={cls} style={style}>
            <div>
                {
                    label && <label>{label}</label>
                }
            </div>
            <div>
                {childEle}
                {error && <div style={{color: 'red'}}>{error}</div>}
            </div>
        </div>
    )
}

export default Item;

安装用到的 async-validator:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm install --save async-validator

然后在 Form/index.tsx 导出下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import InternalForm from './Form';
import Item from './Item';

type InternalFormType = typeof InternalForm;

interface FormInterface extends InternalFormType {
  Item: typeof Item;
} 

const Form = InternalForm as FormInterface;

Form.Item = Item;

export default Form;

主要是把 Item 挂在 Form 下。

在 App.tsx 测试下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Button, Checkbox, Input } from "antd";
import Form from "./Form/index";

const Basic: React.FC = () => {
  const onFinish = (values: any) => {
    console.log('Success:', values);
  };

  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };

  return (
    <Form
      initialValues={{ remember: true, username: '神说要有光' }}
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
    >
      <Form.Item
        label="Username"
        name="username"
        rules={[
          { required: true, message: '请输入用户名!' },
          { max: 6, message: '长度不能大于 6' }
        ]}
      >
        <Input />
      </Form.Item>

      <Form.Item
        label="Password"
        name="password"
        rules={[{ required: true, message: '请输入密码!' }]}
      >
        <Input.TextArea />
      </Form.Item>

      <Form.Item name="remember" valuePropName="checked">
        <Checkbox>记住我</Checkbox>
      </Form.Item>

      <Form.Item>
        <div>
          <Button type="primary" htmlType="submit" >
            登录
          </Button>
        </div>
      </Form.Item>
    </Form>
  );
};

export default Basic;

除了 Form 外,具体表单项用的 antd 的组件。

试一下:

form 的 initialValues 的设置、表单的值的保存,规则的校验和错误显示,都没问题。

这样,Form 组件的核心功能就完成了。

核心就是一个 Store 来保存表单的值,然后用 Item 组件包裹具体表单,设置 value 和 onChange 来同步表单的值。

当值变化以及 submit 的时候用 async-validator 来校验。

那 antd 的 Form 也是这样实现的么?

基本是一样的。

我们来看下源码:

antd 的 Form 有个叫 FormStore 的类:

它的 store 属性保存表单值,然后暴露 getFieldValue、setFieldValue 等方法来读写 store。

然后它提供了一个 useForm 的 hook 来创建 store:

用的时候这样用:

这样,Form 组件里就可以通过传进来的 store 的 api 来读写 store 了:

当然,它会通过 context 把 store 传递下去:

在 Field 也就是 Item 组件里就通过 context 取出 store 的 api 来读写 store:

和我们的实现有区别么?

有点区别,antd 的 FormStore 是可以独立出来的,通过 useForm 创建好传入 Form 组件。

而我们的 Store 没有分离出来,直接内置在 Form 组件里了。

但是实现的思路都是一样的。

提供个 useForm 的 api 的好处是,外界可以拿到 store 的 api 来自己修改 store。

当然,我们也可以通过 ref 来做这个:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import React, { CSSProperties, useState, useRef, FormEvent, ReactNode, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react';
import classNames from 'classnames';
import FormContext from './FormContext';

export interface FormProps extends React.HTMLAttributes<HTMLFormElement> {
    className?: string;
    style?: CSSProperties;
    onFinish?: (values: Record<string, any>) => void;
    onFinishFailed?: (errors: Record<string, any>) => void;
    initialValues?: Record<string, any>;
    children?: ReactNode
}

export interface FormRefApi {
    getFieldsValue: () => Record<string, any>,
    setFieldsValue: (values: Record<string, any>) => void,
}

const Form= forwardRef<FormRefApi, FormProps>((props: FormProps, ref) => {
    const { 
        className, 
        style,
        children, 
        onFinish,
        onFinishFailed,
        initialValues,
        ...others 
    } = props;

    const [values, setValues] = useState<Record<string, any>>(initialValues || {});

    useImperativeHandle(ref, () => {
        return {
            getFieldsValue() {
                return values;
            },
            setFieldsValue(values) {
                for(let [key, value] of Object.entries(values)) {
                    values[key] = value
                }
                setValues(values);
            }
        }
    }, []);

    const validatorMap = useRef(new Map<string, Function>());

    const errors = useRef<Record<string, any>>({});

    const onValueChange = (key: string, value: any) => {
        values[key] = value;
    }

    const handleSubmit = (e: FormEvent) => {
        e.preventDefault();

        for (let [key, callbackFunc] of validatorMap.current) {
            if (typeof callbackFunc === 'function') {
                errors.current[key] = callbackFunc();
            }
        }

        const errorList = Object.keys(errors.current).map(key => {
                return errors.current[key]
        }).filter(Boolean);

        if (errorList.length) {
            onFinishFailed?.(errors.current);
        } else {
            onFinish?.(values);
        }
    }

    const handleValidateRegister = (name: string, cb: Function) => {
        validatorMap.current.set(name, cb);
    }

    const cls = classNames('ant-form', className);

    return (
        <FormContext.Provider
            value={{
                onValueChange,
                values,
                setValues: (v) => setValues(v),
                validateRegister: handleValidateRegister
            }}
        >
            <form {...others} className={cls} style={style} onSubmit={handleSubmit}>{children}</form>
        </FormContext.Provider>
    );
})

export default Form;

然后在 App.tsx 试试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Button, Checkbox, Input } from "antd";
import Form from "./Form/index";
import { useEffect, useRef } from "react";
import { FormRefApi } from "./Form/Form";

const Basic: React.FC = () => {
  const onFinish = (values: any) => {
    console.log('Success:', values);
  };

  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };

  const form = useRef<FormRefApi>(null);

  return (
    <>
      <Button type="primary" onClick={() => {
        console.log(form.current?.getFieldsValue())
      }}>打印表单值</Button>

      <Button type="primary" onClick={() => {
        form.current?.setFieldsValue({
          username: '东东东'
        })
      }}>设置表单值</Button>

      <Form
        ref={form}
        initialValues={{ remember: true, username: '神说要有光' }}
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
      >
        <Form.Item
          label="Username"
          name="username"
          rules={[
            { required: true, message: '请输入用户名!' },
            { max: 6, message: '长度不能大于 6' }
          ]}
        >
          <Input />
        </Form.Item>

        <Form.Item
          label="Password"
          name="password"
          rules={[{ required: true, message: '请输入密码!' }]}
        >
          <Input.TextArea />
        </Form.Item>

        <Form.Item name="remember" valuePropName="checked">
          <Checkbox>记住我</Checkbox>
        </Form.Item>

        <Form.Item>
          <div>
            <Button type="primary" htmlType="submit" >
              登录
            </Button>
          </div>
        </Form.Item>
      </Form>
    </>
  );
};

export default Basic;

当然,你也可以把 store 的 api 处理出来,然后封装个 useForm 的 hook 来传入 Form 组件。

这样,用法比 ref 的方式简单点。

至此,我们就实现了 antd 的 Form 的功能。

案例代码上传了 react 小册仓库:https://github.com/QuarkGluonPlasma/react-course-code/tree/main/form-component

总结

我们每天都在用 antd 的 Form 组件,今天自己实现了下。

其实原理不复杂,就是把 Form 的表单项的值存储到 Store 中。

在 Form 组件里把 Store 放到 Context,在 Item 组件里取出来。

用 Item 组件包裹表单项,传入 value、onChange 参数用来同步表单值到 Store。

这样,表单项的值变化或者 submit 的时候,就可以根据 rules 用 async-validator 来校验。

此外,我们还通过 ref 暴露出了 setFieldsValue、getFieldsValue 等 store 的 api。

当然,在 antd 的 Form 里是通过 useForm 这个 hook 来创建 store,然后把它传入 Form 组件来用的。

两种实现方式都可以。

每天都用 antd 的 Form 组件,不如自己手写一个吧!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神光的编程秘籍 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
LINUX下tar.bz2包的安装方法
声明:个人觉得文章不错,所以转载过来分享以及自己收藏,只是原出处已经无法查明,只能附上我见文章的地址:
全栈程序员站长
2022/09/06
4.1K0
Linux下编译安装Apache httpd 2.4
Apache是世界使用排名第一的Web服务器软件。它可以运行在几乎所有广泛使用的计算机平台上,由于其跨平台和安全性被广泛使用,是最流行的Web服务器端软件之一。当前Apache版本为2.4,本文主要描述基于CentOS 6.5以源码方式安装Apache httpd。 一、编译安装的优势 源码的编译安装一般由3个步骤组成: 配置(configure),通常依赖gcc编译器,binutils,glibc。配置软件特性,检查编译环境,生成 Makefile文件 编译(make) 安装
Leshami
2018/08/13
2.7K0
Linux安装软件常用方法总结
1、找到相应的软件包,比如soft.version-rpm,wget soft-version.rpm; 2、cd soft.version.rpm所在的目录; 3、rpm -ivh soft.version.rpm即可安装
菲宇
2019/06/12
1.1K0
Linux安装Nginx二-基于源码编译安装
在上一篇文章中,Linux安装Nginx1-基于yum安装 只是基于yum安装的。本篇,咱们就来讲讲基于源码编译安装nginx.
凯哥Java
2022/12/16
1.1K0
Linux安装Nginx二-基于源码编译安装
快速学习-Linux软件的安装方式
在Linux CentOS系统中,软件的管理方式有三种:rpm、yum、编译方式。
cwl_java
2020/03/28
9990
源码编译安装nginx
1、下载 2、解压源码包 3、准备编译环境 4、检查(依赖,兼容),预编译 5、编译 6、安装
程裕强
2022/05/06
5400
源码编译安装nginx
linux上安装软件详细步骤(开关安装方法图解)
一.软件的类型 二.Tar包安装、升级、卸载(必须会) 三.RPM软件包安装及管理(必须会) 四.脚本安装、升级、卸载 五.SRPM包安装(知道就行,很少用)
全栈程序员站长
2022/07/31
2.7K0
linux上安装软件详细步骤(开关安装方法图解)
CentOS 6.5编译安装httpd-2.4.7
[root@NFSServer ~]# yum groupinstall "Development tools"
星哥玩云
2022/06/29
3160
PHP编译安装
#wgethttp://museum.php.net/php5/php-5.2.6.tar.gzPHP
Java架构师必看
2021/03/22
1.5K0
安装PHP5,安装PHP7
PHP主流版本是5.x/7.x,不过大部分企业都是使用着PHP5.x版本,因为有些程序是基于5.x版本开发的,如果使用7.x版本可能就会出问题,7.x是这两年才出来的,这两个版本区别比较大,7.x的性能要比5.x版本有所提升。
端碗吹水
2020/09/23
3K0
安装PHP5,安装PHP7
CentOS 6.5上编译安装httpd-2.4和2.4版本特性介绍
1) MPM支持在运行时装载; --enalbe-mpm-shared=all --with-mpm={prefork|worker|event} 2) 支持event mpm 3) 异步读写 4) 在每模块及每目录分别使用不同的日志级别 5) 每请求的配置: <If>,<Elseif> 6) 增强版的表达式分析器 7) 毫秒级的keep alive的timeout 8) 基于FQDN的虚拟主机不再需要NameVirtualHost指令; 9) 支持用户使用自定义变量
星哥玩云
2022/07/04
6100
CentOS 6.5上编译安装httpd-2.4和2.4版本特性介绍
Linux如何下载安装软件超详细解析
网上很多Linux下载软件的方法,看了很多帖子感觉Linux下载软件的方式有很多,每个人都有自己的习惯,对于一个新手来说及其不友好,有时候会看的很蒙。在这里做出总结。
全栈程序员站长
2022/07/31
7.5K0
Linux如何下载安装软件超详细解析
编译安装最新版httpd-2.4
新版本的httpd-2.4新增以下特性; 新增模块; mod_proxy_fcgi(可提供fcgi代理) mod_ratelimit(限制用户带宽) mod_request(请求模块,对请求做过滤) mod_remoteip(匹配客户端的IP地址) 对于基于IP的访问控制做了修改,不再支持allow,deny,order机制,而是统一使用require进行
星哥玩云
2022/06/30
6240
编译安装httpd-2.4.9及新特性详解
前言 前面我们讲解了httpd在CentOS6上(httpd-2.2)的相关功能配置,而 CentOS7上采用了httpd-2.4的版本,那么httpd-2.4增加了哪些特性呢?接下来让我们在CentOS6.6上手动编译安装一下 httpd-2.4.9,看一下和httpd-2.2有什么不同,顺便补充说一下httpd的其它功能。 环境及新特性介绍 环境介绍 系统环境:CentOS6.6 所需软件包:apr-1.5.0.tar.bz2、apr-util-1.5.3.tar.bz2、httpd-2.4.9
小小科
2018/05/02
8870
编译安装httpd-2.4.9及新特性详解
如何编译安装PHP扩展
为什么80%的码农都做不了架构师?>>> 一开始安装PHP的时候,我们并不知道需要哪些扩展,所以只有等到我们真正用到的时候才想办法去安装。 安装PHP扩展最简单的办法就是 sudo apt-get
lilugirl
2019/05/26
1.9K0
yum更换国内源,yum下载rpm包,源码包 安装
CentOS自带yum仓库源网址是国外的网址,所以从国内下载国外网址的rpm包有时候会很慢或者无法下载,这时可以更换国内的yum仓库源来解决这个问题。
端碗吹水
2020/09/23
2.5K0
yum更换国内源,yum下载rpm包,源码包 安装
linux基础(day22)
7.6 yum更换国内源 更换yum国内源 cd /etc/yum.repos.d/ rm -f dvd.repo wget http://mirrors.163.com/.help/CentOS7-Base-163.repo 或者 curl -O http://mirrors.163.com/.help/CentOS7-Base-163.repo yum list 更换国内yum源 1.首先切换到该目录下,并cp复制之前备份的文件 [root@hf-01 ~]# cd /etc/yum.repos.d
运维小白
2018/02/06
9400
Linux系统中安装软件的三种方法
备注:1)在安装软件时,一般选项 -ivh 一起使用,这样可以看到安装进度与安装信息;
全栈程序员站长
2022/07/23
5.5K0
Linux系统中安装软件的三种方法
第十四章·Linux软件管理-YUM工具及源码包
-多年互联网运维工作经验,曾负责过大规模集群架构自动化运维管理工作。 -擅长Web集群架构与自动化运维,曾负责国内某大型金融公司运维工作。 -devops项目经理兼DBA。 -开发过一套自动化运维平台(功能如下): 1)整合了各个公有云API,自主创建云主机。 2)ELK自动化收集日志功能。 3)Saltstack自动化运维统一配置管理工具。 4)Git、Jenkins自动化代码上线及自动化测试平台。 5)堡垒机,连接Linux、Windows平台及日志审计。 6)SQL执行及审批流程。 7)慢查询日志分析web界面。
DriverZeng
2022/09/26
7790
第十四章·Linux软件管理-YUM工具及源码包
linux安装软件的三种方式:yum install 、rpm安装以及源码包安装
在windows下安装一个软件很轻松,只要双击setup或者.exe的文件,安装提示连续“下一步”即可,然而linux系统下安装一个软件似乎并不那么轻松了,因为我们不是在图形界面下。所以你要学会如何在linux下安装一个软件。
全栈程序员站长
2022/09/01
19K0
相关推荐
LINUX下tar.bz2包的安装方法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验