项目完工
- 前端源码地址:https://github.com/superBiuBiuMan/-zhuhu_daily
- 后端源码地址:https://github.com/superBiuBiuMan/zhihu-daily-admin
- 学习地址:Bilibili
- https://www.bilibili.com/video/BV1wx4y157Gu
初始化项目
ts方式(此项目以ts运行)
create-react-app zhihu-daily --template typescript
- 没有安装create-react-app的同学,请使用npx命令
npx create-react-app zhihu-daily --template typescript
js方式
- 删除后面的
typescript
即可
Rem响应式处理
手动处理
-
我们制作移动端网页的时候,需要考虑兼容性,比如我们UI给出的原型图是以
iPhone5/6
或者其他手机尺寸为参考的,这里就设置设计稿的宽度为375px
,同时为了方便计算,我们设置1rem = 100px
-
然后我们测量UI图的尺寸的时候,就**默认除以100,**这样子就得到了rem单位
- 但是呢,每一人的手机不一定是
375px
宽度,我们在375
宽度下设置了1rem = 100px
,其他手机宽度的转换公式就如下
- 最后就可以得到在不同手机上
1rem
应该等于多少px计算公式( 设备宽度 x 100 / 375 = ?px )
- 知道了原理,书写下代码
<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>
*{
margin: 0;
padding: 0;
}
html{
/* 默认设置为100px */
font-size: 100px;
}
#abc{
width: 2rem;
height: 1rem;
font-size:0.18rem ;
background-color: rebeccapurple;
}
</style>
</head>
<body>
<div id="abc">
你好
</div>
<script>
(() => {
const computed = () => {
const html = document.documentElement;//获取html元素
const deviceWidth = html.clientWidth;//获取设备宽度
const designWidth = 375;//设计图的宽度
const ratio = deviceWidth * 100 / designWidth;
/* 这里你可以默认缩放,也可以设置超出设计图不进行扩展 */
// if(deviceWidth > designWidth) {
// html.style.fontSize = '100px';
// return;
// }
html.style.fontSize = ratio + 'px';
}
computed();
window.addEventListener('resize',computed);
})();
</script>
</body>
</html>
自动处理
- postcss-pxtorem:将px转换为px
- amfe-flexible:为html、body添加font-size,窗口调整时候重新设置font-size
- 安装
npm install amfe-flexible -S
npm install postcss-pxtorem -D
- 在主入口文件引入
amfe-flexible
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import "amfe-flexible"
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
-
配置postcss-pxtorem,可
vue.config.js
、.postcssrc.js
、postcss.config.js
其中之一配置,权重从左到右降低,没有则新建文件,只需要设置其中一个即可: -
如果是react项目一开始没有eject,就需要安装下
CRACO
,这里就以这个为例子(好像还有react-app-rewired
)
npm install @craco/craco --save
- 在项目根目录下创建配置文件craco.config.js,并根据实际情况完善配置
module.exports = {
style: {
postcss: {
mode: 'extends',
loaderOptions: {
postcssOptions: {
ident: 'postcss',
plugins: [
[
'postcss-pxtorem',
{
rootValue: 750/10, // (Number | Function) 表示根元素字体大小或根据input参数返回根元素字体大小
//unitPrecision: 5, // (数字)允许 REM 单位增长到的十进制数字
propList: ['*'], // 可以从 px 更改为 rem 的属性 使用通配符*启用所有属性
//selectorBlackList: [],// (数组)要忽略并保留为 px 的选择器。
//replace: true, // 替换包含 rems 的规则,而不是添加回退。
//mediaQuery: false, // 允许在媒体查询中转换 px
//minPixelValue: 0, // 最小的转化单位
exclude: /node_modules/i // 要忽略并保留为 px 的文件路径
}
]
],
},
},
},
},
};
- 修改
package.json
中的scripts
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
},
- 最终可以看到进行了更改
.App {
width: 250px;
height: 100px;
background-color: red;
}
//自动转化为了
.App {
width: 3.33333rem;
height: 1.33333rem;
background-color: red
}
package.json列表
{
"name": "zhihu-daily",
"version": "0.1.0",
"private": true,
"dependencies": {
"@craco/craco": "^7.1.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"amfe-flexible": "^2.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"postcss-pxtorem": "^6.0.0"
}
}
参考
-
https://www.jianshu.com/p/e69a96e85603
-
https://blog.csdn.net/qq_39223195/article/details/106287522
-
万字长文详解react项目使用craco进行配置并集成Prettier、Eslint、husky、lint-staged
-
https://zhuanlan.zhihu.com/p/528295053?utm_id=0
- 好像提到了
CRA5
版本
- 好像提到了
使用reduxjs/toolkit
- 安装
yarn add reduxjs/toolkit react-redux
-
使用起来也很方便,先抛弃一切redux的,这里只有切片,我们除了创建切片和一个主入口文件,其他什么都没有了
-
创建切片
src\store/slice/base/index.ts
import {
createSlice } from "@reduxjs/toolkit";
export const Info = {
}
export const Base = createSlice({
name:'base',
initialState:() => {
return Info;
},
reducers:{
}
})
export const BaseSliceAction = Base.actions;
export const BaseSliceReducer = Base.reducer;
- 主入口文件
src\store/index.ts
import {
configureStore } from "@reduxjs/toolkit";
import {
BaseSliceReducer } from "@/store/slice/base";
const Store = configureStore({
reducer:{
base:BaseSliceReducer,
}
})
export default Store;
- 传递各个组件
import {
Provider } from "react-redux";
import store from "@/store";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<ConfigProvider locale={
zhCN}>
<Provider store={
store}>
<App />
</Provider>
</ConfigProvider>
);
-
组件使用
- 获取设置的state参数
const { useSelector } from "react-redux"
import { useSelector} from "react-redux" const selectProjectModalOpen = state => state.projectList.projectModalOpen; const showModal = useSelector(selectProjectModalOpen); //等同于 const showModal = useSelector((state) => state.projectList.projectModalOpen)
- 调用设置的方法
const { useDispatch } from "react-redux"
const { useDispatch } from "react-redux"; import { projectListSliceActions} from "../projectList/projectList.slice"; const dispatch = useDispatch();//不需要传入任何参数,react-redux会自动去处理store <button onClick={ () => dispatch(projectListSliceActions.closeProjectModal())}>点击我关闭</button>
- 获取设置的state参数
元素隐藏/显示
详情页
可以利用useEffect来实现并发操作
useEffect(() => {
(async () => {
//获取详情图
const result = await api.queryNewsInfo(id ?? '');
console.log(result)
})()
},[])
useEffect(() => {
(async () => {
//获取点赞信息
const result = await api.queryStoryExtra(id ?? '');
})()
})
React渲染html字符串
- dangerouslySetInnerHTML={ { __html: 内容 }}
- @官网介绍
function createMarkup() {
return {
__html: 'First · Second'};
}
function MyComponent() {
return <div dangerouslySetInnerHTML={
createMarkup()} />;
}
创建的css样式放置到document.head当中
const handleStyle = () => {
const {
css } = info;
if(!Array.isArray(css)) return;
const cssLink = css[0];//获取css链接
console.log(cssLink)
const linkDOM = document.createElement('link');
linkDOM.rel = 'stylesheet';
linkDOM.href = cssLink;
document.head.append(linkDOM);
}
使用flushSync
- 通俗来说这里的用法就是插队更新,让其更新完毕在执行后续代码
- 可以看看这篇文章
import React, {
useState } from 'react';
import {
flushSync } from 'react-dom';
const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={
() => {
flushSync(() => {
setCount1(count => count + 1);
});
// 第一次更新
flushSync(() => {
setCount2(count => count + 1);
});
// 第二次更新
}}
>
<div>count1: {
count1}</div>
<div>count2: {
count2}</div>
</div>
);
};
export default App;
-
需要注意的是,如果通过
useEffect
来,且依赖收集为一个空数组,那么就需要注意函数调用的state值的问题了- 初次渲染的时候,函数指向的是初始化时候的值,当有数据重新渲染的时候,如果不进行依赖收集去更新
useEffect
当中函数的指向,那么就会导致useEffect
指向的永远是初始化时候的函数,从而导致函数内部的state值永远为初始化时候的值 - 所以老师的解决办法如下
- 也就是传入参数的方式
//老师解决办法 useEffect(() => { (async () => { //获取详情图 const result = await api.queryNewsInfo(id ?? ''); flushSync(() => { setInfo(result) handleStyle(result); }) handleImage(result); })() },[]) //下面这种是错误的,handleStyle和handleImage无法获取到最新的state值 useEffect(() => { (async () => { //获取详情图 const result = await api.queryNewsInfo(id ?? ''); flushSync(() => { setInfo(result) handleStyle(); }) //保证DOM可以获取到 handleImage(); })() },[]) //顺带一提,输出结果为 1,2,3,4 useEffect(() => { (async () => { //获取详情图 const result = await api.queryNewsInfo(id ?? ''); console.log(1) flushSync(() => { console.log(2) setInfo(result) console.log(3) }) console.log(4,info) handleStyle(result); //保证DOM可以获取到 handleImage(result); })() },[])
- 初次渲染的时候,函数指向的是初始化时候的值,当有数据重新渲染的时候,如果不进行依赖收集去更新
设置图片
- 图片设置
- 为了更加好的体验,加入了
onload
和onerror
事件
- 为了更加好的体验,加入了
/* 处理大图 */
const handleImage = (info:any) => {
const {
image } = info;
if(!image) return;
const picDOM = document.querySelector<HTMLElement>('.img-place-holder');
const imgDOM = document.createElement('img');
imgDOM.src = image;
imgDOM.onload = () => {
//完成加载
imgDOM.style.cssText = 'width:100%'
//@ts-ignore;
picDOM.style.cssText = 'overflow:hidden';
picDOM?.appendChild(imgDOM);
}
imgDOM.onerror = () => {
//移除外层容器
const parent = picDOM?.parentElement;
parent?.removeChild(picDOM as any);
}
}
登录页面
- reduxjs/toolkit
reduxjs/toolkit使用异步-方法1
-
缺点是需要使用
@ts-ignore
,否者会报A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
警告 -
异步函数
import {createAsyncThunk} from "@reduxjs/toolkit";
export const fetchUserDataAction = createAsyncThunk('fetch/fetchUserDataAction',() => {
console.log('执行了我')
//将作为payload值
return {
age:'18888'
}
})
- store基本步骤
import {
fetchUserDataAction} from "./actions";
import {
createSlice} from "@reduxjs/toolkit";
//创建
export const Base = createSlice({
name:'base',
initialState:() => {
return Info;
},
reducers:{
userInfo: (state, action) => {
console.log(state,action)
state.other = {
name:'我是新名称'
}
}
},
extraReducers:{
//@ts-ignore
[fetchUserData.fulfilled](state,{
payload}){
console.log('请看下图',action)
}
},
})
//主入口
import {
configureStore } from "@reduxjs/toolkit";
import {
BaseSliceReducer } from "@/store/slice/base";
const Store = configureStore({
reducer:{
base:BaseSliceReducer,
}
})
export default Store;
- 使用
import {
fetchUserDataAction} from "@/store/slice/base/actions";
import {
useDispatch} from "react-redux";
const dispatch = useDispatch()
//调用异步
dispatch(fetchUserDataAction() as any);
reduxjs/toolkit使用异步-方法2
- 异步函数
import {
createAsyncThunk} from "@reduxjs/toolkit";
export const fetchUserDataAction = createAsyncThunk('fetch/fetchUserDataAction',() => {
console.log('执行了我')
//将作为payload值
return {
age:'18888'
}
})
- store基本步骤
import {
fetchUserDataAction} from "./actions";
import {
createSlice} from "@reduxjs/toolkit";
export const Base = createSlice({
name:'base',
initialState:() => {
return Info;
},
reducers:{
userInfo: (state, action) => {
console.log(state,action)
state.other = {
name:'我是新名称'
}
}
},
extraReducers(builder){
builder
.addCase(fetchUserDataAction.fulfilled,(state, action) => {
console.log('执行了我',action)
})
}
})
//主入口
import {
configureStore } from "@reduxjs/toolkit";
import {
BaseSliceReducer } from "@/store/slice/base";
const Store = configureStore({
reducer:{
base:BaseSliceReducer,
}
})
export default Store;
- 使用
import {
fetchUserDataAction} from "@/store/slice/base/actions";
import {
useDispatch} from "react-redux";
const dispatch = useDispatch()
//调用异步
dispatch(fetchUserDataAction() as any);
需要做的跳转处理
路由设置
- 类似于Vued的路由前置守卫
- 做法
- 看下图
- 代码这里借助一个hooks来书写
* 路由校验-判断是否需要登录 */
export const useCheckNeedAuth = (path:string) => {
const [,setRandomValue] = useState<string>('');
const {
info }: any = useSelector<any>((state) => state.base);
const needAuth = !info && needAuthPath.includes(path)//登录信息不存在,并且访问的路径在需要认证的路由路径就需要认证;
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
(async () => {
//不需要校验,直接返回
if(!needAuth) return;
//需要校验,请求获取用户信息(因为redux刷新后状态就没有了,需要重新请求)
const {
payload:info} = await dispatch(fetchUserDataAction() as any).catch(() => {
})
if(!info){
Toast.show({
icon:'fail',
content:'请先登录'
})
//console.log('执行跳转');
navigate({
pathname:'/login',
search:`redirect=${
location.pathname}`,
})
return;
}
//console.log('获取到了信息')
//这里采用的方法就是更新一个值,从而去触发重新渲染,老师用的是时间戳,我这里就用uuid
setRandomValue(uuidv4());
})();
})
return [
needAuth,
]
}
- 顺带一提,直接在
useEffect
当中使用async
也是不被允许的
// 错误写法
// useEffect(async () => {
// const result = await axios(
// 'https://hn.algolia.com/api/v1/search?query=redux',
// );
// setData(result.data);
// });
// 正确写法
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
收藏/取消收藏
- 需要注意的是,如果我们想携带params参数和search参数,就需要自己组合了
- 当然,你也可以使用
window.location.href
获取完整的路径信息,不过需要自己对http://localhost
做处理
- 当然,你也可以使用
- 所以我解决办法就是,使用组合
props.navigate({
pathname:'/login',
search:props.location.pathname + props.location.search,
})
收藏夹
- 就添加了确认弹窗操作
实现组件的缓存
- 缓存的方式
主流思想上:
1.不是标准的组件缓存,只是数据缓存A->B在A组件路由跳转的时候,把A组件中需要的数据「或者A组件的全部虚拟DOM」存储到redux中! !A组件释放,B组件加载! !当从B回到A的时候,A开始加载首先判断redux中是否存储了数据(或者虚拟DOM),如果没存储,就是第一次加载逻辑的处理; 如果存储了,则把存储的数据拿来渲染! !
2.修改路由的跳转机制,在路由跳转的时候,把指定的组件不销毁,只是控制display:none隐藏;后期从B回到A的时候,直接让A组件display:block! !
3.把A组件的真实DOM等信息,直接缓存起来;从B跳转回A的时候,直接把A之前缓存的信息拿出来用! !
老师用的是一个老师的组件,这里就使用cjy0208大佬的react-activation
(毕竟下载人数多嘛)- 好吧,此组件react18当中bug太多了…并且需要使用老的ReactDOM.render写法,就不使用了
yarn add react-activation
# 或者
npm install react-activation
- 跳过…
修改个人信息-文件上传
- 一开始纠结文件上传失败后还显示图片,后面解决办法很简单
在uplod中上传失败之后抛出异常
throw new Error('上传失败,请重新上传')
然后在ImageUpload中加入属性 showFailed: false
遇到的问题
无法解析scss/sass提示create-react-app Cannot find module ‘sass’
- 使用 create-react-app 的创建的项目,其默认的 webpack.config.js (这个文件默认隐藏,要查看需要运行 npm run eject,运行这个命令前需要本地 commit 代码)的文件中,可以看到是默认配置了 sass-loader 的选项的,所以在 react 项目中使用 sass 还是比较方便的。虽然默认配置了 sass-loader,但要使用 sass 还是需要先安装一下的,不然就会像我一样出现
create-react-app Cannot find module 'sass'
npm install sass -D
or
yarn add sass -D
无法解析less或提示Cannot find module ‘./index.module.less’ or its corresponding type declarations
- 安装
npm install craco-less -D
or
yarn add craco-less -D
- 编辑
craco.config.js
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
// 此处根据 less-loader 版本的不同会有不同的配置,详见 less-loader 官方文档
lessLoaderOptions: {
lessOptions: {
modifyVars: {
},
javascriptEnabled: true
}
}
}
}
]
};
- 这样我们就可以使用下面命令来使用less了
import "./index.less"
- 可能会出现下面问题
Cannot find module './index.module.less' or its corresponding type declarations.
- 找到
src\react-app-env.d.ts
,添加如下内容
declare module "*.less" {
const content: {
[className: string]: string };
export default content;
}
配置别名
- 更改
craco.config.js
const path = require('path');
const resolve = dir => path.resolve(__dirname,dir);/* 计算路径 */
module.exports = {
webpack:{
alias:{
"@":resolve('src'),
}
}
};
- 然后就可以使用了
//component:lazy(() => import("../views/Login")),
component:lazy(() => import("@/views/Login")),
- 不过可能会出现
Cannot find module '@/views/Login' or its corresponding type declarations.
- 可以在 项目的根目录创建一个
jsconfig.json
或者tsconfig.json
,添加如下内容即可
- 可以在 项目的根目录创建一个
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
}
}
- 更改完成记得重启服务,如果上述服务都没有用,可以试试看
craco-alias
(不过这个库已经被废弃了)
依赖收集导致无法获取到最新state值
- 在做下拉加载组件的时候,下面的代码有问题
const {
onBottom,options,style } = props;
const loadRef = useRef<any>();
useEffect(() => {
const loadRefCurrent = loadRef.current;
const observer = new IntersectionObserver((change) => {
const {
isIntersecting} = change[0];
if(isIntersecting){
onBottom && onBottom();
}
},options ?? {
});
observer.observe(loadRef.current);
return () => {
//移除监听
observer.unobserve(loadRefCurrent);
}
//这里有问题,依赖未写入
},[])
- 导致父组件当中
/* 执行到底部的回调 */
const handleOnBottom = () => {
//子组件未添加依赖收集,导致newList永远是初始阶段的值
console.log(newList)
}
- 所以需要添加依赖
const {
onBottom,options,style } = props;
const loadRef = useRef<any>();
useEffect(() => {
const loadRefCurrent = loadRef.current;
const observer = new IntersectionObserver((change) => {
const {
isIntersecting} = change[0];
if(isIntersecting){
onBottom && onBottom();
}
},options ?? {
});
observer.observe(loadRef.current);
return () => {
//移除监听
observer.unobserve(loadRefCurrent);
}
//注意添加依赖
},[onBottom,options,style])
dispatch出现Argument of type ‘AsyncThunkAction{ age: string; }, void, AsyncThunkConfig>’ is not assignable to parameter of type ‘AnyAction’
- 方法1:设置为any
import {
fetchUserDataAction} from "@/store/slice/base/actions";
import {
useDispatch} from "react-redux";
const dispatch = useDispatch()
dispatch(fetchUserDataAction() as any)
- 方法2:暴露Store当中的dispatch类型
import {
configureStore } from "@reduxjs/toolkit";
const Store = configureStore({
reducer:{
}
})
export type AppDispatch = typeof Store.dispatch
export default Store;
//使用
import {
AppDispatch} from "@/store";
import {
useDispatch} from "react-redux";
import {
fetchUserDataAction} from "@/store/slice/base/actions";
const dispatch = useDispatch<AppDispatch>()
dispatch(fetchUserDataAction())
小知识点
stylesheet引入html文档的外部样式表
rel="styleSheet"
打个比喻:就好比你带了个妞去一个party,虽然你知道这个妞是谁,但是你没给别人介绍啊,谁知道这个妞是干嘛的。
于是你加上rel="stylesheet",然后人们就知道了,哦......原来这个妞是你带来蹭饭的!
那么type="text/css" 也是一个道理,都是用来告诉浏览器的,我这个是一个css的文本,你要是不认识就别乱搞。
对于一些特殊浏览器 不能识别css的,会将代码认为text,从而不显示也不报错。
不过根据官方建议 ,一般还是加上比较好。
因为这个表示的是浏览器的解释方式,如果不定义的话,有些CSS效果浏览器解释得不一样。
React默认Event类型可以使用React.MouseEvent来指明
解构赋值省略掉部分参数
- 笔记摘录:https://blog.csdn.net/qq_43379916/article/details/115632238
// 解构赋值省略掉部分参数
let obj = {
name:'法外狂徒',
age:18,
sex:'男',
qq:'15946875963',
phone:'15689784598'
}
// 利用解构得rest语法收集那些尚未被解构模式拾取的剩余可枚举属性键
/*
rest语法:剩余参数语法
官网:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Rest_parameters
如果函数(对象)的最后一个命名参数以...为前缀,则它将成为一个由剩余参数组成的真数组(对象)
*/
//remaining参数可以随意命名
let {
name,...remaining} = obj;
console.log(remaining) //{ age: 18, sex: '男', qq: '15946875963', phone: '15689784598' }
才发现注释可以标明类型
- 在非ts的情况下
标准React组件的类型
- 可以使用
React.ReactNode
老师正则的意思
- 老师写了一个正则
let reg = /\/api(\/[^/?#]+)/
- 如果不考虑转义的问题和首尾固定的
/ /
这二个符号,这个正则就可以简写为下面这种
/api(/[^/?#])
- 如果不考虑分组捕获,可以再简化
- 含义: 匹配字符串当中具有
/api
内容的,并且获取后面内容不是?
或者是#
或者是/
的字符内容
- 含义: 匹配字符串当中具有
/api/[^/?#]
- 图示
- 分组捕获添加上去后老师的演示代码
less文件引入图片
- 前提有别名~才可以这样子,否者要一层一层找下去
background-image: url("~@/assets/images/personBg.png");
自定义虚线
- 示例1
//自定义虚线
&_dashed{
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px;
background-image: linear-gradient(
to right,
#ccc 0%,
#ccc 50%,
transparent 50%
);
//可以设置此值大小来改变间距
background-size: 14px 1Px;
background-repeat: repeat-x;
}
- 示例2
background: linear-gradient(
to right,
transparent 0%,
transparent 50%,
#ccc 50%,
#ccc 100%
);
background-size: 50px 1px;
background-repeat: repeat-x;