Vue单页应用的配置
前面通过几篇文章了解并掌握了 Vue 项目构建及运行的前期工作 。接下来我们可以走进 Vue 项目的内部,一探其内部配置的基本构成。
1. 路由配置
由于 Vue 这类型的框架都是以一个或多个单页构成,在单页内部跳转并不会重新渲染 HTML 文件,其路由可以由前端进行控制,因此我们需要在项目内部编写相应的路由文件,Vue 会解析这些文件中的配置并进行对应的跳转渲染。
我们来看一下 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
}]
})
这份配置可以算是最基础的路由配置,有以下几点需要进行优化:
-
如果路由存在二级目录,需要添加 base 属性,否则默认为 "/"
-
默认路由模式是 hash 模式,会携带 # 标记,与真实 url 不符,可以改为 history 模式
-
页面组件没有进行按需加载,可以使用
require.ensure()
来进行优化
下面是我们优化结束的代码:
/* router.js */
import Vue from 'vue'
import Router from 'vue-router'
// 引入 Home 组件
// 使用 require.ensure 实现代码分割,只有在访问该路由时才会加载 Home.vue 组件,实现懒加载
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
}]
})
改为 history 后我们 url 的路径就变成了 http://127.0.0.1:8080/vue/about
,而不是原来的 http://127.0.0.1:8080/vue/#/about
,但是需要注意页面渲染 404 的问题,具体可查阅:HTML5 History 模式。
而在异步加载的优化上,我们使用了 webpack 提供的 require.ensure() 进行了代码拆分,主要区别在于没有优化前,访问 Home 页面会一起加载 About 组件的资源,因为它们打包进了一个 app.js 中:
但是优化过后,它们分别被拆分成了 2.js 和 3.js:
如此,只有当用户点击了某页面,才会加载对应页面的 js 文件,实现了按需加载的功能。
webpack 在编译时,会静态地解析代码中的 require.ensure(),同时将模块添加到一个分开的 chunk 当中。这个新的 chunk 会被 webpack 通过 jsonp 来按需加载。 关于
require.ensure()
的知识点可以参考官方文档:require.ensure。
当然,除了使用 require.ensure 来拆分代码,Vue Router 官方文档还推荐使用动态 import
语法来进行代码分块,比如上述 require.ensure 代码可以修改为
// 引入 Home 组件
const Home = () => import('./views/Home.vue');
// 引入 About 组件
const About = () => import('./views/About.vue');
其余代码可以保持不变,仍然可以实现同样的功能。如果你想给拆分出的文件命名,可以尝试一下 webpack 提供的 Magic Comments
(魔法注释):
const Home = () => import(/* webpackChunkName:'home'*/ './views/Home.vue');
2. Vuex 配置
除了 vue-router,如果你的项目需要用到 Vuex ,那么你应该对它有一定的了解,Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。这里我们先来看一下使用 CLI 生成的配置文件 store.js 中的内容:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
}
})
该配置文件便是 Vuex 的配置文件,主要有 4 个核心点:state、mutations、actions 及 getter,详细的介绍大家可以参考官方文档:核心概念,这里我用一句话介绍它们之间的关系就是:我们可以通过 actions 异步提交 mutations 去 修改 state 的值并通过 getter 获取。
需要注意的是不是每一个项目都适合使用 Vuex,如果你的项目是中大型项目,那么使用 Vuex 来管理错综复杂的状态数据是很有帮助的,而为了后期的拓展性和可维护性,这里不建议使用 CLI 生成的一份配置文件来管理所有的状态操作,我们可以把它拆分为以下目录:
└── 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
上方我们导出 A 模块,并在 index.js 中引入:
/* index.js */
import Vue from 'vue'
import Vuex from 'vuex'
import moduleA from './modules/moduleA'
import moduleB from './modules/moduleB'
import { mutations } from './mutations'
import actions from './actions'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
groups: [1]
},
modules: {
moduleA, // 引入 A 模块
moduleB, // 引入 B 模块
},
actions, // 根级别的 action
mutations, // 根级别的 mutations
// 根级别的 getters
getters: {
getGroups (state) {
return state.groups
}
}
})
3. 接口配置
在项目的开发过程中,我们也少不了与后台服务器进行数据的获取和交互,这一般都是通过接口完成的,那么我们如何进行合理的接口配置呢?我们可以在 src 目录下新建 services 文件夹用于存放接口文件:
└── src
└── services
├── http.js # 接口封装
├── moduleA.js # A模块接口
└── moduleB.js # B模块接口
为了让接口便于管理,我们同样使用不同的文件来配置不同模块的接口,同时由于接口的调用 ajax 请求代码重复部分较多,我们可以对其进行简单的封装,比如在 http.js 中(fetch为例):
/* http.js */
import 'whatwg-fetch'
// HTTP 工具类
export default class Http {
static async request(method, url, data) {
const param = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if (method === 'GET') {
url += this.formatQuery(data)
} else {
param['body'] = JSON.stringify(data)
}
// Tips.loading(); // 可调用 loading 组件
return fetch(url, param).then(response => this.isSuccess(response))
.then(response => {
return response.json()
})
}
// 判断请求是否成功
static isSuccess(res) {
if (res.status >= 200 && res.status < 300) {
return res
} else {
this.requestException(res)
}
}
// 处理异常
static requestException(res) {
const error = new Error(res.statusText)
error.response = res
throw error
}
// url处理
static formatQuery(query) {
let params = [];
if (query) {
for (let item in query) {
let vals = query[item];
if (vals !== undefined) {
params.push(item + '=' + query[item])
}
}
}
return params.length ? '?' + params.join('&') : '';
}
// 处理 get 请求
static get(url, data) {
return this.request('GET', url, data)
}
// 处理 put 请求
static put(url, data) {
return this.request('PUT', url, data)
}
// 处理 post 请求
static post(url, data) {
return this.request('POST', url, data)
}
// 处理 patch 请求
static patch(url, data) {
return this.request('PATCH', url, data)
}
// 处理 delete 请求
static delete(url, data) {
return this.request('DELETE', url, data)
}
}
封装完毕后我们在 moduleA.js 中配置一个 github 的开放接口:https://api.github.com/repos/octokit/octokit.rb
/* moduleA.js */
import Http from './http'
// 获取测试数据
export const getTestData = () => {
return Http.get('https://api.github.com/repos/octokit/octokit.rb')
}
然后在项目页面中进行调用,会成功获取 github 返回的数据,但是一般我们在项目中配置接口的时候会直接省略项目 url 部分,比如:
/* moduleA.js */
import Http from './http'
// 获取测试数据
export const getTestData = () => {
return Http.get('/repos/octokit/octokit.rb')
}
这时候我们再次调用接口的时候会发现其调用地址为本地地址:http://127.0.0.1:8080/repos/octokit/octokit.rb
,这便是CORS(跨域资源共享)问题。那么为了让其指向 https://api.github.com
,我们需要在 vue.config.js 中进行 devServer 的配置:
/* vue.config.js */
module.exports = {
...
devServer: {
// string | Object 代理设置
proxy: {
// 接口是 '/repos' 开头的才用代理
'/repos': {
// 目标API地址,所有匹配 /repos 的请求都会转发到这个地址
target: 'https://api.github.com',
// 允许跨域
changeOrigin: true,
// 重新写路径,将 '/api' 前缀去掉,这样,实际请求发送到 https://api.github.com 时不会再包含 /api 前缀
pathRewrite: { '^/api': '' },
}
},
}
...
}
在 devServer 中 我们配置 proxy 进行接口的代理,将我们本地地址转换为真实的服务器地址,此时我们同样能顺利的获取到数据,不同点在于接口状态变成了 304(重定向):
拓展1
304 状态码
1.304 状态码的含义
- 304 Not Modified: 这个状态码表示自从上次请求以来,资源没有被修改。服务器会在响应中不返回资源的实体内容,而是告诉客户端使用缓存的版本。
为什么会出现 304 状态码?
当你在开发环境中使用代理设置时,devServer
可能会利用浏览器的缓存机制。以下是一些可能导致出现 304
状态码的原因:
-
浏览器缓存: 浏览器会根据
Cache-Control
和ETag
等 HTTP 头信息来缓存资源。如果你之前请求过某个接口,且没有添加任何请求参数或头信息,浏览器可能会直接从缓存中返回304
状态。 -
代理配置的影响: 如果
proxy
的目标服务器支持缓存,并且根据请求头(如If-None-Match
)返回304
,则会在代理过程中看到这一返回状态。
如何处理 304 状态码?
如果你希望确保始终获取最新的数据,而不是使用缓存,可以考虑以下几种方法:
-
禁用缓存: 在请求中添加一些请求头,告知服务器和浏览器不使用缓存。
axios.get('/api/repos/octokit/octokit.rb', { headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Expires': '0' } });
-
使用唯一的请求参数: 在请求 URL 中添加随机参数或时间戳,以确保请求是唯一的。
axios.get(`/api/repos/octokit/octokit.rb?timestamp=${new Date().getTime()}`);
-
检查服务器响应: 确保你的服务器设置了适当的缓存策略。如果你在开发环境中使用的是某种 API,可以检查其文档,确保它按预期工作。
2.什么是重定向
在 HTTP 协议中,重定向是指服务器向客户端发送一个响应,告知其需要访问其他的 URL。这通常发生在请求的资源位置发生变化、需要进行身份验证、或者为了其他目的引导用户访问不同的页面或接口。
常见的重定向状态码
以下是一些常见的 HTTP 重定向状态码及其含义:
-
301 Moved Permanently: 永久重定向,表示请求的资源已被永久移至新位置。客户端应使用新 URL 进行后续请求。
-
302 Found: 临时重定向,表示请求的资源临时被移至新位置。客户端仍应使用原 URL 进行后续请求。
-
303 See Other: 表示请求的响应可以在另一个 URL 上找到,通常用于 POST 请求之后重定向到 GET 请求。
-
307 Temporary Redirect: 临时重定向,表示请求的资源临时移至新位置,客户端应使用新 URL 进行后续请求,但仍应使用原请求方法。
-
308 Permanent Redirect: 永久重定向,表示请求的资源已被永久移至新位置,客户端应使用新 URL 进行后续请求,且保留原请求方法。
重定向的工作流程
- 客户端发送请求到一个特定的 URL。
- 服务器处理请求并返回一个重定向状态码(如 301 或 302),通常附带一个新的 URL。
- 客户端接收到响应,并根据新的 URL 再次发送请求。
- 服务器响应新的请求,返回最终的资源。
开发中重定向的应用
在前端开发中,重定向常用于以下场景:
- 用户登录: 在用户成功登录后,将其重定向到首页或仪表板。
- 资源迁移: 当 API 路径发生变化时,可以通过重定向引导用户使用新的路径。
- 访问控制: 未经授权的用户访问受保护的资源时,可以重定向到登录页面。
示例:使用 Axios 处理重定向
在前端使用 Axios 进行 HTTP 请求时,如果遇到重定向,Axios 会自动处理这些重定向。以下是一个简单的示例:
import axios from 'axios';
axios.get('https://example.com/api/resource')
.then(response => {
console.log('最终响应数据:', response.data);
})
.catch(error => {
console.error('请求出错:', error);
});
在这个示例中,如果 https://example.com/api/resource
发生重定向,Axios 会自动跟踪重定向并最终返回最终资源的数据。
4. 公共设施配置
最后我们项目开发中肯定需要对一些公共的方法进行封装使用,这里我把它称之为公共设施,那么我们可以在 src 目录下建一个 common 文件夹来存放其配置文件:
/* index.js */
import Validate from './validate'
import Other from './other'
export {
Validate,
Other,
}
这样我们在页面中只需要引入一个 index.js 即可。
本案例代码地址:single-page-project
拓展2
1.devServer 中 proxy 的 key 值代表什么?如果再添加一个 /reposed
的配置会产生什么隐患?
在 devServer
的 proxy
配置中,key
值(即配置的 URL 路径)是用来匹配请求的 URL 的前缀。当你的开发服务器收到请求时,它会检查请求的 URL 是否以配置的 key
开头。如果匹配,则会将该请求转发到指定的目标服务器。
例如:
javascript
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://example.com',
changeOrigin: true
},
'/repos': {
target: 'http://another-api.com',
changeOrigin: true
}
}
}
};
在这个示例中:
- 所有以
/api
开头的请求将会被代理到http://example.com
。 - 所有以
/repos
开头的请求将会被代理到http://another-api.com
。
添加 /repos
配置的隐患
-
冲突: 如果
/repos
与其他 URL 路径重叠,可能会导致请求的目标不明确。例如,如果你有一个/api/repos
的请求,可能会导致混淆,无法确定请求应该被代理到哪个目标。 -
优先级: 在
proxy
的配置中,匹配是基于先出现的规则。如果/api
和/repos
都存在,并且请求是/api/repos
,则首先匹配到/api
,而不会转发到/repos
。这可能导致一些意外的行为,尤其是当你希望某些请求到达特定的服务器时。