GithubHelp home page GithubHelp logo

blog's Introduction

我的技术博客

这个项目是我brickspert(砖家) 的技术博客,使用了 GitHub 的 issues 区域来作为博文发布区。

进入博客

您的Star是我进行创作的动力!

目录

  1. 从零搭建React全家桶框架教程 2017-09-02
  2. 助你完全理解React高阶组件(Higher-Order Components) 2017-09-09
  3. react-router v4 使用 history 控制路由跳转 2017-09-12
  4. react-family框架兼容IE8教程 2017-09-19
  5. 前端小白半年准备进BAT 2018-04-09
  6. CSS单边颜色渐变倒计时圆环实现 2018-08-20
  7. 完全理解 redux(从零实现一个 redux) 2018-11-11
  8. React Hooks 原理 2019-04-11
  9. React Custom Hooks 最佳实践 2019-10-13
  10. antd 自定义 Icon 的几种方式及其优劣 2019-12-08
  11. Umi Hooks - 助力拥抱 React Hooks 2020-01-17
  12. useRequest-蚂蚁中台标准请求 Hooks 2020-02-13
  13. React 项目性能分析及优化 2020-03-30
  14. Pull Request 与 Merge Request 的区别 2020-05-07
  15. Recoil - Facebook 官方 React 状态管理器 2020-05-17
  16. 基于微前端的大型中台项目融合方案 2020-08-28
  17. React Hooks 在 react-refresh 模块热替换(HMR)下的异常行为 2021-05-10
  18. React Hooks 在 SSR 模式下常见问题及解决方案 2021-05-17
  19. React Hooks 使用误区,驳官方文档 2021-12-27
  20. 如何升级到 React 18 2022-03-24
  21. React 18 对 Hooks 的影响:一 2022-03-31
  22. React 18 总览 2022-04-15
  23. 我认为 web3 是什么(大白话 web3) 2022-04-28
  24. React useEvent:砖家说的没问题 2022-05-15
  25. Base64 编码原来这么简单 2022-06-05
  26. 我要去哪里?- 写在我的 30 岁 2022-07-10
  27. 前端好还是后端好,看看7年前端和后端怎么说 2022-08-21
  28. 只想做开源项目、技术项目,不想做业务,有办法吗? 2022-08-28
  29. 前端小白半年准备进大厂 2022-09-04
  30. 程序员如何实现财富自由 2022-09-12
  31. 离职后聊一聊我眼中的蚂蚁 2022-10-26
  32. 前端工程师个人的价值在哪里(换一个人能不能做?)【前端晋升必看】 2022-11-06
  33. 2022 年:我在死亡边缘走过 2023-01-02
  34. 前端质量体系之纸上谈兵 2023-02-19
  35. 大白话虚拟货币理财原理 2023-05-28
  36. 前端没了?也许是刚开始 2023-06-18
  37. Telegram bot 和 mini apps 开发简易教程 2023-10-10

建博初衷

  1. 有些东西看能看懂,会写。但是给能给别人讲明白又是另外一回事了。通过写文章,能更加深入了解各个知识点。
  2. 经常查问题的时候,看到有些人写的文章,满篇名词,晦涩难懂,看了简直要抓狂了,当时的感觉就是,给自己一巴掌,怎么这么笨?给博主一巴掌,能讲人话吗!所以我致力于写最通俗易懂的博客。像阮一峰大神的博客一样。

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

blog's People

Contributors

brickspert avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

CSS单边颜色渐变倒计时圆环实现

CSS单边颜色渐变倒计时圆环实现

工作中需要实现尾部红色警告的一个圆环倒计时,网上搜了一圈,同时满足css单边颜色渐变圆形的案例还真没有,光单边颜色渐变的案例都几乎没有。那我自己实现一个吧,不做不知道,一做吓一跳,竟然花了好几个小时才完成,特此记录一下,有缘人拿去。

直接上结果图

drawing

1. 拆解

这个进度条可以拆解成两部分

  1. 画一个三边绿色,一边渐变的圆环
  2. 灰色进度条按进度覆盖在彩色的圆环上面。

2. 单边渐变的圆环

思考下思路:一个盒子,三个边是绿色,一个边是绿色到红色的渐变色,然后用border-radius弯曲成一个圆。

哈哈,这么一想,好简单啊。

but,but,只有单边颜色渐变用css是没法实现的。吐血~,不信你去试试,去查查。

难点就在如何实现单边颜色渐变这里。

follow me~

2.1 三边绿色,一边透明的圆环

这步非常简单

drawing

  <div class='box'>
    <div class='green-border'></div>
  </div>
  
  <style>
    *{
      box-sizing: border-box;
    }
    .box {
      width: 240px;
      height: 240px;
    }

    .green-border {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      border: 20px solid #00a853;
      border-bottom-color: transparent;
      transform: rotate(45deg);
    }
  </style>

2.2 渐变块实现

难点就在这里,我们画一个从上到下渐变的方块,放在空白圆环那里。

drawing

<div class='red-gradients'></div>
 
 <style>
    .box{
        position: relative;
    }
    .red-gradients {
      width: 120px;
      height: 120px;
      background: linear-gradient(to right, #00a853, #F04134);
      position: absolute;
      bottom: 0;
      left: 0;
      z-index: 1;
    }
  </style>  

2.3 覆盖多余的内容

接下来我们要覆盖多余的内容,圆内放一个div,盖住多余的部分。外面的通过boxoverflow:hidden来隐藏。

<div class='inner-circle'></div>

  <style>
  	.box{
      border-radius: 50%;
      overflow: hidden;
  	}
    .inner-circle {
      width: 200px;
      height: 200px;
      border-radius: 50%;
      position: absolute;
      z-index: 2;
      top: 20px;
      left: 20px;
      background-color: white;
    }
  </style>

大功告成了,真是机智!

灰色动态进度条

接下来我们讲讲如何实现灰色动态进度条。

算了,不写了~网上讲圆环进度条的一大堆,我就不重复讲了,随便找个例子推荐下:https://www.xiabingbao.com/css/2015/07/27/css3-animation-circle.html

完整源码在这里,祝你好运!

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

第三种hack的疑问

你好,我想问一下在用的时候不需要npm install --save history 这个包就可以直接用吗?谢谢

从零搭建React全家桶框架教程

从零搭建React全家桶框架教程

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

源码地址:https://github.com/brickspert/react-family
提问反馈:blog

因为本教程写于2017年9月,然而前端技术发展太快了。有些库的版本一直在升级,所以你如果碰到奇怪的问题,请先检查下安装的库版本是否和我源码中的一样。please~

大家阅读的时候,照着目录来阅读哦,有些章节不在文章里面。要点链接的~

目录

  1. 写在前面
  2. 说明
  3. init项目
  4. webpack
  5. babel
  6. react
  7. 命令优化
  8. react-router
  9. webpack-dev-server
  10. 模块热替换(Hot Module Replacement)
  11. 文件路径优化
  12. redux
  13. devtool优化
  14. 编译css
  15. 编译图片
  16. 按需加载
  17. 缓存
  18. HtmlWebpackPlugin
  19. 提取公共代码
  20. 生产坏境构建
  21. 文件压缩
  22. 指定环境
  23. 优化缓存
  24. public path
  25. 打包优化
  26. 抽取css
  27. 使用axiosmiddleware优化API请求
  28. 合并提取webpack公共配置 webpack-common-config(2017-09-04)
  29. 优化目录结构并增加404页面(2017-09-04)
  30. 加入 babel-plugin-transform-runtime 和 babel-polyfill(2017-09-17)
  31. 集成PostCSS(2017-09-17)
  32. redux 模块热替换配置(2017-09-26)
  33. 模拟AJAX数据之Mock.js(2017-11-21)
  34. 使用 CSS Modules(2017-12-13)
  35. 使用 json-server 代替 Mock.js(2017-12-18)
  36. 问题修复

写在前面

当我第一次跟着项目做react项目的时候,由于半截加入的,对框架了解甚少,只能跟着别人的样板写。对整个框架没有一点了解。

做项目,总是要解决各种问题的,所以每个地方都需要去了解,但是对整个框架没有一个整体的了解,实在是不行。

期间,我也跟着别人的搭建框架的教程一步一步的走,但是经常因为自己太菜,走不下去。在经过各种蹂躏之后,对整个框架也有一个大概的了解,
我就想把他写下来,让后来的菜鸟能跟着我的教程对react全家桶有一个全面的认识。

我的这个教程,从新建根文件夹开始,到成型的框架,每个文件为什么要建立?建立了干什么?每个依赖都是干什么的?一步一步写下来,供大家学习。

当然,这个框架我以后会一直维护的,也希望大家能一起来完善这个框架,如果您有任何建议,欢迎在这里留言,欢迎fork源码react-family

我基于该框架react-family又做了一个兼容IE8的版本,教程在这里react-family框架兼容IE8教程

说明

  1. 每个命令行块都是以根目录为基础的。例如下面命令行块,都是基于根目录的。
cd src/pages
mkdir Home
  1. 技术栈均是目前最新的。
  • react 15.6.1
  • react-router-dom 4.2.2
  • redux 3.7.2
  • webpack 3.5.5
  1. 目录说明
  .babelrc                          #babel配置文件
  package-lock.json
  package.json
  README.MD
  webpack.config.js                 #webpack生产配置文件
  webpack.dev.config.js             #webpack开发配置文件
  
├─dist
├─public                             #公共资源文件
└─src                                #项目源码
      index.html                    #index.html模板
      index.js                      #入口文件
      
    ├─component                      #组建库
      └─Hello
              Hello.js
              
    ├─pages                          #页面目录
      ├─Counter
            Counter.js
            
      ├─Home
            Home.js
            
      ├─Page1
          Page1.css                #页面样式
          Page1.js
          
        └─images                    #页面图片
                brickpsert.jpg
                
      └─UserInfo
              UserInfo.js
              
    ├─redux
        reducers.js
        store.js
        
      ├─actions
            counter.js
            userInfo.js
            
      ├─middleware
            promiseMiddleware.js
            
      └─reducers
              counter.js
              userInfo.js
              
    └─router                        #路由文件
            Bundle.js
            router.js
            

init项目

  1. 创建文件夹并进入

    mkdir react-family && cd react-family

  2. init npm

    npm init 按照提示填写项目基本信息

webpack

  1. 安装 webpack

    npm install --save-dev webpack@3

    Q: 什么时候用--save-dev,什么时候用--save

    A: --save-dev 是你开发时候依赖的东西,--save 是你发布之后还依赖的东西。看这里

  2. 根据webpack文档编写最基础的配置文件

    新建webpack开发配置文件 touch webpack.dev.config.js

    webpack.dev.config.js

    const path = require('path');
    
    module.exports = {
     
        /*入口*/
        entry: path.join(__dirname, 'src/index.js'),
        
        /*输出到dist文件夹,输出文件名字为bundle.js*/
        output: {
            path: path.join(__dirname, './dist'),
            filename: 'bundle.js'
        }
    };
  3. 学会使用webpack编译文件

    新建入口文件

    mkdir src && touch ./src/index.js

    src/index.js 添加内容

    document.getElementById('app').innerHTML = "Webpack works"

    现在我们执行命令 webpack --config webpack.dev.config.js

    webpack如果没有全局安装,这里会报错哦。命令建议全局安装,如果编译有问题看这里 #1 (comment)

    我们可以看到生成了dist文件夹和bundle.js

  4. 现在我们测试下~

    dist文件夹下面新建一个index.html

    touch ./dist/index.html

    dist/index.html填写内容

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="text/javascript" src="./bundle.js" charset="utf-8"></script>
    </body>
    </html>

    用浏览器打开index.html,可以看到Webpack works!

    webpack

    现在回头看下,我们做了什么或者说webpack做了什么。

    把入口文件 index.js 经过处理之后,生成 bundle.js。就这么简单。

babel

Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。

通俗的说,就是我们可以用ES6, ES7等来编写代码,Babel会把他们统统转为ES5。

    {
      "presets": [
        "es2015",
        "react",
        "stage-0"
      ],
      "plugins": []
    }

修改webpack.dev.config.js,增加babel-loader

    /*src文件夹下面的以.js结尾的文件,要使用babel解析*/
    /*cacheDirectory是用来缓存编译结果,下次编译加速*/
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader?cacheDirectory=true'],
            include: path.join(__dirname, 'src')
        }]
    }

现在我们简单测试下,是否能正确转义ES6~

修改 src/index.js

    /*使用es6的箭头函数*/
    var func = str => {
        document.getElementById('app').innerHTML = str;
    };
    func('我现在在使用Babel!');

执行打包命令webpack --config webpack.dev.config.js

浏览器打开index.html,我们看到正确输出了我现在在使用Babel!

babel

然后我们打开打包后的bundle.js,翻页到最下面,可以看到箭头函数被转换成普通函数了!

babel-bundle.png

Q: babel-preset-state-0,babel-preset-state-1,babel-preset-state-2,babel-preset-state-3有什么区别?

A: 每一级包含上一级的功能,比如 state-0包含state-1的功能,以此类推。state-0功能最全。具体可以看这篇文章:babel配置-各阶段的stage的区别

参考地址:

  1. https://segmentfault.com/a/1190000008159877

  2. http://www.ruanyifeng.com/blog/2016/01/babel.html

react

npm install --save react react-dom

修改 src/index.js使用react

import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(
    <div>Hello React!</div>, document.getElementById('app'));

执行打包命令webpack --config webpack.dev.config.js

打开index.html 看效果。

我们简单做下改进,把Hello React放到组件里面。体现组件化~

cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch Hello.js

按照React语法,写一个Hello组件

import React, {Component} from 'react';

export default class Hello extends Component {
    render() {
        return (
            <div>
                Hello,React!
            </div>
        )
    }
}

然后让我们修改src/index.js,引用Hello组件!

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import Hello from './component/Hello/Hello';

ReactDom.render(
    <Hello/>, document.getElementById('app'));

根目录执行打包命令

webpack --config webpack.dev.config.js

打开index.html看效果咯~

命令优化

Q:每次打包都得在根目录执行这么一长串命令webpack --config webpack.dev.config.js,能不打这么长吗?

A:修改package.json里面的script,增加dev-build

package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js"
  }

现在我们打包只需要执行npm run dev-build就可以啦!

参考地址:

http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

react-router

npm install --save react-router-dom

新建router文件夹和组件

cd src
mkdir router && touch router/router.js

按照react-router文档编辑一个最基本的router.js。包含两个页面homepage1

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';


const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

新建页面文件夹

cd src
mkdir pages

新建两个页面 Home,Page1

cd src/pages
mkdir Home && touch Home/Home.js
mkdir Page1 && touch Page1/Page1.js

填充内容:

src/pages/Home/Home.js

import React, {Component} from 'react';

export default class Home extends Component {
    render() {
        return (
            <div>
                this is home~
            </div>
        )
    }
}

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~
            </div>
        )
    }
}

现在路由和页面建好了,我们在入口文件src/index.js引用Router。

修改src/index.js

import React from 'react';
import ReactDom from 'react-dom';

import getRouter from './router/router';

ReactDom.render(
    getRouter(), document.getElementById('app'));

现在执行打包命令npm run dev-build。打开index.html查看效果啦!

那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌,这是正常的。

我们之前一直用这个路径访问index.html,类似这样:file:///F:/react/react-family/dist/index.html
这种路径了,不是我们想象中的路由那样的路径http://localhost:3000~我们需要配置一个简单的WEB服务器,指向
index.html~有下面两种方法来实现

  1. Nginx, Apache, IIS等配置启动一个简单的的WEB服务器。
  2. 使用webpack-dev-server来配置启动WEB服务器。

下一节,我们来使用第二种方法启动服务器。这一节的DEMO,先放这里。

参考地址

  1. http://www.jianshu.com/p/e3adc9b5f75c
  2. http://reacttraining.cn/web/guides/quick-start

webpack-dev-server

简单来说,webpack-dev-server就是一个小型的静态文件服务器。使用它,可以为webpack打包生成的资源文件提供Web服务。

npm install webpack-dev-server@2 --save-dev

2017.11.16补充:这里webpack-dev-server需要全局安装,要不后面用的时候要写相对路径。需要再执行这个 npm install webpack-dev-server@2 -g

修改webpack.dev.config.js,增加webpack-dev-server的配置。

webpack.dev.config.js

    devServer: {
        contentBase: path.join(__dirname, './dist')
    }

现在执行

webpack-dev-server --config webpack.dev.config.js

浏览器打开http://localhost:8080,OK,现在我们可以点击首页,Page1了,
看URL地址变化啦!我们看到react-router已经成功了哦。

Q: --content-base是什么?

A:URL的根目录。如果不设定的话,默认指向项目根目录。

重要提示:webpack-dev-server编译后的文件,都存储在内存中,我们并不能看见的。你可以删除之前遗留的文件dist/bundle.js
仍然能正常打开网站!

每次执行webpack-dev-server --config webpack.dev.config.js,要打很长的命令,我们修改package.json,增加script->start:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js",
    "start": "webpack-dev-server --config webpack.dev.config.js"
  }

下次执行npm start就可以了。

既然用到了webpack-dev-server,我们就看看它的其他的配置项
看了之后,发现有几个我们可以用的。

  • color(CLI only) console中打印彩色日志
  • historyApiFallback 任意的404响应都被替代为index.html。有什么用呢?你现在运行
    npm start,然后打开浏览器,访问http://localhost:8080,然后点击Page1到链接http://localhost:8080/page1
    然后刷新页面试试。是不是发现刷新后404了。为什么?dist文件夹里面并没有page1.html,当然会404了,所以我们需要配置
    historyApiFallback,让所有的404定位到index.html
  • host 指定一个host,默认是localhost。如果你希望服务器外部可以访问,指定如下:host: "0.0.0.0"。比如你用手机通过IP访问。
  • hot 启用Webpack的模块热替换特性。关于热模块替换,我下一小节专门讲解一下。
  • port 配置要监听的端口。默认就是我们现在使用的8080端口。
  • proxy 代理。比如在 localhost:3000 上有后端服务的话,你可以这样启用代理:
    proxy: {
      "/api": "http://localhost:3000"
    }
  • progress(CLI only) 将编译进度输出到控制台。

根据这几个配置,修改下我们的webpack-dev-server的配置~

webpack.dev.config.js

    devServer: {
        port: 8080,
        contentBase: path.join(__dirname, './dist'),
        historyApiFallback: true,
        host: '0.0.0.0'
    }

CLI ONLY的需要在命令行中配置

package.json

"start": "webpack-dev-server --config webpack.dev.config.js --color --progress"

现在我们执行npm start 看看效果。是不是看到打包的时候有百分比进度?在http://localhost:8080/page1页面刷新是不是没问题了?
用手机通过局域网IP是否可以访问到网站?

参考地址:

  1. https://segmentfault.com/a/1190000006670084
  2. https://webpack.js.org/guides/development/#using-webpack-dev-server

模块热替换(Hot Module Replacement)

到目前,当我们修改代码的时候,浏览器会自动刷新,不信你可以去试试。(如果你的不会刷新,看看这个调整文本编辑器

我相信看这个教程的人,应该用过别人的框架。我们在修改代码的时候,浏览器不会刷新,只会更新自己修改的那一块。我们也要实现这个效果。

我们看下webpack模块热替换教程。

我们接下来要这么修改

package.json 增加 --hot

"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"

src/index.js 增加module.hot.accept(),如下。当模块更新的时候,通知index.js

src/index.js

import React from 'react';
import ReactDom from 'react-dom';

import getRouter from './router/router';

if (module.hot) {
    module.hot.accept();
}

ReactDom.render(
    getRouter(), document.getElementById('app'));

现在我们执行npm start,打开浏览器,修改Home.js,看是不是不刷新页面的情况下,内容更新了?惊不惊喜?意不意外?

做模块热替换,我们只改了几行代码,非常简单的。纸老虎一个~

现在我需要说明下我们命令行使用的--hot,可以通过配置webpack.dev.config.js来替换,
向文档上那样,修改下面三处。但我们还是用--hot吧。下面的方式我们知道一下就行,我们不用。同样的效果。

const webpack = require('webpack');

devServer: {
    hot: true
}

plugins:[
     new webpack.HotModuleReplacementPlugin()
]

HRM配置其实有两种方式,一种CLI方式,一种Node.js API方式。我们用到的就是CLI方式,比较简单。
Node.js API方式,就是建一个server.js等等,网上大部分教程都是这种方式,这里不做讲解了。

你以为模块热替换到这里就结束了?nonono~

上面的配置对react模块的支持不是很好哦。

例如下面的demo,当模块热替换的时候,state会重置,这不是我们想要的。

修改Home.js,增加计数state

src/pages/Home/Home.js

import React, {Component} from 'react';

export default class Home extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }

    _handleClick() {
        this.setState({
            count: ++this.state.count
        });
    }

    render() {
        return (
            <div>
                this is home~<br/>
                当前计数:{this.state.count}<br/>
                <button onClick={() => this._handleClick()}>自增</button>
            </div>
        )
    }
}

你可以测试一下,当我们修改代码的时候,webpack在更新页面的时候,也把count初始为0了。

为了在react模块更新的同时,能保留state等页面中其他状态,我们需要引入react-hot-loader~

Q: 请问webpack-dev-serverreact-hot-loader两者的热替换有什么区别?

A: 区别在于webpack-dev-server自己的--hot模式只能即时刷新页面,但状态保存不住。因为React有一些自己语法(JSX)是HotModuleReplacementPlugin搞不定的。
react-hot-loader--hot基础上做了额外的处理,来保证状态可以存下来。(来自segmentfault

下面我们来加入react-hot-loader v3,

安装依赖

npm install react-hot-loader@next --save-dev

根据文档
我们要做如下几个修改~

  1. .babelrc 增加 react-hot-loader/babel

.babelrc

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "react-hot-loader/babel"
  ]
}
  1. webpack.dev.config.js入口增加react-hot-loader/patch

webpack.dev.config.js

    entry: [
        'react-hot-loader/patch',
        path.join(__dirname, 'src/index.js')
    ]
  1. src/index.js修改如下

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';

import getRouter from './router/router';

/*初始化*/
renderWithHotReload(getRouter());

/*热更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('./router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            {RootElement}
        </AppContainer>,
        document.getElementById('app')
    )
}

现在,执行npm start,试试。是不是修改页面的时候,state不更新了?

参考文章:

  1. gaearon/react-hot-loader#243

文件路径优化

做到这里,我们简单休息下。做下优化~

在之前写的代码中,我们引用组件,或者页面时候,写的是相对路径~

比如src/router/router.js里面,引用Home.js的时候就用的相对路径

import Home from '../pages/Home/Home';

webpack提供了一个别名配置,就是我们无论在哪个路径下,引用都可以这样

import Home from 'pages/Home/Home';

下面我们来配置下,修改webpack.dev.config.js,增加别名~

webpack.dev.config.js

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router')
        }
    }

然后我们把之前使用的绝对路径统统改掉。

src/router/router.js

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';

src/index.js

import getRouter from 'router/router';

我们这里约定,下面,我们会默认配置需要的别名路径,不再做重复的讲述哦。

redux

接下来,我们就要就要就要集成redux了。

要对redux有一个大概的认识,可以阅读阮一峰前辈的Redux 入门教程(一):基本用法

如果要对redux有一个非常详细的认识,我推荐阅读中文文档,写的非常好。读了这个教程,有一个非常深刻的感觉,redux并没有任何魔法。

不要被各种关于 reducers, middleware, store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。

当然,我这篇文章是写给新手的,如果看不懂上面的文章,或者不想看,没关系。先会用,多用用就知道原理了。

开始整代码!我们就做一个最简单的计数器。自增,自减,重置。

先安装redux npm install --save redux

初始化目录结构

cd src
mkdir redux
cd redux
mkdir actions
mkdir reducers
touch reducers.js
touch store.js
touch actions/counter.js
touch reducers/counter.js

先来写action创建函数。通过action创建函数,可以创建action~
src/redux/actions/counter.js

/*action*/

export const INCREMENT = "counter/INCREMENT";
export const DECREMENT = "counter/DECREMENT";
export const RESET = "counter/RESET";

export function increment() {
    return {type: INCREMENT}
}

export function decrement() {
    return {type: DECREMENT}
}

export function reset() {
    return {type: RESET}
}

再来写reducer,reducer是一个纯函数,接收action和旧的state,生成新的state.

src/redux/reducers/counter.js

import {INCREMENT, DECREMENT, RESET} from '../actions/counter';

/*
* 初始化state
 */

const initState = {
    count: 0
};
/*
* reducer
 */
export default function reducer(state = initState, action) {
    switch (action.type) {
        case INCREMENT:
            return {
                count: state.count + 1
            };
        case DECREMENT:
            return {
                count: state.count - 1
            };
        case RESET:
            return {count: 0};
        default:
            return state
    }
}

一个项目有很多的reducers,我们要把他们整合到一起

src/redux/reducers.js

import counter from './reducers/counter';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}

到这里,我们必须再理解下一句话。

reducer就是纯函数,接收stateaction,然后返回一个新的 state

看看上面的代码,无论是combineReducers函数也好,还是reducer函数也好,都是接收stateaction
返回更新后的state。区别就是combineReducers函数是处理整棵树,reducer函数是处理树的某一点。

接下来,我们要创建一个store

前面我们可以使用 action 来描述“发生了什么”,使用action创建函数来返回action

还可以使用 reducers 来根据 action 更新 state

那我们如何提交action?提交的时候,怎么才能触发reducers呢?

store 就是把它们联系到一起的对象。store 有以下职责:

  • 维持应用的 state
  • 提供 getState() 方法获取 state
  • 提供 dispatch(action) 触发reducers方法更新 state
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

src/redux/store.js

import {createStore} from 'redux';
import combineReducers from './reducers.js';

let store = createStore(combineReducers);

export default store;

到现在为止,我们已经可以使用redux了~

下面我们就简单的测试下

cd src
cd redux
touch testRedux.js

src/redux/testRedux.js

import {increment, decrement, reset} from './actions/counter';

import store from './store';

// 打印初始状态
console.log(store.getState());

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
);

// 发起一系列 action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());

// 停止监听 state 更新
unsubscribe();

当前文件夹执行命令

webpack testRedux.js build.js

node build.js

是不是看到输出了state变化?

{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }


做这个测试,就是为了告诉大家,reduxreact没关系,虽说他俩能合作。

到这里,我建议你再理下redux的数据流,看看这里

  1. 调用store.dispatch(action)提交action
  2. redux store调用传入的reducer函数。把当前的stateaction传进去。
  3. reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

就是酱紫~~

这会webpack.dev.config.js路径别名增加一下,后面好写了。

webpack.dev.config.js

          alias: {
              ...
              actions: path.join(__dirname, 'src/redux/actions'),
              reducers: path.join(__dirname, 'src/redux/reducers'),
              redux: path.join(__dirname, 'src/redux')
          }
  

把前面的相对路径都改改。

下面我们开始搭配react使用。

写一个Counter页面

cd src/pages
mkdir Counter
touch Counter/Counter.js

src/pages/Counter/Counter.js

import React, {Component} from 'react';

export default class Counter extends Component {
    render() {
        return (
            <div>
                <div>当前计数为(显示redux计数)</div>
                <button onClick={() => {
                    console.log('调用自增函数');
                }}>自增
                </button>
                <button onClick={() => {
                    console.log('调用自减函数');
                }}>自减
                </button>
                <button onClick={() => {
                    console.log('调用重置函数');
                }}>重置
                </button>
            </div>
        )
    }
}

修改路由,增加Counter

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

npm start看看效果。

下一步,我们让Counter组件和Redux联合起来。使Counter能获得到Reduxstate,并且能发射action

当然我们可以使用刚才测试testRedux的方法,手动监听~手动引入store~但是这肯定很麻烦哦。

react-redux提供了一个方法connect

容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。

connect接收两个参数,一个mapStateToProps,就是把reduxstate,转为组件的Props,还有一个参数是mapDispatchToprops,
就是把发射actions的方法,转为Props属性函数。

先来安装react-redux

npm install --save react-redux

src/pages/Counter/Counter.js

import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';

import {connect} from 'react-redux';

class Counter extends Component {
    render() {
        return (
            <div>
                <div>当前计数为{this.props.counter.count}</div>
                <button onClick={() => this.props.increment()}>自增
                </button>
                <button onClick={() => this.props.decrement()}>自减
                </button>
                <button onClick={() => this.props.reset()}>重置
                </button>
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        counter: state.counter
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        increment: () => {
            dispatch(increment())
        },
        decrement: () => {
            dispatch(decrement())
        },
        reset: () => {
            dispatch(reset())
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

下面我们要传入store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';

import getRouter from 'router/router';

/*初始化*/
renderWithHotReload(getRouter());

/*热更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            <Provider store={store}>
                {RootElement}
            </Provider>
        </AppContainer>,
        document.getElementById('app')
    )
}

到这里我们就可以执行npm start,打开localhost:8080/counter看效果了。

但是你发现npm start一直报错

ERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\node_modules\react-redux\es\connect'

ERROR in ./src/redux/store.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\src\redux'

WTF?这个错误困扰了半天。我说下为什么造成这个错误。我们引用redux的时候这样用的

import {createStore} from 'redux'

然而,我们在webapck.dev.config.js里面这样配置了

    resolve: {
        alias: {
            ...
            redux: path.join(__dirname, 'src/redux')
        }
    }

然后webapck编译的时候碰到redux都去src/redux去找了。但是找不到啊。所以我们把webpack.dev.config.js里面redux这一行删除了,就好了。
并且把使用我们自己使用redux文件夹的地方改成相对路径哦。

现在你可以npm start去看效果了。

这里我们再缕下(可以读React 实践心得:react-redux 之 connect 方法详解

  1. Provider组件是让所有的组件可以访问到store。不用手动去传。也不用手动去监听。
  2. connect函数作用是从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)函数到props

接下来,我们要说异步action

参考地址: http://cn.redux.js.org/docs/advanced/AsyncActions.html

想象一下我们调用一个异步get请求去后台请求数据:

  1. 请求开始的时候,界面转圈提示正在加载。isLoading置为true
  2. 请求成功,显示数据。isLoading置为false,data填充数据。
  3. 请求失败,显示失败。isLoading置为false,显示错误信息。

下面,我们以向后台请求用户基本信息为例。

  1. 我们先创建一个user.json,等会请求用,相当于后台的API接口。
cd dist
mkdir api
cd api
touch user.json

dist/api/user.json

{
  "name": "brickspert",
  "intro": "please give me a star"
}
  1. 创建必须的action创建函数。
cd src/redux/actions
touch userInfo.js

src/redux/actions/userInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

function getUserInfoRequest() {
    return {
        type: GET_USER_INFO_REQUEST
    }
}

function getUserInfoSuccess(userInfo) {
    return {
        type: GET_USER_INFO_SUCCESS,
        userInfo: userInfo
    }
}

function getUserInfoFail() {
    return {
        type: GET_USER_INFO_FAIL
    }
}

我们创建了请求中,请求成功,请求失败三个action创建函数。

  1. 创建reducer

再强调下,reducer是根据stateaction生成新state纯函数

cd src/redux/reducers
touch userInfo.js

src/redux/reducers/userInfo.js

import {GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL} from 'actions/userInfo';


const initState = {
    isLoading: false,
    userInfo: {},
    errorMsg: ''
};

export default function reducer(state = initState, action) {
    switch (action.type) {
        case GET_USER_INFO_REQUEST:
            return {
                ...state,
                isLoading: true,
                userInfo: {},
                errorMsg: ''
            };
        case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.userInfo,
                errorMsg: ''
            };
        case GET_USER_INFO_FAIL:
            return {
                ...state,
                isLoading: false,
                userInfo: {},
                errorMsg: '请求错误'
            };
        default:
            return state;
    }
}

这里的...state语法,是和别人的Object.assign()起同一个作用,合并新旧state。我们这里是没效果的,但是我建议都写上这个哦

组合reducer

src/redux/reducers.js

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action),
        userInfo: userInfo(state.userInfo, action)
    }
}
  1. 现在有了action,有了reducer,我们就需要调用把action里面的三个action函数和网络请求结合起来。
    • 请求中 dispatch getUserInfoRequest
    • 请求成功 dispatch getUserInfoSuccess
    • 请求失败 dispatch getUserInfoFail

src/redux/actions/userInfo.js增加

export function getUserInfo() {
    return function (dispatch) {
        dispatch(getUserInfoRequest());

        return fetch('http://localhost:8080/api/user.json')
            .then((response => {
                return response.json()
            }))
            .then((json) => {
                    dispatch(getUserInfoSuccess(json))
                }
            ).catch(
                () => {
                    dispatch(getUserInfoFail());
                }
            )
    }
}

我们这里发现,别的action创建函数都是返回action对象:

{type: xxxx}

但是我们现在的这个action创建函数 getUserInfo则是返回函数了。

为了让action创建函数除了返回action对象外,还可以返回函数,我们需要引用redux-thunk

npm install --save redux-thunk

这里涉及到redux中间件middleware,我后面会讲到的。你也可以读这里Middleware

简单的说,中间件就是action在到达reducer,先经过中间件处理。我们之前知道reducer能处理的action只有这样的{type:xxx},所以我们使用中间件来处理
函数形式的action,把他们转为标准的actionreducer。这是redux-thunk的作用。
使用redux-thunk中间件

我们来引入redux-thunk中间件

src/redux/store.js

import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import combineReducers from './reducers.js';

let store = createStore(combineReducers, applyMiddleware(thunkMiddleware));

export default store;

到这里,redux这边OK了,我们来写个组件验证下。

cd src/pages
mkdir UserInfo
cd UserInfo
touch UserInfo.js

src/pages/UserInfo/UserInfo.js

import React, {Component} from 'react';
import {connect} from 'react-redux';
import {getUserInfo} from "actions/userInfo";

class UserInfo extends Component {

    render() {
        const {userInfo, isLoading, errorMsg} = this.props.userInfo;
        return (
            <div>
                {
                    isLoading ? '请求信息中......' :
                        (
                            errorMsg ? errorMsg :
                                <div>
                                    <p>用户信息:</p>
                                    <p>用户名:{userInfo.name}</p>
                                    <p>介绍:{userInfo.intro}</p>
                                </div>
                        )
                }
                <button onClick={() => this.props.getUserInfo()}>请求用户信息</button>
            </div>
        )
    }
}

export default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);

这里你可能发现connect参数写法不一样了,mapStateToProps函数用了es6简写,mapDispatchToProps用了react-redux提供的简单写法。

增加路由
src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
import UserInfo from 'pages/UserInfo/UserInfo';

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
                <Route path="/userinfo" component={UserInfo}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

现在你可以执行npm start去看效果啦!

redux

到这里redux集成基本告一段落了,后面我们还会有一些优化。

combinReducers优化

redux提供了一个combineReducers函数来合并reducer,不用我们自己合并哦。写起来简单,但是意思和我们
自己写的combinReducers也是一样的。

src/redux/reducers.js

import {combineReducers} from "redux";

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';


export default combineReducers({
    counter,
    userInfo
});

devtool优化

现在我们发现一个问题,代码哪里写错了,浏览器报错只报在build.js第几行。

错误图片

这让我们分析错误无从下手。看这里

我们增加webpack配置devtool

webpack.dev.config.js增加

devtool: 'inline-source-map'

这次看错误信息是不是提示的很详细了?

错误图片

同时,我们在srouce里面能看到我们写的代码,也能打断点调试哦~

错误图片

编译css

先说这里为什么不用scss,因为Windows使用node-sass,需要先安装 Microsoft Windows SDK for Windows 7 and .NET Framework 4
我怕有些人copy这份代码后,没注意,运行不起来。所以这里不用scss了,如果需要,自行编译哦。

npm install css-loader style-loader --save-dev

css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能;

style-loader将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

webpack.dev.config.js rules增加

{
   test: /\.css$/,
   use: ['style-loader', 'css-loader']
}

我们用Page1页面来测试下

cd src/pages/Page1
touch Page1.css

src/pages/Page1/Page1.css

.page-box {
    border: 1px solid red;
}

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
            </div>
        )
    }
}

好了,现在npm start去看效果吧。

编译图片

npm install --save-dev url-loader file-loader

webpack.dev.config.js rules增加

{
    test: /\.(png|jpg|gif)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}

options limit 8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求。

我们来用Page1 测试下

cd src/pages/Page1
mkdir images

images文件夹放一个图片。

修改代码,引用图片

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

import image from './images/brickpsert.jpg';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
                <img src={image}/>
            </div>
        )
    }
}

可以去看看效果啦。

按需加载

为什么要实现按需加载?

我们现在看到,打包完后,所有页面只生成了一个build.js,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js了哦。

如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。

react-router 2.0时代, 按需加载需要用到的最关键的一个函数,就是require.ensure(),它是按需加载能够实现的核心。

在4.0版本,官方放弃了这种处理按需加载的方式,选择了一个更加简洁的处理方式。

传送门

根据官方示例,我们开搞

  1. npm install bundle-loader --save-dev
  2. 新建bundle.js
cd src/router
touch Bundle.js

src/router/Bundle.js

import React, {Component} from 'react'

class Bundle extends Component {
    state = {
        // short for "module" but that's a keyword in js, so "mod"
        mod: null
    };

    componentWillMount() {
        this.load(this.props)
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.load !== this.props.load) {
            this.load(nextProps)
        }
    }

    load(props) {
        this.setState({
            mod: null
        });
        props.load((mod) => {
            this.setState({
                // handle both es imports and cjs
                mod: mod.default ? mod.default : mod
            })
        })
    }

    render() {
        return this.props.children(this.state.mod)
    }
}

export default Bundle;
  1. 改造路由器

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Bundle from './Bundle';

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';

const Loading = function () {
    return <div>Loading...</div>
};

const createComponent = (component) => (props) => (
    <Bundle load={component}>
        {
            (Component) => Component ? <Component {...props} /> : <Loading/>
        }
    </Bundle>
);

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={createComponent(Home)}/>
                <Route path="/page1" component={createComponent(Page1)}/>
                <Route path="/counter" component={createComponent(Counter)}/>
                <Route path="/userinfo" component={createComponent(UserInfo)}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

现在你可以npm start,打开浏览器,看是不是进入新的页面,都会加载自己的JS的~

但是你可能发现,名字都是0.bundle.js这样子的,这分不清楚是哪个页面的js呀!

我们修改下webpack.dev.config.js,加个chunkFilenamechunkFilename是除了entry定义的入口js之外的js~

    output: {
        path: path.join(__dirname, './dist'),
        filename: 'bundle.js',
        chunkFilename: '[name].js'
    }

现在你运行发现名字变成home.js,这样的了。棒棒哒!

那么问题来了home是在哪里设置的?webpack怎么知道他叫home

其实在这里我们定义了,router.js里面

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';

看到没。这里有个name=home。嘿嘿。

参考地址:

  1. http://www.jianshu.com/p/8dd98a7028e0
  2. https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/code-splitting.md
  3. https://segmentfault.com/a/1190000007949841
  4. http://react-china.org/t/webpack-react-router/10123
  5. https://juejin.im/post/58f9717e44d9040069d06cd6

缓存

想象一下这个场景~

我们网站上线了,用户第一次访问首页,下载了home.js,第二次访问又下载了home.js~

这肯定不行呀,所以我们一般都会做一个缓存,用户下载一次home.js后,第二次就不下载了。

有一天,我们更新了home.js,但是用户不知道呀,用户还是使用本地旧的home.js。出问题了~

怎么解决?每次代码更新后,打包生成的名字不一样。比如第一次叫home.a.js,第二次叫home.b.js

文档看这里

我们照着文档来

webpack.dev.config.js

    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js',
        chunkFilename: '[name].[chunkhash].js'
    }

每次打包都用增加hash~

现在我们试试,是不是修改了文件,打包后相应的文件名字就变啦?

package

但是你可能发现了,网页打开报错了~因为你dist/index.html里面引用js名字还是bundle.js老名字啊,改成新的名字就可以啦。

啊~那岂不是我每次编译打包,都得去改一下js名字?欲知后事如何,且看下节分享。

HtmlWebpackPlugin

这个插件,每次会自动把js插入到你的模板index.html里面去。

npm install html-webpack-plugin --save-dev

新建模板index.html

cd src
touch index.html

src/index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

修改webpack.dev.config.js,增加plugin

var HtmlWebpackPlugin = require('html-webpack-plugin');

    plugins: [new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, 'src/index.html')
    })],

npm start运行项目,看看是不是能正常访问啦。~

说明一下:npm start打包后的文件存在内存中,你看不到的。~ 你可以把遗留dist/index.html删除掉了。

提取公共代码

想象一下,我们的主文件,原来的bundle.js里面是不是包含了react,redux,react-router等等
这些代码??这些代码基本上不会改变的。但是,他们合并在bundle.js里面,每次项目发布,重新请求bundle.js的时候,相当于重新请求了
react等这些公共库。浪费了~

我们把react这些不会改变的公共库提取出来,用户缓存下来。从此以后,用户再也不用下载这些库了,无论是否发布项目。嘻嘻。

webpack文档给了教程,看这里

webpack.dev.config.js

    var webpack = require('webpack');

    entry: {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, 'src/index.js')
        ],
        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
    }
    
        /*plugins*/
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })

react等库生成打包到vendor.hash.js里面去。

但是你现在可能发现编译生成的文件app.[hash].jsvendor.[hash].js生成的hash一样的,这里是个问题,因为呀,你每次修改代码,都会导致vendor.[hash].js名字改变,那我们提取出来的意义也就没了。其实文档上写的很清楚,

   output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js', //这里应该用chunkhash替换hash
        chunkFilename: '[name].[chunkhash].js'
    }

但是无奈,如果用chunkhash,会报错。和webpack-dev-server --hot不兼容,具体看这里

现在我们在配置开发版配置文件,就向webpack-dev-server妥协,因为我们要用他。问题先放这里,等会我们配置正式版webpack.config.js的时候要解决这个问题。

生产坏境构建

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

文档看这里

我们要开始做了~

touch webpack.config.js

webpack.dev.config.js的基础上先做以下几个修改~

  1. 先删除webpack-dev-server相关的东西~
  2. devtool的值改成cheap-module-source-map
  3. 刚才说的hash改成chunkhash

webpack.config.js

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

module.exports = {
    devtool: 'cheap-module-source-map',
    entry: {
        app: [
            path.join(__dirname, 'src/index.js')
        ],
        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].js'
    },
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader'],
            include: path.join(__dirname, 'src')
        }, {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname, 'src/index.html')
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })
    ],

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router'),
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers')
        }
    }
};

package.json增加打包脚本

"build":"webpack --config webpack.config.js"

然后执行npm run build~看看dist文件夹是不是生成了我们发布要用的所有文件哦?

接下来我们还是要优化正式版配置文件~

文件压缩

webpack使用UglifyJSPlugin来压缩生成的文件。

npm i --save-dev uglifyjs-webpack-plugin

webpack.config.js

const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJSPlugin()
  ]
}

npm run build发现打包文件大小减小了好多。

uglify

指定环境

许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:

webpack.config.js

module.exports = {
  plugins: [
       new webpack.DefinePlugin({
          'process.env': {
              'NODE_ENV': JSON.stringify('production')
           }
       })
  ]
}

npm run build后发现vendor.[hash].js又变小了。

uglify

优化缓存

刚才我们把[name].[hash].js变成[name].[chunkhash].js后,npm run build后,
发现app.xxx.jsvendor.xxx.js不一样了哦。

但是现在又有一个问题了。

你随便修改代码一处,例如Home.js,随便改变个字,你发现home.xxx.js名字变化的同时,
vendor.xxx.js名字也变了。这不行啊。这和没拆分不是一样一样了吗?我们本意是vendor.xxx.js
名字永久不变,一直缓存在用户本地的。~

官方文档推荐了一个插件HashedModuleIdsPlugin

    plugins: [
        new webpack.HashedModuleIdsPlugin()
    ]

现在你打包,修改代码再试试,是不是名字不变啦?错了,现在打包,我发现名字还是变了,经过比对文档,我发现还要加一个runtime代码抽取,

new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime'
})

加上这句话就好了~为什么呢?看下解释

注意,引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例之前引入。

public path

想象一个场景,我们的静态文件放在了单独的静态服务器上去了,那我们打包的时候,如何让静态文件的链接定位到静态服务器呢?

看文档Public Path

webpack.config.js output 中增加一个publicPath,我们当前用/,相对于当前路径,如果你要改成别的url,就改这里就好了。

    output: {
        publicPath : '/'
    }

打包优化

你现在打开dist,是不是发现好多好多文件,每次打包后的文件在这里混合了?我们希望每次打包前自动清理下dist文件。

npm install clean-webpack-plugin --save-dev

webpack.config.js

const CleanWebpackPlugin = require('clean-webpack-plugin');


plugins: [
    new CleanWebpackPlugin(['dist'])
]

现在npm run build试试,是不是之前的都清空了。当然我们之前的api文件夹也被清空了,不过没关系哦~本来就是测试用的。

抽取css

目前我们的css是直接打包进js里面的,我们希望能单独生成css文件。

我们使用extract-text-webpack-plugin来实现。

npm install --save-dev extract-text-webpack-plugin

webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
     new ExtractTextPlugin({
         filename: '[name].[contenthash:5].css',
         allChunks: true
     })
  ]
}

npm run build后发现单独生成了css文件哦

使用axiosmiddleware优化API请求

先安装下axios

npm install --save axios

我们之前项目的一次API请求是这样写的哦~

action创建函数是这样的。比我们现在写的fetch简单多了。

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
        afterSuccess:(dispatch,getState,response)=>{
            /*请求成功后执行的函数*/
        },
        otherData:otherData
    }
}

然后在dispatch(getUserInfo())后,通过redux中间件来处理请求逻辑。

中间件的教程看这里

我们想想中间件的逻辑

  1. 请求前dispatch REQUEST请求。
  2. 成功后dispatch SUCCESS请求,如果定义了afterSuccess()函数,调用它。
  3. 失败后dispatch FAIL请求。

来写一个

cd src/redux
mkdir middleware
cd middleware
touch promiseMiddleware.js

src/redux/middleware/promiseMiddleware.js

import axios from 'axios';

export default  store => next => action => {
    const {dispatch, getState} = store;
    /*如果dispatch来的是一个function,此处不做处理,直接进入下一级*/
    if (typeof action === 'function') {
        action(dispatch, getState);
        return;
    }
    /*解析action*/
    const {
        promise,
        types,
        afterSuccess,
        ...rest
    } = action;

    /*没有promise,证明不是想要发送ajax请求的,就直接进入下一步啦!*/
    if (!action.promise) {
        return next(action);
    }

    /*解析types*/
    const [REQUEST,
        SUCCESS,
        FAILURE] = types;

    /*开始请求的时候,发一个action*/
    next({
        ...rest,
        type: REQUEST
    });
    /*定义请求成功时的方法*/
    const onFulfilled = result => {
        next({
            ...rest,
            result,
            type: SUCCESS
        });
        if (afterSuccess) {
            afterSuccess(dispatch, getState, result);
        }
    };
    /*定义请求失败时的方法*/
    const onRejected = error => {
        next({
            ...rest,
            error,
            type: FAILURE
        });
    };

    return promise(axios).then(onFulfilled, onRejected).catch(error => {
        console.error('MIDDLEWARE ERROR:', error);
        onRejected(error)
    })
}

修改src/redux/store.js来应用这个中间件

import {createStore, applyMiddleware} from 'redux';
import combineReducers from './reducers.js';

import promiseMiddleware from './middleware/promiseMiddleware'

let store = createStore(combineReducers, applyMiddleware(promiseMiddleware));

export default store;

修改src/redux/actions/userInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
    }
}

是不是简单清新很多啦?

修改src/redux/reducers/userInfo.js

        case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.result.data,
                errorMsg: ''
            };

action.userInfo修改成了action.result.data。你看中间件,请求成功,会给action增加一个result字段来存储响应结果哦~不用手动传了。

npm start看看我们的网络请求是不是正常哦。

调整文本编辑器

使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。

要在一些常见的编辑器中禁用此功能,请查看以下列表:

  • Sublime Text 3 - 在用户首选项(user preferences)中添加 atomic_save: "false"。
  • IntelliJ - 在首选项(preferences)中使用搜索,查找到 "safe write" 并且禁用它。
  • Vim - 在设置(settings)中增加 :set backupcopy=yes。
  • WebStorm - 在 Preferences > Appearance & Behavior > System Settings 中取消选中 Use "safe write"。

合并提取webpack公共配置

想象一个场景,现在我想给webpack增加一个css modules依赖,你会发现,WTF?我即要修改webpack.dev.config.js,又要修改webpack.config.js~

这肯定不行啊。所以我们要把公共的配置文件提取出来。提取到webpack.common.config.js里面~

webpack.dev.config.jswebpack.config.js写自己的特殊的配置。

这里我们需要用到webpack-merge来合并公共配置和单独的配置。

这样说一下,应该看代码就能看懂了。下次公共配置直接就写在webpack.common.config.js里面啦。

这里偷偷说下,我修改了CleanWebpackPlugin的参数,不让他每次构建都删除api文件夹了。要不每次都得复制进去。麻烦~

npm install --save-dev webpack-merge

touch webpack.common.config.js

webpack.common.config.js

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

commonConfig = {
    entry: {
        app: [
            path.join(__dirname, 'src/index.js')
        ],
        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].js',
        publicPath: "/"
    },
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader?cacheDirectory=true'],
            include: path.join(__dirname, 'src')
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname, 'src/index.html')
        }),
        new webpack.HashedModuleIdsPlugin(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'runtime'
        })
    ],

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            components: path.join(__dirname, 'src/components'),
            router: path.join(__dirname, 'src/router'),
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers')
        }
    }
};

module.exports = commonConfig;

webpack.dev.config.js

const merge = require('webpack-merge');
const path = require('path');

const commonConfig = require('./webpack.common.config.js');

const devConfig = {
    devtool: 'inline-source-map',
    entry: {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, 'src/index.js')
        ]
    },
    output: {
        /*这里本来应该是[chunkhash]的,但是由于[chunkhash]和react-hot-loader不兼容。只能妥协*/
        filename: '[name].[hash].js'
    },
    module: {
        rules: [{
            test: /\.css$/,
            use: ["style-loader", "css-loader"]
        }]
    },
    devServer: {
        contentBase: path.join(__dirname, './dist'),
        historyApiFallback: true,
        host: '0.0.0.0',
    }
};

module.exports = merge({
    customizeArray(a, b, key) {
        /*entry.app不合并,全替换*/
        if (key === 'entry.app') {
            return b;
        }
        return undefined;
    }
})(commonConfig, devConfig);

webpack.config.js

const merge = require('webpack-merge');

const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");

const commonConfig = require('./webpack.common.config.js');

const publicConfig = {
    devtool: 'cheap-module-source-map',
    module: {
        rules: [{
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: "css-loader"
            })
        }]
    },
    plugins: [
        new CleanWebpackPlugin(['dist/*.*']),
        new UglifyJSPlugin(),
        new webpack.DefinePlugin({
            'process.env': {
                'NODE_ENV': JSON.stringify('production')
            }
        }),
        new ExtractTextPlugin({
            filename: '[name].[contenthash:5].css',
            allChunks: true
        })
    ]

};

module.exports = merge(commonConfig, publicConfig);

优化目录结构并增加404页面

现在我们优化下目录结构,把routernav分开,新建根组件App

  1. component改名为components,因为是复数。。。注意修改引用的地方哦。
  2. 新建根组件components/App/APP.js
import React, {Component} from 'react';

import Nav from 'components/Nav/Nav';
import getRouter from 'router/router';

export default class App extends Component {
    render() {
        return (
            <div>
                <Nav/>
                {getRouter()}
            </div>
        )
    }
}
  1. 新建components/Nav/Nav组件,把router/router.js里面的nav提出来。
  2. 新建components/Loading/Loading组件,把router/router.js里面的Loading提出来。
  3. 入口文件src/index.js修改
import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';
import {BrowserRouter as Router} from 'react-router-dom';
import App from 'components/App/App';

renderWithHotReload(App);

if (module.hot) {
    module.hot.accept('components/App/App', () => {
        const NextApp = require('components/App/App').default;
        renderWithHotReload(NextApp);
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            <Provider store={store}>
                <Router>
                    <RootElement/>
                </Router>
            </Provider>
        </AppContainer>,
        document.getElementById('app')
    )
}
  1. 新建pages/NotFound/NotFound组件。
  2. 修改router/router.js,增加404
import NotFound from 'bundle-loader?lazy&name=notFound!pages/NotFound/NotFound';

<Route component={createComponent(NotFound)}/>

加入 babel-plugin-transform-runtime 和 babel-polyfill

  1. 先来说说babel-plugin-transform-runtime

    在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。

    所以 babel 提供了 transform-runtime 来将这些辅助函数“搬”到一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。

    npm install --save-dev babel-plugin-transform-runtime

修改`.babelrc`配置文件,增加配置

.babelrc

​```javascript
     "plugins": [
       "transform-runtime"
     ]
​```

  1. 再来看babel-polyfill

    Q: 为什么要集成babel-polyfill?

    A:

    Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。
    举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

    网上很多人说,集成了transform-runtime就不用babel-polyfill了,其实不然,看看官方怎么说的:

    NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing built-ins (Use babel-polyfill for that).

    所以,我们还是需要babel-polyfill哦。

    npm install --save-dev babel-polyfill

    修改webpack两个配置文件。

    webpack.common.config.js

         app: [
             "babel-polyfill",
             path.join(__dirname, 'src/index.js')
         ]

    webpack.dev.config.js

         app: [
             'babel-polyfill',
             'react-hot-loader/patch',
             path.join(__dirname, 'src/index.js')
         ]

参考地址:

  1. http://www.ruanyifeng.com/blog/2016/01/babel.html
  2. lmk123/blog#45
  3. https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/README.md

集成PostCSS

官方文档看这里

Q: 这是啥?为什么要用它?

他有很多很多的插件,我们举几个例子~

Autoprefixer这个插件,可以自动给css属性加浏览器前缀。

/*编译前*/
.container{
    display: flex;
}
/*编译后*/
.container{
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
}

postcss-cssnext 允许你使用未来的 CSS 特性(包括 autoprefixer)

当然,它有很多很多的插件可以用,你可以去官网详细了解。我们今天只用postcss-cssnext。(它包含了autoprefixer)

npm install --save-dev  postcss-loader
npm install --save-dev  postcss-cssnext

修改webpack配置文件,增加postcss-loader

webpack.dev.config.js

        rules: [{
            test: /\.(css|scss)$/,
            use: ["style-loader", "css-loader", "postcss-loader"]
        }]

webpack.config.js

        rules: [{
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: ["css-loader", "postcss-loader"]
            })
        }]

根目录增加postcss配置文件。

touch postcss.config.js

postcss.config.js

module.exports = {
    plugins: {
        'postcss-cssnext': {}
    }
};

现在你运行代码,然后写个css,去浏览器审查元素,看看,属性是不是生成了浏览器前缀?

redux 模块热替换配置

今天突然发现,当修改reducer代码的时候,页面会整个刷新,而不是局部刷新唉。

这不行,就去查了webpack文档,果然是要配置的。看这里

代码修改起来也简单,增加一段监听reducers变化,并替换的代码。

src/redux/store.js

if (module.hot) {
    module.hot.accept("./reducers", () => {
        const nextCombineReducers = require("./reducers").default;
        store.replaceReducer(nextCombineReducers);
    });
}

哦了~

模拟AJAX数据之Mock.js

每个改进都是为了解决问题。

现在我在开发中碰到了问题,我先描述下问题:

我们现在做前后端完全分离的应用,前端写前端的,后端写后端的,他们通过API接口连接。

前端同学心理路程:"后端同学接口写的好慢,我都没法调试了。"

是不是有这个问题呢?一般我们怎么解决?

第一种:自己这边随便造点数据,等后端接口写好了之后,再小修改,再调试。

第二种:想想我们之前获得用户信息的dist/api/user.json,我们可以用这种方式来调试。
但是想象下,我们要模拟一个文章列表,就要手动写几十列。oh~no!

并且,后端接口一般都不带.json,到时候对接,是不是还得改代码?

好了,下面介绍下今天的主角Mock.js

他会做一件事情:拦截AJAX请求,返回需要的数据!

我们写AJAX请求的时候,正常写,Mock.js会自动拦截的。

Mock.js提供各种随机生成数据。具体可以去官网看~

下面我们就在项目中集成咯:

  1. npm install mockjs --save-dev

  2. 新建mock文件夹

    touch mock

  3. 模拟一个我们之前用到的/api/user接口

     cd mock
     touch mock.js
    
    

    mock/mock.js

     import Mock from 'mockjs';
     
     let Random = Mock.Random;
     
     Mock.mock('/api/user', {
         'name': '@cname',
         'intro': '@word(20)'
     });
    

    上面代码的意思就是,拦截/api/user,返回随机的一个中文名字,一个20个字母的字符串。

    我知道你看不懂,你去看看Mock.js文档就能看懂啦!

  4. 与我们的项目连接。到目前为止,刚才定义的接口和我们的项目还没有关系。

    先来做,在src/index.js里面增加一行代码:

    src/index.js

    import '../mock/mock';
    
    
  5. 现在我们删除dist/api文件夹,然后修改之间的接口路径,把.json去掉。

    rm -rf dist/api

    src/redux/actions/userInfo.js

    /*promise: client => client.get(`/api/user.json`)*/
    
    promise: client => client.get(`/api/user`)
    
    

    现在我们运行npm start,到获取用户信息界面,看每次获取用户信息都会变化呀?

到这里还没完,我们还要配置:只有在开发坏境下,才引入mock,在生产坏境,不引入。

跟着我做:

先给mock文件夹加个别名,这个我就不单独介绍了:

webpack.common.config.js

    resolve: {
        alias: {
            ...
            mock: path.join(__dirname, 'mock')
        }
    }

webpack.dev.config.js增加

   const webpack = require('webpack');


   plugins:[
        new webpack.DefinePlugin({
               MOCK: true
        })
    ]

然后修改src/index.js刚才加的那句话为下面这样

if (MOCK) {
    require('mock/mock');
}

这样,就只会在npm start 开发模式下,才会应用mock,如果你不想用,就把MOCK改成false就好了。

哦了,到这里就结束了~回头缕下:

我们定义了mock,在index.js引入。

mock的工作就是,拦截AJAX请求,返回模拟数据。

参考文章:

http://www.jianshu.com/p/dd23a6547114

https://segmentfault.com/a/1190000005793320

使用 CSS Modules

关于什么是CSS Modules,我这里不介绍。

可以去看阮一峰的文章CSS Modules 用法教程

修改以下几个地方:

  1. webpack.dev.config.js

    module: {
            rules: [{
                test: /\.css$/,
                use: ["style-loader", "css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
            }]
        }
  2. webpack.config.js

    module: {
        rules: [{
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: ["css-loader?modules&localIdentName=[local]-[hash:base64:5]", "postcss-loader"]
            })
        }]
    }
  3. src/pages/Page1/page1.css

    .box {
        border: 1px solid red;
    }
  4. src/pages/Page1/Page1.js

    import React, {Component} from 'react';
    
    import style from './Page1.css';
    
    import image from './images/brickpsert.jpg';
    
    export default class Page1 extends Component {
        render() {
            return (
                <div className={style.box}>
                    this is page1~
                    <img src={image}/>
                </div>
            )
        }
    }

enjoy it!

使用 json-server 代替 Mock.js

json-serverMock.js一样,都是用来模拟接口数据的。

json-server功能更强大,支持分页,排序,筛选等等,具体的可以去看文档

我们用json-server代替之前的Mock.js

  1. 删除Mock.js相关代码。

    一共两处,webpack.dev.config.js,src/index.js

  2. npm install --save-dev json-server

  3. 写个demo,我们生成虚假数据还是用mockjs

mock/mock.js

let Mock = require('mockjs');

var Random = Mock.Random;

module.exports = function () {
    var data = {};
    data.user = {
        'name': Random.cname(),
        'intro': Random.word(20)
    };
    return data;
};
  1. 设置启动脚本

package.json

"mock": "json-server mock/mock.js --watch --port 8090",
"mockdev": "npm run mock & npm start"

  1. webpack.dev.config.js 增加个代理,把我们的API请求,代理到json-server服务器去。
   devServer: {
        ...
        proxy: {
                    "/api/*": "http://localhost:8090/$1"
                }
    }

哦了,你可以npm run mockdev启动项目,然后访问我们之前的用户信息接口,试试啦。

问题:windows不支持命令并行执行&,你可以分开执行,或者使用npm-run-all

问题修复

1. react热模块加载无效

举例:在首页中,当我们计数加上去,然后修改代码,计数又恢复成0了。也就是热模块加载的时候,重置了reactstate

如果我们不使用code splitting,是没有这个问题的。但是我们就是要用,哼!

解决问题参考这里:https://github.com/gaearon/react-hot-loader/tree/next#code-splitting

解决步骤:

  1. npm install react-hot-loader@next
  2. 首页举例子
import {hot} from 'react-hot-loader';
...
export default hot(module)(Home);

其他模块如果需要,可以自己同理修改哦。

Pull Request 与 Merge Request 的区别

这篇文章只为说明一个问题:“Pull Request 与 Merge Request 有什么区别?”
在我的想象中,有一双滑板鞋~不好意思,跑偏了。
在我的想象中,它俩肯定是不一样的,并且大部分人的想法应该和我是一样的,我先来说说我的想法。

自我 YY

如果经常用 Github,一定十分了解 Pull Request。
如果经常用 Gitlab,一定十分了解 Merge Request。
基于对 Github 和 Gitlab 的了解,我潜意识里感知到 Pull Request 与 Merge Request 是有区别的。


Github 一般是公开库,当然没有人愿意别人直接在自己的仓库上面修改代码。所以我们如果要给别人的仓库贡献代码,一般是要 fork 一个仓库,在自己的仓库改完后,给原仓库提交 PR 请求,请求原仓库主人把你的代码拉(pull)回去
下图是一般的 Github 工作流程。
image.png


Gitlab 一般是私有库,一个团队维护一个仓库,通常大家会新建自己的分支,开发完成后,请求合并回主干分支。
下图是一般的 Gitlab 工作流程。
image.png


基于上面的认知,我起初觉得

  • Github 这种需要 fork 仓库的模式,应该叫 Pull Requset,请求目标仓库来拉你的代码。
    “我改了你们的代码,你们拉回去看看吧 !”
  • Gitlab 这种纯分支模式,应该叫 Merge Request,是自己请求把代码合并进主干。
    “请求合并代码!”


说实话,我自己都说服不了自己,上面的理解是正确的。毕竟

  • 在 Github 上也可以玩分支模式,提交合并请求同样用 Pull Request。
  • 在 Gitlab 上也可以玩 fork 模式,提交合并请求还是 Merge Request。

真实情况

我们来看看 gitlab 官方是怎么说的:

Merge or pull requests are created in a git management application and ask an assigned person to merge two branches. Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch. Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee.

大概意思就是 Merge Request 和 Pull Request 是同一个东西,仅仅只是名字不一样。
一般我们执行分支合并,需要执行下面两个命令:

git pull // 拉回需要合并的分支
git merge // 合并进目标分支

Github 选择了第一个命令来命名,叫 Pull Request。
Gitlab 选择了最后一个命令来命名,叫 Merge Request。
这个理由是 Gitlab 官方给的,我觉得还是可信的。所以我们的结论就是**“Pull Request 和 Merge Request”是一个东西**。

吐槽

Pull Request 这个词起的真不好!我想起来我刚开始用 Github 的时候,根本看不懂 Pull Request 是干啥的。

  • Pull Request 是不是请求别人允许我拉他们的代码?如果我不请求,就不能拉别人的代码?
  • 提交代码是不是叫 Push Request 会更好?表示我想给别人的仓库 Push 代码。

如果我来起名的话,我应该会起这几个名字:

  • Merge Request 请求把代码合并进去
  • Push Request 请求把代码推进去
  • Check In Requset 发起代码准入检查
  • ......

无论如何也想不到 Pull Request,一个好名字还是非常非常重要的。网上能搜到很多人问 Pull Request 是什么意思,然后大家的解释都差不多“请求别人拉你的代码”。如果当时不用这个名字,大家一看就明白了,也不用问了。

image.png
又让我想起了小白时期被 redux 中的名词支配的恐惧, reducer 等新名字,看的我一愣一愣的。
就像我在 github 的简介中写的话一样,一切都是纸老虎。

有时候人们很喜欢造一些名字很吓人的名词,让人一听这个名词就觉得自己不可能学会,从而让人望而却步。但是其实这些名词背后所代表的东西其实很简单。



你有没有被奇奇怪怪的名字支配的恐惧呢?
如果是你,你会给 Pull Request 取什么名字呢?
欢迎留言互动,让大家看到你的想法。

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

React 18 对 Hooks 的影响:一

1. 译者前言

最近 React 18 发布后,部分改动对我们使用 React Hooks 有一些影响。这篇文章对官方的文档《Update to remove the "setState on unmounted component" warning》做了翻译,好让大家清晰的认识到这个改动的背景和影响。
这是 React 18 对 Hooks 的影响系列第一篇,后面我还会整理其它有影响的改动,关注不迷路。

2. 翻译

2.1 背景

之前在已经卸载的组件中调用 setState时,会有一个警告,本次我们将这个警告移除了。警告内容如下:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

警告:不能在已经卸载的组件中更改 state。这是一个无用的操作,它表明你的项目中存在内存泄漏。要解决这个问题,请在 useEffect 清理函数中取消所有订阅和异步任务。

不幸的是,这个警告经常被误解,并且会误导大家。

它原本是想保证如下示例能正常工作的:

useEffect(() => {
  function handleChange() {
     setState(store.getState())
  }
  store.subscribe(handleChange)
  return () => store.unsubscribe(handleChange)
}, [])

在这个例子中,如果你忘记了在 effect 清理函数中调用 unsubscribe,那肯定是有内存泄漏的。

2.2 为什么说这个警告会误导大家呢?

事实上,上面的场景并不常见。反而如下场景是更常见的:

async function handleSubmit() {
  setPending(true)
  await post('/someapi') // component might unmount while we're waiting
  setPending(false)
}

在上面的代码中,如果发送请求时,组件卸载了,会抛出警告。但是,在这种场景下,警告误导了大家。

这里其实没有内存泄漏:

  • Promise 会很快完成执行,然后内存被垃圾回收机制回收
  • 即使没有很快完成执行,这个警告也是没用的,因为垃圾回收也还是得等 Promise 执行完回收,你啥也不能做

一般,我们会通过如下代码来消除警告:

let isMountedRef = useRef(false)
useEffect(() => {
  isMountedRef.current = true
  return () => {
    isMountedRef.current = false
  }
}, [])

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  if (!isMountedRef.current) {
    setPending(false)
  }
}

实际上,这种写法是没用的,并没有解决所谓的“内存泄漏”,它仅仅只是抑制了警告。正如前面说的,这里其实是没有内存泄漏的。内存会随着 Promise 执行完而释放,并没有什么在无限执行。

2.3 上述抑制警告方案比不处理更糟糕

上述抑制警告的解决方案,现在非常非常普遍。但它其实没任何好处,反而比不处理更糟糕:

  • 未来,React 会提供一个新能力,在组件卸载不可见时,我们会保存组件现在的 state,但仍会卸载组件。下次加载组件的时候,我们会用之前保存的 state 来渲染组件,以便恢复之前的页面。
    在组件卸载之后,setPending(false)不会被执行到,所以 pending会一直是 true,那下次恢复组件的时候,看起来是请求没有执行完成,会变的更糟糕。(译者注:关于这一块详细的行为和影响面,下一篇文章介绍)

  • 假设用户点击一个按钮,发起一个网络请求,请求结束后更新 state。为了避免这个警告,有些人会将请求行为放到 useEffect 中,因为在 useEffect 中可以监听到组件卸载,以忽略后续的 state 更新,消除警告。这样代码变的非常不清晰,非常糟糕!就是因为这个错误的警告,会让大家写出更烂的代码。

2.4 移除警告

最终,我们决定移除这个警告。这个警告想解决的订阅问题,在日常代码中并不常见。大部分情况下,它反而会误导大家,为了避免告警写出更烂的代码。
希望这个警告的移除,可以让你移除代码中的 isMounted

3. 译者总结

在 ahooks 中的 useUnmountedRefuseSafeState 等都是为了解决这个警告而生的。同时我们在 ahooks 中必要的地方,为了避免这个告警,也会在组件卸载后,忽略后续的 setState

目前来看,这些代码是多余的,后续 ahooks 会陆续优化相关场景,但不会太快。因为在 react 16、17 中这个告警仍然会有,会对新人造成不必要的困扰。我们会等 React 18 覆盖面比较广之后,再进行代码优化。

以后在代码中大家应该不需要再考虑这个告警了,不需要再使用 useUnmountedRefuseSafeState 等 Hooks 了。

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

完全理解 redux(从零实现一个 redux)

完全理解 redux

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

目录

  1. 前言
  2. 状态管理器
    • 简单的状态管理器
    • 有计划的状态管理器
  3. 多文件协作
    • reducer 的拆分和合并
    • state 的拆分和合并
  4. 中间件 middleware
  5. 完整的 redux
  6. 最佳实践
    • 纯函数
  7. 总结

前言

记得开始接触 react 技术栈的时候,最难理解的地方就是 redux。全是新名词:reducer、store、dispatch、middleware 等等,我就理解 state 一个名词。

网上找的 redux 文章,要不有一本书的厚度,要不很玄乎,晦涩难懂,越看越觉得难,越看越怕,信心都没有了!

花了很长时间熟悉 redux,慢慢的发现它其实真的很简单。本章不会把 redux 的各种概念,名词解释一遍,这样和其他教程没有任何区别,没有太大意义。我会带大家从零实现一个完整的 redux,让大家知其然,知其所以然。

开始前,你必须知道一些事情:

  • redux 和 react 没有关系,redux 可以用在任何框架中,忘掉 react。
  • connect 不属于 redux,它其实属于 react-redux,请先忘掉它,下一章节,我们会介绍它。
  • 请一定先忘记 reducer、store、dispatch、middleware 等等这些名词。
  • redux 是一个状态管理器。

Let's Go!

状态管理器

简单的状态管理器

redux 是一个状态管理器,那什么是状态呢?状态就是数据,比如计数器中的 count。

let state = {
  count: 1
}

我们来使用下状态

console.log(state.count);

我们来修改下状态

state.count = 2;

好了,现在我们实现了状态(计数)的修改和使用了。

读者:你当我傻吗?你说的这个谁不知道?捶你👊!

笔者:哎哎哎,别打我!有话好好说!redux 核心就是这个呀!我们一步一步扩展开来嘛!

当然上面的有一个很明显的问题:修改 count 之后,使用 count 的地方不能收到通知。我们可以使用发布-订阅模式来解决这个问题。

/*------count 的发布订阅者实践------*/
let state = {
  count: 1
};
let listeners = [];

/*订阅*/
function subscribe(listener) {
  listeners.push(listener);
}

function changeCount(count) {
  state.count = count;
  /*当 count 改变的时候,我们要去通知所有的订阅者*/
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i];
    listener();
  }
}

我们来尝试使用下这个简单的计数状态管理器。

/*来订阅一下,当 count 改变的时候,我要实时输出新的值*/
subscribe(() => {
  console.log(state.count);
});

/*我们来修改下 state,当然我们不能直接去改 state 了,我们要通过 changeCount 来修改*/
changeCount(2);
changeCount(3);
changeCount(4);

现在我们可以看到,我们修改 count 的时候,会输出相应的 count 值。

现在有两个新的问题摆在我们面前

  • 这个状态管理器只能管理 count,不通用
  • 公共的代码要封装起来

我们尝试来解决这个问题,把公共的代码封装起来

const createStore = function (initState) {
  let state = initState;
  let listeners = [];

  /*订阅*/
  function subscribe(listener) {
    listeners.push(listener);
  }

  function changeState(newState) {
    state = newState;
    /*通知*/
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  return {
    subscribe,
    changeState,
    getState
  }
}

我们来使用这个状态管理器管理多个状态 counter 和 info 试试

let initState = {
  counter: {
    count: 0
  },
  info: {
    name: '',
    description: ''
  }
}

let store = createStore(initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(`${state.info.name}${state.info.description}`);
});
store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count);
});

store.changeState({
  ...store.getState(),
  info: {
    name: '前端九部',
    description: '我们都是前端爱好者!'
  }
});

store.changeState({
  ...store.getState(),
  counter: {
    count: 1
  }
});

到这里我们完成了一个简单的状态管理器。

这里需要理解的是 createStore,提供了 changeStategetStatesubscribe 三个能力。

本小节完整源码见 demo-1

有计划的状态管理器

我们用上面的状态管理器来实现一个自增,自减的计数器。

let initState = {
  count: 0
}
let store = createStore(initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.count);
});
/*自增*/
store.changeState({
  count: store.getState().count + 1
});
/*自减*/
store.changeState({
  count: store.getState().count - 1
});
/*我想随便改*/
store.changeState({
  count: 'abc'
});

你一定发现了问题,count 被改成了字符串 abc,因为我们对 count 的修改没有任何约束,任何地方,任何人都可以修改。

我们需要约束,不允许计划外的 count 修改,我们只允许 count 自增和自减两种改变方式!

那我们分两步来解决这个问题

  1. 制定一个 state 修改计划,告诉 store,我的修改计划是什么。
  2. 修改 store.changeState 方法,告诉它修改 state 的时候,按照我们的计划修改。

我们来设置一个 plan 函数,接收现在的 state,和一个 action,返回经过改变后的新的 state。

/*注意:action = {type:'',other:''}, action 必须有一个 type 属性*/
function plan(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state;
  }
}

我们把这个计划告诉 store,store.changeState 以后改变 state 要按照我的计划来改。

/*增加一个参数 plan*/
const createStore = function (plan, initState) {
  let state = initState;
  let listeners = [];

  function subscribe(listener) {
    listeners.push(listener);
  }

  function changeState(action) {
    /*请按照我的计划修改 state*/  
    state = plan(state, action);
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }

  return {
    subscribe,
    changeState,
    getState
  }
}

我们来尝试使用下新的 createStore 来实现自增和自减

let initState = {
  count: 0
}
/*把plan函数*/
let store = createStore(plan, initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.count);
});
/*自增*/
store.changeState({
  type: 'INCREMENT'
});
/*自减*/
store.changeState({
  type: 'DECREMENT'
});
/*我想随便改 计划外的修改是无效的!*/
store.changeState({
  count: 'abc'
});

到这里为止,我们已经实现了一个有计划的状态管理器!

我们商量一下吧?我们给 plan 和 changeState 改下名字好不好?**plan 改成 reducer,changeState 改成 dispatch!**不管你同不同意,我都要换,因为新名字比较厉害(其实因为 redux 是这么叫的)!

本小节完整源码见 demo-2

多文件协作

reducer 的拆分和合并

这一小节我们来处理下 reducer 的问题。啥问题?

我们知道 reducer 是一个计划函数,接收老的 state,按计划返回新的 state。那我们项目中,有大量的 state,每个 state 都需要计划函数,如果全部写在一起会是啥样子呢?

所有的计划写在一个 reducer 函数里面,会导致 reducer 函数及其庞大复杂。按经验来说,我们肯定会按组件维度来拆分出很多个 reducer 函数,然后通过一个函数来把他们合并起来。

我们来管理两个 state,一个 counter,一个 info。

let state = {
  counter: {
    count: 0
  },
  info: {
    name: '前端九部',
    description: '我们都是前端爱好者!'
  }
}

他们各自的 reducer

/*counterReducer, 一个子reducer*/
/*注意:counterReducer 接收的 state 是 state.counter*/
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - 1
      }
    default:
      return state;
  }
}
/*InfoReducer,一个子reducer*/
/*注意:InfoReducer 接收的 state 是 state.info*/
function InfoReducer(state, action) {
  switch (action.type) {
    case 'SET_NAME':
      return {
        ...state,
        name: action.name
      }
    case 'SET_DESCRIPTION':
      return {
        ...state,
        description: action.description
      }
    default:
      return state;
  }
}

那我们用 combineReducers 函数来把多个 reducer 函数合并成一个 reducer 函数。大概这样用

const reducer = combineReducers({
    counter: counterReducer,
    info: InfoReducer
});

我们尝试实现下 combineReducers 函数

function combineReducers(reducers) {

  /* reducerKeys = ['counter', 'info']*/
  const reducerKeys = Object.keys(reducers)

  /*返回合并后的新的reducer函数*/
  return function combination(state = {}, action) {
    /*生成的新的state*/
    const nextState = {}

    /*遍历执行所有的reducers,整合成为一个新的state*/
    for (let i = 0; i < reducerKeys.length; i++) {
      const key = reducerKeys[i]
      const reducer = reducers[key]
      /*之前的 key 的 state*/
      const previousStateForKey = state[key]
      /*执行 分 reducer,获得新的state*/
      const nextStateForKey = reducer(previousStateForKey, action)

      nextState[key] = nextStateForKey
    }
    return nextState;
  }
}

我们来尝试下 combineReducers 的威力吧

const reducer = combineReducers({
  counter: counterReducer,
  info: InfoReducer
});

let initState = {
  counter: {
    count: 0
  },
  info: {
    name: '前端九部',
    description: '我们都是前端爱好者!'
  }
}

let store = createStore(reducer, initState);

store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count, state.info.name, state.info.description);
});
/*自增*/
store.dispatch({
  type: 'INCREMENT'
});

/*修改 name*/
store.dispatch({
  type: 'SET_NAME',
  name: '前端九部2号'
});

本小节完整源码见 demo-3

state 的拆分和合并

上一小节,我们把 reducer 按组件维度拆分了,通过 combineReducers 合并了起来。但是还有个问题, state 我们还是写在一起的,这样会造成 state 树很庞大,不直观,很难维护。我们需要拆分,一个 state,一个 reducer 写一块。

这一小节比较简单,我就不卖关子了,用法大概是这样(注意注释)

/* counter 自己的 state 和 reducer 写在一起*/
let initState = {
  count: 0
}
function counterReducer(state, action) {
  /*注意:如果 state 没有初始值,那就给他初始值!!*/  
  if (!state) {
      state = initState;
  }
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      }
    default:    
      return state;
  }
}

我们修改下 createStore 函数,增加一行 dispatch({ type: Symbol() })

const createStore = function (reducer, initState) {
  let state = initState;
  let listeners = [];

  function subscribe(listener) {
    listeners.push(listener);
  }

  function dispatch(action) {
    state = reducer(state, action);
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  function getState() {
    return state;
  }
  /* 注意!!!只修改了这里,用一个不匹配任何计划的 type,来获取初始值 */
  dispatch({ type: Symbol() })

  return {
    subscribe,
    dispatch,
    getState
  }
}

我们思考下这行可以带来什么效果?

  1. createStore 的时候,用一个不匹配任何 type 的 action,来触发 state = reducer(state, action)
  2. 因为 action.type 不匹配,每个子 reducer 都会进到 default 项,返回自己初始化的 state,这样就获得了初始化的 state 树了。

你可以试试

/*这里没有传 initState 哦 */
const store = createStore(reducer);
/*这里看看初始化的 state 是什么*/
console.dir(store.getState());

本小节完整源码见 demo-4

到这里为止,我们已经实现了一个七七八八的 redux 啦!

中间件 middleware

中间件 middleware 是 redux 中最难理解的地方。但是我挑战一下用最通俗的语言来讲明白它。如果你看完这一小节,还没明白中间件是什么,不知道如何写一个中间件,那就是我的锅了!

中间件是对 dispatch 的扩展,或者说重写,增强 dispatch 的功能!

记录日志

我现在有一个需求,在每次修改 state 的时候,记录下来 修改前的 state ,为什么修改了,以及修改后的 state。我们可以通过重写 store.dispatch 来实现,直接看代码

const store = createStore(reducer);
const next = store.dispatch;

/*重写了store.dispatch*/
store.dispatch = (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

我们来使用下

store.dispatch({
  type: 'INCREMENT'
});

日志输出为

this state { counter: { count: 0 } }
action { type: 'INCREMENT' }
1
next state { counter: { count: 1 } }

现在我们已经实现了一个完美的记录 state 修改日志的功能!

记录异常

我又有一个需求,需要记录每次数据出错的原因,我们扩展下 dispatch

const store = createStore(reducer);
const next = store.dispatch;

store.dispatch = (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

这样每次 dispatch 出异常的时候,我们都会记录下来。

多中间件的合作

我现在既需要记录日志,又需要记录异常,怎么办?当然很简单了,两个函数合起来呗!

store.dispatch = (action) => {
  try {
    console.log('this state', store.getState());
    console.log('action', action);
    next(action);
    console.log('next state', store.getState());
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

如果又来一个需求怎么办?接着改 dispatch 函数?那再来10个需求呢?到时候 dispatch 函数肯定庞大混乱到无法维护了!这个方式不可取呀!

我们需要考虑如何实现扩展性很强的多中间件合作模式。

  1. 我们把 loggerMiddleware 提取出来

    const store = createStore(reducer);
    const next = store.dispatch;
    
    const loggerMiddleware = (action) => {
      console.log('this state', store.getState());
      console.log('action', action);
      next(action);
      console.log('next state', store.getState());
    }
    
    store.dispatch = (action) => {
      try {
        loggerMiddleware(action);
      } catch (err) {
        console.error('错误报告: ', err)
      }
    }
  2. 我们把 exceptionMiddleware 提取出来

    const exceptionMiddleware = (action) => {
      try {
        /*next(action)*/
        loggerMiddleware(action);
      } catch (err) {
        console.error('错误报告: ', err)
      } 
    }
    store.dispatch = exceptionMiddleware;
  3. 现在的代码有一个很严重的问题,就是 exceptionMiddleware 里面写死了 loggerMiddleware,我们需要让 next(action)变成动态的,随便哪个中间件都可以

    const exceptionMiddleware = (next) => (action) => {
      try {
        /*loggerMiddleware(action);*/
        next(action);
      } catch (err) {
        console.error('错误报告: ', err)
      } 
    }
    /*loggerMiddleware 变成参数传进去*/
    store.dispatch = exceptionMiddleware(loggerMiddleware);
  4. 同样的道理,loggerMiddleware 里面的 next 现在恒等于 store.dispatch,导致 loggerMiddleware 里面无法扩展别的中间件了!我们也把 next 写成动态的

    const loggerMiddleware = (next) => (action) => {
      console.log('this state', store.getState());
      console.log('action', action);
      next(action);
      console.log('next state', store.getState());
    }

到这里为止,我们已经探索出了一个扩展性很高的中间件合作模式!

const store = createStore(reducer);
const next = store.dispatch;

const loggerMiddleware = (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next));

这时候我们开开心心的新建了一个 loggerMiddleware.js,一个exceptionMiddleware.js文件,想把两个中间件独立到单独的文件中去。会碰到什么问题吗?

loggerMiddleware 中包含了外部变量 store,导致我们无法把中间件独立出去。那我们把 store 也作为一个参数传进去好了~

const store = createStore(reducer);
const next  = store.dispatch;

const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));

到这里为止,我们真正的实现了两个可以独立的中间件啦!

现在我有一个需求,在打印日志之前输出当前的时间戳。用中间件来实现!

const timeMiddleware = (store) => (next) => (action) => {
  console.log('time', new Date().getTime());
  next(action);
}
...
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));

本小节完整源码见 demo-6

中间件使用方式优化

上一节我们已经完全实现了正确的中间件!但是中间件的使用方式不是很友好

import loggerMiddleware from './middlewares/loggerMiddleware';
import exceptionMiddleware from './middlewares/exceptionMiddleware';
import timeMiddleware from './middlewares/timeMiddleware';

...

const store = createStore(reducer);
const next = store.dispatch;

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
const time = timeMiddleware(store);
store.dispatch = exception(time(logger(next)));

其实我们只需要知道三个中间件,剩下的细节都可以封装起来!我们通过扩展 createStore 来实现!

先来看看期望的用法

/*接收旧的 createStore,返回新的 createStore*/
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);

/*返回了一个 dispatch 被重写过的 store*/
const store = newCreateStore(reducer);

实现 applyMiddleware

const applyMiddleware = function (...middlewares) {
  /*返回一个重写createStore的方法*/
  return function rewriteCreateStoreFunc(oldCreateStore) {
     /*返回重写后新的 createStore*/
    return function newCreateStore(reducer, initState) {
      /*1. 生成store*/
      const store = oldCreateStore(reducer, initState);
      /*给每个 middleware 传下store,相当于 const logger = loggerMiddleware(store);*/
      /* const chain = [exception, time, logger]*/
      const chain = middlewares.map(middleware => middleware(store));
      let dispatch = store.dispatch;
      /* 实现 exception(time((logger(dispatch))))*/
      chain.reverse().map(middleware => {
        dispatch = middleware(dispatch);
      });

      /*2. 重写 dispatch*/
      store.dispatch = dispatch;
      return store;
    }
  }
}

让用户体验美好

现在还有个小问题,我们有两种 createStore 了

/*没有中间件的 createStore*/
import { createStore } from './redux';
const store = createStore(reducer, initState);

/*有中间件的 createStore*/
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(reducer, initState);

为了让用户用起来统一一些,我们可以很简单的使他们的使用方式一致,我们修改下 createStore 方法

const createStore = (reducer, initState, rewriteCreateStoreFunc) => {
    /*如果有 rewriteCreateStoreFunc,那就采用新的 createStore */
    if(rewriteCreateStoreFunc){
       const newCreateStore =  rewriteCreateStoreFunc(createStore);
       return newCreateStore(reducer, initState);
    }
    /*否则按照正常的流程走*/
    ...
}

最终的用法

const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);

const store = createStore(reducer, initState, rewriteCreateStoreFunc);

本小节完整源码见 demo-7

完整的 redux

退订

不能退订的订阅都是耍流浪!我们修改下 store.subscribe 方法,增加退订功能

  function subscribe(listener) {
    listeners.push(listener);
    return function unsubscribe() {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

使用

const unsubscribe = store.subscribe(() => {
  let state = store.getState();
  console.log(state.counter.count);
});
/*退订*/
unsubscribe();

中间件拿到的store

现在的中间件拿到了完整的 store,他甚至可以修改我们的 subscribe 方法,按照最小开放策略,我们只用把 getState 给中间件就可以了!因为我们只允许你用 getState 方法!

修改下 applyMiddleware 中给中间件传的 store

/*const chain = middlewares.map(middleware => middleware(store));*/
const simpleStore = { getState: store.getState };
const chain = middlewares.map(middleware => middleware(simpleStore));

compose

我们的 applyMiddleware 中,把 [A, B, C] 转换成 A(B(C(next))),是这样实现的

const chain = [A, B, C];
let dispatch = store.dispatch;
chain.reverse().map(middleware => {
   dispatch = middleware(dispatch);
});

redux 提供了一个 compose 方式,可以帮我们做这个事情

const chain = [A, B, C];
dispatch = compose(...chain)(store.dispatch)

看下他是如何实现的

export default function compose(...funcs) {
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

当然 compose 函数对于新人来说可能比较难理解,你只需要他是做什么的就行啦!

省略initState

有时候我们创建 store 的时候不传 initState,我们怎么用?

const store = createStore(reducer, {}, rewriteCreateStoreFunc);

redux 允许我们这样写

const store = createStore(reducer, rewriteCreateStoreFunc);

我们仅需要改下 createStore 函数,如果第二个参数是一个object,我们认为他是 initState,如果是 function,我们就认为他是 rewriteCreateStoreFunc。

function craeteStore(reducer, initState, rewriteCreateStoreFunc){
    if (typeof initState === 'function'){
    rewriteCreateStoreFunc = initState;
    initState = undefined;
  }
  ...
}

2 行代码的 replaceReducer

reducer 拆分后,和组件是一一对应的。我们就希望在做按需加载的时候,reducer也可以跟着组件在必要的时候再加载,然后用新的 reducer 替换老的 reducer。

const createStore = function (reducer, initState) {
  ...
  function replaceReducer(nextReducer) {
    reducer = nextReducer
    /*刷新一遍 state 的值,新来的 reducer 把自己的默认状态放到 state 树上去*/
    dispatch({ type: Symbol() })
  }
  ...
  return {
    ...
    replaceReducer
  }
}

我们来尝试使用下

const reducer = combineReducers({
  counter: counterReducer
});
const store = createStore(reducer);

/*生成新的reducer*/
const nextReducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer);

replaceReducer 示例源码见 demo-5

bindActionCreators

bindActionCreators 我们很少很少用到,一般只有在 react-redux 的 connect 实现中用到。

他是做什么的?他通过闭包,把 dispatch 和 actionCreator 隐藏起来,让其他地方感知不到 redux 的存在。

我们通过普通的方式来 隐藏 dispatch 和 actionCreator 试试,注意最后两行代码

const reducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});
const store = createStore(reducer);

/*返回 action 的函数就叫 actionCreator*/
function increment() {
  return {
    type: 'INCREMENT'
  }
}

function setName(name) {
  return {
    type: 'SET_NAME',
    name: name
  }
}

const actions = {
  increment: function () {
    return store.dispatch(increment.apply(this, arguments))
  },
  setName: function () {
    return store.dispatch(setName.apply(this, arguments))
  }
}
/*注意:我们可以把 actions 传到任何地方去*/
/*其他地方在实现自增的时候,根本不知道 dispatch,actionCreator等细节*/
actions.increment(); /*自增*/
actions.setName('九部威武'); /*修改 info.name*/

我眼睛一看,这个 actions 生成的时候,好多公共代码,提取一下

const actions = bindActionCreators({ increment, setName }, store.dispatch);

来看一下 bindActionCreators 的源码,超级简单(就是生成了刚才的 actions)

/*核心的代码在这里,通过闭包隐藏了 actionCreator 和 dispatch*/
function bindActionCreator(actionCreator, dispatch) {
  return function () {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

/* actionCreators 必须是 function 或者 object */
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error()
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreators 示例源码见 demo-8

大功告成

完整的示例源码见 demo-9,你可以和 redux 源码做一下对比,你会发现,我们已经实现了 redux 所有的功能了。

当然,为了保证代码的理解性,我们少了一些参数验证。比如 createStore(reducer)的参数 reducer 必须是 function 等等。

最佳实践

纯函数

什么是纯函数?

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

通俗来讲,就两个要素

  1. 相同的输入,一定会得到相同的输出
  2. 不会有 “触发事件”,更改输入参数,依赖外部参数,打印 log 等等副作用
/*不是纯函数,因为同样的输入,输出结果不一致*/
function a( count ){
   return count + Math.random();
}

/*不是纯函数,因为外部的 arr 被修改了*/
function b( arr ){
    return arr.push(1);
}
let arr = [1, 2, 3];
b(arr);
console.log(arr); //[1, 2, 3, 1]

/*不是纯函数,以为依赖了外部的 x*/
let x = 1;
function c( count ){
    return count + x;
}

我们的 reducer 计划函数,就必须是一个纯函数!

只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

总结

到了最后,我想把 redux 中关键的名词列出来,你每个都知道是干啥的吗?

  • createStore

    创建 store 对象,包含 getState, dispatch, subscribe, replaceReducer

  • reducer

    reducer 是一个计划函数,接收旧的 state 和 action,生成新的 state

  • action

    action 是一个对象,必须包含 type 字段

  • dispatch

    dispatch( action ) 触发 action,生成新的 state

  • subscribe

    实现订阅功能,每次触发 dispatch 的时候,会执行订阅函数

  • combineReducers

    多 reducer 合并成一个 reducer

  • replaceReducer

    替换 reducer 函数

  • middleware

    扩展 dispatch 函数!

你再看 redux 流程图,是不是大彻大悟了?

redux 流程图

(redux 流程图)

React 18 总览

在 2021 年 6 月 8 号,React 公布了 v18 版本的发布计划,并发布了 alpha 版本。经过将近一年的发布前准备,在 2022 年 3 月 29 日,React 18 正式版终于和大家见面了。

React 18 应该是最近几年的一个重磅版本,React 官方对它寄予了厚望。不然也不会将 React 17 作为一个过渡版本,也不会光发布准备工作就做了一年。

在过去一年,我们已经或多或少了解到一些 React 18 的新功能。这篇文章我会通过丰富的示例,向大家系统的介绍 React 18 带来的改变。当然本文融入了很多个人理解,如有不对,烦请指正。

Concurrent Mode

Concurrent Mode(以下简称 CM)翻译叫并发模式,这个概念我已经听了好多年了,并且一度非常担忧

  • React 官方憋了好多年的大招,会不会是一个破坏性不兼容的超级大版本?就像 VUE v3 和 v2。
  • 现有的生态是不是都得跟着大版本升级?比如 ant design,ahooks 等。

随着对 CM 的了解,我发现它其实是人畜无害的。

CM 本身并不是一个功能,而是一个底层设计,它使 React 能够同时准备多个版本的 UI

在以前,React 在状态变更后,会开始准备虚拟 DOM,然后渲染真实 DOM,整个流程是串行的。一旦开始触发更新,只能等流程完全结束,期间是无法中断的。

在 CM 模式下,React 在执行过程中,每执行一个 Fiber,都会看看有没有更高优先级的更新,如果有,则当前低优先级的的更新会被暂停,待高优先级任务执行完之后,再继续执行或重新执行。

CM 模式有点类似计算机的多任务处理,处理器在同时进行的应用程序之间快速切换,也许 React 应该改名叫 ReactOS 了。

这里举个例子:我们正在看电影,这时候门铃响了,我们要去开门拿快递。
在 React 18 以前,一旦我们开始看电影,就不能被终止,必须等电影看完之后,才会去开门。
而在 React 18 CM 模式之后,我们就可以暂停电影,等开门拿完快递之后,再重新继续看电影。

不过对于普通开发者来说,我们一般是不会感知到 CM 的存在的,在升级到 React 18 之后,我们的项目不会有任何变化

我们需要关注的是基于 CM 实现的上层功能,比如 Suspense、Transitions、streaming server rendering(流式服务端渲染), 等等。

React 18 的大部分功能都是基于 CM 架构实现出来的,并且这这是一个开始,未来会有更多基于 CM 实现的高级能力。

startTransition

我们如果要主动发挥 CM 的优势,那就离不开 startTransition。

React 的状态更新可以分为两类:

  • 紧急更新(Urgent updates):比如打字、点击、拖动等,需要立即响应的行为,如果不立即响应会给人很卡,或者出问题了的感觉
  • 过渡更新(Transition updates):将 UI 从一个视图过渡到另一个视图。不需要即时响应,有些延迟是可以接受的。

我以前会认为,CM 模式会自动帮我们区分不同优先级的更新,一键无忧享受。很遗憾的是,CM 只是提供了可中断的能力,默认情况下,所有的更新都是紧急更新。

这是因为 React 并不能自动识别哪些更新是优先级更高的。

const [inputValue, setInputValue] = useState();

const onChange = (e)=>{
  setInputValue(e.target.value);
  // 更新搜索列表
  setSearchQuery(e.target.value);
}

return (
  <input value={inputValue} onChange={onChange} />
)

比如以上示例,用户的键盘输入操作后,setInputValue会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery是根据用户输入,查询相应的内容,是非紧急的。

但是 React 确实没有能力自动识别。所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的。

// 紧急的
setInputValue(e.target.value);
startTransition(() => {
  setSearchQuery(input); // 非紧急的
});

如上代码,我们通过 startTransition来标记一个非紧急更新,让该状态触发的变更变成低优先级的。

光用文字描述大家可能没有体验,接下来我们通过一个示例来认识下可中断渲染对性能的爆炸提升。

示例页面:https://react-fractals-git-react-18-swizec.vercel.app/

如下图,我们需要画一个毕达哥拉斯树,通过一个 Slider 来控制树的倾斜。

Kapture 2022-04-10 at 21.04.17.gif
那我们的代码会很简单,如下所示,我们只需要一个 treeLeanstate 来管理状态。

const [treeLean, setTreeLean] = useState(0)

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLean(value);
}

return (
  <>
    <input type="range" value={treeLean} onChange={changeTreeLean} />
    <Pythagoras lean={treeLean} />
  </>
)

在每次 Slider 拖动后,React 执行流程大致如下:

  1. 更新 treeLean
  2. 渲染 input,填充新的 value
  3. 重新渲染树组件 Pythagoras

每一次用户拖动 Slider,都会同步执行上述三步。但当树的节点足够多的时候,Pythagoras 渲染一次就非常慢,就会导致 Slider 的 value 回填变慢,用户感觉到严重的卡顿。如下图。

Kapture 2022-04-10 at 21.11.30.gif
当数的节点足够大时,已经卡到爆炸了。在 React 18 以前,我们是没有什么好的办法来解决这个问题的。但基于 React 18 CM 的可中断渲染机制,我们可以将树的更新渲染标记为低优先级的,就不会感觉到卡顿了。

Kapture 2022-04-10 at 21.16.29.gif

const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)

  // 将 treeLean 的更新用 startTransition 包裹
  React.startTransition(() => {
    setTreeLean(value);
  });
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Pythagoras lean={treeLean} />
  </>
)

以上代码,我们通过 startTransition 标记了非紧急更新,让树的更新变成低优先级的,可以被随时中止,保证了高优先级的 Slider 的体验。

此时更新流程变为了

  1. input 更新
    1. treeLeanInput 状态变更
    2. 准备新的 DOM
    3. 渲染 DOM
  2. 树更新(这一次更新是低优先级的,随时可以被中止)
    1. treeLean 状态变更
    2. 准备新的 DOM
    3. 渲染 DOM

React 会在高优先级更新渲染完成之后,才会启动低优先级更新渲染,并且低优先级渲染随时可被其它高优先级更新中断。

当然,在低优先状态等待更新过程中,如果能有一个 Loading 状态,那就更好了。React 18 提供了 useTransition来跟踪 transition 状态。

const [treeLeanInput, setTreeLeanInput] = useState(0);
const [treeLean, setTreeLean] = useState(0);

// 实时监听 transition 状态
const [isPending, startTransition] = useTransition();

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)

  React.startTransition(() => {
    setTreeLean(value);
  });
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Spin spinning={isPending}>
      <Pythagoras lean={treeLean} />
    </Spin>
  </>
)

自动批处理 Automatic Batching

批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能。比如

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

在 React 18 之前,React 只会在事件回调中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 会 render 两次,每次 state 变化更新一次
}, 1000);

而在 React 18 中,所有的状态更新,都会自动使用批处理,不关心场景。

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}, 1000);

如果你在某种场景下不想使用批处理,你可以通过 flushSync来强制同步执行(比如:你需要在状态更新后,立刻读取新 DOM 上的数据等。)

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React 更新一次 DOM
  flushSync(() => {
    setFlag(f => !f);
  });
  // React 更新一次 DOM
}

React 18 的批处理在绝大部分场景下是没有影响,但在 Class 组件中,如果你在两次 setState 中间读取了 state 值,会出现不兼容的情况,如下示例。

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // 在 React17 及之前,打印出来是 { count: 1, flag: false }
    // 在 React18,打印出来是 { count: 0, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

当然你可以通过 flushSync来修正它。

handleClick = () => {
  setTimeout(() => {
    ReactDOM.flushSync(() => {
      this.setState(({ count }) => ({ count: count + 1 }));
    });

    // 在 React18,打印出来是 { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

流式 SSR

SSR 一次页面渲染的流程大概为:

  1. 服务器 fetch 页面所需数据
  2. 数据准备好之后,将组件渲染成 string 形式作为 response 返回
  3. 客户端加载资源
  4. 客户端合成(hydrate)最终的页面内容

在传统的 SSR 模式中,上述流程是串行执行的,如果其中有一步比较慢,都会影响整体的渲染速度。

而在 React 18 中,基于全新的 Suspense,支持了流式 SSR,也就是允许服务端一点一点的返回页面。

假设我们有一个页面,包含了 NavBar、Sidebar、Post、Comments 等几个部分,在传统的 SSR 模式下,我们必须请求到 Post 数据,请求到 Comments 数据后,才能返回完整的 HTML。

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section>
    <!-- Comments -->
    <p>First comment</p>
    <p>Second comment</p>
  </section>
</main>

image.png
但如果 Comments 数据请求很慢,会拖慢整个流程。

在 React 18 中,我们通过 Suspense包裹,可以告诉 React,我们不需要等这个组件,可以先返回其它内容,等这个组件准备好之后,单独返回。

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

如上,我们通过 Suspense包裹了 Comments 组件,那服务器首次返回的 HTML 是下面这样的,<Comments />组件处通过 loading进行了占位。

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>

image.png
<Comments /> 组件准备好之后,React 会通过同一个流(stream)发送给浏览器(res.send 替换成 res.socket),并替换到相应位置。

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

更多关于流式 SSR 的讲解可见:reactwg/react-18#37

Server Component

Server Component 叫服务端组件,目前还在开发过程中,没有正式发布,不过应该很快就会和我们见面的。
image.png
Server Component 的本质就是由服务端生成 React 组件,返回一个 DSL 给客户端,客户端解析 DSL 并渲染该组件。

Server Component 带来的优势有:

  1. 零客户端体积,运行在服务端的组件只会返回最终的 DSL 信息,而不包含其他任何依赖。
// NoteWithMarkdown.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

假设我们有一个 markdown 渲染组件,以前我们需要将依赖 markedsanitize-html打包到 JS 中。如果该组件在服务端运行,则最终返回给客户端的是转换完成的文本。

  1. 组件拥有完整的服务端能力
    由于 Server Component 在服务端执行,拥有了完整的 NodeJS 的能力,可以访问任何服务端 API。
// Note.server.js - Server Component
import fs from 'react-fs';

function Note({id}) {
  const note = JSON.parse(fs.readFile(`${id}.json`));
  return <NoteWithMarkdown note={note} />;
}
  1. 组件支持实时更新
    由于 Server Component 在服务端执行,理论上支持实时更新,类似动态 npm 包,这个还是有比较大的想象空间的。也许 React Component as a service 时代来了。

当然说了这么多好处,Server Component 肯定也是有一些局限性的:

  1. 不能有状态,也就是不能使用 state、effect 等,那么更适合用在纯展示的组件,对性能要求较高的一些前台业务
  2. 不能访问浏览器的 API
  3. props 必须能被序列化

OffScreen

OffScreen 目前也在开发中,会在未来某个版本中发布。但我们非常有必要提前认识下它,因为你现在的代码很可能已经有问题了。

OffScreen 支持只保存组件的状态,而删除组件的 UI 部分。可以很方便的实现预渲染,或者 Keep Alive。比如我们在从 tabA 切换到 tabB,再返回 tabA 时,React 会使用之前保存的状态恢复组件。

为了支持这个能力,React 要求我们的组件对多次安装和销毁具有弹性。那什么样的代码不符合弹性要求呢?其实不符合要求的代码很常见。

async function handleSubmit() {
  setPending(true)
  await post('/someapi') // component might unmount while we're waiting
  setPending(false)
}

在上面的代码中,如果发送请求时,组件卸载了,会抛出警告。

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

警告:不能在已经卸载的组件中更改 state。这是一个无用的操作,它表明你的项目中存在内存泄漏。要解决这个问题,请在 useEffect 清理函数中取消所有订阅和异步任务。

所以我们一般都会通过一个 unmountRef来标记当前组件是否卸载,以避免所谓的「内存泄漏」。

function SomeButton(){
  const [pending, setPending] = useState(false)
  const unmountRef = useUnmountedRef();

  async function handleSubmit() {
    setPending(true)
    await post('/someapi')
    if (!unmountRef.current) {
      setPending(false)
    }
  }

  return (
    <Button onClick={handleSubmit} loading={pending}>
      提交
    </Button>
  )
}

我们来模拟执行一次组件,看看组件的变化状态:

  1. 首次加载时,组件的状态为:pending = false
  2. 点击按钮后,组件的状态会变为:pending = true
  3. 假如我们在请求过程中卸载了组件,那此时的状态会变为:pending = true

在 OffScreen 中,React 会保存住最后的状态,下次会用这些状态重新渲染组件。惨了,此时我们发现重新渲染组件一直在 loading。

怎么解决?解决办法很简单,就是回归最初的代码,删掉 unmountRef的逻辑。至于「内存泄漏」的警告,React 18 删除了,因为这里不存在内存泄漏(参考:https://mp.weixin.qq.com/s/fgT7Kxs_0feRx4TkBe6G5Q)。

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  setPending(false)
}

为了方便排查这类问题,在 React 18 的 Strict Mode 中,新增了 double effect,在开发模式下,每次组件初始化时,会自动执行一次卸载,重载。

* React mounts the component.
  * Layout effects are created.
  * Effects are created.
* React simulates unmounting the component.
  * Layout effects are destroyed.
  * Effects are destroyed.
* React simulates mounting the component with the previous state.
  * Layout effects are created.
  * Effects are created.

这里还是要再提示下:开发环境,在 React 18 的严格模式下,组件初始化的 useEffect 会执行两次,也就是可能 useEffect 里面的请求被执行了两次等。

新 Hooks

useDeferredValue

const deferredValue = useDeferredValue(value);

useDeferredValue 可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValue 和 startTransition 一样,都是标记了一次非紧急更新。

之前 startTransition 的例子,就可以用 useDeferredValue来实现。

const [treeLeanInput, setTreeLeanInput] = useState(0);

const deferredValue = useDeferredValue(treeLeanInput);

function changeTreeLean(event) {
  const value = Number(event.target.value);
  setTreeLeanInput(value)
}

return (
  <>
    <input type="range" value={treeLeanInput} onChange={changeTreeLean} />
    <Pythagoras lean={deferredValue} />
  </>
)

useId

const id = useId();

支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容。原理是每个 id 代表该组件在组件树中的层级结构。

useSyncExternalStore

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

useSyncExternalStore 能够让 React 组件在 Concurrent Mode 下安全地有效地读取外接数据源。
在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。
useSyncExternalStore 一般是三方状态管理库使用,一般我们不需要关注。

useInsertionEffect

useInsertionEffect(didUpdate);

这个 Hooks 只建议 css-in-js库来使用。
这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 生效之前,一般用于提前注入 <style> 脚本。

如何升级到 React 18

参考:https://mp.weixin.qq.com/s/2QYEmFlIIMQkXR-Q9DVG2Q

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

可能是你见过最好的 React Hooks 库

image

ahooks 是由蚂蚁 umi 团队、淘系 ice 团队以及阿里体育团队共同建设的 React Hooks 工具库。ahooks 基于 React Hooks 的逻辑封装能力,提供了大量常见好用的 Hooks,可以极大降低代码复杂度,提升开发效率。
ahooks 致力成为和 antd/fusion 一样的 React 基础设施,帮助开发者在逻辑层面省去大量的重复工作。

ahooks 前身

ahooks 的前身是蚂蚁开源的 @umijs/hooks,可以说 ahooks 是 umi hooks 的 2.0 版本。
umi hooks 从 2019年9月 发布 v1.0 之后,一路前行,得到了不少用户的青睐。截至当前,umi hooks 在社区收获了 2.2k star,npm 周下载量最高 7000+,tnpm 周下载量 8000+。
同时在蚂蚁内部,umi hooks 也已经成为标准 React Hooks 库,截至当前,能统计到的项目中有 600+ 项目依赖了 umi hooks。并且 useRequest 也已经成为 umi3 内置请求方案
但 umi hooks 半年来的野蛮生长,也带来了一些副作用。

  • 部分 Hooks 设计不合理,后期进行了部分 Hooks 合并,废弃了一些 Hooks。
  • 没有制定 API 标准,导致已有 Hooks 的 API 格式与命名不统一。

我们希望有个机会能彻底解决这两个心病。

共建

随着 React Hooks 的发展,各个团队都开始尝试使用 Hooks 代替 Class,Hooks 正逐渐成为 React 组件的主流写法。得益于 Hooks 的逻辑封装能力,我们可以将常见的逻辑封装起来,以减少代码复杂度。或者使用社区上别人封装的 Hooks,比如 react-use 等。
当然出于种种原因,很多团队希望建设自己的 Hooks 库。但在建设过程中,能发现各个 Hooks 库提供的 Hooks 大同小异,尤其是基础类 Hooks 几乎都是一样的。
基于避免重复建设的目的,以及 umi hooks 的积累,我们与集团 ice 团队,阿里体育团队一拍即合,决定基于 umi hooks 共同建设 React Hooks 工具库,ahooks 随即诞生。

现状

经过一个半月的改造,ahooks 已经发布了 v1.0 版本,并开源在 https://github.com/alibaba/hooks 仓库,你可以放心的在生产环境使用。
ahooks 相较于 umi hooks,有了自己的 API 规范,我们基于这套规范,重新整理了所有 Hooks 的 API,你可以在这里找到升级详情。
在 ahooks 的开发过程中,集团内也有其它很多部门参与进来,出谋划策,感谢大家。

规划

如前面所说,ahooks 致力成为向 antd/fusion 一样的 React 基础设施。为了达到这个目标,我们正在全力开发更多的 Hooks,同时我们也期望大家能将日常封装的 Hooks 贡献到 ahooks 中,一起来帮助 ahooks 成长。

  • 你可以提交一个 RFC,我们会帮你评估 Hooks 的必要性及 API 的规范。
  • 你也可以提交一个 idea,我们帮你实现。

除了 Hooks 库,我们也在准备 React Hooks 系列教程。不得不承认,虽然 React Hooks 很好用,但其中确实有有不少的明坑暗坑,我们希望通过系列教程,减少大家在使用 Hooks 时的困惑,避免走弯路。

可以不用看的附录

  • 推荐之前的几篇文章,可以帮助你对 umi hooks/ahooks 有一个更深入的认识:

  • 应该很多人想问,为什么不直接用 react-use,而是要自己建设 React Hooks 库呢?

    • 正如之前很多文章中说的,react-use 大版本升级太快了,实在跟不上。我第一次用的时候是 v9,上次写文章的时候是 v13,现在是 v15。如果大面积使用起来,升级起来太麻烦了。
    • 另外一点就是 react-use 的 API 设计也是没有规范的,同类 Hooks 的 API 各种各样。
    • 当然不可否认的是,react-use 是社区最流行的 Hooks 库,为 ahooks 提供了很多灵感。

React Custom Hooks 最佳实践

组件

组件是 UI + 逻辑的复用,但逻辑复用能力等于 0。

一个 React 项目,是由无数个大大小小的组件组合而成的。在 React 的世界中,组件是一等公民。而我们平时拆分组件的依据无非是:尽量的复用代码。

组件是 UI + 逻辑的复用,你不能将 UI 和逻辑拆开。比如 Antd 的 Cascader 级联选择 组件,内置了样式和级联选择的逻辑,用户使用的时候相当于一个黑盒,只管用就行了。但是有一个很现实的问题,当该组件的样式不能满足我们需求的时候,我们需要从 0 重新实现一个组件,重写 UI + 逻辑,哪怕逻辑真的一模一样。组件的逻辑复用能力等于 0。我可以想到一个可怕的事实,社区上的同类组件,大部分的逻辑都是可以复用的,只是在样式上有差异,但逻辑共享在社区上并没有很流行。

HOC 与 Render Props

HOC 与 Render Props 可以把逻辑抽出来复用,但并没有让逻辑复用流行起来。

当然,我上面说的不能复用有点夸张,React 提供了 HOC 与 Render Props 两种方式来解决逻辑复用的问题。比如下面的监听鼠标位置的逻辑,我们就可以通过 Render Props 来复用。

<Mouse>
  (position)=> <OurComponent />
</Mouse>

同类逻辑包括监听 window size,监听 scroll 位置等等。但是我们一般很少用 render props 来封装逻辑,更少去和其它项目去共享逻辑。为什么呢?想想多个逻辑复用会怎么样,你就知道多可怕了。

<WindowSize>
  (size)=> (
        <Mouse>
        (position)=> <OurComponent size={size} mouse={position}/>
    </Mouse>
    )
</WindowSize> 

嵌套地狱的代码是我们不能忍受的,同时 HOC 也存在类似的问题,这可能是导致逻辑复用不能流行起来的一个重要原因。

React Hooks

React Hooks 很好的解决了逻辑复用的问题,同时社区中诞生了一批比较好的 React Hooks 库。

React Hooks 是今年 React 的一个重磅炸弹,在社区引起了激烈的回响。随着 Hooks 的诞生,我们可以通过 Custom Hooks 很方便的封装逻辑,逻辑共享也成为了潮流。比如上面的例子,我们就可以通过 react-use 很方便的实现。

import {useMouse, useWindowSize, useScroll} from 'react-use'

function Demo(){
  const mousePosition = useMouse();
  const windowSize = useWindowSize();
}

react-use 是社区中比较优秀的 Hooks 库,封装了很多常用的基础逻辑,在日常开发中必不可少。但是只用 react-use 就够了吗?显然不是。react-use 中的 Hooks 粒度比较小,类似于工具库。而在中台产品中,有很多特定的场景逻辑,需要多个 Hooks 进行组合,或者定制特定的逻辑。基于此,我们创建了 @umijs/hooks ,定位为为中台场景服务的 Hooks 库。

@umijs/hooks

@umijs/hooks 是面向中台应用场景的 Hooks 库,封装了中台常见场景的逻辑,让中台开发变得超级简单。@umijs/hooks 已经在蚂蚁金服多个产品中落地,口碑很好,提效明显。当然,你可能不信,口说无凭,那就用例子来说话。

useAntdTable

中台开发中,table 页面应该算最多的一个了,我们一般会使用 Antd 的 Table 组件来搭建,但是其中还是有很多逻辑,我们是无法避免的。

  1. 分页管理
  2. pageSize管理
  3. 分页变化,pageSize 变化时重新进行异步请求
  4. 筛选条件变化时,重置分页,并重新请求数据
  5. 异步请求的 loading 处理
  6. 异步请求的竞态处理
  7. 组件卸载时丢弃进行中的异步请求(很多人通常不处理,在某些情况会报警告)

上面的逻辑,我们在几乎所有的 table 页是必须要处理的,想想都可怕。useAntdTable 至少封装了上面 7 个逻辑,列表页开发从未变得如此简单

const { table } = useAntdTable(asyncFn);
const columns = [];
return (
  <Table columns={columns} rowKey="id" {...table} />
)

useSearch

2019-10-13 16 40 37

常见的异步搜索场景,我们一般要集成:

  1. 防抖
  2. 异步请求的 loading 处理
  3. 异步请求的请求时序控制
  4. 组件卸载时取消防抖及异步请求等逻辑

现在一切变得如此简单:

const { data, loading, onChange } = useSearch(asyncFn);

<Select
  onSearch={onChange}
  loading={loading}
>
  {data.map((item)=><Option />)}
</Select>

更多的 Custom Hooks

当然,我们还有更多极大提效的 CustomHooks,你能想象不用写一行逻辑,就能实现异步 loadmore 功能吗?

2019-10-13 16 45 38

你能想象不用写一行逻辑,就能实现动态增删,排序的表单吗?

2019-10-13 16 50 18

各种常见场景,通通不用写逻辑,通通不用写逻辑。

写在最后

umi hooks 让中台开发变得如此简单,我能想象,不久的将来,中台开发可以不用写一行逻辑,这也是我们为之奋斗的目标。

同时,我也知道,大家平时工作中也积累了很多常用的 Hooks,我们非常希望大家能参与进来,共建 umi hooks,无论对我们,还是用户,都是最好的福音。

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

模块热替换保存状态的问题

模块热替换保存状态的部分,按照您这边的教程写的,但是没有实现出来。
点击加号之后数字更新,这时候修改home.js文件,就会重新更新了。

react-family框架兼容IE8教程

react-family框架兼容IE8的艰辛旅程

本着学习的目的,在react-family框架基础上,做了最小的修改,使框架兼容了IE8浏览器。

预览地址:https://brickspert.github.io/react-family-ie8/index.html
源码地址:react-family-ie8

这是一个痛苦的过程,不过看到结果还是非常开心的。

下面就让我们开始吧。

第一部分,我们先修改开发坏境及开发配置文件~

  1. react降级

    npm install [email protected] [email protected] --save

  2. webpack降级到v1

    备注:网上也有很多人说,webpack v3也能兼容IE8,但是我试了很长时间也没搞好。被迫降版本了,这里应该是我的问题。

    npm install [email protected] [email protected] --save-dev

  3. babel-loader降级

    webpack 1.x 对应 babel-loader <= 6.x

    npm install [email protected] --save-dev

  4. 修改webpack配置文件,至兼容v1的状态。在我们的项目中,主要修改
    webpack.common.config.jswebpack.dev.config.js里面的module->rules,改回loaders

  5. 删除react-hot-loader相关的代码

    • webpack.dev.config.js 直接删除entry,删除尾部的webpack.merge的自定义函数。
    • .babelrc
    • src/index.js
  6. new webpack.HashedModuleIdsPlugin()删除。因为这不是webpack v1的。

  7. 现在就可以npm start启动了,IE8浏览器打开来,你发现是空白的。没关系,打开调试器,看什么错误。

    这里注意下,我用虚拟机通过局域网IP访问的,直接打不开页面,报错Invalid Host header

    修改package.json->start命令,增加--public 192.168.x.x ,后面的IP为你的局域网IP就可以啦。

  8. 接着说错误。现在我们看到的脚本错误应该是“缺少标识符”。

    参考这里,我们使用es3ify

    npm install --save es3ify-webpack-plugin

    webpack.commn.config.js使用插件。

  9. npm start,发现错误换了,“对象不支持此属性或方法”。

    这次我们使用es5-shim

    npm install --save es5-shim

    webpack.common.config.js修改入口entry->app

        app: [
            "es5-shim", "es5-shim/es5-sham",
            "babel-polyfill",
            path.join(__dirname, 'src/index.js')
        ]

    这里我有个问题,必须删除entry里面的vendorplugins里面CommonsChunkPlugin相关的代码。

    不删除IE8就一直报错,我开始猜是要把es5-shim/shame/babel-polyfill等提取出来,独立于appvendor。但是提取了还是不行唉,学艺不精~只能先删除了。

  10. 继续执行,又是错误“例外被抛出且未被接住”。

    npm install export-from-ie8 --save

    然后webpack.common.config.js使用

        postLoaders: [
            {
                test: /\.js$/,
                loaders: ['export-from-ie8/loader']
            }
        ]
  11. 现在你再执行,发现IE8可以正常访问了。嘿嘿嘿。最好我们把BrowserRouter改成HashRouter,这样路由切换页面就不会刷新啦。

到目前为止,开发坏境已经OK了,下面就是修改生产坏境的配置文件了。

  1. extract-text-webpack-plugin插件降级。

    npm install --save-dev [email protected]

    然后按文档修改配置文件。

  2. uglifyjs-webpack-plugin增加兼容IE8参数。

做了这两项,你执行npm run bundle,发现生产坏境也OK了。

虽然最终做出了兼容IE8的版本,但是还有很多地方搞不太懂的。后续会继续学习,改进,更新的。

做这个任务的时候,参考了很多很多的文章,我就不一一列举了,感谢!

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

React Hooks 使用误区,驳官方文档

React Hooks 使用误区,驳官方文档

作为 React Hooks 库 ahooks 的作者,我应该算一个非常非常资深的 React Hooks 用户。在两年多的 React Hooks 使用过程中,我越来越发现大家(包括我自己)对 React Hooks 的使用姿势存在很大误区,归根到底是官方文档的教程很不严谨,存在错误的指引。

1. 不是所有的依赖都必须放到依赖数组中

对于所有的 React Hooks 用户,都有一个共识:“useEffect 中使用到外部变量,都应该放到第二个数组参数中”,同时我们会安装 eslint-plugin-react-hooks 插件,来提醒自己是不是忘了某些变量。

以上共识来自官方文档:

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597739599-eff35ac0-9dee-4f38-b0d0-5b1f18bb3a03 png sign=e6b6b5271f2db940858c630e075c09bac672e2f53fe36a2ec00ef8e7336e7dd8

我愿称该条规则为万恶之源,这条规则以高亮展示,所有的新人都很重视,包括我自己。然而在实际的开发中,发现事情并不是这样的。

下面举一个比较简单的例子,要求如下:当 props.countcount 变化时,上报当前所有数据。

这个例子比较简单,先贴下源码:

function Demo(props) {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  const [a, setA] = useState('');

  useEffect(() => {
    monitor(props.count, count, text, a);
  }, [props.count, count]);

  return (
    <div>
      <button
        onClick={() => setCount(c => c + 1)}
      >
        click
      </button>
      <input value={text} onChange={e => setText(e.target.value)} />
      <input value={a} onChange={e => setA(e.target.value)} />
    </div>
  )
}

我们能看到示例代码中,useEffect 是不符合 React 官方建议的,texta 变量没有放到依赖数组中,ESLint 警告如下:

image

那如果按照规范,我们把依赖项都放到第二个数组参数中,会怎样呢?

  useEffect(() => {
    monitor(props.count, count, text, a);
  }, [props.count, count, text, a]);

如上的代码虽然符合了 React 官方的规范,但不满足我们的业务需求了,当 texta 变化时,也触发了函数执行。

此时陷入了困境,当满足 useEffect 使用规范时,业务需求就不能满足了。当满足业务需求时,useEffect 就不规范了。

我的建议为:

  1. 不要使用 eslint-plugin-react-hooks 插件,或者可以选择性忽略该插件的警告。
  2. 只有一种情况,需要把变量放到 deps 数组中,那就是当该变量变化时,需要触发 useEffect 函数执行。而不是因为 useEffect 中用到了这个变量!

2. deps 参数不能缓解闭包问题

假如完全按第二个建议来写代码,很多人又担心,会不会造成一些不必要的闭包问题?我的结论是:闭包问题和 useEffect 的 deps 参数没有太大关系。

比如我有一个这样的需求:当进入页面 3s 后,输出当前最新的 count。代码如下:

function Demo() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(count)
    }, 3000);
    return () => {
      clearTimeout(timer);
    }
  }, [])

  return (
    <button
      onClick={() => setCount(c => c + 1)}
    >
      click
    </button>
  )
}

以上代码,实现了初始化 3s 后,输出 count。但很遗憾,这里肯定会出闭包问题,哪怕进来之后我们多次点击了 button,输出的 count 仍然为 0。

那假如我们把 count 放到 deps 中,是不是就好了?

  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(count)
    }, 3000);
    return () => {
      clearTimeout(timer);
    }
  }, [count])

如上代码,此时确实没有闭包问题了,但在每次 count 变化时,定时器卸载并重新开始计时了,不满足我们的最初需求了。

要解决的唯一办法为:

const [count, setCount] = useState(0);

// 通过 ref 来记忆最新的 count
const countRef = useRef(count);
countRef.current = count;

useEffect(() => {
  const timer = setTimeout(() => {
    console.log(countRef.current)
  }, 3000);
  return () => {
    clearTimeout(timer);
  }
}, [])

虽然上面的代码,很绕,但确实,只有这个解决方案。请记住这段代码,功能真的很强大。

const countRef = useRef(count);
countRef.current = count;

上面的例子,可以发现,闭包问题是不能仅仅通过遵守 React 规则来避免的。我们必须清晰的知道,在什么场景下会出现闭包问题。

2.1 正常情况下是不会有闭包问题的

const [a, setA] = useState(0);
const [b, setB] = useState(0);

const c = a + b;

useEffect(()=>{
	console.log(a, b, c)
}, [a]);

useEffect(()=>{
	console.log(a, b, c)
}, [b]);

useEffect(()=>{
	console.log(a, b, c)
}, [c]);

在一般的使用过程中,是不会有闭包问题的,如上代码中,完全不会有闭包问题,和 deps 怎么写没有任何关系。

2.2 延迟调用会存在闭包问题

在延迟调用的场景下,一定会存在闭包问题。 什么是延迟调用?

  1. 使用 setTimeout、setInterval、Promise.then 等
  2. useEffect 的卸载函数
const getUsername = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('John');
    }, 3000);
  })
}

function Demo() {
  const [count, setCount] = useState(0);

  // setTimeout 会造成闭包问题
  useEffect(() => {
    const timer = setTimeout(() => {
      console.log(count);
    }, 3000);
    return () => {
      clearTimeout(timer);
    }
  }, [])

  // setInterval 会造成闭包问题
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count);
    }, 3000);
    return () => {
      clearInterval(timer);
    }
  }, [])

  // Promise.then 会造成闭包问题
  useEffect(() => {
    getUsername().then(() => {
      console.log(count);
    });
  }, [])

  // useEffect 卸载函数会造成闭包问题
  useEffect(() => {
    return () => {
      console.log(count);
    }
  }, []);

  return (
    <button
      onClick={() => setCount(c => c + 1)}
    >
      click
    </button>
  )
}

在以上示例代码中,四种情况均会出现闭包问题,永远输出 0。这四种情况的根因都是一样的,我们看一下代码的执行顺序:

  1. 组件初始化,此时 count = 0
  2. 执行 useEffect,此时 useEffect 的函数执行,JS 引用链记录了对 count=0 的引用关系
  3. 点击 button,count 变化,但对之前的引用已经无能为力了

可以看到,闭包问题均是出现在延迟调用的场景下。解决办法如下:

const [count, setCount] = useState(0);

// 通过 ref 来记忆最新的 count
const countRef = useRef(count);
countRef.current = count;

useEffect(() => {
  const timer = setTimeout(() => {
    console.log(countRef.current)
  }, 3000);
  return () => {
    clearTimeout(timer);
  }
}, [])

......

通过 useRef 来保证任何时候访问的 countRef.current 都是最新的,以解决闭包问题。

到这里,我重申下我对 useEffect 的建议:

  1. 只有变化时,需要重新执行 useEffect 的变量,才要放到 deps 中。而不是 useEffect 用到的变量都放到 deps 中。
  2. 在有延迟调用场景时,可以通过 ref 来解决闭包问题。

3. 尽量不要用 useCallback

我建议在项目中尽量不要用 useCallback,大部分场景下,不仅没有提升性能,反而让代码可读性变的很差。

3.1 useCallback 大部分场景没有提升性能

useCallback 可以记住函数,避免函数重复生成,这样函数在传递给子组件时,可以避免子组件重复渲染,提高性能。

const someFunc = useCallback(()=> {
   doSomething();
}, []);

return <ExpensiveComponent func={someFunc} />

基于以上认知,很多同学(包括我自己)在写代码时,只要是个函数,都加个 useCallback,是你么?反正我以前是。

但我们要注意,提高性能还必须有另外一个条件,子组件必须使用了 shouldComponentUpdate 或者 React.memo 来忽略同样的参数重复渲染。

假如 ExpensiveComponent 组件只是一个普通组件,是没有任何用的。比如下面这样:

const ExpensiveComponent = ({ func }) => {
  return (
    <div onClick={func}>
    	hello
    </div>
  )
}

必须通过 React.memo 包裹 ExpensiveComponent ,才会避免参数不变的情况下的重复渲染,提高性能。

const ExpensiveComponent = React.memo(({ func }) => {
  return (
    <div onClick={func}>
    	hello
    </div>
  )
})

所以,useCallback 是要和 shouldComponentUpdate/React.memo 配套使用的,你用对了吗?当然,我建议一般项目中不用考虑性能优化的问题,也就是不要使用 useCallback 了,除非有个别非常复杂的组件,单独使用即可。

3.2 useCallback 让代码可读性变差

我看到过一些代码,使用 useCallback 后,大概长这样:

const someFuncA = useCallback((d, g, x, y)=> {
   doSomething(a, b, c, d, g, x, y);
}, [a, b, c]);

const someFuncB = useCallback(()=> {
   someFuncA(d, g, x, y);
}, [someFuncA, d, g, x, y]);

useEffect(()=>{
  someFuncB();
}, [someFuncB]);

在上面的代码中,变量依赖一层一层传递,最终要判断具体哪些变量变化会触发 useEffect 执行,是一件很头疼的事情。

我期望不要用 useCallback,直接裸写函数就好:

const someFuncA = (d, g, x, y)=> {
   doSomething(a, b, c, d, g, x, y);
};

const someFuncB = ()=> {
   someFuncA(d, g, x, y);
};

useEffect(()=>{
  someFuncB();
}, [...]);

在 useEffect 存在延迟调用的场景下,可能造成闭包问题,那通过咱们万能的方法就能解决:

const someFuncA = (d, g, x, y)=> {
   doSomething(a, b, c, d, g, x, y);
};

const someFuncB = ()=> {
   someFuncA(d, g, x, y);
};

+ const someFuncBRef = useRef(someFuncB);
+ someFuncBRef.current = someFuncB;

useEffect(()=>{
+  setTimeout(()=>{
+    someFuncBRef.current();
+  }, 1000)
}, [...]);

对 useCallback 的建议就一句话:没事别用 useCallback。

4. useMemo 建议适当使用

相较于 useCallback 而言,useMemo 的收益是显而易见的。

// 没有使用 useMemo
const memoizedValue = computeExpensiveValue(a, b);

// 使用 useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

如果没有使用 useMemo,computeExpensiveValue 会在每一次渲染的时候执行。如果使用了 useMemo,只有在 ab 变化时,才会执行一次 computeExpensiveValue

这笔账大家应该都会算,所以我建议 useMemo 可以适当使用。

当然也不是无节制的使用,在很简单的基础类型计算时,可能 useMemo 并不划算。

const a = 1;
const b = 2;

const c = useMemo(()=> a + b, [a, b]);

比如上面的例子,请问计算 a+b 的消耗大?还是记录 a/b ,并比较a/b 是否变化的消耗大?

明显 a+b 消耗更小。

const a = 1;
const b = 2;

const c = a + b;

这笔账大家可以自己算,我建议简单的基础类型计算,就不要用 useMemo 了~

5. useState 的正确使用姿势

useState 应该算最简单的一个 Hooks,但在使用中,也有很多技巧可循,如果严格按照以下几点,代码可维护性直接翻倍。

5.1 能用其他状态计算出来就不用单独声明状态

一个 state 必须不能通过其它 state/props 直接计算出来,否则就不用定义 state。

const SomeComponent = (props) => {
  
  const [source, setSource] = useState([
      {type: 'done', value: 1},
      {type: 'doing', value: 2},
  ])
  
  const [doneSource, setDoneSource] = useState([])
  const [doingSource, setDoingSource] = useState([])

  useEffect(() => {
    setDoingSource(source.filter(item => item.type === 'doing'))
    setDoneSource(source.filter(item => item.type === 'done'))
  }, [source])
  
  return (
    <div>
       ..... 
    </div>
  )
}

上面的示例中,变量 doneSourcedoingSource 可以通过变量 source 计算出来,那就不要定义 doneSourcedoingSource 了!

const SomeComponent = (props) => {
  
  const [source, setSource] = useState([
      {type: 'done', value: 1},
      {type: 'doing', value: 2},
    ])
  
  const doneSource = useMemo(()=> source.filter(item => item.type === 'done'), [source]);
  const doingSource = useMemo(()=> source.filter(item => item.type === 'doing'), [source]);
  
  return (
    <div>
       ..... 
    </div>
  )
}

一般在项目中此类问题都比较隐晦,层层传递,在 Code Review 中很难一眼看出。如果能把变量定义清楚,那事情就成功了一半。

5.2 保证数据源唯一

在项目中同一个数据,保证只存储在一个地方。

不要既存在 redux 中,又在组件中定义了一个 state 存储。

不要既存在父级组件中,又在当前组件中定义了一个 state 存储。

不要既存在 url query 中,又在组件中定义了一个 state 存储。

function SearchBox({ data }) {
  const [searchKey, setSearchKey] = useState(getQuery('key'));
  
  const handleSearchChange = e => {
    const key = e.target.value;
    setSearchKey(key);
    history.push(`/movie-list?key=${key}`);
  }
  
  return (
      <input
        value={searchKey}
        placeholder="Search..."
        onChange={handleSearchChange}
      />
  );
}

在上面的示例中,searchKey 存储在两个地方,既在 url query 上,又定义了一个 state。完全可以优化成下面这样:

function SearchBox({ data }) {
  const searchKey = parse(localtion.search)?.key;
  
  const handleSearchChange = e => {
    const key = e.target.value;
    history.push(`/movie-list?key=${key}`);
  }
  
  return (
      <input
        value={searchKey}
        placeholder="Search..."
        onChange={handleSearchChange}
      />
  );
}

在实际项目开发中,此类问题也是比较隐晦,编码时应注意。

5.3 useState 适当合并

项目中有木有写过这样的代码:

const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const [school, setSchool] = useState();
const [age, setAge] = useState();
const [address, setAddress] = useState();

const [weather, setWeather] = useState();
const [room, setRoom] = useState();

反正我最开始是写过,useState 拆分过细,导致代码中一大片 useState。

我建议,同样含义的变量可以合并成一个 state,代码可读性会提升很多:

const [userInfo, setUserInfo] = useState({
  firstName,
  lastName,
  school,
  age,
  address
});

const [weather, setWeather] = useState();
const [room, setRoom] = useState();

当然这种方式我们在变更变量时,一定不要忘记带上老的字段,比如我们只想修改 firstName

setUserInfo(s=> ({
  ...s,
  fristName,
}))

其实如果是 React Class 组件,state 是会自动合并的:

this.setState({
  firstName
})

在 Hooks 中,可以有这种用法吗?其实是可以的,我们自己封装一个 Hooks 就可以,比如 ahooks 的 useSetState,就封装了类似的逻辑:

const [userInfo, setUserInfo] = useSetState({
  firstName,
  lastName,
  school,
  age,
  address
});

// 自动合并
setUserInfo({
  firstName
})

我自己在项目中大量使用了 useSetState 来代替 useState,来管理复杂类型的 state,妈妈更爱我了。

六、总结

作为资深的 React Hooks 用户,我很认可 React Hooks 带来的提效,这也是我这几年完全拥抱 Hooks 的原因。同时我也越来越觉得 React Hooks 难驾驭,尤其随着 React 18 的 concurrent mode 的到来,不知道会带来什么坑。

最后再给大家三个建议:

  1. 可以多使用别人封装好的高级 Hooks 来提效,比如 ahooks 库(哈哈哈
  2. 可以多看看别人封装好的 Hooks 源码,加深对 React Hooks 理解,比如 ahooks 库(哈哈哈
  3. 可以关注下我的公众号,我会经常发布一些我自己写的技术文章,以及转发一些我认为比较好的文章,爱你哟(づ ̄3 ̄)づ╭❤~

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

我认为 web3 是什么(大白话 web3)

blbcover.gif

说到 web3,很多人觉得这是骗局,是割韭菜。是因为大部分介绍 web3 的文章都离不开 NFT、数字货币、区块链、比特币、以太坊、元宇宙等概念,玄之又玄,脱离我们的生活,没解决我们的痛点。一般文章最后还教给我们怎么炒币,怎么买卖 NFT,妥妥的割韭菜套路,所以大部分觉得 web3 就是在忽悠人。

说实话,很多东西我也觉得是在炒作,我也不信。

一个图片卖几百万美金?
一个空气币要卖我钱?
元宇宙?可能要等我儿子长大了玩吧?

关于 web3 是什么,可能每篇文章介绍的都不一样,好像现在大家对 web3 是什么还没达成共识。但对于普通人来讲,我们只关心 web3 有没有解决我的痛点,有没有给我带来价值

本篇文章,我会通过大白话来介绍我认可的 web3 形态,极大解决了我们的痛点,我愿称之为未来!

本文不会涉及任何韭菜币等相关概念(我也不懂),请放心食用。

web2 的牢笼(现在的痛点)

通常大家说的 web1,是指门户时代,内容由各大门户网站创作,普通用户只是作为浏览方。数据由门户网站产出,收益当然归他们所有,价值流向正确。总结:平台创造、平台所有、平台控制、平台受益。

通常大家说的 web2,就是我们当下的互联网形态:用户创造、平台所有、平台控制、平台分配。

举一些典型的例子:

  • 我们在抖音创作了短视频,抖音拥有了我们的视频,控制视频分发,间接产生的收益也归抖音平台所得
  • 我们在微信上维护的社交关系、聊天记录、朋友圈数据等,也归微信平台所有,间接产生的收益归微信所得
  • 我们在微信淘宝购买了商品,海量的购买记录归淘宝所有,淘宝通过分析购买记录间接产生收益
  • ......

web2 的问题有很多:

  • 所有权和收益权不合理: 不符合 “谁创造、谁拥有、谁收益”的市场规则。比如:我在知乎、公众号等渠道发布的几十篇文章,没有收益。
  • 平台垄断,并控制用户: 数亿用户创造的海量内容,无偿提供给平台,催生了多个超级巨头的产生。反过来,巨头开始控制用户,比如:
    • 各大内容平台通过算法控制推荐给用户的内容 😡
    • 百度搜索首页全是付费广告 😡
    • 平台割裂,微信屏蔽支付宝、淘宝、抖音分享,淘宝不支持微信支付等 😡
    • 电商平台杀熟 😡
    • 在朋友圈发布的内容,被屏蔽仅自己可见 😡
  • 数据割裂,且无法迁移: 即使我们对各大平台的控制已经忍无可忍,但我们没有办法去改变。我们的数据归平台所有,无法迁移。另外就是在当前的模式下,很难诞生白莲花平台。
    • 我每次写一篇文章,需要在公众号、掘金、知乎、Github 等各个渠道发布一次。大家对于文章的评论、点赞、关注都是割裂的,属于各个平台,而不是属于我这篇文章
    • 我忍受不了微信的垄断,但我也没办法把微信的好友、聊天记录、朋友圈等我创作的内容迁移到其它平台
    • 我没办法把知乎的粉丝、文章、评论、关注迁移到掘金
    • 我在 QQ 音乐购买的音乐,没法在网易云音乐播放器播放
  • 隐私问题: 我们的社交关系、聊天记录、购物记录、搜索记录等数据,均由各大平台控制,他们可以随意使用我们的隐私数据
  • 数据可信度: 平台提供的数据不可信,比如:
    • 我的文章阅读量可能都是虚假的
    • 商品的购买量可能都是虚假的
    • 推荐的热门视频可能并不是真的热门
  • 数据安全性: 我们的数据由平台中心化存储,如果平台挂了,那我们的数据也没了

就我个人而言,我对 web2 中的很多点已经达到忍无可忍的程度,如果 web3 能解决这些问题,那我就是 web3 的粉丝。

自由和发展

基于市场经济,web3 的宗旨必须是用户创造、用户所有、用户控制、协议分配

在 web3 中,用户的所有数据都归属用户个人,用户可以授权其它平台访问自己的某类数据。想象一下这种场景:

  • 社交关系、聊天记录、实时聊天消息等由我个人管理,微信做的不好,我就炒他鱿鱼,不给他授权了。我把这些信息授权给另外一个体验真正好的聊天工具。
  • 我购买的音乐由我个人管理,QQ 音乐让我不爽,我就炒了他,把我的音乐授权给另外一个音乐软件。
  • 我写的文章、以及文章的评论、收藏、关注等数据我个人管理,哪个平台体验好,我就授权给他。

总结就是,哪个软件让我不爽,我就无缝切换到另外一个软件,让软件供应商去内卷吧。

能随意炒微信的鱿鱼,想想都开心。以前不可能,但 web3 确实带来了这种机会。我认为 web3 的实现思路应该是这样的:

  1. 用户的数据存储在某个地方,用户对数据拥有完全控制权,未经授权,任何人不能访问和修改数据。
  2. 软件需要经过用户授权,才能访问用户特定的数据。

针对数据存储部分的技术解决方案,我们要求能解决信任问题。即用户完全信任数据存储方,非用户授权,任何人无法访问和修改数据。中心化的数据存储肯定不能符合要求,从目前来看,「区块链」去中心化技术就非常适合充当这里的可信任数据存储解决方案。这也是为什么 web3 总是和区块链绑定在一起。其实如果有其它技术能解决信任问题,也是 OK 的。

针对软件提供方,未来可能会更轻量。比如很多软件可能都不需要数据库了,数据完全由用户自带。软件需要做的就是帮用户管理数据,比如播放音乐、管理文章、聊天等。软件提供方必须用优质的体验来吸引用户,想想都幸福。

大家快来学前端吧 😏,按我讲的,前端就是未来呀!欢迎关注我的公众号《前端技术砖家》跟我学前端。哈哈哈。

总结

哪里有压迫,哪里就有革命。web3 让我们可以完全控制自己的数据,可以让我们用上更好的软件,用户会用脚投票的,为何不会成功呢?所以我还是笃定 web3 这个方向的,虽然这条路真的很难,需要无数人去探索,但互联网的未来一定属于 web3!

如果下次还有谁介绍 web3 的时候,提到数字货币、NFT 啥的,那就妥妥的是割韭菜了。

最后需要说明的是,本篇文章关于 web3 的定义不一定准确,纯属我一个菜鸡的 YY,另外文中的很多观点和话术来自网络,感谢互联网。

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

react-router v4 使用 history 控制路由跳转

react-router v4 使用 history 控制路由跳转

问题

当我们使用react-router v3的时候,我们想跳转路由,我们一般这样处理

  1. 我们从react-router导出browserHistory
  2. 我们使用browserHistory.push()等等方法操作路由跳转。

类似下面这样

import browserHistory from 'react-router';

export function addProduct(props) {
  return dispatch =>
    axios.post(`xxx`, props, config)
      .then(response => {
        browserHistory.push('/cart'); //这里
      });
}

but!! 问题来了,在react-router v4中,不提供browserHistory等的导出~~

那怎么办?我如何控制路由跳转呢???

解决方法

1. 使用 withRouter

withRouter高阶组件,提供了history让你使用~

import React from "react";
import {withRouter} from "react-router-dom";

class MyComponent extends React.Component {
  ...
  myFunction() {
    this.props.history.push("/some/Path");
  }
  ...
}
export default withRouter(MyComponent);

这是官方推荐做法哦。但是这种方法用起来有点难受,比如我们想在redux里面使用路由的时候,我们只能在组件把history传递过去。。

就像问题章节的代码那种场景使用,我们就必须从组件中传一个history参数过去。。。

2. 使用 Context

react-router v4Router 组件中通过Contex暴露了一个router对象~

在子组件中使用Context,我们可以获得router对象,如下面例子~

import React from "react";
import PropTypes from "prop-types";

class MyComponent extends React.Component {
  static contextTypes = {
    router: PropTypes.object
  }
  constructor(props, context) {
     super(props, context);
  }
  ...
  myFunction() {
    this.context.router.history.push("/some/Path");
  }
  ...
}

当然,这种方法慎用~尽量不用。因为react不推荐使用contex哦。在未来版本中有可能被抛弃哦。

3. hack

其实分析问题所在,就是v3中把我们传递给Router组件的history又暴露出来,让我们调用了~~

react-router v4 的组件BrowserRouter自己创建了history
并且不暴露出来,不让我们引用了。尴尬~

我们可以不使用推荐的BrowserRouter,依旧使用Router组件。我们自己创建history,其他地方调用自己创建的history。看代码~

  1. 我们自己创建一个history
// src/history.js

import createHistory from 'history/createBrowserHistory';

export default createHistory();
  1. 我们使用Router组件
// src/index.js

import { Router, Link, Route } from 'react-router-dom';
import history from './history';

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      ...
    </Router>
  </Provider>,
  document.getElementById('root'),
);
  1. 其他地方我们就可以这样用了
import history from './history';

export function addProduct(props) {
  return dispatch =>
    axios.post(`xxx`, props, config)
      .then(response => {
        history.push('/cart'); //这里
      });
}

4. 我非要用BrowserRouter

确实,react-router v4推荐使用BrowserRouter组件,而在第三个解决方案中,我们抛弃了这个组件,又回退使用了Router组件。

怎么办。 你去看看BrowserRouter源码,我觉得你就豁然开朗了。

源码非常简单,没什么东西。我们完全自己写一个BrowserRouter组件,然后替换第三种解决方法中的Router组件。嘿嘿。

讲到这里也结束了,我自己目前在使用第三种方法,虽然官方推荐第一种,我觉得用着比较麻烦唉。~

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

早上好,砖家。

请教您一个问题,就是 react-router 4.0 + 可以实现嵌套路由吗,网上的资料确实有限,目前还没发现一个标准答案,嘿嘿。

antd 自定义 Icon 的几种方式及其优劣

虽然 antd 提供了大量的 Icon 图标,但是在平时的设计稿中,会出现各种设计师自定义的 Icon,对于这类图标,怎样处理更好呢?

多种方法

直接当做图片使用

svg 也是一种图片,可以作为 imgsrc 使用。

通过配置 url-loader 的配置,我们可以直接引用 svg 资源。

{
    test: /\.(png|jpg|gif|svg)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}
import customSvg from '../assets/images/xxx.svg';

<img src={customSvg} />  

优劣

  • ✅快,无脑,无理解成本。
  • ❌不能像自带的 Icon 一样,设置 colorfont-size 等。

自定义 font 图标

参见官方文档

在 antd v3.9.0 之后,提供了一个 createFromIconfontCN 方法,方便开发者调用在 iconfont.cn 上自行管理的图标。

首先你需要在 iconfont 上创建自己的图标库,并上传自定义的 Icon,然后就可以通过自定义组件使用了。

iconfont 图标库

iconfont 上新建图标库比较简单,我就不赘述了。我讲一下在上传自定义 Icon 时的几个坑。

Q:自定义 SVG 上传后,显示为空白。

如果 SVG 图标不是封闭的,上传到 iconfont 之后,会显示为空白。那什么是封闭呢?大概就是图标有缺口~

比如下面的线条就是非封闭的,上传到 iconfont 就变成空白了。

1
2

我们只需要选中该图标,进行“轮廓化”处理即可。

3

Q:自定义 SVG 上传后,图标显示不全。

如果 SVG 图标由多个部分组成,但是没有进行“轮廓化”,那上传到 iconfont 之后,可能会显示不全。比如下图的图标,上传到 iconfont 后,图标显示不全了。

4
5

我们选中该图标多个部分,进行“轮廓化”即可。

6

**Q:自定义 SVG 上传后,无法通过 color 属性设置颜色。

我们从 sketch 导出的 svg,一般都是自带颜色的,我们在上传时,选择去除颜色并提交即可。

7

自定义 MyIcon 组件

经过上面的步骤,我们已经把需要的 Icon 上传到 iconfont 了,同时我们可以拿到字体的链接。

8

然后通过 antd 的 createFromIconfontCN 包装下即可使用。

const MyIcon = Icon.createFromIconfontCN({
  scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成
});

/* 使用起来还比较简单 */
<MyIcon type="xxx"/>

优劣

  • ✅如果只管使用 MyIcon,而不管维护,那是比较爽的。
  • ❌维护很麻烦!每次上传一个新图标,字体链接都会变化一次,组件中就得替换一下,烦不胜烦啊。
  • ❌由于是通过字体包引用进来的,无法做到按需加载。

自定义 SVG 图标

通过 svgr 我们可以将一个普通的 svg 图片,转成 React 组件。

通过 webpack 将 SVG 转成组件

参见官方文档

// webpack.config.js
// umijs 配置见 https://github.com/umijs/umi/issues/1078
{
  test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
  use: [
    {
      loader: 'babel-loader',
    },
    {
      loader: '@svgr/webpack',
      options: {
        babel: false,
        icon: true,
      },
    },
  ],
}
import { Icon } from 'antd';
import MessageSvg from 'path/to/message.svg'; // path to your '*.svg' file.

ReactDOM.render(<Icon component={MessageSvg} />, mountNode);

对于带颜色的图标,loader 并不知道 svg 上哪个颜色是需要自定义的,所以需要手动将 svg 中写死的 color 值改成 currentColor

9

优劣
  • ✅配置完 webpack 后,使用真的很简单。
  • ❌对于带颜色的 SVG 图标,需要手动去 svg 文件中改下 currentColor。

通过 cli 将 SVG 转成组件

svgr 可以通过 cli,将 SVG 转换为组件,同时可以将 svg 中的颜色,设置成变量。

正常情况下,我们导出的 SVG 长这样:

<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 58 (84663) - https://sketch.com -->
    <title>Group</title>
    <desc>Created with Sketch.</desc>
    <g id="组件" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="一级导航" transform="translate(-1197.000000, -18.000000)">
            <g id="Group" transform="translate(1197.000000, 18.000000)">
                <circle id="Oval" fill="#2F54EB" cx="10" cy="10" r="10"></circle>
                <g id="plus" transform="translate(3.000000, 3.000000)" fill-rule="nonzero">
                    <rect id="Rectangle-path" fill="#000000" opacity="0" x="0" y="0" width="14" height="14"></rect>
                    <path d="M11.59375,6.48046875 L7.51953125,6.48046875 L7.51953125,2.078125 L6.48046875,2.078125 L6.48046875,6.48046875 L2.40625,6.48046875 C2.34609375,6.48046875 2.296875,6.5296875 2.296875,6.58984375 L2.296875,7.41015625 C2.296875,7.4703125 2.34609375,7.51953125 2.40625,7.51953125 L6.48046875,7.51953125 L6.48046875,11.921875 L7.51953125,11.921875 L7.51953125,7.51953125 L11.59375,7.51953125 C11.6539063,7.51953125 11.703125,7.4703125 11.703125,7.41015625 L11.703125,6.58984375 C11.703125,6.5296875 11.6539063,6.48046875 11.59375,6.48046875 Z" id="Shape" fill="#FFFFFF"></path>
                </g>
            </g>
        </g>
    </g>
</svg>

通过 svgr 的命令行尝试转一下:

npx @svgr/cli --icon --replace-attr-values "#2F54EB=currentColor" message.svg

输出结果为:

import React from "react";

const MessageSvg = props => (
  <svg width="1em" height="1em" viewBox="0 0 20 20" {...props}>
    <g fill="none" fillRule="evenodd">
      <circle fill="currentColor" cx={10} cy={10} r={10} />
      <path
        d="M14.594 9.48H10.52V5.078H9.48V9.48H5.406a.11.11 0 00-.11.11v.82c0 .06.05.11.11.11H9.48v4.402h1.04V10.52h4.074c.06 0 .11-.05.11-.11v-.82a.11.11 0 00-.11-.11z"
        fill="#FFF"
        fillRule="nonzero"
      />
    </g>
  </svg>
);

export default MessageSvg;

good job~ 现在我们获得了一个纯 React 组件,直接使用即可。

import { Icon } from 'antd';
import MessageSvg from './MessageSvg';

ReactDOM.render(<Icon component={MessageSvg} />, mountNode);
优劣
  • ✅可以满足功能需要,获得一个可以自定义颜色的 Icon。
  • ❌项目中不是维护 svg 图片,而是维护转换过后的 React 组件。

总结

上面的几种方案我有在多个项目中尝试过,目前觉得 通过 webpack 将 SVG 转成组件 方案不错。

  • 首先可以满足需求,与 antd 原生 Icon 使用无差异,可以通过 colorfont-size 等属性控制样式。
  • 使用简单,配置完 webpack 之后,就变成傻瓜式操作了。
  • 可以按需加载。
  • 不依赖第三方,比如 iconfont。

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

前端小白半年准备,成功进入BAT

前端小白半年准备进BAT

先介绍下背景

非211,985本科毕业。一年半PHP经验,一年半前端经验,前端一直在做React开发。

半年之前,我是一个前端小小小白。多么小白呢?

  1. css调样式全靠试。
  2. 盒模型,好像知道是啥?好像又不知道!
  3. 看到别人说BFC,啥是BFC?为啥外边距会合并?有些会合并,有些不会合并,这都是啥玩意?
  4. z-index为啥有时候有效果,有时候没效果?为啥有时候小的值还在大的上面?
  5. js就会基础使用,稍微复杂的一脸懵逼。
  6. 看别人的文章,一看到prototype,立马头疼,这都是啥!什么原型,继承,离我远点!
  7. 闭包,好像知道是啥。但是说不出来。
  8. arguments,作用域链等等都是啥?
  9. …...

我都不想去思考这些问题,啊,,,头疼,这都是什么?我都不会啊!

这样的我,怎么出去面试?别人随便问个问题,我都不会!

我又去网上看了别人的面试题,娘的哟,这是啥?这又是啥?好像会点,但是说不出来~~

不行不行,我得赶紧学习了。但是我要怎么去准备呢?好像js,html,css,http都没系统学过啊?好像react,webpack这些玩意也没系统整理过啊。好多啊!

废话不多说,我们开始吧~~

吭哧

吭哧

吭哧

…...

经过半年的准备,我成功面试进了BAT

所以相信自己,从现在开始!你应该比我厉害吧?

我看到很多像我之前一样迷茫的人,我觉得我的经历是可以复制的。就写下来,共勉!

过程分为:

1. 系统学习基础知识
2. 面试题提高
3. 项目

系统学习基础知识

基础知识通过看书来系统学习。

  1. 《JavaScript高级程序设计(第3版)》

    系统学习一遍js,看这本书是非常痛苦的过程。预计时间在2个月左右。

    不要心急,一章一章的过,每个知识点都要去理解,做笔记。

    碰到看不懂的,反复去读,去网上查,一定要搞懂!

  2. 《CSS权威指南(第三版)》

    系统学习css,预计时间2星期。记得做笔记,不会的点,反复去读,去查。

  3. css3教程学习

    这个去网上找份教程,过一遍。每个新属性都去写下。

  4. 《html5与css3权威指南》

    这个快速过下前面html部分就好了。

    1个星期左右读完。

  5. 《图解HTTP》

    非常重要!系统学习HTTP。半个月时间~

  6. 《ECMAScript 6 入门》

    阮一峰写的。系统学习ES6。这个因人而异,时间长短不一,不过真的很重要!

​ 好了,到这里为止,我们已经系统的学习了前端的各种知识了。应该花费了3个多月了。如果你能坚持下来,绝对会有一个质的飞跃。

面试题提高

下面我的策略是,找面试题,把网上能找到的面试都记下来,每个题都去深入研究!

注意,是深入研究,不是浅显的知道答案就行了

面试题大部分来源于这里https://github.com/h5bp/Front-end-Developer-Interview-Questions/tree/master/Translations/Chinese,还有很多很多其他的面经,我就不一一贴出来了,反正能搜到的我都看了~

我把我整理的面试题列出来,希望每个题你都能深入去研究

HTML

  1. DOCTYPE(文档类型)的作用是什么?

    参考https://witcher42.github.io/2014/05/28/doctype/

  2. 浏览器标准模式 (standards mode) 、几乎标准模式(almost standards mode)和怪异模式 (quirks mode) 之间的区别是什么?

    • 产生的历史原因是啥?
    • 怪异模式有哪些怪异的行为?
  3. 使用 data- 属性的好处是什么?

    可以去实践下data-*的使用啦

  4. 如果把 HTML5 看作做一个开放平台,那它的构建模块有哪些?

    研究下HTML5的所有模块

  5. cookiessessionStorage localStorage 的区别

  6. 请解释 <script><script async><script defer> 的区别。

  7. 为什么通常推荐将 CSS <link> 放置在 <head></head> 之间,而将 JS <script> 放置在 </body> 之前?你知道有哪些例外吗?

  8. 什么是渐进式渲染 (progressive rendering)?

  9. HTMLXHTML 有什么区别?

  10. HMTL5新标签

CSS

  1. CSS 中类 (class) 和 ID 的区别

  2. 请问 "resetting" 和 "normalizing" CSS 之间的区别?你会如何选择,为什么?

    https://github.com/necolas/normalize.css

    https://github.com/shannonmoeller/reset-css/blob/master/reset.css

  3. 请解释浮动 (Floats) 及其工作原理

    这个非常重要,前面读的书上有这个,一定要完全搞懂。

  4. 清除浮动

    重要

  5. 描述z-index和叠加上下文是如何形成的?

    重要,书上有,先理解。然后我推荐两个文章

    https://www.w3cplus.com/css/what-no-one-told-you-about-z-index.html

    http://www.zhangxinxu.com/wordpress/2016/01/understand-css-stacking-context-order-z-index/

  6. 请描述 BFC(Block Formatting Context) 及其如何工作?

    理解BFC的特性及如何触发BFC

  7. CSS sprites

    优点,缺点

  8. 图片替换文字方案

  9. 你会如何解决特定浏览器的样式问题?

  10. 如何为有功能限制的浏览器提供网页?

    渐进增强,优雅降级等等等等

  11. 有哪些的隐藏内容的方法?

  12. 栅格系统 (grid system)

  13. 你用过媒体查询,或针对移动端的布局CSS 吗?

  14. 如何优化网页的打印样式?

  15. 在书写高效 CSS 时会有哪些问题需要考虑?

  16. 使用 CSS 预处理器的优缺点有哪些?

  17. 如果设计中使用了非标准的字体,你该如何去实现?

  18. 请解释浏览器是如何判断元素是否匹配某个 CSS 选择器?

  19. 请描述伪元素 (pseudo-elements) 及其用途

    伪元素,伪类等等都去研究下

  20. 请解释你对盒模型的理解,以及如何在 CSS 中告诉浏览器使用不同的盒模型来渲染你的布局

  21. 请罗列出你所知道的 display 属性的全部值

  22. 请解释 inline inline-block 的区别

  23. 请解释 relativefixedabsolutestatic 元素的区别

  24. 请问你有尝试过 CSS Flexbox 或者 Grid 标准规格吗

    flex很重要,每个属性都要知道。建议去读阮一峰的flex文章

  25. 为什么响应式设计 (responsive design) 和自适应设计 (adaptive design) 不同?

  26. 你有兼容 retina 屏幕的经历吗?如果有,在什么地方使用了何种技术?

    移动端开发必须知道!

  27. 请问为何要使用 translate() 而非 absolute position,或反之的理由?为什么?

  28. 如果实现一个高性能的CSS动画效果?

  29. IFC

  30. css3动画

    各种属性熟悉下

  31. 布局之:左边定宽,右边自适应

  32. 圣杯布局,双飞翼布局

  33. 实现垂直居中和水平居中

Javascript

  1. 事件代理

  2. 请解释 JavaScript this 是如何工作的

  3. javascript继承

    这个不多说,十分十分重要。建议按照《高程三》的继承那里,仔细理解哦。

  4. javascript模块化

    理解模块化发展过程,理解 commonJSAMDCMDES6模块化

  5. IIFE 立即执行函数

  6. null undefined区别

  7. 闭包 与 作用域

    非常重要,书上有!

  8. 匿名函数

  9. 你是如何组织自己的代码?是使用模块模式,还是使用经典继承的方法?

  10. 宿主对象 (host objects) 和原生对象 (native objects)

  11. 请指出以下代码的区别:function Person(){}var person = Person()var person = new Person()

  12. apply call bind

    深入到源码如何实现这三个功能的。

  13. new

    源码如何实现的?

  14. document.write()

  15. 特性检测 特性推断 UA字符串嗅探

  16. Ajax工作原理

    着重理解XMLHttpRequest!!

  17. 跨域

    图片ping, JSONP, CORS

    这是面试必问的点。注意一定要完全理解,完全!

  18. 变量声明提升

  19. 冒泡机制

  20. attributeproperty

  21. document loaddocument DOMContentLoaded

    非常重要哦

  22. ===== 有什么不同

  23. 同源策略 (same-origin policy)

    CookieiframeAJAX同源

  24. strict模式

  25. 为何通常会认为保留网站现有的全局作用域 (global scope) 不去改变它,是较好的选择

  26. 为何你会使用 load 之类的事件 (event)?此事件有缺点吗?你是否知道其他替代品,以及为何使用它们?

  27. 请解释什么是单页应用 (single page app), 以及如何使其对搜索引擎友好 (SEO-friendly)

    相当重要

  28. Promise

    怎么用?源码如何实现的?

    推荐文章:xieranmaya/blog#3

  29. 使用一种可以编译成 JavaScript 的语言来写 JavaScript 代码有哪些优缺点?

  30. javascript调试工具

  31. 对象遍历 和 数组遍历

  32. 可变对象和不可变对象

  33. 什么是事件循环 (event loop)

    非常重要,面试必问。

    深入原理,宏任务,微任务等等

  34. let var const

  35. 数组的方法

  36. web worker

  37. 柯里化

  38. 创建对象的三种方法

  39. 深拷贝和浅拷贝

    可以实现手写深拷贝

  40. 图片懒加载

    咋实现的?

  41. 网页各种高度

    这个好难记,我也没记住~

  42. 实现页面加载进度条

  43. 箭头函数ES5如何实现

    • 箭头函数和普通函数的区别

React

  1. 虚拟DOM是啥?以及diff算法原理

  2. react 事件绑定

  3. 生命周期

  4. 函数式编程,纯函数

  5. React创建组件的方式

  6. 组件性能优化

    shuouldComponentUpdate

    pureComponent

    不可变数据

    key

    等等优化方法,每一点的优点和缺点

  7. 如何设计一个好组件

  8. 哪里进行网络请求?为什么

  9. 调用setState之后发生了什么

  10. refs

  11. react16新特性

    尤其理解time slicesuspense

  12. React 当中 ElementComponent 有何区别

  13. 容器组件和展示组件

  14. props.children

  15. 路由实现原理

  16. reactsetState同步还是异步?

  17. Reduxreact-redux等原理

  18. 如何实现异步网络请求的?

  19. 组件间通信

  20. 高阶组件是什么和常见的高阶组件

  21. React key是干嘛的?

webpack

  1. loader

    自己如何写一个loader

  2. plugin

    自己如何写一个plugin

  3. webpack原理之普通打包

  4. webpack原理之多文件打包

  5. webpack原理之提取公共文件

  6. webpack 如何做到 tree shaking

  7. webpack配置文件基本概念

  8. webpack构建流程

  9. 前端模块化的理解

  10. 打包很慢,怎么解决?

  11. 打包出来的文件很大,怎么解决?

安全问题

  1. xss
  2. csrf
  3. 等等....

HTTP

  1. 为什么传统上利用多个域名来提供网站资源会更有效

  2. Long-PollingWebsocketsServer-Sent Event

  3. 常见的请求头和响应头

  4. 和缓存有关的HTTP首部字段

    相当重要。如何应用的?

  5. HTTP method

  6. HTTP状态码

  7. https 加密过程

  8. http2新特性

性能

  1. 你会用什么工具来查找代码中的性能问题?

  2. 增强网站的页面滚动效能

  3. 重排,重绘,合成

    相当相当重要

  4. 合成层

    我在这里理解了一个多星期,静下心来去理解。

    http://taobaofed.org/blog/2016/04/25/performance-composite/

  5. 前端优化方法

  6. css3动画和js动画对比

其他问题

  1. 常见排序算法
  2. babel原理
  3. 实现一个幻灯片功能
  4. 你最近遇到过什么技术挑战?你是如何解决的?
  5. 浏览器输入网址后做了什么?

项目

面试问的最多的,除了基础知道,就是项目了。

必须对自己做的项目,有十足的掌握。做项目的时候,有主人翁意识~

项目业务理解,性能优化等等~

行了,到这里就结束了。如果你能坚持下来,我觉得你自己就有很足的自信了!

从一个弱鸡,成长为一个合格的前端菜鸟啦!

路漫漫其修远兮~加油!

一堆胡言乱语,希望对你有帮助。

我再强调一下:基础很重要!项目很重要!

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

React useEvent:砖家说的没问题

之前写了一篇文章《React Hooks 使用误区,驳官方文档》,文中抛出了两个观点:

  1. 不是所有的依赖都必须放到依赖数组中
  2. deps 参数不能缓解闭包问题

这两个观点引起了剧烈的讨论,当然大多数人还是持反对意见的,甚至质疑我不会用 Hooks,(⊙o⊙)… 我想说我写的 Hooks 比你吃的盐都多(开玩笑 😋 ~)

然后呢,知乎上来了个提问《如何看待《React Hooks 使用误区,驳官方文档》?》,大家依旧是讨论激烈,甚至 #黄玄 大佬也亲自来回答了。

很多同学极力反对我的观点,刚开始我还想一争高下,后来实在没精力一个一个对线。

这不,React 官方来帮我助阵了?React 官方为啥出 useEvent?就是发现以前要求的依赖写法,实在有太大问题,不加一个新的 API,官方示例都没法写了 🙂。

image.png

以前一直觉得 React Hooks 教程,包括 Dan 写的 useEffect 教程,都只是写了基础场景,对于稍微复杂点的场景,都避而不谈。因为这些复杂场景,在之前的规则下,确实是没法玩。

什么是 useEvent

关于 useEvent 是什么,官方 RFC 文档有非常详细的解释,并且目前社区上也有非常多的文章介绍(其实很多介绍都是有问题的)。接下来用一个官方文档上的一个例子,来认识一下 useEvent。
需求很简单,我们希望 url 变化的时候,上报下当前 urlusername

function Page({ route, currentUser }) {
  useEffect(() => {
    logAnalytics('visit_page', route.url, currentUser.name);
  }, [route.url]);
  // ...
}

如上代码,会有 warning,告诉我们 currentUser.name要放到 deps 中。修正后代码是这样

function Page({ route, currentUser }) {
  useEffect(() => {
    logAnalytics('visit_page', route.url, currentUser.name);
  }, [route.url, currentUser.name]);
  // ...
}

但这样明显满足不了我们的业务需求,因为 currentUser.name变化后,也触发了上报请求。

很多杠精就问,为啥你的需求要这样设计?为啥 currentUser.name变化后不要上报?你的需求不合理吧?
这个你去问 dan 吧~

以前的解决方案可能有两个:

  1. 忽略警告,把 eslint-plugin-react-hooks卸载掉
  2. 通过 ref 来标记 currentUser.name
function Page({ route, currentUser }) {
  const ref = useRef(currentUser.name);
  ref.current = currentUser.name;

  useEffect(() => {
    logAnalytics('visit_page', route.url, ref.current);
  }, [route.url]);
  // ...
}

两个方案都有缺点:

  1. 打破了所谓的 React 对 deps 的限制规则
  2. 写法太麻烦,项目复杂后要定义无数个 ref

基于 useEvent 改造起来就很简单了

function Page({ route, currentUser }) {
  // ✅ Stable identity
  const onVisit = useEvent(visitedUrl => {
    logAnalytics('visit_page', visitedUrl, currentUser.name);
  });

  useEffect(() => {
    onVisit(route.url);
  }, [route.url]); // ✅ Re-runs only on route change
  // ...
}

useEvent 会将一个函数「持久化」,同时可以保证函数内部的变量引用永远是最新的。如果你用过 ahooks 的 useMemoizedFn,实现的效果是几乎一致的。
再强调下 useEvent 的两个特性:

  1. 函数地址永远是不变的
  2. 函数内引用的变量永远是最新的

useEvent 可以用来代替 useCallback,以前这样写,在 text 变化的时候,函数地址会变化。

function Chat() {
  const [text, setText] = useState('');

  // 🟡 A different function whenever `text` changes
  const onClick = useCallback(() => {
    sendMessage(text);
  }, [text]);

  return <SendButton onClick={onClick} />;
}

通过 useEvent 代替 useCallback 后,不用写 deps 函数了,并且函数地址永远是固定的,text也永远是最新的。

function Chat() {
  const [text, setText] = useState('');

  // ✅ Always the same function (even if `text` changes)
  const onClick = useEvent(() => {
    sendMessage(text);
  });

  return <SendButton onClick={onClick} />;
}

useEvent 是怎么实现的

useEvent 的实现原理比较简单,但现在看到的社区上的介绍文章几乎都有问题。

// (!) Approximate behavior

function useEvent(handler) {
  const handlerRef = useRef(null);

  // In a real implementation, this would run before layout effects
  useLayoutEffect(() => {
    handlerRef.current = handler;
  });

  return useCallback((...args) => {
    // In a real implementation, this would throw if called during render
    const fn = handlerRef.current;
    return fn(...args);
  }, []);
}

上面的代码是官方提供的一个示例代码,需要重点注意这句注释 In a real implementation, this would run before layout effects,翻译过来就是 “在真实的实现中,这里用的 Hooks 执行时机在 useLayoutEffect之前”。

这里一定是不能用 useLayoutEffect来更新 ref的,因为子组件的 useLayoutEffect比父组件的执行更早,如果这样用的话,子组件的 useLayoutEffect中访问到的 ref一定是旧的。

所以官方为了实现 useEvent,一定是要加一个在 useLayoutEffect 之前执行的 Hooks 的,并且这个 Hooks 应该不会开放给普通用户使用的。

另外 React 要求不要在 render 中直接调用 useEvent返回的函数,原理也是一样的,在 render 中访问的函数一定是旧的,因为 useLayoutEffect还没执行呢。

useMemoizedFn 和 useEvent 的差异

在 React 18 之前,社区上有很多类似 useEvent 的实现,比如 ahooks 的 useMemoizedFn,类似下面这样

function useMemoizedFn(fn) {

  const fnRef = useRef(fn);
  fnRef.current = useMemo(() => fn, [fn]);

  return useCallback((...args) => {
    return fnRef.current.apply(args);
  }, []);
}

之前很多同学问,为啥不用 useLayoutEffect,是不是有问题?现在应该明白了吧?我们需要一个比useLayoutEffect执行更早的 Hooks,很遗憾的是之前更没有,所以只能放到 render 中。

为什么之前官方没有提供类似的 Hooks?useMemoizedFn 有问题吗?
之前 React Issue #16956 上对类似的封装做了很多讨论,官方的态度一直是 “在 concurrent 下可能会存在问题” ,也就是官方也吃不准未来会不会出问题。随着 React 18 发布,concurrent 模式稳定之后,官方发现,这种写法不会有问题,索性就自己提供了一个。

在 React 18 之前,因为没有 concurrent,所以 useMemoizedFn 不会有任何问题。在 React 18 之后,我目前也没看到有什么问题。不过为了稳妥起见,后面 ahooks 的 useMemoizedFn 会做一次升级,向官方的 useEvent 看齐。

最后用知乎上一个同学的评论结尾“面多了加水,水多了加面”。

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

React Hooks 原理

前言

目前,Hooks 应该是 React 中最火的概念了,在阅读这篇文章之前,希望你已经了解了基本的 Hooks 用法。

在使用 Hooks 的时候,我们可能会有很多疑惑

  1. 为什么只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用?
  2. 为什么 useEffect 第二个参数是空数组,就相当于 ComponentDidMount ,只会执行一次?
  3. 自定义的 Hook 是如何影响使用它的函数组件的?
  4. Capture Value 特性是如何产生的?
  5. ......

这篇文章我们不会讲解 Hooks 的概念和用法,而是会带你从零实现一个 tiny hooks,知其然知其所以然。

useState

  1. 最简单的 useState 用法是这样的:

    demo1: https://codesandbox.io/s/v0nqm309q3

    function Counter() {
      var [count, setCount] = useState(0);
    
      return (
        <div>
          <div>{count}</div>
          <Button onClick={() => { setCount(count + 1); }}>
            点击
          </Button>
        </div>
      );
    }
  2. 基于 useState 的用法,我们尝试着自己实现一个 useState:

    demo2:https://codesandbox.io/s/myy5qvoxpp

    function useState(initialValue) {
      var state = initialValue;
      function setState(newState) {
        state = newState;
        render();
      }
      return [state, setState];
    }
  3. 这时我们发现,点击 Button 的时候,count 并不会变化,为什么呢?我们没有存储 state,每次渲染 Counter 组件的时候,state 都是新重置的。

    自然我们就能想到,把 state 提取出来,存在 useState 外面。

    demo3:https://codesandbox.io/s/q9wq6w5k3w

    var _state; // 把 state 存储在外面
    
    function useState(initialValue) {
      _state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
      function setState(newState) {
        _state = newState;
        render();
      }
      return [_state, setState];
    }

到目前为止,我们实现了一个可以工作的 useState,至少现在来看没啥问题。

接下来,让我们看看 useEffect 是怎么实现的。

useEffect

useEffect 是另外一个基础的 Hook,用来处理副作用,最简单的用法是这样的:

demo4:https://codesandbox.io/s/93jp55qyp4

 useEffect(() => {
    console.log(count);
 }, [count]);

我们知道 useEffect 有几个特点:

  1. 有两个参数 callback 和 dependencies 数组
  2. 如果 dependencies 不存在,那么 callback 每次 render 都会执行
  3. 如果 dependencies 存在,只有当它发生了变化, callback 才会执行

我们来实现一个 useEffect

demo5:https://codesandbox.io/s/3kv3zlvzl1

let _deps; // _deps 记录 useEffect 上一次的 依赖

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray; // 如果 dependencies 不存在
  const hasChangedDeps = _deps
    ? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
    : true;
  /* 如果 dependencies 不存在,或者 dependencies 有变化*/
  if (hasNoDeps || hasChangedDeps) {
    callback();
    _deps = depArray;
  }
}

到这里,我们又实现了一个可以工作的 useEffect,似乎没有那么难。

此时我们应该可以解答一个问题:

Q:为什么第二个参数是空数组,相当于 componentDidMount

A:因为依赖一直不变化,callback 不会二次执行。

Not Magic, just Arrays

到现在为止,我们已经实现了可以工作的 useState 和 useEffect。但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。比如

const [count, setCount] = useState(0);
const [username, setUsername] = useState('fan');

count 和 username 永远是相等的,因为他们共用了一个 _state,并没有地方能分别存储两个值。我们需要可以存储多个 _state 和 _deps。

如 《React hooks: not magic, just arrays》所写,我们可以使用数组,来解决 Hooks 的复用问题。

demo6:https://codesandbox.io/s/50ww35vkzl

代码关键在于:

  1. 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
  2. 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
  3. 如果还是不清楚,可以看下面的图。
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

我们用图来描述 memoizedState 及 cursor 变化的过程。

1. 初始化


1

2. 初次渲染

2

3. 事件触发

3

4. Re Render

4

到这里,我们实现了一个可以任意复用的 useState 和 useEffect。

同时,也可以解答几个问题:

Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。

A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

Q:自定义的 Hook 是如何影响使用它的函数组件的?

A:共享同一个 memoizedState,共享同一个顺序。

Q:“Capture Value” 特性是如何产生的?

A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

真正的 React 实现

虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。

  • React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。

    type Hooks = {
    	memoizedState: any, // 指向当前渲染节点 Fiber
      baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
      baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
      queue: UpdateQueue<any> | null,// UpdateQueue 通过
      next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
    }
     
    type Effect = {
      tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
      create: () => mixed, // 初始化 callback
      destroy: (() => mixed) | null, // 卸载 callback
      deps: Array<mixed> | null,
      next: Effect, // 同上 
    };
  • memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?

    我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

5

参考文章

  1. Deep dive: How do React hooks really work?
  2. React hooks: not magic, just arrays

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

一个词语的错误

HTML:
3. 使用 data- 属性的好处是什么?
可以去 时间 下data-*的使用啦

++++
“时间”是不是用错啦,应该改为“实践”?
++++

如何升级到 React 18

这是 React 官方 2022.03.08 发表的文章《How to Upgrade to the React 18 Release Candidate》的译文,通过本文,可以对 React 18 的新特性有一个全面的认知。

接下来,我还会翻译其它几篇比较重要的 React 18 文章,以便以更好的姿势使用 React 18,关注不迷路。

今天,我们发布了 React 18 RC 版本。正如我们在 React Conf 上分享的那样,React 18 基于 concurrent 模式,带来了更多能力,同时提供了渐进升级的方法。在这篇文章中,我们会一步一步的带您升级到 React 18。

安装

使用 @rc标签来安装最新版 React

## npm
$ npm install react@rc react-dom@rc

## yarn
$ yarn add react@rc react-dom@rc

客户端渲染 API 更新

当你首次安装 React 18 的时候,你会看到如下警告

ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it’s running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

React 18 提供了更合理的初始化 API,使用该 API,会自动启用 concurrent 模式:

// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App tab="home" />);

同时我们将卸载方法从 unmountComponentAtNode 修改为 root.unmount

// Before
unmountComponentAtNode(container);

// After
root.unmount();

我们移除了 ReactDOM.render 函数的 callback,因为当使用 Suspense 的时候,它会有问题:

// Before
const container = document.getElementById('app');
ReactDOM.render(<App tab="home" />, container, () => {
  console.log('rendered');
});

// After
function AppWithCallbackAfterRender() {
  useEffect(() => {
    console.log('rendered');
  });

return <App tab="home" />
}

const container = document.getElementById('app');
const root = ReactDOM.createRoot(container);
root.render(<AppWithCallbackAfterRender />);

最后,如果你使用 hydration 来实现了 SSR,需要将 hydrate 替换为 hydrateRoot

// Before
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);

// After
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here.

更多信息可见 Replacing render with createRoot

SSR API 更新

在 React 18 中,为了支持服务端的 Suspense 和流式 SSR,优化了 react-dom/server 的 API。

使用以下 API,将会抛出警告:

  • renderToNodeStream:废弃 ⛔️️

相反,对于 Node 环境中的流式传输,请使用:

  • renderToPipeableStream:新增

我们还引入了一个新的 API,以在现代边缘运行时环境支持流式 SSR 和 Suspense,例如 Deno 和 Cloudflare workers:

  • renderToReadableStream:新增

下面的两个 API 可以继续使用,但是不支持 Suspense:

  • renderToString:限制 ⚠️
  • renderToStaticMarkup:限制 ⚠️

下面的 API 没有变化:

  • renderToStaticNodeStream

更多信息可见Upgrading to React 18 on the serverNew Suspense SSR Architecture in React 18

自动批处理 Automatic Batching

批处理是指:React 将多个状态更新,聚合到一次 render 中执行,以提升性能。

在 React 18 之前,只能在 React 自己的事件机制中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。

React 18 支持了更多场景下的批处理,以提供更好的性能。

// 在 React 18 之前,只有 React 事件,才会使用批处理

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 会 render 两次,每次 state 变化更新一次
}, 1000);

使用 createRoot初始化 React 18 之后,所有的状态更新,会自动使用批处理,不关心应用场景。

// React 18 之后,Promise、setTimeout、原生事件中,都会自动批处理

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}, 1000);

这是一个 break change,但是我们希望这能提升你的产品性能。当然,你仍然可以使用 flushSync 来手动取消批处理,强制同步执行:

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React 更新一次 DOM
  flushSync(() => {
    setFlag(f => !f);
  });
  // React 更新一次 DOM
}

更多信息可见 Automatic batching for fewer renders in React 18

三方库 API

在 React 18 中,我们和三方库作者合作,定义了一些新的 API,以满足三方库在 concurrent 模式下特定场景的诉求。比如 styles 管理、外部状态管理、可访问性(accessibility)等场景。

为了支持 React 18,一些三方库可能需要用到下面的 API:

  • useId 是一个新的 Hook,支持在客户端和服务端生成唯一的 ID,同时避免 hydration 的不兼容。它可以解决在 React 17 及更低版本一直存在的问题。在 React 18 中,这个问题尤为重要,因为流式 SSR 返回的 HTML 片段是无序的。更多信息可见 Intent to Ship: useId

  • useSyncExternalStore是一个新的 Hook,允许外部状态管理器,强制立即同步更新,以支持并发读取。这个新的 API 推荐用于所有 React 外部状态管理库。详情见 useSyncExternalStore overview postuseSyncExternalStore API details

  • useInsertionEffect是一个新的 Hook,它可以解决 CSS-in-JS 库在渲染中动态注入样式的性能问题。除非你已经构建了一个 CSS-in-JS 库,否则我们不希望你使用它。这个 Hook 执行时机在 DOM 生成之后,Layout Effect 执行之前。更多信息可见 Library Upgrade Guide for style

React 18还为 concurrent 渲染引入了新的 API,例如 startTransitionuseDeferredValue,在即将发布的稳定版本中会分享更多相关内容。

严格模式 Strict Mode

未来,我们希望添加一个功能,允许 React 保存组件的状态,但移除 UI 部分。比如在返回旧的页面时,React 立即恢复之前的内容。为此,React 将使用之前保留的状态重新加载组件。

这个功能会给 React 项目带来非常好的体验,但要求组件支持 state 不变的情况下,组件多次卸载和重载。

为了检查出不合适的组件写法,React 18 在开发模式渲染组件时,会自动执行一次卸载,再重新加载的行为,以便检查组件是否支持 state 不变,组件卸载重载的场景。

在以前,React 加载组件的逻辑为:

* React mounts the component.
    * Layout effects are created.
    * Effect effects are created.

在 React 18 严格模式的开发环境,React 会模拟卸载并重载组件:

* React mounts the component.
    * Layout effects are created.
    * Effect effects are created.
* React simulates unmounting the component.
    * Layout effects are destroyed.
    * Effects are destroyed.
* React simulates mounting the component with the previous state.
    * Layout effect setup code runs
    * Effect setup code runs

更多信息可见: Adding Strict Effects to Strict ModeHow to Support Strict Effects

配置测试环境

当你第一次在测试用例中使用 createRoot时候,你会看到以下警告:

The current testing environment is not configured to support act(…)

为了修复这个问题,你需要在执行用例之前设置 globalThis.IS_REACT_ACT_ENVIRONMENTtrue

// In your test setup file
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

这个标记告诉 React,它在一个类似单元测试的环境中运行。如果你忘了使用 act,React 将打印一些有用的警告。
你也可以将标志设置为 false 来告诉 React 不需要 act。 这对于模拟浏览器环境的端到端测试很有用。
当然,我们希望测试库会自动为您加上这个配置。 例如,下一个版本的 React Testing Library 内置了对 React 18 的支持,无需任何额外配置。

更多信息可见:More background on the the act testing API and related changes

移除了 IE 支持

在此版本中,React 将放弃对 Internet Explorer 的支持。我们进行此更改是因为 React 18 中引入的新功能是基于现代浏览器开发的,部分能力在 IE 上是不支持的,比如 microtasks。

如果您需要支持 Internet Explorer,我们建议您继续使用 React 17。

其它变更

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

useRequest-蚂蚁中台标准请求 Hooks

useRequst 文档:https://hooks.umijs.org/zh-CN/async

Umi Hooks Github 地址:https://github.com/umijs/hooks

useRequest 是一个超级强大,且生产完备的网络请求 Hooks,目前已经成为蚂蚁中台最佳实践内置网络请求方案。在蚂蚁内部中台应用,写网络请求,都推荐用 useRequest。

useRequest 可能是目前社区中最强大,最接地气的请求类 Hooks 了。可以覆盖 99% 的网络请求场景,无论是读还是写,无论是普通请求还是分页请求,无论是缓存还是防抖节流,通通都能支持。只有你想不到,没有它做不到(吹牛🐂~)。

为什么要做 useRequest?

在组件开发中,要实现一个健壮的网络请求,并不是一个简单的事情。正如我上一篇文章《Umi Hooks - 助力拥抱 React Hooks》举的例子,实现一个网络请求,我们需要考虑 loading、竞态处理、组件卸载等等方面。

当然通过 React Hooks 的逻辑封装能力,我们可以将网络请求相关的逻辑封装起来。Umi Hooks 中的 useAsync 就做了这个事情,一行代码就可以实现网络请求,提效非常明显。

但日常工作中,只用一个 useAsync 还是不够的,Umi Hooks 中和网络请求相关的 Hooks 就有非常多。比如和分页请求相关的 usePagination,请求自带防抖的 useSearch,内置 umi-request 的 useAPI,加载更多场景的 useLoadMore,等等等等。

目前已有 Hooks 有几个很明显的缺点:

  • 上手成本偏高,需要针对不同场景选择不同的 Hooks。
  • 所有网络请求 Hooks API,底层能力不一致。比如 usePagination 不支持手动触发、不支持轮询等等。
  • useAsync 能力不足,很多场景无法满足需求,比如并行请求。

同时随着 zeit/swr 的诞生,给了我们很多灵感,原来网络请求还可以这么玩!swr 有非常多好用,并且我们想不到的能力。比如:

这里我简单科普下 swr。swr 是 stale-while-revalidate 的简称,最主要的能力是:我们在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染。swr 特性在特定场景,对用户非常友好。

基于上面两点,经过内部多次讨论,最终决定,我们要做一个能力强大,覆盖所有场景的网络请求 Hooks!useRequest 诞生了!它不仅囊括了当前 Umi Hooks 中所有和网络请求相关的 Hooks 的能力,也大量借鉴了 swr 的优秀特性,香的不得了。

能力介绍

基础网络请求

import { useRequest } from '@umijs/hooks';

function getUsername() {
  return Promise.resolve('jack');
}

export default () => {
  const { data, error, loading } = useRequest(getUsername)
  
  if (error) return <div>failed to load</div>
  if (loading) return <div>loading...</div>
  return <div>Username: {data}</div>
}

这是一个最简单的网络请求示例。在这个例子中 useRequest 接收了一个 Promise 函数。在组件初始化时,会自动触发 getUsername 执行,并自动管理 dataloadingerror 等数据,我们只需要根据状态来写相应的 UI 实现即可。

在线 demo

手动请求

对于“写”请求,我们一般需要手动触发,比如添加用户,编辑信息,删除用户等等。 useRequest 只需要配置 manual = true ,即可阻止初始化执行。只有触发 run 时才会开始执行。

2020-02-13 20.43.15.gif

import { useRequest } from '@umijs/hooks';

export default () => {
  const { run, loading } = useRequest(changeUsername, {manual: true})
  
  return (
    <Button onClick={() => run('new name')} loading={loading}>
       Edit
    </Button>
    )
}

在线 demo

轮询

对于需要保持新鲜度的数据,我们通常需要不断发起网络请求以更新数据。 useRequest 只要配置 poilingInterval 即可自动定时发起网络请求。

import { useRequest } from '@umijs/hooks';

export default () => {
  const { data } = useRequest(getUsername, { pollingInterval: 1000 })

  return <div>Username: {data}</div>
}

同时通过设置 pollingWhenHidden ,我们可以智能的实现在屏幕隐藏时,暂停轮询。等屏幕恢复可见时,继续请求,以节省资源。

当然你也可以通过 run/cancel 来手动控制定时器的开启和关闭。

在线 demo

并行请求

什么是并行请求?看了下图应该就明白了,也就是同一个接口,我们需要维护多个请求状态。

示例中的并行请求有几个特点:

  • 删除 n 个不同的用户,则需要维护 n 个请求状态。
  • 多次删除同一个用户,则只需要维护最后一个请求。

2020-02-13 21.03.55.gif

useRequest 通过设置 fetchKey ,即可对请求进行分类。相同分类的请求,只会维护一份状态。不同分类的请求,则会维护多份状态。在下面的代码中,我们通过 userId 将请求进行分类,同时我们可以通过 fetches[userId] 拿到当前分类的请求状态!

export default () => {
  const { run, fetches } = useRequest(deleteUser, {
    manual: true,
    fetchKey: id => id, // 不同的 ID,分类不同
  });

  return (
    <div>
      <Button loading={fetches.A?.loading} onClick={() => { run('A') }}>删除 1</Button>
      <Button loading={fetches.B?.loading} onClick={() => { run('B') }}>删除 2</Button>
      <Button loading={fetches.C?.loading} onClick={() => { run('C') }}>删除 3</Button>
    </div>
  );
};

在线 demo

防抖 & 节流

通常在边输入边搜索的场景中,我们会用到防抖功能,以节省不必要的网络请求。通过 useRequest ,只需要配置一个 debounceInterval ,就可以非常简单的实现对网络请求的节流操作。

2020-02-13 21.24.40.gif

在下面的例子中,无论调用了多少次 run ,只会在输入停止后,发送一次请求。

import { useRequest } from '@umijs/hooks';

export default () => {
  const { data, loading, run, cancel } = useRequest(getEmail, {
    debounceInterval: 500,
    manual: true
  });

  return (
    <div>
      <Select onSearch={run} loading={loading}>
        {data && data.map(i => <Option key={i} value={i}>{i}</Option>)}
      </Select>
    </div>
  );
};

节流与防抖是同样的道理,只需要配置了 throttleInterval ,即可实现节流功能。

在线 demo

缓存 & SWR & 预加载

在前面我讲了什么是 SWR,在 SWR 场景下,我们会对接口数据进行缓存,当下次请求该接口时,我们会先返回缓存的数据,同时,在背后发起新的网络请求,待新数据拿到后,重新触发渲染。

对于一些数据不是经常变化的接口,使用 SWR 后,可以极大提高用户使用体验。比如下面的图片例子,当我们第二次访问该文章时,直接返回了缓存的数据,没有任何的等待时间。同时,我们可以看到“最新访问时间”在 2 秒后更新了,这意味着新的请求数据返回了。

2020-02-13 21.58.31.gif

useRequest 通过配置 cacheKey ,即可进入 SWR 模式,相当简单。

  const { data, loading } = useRequest(getArticle, {
    cacheKey: 'articleKey',
  });

同时需要注意,同一个 cacheyKey 的数据是全局共享的。通过这个特性,我们可以实现“预加载”功能。比如鼠标 hover 到文章标题时,我们即发送读取文章详情的请求,这样等用户真正点进文章时,数据早已经缓存好了。

在线 demo

屏幕聚焦重新请求

通过配置 refreshOnWindowFocus ,我们可以实现,在屏幕重新聚焦或可见时,重新发起网络请求。这个特性有什么用呢?它可以保证多个 tab 间数据的同步性。也可以解决长间隔之后重新打开网站的数据新鲜度问题。

这里借用 swr 的一个图来说明问题。

2020-02-13 22.12.25.gif

在线 demo

集成请求库

考虑到使用便捷性, useRequest 集成了 umi-request。如果第一个参数不是 Promise,我们会通过 umi-request 来发起网络请求。

当然如果你想用 axios,也是可以的,通过 requstMethod 即可定制你自己的请求方法。

// 用法 1
const { data, error, loading } = useRequest('/api/userInfo');

// 用法 2
const { data, error, loading } = useRequest({
  url: '/api/changeUsername',
  method: 'post',
});

// 用法 3
const { data, error, loading } = useRequest((userId)=> `/api/userInfo/${userId}`);

// 用法 4
const { loading, run } = useRequest((username) => ({
  url: '/api/changeUsername',
  method: 'post',
  data: { username },
}));

在线 demo

分页

中台应用中最多的就是表格和表单了。对于一个表格,我们要处理非常多的请求逻辑,包括不限于:

  • page、pageSize、total 管理
  • 筛选条件变化,重置分页,重新发起网络请求

useRequest 通过配置 paginated = true ,即可进入分页模式,自动帮你处理表格常见逻辑,同时我们对 antd Table 做了特殊支持,只用简单几行代码,就可以实现下面图中这样复杂的逻辑,提效百倍。

2020-02-13 22.34.40.gif

import {useRequest} from '@umijs/hooks';

export default () => {
  const [gender, setGender] = useState('male');
  const { tableProps } = useRequest((params)=>{
    return getTableData({...params, gender})
  }, {
    paginated: true,
    refreshDeps: [gender]
  });

  const columns = [];

  return (
    <Table columns={columns} rowKey="email" {...tableProps}/>
  );
};

在线 demo

加载更多

加载更多的场景也是日常开发中常见的需求。在加载场景中,我们一般需要处理:

  • 分页 offset、pageSize 等管理
  • 首次加载,加载更多状态管理
  • 上拉自动加载更多
  • 组件第二次加载时,希望能记录之前的数据,并滚动到之前的位置

useRequest 通过设置 loadMore = true ,即可进入加载更多模式,配合其它参数,可以帮你处理上面所有的逻辑。

2020-02-13 22.46.16.gif

const { data, loading, loadMore, loadingMore } = useRequest((d) => getLoadMoreList(d?.nextId, 3), {
  loadMore: true,
  cacheKey: 'loadMoreDemoCacheId',
  fetchKey: d => `${d?.nextId}-`,
});

在线 demo

更多

当然我前面也说了, useReqeust 的功能只有你想不到,没有它没有的。哈哈哈~

除了上面的特性,我们还有一些其它的能力,可以在文档中发现。比如 loadingDelay

loadingDelay

通过设置 loadingDelay ,延迟 loading 变为 true 的时间,当请求很快响应时,可以有效避免 loading 变化导致的抖动。

2020-02-13 22.49.43.gif

总结

虽然 useRequest 的功能很多,也避免不了有些你想用的特性它不支持。但不用担心,你可以很方面的基于 useRequest 去扩展。我们的分页模式,及加载更多模式均是基于底层能力扩展实现的。你可以参考它们的代码,实现自己的特有能力。

通过 useRequest ,可以解决日常 99% 的网络请求需求。奥利给!奥利给!

招聘

最后打个招聘广告,蚂蚁金服体验技术部招聘前端啦!要求 P6 及以上!有兴趣的同学可以发简历到

[email protected]

[email protected]

[email protected]

我会帮您跟进面试进度的,期待您的加入~

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

助你完全理解React高阶组件(Higher-Order Components)

助你完全理解React高阶组件(Higher-Order Components)

有时候人们很喜欢造一些名字很吓人的名词,让人一听这个名词就觉得自己不可能学会,从而让人望而却步。但是其实这些名词背后所代表的东西其实很简单。来自React.js 小书

高阶组件定义

a higher-order component is a function that takes a component and returns a new component.

翻译:高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

理解了吗?看了定义似懂非懂?继续往下看。

函数模拟高阶组件

我们通过普通函数来理解什么是高阶组件哦~

  1. 最普通的方法,一个welcome,一个goodbye。两个函数先从localStorage读取了username,然后对username做了一些处理。
function welcome() {
    let username = localStorage.getItem('username');
    console.log('welcome ' + username);
}

function goodbey() {
    let username = localStorage.getItem('username');
    console.log('goodbey ' + username);
}

welcome();
goodbey();
  1. 我们发现两个函数有一句代码是一样的,这叫冗余唉。不好不好~(你可以把那一句代码理解成平时的一大堆代码)

    我们要写一个中间函数,读取username,他来负责把username传递给两个函数。

function welcome(username) {
    console.log('welcome ' + username);
}

function goodbey(username) {
    console.log('goodbey ' + username);
}

function wrapWithUsername(wrappedFunc) {
    let newFunc = () => {
        let username = localStorage.getItem('username');
        wrappedFunc(username);
    };
    return newFunc;
}

welcome = wrapWithUsername(welcome);
goodbey = wrapWithUsername(goodbey);

welcome();
goodbey();

好了,我们里面的wrapWithUsername函数就是一个“高阶函数”。
他做了什么?他帮我们处理了username,传递给目标函数。我们调用最终的函数welcome的时候,根本不用关心username是怎么来的。

我们增加个用户study函数。

function study(username){
    console.log(username+' study');
}
study = wrapWithUsername(study);

study();

这里你是不是理解了为什么说wrapWithUsername是高阶函数?我们只需要知道,用wrapWithUsername包装我们的study函数后,study函数第一个参数是username

我们写平时写代码的时候,不用关心wrapWithUsername内部是如何实现的。

高阶组件

高阶组件就是一个没有副作用的纯函数。

我们把上一节的函数统统改成react组件。

  1. 最普通的组件哦。

welcome函数转为react组件。

import React, {Component} from 'react'

class Welcome extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: ''
        }
    }

    componentWillMount() {
        let username = localStorage.getItem('username');
        this.setState({
            username: username
        })
    }

    render() {
        return (
            <div>welcome {this.state.username}</div>
        )
    }
}

export default Welcome;

goodbey函数转为react组件。

import React, {Component} from 'react'

class Goodbye extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: ''
        }
    }

    componentWillMount() {
        let username = localStorage.getItem('username');
        this.setState({
            username: username
        })
    }

    render() {
        return (
            <div>goodbye {this.state.username}</div>
        )
    }
}

export default Goodbye;
  1. 现在你是不是更能看到问题所在了?两个组件大部分代码都是重复的唉。

按照上一节wrapWithUsername函数的思路,我们来写一个高阶组件(高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件)。

import React, {Component} from 'react'

export default (WrappedComponent) => {
    class NewComponent extends Component {
        constructor() {
            super();
            this.state = {
                username: ''
            }
        }

        componentWillMount() {
            let username = localStorage.getItem('username');
            this.setState({
                username: username
            })
        }

        render() {
            return <WrappedComponent username={this.state.username}/>
        }
    }

    return NewComponent
}

这样我们就能简化Welcome组件和Goodbye组件。

import React, {Component} from 'react';
import wrapWithUsername from 'wrapWithUsername';

class Welcome extends Component {

    render() {
        return (
            <div>welcome {this.props.username}</div>
        )
    }
}

Welcome = wrapWithUsername(Welcome);

export default Welcome;
import React, {Component} from 'react';
import wrapWithUsername from 'wrapWithUsername';

class Goodbye extends Component {

    render() {
        return (
            <div>goodbye {this.props.username}</div>
        )
    }
}

Goodbye = wrapWithUsername(Goodbye);

export default Goodbye;

看到没有,高阶组件就是把username通过props传递给目标组件了。目标组件只管从props里面拿来用就好了。

到这里位置,高阶组件就讲完了。你再返回去理解下定义,是不是豁然开朗~

你现在理解react-reduxconnect函数~

reduxstateaction创建函数,通过props注入给了Component
你在目标组件Component里面可以直接用this.props去调用redux stateaction创建函数了。

ConnectedComment = connect(mapStateToProps, mapDispatchToProps)(Component);

相当于这样

// connect是一个返回函数的函数(就是个高阶函数)
const enhance = connect(mapStateToProps, mapDispatchToProps);
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(Component);

antd的Form也是一样的

const WrappedNormalLoginForm = Form.create()(NormalLoginForm);

参考地址:

  1. http://huziketang.com/books/react/lesson28
  2. https://react.bootcss.com/react/docs/higher-order-components.html

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

Umi Hooks - 助力拥抱 React Hooks

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

这是蚂蚁金服内部技术分享的文字稿,本次分享主要介绍了为什么要用 Hooks?以及如何使用 Umi Hooks 提效?

Umi Hooks http://github.com/umijs/hooks

开场

image-20200116191741370

大家好,我叫尽龙,来自体验技术部。社区名称叫 brickspert,砖家的意思。

自从 React 推出 React Hooks 后,就开始尝试使用 Hooks,并逐渐喜欢上了它。目前,几乎 100% 的组件都是使用 Hooks 开发。经过大半年的实践,在 Hooks 使用方面沉淀了一些经验,很高兴今天有机会能分享给大家。

image-20200116191759286

在分享开始之前,我想了解下:“有多少同学目前已经在项目中大量使用 Hooks 了?”

嗯嗯,谢谢。看举手的同学,大概一半一半吧。没关系,听完今天的分享,我相信你一定有兴趣尝试下 Hooks 的。

React Hooks 是 react v16.8 的一个新特性,很佩服这么重磅的功能,在一个小版本中发布,说明 React 团队有足够的信心向上兼容。

Why Hooks?

image-20200116191817331

为什么要放弃 Class,转用 Hooks 呢?在内部外部有很多争论,包括知乎也有类似提问。我们也不免俗套的要对比下 Class 和 Hooks 了。当然为了保证今天的分享效果,我肯定会偏向 Hooks 的(哈哈哈哈)。

image-20200116192324872

Class 学习成本高

Class 学习成本很高。首当其中的就是生命周期,多,太多了。不仅多,还会变!React v15 和 v16 就不一样。下面是我在网上随便找的一张图。

image-a5b927b35025

这个是 React v15 的生命周期,你都掌握了吗?你知道 v16 有什么变化吗?

之前无论你去哪里面试,基本都会有几个必问问题:

  • 讲讲 React 生命周期?React v15 和 React v16 生命周期有啥变化?
  • 如何优化 Class 组件?shouldComponentUpdate 是做什么的?如何用?
  • 一般在哪个生命周期发送网络请求?为什么?
  • ......

生命周期最重要,但是有很高的学习成本,需要大量实践才能积累足够的经验。当然,这几个问题回答不好,百分之八十以上的几率会挂掉。

当然不止是生命周期,this 也是一个很大的问题。你有没有在组件写很多 bind?或者所有的函数都用箭头函数定义?

this.someFunction = this.someFunction.bind(this);

// 或
someFunction = ()=>{}

为什么要这样写呢?如果不写会有什么问题?哎呦,又多了一个面试题,你会吗?

Hooks 学习成本低

对比 Class,Hooks 的学习成本可就太低了!掌握了 useState 和 useEffect,80% 的事情就搞定了。

image-7cbf7879e7cf

Class 业务逻辑分散

Class 业务逻辑分散,实现一个功能,我要写在不同的生命周期里面,不聚合~

比如,如果你有个定时器,你一定要在 componentWillUnMount 去卸载。

image-67b7d915f6af

再比如,我们要写一个请求用户信息的组件,当userId 变化时,要重新发起请求。我们就要在两个生命中期中写请求的逻辑。

image-4059e72aa129

相信上面的逻辑,大家也是经常会写的吧。

奥奥,sorry,上面的 componentWillReceiveProps 已经被废弃了,我们应该用 componentDidUpdate 来代替。

“咦,这是为啥呢?好好的为什么要废弃,不让这么用了?”

又来一个面试题!你知道答案吗?

Hooks 业务逻辑聚合

而 Hooks 的业务逻辑就非常聚合了。上面的两个例子,改成 Hooks 你会写吗?

image-3780dc60b735

image-88e9ba8a7add

简直不要太简单!香啊!我可以提前下班了。

Class 逻辑复用困难

说到逻辑复用,很多同学会说 Class 的 Render Props 和 HOC(高阶组件)可以做逻辑复用!那我们看看 Class 的逻辑复用有多么的惨不忍睹。

首先我们看看 Render Props。

首先我们想复用监听 window size 变化的逻辑,开开心心的写了下面的代码。

image-f9273eefa2ef

然后,我又想复用监听鼠标位置的逻辑,我只能这么写了。

image-d60b6492b570

到这里你应该看到了问题所在。这简直就是地狱!我不忍心复用其它逻辑了。

我们放过 Render Props,来看看 HOC 吧。

如果你要问什么是 HOC,那我不得不推荐我的另外一篇文章《助你完全理解React高阶组件(Higher-Order Components)》。

哪怕你不知道 HOC 是啥,你也一定用过。比如 redux 的 connect。

image-20200116200932301

上面的代码,我用了三个 HOC,分别是 redux 的 connect,react-intl 的 injectIntl,以及 AntD 的 Form.create()。

这是一个非常常见的用法。如果你光看代码,大概已经懵圈了。“我是谁?我在哪?我要干什么?”

这会我仿佛听见 HOC 在说:“我不仅让你看不懂我,我还很容易出各种问题。”

是的,HOC 很容易出问题。大家都往组件的 props 上面挂属性,万一有个重名的,那就只能说一句“不好意思,GG思密达”!

Hooks 逻辑复用简单

Hooks 来了,它表示,我要一个打五个!Render Props 和 HOC 联合起来也被我秒杀!

image-5a6f5d648ca9

Hooks 表示,来十个,来一百个我也能打。

Hooks 最强的能力就是逻辑复用了,这是我最最最爱的能力了。

Hooks 会产生很多闭包问题

是的,我也不偏袒 Hooks,由于 React Hooks 的机制,如果用法不正确,会导致各种奇怪的闭包问题。

如果你要问 React Hooks 的机制是什么的话,我又要给你推荐一篇我之前写的文章了:《React Hooks 原理》。

那面对这个问题,怎么解呢?说实话,我也没有很好的解决办法。

但是,这可能也有好处。如果碰到想不明白的问题,那 99% 是由于闭包导致的,我们有很确定的方向去排查问题。

image-4636b47be14f

记住这句话,你可以少走很多弯路。

Show Case

image-20200116203233594

当然,说再多,吹再好,也没多大用。我上面讲的 Class 和 Hooks 的优缺点,网上的也有很多人讲,大家也肯定都看过。

用程序员的交流方式,就是“Talk is cheap,Show me the code.”。

亮剑吧!

接下来,我会用一个例子,让你折服,拜倒在 Hooks 的石榴裙下。如果你不服,咱们单独撕~

网络请求组件实现

image-20200116214124140

接下来,我们来实现一个最最最常见的组件。该组件接收 userId,然后发起网络请求,获得用户信息。

说白了,就是最简单的发起网络请求的组件。我们先用 Class 来实现看看。

image-20200116214639300

这段代码,是最简单的网络请求。

  • 定义一个 username 状态。
  • componentDidMount 的时候发起网络请求。
  • 网络请求结束,更新 username。

美滋滋。但是少了点东西。网络请求,我们肯定要维护一个 loading 状态,保证用户体验比较好。

那我们加上吧。

image-20200116214918755

这张图,我们增加了 loading 状态,在网络请求发起前,置为 true,在网络请求结束后,置为 false。

美滋滋。但是还是少点东西。userId 变化后,我要重新发起网络请求吧。

我们再加点代码吧。

image-20200116215101730

我们增加了对 userId 变化的监听,如果 userId 变化后,重新发起请求。

这次稳了吧?

不不不,还不够。如果 userId 连续变化了 5 次,发送了 5 个网络请求,我们要保证总是最后一次网络请求有效。也就是经常说的的“竞态处理”或者“时序控制”。

我加!加还不行吗!

image-20200116215409524

其实到这里,有些同学已经懵了。“你说的时序控制,听着很有道理,但我平时都没处理过这个问题,我看下你怎么实现的。”

确实,时序控制不算一个简单的问题,很多新手都不会解决这个问题。

稳了!到这里你觉得稳了吧。

还是年轻啊,小伙子。

image-20200116220003295

如果用上面的代码来玩,你可能会偶尔碰到上面的警告。这个警告是怎么造成的呢?我说一下你就明白了。下面四个步骤执行,必会报警告:

  1. 组件加载
  2. 发起网络请求
  3. 组件卸载
  4. 网络请求请求成功,触发 setState

看出问题了吗?组件已经卸载了,还去 setState,造成了内存溢出。

怎么解决呢?

image-20200116220311200

在组件卸载的时候,放弃最后一次请求。

到这里为止,我们就完成了一个完美的网络请求。这次真结束了!

看下写了多少行代码。

image-20200116220531399

除去空格,我们写了 38 行代码。实话说,38 行代码我能忍,但是这些逻辑我忍不了!回想下我们处理了多少逻辑:

  • 网络请求
  • loading
  • userId 变化重新发起请求
  • 竞态处理
  • 组件卸载放弃网络请求

关键这些逻辑是没办法复用的!每个项目可能有数十上百个组件会发网络请求,我就要写几十,几百遍这样的逻辑。想想我都难受。

说实话,我在写项目的时候经常会偷懒。要不就不写 loading,要不就不管竞态,要不就不管最后的内存溢出警告。

你有没有和我一样呢?嘿嘿。

言归正传,接下来就邀请 Hooks 登场了。

image-20200116221123031

三下五除二,我们用 Hooks 实现了刚才所有的逻辑。

image-20200116221212124

17 行!代码量减少了 50% 以上。好像还行!

但是,别忘了,Hooks 最重要的能力就是逻辑复用!这些逻辑我们完全可以封装起来!我们把刚才的逻辑全部封装起来!

image-20200116221359407

useAsync 封装了刚才我们说的所有功能,一行代码完成了网络请求。

最后整个组件会长这样。

image-20200116221605411

哇!我自己都佩服自己!简直了!美呆了,帅毙了,感觉自己无敌了!提前完成工作,下班回家!

image-20200116221755193

通过这个例子,我想证明一个论点:“使用 Hooks 封装逻辑的能力,可以极大提高开发效率”。

Umi Hooks

这时候你肯定要问,useAsync 在哪里?给我瞧瞧?

image-20200116221941442

useAsync

useAsync 在这里,快来瞧,快来看啦!

useAsync 是 Umi Hooks 库的核心 Hooks 之一,Umi Hooks 提供了大量提炼自业务的 Hooks。一行代码真的可以实现很多功能!

Umi Hooks 在这里在这里!你懂的~~

image-20200116222352082

当然,useAsync 不止包含上面说的功能,还支持“手动触发”执行,还支持“轮询”!

只要简单的配置一个 pollingInterval ,就能轮询发送请求了。快去试试啦

接下来我们会介绍几个更牛的 Hooks 给大家认识!

useAntdTable

image-cc2f4b087aca

AntD 的 Table 组件,想必大家在项目中经常用到吧!除了刚才异步请求的所有逻辑外,你还得处理其它的逻辑。

image-20200116232857059

比如维护 page、pageSize、sorter、filter 的状态,还得考虑搜索条件变化后,重置 page 到第一页。这些逻辑光想想就头疼了,别说写了。

现在一行代码就可以实现了!useAntdTable,封装了所有的逻辑,只要一行代码!如图上所示,你只要 ...tableProps,就可以了。这也许就是幸福的味道吧~

useLoadMore

加载更多的场景,比如下面动图的场景,想必大家在工作中都写过。

image-22fa47992b6f

这样一个加载更多的场景,我们要维护多少状态?写多少行逻辑?本来我打算写个 Class 实现的例子贴出来的,但是我放弃了,因为太难了~~

随便想想要处理的逻辑:

  • 第一次加载时候的 loading
  • 加载更多时候的 loading
  • 维护 page 和 pageSize
  • 网络请求
  • 是不是加载全了
  • 搜索条件变化后,重置到第一页。
  • .....

脑壳疼,真的脑壳疼。我会写,但是写起来真的好累。

还没完,一般产品同学还会要求,上拉加载更多......

image-cf629db68ebc

这时候我们还得监听滚动位置,如果快到底了,触发加载更多。脑壳更疼了!

image-20200116235013101

Umi Hooks 听到了你的求救,派出 useLoadMore 来拯救你了。一行代码就可以实现所有的功能!一个小时变一分钟,又可以早点下班了。

useDynamicList

image-20200116235455431

还有更好用的,比如 useDynamicList,下面的动态列表,一行代码搞定。

image-16556dfcf0e8

useBoolean

不仅是上面讲到的各种复杂逻辑可以封装。简单的逻辑封装起来也是极其好用的,比如 Boolean 值的管理。

我们一般控制 Modal,Popover 等显示隐藏的时候,都要维护一个 visible 状态。大概会是这样。

image-20200116235651552

这样的逻辑,你写过多少遍?没有几千也有几百吧!

image-20200116235850057

以后你就可以用 useBoolean 咯!

More

image-20200116235957789

不仅是上面讲到的这些,我们还有很多很多的 Hooks。

比如 useSearch,就封装了通常异步搜索场景的逻辑,比如 debounce。

比如 useVirtualList,就封装了虚拟列表的逻辑。

image-20200117000249183

比如 useMouse,封装了监听鼠标位置的逻辑。

比如 useKeyPress,封装了监听键盘按键的逻辑。

image-20200117000412951

30+ Hooks 供您选择,并且我们仍然处于婴儿期,快速发展中。我们的愿景就是:封装别人的逻辑,让别人无逻辑可写。

未来规划

image-20200117001130397

更多的 Hooks 开发

如上面所述,我们现在还处于婴儿期,需要不断汲取能量,更多的 Hooks 正在路上!要实现“让别人无逻辑可写”的目标,还需继续奋斗。

更强大的 useRequest

image-20200117001209280

大家应该都听过 useSWR 吧?是 zeit 公司开发的一个专门做网络请求的 Hooks,提供了很多新颖的思路,给了我们非常大的启发,github star 就像坐火箭一样。但在实际项目使用中,还是会有很多地方不符合蚂蚁内部的体系。但是它给我们非常大的启发,基于 swr 的思路,我们可以实现更强大的 useRequest!图上的能力,我们都要!

useRequest 目前已经处于内测期了,下个版本将会与大家见面!我们的目标是:所有的网络请求,只用 useRequest 就够了!

Hooks 生态

目前社区上 Hooks 相关的基础教程、进阶教程、原理深入、常见问题等文档都比较分散,我们准备向 Hooks 生态发展,提供各式各样的文章。以后学习 Hooks,使用 Hooks,找 Umi Hooks 就对了。

当然,生态方面目前正在规划中,预计年后启动。

总结

image-20200117001711649

Umi Hooks,你值得拥有。

我们目前处于发展阶段,欢迎大家一起共建。

你可以提 idea,我们负责实现。

你可以提 issue,我们负责改 bug。

你可以提 PR,将你封装的 Hooks 分享给大家,让更多人收益。

❤️期待您的参与。

基于微前端的大型中台项目融合方案

关于微前端是什么,以及微前端落地方案,社区遍地都是,本篇文章不会再赘述这些基础知识。当然如果你没了解过上述知识,也可以直接读下这篇文章,足够浅显易懂。
这篇文章通过实现一个商城后台,介绍了基于 umi 框架的微前端落地方案,通过这篇文章,你可以收获

  • 超级简单的、可用于生产环境的基于 umi 的微前端实践,包括一套示例代码
  • 全新的、基于微前端的大型中台项目前端组织方式

一些技术栈

  • umi 插件化的企业级前端应用框架,帮助你更好更快的开发 React 应用
  • qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统
  • @umijs/plugin-qiankun umi 的 qiankun 插件,极其方便的在 umi 框架中使用 qiankun 的微前端能力

PS:想做 qiankun、umi、antd 就来蚂蚁金服体验技术部,我们目前正在大力招前端!!见《宇宙第三前端团队招人啦!


本篇的文章的示例源码见 umi-micro-apps

总分式的中台应用

假如我们有一个超大型的中台,不同的模块是由不同团队维护的,那我们完全可以让各自团队维护自己的前端,然后通过微前端把它们组合起来。
比如我们要做一个大型的商城后台,商铺管理和用户管理分别由两个团队开发维护,那我们可以将前端应用拆为主应用(layout)、商铺管理应用(shop)、用户管理应用(user),然后通过微前端组织起来,最终效果如下图。

1


2

1. 初始化应用

我们根据 umi 教程,初始化三个前端应用(源码见这里 umi-micro-apps

  • layout:包含菜单等 Layout
  • shop:路由以 /shop 开头,包含商铺列表和商铺详情
  • user:路由以 /user 开头,包含用户列表和用户详情

3

2. 微前端集成

umi 提供了一个 plugin-qiankun 插件,可以非常方便的使用 qiankun 的微前端能力。

  1. 首先我们在三个应用中安装 @umijs/plugin-qiankun
npm install @umijs/plugin-qiankun -S
  1. 在子应用中配置 qiankun 开启
// .umirc.ts

qiankun: {
  slave: {}
},
  1. 在主应用中打开 qiankun,并集成子应用。
// .umirc.ts

{
  qiankun: {
    // qiankun.master.apps 配置了子应用的唯一名称,及入口 html 地址
    master: {
      apps: [{
        name: 'shop',
        entry: 'http://localhost:8001'
      }, {
        name: 'user',
        entry: 'http://localhost:8002'
      }]
    },
  },
  routes: [
    {
      path: '/',
      component: '@/layouts/index',
      routes: [
        { path: '/', component: '@/pages/index' },
        { path: '/shop', microApp: 'shop' }, // 所有以 /shop 开头的路径,加载 shop 应用
        { path: '/user', microApp: 'user' }, // 所有以 /user 开头的路径,加载 shop 应用
      ]
    }
  ],
}

上述配置非常简单,我们声明了要嵌套的子应用的名称和入口 html 地址,然后在路由上配置了某个路径加载相应的子应用。
通过三步,我们已经完成了一个总分结构的微前端商城开发,步骤是真的真的超级简单,为 umi 和 qiankun 打 call~~~

  • 我们先初始化了三个应用
  • 然后我们加了几行 umi 配置

总分中台是最常见的微前端落地场景,可以完美的拆分大型中台项目,也可以平滑的升级历史应用。

套娃式的中台应用

如果我们今天的文章只是介绍“总分式的中台应用”,那这篇文章的意义就很小了,毕竟社区上把这一套都玩烂了。
想象这个场景:我们想管理某个商铺的用户,也就是在商铺详情中,我们需要查看并管理商铺名下的用户列表,以及用户详情。如下图所示:

4


商铺名下的用户展示、数据来源与“用户管理”模块一致,我们肯定不可能重复写一套。那有没有办法实现复用呢?


我们知道,shop 应用 与 user 应用的页面都是通过浏览器的 url 来驱动跳转定位的,所以同时只能存活一个应用, url 为 /shop 时展示 shop 应用,为 /user 时展示 user 应用,两个应用是不能同时激活的,否则 url 要打架了。
但困难难不倒聪明的劳动人民,我们知道 history 分为 browerhashmemory 三种,其中 memory 路由是不依赖浏览器的 url 的,如果我们能在运行时动态的将 user 应用的 history 改为 memory 类型,那完全可以实现 shop 和 user 同时存在。
plugin-qiankun 提供了一个组件 MicroAppWithMemoHistory ,该组件可以在运行时,修改子应用为 memory 路由。
要实现上述套娃,我们只需要在 shop 应用中做简单配置即可:

  1. shop 增加 qiankun.master 配置,声明子应用信息,将 user 应用作为嵌套子应用引入
// .umirc.ts
{
  qiankun: {
    master: {
      apps: [{
        name: 'user',
        entry: 'http://localhost:8002'
      }]
    },
    slave: {}
  },
}
  1. 通过 MicroAppWithMemoHistory 组件使用子应用
import { MicroAppWithMemoHistory } from 'umi';

<Drawer>
  <MicroAppWithMemoHistory name="user" url='/' />
</Drawer>

只需极其简单的代码,我们就能实现任意套娃,给中台项目的组合带来非常大的想象空间。

总结

微前端为大型中台项目带来了福音,我们可以非常灵活的进行应用拆分和组合。基于这一套玩法,我们不仅可以完成“总分”形式的组合,也可以实现“任意套娃”,极大的提升了中台应用的灵活性。
我想象中未来的中台前端也是微服务化,每个小组维护自己的数据和页面,通过“总分”和“套娃”组成一个大型中台应用。

后记

其实不应该有后记的,但是我还是想介绍个东西,不介绍心里堵得慌,这么优秀的东西应该让大家知道😋。
关于通信,我们可以像使用普通的 React 组件一样,实现父子应用的通信,超级舒服。
在父级应用中,我们可以使用 MicroAppWithMemoHistory 嵌入子应用,同时可以像普通的 React 组件一样,传递 props 进去,比如我们传递一个 shopId 给子应用。

<MicroAppWithMemoHistory 
  name="user" 
  url='/'
  shopId={1}
/>

在子应用中,我们通过一个高阶组件 connectMaster 包裹组件,即可从 props 中拿到父级传递过来的 shopId。

import React from 'react';
import { connectMaster } from 'umi';

const UserList = (props)=>{
	const {shopId} = props;
  ...
}

export default connectMaster(UserList);

UserList 也可以在子组件中使用,接收 shopId。对于 UserList 组件来讲,它根本不关心是微前端在使用,还是自己内部在使用,完全不用为微前端定制任何逻辑。

<UserList shopId={1} />

通信的心智成本相当低,我们可以像使用 React 组件一样使用其它应用,实现无限套娃。


心动不如行动,如果你想做微前端,何不选择如此简单的 umi 系列呢?
最后再打个招聘广告:宇宙第三前端团队招人啦!

学习中发现问题8.react-router章节 src/router/router.js 是否是少了 return()

src/router/router.js

少了 return();

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';


const getRouter = () => (
    return (
        <Router>
            <div>
                <ul>
                    <li><Link to="/">首页</Link></li>
                    <li><Link to="/page1">Page1</Link></li>
                </ul>
                <Switch>
                    <Route exact path="/" component={Home}/>
                    <Route path="/page1" component={Page1}/>
                </Switch>
            </div>
        </Router>
    );
);

export default getRouter;

这样写后 webpack-dev-server 才编译显示出来
确认下是这样的么?

react-router 中没有return返回

react-router 中没有return返回

import React from 'react';
import {
    BrowserRouter as Router,
    Route,
    Switch,
    Link
} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';

const getRouter = () => {
    return (
        <Router>
            <div>
                <ul>
                    <li><Link to="/">首页</Link></li>
                    <li><Link to="/page1">Page1</Link></li>
                </ul>
                <Switch>
                    <Route exact path="/" component={Home}/>
                    <Route path="/page1" component={Page1}/>
                </Switch>
            </div>
        </Router>
    )
}

export default getRouter;

hox - 下一代 React 状态管理器

github: https://github.com/umijs/hox

别着急喷,我已经能想到你为什么会进来看这个文章了,当你看到这个题目的时候,你一定会有几连问:

基于 React Hooks 状态管理器的轮子太多了,你们再造一个有什么意思?

我并不是针对某个轮子,我只想说现有所有的轮子都囿于 reduxunstated-next 的**,无非就是 actiondispatchreduceruseStoreProviderContext 这些东西,在这些东西上做排列组合。概念一大堆,理解成本不低,用起来还都差不多。

为什么你敢说你们是“下一代”?

hox 够简单,一个 API,几乎无学习成本。够好用,你会用 Hooks,就会用 hox。我想象不到比我们更简单,更好用的轮子怎么造出来?

不想看,不想学,学不动了,咋办?

一个 API,眼睛一瞪就会用,没有任何学习成本。

你们够权威吗?你们会弃坑吗?

hox 的开发者来自蚂蚁金服体验技术部,我们有 umi、dva、antd、antv 等一堆开源软件,团队足够权威。

同时 hox 的**足够简单,放心用好了。

你们能完全替代 redux,dva 吗?

状态管理器解决的问题都一样,用 hox 完全可以实现所有需求。

hox 介绍

hox 是完全拥抱 React Hooks 的状态管理器,model 层也是用 custom Hook 来定义的,它有以下几个特性:

  • 只有一个 API,简单高效,几乎无需学习成本
  • 使用 custom Hooks 来定义 model,完美拥抱 React Hooks
  • 完美的 TypeScript 支持
  • 支持多数据源,随用随取

下面我们进入正题,hox 怎么用?

定义 Model

任意一个 custom Hook ,用 createModel 包装后,就变成了持久化,且全局共享的数据。

import { createModel } from 'hox';

/* 任意一个 custom Hook */
function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  return {
    count,
    decrement,
    increment
  };
}

export default createModel(useCounter)

使用 Model

createModel 返回值是个 Hook,你可以按 React Hooks 的用法正常使用它。

import { useCounterModel } from "../models/useCounterModel";

function App(props) {
  const counter = useCounterModel();
  return (
    <div>
      <p>{counter.count}</p>
      <button onClick={counter.increment}>Increment</button>
    </div>
  );
}

useCounterModel 是一个真正的 Hook,会订阅数据的更新。也就是说,当点击 "Increment" 按钮时,会触发 counter model 的更新,并且最终通知所有使用 useCounterModel 的组件或 Hook。

其它

  • 基于上面的用法,你肯定已经知道了在 model 之间互相依赖怎么写了,就是单纯的 Hooks 互相依赖,自然而然咯。
import { useCounterModel } from "./useCounterModel";

export function useCounterDouble() {
  const counter = useCounterModel();
  return {
    ...counter,
    count: counter.count * 2
  };
}
  • 只读不订阅更新就更简单了。
import { useCounterModel } from "./useCounterModel";

export function useCounterDouble() {
  const counter = useCounterModel.data;
  return {
    ...counter,
    count: counter.count * 2
  };
}
  • 支持在 class 组件使用哦。

经典用户故事

你肯定遇到过这样的场景:

  • 一开始你把逻辑和数据都放在组件中,每次组件重建,数据都会重置掉。
  • 某一天,你想在把数据存起来,每次组件重建,不会重新刷新数据了。
  • 假设你用的 redux,你需要重新翻译一遍逻辑,完全重写逻辑和数据层,不知道有多痛苦。

如果你用 hox,故事就完全不一样了,你只需要把逻辑和数据层代码直接复制出去就完事了。

  • 比如你开始的代码是这样的:
const CountApp = () => {
  const [count, setCount] = useState(0)
  const decrement = () => setCount(count - 1)
  const increment = () => setCount(count + 1)

  return (
    <div>
      count: {count}
      <button onClick={increment}>自增</button>
      <button onClick={decrement}>自减</button>
    </div>
  )
}
  • 如果你想持久化数据,每次进来想恢复上一次的 count,把逻辑代码复制出来,用 createModel 包一层就完事了。
import { createModel } from 'hox';

/* 逻辑原样复制过来 */
function useCounter() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count - 1);
  const increment = () => setCount(count + 1);
  return {
    count,
    decrement,
    increment
  };
}
/* 用 createModel 包一下就行 */
export default createModel(useCounter)
import { useCounterModel } from "./useCounterModel";

export function CountApp() {
  const {count, increment, decrement} = useCounterModel();
  return (
    <div>
      count: {count}
      <button onClick={increment}>自增</button>
      <button onClick={decrement}>自减</button>
    </div>
  )
}

总结

讲完了,核心内容很短,因为足够简单,更多内容可以见 github。如果你觉得 redux、dva 等太难学习,使用繁琐,如果你觉得 unstated-next Provider 嵌套太多,太乱的话,不妨试试 hox,保证会给你全新的开发体验。

hox,下一代 React 状态管理器。

文末再打个广告:umi hooks 最好的 react hooks 逻辑库。

关于webpack-dev-server配置和热更新问题?

按照你的方式搭建到 webpack-dev-server这一步的时候。增加配置

  devServer: {
        contentBase: path.join(__dirname, './dist')
    } 

但目录结构是:

tim 20180117125043

根据这句 contentBase: path.join(__dirname, './dist')说明服务器根目录是子/dist/下面,但项目入口是在
/src/index.html。根本就没法访问啊。
tim 20180117125347

我把contentBase换成src·,可以实现访问,但不能热更新。
麻烦帮解答下

React Hooks 在 SSR 模式下常见问题及解决方案

服务端渲染(Server-Side Rendering),是指由服务侧完成页面的 HTML 结构拼接的页面处理技术。一般用于解决 SEO 问题和首屏加载速度问题。

由于 SSR 是在非浏览器环境执行 JS 代码,所以会出现很多问题。本文主要介绍 React Hooks 在 SSR 模式下常见问题及解决方案。

更多关于 SSR 的介绍可以看 UmiJS 的文档《服务端渲染(SSR)》。

问题一:DOM/BOM 缺失

SSR 是在 node 环境下运行 React 代码,而此时 window、document、navigator 等全局属性没有。如果直接使用了这些属性,就会报错 window is not defined, document is not defined, navigator is not defined 等。

常见的错误用法是在 Hooks 执行过程中,直接使用了 document 等全局属性。

import React, { useState } from 'react';

export default () => {
  const [state, setState] = useState(document.visibilityState);
  return state;
}

解决方案

  1. 将访问 DOM/BOM 的方法放在 useEffect/useLayoutEffect 中(服务端不会执行),避免服务端执行时报错,例如:
import React, { useState, useEffect } from 'react';

export default () => {
  const [state, setState] = useState();
  
  useEffect(()=>{
    setState(document.visibilityState);
  }, []);
  
  return state;
}
  1. 通过 isBrowser 来做环境判断
import React, { useState } from 'react';

function isBrowser() {
  return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}

export default () => {
  const [state, setState] = useState(isBrowser() && document.visibilityState);
  
  return state;
}

问题二 useLayoutEffect Warning

如果使用了 useLayoutEffect,在 SSR 模式下,会出现以下警告

⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.

解决方案

  1. 使用 useEffect 代替 useLayoutEffect(废话)
  2. 根据环境动态的指定是使用 useEffect 还是 useLayoutEffect。这是来自社区的一种 hack 解决方案,目前在 react-reduxreact-usereact-beautiful-dnd 均使用的这种方案。
import { useLayoutEffect, useEffect } from 'react';
const useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;

总结:写 Hooks 时需要注意

  1. 不要在非 useEffect/useLayoutEffect 中,直接使用 DOM/BOM 属性
  2. 在非 useEffect/useLayoutEffect 使用 DOM/BOM 属性时,使用 isBrowser 判断是否在浏览器环境执行
  3. 如果某个 Hooks 需要接收 DOM/BOM 属性,需要支持函数形式传参。以 ahooks 的 useEventListener 举例,必须支持函数形式来指定 target 属性。
import React, { useState } from 'react';
import { useEventListener } from 'ahooks';

export default () => {
  const [value, setValue] = useState(0);

  const clickHandler = () => {
    setValue(value + 1);
  };

  useEventListener(
    'click', 
    clickHandler, 
    { 
-       target: document.getElemenetById('click-btn') 
+       target: () => document.getElemenetById('click-btn') 
    }
  );

  return (
    <button id="click-btn" type="button">
      You click {value} times
    </button>
  );
};
  1. 使用 useIsomorphicLayoutEffect 来代替 useLayoutEffect

参考资料

❤️感谢阅读

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

Recoil - Facebook 官方 React 状态管理器

说到状态管理器,轮子满天飞。在 Class 时代,redux 与 mobx 几乎占据了全部市场,几乎没有没用过 redux 的同学。随着 Hooks 的诞生,新的一批轮子应运而生,其中有代表性的有 unstated-next、constate 等等。
当然无论什么轮子,要解决的问题都是一样的:**跨组件状态共享。**在解决这个核心问题的同时,需要尽可能的满足以下几个特性:

  • TypeScript 支持
  • 友好的异步支持
  • 支持状态互相依赖
  • 同时支持 Class 与 Hooks 组件
  • 使用简单

Recoil 体验

最近,facebook 官方出了一个状态管理器解决方案 Recoil,我们来体验一下。

准备工作

使用 Recoil,我们需要在项目最外层包一个 RecoilRoot ,这个和大部分状态管理器一致,通过 context 来跨组件传递数据。

import React from 'react';
import { RecoilRoot } from 'recoil';

function App() {
  return (
    <RecoilRoot>
        ...
    </RecoilRoot>
  );
}

跨组件状态共享

状态最简单的就是定义和使用。在 Recoil 中,通过 atom 来定义一个状态。

const inputValueState = atom({
  key: "inputValue",
  default: ""
});

如上面的代码所示,我们定义了一个 inputValue 状态,它的默认值是空字符串。
需要注意的是 key 字段,它应该是全局唯一的。这个 key 主要为了 debug 方便,持久化数据(数据恢复时的唯一标识),以及可以方便的看到全局 atoms 树。


消费状态也比较简单,通过 useRecoilState 来消费状态。

import React from "react";
import { useRecoilState } from "recoil";
import { inputValue } from "../store";

const InputA = () => {
  const [value, setValue] = useRecoilState(inputValueState);

  return <input value={value} onChange={e => setValue(e.target.value)} />;
};

export default InputA;

是不是很简单?Recoil 的基础用法就是这样的。我在这里写了一个 demo,你可以体验下。
2020-05-17 13.09.49.gif

状态互相依赖

有些状态需要依赖其它状态,这时候就要用 selector 来定义这个状态了。
比如,我们需要定义一个新的状态 filterdInputValue ,它是过滤 inputValue 中的数字后的值。

const filterdInputValue = selector({
  key: "filterdInputValue",
  get: ({get}) => {
    // 通过 get 可以读取其它状态
    const inputValue = get(inputValueState);
    return inputValue.replace(/[0-9]/ig, "");
  },
});

selector 比较简单,就是为了实现状态的依赖。你可以在这个 demo 体验下。
2020-05-17 13.29.55.gif

异步支持

良好的异步请求支持是状态管理器必不可少的。Recoil 提供了一个 useRecoilValueLoadable 来处理异步请求。直接上例子:

const currentUserNameQuery = selector({
  key: "CurrentUserName",
  get: async () => {
    const response = await queryUserInfo();
    return response.name;
  }
});

我们需要通过 selector 来定义异步状态,如果 get 函数是一个 Promise,则代表该状态为异步状态,需要使用 useRecoilValueLoadable 来消费该状态。

const UserName = () => {
  const userNameLoadable = useRecoilValueLoadable(currentUserNameQuery);
  switch (userNameLoadable.state) {
    case "hasValue":
      return <div>{userNameLoadable.contents}</div>;
    case "loading":
      return <div>Loading...</div>;
    case "hasError":
      throw userNameLoadable.contents;
  }
};

从上面例子可以看到, useRecoilValueLoadable 返回的状态,可以通过 state 字段读取到异步请求的状态。我写了个 demo,你可以体验下。
2020-05-17 15.34.53.gif


当然通过 useRecoilValueLoadable 来消费异步状态,比较符合我们当前的习惯。但 Recoil 更推荐通过 React.Suspense 来消费异步状态,这里就仁者见仁了,虽然 Suspense 可能是方向,但用起来是还不太习惯。

const UserName = () => {
  const userName = useRecoilValue(currentUserNameQuery);
  return <>{userName}</>
 }
};
function MyApp() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <UserName />
    </React.Suspense>
  );
}

评价

优点

  • 之前状态管理器满天飞,如果官方能一统天下,应该算一件好事情。
  • 对 React concurrent 模式支持良好。

不足

当前 Recoil 还处于开发阶段,文档都还不是很全。基于现状,说几点我的感受。

1. 没有使用 ts 实现,目前不支持 ts

这点我很惊讶,也是写这个文章的时候才发现的,很奇怪。讲道理 Recoil 支持 typescript 应该是顺手的事情,可能后期需要来个 @types/recoil 吧。

2. 目前没有支持 Class 组件消费状态。

这个特性应该是必备的,应该不会彻底抛弃 Class 组件。估计下个版本肯定会支持的这个特性的。实现成本较低,不支持的话就太反人类了。

3. API 偏多,有一定上手成本。

image.png
各类 API 一共有 19 个,偏复杂了。感觉很多都是可以合并的,比如 atom 和 selector 合并成一个等等(也可能是我考虑不成熟)。建议官方可以考虑精简精简,本来是一个很简单的东西,搞的太复杂了。

4. 消费较繁琐

我们需要消费一个状态的时候,需要 import 两个东西,比较繁琐。

import { useRecoilState } from "recoil";
import { inputValueState } from "../store";

// 用法
useRecoilState(inputValueState);

本来应该可以直接通过字符串 key 消费的,但这样和 redux 问题一样了,无法支持 ts。

import { useRecoilState } from "recoil";

useRecoilState('inputValueState');

无论如果,import 两个东西不是一个好的用法。

5. 没有足够的亮点

没有看到让人眼前一亮的东西,没有使用冲动。静观发展~

后记

Recoil 整体看下来,比较中庸,需要静观发展。
另外推荐一下我目前正在用的最简单的 React 状态管理器 hox,只有一个 API,非常符合直觉,没有任何上手成本,完全拥抱 Hooks 😋。

❤️感谢阅读

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

React Hooks 在 react-refresh 模块热替换(HMR)下的异常行为

什么是 react-refresh

react-refresh-webpack-plugin 是 React 官方提供的一个 模块热替换(HMR)插件。

A Webpack plugin to enable "Fast Refresh" (also previously known as Hot Reloading) for React components.

在开发环境编辑代码时,react-refresh 可以保持组件当前状态,仅仅变更编辑的部分。在 umi 中可以通过 fastRefresh: {}快速开启该功能。

fast-refresh.gif

这张 gif 动图展示的是使用 react-refresh 特性的开发体验,可以看出,修改组件代码后,已经填写的用户名和密码保持不变,仅仅只有编辑的部分变更了。

react-refresh 的简单原理

对于 Class 类组件,react-refresh 会一律重新刷新(remount),已有的 state 会被重置。而对于函数组件,react-refresh 则会保留已有的 state。所以 react-refresh 对函数类组件体验会更好。
本篇文章主要讲解 React Hooks 在 react-refresh 模式下的怪异行为,现在我来看下 react-refresh 对函数组件的工作机制。

  • 在热更新时为了保持状态,useStateuseRef 的值不会更新。
  • 在热更新时,为了解决某些问题useEffectuseCallbackuseMemo 等会重新执行。

When we update the code, we need to "clean up" the effects that hold onto past values (e.g. passed functions), and "setup" the new ones with updated values. Otherwise, the values used by your effect would be stale and "disagree" with value used in your rendering, which makes Fast Refresh much less useful and hurts the ability to have it work with chains of custom Hooks.

Kapture 2021-05-10 at 11.37.54.gif

如上图所示,在文本修改之后,state保持不变,useEffect被重新执行了。

react-refresh 工作机制导致的问题

在上述工作机制下,会带来很多问题,接下来我会举几个具体的例子。

第一个问题

import React, { useEffect, useState } from 'react';

export default () => {
  const [count, setState] = useState(0);

  useEffect(() => {
    setState(s => s + 1);
  }, []);

  return (
    <div>
      {count}
    </div>
  )
}

上面的代码很简单,在正常模式下,count值最大为 1。因为 useEffect 只会在初始化的时候执行一次。
但在 react-refresh 模式下,每次热更新的时候,state 不变,但 useEffect 重新执行,就会导致 count 的值一直在递增。

Kapture 2021-05-10 at 12.09.47.gif

如上图所示,count 随着每一次热更新在递增。

第二个问题

如果你使用了 ahooks 或者 react-useuseUpdateEffect,在热更新模式下也会有不符合预期的行为。

import React, { useEffect } from 'react';
import useUpdateEffect from './useUpdateEffect';

export default () => {

  useEffect(() => {
    console.log('执行了 useEffect');
  }, []);

  useUpdateEffect(() => {
    console.log('执行了 useUpdateEffect');
  }, []);

  return (
    <div>
      hello world
    </div>
  )
}

useUpdateEffectuseEffect相比,它会忽略第一次执行,只有在 deps 变化时才会执行。以上代码的在正常模式下,useUpdateEffect 是永远不会执行的,因为 deps 是空数组,永远不会变化。
但在 react-refresh 模式下,热更新时,useUpdateEffectuseEffect 同时执行了。

Kapture 2021-05-10 at 12.26.19.gif

造成这个问题的原因,就是 useUpdateEffectref 来记录了当前是不是第一次执行,见下面的代码。

import { useEffect, useRef } from 'react';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;

上面代码的关键在 isMounted

  • 初始化时,useEffect 执行,标记 isMountedtrue
  • 热更新后,useEffect 重新执行了,此时 isMountedtrue,就往下执行了

第三个问题

最初发现这个问题,是 ahooks 的 useRequest 在热更新后,loading 会一直为 true。经过分析,原因就是使用 isUnmount ref 来标记组件是否卸载。

import React, { useEffect, useState } from 'react';

function getUsername() {
  console.log('请求了')
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('test');
    }, 1000);
  });
}

export default function IndexPage() {

  const isUnmount = React.useRef(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    getUsername().then(() => {
      if (isUnmount.current === false) {
        setLoading(false);
      }
    });
    return () => {
      isUnmount.current = true;
    }
  }, []);

  return loading ? <div>loading</div> : <div>hello world</div>;
}

如上代码所示,在热更新时,isUnmount 变为了true,导致二次执行时,代码以为组件已经卸载了,不再响应异步操作。

如何解决这些问题

方案一

第一个解决方案是从代码层面解决,也就是要求我们在写代码的时候,时时能想起来 react-refresh 模式下的怪异行为。
比如 useUpdateEffect 我们就可以在初始化或者热替换时,将 isMounted ref 初始化掉。如下:

import { useEffect, useRef } from 'react';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

+  useEffect(() => {
+  	isMounted.current = false;
+  }, []);
  
  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;

这个方案对上面的问题二和三都是有效的。

方案二

根据官方文档,我们可以通过在文件中添加以下注释来解决这个问题。

/* @refresh reset */

添加这个问题后,每次热更新,都会 remount,也就是组件重新执行。useStateuseRef 也会重置掉,也就不会出现上面的问题了。

官方态度

本来 React Hooks 已经有蛮多潜规则了,在使用 react-refresh 时,还有潜规则要注意。但官方回复说这是预期行为,见该 issue

Effects are not exactly "mount"/"unmount" — they're more like "show"/"hide".

不管你晕没晕,反正我是晕了,╮(╯▽╰)╭。

❤️感谢阅读

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

React 项目性能分析及优化

招聘:《宇宙第三前端团队招人啦!

招聘:《宇宙第三前端团队招人啦!

招聘:《宇宙第三前端团队招人啦!

原创 for:前端技术砖家 公众号

性能优化不是一个简单的事情,但在 95% 以上的 React 项目中,是不需要考虑的,按自己的想法奔放的使用就可以了。

我认为性能优化最好的时候是项目启动时。在项目启动时,需要充分考虑页面的复杂度,如果非常复杂,则必须提前制定各种措施,防止出现性能问题。如果前期评估页面不复杂,那大概率不会出现什么性能问题。最惨的事情就是前期没有评估,中后期碰到了性能问题,解决起来就相当棘手了。

这篇文章会分享 React 项目常见的性能分析手段及优化手段,碰到性能问题的同学可以看看,没碰到性能问题的同学也需要提前预警了。

性能分析

Performance

说到性能分析,当然要有一些指标,来度量现在网页“卡”的程度,并指导我们持续改进。chrome 自带的 Performance,一般就足够我们进行分析了。

image.png

我写了一个简单的卡顿的例子,我们尝试通过 Performance 来分析出这个例子中哪一行代码卡。首先你可以打开这个示例页面,在这个页面的 input 框中输入的时候,你能明显感觉到非常卡顿。

2020-03-29 20.38.01.gif

从上面的动图可以看到,最后上面一栏出现很多红线,这就代表性能出问题了。

image.png

image.png

我们看下 Frames(帧) 这一栏,能看到红框中在一次输入中,776.9 ms 内都是 1 fps 的。这代表什么意思?我们知道正常网页刷新频率一般是 60 帧,也就是 16.67ms(1s/60)必须要刷新一次,否则就会有卡顿感,刷新时间越长,就越卡顿,在当前例子中,我们输入字符后,776.9 ms 后才触发更新,可以说是相当相当卡了。

我们知道 JS 是单线程的,也就是执行代码与绘制是同一个线程,必须等代码执行完,才能开始绘制。那具体是那一块代码执行时间长了呢?这里我们就要看 Main 这一栏,这一栏列出了 JS 调用栈。

image.png

在 Main 这一栏中,可以看到我们的 KeyPress 事件执行了 771.03ms,然后往上托动,就能看到 KeyPress 中 JS 的执行栈,能找到每个函数的执行时间。

image.png

拖动到最下面,你可以看到 onChange 函数执行了很长时间,点击它,你可以在下面看到这个函数的具体信息,点击 demo1.js:7 甚至能看到每一行执行了多长时间。

image.png

罪魁祸首找到了,第九行代码执行了 630ms,找到问题所在,就好解决了。

这是一个最简单的例子,这种由单个地方引起的性能问题,也是比较好解决的。找到它、修改它、解决它!

React Profiler

React.Profiler 是 React 提供的,分析组件渲染次数、开始时间及耗时的一个 API,你可以在官网找到它的文档

当然我们不需要每个组件都去加一个 React.Profiler 包裹,在开发环境下,React 会默记录每个组件的信息,我们可以通过 Chrome Profiler Tab 整体分析。

当然我们的 Chrome 需要安装 React 扩展,才能在工具栏中找到 Profiler 的 Tab。

image.png

Profiler 的用法和 Performance 用法差不多,点击开始记录,操作页面,然后停止记录,就会产出相关数据。

2020-03-29 22.18.35.gif

我找了一张比较复杂的图来做个示例,图中的数字分别表示:本次操作 React 做了 26 次 commit,第 14 次 commit 耗时最长,该次 commit 从 3.4s 时开始,消耗了 89.1 ms。

image.png

同时我们切换到 Ranked 模式,可以看到该次 commit,每个组件的耗时排名。比如下图表示 MarkdownText 组件耗时最长,达到 13.7 ms。

image.png

通过 React.Profiler,我们可以清晰的看到 React 组件的执行次数及时间,为我们优化性能指明了方向。

但我们需要注意的是,React.Profiler 记录的是 commit 阶段的数据。React 的执行分为两个阶段:

  • render 阶段:该阶段会确定例如 DOM 之类的数据需要做那些变化。在这个阶段,React 将会执行 render 及 render 之前的生命周期。
  • commit 阶段:该阶段 React 会提交更新,同时在这个阶段,React 会执行像 componentDidMountcomponentDidUpdate 之类的生命周期函数。

所以 React.Profiler 的分析范围是有限的,比如我们最开始的 input 示例,通过 React Profiler 是分析不出来性能问题的。

性能改进

如果所有的性能问题都像上面这么简单就好了。某个点耗时极长,找到它并改进之,皆大欢喜。但在 React 项目中,最容易出现的问题是组件太多,每个组件执行 1ms,一百个组件就执行了 100ms,怎么优化?没有任何一个突出的点可以攻克,我们也不可能把一百个组件都优化成 0.01 ms。

class App extend React.Component{
    constructor(props){
    super(props);
    this.state={
      count: 0
    }
  }
  render(){
    return (
      <div>
        <A />
        <B />
        <C />
        <D />
        <Button onClick={()=>{ this.setState({count: 1}) }}>click</Button>
      </div>
    )
  }
}

就像上面这个组件一样,当我们点击 Button 更新 state 时,A/B/C/D 四个组件均会执行一次 render 计算,这些计算完全是无用的。当我们组件够多时,会逐渐成为性能瓶颈!我们目标是减少不必要的 render。

PureComponent/ShouldComponentUpdate

说到避免 Render,当然第一时间想到的就是 ShouldComponentUpdate 这个生命周期,该生命周期通过判断 props 及 state 是否变化来手动控制是否需要执行 render。当然如果使用 PureComponent,组件会自动处理 ShouldComponentUpdate。

使用 PureComponent/ShouldComponentUpdate 时,需要注意几点:

  1. PureComponent 会对 props 与 state 做浅比较,所以一定要保证 props 与 state 中的数据是 immutable 的。
  2. 如果你的数据不是 immutable 的,或许你可以自己手动通过 ShouldComponentUpdate 来进行深比较。当然深比较的性能一般都不好,不到万不得已,最好不要这样搞。

React.memo

React.memo 与 PureComponent 一样,但它是为函数组件服务的。React.memo 会对 props 进行浅比较,如果一致,则不会再执行了。

const App = React.memo(()=>{
  return <div></div>
});

当然,如果你的数据不是 immutable 的,你可以通过 React.memo 的第二个参数来手动进行深比较,同样极其不推荐。

React.memo 对 props 的变化做了优化,避免了无用的 render。那 state 要怎么控制呢?

const [state, setState] = useState(0);

React 函数组件的 useState,其 setState 会自动做浅比较,也就是如果你在上面例子中调用了 setState(0) ,函数组件会忽略这次更新,并不会执行 render 的。一般在使用的时候要注意这一点,经常有同学掉进这个坑里面。

善用 React.useMemo

React.useMemo 是 React 内置 Hooks 之一,主要为了解决函数组件在频繁 render 时,无差别频繁触发无用的昂贵计算 ,一般会作为性能优化的手段之一。

const App = (props)=>{
  const [boolean, setBoolean] = useState(false);
  const [start, setStart] = useState(0);
  
  // 这是一个非常耗时的计算
  const result = computeExpensiveFunc(start);
}

在上面例子中, computeExpensiveFunc 是一个非常耗时的计算,但是当我们触发 setBoolean 时,组件会重新渲染, computeExpensiveFunc 会执行一次。这次执行是毫无意义的,因为 computeExpensiveFunc 的结果只与 start 有关系。

React.useMemo 就是为了解决这个问题诞生的,它可以指定只有当 start 变化时,才允许重新计算新的 result

const result = useMemo(()=>computeExpensiveFunc(start), [start]);

我建议 React.useMemo 要多用,能用就用,避免性能浪费。

合理使用 React.useCallback

在函数组件中,React.useCallback 也是性能优化的手段之一。

const OtherComponent = React.memo(()=>{
    ...
});
  
const App = (props)=>{
  const [boolan, setBoolean] = useState(false);
  const [value, setValue] = useState(0);
 
  const onChange = (v)=>{
      axios.post(`/api?v=${v}&state=${state}`)
  }
 
  return (
    <div>
        {/* OtherComponent 是一个非常昂贵的组件 */}
        <OtherComponent onChange={onChange}/>
    </div>
  )
}

在上面的例子中, OtherComponent 是一个非常昂贵的组件,我们要避免无用的 render。虽然 OtherComponent 已经用 React.memo 包裹起来了,但在父组件每次触发 setBoolean 时, OtherComponent 仍会频繁 render。

因为父级组件 onChange 函数在每一次 render 时,都是新生成的,导致子组件浅比较失效。通过 React.useCallback,我们可以让 onChange 只有在 state 变化时,才重新生成。

const onChange = React.useCallback((v)=>{
  axios.post(`/api?v=${v}&state=${state}`)
}, [state])

通过 useCallback 包裹后, boolean 的变化不会触发 OtherComponent ,只有 state 变化时,才会触发,可以避免很多无用的 OtherComponent 执行。

但是仔细想想, state 变化其实也是没有必要触发 OtherComponent 的,我们只要保证 onChange 一定能访问到最新的 state ,就可以避免 state 变化时,触发 OtherComponent 的 render。

const onChange = usePersistFn((v)=>{
  axios.post(`/api?v=${v}&state=${state}`)
})

上面的例子,我们使用了 Umi Hooks 的 usePersistFn,它可以保证函数地址永远不会变化,无论何时, onChange 地址都不会变化,也就是无论何时, OtherComponent 都不会重新 render 了。

谨慎使用 Context

Context 是跨组件传值的一种方案,但我们需要知道,我们无法阻止 Context 触发的 render。

不像 props 和 state,React 提供了 API 进行浅比较,避免无用的 render,Context 完全没有任何方案可以避免无用的渲染。

有几点关于 Context 的建议:

  • Context 只放置必要的,关键的,被大多数组件所共享的状态。
  • 对非常昂贵的组件,建议在父级获取 Context 数据,通过 props 传递进来。

小心使用 Redux

Redux 中的一些细节,稍不注意,就会触发无用的 render,或者其它的坑。

精细化依赖

const App = (props)=>{
  return (
    <div>
        {props.project.id}
    </div>
  )
}
export default connect((state)=>{
  layout: state.layout,
  project: state.project,
  user: state.user
})(App);

在上面的例子中,App 组件显示声明依赖了 redux 的 layoutprojectuser 数据,在这三个数据变化时,都会触发 App 重新 render。

但是 App 只需要监听 project.id 的变化,所以精细化依赖可以避免无效的 render,是一种有效的优化手段。

const App = (props)=>{
  return (
    <div>
        {props.projectId}
    </div>
  )
}
export default connect((state)=>{
  projectId: state.project.id,
})(App);

不可变数据

我们经常会不小心直接操作 redux 源数据,导致意料之外的 BUG。

我们知道,JS 中的 数组/对象 是地址引用的。在下面的例子中,我们直接操作数组,并不会改变数据的地址。

const list = ['1'];
const oldList = list;
list.push('a');

list === oldList; //true

在 Redux 中,就经常犯这样的错误。下面的例子,当触发 PUSH 后,直接修改了 state.list ,导致 state.list 的地址并没有变化。

let initState = {
  list: ['1']
}

function counterReducer(state, action) {
  switch (action.type) {
    case 'PUSH':
      state.list.push('2');
      return {
        list: state.list
      }
    default:    
      return state;
  }
}

如果组件中使用了 ShouldComponentUpdate 或者 React.memo ,浅比较 props.list === nextProps.list ,会阻止组件更新,导致意料之外的 BUG。

所以如果大量使用了 ShouldComponentUpdateReact.meo ,则一定要保证依赖数据的不可变性!建议使用 immer.js 来操作复杂数据。

总结

在项目初期,一定要考虑项目的复杂度,及早采取有效的措施,防止产生性能问题。如果在中后期才考虑性能问题,则难度会增加数十倍不止。

❤️感谢大家

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

两处疑问

src/redux/reducers/counter.js

import {INCREMENT, DECREMENT, RESET} from '../actions/counter';

const initState = {
    count: 0
};

export default function reducer(state = initState, action) {
    switch (action.type) {
        case INCREMENT:
            return {
                count: state.count + 1
            };
        case DECREMENT:
            return {
                count: state.count - 1
            };
        case RESET:
            return {count: 0}; //这里是不是应该是 return 0
        default:
            return state
    }
}

如果reset返回的不是0而是一个对象,console的结果会是
{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: { count: 0 } } }

webpack testRedux.js build.js
这里我这么写会报错,最后用的是 webpack testRedux.js -o build.js
是不是因为我用的webpack版本是4.16.5?所以不一样

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.