what is react-helmet?
是一个用来管理document.head的一个react component;支持服务端渲染
源码
Helmet.js代码框架
// 导入依赖
...
import withSideEffect from "react-side-effect";
...
// 高阶组件
const Helmet = Component =>
class HelmetWrapper extends React.Component {
// logic code
}
const NullComponent = () => null;
const HelmetSideEffects = withSideEffect(
reducePropsToState,
handleClientStateChange,
mapStateOnServer
)(NullComponent);
const HelmetExport = Helmet(HelmetSideEffects);
HelmetExport.renderStatic = HelmetExport.rewind;
// export
export {HelmetExport as Helmet};
export default HelmetExport;
看到了高阶组件,那就从这个开始吧
const Helmet = Component =>
class HelmetWrapper extends React.Component {
static propTypes = {
// 属性类型检查
title: PropTypes.string,
...
}
// 默认属性值
static defaultProps = {
//...
}
// 测试使用 忽略它
static peek = Component.peek;
// 没写注释 先略过
static rewind = () => {
}
// 看命名猜测是和环境相关
static set canUseDOM(canUseDOM) {
Component.canUseDOM = canUseDOM;
}
// 使用深比较判断组件是否需要更新
// isEqual 使用了react-fast-compare这个库,稍后看
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
// 看命名是和处理子组件 title meta等相关,先跳过
mapNestedChildrenToProps(child, nestedChildren) {
//...
}
// 跳过
flattenArrayTypeChildren() {
}
// 跳过
mapObjectTypeChildren() {}
...
//直接看render 其他的在调用的时候再看
render() {
// children fiberNode子节点
// props 其他的属性
const {children, ...props} = this.props;
let newProps = {...props};
if (children) {
// 看到了这里
newProps = this.mapChildrenToProps(children, newProps);
}
return <Component {...newProps} />;
}
}
this.mapChildrenToProps
最终经过这个函数得到了一个对象, 传递给了Component,Component就是HelmetSideEffects。
HelmetSideEffects如何被创建的
withSideEffect(
reducePropsToState,
handleClientStateChange,
mapStateOnServer
)(NullComponent);
withSideEffect
所以关键就是 withSideEffect的功能了;它来自这个包react-side-effect
withSideEffect(
reducePropsToState,
handleStateChangeOnClient,
mapStateOnServer,// 可选
)(YourComonent);
这个高阶组件的作用是:
YourComonent可以多次,也可以嵌套的使用(内部维护了一个实例数组)withSideEffect能够监听所有YourComonentwillMountwillUnMountcomponentDidUpdate的触发,按照嵌套规则收集YourComponent上的props到一个propsList数组中- 然后高阶组件的第一个参数
reducePropsToState你可以拿到propsList,去做自己的业务逻辑处理
function reducePropsToState(propsList) {
// logic code
// maybe return last props.title
// return propsList[propsList.length -1].title
}
reducePropsToState每次执行之后,都会执行handleStateChangeOnClient,这个方法用来在客户端执行相应的副作用操作,比如修改document.titlemapStateOnServer是用于将reducePropsToState返回的state,再进行数据处理,以便在服务端渲染的时候进行使用
处理title的一个简单实现,具体查看这个关于document.title的处理react-document-title
下面决定看下 react-side-effect的实现
withSideEffect的实现
export default function withSideEffect(
reducePropsToState,
handleStateChangeOnClient,
mapStateOnServer
) {
// 参数校验 跳过
//...
// 设置displayName 跳过
// 返回一个高阶组件
return function wrap(WrappedComponent) {
if (typeof WrappedComponent !== 'function') {
throw new Error('Expected WrappedComponent to be a React component.');
}
// 维护了所有你传入的组件的实例对象,方便在服务端渲染的时候使用
let mountedInstances = [];
// 状态 handleStateChangeOnClient or mapStateOnServer的入参
let state;
// 触发整个更新过程 reducePropsToState-> newState -> handleStateChangeOnClient|mapStateOnServer
function emitChange() {
state = reducePropsToState(mountedInstances.map(function (instance) {
return instance.props;
}));
if (SideEffect.canUseDOM) {
// 客户端执行回调
handleStateChangeOnClient(state);
} else if (mapStateOnServer) {
// 服务端渲染执行执行逻辑
state = mapStateOnServer(state);
}
}
// 内部类,我们在代码中引用的组件其实是这个
class SideEffect extends PureComponent {
// Try to use displayName of wrapped component
static displayName = `SideEffect(${getDisplayName(WrappedComponent)})`;
// Expose canUseDOM so tests can monkeypatch it
static canUseDOM = canUseDOM;
// 获取state
static peek() {
return state;
}
// 服务端渲染的时候 获取到当前的state,并且重置了一些内部状态
static rewind() {
if (SideEffect.canUseDOM) {
throw new Error('You may only call rewind() on the server. Call peek() to read the current state.');
}
let recordedState = state;
state = undefined;
mountedInstances = [];
return recordedState;
}
// willMount的时候触发一次emitChange
UNSAFE_componentWillMount() {
// 每一个组件挂载之前 都添加到mountedInstances 闭包变量中
mountedInstances.push(this);
emitChange();
}
// didUpdate的时候触发一次emitChange
componentDidUpdate() {
emitChange();
}
// willUnmount的时候触发一次emitChange
componentWillUnmount() {
const index = mountedInstances.indexOf(this);
mountedInstances.splice(index, 1);
emitChange();
}
render() {
// 这里是你的组件 YourComponent
return <WrappedComponent {...this.props} />;
}
}
return SideEffect;
}
}
总结
回头梳理Helmet的代码,整个流程就清晰了(伪代码逻辑)。
//HelmetSideEffects 的逻辑 ---start
//只负责设置header内的结构,不渲染其他
//故 <Helmet>hello</Helmet> --> 并不会渲染hello字符串
const NullComponent = () => null;
// 使用withSideEffect
// 这一步达到的效果是 HelmetSideEffects 实例的挂载、更新、卸载都会触发
// reducePropsToState-->handleClientStateChange|mapStateOnServer
const HelmetSideEffects = withSideEffect(
reducePropsToState,
handleClientStateChange,
mapStateOnServer
)(NullComponent);
//HelmetSideEffects 的逻辑 ---end
// 导出的模块Helmet
const HelmetExport = Helmet(HelmetSideEffects);
const Helmet = (Component)=>
class HelmetWrapper extends React.Component {
render() {
// 内部封装的所有逻辑在做这个事情,将react children(fiberNode)转换成了HelmetSideEffects的props
// props.children-> newProps
//
return <Component {...newProps} />;
}
}
// 服务端渲染使用 获取最终的state
HelmetExport.renderStatic = HelmetExport.rewind;
// 我们使用的组件 Helmet -> <Helmet>...</Helmet>
export default HelmetExport;