# 开始做一个 React 项目叭 —— 历程记录

有几次 vue 项目经历,尝试写个 react 小项目(比着跟官方教程写的 tictactoe 抄抄就行了),在此做点记录。

来自字学镜像计划,最终代码存在 github:ERUIHNIYHBKBNF/react-juejin

# 初始化

按官方文档来,创建一个新的单页应用。

npx create-react-app react-juejin
cd react-juejin
npm start

然后一堆奇奇怪怪看不懂的文件全删掉(逃),最后留一点变成这样就好了叭:

# 添加路由

看文档之前一定先看好版本 QAQ

看上去还不错的一篇文章:React Router v6 使用指南

抄一下官方的 demo:Basic Example

看起来只有 文章列表 和 文章 两个页面,不是很麻烦的样子 qwq

npm install -s react-router-dom@6

然后用 Router 就相当于一个组件一样方便使用,具体去翻文档就好了唔。

index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

App.js:

import { Routes, Route } from 'react-router-dom';
import ArticleList from './articleList';
import Article from './article';
export default function App() {
  return (
    <Routes>
      <Route path="/" element={<ArticleList />} />
      <Route path="/article" element={<Article />} />
    </Routes>
  );
}

然后写写 articleList 和 article 两个组件的初始化:

import React from "react";
export default class ArticleList extends React.Component {
  render() {
    return (
      <div>ArticleList</div>
    );
  }
}

可以正常访问,然后开始撸页面就好了唔。

顺带现在长这个样子:<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/frontend/2022020703.webp" width="200px">

# 开始撸页面

# 使用 css

阮一峰 - CSS Modules 用法教程

CSS 的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。

产生局部作用域的唯一方法,就是使用一个独一无二的 class 的名字,不会与其他选择器重名。这就是 CSS Modules 的做法。

算了算了,反正也不急着弄明白啥意思(赶紧水完项目要紧)简单来说,局部样式和全局样式分别这样用:

  • 局部
    • 命名: style.module.css
    • 引入: import style from 'style.module.css'
    • 使用: <div className={style[className]}>
  • 全局
    • 命名: style.css
    • 引入: import 'style.css'
    • 使用: <div className='className'>

顺带用一下 scss,至少能嵌套一下就很方便 qwq

npm install -d node-sass

于是大部分就这样子写了:

import React from "react";
import style from "../style.module.scss";
export default class Bottom extends React.Component {
  render() {
    return (
      <div className={ style['bottom'] }>
        <ul>
            <li>最新</li>
            <li>热门</li>
            <li>历史</li>
        </ul>
      </div>
    );
  }
}

# 基础页面绘制

写过一堆 css 之后变成了这个样子:<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/frontend/2022020801.webp" width="200px">

<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/frontend/2022020802.webp" width="1000px">

# 文章列表相关动态操作

# 切换 tabs

这里所有 tab 的状态统一存到了公共的父组件 /articleList/index.js 里面,仍然以最底部的导航栏为例,父组件的 state 里存两个东西:

bottomTabs: ['热门', '最新', '历史'], activeBottomTab: 0,

分别表示都有哪些 tabs 和当前点击的 tab 是哪个,作为 props 传给底部导航栏组件处理即可,顺带父组件的 changeTab 事件也一并作为 props 传过去便于切换。

index.js:

<Bottom
  tabs={ this.state.bottomTabs }
  activeTab={ this.state.activeBottomTab }
  changeTab={ this.changeBottomTab }
/>

bottom.js:

import React from "react";
import style from "../style.module.scss";
export default class Bottom extends React.Component {
  render() {
    return (
      <div className={style['bottom']}>
        <ul>
          {this.props.tabs.map((item, index) => (
            <li
              onClick={ () => this.props.changeTab(index) }
              // 为选中的组件动态绑定蓝色特效
              className={ this.props.activeTab === index ? style['active-tab'] : '' }
              key={ index }
            >
              {item}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

# 动态渲染文章预览

其实跟 tabs 差不多,仍然是父组件 /articleList/index.js 去存储文章列表,把文章列表传给了 body.js 也就是页面主体部分,再由其分别传给每个小卡片。

感觉这样安排也许会有些不合理的地方,不过主要考虑是,接口获取文章列表也需要当前各种 tabs 的选中情况,而这些状态都存在了 index.js 里,所以为了对接口方便只好这样安排了 qwq

# 对接 fake-api 动态获取数据

因为是假的接口不需要网络请求,写起来就非常简单了,直接参考 fake-api/index.js 里的注释就好:

/articleList/index.js:

//...
import { getCategories, getArticles } from "../fake-api";
//...
export default class ArticleList extends React.Component {
  constructor(props) { /*......*/ }
  componentDidMount() {
    this.fetchCategories();
    this.fetchArticles();
  }
  async fetchCategories() { /*......*/ }
  async fetchArticles() { /*......*/ }
  //...
}

这样看上去就比较像样子了。

<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/frontend/2022020803.webp" width="600px">

关于无限下拉列表,跟上面提到的一样,这里父子组件关系安排有点乱,不如记录文章评论那里的无限下拉列表比较方便 qwq

# 文章内容相关动态操作

# 路由跳转

一种方式:

// routes
import { Routes, Route } from 'react-router-dom';
<Routes>
  <Route path="/" element={<ArticleList />} />
  <Route path="/article/:id" element={<Article />} />
</Routes>
// link
import { Link } from "react-router-dom";
<Link
  style=<!--swig0-->
  to={ '/article/' + item.article_id }
>
</Link>
// 获取参数
import { useParams } from "react-router-dom";
const { id } = useParams();

# 组件功能划分

文件结构长这样:<img src="https://cdn.jsdelivr.net/gh/ERUIHNIYHBKBNF/picapica@main/frontend/2022020804.webp" width="250px">

其中 index.js 负责从路由获取文章 id,分别传给文章主体部分 body.js 和评论部分 comments.js ,相关内容由这两个组件分别获取。

文章接口可以直接获取 html 代码,无视 xss 使用 <div dangerouslySetInnerHTML={{__html: this.state.article }}/> 直接嵌入即可。

# 评论区无限下拉实现

实现思路主要是这些:

  • 设定好一个 加载中 元素始终位于最底部。
  • 使用 IntersectionObserver 监听这个 加载中 元素与视窗的重合部分。
  • 到达一定重合面积(也就是当用户能看到 "加载中" 这三个字)时,获取新的数据, offset 就是本地已获取的数据列表长度。
  • 接口提供总数据量 total ,当本地已获取数据列表长度与之相等时,代表所有数据已经加载完毕。
  • 之后清除 observer 并将 加载中 替换为 没有更多了

也许整个组件的代码都可以丢这里占篇幅 QwQ

comments.js:

import React from "react";
import style from "../style.module.scss";
import { getCommentsByArticleId } from "../../fake-api";
export default class Comments extends React.Component {
  constructor(props) {
    super(props);
    this.loading = React.createRef();
    this.state = {
      comments: [],
      observer: null,
      total: 0,
    }
  }
  componentDidMount() {
    // 创建 observer,重合 10% 时开始获取新的数据。
    const observer = new IntersectionObserver(
      () => {
        this.fetchComments();
      },
      {
        threshold: 0.1,
      }
    );
    observer.observe(this.loading.current);
    this.fetchComments();
  }
  // 获取新的数据
  async fetchComments() {
    const response = await getCommentsByArticleId(this.props.id, this.state.comments.length);
    let comments = this.state.comments;
    comments = comments.concat(response.data.comments);
    this.setState({
      comments,
      total: response.total,
    });
  }
  renderCommentCard(comment) {
    /* 渲染一个评论卡片 */
  }
  render() {
    return (
      <div className={style['comments']}>
        <ul>
          { this.state.comments.map(this.renderCommentCard) }
        </ul>
          {/* 这个 span 就是实现无限下拉的关键元素 */}
          <span ref={this.loading} className={ style['loading'] }>
            { this.state.total === this.state.comments.length ? '没有更多了嘤~' : '加载中...' }
          </span>
      </div>
    );
  }
}

# 历史记录

简单粗暴,在用户点击一篇文章时直接存 localStorage 即可:

这里除了暴力没有想到什么比较好的去重方法,所以直接懒得写没有写去重 qwq

addToHistory(articleInfo) {
  const history = localStorage.getItem('historyArticles');
  if (history) {
    const historyArticles = JSON.parse(history);
    historyArticles.unshift(articleInfo);
    localStorage.setItem('historyArticles', JSON.stringify(historyArticles));
  } else {
    localStorage.setItem('historyArticles', JSON.stringify([articleInfo]));
  }
}

在获取文章时,先检查当前选中的 tab 是否为 历史 ,如果是,则文章列表直接在 localStorage 中获取:

async fetchArticles() {
  if (this.state.activeBottomTab === 2) {
    this.fetchHistoryArticles();
  } else {
	/*......*/
  }
}
fetchHistoryArticles() {
  const history = localStorage.getItem('historyArticles');
  // 是否有历史记录
  if (history) {
    const historyArticles = JSON.parse(history);
    this.setState({
      articleList: historyArticles,
      totalArticles: historyArticles.length,
    });
  } else {
    this.setState({
      articleList: [],
      totalArticles: 0,
    });
  }
}

# 总结

唔姆唔姆,不知道总结什么,但总该有个结尾 qwq

普普通通的一个项目叭,算是第一次体验了一下使用 react 进行开发~~(tictactoe 不算)~~,用起来还挺舒服的。