原生html实现一个mini-react-router
前言
实现一个简单版本的react-router
, 揭秘路由的神秘面纱
思考
• 前端路由本质上是什么
• 前端路由里的一些坑和注意点
• hash路由和history路由的区别
• Router组件和Route组件分别做了什么
路由的本质
浏览器端的路由不是真实的网页跳转,和服务器没有任何交互,本质上就是对url进行监听,让某个dom节点显示对应的视图 路由的区别
路由的区别
一般来说,前端路由分为两种 1、hash 路由, 特征是url后面会有 # 号, 如 baidu.com/#foo/bar/baz 2、history 路由, url和普通路由没有差异。 如 baidu.com/foo/bar/baz
实际上只要搞清楚两种路由分别是如何改变,并且组件是如何完成视图的展示的
hash
通过location.hash = 'foo'
这样的语法来改变, 路径, 路径就会由baidu.com
变成baidu.com/#foo
通过window.addEventListener('hashchange')
这个事件监听到hash
值的变化
- history
通过window.history.pushState(data, title, targetURL)
- @状态对象:传给目标路由的信息,可为空
- @页面标题:目前所有浏览器都不支持,填空字符串即可
- @可选url:目标url,不会检查url是否存在,且不能跨域。如不传该项,即给当前url添加data
通过
history.pushState({}, '', 'foo')
,可以让 baidu.com 变化为 baidu.com/foo
!坑
history
路由的监听,浏览器虽然提供了window.addEventListener('popstate事件'),但是只能监听浏览器回退和前进产生的路由变化,对于主动的pushState
却监听不到
基于history版本从零到1实现 react-mini-router
实现一个history
1、history.push 2、history.listen
利用观察者模式封装简单的listen API。让用户监听到history.push 产生的路径变化
// 参考 https://github.com/sl1673495/react-mini-router
let listeners = [];
function listen(fn){
listeners.push(fn)
}
function push(to, state) {
// 解析用户传入的 url
// 调用原生 history 的方法改变路由
window.history.pushState(state, '', to);
// 执行用户传入的监听函数
listeners.forEach(fn => fn(location));
}
// 用于处理浏览器前进后退操作
window.addEventListener('popstate', () => {
listeners.forEach(fn => fn(location));
});
history.listen = listen
history.push = push
history.location = location
简单实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Router History</title>
</head>
<body>
<div id="app"><div>
</body>
<script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script>
// 参考 https://github.com/sl1673495/react-mini-router
let listeners = [];
function listen(fn){
listeners.push(fn)
}
function push(to, state) {
// 解析用户传入的 url
// 调用原生 history 的方法改变路由
window.history.pushState(state, '', to);
// 执行用户传入的监听函数
listeners.forEach(fn => fn(location));
}
// 用于处理浏览器前进后退操作
window.addEventListener('popstate', () => {
listeners.forEach(fn => fn(location));
});
history.listen = listen
history.push = push
history.location = location
</script>
<script type="text/babel">
class App extends React.Component{
constructor(props){
super(props)
this.state={
showFoo: false
}
}
componentDidMount(){
history.listen(location => {
console.log(location,'location');
const {pathname} = location;
if(pathname == '/Router/foo'){
this.setState({
showFoo: true
})
}else{
this.setState({
showFoo: false
})
}
});
}
changeRouter(to){
history.push(to, {});
}
render(){
const {showFoo} = this.state;
return(
<div>
<button onClick={()=>{this.changeRouter('foo')}}>展示foo组件</button>
<button onClick={()=>{this.changeRouter('index')}}>回到首页</button>
{!showFoo && <div>首页</div>}
{showFoo && <div>我是Foo</div>}
</div>
)
}
}
ReactDOM.render(<App/>,document.getElementById('app'));
</script>
</html>
实现 Router
Router的核心原理就是通过Provider把location和history等路由关键信息传递给子组件,并切在路由发生变化的时候让子组件可以感知
const RouterContext = React.createContext(null)
class Router extends React.Component{
constructor(props){
super(props)
this.state={
location: location
}
}
componentDidMount(){
history.listen(location => {
this.setState({
location
})
});
}
render(){
const { location } = this.state
return(
<div>
<RouterContext.Provider value={{ history, location }}>
{this.props.children}
</RouterContext.Provider>
</div>
)
}
}
实现 Route
Route 组件接受 path 和 children两个 prop ,本质上就决定了在某个路径下需要渲染什么组件,我们又可以通过 Router 的 Provider 传递下来的 location 信息拿到当前路径,所以这个组件需要做的就是判断当前的路径是否匹配,渲染对应组件
const Route = ({ path, children }) => {
let {history,location} = React.useContext(RouterContext);
let { pathname } = location
console.log(pathname,path,'iii');
if(pathname === path){
return children
}
return null
};
实现 Link
Link 组件接受to和name两个参数,通过 Router 的 Provider 传递下来的 history 拿到当前的push方法,点击的时候去触发
const Link = ({name,to})=>{
let {history} = React.useContext(RouterContext);
function changeRouter(to){
history.push(to, {});
}
return (
<div>
<button onClick={()=>{changeRouter(to)}}>{name}</button>
</div>
)
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Router History</title>
</head>
<body>
<div id="app"><div>
</body>
<script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="./history.js"></script>
<script type="text/babel">
const RouterContext = React.createContext(null)
class Router extends React.Component{
constructor(props){
super(props)
this.state={
location: location
}
}
componentDidMount(){
history.listen(location => {
this.setState({
location
})
});
}
render(){
const { location } = this.state
return(
<div>
<RouterContext.Provider value={{ history, location }}>
{this.props.children}
</RouterContext.Provider>
</div>
)
}
}
const Route = ({ path, children }) => {
let {history,location} = React.useContext(RouterContext);
let { pathname } = location
console.log(pathname,path,'iii');
if(pathname === path){
return children
}
return null
};
const Link = ({name,to})=>{
let {history} = React.useContext(RouterContext);
function changeRouter(to){
history.push(to, {});
}
return (
<div>
<button onClick={()=>{changeRouter(to)}}>{name}</button>
</div>
)
}
class App extends React.Component{
constructor(props){
super(props)
}
render(){
return(
<div>
<Router>
<Link to="/foo" name="展示foo组件"/>
<Link to="/index" name="回到首页"/>
<Route path="/index">
<Index/>
</Route>
<Route path="/foo">
<Foo/>
</Route>
</Router>
</div>
)
}}
class Foo extends React.Component{
render(){
return <div>我是foo</div>
}
}
class Index extends React.Component{
render(){
return <div>我是index1</div>
}
}
ReactDOM.render(<App/>,document.getElementById('app'));
</script>
</html>
参考
最后
大家可以关注我的公众号,回复vue源码,可以得到完整的代码,也可以不懂的地方在下面留言!
码字不易,给个关注吧!!
rockshang