简书Header组件开发(使用iconfont、实现搜索框动画效果)

总结

  • 上一篇笔记中,我们在定义组件样式时,使用过 &.yourClassName来给使用同一组件名创造的不同组件设置独特的样式
    • 比如:在style.js中我们<A>组件规定了统一的样式,在统一的样式中又使用&.aa{样式们}&.bb{}设置了不同的样式,那么在index.js中我们就可以使用<A className="aa"><A/><A className="bb" ><A/>来创建样式有些许不同的A组件了。
  • 也在定义组件的样式中使用过 &::placeholder给组件的placeholder属性 设置样式
  • 之前我们都是给同一层级的组件定义样式。而 .yourClassName是给组件中的组件(即子组件)设置样式
    • 比如:我们在index.js中使用 iconfont图标 时组件是span,这并不是我们自己定义的组件,所以想要修改iconfont图标的部分样式时,我们 可以在SearchWrapper的样式中加入.iconfont{样式们} 来设置。
      1
      2
      3
      4
      <SearchWrapper>
      <NavSearch />
      <span className="iconfont">&#xe62d;</span>
      </SearchWrapper>

注意

  1. 同时调用多个样式名时,用空格分隔样式名
  2. &表示当前元素
    • 例如下面代码**编译之后就是a.b{}**(表示className为b的a组件),没有&就成了a .b(中间有个空格,b这个class成了a的后代元素的了)。
      1
      2
      3
      a{
      &.b {}
      }

使用iconfont嵌入头部图标

需要借助**iconfont官网**完成以下操作。

新建项目

打开iconfont官网-注册登录-“图标管理”-“我的项目”-(点击右边图标)新建项目(图标仓库)
新建项目

目前我们需要3个图标:
目前我们需要3个图标

添加图标

1.在顶部搜索框中搜索我们需要的图标-加入“购物车”
搜索并添加图标

2.点击右上角 购物车图标 -“添加至项目”
添加至项目

3.修改羽毛颜色后,“下载至本地”
修改羽毛颜色

4.得到压缩文件download,此时文件中**demo_index.html教我们如何使用iconfont**,真正有用的是下面5个文件:
有用的6个文件

5.将有用的6个文件添加至 src\statics下新建的iconfont文件夹中。

使用图标

修改iconfont.css为全局样式组件

iconfont.css是iconfont主要的css文件。

改为相对路径

1.我们需要给iconfont.css中每个url的路径前加./,将其改为相对路径(data:开头的路径不需要加,他是base64的文件)
添加./改为相对路径

2.下面的几个class可以先删除:
image.png

改为全局样式组件

因为我们整个项目都可能需要用到iconfont,所以iconfont.css 改为全局样式组件会更合适。

  1. 定义全局样式组件:将iconfont.css重命名为iconfont.js,在iconfont.js中,借助’styled-components’的createGlobalStyle定义全局样式组件GlobalIconFontStyle
  2. 使用全局样式组件:在src-App.js中,引入并使用 全局样式组件GlobalIconFontStyle(注意引用路径和Globalstyle不同)

iconfont.js中,定义全局样式组件GlobalIconFontStyle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createGlobalStyle } from 'styled-components'

export const GlobalIconFontStyle = createGlobalStyle`
@font-face {font-family: "iconfont";
src: url('./iconfont.eot?t=1582706509034'); /* IE9 */
src: url('./iconfont.eot?t=1582706509034#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAARMAAsAAAAACGQAAAP/AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDHAqEJINqATYCJAMQCwoABCAFhG0HQhtWB1GUL0qG7EtMRUbWKynSLdb02Bgu0Ffng0EHFQEAALwATC6AXxwQD1/7vZ67e9/PBphcJjYpjCdUBEa0sqo1hoyr8R3PqkLlx7XfvhgmfZh9bYj7ydv9/fPdsOahkgid6pFkmvDMQJ5WOIOSTiJe5+CkN90D/8yLK9Q8y/lsov5/rqmXzw8stm9nc4lqq6S2wwGOWwcW0VQgA96Abawbxq68iMMEBqyo5braPTiJgpIxKhBvqSKgYMoma6jQjL2Shdkfn3CdVj4QDvnofz/+iUeQoquMo64975JR60+llP4v9aiW4d6N5wFbRsURJImXpb7HPGHiiMeA0t3rFNI0hbHaqpBiL7pm/uVRhEr0GLohRJ5o64nJbY7CzziCn2wqxHN4Qz++4T8gl+6eqtLaNJAxNHosmZ12J8MzLSHdb/5QmHHE6yucA8MZiIFtp4z62/gyH58df3NDr81ZXOkc0HCBx0egqDeG+SCIFwwHWxrPQJK8FEXz6iHv7rKFGdCrq6+3k6eAMQ17Tq4Kdu8z6mV3o0fAMAgeyip29oFdQAOz58IEmJJB/nywJ8LMsvlY+KV3JzRErGege5pnwDfFfDDMoIe5hYBAD6aAvYNdD0+jbR5lLe9Fsf0XLd7ijV283ObhtBnz/jbOyLzSVcGTzZRT5LzjMoMAB9P6t8s+/JoP/M0v9VFKGHcnv4t4fUXsMsWu2DF1xtmdRfnuiQkCVOdjCbQRxrEY/R7oTRprza++OfHS2t/eamE/SxlebBNdv0nF6Rpw6d7CPELhQ6vrevmGPWwnOtzOhmm2iazIIGs+3lOAcH3XkSyVfnMwvxXtVUGBnhXanWzxxJn5XraZuV8epThC3gpPRaL9yaa5W7BZr1w7fW7evnoV0ghGCcuUXlKk5rtidwT4jSILwsV/p9Uf/t7javj/X6Wpvv/xg9z93H4ewm74tQFXHv6x6yBStdP7e/LW8t9sSYka5K9KVRr2TLlcbuJQuvgnBNN1g48oGDCAn5thwucY+ukZZG6gmY6h6LMMVbOFJnkEnUEn0NOcwYBDPcsHTWI8Ivtx4ABAGHMMxYjvUI25RZP8gM6Mf+gZCw8MuBPcGw7aCZpmwUDgIGNxC1PJNIjl5GlB7jgoa5rAkkqfngbm0DCOhaPZdA8YwMaY4qwrcc4JJszUcTc4DjTNxBYzUyDxcJJzqz4SIWUvCkumjtJcwQABB2SYaAtGSUwG4lbn0nKfHwcUazQCVsOriE4DjIN2jsWERVugPTqjFe9aujvWKeI4juBhBGPSYd3QAzQzYcKs8n4pQMKFJXvELPUiqBJpqwvPb9CfcA0MMG7PKFEjo1MuCSEnQTEoocACeWu6YIoUIQA=') format('woff2'),
url('./iconfont.woff?t=1582706509034') format('woff'),
url('./iconfont.ttf?t=1582706509034') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('./iconfont.svg?t=1582706509034#iconfont') format('svg'); /* iOS 4.1- */
}

.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
`

App.js中,引入并使用 全局样式组件GlobalIconFontStyle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component,Fragment } from 'react';
import { Globalstyle } from './style';
import Header from "./common/header";
import {GlobalIconFontStyle} from "./statics/iconfont/iconfont"

class App extends Component {
render() {
return (
<Fragment>
<Globalstyle />
<GlobalIconFontStyle />
<Header />
</Fragment>
);
}
}

export default App;

控制台未报错,说明iconfont成功引入:
控制台未报错


使用Aa图标

根据demo的使用方法,使用<span>标签替换我们common-header-index.js中的Aa.
注意:在react中使用className代替class

1.文件夹中的**demo_index.html-Unicode拉到最下面有教我们使用方法**:
使用方法
几年前是<i>标签用于引用字体图标,现在文档改为了<span>,其实你写<div>也可以,但最好和官方保持一致。

文档中的字体编码:
文档中的字体编码

2.common-header-index.js中,使用<span>标签替换我们原本的字符串Aa:

1
2
3
<NavItem className="right">
<span className="iconfont">&#xe636;</span>
</NavItem>

注意:在react中使用className代替class
成功


使用羽毛笔图标

common-header-index.js:

1
2
3
4
<Button className="writting">
<span className="iconfont">&#xe6e5;</span>
写文章
</Button>

使用搜索图标(.iconfont设置图标样式)

  • 使用SearchWrapper组件:因为搜索框(input)内不能再放置组件,所以我们只能把搜索图标放在搜索框外,然后使用SearchWrapper组件(div)将他们包裹**起来。
  • 使用子绝父相对图标进行定位:为了让图标出现在搜索框内,我们需要将它绝对定位以脱离文档流。为了布局方便,他的父元素SearchWrapper组件需要设置为相对定位。
  • 注意:在包裹图标的 父组件样式 中添加.iconfont设置(子组件)图标的样式
    • 回顾
    • 以前我们在定义组件样式时,使用过**&.yourClassName来给使用某个className的样式组件设置不同的样式**(比如<A className="aa" /><A className="bb" />就可以在样式上稍有不同)。
    • 也使用过**&::placeholderplaceholder属性 设置样式**。他们都是同一个组件中的内容。
    • iconfont使用的span组件并不是我们自己定义的,所以他的样式要去父组件中给子组件(即span)设置,我们可以通过在父组件中增加.iconfont给组件中的组件(即子组件)设置样式

common-header-style.js:
复习:line-height设置

  1. SearchWrapper组件需要左浮动:因为他前面的组件全都浮动了,浮动的组件脱离了文档流,所以她也需要浮动才能出现在我们希望的位置。
  2. 图标line-hight垂直居中:因为这个图标是<span>元素,所以可以使用line-hight实现垂直居中。【绝对定位使得元素成为inline-block,所以虽然表现为块状,但仍然可以使用行内元素的line-hight属性实现垂直居中】
  3. 图标text-align水平居中:因为这个图标是<span>元素,所以可以使用text-align水平居中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const SearchWrapper = styled.div`
position: relative;
float:left;
.iconfont{
position: absolute;
right:5px;
bottom:5px;
width:30px;
line-height:30px;
text-align:center;
border-radius:15px;
background:green;
}
`;

common-header-index.js:

1
2
3
4
<SearchWrapper>
<NavSearch />
<span className="iconfont">&#xe62d;</span>
</SearchWrapper>

最终效果
搜索图标上的绿色背景是为点击以后的动态效果所保留的(具体效果可以从简书官网看到)


实现搜索框动画效果

实现的效果:点击搜索框时搜索框变长且搜索按钮变为灰底白字

搜索框两种样式

首先我们不考虑逻辑,只考虑如何做出两个样式
样式1:我们原本的搜索框样式(窄且按钮无底色)
样式2:点击后显示的样式,搜索框长且按钮为灰底白字

注意: react中我们尽量不操作DOM,我们使用数据的改变来让页面发生改变

思路

  1. 在Header组件的index.js中创造一个构造函数,在state中保存一个变量focused表示搜索框是否被选中,选中则为true,未选中则为false。
  2. 在Header组件的style.js中,给搜索框的样式NavSearch增加&.focused{}样式,设置搜索框宽度拉长
  3. 在Header组件的style.js中,.iconfont{}中添加&.focused{}设置点击后背景变为灰色,图标变为白色
  4. 在组件的className中结合 三目运算符实现:根据state中 focused变量 值 改变 样式名
    • 假设点击时focused为true,则搜索框的样式对应&.focused{},图标的样式对应.iconfont{}中添加&.focused{}
    • 假设未点击时focused为false,则搜索框的样式对应空(即使用NavSearch组件原本的样式),图标的样式对应 iconfont(即原定样式)
    • 当然此时我们还没做逻辑判断,通过手动修改state中focused的值来看两种不同的效果
    • 注意:组件使用多个样式表时,使用 空格 分隔多个样式表名

代码实现

1.Header-index.js
在Header组件中创造一个构造函数,在state中保存一个变量focused表示搜索框是否被选中,选中则为true,未选中则为false:

1
2
3
4
5
6
constructor(props) {
super(props);
this.state = {
focused: false
}
}

2.Header-style.js:
给搜索框的样式NavSearch增加&.focused{}样式,设置搜索框宽度拉长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const NavSearch = styled.input.attrs({
placeholder: "搜索"
})`
width: 160px;
height: 38px;
padding: 0 30px 0 20px;
margin-top:9px;
margin-left:20px;
box-sizing:border-box;
border:none;
outline:none;
border-radius:19px;
background: #eee;
font-size: 14px;
color: #666;
&::placeholder{
color: #999;
}
&.focused{
width:240px;
}
`;

3..iconfont{}中添加&.focused{}设置点击后背景变为灰色,图标变为白色
(注意:是在.iconfont{}中添加,由于iconfont不是我们定义的组件,所以想要给她设置样式则需要通过 在iconfont的父组件中设置子组件的样式 的方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const SearchWrapper = styled.div`
position: relative;
float:left;
.iconfont{
position: absolute;
right:5px;
bottom:5px;
width:30px;
line-height:30px;
text-align:center;
border-radius:15px;
&.focused{
background: #777;
color: #fff;
}
}
`;

4.index.js:
在组件的className中结合 三目运算符实现:根据state中 focused变量 值 改变 样式名

1
2
3
4
5
6
<SearchWrapper>
<NavSearch className={this.state.focused ? "focused" : ""} />
<span className={this.state.focused ? "focused iconfont" : "iconfont"}>
&#xe62d;
</span>
</SearchWrapper>

在上面的代码中,我们假设点击时focused为true,则搜索框的样式对应&.focused{},图标的样式对应.iconfont{}中添加&.focused{}
假设未点击时focused为false,则搜索框的样式对应空(即使用NavSearch组件原本的样式),图标的样式对应 iconfont(即原定样式)。
注意:组件使用多个样式表时,使用 空格 分隔多个样式表名

5.效果展示
当然此时我们还没做逻辑判断,可以通过手动修改state中focused的值来看两种不同的效果

focused: false时:

focused: true时:


实现逻辑判断

实现效果:点击搜索框则修改state中的focused值为true,搜索框失去焦点时state中的focused值变为false。

思路

  1. 在Header组件中,给搜索框NavSearch绑定onFocus事件函数handleInputFocus(记得在构造函数中使用bind()绑定this指向)
  2. 创建handleInputFocus函数,在函数中调用setState()将focused值设为true。
  3. 给搜索框NavSearch绑定onBlur事件函数handleInputBlur(记得在构造函数中使用bind()绑定this指向)
  4. 创建handleInputBlur函数,在函数中调用setState()将focused值设为 false。

通过focused值的改变就能改变样式,但目前还没有动画效果

代码实现

Header-index.js
给搜索框NavSearch绑定onFocus事件函数handleInputFocus,以及**onBlur事件函数**handleInputBlur

1
2
3
4
5
<NavSearch
className={this.state.focused ? "focused" : ""}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
/>

记得在构造函数中使用bind()绑定this指向:

1
2
3
4
5
6
7
8
constructor(props) {
super(props);
this.state = {
focused: false
}
this.handleInputFocus = this.handleInputFocus.bind(this);
this.handleInputBlur = this.handleInputBlur.bind(this);
}

创建handleInputFocus函数,在函数中调用setState()将focused值设为true。创建handleInputBlur函数,在函数中调用setState()将focused值设为 false:

1
2
3
4
5
6
7
8
9
10
handleInputFocus() {
this.setState({
focused: true
})
}
handleInputBlur() {
this.setState({
focused: false
})
}

实现效果


实现动画效果

要实现动画效果就需要用到之前笔记提到过的**react-transition-group**(可以在github上搜索到)

**1.在项目中安装react-transition-group**:

1
yarn add react-transition-group

2.使用CSSTransition实现简单的过渡动画(可参考笔记):

header-index.js:
(复习CSS transition属性
引入CSSTransition组件:

1
import { CSSTransition } from 'react-transition-group';

使用 CSSTransition组件 包裹 NavSearch组件:

1
2
3
4
5
6
7
8
9
10
11
<CSSTransition
in={this.state.focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={this.state.focused ? "focused" : ""}
onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur}
/>
</CSSTransition>

注意:classNames属性值决定了对应的样式前缀

style.js:
由于CSSTransition依旧不是我们自定义的组件,所以想定义他的样式要通过去父组件中给子组件定义的方式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export const SearchWrapper = styled.div`
position: relative;
float:left;
// 入场动画
.slide-enter{
transition: all .2s ease-out;
}
.slide-enter-active{
width:240px;
}
// 出场动画
.slide-exit{
transition: all .2s ease-out;
}
.slide-exit-active{
width:160px;
}
.iconfont{
position: absolute;
right:5px;
bottom:5px;
width:30px;
line-height:30px;
text-align:center;
border-radius:15px;
&.focused{
background: #777;
color: #fff;
}
}
`;

优化代码

动画效果的样式是针对NavSearch组件的,可我们上面把他们放在了CSSTransition的父组件SearchWrapper的样式中,这样虽然执行没问题,但我们应该将它们放到NavSearch组件的样式中会更好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export const NavSearch = styled.input.attrs({
placeholder: "搜索"
})`
width: 160px;
height: 38px;
padding: 0 30px 0 20px;
margin-top:9px;
margin-left:20px;
box-sizing:border-box;
border:none;
outline:none;
border-radius:19px;
background: #eee;
font-size: 14px;
color: #666;
&::placeholder{
color: #999;
}
&.focused{
width:240px;
}
// 入场动画
&.slide-enter{
transition: all .2s ease-out;
}
&.slide-enter-active{
width:240px;
}
// 出场动画
&.slide-exit{
transition: all .2s ease-out;
}
&.slide-exit-active{
width:160px;
}
`;

注意:在这里动画样式是针对NavSearch组件的,不再是针对子组件,所以要加上&表示当前组件的动画样式

复习:
&表示当前元素,例如:

1
2
3
a{
&.b {}
}

编译之后就是a.b{}
没有&就成了a .b(中间有个空格,b这个class成了a的后代元素的了)

,