STAY HUNGRY , STAY FOOLISH.

求知若饥,虚心若愚。

       浏览:

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

最近分配到公司其他项目组,主要技术栈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。

基本目录讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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绑定

执行的常用命令:

1
2
3
4
5
6
7
8
// 创建新vue项目
vue create project_name

// 开发模式
npm run serve

// 编译打包
npm run build

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

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"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:

1
2
3
4
// 仅依赖vue
"dependencies": {
"vue": "^2.5.21"
}

main.js:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
// 依赖vue、vue-router、vuex
"dependencies": {
"vue": "^2.5.21",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
}

main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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目录蛮类似的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
5
6
7
8
9
//依赖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:

1
2
3
4
5
6
7
8
9
10
11
12
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* 主入口
*/

// 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,该项目依赖哪些第三方库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"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中引入它,将它设置成默认

1
2
3
4
5
6
7
8
9
10
11
12
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里的文案

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

成功:

react_intl_ok


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

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

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

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

Wrapper.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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样式继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 }的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...//export defaultimport
//如果用的import xxx导入,那么导出方式一定是export default
// 导入styled
import styled from 'styled-components';

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

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

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

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

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

3.导入别名的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//多个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

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 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部分

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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片段如下:

1
2
3
4
5
6
7
8
9
<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:

1
2
3
4
export default styled(Link)`
xxxxx
....
`

Link用法一:
string

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

Link用法二:
object

1
2
3
4
5
6
7
8
<HeaderLink to= {{
pathname: '/features',
search: '?token=xxxx',
hash: '#hash',
state: {
fromDashboard: true
}
}}>

4.6 理解一个完整的页面

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/*
* 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里:

1
2
3
4
5
6
<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重新赋值。

1
2
3
4
5
6
7
<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组件负责页面渲染,容器组件负责页面逻辑。


普通组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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组件:

1
2
3
4
5
6
7
8
9
10
11
12
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>
)
}
}


容器组件:

1
2
3
4
5
6
7
8
9
10
11
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组件:

1
2
3
4
5
6
7
8
9
10
11
12
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>
)
}
}

无状态组件:

1
2
3
4
5
6
7
8
9
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的方式

1
2
3
4
5
6
7
8
9
10
11
12
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中组件传递,可以是一个字符串、对象、也可以是函数。

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

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


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

1
2
3
4
5
6
7
8
9
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对象不就行了吗?
真正的原理如下:

1
2
3
4
5
6
7
8
9
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指向当前实例对象。
方式一:

1
2
3
4
5
6
7
8
9
10
import React, { Component } from 'react';
class Simple extends Component {
handleClick() { ... }
render() {
return (
<button onClick={this.handleClick.bind(this)}>点击</button>
)
}
}
export default Simple

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

方式三:(推荐写法)

1
2
3
4
5
6
7
8
9
10
import React, { Component } from 'react';
class Simple extends Component {
handleClick = (e)=> { ... }
render() {
return (
<button onClick={this.handleClick}>点击</button>
)
}
}
export default Simple

方式三的原理如下:

1
2
3
4
5
6
7
8
9
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为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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库

1
yarn add redux

2.新建stote/index.js

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

3.新建store/reducer.js

1
2
3
4
const defaultState = {}
export default (state = defaultState ,action) => {
return state
}

4.将reducer注入store

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

5.在react组件使用store

1
2
3
4
5
6
7
8
9
10
11
12
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库

1
yarn add react-redux

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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之间的关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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组件替换成无状态组件,性能更高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

1
2
3
4
5
6
7
8
import { combineReducers } from 'redux';
import simpleReducer from '../containers/Simple/reducer';

const reducer = combineReducers({
simple: simpleReducer
})

export default reducer;

子containers/Simple/reducer.js

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
import * as CONST from './constants';
export function setTitle(title) {
return {
type: CONST.SET_CHANGE_TITLE,
value: title
};
}

也可以用ES6箭头函数实现

1
2
3
4
5
6
7
import * as CONST from './constants';
export const setTitle = (title) => {
return {
type: CONST.SET_CHANGE_TITLE,
value: title
};
}

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

1
export const SET_CHANGE_TITLE = 'SET_CHANGE_TITLE';

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

1
2
3
4
5
6
7
8
9
10
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中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,二选一即可。

1
2
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库

1
yarn add redux-thunk

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

1
2
3
4
5
6
7
8
9
//使用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对象

1
2
3
4
5
6
7
import * as CONST from './constants';
export const setTitle = (title) => {
return {
type: CONST.SET_CHANGE_TITLE,
value: title
};
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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库

1
yarn add redux-saga

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

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
import simpleSagas from '../containers/Simple/sagas';
export default (runSagas) => {
const allSagas = [
...simpleSagas
];
allSagas.map(runSagas);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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,不需要加任何参数

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
export const SET_CHANGE_TITLE = 'SET_CHANGE_TITLE';
export const WATCH_CHANGE_TITLE = 'WATCH_CHANGE_TITLE';

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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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进行异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 无中间件,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中间件

声明空的中间件格式:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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库

1
yarn add immutable

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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对象的获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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库

1
yarn add redux-immutable

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

1
2
3
4
5
6
7
// 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对象的获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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库

1
yarn add reselect

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:

1
2
3
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库

1
yarn add react-router-dom

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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库

1
2
yarn add styled-components
yarn add normalize.css

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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中导入全局样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
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样式,导出该组件

1
2
3
4
5
6
7
8
import styled from 'styled-components';

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

export default AboutWrapper;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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