06.简书项目实战三:详情页面和登录功能实现

简书项目实战三:详情页面和登录功能实现

1. 详情页面布局

这部分的布局比之前的简单多了,就一个标题加上主要内容而已。

export default class Detail extends Component {
  render() {
    return (
      <DetailWrapper>
        <Header>你听过婚姻式扶贫吗</Header>
        <Content>
          内容
        </Content>
      </DetailWrapper>
    );
  }
}

2. 使用 redux 管理详情页面数据

当然,标题、图片、文章内容并不是写死的,而是放到 redux store 里进行管理。因此根 reducer 组合详情页面的 reducer 后,创建详情页面的 redux 相关文件 (和前两部分的构建方法一样)。

import React, { Component } from "react";
import { connect } from "react-redux";
import { Content, DetailWrapper, Header } from "./style";

class Detail extends Component {
  render() {
    const { title, content } = this.props;
    return (
      <DetailWrapper>
        <Header>{title}</Header>
        <Content>{content}</Content>
      </DetailWrapper>
    );
  }
}

const mapState = (state) => ({
  title: state.getIn(["detail", "title"]),
  content: state.getIn(["detail", "content"]),
});

export default connect(mapState)(Detail);

显示效果:

不对呀,怎么转义了?因为安全问题,react 帮忙做了转义。如果想要标签实现应有的效果而不是被转义,需要改成这种写法:

<Content dangerouslySetInnerHTML={{ __html: content }} />

改完就正常了。

3. 异步获取数据

刚才的展示,数据都是放在 store 里的,相当于写死了。正常情况下,应该是通过接口获取数据。

获取流程:通过 componentDidMount 生命周期函数在组件挂载的时候,通过 ajax 获取数据后存储到 store 里。

// Detail/index.js
class Detail extends Component {
  render() {
    const { title, content } = this.props;
    return (
      <DetailWrapper>
        <Header>{title}</Header>
        <Content dangerouslySetInnerHTML={{ __html: content }} />
      </DetailWrapper>
    );
  }

  componentDidMount() {
    const { getDetail } = this.props;
    getDetail();
  }
}

const mapState = (state) => ({
  title: state.getIn(["detail", "title"]),
  content: state.getIn(["detail", "content"]),
});

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

export default connect(mapState, mapDispatch)(Detail);
// Detail/store/actionCreators.js
const changeDetail = (title, content) => ({
  type: constants.CHANGE_DETAIL,
  title,
  content,
});

export const getDetail = () => {
  return (dispatch) => {
    axios.get("/api/detail.json").then((res) => {
      const result = res.data.data;
      dispatch(changeDetail(result.title, result.content));
    });
  };
};
// Detial/store/reducer.js
const defaultState = fromJS({
  title: "",
  content: "",
});

const changeDetail = (state, action) =>
  state.merge({
    title: fromJS(action.title),
    content: fromJS(action.content),
  });

const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case constants.CHANGE_DETAIL:
      return changeDetail(state, action);
    default:
      return state;
  }
};

export default reducer;

4. 页面路由参数的传递

每个文章都有一个对应的 id,因此进入详情页面并通过 componentDidMount 加载文章数据的时候,必须携带 id。

// Home/components/List.js
    return (
      <Fragment>
        {articleList.map((item) => (
          <Link key={item.get("id")} to={`/detail/${item.get("id")}`}>
            ......
        ))}
        <LoadMore onClick={() => getMoreList(articlePage)}>阅读更多</LoadMore>
      </Fragment>
    );

然后现在的路由需要修改一下:

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

/detail/:id 接收一个额外的 id 参数后就能匹配上了。

接下来,如何获取 id 呢?要接收 params 的组件包裹 withRouters 方法,通过 this.props.history 即可获取到 (v5 版本的 react-router-dom 的用法)。然后发送 ajax 请求的时候携带上 id 即可。

注:react-router-dom V6 版本获取 params
注意:

大坑,v6 版本的 react-router-dom 放弃了 withRouter,即放弃了 class 组件,全面拥抱了函数式组件,因此得用 React Hooks 的写法来获取 params,网上大部分的通过 class 组件获取 params 的方法都很难使或者根本不好使。

// Detail/index.js
function Detail(props) {
  const { title, content, getDetail } = props;
  const { id } = useParams();

  useEffect(() => {
    getDetail(id);
  });

  return (
    <DetailWrapper>
      <Header>{title}</Header>
      <Content dangerouslySetInnerHTML={{ __html: content }} />
    </DetailWrapper>
  );
}

const mapState = (state) => ({
  title: state.getIn(["detail", "title"]),
  content: state.getIn(["detail", "content"]),
});

const mapDispatch = (dispatch) => ({
  getDetail(id) {
    dispatch(actionCreators.getDetail(id));
  },
});

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

携带 id 发送 ajax 请求:

import { constants } from ".";
import axios from "axios";

const changeDetail = (title, content) => ({
  type: constants.CHANGE_DETAIL,
  title,
  content,
});

export const getDetail = (id) => {
  return (dispatch) => {
    axios.get(`/api/detail.json?id=${id}`).then((res) => {
      const result = res.data.data;
      dispatch(changeDetail(result.title, result.content));
    });
  };
};

5. 登录页面布局

现在写登录页面。在 pages 文件夹里增添 Login 文件夹,同时 App 组件内添加路由。

function App() {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <Header />
        <Routes>
          <Route path="/" exact element={<Home />} />
          <Route path="/login" exact element={<Login />} />
          <Route path="/detail/:id" exact element={<Detail />} />
        </Routes>
      </BrowserRouter>
    </Provider>
  );
}

现在写登录页面的布局:

class Login extends PureComponent {
  render() {
    return (
      <LoginWrapper>
        <LoginBox>
          <Input placeholder="账号" />
          <Input placeholder="密码" />
          <Button>登录</Button>
        </LoginBox>
      </LoginWrapper>
    );
  }
}

6. 登录功能实现

在 Login 文件夹里创建跟 redux 相关的文件,并将 reducer 传递给根 reducer。这样后,Provider 包裹的组件都可以获取到登录的状态。

例如,Header 模块有个用户区域,如果已登录,则显示退出,如果未登录,则显示登录。

// Header/index.js
        <Nav>
          <NavItem className="left active">首页</NavItem>
          <NavItem className="left">下载App</NavItem>
          {login ? (
            <NavItem className="right">退出</NavItem>
          ) : (
            <Link to={"/login"}>
              <NavItem className="right">登录</NavItem>
            </Link>
          )}

现在回到 Login 模块,点击登录按钮的时候,应当携带账号和密码给 ajax 发送请求(这里发送请求的方式是不安全的,应当使用 post 方法,但是没后端就先应付一下下)。

// pages/Login/index.js
class Login extends PureComponent {
  render() {
    const { login, handleLogin } = this.props;
    return !login ? (
      <LoginWrapper>
        <LoginBox>
          <Input
            placeholder="账号"
            ref={(input) => {
              this.account = input;
            }}
          />
          <Input
            placeholder="密码"
            ref={(input) => {
              this.password = input;
            }}
          />
          <Button onClick={() => handleLogin(this.account, this.password)}>
            登录
          </Button>
        </LoginBox>
      </LoginWrapper>
    ) : (
      <Navigate to="/" />
    );
  }
}

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

const mapDispatch = (dispatch) => ({
  handleLogin(accountElem, passwordElem) {
    dispatch(actionCreators.login(accountElem.value, passwordElem.value));
  },
});

export default connect(mapState, mapDispatch)(Login);
// pages/Login/store/actionCreators.js
import { constants } from ".";
import axios from "axios";

const changeLogin = () => ({
  type: constants.CHANGE_LOGIN,
  value: true,
});

export const logout = () => ({
  type: constants.CHANGE_LOGIN,
  value: false,
});

export const login = (account, password) => {
  return (dispatch) => {
    axios
      .get(`/api/login.json?account=${account}&password=${password}`)
      .then((res) => {
        const result = res.data.data;
        if (result) {
          dispatch(changeLogin());
        } else {
          alert("登录失败");
        }
      });
  };
};
// pages/Login/store/reducer.js
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case constants.CHANGE_LOGIN:
      return state.set("login", action.value);
    default:
      return state;
  }
};

7. 登录鉴权

有了登录的状态,通过登录与否可以实现很多登录相关的鉴权。

现在要实现一个功能,在 Header 上点击”写文章“按钮,如果此时没有登录,就跳转到登录页面,只有登录成功才可以进入写文章页面。

// pages/Write/index.js
class Write extends PureComponent {
  render() {
    const { loginStatus } = this.props;
    return loginStatus ? <div>Write</div> : <Navigate to="/login" />;
  }
}

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

export default connect(mapState, null)(Write);

Header 部分需要 Link 标签包裹一下:

        <Addition>
          <Link to="/write">
            <Button className="writing">
              <span className="iconfont">&#xe600;</span>
              写文章
            </Button>
          </Link>
          <Button className="reg">注册</Button>
        </Addition>

然后 App 部分添加 write 路由:

function App() {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <Header />
        <Routes>
          <Route path="/" exact element={<Home />} />
          <Route path="/login" exact element={<Login />} />
          <Route path="/write" exact element={<Write />} />
          <Route path="/detail/:id" exact element={<Detail />} />
        </Routes>
      </BrowserRouter>
    </Provider>
  );
}

8. 异步组件

webpack 打包的规则是,入口进入后,就将相关的同步组件引用都包括进去,如果组件没有异步加载,会造成进入主页的时候,一口气把其他的组件全部加载出来。这样的首页加载速度会很慢。

解决办法:按需加载,首页只加载首页代码,点进详情页再加载详情页面的组件。

使用方法,react.lazy,官方也有示例:

React, { lazy,Suspense }  from 'react'

const OtherComponent = React.lazy(() => import('./OtherComponent'))


function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Suspense 为组件未加载出来前的顶替组件或 DOM 元素。

咱们在路由那边写懒加载,当 url 匹配上了,比如详情页,才把详情页加载出来。

// src/App.js
import { Provider } from "react-redux";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import store from "./store";
import Header from "./common/Header";
import { lazy, Suspense } from "react";

const Home = lazy(() => import("./pages/Home"));
const Login = lazy(() => import("./pages/Login"));
const Write = lazy(() => import("./pages/Write"));
const Detail = lazy(() => import("./pages/Detail"));

function App() {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <Header />
        <Suspense fallback={<div>Loading...</div>}>
          <Routes>
            <Route path="/" exact element={<Home />} />
            <Route path="/login" exact element={<Login />} />
            <Route path="/write" exact element={<Write />} />
            <Route path="/detail/:id" exact element={<Detail />} />
          </Routes>
        </Suspense>
      </BrowserRouter>
    </Provider>
  );
}

export default App;

这样写完后,网页正常运行,查看 network,如果进入不同的路由,才会按需加载不同的组件,说明懒加载功能已经实现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值