简单来看一下官方的例子, 感受下:
import { Form, Icon, Input, Button } from ‘antd’;
function hasErrors(fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field]);
}
class HorizontalLoginForm extends React.Component {
componentDidMount() {
// To disable submit button at the beginning.
this.props.form.validateFields();
}
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
render() {
const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;
// Only show error after a field is touched.
const usernameError = isFieldTouched(‘username’) && getFieldError(‘username’);
const passwordError = isFieldTouched(‘password’) && getFieldError(‘password’);
return (
<Form.Item validateStatus={usernameError ? ‘error’ : ‘’} help={usernameError || ‘’}>
{getFieldDecorator(‘username’, {
rules: [{ required: true, message: ‘Please input your username!’ }],
})(
<Input
prefix={<Icon type=“user” style={{ color: ‘rgba(0,0,0,.25)’ }} />}
placeholder=“Username”
/>,
)}
</Form.Item>
<Form.Item validateStatus={passwordError ? ‘error’ : ‘’} help={passwordError || ‘’}>
{getFieldDecorator(‘password’, {
rules: [{ required: true, message: ‘Please input your Password!’ }],
})(
<Input
prefix={<Icon type=“lock” style={{ color: ‘rgba(0,0,0,.25)’ }} />}
type=“password”
placeholder=“Password”
/>,
)}
</Form.Item>
<Form.Item>
Log in
</Form.Item>
);
}
}
const WrappedHorizontalLoginForm = Form.create({ name: ‘horizontal_login’ })(HorizontalLoginForm);
ReactDOM.render(, mountNode);
对应的页面如下图:
3.1.3 试跑源码
简单克隆下源码,nvm 切换到 10.x 下,yarn 然后 yarn start 即可,本身项目自带 dev server,然后尝试修改源码即可立即生效。
源码阅读过程中,有一些不太常用的小知识,希望对大家阅读源码有帮助:
Mixin[3]
react 早期提供的基于 createReactClass 的逻辑复用手段,不过官方文档说问题很多, 在 ES6 class 中甚至直接不支持了,从目的来看是为了复用一些具体组件类无关的通用方法。
hoist-non-react-statics[4]
用来拷贝 React Class Component 中的静态方法。应用场景是 Hoc 包裹的时候,很难知道有哪些方法是 React 提供的,哪些是用户定义的,而这个方法可以方便地把被包裹组件的非 React 静态方法拷贝到向外暴露的 HOC 上面(详见链接)[5], 这样即使用 HOC 包裹过的组件类上的静态方法也不会丢失。
3.1.4 大体思路
这是我当时阅读源码时留下的一张图, 希望对大家有所帮助。
3.1.5 整体设计
实际上,感觉 rc-form 整体设计上还是比较简单的, 从官方的受控组件出发,getDecorator 实际上就是个 HOC,内部用 React#cloneElement 将 value & onChange 注入到表单控件上去。通过这种方法,所有的表单控件都被 rc-form 接管了,后续交互过程中不管是输入输出,校验后的错误状态信息都被托管在了组件的内部,用户确实被解放了出来。
美中不足的是,rc-form 使用了一个 forceUpdate 来完成内部状态与视图 UI 的同步,这个 forceUpdate 被放在了 Form 组件中,换句话说,任何一个微小的变动 (比如某个字段的输入) 都将导致整个表单层面的渲染 (我们习惯称之为全局渲染),这也就是后来 rc-form 被广为诟病存在性能问题的根本原因,其本质就在于它粗犷的渲染粒度。
3.2 rc-field-form[6] (Antd Form 4.x[7])
3.2.1 背景
基于上述提到的一些缺陷,Antd 4.x 对于表单模块进行了重新设计。更具体来说,设计思路上,渲染粒度的把控从表单级别具化到了组件级别,使得表单性能大幅度提升。源码中大量使用 React Hooks,同时简化了暴露的 API, 提升了易用性。
3.2.2 例子
可以和上述的代码实例对比下,最直观的变化是砍掉了不知所谓的 getFieldDecorator 整体上感觉确实清爽不少。
import { Form, Input, Button, Checkbox } from ‘antd’;
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 },
};
const Demo = () => {
const onFinish = values => {
console.log(‘Success:’, values);
};
const onFinishFailed = errorInfo => {
console.log(‘Failed:’, errorInfo);
};
return (
{…layout}
name=“basic”
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
<Form.Item
label=“Username”
name=“username”
rules={[{ required: true, message: ‘Please input your username!’ }]}
</Form.Item>
<Form.Item
label=“Password”
name=“password”
rules={[{ required: true, message: ‘Please input your password!’ }]}
<Input.Password />
</Form.Item>
<Form.Item {…tailLayout} name=“remember” valuePropName=“checked”>
Remember me
</Form.Item>
<Form.Item {…tailLayout}>
Submit
</Form.Item>
);
};
ReactDOM.render(, mountNode);
3.2.3 试跑源码
简单克隆下源码,yarn 然后 yarn start 居然跑步起来,提示我漏装了 hast-util-is-element,安装完成后 yarn start 成功启动了 dev server,然后尝试修改源码发现可立即生效。这是我当时整理的一张大概的流转图:
-
整体的代码还是比较清晰可读的,其实这里的想法很简单,我们知道其实强制 rerender 本质上是将最新的 props / state 重新地沿着组件树往下传递,其本质也可以理解为一种通讯的方式。只不过这种通讯方法的代价就是导致所有的组件都重新渲染。
-
既然在 Form 上无脑地 rerender 将导致性能问题,那么解决的方向一定是尽可能地缩小 rerender 的范围,如何将最新的表单状态仅仅是同步到需要同步的表单项呢? 很自然地,这里用到了订阅模式模式。
-
基于上面的设计,展开其实就是 2 点:
a. 每个表单项各自维护自身的状态变化,value-onChange 实际只在当前表 单项上面流传;
b. 流转之后通知其他表单项,这时候其他表单项如何知道自己关心当前变 化的表单项呢? 这里 field-form 引入了 dependencies,shouldUpdate 来 帮助使用者方便地声明自己所依赖的表单项。
3.2.4 支持嵌套数据结构
其实在 rc-form 中我漏了一块比较重要的内容没讲,那就是对嵌套数据结构的支持,比如我们知道实际上用户填写表单最终得到的实际上是一个大 json。对于比较简单的场景,可能表单的 json 只有一级。
譬如登录场景下,比较常见的结构:
{
phoneNumber: ‘110’, // 手机
captcha: ‘YxZd’ , // 图片校验码
verificationCode: ‘2471’, // 短信验证码
}
但是实际上在复杂场景下,很有可能会出现形如:
{
companyName: ‘ALBB’,
location: ‘London’,
business: {
commerce: {
income: {
curYear: 110,
lastYear: 90,
}
},
data: {
dau: 1,
}
},
staffList: [
{
name: ‘zhang3’,
age: ‘22’
},
{
name: ‘li3’,
age: ‘24’
},
]
}
这时候, 简单的 <FormItem name=“field1” 可能就不太够了,比如这里我们就需要表单项能表单包括对象和列表的嵌套结构,我们就势必需要在表单层面上表示我们所谓的嵌套关系。通常来讲,表单对于嵌套数据的支持都是通过表单项的唯一标识 name 上,这里就以 field-form 为例来看:
import React from “react”;
import ReactDOM from “react-dom”;
import “antd/dist/antd.css”;
import “./index.css”;
import { Form, Input, InputNumber, Button } from “antd”;
const Demo = () => {
const onFinish = (values) => {
console.log(values);
};
return (
<Form.Item
name={[“user”, “name”]}
label=“Name”
rules={[{ required: true }]}
</Form.Item>
<Form.Item
name={[“user”, “email”]}
label=“Email”
rules={[{ type: “email” }]}
</Form.Item>
<Form.Item
name={[“user”, “age”]}
label=“Age”
rules={[{ type: “number”, min: 0, max: 99 }]}
</Form.Item>
<Form.Item name={[“list”, 0]} label=“address1”>
</Form.Item>
<Form.Item name={[“list”, 1]} label=“address2”>
<Input.TextArea />
</Form.Item>
<Form.Item>
Submit
</Form.Item>
);
};
ReactDOM.render(, document.getElementById(“container”));
可以看到本质上就是将数据字段的嵌套的信息拍平存储在了 name 中(此时 name 是数组),当然更常见的一种做法是形如 lodash get/set 类似的 path 规则:user.name user.age address[0],只是表现上不同, 本质上都是一样的。关于为什么 antd 4.x 要使用数组而不是更常见的做法,官方也给出了解释:
In rc-form, we support like user.name to be a name and convert value to { user: { name: ‘Bamboo’ } }. This makes ‘.’ always be the route of variable, this makes developer have to do additional work if name is real contains a point like app.config.start to be app_config_start and parse back to point when submit.
Field Form will only trade [‘user’, ‘name’] to be { user: { name: ‘Bamboo’ } }, and user.name to be { [‘user.name’]: ‘Bamboo’ }.
正如你所理解的一样,实际在表单的内部也确实是这么做的,所以的字段,无论层级,都是被拍平管理的,虽然本质上可以认为是一颗树,但是关于结构的信息只体现在了 name。例如我们把上面的例子打印出来本质上就是:
其实了解了这一点,再回过头来看 Antd 4.x 中 form 的一众 API 中的 NamePath 也不难理解了:
对于表单项的操作都是接受 NamePath 然后对对应的匹配项进行操作,换个维度来理解的话,本质上你传入的 NamePath 就是需要操作节点的路径。当然这里稍微有个要注意的点就是当操作的目标节点为非叶子节点时,更新需要同步到它的所有子孙上去。
这一块实现上并不复杂,想了解的同学可以翻翻源码,看看 valueUtil.ts,Field.ts#onStoreChange 即可,此处不再赘述。
3.2.5 动态增减数组
现在有了嵌套数据的支持,来看看另一个问题,很多时候我们并不事先知道表单中的某个数组总共有多少项,比如用户再输入他的爱好时,他可以有任意多个爱好,这时候我们就需要使用到动态增减数组。
虽然 antd 4.x 提供了 Form.List 组件来帮助我们很方便地构建动态增减数组的表单,但是实现上绕不开我们前面所说的嵌套数据结构。先看个官方的例子:
import React from “react”;
import ReactDOM from “react-dom”;
import “antd/dist/antd.css”;
import “./index.css”;
import { Form, Input, Button, Space } from “antd”;
import { MinusCircleOutlined, PlusOutlined } from “@ant-design/icons”;
const Demo = () => {
return (
<Form.List name=“sights”>
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<Form.Item
{…field}
label=“Price”
name={[field.name, “price”]}
fieldKey={[field.fieldKey, “price”]}
rules={[{ required: true, message: “Missing price” }]}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(field.name)} />
))}
<Form.Item>
<Button
type=“dashed”
onClick={() => add()}
block
icon={}
Add sights
</Form.Item>
</>
)}
</Form.List>
<Form.Item>
Submit
</Form.Item>
);
};
ReactDOM.render(, document.getElementById(“container”));
实际渲染的页面长这样:
关于这一块的源码很简单我就不多赘述, 感兴趣可以看看 rc-field-form#src#List.tsx,本质上它要做的事情只有 3 个:
-
只是替用户维护一个可增减的数组, 数组中每个对象对应表单数组中的一个节点;
-
当用户想新增 item 时, 在数组中创建一个新的 field,并给予一个唯一的 key 同时根据 item 的位置生成 name,同时调用 onChange 来通知表单对象更新内部状态;
-
删除和新增同理。
问题: 那么在 antd 中上述每个 field 对象上的三个属性: fieldKey,key,name 都是干嘛的?
其中 name 最好理解,对应的是每个输入框在最终提交的数据结构中对应的位置,key 表示的是该节点的唯一 id (可做 react loop 中的 key),一直让我很费解的是 fieldKey,因为事实上在 field-form 的源码中 field 并没有包含该值,antd 文档中也没有对该值进行特别的解释。不过最后发现 fieldKey 和 key 本质上是同一个东西,因为在 antd#components#Form#FormList 中我发现:
3.2.6 感想
其实单单对比 Antd 3.x 以及 Antd 4.x 背后的表单, 我们已经可以得出一个有趣的结论:表单方案的性能问题本质是在解决各个表单项与项之间及与表单整体之间的通信问题。这里 antd 4.x 利用订阅有选择地通知替代了无脑 rerender 流重绘,实质上是更细粒度的组件通讯实现了更小的通讯代价。接下来我们会看看国外几个类似表单的发展,恰恰也印证了这个观点。
3.3 ------- 国际分割线 --------
其实国内社区主要还是是以 antd form 为主, 但是实际上,国外 React 社区的表单方案也有着类似的发展思路。下面大概过一下,思路上类似的地方就不再展开细讲了。
3.4 Redux-Form
Redux-form 算是比较早的一款 react 表单方案了,由于 redux 的流行,使用 redux 进行表单的状态管理是一个很自然的思路。v6 以前的表单大概是这样的:
这里存在两个问题:
-
性能问题:用户的任何一个按键操作都会引发状态的更新而后全局渲染整个表单,可以看到这个问题和我们之前说的 rc-form 存在的问题是类似的。
-
依赖 redux:用 redux 来管理表单数据流后来被证明是没有必要的,总的来说就是增大了体积,而且导致很多原本不需要 redux 的项目强制安装 redux。关于这个可以看看 Dan 的说法。
如果你还在犹豫,可以看看:
Dan 发表的看法:良好实践标准[8]
redux 官网的看法:Should I put form state or other UI state in my store?[9]
redux-form 作者看法:redux-form[10]
事实上 redux-form 在 v6 之后更改了更新策略,将 Field 单独注册到 Redux,用 Redux 天然的订阅机制实现了直接对 Field 的更新。可以认为实现了类似 rc-field-form 对于 rc-form 的优化。
3.5 Formik
3.5.1 背景
鉴于 Redux-form 强依赖 Redux 所带来的的问题,Formik 抛开了 Redux,自己在内部维护了一个表单状态。大概看了下作者的设计动机,主要是减少模板代码&兼顾表单性能。虽然提供了 FastField 来做一定的性能优化,不过仍然是以表单整体为视角的粗粒度的状态更新,所以本质上并没有逃开全局渲染的,抛开 FastField 来看,它和 rc-field 的设计思路甚至有些类似。
3.5.2 例子
3.5.3 核心思路
所谓的 FastField 性能优化,不过是通过一层 HOC 包裹实际的 Field,然后在这个中间层中用 shouldComponentUpdate 决定当前更新的状态是否为该 HOC 包裹的 Field 状态。
可以看出来就是粗暴地对表单中几个关键的状态 value
, error
,touched
以及传入的 prop 的长度和 isSubmit 几个关键字段进行浅比较,如果碰到类似字段 a 决定字段 b 是一个输入框还是下拉框这种场景,还得自己实现 shouldUpdate 的逻辑。所以整体来看可以认为是这样:
3.5.4 React DevTools Tips
在探究这个表单的时候,碰到这里有个很有意思的结论, 由于只是 FastField 这种外层 connect 接入 context,内层 shouldComponentUpdate 做优化的机制,这时候通过 devtools 中 highlight updates 并不能看出是否是全局渲染,那玩意在这种情况下会给你误导,这时候更好的方法应该是使用插件提供的 profiler,下面这个例子很好地诠释了这一点:
Demo 地址:https://codesandbox.io/s/7vhsw
3.5.5 感想
总的来看,Formik 完成了它最初的设计,从 redux 中解放出来,并一定程度缓解了表单全局渲染导致的性问题,打包体积也很小只有 12.7k,在 redux-form 横行的年代确实给力(怪不得会被官方推荐)。但是由于发布的年代早, 对于后续 react hooks 的支持不是十分全面,且底层设计上还是没有支持到更细的空间更新粒度,所以当表单膨胀或是联动场景较多时,单单靠 FastField 也就力不从心了。
3.6 React-final-form
这是 Redux-form 的作者在维护了多年 Redux-form 之后的又一力作,作者的本意是写一个“无第三方依赖、pure JS、插件式”的表单。在渲染上,final-form 也采取了和 rc-field-form 类似的思路,Form 作为中心订阅器,负责各组件事件的分发,而每个 Field 管理自己的数据,独立地订阅自己感兴趣的其他表单项,并独立地完成渲染。
Final-form 我没有细看,只是大概了解了下,感兴趣的同学可以看看 final-form 作者的演讲: Next Generation Forms with React Final Form[11]
3.7 上述五个表单方案的变迁
其实你会发现历史总是惊人地相似,国内由于饱受 rc-form 全局渲染的困扰而推出了基于订阅的 rc-field-form,国外的发展历程也是类似的: redux-form(v5) => redux-form(v6)
,formik => react-final-form
。总结下,整个变迁大概可以这么理解:
3.8 React-hook-form
3.8.1 背景
上面已经说了几个表单,我们可以发现这些表单都是通过维护了一个 state 来处理表单的状态,无论是集中更新,还是以订阅的方式进行分布式更新,它们都是基于这个表单状态来完成的,这就是 react 的受控表单模式。
现在,我们不妨换个思路,从非受控的角度入手,非受控表单就不是使用 state 那一套了,它是通过 ref 来直接拿到表单组件,从而可以直接拿到表单的值,不需要对表单的值进行状态维护,这就使得非受控表单可以减少很多不必要的渲染。
但是非受控表单也存在着它的问题,在动态校验、动态修改(联动)方面不是很方便,于是在 react hooks 出现以后诞生了一个以非受控思想为基础的表单库 react-hook-form, 这个表单的设计思路很新奇,完全是拥抱原生, 拥抱非受控. 核心思路是各个组件自身维护各自的 ref, 当校验、提交等发生时,便通过这些 ref 来获取表单项的值。
3.8.2 简单例子
3.8.3 核心思路
源码 Commit:1441a0186b8eab5dccb8d85fddb129d6938b994e
Demo 地址: https://codesandbox.io/s/happy-mccarthy-1nxuq?file=/src/App.js
这里其实有一些问题,由于所有的 rerender 都是 field 级别而不是 form 级别的 (表单渲染),即:
- 性能的优化:由于各个字段状态由组件自己托管,并不需要数据回流,除了这一大块以外,代码中也有很多处理很好的细节:
a. 对错误进行浅层比较,例如上一轮渲染已经展示了错误信息 a, 如果这一轮渲染错误信息不变的话, 则不重新渲染.
b. 表单的内部状态(isDirty,touched,submitCount,isSubmitting 等等)统一用过 Proxy 包装, 在初次渲染的时候利用 Proxy 记录用户对于各个状态的订阅情况,不订阅的话变化将被忽略,不引发重新渲染。
c. 虽然 watch 默认会触发全局渲染,不过 useWatch 可以做到不触发全局渲染的情况下通知某个字段的更新,本质上是订阅机制,将 useWatch 调用方的 state hook 维护在了表单的内部对象上,一旦有更新通过这种方式可以做到仅仅通知订阅组件。
- 可惜的是,任何表单下的错误信息变化,都会触发全局的渲染,这一点感觉不是特别好。至少可以 ErrorMessage 里面可以来个浅层比较。
3.8.4 动态校验以及联动
为了支持动态校验,react-hook-form 在进行表单注册的时候还会将 onChange、onBlur 等事件挂载到表单组件上,保证对与用户输入、修改行为的监听,从而可以对表单校验、表单值监听等进行触发。
非受控表单除了动态校验的问题,还存在联动实现的问题。由于 react-hook-form 不会将表单的值维护在 state 中,用户输入不会触发整表层的 JSX 更新,因此 react-hook-form 提供了 watch,以及性能更好的 useWatch,来对于需要进行联动的表单进行注册,当用户进行修改的时候会调用更新。(本质上这两个东西和 rc-field-form 中的 dependences / shouldUpdate 目的类似)。
关于 watch 和 useWatch
其实两者是存在性能差异的,useWatch 可以认为是把更新移到了更局部的位置,所以性能上会更有优势:
3.8.5 兼容三方 UI 库
由于大部分第三方 UI 库是遵照了 react 受控思想设计的,例如 Antd#Input 并没有出 ref,官方也提供了 Controller 组件,用它包装的三方组件只需要遵循默认的受控规范 onChange / value 即可,Controller 会在其构建内部状态,变相模拟出了 Uncontrolled Component。
3.8.6 感想
最终来看 react-hook-form 的其实与其他受控表单库可以说是殊途同归,都是希望状态分布管理,单个表单项的更新不影响其他的表单项,而联动都可以说是使用了订阅来做到的,从某种程度上看,基于非受控实现的 react-hook-form 来看这一切甚至更加自然。可以看到, 非受控也可以做到任何受控表单能做的事情,这也是为什么我个人在 2.3 小节中提到, 受控和非受控从某种层面上看可以互相实现。
3.9 横向比较
这里再给个横向比较图:
4. Formily (1.x)
4.1 背景
如果不了解之前没有了解过 Formily 可能下面的内容会有些突兀,建议可以先大概了解下。因为这一节我不打算讲 Formily 的基本用法,只是谈谈我对于 Formily 的一些理解。
之所以把 Formily 单独抽出来作为一讲来讲,是因为在我做表单调研工作的这个阶段 (2020.10),毫不夸张地说,Formily 是我当时认知范围内,设计理念最先进 (也可以说是激进),表单领域研究最透彻的一个表单解决方案. 在我写总结的时候,它的断代全新版本 2.x 也已经算是发了 preview 版,据说有挺多改进的, 不过还没来得及仔细看,所以以下内容只针对 1.x。
4.2 场景
在我的认知里,虽然 Formily 初衷是大而全,对于场景的预设考虑比较完备,但是我感觉它还是比较适合高复杂度,表单联动多,有比较极端的性能要求的表单场景, 如果只是简单场景确实没必要,正如我们所说的,antd 4.x 已经拜托了全量渲染,如果能合理地利用 dependencies / shouldUpdate 其实性能表现表现上已经足够好。不过如果你的场景特别复杂,特别是联动逻辑比较多,利用 formily 还是能够有效地帮助你收敛逻辑,降低心智负担的。
4.3 学习成本
虽然我上面说到 Formily 理念先进,但其实业界对于它可以说是褒贬不一的,被诟病最多的问题就是学习成本。我个人也认为这个是阻碍 Formily 火起来的最最核心的问题 (我写文章的时候 Formily 已经有 3000 多个 star 了)。坦率来讲, Formily 的学习成本相较于社区其他表单方案还是偏高的,其实原因主要是两方面的:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。
因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!
由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频
如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
20.10),毫不夸张地说,Formily 是我当时认知范围内,设计理念最先进 (也可以说是激进),表单领域研究最透彻的一个表单解决方案. 在我写总结的时候,它的断代全新版本 2.x 也已经算是发了 preview 版,据说有挺多改进的, 不过还没来得及仔细看,所以以下内容只针对 1.x。
4.2 场景
在我的认知里,虽然 Formily 初衷是大而全,对于场景的预设考虑比较完备,但是我感觉它还是比较适合高复杂度,表单联动多,有比较极端的性能要求的表单场景, 如果只是简单场景确实没必要,正如我们所说的,antd 4.x 已经拜托了全量渲染,如果能合理地利用 dependencies / shouldUpdate 其实性能表现表现上已经足够好。不过如果你的场景特别复杂,特别是联动逻辑比较多,利用 formily 还是能够有效地帮助你收敛逻辑,降低心智负担的。
4.3 学习成本
虽然我上面说到 Formily 理念先进,但其实业界对于它可以说是褒贬不一的,被诟病最多的问题就是学习成本。我个人也认为这个是阻碍 Formily 火起来的最最核心的问题 (我写文章的时候 Formily 已经有 3000 多个 star 了)。坦率来讲, Formily 的学习成本相较于社区其他表单方案还是偏高的,其实原因主要是两方面的:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。
因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
[外链图片转存中…(img-4UiJwyj5-1712532455554)]
[外链图片转存中…(img-lbbvN1wz-1712532455554)]
[外链图片转存中…(img-yXXXy0sk-1712532455554)]
既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!
由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频
如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
[外链图片转存中…(img-XsFuMDRE-1712532455555)]