前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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单页应用的基本配置 路由 Vuex 与接口层分层

首页2019-06-02 00:13:32Front-End
VueVue CLI工程化

vue create 敲完,项目跑起来了,Home.vue 和 About.vue 也能来回切。看着挺完整,但真接上业务就会发现,路由是全量打进一个 bundle 的,Vuex 只有一个空壳 store.js,请求是散在各个组件里的 axios.get,公共方法找不到地方放。这些坑我在几个项目上都重复踩过一遍。这篇把单页项目开工前该定下来的四件事捋一遍,路由、状态、接口、公共设施,每一处都给出改动前后的代码和改动的理由。读完你能拿到一份可以直接抄进新项目的目录约定。

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

  • CLI 默认生成的 router.js 有哪三处必须改
  • base、history 模式、路由懒加载各自解决什么问题,代价是什么
  • require.ensure 和动态 import() 的区别,以及 webpack 魔法注释怎么给 chunk 命名
  • Vuex 从单文件拆成模块的目录约定,四个核心概念之间的调用顺序
  • 接口层怎么封装成一个 Http 类,devServer.proxy 怎么绕开本地跨域
  • 这套 2019 年的配置,放到 Vite + Vue 3 时代对应的写法是什么

# 一、路由配置从默认模板改起

# CLI 生成的那份配置

先看 CLI 给我们生成的 router.js 文件的配置:

/* router.js */

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue' // 引入 Home 组件
import About from './views/About.vue' // 引入 About 组件

Vue.use(Router) // 注册路由

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

这份配置能跑,但它是给「跑通 demo」准备的,不是给上线准备的。有三点需要优化:

  • 如果路由存在二级目录,需要添加 base 属性,否则默认为 "/"
  • 默认路由模式是 hash 模式,会携带 # 标记,与真实 url 不符,可以改为 history 模式
  • 页面组件没有进行按需加载,可以使用 require.ensure() 来进行优化

下面是我们优化结束的代码:

/* router.js */

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

// 引入 Home 组件
const Home = resolve => {
    require.ensure(['./views/Home.vue'], () => {
        resolve(require('./views/Home.vue'))
    })
}

// 引入 About 组件
const About = resolve => {
    require.ensure(['./views/About.vue'], () => {
        resolve(require('./views/About.vue'))
    })
}

Vue.use(Router)

let base = `${process.env.BASE_URL}` // 动态获取二级目录

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

三处改动,三个不同的问题。挨个说。

# base 是给二级目录准备的

很多人第一次遇到 base 是在部署之后。本地 yarn serve 一切正常,传到服务器上放在 https://xxx.com/vue/ 这个路径下,页面一片空白,控制台全是 404。

原因是路由默认认为自己挂在域名根路径上。你访问 /vue/about,vue-router 拿到的 path 是 /vue/about,而路由表里注册的是 /about,匹配不上。

base 做的事就是告诉路由「前面这一截不归你管」。配上 base: '/vue/' 之后,vue-router 会先把这一段截掉再去匹配。

这里用 process.env.BASE_URL 而不是写死字符串,是因为 Vue CLI 3 会自动把 vue.config.js 里的 publicPath 注入成这个环境变量。这样部署路径只需要改一个地方,路由和静态资源引用会同步跟着走,不会出现「JS 加载成功但路由匹配不上」这种最难查的半残状态。

顺带说一句,publicPath 这个字段在 Vue CLI 3.3 之前叫 baseUrl,老文章和老项目里两种写法都能见到,看到 baseUrl 别以为是笔误。

# history 模式要后端配合

hash 模式的地址长这样:https://xxx.com/#/about。井号后面的内容不会发给服务器,所以刷新页面永远命中的是同一个 index.html,天然不需要后端做任何事。代价是 URL 里挂着一个多余的 #,做 SEO 和分享的时候都不好看。

history 模式用的是 HTML5 的 pushState,地址是干净的 https://xxx.com/about。但你在这个地址上按刷新,请求是真的会发到服务器的,服务器上并没有 /about 这个文件,返回 404。

所以 history 模式不是改一行配置就完事的。

服务端需要配一条兜底规则,把所有匹配不到静态文件的请求都指回 index.html。nginx 里就是 try_files $uri $uri/ /index.html; 这一行。这块在多页应用里要复杂得多,每个单页都得单独指回自己的 html,我在 Vue多页路由与模板解析 里专门写了怎么配。

# 路由懒加载,两种写法

不做懒加载的项目,打包出来就是一个几百 KB 的 app.js,用户打开首页要把整站所有页面的代码都下载完。页面越多,首屏越慢。

懒加载做的事是把每个路由组件切成单独的 chunk,访问到哪个路由才去下载哪个 chunk。

上面的优化版用的是 require.ensure,这是 webpack 早期提供的代码分割 API。它能用,但语法比较绕,resolve 回调那一层完全是仪式感。

除了使用 require.ensure 来拆分代码,Vue Router 官方文档还推荐使用动态 import 语法来进行代码分块,比如上述 require.ensure 代码可以修改为:

// 引入 Home 组件
const Home = () => import('./views/Home.vue');

// 引入 About 组件
const About = () => import('./views/About.vue');
@前端进阶之旅: 代码已经复制到剪贴板

一行搞定,而且这是 ECMAScript 的标准提案,不绑定 webpack。新项目直接用这个就行,require.ensure 只在维护老代码时才需要认识。

拆出来的 chunk 默认叫 0.js、1.js 这种数字名,线上排查问题的时候完全看不出是哪个页面。如果你想给拆分出的文件命名,可以尝试一下 webpack 提供的 Magic Comments(魔法注释):

const Home = () => import(/* webpackChunkName:'home'*/ './views/Home.vue');
@前端进阶之旅: 代码已经复制到剪贴板

这个注释会被 webpack 读走,打包产物就变成 home.js。多个路由用同一个 webpackChunkName,它们还会被合并进同一个 chunk,适合把几个总是一起访问的页面打包在一起,省几次请求。

# 二、Vuex 从单文件拆成模块

CLI 生成的 store.js 是这样一个空壳:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {

    },
    mutations: {

    },
    actions: {

    }
})
@前端进阶之旅: 代码已经复制到剪贴板

主要有 4 个核心点:state、mutations、actions 及 getter。

这里我用一句话介绍它们之间的关系就是:我们可以通过 actions 异步提交 mutations 去修改 state 的值并通过 getter 获取。

这个顺序不是 Vuex 故意找麻烦。mutations 必须是同步的,因为 devtools 要靠它记录每一次状态变更的快照,异步操作一进来时间线就乱了。所以异步逻辑统一放 actions,拿到结果再 commit 给 mutations。你在 actions 里直接改 state 也能改动,但 devtools 追踪不到,出问题的时候你会很想抽自己。

项目一大,所有状态堆在一个文件里就没法看了。按模块拆开:

└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── moduleA.js    # A模块
        └── moduleB.js    # B模块
@前端进阶之旅: 代码已经复制到剪贴板

与单个 store.js 文件不同的是,我们按模块进行了划分,每个模块中都可以包含自己 4 个核心功能。比如模块 A 中:

/* moduleA.js */

const moduleA = {
    state: { 
        text: 'hello'
    },
    mutations: {
        addText (state, txt) {
            // 这里的 `state` 对象是模块的局部状态
            state.text += txt
        }
    },
    
    actions: {
        setText ({ commit }) {
            commit('addText', ' world')
        }
    },

    getters: {
        getText (state) {
            return state.text + '!'
        }
    }
}

export default moduleA
@前端进阶之旅: 代码已经复制到剪贴板

注意 mutations 里那个 state 参数,它是模块的局部状态,不是根 state。想在模块里访问根 state,得从 actions 的第一个参数里解构 rootState。这个我踩过,在模块的 getters 里直接写 state.someGlobalFlag 拿到 undefined,查了半天才反应过来作用域不对。

然后在 index.js 里组装:

fe
  • 一、路由配置从默认模板改起
    • CLI 生成的那份配置
    • base 是给二级目录准备的
    • history 模式要后端配合
    • 路由懒加载,两种写法
  • 二、Vuex 从单文件拆成模块
  • 三、接口层封装成一个 Http 类
    • devServer 代理
  • 四、公共设施配置
  • 五、这套配置放到今天怎么写
  • 总结
  • 参考

← Vue CLI3之pages 构建多页应用 多入口与多模板自动化Vue CLI 3 项目构建基础 vue.config.js 配置详解 →