蓝岚爱肉肉

蓝岚爱肉肉 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 hahah.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

蓝岚爱肉肉 赞了文章 · 1月7日

Webpack5 搭建 React 多页面应用

介绍

react-multi-page-app 是一个基于 webpack5 搭建的 react 多页面应用。

为什么搭建多页面应用:

  • 多个页面之间业务互不关联,页面之间并没有共享的数据
  • 多个页面使用同一个后端服务、使用通用的组件和基础库

搭建多页面应用的好处:

  • 保留了传统单页应用的开发模式:支持模块化打包,你可以把每个页面看成是一个单独的单页应用
  • 独立部署:每个页面相互独立,可以单独部署,解耦项目的复杂性,甚至可以在不同的页面选择不同的技术栈
  • 减少包的体积,优化加载渲染流程

快速上手

clone

git clone https://github.com/zhedh/react-multi-page-app.git

安装依赖

yarn install

开发

yarn start
http://localhost:8000/page1

打包

yarn build

简易搭建流程

npm 初始化

yarn init

约定目录

|____README.md
|____package.json
|____src
| |____utils
| |____components
| |____pages
| | |____page2
| | | |____index.css
| | | |____index.jsx
| | |____page1
| | | |____index.css
| | | |____index.jsx

Webpack 配置

安装 Webpack

yarn add -D 可以使用 npm i --save-dev 替代

yarn add -D webpack webpack-cli

创建配置文件

touch webpack.config.js

入口配置

module.exports = {
  entry: {
    page1: "./src/pages/page1/index.jsx",
    page2: "./src/pages/page2/index.jsx",
    // ...
  },
};

输出配置

module.exports = {
  entry: {
    page1: "./src/pages/page1/index.jsx",
    page2: "./src/pages/page2/index.jsx",
    // ...
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "[name]/index.js",
  },
};

js|jsx 编译

安装 babel 插件

yarn add -D babel-loader @babel/core @babel/preset-env
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
}

css 编译

安装 loader

yarn add -D style-loader css-loader
module.exports = {
  ...
  module: {
    ...
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader',
          'css-loader'
        ],
      },
    ]
  },
}

html 插件配置

安装 html-webpack-plugin

yarn add -D html-webpack-plugin
module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'page1/index.html',
      chunks: ['page1']
    }),
    new HtmlWebpackPlugin({
      filename: 'page2/index.html',
      chunks: ['page2']
    }),
  ],
}

页面编辑

page1

index.jsx

import "./index.css";

document.querySelector("body").append("PAGE1");

index.css

body {
  color: blue;
}

page2

index.jsx

import "./index.css";

document.querySelector("body").append("PAGE2");

index.css

body {
  color: green;
}

打包

执行

webpack

输出 dist 包产物如下:

├── page1
│   ├── index.html
│   └── index.js
└── page2
    ├── index.html
    └── index.js

完整的配置文件

webpack.config.js

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

module.exports = {
  entry: {
    page1: "./src/pages/page1/index.jsx",
    page2: "./src/pages/page2/index.jsx",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "[name]/index.js",
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.m?jsx$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: "page1/index.html",
      chunks: ["page1"],
      // chunks: ['page1', 'page1/index.css']
    }),
    new HtmlWebpackPlugin({
      filename: "page2/index.html",
      chunks: ["page2"],
    }),
  ],
};

package.json

{
  "name": "react-multi-page-app",
  "version": "1.0.0",
  "description": "React 多页面应用",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@babel/preset-env": "^7.12.7",
    "babel-loader": "^8.2.2",
    "css-loader": "^5.0.1",
    "html-webpack-plugin": "^4.5.0",
    "style-loader": "^2.0.0",
    "webpack": "^5.9.0",
    "webpack-cli": "^4.2.0"
  }
}

去 github 查看简易版完整代码react-multi-page-app

流程优化

分离开发生产环境

新建 config 目录,并创建配置文件

mkdir config
cd config
touch webpack.base.js
touch webpack.dev.js
touch webpack.prod.js
├── webpack.base.js
├── webpack.dev.js
└── webpack.prod.js

基础配置

webpack.base.js

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

module.exports = {
  entry: {
    page1: "./src/pages/page1/index.jsx",
    page2: "./src/pages/page2/index.jsx",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "[name]/index.js",
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: "page1/index.html",
      chunks: ["page1"],
    }),
    new HtmlWebpackPlugin({
      filename: "page2/index.html",
      chunks: ["page2"],
    }),
  ],
};

开发配置

  • 安装 webpack-merge,用于合并 webpack 配置信息
yarn add -D webpack-merge
  • 安装 webpack-dev-server,用于启动开发服务
yarn add -D webpack-dev-server
  • 开发配置如下

webpack.dev.js

const { merge } = require("webpack-merge");
const path = require("path");
const base = require("./webpack.base");

module.exports = merge(base, {
  mode: "development",
  devtool: "inline-source-map",
  target: "web",
  devServer: {
    open: true,
    contentBase: path.join(__dirname, "./dist"),
    historyApiFallback: true, //不跳转
    inline: true, //实时刷新
    hot: true, // 开启热更新,
    port: 8000,
  },
});
  • 配置启动命令

package.json

{
  "scripts": {
    "start": "webpack serve --mode development --env development --config config/webpack.dev.js"
  },
}
  • 启动
yarn start
  • 预览
http://localhost:8000/page1
http://localhost:8000/page2

生产配置

  • 配置如下

webpack.prod.js

const { merge } = require('webpack-merge')
const base = require('./webpack.base')

module.exports = merge(base, {
    mode: 'production',
})
  • 配置打包命令

package.json

{
  "scripts": {
    "start": "webpack serve --mode development --env development --config config/webpack.dev.js",
    "build": "webpack --config config/webpack.prod.js"
  },
}
  • 打包
yarn build

引入React

以page1页面为例

  • 约定目录
├── page1
│   ├── app.jsx
│   ├── index.jsx
│   └── index.css
└── page2
    ├── app.js
    ├── index.jsx
    └── index.css
  • 安装react
yarn add react react-dom
  • 代码如下

app.js

import React from 'react'

function App() {
  return (
    <div id="page1">
      我是PAGE1,Hello World
    </div>
  )
}

export default App

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
import './index.css'

ReactDOM.render(<App />, document.getElementById('root'))

index.css

body{
  background-color: #ccc;
}

#page1 {
  color: rebeccapurple;
}
  • 添加react编译

webpack.base.js

module.exports = {
  module: {
    // ...
    rules: [
      // ...
      {
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
            plugins: ['@babel/plugin-proposal-class-properties']
          }
        }
      }
    ]
  },
  // ...
}
  • 省略获取文件后缀

webpack.base.js

module.exports = {
  // ...
  resolve: {
    extensions: ['.js', '.jsx', '.json']
  }
}
  • 添加html默认模版,挂载 React DOM
cd src
touch template.html

template.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

webpack.base.js

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'page1/index.html',
      chunks: ['page1'],
      template: './src/template.html'
    }),
    new HtmlWebpackPlugin({
      filename: 'page2/index.html',
      chunks: ['page2'],
      template: './src/template.html'
    }),
  ],
}
  • 安装依赖
yarn add @babel/preset-react @babel/plugin-proposal-class-properties

引入Sass

以 page1 为例

  • 将 index.css 变更为 index.scss

index.scss

body {
  background-color: #ccc;

  #page1 {
    color: rebeccapurple;
  }
}
  • 添加scss编译

webpack.base.js

module.exports = {
  // ...
  module: {
    // ...
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          'style-loader',
          'css-loader',
          'resolve-url-loader',
          'sass-loader'
        ]
      },
    ]
  },
  // ...
}
  • 安装依赖
yarn add -D resolve-url-loader sass-loader

到此,一个完整的 React 多页面应用搭建完成,查看完整代码react-multi-page-app

入口配置和模版自动匹配

为了不用每次新增页面都要新增入口页面配置,我们将入口配置改成自动匹配

  • 入口文件自动匹配
cd config
touch webpack.util.js

webpack.util.js

const glob = require('glob')

function setEntry() {
  const files = glob.sync('./src/pages/**/index.jsx')
  const entry = {}
  files.forEach(file => {
    const ret = file.match(/^\.\/src\/pages\/(\S*)\/index\.jsx$/)
    if (ret) {
      entry[ret[1]] = {
        import: file,
      }
    }
  })

  return entry
}

module.exports = {
  setEntry,
}

webpack.base.js

const { setEntry } = require('./webpack.util')

module.exports = {
  entry: setEntry,
}
  • 拆分 React 依赖,将 React 单独打包出一个 bundle,作为公共依赖引入各个页面

webpack.util.js

function setEntry() {
  const files = glob.sync('./src/pages/**/index.jsx')
  const entry = {}
  files.forEach(file => {
    const ret = file.match(/^\.\/src\/pages\/(\S*)\/index\.jsx$/)
    if (ret) {
      entry[ret[1]] = {
        import: file,
        dependOn: 'react_vendors',
      }
    }
  })

  // 拆分react依赖
  entry['react_vendors'] = {
    import: ['react', 'react-dom'],
    filename: '_commom/[name].js'
  }

  return entry
}
  • html 模版自动匹配,并引入react bundle

以 page1 为例子,引入页面自定义模版目录约定如下

├── app.jsx
├── index.html
├── index.jsx
└── index.scss

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>页面1</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

如果匹配不到自定义模版,会匹配默认模版,配置如下:

webpack.util.js

function getTemplate(name) {
  const files = glob.sync(`./src/pages/${name}/index.html`)
  if (files.length > 0) {
    return files[0]
  }
  return './src/template.html'
}

function setHtmlPlugin() {
  const files = glob.sync('./src/pages/**/index.jsx')
  const options = []
  files.forEach(file => {
    const ret = file.match(/^\.\/src\/pages\/(\S*)\/index\.jsx$/)
    if (ret) {
      const name = ret[1]
      options.push(new HtmlWebpackPlugin({
        filename: `${name}/index.html`,
        template: getTemplate(name),
        chunks: ['react_vendors', name,]
      }))
    }
  })
  return options
}

module.exports = {
  setEntry,
  setHtmlPlugin
}

webpack.base.js

const { setEntry, setHtmlPlugin } = require('./webpack.util')

module.exports = {
  plugins: [
    ...setHtmlPlugin(),
  ]
}
  • 安装相关依赖
yarn add -D html-webpack-plugin glob

配置优化

  • 清除之前打包文件

webpack.base.js

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

module.exports = {
  plugins: [
    new CleanWebpackPlugin(),
  ]
}
yarn add -D clean-webpack-plugin
  • 分离并压缩 css

webpack.base.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
   module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        use: [
          // 'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader',
          'resolve-url-loader',
          'sass-loader'
        ]
      },
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]/index.css',
    }),
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    })
  ]
}

html 中注入 css
webpack.util.js

function setHtmlPlugin() {
  const files = glob.sync('./src/pages/**/index.jsx')
  const options = []
  files.forEach(file => {
    const ret = file.match(/^\.\/src\/pages\/(\S*)\/index\.jsx$/)
    if (ret) {
      const name = ret[1]
      options.push(new HtmlWebpackPlugin({
        filename: `${name}/index.html`,
        template: getTemplate(name),
        chunks: ['react_vendors', name, '[name]/index.css']
      }))
    }
  })
  return options
}
yarn add -D mini-css-extract-plugin optimize-css-assets-webpack-plugin

在 package.json 配置 sideEffects,避免 webpack Tree Shaking 移除.css、.scss 文件
package.json

​```json
{
  "sideEffects": [
    "*.css",
    "*.scss"
  ]
}

至此,项目配置完成

项目源码

完整代码react-multi-page-app,喜欢给个star

问题&解答

Cannot read property 'createSnapshot' of undefined

报错:UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'createSnapshot' of undefined

原因:因为同时运行 2 个不同版本的 webpack。我们项目中没有安装 webpack-cli,webpack 会默认使用全局的 webpack-cli,webpack5 和 webpack-cli3 不兼容

解决:升级全局 webpack-cli3 到 webpack-cli4 或在项目中安装最新版本的 webpack-cli4

参考:https://github.com/jantimon/h...

查看原文

赞 17 收藏 11 评论 4

蓝岚爱肉肉 收藏了文章 · 2019-04-10

【React深入】从Mixin到HOC再到Hook

导读

前端发展速度非常之快,页面和组件变得越来越复杂,如何更好的实现状态逻辑复用一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。

本文介绍了React采用的三种实现状态逻辑复用的技术,并分析了他们的实现原理、使用方法、实际应用以及如何选择使用他们。

本文略长,下面是本文的思维导图,您可以从头开始阅读,也可以选择感兴趣的部分阅读:

image

Mixin设计模式

image

Mixin(混入)是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上面去,不过你可以拷贝任意多个对象的任意个方法到一个新对象上去,这是继承所不能实现的。它的出现主要就是为了解决代码复用问题。

很多开源库提供了Mixin的实现,如Underscore_.extend方法、JQueryextend方法。

使用_.extend方法实现代码复用:

var LogMixin = {
  actionLog: function() {
    console.log('action...');
  },
  requestLog: function() {
    console.log('request...');
  },
};
function User() {  /*..*/  }
function Goods() {  /*..*/ }
_.extend(User.prototype, LogMixin);
_.extend(Goods.prototype, LogMixin);
var user = new User();
var good = new Goods();
user.actionLog();
good.requestLog();

我们可以尝试手动写一个简单的Mixin方法:

function setMixin(target, mixin) {
  if (arguments[2]) {
    for (var i = 2, len = arguments.length; i < len; i++) {
      target.prototype[arguments[i]] = mixin.prototype[arguments[i]];
    }
  }
  else {
    for (var methodName in mixin.prototype) {
      if (!Object.hasOwnProperty(target.prototype, methodName)) {
        target.prototype[methodName] = mixin.prototype[methodName];
      }
    }
  }
}
setMixin(User,LogMixin,'actionLog');
setMixin(Goods,LogMixin,'requestLog');

您可以使用setMixin方法将任意对象的任意方法扩展到目标对象上。

React中应用Mixin

React也提供了Mixin的实现,如果完全不同的组件有相似的功能,我们可以引入来实现代码复用,当然只有在使用createClass来创建React组件时才可以使用,因为在React组件的es6写法中它已经被废弃掉了。

例如下面的例子,很多组件或页面都需要记录用户行为,性能指标等。如果我们在每个组件都引入写日志的逻辑,会产生大量重复代码,通过Mixin我们可以解决这一问题:

var LogMixin = {
  log: function() {
    console.log('log');
  },
  componentDidMount: function() {
    console.log('in');
  },
  componentWillUnmount: function() {
    console.log('out');
  }
};

var User = React.createClass({
  mixins: [LogMixin],
  render: function() {
    return (<div>...</div>)
  }
});

var Goods = React.createClass({
  mixins: [LogMixin],
  render: function() {
    return (<div>...</div>)
  }
});

Mixin带来的危害

React官方文档在Mixins Considered Harmful一文中提到了Mixin带来了危害:

  • Mixin 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的 Mixin 中的方法可能会相互冲突
  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

React现在已经不再推荐使用Mixin来解决代码复用问题,因为Mixin带来的危害比他产生的价值还要巨大,并且React全面推荐使用高阶组件来替代它。另外,高阶组件还能实现更多其他更强大的功能,在学习高阶组件之前,我们先来看一个设计模式。

装饰模式

image

装饰者(decorator)模式能够在不改变对象自身的基础上,在程序运行期间给对像动态的添加职责。与继承相比,装饰者是一种更轻便灵活的做法。

高阶组件(HOC)

image

高阶组件可以看作React对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

高阶组件(HOC)是React中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。
function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, ...props } = this.props;
      if (visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  }
}

上面的代码就是一个HOC的简单应用,函数接收一个组件作为参数,并返回一个新组件,新组建可以接收一个visible props,根据visible的值来判断是否渲染Visible。

下面我们从以下几方面来具体探索HOC

image

HOC的实现方式

属性代理

函数返回一个我们自己定义的组件,然后在render中返回要包裹的组件,这样我们就可以代理所有传入的props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible就是一个HOC属性代理的实现方式。

function proxyHOC(WrappedComponent) {
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}

对比原生组件增强的项:

  • 可操作所有传入的props
  • 可操作组件的生命周期
  • 可操作组件的static方法
  • 获取refs

反向继承

返回一个组件,继承原组件,在render中调用原组件的render。由于继承了原组件,能通过this访问到原组件的生命周期、props、state、render等,相比属性代理它能操作更多的属性。

function inheritHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      return super.render();
    }
  }
}

对比原生组件增强的项:

  • 可操作所有传入的props
  • 可操作组件的生命周期
  • 可操作组件的static方法
  • 获取refs
  • 可操作state
  • 可以渲染劫持

HOC可以实现什么功能

组合渲染

可使用任何其他组件和原组件进行组合渲染,达到样式、布局复用等效果。

通过属性代理实现
function stylHOC(WrappedComponent) {
  return class extends Component {
    render() {
      return (<div>
        <div className="title">{this.props.title}</div>
        <WrappedComponent {...this.props} />
      </div>);
    }
  }
}
通过反向继承实现
function styleHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      return <div>
        <div className="title">{this.props.title}</div>
        {super.render()}
      </div>
    }
  }
}

条件渲染

根据特定的属性决定原组件是否渲染

通过属性代理实现
function visibleHOC(WrappedComponent) {
  return class extends Component {
    render() {
      if (this.props.visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  }
}
通过反向继承实现
function visibleHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      if (this.props.visible === false) {
        return null
      } else {
        return super.render()
      }
    }
  }
}

操作props

可以对传入组件的props进行增加、修改、删除或者根据特定的props进行特殊的操作。

通过属性代理实现
function proxyHOC(WrappedComponent) {
  return class extends Component {
    render() {
      const newProps = {
        ...this.props,
        user: 'ConardLi'
      }
      return <WrappedComponent {...newProps} />;
    }
  }
}

获取refs

高阶组件中可获取原组件的ref,通过ref获取组件实力,如下面的代码,当程序初始化完成后调用原组件的log方法。(不知道refs怎么用,请👇Refs & DOM)

通过属性代理实现
function refHOC(WrappedComponent) {
  return class extends Component {
    componentDidMount() {
      this.wapperRef.log()
    }
    render() {
      return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />;
    }
  }
}

这里注意:调用高阶组件的时候并不能获取到原组件的真实ref,需要手动进行传递,具体请看传递refs

状态管理

将原组件的状态提取到HOC中进行管理,如下面的代码,我们将Inputvalue提取到HOC中进行管理,使它变成受控组件,同时不影响它使用onChange方法进行一些其他操作。基于这种方式,我们可以实现一个简单的双向绑定,具体请看双向绑定

通过属性代理实现
function proxyHoc(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props);
      this.state = { value: '' };
    }

    onChange = (event) => {
      const { onChange } = this.props;
      this.setState({
        value: event.target.value,
      }, () => {
        if(typeof onChange ==='function'){
          onChange(event);
        }
      })
    }

    render() {
      const newProps = {
        value: this.state.value,
        onChange: this.onChange,
      }
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  }
}

class HOC extends Component {
  render() {
    return <input {...this.props}></input>
  }
}

export default proxyHoc(HOC);

操作state

上面的例子通过属性代理利用HOC的state对原组件进行了一定的增强,但并不能直接控制原组件的state,而通过反向继承,我们可以直接操作原组件的state。但是并不推荐直接修改或添加原组件的state,因为这样有可能和组件内部的操作构成冲突。

通过反向继承实现
function debugHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      console.log('props', this.props);
      console.log('state', this.state);
      return (
        <div className="debuging">
          {super.render()}
        </div>
      )
    }
  }
}

上面的HOCrender中将propsstate打印出来,可以用作调试阶段,当然你可以在里面写更多的调试代码。想象一下,只需要在我们想要调试的组件上加上@debug就可以对该组件进行调试,而不需要在每次调试的时候写很多冗余代码。(如果你还不知道怎么使用HOC,请👇如何使用HOC)

渲染劫持

高阶组件可以在render函数中做非常多的操作,从而控制原组件的渲染输出。只要改变了原组件的渲染,我们都将它称之为一种渲染劫持

实际上,上面的组合渲染条件渲染都是渲染劫持的一种,通过反向继承,不仅可以实现以上两点,还可直接增强由原组件render函数产生的React元素

通过反向继承实现
function hijackHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      const tree = super.render();
      let newProps = {};
      if (tree && tree.type === 'input') {
        newProps = { value: '渲染被劫持了' };
      }
      const props = Object.assign({}, tree.props, newProps);
      const newTree = React.cloneElement(tree, props, tree.props.children);
      return newTree;
    }
  }
}

注意上面的说明我用的是增强而不是更改render函数内实际上是调用React.creatElement产生的React元素

image
虽然我们能拿到它,但是我们不能直接修改它里面的属性,我们通过getOwnPropertyDescriptors函数来打印下它的配置项:

image

可以发现,所有的writable属性均被配置为了false,即所有属性是不可变的。(对这些配置项有疑问,请👇defineProperty

不能直接修改,我们可以借助cloneElement方法来在原组件的基础上增强一个新组件:

React.cloneElement()克隆并返回一个新的React元素,使用 element 作为起点。生成的元素将会拥有原始元素props与新props的浅合并。新的子级会替换现有的子级。来自原始元素的 key 和 ref 将会保留。

React.cloneElement() 几乎相当于:

<element.type {...element.props} {...props}>{children}</element.type>

如何使用HOC

上面的示例代码都写的是如何声明一个HOCHOC实际上是一个函数,所以我们将要增强的组件作为参数调用HOC函数,得到增强后的组件。

class myComponent extends Component {
  render() {
    return (<span>原组件</span>)
  }
}
export default inheritHOC(myComponent);

compose

在实际应用中,一个组件可能被多个HOC增强,我们使用的是被所有的HOC增强后的组件,借用一张装饰模式的图来说明,可能更容易理解:

image

假设现在我们有loggervisiblestyle等多个HOC,现在要同时增强一个Input组件:

logger(visible(style(Input)))

这种代码非常的难以阅读,我们可以手动封装一个简单的函数组合工具,将写法改写如下:

const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
compose(logger,visible,style)(Input);

compose函数返回一个所有函数组合后的函数,compose(f, g, h)(...args) => f(g(h(...args)))是一样的。

很多第三方库都提供了类似compose的函数,例如lodash.flowRightRedux提供的combineReducers函数等。

Decorators

我们还可以借助ES7为我们提供的Decorators来让我们的写法变的更加优雅:

@logger
@visible
@style
class Input extends Component {
  // ...
}

DecoratorsES7的一个提案,还没有被标准化,但目前Babel转码器已经支持,我们需要提前配置babel-plugin-transform-decorators-legacy

"plugins": ["transform-decorators-legacy"]

还可以结合上面的compose函数使用:

const hoc = compose(logger, visible, style);
@hoc
class Input extends Component {
  // ...
}

HOC的实际应用

下面是一些我在生产环境中实际对HOC的实际应用场景,由于文章篇幅原因,代码经过很多简化,如有问题欢迎在评论区指出:

日志打点

实际上这属于一类最常见的应用,多个组件拥有类似的逻辑,我们要对重复的逻辑进行复用,
官方文档中CommentList的示例也是解决了代码复用问题,写的很详细,有兴趣可以👇使用高阶组件(HOC)解决横切关注点

某些页面需要记录用户行为,性能指标等等,通过高阶组件做这些事情可以省去很多重复代码。

function logHoc(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      this.start = Date.now();
    }
    componentDidMount() {
      this.end = Date.now();
      console.log(`${WrappedComponent.dispalyName} 渲染时间:${this.end - this.start} ms`);
      console.log(`${user}进入${WrappedComponent.dispalyName}`);
    }
    componentWillUnmount() {
      console.log(`${user}退出${WrappedComponent.dispalyName}`);
    }
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

可用、权限控制

function auth(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, auth, display = null, ...props } = this.props;
      if (visible === false || (auth && authList.indexOf(auth) === -1)) {
        return display
      }
      return <WrappedComponent {...props} />;
    }
  }
}

authList是我们在进入程序时向后端请求的所有权限列表,当组件所需要的权限不列表中,或者设置的
visiblefalse,我们将其显示为传入的组件样式,或者null。我们可以将任何需要进行权限校验的组件应用HOC

  @auth
  class Input extends Component {  ...  }
  @auth
  class Button extends Component {  ...  }

  <Button auth="user/addUser">添加用户</Button>
  <Input auth="user/search" visible={false} >添加用户</Input>

双向绑定

vue中,绑定一个变量后可实现双向数据绑定,即表单中的值改变后绑定的变量也会自动改变。而React中没有做这样的处理,在默认情况下,表单元素都是非受控组件。给表单元素绑定一个状态后,往往需要手动书写onChange方法来将其改写为受控组件,在表单元素非常多的情况下这些重复操作是非常痛苦的。

我们可以借助高阶组件来实现一个简单的双向绑定,代码略长,可以结合下面的思维导图进行理解。

image

首先我们自定义一个Form组件,该组件用于包裹所有需要包裹的表单组件,通过contex向子组件暴露两个属性:

  • model:当前Form管控的所有数据,由表单namevalue组成,如{name:'ConardLi',pwd:'123'}model可由外部传入,也可自行管控。
  • changeModel:改变model中某个name的值。

class Form extends Component {
  static childContextTypes = {
    model: PropTypes.object,
    changeModel: PropTypes.func
  }
  constructor(props, context) {
    super(props, context);
    this.state = {
      model: props.model || {}
    };
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.model) {
      this.setState({
        model: nextProps.model
      })
    }
  }
  changeModel = (name, value) => {
    this.setState({
      model: { ...this.state.model, [name]: value }
    })
  }
  getChildContext() {
    return {
      changeModel: this.changeModel,
      model: this.props.model || this.state.model
    };
  }
  onSubmit = () => {
    console.log(this.state.model);
  }
  render() {
    return <div>
      {this.props.children}
      <button onClick={this.onSubmit}>提交</button>
    </div>
  }
}

下面定义用于双向绑定的HOC,其代理了表单的onChange属性和value属性:

  • 发生onChange事件时调用上层FormchangeModel方法来改变context中的model
  • 在渲染时将value改为从context中取出的值。
function proxyHoc(WrappedComponent) {
  return class extends Component {
    static contextTypes = {
      model: PropTypes.object,
      changeModel: PropTypes.func
    }

    onChange = (event) => {
      const { changeModel } = this.context;
      const { onChange } = this.props;
      const { v_model } = this.props;
      changeModel(v_model, event.target.value);
      if(typeof onChange === 'function'){onChange(event);}
    }

    render() {
      const { model } = this.context;
      const { v_model } = this.props;
      return <WrappedComponent
        {...this.props}
        value={model[v_model]}
        onChange={this.onChange}
      />;
    }
  }
}
@proxyHoc
class Input extends Component {
  render() {
    return <input {...this.props}></input>
  }
}

上面的代码只是简略的一部分,除了input,我们还可以将HOC应用在select等其他表单组件,甚至还可以将上面的HOC兼容到span、table等展示组件,这样做可以大大简化代码,让我们省去了很多状态管理的工作,使用如下:

export default class extends Component {
  render() {
    return (
      <Form >
        <Input v_model="name"></Input>
        <Input v_model="pwd"></Input>
      </Form>
    )
  }
}

表单校验

基于上面的双向绑定的例子,我们再来一个表单验证器,表单验证器可以包含验证函数以及提示信息,当验证不通过时,展示错误信息:

function validateHoc(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props);
      this.state = { error: '' }
    }
    onChange = (event) => {
      const { validator } = this.props;
      if (validator && typeof validator.func === 'function') {
        if (!validator.func(event.target.value)) {
          this.setState({ error: validator.msg })
        } else {
          this.setState({ error: '' })
        }
      }
    }
    render() {
      return <div>
        <WrappedComponent onChange={this.onChange}  {...this.props} />
        <div>{this.state.error || ''}</div>
      </div>
    }
  }
}
const validatorName = {
  func: (val) => val && !isNaN(val),
  msg: '请输入数字'
}
const validatorPwd = {
  func: (val) => val && val.length > 6,
  msg: '密码必须大于6位'
}
<HOCInput validator={validatorName} v_model="name"></HOCInput>
<HOCInput validator={validatorPwd} v_model="pwd"></HOCInput>

当然,还可以在Form提交的时候判断所有验证器是否通过,验证器也可以设置为数组等等,由于文章篇幅原因,代码被简化了很多,有兴趣的同学可以自己实现。

Redux的connect

image

redux中的connect,其实就是一个HOC,下面就是一个简化版的connect实现:

export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {} 
      let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {} 
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }
  return Connect
}

代码非常清晰,connect函数其实就做了一件事,将mapStateToPropsmapDispatchToProps分别解构后传给原组件,这样我们在原组件内就可以直接用props获取state以及dispatch函数了。

使用HOC的注意事项

告诫—静态属性拷贝

当我们应用HOC去增强另一个组件时,我们实际使用的组件已经不是原组件了,所以我们拿不到原组件的任何静态属性,我们可以在HOC的结尾手动拷贝他们:

function proxyHOC(WrappedComponent) {
  class HOCComponent extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
  HOCComponent.staticMethod = WrappedComponent.staticMethod;
  // ... 
  return HOCComponent;
}

如果原组件有非常多的静态属性,这个过程是非常痛苦的,而且你需要去了解需要增强的所有组件的静态属性是什么,我们可以使用hoist-non-react-statics来帮助我们解决这个问题,它可以自动帮我们拷贝所有非React的静态方法,使用方式如下:

import hoistNonReactStatic from 'hoist-non-react-statics';
function proxyHOC(WrappedComponent) {
  class HOCComponent extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
  hoistNonReactStatic(HOCComponent,WrappedComponent);
  return HOCComponent;
}

告诫—传递refs

使用高阶组件后,获取到的ref实际上是最外层的容器组件,而非原组件,但是很多情况下我们需要用到原组件的ref

高阶组件并不能像透传props那样将refs透传,我们可以用一个回调函数来完成ref的传递:

function hoc(WrappedComponent) {
  return class extends Component {
    getWrappedRef = () => this.wrappedRef;
    render() {
      return <WrappedComponent ref={ref => { this.wrappedRef = ref }} {...this.props} />;
    }
  }
}
@hoc
class Input extends Component {
  render() { return <input></input> }
}
class App extends Component {
  render() {
    return (
      <Input ref={ref => { this.inpitRef = ref.getWrappedRef() }} ></Input>
    );
  }
}

React 16.3版本提供了一个forwardRef API来帮助我们进行refs传递,这样我们在高阶组件上获取的ref就是原组件的ref了,而不需要再手动传递,如果你的React版本大于16.3,可以使用下面的方式:

function hoc(WrappedComponent) {
  class HOC extends Component {
    render() {
      const { forwardedRef, ...props } = this.props;
      return <WrappedComponent ref={forwardedRef} {...props} />;
    }
  }
  return React.forwardRef((props, ref) => {
    return <HOC forwardedRef={ref} {...props} />;
  });
}

告诫—不要在render方法内创建高阶组件

ReactDiff算法的原则是:

  • 使用组件标识确定是卸载还是更新组件
  • 如果组件的和前一次渲染时标识是相同的,递归更新子组件
  • 如果标识不同卸载组件重新挂载新组件

每次调用高阶组件生成的都是是一个全新的组件,组件的唯一标识响应的也会改变,如果在render方法调用了高阶组件,这会导致组件每次都会被卸载后重新挂载。

约定-不要改变原始组件

官方文档对高阶组件的说明:

高阶组件就是一个没有副作用的纯函数。

我们再来看看纯函数的定义:

如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。
该函数不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变。

如果我们在高阶组件对原组件进行了修改,例如下面的代码:

InputComponent.prototype.componentWillReceiveProps = function(nextProps) { ... }

这样就破坏了我们对高阶组件的约定,同时也改变了使用高阶组件的初衷:我们使用高阶组件是为了增强而非改变原组件。

约定-透传不相关的props

使用高阶组件,我们可以代理所有的props,但往往特定的HOC只会用到其中的一个或几个props。我们需要把其他不相关的props透传给原组件,如下面的代码:

function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, ...props } = this.props;
      if (visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  }
}

我们只使用visible属性来控制组件的显示可隐藏,把其他props透传下去。

约定-displayName

在使用React Developer Tools进行调试时,如果我们使用了HOC,调试界面可能变得非常难以阅读,如下面的代码:

@visible
class Show extends Component {
  render() {
    return <h1>我是一个标签</h1>
  }
}
@visible
class Title extends Component {
  render() {
    return <h1>我是一个标题</h1>
  }
}

image

为了方便调试,我们可以手动为HOC指定一个displayName,官方推荐使用HOCName(WrappedComponentName)

static displayName = `Visible(${WrappedComponent.displayName})`

image

这个约定帮助确保高阶组件最大程度的灵活性和可重用性。

使用HOC的动机

回顾下上文提到的 Mixin 带来的风险:

  • Mixin 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的 Mixin 中的方法可能会相互冲突
  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

image

HOC的出现可以解决这些问题:

  • 高阶组件就是一个没有副作用的纯函数,各个高阶组件不会互相依赖耦合
  • 高阶组件也有可能造成冲突,但我们可以在遵守约定的情况下避免这些行为
  • 高阶组件并不关心数据使用的方式和原因,而被包裹的组件也不关心数据来自何处。高阶组件的增加不会为原组件增加负担

HOC的缺陷

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突。

Hooks

image

HooksReact v16.7.0-alpha中加入的新特性。它可以让你在class以外使用state和其他React特性。

使用Hooks,你可以在将含有state的逻辑从组件中抽象出来,这将可以让这些逻辑容易被测试。同时,Hooks可以帮助你在不重写组件结构的情况下复用这些逻辑。所以,它也可以作为一种实现状态逻辑复用的方案。

阅读下面的章节使用Hook的动机你可以发现,它可以同时解决MixinHOC带来的问题。

官方提供的Hooks

State Hook

我们要使用class组件实现一个计数器功能,我们可能会这样写:

export default class Count extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 }
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => { this.setState({ count: this.state.count + 1 }) }}>
          Click me
        </button>
      </div>
    )
  }
}

通过useState,我们使用函数式组件也能实现这样的功能:

export default function HookTest() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>
        Click me
        </button>
    </div>
  );
}

useState是一个钩子,他可以为函数式组件增加一些状态,并且提供改变这些状态的函数,同时它接收一个参数,这个参数作为状态的默认值。

Effect Hook

Effect Hook 可以让你在函数组件中执行一些具有 side effect(副作用)的操作

参数

useEffect方法接收传入两个参数:

  • 1.回调函数:在第组件一次render和之后的每次update后运行,React保证在DOM已经更新完成之后才会运行回调。
  • 2.状态依赖(数组):当配置了状态依赖项后,只有检测到配置的状态变化时,才会调用回调函数。
  useEffect(() => {
    // 只要组件render后就会执行
  });
  useEffect(() => {
    // 只有count改变时才会执行
  },[count]);

回调返回值

useEffect的第一个参数可以返回一个函数,当页面渲染了下一次更新的结果后,执行下一次useEffect之前,会调用这个函数。这个函数常常用来对上一次调用useEffect进行清理。

export default function HookTest() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('执行...', count);
    return () => {
      console.log('清理...', count);
    }
  }, [count]);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => { setCount(count + 1); setNumber(number + 1); }}>
        Click me
        </button>
    </div>
  );
}

执行上面的代码,并点击几次按钮,会得到下面的结果:

image

注意,如果加上浏览器渲染的情况,结果应该是这样的:

 页面渲染...1
 执行... 1
 页面渲染...2
 清理... 1
 执行... 2
 页面渲染...3
 清理... 2
 执行... 3
 页面渲染...4
 清理... 3
 执行... 4

那么为什么在浏览器渲染完后,再执行清理的方法还能找到上次的state呢?原因很简单,我们在useEffect中返回的是一个函数,这形成了一个闭包,这能保证我们上一次执行函数存储的变量不被销毁和污染。

你可以尝试下面的代码可能更好理解

    var flag = 1;
    var clean;
    function effect(flag) {
      return function () {
        console.log(flag);
      }
    }
    clean = effect(flag);
    flag = 2;
    clean();
    clean = effect(flag);
    flag = 3;
    clean();
    clean = effect(flag);

    // 执行结果

    effect... 1
    clean... 1
    effect... 2
    clean... 2
    effect... 3

模拟componentDidMount

componentDidMount等价于useEffect的回调仅在页面初始化完成后执行一次,当useEffect的第二个参数传入一个空数组时可以实现这个效果。

function useDidMount(callback) {
  useEffect(callback, []);
}
官方不推荐上面这种写法,因为这有可能导致一些错误。

模拟componentWillUnmount

function useUnMount(callback) {
  useEffect(() => callback, []);
}
不像 componentDidMount 或者 componentDidUpdate,useEffect 中使用的 effect 并不会阻滞浏览器渲染页面。这让你的 app 看起来更加流畅。

ref Hook

使用useRef Hook,你可以轻松的获取到domref

export default function Input() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}

注意useRef()并不仅仅可以用来当作获取ref使用,使用useRef产生的refcurrent属性是可变的,这意味着你可以用它来保存一个任意值。

模拟componentDidUpdate

componentDidUpdate就相当于除去第一次调用的useEffect,我们可以借助useRef生成一个标识,来记录是否为第一次执行:

function useDidUpdate(callback, prop) {
  const init = useRef(true);
  useEffect(() => {
    if (init.current) {
      init.current = false;
    } else {
      return callback();
    }
  }, prop);
}

使用Hook的注意事项

使用范围

  • 只能在React函数式组件或自定义Hook中使用Hook

Hook的提出主要就是为了解决class组件的一系列问题,所以我们能在class组件中使用它。

声明约束

  • 不要在循环,条件或嵌套函数中调用Hook。

Hook通过数组实现的,每次 useState 都会改变下标,React需要利用调用顺序来正确更新相应的状态,如果 useState 被包裹循环或条件语句中,那每就可能会引起调用顺序的错乱,从而造成意想不到的错误。

我们可以安装一个eslint插件来帮助我们避免这些问题。

// 安装
npm install eslint-plugin-react-hooks --save-dev
// 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

自定义Hook

像上面介绍的HOCmixin一样,我们同样可以通过自定义的Hook将组件中类似的状态逻辑抽取出来。

自定义Hook非常简单,我们只需要定义一个函数,并且把相应需要的状态和effect封装进去,同时,Hook之间也是可以相互引用的。使用use开头命名自定义Hook,这样可以方便eslint进行检查。

下面我们看几个具体的Hook封装:

日志打点

我们可以使用上面封装的生命周期Hook

const useLogger = (componentName, ...params) => {
  useDidMount(() => {
    console.log(`${componentName}初始化`, ...params);
  });
  useUnMount(() => {
    console.log(`${componentName}卸载`, ...params);
  })
  useDidUpdate(() => {
    console.log(`${componentName}更新`, ...params);
  });
};

function Page1(props){
  useLogger('Page1',props);
  return (<div>...</div>)
}

修改title

根据不同的页面名称修改页面title:

function useTitle(title) {
  useEffect(
    () => {
      document.title = title;
      return () => (document.title = "主页");
    },
    [title]
  );
}
function Page1(props){
  useTitle('Page1');
  return (<div>...</div>)
}

双向绑定

我们将表单onChange的逻辑抽取出来封装成一个Hook,这样所有需要进行双向绑定的表单组件都可以进行复用:

function useBind(init) {
  let [value, setValue] = useState(init);
  let onChange = useCallback(function(event) {
    setValue(event.currentTarget.value);
  }, []);
  return {
    value,
    onChange
  };
}
function Page1(props){
  let value = useBind('');
  return <input {...value} />;
}

当然,你可以向上面的HOC那样,结合contextform来封装一个更通用的双向绑定,有兴趣可以手动实现一下。

使用Hook的动机

减少状态逻辑复用的风险

HookMixin在用法上有一定的相似之处,但是Mixin引入的逻辑和状态是可以相互覆盖的,而多个Hook之间互不影响,这让我们不需要在把一部分精力放在防止避免逻辑复用的冲突上。

在不遵守约定的情况下使用HOC也有可能带来一定冲突,比如props覆盖等等,使用Hook则可以避免这些问题。

避免地狱式嵌套

大量使用HOC的情况下让我们的代码变得嵌套层级非常深,使用HOC,我们可以实现扁平式的状态逻辑复用,而避免了大量的组件嵌套。

让组件更容易理解

在使用class组件构建我们的程序时,他们各自拥有自己的状态,业务逻辑的复杂使这些组件变得越来越庞大,各个生命周期中会调用越来越多的逻辑,越来越难以维护。使用Hook,可以让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。

使用函数代替class

相比函数,编写一个class可能需要掌握更多的知识,需要注意的点也越多,比如this指向、绑定事件等等。另外,计算机理解一个函数比理解一个class更快。Hooks让你可以在classes之外使用更多React的新特性。

理性的选择

实际上,Hookreact 16.8.0才正式发布Hook稳定版本,笔者也还未在生产环境下使用,目前笔者在生产环境下使用的最多的是`HOC
`。

React官方完全没有把classesReact中移除的打算,class组件和Hook完全可以同时存在,官方也建议避免任何“大范围重构”,毕竟这是一个非常新的版本,如果你喜欢它,可以在新的非关键性的代码中使用Hook

小结

mixin已被抛弃,HOC正当壮年,Hook初露锋芒,前端圈就是这样,技术迭代速度非常之快,但我们在学习这些知识之时一定要明白为什么要学,学了有没有用,要不要用。不忘初心,方得始终。

文中如有错误,欢迎在评论区指正,谢谢阅读。

推荐阅读

推荐关注

想阅读更多优质文章,或者需要文章中思维导图源文件可关注我的github博客,欢迎star✨。

推荐关注我的微信公众号【code秘密花园】,我们一起交流成长。
图片描述

查看原文

蓝岚爱肉肉 收藏了文章 · 2019-03-27

zsh+on-my-zsh配置教程指南(程序员必备)【已备份】

本文以CentOS 7/Mac 为例,介绍zsh的配置使用教程。

准备

查看当前环境shell

echo $SHELL

<!-- more -->

查看系统自带哪些shell

cat /etc/shells

安装zsh

yum install zsh # CentOS
brew install zsh # mac安装

zsh设置为默认shell

chsh -s /bin/zsh # CentOS
# Mac如下
# 在 /etc/shells 文件中加入如下一行
/usr/local/bin/zsh
# 接着运行
chsh -s /usr/local/bin/zsh

可以通过echo $SHELL查看当前默认的shell,如果没有改为/bin/zsh,那么需要重启shell。

oh-my-zsh

配置zsh是一件麻烦的事儿,爱折腾的程序猿怎么可能忍受?!于是,oh-my-zsh出现了,有了这个东东,zsh配置起来就方便多了!

安装oh-my-zsh

有若干安装方式,介绍三种:
1.自动安装

wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh

2.手动安装

git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc

3.真-手动安装

  • oh-my-zsh的github主页,手动将zip包下载下来。
  • 将zip包解压,拷贝至~/.oh-my-zsh目录。此处省略拷贝的操作步骤。
  • 执行cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc

三选一即可,适合各种环境下的安装,然后需要source ~/.zshrc将配置生效。以下修改了.zshrc文件之后,都执行一下这个命令。

zsh主题

通过如下命令可以查看可用的Theme

# ls ~/.oh-my-zsh/themes

如何修改zsh主题呢?
编辑~/.zshrc文件,将ZSH_THEME="candy",即将主题修改为candy。我采用的steeef

zsh扩展

~/.zshrc中找到plugins关键字,就可以自定义启用的插件了,系统默认加载git

git插件

命令内容可以参考cat ~/.oh-my-zsh/plugins/git/git.plugin.zsh

常用的:

gapa    git add --patch
gc!    git commit -v --amend
gcl    git clone --recursive
gclean    git reset --hard && git clean -dfx
gcm    git checkout master
gcmsg    git commit -m
gco    git checkout
gd    git diff
gdca    git diff --cached
gp    git push
grbc    git rebase --continue
gst    git status
gup    git pull --rebase

完整列表:https://github.com/robbyrussell/oh-my-zsh/wiki/Plugin:git

extract

解压文件用的,所有的压缩文件,都可以直接x filename,不用记忆参数

当然,如果你想要用tar命令,可以使用tar -tab键,zsh会列出参数的含义。

autojump

按照官方文档介绍,需要使用如下命令安装,而不是一些博客中的介绍:

yum install autojump-zsh # CentOS
brew install autojump # Mac

CentOS安装好之后,需要在~/.zshrc中配置一下,除了在plugins中增加autojump之外,还需要添加一行:

[[ -s ~/.autojump/etc/profile.d/autojump.sh ]] && . ~/.autojump/etc/profile.d/autojump.sh

安装好之后,记得source ~/.zshrc,然后你就可以通过j+目录名快速进行目录跳转。支持目录名的模糊匹配和自动补全。

  • j -stat:可以查看历史路径库

zsh-autosuggestions

zsh-autosuggestions

git clone git://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions

~/.zshrc 中配置

plugins=(其他的插件 zsh-autosuggestions)

因为箭头不太方便,在.zshrc中自定义补全快捷键为逗号,但是又一次遇到了需要输入逗号的情况,所以,并不太推荐如下修改:

bindkey ',' autosuggest-accept

zsh-syntax-highlighting

zsh-syntax-highlighting

git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

~/.zshrc文件中配置:

plugins=(其他的插件 zsh-syntax-highlighting)

git-open

git-open插件可以在你git项目下打开远程仓库浏览项目。

git clone https://github.com/paulirish/git-open.git $ZSH_CUSTOM/plugins/git-open

bat

bat 代替 cat
cat 某个文件,可以在终端直接输出文件内容,bat 相比 cat 增加了行号和颜色高亮 👍

brew install bat

常用快捷键

  • 命令历史记录

    • 一旦在 shell 敲入正确命令并能执行后,shell 就会存储你所敲入命令的历史记录(存放在~/.zsh_history 文件中),方便再次运行之前的命令。可以按方向键↑和↓来查看之前执行过的命令
    • 可以用 r来执行上一条命令
    • 使用 ctrl-r 来搜索命令历史记录
  • 命令别名

    • 可以简化命令输入,在 .zshrc 中添加 alias shortcut='this is the origin command' 一行就相当于添加了别名
    • 在命令行中输入 alias 可以查看所有的命令别名

使用技巧

  • 连按两次Tab会列出所有的补全列表并直接开始选择,补全项可以使用 ctrl+n/p/f/b上下左右切换
  • 智能跳转,安装了 autojump 之后,zsh 会自动记录你访问过的目录,通过 j 目录名 可以直接进行目录跳转,而且目录名支持模糊匹配和自动补全,例如你访问过 hadoop-1.0.0 目录,输入j hado 即可正确跳转。j --stat 可以看你的历史路径库。
  • 命令选项补全。在zsh中只需要键入 tar -<tab> 就会列出所有的选项和帮助说明
  • 在当前目录下输入 .. 或 ... ,或直接输入当前目录名都可以跳转,你甚至不再需要输入 cd 命令了。在你知道路径的情况下,比如 /usr/local/bin 你可以输入 cd /u/l/b 然后按进行补全快速输入
  • 目录浏览和跳转:输入 d,即可列出你在这个会话里访问的目录列表,输入列表前的序号,即可直接跳转。
  • 命令参数补全。键入 kill <tab> 就会列出所有的进程名和对应的进程号
  • 更智能的历史命令。在用或者方向上键查找历史命令时,zsh支持限制查找。比如,输入ls,然后再按方向上键,则只会查找用过的ls命令。而此时使用则会仍然按之前的方式查找,忽略 ls
  • 多个终端会话共享历史记录
  • 通配符搜索:ls -l **/*.sh,可以递归显示当前目录下的 shell 文件,文件少时可以代替 find。使用 **/ 来递归搜索
  • 扩展环境变量,输入环境变量然后按 就可以转换成表达的值
  • 在 .zshrc 中添加 setopt HIST_IGNORE_DUPS 可以消除重复记录,也可以利用 sort -t ";" -k 2 -u ~/.zsh_history | sort -o ~/.zsh_history 手动清除

最后

参考

Linux

Mac

查看原文

蓝岚爱肉肉 收藏了文章 · 2019-03-18

30分钟精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?
——拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function。

你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?
——拥有了Hooks,生命周期钩子函数可以先丢一边了。

你在还在为组件中的this指向而晕头转向吗?
——既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this。

这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。如果你也对react感兴趣,或者正在使用react进行项目开发,答应我,请一定抽出至少30分钟的时间来阅读本文好吗?所有你需要了解的React Hooks的知识点,本文都涉及到了,相信完整读完后你一定会有所收获。

一个最简单的Hooks

首先让我们看一下一个简单的有状态组件:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

我们再来看一下使用hooks后的版本:

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

是不是简单多了!可以看到,Example变成了一个函数,但这个函数却有自己的状态(count),同时它还可以更新自己的状态(setCount)。这个函数之所以这么了不得,就是因为它注入了一个hook--useState,就是这个hook让我们的函数变成了一个有状态的函数。

除了useState这个hook外,还有很多别的hook,比如useEffect提供了类似于componentDidMount等生命周期钩子的功能,useContext提供了上下文(context)的功能等等。

Hooks本质上就是一类特殊的函数,它们可以为你的函数型组件(function component)注入一些特殊的功能。咦?这听起来有点像被诟病的Mixins啊?难道是Mixins要在react中死灰复燃了吗?当然不会了,等会我们再来谈两者的区别。总而言之,这些hooks的目标就是让你不再写class,让function一统江湖。

React为什么要搞一个Hooks?

想要复用一个有状态的组件太麻烦了!

我们都知道react都核心思想就是,将一个页面拆成一堆独立的,可复用的组件,并且用自上而下的单向数据流的形式将这些组件串联起来。但假如你在大型的工作项目中用react,你会发现你的项目中实际上很多react组件冗长且难以复用。尤其是那些写成class的组件,它们本身包含了状态(state),所以复用这类组件就变得很麻烦。

那之前,官方推荐怎么解决这个问题呢?答案是:渲染属性(Render Props)高阶组件(Higher-Order Components)。我们可以稍微跑下题简单看一下这两种模式。

渲染属性指的是使用一个值为函数的prop来传递需要动态渲染的nodes或组件。如下面的代码可以看到我们的DataProvider组件包含了所有跟状态相关的代码,而Cat组件则可以是一个单纯的展示型组件,这样一来DataProvider就可以单独复用了。

import Cat from 'components/cat'
class DataProvider extends React.Component {
  constructor(props) {
    super(props);
    this.state = { target: 'Zac' };
  }

  render() {
    return (
      <div>
        {this.props.render(this.state)}
      </div>
    )
  }
}

<DataProvider render={data => (
  <Cat target={data.target} />
)}/>

虽然这个模式叫Render Props,但不是说非用一个叫render的props不可,习惯上大家更常写成下面这种:

...
<DataProvider>
  {data => (
    <Cat target={data.target} />
  )}
</DataProvider>

高阶组件这个概念就更好理解了,说白了就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。看下面的代码示例,withUser函数就是一个高阶组件,它返回了一个新的组件,这个组件具有了它提供的获取用户信息的功能。

const withUser = WrappedComponent => {
  const user = sessionStorage.getItem("user");
  return props => <WrappedComponent user={user} {...props} />;
};

const UserPage = props => (
  <div class="user-container">
    <p>My name is {props.user}!</p>
  </div>
);

export default withUser(UserPage);

以上这两种模式看上去都挺不错的,很多库也运用了这种模式,比如我们常用的React Router。但我们仔细看这两种模式,会发现它们会增加我们代码的层级关系。最直观的体现,打开devtool看看你的组件层级嵌套是不是很夸张吧。这时候再回过头看我们上一节给出的hooks例子,是不是简洁多了,没有多余的层级嵌套。把各种想要的功能写成一个一个可复用的自定义hook,当你的组件想用什么功能时,直接在组件里调用这个hook即可。

图片描述

生命周期钩子函数里的逻辑太乱了吧!

我们通常希望一个函数只做一件事情,但我们的生命周期钩子函数里通常同时做了很多事情。比如我们需要在componentDidMount中发起ajax请求获取数据,绑定一些事件监听等等。同时,有时候我们还需要在componentDidUpdate做一遍同样的事情。当项目变复杂后,这一块的代码也变得不那么直观。

classes真的太让人困惑了!

我们用class来创建react组件时,还有一件很麻烦的事情,就是this的指向问题。为了保证this的指向正确,我们要经常写这样的代码:this.handleClick = this.handleClick.bind(this),或者是这样的代码:<button onClick={() => this.handleClick(e)}>。一旦我们不小心忘了绑定this,各种bug就随之而来,很麻烦。

还有一件让我很苦恼的事情。我在之前的react系列文章当中曾经说过,尽可能把你的组件写成无状态组件的形式,因为它们更方便复用,可独立测试。然而很多时候,我们用function写了一个简洁完美的无状态组件,后来因为需求变动这个组件必须得有自己的state,我们又得很麻烦的把function改成class。

在这样的背景下,Hooks便横空出世了!

什么是State Hooks?

回到一开始我们用的例子,我们分解来看到底state hooks做了什么:

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

声明一个状态变量

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

useState是react自带的一个hook函数,它的作用就是用来声明状态变量。useState这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]项是当前当前的状态值,第[1]项是可以改变状态值的方法函数。

所以我们做的事情其实就是,声明了一个状态变量count,把它的初始值设为0,同时提供了一个可以更改count的函数setCount。

上面这种表达形式,是借用了es6的数组解构(array destructuring),它可以让我们的代码看起来更简洁。不清楚这种用法的可以先去看下我的这篇文章30分钟掌握ES6/ES2015核心内容(上)

如果不用数组解构的话,可以写成下面这样。实际上数组解构是一件开销很大的事情,用下面这种写法,或者改用对象解构,性能会有很大的提升。具体可以去这篇文章的分析Array destructuring for multi-value returns (in light of React hooks),这里不详细展开,我们就按照官方推荐使用数组解构就好。

let _useState = useState(0);
let count = _useState[0];
let setCount = _useState[1];

读取状态值

<p>You clicked {count} times</p>

是不是超简单?因为我们的状态count就是一个单纯的变量而已,我们再也不需要写成{this.state.count}这样了。

更新状态

  <button onClick={() => setCount(count + 1)}>
    Click me
  </button>

当用户点击按钮时,我们调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给react了,react将会重新渲染我们的Example组件,并且使用的是更新后的新的状态,即count=1。这里我们要停下来思考一下,Example本质上也是一个普通的函数,为什么它可以记住之前的状态?

一个至关重要的问题

这里我们就发现了问题,通常来说我们在一个函数中声明的变量,当函数运行完成后,这个变量也就销毁了(这里我们先不考虑闭包等情况),比如考虑下面的例子:

function add(n) {
    const result = 0;
    return result + 1;
}

add(1); //1
add(1); //1

不管我们反复调用add函数多少次,结果都是1。因为每一次我们调用add时,result变量都是从初始值0开始的。那为什么上面的Example函数每次执行的时候,都是拿的上一次执行完的状态值作为初始值?答案是:是react帮我们记住的。至于react是用什么机制记住的,我们可以再思考一下。

假如一个组件有多个状态值怎么办?

首先,useState是可以多次调用的,所以我们完全可以这样写:

function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

其次,useState接收的初始值没有规定一定要是string/number/boolean这种简单数据类型,它完全可以接收对象或者数组作为参数。唯一需要注意的点是,之前我们的this.setState做的是合并状态后返回一个新状态,而useState是直接替换老状态后返回新状态。最后,react也给我们提供了一个useReducer的hook,如果你更喜欢redux式的状态管理方案的话。

从ExampleWithManyStates函数我们可以看到,useState无论调用多少次,相互之间是独立的。这一点至关重要。为什么这么说呢?

其实我们看hook的“形态”,有点类似之前被官方否定掉的Mixins这种方案,都是提供一种“插拔式的功能注入”的能力。而mixins之所以被否定,是因为Mixins机制是让多个Mixins共享一个对象的数据空间,这样就很难确保不同Mixins依赖的状态不发生冲突。

而现在我们的hook,一方面它是直接用在function当中,而不是class;另一方面每一个hook都是相互独立的,不同组件调用同一个hook也能保证各自状态的独立性。这就是两者的本质区别了。

react是怎么保证多个useState的相互独立的?

还是看上面给出的ExampleWithManyStates例子,我们调用了三次useState,每次我们传的参数只是一个值(如42,‘banana’),我们根本没有告诉react这些值对应的key是哪个,那react是怎么保证这三个useState找到它对应的state呢?

答案是,react是根据useState出现的顺序来定的。我们具体来看一下:

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  useState('banana');  //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
  useState([{ text: 'Learn Hooks' }]); //...

假如我们改一下代码:

let showFruit = true;
function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  
  if(showFruit) {
    const [fruit, setFruit] = useState('banana');
    showFruit = false;
  }
 
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

这样一来,

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  // useState('banana');  
  useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错

鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。

什么是Effect Hooks?

我们在上一节的例子中增加一个新功能:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 类似于componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 更新文档的标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

我们对比着看一下,如果没有hooks,我们会怎么写?

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起ajax请求获取数据,添加一些监听的注册和取消注册,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。它以一抵三。

同时,由于前文所说hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰。

useEffect做了什么?

我们再梳理一遍下面代码的逻辑:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

首先,我们声明了一个状态变量count,将它的初始值设为0。然后我们告诉react,我们的这个组件有一个副作用。我们给useEffecthook传了一个匿名函数,这个匿名函数就是我们的副作用。在这个例子里,我们的副作用是调用browser API来修改文档标题。当react要渲染我们的组件时,它会先记住我们用到的副作用。等react更新了DOM之后,它再依次执行我们定义的副作用函数。

这里要注意几点:
第一,react首次渲染和之后的每次渲染都会调用一遍传给useEffect的函数。而之前我们要用两个声明周期函数来分别表示首次渲染(componentDidMount),和之后的更新导致的重新渲染(componentDidUpdate)。

第二,useEffect中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而之前的componentDidMount或componentDidUpdate中的代码则是同步执行的。这种安排对大多数副作用说都是合理的,但有的情况除外,比如我们有时候需要先根据DOM计算出某个元素的尺寸再重新渲染,这时候我们希望这次重新渲染是同步发生的,也就是说它会在浏览器真的去绘制这个页面前发生。

useEffect怎么解绑一些副作用

这种场景很常见,当我们在componentDidMount里添加了一个注册,我们得马上在componentWillUnmount中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了。

怎么清除呢?让我们传给useEffect的副作用函数返回一个新的函数即可。这个新的函数将会在组件下一次重新渲染之后执行。这种模式在一些pubsub模式的实现中很常见。看下面的例子:

import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 一定注意下这个顺序:告诉react在下次重新渲染组件之后,同时是下次调用ChatAPI.subscribeToFriendStatus之前执行cleanup
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

这里有一个点需要重视!这种解绑的模式跟componentWillUnmount不一样。componentWillUnmount只会在组件被销毁前执行一次而已,而useEffect里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍。所以我们一起来看一下下面这个问题。

为什么要让副作用函数每次组件更新都执行一遍?

我们先看以前的模式:

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

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

很清除,我们在componentDidMount注册,再在componentWillUnmount清除注册。但假如这时候props.friend.id变了怎么办?我们不得不再添加一个componentDidUpdate来处理这种情况:

...
  componentDidUpdate(prevProps) {
    // 先把上一个friend.id解绑
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 再重新注册新但friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
...

看到了吗?很繁琐,而我们但useEffect则没这个问题,因为它在每次组件更新后都会重新执行一遍。所以代码的执行顺序是这样的:

1.页面首次渲染
2.替friend.id=1的朋友注册

3.突然friend.id变成了2
4.页面重新渲染
5.清除friend.id=1的绑定
6.替friend.id=2的朋友注册
...

怎么跳过一些不必要的副作用函数

按照上一节的思路,每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可。用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数(第一个参数)。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句

当我们第二个参数传一个空数组[]时,其实就相当于只在首次渲染的时候执行。也就是componentDidMount加componentWillUnmount的模式。不过这种用法可能带来bug,少用。

还有哪些自带的Effect Hooks?

除了上文重点介绍的useState和useEffect,react还给我们提供来很多有用的hooks:

useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect

我不再一一介绍,大家自行去查阅官方文档。

怎么写自定义的Effect Hooks?

为什么要自己去写一个Effect Hooks? 这样我们才能把可以复用的逻辑抽离出来,变成一个个可以随意插拔的“插销”,哪个组件要用来,我就插进哪个组件里,so easy!看一个完整的例子,你就明白了。

比如我们可以把上面写的FriendStatus组件中判断朋友是否在线的功能抽出来,新建一个useFriendStatus的hook专门用来判断某个id是否在线。

import { useState, useEffect } from 'react';

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

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

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

  return isOnline;
}

这时候FriendStatus组件就可以简写为:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

简直Perfect!假如这个时候我们又有一个朋友列表也需要显示是否在线的信息:

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

简直Fabulous!

结尾

不知道你阅读完整篇文章的感受如何,或者对hooks有任何角度的看法和思考都欢迎在评论区一起讨论。另外如果你有换工作的打算,我们部门真的很缺人,欢迎私信勾搭~(阿里巴巴,base在深圳的部门)

查看原文

蓝岚爱肉肉 赞了文章 · 2019-01-06

10分钟打造一款属于自己的Ghsot博客主题,持续更新中......

就我目前所知,对个人博客感兴趣的有这些群体:学生、作者、程序员、设计师...

对于开源的博客框架也有很多种选择,你可以用老牌的 WordPress 、 也可以用 Github + Jekyll 搭一个静态的站点、也可以用二次元码农喜欢的 Hexo , 还有 Hugo 等等,当然还有我们的 Ghost

以上的大部分博客框架我都有尝试过,最终还是觉得 Ghost 用得比较顺手。优缺点什么的我这里就不做对比了,没有最好的,只有最合适的。

下面是我自己做的一款主题,可以去我的主页预览,项目主页:maple,欢迎 star

专题教程持续更新中,想要自己制作主题的同学可以关注一下

image_1bn5atecg1tl052g1nhpnk2p7u9.png

须要用到的技能

对于主题制作其实不需要太多的知识,你只需要知道简单的 HTML + CSS 就可以了 , 如果知道一点 JS那更好了。

但如果你想随心所欲地打造一款看起来不错而且工程上比较规范的主题,你需要掌握以下技能

  • HTML5 + CSS3 + JavaScript (ES6)
  • Sass 或 Less
  • Handlebars (ghost所用的模板语言)
  • gulp 或 webpack (构建工具)
  • 英文文档阅读能力
  • 设计能力

HTML 的基础知识可以去 w3school get 到。Sass 和 Less 是动态的 CSS 语言,也不是很难,网上有很多十分钟入门教程,花十分钟看一下就会了。构建工具我们选择 Gulp 吧,也不是什么大的工程,这个相对易于学习一点。Handlebars 是一种模板语言,它有自己的官网,但对于我们开发主题来说,只需要了解几个简单的表达式就可以了。

Handlebars

Handlebars 可以说是 Ghost 主题的灵魂了。我们用它配合后台提供的上下文数据,就可以很方便地在 HTML 中使用动态的数据。

我们先看一下它官网提供的示例吧

<div class ="entry"> 
  <h1> {{title}} </h1> 
  <div class = "body">
     {{body}} 
  </div> 
</div>

Handlebars 模板是由普通的 HTML 和 Handlebars 表达式组成的。所有的 Handlebars 表达式都是用大括号包裹起来的。

然后我们需要提供 context (上下文数据)

{
  title: "My New Post",
  body: "This is my first post!"
}

最终的结果如下

<div  class = "entry"> 
  <h1> My New Post </h1> 
  <div class = "body">
     This is my first post!
  </div> 
</div>

在我们的 Ghost 主题里,每个模板都有特定的 context , 我们通过官方提供的 context-table ,可以找出当前模板可以访问的数据。

模板可以访问的所有数据实际上都是一个很大的 JSON 对象 ( 一组嵌套的属性及其值),这对于一篇文章来说看起来像这样:

{
  post: {
    id: "xyz",
    title: "My blog post title",
    slug: "welcome",
    ... 
      author: {
      id: "abc",
      name: "Jo Bloggs",
      ...
    }
  }
}   

Data Expressions (数据表达式)

数据表达式是最简单也是最常用的,主要用来输出模板可以访问的数据,比如 {{title}}
可以输出文章的标题

HTML Data Expressions

例如:{{{bio}}}

有时,我们需要输出的数据可能包含 HTML ,但默认的 {{}}表达式会将 HTML 当成纯文本输出,使用 {{{}}} 才能正确呈现 HTML

Global Data

例如:{{@blog.title}}

Ghost 可以通过 {{@blog}} 属性访问一些全局数据,@标志也可以在块级表达式中提供特殊的属性,后面会在具体的使用场景中介绍到

Path Expressions

例如:{{author.name}}

有时,你想要输出的属性在当前的作用域中不可用,因为它属于另一个对象。例如,如果你想输出你文章的作者姓名,用{{name}}是不行的。只能通过 {{author.name}}来访问。

Block Expressions (Scopes)

例如:{{#post}}{{/post}}

有时,你想要输出比较多的属性,例如,在文章里需要访问大量作者属性。你可以使用块级表达式来框定一个作用域,那样就不用频繁使用 Path Expressions 了。

块级表达式类似于 HTML 标签,有开始和结束标记,在它们之间我们可以直接访问当前对象的属性,例如:

{{#author}}
    My name is {{name}}, visit my <a href="{{website}}">Website!</a>
{{/author}}

Block expressions vs path expressions

{{#author}}{{name}}{{/author}}{{author.name}}在很大程度上是一样的。这两种写法最大的不同就是在块级表达式中我们可以使用 handlebars helpers 。直接这样解释有点不好懂,我们举个例子吧

{{#post}}
    <small>
        {{author.name}}
        {{author.url}}
    </small>

    <br>

    <small>
        {{#author}}
            {{name}}
            {{url}}
        {{/author}}
    </small>

{{/post}}

输出结果

image_1c735ghjd13rghuj1dgv5c21uvm9.png-2.1kB

可以看到第一种写法是不能输出作者的 url 的。官方文档里的解释是,author 是没有 url 这个属性的,所以{{author.url}}不起作用,但是它提供了{{url}}助手表达式,所以第二种写法是 OK 的

Block + Path Expressions

这两种表达式还可以有更高级的用法,比如结合起来使用以达到你希望的结果。下面的例子显示了如何使用主页上的最新帖子

{{#posts.[0]}}
  <h3 class="first_title">{{title}}</h3>
  <div class="first_content">{{content}}</div>
{{/posts.[0]}}

在实际的主题开发中,这是很有用的。比如你想针对最新的文章设计样式,突出显示

Helpers (助手)

例如:{{content}}{{url}}

助手表达式与其它表达式的区别在于它提供了一些方法,而不仅仅是输出数据。本质上,数据表达式是变量,助手表达式是函数。

官方文档里提供了所有的助手参考,具体的用法可以去看一下

image_1c736gslu1bpg1ekest41euako9m.png-15.8kB

Block Helpers

例如:{{#if}}{{/if}}{{#foreach posts}}{{/foreach}}

这个就很好懂了,见名知意。列举一下比较常用的:

  • {{#if}}{{else}}{{/if}}
  • {{#is}}{{/is}}
  • {{#unless}}{{else}}{{/unless}} ,与 if 相反
  • {{#foreach}}{{/foreach}}

Layout Expressions (布局表达式)

例如:{{!< default}}{{{body}}}

布局的作用是允许你你定义一个其他模板可以扩展的基本模板。在 Ghost 主题中常见的做法是新建一个default.hbs的默认模板,里面包含一个 HTML 页面的核心元素:header 、 footer 和 内容。然后我们会用 {{{body}}}这个助手函数来加载其他模板的内容。其他模板都以{{!< default}}表达式开始,表明它扩展了默认的布局

Partial Expressions

例如:{{> loop}}

Partial 也是 Ghost 主题里面很重要的内容。它允许你创建包含在其他模板里的小型可重用模板。通常我们会在根目录创建一个partials目录来存放你创建的各种小的模板,比如:loop.hbs,然后在其他的模板中可以使用{{> loop}} 输出内容。注意:如果partials目录里面还有子目录,我们输出的时候要将路径带上,比如:{{> author/mini-bio}}

Comments (注释)

例如:{{!-- comments --}}

Handlebars 有自己的注释方式,跟 HTML 不一样,需要注意一下。

查看原文

赞 2 收藏 1 评论 0

蓝岚爱肉肉 关注了专栏 · 2019-01-05

前端下午茶公众号

你不能把这个世界,让给你鄙视的人

关注 2843

蓝岚爱肉肉 关注了专栏 · 2019-01-05

前端早读君

前端早读君,一个每天陪你度过上班路程的早读君。 无论你在地铁、公交,每天?推送web前端文章给您。 伴你同行。

关注 25

蓝岚爱肉肉 关注了专栏 · 2019-01-05

前端修炼之路

记录一下前端那些事

关注 10

蓝岚爱肉肉 关注了专栏 · 2019-01-05

前端周刊

薄荷前端周刊,每周至少一篇技术分享

关注 3193

认证与成就

  • 获得 0 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-02-25
个人主页被 455 人浏览