我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

兼容性问题

第三方依赖兼容问题

  • React - 最低 v16.9,部分组件使用 hooks 重构 react升级相关文档
  • Less - 最低 v3.1.0,建议升级到 less 4.x
  • @ant-design/icons-antd - 不再内置 Icon 组件,请使用独立的包

对 3.x 的兼容性处理

或许是考虑到部分组件升级的毁坏性,antd4.x 中依然保留了对 3.x 版本的兼容,废弃的组件通过 @ant-design/compatible 保持兼容,例如 Icon, Form

注:建议 @ant-design/compatible 仅在升级过程中稍作依赖,升级 4.x 请完全剔除对该过渡包的依赖

升级步骤(只有一步)

1、@ant-design/codemod-v4 自带升级脚本,会自动替换代码

# 通过 npx 直接运行
npx -p @ant-design/codemod-v4 antd4-codemod apps/xxxx # 或者全局安装
# 使用 npm
npm i -g @ant-design/codemod-v4
# 或者使用 yarn
yarn global add @ant-design/codemod-v4 # 运行
antd4-codemod src

注意: 该命令和脚本只会进行代码替换,不会进行AntD的版本升级,需要手动将其升级至4.22.5

该命令完成的工作:
1. 将 Form 与 Mention 组件通过 @ant-design/compatible 包引入
2. 用新的 @ant-design/icons 替换字符串类型的 icon 属性值
3. 将 Icon 组件 + type =“” 通过 @ant-design/icons 引入
4. 将 v3 LocaleProvider 组件转换成 v4 ConfigProvider 组件
5. 将 Modal.method() 中字符串 icon 属性的调用转换成从 @ant-design/icons 中引入

antd4-codemod



上图这类报错是 Icon 组件自动替换错误,有 2 种处理方式:

  1. 报错文件的 Icon 比较少的情况,可以直接手动替换该文件中的 Icon 组件。具体替换成 Icon 中的哪个组件可以根据 type 在 Icon文档 中找。

  2. 下图中是具体报错的节点,可以看到 JSXSpreadAttribute 节点也就是拓展运算符中没有 name 属性,所以把 Icon 组件的拓展运算符改一下再执行替换脚本就可以了

antd4 问题修复

styled-components

styled-components 依赖需要转换写法

Icon

不要使用兼容包的 icon

在 3.x 版本中,Icon 会全量引入所有 svg 图标文件,增加了打包产物

在 4.x 版本中,对 Icon 进行了按需加载,将每个 svg 封装成一个组件

注:antd 不再内置 Icon 组件,请使用独立的包 @ant-design/icons

  • 使用

    import { Icon } from 'antd';
    mport { SmileOutlined } from '@ant-design/icons'; const Demo = () => (
    <div>
    <Icon type="smile" />
    <SmileOutlined />
    <Button icon={<SmileOutlined />} />
    </div>
    );
  • 兼容

    import { Icon } from '@ant-design/compatible';
    const Demo = () => (
    <div>
    <Icon type="smile" />
    <Button icon="smile" />
    </div>
    );

Form

antd Form 从 v3 到 v4

Form.create()

在 3.x 中,表单中任意一项的修改,都会导致 Form.create() 包裹的表单重新渲染,造成性能消耗

在 4.x 中,Form.create() 不再使用

如果需要使用 form 的 api,例如 setFieldsValue 等,需要通过 Form.useForm() 创建 Form 实体进行操作

  • 函数组件写法
    // antd v4
    const Demo = () => {
    const [form] = Form.useForm(); React.useEffect(() => {
    form.setFieldsValue({
    username: 'Bamboo',
    });
    }, []); return (
    <Form form={form} {...props}> ... </Form>
    )
    };
  • 如果是 class component, 也可以通过 ref 获取
    class Demo extends React.Component {
    formRef = React.createRef(); componentDidMount() {
    this.formRef.current.setFieldsValue({
    username: 'Bamboo',
    });
    } render() {
    return (
    <Form ref={this.formRef}>
    <Form.Item name="username" rules={[{ required: true }]}>
    <Input />
    </Form.Item>
    </Form>
    );
    }
    }

当我们使用 From.create() 的时候,可能会传入参数,做数据处理,例如:

export const FilterForm: any = Form.create<Props>({
onValuesChange: (props, changedValues, allValues) => {
const { onChange } = props;
onChange(allValues);
},
})(Filter);

由于 Form.create 的删除,需要放到 <Form>

<Form
ref={this.formRef}
layout="vertical"
className="meta_form"
onValuesChange={(_, allValues) => {
const { onChange } = this.props;
onChange(allValues);
}}
>

getFieldDecorator

在 4.x 中,不在需要 getFieldDecorator 对 Item 进行包裹。

注意以下问题

  • 将之前写在 getFieldDecorator 中的 name, rules 等移到属性中
  • 初始化在 form 中处理,避免同名字段冲突问题
  • 关于表单联动的问题,官方提供了 shouldUpdate 方法,
// antd v4
const Demo = () => (
<Form initialValues={{ username: 'yuwan' }}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);

initialValue

历史问题

initialValue 从字面意来看,就是初始值 defaultValue,但是可能会有部分同学使用他的时候会误以为 initialValue 等同于 value

造成这样的误解是因为在 3.x 的版本中,一直存在一个很神奇的问题,受控组件的值会跟随 initialValue 改变

看下面的例子,点击 button 修改 username, input 框的 value 也会随之改变

const Demo = ({ form: { getFieldDecorator } }) => (
const [username, setUsername] = useState('');
const handleValueChange = () => {
setUsername('yuwan');
}
return (
<Fragment>
<Form>
<Form.Item>
{getFieldDecorator('username', {
initialValue: username,
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
<Button onClick={handleValueChange}>Change</Button>
</Fragment>
)
); const WrappedDemo = Form.create()(Demo);

但当 input 框被编辑过,initialValue 和 input 的绑定效果就消失了,正确的做法应该是通过 setFieldsVlaue 方法去 set 值

4.x 版本的 initialValue

在 4.x,antd 团队已经把这个 bug 给解了,并且其一是为了 name 重名问题,二是再次强调其初始值的功能,现在提到 Form 中了,

当然,如果继续写在 Form. Item 中也是可以的,但需要注意优先级

shouldUpdate

前面有说过,form 表单不再会因为表单内部某个值的改变而重新渲染整个结构,而设有 shouldUpdate 为 true 的 Item,任意变化都会使该 Form. Item 重新渲染

它会接收 render props,从而允许你对此进行控制

这里稍微注意一下,请勿在设置 shouldUpdate 的外层 Form. Item 上添加 name, 否则,你会得到一个 error

<Form.Item shouldUpdate={(prev, next) => prev.name !== next.name}>
{form => form.getFieldValue('name') === 'antd' && (
<Form.Item name="version">
<Input />
</Form.Item>
)}
</Form.Item>

在使用 shouldUpdate 的时候,需要在第一个 Form.Item 上加上 noStyle,否则就会出现下面的情况,会有留白占位的情况

validateTrigger

onBlur 时不再修改选中值,且返回 React 原生的 event 对象。

如果你在使用兼容包的 Form 且配置了 validateTriggeronBlur ,请改至 onChange 以做兼容。

validator

在 antd3 时,我们使用 callback 返回报错。但是 antd4 对此做了修改,自定义校验,接收 Promise 作为返回值。示例参考

  • antd3 的写法
<FormItem label="具体时间" {...formItemLayout}>
{getFieldDecorator('specificTime', {
rules: [
{
required: true,
validator: (_, value, callback) => {
if (!value || !value.hour || !value.min) {
return callback('具体时间不可为空');
}
callback();
},
},
],
})(<SpecificTime />)}
</FormItem>
  • antd4 的写法
<FormItem
label="具体时间"
{...formItemLayout}
name="specificTime"
rules={[
{
required: true,
validator: (_, value) => {
if (!value || !value.hour || !value.min) {
return Promise.reject('具体时间不可为空');
}
return Promise.resolve();
},
},
]}
>
<SpecificTime />)
</FormItem>

validateFields

不在支持 callback,该方法会直接返回一个 Promise,可以通过 then / catch 处理

this.formRef.validateFields()
.then((values) => {
onOk({ ...values, id: appInfo.id || '' });
})
.catch(({ errorFields }) {
this.formRef.scrollToField(errorFields[0].name);
})

或者使用 async/await

try {
const values = await validateFields();
} catch ({ errorFields }) {
scrollToField(errorFields[0].name);
}

validateFieldsAndScroll

该 api 被拆分了,将其拆分为更为独立的 scrollToField 方法

onFinishFailed = ({ errorFields }) => {
form.scrollToField(errorFields[0].name);
};

form.name

在 antd 3.x 版本,绑定字段时,可以采用.分割的方式。如:

getFieldDecorator('sideTableParam.primaryKey')
getFieldDecorator('sideTableParam.primaryValue')
getFieldDecorator('sideTableParam.primaryName')

在最终获取 values 时,antd 3.x 的版本会对字段进行汇总,得到如下:

const values = {
sideTableParam: {
primaryKey: xxx,
primaryValue: xxx,
primaryName: xxx,
}
}

而在 antd 4.x下,会得到如下的values 结果:

const values = {
'sideTableParam.primaryKey': xxx,
'sideTableParam.primaryValue': xxx,
'sideTableParam.primaryName': xxx,
}

解决方法:

在 antd 4.x 版本传入数组

name={['sideTableParam', 'primaryKey']}
name={['sideTableParam', 'primaryValue']}
name={['sideTableParam', 'primaryName']}

使用 setFieldsValue 设置值:

setFieldsValue({
sideTableParam: [
{
primaryKey: 'xxx',
primaryValue: 'xxx',
primaryName: 'xxx',
},
],
});

当我们使用 name={['sideTableParam', 'primaryKey']} 方式绑定值的时候,与其关联的 dependencies/getFieldValue 都需要设置为['sideTableParam', 'primaryKey']

例如:


<FormItem dependencies={[['alert', 'sendTypeList']]} noStyle>
{({ getFieldValue }) => {
const isShowWebHook = getFieldValue(['alert', 'sendTypeList'])?.includes(
ALARM_TYPE.DING
);
return (
isShowWebHook &&
RenderFormItem({
item: {
label: 'WebHook',
key: ['alert', 'dingWebhook'],
component: <Input placeholder="请输入WebHook地址" />,
rules: [
{
required: true,
message: 'WebHook地址为必填项',
},
],
initialValue: taskInfo?.alert?.dingWebhook || '',
},
})
);
}}
</FormItem>

当我们希望通过 validateFields 拿到的数据是数组时,例如这样:

我们可以设置为这样

const formItems = keys.map((k: React.Key) => (
<Form.Item key={k} required label="名称">
<Form.Item
noStyle
name={['names', k]}
rules={[
{ required: true, message: '请输入标签名称' },
{ validator: utils.validateInputText(2, 20) },
]}
>
<Input placeholder="请输入标签名称" style={{ width: '90%', marginRight: 8 }} />
</Form.Item>
<i className="iconfont iconicon_deletecata" onClick={() => this.removeNewTag(k)} />
</Form.Item>
));

Tooltip



extra

<FormItem
label="过滤条件"
extra={
<Tooltip title={customSystemParams}>
系统参数配置&nbsp;
<QuestionCircleOutlined />
</Tooltip>
}
>
<Input.TextArea />
</FormItem>

Select

rc-select

底层重写

  • 解决些许历史问题

    • rc-select & rc-select-tree 的 inputValue & searchValue 之争

      rc-select-tree 是 rc-select 结合 tree 写的一个组件,相似但又不同,searchValue 就是其中一点,也不是没人提过 issue,只是人的忘性很大,时间长了就忘了,混了,导致在 rc-select 中甚至出现了 searchValue 的字样
    • inputValue 历史问题,this.state.inputValue



      也不是不想改,只是改了之后改出了一堆 bug,真真应了一句话,每一个历史包袱的存在,都有他存在的原因,,
    • onSelect 清空了值,又会被 onChange 赋值回来
  • 模块复用

在新版的 rc-select 中,antd 官方抽取了一个 generator 方法。它主要接收一个 OptionList 的自定义组件用于渲染下拉框部分。这样我们就可以直接复用选择框部分的代码,而自定义 Select 和 TreeSelect 对应的列表或者树形结构了。

labelInValue

在 3.x 版本为

在 4.x 版本为

Table

fixed



固定列时,文字过长导致错位的问题,被完美解决了,✿✿ヽ(°▽°)ノ✿

历史原因

3.x 中对 table fixed 的实现,是写了两个 table, 顶层 fixed 的是一个,底层滚动的是一个,这样,出现这种错位的问题就很好理解了。

要解决也不是没有办法,可以再特定的节点去测算表格列的高度,但是这个行为会导致重排,会影响性能问题

解决方案

4.x 中,table fixed 不在通过两个 table 来实现,他使用了一个 position 的新特性:position: sticky;

元素根据正常文档流进行定位,然后相对它的_最近滚动祖先(nearest scrolling ancestor)_和 containing block (最近块级祖先 nearest block-level ancestor),包括 table-related 元素,基于 top , right , bottom , 和 left 的值进行偏移。偏移值不会影响任何其他元素的位置。

优点

  • 根据正常文档流进行定位
  • 相对最近滚动祖先 & 最近块级祖先进行偏移

缺点

  • 不兼容 <= IE11

解决了使用 absolute | fixed 脱离文档流无法撑开高度的问题,也不在需要对高度进行测量

table.checkbox

问题描述

资产升级后,checkbox 宽度被挤压了。

解决方案

通过在 rowSelection 中设置 columnWidth 和 fixed 解决。

const rowSelection = {
fixed: true,
columnWidth: 45,
selectedRowKeys,
onChange: this.onSelectChange,
};

渲染条件

antd4 Table 对渲染条件进行了优化,对 props 进行“浅比较”,如果没有变化不会触发 render。

类名更改

. ant-table-content 更改为 .ant-table-container

.ant-form-explain 更改为 .ant-form-item-explain

dataIndex 修改

在 antd3.0 的时候,我们采用 user.userName 能够读到嵌套的属性

{
title: '账号',
dataIndex: 'user.userName',
key: 'userName',
width: 200,
}

antd4.0 对此做了修改,同 Form 的 name

{
title: '账号',
dataIndex: ['user', 'userName'],
key: 'userName',
width: 200,
}

table pagination showSizeChanger

问题描述

升级 antd4 后,发现一些表格分页器多了 pageSize 切换的功能,代码中 onChange 又未对 size 做处理,会导致 底部分页器 pageSize 和数据对不上,因此需要各自排查 Table 的 pagination 和 Pagination 组件,和请求列表接口的参数

<Table
rowKey="userId"
pagination={{
total: users.totalCount,
defaultPageSize: 10,
}}
onChange={this.handleTableChange}
style={{ height: tableScrollHeight }}
loading={this.state.loading}
columns={this.initColumns()}
dataSource={users.data}
scroll={{ x: 1100, y: tableScrollHeight }}
/> handleTableChange = (pagination: any) => {
this.setState(
{
current: pagination.current,
},
this.search
);
}; search = (projectId?: any) => {
const { name, current } = this.state;
const { project } = this.props;
const params: any = {
projectId: projectId || project.id,
pageSize: 10,
currentPage: current || 1,
name: name || undefined,
removeAdmin: true,
};
this.loadUsers(params);
};

antd4.0 对此做了修改,同 Form 的 name

<Table
rowKey="userId"
pagination={{
showTotal: (total) => `共${total}条`,
total: users.totalCount,
current,
pageSize,
}}
onChange={this.handleTableChange}
style={{ height: tableScrollHeight }}
loading={this.state.loading}
columns={this.initColumns()}
dataSource={users.data}
scroll={{ x: 1100, y: tableScrollHeight }}
/> handleTableChange = (pagination: any) => {
this.setState(
{
current: pagination.current,
pageSize: pagination.pageSize,
},
this.search
);
}; search = (projectId?: any) => {
const { name, current, pageSize } = this.state;
const { project } = this.props;
const params: any = {
projectId: projectId || project.id,
pageSize,
currentPage: current || 1,
name: name || undefined,
removeAdmin: true,
};
this.loadUsers(params);
};

另外,一些同学在 Table 中 既写了 onChange,也写了 onShowSizeChange,这个时候要注意,当切换页码条数的时候两个方法都会触发,onShowSizeChange 先触发,onChange 后触发,这个时候如果 onChange 内未对 pageSize 做处理可能导致切页失败,看下面代码就明白了,写的时候稍微注意一下即可。

table sorter columnKey

问题描述

表格中如果要对表格某一字段进行排序需要在 columns item 里设置 sorter 字段,然后在 onChange 里拿到 sorter 对象进行参数处理,再请求数据,需要注意的是,很多用到了 sorter.columnKey 来进行判断,容易出现问题,sorter.columnKey === columns item.key,如果未设置 key,那么获取到的 columnKey 就为空,导致搜索失效,要么设置 key,再进行获取,同理, sorter.field === columns item.dataIndex,设置 dataIndex,通过 sorter.field 进行获取,两者都可以

columns={
[
{
title: '创建时间',
dataIndex: 'gmtCreate1',
key: 'aa',
sorter: true,
render(n: any, record: any) {
return DateTime.formatDateTime(record.gmtCreate);
}
},
...
]
}
onChange={(pagination: any, filters: any, sorter: any) {
console.log(pagination, '--pagination');
console.log(filters, '--filters');
console.log(sorter, '--sorter');
}}

Tree

Tree 组件取消 value 属性,现在只需要添加 key 属性即可

特别注意, 此问题会导致功能出问题,需要重点关注!!!

在项目中经常在 TreeItem 中增加参数,如:<TreeItem value={value} data={data} >。在拖拽等回调中就可以通过 nodeData.props.data的方式获取到 data 的值。

但在 antd4 中,获取参数的数据结构发生了改变,原先直接通过 props 点出来的不行了。

有两种方式取值

  1. 不使用props。直接采用 nodeData.data 的方式,也可以直接拿到
  2. 继续使用 props。在antd4中,还是可以通过 props 找到参数,只不过 antd 会把所有参数使用 data 进行包裹。就需要改成 nodeData.props.data.data

新版数据结构如下:

drag

拖拽节点位置的确定与 3.x 相比进行了变更,官网并没有说明。具体如下图



左侧为 3.x,右侧为 4.x。

在3.x版本,只要把节点拖拽成目标节点的上中下,即代表着目标节点的同级上方,子集,同级下方

在 4.x 版本,是根据当前拖拽节点与目标节点的相对位置进行确定最终的拖拽结果。

当拖拽节点处于目标节点的下方,且相对左侧对齐的位置趋近于零,则最终的位置为目标节点的同级下方。

当拖拽节点处于目标节点的下方,且相对左侧一个缩近的位置。则最终的位置为目标节点的子集。

当拖拽节点处于目标节点的上方,且相对左侧对齐的位置趋近于零,则最终的位置为目标节点的同级上方。

Pagination

Pagination自 4.1.0 版本起,会默认将 showSizeChanger 参数设置为 true ,因而在数据条数超过50时,pageSize 切换器会默认显示。这个变化同样适用于 Table 组件。可通过 showSizeChanger: false 关闭

如果 size 属性值为 small,则删除 size 属性。

Drawer

当我们在 Drawer 上 设置了 getContainer={false} 属性之后,Drawer 会添加上 .ant-drawer-inline 的类名导致我们 position: fixed 失效

Button

在 antd 3.0 中危险按钮采用 type

使用如下:

设计改动点 type、dangr 属性

Tabs

使标签页不被选中

// 3.x
activeKey={undefined}
// 4.x
activeKey={null}

总结

该篇文章详细讲解了如何从 antd3 升级到 antd4 其中的步骤,以及团队在实践过程中发现的一些问题和对应的解决方案。

antd 3.x升4.x踩坑之路~的更多相关文章

  1. html2canvas的踩坑之路

    html2canvas的踩坑之路 前言 早有耳闻这个html2canvas比较坑,但无奈于产品需求的压迫,必须实现html转图片的功能,自此走上了填坑之路,好在最后的效果还算令人满意,这才没有误了产品 ...

  2. MySQL Connector/NET 使用小结(踩坑之路)

    背景描述 根据项目的需要,需连接MySQL获取数据. 首先,先了解一下项目的情况: 之前的代码是C#编写的的, 运行时:.NETFramework3.5. 由于项目已经部署上线,因此不能升级运行时,这 ...

  3. Android 上传开源项目到 jcenter 实战踩坑之路

    本文微信公众号「AndroidTraveler」首发. 背景 其实 Android 上传开源项目到 jcenter 并不是一件新鲜事,网上也有很多文章. 包括我本人在将开源项目上传到 jcenter ...

  4. Android SDK 开发——发布使用踩坑之路

    前言 在 Android 开发过程中,有些功能是通用的,或者是多个业务方都需要使用的. 为了统一功能逻辑及避免重复开发,因此将该功能开发成一个 SDK 是相当有必要的. 背景 刚好最近自己遇到了类似需 ...

  5. jQuery升级踩坑之路

    1.使用了被废弃的jQuery.browser属性 jQuery 从 1.9 版开始,移除了 $.browser 和 $.browser.version , 取而代之的是 $.support . 在更 ...

  6. Java踩坑之路

    陆陆续续学Java也快一年多了,从开始的一窍不通到现在的初窥门径,我努力过,迷茫过,痛过,乐过,反思过,沉淀过.趁着新年,我希望能把这些东西记下来,就当是我一路走来的脚印. 一.初识网站应用 记得第一 ...

  7. Mahout踩坑之路

    一.版本对比 公司版Mahout 由于Mahout只能允许于hadoop0.20以上版本上,而百度的hadoop是hadoop0.19的一个分支.因此百度HPC组曾经将Mahout移植到百度的hado ...

  8. webpack踩坑之路——构建基本的React+ES6项目

    转自:http://www.cnblogs.com/ghost-xyx/p/5483464.html webpack是最近比较火的构建工具,搭配上同样比较火的ReacJS与ES6(ES2015)一定是 ...

  9. webpack踩坑之路——图片的路径与打包

    转自:http://www.cnblogs.com/ghost-xyx/p/5812902.html 刚开始用webpack的同学很容易掉进图片打包这个坑里,比如打包出来的图片地址不对或者有的图片并不 ...

  10. 踩坑之路_"var name = ' ';"_迷之BUG

    情景介绍:最近写一个拖拽生成图表的工具,自己的思路每次mousedown的时候动态将this的name属性值赋值给全局中变量(自己手贱测试时直接将变量名命名为了'name',一大波bug还有30s到达 ...

随机推荐

  1. Java使用lamda表达式简化代码

    代码,自然写的越简洁越好啦,写的人舒服,看的人也舒服,一切为了高效. 要把有限的时间花到其它有意思的事情上去. 目的 学习简化代码的思路,使用jdk8新特性lamada表达式. 使用 某接口,只有一个 ...

  2. 陪你去看 Lodash.js 起步

    lodash 起步(数组) Lodash 是一个较为流行的 JavaScript 的实用工具库. 在开发过程中如果能熟练使用一些工具库提供的方法,有利于提高开发效率. 笔者从 API 上入手,不分析其 ...

  3. AR Engine光照估计能力,让虚拟物体在现实世界更具真实感

    AR是一项现实增强技术,即在视觉层面上实现虚拟物体和现实世界的深度融合,打造沉浸式AR交互体验.而想要增强虚拟物体与现实世界的融合效果,光照估计则是关键能力之一. 人们所看到的世界外观,都是由光和物质 ...

  4. ThinkPhp5 自定义异常处理类

    在项目的开发过程中异常抛出尤为重要不仅能够做出友好提示帮助掩盖我们伟大的程序员们尴尬的瞬间,还能做到提示开发人员代码白编写的错误,下面进行自定义异常抛出类,纯属个人理解,希望大家指正 首先在框架中我们 ...

  5. ubuntu undefined reference to

    温馨提示,请使用ctrl+F进行快速查找 libdl.so undefined reference to `dlsym' undefined reference to `dlopen' undefin ...

  6. float16与float32转换

    // based on https://gist.github.com/martin-kallman/5049614 // float32 // Martin Kallman // // Fast h ...

  7. apktool回编译报错

    报错 error: No resource identifier found for attribute 'XXX' in package 'XXX' 解决 将xml文件中 "http:// ...

  8. 关于mysql数据库user表没有password字段

    解决 这个是因为mysql的版本问题,是mysql 5.7版本出现的,具体是mysql 5.7.x 开始变化的我不知道 新的字段变更为authentication_string 修改密码的方式还是和原 ...

  9. MySQL数据库下载以及启动软件的详细步骤

    第一步>>>在浏览器上百度上搜索MySQL 如何判断官网?有官网两个字的或者纯英文解释的大概率就是官网 第二步>>>点击DOWNLOAWDS 第三步>> ...

  10. 大前端html基础学习03-定位锚点透明

    一.position 定位属性和属性值position 定位属性,检索对象的定位方式:语法:position:static /absolute/relative/fixed/sticky/unset/ ...