前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Vue多页路由与模板解析 单页间跳转与模板定制

首页2019-06-02 00:15:32Front-End
VueVue RouterWebpack工程化

多页应用的构建配好之后,第一个真正卡住人的问题往往是:从 page1 点一个链接跳到 page2,this.$router.push('/page2') 敲下去,地址栏变了,页面没换,控制台还一声不吭。接着是本地跑得好好的,刷新 /vue/page1 直接 404。这两个坑都出在同一个地方,多页应用的每个 html 是独立的运行时,路由的边界跟单页完全不是一回事。这篇把多页的路由前缀、跨单页跳转、服务端重写,还有 html 模板怎么按环境定制,一条线讲清楚。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 多页应用里为什么 vue-router 跳不到别的单页,只能用 location
  • 用 base 给每个单页的路由加前缀,避免路径互相打架
  • 把跨单页跳转封装成 $openRouter,保持和 vue-router 一致的调用手感
  • devServer.historyApiFallback.rewrites 怎么把 /vue/page1 重写到 page1.html
  • html-webpack-plugin 的默认模板语法,htmlWebpackPlugin.files 里到底有什么
  • 用自定义配置在生产环境往指定模板里注入统计脚本

# 一、路由配置

# 跨单页跳转只能靠 location

在配置路由前,首先我们要明确一点就是,多页应用中的每个单页都是相互隔离的。即如果你想从 page1 下的路由跳到 page2 下的路由,你无法使用 vue-router 中的方法进行跳转,需要使用原生方法 location.href 或 location.replace。

为什么?因为 vue-router 从头到尾都活在一个 html 的内存里。它的路由表是这个页面加载时注册的,push 做的事情是调 history.pushState 改地址栏,然后在自己的路由表里找匹配项去换 <router-view> 的内容。page2 的路由表在另一个 html 里,当前这个页面根本不知道它的存在,匹配不上就只能落到 404 或者什么都不做。

所以跨单页跳转必须让浏览器真的去请求另一个文档。

location.href 和 location.replace 的区别在历史记录上。前者会往历史栈里压一条,用户按返回键能回来;后者是替换当前记录,回不去。登录跳转、支付完成这类不希望用户退回去的场景用 replace,普通导航用 href。

此外为了能够清晰地分辨路由属于哪个单页,我们应该给每个单页路由添加前缀,比如:

  • index 单页:/vue/
  • page1 单页:/vue/page1/
  • page2 单页:/vue/page2/

其中 /vue/ 为项目的二级目录,其后的目录代表路由属于哪个单页。因此我们每个单页的路由配置可以像这样:

/* page1 单页路由配置 */

import Vue from 'vue'
import Router from 'vue-router'

// 首页
const Home = (resolve => {
    require.ensure(['../views/home.vue'], () => {
        resolve(require('../views/home.vue'))
    })
})

Vue.use(Router)

let base = `${process.env.BASE_URL}` + 'page1'; // 添加单页前缀

export default new Router({
    mode: 'history',
    base: base,
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home
        },
    ]
})
@前端进阶之旅: 代码已经复制到剪贴板

我们通过设置路由的 base 值来为每个单页添加路由前缀,如果是 index 单页我们无需拼接路由前缀,直接跳转至二级目录即可。

这里的巧妙之处在于,每个单页内部的路由表都可以从 / 开始写,互相之间不用协调命名。page1 有一个 /detail,page2 也有一个 /detail,两者实际的 URL 是 /vue/page1/detail 和 /vue/page2/detail,各走各的。前缀这件事交给 base 统一处理,业务代码里一个字都不用改。

process.env.BASE_URL 是 Vue CLI 3 根据 vue.config.js 里的 publicPath 自动注入的,这一层的来龙去脉我在 Vue单页应用的基本配置 里说过。

那么在单页间跳转的地方,我们可以这样写:

<template>
  <div id="app">
    <div id="nav">
      <a @click="goFn('')">Index</a> |
      <a @click="goFn('page1')">Page1</a> |
      <a @click="goFn('page2')">Page2</a> |
    </div>
    <router-view/>
  </div>
</template>

<script>
export default {
    methods: {
        goFn(name) {
            location.href = `${process.env.BASE_URL}` + name
        }
    }
}
</script>
@前端进阶之旅: 代码已经复制到剪贴板

能用,但难受。项目里一半跳转写 this.$router.push({ name, query }),另一半写 location.href = 拼字符串,参数传递的写法也完全不同,新人过来根本分不清什么时候该用哪个。

为了保持和 Vue 路由跳转同样的风格,我可以对单页之间的跳转做一下封装,实现一个 Navigator 类。封装完成后我们可以将跳转方法修改为:

this.$openRouter({
    name: name, // 跳转地址
    query: {
        text: 'hello' // 可以进行参数传递
    },
})
@前端进阶之旅: 代码已经复制到剪贴板

调用手感和 this.$router.push 对齐了,Navigator 内部做的事情就是把 name 拼成完整路径、把 query 对象序列化成 query string,最后走 location.href。这样业务代码里两种跳转看起来是一套东西,未来哪天页面合并回单页,改起来也只动 Navigator 一个文件。

使用上述 $openRouter 方法我们还需要一个前提条件,便是将其绑定到 Vue 的原型链上。我们在所有单页的入口文件中添加:

import { Navigator } from '../../common' // 引入 Navigator

Vue.prototype.$openRouter = Navigator.openRouter; // 添加至 Vue 原型链
@前端进阶之旅: 代码已经复制到剪贴板

「在所有单页的入口文件中添加」这句话本身就是个信号。三个入口文件里抄三遍同样的代码,改一次要改三处。这个问题有专门的解法,把入口的公共配置抽成一个函数,我在 Vue之项目整合与优化 里写了具体怎么做。

至此我们已经能够成功模仿 vue-router 进行单页间的跳转,但是需要注意的是因为其本质使用的是 location 跳转,所以必然会产生浏览器的刷新与重载。这个成本躲不掉,页面状态全丢,公共依赖重新解析一遍。所以多页的切分粒度要想清楚,把用户会频繁来回切的两个页面拆成两个单页,体验是倒退的。

# 重定向

当我们完成上述路由跳转的功能后,可以在本地服务器上来进行一下测试。你会发现 Index 首页可以正常打开,但是跳转 Page1、Page2 却仍然处于 Index 父组件下。

这是因为浏览器认为你所要跳转的页面还是在 Index 根路由下,同时又没有匹配到 Index 单页中对应的路由。

说得再具体一点,你请求的是 /vue/page1 这个路径,服务器上并没有一个叫 page1 的文件或目录,开发服务器的 history 兜底规则把它交还给了 index.html。于是加载的还是 Index 单页,它的路由表里没有 /page1,就停在那儿了。

这时候我们服务器需要做一次重定向,将下方路由指向对应的 html 文件即可:

/vue/page1 -> /vue/page1.html
/vue/page2 -> /vue/page2.html
@前端进阶之旅: 代码已经复制到剪贴板

在 vue.config.js 中,我们需要对 devServer 进行配置,添加 historyApiFallback 配置项。该配置项主要用于解决 HTML5 History API 产生的问题,比如其 rewrites 选项用于重写路由:

/* vue.config.js */

let baseUrl = '/vue/';

module.exports = {
    ...
    
    devServer: {
        historyApiFallback: {
            rewrites: [
                { from: new RegExp(baseUrl + 'page1'), to: baseUrl + 'page1.html' },
                { from: new RegExp(baseUrl + 'page2'), to: baseUrl + 'page2.html' },
            ]
        }
    }
    
    ...
}
@前端进阶之旅: 代码已经复制到剪贴板

上方我们通过 rewrites 匹配正则表达式的方式将 /vue/page1 这样的路由替换为访问服务器下正确 html 文件的形式,如此不同单页间便可以进行正确跳转和访问了。

两个提醒。

一是 rewrites 是按数组顺序匹配的,命中第一条就停。规则写得越宽泛越要往后放,否则会把后面的都吃掉。这里的正则没加 ^ 和边界,/vue/page10 也会被 page1 这条命中,页面数量多起来之后建议把正则写严一点。

二是这只是开发服务器的配置。生产环境上 devServer 整个不存在,同样的重写规则得在 nginx 或者你用的静态服务器上再配一遍。这件事我第一次做多页的时候忘了,本地全绿,一上测试环境所有子页面 404,排查了半天才想起来 devServer 三个字的含义。

# 二、模板配置

# 模板渲染

这里所说的模板渲染是在我们的 html 模板文件中使用 html-webpack-plugin 提供的 default template 语法进行模板编写。比如:

fe
  • 一、路由配置
    • 跨单页跳转只能靠 location
    • 重定向
  • 二、模板配置
    • 模板渲染
    • 自定义配置
    • 这套写法在今天
  • 总结
  • 参考

← Vue之项目整合与优化 alias 入口收敛与 Gzip 压缩Vue CLI3之pages 构建多页应用 多入口与多模板自动化 →