本文主要内容
写react和传统的js差不多。只是有一个设计思想贯穿了整个框架。用一个公式来表达就是:
// 状态机模型
UI=f(state)
在国内最出名的react组件库就是antD了。
官方文档:https://ant.design/index-cn
npm install antd --save
在组件中可以这么用:
import React, { Component } from 'react'
import Button from 'antd/lib/button'
import 'antd/dist/antd.css'
export default class Test extends Component {
render() {
return (
<div>
<Button>aaa</Button>
</div>
)
}
}
这是一种低效且lowB的方式:用起来繁琐,加载的是整个antd的css。实际生产中需要做按需加载。
上讲留了一个坑,就是css全局污染问题。如何避免?
npm run eject
// react-scripts eject
执行后多了一个config文件夹,可以获得完整的webpack.config.js的控制权。
然后以 webpack.config.dev.js
为例,找到:
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use a plugin to extract that CSS to a file, but
// in development "style" loader enables hot editing of CSS.
{
test: /\.css$/,
loader: 'style!css?importLoaders=1!postcss'
},
postcss loader 将autoprefixer应用于CSS。 css loader 解析css中的路径并将静态资源作为依赖项添加。 style loader 将CSS转换为注入
<style>
标记的JS模块。 在生产环境中,我们使用插件将该CSS提取到文件中,但是 在开发环境下,style loader启用CSS的热编辑。
做如下修改:
{
test: /\.css$/,
loader: "style-loader!css-loader?modules"
},
上面代码中,关键的一行是 style-loader!css-loader?modules
,它在 css-loader
后面加了一个查询参数 modules
,表示打开 CSS Modules 功能。
使用时:
/* App.module.css */
.aaa {
color: red
}
.bbb{
color: blue
}
然后导入:
import style from './App.module.css'
// ...
render() {
return (
<div className={style.aaa} >
111<div className={style.bbb}>222</div>
</div>
)
}
即可实现局部应用css
上图中,class为aaa的不会生效。
做按需加载用 eject
实在是太不优雅了,优雅实现需要引入以下三个依赖
react-app-rewired
:辅助启动customize-cra
:可扩展webpack的配置 ,类似vue.config.jsbabel-plugin-import
库npm install react-app-rewired customize-cra babel-plugin-import -D
在根目录创建 config-overrides.js
:
// 无损复写webpack配置
// override返回一个函数,以下会成为一个webpack配置对象。
const {override,fixBabelImports}=require('customize-cra')
module.exports=override(
fixBabelImports('import',{
libraryName:'antd',
libraryDirectory:'es',//引入antd组件库下的es下的css
style:'css'
})
)
// 源码
const override = (...plugins) => flow(...plugins.filter(f => f));
//flow源码:参数是由函数组成数组,返回一个任意类型的值。转化为webpack配置
flow(...func: Array<Many<(...args: any[]) => any>>): (...args: any[]) => any;
// fixBabelImports源码,库名,配置
const fixBabelImports = (libraryName, options) =>
addBabelPlugin([
"import",
Object.assign(
{},
{
libraryName
},
options
),
`fix-${libraryName}-imports`
]);
同时修改 package.json
"scripts":{
"start":"react-app-rewired start",
"build":"react-app-rewired build",
"test":"react-app-rewired test",
"eject":"react-app-rewired eject"
}
那么在应用中可以实现按需加载antd组件:
import {Button} from 'antd'
基本原则:容器组件负责数据获取,展示组件根据props获取信息。
import React, { Component } from "react";
// 展示组件
function Comment({ data }) {
return (
<div>
<p>{data.body}</p>
<p> --- {data.author}</p>
</div>)
}
// 容器组件
export default class CommentList extends Component {
constructor(props) {
super(props);
this.state = {
comments: []
};
}
componentDidMount() {
setTimeout(() => {
this.setState({
comments: [
{ body: "react is very good", author: "facebook" },
{ body: "vue is very good", author: "youyuxi" }
]
});
}, 1000);
}
render() {
return (
<div>
{this.state.comments.map((c, i) => (
<Comment key={i} data={c} />
))}
</div>
);
}
}
这个页面将在1s后打印留言列表。
Comment非常纯粹,毫无业务逻辑。这样写有许多优势:
展示型组件最好用函数实现。
假设页面请求是每隔1s进行的轮询。我们在 Comment
组件内打印记录,会很好调试渲染次数。(每秒渲染2次)
再假如,轮询的数据常年不变,开发者并不希望频繁渲染,应该怎么做呢?
固然可以用 shouldComponentUpdate(nextProps)
。但是需要一一对原来的数据进行判断。非常繁琐。
先介绍一下 PureComponent,平时我们创建 React 组件一般是继承于 Component,而 PureComponent 相当于是一个更纯净的 Component,对更新前后的数据进行了一次浅比较。只有在数据真正发生改变时,才会对组件重新进行 render。因此可以大大提高组件的性能。
可以把Comment改写为Class形式:
// 父组件直接把props传进去
<Comment key={i} {...c} />
// 展示组件
class Comment extends PureComponent {
render() {
console.log('render Comment')
let {body,author}=this.props;
return (
<div>
<p>{body}</p>
<p> --- {author}</p>
</div>)
}
}
PureComponent由15.3版本引入。现在可以阅读一下它的源码
(地址:https://github.com/facebook/react/blob/master/packages/react/src/ReactBaseClasses.js):
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
可以看到 PureComponent 的使用和 Component 一致,只时最后为其添加了一个 isPureReactComponent 属性。ComponentDummy 就是通过原型模拟继承的方式将 Component 原型中的方法和属性传递给了 PureComponent。同时为了避免原型链拉长导致属性查找的性能消耗,通过 Object.assign 把属性从 Component 拷贝了过来。
* `PureComponent` implements a shallow comparison on props and state and returns true if any props or states have changed.
它首先继承了普通React.Component的方法,然后shouldComponentUpdate函数被复写:
import is from './objectIs';
const hasOwnProperty = Object.prototype.hasOwnProperty;
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 先判断引用地址
if (is(objA, objB)) {
return true;
}
// 判断类型
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// 再看属性长度
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// 浅层比较-出于性能考虑。
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
export default shallowEqual;
由于比较方式是浅比较(引用地址,key),注意传值方式,值类型或者地址不变的且仅根属性变化的引用类型才能享受该特性。
还是不够优雅,尝试使用memo:
React 16.6.0 使用 React.memo 让函数式的组件也有PureComponent的功能。
// 展示组件=>高阶组件
const Comment=React.memo(data=>{
console.log('render Comment')
return (
<div>
<p>{data.body}</p>
<p> --- {data.author}</p>
</div>)
})
问题来了,memo是什么?
高阶组件(HOC,Higher-Order Components)是React非常重要的扩展组件方式。高阶组件是React中重用组件逻辑的高级技术,它不是react的api,而是一种组件增强模式。高阶组件是一个函数,它返回另外一个组件,产生新的组件可以对被包装组件属性进行包装,甚至重写部分生命周期。
设计组件时,粒度需要尽可能小,同时尽可能复用。但是在非常复杂的情况下,就需要对粒度很小的组件进行处理。这就是高阶组件的诞生背景。在官方文档中更加推荐这种写法,甚于传统的继承写法。
需求:考虑有这样一个aaa组件
function aaa(props){
return <div>{props.stage}-{props.name}</div>
}
在某种场景下,我需要让stage固定为react,意味着需要对aaa进行进一步封装:
const withStage = (Component) => {
const NewComponent = (props) => {
return <Component {...props} stage="react" />;
};
return NewComponent;
};
// 导出:
export default withStage(aaa)
那么使用时可以肆意搞起来了:
import Hoc from './withStage'
// ...
render(){
return (
<div>
<Hoc name="aaa"/>
<Hoc name="aaabbb"/>
</div>
);
}
渲染结果:
上述withStage组件,代理了Component,只是多传了一个name参数。
name可以用ajax请求获得,或者继承于其它组件。
withRouter也是一个高阶组件。
链式调用就是一个中间件。
假如我要做一个日志记录。
import React, { Component } from 'react'
import { Button } from 'antd'
const withHoc = (Component) => {
const NewComponent = (props) => {
return <Component {...props} name="my-Hoc" />;
};
return NewComponent;
};
const withLog = Component => {
class NewComponent extends React.Component {
componentDidMount() {
console.log('didMount', this.props)
}
render() {
return <Component {...this.props} />;
}
}
return NewComponent
}
class App extends Component {
render() {
return (
<div className="App">
<h2>hi,{this.props.name}</h2>
<Button type="primary">Button</Button>
</div>
)
}
}
export default withHoc(withLog(App))
以上链式调用实现中,这种嵌套其实很难看。es7中支持了一种优秀的写法——装饰器。专门处理这种问题。给webpack装个插件吧:
npm install -D @babel/plugin-proposal-decorators
在 config-override.js
中,
const {addDecoratorsLegacy}=require('customise-cra');
module.exports={
// ... ,
addDecaratorsLefacy()
};
在写完withHoc和withLog后:
// ...
// 如果你想,还可以@多次上诉中间件
@withHoc
@withLog
class App extends Component {
render() {
return (
<div className="App">
<h2>hi,{this.props.name}</h2>
<Button type="primary">Button</Button>
</div>
)
}
}
export default App
复合组件使我们以更敏捷的方式定义组件的外观和行为,比起继承的方式它更明确和安全。 ——官方文档
import React,{Component} from 'react'
// Dialog作为容器不关心内容和逻辑
function Dialog(props) {
let {color}=props;
// children是固定名称(不能改),类似匿名插槽
return <div style={{ border: `4px solid ${color}` }}>{props.children}</div>;
}
// WelcomeDialog通过复合提供内容 function WelcomeDialog() {
function WelcomeDialog() {
return (
<Dialog color="blue">
<h1>欢迎光临</h1>
<p>感谢使用react</p>
</Dialog>
);
}
export default function Composition(){
return (
<WelcomeDialog />
)
}
这是一个类似插槽的功能,容器型组件都可以这么写。
Dialog
的传值可以设置多个属性,表达式,jsx都可以。
function Dialog(props) {
let {color,footer}=props;
return <div style={{ border: `4px solid ${color}` }}>
{props.children}
{footer}
</div>;
}
// WelcomeDialog通过复合提供内容 function WelcomeDialog() {
function WelcomeDialog() {
return (
<Dialog color="blue" footer={ <button>确定</button>}>
<h1>欢迎光临</h1>
<p>感谢使用react</p>
</Dialog>
);
}
这就像具名插槽。
import React,{Component} from 'react'
// Dialog作为容器不关心内容和逻辑
function Dialog(props) {
let {color,footer}=props;
// children是固定名称(不能改),类似插槽
return(
<div style={{ border: `4px solid ${color}` }}>
{props.children}
{props.foo('i come from foo')}
{footer}
</div>
)
}
// WelcomeDialog通过复合提供内容 function WelcomeDialog() {
function WelcomeDialog() {
return (
<Dialog color="blue" footer={ <button>确定</button>} foo={(e)=>{return <p>{e}</p>}}>
<h1>欢迎光临</h1>
</Dialog>
);
}
props.children究竟是什么?
children可以说是jsx。但是:
function Dialog(props) {
let { color, footer } = props;
return (
<div style={{ border: `4px solid ${color}` }}>
{props.children()}
{props.foo('i come from foo')}
{footer}
</div>
)
}
function WelcomeDialog() {
return (
<Dialog
color="blue"
footer={<button>确定</button>}
foo={(e) => { return <p>{e}</p> }}>
{e =>(
<div>
<p>welcome</p>
</div>
)}
</Dialog>
);
}
其实children完全可以是一个合法的jsx表达式。
那就可以有很多丰富的操作了。
需求1 比如说我要设置一个脏话过滤系统:
function Dialog(props) {
let { color, footer } = props;
// children是固定名称(不能改),类似插槽
return (
<div style={{ border: `4px solid ${color}` }}>
{props.children()}
{props.foo('i come from foo')}
{footer}
</div>
)
}
// WelcomeDialog通过复合提供内容 function WelcomeDialog() {
function WelcomeDialog() {
return (
<Dialog color="blue" footer={<button>确定</button>} foo={(e) => { return <p>{e}</p> }}>
{/* <h1>欢迎光临</h1> */}
{e =>(
<div>
<p>welcome</p>
</div>
)}
</Dialog>
);
}
function Filter(props){
return (
<div>
{React.Children.map(props.children,child=>{
console.log(child.props.children.indexOf('uck'))
if(child.props.children.indexOf('uck')<0){
return child
}
return false;
})}
</div>
)
}
export default function Composition() {
return (
<>
<WelcomeDialog />
<Filter>
<h1>fuck</h1>
<h2>suck</h2>
<p>dangjingtao</p>
</Filter>
</>
)
}
所有带uck的脏话都过滤了。map可以对类似数组结构进行遍历。
需求2:实现一个RadioGroup的组件,下属有Radio子组件
比如说:
<RadioGroup name="mvvm">
<Radio value="react">react</Radio>
<Radio value="vue">vue</Radio>
<Radio value="Angular">Angular</Radio>
</RadioGroup>
根据之前的方案,可得:
function RadioGroup(props){
// 把name赋值给Radio
return (
<div style={{border:'1px solid #ccc'}}>
{React.Children.map(props.children,child=>{
console.log(child)
child.props.name=props.name
return child;
})}
</div>
)
}
function Radio(props){
return (
<label htmlFor={`${props.name}`} style={{margin:0}}>
<input type="radio" name={`${props.name}`} value={`${props.value}`} /> {props.children}
</label>
)
}
结果报错。因为虚拟dom是不可扩展的。
不可扩展,只能复制:
function RadioGroup(props){
// 把name赋值给Radio
return (
<div style={{border:'1px solid #ccc'}}>
{React.Children.map(props.children,child=>{
return React.cloneElement(child,{name:props.name});
})}
</div>
)
}
这种模式下有两个角色:
import React, { Component } from 'react'
const FormContext = React.createContext()
console.log(FormContext)
const FormProvider = FormContext.Provider
const FormConsumer = FormContext.Consumer
const store = {
name: 'djtao',
}
class ContextTest extends Component {
render() {
return (
<FormProvider value={store}>
<FormConsumer>
{store => <p>{store.name}</p>}
</FormConsumer>
</FormProvider >
)
}
}
export default ContextTest;