react-helmet 源码学习笔记

1,609 阅读3分钟

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

最终经过这个函数得到了一个对象, 传递给了ComponentComponent就是HelmetSideEffects

image.png

HelmetSideEffects如何被创建的

withSideEffect(
    reducePropsToState,
    handleClientStateChange,
    mapStateOnServer
)(NullComponent);

withSideEffect

所以关键就是 withSideEffect的功能了;它来自这个包react-side-effect

withSideEffect(
  reducePropsToState,
  handleStateChangeOnClient,
  mapStateOnServer,// 可选
)(YourComonent);

这个高阶组件的作用是:

  1. YourComonent 可以多次,也可以嵌套的使用(内部维护了一个实例数组)
  2. withSideEffect能够监听所有YourComonent willMount willUnMount componentDidUpdate的触发,按照嵌套规则收集YourComponent上的props到一个propsList数组中
  3. 然后高阶组件的第一个参数reducePropsToState 你可以拿到propsList,去做自己的业务逻辑处理
function reducePropsToState(propsList) {
    // logic code
    // maybe return last props.title
    // return propsList[propsList.length -1].title
}
  1. reducePropsToState每次执行之后,都会执行handleStateChangeOnClient,这个方法用来在客户端执行相应的副作用操作,比如修改document.title
  2. mapStateOnServer是用于将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;