Blog Logo

重回react,自己搭建类似react-boilerplate框架

写于2019-02-28 20:09 阅读耗时71分钟 阅读量


最近分配到公司其他项目组,主要技术栈react,自己蛮期待react的,虽说自己擅长vue,不过也还好,毕竟一年前使用过react,再看react,会有新的体会。

文章主要介绍以下内容:

  • 脚手架
  • vue脚手架
  • react脚手架
  • react-boilerplate脚手架上手
  • 自己搭建简洁版react-boilerplate脚手架
    • react组件
      • react组件简介
      • react组件分类
      • react普通组件拆分成UI组件和容器组件
      • react无状态组件
      • react细节说明
    • redux整合
      • redux简介
      • redux工作流
      • react与redux的整合
      • react与react-redux整合
      • redux优化
      • redux中间件
      • 整合redux-thunk及使用
      • 整合redux-saga
      • redux-saga常见使用
      • 自定义redux中间件
      • 增加immutable库,提升reducer state性能
      • 增加redux-immutable库,统一对象
      • 增加reselect库,提升mapStateToProps计算性能
    • react-router路由
    • styled-components样式组件

1.脚手架

目前vue在github上的star数是129k,react123k,说明vue比react要稍火点。尽管如此,react是最影响前端行业,带领我们走向新的思路的存在,很多时候vue借鉴react的思想,当然两个技术栈我都喜欢,各有各的优势。

对我而言,全栈是一方面,另一方面是找到一个学习点,努力持之以恒下去,精通它。广还不行,还需要

前端开发细化下来从事的工种有很多: H5游戏开发(技术栈:cocos2d、engine、typescript...)

H5页面开发(移动app、移动pad | 微信网页 | 浏览器网页)(技术栈:jsbradge、weixin sdk、zepto、html5、css3、vue、react...)

web前端开发(偏小程序 | 游戏小程序)(技术栈:javascript、wxml、wxss、weixin program api、vue、react...)

web前端开发(偏PC | 管理后台)(技术栈:jquery、vue、react...)

web前端开发(偏客户端)(技术栈:electron、vue、react...)

你会惊讶的发现在细化的工种里重复出现次数最多的是vue和react,vue、react技术栈在前端领域起着绝对性作用。往vue深入还是往react深入都行,我个人倾向和喜爱react。

再说回脚手架,前端开发人员对脚手架这个词肯定不陌生,脚手架能快速搭建自己所需的项目基本结构。 vue脚手架是vue官网自己提供的vue-cli,可自定义自己所需的组件和构建工具,因此可简单可复杂。 react脚手架是facebook自己提供的create-react-app,优势功能简单,缺憾是无整合第三方优秀的库,可简单不可复杂。


2.vue脚手架

2.1 简单vue

下面是vue官方脚手架vue-cli 3.0搭出的基本目录结构:

vue_source

可以看到一个完整的vue文件包含template、script、style三种xml名称,分别对应html、js、css。

基本目录讲解:

my-app
├── README.md //markdown当前项目的说明
├── node_modules //node包
├── babel.config.js //配置babel规则和插件
├── package.json //当前项目安装的包列表
├── yarn.lock //锁定安装每个依赖包的版本
├── .gitignore //git上传时忽略的目录或文件
├── dist //build后生成的编译包
├── public //主页index.html
└── src
    ├── asset //静态资源
    ├── components //公用组件
    ├── App.vue //根组件
    └── main.js //根函数,页面与vue绑定

执行的常用命令:

// 创建新vue项目
vue create project_name

// 开发模式
npm run serve

// 编译打包
npm run build

简单科普下Babel和ESLint, Babel 是一个编译工具,支持ES7,各种语法糖; ESLint 是一个整合编码规范和检测功能的工具。

有它们俩,就能写出简洁新语法特性且语法规范的代码。

下面是ESLint报错时的提示信息: eslint


使用vs code,支持eslint报错的setting.json配置如下:

{
    "editor.tabSize": 2,
    "editor.fontSize": 16,
    "files.associations": {
        "*.vue": "vue"
    },
    "eslint.autoFixOnSave": true,
    "eslint.options": {
        "extensions": [
            ".js",
            ".vue"
        ]
    },
  "eslint.validate": [
      "javascript",{
          "language": "vue",
          "autoFix": true
      },"html",
      "vue"
  ],
    "search.exclude": {
        "**/node_modules": true,
        "**/bower_components": true,
        "**/dist": true
    },
    "emmet.syntaxProfiles": {
        "javascript": "jsx",
        "vue": "html",
        "vue-html": "html"
    },
    "git.confirmSync": false,
    "window.zoomLevel": 0,
    "editor.renderWhitespace": "boundary",
    "editor.cursorBlinking": "smooth",
    "editor.minimap.enabled": true,
    "editor.minimap.renderCharacters": false,
    "editor.fontFamily": "'Droid Sans Mono', 'Courier New', monospace, 'Droid Sans Fallback'",
    "window.title": "${dirty}${activeEditorMedium}${separator}${rootName}",
    "editor.codeLens": true,
    "editor.snippetSuggestions": "top",
  }

2.2 复杂vue

在创建新vue项目的时候,默认有两个选项,选项一则是空的简洁vue、选项二则是按你需要增加常用的第三方库。

安装图如下:

vue_router

vue里面最常用的第三方库当属vue-routervuex,一个负责页面路由跳转、另一个负责状态管理。 其余的TypeSciprt,PWA、CSS Pre、Testing都是辅助。习惯TypeScript编程语言、熟悉PWA模式、经常使用SCSS、LESS CSS预编译、想自测达到代码完整性的前端人员,可以选择它们。对我来说,只需一个稍简单的复杂vue项目就行。


与简单vue的基本目录结构相比,增加vue-router和vuex的整合。 简单vue package.json:

// 仅依赖vue
"dependencies": {
    "vue": "^2.5.21"
}

main.js:

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// 将vue与id为app的DOM节点进行绑定
new Vue({
  render: h => h(App),
}).$mount('#app')

复杂vue package.json:

// 依赖vue、vue-router、vuex
"dependencies": {
    "vue": "^2.5.21",
    "vue-router": "^3.0.1",
    "vuex": "^3.0.1"
}

main.js:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

// 将vue与id为app的DOM节点进行绑定
// 且注入router路由和store状态管理
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

提供下参考链接,方便以后查询: vue-cli脚手架官网:https://cli.vuejs.org/zh/ vue官网:https://cn.vuejs.org/zh/ vue-router官网:https://router.vuejs.org/zh/ vuex官网:https://vuex.vuejs.org/zh/

vue的技术栈就先简单介绍这么多,毕竟今天的主角是react技术栈。


3.react脚手架

3.1 简单react

下面是react官方脚手架create-react-app搭出的基本目录结构:

react_create

基本目录讲解,和vue目录蛮类似的:

my-app
├── README.md //markdown当前项目的说明
├── node_modules //node包
├── package.json //当前项目安装的包列表
├── .gitignore //git上传时忽略的目录或文件
├── build //build后生成的编译包
├── public
│   ├── favicon.ico //图标
│   ├── index.html //首页
│   └── manifest.json //PWA模式
└── src
    ├── App.css //根样式
    ├── App.js //根组件
    ├── App.test.js
    ├── index.css //公共样式
    ├── index.js //根函数,页面与vue绑定
    ├── logo.svg //react logo
    └── serviceWorker.js //PWA模式

3.2 科普下PWA

在vue-cli3.0和create-react-app中,都有提到PWA,那什么是PWA? PWA全称Progressive Web Apps,即渐进式WEB应用, 由Chrome 团队提出。简单理解PWA就是一种模式或理念,和当年的SPA(全称是Single Page Application,即单页Web应用)类比。 PWA的出现,主要目的是希望Web 应用能渐进式接近原生 App


PWA解决哪些问题?

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏
  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能
  • 实现消息推送功能

PWA中的三个关键技术:

  • Manifest 应用清单
  • Service Worker 服务工厂
  • Push Notification 消息推送

再回顾上面简单react,看到的manifest.json和serviceWorker.js似乎有所明白。 manifest.json的目的实现添加至主屏幕, serviceWorker.js的目的实现本地离线缓存和消息推送。

关于PWA详细的说明请参考文章: 讲讲PWA:https://segmentfault.com/a/1190000012353473?utm_source=tag-newest,这里不一一展开。


3.3 分析依赖及根函数

package.json:

//依赖react、react-dom、react-scripts
//react 核心库
//react-dom 提供与 DOM 相关的功能
//react-scripts 由webpack封装的react配置
"dependencies": {
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-scripts": "2.1.3"
 }

index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

// 将react与id为root的DOM节点进行绑定
ReactDOM.render(<App />, document.getElementById('root'));

// PWA模式不使用Service Worke
// 默认不使用,如果想使用,改成register()即可
serviceWorker.unregister();

3.4 复杂react

react官方facebook并没有提供复杂的脚手架,毕竟很多第三方库不是自家的,不像vue,vuex、vue-router都来自同一个组织vuejs,且创始人都来自一人尤雨溪。

于是乎今天的主角登场,名字叫react-boilerplate脚手架。当然你也可以自己搭建和整合react-router、react-redux等第三方库,但稍微有点麻烦,脚手架诞生的目的就是整合资源,快速搭建所需的项目。

react-boilerplate

react-boilerplate是一个高度可扩展、离线优先为基础的,专注于性能和最佳实践的react脚手架工具。


react_boilerplate_source

说了这么多,我们来大致看下目录基本结构:

my-app
... //文档说明
├── README.md //项目说明
├── LICENSE.md //协议说明
├── CODE_OF_CONDUCT.md //贡献说明
├── CONTRIBUTING.md //帮助说明
├── Changelog.md //更新日志说明
... //隐藏配置.
├── .editorconfig // editorconfig配置,统一多人开发同一项目编码风格
├── .eslintrc.js //eslint配置,代码风格检测
├── .git //git仓库
├── .gitattributes //git文件属性
├── .github //github仓库
├── .gitignore //git忽略上传的目录或文件
├── .nvmrc //nvm配置项目所使用的node版本
├── .prettierignore //prettier配置忽略代码格式化
├── .prettierrc //prettier配置代码格式化
├── .stylelintrc //stylelint配置样式格式
├── .travis.yml //travis配置持续集成
... //其他配置
├── babel.config.js //babel配置编译
├── jest.config.js //jest配置单元测试
├── package.json //node安装包列表
├── package-lock.json //锁定安装的包版本号
├── appveyor.yml //appveyor配置持续集成
... //文件夹
├── build //build后生成的编译包
├── node_modules //node包
├── app //源码
├── docs //更多说明文档
├── internals //工具配置
└── server //配置本地服务

第一印象觉得这个脚手架生成的项目很重,有太多需要细看的知识点和内容,接下来的章节中会依次讲解文件夹里面的内容。我会从易到难的文件夹,逐个讲解。


3.5 react-boilerplate中的docs

build和node_modules文件夹的内容就不作过多解释, internals是内置项目的工具配置, server是配置本地服务的,这些文件夹对于我们而言不需过多的关心,开发的重点应该放在docs、app。

react_docs

docs目录结构如下:

docs
├── README.md //根目录
├── css
│   ├── README.md //css讲解,用到了styled-components
│   ├── linting.md //css编码规范
│   ├── remove.md //移除sanitize.css说明和检测说明
│   └── sanitize.md //sanitize.css的讲解,重置css默认样式
├── forks
│   └── README.md //提及与electron整合的reactron、服务器端渲染ssr的react-boilerplate-ssr
│   //及支持typescript的react-boilerplate-typescript
├── general
│   ├── README.md //项目特点说明
│   ├── commands.md //命令行说明
│   ├── components.md //扩展组件说明
│   ├── debugging.md //调试说明
│   ├── deployment.md //部署说明
│   ├── eitor.md //编辑说明
│   ├── faq.md //常见问题说明
│   ├── file.md //文件说明
│   ├── gotchas.md //陷阱说明
│   ├── introduction.md //项目介绍说明
│   ├── remove.md //移除离线访问说明
│   ├── server-configs.md //服务配置说明
│   ├── webstorm-debug.png //webstorm debug配置图
│   ├── webstorm-eslint.png //webstorm eslint配置图
│   └── workflow.png //项目流程图
├── js
│   ├── README.md //js讲解,用到哪些核心第三方库
│   ├── async-components.md //异步加载组件,减少bundle大小无压力,
│   //用的loadable-components
│   ├── i18n.md //国际化,用的react-intl
│   ├── immutablejs.md //持久化数据结构,用的immutable-js
│   ├── redux-saga.md //redux异步中间件,用的redux-saga
│   ├── redux.md //状态管理容器,用的redux
│   ├── remove.md //移除redux-saga中间件说明
│   ├── reselet.md //state变化减少渲染压力中间件,用的reselect
│   └── routing.md //路由说明,用的react-router和connected-react-router
├── maintenance
│   └── dependency.md //依赖包说明
└── testing
    ├── README.md //测试讲解
    ├── component-testing.md //组件测试说明
    ├── remote-testing.md //远程可用性测试说明
    └── unit-testing.md //单元测试说明

docs目录是该项目的手册和文档,里面讲解到的知识点和框架,有时间一定要过一遍里面的内容。


3.6 react-boilerplate中的app

react_app_source

先讲下大致的,app目录结构如下:

app
├── components //页面组件
├── containers //页面
├── images //静态图片
├── tests //测试配置
├── translations //国际化配置
├── utils //公共工具
├── .htaccess //分布式配置
├── .nginx.conf //nginx配置
├── app.js //根函数
├── configureStore.js //store配置
├── reducers.js //reducers配置
├── global-styles.js //全局样式
├── i18n.js //国际化配置
└── index.html //主页 

开始分析主入口app.js:

/**
 * 主入口
 */

// redux-saga需要es6 generator生成器函数支持
import '@babel/polyfill';

// 导入的第三方库
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router/immutable';
import FontFaceObserver from 'fontfaceobserver';
import history from 'utils/history';
import 'sanitize.css/sanitize.css';

// 导入根组件
import App from 'containers/App';

// 导入国际化Provider组件
import LanguageProvider from 'containers/LanguageProvider';

// 导入favicon图标和.htaccess分布式文件
import '!file-loader?name=[name].[ext]!./images/favicon.ico';
import 'file-loader?name=.htaccess!./.htaccess'; // eslint-disable-line import/extensions

// 导入配置的store
import configureStore from './configureStore';

// 导入i18国际化信息
import { translationMessages } from './i18n';

// 创建自定义web字体Open Sans对象,@font-face
const openSansObserver = new FontFaceObserver('Open Sans', {});

// 当Open Sans字体加载后,将该字体加入到body标签内
openSansObserver.load().then(() => {
  document.body.classList.add('fontLoaded');
});

// 创建带管理会话历史记录的store,获取根节点
const initialState = {};
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById('app');

// 声明一个render渲染函数
// 将react与id为app的DOM节点进行绑定
// 将页面包裹三层、一层是state、二层是国际化、三层是会话历史记录
// 换句话说App的所有子组件默认都可以拿到state、i18n、history
// 可以直接在任何子组件内使用state、i18n、history
const render = messages => {
  ReactDOM.render(
    <Provider store={store}>
      <LanguageProvider messages={messages}>
        <ConnectedRouter history={history}>
          <App />
        </ConnectedRouter>
      </LanguageProvider>
    </Provider>,
    MOUNT_NODE,
  );
};

if (module.hot) {
  // 实现热更新
  // 加载i18n、App,销毁app的DOM节点
  // 重新渲染react组件和国际化
  module.hot.accept(['./i18n', 'containers/App'], () => {
    ReactDOM.unmountComponentAtNode(MOUNT_NODE);
    render(translationMessages);
  });
}

// 如果当前浏览器不支持国际化
// 则手动导入国际化信息,重新渲染
if (!window.Intl) {
  new Promise(resolve => {
    resolve(import('intl'));
  })
    .then(() =>
      Promise.all([
        import('intl/locale-data/jsonp/en.js'),
        import('intl/locale-data/jsonp/de.js'),
      ]),
    )
    .then(() => render(translationMessages))
    .catch(err => {
      throw err;
    });
} else {
  render(translationMessages);
}

// 如果是生产环境,使用ServiceWorker和AppCache实现离线缓存体验
if (process.env.NODE_ENV === 'production') {
  require('offline-plugin/runtime').install();
}

看完后,回顾之前的文档说明和主要入口,对react-boilerplate整个项目的搭建稍微清晰点。


我们再看下package.json,该项目依赖哪些第三方库:

"dependencies": {
... //服务器有关,或node
    "@babel/polyfill": "7.0.0", //支持JS新特性,ES6、ES7语法
    "chalk": "^2.4.1", //console高亮
    "compression": "1.7.3", //express压缩中间件
    "cross-env": "5.2.0", //支持js运行跨平台环境(如:run start/build)
    "express": "4.16.4", //node服务器
    "invariant": "2.2.4", //开发环境描述错误
    "ip": "1.1.5", //ip地址工具类
    "minimist": "1.2.0", //解析命令行参数
    "warning": "4.0.2", //警告库
... //react相关
    "prop-types": "15.6.2", //react属性类型库
    "react": "16.6.0", //react核心库
    "react-dom": "16.6.0", //与dom整合的react核心库
    "reselect": "4.0.0", //redux选择器库
    "redux": "4.0.1", //redux核心库
    "react-redux": "5.0.7", //与react整合的redux
    "redux-saga": "0.16.2",//redux异步中间件库
    "immutable": "3.8.2",//immutable核心库
    "redux-immutable": "4.0.0", //与redux整合的immutable
    "react-router-dom": "4.3.1",//与react整合dom整合的router库
    "connected-react-router": "4.5.0", //与router整合的redux
    "intl": "1.2.5", //intl核心库
    "react-intl": "2.7.2", //与react整合的intl
    "react-helmet": "5.2.0", //react head标签管理库
... //前端工具相关    
    "fontfaceobserver": "2.0.13", //自定义web字体
    "history": "4.7.2", //管理会话历史记录
    "hoist-non-react-statics": "3.0.1", //react reducer中state深拷贝、类似Object.assign
    "loadable-components": "2.2.3", //减少bundle大小
    "lodash": "4.17.11", //javascript工具库
    "sanitize.css": "4.1.0", //重置css默认样式
    "styled-components": "4.0.2", //react样式对应html标签模板
}

该项目用到蛮多技术点的,如果只是开发做业务的话,我们无需关系它如何整合这些在一起的,但是需要了解在哪儿实现业务,书写页面。


4.react-boilerplate脚手架上手

这章我会实现几个demo或解析理解其js逻辑来快速上手它。

4.1 功能一:添加支持中文国际化

react_intl

当前页面的国际化是英文和德文,没有中文,我们可以完成一个小功能来学习它。将页面底部添加一个中文zh选项,且This project is licensed under the MIT license.变成中文。

1.在translations目录下添加zh.json 2.在i18n.js中引入它,将它设置成默认

const zhLocaleData = require('react-intl/locale-data/zh');
...
const zhTranslationMessages = require('./translations/zh.json');
...
addLocaleData(zhLocaleData);
...
const DEFAULT_LOCALE = 'zh';
const appLocales = [
  'zh',
  'en',
  'de',
];

3.修改zh.json里的文案

"boilerplate.components.Footer.license.message": "这个项目是MIT协议,开源免费",

成功:

react_intl_ok


4.2 功能二:实现一个react小组件

组件的概念不多说,可以简单理解成一个小片段的html,简单到一个button按钮、一个a标签、一个img图片等,各个小组件可以合成一个大组件,大组件也可以合成更大的组件,最终形成页面。组件化的目的是封装,一层一层抽象,颗粒化的实现组件,开始写的时候代码量可能比较大,但后期代码会越写越少

首先我们看下小组件的目录结构,以components目录下的Footer组件举例:

Footer
├── tests //单元测试js
├── Wrapper.js //css样式
├── message.js //i18n国际化
└── index.js //html片段

Wrapper.js

import styled from 'styled-components';

// 添加css样式时自动生成对应的html dom节点,当前是footer
// 如果是给a标签声明样式,style.a即可
// 格式:styled.xxx``
// 注意:值千万别加引号,如'flex'、'space-between'
const Wrapper = styled.footer`
  display: flex;
  justify-content: space-between;
  padding: 3em 0;
  border-top: 1px solid #666;
`;

export default Wrapper;

补充一下css样式继承:

const A = styled.a`
  color: #41addd;

  &:hover {
    color: #6cc0e5;
  }
`;

const A2 = styled(A)`
  padding: 2em 0;
`;
export default A2;

// 类似scss
.A {
  color: '#41addd';
  &:hover {
    color: '#6cc0e5';
  }
}


.A2 {
 @extend: .A;
 padding: '2em 0';
}

message.js

/*
 * Footer 信息
 * 包含Footer组件所有国际化内容
 */
import { defineMessages } from 'react-intl';

// 声明默认作用域,名称与translations目录下的json对应
export const scope = 'boilerplate.components.Footer';

// 定义该组件用到的文案
// 自定义文案名,值为id和defaultMessage组成的对象
// 注意:defaultMessage默认信息是在读取不到配置信息时展示的文案
// react-intl首先去加载translations下面的zh.json,如果读取值为空,则展示defaultMessage的内容
// 先translations,再defineMessages
export default defineMessages({
  licenseMessage: {
    id: `${scope}.license.message`,
    defaultMessage: 'This project is licensed under the MIT license.',
  },
  authorMessage: {
    id: `${scope}.author.message`,
    defaultMessage: `
      Made with love by {author}.
    `,
  },
});

index.js

import React from 'react';
import { FormattedMessage } from 'react-intl';

import A from 'components/A';
import LocaleToggle from 'containers/LocaleToggle';
import Wrapper from './Wrapper';
import messages from './messages';

// Wrapper相当于footer,FormattedMessage格式化信息解构
// FormattedMessage属性值有id、defaultMessage、values,
// values是声明在defaultMessage中使用的值
// ...ES6语法,解构出定义后的文案名
function Footer() {
  return (
    <Wrapper>
      <section>
        <FormattedMessage {...messages.licenseMessage} />
      </section>
      <section>
        <LocaleToggle />
      </section>
      <section>
        <FormattedMessage
          {...messages.authorMessage}
          values={{
            author: <A href="https://twitter.com/mxstbr">Max Stoiber</A>,
          }}
        />
      </section>
    </Wrapper>
  );
}

export default Footer;

这些js完全可以写在一个文件js里面,比如vue,上面提及过一个完整的vue文件包含template、script、style,react同样可以。


我们改造一下,直接将它们放在一个js里:

import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';

import A from 'components/A';
import LocaleToggle from 'containers/LocaleToggle';
import styled from 'styled-components';

const Wrapper = styled.footer`
  display: flex;
  justify-content: space-between;
  padding: 3em 0;
  border-top: 1px solid #666;
`;

const messages = defineMessages({
  licenseMessage: {
    id: `boilerplate.components.Footer.license.message`,
    defaultMessage: 'This project is licensed under the MIT license.',
  },
  authorMessage: {
    id: `boilerplate.components.Footer.author.message`,
    defaultMessage: `
      Made with love by {author}.
    `,
  },
});


function Footer() {
  return (
    <Wrapper>
      <section>
        <FormattedMessage {...messages.licenseMessage} />
      </section>
      <section>
        <LocaleToggle />
      </section>
      <section>
        <FormattedMessage
          {...messages.authorMessage}
          values={{
            author: <A href="https://twitter.com/mxstbr">Max Stoiber</A>,
          }}
        />
      </section>
    </Wrapper>
  );
}

export default Footer;

4.3 export defualt及相关知识点

科普下export default的意义,首先ES6增加模块体系exportimport。 export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

export default可以用于导出常量、函数、集合、文件、模块等等; 在一个文件或模块中,import可以有多个,export default仅能有一个; 你会发现导入和导出,还有些语法,而且稍有不同。


1.expoert 和 expoert deafult的区别,import xxx 和 import { xxx }的区别

...//export default和import
//如果用的import xxx导入,那么导出方式一定是export default
// 导入styled
import styled from 'styled-components';

// 导出方式export default
export default styled = xxx

...//export和import
//如果用的import { xxx },那么导出方式一定是export
// 导入defineMessages、FormattedMessage函数
import { defineMessages, FormattedMessage } from 'react-intl';

// 导出方式export
export const defineMessages = () => { xxx }
export const FormattedMessage = () => { xxx }

2.require导入和import导入的区别

require: node 和 es6 都支持的引入
export / import : 只有es6 支持的导出引入
module.exports / exports: 只有 node 支持的导出

3.导入别名的区别

//多个export导出的导入
export const defineMessages = () => { xxx }
export const FormattedMessage = () => { xxx }

//方案一,{xxx}
import { defineMessages, FormattedMessage } from 'react-intl';

//方案二,别名
import * as reactIntl from 'react-intl';
//使用
reactIntl.defineMessages
reactIntl.FormattedMessage

//单个export default导出的导入
export default styled = xxx

//导入,无别名
import styled from 'styled-components';


// 函数的别名
import { defineMessages as define , FormattedMessage as Message } from 'react-intl';
// 使用
define
Message

4.4 理解container的App

代码如下:

/**
 * App
 * 所有页面的
 */

import React from 'react';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
// React Router里面提供的API后面会有详细说明
import { Switch, Route } from 'react-router-dom';

import HomePage from 'containers/HomePage/Loadable';
import FeaturePage from 'containers/FeaturePage/Loadable';
import NotFoundPage from 'containers/NotFoundPage/Loadable';
import Header from 'components/Header';
import Footer from 'components/Footer';
// 导入全局样式
import GlobalStyle from '../../global-styles';

const AppWrapper = styled.div`
  max-width: calc(768px + 16px * 2);
  margin: 0 auto;
  display: flex;
  min-height: 100%;
  padding: 0 16px;
  flex-direction: column;
`;

export default function App() {
  return (
    <AppWrapper>
      <Helmet
        titleTemplate="%s - React.js Boilerplate"
        defaultTitle="React.js Boilerplate"
      >
        <meta name="description" content="A React.js Boilerplate application" />
      </Helmet>
      <Header />
      // Switch 只会渲染一个子元素
      // 根据路由渲染对应的 Route
      // exact 完全匹配,如/a,/a可以访问,/a/b不可以访问
      // strict 严格匹配,如/a,/a和/a/b都可以访问
      <Switch>
        <Route exact path="/" component={HomePage} />
        <Route path="/features" component={FeaturePage} />
        <Route path="" component={NotFoundPage} />
      </Switch>
      <Footer />
      <GlobalStyle />
    </AppWrapper>
  );
}

App/index.js里面可以配置router,类似vue的router/index.js。


4.5 理解components的Header部分

渲染和路由切换,代码如下:

import React from 'react';
import { FormattedMessage } from 'react-intl';

import A from './A';
import Img from './Img';
import NavBar from './NavBar';
import HeaderLink from './HeaderLink';
import Banner from './banner.jpg';
import messages from './messages';

class Header extends React.Component {
  render() {
    return (
      <div>
        <A href="https://twitter.com/mxstbr">
          <Img src={Banner} alt="react-boilerplate - Logo" />
        </A>
        <NavBar>
          <HeaderLink to="/">
            <FormattedMessage {...messages.home} />
          </HeaderLink>
          <HeaderLink to="/features">
            <FormattedMessage {...messages.features} />
          </HeaderLink>
        </NavBar>
      </div>
    );
  }
}

export default Header;

可以想象成的html片段如下:

<div>
    <a href="https://twitter.com/mxstbr">
        <img src="./banner.jpg" alt="react-boilerplate - Logo"/>
    </a>
    <div>
        <a href="/">Home</a>
        <a href="/features">Features</a>
    </div>
</div>

需要注意的是HeaderLink,用到了React Router里的Link,导航链接 HeaderLink/index.js:

export default styled(Link)`
    xxxxx
    ....
`

Link用法一: string

<HeaderLink to="/features">
//带参数的链接
<HeaderLink to="/features?token=xxxx">

Link用法二: object

<HeaderLink to= {{
  pathname: '/features',
  search: '?token=xxxx',
  hash: '#hash',
  state: {
    fromDashboard: true
  }
}}>

4.6 理解一个完整的页面

以containers/HomePage为例,代码如下:

/*
 * HomePage
 * 简单说下结构
 */

/**-------导入部分------**/
/*定义组件,需继承React.Component或React.PureComponent*/
import React from 'react';
/**定义组件的属性类型和默认属性**/
import PropTypes from 'prop-types';
/**定义当前页面head**/
import { Helmet } from 'react-helmet';
/**导入格式化信息方法,提供国际化**/
import { FormattedMessage } from 'react-intl';
/**连接组件与store**/
import { connect } from 'react-redux';
/**组装函数,增强store功能**/
import { compose } from 'redux';
/**创建selector对象**/
import { createStructuredSelector } from 'reselect';
/**注入reducer**/
import injectReducer from 'utils/injectReducer';
/**注入saga**/
import injectSaga from 'utils/injectSaga';
/**导入App/selectors里面的方法**/
import {
  makeSelectRepos,
  makeSelectLoading,
  makeSelectError,
} from 'containers/App/selectors';
/**导入各类组件**/
import H2 from 'components/H2';
import ReposList from 'components/ReposList';
import AtPrefix from './AtPrefix';
import CenteredSection from './CenteredSection';
import Form from './Form';
import Input from './Input';
import Section from './Section';
/**导入国际化信息**/
import messages from './messages';
/**导入actions方法**/
import { loadRepos } from '../App/actions';
import { changeUsername } from './actions';
/**导入当前页面selectors里面的方法**/
import { makeSelectUsername } from './selectors';
/**导入当前页面reducer**/
import reducer from './reducer';
/**导入当前页面saga**/
import saga from './saga';

/**------------定义HomePage组件-------------**/
class HomePage extends React.PureComponent {
  /**
   * when initial state username is not null, submit the form to load repos
   */
  componentDidMount() {
    if (this.props.username && this.props.username.trim().length > 0) {
      this.props.onSubmitForm();
    }
  }

  render() {
    const { loading, error, repos } = this.props;
    const reposListProps = {
      loading,
      error,
      repos,
    };

    return (
      <article>
        <Helmet>
          <title>Home Page</title>
          <meta
            name="description"
            content="A React.js Boilerplate application homepage"
          />
        </Helmet>
        <div>
          <CenteredSection>
            <H2>
              <FormattedMessage {...messages.startProjectHeader} />
            </H2>
            <p>
              <FormattedMessage {...messages.startProjectMessage} />
            </p>
          </CenteredSection>
          <Section>
            <H2>
              <FormattedMessage {...messages.trymeHeader} />
            </H2>
            <Form onSubmit={this.props.onSubmitForm}>
              <label htmlFor="username">
                <FormattedMessage {...messages.trymeMessage} />
                <AtPrefix>
                  <FormattedMessage {...messages.trymeAtPrefix} />
                </AtPrefix>
                <Input
                  id="username"
                  type="text"
                  placeholder="mxstbr"
                  value={this.props.username}
                  onChange={this.props.onChangeUsername}
                />
              </label>
            </Form>
            <ReposList {...reposListProps} />
          </Section>
        </div>
      </article>
    );
  }
}

/**定义当前页面的props属性**/
HomePage.propTypes = {
  loading: PropTypes.bool,
  error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
  repos: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]),
  onSubmitForm: PropTypes.func,
  username: PropTypes.string,
  onChangeUsername: PropTypes.func,
};

export function mapDispatchToProps(dispatch) {
  return {
    onChangeUsername: evt => dispatch(changeUsername(evt.target.value)),
    onSubmitForm: evt => {
      if (evt !== undefined && evt.preventDefault) evt.preventDefault();
      dispatch(loadRepos());
    },
  };
}

const mapStateToProps = createStructuredSelector({
  repos: makeSelectRepos(),
  username: makeSelectUsername(),
  loading: makeSelectLoading(),
  error: makeSelectError(),
});

const withConnect = connect(
  mapStateToProps,
  mapDispatchToProps,
);

const withReducer = injectReducer({ key: 'home', reducer });
const withSaga = injectSaga({ key: 'home', saga });

/**------------导出HomePage组件-------------**/
export default compose(
  withReducer,
  withSaga,
  withConnect,
)(HomePage);

在App的index.js里:

<Helmet
    titleTemplate="%s - React.js Boilerplate"
    defaultTitle="React.js Boilerplate"
    >
    <meta name="description" content="A React.js Boilerplate application" />
</Helmet>

默认title是defaultTitle的值, titleTemplate是模板,通过HomePage里面的title和meta重新赋值。

<Helmet>
  <title>Home Page</title>
  <meta
    name="description"
    content="A React.js Boilerplate application homepage"
  />
</Helmet>

5.自己搭建简洁版react-boilerplate脚手架

将react-boilerplate项目结构了解差不多且上手体验后,自己来搭一个类似react-boilerplate框架,既能学习到很多知识点,也为自己成为全栈工程师做准备。 从该章节内容比较多,知识点也比较复杂。

5.1.1 react组件简介

与vue相比较,在写vue组件的时候,我们通常会把传统的html、js、css写在同一个文件,因此代码量不是很多。但react的jsx语法,css定义的语法、js都稍有不同,写个简单的页面比用vue写的代码量增加不是一点点,因此建议需要对组件进行拆分! 在此之前,我们需要理解几个词:页面、组件、类,并清楚它们之间的关系。 页面:我们具体看到的某个视图 组件:用react实现这个视图的代码 类:es6提供的用class关键字定义类

关于组件和页面的疑问:在react中,所有的页面皆组件,页面只是不同的组件组合,它也是一个标准的react组件。

关于组件和类的疑问:组件也是一个特殊的类,只是组件需要继承React.Component或React.PureComponent,另外组件有React的生命周期钩子方法,而且组件必须提供render方法。


5.1.2 react组件分类

讲解完页面、组件、类之间的关系后,我们来说下react中对于组件的分类。 组件的分类:普通组件、UI组件、容器组件、无状态组件。 严格意思上讲不需要分的那么细,只是概念上要理解为什么要这么分,这么分的意义和使用场景在哪儿? 在react中,如果将逻辑和渲染写在同一个组件去管理的时候,这个组件内容比较多,维护起来显得比较困难。因此建议把一个普通组件拆分成UI组件和容器组件,UI组件负责页面渲染,容器组件负责页面逻辑。


普通组件:

import React, { Component } from 'react';
class Simple extends Component {
    handleClick() { ... }
    render() {
        const { title } = this.state
        return (
        <div>
            <p style={{fontSize:'20px',color:'#FFF'}}>{ title }</p>
            <button style={{ marginTop:'40px' }} onClick={this.handleClick.bind(this)}>点击</button>
        </div>
        )
    }
}
export default Simple

5.1.3 react普通组件拆分成UI组件和容器组件

将普通组件拆分成UI组件和容器组件 UI组件:

import React, { Component } from 'react';
export default class SimpleUI extends Component {
    render() {
        return (
        <div>
            <p style={{fontSize:'20px',color:'#FFF'}}>{this.props.title}</p>
            <button style={{ marginTop:'40px' }} onClick={this.props.handleClick}>点击</button>
        </div>
        )
    }
}

容器组件:

import React, { Component } from 'react';
import SimpleUI form './SimpleUI';
export default class Simple extends Component {
    handleClick() { ... }
    render() {
        const { title } = this.state
        return (
            <SimpleUI title={title} handleClick={this.handleClick.bind(this)} />
        )
    }
}

说下这样写的意义,以前我们会将组件内的state直接渲染给页面来使用,绑定的事件也是当前组件内的方法,如果通过props将值或事件传递给渲染的页面的话,重用性会更高。


5.1.4 无状态组件:

UI组件和无状态组件对比 UI组件:

import React, { Component } from 'react';
export default class SimpleUI extends Component {
    render() {
        return (
        <div>
            <p style={{fontSize:'20px',color:'#FFF'}}>{this.props.title}</p>
            <button style={{ marginTop:'40px' }} onClick={this.props.handleClick}>点击</button>
        </div>
        )
    }
}

无状态组件:

const SimpleUI = (props) => {
    return (
    <div>
        <p style={{fontSize:'20px',color:'#FFF'}}>{props.title}</p>
        <button style={{ marginTop:'40px' }} onClick={props.handleClick}>点击</button>
    </div>
    )
}
export default SimpleUI

无状态组件和UI组件类似,写法和意义上都有不同。 无状态组件是个带props参数的匿名函数,UI组件是个普通组件,只是负责render渲染。 无状态组件和普通组件相比,性能比较高。原因是因为无状态组件仅有render的部分,普通组件有render,生命周期各种函数等等。

最后总结一下: 如果你声明的组件用不到生命周期,只是纯渲染,那么建议定义无状态组件; 如果有很多复杂的交互,建议定义UI组件和容器组件; 如果仅有几个简单交互,建议普通组件。


5.1.5 细节说明

注意两个小细节: 1.react定义state的方式

class Simple extends React.Component {
    //方式二
    state = {
        name: 'lan'
    }
    //方式一
    constructor() {
        this.state = {
            name: 'lan'
        }
    } 
}

第一种方式通过构建函数constructor里面去声明state,也是最常见的方式。 第二种方式是被babel支持转义的写法,需要安装插件plugin-proposal-class-properties。看项目package.json的devDependencies,有用到话,就可以这样写。


2.react事件处理函数为什么使用bind(this) 可以发现在写Simple组件的时候,声明的事件后面必须有bind(this),这是为什么呢? 在react中组件传递,可以是一个字符串、对象、也可以是函数。

<Simple
title={'xxx'}
data={{...}}
onClick={this.handleClick.bind(this)} />

如果写法是onClick={this.handleClick},此时onClick是中间变量,处理函数中的this指向会丢失。解决这个问题就是给调用函数时bind(this),从而使得无论事件处理函数如何传递,this指向都是当前实例化对象。


声明的Simple组件会被JSX语法转义成一个Object对象, 原理如下:

const Simple = {
    title:'xxx',
    data:{'xxx':'xxx'},
    onClick:function(){
        console.log(this.title)
    }
}
const handleClick = Simple.onClick;
handleClick();

打开Chrome控制台,复制上面的代码,当执行handleClick函数的时候,this指向已不是Simple对象,而是指向window对象,因此获取this.title的时候是undefined。


那么通过bind(this),将this指向Simple对象不就行了吗? 真正的原理如下:

const Simple = {
    title:'xxx',
    data:{'xxx':'xxx'},
    onClick:function(){
        console.log(this.title)
    }
}
const handleClick = Simple.onClick.bind(Simple);
handleClick();

将this指向Simple,调用handleClick函数的时候,this.title的值xxx就能获取到了。


3.react定义函数的方式 最后总结一下,在react中定义函数的时候,需要将this指向当前实例对象。 方式一:

import React, { Component } from 'react';
class Simple extends Component {
    handleClick() { ... }
    render() {
        return (
            <button onClick={this.handleClick.bind(this)}>点击</button>
        )
    }
}
export default Simple

方式二:

import React, { Component } from 'react';
class Simple extends Component {
    handleClick() { ... }
    constructor(props){
        super(props);
        this.handleClick = this.handleClick.bind(this)
    }
    render() {
        return (
            <button onClick={this.handleClick}>点击</button>
        )
    }
}
export default Simple

方式三:(推荐写法)

import React, { Component } from 'react';
class Simple extends Component {
    handleClick = (e)=> { ... }
    render() {
        return (
            <button onClick={this.handleClick}>点击</button>
        )
    }
}
export default Simple

方式三的原理如下:

const Simple = {
    title:'xxx',
    data:{'xxx':'xxx'},
    onClick:function(){
        console.log(this.title)
    }
}
const handleClick = Simple.onClick();
handleClick;

5.2.1 redux简介

2017年8月,在学react native的时候说过一句话:

上一篇《React-Redux基本用法》文章重点讲解了它,在这里就不做过多说明。它的存在对于React Native意义重大,其实自己现在对Redux也是一知半解,等我把APP弄上线,一定会再来学习Redux。

兑现承诺的时刻来到,我来具体细说redux。

react-redux

redux存在的意义:统一管理数据。 react在处理小型项目是完全OK的,但是react处理大型项目的时候自己是不够的。 你必须使用redux帮你去管理数据。 页面数据尽量全存在redux中存储进行管理,后面维护上会有非常大的帮助。


5.2.2 redux工作流

redux

图上是redux的完整流程,一定要重点掌握。

流程简介:

首先我们要有一个store,然后页面从store里面取数据,如果页面想改变store里面的数据,需走一个流程,首先是派发一个action给store,store把action和之前的数据一起给到reducer,reducer结合这个action和之前的数据返回一个新的数据给到store,store更新自己的数据之后,告诉页面,我的数据被更新了,页面就会自动跟着联动。

流程详细说明: 以Simple的点击onClick为例:

import React, { Component } from 'react';
import store from './store';
class Simple extends Component {
    state = store.getState()
    handleClick = ()=> { 
        //1.创建action,action是一个object对象
        const action = {
            type: 'set_change_title', //告诉store做啥事
            value: '已点击' //给store值
        }
        //2.通过dispatch传给store
        store.dispatch(action)
    }
    render() {
        const { title } = this.state
        return (
        <div className="App">
            <p>{title}</p>
            <button onClick={this.handleClick}>点击</button>
        </div>
        )
    }
}
export default Simple

3.dispatch后store会自动把它当前存储的数据和action传来的数据转发给reducer;

const defaultState = {
    title: ''
}
export default (state = defaultState ,action) => {
    // reducer根据type找到对应操作
    // 深拷贝一份原来的数据修改后生成新的数据
    if (action.type === 'set_change_title') {
        const newState = Object.assign({}, state)
        newState.title = action.value
        return newState
    }
    return state
}

4.return后reducer会自动把新的数据返回给store;

import React, { Component } from 'react';
import store from './store';
class Simple extends Component {
    state = store.getState()
    constructor(props) {
        //5.让组件订阅store,有情况,则通知组件更新
        store.subscribe(()=>{
            this.setState(store.getState())
        })
    }
    handleClick = ()=> { ... }
    render() {
        const { title } = this.state
        return (
        <div className="App">
            <p>{title}</p>
            <button onClick={this.handleClick}>点击</button>
        </div>
        )
    }
}
export default Simple

5.2.3 react与redux的整合

上面说完工作流,下面说说react与redux如何整合在一起。 1.安装redux库

yarn add redux

2.新建stote/index.js

import { createStore } from 'redux';
const store = createStore();
export default store;

3.新建store/reducer.js

const defaultState = {}
export default (state = defaultState ,action) => {
    return state
}

4.将reducer注入store

import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;

5.在react组件使用store

import React, { Component } from 'react';
import store from './store';
class Simple extends Component {
    state = store.getState()
    render() {
        const { title } = this.state
        return (
            <p>{title}</p>
        )
    }
}
export default Simple

5.2.4 react与react-redux整合

1.安装react-redux库

yarn add react-redux

2.在App.js中引入Provider,将store传入

import React, { Component } from 'react';
import Simple from './contianers/simple';
import store from './store';
import { Provider } from 'react-redux';

class App extends Component {
  render() {
    return (
    <Provider store={store}>
          <Simple/>
    </Provider>
    );
  }
}

export default App;

3.在组件containers/Simple/index.js中使用connect,实现组件和store之间的关联

import React, { Component } from 'react';
import { connect } from 'react-redux';
class Simple extends Component {
    render() {
        const { title } = this.props
        return (
            <div className="App">
                <p>{title}</p>
                <button onClick={this.props.handleClick}>点击</button>
            </div>
        )
    }
}
const mapStateToProps = (state) => {
  return {
    title: state.title
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
        handleClick () { 
            const action = {
                type: 'set_change_title',
                value: '已点击'
            }
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

4.将Simple组件替换成无状态组件,性能更高

import React from 'react';
import { connect } from 'react-redux';
const Simple = (props) => {
    return (
        <div className="App">
            <p>{props.title}</p>
            <button onClick={props.handleClick}>点击</button>
        </div>
    )
}
const mapStateToProps = (state) => {
  return {
    title: state.title
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
        handleClick () { 
            const action = {
                type: 'set_change_title',
                value: '已点击'
            }
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

5.2.5 redux优化

优化1:拆分reducer 优化目的:方便维护每个组件自己的reducer,缩减总reducer代码量压力。 优化实现:将总reducer拆分到每个组件的子reducer,最后整合。 总reducer.js

import { combineReducers } from 'redux';
import simpleReducer from '../containers/Simple/reducer';

const reducer = combineReducers({
  simple: simpleReducer
})

export default reducer;

子containers/Simple/reducer.js

const defaultState = {}
export default (state = defaultState ,action) => {
  if (action.type === 'set_change_title') {
        const newState = Object.assign({}, state)
        newState.title = action.value
        return newState
    }
    return state
}

优化2:整合action 优化目的:方便维护每个组件自己的action,减少出错。 优化实现:将每个组件自己的action整合在同一个地方,且声明reducer手册的常量,便于管理。 将Simple组件的action创建,放在containers/Simple/actions.js

import * as CONST from './constants';
export function setTitle(title) {
  return {
     type: CONST.SET_CHANGE_TITLE,
     value: title
  };
}

也可以用ES6箭头函数实现

import * as CONST from './constants';
export const setTitle = (title) => {
  return {
     type: CONST.SET_CHANGE_TITLE,
     value: title
  };
}

将action创建定义的reducer手册的常量,放在containers/Simple/constants.js

export const SET_CHANGE_TITLE = 'SET_CHANGE_TITLE';

将Simple组件的reducer.js中引用的常量,变成引用constants.js中的常量

import * as CONST from './constants';
const defaultState = {}
export default (state = defaultState ,action) => {
  if (action.type === CONST.SET_CHANGE_TITLE) {
        const newState = Object.assign({}, state)
        newState.title = action.value
        return newState
    }
    return state
}

最后Simple组件的index.js里面的action创建,变成引用actions.js中的方法

import React from 'react';
import { connect } from 'react-redux';
import { setTitle } from './actions'
const Simple = (props) => {
    return (
        <div className="App">
            <p>{props.title}</p>
            <button onClick={props.handleClick}>点击</button>
        </div>
    )
}
const mapStateToProps = (state) => {
  return {
    title: state.simple.title
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
        handleClick () { 
            const action = setTitle('已点击')
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

到这里,react与redux、react-redux的整合就全部结束了。简单总结下,整合reudx首先要一定要清晰redux的工作流程,以上面用户点击按钮后出现文字举例:

页面效果:用户在页面点击按钮,页面出现“已点击”。

redux具体流程: 用户触发点击事件,点击事件里触发一个出现文字的action; 通过dispatch将action传递给store; 接着store将action和之前的数据state一起给到reducer; reducer根据action里面的type类型去找到对应的新state返回给store; 订阅过store的组件,监听到state里面有变化后,将页面的内容更新,出现“已点击”文字。

redux简写流程:触发action-传store-查reducer-变store-页面变。

redux核心业务:变store。

附上项目架构图:

redux_project


5.2.6 redux中间件

让我们继续增强项目架构的功能,会依次讲到对dispatch和reducer的扩展和升级,首先我先讲redux中间件,然后是selectors和immutable。

redux-middleware

简单说redux中间件就是对dispatch方法的封装和升级。 常见的redux中间件有: 异步解决方案:redux-saga、redux-thunk、redux-promise、redux-actions 打印日志:redux-logger 当然,也可以自己自定义redux中间件。

异步中间件那么多,那我们该如何选择呢? 目前火的是前面两个,建议redux-saga或redux-thunk,二选一即可。

action - dispatch - store
action - 中间件 - store

我们知道action默认只能传对象object通过dispatch传给store。 redux-saga/redux-thunk中间件:支持action是对象object,也可以是函数function,如果是函数,可以将异步放在action里面操作。 为什么要这样做呢? 一般调用接口是放在componentDidMount生命周期里,一个接口代码量还能接受,万一调用几个接口,那么在componentDidMount方法里代码量就变多,也不方便维护。 别在生命周期里直接写异步请求,否则越来越复杂,越来越多,组件越来越大,正确的方案是将异步放在action里面操作, 这就是redux-saga/redux-thunk中间件诞生的意义。


5.2.7 整合redux-thunk及使用

1.安装redux-thunk库

yarn add redux-thunk

2.store/index.js加入redux-thunk中间件

//使用redux中的applyMiddleware,将redux-thunk注入到store中
import { createStore ,applyMiddleware } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';
const middlewares = [
    thunk
];
const store = createStore(reducer, applyMiddleware(...middlewares));
export default store;

3.将actions.js中的action,改成异步请求 更改前: 返回的是object对象

import * as CONST from './constants';
export const setTitle = (title) => {
  return {
     type: CONST.SET_CHANGE_TITLE,
     value: title
  };
}

更改后: 返回的是带dispatch参数的function函数

import * as CONST from './constants';
export const setTitle = (title) => {
  return (dispatch) => {
    //模拟请求
    setTimeout(()=>{
      const action = {
        type: CONST.SET_CHANGE_TITLE,
        value: title
      }
      dispatch(action)
    }, 1000)
  }
}

5.2.8 整合redux-saga

介绍完redux-thunk中间件,在介绍下redux-saga中间件 1.安装redux-saga库

yarn add redux-saga

2.store/index.js加入redux-saga库中间件

import { createStore ,applyMiddleware } from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
import sagas from './sagas';
const sagaMiddleware = createSagaMiddleware();
const middlewares = [
    sagaMiddleware
];
const store = createStore(reducer, applyMiddleware(...middlewares));
sagas(sagaMiddleware.run)
export default store;

3.新增总sagas和子sagas store/sagas.js:遍历Generator函数,去运行sagaMiddleware.run

import simpleSagas from '../containers/Simple/sagas';
export default  (runSagas) => {
  const allSagas = [
    ...simpleSagas
  ];
  allSagas.map(runSagas);
}

Simple/sagas.js:里面必须是ES6 Generator 函数, 和普通函数相比,多了两个特征,一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式。 在获取请求数据成功后,调用设置title的action

import { takeEvery,put } from 'redux-saga/effects';
import { WATCH_CHANGE_TITLE } from './constants';
import { setTitle } from './actions'

let ajax = function* (){
    const data = yield new Promise(function(resolve, reject){
        setTimeout(()=>{
            resolve('已点击');
        },1000);
    });
    const action = setTitle(data);
    yield put(action);
};
function* mySaga() { 
  yield takeEvery(WATCH_CHANGE_TITLE, ajax)
}
export default [
    mySaga
];

4.在actions.js里面增加设置title的action 监听title的action,不需要加任何参数

import * as CONST from './constants';
export const setTitle = (title) => {
  return {
     type: CONST.SET_CHANGE_TITLE,
     title
  };
}

export const watchTitle = (title) => {
  return {
     type: CONST.WATCH_CHANGE_TITLE
  };
}

5.在constants.js里面增加设置title的type

export const SET_CHANGE_TITLE = 'SET_CHANGE_TITLE';
export const WATCH_CHANGE_TITLE = 'WATCH_CHANGE_TITLE';

6.在reducer.js里面修改设置title的判断

import * as CONST from './constants';
const defaultState = {}
export default (state = defaultState ,action) => {
  if (action.type === CONST.SET_CHANGE_TITLE) {
        const newState = Object.assign({}, state)
        newState.title = action.title
        return newState
    }
    return state
}

7.在Simple组件里面去调用监听title的action

import React from 'react';
import { connect } from 'react-redux';
import { watchTitle } from './actions'
const Simple = (props) => {
    return (
        <div className="App">
            <p>{props.title}</p>
            <button onClick={props.handleClick}>点击</button>
        </div>
    )
}
const mapStateToProps = (state) => {
  return {
    title: state.simple.title
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
        handleClick () { 
            const action = watchTitle()
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

5.2.9 redux-saga常见使用

前面在Simple/saga.js中仅使用到takeEvery、put两个方法,它们究竟是啥? 具体api见redux-saga官方文档: https://redux-saga.js.org/docs/api redux-saga中文文档: https://redux-saga-in-chinese.js.org/docs/api 下面使用常见的api进行异步操作:

import { takeEvery, takeLatest, put, call } from 'redux-saga/effects';
import { WATCH_CHANGE_TITLE } from './constants';
import { setTitle } from './actions'

let ajax = function* (){
    // put (启动一个action)
    // call (阻塞地调用一个函数)
    // fork (非阻塞地调用一个函数)
    // take (监听且只监听一次action)
    // delay(延迟)
    // race (只处理最先完成的任务)
    const data = yield new Promise(function(resolve, reject){
        setTimeout(()=>{
            resolve('已点击');
        },1000);
    });
    yield call((msg)=>{console.log('请求数据成功:',msg)}, data);
    const action = setTitle(data);
    yield put(action);
};

function* watchTitleSaga() { 
    /**
    允许并发
    同时处理多个相同的action,全部执行
    **/
    yield takeEvery(WATCH_CHANGE_TITLE, ajax)
    /**
    不允许并发
    同时处理多个相同的action,
    之前有action在处理中,则取消之前,
    只执行最后一次
    **/
    yield takeLatest(WATCH_CHANGE_TITLE, ajax)
}

export default [
  watchTitleSaga
];

redux-saga

上面是sagas的流程图,总的来看redux-saga中间件还是蛮有难度的,总结一下它。 传统无中间件,用redux-thunk,用redux-saga对比:

// 无中间件,action是object对象
// { type: xxx, title: xxxx }
export const setTitle = (title) => {
  return {
     type: CONST.SET_CHANGE_TITLE,
     title
  };
}
// 调用dispatch,action传入store,store将值自动传入reducer
const action = { type: xxx, title: xxxx }
store.dispatch(action)

/**-------------------------------------------------**/
// redux-thunk,action是function函数
// (dispatch) => { dispatch(action) }
export const setTitle = (title) => {
  return (dispatch) => {
    //模拟请求
    setTimeout(()=>{
      const action = {
        type: CONST.SET_CHANGE_TITLE,
        value: title
      }
      dispatch(action)
    }, 1000)
  }
}

// 调用dispatch,执行actoin函数里面的内容,执行完后再回调dispatch自身,将action传入store,store将值自动传入reducer
// 相当于多了一步获取异步请求数据方法,获取数据成功后回调自身再继续执行
const action = (dispatch) => { 
    ...
    dispatch({ type: xxx, title: xxxx }) 
}
store.dispatch(action)

/**-------------------------------------------------**/

// redux-saga,action是object对象
// {type: 'watch_xxxx'}
export const watchTitle = (title) => {
  return {
     type: CONST.WATCH_CHANGE_TITLE
  };
}
export const setTitle = (title) => {
  return {
     type: CONST.SET_CHANGE_TITLE,
     title
  };
}

/**-------------------------------------------------**/
// 提前创建与之对应的action监听函数
// 且该函数必须是Generator函数
const const ajax = function* (){
    const xxxx = ...yield ''
    const action = { type: xxx, title: xxxx }
    put(action)
}
function* watchTitleSaga() {
    yield takeLatest('watch_xxxx', ajax)
}

// 调用Generator函数中,执行该函数里面的内容,执行完后再调put启动它,将action传入store,store将值自动传入reducer
// 相当于多了一个监听action方法,获取数据成功后再dispatch继续执行,这里的dispatch就是put
const action = {type: 'watch_xxxxx'}
store.dispatch(action)

5.2.10 自定义redux中间件

声明空的中间件格式:

const template = store => next => action => { next(action) }

讲解完redux-thunk、redux-saga中间件后,我们也可以自定义适合自己业务的中间件。 比如实现一个打印log日志的中间件:

import { createStore ,applyMiddleware } from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
import sagas from './sagas';
const sagaMiddleware = createSagaMiddleware();
/**
 * 自定义log中间件
 * @param store
 */
const logger = store => next => action => {
    if (typeof action === 'function') {
        console.log('dispatching a function');
    } else {
        console.log('dispatching ', action);
    }
    const result = next(action);
    console.log('nextState ', store.getState());
    return result;
};
// 整合自定义logger中间件
const middlewares = [
  logger,
    sagaMiddleware
];
const store = createStore(reducer, applyMiddleware(...middlewares));
sagas(sagaMiddleware.run)
export default store;

最后来个总结,除开redux,react的学习其实蛮简单的,一旦加入redux,项目的复杂性都会逐级递增。 reudx存在的理由也很充分,主要针对中大型项目,比如公司、企业项目,那么必须通过store方便管理整个项目的组件状态。 随着业务的增加,项目肯定也越来越大,没有一整套规范和流程化开发,效率会降低。 小型项目,比如个人、私人项目,那么没必要去使用redux,通过react原始的state和prop传递,基本能实现所有功能。如果还是想再规范点,希望加入状态管理的话,我推荐使用类似redux的轻量级框架mobx

介绍完中间件,接下来还需要优化数据state,优化有两个地方,一个是reducer,另一个是mapStateToProps。


5.2.11 增加immutable库,提升reducer state性能

reducer相当于一个api手册,类似java的swagger,告诉action做什么。 reducer可以接收state,但绝不能修改state,需要返回新的state。 为避免出错,immutable.js可以将reducer的state对象变成不可改变的对象,这样每次reducer的state是都是新的state。 1.安装immutable.js库

yarn add immutable

2.将Simple/reducer.js的defaultState用fromJS方法转换对象

import * as CONST from './constants';
import { fromJS } from 'immutable';
export const defaultState = fromJS({
  count:0
});
export default (state = defaultState ,action) => {
  if (action.type === CONST.SET_CHANGE_TITLE) {
      // const newState = Object.assign({}, state)
      const newState = state.set('count', action.count)
        return newState;
    }
    return state
}

3.将Simple/index.js的js中simple js对象获取方式换成immutable对象的获取方式

import React from 'react';
import { connect } from 'react-redux';
import { watchTitle } from './actions';
const Simple = (props) => {
    return (
        <div className="App">
            <p>{props.count}</p>
            <button onClick={props.handleClick}>点击</button>
        </div>
    )
}
const mapStateToProps = (state) => {
  return {
      // count:state.simple.count
    count:state.simple.get('count')
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
        handleClick () { 
            const action = watchTitle()
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

5.2.12 增加redux-immutable库,统一对象

在上面的实例中,我们通过fromJS方法将state.simple对象成功转换成immutable对象,但是state对象不是,为了统一规范,还需要将最外层的state对象换成immutable对象。 1.安装redux-immutable库

yarn add redux-immutable

2.将总reducer,store/reducer.js的combineReducers从redux引入换成redux-immutable引入

// import { combineReducers } from 'redux';
import { combineReducers } from 'redux-immutable';
import simpleReducer from '../containers/Simple/reducer';
const reducer = combineReducers({
  simple: simpleReducer
});
export default reducer;

3.将Simple/index.js的js中state js对象获取方式换成immutable对象的获取方式

import React from 'react';
import { connect } from 'react-redux';
import { watchTitle } from './actions';
const Simple = (props) => {
    return (
        <div className="App">
            <p>{props.count}</p>
            <button onClick={props.handleClick}>点击</button>
        </div>
    )
}
const mapStateToProps = (state) => {
  return {
        // count:state.simple.count
    // count:state.get('simple').get('count')
        count:state.getIn(['simple', 'count'])
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
        handleClick () { 
            const action = watchTitle()
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

最后总结一下, immutable可以将普通js对象转换成immutable对象,通过fromJS方法。反之,将immutable对象转换成普通js对象,通过toJS方法。 set或setIn方法返回一个全新的state; get或getIn方法获取state的属性值。 更多方法,请参考官方文档: https://immutable-js.github.io/immutable-js/docs


5.2.13 增加reselect库,提升mapStateToProps计算性能

mapStateToProps也被叫做selector,在store发生变化的时候就会被调用,而不管是不是selector关心的数据发生改变它都会被调用,所以如果selector计算量非常大,每次更新都需要重新计算会带来性能问题。reselect能帮你省去这些没必要的重新计算。

1.安装reselet库

yarn add reselect

2.在组件创建一个Simple/selectors.js

import { createSelector, createStructuredSelector } from 'reselect';

const count = state => state.getIn(['simple', 'count']);
const countTotal = count => count * 3 +1;

const title = state => state.getIn(['simple', 'obj', 'title']);
const desc =  state => state.getIn(['simple', 'obj', 'desc']);


const descSelector = createSelector(
  desc,
  desc => desc
)

const countTotalSelector = createSelector(
  count,
  count => countTotal(count)
)

const titleSelector = createStructuredSelector({ title })

export {
  descSelector,
  countTotalSelector,
  titleSelector
}

3.在Simple/index.js中使用selectors

import React from 'react';
import { connect } from 'react-redux';
import { watchTitle, watchObject } from './actions';
import { descSelector, countTotalSelector, titleSelector } from './selectors';
const Simple = (props) => {
    return (
        <div className="App">
            <p>{props.count}</p>
            <p>{props.obj.title}</p>
            <p>{props.desc}</p>
            <button onClick={props.handleClick}>点击</button>&nbsp;&nbsp;
            <button onClick={props.handleClick2}>点击2</button>
        </div>
    )
}
const mapStateToProps = (state) => {
    return {
        desc: descSelector(state),
        count: countTotalSelector(state),
        obj:titleSelector(state),
    }
}
const mapDispatchToProps = (dispatch) => {
    return {
        handleClick () { 
            const action = watchTitle()
            dispatch(action)
        },
        handleClick2 () { 
            const action = watchObject()
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

4.在Simple/reducer.js中使用action传来的count和obj

import * as CONST from './constants';
import { fromJS } from 'immutable';
export const defaultState =fromJS ({
  count:0,
  obj:{
    title:'hello',
    desc:'xxx'
  }
});
export default (state = defaultState ,action) => {
  if (action.type === CONST.SET_CHANGE_TITLE) {
      const newState = state.set('count', action.count)
        return newState;
    }
    if (action.type === CONST.SET_OBJECT) {
      const newState = state.setIn(['obj', 'title'], action.obj.title)
        return newState;
    }
    return state
}

总结一下reselct:

createSelector([arg1,arg2, ...], resultFun)
createSelector(arg, resultFun)
createStructuredSelector(obj)

reselect提供一个createSelector方法来创建一个记忆selectors。 createSelector接收一个值或数组 和 一个转换方法作为参数。 如果redux的state发生改变引起一个参数arg的值发生改变时,selector会调用转换方法,返回一个结果。 如果input-selector返回的结果和前面的一样,则它将返回先前计算的值,而不是调用转换方法。

reselect提供一个createStructuredSelector方法接收一个对象,该方法返回一个selector对象。 selector对象的键和传入的参数的键是相同的,但是使用传入的值替换其中的值。

reselect主要解决的是在组件交互操作的时候,state发生变化的时候如何减少渲染的压力。


看到这,自己搭建的类似react-boilerplate框架已经完成98%。 react-boilerplate框架的页面目录格式:

react_container_modle


自己搭建的页面目录格式:

react_my_conatiner_model


5.3 react-router路由

整合完redux一系列库后,离项目的完整架构就差两步,那就是路由和样式。 路由控制着整个项目的页面跳转、返回等操作,在web应用中用的最多的是react-router-dom,在rn应用中用的最多的是react-navigation。 不建议直接在组件中引入css,因为组件之间css的样式可能会发生冲突,因此推荐使用styled-component。首先我先讲解下路由的引入,很简单。

1.安装react-router-dom库

yarn add react-router-dom

2.App.js中增加BrowserRouter、Route组件

import React, { Component } from 'react';
import store from './store';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';

import Simple from './containers/Simple';
import About from './containers/About';

class App extends Component {
  render() {
    // 根据相应路由渲染对应的组件
    // exact 完全匹配,如/a,/a可以访问,/a/b不可以访问
    // strict 严格匹配,如/a,/a和/a/b都可以访问
    return (
    <Provider store={store}>
      <BrowserRouter>
        <div id="app">
          <Route exact path='/' component={Simple}></Route>
          <Route exact path='/about' component={About}></Route>
        </div>
      </BrowserRouter>
    </Provider>
    );
  }
}
export default App;

3.实现页面跳转 在Simple/index.js中加入Link组件

import React from 'react';
import { connect } from 'react-redux';
import { watchTitle, watchObject } from './actions';
import { descSelector, countTotalSelector, titleSelector } from './selectors';
import { Link } from 'react-router-dom';
const Simple = (props) => {
    return (
        <div className="App">
            <p>{props.count}</p>
            <p>{props.obj.title}</p>
            <p>{props.desc}</p>
            <button onClick={props.handleClick}>点击</button>&nbsp;&nbsp;
            <button onClick={props.handleClick2}>点击2</button>&nbsp;&nbsp;
            <Link to="/about">
                <button>跳转about</button>
            </Link>
        </div>
    )
}
const mapStateToProps = (state) => {
    return {
        desc: descSelector(state),
        count: countTotalSelector(state),
        obj:titleSelector(state),
    }
}
const mapDispatchToProps = (dispatch) => {
    return {
        handleClick () { 
            const action = watchTitle()
            dispatch(action)
        },
        handleClick2 () { 
            const action = watchObject()
            dispatch(action)
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Simple)

5.4 styled-components样式组件

styled-components可以将react组件之间的样式独立,这样避免组件间样式重名发生冲突。 讲解styled-components的引入,也很简单。 1.安装styled-components库和normalize.css库

yarn add styled-components
yarn add normalize.css

2.根目录下新建global-styles.js,修改App.js global-styles.js中引入styled-components创建全局的样式

import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  html,
  body {
    height: 100%;
    width: 100%;
  }

  body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  }

  body.fontLoaded {
    font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  }

  #app {
    background-color: #fafafa;
    min-height: 100%;
    min-width: 100%;
  }

  p,
  label {
    font-family: Georgia, Times, 'Times New Roman', serif;
    line-height: 1.5em;
  }
`;

export default GlobalStyle;

App.js中导入全局样式

import React, { Component } from 'react';
import store from './store';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';

import Simple from './containers/Simple';
import About from './containers/About';

//引入全局样式,记得在根组件内加上该引入
import GlobalStyle from './global-styles.js';

class App extends Component {
  render() {
    // 根据路由渲染对应的组件
    // exact 完全匹配,如/a,/a可以访问,/a/b不可以访问
    // strict 严格匹配,如/a,/a和/a/b都可以访问
    return (
    <Provider store={store}>
      <BrowserRouter>
        <div id="app">
          <Route exact path='/' component={Simple}></Route>
          <Route exact path='/about' component={About}></Route>
          <GlobalStyle/>
        </div>
      </BrowserRouter>
    </Provider>
    );
  }
}
export default App;

3.修改index.js、引入重置normalize.css 浏览器的不同,导致默认的css会有所不同,我们需要统一。常见的重置css有:reset.cssnormalize.csssanitize.css。 三个相比较的话,reset.css要暴力得多,normalize.css和sanitize.css相对温柔一点。 因为reset通过为几乎所有的元素施加默认样式,强行使得元素有相同的视觉效果。相比之下,normalize.css和sanitize.css保持了许多默认的浏览器样式。个人推荐的话,选normalize.css,尽管react-boilerplate选择的是sanitize.css。 index.js中去掉用index.css,导入normalize.css

import React from 'react';
import ReactDOM from 'react-dom';
//import './index.css'; //注释或直接删掉它
import 'normalize.css'; //导入normalize.css
import App from './App';
import * as serviceWorker from './serviceWorker';

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

4.使用styled,在需要定义样式的组件或页面中 在container/About下新建Wrapper.js 定义一个AboutWrapper,可理解成也是一个组件, 组件名叫AboutWrapper,定义一个div及其内联css样式,导出该组件

import styled from 'styled-components';

const AboutWrapper = styled.div`
  width: auto;
  height: auto;
`;

export default AboutWrapper;

5.使用定义好的内联css组件 container/About/index.js

import React, { Component } from 'react';
import ComptOne from '../../components/ComptOne';
import ComptTwo from '../../components/ComptTwo';
import AboutWrapper from './Wrapper';

class About extends Component {
  render() {
    return (
      <AboutWrapper className='About'>
        <ComptOne></ComptOne>
        <ComptTwo></ComptTwo>
      </AboutWrapper>
    )
  }
}

export default About;

因为定义的都是内联css,且css名随机且不重复,因此不会发生重名,也就不会发生css样式冲突。 使用styled注意两点:1.千万别加引号,如display:"flex" 2.可以实现css继承,不需要额外引入sass、less。

最后,类似react-boilerplate框架的搭建大功告成,看下最终结构图。(可缩放看图)

me

最后说说自己对IT行业或研发的看法,我写不出优秀的组件、但是能用大牛们封装且优秀的组件,我也觉得很知足。 因为,在我的眼中大牛有三种: 1.能自食其力,独立自主地研发一个较成熟的框架或爱与别人分享。(vue创始人尤雨溪/阮一峰大神) 2.能对技术有自己独特的见解,愿意深入学习底层知识及原理,如算法、数据结构、设计模式等。(架构师、专家) 3.能了解各种编程语言、框架及插件,根据其优势能熟练地运用到不同的项目或创业中。 我不想成为架构师、也不想当专家,只想成为一名普普通通的全栈工程师。 我喜欢专注自己喜欢的语言和框架,能将其优势熟练地运用到不同的项目或创业中。 我从不专注于自己发明一个框架出来,也从不专注于将曾用过的工具或框架都刨根问底,看其怎么实现,但是马云曾说过:我这个人性格之中喜欢挑战变化,我爸从小希望我专注一样东西,但是我永远没专注过。我认为不专注就是一个最大的专注。 不需要随波逐流、随心走、简简单单做自己就好!

附上自己的demo源码,有兴趣的同学可作学习参考:https://github.com/ww930912/react-boilerplate-demo

Headshot of Maxi Ferreira

怀着敬畏之心,做好每一件事。