传统的多页面应用构建方式:
jsp
,jade
,'ejs','tempalte'等技术在后台先拼接成对应的HTML
结构,然后转换成字符串,在每个对应的路由返回对应的数据(文件)即可
Jade
模版服务端渲染,代码实现:
const express= require('express')
const app =express()
const jade = require('jade')
const result = ***
const url path = ***
const html = jade.renderFile(url, { data: result, urlPath })//传入数据给模板引擎
app.get('/',(req,res)=>{
res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回给客户端
}) //RestFul接口
app.listen(3000,err=>{
//do something
})
jQuery
等传统库绘制的前端页面SEO
友好,因为返回给前端的是渲染好的HTML
结构,里面的内容都可以被爬虫抓取到。HTML
结构jQuery
写的,如果注释和文档不是非常齐全,那么真的会无从下手vue,react
等框架基础上,他们都有一套自己的运行机制,有自己的生命周期,并且不像传统的应用,还加上了一层虚拟DOM
以及diff
算法Ant-Design-pro
这样的开箱即用的库已经很多,单页面应用的学习和开发成本已经很低很低,如果还在使用传统的技术去开发新的应用,对于开发人员多内心来说也是一种折磨。这里并不是说多页面应用不好,只能说各有各自的好,单页面应用如果通过大量的极致优化手段,是可以从不少方面跟原生一拼。
DIV
标签,其他都是js
动态生态的内容index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
<script>
</script>
</html>
vue react
框架的入口文件中指定对应的渲染元素:import React from 'react;
import ReactDOM from 'react-dom';
ReactDOM.render(
<App/>,
document.querySelector("#root")
)
react-router或者 react-router-dom,dva
等路由跳转的库<HashRouter>//这里使用HashRouter
<ErrorBoundary>//React错误边界
<Switch>
<Route path="/login" component={Login} />
<Route path="/home" component={Home} />
<Route path="/" component={NotFound} />//404路由或者重定向都可以
</Switch>
</ErrorBoundary>
</HashRouter>
url
地址发生变化,但是其实并没有发送请求,也没有刷新整个页面url
地址栏会变化HashRouter
和BrowserRouter
两种模式Hash
模式跳转:hash 就是指 url 后的 # 号以及后面的字符。例如
www.baidu.com/#segmentfault
,那么#segmentfault
就是hash
值
window.location.hash
= '**'; // 设置当前的hash值const hash = window.location.hash
获取当前的hash值hash
改变会触发window
的hashchange
事件window.onhashchange=function(e){
let newURL = e.newURL; // 改变后的新 url地址
let oldURL = e.oldURL; // 改变前的旧 url地址
}
这里特别注意,
hash
改变并不会发送请求
Hash
模式跳转:ES6
的class实现:hash
值,对应不同的函数调用处理。class Router {
constructor() {
this.routes = {};
this.currentUrl = '';
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
updateView() {
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl] && this.routes[this.currentUrl]();
}
init() {
window.addEventListener('load', this.updateView.bind(this), false);
window.addEventListener('hashchange', this.updateView.bind(this), false);
}
}
<div id="app">
<ul>
<li>
<a href="#/">home</a>
</li>
<li>
<a href="#/about">about</a>
</li>
<li>
<a href="#/topics">topics</a>
</li>
</ul>
<div id="content"></div>
</div>
<script src="js/router.js"></script>
<script>
const router = new Router();
router.init();
router.route('/', function () {
document.getElementById('content').innerHTML = 'Home';
});
router.route('/about', function () {
document.getElementById('content').innerHTML = 'About';
});
router.route('/topics', function () {
document.getElementById('content').innerHTML = 'Topics';
});
</script>
这样一个简单的
hash
模式路由就做好了,剩下的就是路由嵌套,以及错误边界的处理
History
模式实现:History
来自Html5
的规范History
模式,url
地址栏的改变并不会触发任何事件History
模式,可以使用history.pushState
,history.replaceState
来控制url
地址,history.pushState() 和 history.replaceState() 的区别在于:
history.pushState()
在保留现有历史记录的同时,将 url 加入到历史记录中。history.replaceState()
会将历史记录中的当前页面历史替换为 url。History
模式下,刷新页面会404,需要后端配合匹配一个任意路由,重定向到首页,特别是加上Nginx
反向代理服务器的时候我们需要换个思路,我们可以罗列出所有可能触发 history 改变的情况,并且将这些方式一一进行拦截,变相地监听 history 的改变。
只要对上述三种情况进行拦截,就可以变相监听到 history 的改变而做出调整。针对情况 1,HTML5 规范中有相应的 onpopstate 事件,通过它可以监听到前进或者后退按钮的点击,值得注意的是,调用 history.push(replace)State 并不会触发 onpopstate 事件。
class Router {
constructor() {
this.routes = {};
this.currentUrl = '';
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
updateView(url) {
this.currentUrl = url;
this.routes[this.currentUrl] && this.routes[this.currentUrl]();
}
bindLink() {
const allLink = document.querySelectorAll('a[data-href]');
for (let i = 0, len = allLink.length; i < len; i++) {
const current = allLink[i];
current.addEventListener(
'click',
e => {
e.preventDefault();
const url = current.getAttribute('data-href');
history.pushState({}, null, url);
this.updateView(url);
},
false
);
}
}
init() {
this.bindLink();
window.addEventListener('popstate', e => {
this.updateView(window.location.pathname);
});
window.addEventListener('load', () => this.updateView('/'), false);
}
}
<div id="app">
<ul>
<li><a data-href="/" href="#">home</a></li>
<li><a data-href="/about" href="#">about</a></li>
<li><a data-href="/topics" href="#">topics</a></li>
</ul>
<div id="content"></div>
</div>
<script src="js/router.js"></script>
<script>
const router = new Router();
router.init();
router.route('/', function() {
document.getElementById('content').innerHTML = 'Home';
});
router.route('/about', function() {
document.getElementById('content').innerHTML = 'About';
});
router.route('/topics', function() {
document.getElementById('content').innerHTML = 'Topics';
});
</script>
setTimeout(() => {
history.pushState({}, null, '/about');
router.updateView('/about');
}, 2000);
React-router-dom
源码:Router
组件:export class Route extends Component {
componentWillMount() {
window.addEventListener('hashchange', this.updateView, false);
}
componentWillUnmount() {
window.removeEventListener('hashchange', this.updateView, false);
}
updateView = () => {
this.forceUpdate();
}
render() {
const { path, exact, component } = this.props;
const match = matchPath(window.location.hash, { exact, path });
if (!match) {
return null;
}
if (component) {
return React.createElement(component, { match });
}
return null;
}
}
hash change
原生事件,将要卸载时候移除事件监听防止内存泄漏hash
改变,就触发所有对应hash
的回掉,所有的Router
都去更新视图Router
组件中,都去对比当前的hash
值和这个组件的path
属性,如果不一样,那么就返回null
,·否则就渲染这个组件对应的视图History
模式的实现:这里想多留些时间写其他源码,这篇文章写得非常好,大家也可以去看看,本文很多借鉴他的。
withRouter
高阶函数的源码:var withRouter = function withRouter(Component) {
var C = function C(props) {
var wrappedComponentRef = props.wrappedComponentRef,
remainingProps = _objectWithoutProperties(props, ["wrappedComponentRef"]);
return _react2.default.createElement(_Route2.default, {
children: function children(routeComponentProps) {
return _react2.default.createElement(Component, _extends({}, remainingProps, routeComponentProps, {
ref: wrappedComponentRef
}));
}
});
};
C.displayName = "withRouter(" + (Component.displayName || Component.name) + ")";
C.WrappedComponent = Component;
C.propTypes = {
wrappedComponentRef: _propTypes2.default.func
};
return (0, _hoistNonReactStatics2.default)(C, Component);
};
Switch
组件:Switch.prototype.render = function render() {
var route = this.context.router.route;
var children = this.props.children;
var location = this.props.location || route.location;
var match = void 0,
child = void 0;
_react2.default.Children.forEach(children, function (element) {
if (match == null && _react2.default.isValidElement(element)) {
var _element$props = element.props,
pathProp = _element$props.path,
exact = _element$props.exact,
strict = _element$props.strict,
sensitive = _element$props.sensitive,
from = _element$props.from;
var path = pathProp || from;
child = element;
match = (0, _matchPath2.default)(location.pathname, { path: path, exact: exact, strict: strict, sensitive: sensitive }, route.match);
}
});
return match ? _react2.default.cloneElement(child, { location: location, computedMatch: match }) : null;
};