0
点赞
收藏
分享

微信扫一扫

05.简书项目实战二:首页开发

yeamy 2022-05-02 阅读 33

简书项目实战二:首页开发

本文是 React 的仿写简书项目开发学习笔记。代码仓库:https://github.com/shijuhong/jianshu

1. React 路由

路由就是根据 url 的不同来显示不同的内容。

安装 react 路由

yarn add react-router-dom

创建 pages 文件夹来放页面。然后创建两个文件夹,见名知意,Home 文件夹和 Detail 文件夹。然后写最基础的类组件。

现在想要的效果是,根据不同的 url 来分别加载 Home 和 Detail 组件。

// src/App.js
function App() {
  return (
    <Provider store={store}>
      <Header />
      <BrowserRouter>
        <Routes>
          <Route path="/" exact element={<Home />}></Route>
          <Route path="/detail" exact element={<Detail />}></Route>
        </Routes>
      </BrowserRouter>
    </Provider>
  );
}

注意添加 exact,只有 url 完全对上了才会对相应的组件进行加载。否则,/detail 对于 Home 来说,也是可以匹配上的,没有 exact 就会加载两个组件。

效果如下:

2. 首页组件的拆分

去简书看一下规划,可以看到首页有两栏,所以用 HomeLeft 和 HomeRight 拆分,然后 HomeWrapper 包裹 HomeLeft 和 HomeRight。

前端组件化的意义:如果把所有的内容写在 index.js 文件里,内容将会特别多。一个大的页面拆分成一个个的小部分,每个部分都是一个组件,组件自己维护自己的内容和交互逻辑。

组件拆分的粒度需要把控好!

拆分后,左侧除了图片,还有专题推荐 Topic 和文章列表 List,右侧有热门推荐 Recommend 和推荐作者 Writer。

export default class Home extends Component {
  render() {
    return (
      <HomeWrapper>
        <HomeLeft>
          <img
            className="banner-img"
            src="https://upload.jianshu.io/admin_banners/web_images/5055/348f9e194f4062a17f587e2963b7feb0b0a5a982.png?imageMogr2/auto-orient/strip|imageView2/1/w/1250/h/540"
            alt=""
          />
          <Topic />
          <List />
        </HomeLeft>
        <HomeRight>
          <Recommend />
          <Writer />
        </HomeRight>
      </HomeWrapper>
    );
  }
}

3. 首页专题 Topic 区域布局及 reducer 的设计

代码结构如下:

export default class Topic extends Component {
  render() {
    return (
      <TopicWrapper>
        <TopicItem>
          <img
            className="topic-pic"
            src="xxx"
            alt=""
          />
          社会热点
        </TopicItem>
......

每个 TopicItem 代表每个专题,里边包裹图片和专题名称。

但是图片的 src 和专题名称都不是写死的,所以需要使用 ajax 获取数据,用循环来生成 TopicItem,数据用 store 进行管理。

// Home/components/Topic.js
      <TopicWrapper>
        {this.props.topicList.map((item) => (
          <TopicItem key={item.get("id")}>
            <img className="topic-pic" src={item.get("imgUrl")} alt="" />
            {item.get("title")}
          </TopicItem>
        ))}
      </TopicWrapper>

显示效果:

4. 首页文章列表 List 制作

同样是用 redux 管理数据,然后用 map 来遍历渲染。

代码结构如下:

// components/List.js
      <Fragment>
        {this.props.articleList.map((item) => (
          <ListItem key={item.get("id")}>
            <img className="pic" src={item.get("imgUrl")} alt="" />
            <ListInfo>
              <h3 className="title">{item.get("title")}</h3>
              <p className="desc">{item.get("desc")}</p>
            </ListInfo>
          </ListItem>
        ))}
      </Fragment>

显示效果:

图片没显示是因为没权限获取简书上的图片。后面自己找些图片换一下就行,详细文字离图片也太近了,稍微改改。

5. 首页推荐部分 Recommend 代码编写

代码结构:

// components/Recommend.js
class Recommend extends Component {
  render() {
    const { recommendList } = this.props;
    return (
      <RecommendWrapper>
        {recommendList.map((item) => (
          <RecommendItem key={item.get("id")} imgUrl={item.get("imgUrl")} />
        ))}
      </RecommendWrapper>
    );
  }
}

显示效果:

难点:

因为每个组件对应了不同 url 的背景图片,因此需要将 url 传入 RecommendItem 组件。

styled-components 接收参数的方法见下面的代码:

// Home/style.js
export const RecommendItem = styled.div`
  width: 280px;
  height: 50px;
  background: url(${(props) => props.imgUrl});
  background-size: contain;
`;

6. 推荐作者 Writer 模块编写

代码结构:

class Writer extends Component {
  render() {
    const { writerList } = this.props;
    return (
      <WriterWrapper>
        {writerList.map((item) => (
          <WriterItem key={item.get("id")}>
            <img className="pic" src={item.get("imgUrl")} alt="" />
            <div className="follow">关注</div>
            <span>{item.get("name")}</span>
            <p>
              写了{item.get("wordCount")}k字 · {item.get("likeCount")}k喜欢
            </p>
          </WriterItem>
        ))}
      </WriterWrapper>
    );
  }
}

显示效果:

没啥好说的,套路和上面的都一样。

7. 首页异步数据获取

上面的数据,需要从后端获得。一样的,咱们自己造假数据来创建接口。

在 public 文件夹里添加 home.json 文件,里面提供 ajax 返回的数据。在加载首页挂载成功的时候获取到数据并 dispatch action 来更新 store 里的 state 即可。

// Home/index.js
  componentDidMount() {
    const { changeHomeData } = this.props;
    axios.get("/api/home.json").then((res) => {
      const result = res.data.data;
      const action = {
        type: "change_home_data",
        topicList: result.topicList,
        articleList: result.articleList,
        recommendList: result.recommendList,
        writerList: result.writerList,
      };
      changeHomeData(action);
    });
  }

const mapDispatch = (dispatch) => ({
  changeHomeDate(action) {
    dispatch(action);
  },
});

export default connect(null, mapDispatch)(Home);
// Home/store/reducer.js
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case "change_home_data":
      return state.merge({
        topicList: fromJS(action.topicList),
        articleList: fromJS(action.articleList),
        recommendList: fromJS(action.recommendList),
        writerList: fromJS(action.writerList),
      });
    default:
      return state;
  }
};

写这两步后,主页便可拿到 json 数据。功能实现后,后边就需要代码优化了。ajax 请求通过 redux-thunk 在actionCreators 里执行,action 和 action types 要分别转移到 actionCreators 和 constants。

优化后:

// Home/index.js
  componentDidMount() {
    const { changeHomeData } = this.props;
    changeHomeData();
  }
}

const mapDispatch = (dispatch) => ({
  changeHomeData() {
    dispatch(actionCreators.getHomeInfo());
  },
});
// Home/store/actionCreator.js
const changeHomeData = (result) => ({
  type: constants.CHANGE_HOME_DATA,
  topicList: result.topicList,
  articleList: result.articleList,
  recommendList: result.recommendList,
  writerList: result.writerList,
});

export const getHomeInfo = () => {
  return (dispatch) => {
    axios.get("/api/home.json").then((res) => {
      const result = res.data.data;
      dispatch(changeHomeData(result));
    });
  };
};

8. 实现加载更多功能

文章列表的内容并不是一股全加载出来的,如果有 100 篇文章,一股脑加载出来,没必要的同时还增加了服务器压力。因此”阅读更多“按钮是很有必要的。

首先,还是一样的,模拟接口,然后点击”阅读更多“触发事件派发 action,然后在 actionCreators 里进行 ajax 请求后,派发 action 给 reducer。

// components/List.js
class List extends Component {
  render() {
    const { articleList, getMoreList } = this.props;
    return (
      <Fragment>
        {articleList.map((item) => (
          <ListItem key={item.get("id")}>
            <img className="pic" src={item.get("imgUrl")} alt="" />
            <ListInfo>
              <h3 className="title">{item.get("title")}</h3>
              <p className="desc">{item.get("desc")}</p>
            </ListInfo>
          </ListItem>
        ))}
        <LoadMore onClick={getMoreList}>阅读更多</LoadMore>
      </Fragment>
    );
  }
}

const mapState = (state) => ({
  articleList: state.getIn(["home", "articleList"]),
});

const mapDispatch = (dispatch) => ({
  getMoreList() {
    dispatch(actionCreators.getMoreList());
  }
})

export default connect(mapState, mapDispatch)(List);
// Home/store/actionCreators.js
const addHomeList = (list) => ({
  type: constants.ADD_ARTICLE_LIST,
  list: fromJS(list),
});

export const getMoreList = () => {
  return (dispatch) => {
    axios.get("/api/homeList.json").then((res) => {
      const result = res.data.data;
      dispatch(addHomeList(result));
    });
  };
};
// Home/store/reducer.js
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    ......
    case constants.ADD_ARTICLE_LIST:
      return state.set(
        "articleList",
        state.get("articleList").concat(action.list)
      );
    default:
      return state;
  }
};

上面已经实现了点击”阅读更多“后,将所有的内容加载出来的功能,但是这里还是有问题,”阅读更多“点击后不应该将所有的数据加载出来,而是每次加载一点点而且每次加载不一样。

其实,阅读更多和下一页是一个意思。store 里添加 articlePage 属性,用来记录当前点击了几次”阅读更多“,或者讲,记录翻到了第几页,记录在 url 里,然后和后端进行沟通即可。

// components/List.js
class List extends Component {
  render() {
    const { articleList, articlePage, getMoreList } = this.props;
    return (
      <Fragment>
        {articleList.map((item) => (
          <ListItem key={item.get("id")}>
            <img className="pic" src={item.get("imgUrl")} alt="" />
            <ListInfo>
              <h3 className="title">{item.get("title")}</h3>
              <p className="desc">{item.get("desc")}</p>
            </ListInfo>
          </ListItem>
        ))}
        <LoadMore onClick={() => getMoreList(articlePage)}>阅读更多</LoadMore>
      </Fragment>
    );
  }
}

const mapState = (state) => ({
  articleList: state.getIn(["home", "articleList"]),
  articlePage: state.getIn(["home", "articlePage"]),
});

const mapDispatch = (dispatch) => ({
  getMoreList(page) {
    dispatch(actionCreators.getMoreList(page));
  },
});

export default connect(mapState, mapDispatch)(List);
// Home/store/acitonCreators.js
const addHomeList = (list, nextPage) => ({
  type: constants.ADD_ARTICLE_LIST,
  list: fromJS(list),
  nextPage,
});

export const getMoreList = (page) => {
  return (dispatch) => {
    axios.get(`/api/homeList.json?page=${page}`).then((res) => {
      const result = res.data.data;
      dispatch(addHomeList(result, page + 1));
    });
  };
};
// Home/store/reducer.js
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case constants.CHANGE_HOME_DATA:
      ......
    case constants.ADD_ARTICLE_LIST:
      return state.merge({
        articleList: state.get("articleList").concat(action.list),
        articlePage: action.nextPage,
      });
    default:
      return state;
  }
};

9. 返回顶部功能实现

返回顶部是很多网页都会提供的功能。这个功能的注意点就只有 position: fixed,点击的时候触发 window.scrollTo(0, 0); 即可。

拓展:当滚动条往下拉到一定程度才显示”返回顶部"按钮,这个功能需要通过变量来控制“返回顶部”的显示或隐藏。此时,在 store 里增添 showScroll 属性,初始值为 false。当 showScroll 变为 true 的时候,“返回顶部”按钮才会展示。

// Home/index.js
class Home extends Component {
  handleScrollTop() {
    window.scrollTo(0, 0);
  }

  render() {
    const { showScroll } = this.props;
    return (
      <HomeWrapper>
        <HomeLeft>
          <img
            className="banner-img"
            src="https://upload.jianshu.io/admin_banners/web_images/5055/348f9e194f4062a17f587e2963b7feb0b0a5a982.png?imageMogr2/auto-orient/strip|imageView2/1/w/1250/h/540"
            alt=""
          />
          <Topic />
          <List />
        </HomeLeft>
        <HomeRight>
          <Recommend />
          <Writer />
        </HomeRight>
        {showScroll && (
          <BackTop onClick={this.handleScrollTop}>回到顶部</BackTop>
        )}
      </HomeWrapper>
    );
  }
  componentDidMount() {
    ......
    this.bindEvents();
  }

  componentWillUnmount() {
    // 组件销毁之前,事件要解绑
    const { changeScrollTopShow } = this.props;
    window.removeEventListener("scroll", changeScrollTopShow);
  }

  bindEvents() {
    const { changeScrollTopShow } = this.props;
    window.addEventListener("scroll", changeScrollTopShow);
  }
}

const mapState = (state) => ({
  showScroll: state.getIn(["home", "showScroll"]),
});

const mapDispatch = (dispatch) => ({
  ......
  changeScrollTopShow() {
    if (document.documentElement.scrollTop > 400) {
      dispatch(actionCreators.toggleTopShow(true));
    } else {
      dispatch(actionCreators.toggleTopShow(false));
    }
  },
});

export default connect(mapState, mapDispatch)(Home);

action 派发给 reducer 处理即可。

10. 首页性能优化及路由跳转

  1. SCU (ShouldComponentUpdate) 优化

    首页的数据用了 connect 和 store 连接,一旦 store 里数据进行改变后,首页的子组件都会被重新渲染,即 render 重新执行,不管更改的数据和子组件有没有关系,这样会使性能较低。

    因此就在 shouldComponentUpdate 生命周期函数里判断更改的数据是否与该组件相关。不相关的话返回 false,就不会触发更新了。

    当然,react 内置了一种组件类型 PureComponent,内置了浅比较的 SCU。

  2. 防抖节流

    进度条滚动就触发 scroll 事件,频率非常高,但是并没有必要那么频繁触发事件,如果每隔 0.5 秒触发一次,这已经足够了。

    使用 lodash 模块的 debounce 和 throttle,可以方便地实现防抖和节流。

      componentWillUnmount() {
        // 组件销毁之前,事件要解绑
        const { changeScrollTopShow } = this.props;
        window.removeEventListener("scroll", _.throttle(changeScrollTopShow, 500));
      }
    
      bindEvents() {
        const { changeScrollTopShow } = this.props;
        window.addEventListener("scroll", _.throttle(changeScrollTopShow, 500));
      }
    
  3. 路由跳转

    使用 Link 标签(react-router 提供)来实现路由跳转。可以把 Link 想象成高级的 a 标签,但是是专供给单页面使用,但是 a 标签会导致另起一页,组件都重新加载了。

    具体用法:

    // Home/components/List.js
    <Link key={item.get("id")} 
      // Link 标签内容
    </Link>
    
举报

相关推荐

0 条评论