Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于react-router #2

Open
Kehao opened this issue Sep 19, 2019 · 0 comments
Open

关于react-router #2

Kehao opened this issue Sep 19, 2019 · 0 comments

Comments

@Kehao
Copy link
Owner

Kehao commented Sep 19, 2019

一次性让你明白react-router所有的工作原理~

🎧 浏览器实现单页面应用的工作原理:

关键点是可以改变浏览器地址,又不刷新页面, 页面刷新了就不叫单页应用了~

1.通过Hash实现前端路由(一般不用这个)

因为改变url的hash值是不会刷新页面的, 所以可以通过hash来实现前端路由,从而实现无刷新的效果。

// hash属性位于location对象中,在当前页面中,可以通过:
window.location.hash='edit'
// 来实现改变当前url的hash值。执行上述的hash赋值后,页面的url发生改变。
// 赋值前:http://localhost:3000
// 赋值后:http://localhost:3000/#edit

hashchange的事件,可以监听hash的变化,我们可以通过下面两种方式来监听hash的变化:

// window.onhashchange:当前 URL 的锚部分(以 '#' 号为开始) 发生改变时触发。触发的情况如下:
// a、通过设置Location 对象 的 location.hash 或 location.href 属性修改锚部分;
// b、使用不同history操作方法到带hash的页面;
// c、点击链接跳转到锚点。<a href="#edit">edit</a>
window.onhashchange=function(event){
   console.log(event);
}
window.addEventListener('hashchange',function(event){
   console.log(event);
})
// HashChangeEvent的具体值为:
{isTrusted: true, oldURL: "http://localhost:3000/", newURL:   "http://localhost:3000/#teg", type: "hashchange".....}

有了监听事件,且改变hash页面不刷新,这样我们就可以在监听事件的回调函数中,执行我们展示和隐藏不同UI显示的功能,从而实现前端路由。

hash的兼容性较好,因此在早期的前端路由中大量的采用,但是使用hash也有很多缺点:

  • 搜索引擎对带有hash的页面不友好
  • 带有hash的页面内难以追踪用户行为

2.通过Html5的history实现前端路由(目前用的)

HTML5的History接口(window.history),History接口允许我们操作浏览器会话历史记录,有一个npm包'history'就是封装了这个接口: npm install history -S

兼容性
export const supportsHistory = () => {
  const ua = window.navigator.userAgent;

  if (
    (ua.indexOf("Android 2.") !== -1 || ua.indexOf("Android 4.0") !== -1) &&
    ua.indexOf("Mobile Safari") !== -1 &&
    ua.indexOf("Chrome") === -1 &&
    ua.indexOf("Windows Phone") === -1

  )
    return false;

  return window.history && "pushState" in window.history;

};
// 从中看出,window.history在chrome、mobile safari和windows phone下是支持的,但不支持安卓2.x以及安卓4.0。
// pushState直到IE10才被支持。https://www.caniuse.com/#search=pushState
// 当在不支持的浏览器中调用相关方法时,就会用刷新页面的方式来向下兼容。
History接口:
// 2个主要的属性:
history.length
// 返回在会话历史中有多少条记录,包含了当前会话页面。
// 如果打开一个新的Tab,那么这个length的值为1。
// history.length 为 3  表示当前窗口一共访问过3个网址。
history.state
// history 保存了会出发popState事件的方法,所传递过来的属性对象。

// 5个主要方法:
// 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同。
window.history.back();
// 指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同。
window.history.forward();
// 可以跳转到浏览器会话历史中的指定的某一个记录页。
window.history.go(-1);
window.history.go(1);
window.histroy.go(num); // 跳转到相应的访问记录,其中num大于0,则前进, 小于0,则后退。

// pushState可以将给定的数据压入到浏览器会话历史栈中,该方法接收3个参数,对象,title和一串url。pushState后会改变当前页面url,但是不会伴随着刷新。
history.pushState(stateData, title, url);
window.history.pushState({foo:'bar'}, "page 2", "bar.html");

 // replaceState将当前的会话页面的url替换成指定的数据,replaceState后也会改变当前页面的url,但是也不会刷新页面。              
history.replaceState(stateData, title, url);

// stateData:存储JSON字符串,可以用在popstate事件中。
// title:现在大多数浏览器不支持或者忽略这个参数,最好用null代替。
// url任意有效的URL,用于更新浏览器的地址栏,并不在乎URL是否已经存在地址列表中。
// pushState是压入浏览器的会话历史栈中,会使得History.length加1,而replaceState是替换当前的这条会话历史,因此不会增加History.length。

// 1个事件
// 当调用history.go()、history.back()、history.forward()时触发;pushState()\replaceState()方法不触发
window.onpopstate = function() {} 
window.history.pushState({foo:'bar'}, "page 2", "bar.html");
// 执行上述方法前:http://localhost:3000
//执行上述方法后:http://localhost:3000/bar.html

//输出window.history.state:
console.log(window.history.state);
// {foo:'bar'}

// window.history.state就是我们pushState的第一个对象参数。
// history.replaceState()方法不会改变hitroy的长度
总结:

如果用history做为前端路由的基础,那么需要用到的是history.pushState和history.replaceState,在不刷新的情况下可以改变url的地址,如果页面发生回退back或者forward时,会触发popstate事件。

优点:

  • 对搜索引擎友好
  • 方便统计用户行为

缺点:

  • 兼容性不如hash
  • 需要后端做相应的配置,否则直接访问子页面会出现404错误

🎸 react-router源码解析

router的几个重要的npm包:

history
// window.histroy的封装,react-router中使用history.listen来监听跳转和刷新组件。

react-router 
// 实现了路由的核心功能
// 主要文件有:  Router, Route, Switch, withRouter, Redirect, matchPath等

react-router-dom 
//  基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件,BrowserRouter和HashRouter 组件。
//  安装react-router-dom后,不需要再显式的引用react-router,  npm会自动解析react-router-dom包中package.json的依赖并安装react-router。

react-router-redux 
// 用redux的方式去操作react-router。
// 例如,react-router 中跳转需要调用 router.push(path),集成了react-router-redux 就可以通过dispatch的方式使用router,跳转可以这样做 store.dispatch(push(url))。
// 本质上,是把react-router自己维护的状态,例如location、history、match等等,也交给redux管理。
//  react-router v4后路由组件可以直接获取这些属性, 非路由组件就必须通过withRouter修饰后才能获取这些属性了。
//  react-router v4后官方不再维护,也不需要再引入这个库。
//  目前dva还依赖该组件。

react-router-config 
// 静态路由配置的小助手

react-router-native
// 基于react-router,类似react-router-dom,加入了react-native运行环境下的一些功能。

withRouter的作用:

withRouter高阶函数,使用Route来包裹组件,用来为组件添加当前路由状态。

作用1, 在组件中想使用history来控制路由跳转, 读取location、history、match状态。
  • 路由组件可以直接获取这些属性,如
<Route path='/' component={App}/>
  • App组件中如果有一个子组件Foo,那么Foo就不能直接获取路由中的属性了,必须通过withRouter修饰后才能获取到,如withRouter(connect(Component))
作用2,withRouter是专门用来处理数据更新问题的,避免更新受阻。
  • 在使用一些redux的的connect()或者mobx的inject()的组件中,如果依赖于路由的更新要重新渲染,会出现路由更新了但是组件没有重新渲染的情况。这是因为redux和mobx的这些连接方法会修改组件的shouldComponentUpdate。
  • 在使用withRouter解决更新问题的时候,一定要保证withRouter在最外层,比如withRouter(connect(Component))

路由初始化

import { Router, Route, IndexRoute } from 'react-router'

const PrimaryLayout = props => (
  <div className="primary-layout">
    <header>
      Our React Router 3 App
    </header>
    <main>
      {props.children}
    </main>
  </div>
)

const HomePage = () => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>

const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={PrimaryLayout}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
)

render(<App />, document.getElementById('root'))

Router, BrowserRouter, ConnectedRouter

这几个组件没有很大的区别,BrowserRouter, ConnectedRouter都包裹Router:

  • react-router v4里的BrowserRouter组件: 使用createHistory为histroy包裹Router。
// BrowserRouter.js
//省略......
import { createBrowserHistory as createHistory } from "history";
//省略......
class BrowserRouter extends React.Component {
  // histroy默认为BrowserHistory 
  history = createHistory(this.props);
  render() {
    // 渲染Router
    return <Router history={this.history} children={this.props.children} />;
  }
}
// 省略......
export default BrowserRouter;
  • react-router-redux里的ConnectedRouter组件: 监听浏览器状态,实时修改store里location的值,并包裹Router。
// ConnectedRouter.js
class ConnectedRouter extends PureComponent {
    constructor(props) {
      super(props)
      // 省略......
      // 监听history事件,当浏览器地址变化时, store.dispatch({type: LOCATION_CHANGE, payload: location}); 改变store里location的状态
      this.unlisten = history.listen(handleLocationChange)
      handleLocationChange(history.location, history.action, true)
    }
    // 省略......
    render() {
      const { history, children } = this.props
      return (
        // 渲染Router
        <Router history={history}>
          { children }
        </Router>
      )
    }
  }
  • 💸 Router组件: react-router最外层组件,主要负责监听history和刷新整个组件,并把router对象放入到context里。
// Router.js
class Router extends React.Component {
  // 省略....
  getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location,
          match: this.state.match
        }
      }
    };
  }
  // 省略....
  componentWillMount() {
    const { children, history } = this.props;
    // 省略....
    // 调用history.listen监听方法,该方法的返回函数是一个移除监听的函数
    // 当url改变时,更改Router下不同path组件的match结果, 并且触发一次setState
    // 作为Router的最外层,刷新的时候,所有的children都会进行一次render计算 
    this.unlisten = history.listen(() => {
      this.setState({
        match: this.computeMatch(history.location.pathname)
      });
    });
  }
  // 省略....
  render() {
    const { children } = this.props;
    return children ? React.Children.only(children) : null;
  }
}

export default Router;
  • Route组件: 当url改变的时候,将path属性与改变后的url做对比,如果匹配成功,则渲染该组件的componet或者children属性所赋值的那个组件。
// Route.js
class Route extends React.Component {
  // 省略....
  state = {
    match: this.computeMatch(this.props, this.context.router)
  };
  // 省略....
  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    // 如果外层组件是<Switch>,并且当前组件已经被Switch match则直接返回这个match
    if (computedMatch) return computedMatch; 

    invariant(
      router,
      "You should not use <Route> or withRouter() outside a <Router>"
    );

    const { route } = router;
    const pathname = (location || route.location).pathname;

    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }
  // 省略....
  componentWillReceiveProps(nextProps, nextContext) {
    // Router被刷新后,会被重新setState
    // 判断当前location是否match, 如果是则返回这个match并渲染这个组件。
    // 如果不是,则返回null, 如果当前组件已被渲染,则会被unmout。
    // match的结构:{
    //     isExact: true,
    //     params: {},
    //     path: "/dashboard"
    //     url: "/dashboard"
    // }
    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
  }
  // 省略....
  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;
    const { history, route, staticContext } = this.context.router;
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };
    // match是否为null, 如果不是null, 那么根据优先级顺序component属性、render属性和children属性来渲染其所指向的React组件。
    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

    if (typeof children === "function") return children(props);

    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }
  // 省略....
}
  • Switch组件:
    渲染第一个被location匹配到的并且作为子元素的或者, switch组件不是必须的。
class Switch extends React.Component {
  // 省略....
  render() {
    const { route } = this.context.router;
    const { children } = this.props;
    const location = this.props.location || route.location;

    let match, child;
    React.Children.forEach(children, element => {
      if (match == null && React.isValidElement(element)) {
        const {
          path: pathProp,
          exact,
          strict,
          sensitive,
          from
        } = element.props;
        const path = pathProp || from;

        child = element;
        match = matchPath(
          location.pathname,
          { path, exact, strict, sensitive },
          route.match
        );
      }
    });
   // 只返回第一个匹配到的match
    return match
      ? React.cloneElement(child, { location, computedMatch: match }) // 把computedMatch传给Route
      : null;
  }
  // 省略....
}

BrowserHistory的后端设置

单页面应用通常只使用一个索引文件(一般是index.html),该索引文件可以由浏览器访问。当访问其它地址时,后台需要把该请求代理到index.html里,否者会出现404错误。

  • 对于一般的node后台,我们可以使用connect-history-api-fallback这个包
// 比如后台是koa
// npm install koa2-connect-history-api-fallback -D

// 省略......
const Koa = require('koa')
const history = require('koa2-connect-history-api-fallback')
app.use(history())
// 省略......
  • 当部署到正式环境时,我们不再使用node后台,需要在nginx里作相应的设置:
server {
    listen 8011;
    location / {
        root /home/www/web/quickFrontLab_test/dist/admin/;
        try_files $uri /index.html; #设置try_files
    }
}

keep-alive-router(待续)

react-router 5.0(待续)

有用的资源

@Kehao Kehao changed the title 关于react-router的一切 关于react-router Nov 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant