2021-06

122 阅读5分钟

单页应用路由原理

补充知识:window.location

```
http://172.16.49.154:3000/search?q=URL#search
console.log(location.href);  // http://172.16.49.154:3000/search?q=URL#search
console.log(location.protocol); // http:
console.log(location.host); // 172.16.49.154:3000
console.log(location.hostname); // 172.16.49.154
console.log(location.port); // 3000
console.log(location.pathname); // search
console.log(location.search); // ?q=UR
console.log(location.hash); // #search
console.log(location.origin); // http://172.16.49.154:3000

```

1. **Location.reload()**

	重新加载来自当前 URL的资源。他有一个特殊的可选参数,类型为 Boolean,该参数为true时会导致该方法引发的刷新一定会从服务器上加载数据。如果是 false或没有制定这个参数,浏览器可能从缓存当中加载页面。

2. **Location.replace()**

	用给定的URL替换掉当前的资源。与 assign() 方法不同的是用 replace()替换的新页面不会被保存在会话的历史 History中,这意味着用户将不能用后退按钮转到该页面。

补充知识:window.history 和 History

window.history是一个只读属性,用来获取History 对象的引用,History 对象提供了操作浏览器会话历史(浏览器地址栏中访问的页面,以及当前页面中通过框架加载的页面)的接口。

  1. History.length

     会话历史中元素的数目,包括当前加载的页
    
  2. History.state

    返回一个表示历史堆栈顶部的状态的值。

  3. History的 back / forward / go 方法

    back()和forward()表示前往上一页或者下一页, 用户可点击浏览器左上角的返回按钮和前进按钮模拟此方法. 等价于 history.go(-1)或者history.go(1)

    go()通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面,当整数参数超出界限时,那么这个方法没有任何效果也不会报错

  4. History.pushState / repalceState

    pushState() 和 repalceState 按指定的名称和URL(如果提供该参数)将数据push进会话历史栈或者更新历史栈上最新的入口,页面不会跳转。

    // pushState() 需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个URL
    history.pushState({foo: "bar",};, "page 2", "bar.html");
    
  5. window.popstate事件

    // 
    window.addEventListener('popstate', (event) => {
        ...
    });
    

history.js库

以创建history路由进行分析:(方便区分,分为window.history 和 history库)

  1. 辅助函数

    import { 
    	createLocation
    	// 创建history.location格式的对象
    	// 类似 {key: '',state: '',pathname: '',search: '',hash: ''} 
    	// createLocation(path, state, key, currentLocation)
    	// 入参path可以为string类型或者包含 {pathname: '',search: '',hash: ''}的对象
    	// state为一个对象,初始为window.history的state,push,replace等方法可以传入一个state,history把它用来当做传入下一个页面的参数来读取
    	
    } from './LocationUtils';
    
    import {
      addLeadingSlash, // 路径最前面加斜杠
      stripTrailingSlash, // 路径最后面去斜杠
      hasBasename, // 创建history时,是否传入了basename属性
      stripBasename, // 去掉basename
      createPath // 创建完成路径 pathname + search + hash
    } from './PathUtils';
    
    import createTransitionManager from './createTransitionManager';
    
    import {
      canUseDOM, // 是否可以使用DOM API
      getConfirmation, // 跳转前确认的弹框
      supportsHistory, // 是否支持H5的History API
      supportsPopStateOnHashChange, // 是否支持onPopstate事件
      isExtraneousPopstateEvent // 源码注释:Ignore extraneous popstate events in WebKit.
    } from './DOMUtils';
    
  2. createBrowserHistory

    // transitionManager 为一个发布订阅模式,
    // 包含appendListene(添加监听),notifyListeners(触发所有监听),
    // setPrompt(设置一个Prompt,用于跳转前的弹框),
    // comfirmTransitionTo(确认跳转函数)四个方法
    
    function createBrowserHistory(props = {}) {
    	// ...
    	const history = {
    		 // globalHistory.length = window.history.length
    	    length: globalHistory.length, 
    	    action: 'POP', // history内部管理的一个变量,导航的action
    	    location: initialLocation,
    	    createHref, // 生成完整href地址的方法
    	    // push(path,state)方法接受path(string或者对象)和 state为参数
    	    // 随机创建一个key,调用createLocation生成要跳转路由的location
    	    // 调用createHref生成 href
    	    // 调用 transitionManager.confirmTransitionTo 主要用于跳转前的确认
    	    // 如果确认跳转,window.history.pushState({ key, state }, null, href)
    	    // history对象merge上要跳转路由的 key,state,和window.history.length
    	    // 然后出发所有监听事件(传入**location,action**)
    	    push,
    	  	 // replace与push类似
    	    replace,
    	    go, // history.go()
    	    goBack, // history.go(-1)
    	    goForward, // history.go(1)
    	    block,
    	    // 重要! listen方法使用 transitionManager.appendListene方法向订阅数组里推入一个监听函数,返回一个卸载监听的方法(这种设计模式,在react中非常常见),
    	    // 同时调用checkDOMListeners方法,此方法会在订阅数据添加上 **第一个** 监听事件后,同时发起window.addEventListener('popstate', handlePopState); 
    	    // 同时会在卸载完全部监听后,**订阅数组为空时**,移除监听 window.removeEventListener('popstate', handlePopState);
    	    listen
    	 };
    
    	return history;
    }
    
    

react-router

对外暴露了Router,Router,Switch,matchPath,withRouter等API

  1. Router

    class Router extends React.Component {
    
    	static computeRootMatch(pathname) { // 静态方法
    		return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
    	}
    
    	constructor(props) {
    		super(props);
    
    		this.state = {
    			location: props.history.location
    		};
    
    		// 这是一种hack的写法,从新建实例的时候就开始对location的监听
    		this._isMounted = false;
    		this._pendingLocation = null;
    
    		if (!props.staticContext) {
    			this.unlisten = props.history.listen(location => {
    				// 如果组件以加载到真实的DOM,直接设置location的state
    				if (this._isMounted) {
    					this.setState({ location });
    				} else {
    				//  如果没有,先用一个变量进行保存,等到componentDidMount后进行赋值
    					this._pendingLocation = location;
    				}
    			});
    		}
    	}
    
    	componentDidMount() {
    		this._isMounted = true;
    
    		if (this._pendingLocation) {
    			this.setState({ location: this._pendingLocation });
    		}
    	}
    
    	componentWillUnmount() {
    		// 卸载监听
    		if (this.unlisten) this.unlisten();
    	}
    
    	render() {
    		return (
    			// 使用了React新的Context API,在最外层提供了history,location等供全局使用
    			<RouterContext.Provider
    				children={this.props.children || null} // 子代属性
    				value={{
    					// 使用的 路由类型
    					history: this.props.history,
    					// history.js库封装的location,然后内部进行管理
    					location: this.state.location,
    					// 返回类似{ path: "/", url: "/", params: {}, isExact: pathname === 	"/" }的匹配对象;
    					match: Router.computeRootMatch(this.state.location.pathname),
    					// 自定义传入的context
    					staticContext: this.props.staticContext
    				}}
    			/>
    		);
    	}
    }
    
    
  2. Route

    <RouterContext.Consumer>
        {context => {
    		// Router组件传入this.props.location 或者 context 的 location
          const location = this.props.location || context.location; 
          const match = this.props.computedMatch // Router组件传入computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path // Router组件传入path
            // matchPath 函数返回类似{ path: "/", url: "/", params: {}, isExact: pathname === 	"/" }的匹配对象
              ? matchPath(location.pathname, this.props) 
              : context.match;
    	
          const props = { ...context, location, match };
          let { children, component, render } = this.props;
          ......
          // 最后为每个Router提供一个新的Provider,包含history,location,match等
          return (
            <RouterContext.Provider value={props}>
              {children && !isEmptyChildren(children) // children不为空则渲染children
                ? children
                : props.match // 
                  ? component // Router组件传入类组件 使用createElement 创建组件
                    ? React.createElement(component, props)
                    : render // Route组件传入函数组件 传入props执行
                      ? render(props)
                      : null
                  : null} // 没有匹配到返回null
            </RouterContext.Provider>
          );
        }}
    </RouterContext.Consumer>
    
  3. withRouter

    <Router ...>
        <Route ... /> 
        <Route ... />
        <Route ... /> 
        // 这些Route中能从props中获取到 history location match等,
        // 但是Route里面的子组件是没有的,
        // 如果想获得这些属性,可以通过props往下层传递,
        // 或者直接通过withRouter,它是利用RouterContext.Consumer获得
    </Router>
    
  4. generatePath

    根据路径(例如:'/user/:id/:name')和 params(例如:{id: 10001, name: 'bob'})生成完整路径(/user/10001/bob)的函数,就是填充url的字符串

react-router-dom

  1. 导出内容

    export * from "react-router"; // 对react-router的API直接导出
    
    // 导出 BrowserRouter HashRouter Link NavLink
    export { default as BrowserRouter } from "./BrowserRouter";
    export { default as HashRouter } from "./HashRouter";
    export { default as Link } from "./Link";
    export { default as NavLink } from "./NavLink";
    
  2. BrowserRouter

    class BrowserRouter extends React.Component {
    	history = createHistory(this.props); // history库暴露的createHistory方法
    	render() {
    		// Router 组件传入生成的history对象
    		return <Router history={this.history} children={this.props.children} />;
      	}
    }
    export default BrowserRouter;
    // HashRouter 与其一样 创建history对象时使用了hash
    
  3. Link 和 NavLink

    Link封装的 a 标签,NavLink封装的Link组件

总结

react-router提供核心路由功能,react-router-dom用作浏览器端路由,依赖react-router,如果 我们写web应用,直接引入react-router-dom即可,此外还包含react-router-native,可以用于react-native应用