简书项目复刻问题集合
徐徐 抱歉选手

iconfont

iconfont也就是把icon当font来用,通过自定义font-face实现。线上有许多iconfont库可供使用。

一般iconfont解压缩后的文件夹都会放在整个项目的静态资源文件夹中,即src文件夹下的statics中,这涉及到了webpack在生产环境下打包静态资源的问题,与其他图片字体的引用一致,具体参考李熠 从React脚手架工具学习React项目的最佳实践(上):前端基础配置

阿里图标库

  • 在线链接引入
  • 下载项目到本地

需要注意的是一次性最好把想要添加的图标全罗列进去,否则以后再添加还需要重新替换本地文件。

使用

由于历史因素使用i标签容纳无语义图标,但是自己在用的时候使用了span元素。

iconfont本意是将icon当作font来使用,所以也可以像字体一样设置颜色color,大小font-size。

需要注意的是下载的icon的空白部分是透明的还是有背景白色的;也就是要注意边框与实体。

image-20201202174727098

icon+a

当iconfont和超链接a href一起使用的时候,一定要先给a元素width和height,否则a元素就是个没有宽高的空元素,显示不出来。

文字对齐

这里的文字除了一般文字,还包括iconfont。

以想要对齐的对象为基准。它的父元素,需要设置text-align;它本身,可能需要设置vertical-align。

盒子性质

由最基础的inline,block,flex,table衍生出了各种混合属性。这些基本盒子性质都是针对于盒子本身及其内容来说的。比如inline的性质就是内容决定盒子,padding和margin都不可以设置,通过设定内容的大小来决定盒子的大小;block的性质就是盒子就是一整行,通过设定盒子本身的大小以及盒子的margin和padding来决定盒子的大小。

而混合属性,比如inline-block,这个属性inline性质代表的是这个元素在上下文中是inline的性质,他不会占据一整行,可以和其他元素一起排列在一行;block性质代表的是这个元素对于自己内部的内容是block性质,可以设置padding,margin等属性把盒子撑开来。

从树的角度来理解,基础属性就只是从父节点出发去审视子节点,但是混合属性就设计了从该节点去向上看父节点,向下看子节点。我们在谈盒子性质,其实是在谈两个元素/标签之间的关系。

定位方式

此处视角以我们想要让它的位置从正常文档流中脱离的对象来看。应当涉及元素本身和元素的父元素。

absolute+float

第一步: 在父元素上使用position: absolute,同时确定top bottom left right等值。明确盒子性质最基础的inline/block还是较为复杂的inline-block/flex,以及盒子的属性/box-sizing。

第二步: 在元素上使用float属性。如果该元素和他的相邻的元素之间想要同在一行,那么他们之间是inline的关系;如果该元素和他的相邻元素不想在同一行,那么他们之间是block关系。

第三步: 如有必要,清除该浮动元素对后续元素的影响,使用clear。

flex布局

第一步: 在父元素上使用display: flex,决定是flex-direction是row还是columnn,是wrap还是nowrap,对于给定width和height的子元素使用align-items和vertical-align进行对齐。

第二步: 在每个子元素上使用flex: XX,这里的单位可以是百分比也可以是数字,用来说明子元素占用父元素的份额。子元素如果有内容的话(其是相对来说该内容也像子元素),还需要自己设置内容居中。

关于flex布局中flex父元素的子元素定位的问题,好像align-items和vertical-align只支持头尾中这些方式,如果想要特定高度的话,依据百分比自动调节的话还是需要传统定位方式。参考react简书项目login页面的下载app这个flex子元素。

0或负

span的图标想放到block的元素内部去,可以先将span设置为inline/inline-block,然后把margin取负值,至于是左还是右看需要。

image-20201202223902177image-20201202223931227image-20201202224019977

bottom为0似乎也能实现底部效果。

去除浏览器默认样式

一般会在src下创建一个全局样式表,是为了消除浏览器客户端的user agent style。要将这个样式添加在APP组件中,然后在index.js中去渲染。

styled-component使用

教程Reference

#2: Why I prefer Styled components to build React apps

官方文档

中文文档

styled-components:一本通

react-router-dom的Link样式

因为Link标签最终还是被解析为a标签,所以会自带下划线和点击过后的变蓝色,将Link样式化后变成StyledLink使用。

1
2
3
4
5
6
7
8
9
import { Link } from 'react-router-dom';
import styled from 'styled-components';

const StyledLink = styled(Link)`
text-decoration: none;
&:focus, &:hover, &:visited, &:link, &:active {
text-decoration: none;
}
`;

需要记住a标签与Link标签本质上是行内元素,对他设置margin和padding都没有用,通过给a标签与Link标签添加display: block使它变成块级元素后才能实现padding和margin。

待解决的问题

代码优化与简洁。一开始创建主要是styled的基础使用与模版字符串,至于嵌套等高级性质还没有看,代码较为冗长。

数据流管理

slice书写规范

如果不使用createEntityAdapter来管理store中的数据,而选择手写数据的话,需要注意数据本身一定是数组而不是对象构成的,所以在对store中数据修改的时候才能用concat/push这一类的方法。由于数据本身可能还需要描述数据状态的元数据,所以就多了其他的field,从而构成了一个对象。

使用createSlice API自动创建action以及action creators一定会有三个name: '', initialState, reducers:{}这三个field,而extraReducers:{} 存在与否取决于slice本身以及slice负责的UI是否有异步数据管理的需求。


reducer field中的数据一定要注意的是:

  • 这里的state是指这个slice所管理的state,而不是store全体。
  • reducer对象中的每一个函数其实都是对应该action/action creator的回调函数,一定要弄明白是在原state的基础之上增删改,还是通过直接return语句覆盖原state。在initialState定义为{posts: ‘’, id: ‘’, desc: ‘’}对象的情况下,直接的赋值语句state.posts = action.payload会在state对象下重新生成一了posts field来访数据,而不会覆盖初始initialState(当时因为这个调试了好久!)
  • reducer对象中的回调函数一定要export对应的slice.actions。
  • reducer对象本身一定也要export default slice到store中去生成全体store。

从数据store中随机选取数据

对应到项目中就是推荐作者一栏的换一换,采用的是洗牌算法,对store中的所有数据进行洗牌操作,然后选取(0,5)即可,也可以生成一个随机数,然后加上5。

采用Knuth-Durstenfeld Shuffle,每次从未处理的数组中随机取一个元素,然后把该元素放到数组的尾部,即数组的尾部放的就是已经处理过的元素

算法步骤:

  1. 选取数组(长度n)中最后一个元素(arr[length-1]),将其与n个元素中的任意一个交换,此时最后一个元素已经确定
  2. 选取倒数第二个元素(arr[length-2]),将其与n-1个元素中的任意一个交换
  3. 重复第 1 2 步,直到剩下1个元素为止

es6实现:

1
2
3
4
5
6
7
8
function shuffle(arr){
let n = arr.length, random;
while(0!=n){
random = (Math.random() * n--) >>> 0; // 无符号右移位运算符向下取整
[arr[n], arr[random]] = [arr[random], arr[n]] // ES6的结构赋值实现变量互换
}
return arr;
}

异步数据管理

本项目因为使用了Redux Toolkit,所以在操作异步数据的时候会使用createAsyncThunk API,结合axios一起使用的话流程如下:

在slice中定义异步操作:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
// productSlice.js
import { createSlice,createSelector,PayloadAction,createAsyncThunk,} from "@reduxjs/toolkit";
export const fetchProducts = createAsyncThunk(
"products/fetchProducts", async (_, thunkAPI) => {
try {
//const response = await fetch(`url`); //where you want to fetch data
//Your Axios code part.
const response = await axios.get(`url`);//where you want to fetch data
return await response.json();
} catch (error) {
return thunkAPI.rejectWithValue({ error: error.message });
}
});

const productsSlice = createSlice({
name: "products",
initialState: {
products: [],
loading: "idle",
error: "",
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchProducts.pending, (state) => {
state. products = [];
state.loading = "loading";
});
builder.addCase(
fetchProducts.fulfilled, (state, { payload }) => {
state. products = payload;
state.loading = "loaded";
});
builder.addCase(
fetchProducts.rejected,(state, action) => {
state.loading = "error";
state.error = action.error.message;
});
}
});


export const selectProducts = createSelector(
(state) => ({
products: state.products,
loading: state.products.loading,
}), (state) => state
);

export default productsSlice;

在component中使用异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
import { useSelector, useDispatch } from "react-redux";

import {
fetchProducts,
selectProducts,
} from "path/productSlice.js";

const dispatch = useDispatch();
const { products } = useSelector(selectProducts);
React.useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);

这里的定义与使用分别在slice和component中是因为异步数据的产生是在server端的,也就是我们要从服务器,或者在本项目中是提前准备好的public文件下的mock数据。如果异步数据的产生是通过用于与UI交互生成的,那么createAsyncThunk的生成就应该在UI中进行。

代码来源StackOverflow- redux toolkit and axios

路由管理

两个基本模式

路由管理采用react-router-dom,基本模式有两个。

第一个模式就是在App顶层组件中使用BrowseRouter包裹Switch再包裹Router与Redirect。一般Router中会有参数传递。

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
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
} from 'react-router-dom'

function App() {
return (
<Router>
<Switch>
<Route exact path="/"
render={() => (
<React.Fragment>
// 此处可以多个组件组合,但是不可传递参数
</React.Fragment>
)}
/>
<Route exact path="/" component={} />
<Redirect to="/" />
</Switch>
</Router>
)
}

export default App

第二个模式就是在组件内部要进行跳转,使用Link标签包括所有能够通过点击到达另一个页面的内容,记得传递参数。

1
2
// 某个UI的return部分
<Link to={`/posts/${post.id}`} className="button muted-button"> View Post</Link>

参数的传递与获取

Router的参数传递与Link的参数传递有所不同。

1
2
3
<Route exact path="/posts/:postId" component={SinglePostPage} />

<Link to={`/posts/${post.id}`} className="button muted-button"> View Post</Link>

但是对接受这些参数传递的页面的组件来说,处理方式为:

1
2
3
4
export const SinglePostPage = ( { match }) => {
//这里用花括号是因为match是一个对象,包含URL
const { postId } = match.params
}

path和to指向的连接整体会作为一个对象传入UI的函数组件中作为参数,为了提取其中的参数,使用url.params方法,注意这里返回的对象都是字符串类型的。

一般得到链接里的参数后,就会将这个参数作为一个搜寻条件去匹配redux store中的所有数据。这里需要注意,如果redux中的数据id是number类型,而恰巧在useSelector中又使用了全等===符号,那么最后选择出来的结果绝对是undefined。所以一定要注意url参数和redux store的数据类型匹配

1
2
3
4
5
6
// UI中选择id匹配的数据
const post = useSelector(state => selectPostById(state,postId)

// slice文件
export const selectPostById = (state, postId) => state.posts.posts.find(post => post.id === postId)
// 这写slector中作为参数的state是root redux object,因为这个函数最终是要传入useSelector的,而useSelector的作用对象是全体store。
  • 本文标题:简书项目复刻问题集合
  • 本文作者:徐徐
  • 创建时间:2020-12-02 17:37:55
  • 本文链接:https://machacroissant.github.io/2020/12/02/react-project-summary/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论