资料:
H5 端项目演示:http://toutiao.itheima.net/
H5 端项目接口文档:http://toutiao.itheima.net/api.html
H5 项目仓库地址:https://gitee.com/zqran/geek-h5-89
01-项目介绍
目标:了解项目定位和功能
内容:
极客园 H5 端项目:个人自媒体前台
「极客园」对标CSDN、博客园等竞品,致力成为更加贴近年轻 IT 从业者(学员)的科技资讯类应用 产品关键词:IT、极客、活力、科技、技术分享、前沿动态、内容社交 用户特点:年轻有活力,对 IT 领域前言科技信息充满探索欲和学习热情
- 项目功能和演示,包括
- 短信登录、退出
- 首页-频道管理,文章列表,更多操作
- 文章详情-文章详情,文章评论,评论回复,点赞,收藏,关注
- 个人中心-个人资料展示,个人资料编辑
- 技术栈:
- 使用 React CLI 搭建项目:npx create-react-app geek-h5 —template typescript
- 进入项目根目录:cd 项目名称
- 启动项目:yarn start
- 调整项目目录结构:
/src
/assets 项目资源文件,比如,图片 等
/components 通用组件
/pages 页面
/store Redux 状态仓库
/types TS 类型,包括:接口、redux等类型
/utils 工具,比如,token、axios 的封装等
App.scss 根组件样式文件
App.tsx 根组件
index.scss 全局样式
index.tsx 项目入口
核心代码:
src/index.tsx 中:
import ReactDOM from ‘react-dom’;
import ‘./index.scss’;
import App from ‘./App’;
ReactDOM.render(
src/index.scss 中:
html,
body {
margin: 0;
padding: 0;
}
html,
body,
#root {
height: 100%;
}
p,
h2,
h3 {
margin: 0;
}
src/App.tsx 中:
import ‘./App.scss’;
function App() {
return
}
export default App;
src/App.scss 中:
.app {
height: 100%;
}
注:为了统一操作,直接删除 src 下的所有文件后,再调整
03-使用 SASS
目标:能够在 CRA 中使用 sass 写样式步骤:
-
04-使用 git/gitee 管理项目
目标:能够将项目推送到 gitee 远程仓库步骤:
在项目根目录打开终端,并初始化 git 仓库(如果已经有了 git 仓库,无需重复该步),命令:git init
- 添加项目内容到暂存区:git add .
- 提交项目内容到仓库区:git commit -m 项目初始化
- 添加 remote 仓库地址:git remote add origin [gitee 仓库地址]
- 将项目内容推送到 gitee:git push origin master -u
- 安装路由:yarn add react-router-dom@5.3.0 和路由的类型声明文件 yarn add @types/react-router-dom -D
- 在 pages 目录中创建两个文件夹:Login、Layout
- 分别在两个目录中创建 index.tsx 文件,并创建一个简单的组件后导出
- 在 App 组件中,导入路由组件以及两个页面组件
- 配置 Login 和 Layout 的路由规则
核心代码:
pages/Login/index.tsx 中:
const Login = () => {
return
};
export default Login;
App.tsx 中:
// 导入路由
import { BrowserRouter as Router, Route, Switch } from ‘react-router-dom’;
// 导入页面组件
import Login from ‘./pages/Login’;
import Layout from ‘./pages/Layout’;
// 配置路由规则
function App() {
return (
);
}
export default App;
06-默认展示首页内容
目标:能够在打开页面时就展示首页内容
分析说明:
匹配默认路由,进行重定向
- Route render prop
- Route 的 render 属性:用来内联渲染任意内容
步骤:
- 在 App.tsx 中添加一个新的 Route,用来匹配默认路由
- 为 Route 组件添加 render 属性,用来渲染自定义内容
- 在 render 中,渲染 Redirect 实现路由重定向
核心代码:
App.tsx 中:
import { Redirect } from ‘react-router-dom’;
总结:
- Route 通过哪个属性来渲染自定义内容?
-
07-antd-mobile 组件库
目标:能够使用 antd-mobile 的 Button 组件渲染按钮
内容:
antd-mobile 是 Ant Design 的移动规范的 React 实现,服务于蚂蚁及口碑无线业务。开箱即用
antd-mobile 文档
步骤: 安装 antd 组件库:yarn add antd-mobile@next
- 导入 Button 组件
- 在 Login 页面渲染 Button 组件
核心代码:
pages/Login/index.tsx 中:
import { Button } from ‘antd-mobile’;
const Login = () => (
);
08-原生 CSS 变量
目标:能够使用原始 CSS 变量
内容:
CSS 自定义属性,通常称为 CSS 变量。类似于 JS 中声明的变量,可以复用 CSS 属性值。比如:
/
比如,项目中多次使用某一个颜色值,原来需要重复写多次
/
.list-item-active {
color: #fc6627;
}
.tabs-item-active {
color: #fc6627;
}
/
使用 CSS 变量来实现复用
/
/ 1 创建全局 CSS 变量 —geek-color-primary/
:root {
—geek-color-primary: #fc6627;
}
/ 2 复用 /
.list-item-active {
color: var(—geek-color-primary);
}
.tabs-item-active {
color: var(—geek-color-primary);
}
- 特点:
- 可复用
- 语义化,—geek-color-primary 比 #fc6627 更容易让人理解
- 根据 CSS 变量的作用域,分为两种:
- 全局 CSS 变量:全局有效
- 局部 CSS 变量:只在某个作用域内(比如,某个类名中)有效
/
全局 CSS 变量
1. 使用 :root 这个 CSS 伪类匹配文档树的根元素 html。可以在CSS文件的任意位置使用该变量
相当于 JS 变量中的全局
2. CSS 变量通过两个减号(—)开头,多个单词之间推荐使用 - 链接。CSS 变量名可以是任意变量名
/
:root {
—geek-color-primary: #fc6627;
}
/ 使用 /
.tabs-item-active {
color: var(—geek-color-primary);
}
.list-item-active {
color: var(—geek-color-primary);
}
/
局部 CSS 变量
/
.list {
—active-color: #1677ff;
/ 在该 类 内部使用改变量 /
color: var(—active-color);
}
.test {
color: var(—active-color); / 错误演示:无效!效果与不使用该变量时一致/
}
09-组件库 antd-mobile 主题定制
目标:能够使用原生 CSS 变量来定制极客园项目的主题
内容:antd-mobile 主题
核心代码:
src/index.scss 中:
:root:root {
—adm-color-primary: #fc6627;
—adm-font-family: ‘PingFangSC-Regular’;
—font-size: 16px;
}
10-配置路径别名
目标:能够配置@路径别名简化路径处理
内容:
自定义 CRA 的默认配置craco 配置文档
步骤:
- 安装修改 CRA 配置的包:yarn add -D @craco/craco
- 在项目根目录中创建 craco 的配置文件:craco.config.js,并在配置文件中配置路径别名
- 修改 package.json 中的脚本命令
- 在代码中,就可以通过 @ 来表示 src 目录的绝对路径
- 重启项目,让配置生效
核心代码:
/craco.config.js 中:
const path = require(‘path’);
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
‘@’: path.resolve(dirname, ‘src’),
// 约定:使用 @scss 表示全局 SASS 样式所在路径
// 在 SASS 中使用
‘@scss’: path.resolve(dirname, ‘src/assets/styles’),
},
},
};
package.json 中:
// 将 start/build/test 三个命令修改为 craco 方式
“scripts”: {
“start”: “craco start”,
“build”: “craco build”,
“test”: “craco test”,
“eject”: “react-scripts eject”
},
11-@别名路径提示
目标:能够让 vscode 识别@路径并给出路径提示
分析说明:
因为项目使用了 TS,而 TS 带有配置文件 tsconfig.json。因此,不需要再使用 jsconfig.json(实际上,jsconfig.json 是参考 tsconfig.json 的)
VSCode 会自动读取 tsconfig.json 中的配置,让 vscode 知道 @ 就是 src 目录
步骤:
- 创建 path.tsconfig.json 配置文件
- 在该配置文件中添加以下配置
- 在 tsconfig.json 中导入该配置文件,让配置生效
- 重启 VSCode
核心代码:
/path.tsconfig.json 中:
{
“compilerOptions”: {
“baseUrl”: “./“,
“paths”: {
“@/“: [“src/“],
“@scss/“: [“src/assets/styles/“]
}
}
}
/tsconfig.json 中:
{
// 导入配置文件
“extends”: “./path.tsconfig.json”
}
12-移动端适配
目标:能跟通过配置实现自动适配移动端项目
分析说明:
适配概述
- 为什么要适配?
- 为了让我们开发的移动端项目页面,在不同尺寸大小的移动端设备(手机)中,保持相同的比例
- 适配原理
- 选择某个手机的尺寸大小作为基准,其他手机进行等比例缩放
- 一般选择 iPhone 6(2 倍屏幕),屏幕宽度为:375px
- 适配方式
- rem:需要手动修改 html 元素的 font-size;额外设置 body 元素的字体大小为正常值
- vw: 1 vw 等于屏幕宽度的 1%
// rem 适配
// iphone6 html ==> font-size: 37.5px
// iphone6 plus html ==> font-size: 41.4px
//
// iPhone 6 下宽高为 100px:
// 100 / 37.5 ≈ 2.667
// height: 2.667rem; width: 2.667rem;
// vw 适配
// iPhone 6 下宽高为 100px:
// 100 / (375 / 100) = 100 / 3.75 ≈ 26.7vw
// height: 26.667vw; width: 26.667vw;
- 如果每次设置宽高都需要手动计算一次,太繁琐了!因此,需要借助工具来解决!
步骤:
- 安装 px 转 vw 的包:yarn add -D postcss-px-to-viewport
- 包的作用:将 px 转化为 vw,所以有了该工具,只需要在代码中写 px 即可
- 在 craco.config.js 添加相应配置
- 重启项目,让配置生效
核心代码:
/craco.config.js 中:
const pxToViewport = require(‘postcss-px-to-viewport’);
const vw = pxToViewport({
// 视口宽度,一般就是 375( 设计稿一般采用二倍稿,宽度为 375 )
viewportWidth: 375,
});
module.exports = {
// 此处省略 webpack 配置
webpack: {},
style: {
postcss: {
plugins: [vw],
},
},
};
关于设计稿的说明:
- 摹客 - 设计稿、原型图
- 蓝湖 - 设计稿、原型图
- 设计稿,一般使用 2 倍设计稿,也就是 iPhone 6 对应的尺寸大小
- iPhone 6 屏幕宽度:375px,因为是 2 倍屏幕,所以,实际上有 750 个物理像素,因此 1px = 2 个物理像素
在 摹客 中,可以通过自定义来修改设计稿的宽度,一般就修改为:设计稿宽度 375px。这样设计稿中的内容多宽多高,在代码中就写多少
13-移动端 1px 像素边框
目标:能够展示 1px 像素的边框
分析说明:
参考 antd-mobile 的实现
实现原理:伪元素 + transform 缩放伪元素::after或::before独立于当前元素,可以单独对其缩放而不影响元素本身的缩放
核心代码:
// src/assets/styles/hairline.scss
@mixin scale-hairline-common($color, $top, $right, $bottom, $left) {
content: ‘’;
position: absolute;
display: block;
z-index: 1;
top: $top;
right: $right;
bottom: $bottom;
left: $left;
background-color: $color;
}
// 添加边框
/
用法:
// 导入
@import ‘@scss/hairline.scss’;
// 在类中使用
.a {
position: relative;
@include hairline(bottom, #f0f0f0);
}
/
@mixin hairline($direction, $color: #000, $radius: 0) {
@if $direction == top {
border-top: 1px solid $color;
// min-resolution 用来检测设备的最小像素密度
@media (min-resolution: 2dppx), (-webkit-min-device-pixel-ratio: 2) {
border-top: none;
&::before {
@include scale-hairline-common($color, 0, auto, auto, 0);
width: 100%;
height: 1px;
transform-origin: 50% 50%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx), (-webkit-min-device-pixel-ratio: 3) {
transform: scaleY(0.33);
}
}
}
} @else if $direction == right {
border-right: 1px solid $color;
@media (min-resolution: 2dppx), (-webkit-min-device-pixel-ratio: 2) {
border-right: none;
&::after {
@include scale-hairline-common($color, 0, 0, auto, auto);
width: 1px;
height: 100%;
background: $color;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx), (-webkit-min-device-pixel-ratio: 3) {
transform: scaleX(0.33);
}
}
}
} @else if $direction == bottom {
border-bottom: 1px solid $color;
@media (min-resolution: 2dppx), (-webkit-min-device-pixel-ratio: 2) {
border-bottom: none;
&::after {
@include scale-hairline-common($color, auto, auto, 0, 0);
width: 100%;
height: 1px;
transform-origin: 50% 100%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx), (-webkit-min-device-pixel-ratio: 3) {
transform: scaleY(0.33);
}
}
}
} @else if $direction == left {
border-left: 1px solid $color;
@media (min-resolution: 2dppx), (-webkit-min-device-pixel-ratio: 2) {
border-left: none;
&::before {
@include scale-hairline-common($color, 0, auto, auto, 0);
width: 1px;
height: 100%;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx), (-webkit-min-device-pixel-ratio: 3) {
transform: scaleX(0.33);
}
}
}
} @else if $direction == all {
border: 1px solid $color;
border-radius: $radius;
@media (min-resolution: 2dppx), (-webkit-min-device-pixel-ratio: 2) {
position: relative;
border: none;
&::before {
content: ‘’;
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid $color;
border-radius: $radius 2;
transform-origin: 0 0;
transform: scale(0.5);
box-sizing: border-box;
pointer-events: none;
}
}
}
}
// 移除边框
@mixin hairline-remove($position: all) {
@if $position == left {
border-left: 0;
&::before {
display: none !important;
}
} @else if $position == right {
border-right: 0;
&::after {
display: none !important;
}
} @else if $position == top {
border-top: 0;
&::before {
display: none !important;
}
} @else if $position == bottom {
border-bottom: 0;
&::after {
display: none !important;
}
} @else if $position == all {
border: 0;
&::before {
display: none !important;
}
&::after {
display: none *!important;
}
}
}
14-字体图标
目标:能够在项目中使用字体图标
内容:
- 在 public 下 index.html body 中引入该文件
- 在 index.scss 中添加通过 css 代码
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
- 在组件中,使用:
{/ 使用时,只需要将此处的 iconbtn_like_sel 替换为 icon 的名称即可/}
15-封装 Icon 组件
目标:能够封装 Icon 图片通用组件
步骤:
- 在 components 目录中,创建 Icon/index.tsx 文件
- 创建 Icon 组件,并指定 props 类型
- 安装 classnames 包(yarn add classnames),处理类名
核心代码:
components/Icon/index.tsx 中:
import classnames from ‘classnames’;
// 组件 props 的类型
type Props = {
// icon 的类型
type: string;
// icon 的自定义样式
className?: string;
// 点击事件
onClick?: () => void;
};
const Icon = ({ type, className, onClick }: Props) => {
return (
16-配置 Redux
目标:能够完成 Redux 的基础配置
分析说明:
Redux 相关的类型比较多且复杂,可以暂时先用 unknown,后面会专门处理其类型
步骤:
- 安装 redux 相关的包:
- yarn add redux react-redux redux-thunk@2.3.0 redux-devtools-extension axios
- 在 store 目录中分别创建:actions 和 reducers 文件夹、index.ts 文件
- 在 store/index.ts 中,创建 store 并导出
- 创建 reducers/index.ts 文件,创建 rootReducer 并导出
- 创建 reducers/login.ts 文件,创建基础 login reducer 并导出
- 在 src/index.tsx 中为 React 组件接入 Redux
核心代码:
store 目录结构:
/store /actions /reducers index.ts index.ts
store/index.ts 中:
import { createStore, applyMiddleware } from ‘redux’;
import thunk from ‘redux-thunk’;
import { composeWithDevTools } from ‘redux-devtools-extension’;
import rootReducer from ‘./reducers’;
const middlewares = composeWithDevTools(applyMiddleware(thunk));
const store = createStore(rootReducer, middlewares);
export default store;
store/reducers/index.ts 中:
import { combineReducers } from ‘redux’;
import { login } from ‘./login’;
const rootReducer = combineReducers({
login,
});
export default rootReducer;
store/reducers/login.ts 中:
// 注意:该项目中,在 redux 中存储的 token 是个对象,包含两个 token:1 token(登录成功的令牌) 2 refresh_token(刷新token,token过期时换取新的token)
const initialState = {};
export const login = (state = initialState, action: unknown) => {
return state;
};
src/index.tsx 中:
import { Provider } from ‘react-redux’;
import store from ‘./store’;
ReactDOM.render(
document.querySelector(‘#root’),
);
17-配置 Redux 的相关类型
目标:能够配置 Redux 的基础类型
步骤:
- 在 types 目录中创建两个类型声明文件:store.d.ts 和 data.d.ts
- store.d.ts:用来存放跟 Redux 相关类型,比如,action 的类型等
- data.d.ts:用来存放跟数据接口相关类型
- 在 store.d.ts 中添加 Redux 相关类型
核心代码:
types/store.d.ts 中:
import { ThunkAction } from ‘redux-thunk’;
import store from ‘../store’;
// Redux 应用的状态
export type RootState = ReturnType
// 使用 thunk 中间件后的 Redux dispatch 类型
// ReturnType:thunk action 的返回类型
// State: Redux 的状态 RootState
// ExtraThunkArg: 额外的参数,没有用到,可以指定为 unknown
// BasicAction: 非 thunk action,即对象形式的 action
export type RootThunkAction = ThunkAction
// 项目中所有 action 的类型
type RootAction = unknown;
// —————————— Redux 对象形式的 action 的类型 —————————————-
// 登录相关的 action 类型
// 文章相关的 action 类型
// 等等
18-准备登录功能的类型
目标:能够根据接口文档准备好登录的类型
步骤:
- 打开登录接口文档
- 查看登录接口的返回数据
- 在 data.d.ts 中,创建登录接口返回数据的类型 Token
- 根据登录接口返回数据的类型 Token,创建登录 action 类型
- 将登录 action 类型添加到 RootAction 类型中( 目的:dispatch action 时有类型提示 )
核心代码:
types/data.d.ts 中:
// 登录接口返回数据类型
export type Token = {
token: string;
refresh_token: string;
};
types/store.d.ts 中:
import { Token } from ‘./data’;
// 将登录 action 添加到该类型中
type RootAction = LoginAction;
// 登录 action 类型
export type LoginAction = {
type: ‘login/token’;
payload: Token;
};
store/reducers/login.ts 中:
import { Token } from ‘@/types/data’;
const initialState: Token = {
token: ‘’,
refresh_token: ‘’,
};
export const login = (state = initialState, action: unknown): Token => {
return state;
};
19-工具函数
目标:能够复用极客园 PC 端项目创建好的工具函数
步骤:
- 安装 history:yarn add history@4.10.1
- 拷贝极客园 PC 端项目中的工具函数到该项目中
- 将原来的 .js 文件,修改为 .ts 文件
- 解决工具函数的类型错误
- 在 App.tsx 中,配置自定义的 history
核心代码:
utils/token.ts 中:
import { Token } from ‘@/types/data’;
// 使用常量来存储 key
const GEEK_TOKEN_KEY = ‘geek-h5-token’;
// 创建 获取 token
export const getToken = (): Token =>
JSON.parse(localStorage.getItem(GEEK_TOKEN_KEY) || ‘{}’);
// 创建 设置 token
export const setToken = (token: Token) =>
localStorage.setItem(GEEK_TOKEN_KEY, JSON.stringify(token));
// 创建 清除 token
export const clearToken = () => localStorage.removeItem(GEEK_TOKEN_KEY);
// 创建 根据 token 判断是否登录
export const isAuth = () => !!getToken();
utils/http.ts 中:
// 封装 axios
import axios from ‘axios’;
import store from ‘@/store’;
import { Toast } from ‘antd-mobile’;
import { customHistory } from ‘./history’;
// import { logout } from ‘@/store/actions’
const http = axios.create({
baseURL: ‘http://toutiao.itheima.net/v1_0‘,
timeout: 5000,
});
// 将来可以继续进行 拦截器 的处理
// 请求拦截器
http.interceptors.request.use((config) => {
// 获取token
// 注意:极客园h5项目中,login 存储的是一个对象,对象中的 token 属性,才是登录身份令牌
const {
login: { token },
} = store.getState();
// 除了登录请求外,其他请求统一添加 token
if (!config.url?.startsWith(‘/authorizations’)) {
// 此处,需要使用 非空断言 来去掉 headers 类型中的 undefined 类型
config.headers!.Authorization = Bearer ${token}
;
}
return config;
});
// 响应拦截器
http.interceptors.response.use(undefined, (error) => {
// 响应失败时,会执行此处的回调函数
if (!error.response) {
// 网路超时
Toast.show({
content: ‘网络繁忙,请稍后再试’,
duration: 1000,
});
return Promise.reject(error);
}
if (error.response.status === 401) {
// token 过期,登录超时
Toast.show({
content: ‘登录超时,请重新登录’,
duration: 1000,
afterClose: () => {
customHistory.push(‘/login’, {
from: customHistory.location.pathname,
});
// 触发退出 action,将 token 等清除
// store.dispatch(logout())
},
});
}
return Promise.reject(error);
});
export { http };
App.tsx 中:
// 注意:此处需要导入 Router
import { Router } from ‘react-router-dom’;
import { customHistory } from ‘./utils/history’;
const App = () => {
return
};