最近分配到公司其他项目组,主要技术栈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样式组件
- react组件
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文件包含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报错时的提示信息:
使用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里面最常用的第三方库当属vue-router和vuex,一个负责页面路由跳转、另一个负责状态管理。 其余的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搭出的基本目录结构:
基本目录讲解,和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脚手架工具。
说了这么多,我们来大致看下目录基本结构:
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。
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
先讲下大致的,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 功能一:添加支持中文国际化
当前页面的国际化是英文和德文,没有中文,我们可以完成一个小功能来学习它。将页面底部添加一个中文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协议,开源免费",
成功:
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增加模块体系export
和import
。
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。
redux存在的意义:统一管理数据。 react在处理小型项目是完全OK的,但是react处理大型项目的时候自己是不够的。 你必须使用redux帮你去管理数据。 页面数据尽量全存在redux中存储进行管理,后面维护上会有非常大的帮助。
5.2.2 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。
附上项目架构图:
5.2.6 redux中间件
让我们继续增强项目架构的功能,会依次讲到对dispatch和reducer的扩展和升级,首先我先讲redux中间件,然后是selectors和immutable。
简单说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
];
上面是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>
<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框架的页面目录格式:
自己搭建的页面目录格式:
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>
<button onClick={props.handleClick2}>点击2</button>
<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.css、normalize.css、sanitize.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框架的搭建大功告成,看下最终结构图。(可缩放看图)
最后说说自己对IT行业或研发的看法,我写不出优秀的组件、但是能用大牛们封装且优秀的组件,我也觉得很知足。
因为,在我的眼中大牛有三种:
1.能自食其力,独立自主地研发一个较成熟的框架或爱与别人分享。(vue创始人尤雨溪/阮一峰大神)
2.能对技术有自己独特的见解,愿意深入学习底层知识及原理,如算法、数据结构、设计模式等。(架构师、专家)
3.能了解各种编程语言、框架及插件,根据其优势能熟练地运用到不同的项目或创业中。
我不想成为架构师
、也不想当专家
,只想成为一名普普通通的全栈工程师
。
我喜欢专注自己喜欢的语言和框架,能将其优势熟练地运用到不同的项目或创业中。
我从不专注于自己发明一个框架出来,也从不专注于将曾用过的工具或框架都刨根问底,看其怎么实现,但是马云曾说过:我这个人性格之中喜欢挑战变化,我爸从小希望我专注一样东西,但是我永远没专注过。我认为不专注就是一个最大的专注。
不需要随波逐流、随心走、简简单单做自己就好!
附上自己的demo源码,有兴趣的同学可作学习参考:https://github.com/ww930912/react-boilerplate-demo。