写点什么

Antd Form 实现机制解析

  • 2021-03-13
  • 本文字数:10565 字

    阅读完需:约 35 分钟

Antd Form 实现机制解析

背景


在中后台业务中,表单页面基础的场景包括组件值的收集、校验和更新。在 to B 业务中,表单页面的需求往往更复杂和定制化,除了上述的基本操作,还要处理包括自定义表单组件、表单联动、表单的异步检验等复杂场景,在一些大型表单页面中还要考虑性能的问题,表单页面的需求往往是新同学摔得第一个跤。


本文分为两个部分,第一部分会通过对 Antd Form 源码的分析来帮助大家对 Form 的整体设计和流程有一个清晰的概念,第二部分会分享一些复杂场景的解决方案。希望可以帮助大家更容易的处理表单需求和快速定位表单场景中的问题。


本文并不涉及过于具体的源码实现分析,大家可以放松心情,一起来对 Form 有一个感性的认知吧~


Form 组件解决了什么问题


首先我们先看一个简单的表单,收集并校验两个组件的值。只需要通过监听两个表单组件的 onChange 事件,获取表单项的 value,根据定义的校验规则对 value 进行检验,生成检验状态和检验信息,再通过 setState 驱动视图更新,展示组件的值以及校验信息即可。


代码实现可能是这样的:


exportdefaultclass LoginForm extends React.Component {  state = {    username:{      value: '',      error: '',    },    password:{      value: '',      error: '',    },  }    fieldMeta = {    username:{    rules:[],  },    password:{    rules:[],   }, }    onInputChange = (e) => {   const { value,name } = e.target; 	// 获取校验结果   const error = this.doValidate(value, name);      this.setState({     [name]:{       value,       error,     }   })  }    validator = (value, rules) => {  	...  }    doValidate = (value, name) => {    // 读取校验规则    const { rules } = this.fieldMeta[name];    return validator(value,rules);  }
render() { const { username, password } = this.state; return (<div> <div> <Input onChange={this.onInputChange} name='username' value={username.value} /> </div> <div style={errorStyle}> {username.error} </div> <div> <Input onChange={this.onInputChange} name='password' value={password.value}/> </div> <div style={errorStyle}> {password.error} </div> <Button onClick={this.onSubmit}>登录</Button> </div>); }}
复制代码

用流程图来表示是这样的:


上面的实现,我们设定了一个表单数据状态的模型,来维护组件的 value 和校验的错误信息。


this.state = {  [field1]:{    value: '',    error: '',  },  [field2]:{    value: '',    error: '',  },  ...}
复制代码

还有一个字段配置相关的模型,维护组件对应的校验规则。

fieldMeta = {  username:{    rules:[]  },  password:{    rules:[],  }}
复制代码

对于这种简单的业务场景,上述方式完全可以满足需求。


具体到真实的业务场景,往往更复杂,其中包含多种表单组件,如 Input、Checkbox、Radio、Upload,还有一些自定义表单组件。



这个时候如果继续采用这种方式,不仅需要维护多个 onChange 事件,还要对不同组件 value 的取值做差异化处理,以及对各个组件的校验以及触发时机规则进行维护,就很容易出现“祖传代码”。


对表单场景进行归纳,可以发现每个组件的数据收集、校验、数据更新的流程其实是一致的。对这个流程进行抽象,并且通过一些配置屏蔽组件间的差异性,再对组件的值以及组件的配置规则统一管理,就是我们常见的 Form 表单的解决方案。


Antd Form 是怎么实现的


要实现上面的方案需要解决三个问题:


  • 如何实时收集内部组件的数据?

  • 如何对组件的数据进行校验?

  • 如何更新组件的数据?


下面我们就带着这三个问题,一起看看 Antd Form 是如何来做的吧~


先看一下 Form class 的结构,Form 组件有两个静态属性 Item、createFormField 和一个静态方法 create,Item 是对 FormItem 组件的引用,createFormField 指向 rc-form 提供的同名方法,create 方法则是对  rc-form createDOMForm 的调用,为了方便理解,这边隐藏了部分代码,Form class 整体的结构如下:


import * as React from'react';import * as PropTypes from'prop-types';import classNames from'classnames';import createDOMForm from'rc-form/lib/createDOMForm';import createFormField from'rc-form/lib/createFormField';import omit from'omit.js';import { ConfigConsumer, ConfigConsumerProps } from'../config-provider';import { tuple } from'../_util/type';import warning from'../_util/warning';import FormItem from'./FormItem';import { FIELD_META_PROP, FIELD_DATA_PROP } from'./constants';import FormContext from'./context';
const FormLayouts = tuple('horizontal', 'inline', 'vertical');
exportdefaultclass Form extends React.Component{ static defaultProps = { colon: true, layout: 'horizontal', hideRequiredMark: false, onSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); }, };
static propTypes = { prefixCls: PropTypes.string, layout: PropTypes.oneOf(FormLayouts), children: PropTypes.any, onSubmit: PropTypes.func, hideRequiredMark: PropTypes.bool, colon: PropTypes.bool, }; // Item 是对 FormItem 组件的引用 static Item = FormItem; // createFormField 指向 rc-form 提供的同名方法 static createFormField = createFormField;
// create 方法则是对 rc-form createDOMForm 的调用 static create = function create( options ){ return createDOMForm({ fieldNameProp: 'id', ...options, fieldMetaProp: FIELD_META_PROP, fieldDataProp: FIELD_DATA_PROP, }); };
constructor(props) { super(props); }
renderForm = ({ getPrefixCls }: ConfigConsumerProps) => { const { prefixCls: customizePrefixCls, hideRequiredMark, className = '', layout } = this.props; const prefixCls = getPrefixCls('form', customizePrefixCls); const formClassName = classNames( prefixCls, { [`${prefixCls}-horizontal`]: layout === 'horizontal', [`${prefixCls}-vertical`]: layout === 'vertical', [`${prefixCls}-inline`]: layout === 'inline', [`${prefixCls}-hide-required-mark`]: hideRequiredMark, }, className, );
const formProps = omit(this.props, [ 'prefixCls', 'className', 'layout', 'form', 'hideRequiredMark', 'wrapperCol', 'labelAlign', 'labelCol', 'colon', ]);
return<form {...formProps} className={formClassName} />; };
render() { const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props; return ( <FormContext.Provider value={{ wrapperCol, labelAlign, labelCol, vertical: layout === 'vertical', colon }} > <ConfigConsumer>{this.renderForm}</ConfigConsumer> </FormContext.Provider> ); }}
复制代码



从 Form 的源码上看,组件本身并不涉及表单数据流程相关的逻辑,Form 组件以及 FormItem 主要处理布局方式、表单样式、属性必填样式、检验文案等视图层面的逻辑。


对数据的收集、校验、更新的流程的抽象以及组件数据管理主要由 rc-form 实现。下面我们继续来看一下核心的 rc-form 模块是怎样的结构。


rc-form 的 核心文件以及核心类图如下:



rc-form 核心逻辑可以从两个文件来看,createBaseForm.js、createFieldsStore.js。


createBaseForm.js 中暴露出的 createBaseForm 函数,创建了一个高阶组件 decorate,decorate 会为我们的目标组件包裹上一个容器组件,也就是上图中的核心类 BaseForm。


createFieldsStore.js 中暴露的 createFieldsStore 函数用来创建 FieldsStore 类的实例。FieldsStore 类可以理解为组件数据的管理中心,负责数据模型的初始化,并提供 Api 对组件数据进行更新和读取,以及获取组件数据的校验结果和数据更改状态。


Form 组件流程分析


我们通过 Antd Pro 中登录页面的实现来一起看一下,Form 内部的调用流程。


首先来看一下 Form 表单的用法:


class CustomizedForm extends React.Component {}
CustomizedForm = Form.create({})(CustomizedForm);

复制代码


我们有一个自定义组件 CustomizedForm,在使用 Form 表单的时候,我们会先调用 Form.create({})(CustomizedForm)。



初始化阶段


Form.create 函数指向 rc-form 提供的 createBaseForm 方法,createBaseForm 则创建了高阶组件 decorate。


decorate 的参数就是我们的 CustomizedForm 自定义组件。decorate 会创建一个被 BaseForm 组件包裹的自定义表单组件,经过包裹的组件将会自带 this.props.form 属性。为了方便记忆,我们把这个组件称为 FormHocCustomizedForm。


/***  rc-form/createBaseForm.js*/
// 默认的数据收集触发事件const DEFAULT_TRIGGER = 'onChange';
function createBaseForm(option = {}, mixins = []) { const { validateMessages, onFieldsChange, onValuesChange, mapProps = identity, mapPropsToFields, fieldNameProp, fieldMetaProp, fieldDataProp, formPropName = 'form', name: formName, // @deprecated withRef, } = option;
// 高阶组件 returnfunction decorate(WrappedComponent) { const Form = createReactClass({ mixins, ... render() { const { wrappedComponentRef, ...restProps } = this.props; // eslint-disable-line // 为目标组件注入 form props const formProps = { [formPropName]: this.getForm(), }; if (withRef) { if ( process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' ) { warning( false, '`withRef` is deprecated, please use `wrappedComponentRef` instead. ' + 'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140', ); } formProps.ref = 'wrappedComponent'; } elseif (wrappedComponentRef) { formProps.ref = wrappedComponentRef; } const props = mapProps.call(this, { ...formProps, ...restProps, }); return<WrappedComponent {...props} />; }, }); return argumentContainer(unsafeLifecyclesPolyfill(Form), WrappedComponent); };}
export default createBaseForm;
复制代码



组件创建完成之后,FormHocCustomizedForm 就会经历 React 组件的生命周期。


getInitailState 阶段


Form 并没有通过内部的 state 来管理内部组件的值, 而且创建了 FieldsStore 实例,也就是上面提到的组件数据管理中心。



通过上面的类图我们可以看到 FieldsStore 包含两个属性,fields 和 fieldsMeta,fields 主要用来记录每个表单项的实时数据,包含以下属性:


  • dirty 数据是否已经改变,但未校验

  • errors 校验文案

  • name 字段名称

  • touched 数据是否更新过

  • value 字段的值

  • validating 校验状态



fieldsMeta 用来记录元数据,即每个字段对应组件的配置信息:


  • name 字段的名称

  • originalProps 被 getFieldDecorator() 装饰的组件的原始 props

  • rules 校验的规则

  • trigger 触发数据收集的时机 默认 onChange

  • validate 校验规则和触发事件

  • valuePropName 子节点的值的属性,例如 checkbox 应该设为 checked

  • getValueFromEvent 如何从 event 中获取组件的值

  • hidden 为 true 时,校验或者收集数据时会忽略这个字段



Render 阶段


被 Form 管理的组件,需要使用 props.form.getFieldDecorator 来包装,在 Render 阶段需要调用 getFieldDecorator 传入我们的组件配置,包括字段名 name 以及组件元数据 otherOptions,再将字段对应的组件传入 getFieldDecorator 返回的高阶组件。


{getFieldDecorator('name', otherOptions)(<Input />)}
复制代码


/** * rc-form/createBaseForm.js */			// getFieldDecorator 实际上创建了一个高阶组件,参数是字段对应的组件getFieldDecorator(name, fieldOption) {  // 装饰组件的 props  const props = this.getFieldProps(name, fieldOption);  returnfieldElem => {    // We should put field in record if it is rendered    this.renderFields[name] = true;
const fieldMeta = this.fieldsStore.getFieldMeta(name); const originalProps = fieldElem.props; // 校验细节略过 ... fieldMeta.originalProps = originalProps; fieldMeta.ref = fieldElem.ref; return React.cloneElement(fieldElem, { ...props, ...this.fieldsStore.getFieldValuePropValue(fieldMeta), }); };},
复制代码


经过 getFieldDecorator 包装的组件,表单组件会自动添加  value(或 valuePropName 指定的其他属性) onChange(或 trigger 指定的其他属性)属性,接下来的数据同步将被 Form 接管。


getFieldDecorator 主要用于装饰组件,其中调用的  getFieldProps 用于装饰 props,getFieldProps 会将组件的 (DEFAULT_TRIGGER = 'onChange')触发事件指向 FormHocCustomizedForm 的 onCollect 方法,并将配置的 validateTriggers 指向 onCollectValidate。在这个阶段还会收集组件的元数据,也就是我们调用  getFieldDecorator 中传入的 option 配置,这些配置会存入 fieldStore 的 fieldsMeta 对象中,作为组件的元数据。


这里我们就可以回答第一个问题,如何实时收集内部组件的数据?


Form 通过 getFieldDecorator 对组件进行包装,接管组件的 value 和 onChange 属性,当用户输入改变时,触发 onCollect 或 onCollectValidate 来收集组件最新的值。


用户输入


当用户输入触发组件的 onChange 或者其他的 trigger 事件时,执行 onCollect 或者  onCollectValidate,onCollect 执行组件数据收集,onCollectValidate 除了执行组件数据收集,还会根据配置的校验规则来校验组件,其中校验的流程并不由 rc-form 实现,而且通过引入第三方校验库  async-validator 执行。


onCollect 和 onCollectValidate 方法中收集数据的动作主要由 onCollectCommon 来处理。


/** * rc-form/createBaseForm.js */
onCollect(name_, action, ...args) { const { name, field, fieldMeta } = this.onCollectCommon( name_, action, args, ); const { validate } = fieldMeta;
this.fieldsStore.setFieldsAsDirty();
const newField = { ...field, dirty: hasRules(validate), }; this.setFields({ [name]: newField, });},
复制代码


onCollectCommon 负责组件数据的收集,在事件的回调中,通过默认的 getValueFromEvent 方法或者组件配置的 getValueFromEvent 方法,可以从参数 event 中正确的拿到组件的值。


/** * rc-form/createBaseForm.js */
onCollectCommon(name, action, args) { const fieldMeta = this.fieldsStore.getFieldMeta(name);
if (fieldMeta[action]) { fieldMeta[action](...args); } elseif (fieldMeta.originalProps && fieldMeta.originalProps[action]) { fieldMeta.originalProps[action](...args); } const value = fieldMeta.getValueFromEvent ? fieldMeta.getValueFromEvent(...args) : getValueFromEvent(...args); if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) { const valuesAll = this.fieldsStore.getAllValues(); const valuesAllSet = {}; valuesAll[name] = value; Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]), ); onValuesChange( { [formPropName]: this.getForm(), ...this.props, }, set({}, name, value), valuesAllSet, ); } const field = this.fieldsStore.getField(name); return { name, field: { ...field, value, touched: true }, fieldMeta };},
复制代码


针对不同的组件取值差异,由 getValueFromEvent 方法来屏蔽。


/** * rc-form/utils.js */
// 默认的 getValueFromEventexportfunction getValueFromEvent(e) { // To support custom element if (!e || !e.target) { return e; } const { target } = e; return target.type === 'checkbox' ? target.checked : target.value;}
复制代码


收集并校验组件的值。


/** * rc-form/createBaseForm.js */
onCollectValidate(name_, action, ...args) { const { field, fieldMeta } = this.onCollectCommon(name_, action, args); // 获取组件最新的值 const newField = { ...field, dirty: true, };
this.fieldsStore.setFieldsAsDirty(); // 对组件最新的值 进行校验 this.validateFieldsInternal([newField], { action, options: { firstFields: !!fieldMeta.validateFirst, }, });},
复制代码


执行校验。


validateFieldsInternal(  fields,  { fieldNames, action, options = {} },  callback,) {  const allRules = {};  const allValues = {};  const allFields = {};  const alreadyErrors = {};  fields.forEach(field => {    const name = field.name;    if (options.force !== true && field.dirty === false) {      if (field.errors) {        set(alreadyErrors, name, { errors: field.errors });      }      return;    }    const fieldMeta = this.fieldsStore.getFieldMeta(name);    const newField = {      ...field,    };    newField.errors = undefined;    newField.validating = true;    newField.dirty = true;    // 从 fieldMeta 中读取校验规则    allRules[name] = this.getRules(fieldMeta, action);    allValues[name] = newField.value;    allFields[name] = newField;  });  // 校验前更新字段状态  this.setFields(allFields);  // in case normalize  Object.keys(allValues).forEach(f => {    allValues[f] = this.fieldsStore.getFieldValue(f);  });
// AsyncValidator 三方校验库 async-validator; const validator = new AsyncValidator(allRules); if (validateMessages) { validator.messages(validateMessages); } validator.validate(allValues, options, errors => { const errorsGroup = { ...alreadyErrors, }; if (errors && errors.length) { errors.forEach(e => { // 省略... const fieldErrors = get(errorsGroup, fieldName.concat('.errors')); fieldErrors.push(e); }); } const expired = []; const nowAllFields = {}; Object.keys(allRules).forEach(name => { const fieldErrors = get(errorsGroup, name); const nowField = this.fieldsStore.getField(name); // avoid concurrency problems if (!eq(nowField.value, allValues[name])) { expired.push({ name, }); } else { nowField.errors = fieldErrors && fieldErrors.errors; nowField.value = allValues[name]; nowField.validating = false; nowField.dirty = false; nowAllFields[name] = nowField; } }); // 检验完成 更新字段实时数据 this.setFields(nowAllFields); // ... });},
复制代码


到这里我们可以回答上面的第二个问题,如何对组件的数据进行校验?


当通过执行 onCollectCommon 完成了表单数据的收集,onCollectValidate 会调用 validateFieldsInternal 方法创建 AsyncValidator 的实例,由 AsyncValidator 根据组件的配置规则进行校验,并将最终的校验结果和表单数据更新到 fieldStore。


到这里就完成了表单数据的收集和校验的环节,已经拿到了表单最新的数据以及校验结果。


下一步,就是数据的更新,也就是将表单最新的值和校验相关的信息更新到视图上。


在 onCollect 和 validateFieldsInternal 方法中,我们看到最后一步调用了 setFields 来更新实时数据。


/** * rc-form/createBaseForm.js */
setFields(maybeNestedFields, callback) { const fields = this.fieldsStore.flattenRegisteredFields( maybeNestedFields, ); // 更新 fieldsStore this.fieldsStore.setFields(fields);
if (onFieldsChange) { const changedFields = Object.keys(fields).reduce( (acc, name) =>set(acc, name, this.fieldsStore.getField(name)), {}, ); onFieldsChange( { [formPropName]: this.getForm(), ...this.props, }, changedFields, this.fieldsStore.getNestedAllFields(), ); } // 更新 this.forceUpdate(callback);},
复制代码


setFields 方法将字段组件最新的数据更新到 fieldStore。此时 fieldStore 已经收集存储了组件最新的值,下面我们就需要更新组件,将数据正确的在界面上渲染出来。


可以看到,setFields 中最后调用了 React 组件提供的 forceUpdate 函数。这里可以回答第三个问题,如何更新组件的数据?


因为我们在最顶层的 FormHocCustomizedForm 组件中调用 forceUpdate,forceUpdate 会跳过 shouldComponentUpdate 触发组件的 Render 方法,进而触发所有子组件的更新流程。在子组件 Render 的执行过程中, getFieldDecorator 方法从 fieldStore 中读取实时的表单数据以及校验信息,并通过注入 value 或者  valuePropName 的值设定的属性来更新表单。


到这里,一个完整的 Form 数据收集、校验、更新流程就完成了,整个过程的流程图如下所示:



复杂表单场景的最佳实践


看完了上面的 Form 内部的运行流程,下面我们一起来看看 Form 还提供了哪些机制方便我们解决一些复杂场景问题。


嵌套数据结构收集


FieldStore 内部集成了  lodash/set,可以设置对象路径(eg: a.b.c 或者 a.b[0])为字段值,通过使用对象路径字段,我们可以很方便的实现嵌套数据结构值的收集。


<FormItem>  <Col span={16}>    {getFieldDecorator('nested.fieldObj.name')(<Input/>)}  </Col></FormItem><FormItem>  <Col span={16}>    {getFieldDecorator('nested.fieldArray[0].name')(<Input/>)}  </Col></FormItem>
复制代码


上面的代码中,我们通过对象路径的方式来设置 field,在获取表单值的时候已经被转换成了对应路径结构的对象或数组,如下面所示:


{  nested:{   fieldObj:{     name:'嵌套对象的值'   },   fieldArray:['嵌套数组的值']  }}
复制代码


自定义表单接入


上面的分析里提到,Form 通过接管组件的 value 和 onChange 事件来管理组件,想实现一个可以接入 Form 管理的组件,只需要满足下面三个条件


  • 提供受控属性 value 或其它与 valuePropName 的值同名的属性

  • 提供 onChange 事件或 trigger 的值同名的事件

  • 支持 ref:

  • React@16.3.0 之前只有 Class 组件支持

  • React@16.3.0 及之后可以通过 forwardRef 添加 ref 支持(https://codesandbox.io/s/7wj199900x


表单联动


组件的数据由 FieldStore 来统一管理,组件值变化时也会实时更新,所以结合 ES6 的 get 方法可以很简单的实现组件之间的联动。


class Linkage extends Component {  get showInput(){   return this.props.form.getFieldValue('checkbox');  }  render() {    const { form } = this.props;    const { getFieldDecorator } = form;    return (      <div>      {        getFieldDecorator('checkbox', {valuePropName: 'checked',})(          <Checkbox>show Input</Checkbox>        )      }      {       this.showInput && <Input/>	      }      </div>    );  }}
export default Form.create()(Linkage);
复制代码


总结


本文在流程上对 Form 组件的实现机制进行了解析,省略了里面的实现细节,大家对流程有一个整体认知之后,也可以自己翻阅 Form 的源码来了解实现细节。


Antd Form 具有很好的灵活性,可以帮我们快速的实现表单需求,但是也存在一些问题,比如当表单中的任何一个组件值发生改变,触发 onCollect 数据收集、执行更新流程,都会调用 forceUpdate 触发所有组件的更新。


在复杂表单业务,用户频繁的输入场景就会产生性能瓶颈。对于复杂的表单组件,我们可以通过拆分组件的粒度,通过 shouldComponentUpdate 来避免不必要的更新,或者修改组件的数据收集时机来减少数据的收集频率。当然这并不是很优雅的解决方案,在未来要发布的 Antd V4 版本中,Form 的底层实现已经替换为 rc-field-form(https://github.com/react-component/field-form),主页上的介绍是:


React Performance First Form Component.


大家也可以期待一下官方新版本的 Form 组件。



头图:Unsplash

作者:子洋

原文:https://mp.weixin.qq.com/s/LwBCJJzhT5uKB-ix4XkqbA

原文:Antd Form 实现机制解析

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


2021-03-13 13:164218

评论

发布
暂无评论
发现更多内容

网心科技入选“2023年广东省名优高新技术产品”名单

网心科技

云计算 边缘计算 网心科技

保姆级教程:教你UniMRCP对接华为云ASR(Linux版)

华为云开发者联盟

开源 开发 华为云 华为云开发者联盟

AI时代来临我们要如何面对?

小魏写代码

使用云手机提升WhatsApp使用体验

Ogcloud

云手机 海外云手机 云手机海外版 国外云手机 电商云手机

深入探讨iOS开发:从创建第一个iOS程序到纯代码实现全面解析

雪奈椰子

SpringBoot如何优雅的进行参数校验

不在线第一只蜗牛

Java 后端 springboot

Web3 游戏周报(3.17-3.23)

Footprint Analytics

Web3 游戏

不给灰暗留下死角:华为应用市场的安全之光

脑极体

应用

深度|庖丁解InnoDB之Buffer Pool

阿里云瑶池数据库

数据库 云计算 阿里云 polarDB

体育赛事直播源码的价值和意义?不同应用场景获利方法

软件开发-梦幻运营部

从数据存储的演迁,看芯赛云分布式存储应用

科技热闻

那位拿了多个Offer的大佬分享了最新Go面经

王中阳Go

Go 后端 Go 面试题 面经 后端 大厂

青亦学爬虫:根据淘宝天猫商品链接封装淘宝天猫商品详情数据接口

tbapi

淘宝API接口 淘宝商品详情接口 天猫商品详情接口 淘宝数据爬虫 天猫数据爬虫

u-blox 面向多个大众应用市场推出最新 Wi-Fi 6 模块NORA-W4

科技之家

什么样的商品管理系统可以驱动品牌增长?

第七在线

iOS开发优势解析,费用探究以及软件开发详解

云手机为什么能避免海外社媒账号被封?

Ogcloud

TikTok 云手机 海外云手机 tiktok云手机 云手机海外版

HashData的湖仓一体思考:非结构化数据支持(Directory Table等)讲解及演示

酷克数据HashData

postgresql AI 湖仓一体

华为云亮相KubeCon EU 2024,以持续开源创新开启智能时代

华为云开发者联盟

开源 开发 华为云 华为云开发者联盟

宁德时代与特斯拉合作;钟睒睒连续四次中国首富丨 RTE 开发者日报 Vol.171

声网

数字化与敏捷的关系(第2部分)

ShineScrum捷行

软件测试学习笔记丨Allure2 失败重试功能应用场景

测试人

软件测试

利用Python和数据获取技术实现智能旅游情报系统

阿Q说代码

Python 后端 数据获取

How Big Data Works

Jackchang234987

基于 NVIDIA Megatron-Core 的 MoE LLM 实现和训练优化

阿里云大数据AI技术

人工智能 模型训练 大模型 LLM

云手机:实现便携与安全的双赢

Ogcloud

云手机 海外云手机 云手机海外版 国外云手机 电商云手机

Databend 开源周报第 137 期

Databend

智达方通全面预算管理系统,为企业带来更可靠的交付

智达方通

全面预算管理 全面预算管理系统

QCA9882, QCA9880, and MT7915 WiFi cards for OpenWrt:What are the difference?

wallyslilly

QCA9880

PHP调用API接口的方法及实现

Noah

什么是正向代理和反向代理?

EquatorCoco

反向代理 正向代理

Antd Form 实现机制解析_语言 & 开发_政采云前端团队_InfoQ精选文章