初识react之实战分享

// 专业技术:react、react-router-dom 6.2、hook、redux 、ant, codemirror、sass

如果文中有错别字,或者不对的地方请大家私信或者评论中指正。我会第一时间修改。

初始react,实战分享:

在这里插入图片描述

准备工作

node环境
react官方文档
react-router-dom
react hooks
redux、mbox

相关学习链接:
react中文文档:https://zh-hans.reactjs.org/docs/add-react-to-a-website.html
redux中文文档:https://www.redux.org.cn/

开始:

1、npm安装脚手架工具:npm install create-react-app -g
2、创建react项目:create-react-app 项目名
安装报错提示:node版本14以上

npx create-react-app demo
cd demo
$ npm start

清理项目,删除无用的
项目目录整理

引入ant

npm install antd --save

index.css 文件中引入样式,组件按需引入即可
@import ‘antd/dist/antd.css’;
试用下ant是否生效
写代码试下
顺带学习下类组件,函数组件区别用法

react中文文档学习

react中文文档学习地址

包含:核心概念、高级指引,hooks

关于router

回顾一下vue
使用路由:
模式、router-link、router-view、传参、映射与挂载、代码跳转、路由懒加载,嵌套等

先简单说下各自的功能:
react-router: 实现了路由的核心功能
react-router-dom: 基于 react-router,加入了在浏览器运行环境下的一些功能,例如:Link 组件,会渲染一个 a 标签,BrowserRouter 和 HashRouter 组件。显而易见 react-router-dom 功能更丰富,所以选择 react-router-dom 代替 react-router

引入react-router-dom

npm install react-router-dom -S
import React from "react";
import { Route, Link, Switch, Redirect} from 'react-router-dom';

const Home = React.lazy(() => import('./pages/home/index'));
const About = React.lazy(() => import('./pages/about/index'))
const Notfound = React.lazy(() => import('./pages/404/index'))

function App() {
  return (
    <div className="App">
      <ul>
        <li>
            <Link to="/home">Home</Link>
        </li>
        <li>
            <Link to="/about">about</Link>
        </li>
      </ul>
        <Switch>
          <Route path="/home" component={Home}/>
          <Route path="/about" component={About}/>
          <Route path="/" exact render={()=>{
                //重定向
                return <Redirect to="/home"/>
          }}/>
          <Route path="*" exact component={Notfound}/>
        </Switch>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
    </div>
  );
}

export default App;

发现报错

export 'Redirect' (imported as 'Redirect') was not found in 'react-router-dom'
export 'Switch' (imported as 'Switch') was not found in 'react-router-dom'

在React Router v6版本中大量使用了React hooks,因此在尝试使用v6版本之前,需要使用React 16.8或更高版本

V5版本中注册路由,无需包裹组件。
Redirect组件——>Navigate组件
在V6版本中Redirect组件已被移除。在V5版本中使用进行默认路由跳转。而在V6版本中我们可以使用 Navigate 组件来实现默认路由跳转

<Route path="*" element={<Navigate to="/about" />} /> 

通过Route组件中的path="*"来对未匹配的链接进行“兜底”,通过Navigate来实现跳转到哪里去。
如果想要实现点击按钮跳转链接或者定时跳转请使用useNavigate。
Navigate组件与Link、NavLink的属性差不多。都含有to、state、replace属性。使用起来没什么难度。

注意类组件,函数组件区别

在V6版本中我们这样写:

import React, { Fragment } from 'react';
const Home = React.lazy(() => import('./pages/home/index'));
import About from './pages/about/index';
// BrowserRouter\HashRouter
import { HashRouter, Routes, Route, Link, } from 'react-router-dom';

function App() {
    return (
        <Fragment>
        	// BrowserRouter 和 HashRouter
            <HashRouter>
                <ul className="topBar">
                    <li>
                        <Link to="/">Home</Link>
                    </li>
                    <li>
                    	// param
                        <Link to="/about/5/ssss">about</Link> 
                    </li>
                     <li>
                        // search
                        <Link to={`/about?name=xuxiaoli02`}>about</Link>
                    </li>
                </ul>
                <Routes>
                    <Route exact path="/" element={<Home />} />
                    <Route path="/home" element={<Home />} />
                    <Route path="/about/:id/:mes" element={<About />} />
                </Routes>
            </HashRouter>
        </Fragment>
    );
}

export default App;

<Outlet /> 组件。
Outlet可以看作一个“占位符”,在父组件所需要渲染子组件的地方进行占位。若父组件中定义的路由完全匹配则会在此处呈现子组件内容,如果没有匹配则不会展现任何内容。而在此处注册路由的工作则统一在App.js中一同管理。

这里需要注意的一点是 在注册路由中,父路由组件的path需要写为"/home/"末尾一定要添加/,否则无法实现子组件默认路由选中。

向路由组件传递params参数

在这里插入图片描述

<Link to={`/about/5/ssss?name=xuxiaoli02`}>about</Link>
类组件写法
V6版本中如何处理:
import React, { Component } from 'react';
import { useParams, useNavigate } from 'react-router-dom';

// v6使用class组件。需要封装一下。利用hoc组件来获取参数,然后传递给class组件
function myWithRouter(Detail) {
  return (props) => {
    let navigate = useNavigate();
    return <Detail {...props} params={useParams() navigate={navigate}} />
  }
}

const DetailData = [
  { id: '01', content: '11111111111111111111' },
  { id: '02', content: '22222222222222222222' },
  { id: '03', content: '33333333333333333333' }
]

class Detail extends Component {
 	constructor(props) {
        super(props);
      	this.handleclick = this.handleclick.bind(this);
    }
    handleCancle() {}
 	gotoHome() {
     	this.props.navigate('/home');
   	}
   	handleclick() {}
   	render() {
	    const contentStr = DetailData.find((detailObj) => {
	      return detailObj.id === this.props.params.id;
	    })
	   return (
	    <ul>
	      <li>ID:{this.props.params.id}</li>
	      <li>TITLE:{this.props.params.title}</li>
	      <li onClick={() => this.handleCancle()}>点击</li>
	      <li onClick={this.gotoHome.bind(this)}>点击</li>
	      <li onClick={this.handleclick}>点击</li>
	     </ul>
	     )
	  }
}

export default myWithRouter(Detail);

原因就是在V6版本中,不能V5版本中那样从this.props中获取路由组件的相关参数了。你如果打印一下props就会发现,props中毛都没有。这里V6版本中就提供了一个hook(钩子),来让我们可以直接获取到所传递的params参数,这个钩子就是 useParams。那么我们为啥不直接使用呢?因为React的hook只能在hoc函数高阶组件中使用。看到这里是不是就明白了为什么要包一层了吧。

上面代码中定义了一个myWithRouter函数组件并将Detail组件传入,在myWithRouter中获取useParams以及相关props,直接传递给Detail组件使用。这样我们就可以在Detail组件中成功的获取到params参数了。

当然上面不是最好的写法。我们直接用函数组件不是更好么。都不需要再进行在外面封装一层了。

函数组件写法
import { useNavigate, useParams } from "react-router-dom";
 
// 方案一
export default function About() {
  let params = useParams();
  console.log(params)
  return (
      <div>
        <div>这是About组件</div>
        <div>
            ID: {params.id}
        </div>
      </div>
  )
}

向路由组件传递search参数

使用基本相同,举例不通:

 <Link to={`/home/message/detail/id=${messageObj.id}&titile=${messageObj.title}`}>
	{messageObj.title}
 </Link> 

 {/* 传递参数 */}
 <Route path="detail" element={<Detail />} /> 
Detail接收参数:

与传递params参数相同,还是需要使用高阶函数组件进行封装。这里V6版本中提供了useSearchParams钩子 
import React, { Component } from 'react';
import { useSearchParams } from 'react-router-dom';

// v6使用class组件。需要封装一下。利用hoc组件来获取参数,然后传递给class组件
function myWithRouter(Detail) {
  return (props) => {
    // let [searchParams, setSearchParams] = useSearchParams();
    let [searchParams] = useSearchParams();
    const params = {
      id: searchParams.get('id'),
      title: searchParams.get('titile')
    }
    return <Detail {...props} params={params} />
  }
}

const DetailData = [
  { id: '01', content: '11111111111111111111' },
  { id: '02', content: '22222222222222222222' },
  { id: '03', content: '33333333333333333333' }
]

class Detail extends Component {
  render() {
    const contentStr = DetailData.find((detailObj) => {
      return detailObj.id === this.props.params.id;
    })
   return (
    <ul>
      <li>ID:{this.props.params.id}</li>
      <li>TITLE:{this.props.params.title}</li>
      <li>CONTENT:{contentStr.content}</li>
     </ul>
     )
  }
}

export default myWithRouter(Detail);

可以看到上面代码中被注释掉的代码let [searchParams, setSearchParams] = useSearchParams();
可见useSearchParams不仅提供了获取searchParams参数,还提供了设置searchParams参数。
可以通过setSearchParams进行设置。这里就不为大家过多演示。请大家自己尝试吧。

向路由组件传递state参数

在这里插入图片描述
在V6版本中经过我测试,在to中写对象传state的方式并没有用。V6版本中在Link组件(包含NavLink、Navigate)中state作为一个单独的属性与to分开。

this.state.messageArr.map((messageObj) => {
  return (
    <li key={messageObj.id}>
      <Link
        to='/home/message/detail'
        state={{ id: messageObj.id, title: messageObj.title }}
      >
        {messageObj.title}
      </Link>
    </li>
    )
})

Detail组件同样还是高阶函数进行包裹。使用useLocation钩子进行获取state参数
import React, { Component } from 'react';
import { useLocation } from 'react-router-dom';

// v6使用class组件。需要封装一下。利用hoc组件来获取参数,然后传递给class组件
function myWithRouter(Detail) {
  return (props) => {
    let location = useLocation();
    /*
      这里写成 location.state || {}
      因为state在刷新后可能为空,所以这里为空时设置为空对象
    */
    const stateObj = location.state || {};
    const params = {
      id: stateObj.id,
      title: stateObj.title
    }
    return <Detail {...props} params={params} />
  }
}

const DetailData = [
  { id: '01', content: '11111111111111111111' },
  { id: '02', content: '22222222222222222222' },
  { id: '03', content: '33333333333333333333' }
]

class Detail extends Component {
  render() {
    const contentStr = DetailData.find((detailObj) => {
      return detailObj.id === this.props.params.id;
    }) || {}; // 这里 || {} 当未找到匹配的值时contentStr设置为空对象
   return (
    <ul>
      <li>ID:{this.props.params.id}</li>
      <li>TITLE:{this.props.params.title}</li>
      <li>CONTENT:{contentStr.content}</li>
     </ul>
     )
  }
}

export default myWithRouter(Detail);

编程式路由导航

在这里插入图片描述
this.props.history的方式
而在V6版本中则提供了useNavigate来实现前进、后退等操作


import React, { Component } from 'react'
import { Link, Outlet, useNavigate } from 'react-router-dom'

function anonyCom(MessCom) {
  return (props) => {
    let navigate = useNavigate();
    return <MessCom {...props} navigate={navigate} />
  }
}

class Message extends Component {

  state = {
    messageArr: [
      { id: '01', title: 'message1' },
      { id: '02', title: 'message2' },
      { id: '03', title: 'message3' }
    ]
  }

  pushShow = (messageObj) => {
    this.props.navigate(
      '/home/message/detail',
      {
        state: {
          id: messageObj.id, title: messageObj.title
        }
    })
  }

  replaceShow = (messageObj) => {
    this.props.navigate('/home/message/detail',
    {
      replace: true,
      state: {
        id: messageObj.id, title: messageObj.title
      }
    })
  }

  goBack = () => {
    this.props.navigate(-1);
  }

  goOne = () => {
    this.props.navigate(1);
  }

  goTwo = () => {
    this.props.navigate(2);
  }

  render() {
    return (
      <div>
        <ul>
        {
          this.state.messageArr.map((messageObj) => {
            return (
              <li key={messageObj.id}>
                <Link
                  replace to='/home/message/detail'
                  state={{ id: messageObj.id, title: messageObj.title }}
               >
                 {messageObj.title}
               </Link>
               <button onClick={() => { this.pushShow(messageObj) }}>push查看</button>
               <button onClick={() => { this.replaceShow(messageObj) }}>replace查看</button>
             </li>
            )
          })
        }
        </ul>
        <hr />
        <Outlet />
        <hr />
        <button onClick={this.goBack}>回退</button>
        <button onClick={this.goOne}>前进</button>
        <button onClick={this.goTwo}>go 2</button>
      </div>
    )
  }
}

export default anonyCom(Message);
从上面代码中可以看出useNavigate不仅可以传递路由地址,还可以传递数字,传递正数则向前进,传递负数则向后退。并且还可以传递一个对象,对象中包含了replace与state。非常的强大。

到这里我们可以看到在新版本中,大部分功能都改为了Hook的方式使用。所以如果需要使用新版本则一定要学会Hook相关知识。
import React from 'react';
import './index.less';
import { useNavigate } from 'react-router-dom';
// class Hello extends Component {
const Hello = () => {
    // constructor(props) {
    //     super();
    // }
    let navigate = useNavigate();
    // render() {
    return (
        <div className="main">
            <h1>hello , 欢迎学习class组件</h1>
            <p>cccc</p>
            <h1
                onClick={() => {
                    navigate('/home');
                }}
            >
                home
            </h1>
            {/* <h1 onClick={this.gotoHome.bind(this)}>去首页</h1> */}
        </div>
    );
    // }
    // gotoHome() {
    //     console.log(this.props)
    //     // this.props.history.push('/home')
    //     this.props.history.push('/home')
    // }
};

export default Hello;

路由集中式配置:

让我们看下router.js

// 优化自动加载
// 路由懒加载
import React from 'react';
const Home = React.lazy(() => import('@p/home/index.jsx'));
const Layouts = React.lazy(() => import('@p/admin/index.jsx'));

const Rule = React.lazy(() => import('@p/admin/rule/index.jsx'));
const Task = React.lazy(() => import('@p/admin/task/index.jsx'));
const Notfound = React.lazy(() => import('@p/404/index.jsx'));
const Noauthority = React.lazy(() => import('@p/admin/noauthority/index.jsx'));

// 路由配置
const routeConfig = [
    {
        path: '*',
        name: '404',
        // element: () => import('@/pages/layouts/homeLayout.vue'),
        element: <Notfound />,
        redirect: '/404',
    },
    {
        path: '/',
        name: '首页',
        element: <Home />,
        redirect: '/home',
    },
    {
        path: '/home',
        name: '首页',
        element: <Home />,
    },
    {
        path: '/',
        name: '后台首页',
        element: <Layouts />,
        children: [
            {
                path: '/',
                redirect: '/home',
            },
            {
                path: '/rule/list',
                name: '规则管理',
                element: <Rule />,
            },
            {
                path: '/task/list',
                name: '任务管理',
                element: <Task />,
            },
            {
                path: '/noauthority',
                name: 'noauthority',
                meta: {
                    title: 'noauthority',
                },
                element: <Noauthority />,
            },
        ],
    },
];
export default routeConfig;
// 使用:
import { useRoutes, Outlet } from 'react-router-dom';
const element = useRoutes(routeConfig);
return (	
	<div>
		{element}
		<Outlet />
	</div>
)


5,6 对比:https://www.bilibili.com/read/cv14409269

react-router-dom v6.0新特性及路由守卫

v6中提供Outlet组件,用于预留坑渲染嵌套路由页面,是开发更加简单便捷

代理配置及webpack配置优化等:

使用axios

npm install axios -S

代理方案及自定义配置方案

回想一下vue中如何配置?
nodemodules、package.json、config/index.js、vue.config.js、

httpProxyMiddleware
关键代码:

devServer: {
   proxy: {
     '/xxl': {
       target: 'http://www.baidu.com',
       changeOrigin: true
     }
   }
 }

#设置代理
多种方式:

1、nodemodules

2、package.json

3、npm run eject

注意 npm eject 不可逆,弹配置文件
Are you sure you want to eject? This action is permanent

4、setupProxy.js

const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
    app.use(
        '/myflaw',
        createProxyMiddleware({
  
            target: 'xxxxxx机器名',
            changeOrigin: true, //控制服务器接收到的请求头中host字段的值
            // pathRewrite: { '/api': '' }, //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
            headers: {
                cookie: '',
            },
        })
    );
    console.log('徐小丽调试进行啦~');
};

5、customize-cra react-app-rewired 中

customize-cra 修改webpack 基础配置

customize-cra react-app-rewired 中
(1)npm i -D customize-cra react-app-rewired
(2)修改 package.json 的 scripts 配置

"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"

(3)在项目的更目录下创建 config-overrides.js

const { override, overrideDevServer } = require('customize-cra');
const devServerConfig = () => config => {
  return {
      ...config,
      proxy: {
          '/myflaw': {
              target: '机器名',
              changeOrigin: true,
              headers: {
                  cookie: ''
              },
              pathRewrite: {
                  '^/myflaw': '/myflaw',
              },
          },
      }
  };
};
module.exports = {
  devServer: overrideDevServer(
        devServerConfig()
    )
};

6、配置webpack优化以及其他等

const path = require('path');
// show https://github.com/arackaf/customize-cra
const {
    override,
    addWebpackAlias,
    overrideDevServer,
    fixBabelImports,
    addLessLoader
} = require('customize-cra');
const CompressionWebpackPlugin = require('compression-webpack-plugin');

function resolve(dir) {
  return path.join(__dirname, '.', dir);
}

const addCustomize = () => config => {
    if (process.env.NODE_ENV === 'production') {
        // 关闭sourceMap
        config.devtool = false;
        let optimization = {
            // 优化:
            // 把单个bundle文件拆分成若干小的bundle/chunks,为了缩短首屏加载时间,使用splitChunks提取公共代码,拆分src与第三方库
            splitChunks: {
                chunks: 'all',
                cacheGroups: {
                    vendor: {
                        name: 'vendor', // 第三方库名
                        priority: 20, // 拆分优先级
                        minSize: 0, // 最小大小
                        minChunks: 1, // 最小一段
                        test: /[\\/]node_modules[\\/]/,
                        // test: /[\\/]node_modules[\\/]_?vendor(.*)/,
                        chunks: 'all' // initial同步加载, all: 同步+异步加载
                    },
                    src: {
                        name: 'src', // 公共提取
                        priority: 20, // 拆分优先级
                        minSize: 0, // 最小大小
                        test: /[\\/]src[\\/]/, // src文件夹里面
                        chunks: 'all'
                    }
                }
            }
        };
        config.optimization = optimization;
        config.performance = {
            hints: 'warning',
            // 入口起点的最大体积
            maxEntrypointSize: 50000001,
            // 生成文件的最大体积
            maxAssetSize: 30000000,
            // 只给出 js 文件的性能提示
            assetFilter: assetFilename => {
                return assetFilename.endsWith('.js');
            }
        };
        // 配置打包后的文件位置
        // config.output.path = resolve('dist');
        // config.output.publicPath = './';
        // 添加js打包gzip配置
        config.plugins.push(
            new CompressionWebpackPlugin({
                test: /\.js$|\.css|\.jsx|\.less$/,
                threshold: 1024,
            }),
        );
    }
    return config;
};

const devServerConfig = () => config => {
    return {
        ...config,
        proxy: {
            '/myflaw': {
                target: '机器名',
                changeOrigin: true,
                headers: {
                    cookie: ''
                },
                pathRewrite: {
                    '^/myflaw': '/myflaw',
                },
            },
        }
    };
};

module.exports = {
    webpack: override(
    //     // 针对antd 实现按需打包:根据import来打包 (使用babel-plugin-import)
    //     // fixBabelImports('import', {
    //     //     libraryName: 'antd',
    //     //     libraryDirectory: 'es',
    //     //     style: true, // 自动打包相关的样式 默认为 style:'css'
    //     // }),
    //     // 使用less-loader对源码重的less的变量进行重新制定,设置antd自定义主题
    //     // addLessLoader({
    //     //     javascriptEnabled: true,
    //     // }),
    //     // 支持less文件
    //     // 配置路径访问快捷键 @/xxx
        addWebpackAlias({
            '@': resolve('src'),
            '@c': resolve('src/common'),
            '@p': resolve('src/pages'),
            '@r': resolve('src/router'),
            '@u': resolve('src/utils'),
            '@hooks': resolve('src/hooks'),
        }),
    //     // // postCss 自动将px转为rem 需要配合 lib-flexible 使用
    //     // addPostcssPlugins([
    //     //   require('postcss-pxtorem')({ rootValue: 75, propList: ['*'], minPixelValue: 2, selectorBlackList: ['am-'] })
    //     // ]),
    //     // 压缩js等
        addCustomize(),
        plugin()

    ),
    // 本地启动配置,可以设置代理
    devServer: overrideDevServer(
        devServerConfig()
    )
};
打包的时候开启gzip可以很大程度减少包的大小,非常适合于上线部署。更小的体积对于用户体验来说
就意味着更快的加载速度以及更好的用户体验。
安装:npm install compression-webpack-plugin --save-dev

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

const plugin = () => (config) => {
    if (process.env.NODE_ENV === 'production') {
        config.plugins[0] = new HtmlWebpackPlugin({
            template: path.join(__dirname, '/public/index.html'),
            filename: 'dolphin.html',
            inject: true,
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true
            }
        });
    }
    return config;
};```



补充:<代理问题>
跨域:协议,域名,端口号导致
正常解决跨域的形式很多种,这里不一一讲解,就实际开发遇到的问题
代理了还提示跨域,比如多了一层接入的登录验证
处理:保持一致
示例方案:

```javascript
// 方法1
 devServer: {
        open: process.platform === 'darwin',
        host: 'localhost', // 允许外部ip访问
        port: 80001, // 端口
        https: false, // 启用https
        // off 错误、警告在页面弹出
        overlay: {
            warnings: false,
            errors: false,
        },
        proxy: {
            '/api': {
                // 方法1
                target: 'http://baidu.com:8888',
                headers: {
                	// 方法1需要cookie啥的
                    // cookie: ''
                },
                changeOrigin: true, // 允许跨域
                pathRewrite: {
                    '^/api': '/api'
                }
            },
        }, // 代理转发配置,用于调试环境
    },


或者入口哪里全局种cookie:
if (process && process.env && process.env.NODE_ENV && process.env.NODE_ENV === 'development') {
    const cookie = '';
    cookie.split(';').forEach(item => {
        document.cookie = item.trim();
    });
}
// 方法2
 devServer: {
        open: process.platform === 'darwin',
        host: '机器名', // 允许外部ip访问
        port: 8666, // 端口
        https: false, // 启用https
        // off 错误、警告在页面弹出
        overlay: {
            warnings: false,
            errors: false,
        },
        proxy: {
            '/api': {
                // 2, 代理环境的域名假设http://baidu.com:88888
                //  本地配置host  baidu.com 127.0.0.1
                target: 'http://10.xxx.xx.xx:3000',
                headers: {
                	// 方法2不需要cookie,域名一样自动种上了
                    // cookie: ''
                },
                changeOrigin: true, // 允许跨域
                pathRewrite: {
                    '^/api': '/api'
                }
            },
        }, // 代理转发配置,用于调试环境
    },

关于使用scss:

样式引用时候坑
启用 Sass 语法编写 CSS
create-react-app 脚手架中已经添加了 sass-loader 的支持,所以只需要安装 node-sass 插件即可
安装 node-sass 插件

npm install node-sass --save

实例:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.scss';
class App extends Component {
 render() {
  return (
   <div className="App">
    <header className="App-header">
     <img src={logo} className="App-logo" alt="logo" />
    </header>
   </div>
  );
 }
}
export default App;

// 关于样式:
.mudule
import appstyle from './App.scss';
import './App.scss';
效果一样,直接className='header'生效,全局生效,其他地方用到类名也生效
className={appstyle.header} 不生效
import appstyle from './App.module.scss';
className={appstyle.header}生效,哪里用哪里生效
className='header'不生效
import './App.module.scss';
x className={appstyle.header}报错
className='header'不生效

#### 表单封装思路
组件传值、

eslint.pretitter

相关地址:
https://www.cnblogs.com/boyGdm/p/14133675.html
https://github.com/yannickcr/eslint-plugin-react

安装依赖:

npm install eslint eslint-plugin-react --save-dev

package.json去掉eslintconfig等相关配置,目录下面新建.eslintrc.js

// 示例
// 如果需要检查 React 代码:
// 安装eslint-plugin-react, 配置plugins: ['react']
const prettierrc = require('./.prettierrc.js');
module.exports = {
    env: {
        browser: false,
        // 这样设置之后,就支持新的 ES6 全局变量和类型了
        es6: true,
        node: true
    },
    // extends:用于继承某些基础配置
    // extends: 'eslint:recommended',
    extends: [
        // "react-app",
        // 'prettier',
        'eslint:recommended'
        // "plugin:react/recommended",
    ],
    // extends: 'react-app',
    // 第三方插件:增强 eslint 的能力
    plugins: ['react'],
    // plugins: ['react', 'prettier'],
    // parser: 指定要使用的解析器。(parser和parserOptions要同时使用)
    parser: 'babel-eslint',
    parserOptions: {
        // 一个配置对象
        ecmaFeatures: {
            // 启用jsx
            jsx: true
        },
        // 按照 ecma 哪个版本语法做检查
        // 支持es6语法(但不支持新的 ES6 全局变量或类型,如Set)
        ecmaVersion: 6,
        // ·sourceType:默认是 script。模块化的代码要写:module(当前最常见做法)
        sourceType: 'module'
    },
    settings: {
        react: {
            // 使用的 Pragma, 默认to "React"
            pragma: 'React',
            // React 版本。“检测”会自动选择您已安装的版本。
            version: 'detect'
        }
    },
    // globals: {
    //     JSX: true,
    //     React: true,
    //     NodeJS: true,
    //     Promise: true,
    // },
    rules: {
        // 规则太多啦~,参考地址:
        // https://www.cnblogs.com/peter-web/p/15272883.html
        // https://blog.youkuaiyun.com/weixin_30443747/article/details/99622110?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2.pc_relevant_paycolumn_v3&utm_relevant_index=5
        'no-console': 'off',
        indent: [0, 4],
        quotes: ['warn', 'single'],
        'comma-dangle': [
            'warn',
            {
                arrays: 'never',
                objects: 'never',
                imports: 'never',
                exports: 'never',
                functions: 'never'
            }
        ],
        // 控制逗号前后的空格
        'comma-spacing': [
            1,
            {
                before: false,
                after: true
            }
        ],
        'no-undef': 0,
        // 禁止未使用的变量
        'no-unused-vars': 0,
        'no-useless-escape': 0,
        'no-case-declarations': 0,
        'react/jsx-uses-react': 1,
        'react/jsx-uses-vars': 1,
        // 禁止属性前有空白
        'no-whitespace-before-property': 1,
        // 强制在注释中 // 或 /* 使用一致的空格
        'spaced-comment': ['warn', 'always'],
        'no-empty': 0,
        semi: ['warn', 'always']
        // 'prettier/prettier': ['warn', prettierrc],
    }
};


.prettier.js

module.exports = {
    printWidth: 100, // 指定代码长度,超出换行
    tabWidth: 4, // tab 键的宽度
    useTabs: false, // 不使用tab
    semi: true, // 结尾加上分号
    singleQuote: true, // 使用单引号
    quoteProps: 'as-needed', // 要求对象字面量属性是否使用引号包裹,(‘as-needed’: 没有特殊要求,禁止使用,'consistent': 保持一致 , preserve: 不限制,想用就用)
    jsxSingleQuote: false, // jsx 语法中使用单引号
    trailingComma: 'es5', // 确保对象的最后一个属性后有逗号
    bracketSpacing: true, // 大括号有空格 { name: 'rose' }
    jsxBracketSameLine: false, // 在多行JSX元素的最后一行追加 >
    arrowParens: 'always', // 箭头函数,单个参数添加括号
    requirePragma: false, // 是否严格按照文件顶部的特殊注释格式化代码
    insertPragma: false, // 是否在格式化的文件顶部插入Pragma标记,以表明该文件被prettier格式化过了
    proseWrap: 'preserve', // 按照文件原样折行
    htmlWhitespaceSensitivity: 'ignore', // html文件的空格敏感度,控制空格是否影响布局
    endOfLine: 'auto', // 结尾是 \n \r \n\r auto
};

新建.prettierignore 、.eslintignore

// 忽略格式化文件 (根据项目需要自行添加)
node_modules
dist
"lint": "eslint --ext .js,.jsx ./",
"lint:fix": "eslint --fix  --ext .js,.jsx ./",
"fix": "npm run lint && npm run lint:fix && npm run pre",

hook使用:

什么是hook?怎么使用?为什么有hook?

什么是hook?

  • Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
  • Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
  • 没有计划从 React 中移除 class

为什么要使用 hooks

  • 难以理解的 class 、组件中必须去理解 javascript 与 this 的工作方式、需要绑定事件处理器、纯函数组件与 class 组件的差异存在分歧、甚至需要区分两种组件的使用场景;Hook 可以使你在非 class 的情况下可以使用更多的 React 特性。

  • 在组件之间复用状态逻辑很难、大型组件很难拆分和重构,也很难测试。Hook 使你在无需修改组件结构的情况下复用状态逻辑。

  • 复杂组件变得难以理解、业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)

hook规则

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则
(1)只在最顶层使用 Hook
(2)只在 React 函数中调用 Hook
了解更多

react 中我们常用的 hook

React目前提供的Hook

  • useState: 设置和改变state,代替原来的state和setState
  • useEffect: 代替原来的生命周期componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版
  • useLayoutEffect : 与 useEffect 作用相同,但它会同步调用 effect(当useEffect里面的操作DOM,并且会改变页面的样式,就需要用这个,否则可能会出现闪屏问题)
  • useMemo: 控制组件更新条件,可根据状态变化控制方法执行,优化传值
  • useCallback: useMemo优化传值,usecallback优化传的方法,是否更新
  • useRef : 跟以前的ref,一样,只是更简洁了
  • useContext:上下文爷孙及更深组件传值
  • useReducer:代替原来redux里的reducer,配合useContext一起使用
1、useState:
import React from 'react';
import './App.css';
//通常的class写法,改变状态
class App extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      hook:'react hook'
    }
  }
  changehook = () => {
    this.setState({
      hook:'我改变了react hook 的值'
    })
  }
  render () {
    const { hook } = this.state
    return(
         <header className="App-header">
          {hook}
          <button onClick={this.changehook}>
            改变hook
          </button>
        </header>
      )
  }
}
export  {App}
 
//函数式写法,改变状态
function App() {
//创建了一个叫hook的变量,sethook方法可以改变这个变量,初始值为‘react hook’
 const [hook, sethook] = useState("react hook");
  return ( 
    <header className="App-header">
      {hook}{/**这里的变量和方法也是可以直接使用的 */}
      <button onClick={() => sethook("我改变了react hook 的值")}>
        改变hook
      </button>
    </header>
  );
}
export  {App}
 
//箭头函数的函数写法,改变状态
export const App = props => {
  const [hook, sethook] = useState("react hook");
  return (
    <header className="App-header">
      {hook}
      <button onClick={() => sethook("我改变了react hook 的值")}>
        改变hook
      </button>
    </header>
  );
};
2.useEffect & useLayoutEffect

useEffect:
可以让你在函数组件中执行副作用操作
取代生命周期componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版
useEffect( ()=>{ return ()=>{ } } , [ ])第一个参数,是函数,默认第一次渲染和更新时都会触发,默认自带一个return ,return一个函数表示可以再销毁之前可以处理些事情,React 会在组件卸载的时候执行清除操作,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除
第二个参数,数组[],空的时候表示只执行一次,更新时不触发,里面的参数是什么,当参数变化时才会执行
useEffect

useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。 React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。 useEffect 会在每次渲染后都执行 React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
useEffect可以多次使用,按照先后顺序执行


import React, { useState, useEffect, useLayoutEffect } from 'react';
 
//箭头函数的写法,改变状态
const UseEffect = (props) => {
    //创建了一个叫hook的变量,sethook方法可以改变这个变量,初始值为‘react hook 是真的好用啊’
    const [ hook, sethook ] = useState('react hook 是真的好用啊');
    const [ name ] = useState('baby张');
    return (
        <header className="UseEffect-header">
            <h3>UseEffect</h3>
            <Child hook={hook} name={name} />
            {/**上面的变量和下面方法也是可以直接使用的 */}
            <button onClick={() => sethook('我改变了react hook 的值' + new Date().getTime())}>改变hook</button>
        </header>
    );
};
 
const Child = (props) => {
    const [ newhook, setnewhook ] = useState(props.hook);
    //这样写可以代替以前的componentDidMount,第二个参数为空数组,表示该useEffect只执行一次
    useEffect(() => {
        console.log('first componentDidMount');
    }, []);
 
    //第二个参数,数组里是hook,当hook变化时,useEffect会触发,当hook变化时,先销毁再执行第一个函数。
    useEffect(
        () => {
            setnewhook(props.hook + '222222222');
            console.log('useEffect');
            return () => {
                console.log('componentWillUnmount ');
            };
        },
        [ props.hook ]
    );
 
    //useLayoutEffect 强制useeffect的执行为同步,并且先执行useLayoutEffect内部的函数
    useLayoutEffect(
        () => {
            console.log('useLayoutEffect');
            return () => {
                console.log('useLayoutEffect componentWillUnmount');
            };
        },
        [ props.hook ]
    );
 
    return (
        <div>
            <p>{props.name}</p>
            {newhook}
        </div>
    );
};
 
export default UseEffect;

提示: 使用多个 Effect 实现关注点分离
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
可看官方文档示例

为什么每次更新的时候都要运行 Effect?
在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:

 componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

但是当组件已经显示在屏幕上时,friend prop 发生变化时会发生什么?
我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。
在 class 组件中,我们需要添加 componentDidUpdate 来解决这个问题:

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // 取消订阅之前的 friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 订阅新的 friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }


忘记正确地处理 componentDidUpdate 是 React 应用中常见的 bug 来源。
它并不会受到此 bug 影响。(虽然我们没有对它做任何改动。)
并不需要特定的代码来处理更新逻辑,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理
现在看一下使用 Hook 的版本:
unction FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

通过跳过 Effect 进行性能优化:
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。
在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
React 会跳过这个 effect,这就实现了性能的优化。
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新



注意:
1、如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。
2、如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况
3、如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMount 和 componentWillUnmount 思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。
官方推荐:启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。
差异:

useLayoutEffect (留一个问题,大家自己试下,目前没有真正复现阻塞的情况???)

  • useEffect 是异步执行的,而useLayoutEffect是同步执行的。
  • useEffect 的执行时机是浏览器完成渲染之后
    useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount 等价。可以使用它来读取 DOM 布局并同步触发重渲染。
  • 强制useeffect的执行为同步,并且先执行useLayoutEffect内部的函数
  • 他还可以绑定触发更新的依赖状态,默认是状态中任何数据发生变化副作用都会执行,
    第二个参数传入需要绑定的状态,可绑定多个
  • 尽可能使用标准的 useEffect 以避免阻塞视觉更新。

因为client模式useEffect useLayoutEffect在一个时间段,所以无感知

3.useContext() 共享状态钩子

建议先学习react高级指引Context用法,便于理解

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
export const obj = {
  name: 'xuxiaoli',
  age: 18,
  sex: 'girl'
}
const ThemeContext = React.createContext({
  // obj
});
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: obj
    }
  }
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      // <ThemeContext.Provider value="dark">
      <ThemeContext.Provider value={this.state.theme}>
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  // static contextType = ThemeContext;
  render() {
    console.log(ThemeContext)
    console.log(this.context)

    return (
      <div>
        <div>{this.context.name}</div>

      </div>
    )
    // return <Button theme={this.context} />;
  }
}
ThemedButton.contextType = ThemeContext

// 函数组件使用:

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

示例:import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // Theme Toggler 按钮不仅仅只获取 theme 值,
  // 它也从 context 中获取到一个 toggleTheme 函数
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

如果需要在深层次组件之间共享状态,可以使用 useContext()。context 提供了一种在组件之间共享 props 的方式,而不必显示地通过组件树的逐层传递 props; useContext 钩子比原始 class 组件中使用 context 更为方便;

const value = useContext(MyContext);

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染

  • 别忘记 useContext 的参数必须是 context 对象本身:useContext(MyContext),相当class组件的static contextType = MyContext 或者 <MyContext.Consumer>

  • 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。

  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

假设现在有俩个组件 Navbar 和 Messages,我们希望它们之间共享状态。

<div className="test">
  <Navbar />
  <Messages />
</div>

使用方法如下: 第一步在它们的父组件上使用 React 的 Context API,在组件外部建立一个 Context。

import React, { useContext } from 'react';

const TestContext = React.createContext({});

const Navbar = () => {
  const { username } = useContext(TestContext);

  return (
    <div className="navbar">
      <p>{username}</p>
    </div>
  );
};

const Messages = () => {
  const { messageDetail } = useContext(TestContext);

  return (
    <div className="messages">
      <p>1 message for {messageDetail}</p>
    </div>
  );
};

const App = () => {
  return (
    <TestContext.Provider
      value={{
        username: 'superawesome',
        messageDetail: 'hello world',
      }}
    >
      <div className="test">
        <Navbar />
        <Messages />
      </div>
    </TestContext.Provider>
  );
};
export default App;

等价的 class 用例

import React from 'react';

const TestContext = React.createContext({});

const Navbar = () => (
  <TestContext.Consumer>
    {({ username }) => (
      <div className="messages">
        <p>1 message for {username}</p>
      </div>
    )}
  </TestContext.Consumer>
);

const Messages = () => {
  return (
    <TestContext.Consumer>
      {({ messageDetail }) => (
        <div className="messages">
          <p>1 message for {messageDetail}</p>
        </div>
      )}
    </TestContext.Consumer>
  );
};
// class Messages extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  // static contextType = TestContext;
  // render() {
  //  return <Button theme={this.context} />;
  //}
//}



class App extends React.Component {
  render() {
    return (
      <TestContext.Provider
        value={{
          username: 'superawesome',
          messageDetail: 'hello world',
        }}
      >
        <div className="test">
          <Navbar />
          <Messages />
        </div>
      </TestContext.Provider>
    );
  }
}
export default App;
4.useReducer

const [state, dispatch] = useReducer(reducer, initialCount, init);

hook使用了useState,但是很多的时候,一条一条的写很麻烦。忍不了了~
useReducer他来了,改变状态只需要发送一个action即可
这里的useReducer会返回state和dispatch,通过context传递到子组件,然后直接调用state或者触发reducer,我们常用useReducer 与useContext createContext一起用,模拟reudx的传值和重新赋值操作。

function init(initialCount) {
  return {count: initialCount};
}


function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}
// 惰性初始化
// 你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。
// 这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useReducer可以接受两个参数

  • initialState: 初始化的state
  • reducer处理action的纯函数。

【注意】:reducer中不建议去做逻辑运算,会使你的代码看起来很乱

useReducer返回两个参数

  • 当前时刻的state
  • 负责发送action的dispach函数

想要改变某个state?很简单,你只需要用dispach向useReducer发送一个action,然后用action.type来区分reducer收到这个action后的行为即可。

5、useMemo & useCallback
( 在DOM更新也就是渲染前触发的, 可以解决性能优化问题;区别在于useMemo返回的是函数运行的结果,useCallback返回的是函数。)
他们都可以用来优化子组件的渲染问题,或者监听子组件状态变化来处理事件,这一点在以前是很难做到的,因为shouldComponentUpdate 里能监听到是否变化,但没法控制其他的外部方法,只能返回true和false,而componentDidUpdate只能在更新后执行,所以想在渲染之前做些事情就不好搞了。

5、useCallback

返回一个 memoized 回调函数。把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。


const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);


useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
6、useMemo
  • 返回一个 memoized 值。把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
  • 记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

let isHome = useMemo(() => {
        return url === '/home';
    }, [url]);
    

。。。更多其他

自定义hook:

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}


-与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 Hook 的规则
-自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
-在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
-自定义 Hook 如何获取独立的 state?每次调用 Hook,它都会获取独立的 state。


在这里插入图片描述

// mobox redux,redux-thunk
react源码分析,学习中
React Hooks源码解析
~~

React开发者性能优化建议

1. 组件卸载前进行清理操作

在组件中为 window 注册的全局事件, 以及定时器, 在组件卸载前要清理掉, 防止组件卸载后继续执行影响应用性能.
需求:使用 useState 保存一个数值,然后开启定时器改变数值,卸载组件查看定时器是否还在运行。

import React, { useState, useEffect } from "react"
import ReactDOM from "react-dom"

const App = () => {
  let [index, setIndex] = useState(0)
  useEffect(() => {
    let timer = setInterval(() => {
      console.log(index)
      setIndex(++index)
    }, 1000)
    return () => clearInterval(timer)
  }, [])
  return (
    <button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById("root"))}>
      {index}
    </button>
  )
}

export default App

2. 使用纯组件降低组件重新渲染的频率

什么是纯组件
相同的输入 (state、props) 呈现相同的输出. 在输入内容相同的情况下纯组件不会被重新渲染.
如何实现纯组件
React 提供了 PureComponent 类, 类组件在继承它以后, 类组件就变成了纯组件. 纯组件会对 props 和 state 进行浅层比较, 如果上一次的 props、state 和下一次的 props、state 相同, 则不会重新渲染组件.
什么是浅层比较
浅比较指的是比较基本数据类型是否具有相同的值, 比如1是否等于1, true是否等于true. 比较复杂数据类型的第一层值是否相同。
浅层比较难道没有性能消耗吗
和进行 diff 比较操作相比,浅层比较将消耗更少的性能。diff 操作会重新遍历整颗 virtualDOM 树, 而浅层比较值操作当前组件的 state 和 props。
需求:在状态对象中存储 name 值为张三,组件挂载完成后将 name 属性的值再次更改为张三,然后分别将 name 传递给纯组件和非纯组件,查看结果。

import React from "react"
export default class App extends React.Component {
  constructor() {
    super()
    this.state = {name: "张三"}
  }
  updateName() {
    setInterval(() => this.setState({name: "张三"}), 1000)
  }
  componentDidMount() {
    this.updateName()
  }
  render() {
    return (
      <div>
        <RegularComponent name={this.state.name} />
        <PureChildComponent name={this.state.name} />
      </div>
    )
  }
}

class RegularComponent extends React.Component {
  render() {
    console.log("RegularComponent")
    return <div>{this.props.name}</div>
  }
}

class PureChildComponent extends React.PureComponent {
  render() {
    console.log("PureChildComponent")
    return <div>{this.props.name}</div>
  }
}

纯组件只渲染一次,非纯组件会一秒打印一次

3. 使用 React.memo 进行组件缓存

React.memo 是一个高阶组件.
什么是高阶组件 Higher Order Component ( HOC )

高阶组件是 React 应用中共享代码, 增加逻辑复用的一种方式. 比如 A 组件和 B 组件都需要使用一个相同的逻辑,如何将逻辑抽取到一个公共的地方呢?答案就是使用高阶组件。
高阶组件的核心思想就是在组件的外层再包裹一层执行逻辑的组件,在外层组件中执行逻辑,再将逻辑执行的结果传递到内层组件。
高阶组件形式是一个函数,接收组件作为参数, 返回一个新的组件. 参数组件就是需要复用逻辑的组件,函数内部返回的新组件就是执行逻辑的组件,在新组件内部执行完逻辑以后再调用参数组件并将逻辑结果传递给参数组件。图片
函数名通常以 with 开头, 接收的组件形参名称为 WrappedComponent, 返回的组件名称和函数名称一样, 只不过 with 中的 w 要大写.

import React from "react"

export class A extends React.Component {
  render() {
    return <div>{this.props.sizes.join("-")}</div>
  }
}
export class B extends React.Component {
  render() {
    return <div>{this.props.sizes.join("-")}</div>
  }
}

const WrappedA = withResizable(A)
const WrappedB = withResizable(B)
function withResizable(WrappedComponent) {
  class WithResizable extends React.Component {
    constructor() {
      super()
      this.state = {
        sizes: [window.innerWidth, window.innerHeight]
      }
      this.updateSizes = this.updateSizes.bind(this)
    }
    updateSizes() {
      this.setState({
        sizes: [window.innerWidth, window.innerHeight]
      })
    }
    componentDidMount() {
      window.addEventListener("resize", this.updateSizes)
    }
    render() {
      return <WrappedComponent sizes={this.state.sizes} />
    }
  }
  return WithResizable
}
import React from "react"

export default class App extends React.Component {
  render() {
    return (
      <div>
        <WrappedA />
        <WrappedB />
      </div>
    )
  }
}

React.memo 介绍

与 PureComponent 相似, 如果上一次输入的 props 和下一次输入的 props 相同, 则组件不会重新渲染, 从而使组件更高效.
PureComponent 应用于类组件, React.memo 应用于函数组件.
React.memo 使用的也是使用浅层比较.
React.memo 使用

import React, { useState, useEffect, memo } from "react"

export default function App() {
  let [index, setIndex] = useState(0)
  let [name] = useState("张三")
  useEffect(() => {
    let timer = setInterval(() => {
      setIndex(++index)
    }, 1000)
    return () => clearInterval(timer)
  }, [])
  return (
    <>
      {index}
      <ShowNameMemo name={name} />
    </>
  )
}

function ShowName(props) {
  console.log("ShowName")
  return <div>{props.name}</div>
}

const ShowNameMemo = memo(ShowName)

为 memo 传递比较逻辑

memo 方法可以自定义比较逻辑,如果为组件传递的数据涉及嵌套层次,就可以自定义比较逻辑。memo 方法的第二个参数即为比较函数.比较函数的第一个参数为上一次的 props, 比较函数的第二个参数为下一次的 props, 比较函数返回 true, 不进行渲染, 比较函数返回 false, 组件重新渲染.

import React, { useState, useEffect } from "react"

const Home = () => {
  let [index, setIndex] = useState(0)
  useEffect(() => {
    const timer = setInterval(() => {
      setIndex(++index)
    }, 1000)
    return () => clearInterval(timer)
  })

  return (
    <>
      <div>{index}</div>
      <ShowPersonMemoComponent person={{ name: "张三", age: 20 }} />
    </>
  )
}

export default Home
// props => {person: {name: "张三", age: 20}}
const ShowPersonComponent = ({ name, age }) => {
  console.log("ShowNameComponent Rendering")
  return <div>{name} {age}</div>
}

const ShowPersonMemoComponent = React.memo(ShowPersonComponent, comparePerson)

function comparePerson(prevProps, nextProps) {
  if (prevProps.name === nextProps.name && prevProps.age === nextProps.age) {
    return true
  }
  return false
}

4. shouldComponentUpdate 减少组件渲染频率

shouldComponentUpdate 是类组件的生命周期函数, 在组件 props 或者 state 发生改变后调用. 函数默认返回 true, 重新渲染组件, 返回 false, 阻止组件重新渲染.
函数的第一个参数为 nextProps, 第二个参数为 nextState.
需求: 在页面中展示员工信息, 员工信息包括, 姓名, 年龄, 薪水, 职位等等. 但是在页面中只想展示姓名和年龄. 也就是说只有姓名和年龄发生变化时才有必要重新渲染组件, 如果员工的其他信息发生了变化没必要重新渲染组件.

import React from "react"

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {name: "张三", age: 20, job: "waiter"}
  }
  componentDidMount() {
    setTimeout(() => this.setState({ job: "chef" }), 1000)
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.state.name !== nextState.name || this.state.age !== nextState.age) {
      return true
    }
    return false
  }

  render() {
    console.log("rendering")
    let { name, age } = this.state
    return <div>{name} {age}</div>
  }
}

5. 使用组件懒加载

路由组件懒加载

import { lazy, Suspense } from "react"
// import About from "./pages/About"
const About = lazy(() => import(/* webpackChunkName: "about" */ "./pages/About"))

function App() {
  return (
   <Suspense fallback={<div>loading....</div>}>
      <About />
  	</Suspense>
  )
}
export default App

根据条件进行组件懒加载

适用于组件不会随条件频繁切换。好处是可以将两个组件全部从 bundle 中分离出来,减少 bundle 文件的体积。

import { lazy, Suspense } from "react"

export default function About() {
  let LazyComponent = null
  if (true) {
    LazyComponent = lazy(() => import(/* webpackChunkName: "list" */ "../components/List"))
  } else {
    LazyComponent = lazy(() => import(/* webpackChunkName: "news" */ "../components/News"))
  }
  return (
    <div>
      about page works
      <Suspense fallback={<div>loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  )
}

6. 使用 Fragment 以避免额外的标记

React 组件中返回的 jsx 如果有多个同级元素, 多个同级元素必须要有一个共同的父级.
function App() {
return (


message a

message b


)
}
为了满足这个条件我们通常都会在最外层添加一个div, 但是这样的话就会多出一个无意义的标记, 如果每个组件都多出这样的一个无意义标记的话, 浏览器渲染引擎的负担就会加剧.也可能产生意外的bug
为了解决这个问题,:React 推出了 fragment 占位符标记. 使用占位符标记既满足了拥有共同父级的要求又不会多出额外的无意义标记.

import { Fragment } from "react"

function App() {
  return (
    <Fragment>
      <div>message a</div>
      <div>message b</div>
    </Fragment>
  )
}
function App() {
  return (
    <>
      <div>message a</div>
      <div>message b</div>
    </>
  )
}

7. 不要使用内联函数定义 ??

什么是内联函数?

 <div>
        
        {/* 1. 一个内联的“DOM组件”事件处理程序 */}
        <button
          onClick={() => {
            this.setState({ clicked: true })
          }}
        >
          Click!
        </button>
        
        {/* 2. 一个“自定义事件”或“操作” */}
        <Sidebar onToggle={(isOpen) => {
          this.setState({ sidebarIsOpen: isOpen })
        }}/>
        
        {/* 3. 一个 render prop 回调 */}
        <Route
          path="/topic/:id"
          render={({ match }) => (
            <div>
              <h1>{match.params.id}</h1>}
            </div>
          )
        />
      </div>

在使用内联函数后, render 方法每次运行时都会创建该函数的新实例, 导致 React 在进行 Virtual DOM 比对时, 新旧函数比对不相等,导致React 总是为元素绑定新的函数实例, 而旧的函数实例又要交给垃圾回收器处理.

import React from "react"

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {
      inputValue: ""
    }
  }
  render() {
    return (
      <input
        value={this.state.inputValue}
        onChange={e => this.setState({ inputValue: e.target.value })}
        />
    )
  }
}

正确的做法是在组件中单独定义函数, 将函数绑定给事件.

import React from "react"

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {
      inputValue: ""
    }
  }
  setInputValue = e => {
    this.setState({ inputValue: e.target.value })
  }
  render() {
    return (
      <input value={this.state.inputValue} onChange={this.setInputValue} />
    )
  }
}

8. 在构造函数中进行函数this绑定

在类组件中如果使用 fn() {} 这种方式定义事件函数, 事件函数 this 默认指向 undefined. 也就是说函数内部的 this 指向需要被更正.
可以在构造函数中对函数的 this 进行更正, 也可以在行内进行更正, 两者看起来没有太大区别, 但是对性能的影响是不同的.

export default class App extends React.Component {
  handleClick() {
    console.log(this)
  }
  render() {
    return <button onClick={this.handleClick.bind(this)}>按钮</button>
  }
}

上面代码的问题是, render 方法每次执行时都会调用 bind 方法生成新的函数实例.

export default class App extends React.Component {
  constructor() {
    super()
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    console.log(this)
  }
  render() {
    return <button onClick={this.handleClick}>按钮</button>
  }
}

上面代码优化了在每次调用 render 方法时都生成新函数实例的问题, 构造函数只执行一次, 所以函数 this 指向更正的代码也只执行一次.

9. 类组件中的箭头函数

在类组件中使用箭头函数不会存在 this 指向问题, 因为箭头函数本身并不绑定 this.

export default class App extends React.Component {
  handleClick = () => console.log(this)
  render() {
    return <button onClick={this.handleClick}>按钮</button>
  }
}

箭头函数在 this 指向问题上占据优势, 但是同时也有不利的一面.
当使用箭头函数时, 该函数被添加为类的实例对象属性, 而不是原型对象属性. 如果组件被多次重用, 每个组件实例对象中都将会有一个相同的函数实例, 降低了函数实例的可重用性造成了资源浪费.
综上所述, 更正函数内部 this 指向的最佳做法仍是在构造函数中使用 bind 方法进行绑定

10. 避免使用内联样式属性

当使用内联 style 为元素添加样式时, 内联 style 会被编译为 JavaScript 代码, 通过 JavaScript 代码将样式规则映射到元素的身上, 浏览器就会花费更多的时间执行脚本和渲染 UI, 从而增加了组件的渲染时间.

function App() {
  return <div style={{ backgroundColor: "skyblue" }}>App works</div>
}

在上面的组件中, 为元素附加了内联样式, 添加的内联样式为 JavaScript 对象, backgroundColor 需要被转换为等效的 CSS 样式规则, 然后将其应用到元素, 这样涉及到脚本的执行.
更好的办法是将 CSS 文件导入样式组件.

11. 优化条件渲染(减少组件挂载和卸载的次数.)

频繁的挂载和卸载组件是一项耗性能的操作, 为了确保应用程序的性能, 应该减少组件挂载和卸载的次数.
在 React 中我们经常会根据条件渲染不同的组件. 条件渲染是一项必做的优化操作.

function App() {
  if (true) {
    return (
      <>
        <AdminHeader />
        <Header />
        <Content />
      </>
    )
  } else {
    return (
      <>
        <Header />
        <Content />
      </>
    )
  }
}

在上面的代码中, 当渲染条件发生变化时, React 内部在做 Virtual DOM 比对时发现, 刚刚第一个组件是 AdminHeader, 现在第一个组件是 Header, 刚刚第二个组件是 Header, 现在第二个组件是 Content, 组件发生了变化, React 就会卸载 AdminHeader、Header、Content, 重新挂载 Header 和 Content, 这种挂载和卸载就是没有必要的.

function App() {
  return (
    <>
      {true && <AdminHeader />}
      <Header />
      <Content />
    </>
  )
}

12、不要在 render 方法中更改应用状态 当应用程序状态发生更改时, React 会调用 render 方法, 如果在 render 方法中继续更改应用程序状态, 就会发生 render 方法递归调用导致应用报错.

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {name: "张三"}
  }
  render() {
    this.setState({name: "李四"})
    return <div>{this.state.name}</div>
  }
}

与其他生命周期函数不同, render 方法应该被作为纯函数. 这意味着,在 render 方法中不要做以下事情, 比如:

  • 不要调用 setState 方法,
  • 不要使用其他手段查询更改原生 DOM 元素,
  • 以及其他更改应用程序的任何操作. render 方法的执行要根据状态的改变,
    这样可以保持组件的行为和渲染方式一致.

13. 为组件创建错误边界(Error Boundaries)

默认情况下, 组件渲染错误会导致整个应用程序中断, 创建错误边界可确保在特定组件发生错误时应用程序不会中断.
错误边界是一个 React 组件, 可以捕获子级组件在渲染时发生的错误, 当错误发生时, 可以将错误记录下来, 可以显示备用 UI 界面.

  • 错误边界涉及到两个生命周期函数, 分别为 getDerivedStateFromError 和 componentDidCatch.
  • 使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。
    (1) getDerivedStateFromError 为静态方法, 方法中需要返回一个对象, 该对象会和state对象进行合并, 用于更改应用程序状态.
    (2)componentDidCatch 方法用于记录应用程序错误信息. 该方法的参数就是错误对象.
// ErrorBoundaries.js
import React from "react"
import App from "./App"

export default class ErrorBoundaries extends React.Component {
  constructor() {
    super()
    this.state = {
      hasError: false
    }
  }
  componentDidCatch(error) {
    console.log("componentDidCatch")
  }
  static getDerivedStateFromError() {
    console.log("getDerivedStateFromError")
    return {
      hasError: true
    }
  }
  render() {
    if (this.state.hasError) {
      return <div>发生了错误</div>
    }
    return <App />
  }
}
// App.js
import React from "react"

export default class App extends React.Component {
  render() {
    // throw new Error("lalala")
    return <div>App works</div>
  }
}
// index.js
import React from "react"
import ReactDOM from "react-dom"
import ErrorBoundaries from "./ErrorBoundaries"

ReactDOM.render(<ErrorBoundaries />, document.getElementById("root"))

注意: 错误边界不能捕获异步错误, 比如点击按钮时发生的错误.
【注意】
错误边界无法捕获以下场景中产生的错误:

  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

14. 避免数据结构突变

组件中 props 和 state 的数据结构应该保持与原来的一致, 数据结构突变会导致输出不一致.

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {
      employee: {
        name: "张三",
        age: 20
      }
    }
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    this.setState({
      employee: {
        name: "李四"
      }
    })
  }
  render() {
    const { name, age } = this.state.employee
    return (
      <div>
        <div>{name} {age}</div>
        <button onClick={this.handleClick}>更改</button>
      </div>
    )
  }
}

15. 为列表数据添加唯一标识:key

当需要渲染列表数据时, 我们应该为每一个列表项添加 key 属性, key 属性的值必须是唯一的.

  • 避免遍历 Virtual DOM 查找变化所带来的性能消耗.
  • 避免了元素因为位置变化而导致的重新创建.
  • 避免意外的bug产生

16. 节流和防抖

17. 外部资源使用 CDN 加载

  • 什么是 CDN (Content Delivery Network) ?

内容交付网络(CDN)指的是地理上分散的服务器组, 它们一起工作以提供网络内容的快速交付.

  • 使用 CDN 有什么好处 ?
    浏览器对于同一个域下的并发请求有数量上的限制. 主流浏览器的同一域下的并发请求数量是 6 或 10. 意味着超过并发数量的其他资源需要等待, 这就增加了应用呈递的时间. 我们可以将不同的资源放在不同的 CDN 中, 这样就可以突破浏览器的并发限制, 加入应用的呈递速度.
    CDN 通常由大型公司托管, 在全球都分布了数据中心, 当你向 CDN 发送请求时, 它会通过离你最近的数据中心进行响应, 减少网络延迟, 增加程序性能.
    缓存使访问速度更加快速.

提问:当可以npm,也可以cdn引入,你们怎么选择的?

18. 依赖优化

在应用程序中经常会依赖第三方包, 但我们不想引用包中的所有代码, 我们只想用到哪些代码就包含哪些代码. 此时可以使用插件对依赖项进行优化. 优化资源
当前我们就使用 lodash 举例子. 应用基于 create-react-app 脚手架创建。
下载依赖 npm install react-app-rewired customize-cra lodash babel-plugin-lodash
1.react-app-rewired: 覆盖 crate-react-app 的默认配置

module.exports = function (oldConfig) {
  return newConfig
}

// 参数中的 config 就是默认的 webpack config
2. customize-cra: 导出了一些辅助方法, 可以让以上写法更加简洁

const { override, useBabelRc } = require("customize-cra")
module.exports = override(
  (oldConfig) => newConfig,
  (oldConfig) => newConfig
)

override:可以接收多个参数, 每个参数都是一个配置函数, 函数接收 oldConfig, 返回 newConfig

useBabelRc: 允许使用 .babelrc 文件进行 babel配置
  1. babel-plugin-lodash: 对应用中的 lodash 进行精简
    在项目的根目录下新建 config-overrides.js 并加入配置代码
    const { override, useBabelRc } = require(“customize-cra”)

module.exports = override(useBabelRc())
修改 package.json 文件中的构建命令
“scripts”: {
“start”: “react-app-rewired start”,
“build”: “react-app-rewired build”,
“eject”: “react-scripts eject”
}
创建 .babelrc 文件并加入配置
{
“plugins”: [“lodash”]
}
生产环境下的三种 JS 文件
main.[hash].chunk.js: 这是你的应用程序代码, App.js 等.
1.[hash].chunk.js: 这是第三方库的代码, 包含你在 node_modules 中导入的模块
runtime~main.[hash].js webpack运行时代码

19、深入浅出webpack之externals的使用

  • 如果不想把第三方库打包到bundle中,这就有了externals。官方的使用externals比较简单
  • 官网文档解释的很清楚:就是webpack可以不处理应用的某些依赖库,使用externals配置后,依旧可以在代码中通过CMD、AMD或者window/global全局的方式访问
  • 使用:
    1.在HTML中引入第三方库的cdn
    2.在webpack中配置externals
externals: {
 jquery: "jQuery",
 lodash: {
   commonjs: 'lodash',
   commonjs2: 'lodash',
   amd: 'lodash',
   root: '_'
 }
}

3、在js中引用

const $ = require("jquery");
$("#app").html("<h1>hello world</h1>");

20、在React中使用 Ant Design

网上给出方案:
可参考官网:
https://ant.design/docs/react/introduce-cn

1、安装

npm i antd --save

2:按需导入 Antd 的组件

import {DatePicker} from ‘antd’

注意:import ‘antd/dist/antd.css’ //全局导入所有的Antd 样式表 (不推荐!全局导入这种太占用打包文件的体积了,推荐使用按需加载css样式)
3:调用render函数渲染

ReactDOM.render(
,document.getElementById(‘app’)
)

4、按需导入css实现方式

npm i babel-plugin-import -D

5、打开.babelrc文件, 在plugin[ ]数组里面进行配置

{undefined
“plugin” : [ [import,{“libraryName”:“antd”,“libraryDirectory”:“es”,“style”:“css”}] ] //可去参考Ant desinn官网
}

注意 : 配置好cnpm i babel-plugin-import之后,我们就不需要手动导入 样式表了 这个插件会帮我们处理

无效:env配置

// 关闭source map
GENERATE_SOURCEMAP=false

// 关闭自动打开浏览器
// BROWSER=none

// 本地host
HOST=127.0.0.1

//本地端口
PORT=9001

// 公共路径
PUBLIC_URL="./"
// 将所有构建文件放入文件夹
BUILD_PATH=dist

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值