今日目标
✔ 完成项目开发前的准备工作。
✔ 掌握路由配置、CSS 变量定义主题、移动端适配、细线边框、封装图标组件等。
项目介绍
目标
了解项目定位和功能。
内容
极客园 H5 端项目:个人自媒体前台。
「极客园」对标 CSDN
、博客园
等竞品,致力成为更加贴近年轻 IT 从业者(学员)的科技资讯类应用。
产品关键词:IT、极客、活力、科技、技术分享、前沿动态、内容社交。
用户特点:年轻有活力,对 IT 领域前言科技信息充满探索欲和学习热情。
项目功能包括短信登录、退出,首页:频道管理、文章列表、更多操作等;文章详情:文章评论、评论回复、点赞、收藏、关注等;个人资料展示和编辑等。
技术栈:create-react-app
、react hooks、redux、antd-mobile
、axios
、react-router-dom
以及 history
、sass
、CSS Modules、TypeScript、lodash、ahooks、websocket 等。
项目搭建
目标
能够基于脚手架搭建支持 TypeScript 的项目。
步骤
使用 React CLI 搭建项目:npx create-react-app geek-h5 --template typescript
。
进入项目根目录:cd geek-h5
。
启动项目:yarn start
。
调整项目目录结构如下。
1 2 3 4 5 6 7 8 9 10 11 12
| src |-- App.scss 根组件样式文件 |-- App.tsx 根组件 |-- assets 项目资源文件,比如图片等 |-- components 通用组件 |-- pages 页面 |-- index.scss 全局样式 |-- index.tsx 项目入口 |-- react-app-env.d.ts |-- store Redux 状态仓库 |-- types TS 类型,包括:接口、Redux 等类型 `-- utils 工具,比如 token、axios 的封装等
|
安装解析 sass 的包。
src/index.tsx
1 2 3 4 5
| import ReactDOM from 'react-dom' import './index.scss' import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
|
src/index.scss
1 2 3 4 5 6 7 8 9
| * { margin: 0; padding: 0; } html, body, #root { height: 100%; }
|
src/App.tsx
1 2 3 4 5
| import './App.scss'
export default function App() { return <div className='app'>App</div> }
|
src/App.scss
托管项目
目标
能够将项目推送到 Gitee 远程仓库。
步骤
在项目根目录打开终端,并初始化 git 仓库(如果已经有了 git 仓库,无需重复该步),命令:git init
。
添加项目内容到暂存区:git add .
。
提交项目内容到仓库区:git commit -m 项目初始化
。
添加 remote 仓库地址:git remote add origin [gitee 仓库地址]
。
将项目内容推送到 gitee:git push origin master -u
。
配置路由
目标
能够配置登录页面的路由并显示在页面中。
步骤
- 安装路由和类型声明文件。
1
| yarn add react-router-dom@5.3.0 @types/react-router-dom -D
|
在 pages 目录中创建两个文件夹:Login、Layout。
分别在两个目录中创建 index.tsx 文件,并创建一个简单的组件后导出。
在 App 组件中,导入路由组件以及两个页面组件。
配置 Login 和 Layout 的路由规则。
pages/Login/index.tsx
1 2 3
| export default function Login() { return <div>Login</div> }
|
pages/Layout/index.tsx
1 2 3
| export default function Layout() { return <div>布局组件</div> }
|
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import './App.scss' import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom' import Login from './pages/Login' import Layout from './pages/Layout'
export default function App() { return ( <Router> <div className='app'> <ul> <li> <Link to='/login'>登录</Link> </li> <li> <Link to='/home'>首页</Link> </li> </ul> <Switch> <Route path='/login' component={Login} /> <Route path='/home' component={Layout} /> </Switch> </div> </Router> ) }
|
路由重定向
目标
能够在打开页面时就展示首页内容。
分析
匹配默认路由,通过 Route 组件的 render
属性配合 Redirect 组件进行重定向,Route render prop。
步骤
在 App.tsx 中添加一个新的 Route,用来匹配默认路由。
为 Route 组件添加 render
属性,用来渲染自定义内容。
在 render 中,渲染 Redirect 实现路由重定向。
代码
App.tsx
1 2 3 4 5 6 7 8
| <Switch> {} {} {} <Route exact path='/' render={() => <Redirect to='/home' />} /> <Route path='/login' component={Login} /> <Route path='/home' component={Layout} /> </Switch>
|
总结
通过哪个组件来实现路由重定向?
Route 通过哪个属性来渲染自定义内容?
AntD Mobile
AntD Mobile 是 Ant Design 的移动规范的 React 实现,服务于蚂蚁及口碑无线业务。
目标
能够使用 antd-mobile 的 Button 组件渲染按钮。
步骤
安装 antd 组件库:yarn add antd-mobile@next
。
导入 Button 组件。
在 Login 页面渲染 Button 组件。
代码
pages/Login/index.tsx
1 2 3 4 5 6 7 8
| import { Button } from 'antd-mobile' export default function Login() { return ( <div> <Button color='primary'>Button</Button> </div> ) }
|
问题
原生 CSS 变量
目标
能够掌握原生 CSS 变量的使用。
内容
CSS 自定义属性,通常称为 CSS 变量,类似于 JS 中声明的变量,可以复用。比如,项目中多次使用某一个颜色值,原来需要重复写多次,如下。
1 2 3 4 5 6 7
| .list-item-active { color: #fc6627; }
.tabs-item-active { color: #fc6627; }
|
使用 CSS 变量可以实现复用,简化了代码,如下。
1 2 3 4 5 6 7 8 9 10 11 12 13
| :root { --geek-color-primary: #fc6627; }
.list-item-active { color: var(--geek-color-primary); }
.tabs-item-active { color: var(--geek-color-primary); }
|
全局变量
使用 :root
这个 CSS 伪类可以匹配文档树的根元素 html,在里面定义的变量相当于 JS 中的全局变量,在任意位置都可以使用该变量。
定义 CSS 变量通过两个减号(–)开头,多个单词之间推荐使用 - 链接,CSS 变量名可以是任意变量名。
1 2 3 4 5 6 7 8 9
| :root { --geek-color-primary: #fc6627; } .tabs-item-active { color: var(--geek-color-primary); } .list-item-active { color: var(--geek-color-primary); }
|
- 在 html 中声明全局变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> :root { --active-color: green; }
html { --active-color: pink; }
.title, h3 { color: var(--active-color); } </style> </head>
<body> <header class="title">标题</header> <h3>2022~~你好</h3> </body> </html>
|
局部变量
1 2 3 4 5 6 7 8 9
| .list { --active-color: #1677ff; color: var(--active-color); } .test { color: var(--active-color); }
|
自定义主题
目标
能够使用原生 CSS 变量来定制极客园项目的主题,文档。
代码
src/index.scss
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| :root:root { --adm-color-primary: #fc6627; --adm-color-success: #00b578; --adm-color-warning: #ff8f1f; --adm-color-danger: #ff3141; --adm-color-white: #ffffff; --adm-color-weak: #999999; --adm-color-light: #cccccc; --adm-border-color: #eeeeee; --adm-font-size-main: 13px; --adm-color-text: #333333; } * { margin: 0; padding: 0; } html, body, #root { height: 100%; }
|
配置路径别名
自定义 CRA 的默认配置,craco 配置文档。
目标
能够配置@路径别名简化路径处理
步骤
安装修改 CRA 配置的包:yarn add -D @craco/craco
。
在项目根目录中创建 craco 的配置文件:craco.config.js
,并在配置文件中配置路径别名。
修改 package.json
中的脚本命令。
在代码中,就可以通过 @
来表示 src 目录的绝对路径。
重启项目,让配置生效。
代码
/craco.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| const path = require('path')
module.exports = { webpack: { alias: { '@': path.resolve(__dirname, 'src'), '@scss': path.resolve(__dirname, 'src', 'assets', 'styles'), }, }, }
|
package.json
1 2 3 4 5 6 7 8 9
| { "scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" } }
|
配置路径提示
目标
能够让 VSCode 识别 @ 路径并给出路径提示。
分析
实际上 jsconfig.json
是参考 tsconfig.json
的,由于 TS 项目自带此配置文件,因此就不再需要使用 jsconfig.json
了。
步骤
- 创建
path.tsconfig.json
配置文件(名字可以不固定),在该配置文件中添加以下配置。
1 2 3 4 5 6 7 8
| { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] } } }
|
- 在
tsconfig.json
中导入该配置文件,让配置生效。
1 2 3 4
| { "extends": "./path.tsconfig.json" }
|
移动端适配
目标
能跟通过配置实现自动适配移动端项目。
分析
为什么要适配?
为了让我们开发的移动端项目页面,在不同尺寸大小的移动端设备(手机)中,保持相同的比例。
适配原理。
选择某个手机的尺寸大小作为基准,其他手机进行等比例缩放,一般选择 iPhone 6
(2 倍屏幕),屏幕宽度为:375px
。
适配方式。
a,rem:动态修改 html
元素的 font-size
;额外设置 body
元素的字体大小为正常值。
b,vw:1vw
就等于屏幕宽度的 1%
。
步骤
postcss-px-to-viewport
安装 px 转 vw 的包:yarn add -D postcss-px-to-viewport
。
包的作用:将 px
转化为 vw
,所以有了该工具,只需要在代码中写 px
即可。
在 craco.config.js
添加相应配置。
重启项目,让配置生效。
代码
/craco.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const path = require('path')
const pxToViewport = require('postcss-px-to-viewport')
const vw = pxToViewport({ viewportWidth: 375, }) module.exports = { webpack: { alias: { '@': path.resolve(__dirname, 'src'), '@scss': path.resolve(__dirname, 'src', 'assets', 'styles'), }, }, style: { postcss: { plugins: [vw], }, }, }
|
🧐 如果出现这个报错 postcss-px-to-viewport: postcss.plugin was deprecated
,而导致项目不能启动,尝试执行 yarn add react-scripts@4
,或者修改 craco.config.js
配置文件如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const path = require('path')
const pxToViewport = require('postcss-px-to-viewport')
const vw = pxToViewport({ viewportWidth: 375, }) module.exports = { webpack: { alias: { '@': path.resolve(__dirname, 'src'), '@scss': path.resolve(__dirname, 'src', 'assets', 'styles'), }, }, style: { postcss: { mode: 'extends', loaderOptions: { postcssOptions: { ident: 'postcss', plugins: [vw], }, }, }, }, }
|
一像素问题
目标
能够展示 1px 像素的边框,参考 antd-mobile 的实现。
分析
实现原理:伪元素 + transform 缩放,伪元素 ::after
或 ::before
独立于当前元素,可以单独对其缩放而不影响元素本身的缩放。
代码
src/assets/styles/hairline.scss
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
@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; }
@mixin hairline($direction, $color: #000, $radius: 0) { position: relative; @if $direction == top { border-top: 1px solid $color;
@media (min-resolution: 2dppx) { 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) { transform: scaleY(0.33); } } } } @else if $direction == right { border-right: 1px solid $color;
@media (min-resolution: 2dppx) { 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) { transform: scaleX(0.33); } } } } @else if $direction == bottom { border-bottom: 1px solid $color;
@media (min-resolution: 2dppx) { 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) { transform: scaleY(0.33); } } } } @else if $direction == left { border-left: 1px solid $color;
@media (min-resolution: 2dppx) { 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) { transform: scaleX(0.33); } } } } @else if $direction == all { border: 1px solid $color; border-radius: $radius;
@media (min-resolution: 2dppx) { 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; } } }
|
pages/Login/index.tsx
1 2 3 4 5 6 7 8 9
| import { Button } from 'antd-mobile' import './index.scss' export default function Login() { return ( <div className='login'> <Button color='primary'>Button</Button> </div> ) }
|
pages/Login/index.scss
1 2 3 4 5 6
| @import '@scss/hairline.scss'; .login { width: 200px; height: 200px; @include hairline(all, #000); }
|
其他说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @import '@scss/hairline.scss';
@include hairline(all, #000); @include hairline(top, #000); @include hairline(left, #000); @include hairline(bottom, #000); @include hairline(right, #000);
@include hairline-remove(all); @include hairline-remove(left); @include hairline-remove(right); @include hairline-remove(bottom); @include hairline-remove(top);
|
使用字体图标
目标
能够在项目中使用字体图标,彩色图标的使用。
内容
- 在 public 下 index.html 中引入该文件。
1
| <script src="//at.alicdn.com/t/font_2503709_f4q9dl3hktl.js"></script>
|
- 在
index.scss
中添加通用 css 代码。
1 2 3 4 5 6 7
| .icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; }
|
- 在组件中,使用如下。
1 2 3
| <svg className='icon' aria-hidden='true' fontSize={30}> <use xlinkHref='#iconbtn_readingtime'></use> </svg>
|
封装图标组件
目标
能够封装 Icon 图标通用组件。
步骤
在 components 目录中,创建 Icon/index.tsx 文件。
创建 Icon 组件,并指定 props 类型。
安装 classnames 包,处理类名。
代码
components/Icon/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import classnames from 'classnames'
type Props = { type: string className?: string onClick?: () => void }
const Icon = ({ type, className, onClick }: Props) => { return ( <svg className={classnames('icon', className)} aria-hidden='true' onClick={onClick}> <use xlinkHref={`#${type}`}></use> </svg> ) }
export default Icon
|
pages/Login/index.tsx
,注意这个文件后缀一定是 .tsx
,否则不会对 <Icon/>
组件的传参进行校验。
1 2 3 4 5 6 7 8 9 10 11
| import Icon from '@/components/Icon' export default function Login() { return ( <div> {/* <svg className='icon' aria-hidden='true' fontSize={30}> <use xlinkHref='#iconbtn_readingtime'></use> </svg> */} <Icon type='iconbtn_readingtime' /> </div> ) }
|