简书项目实战三:详情页面和登录功能实现
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"></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,如果进入不同的路由,才会按需加载不同的组件,说明懒加载功能已经实现。