什么是路由?
路由这概念最开始是在后端出现的, 在以前前后端不分离的时候, 由后端来控制路由, 服务器接收客户端的请求, 解析对应的 url 路径, 并返回对应的页面 / 资源.
简单的说 路由就是根据不同的 url 地址来展示不同的内容或页面.
前端路由的来源
在很久很久以前~ 用户的每次更新操作都需要重新刷新页面, 非常的影响交互体验, 后来, 为了解决这个问题, 便有了 Ajax(异步加载方案),Ajax 给体验带来了极大的提升.
虽然 Ajax 解决了用户交互时体验的痛点, 但是多页面之间的跳转一样会有不好的体验, 所以便有了 spa(single-page application) 使用的诞生. 而 spa 应用便是基于前端路由实现的, 所以便有了前端路由.
如今比较火的 vue-router/react-router 也是基于前端路由的原理实现的~
前端路由的两种实现原理
1.Hash 模式
Windows 对象提供了 onhashchange 事件来监听 hash 值的改变, 一旦 url 中的 hash 值发生改变, 便会触发该事件.
- Windows.onhashchange = function(){
- // hash 值改变
- // do you want
- }
2.History 模式
html5 的 History API 为浏览器的全局 history 对象增加的扩展方法.
简单来说, history 其实就是浏览器历史栈的一个接口. 这里不细说 history 的每个 API 啦. 具体可查阅 传送门
Windows 对象提供了 onpopstate 事件来监听历史栈的改变, 一旦历史栈信息发生改变, 便会触发该事件.
需要特别注意的是, 调用 history.pushState() 或 history.replaceState() 不会触发 popstate 事件. 只有在做出浏览器动作时, 才会触发该事件.
- Windows.onpopstate = function(){
- // 历史栈 信息改变
- // do you want
- }
history 提供了两个操作历史栈的 API:history.pushState 和 history.replaceState
history.pushState(data[,title][,url]);// 向历史记录中追加一条记录
history.replaceState(data[,title][,url]);// 替换当前页在历史记录中的信息.
- // data: 一个 JavaScript 对象, 与用 pushState() 方法创建的新历史记录条目关联. 无论何时用户导航到新创建的状态, popstate 事件都会被触发, 并且事件对象的 state 属性都包含历史记录条目的状态对象的拷贝.
- //title: Firefox 浏览器目前会忽略该参数, 虽然以后可能会用上. 考虑到未来可能会对该方法进行修改, 传一个空字符串会比较安全. 或者, 你也可以传入一个简短的标题, 标明将要进入的状态.
- //url: 新的历史记录条目的地址. 浏览器不会在调用 pushState() 方法后加载该地址, 但之后, 可能会试图加载, 例如用户重启浏览器. 新的 URL 不一定是绝对路径; 如果是相对路径, 它将以当前 URL 为基准; 传入的 URL 与当前 URL 应该是同源的, 否则, pushState() 会抛出异常. 该参数是可选的; 不指定的话则为文档当前 URL.
两种模式优劣对比
对比 | Hash | History |
---|---|---|
观赏性 | 丑 | 美 |
兼容性 | >ie8 | >ie10 |
实用性 | 直接使用 | 需后端配合 |
命名空间 | 同一 document | 同源 |
造 (cao) 一个简单的前端路由
本 demo 只是想说帮助我们通过实践更进一步的理解前端路由这个概念, 所以只做了简单的实现~
history 模式 404
当我们使用 history 模式时, 如果没有进行配置, 刷新页面会出现 404.
原因是因为 history 模式的 url 是真实的 url, 服务器会对 url 的文件路径进行资源查找, 找不到资源就会返回 404.
这个问题的解决方案这里就不细说了, google 一下, 你就知道~ 我们在以下 demo 使用 webpack-dev-server 的里的 historyApiFallback 属性来支持 HTML5 History Mode.
文件结构
- |-- package.JSON
- |-- webpack.config.JS
- |-- index.HTML
- |-- src
- |-- index.JS
- |-- routeList.JS
- |-- base.JS
- |-- hash.JS
- |-- history.JS
1. 搭建环境
废话不多说, 直接上代码~
- package.JSON
- {
- "name": "web_router",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
- "dev": "webpack-dev-server --config ./webpack.config.js"
- },
- "author": "webfansplz",
- "license": "MIT",
- "devDependencies": {
- "html-webpack-plugin": "^3.2.0",
- "webpack": "^4.28.1",
- "webpack-cli": "^3.2.1",
- "webpack-dev-server": "^3.1.14"
- }
- }
- webpack.config.JS
- 'use strict';
- const path = require('path');
- const webpack = require('webpack');
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- module.exports = {
- mode: 'development',
- entry: './src/index.js',
- output: {
- filename: '[name].js'
- },
- devServer: {
- clientLogLevel: 'warning',
- hot: true,
- inline: true,
- open: true,
- // 在开发单页应用时非常有用, 它依赖于 HTML5 history API, 如果设置为 true, 所有的跳转将指向 index.HTML (解决 histroy mode 404)
- historyApiFallback: true,
- host: 'localhost',
- port: '6789',
- compress: true
- },
- plugins: [
- new webpack.HotModuleReplacementPlugin(),
- new HtmlWebpackPlugin({
- filename: 'index.html',
- template: 'index.html',
- inject: true
- })
- ]
- };
2. 开撸
首先我们先初始化定义我们需要实现的功能及配置参数.
- src/index.JS
- const MODE='';
- const ROUTELIST=[];
- class WebRouter {
- constructor() {
- }
- push(path) {
- ...
- }
- replace(path) {
- ...
- }
- go(num) {
- ...
- }
- }
- new WebRouter({
- mode: MODE,
- routeList: ROUTELIST
- });
- src/routeList.JS
- export const ROUTELIST = [
- {
- path: '/',
- name: 'index',
- component: 'This is index page'
- },
- {
- path: '/hash',
- name: 'hash',
- component: 'This is hash page'
- },
- {
- path: '/history',
- name: 'history',
- component: 'This is history page'
- },
- {
- path: '*',
- name: 'notFound',
- component: '404 NOT FOUND'
- }
- ];
- src/hash.JS
- export class HashRouter{
- }
- src/history.JS
- export class HistoryRouter{
- }
- src/index.JS
- import { HashRouter } from './hash';
- import { HistoryRouter } from './history';
- import { ROUTELIST } from './routeList';
- // 路由模式
- const MODE = 'hash';
- class WebRouter {
- constructor({ mode = 'hash', routeList }) {
- this.router = mode === 'hash' ? new HashRouter(routeList) : new HistoryRouter(routeList);
- }
- push(path) {
- this.router.push(path);
- }
- replace(path) {
- this.router.replace(path);
- }
- go(num) {
- this.router.go(num);
- }
- }
- const webRouter = new WebRouter({
- mode: MODE,
- routeList: ROUTELIST
- });
- <!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>
- 前端路由
- </title>
- </head>
- <body>
- <div id="page">
- </div>
- </body>
- </HTML>
- JS/base.JS
- const ELEMENT = document.querySelector('#page');
- export class BaseRouter {
- //list = 路由列表
- constructor(list) {
- this.list = list;
- }
- render(state) {
- // 匹配当前的路由, 匹配不到则使用 404 配置内容 并渲染~
- let ele = this.list.find(ele => ele.path === state);
- ele = ele ? ele : this.list.find(ele => ele.path === '*');
- ELEMENT.innerText = ele.component;
- }
- }
- src/hash.JS
- import { BaseRouter } from './base.js';
- export class HashRouter extends BaseRouter {
- constructor(list) {
- super(list);
- this.handler();
- // 监听 hash 变化事件, hash 变化重新渲染
- Windows.addEventListener('hashchange', e => {
- this.handler();
- });
- }
- // 渲染
- handler() {
- this.render(this.getState());
- }
- // 获取当前 hash
- getState() {
- const hash = Windows.location.hash;
- return hash ? hash.slice(1) : '/';
- }
- // 获取完整 url
- getUrl(path) {
- const href = Windows.location.href;
- const i = href.indexOf('#');
- const base = i>= 0 ? href.slice(0, i) : href;
- return `${base}#${path}`;
- }
- // 改变 hash 值 实现压入 功能
- push(path) {
- Windows.location.hash = path;
- }
- // 使用 location.replace 实现替换 功能
- replace(path) {
- Windows.location.replace(this.getUrl(path));
- }
- // 这里使用 history 模式的 go 方法进行模拟 前进 / 后退 功能
- go(n) {
- Windows.history.go(n);
- }
- }
- src/history.JS
- import { BaseRouter } from './base.js';
- export class HistoryRouter extends BaseRouter {
- constructor(list) {
- super(list);
- this.handler();
- // 监听历史栈信息变化, 变化时重新渲染
- Windows.addEventListener('popstate', e => {
- this.handler();
- });
- }
- // 渲染
- handler() {
- this.render(this.getState());
- }
- // 获取路由路径
- getState() {
- const path = Windows.location.pathname;
- return path ? path : '/';
- }
- // 使用 pushState 方法实现压入功能
- //PushState 不会触发 popstate 事件, 所以需要手动调用渲染函数
- push(path) {
- history.pushState(null, null, path);
- this.handler();
- }
- // 使用 replaceState 实现替换功能
- //replaceState 不会触发 popstate 事件, 所以需要手动调用渲染函数
- replace(path) {
- history.replaceState(null, null, path);
- this.handler();
- }
- go(n) {
- Windows.history.go(n);
- }
- }
来源: https://juejin.im/post/5c380afde51d4552232fb077