如何为整个应用程序创建包装器组件?

IT技术 javascript reactjs
2021-05-06 15:55:32

我正在尝试为我的 React 应用程序添加一些分析跟踪。基本上只是使用一个组件来添加全局事件侦听器,然后在该组件中适当地处理事件。

我想将我的整个应用程序包装在这个组件中,并让它获取componentWillUpdateprop 更改,以便我可以使用prop.location. 我的问题是我不知道如何设置我的包装器组件来做到这一点。我知道 HOC 的概念可以帮助包装一个组件,我已经测试过它可以工作,但我希望这是一个更通用和全局的组件。

跟踪器.js

import PropTypes from "prop-types"
import * as React from "react"
import { connect } from "react-redux"

import TrackingManager from './TrackingManager'
import ScriptManager from "./ScriptManager"
import { isLeftClickEvent } from "../utils/Utils"

const trackingManager = new TrackingManager()

const config = {
    useTagManager: true,
    tagManagerAccount: 'testCccount', 
    tagManagerProfile: 'testProfile', 
    tagManagerEnvironment: 'dev'
}

/**
 * compares the locations of 2 components, mostly taken from:
 * http://github.com/nfl/react-metrics/blob/master/src/react/locationEquals.js
 *
 * @param a
 * @param b
 * @returns {boolean}
 */
function locationEquals(a, b) {
  if (!a && !b) {
    return true;
  }
  if ((a && !b) || (!a && b)) {
    return false;
  }

  return (
    a.pathname === b.pathname && a.search === b.search && a.state === b.state
  );
}

/**
 * Tracking container which wraps the supplied Application component.
 * @param Application
 * @param beforeAction
 * @param overrides
 * @returns {object}
 */
const track = Application =>
  class TrackingContainer extends React.Component {
    constructor(props) {
      super(props)
    }

    componentDidMount() {
      this._addClickListener()
      this._addSubmitListener()
    }

    componentWillUnmount() {
      // prevent side effects by removing listeners upon unmount
      this._removeClickListener()
      this._removeSubmitListener()
    }

    componentDidUpdate(prevProps) {
      // if and only if the location has changed we need to track a
      // new pageview
      if (!locationEquals(this.props.location, prevProps.location)) {
        this._handlePageView(this.props)
      }
    }


    _addClickListener = () => {
      // bind to body to catch clicks in portaled elements (modals, tooltips, dropdowns)
      document.body.addEventListener("click", this._handleClick)
    }

    _removeClickListener = () => {
      document.body.removeEventListener("click", this._handleClick)
    }

    _addSubmitListener = () => {
      document.body.addEventListener("submit", this._handleSubmit)
    }

    _removeSubmitListener = () => {
      document.body.removeEventListener("submit", this._handleSubmit)
    }

    _handleSubmit = event => {
        console.log(event.target.name)
    }

    _handleClick = event => {
      // ensure the mouse click is an event we're interested in processing,
      // we have discussed limiting to external links which go outside the
      // react application and forcing implementers to use redux actions for
      // interal links, however the app is not implemented like that in
      // places, eg: Used Search List. so we're not enforcing that restriction
      if (!isLeftClickEvent(event)) {
        return
      }

      // Track only events when triggered from a element that has 
      // the `analytics` data attribute.
      if (event.target.dataset.analytics !== undefined) {
        trackingManager.event('pageName', 'User')
      }
    }

    _handlePageView = route => {
      console.log('CHANGE PAGE EVENT')
      console.log(route)
    }

    /**
     * Return  tracking script. 
     */
    _renderTrackingScript() {

        /**
         * If utag is already loaded on the page we don't  want to load it again
         */
        if (window.utag !== undefined) return

        if (config.useTagManager === false) return

        /**
         * Load  utag script. 
         */
        return (
          <ScriptManager
            account={config.tagManagerAccount}
            profile={config.tagManagerProfile}
            environment={config.tagManagerEnvironment}
          />
        )
    }


    render() {
      return (
        <React.Fragment>
          <Application {...this.props} {...this.state} />
          {this.props.children}
          {this._renderTrackingScript()}
        </React.Fragment>
      )
    }
  }

export default track

使用我的 index.js,我想做一些类似的事情:

import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Switch, Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from './lib/store'
import history from './lib/history'
import Loadable from 'react-loadable'
import PageLoader from './components/PageLoader/PageLoader'
import { 
    DEFAULT_PATH, 
    LOGIN_PATH, 
    LOGOUT_PATH, 
    USER_PATH,
} from './lib/paths'

const Login = Loadable({ loader: () => import('./scenes/Auth/Login' /* webpackChunkName: 'login' */), loading: PageLoader })
const Logout = Loadable({ loader: () => import('./scenes/Auth/Logout'/* webpackChunkName: 'logout' */), loading: PageLoader })
const User = Loadable({ loader: () => import('./scenes/Auth/User'/* webpackChunkName: 'user' */), loading: PageLoader })


import Track from './lib/tracking/Tracker'

import './assets/stylesheets/bootstrap.scss'
import './bootstrap-ds.css'
import './index.css'
import './assets/stylesheets/scenes.scss'

ReactDOM.render((
// This is an example of what I want to accomplish
<Track>
    <Provider store={store}>
        <Router history={history}>
            <Switch>
                <Route path={LOGIN_PATH} component={Login}  />
                <Route path={LOGOUT_PATH} component={Logout} />
                <Route path={USER_PATH} component={User} />
            </Switch>
        </Router>
    </Provider>
</Track>
), document.getElementById('root'))

所以,基本上<Track>组件可以包装整个应用程序并仍然使用props并检查它们是否更新。有没有办法做到这一点?我需要改变什么?

2个回答

Context API似乎是您的用例。您需要一种解耦方式来在同一树中的组件之间共享数据。您的包装器可以实现 a Provider,并且所有对共享值感兴趣的组件都将实现 a ConsumerHOC并且render Props有助于共享有状态逻辑,而不是状态本身。

const { Provider, Consumer } = React.createContext()

const Wrapper = ({children}) =>{
    return(
        <Provider value={mySharedValue}>
            {children}
        </Provider>
    )
}

const NestedChildren = () =>{
    return(
        <Consumer>
            {context => <div>{context}</div>}
        </Consumer>
    )
}

const App = () =>{
    return(
        <Wrapper>
            <Child> <NestedChild /> </Child>
        </Wrapper>
    )
}

我们用react-capture-metrics.

您将您的分析 API 提供给顶级提供商,如下所示:

import { MetricsProvider } from 'react-capture-metrics'

const analytics = {
  track: (name, properties) => window.analytics.track(name, properties),
  page: (name, properties, category) => window.analytics.page(...(category ? [category, name, properties] : [name, properties]))
}

function App () {
  return (
    <MetricsProvider analytics={analytics} properties={{ appVersion: pkg.version }}>
      // ...the rest of your app
    </MetricsProvider>
  )
}

然后PageView在您想要调用的任何地方渲染一个组件analytics.page()

function Page() {
  const { PageView } = useMetrics({
    variantId, 
    // ...properties to capture
  }, { ready: variantId !== undefined })
  return (
    <PageView 
      name="Home"
      category="Customer"
      ready={/* some useState value perhaps */ }
    >
      // ...
    </PageView>
  )  
}

您可以使用ready延迟调用事件,直到加载了您要传递的所有属性。或者,您可以pageKey在用户导航到同一页面但具有不同参数时调用该事件。