辰風依恛
文章35
标签0
分类11
Vue项目开发常用功能

Vue项目开发常用功能

Vue项目开发常用功能

vscode@配置

在根目录创建jsconfig.json(如果没有默认生成)

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

默认启动浏览器

修改package.json

1
2
// "serve": "vue-cli-service serve",
"serve": "vue-cli-service serve --open",

在vue.confog.js中添加

1
2
3
4
5
devServer: {
open: true, // 默认打开浏览器
host: 'localhost', // 路径
port: 8080, // 端口
}

重置默认样式

1
npm install --save normalize.css

main.js引入(在自己的样式之前)

1
import "normalize.css"

或者在index.css中引入

1
@import "normalize.css";

axios封装

创建axios.config.js管理不同环境的参数

创建axios.js/index.js管理请求拦截和响应拦截内容

根据对应的模块创建对应的文件/文件夹管理请求的接口

安装

1
npm i axios

案例:

axios.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 根据process.env.NODE_ENV
// - 开发环境:development
// - 生产环境:production
// - 测试环境:test
let BASE_UEL = ''
let TIME_OUT = 10000
if (process.env.NODE_ENV === 'development') {
BASE_UEL = ""
} else if (process.env.NODE_ENV === 'production') {
BASE_UEL = ""
} else {
BASE_UEL = ""
}

export { BASE_UEL, TIME_OUT }

axios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import axios from 'axios';
import { BASE_UEL, TIME_OUT } from './axios.config';
const request = axios.create({
baseURL: BASE_UEL,
timeout: TIME_OUT
});

// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
const user = store.state.user; // 获取token,
if (user) {
config.headers.Authorization = `Bearer ${user.token}`;// 设置token 也有可能是自定义的请求头
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

export {
request
}

模块接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { request } from './axios';

/**
*
* @param {Object} params get请求参数
* @returns
*/
export const get = (params) => {
return request({
method: 'get',
url: '/***',
params
})
}

/**
*
* @param {Object} data post请求参数
* @returns
*/
export const post = (data) => {
return request({
method: 'post',
url: '/***',
data
})
}

无感刷新Token

概述:服务器生成token的过程中,会有两个时间,一个是token失效时间,一个是token刷新时间,刷新时间肯定比失效时间长,当用户的 token 过期时,你可以拿着过期的token去换取新的token,来保持用户的登陆状态,当然你这个过期token的过期时间必须在刷新时间之内,如果超出了刷新时间,那么返回的依旧是 401。

处理流程:

  1. 在axios的拦截器中加入token刷新逻辑
  2. 当用户token过期时,去向服务器请求新的 token
  3. 把旧的token替换为新的token
  4. 然后继续用户当前的请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
* 封装 axios 请求模块
*/
import axios from "axios";
import jsonBig from "json-bigint";
import store from "@/store";
import router from "@/router";

// axios.create 方法:创建一个axios实例
const request = axios.create({
baseURL: "http://ttapi.research.itcast.cn/" // 基础路径
});

/**
* 配置处理后端返回数据中超出 js 安全整数范围问题
*/
request.defaults.transformResponse = [
function(data) {
try {
return jsonBig.parse(data);
} catch (err) {
return {data};
}
}
];

// 请求拦截器
request.interceptors.request.use(
function(config) {
const user = store.state.user;
if (user) {
config.headers.Authorization = `Bearer ${user.token}`;
}
// Do something before request is sent
return config;
},
function(error) {
// Do something with request error
return Promise.reject(error);
}
);

// 响应拦截器
request.interceptors.response.use(
// 响应成功进入第1个函数
// 该函数的参数是响应对象
function(response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
},
// 响应失败进入第2个函数,该函数的参数是错误对象
async function(error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
// 如果响应码是 401 ,则请求获取新的 token

// 响应拦截器中的 error 就是那个响应的错误对象
console.dir(error);
if (error.response && error.response.status === 401) {
// 校验是否有 refresh_token
const user = store.state.user;

if (!user || !user.refresh_token) {
router.push("/login");

// 代码不要往后执行了
return;
}

// 如果有refresh_token,则请求获取新的 token
try {
const res = await axios({
method: "PUT",
url: "http://ttapi.research.itcast.cn/app/v1_0/authorizations",
headers: {
Authorization: `Bearer ${user.refresh_token}`
}
});

// 如果获取成功,则把新的 token 更新到容器中
console.log("刷新 token 成功", res);
store.commit("setUser", {
token: res.data.data.token, // 最新获取的可用 token
refresh_token: user.refresh_token // 还是原来的 refresh_token
});

// 把之前失败的用户请求继续发出去
// config 是一个对象,其中包含本次失败请求相关的那些配置信息,例如 url、method 都有
// return 把 request 的请求结果继续返回给发请求的具体位置
return request(error.config); // 重新请求
} catch (err) {
// 如果获取失败,直接跳转 登录页
console.log("请求刷线 token 失败", err);
router.push("/login");
}
}

return Promise.reject(error);
}
);

export default request;

跨域配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
devServer: {
proxy: {
// 前缀
'/api': {
// target 转发后的目标地址
target: 'http://localhost:8080/',
// pathRewrite 重写路径 调用真正的接口时替换掉请求前缀
// pathRewrite 发送请求时,请求路径重写 /api/xxx -> /xxx (去掉/api)
pathRewrite: { '^/api': '' },
ws: true,//用于支持websocket
changeOrigin: true,//欺骗后端 代理服务器此时会根据请求的 target 地址修改 Host
}
},
},

按需引入官网的小问题

官方网站描述的内容没有及时更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
["@babel/preset-env", { "modules": false }]
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

移动端 rem 适配

postcss-pxtorem 是一款 postcss 插件,用于将单位转化为 rem

lib-flexible 用于设置 rem 基准值

一、使用 lib-flexible 动态设置 REM 基准值(html 标签的字体大小)

1、安装

1
2
# yarn add amfe-flexible
npm i amfe-flexible

2、然后在 main.js 中加载执行该模块

1
import 'amfe-flexible'

最后测试:在浏览器中切换不同的手机设备尺寸,观察 html 标签 font-size 的变化。

二、使用 postcss-pxtorem将 px 转为 rem

1、安装

1
2
3
# yarn add -D postcss-pxtorem
# -D 是 --save-dev 的简写
npm install postcss-pxtorem -D

2、然后在项目根目录中创建 postcss.config.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
plugins: {
// VueCLI 默认配置了
// 'autoprefixer': {
// browsers: ['Android >= 4.0', 'iOS >= 8']
// },
'postcss-pxtorem': {
// 设置设计稿宽度的1/10(插件将一行分为10分)
// rootValue可以接收两种类型:数字/函数
// 375的设计稿就直接使用数字,如果有不同的就使用函数进行判断,postcss-pxtorem会对每一个css进行判断
// rootValue: 37.5,
rootValue({ file }) { // vant案例
// 如果使用别的可以打印查看,然后根据路径来判断
// 此处vant是375的设计稿,项目使用的是750的设计稿
return file.indexOf('vant') !== -1 ? 37.5 : 75;
},
propList: ['*']
}
}
}

3、配置完毕,重新启动服务

最后测试:刷新浏览器页面,审查元素的样式查看是否已将 px 转换为 rem

注意行内样式是不能转换

Vuex持久化管理

持久化- 前端持久化-后端持久化-都是通过缓存来做

localStorage-自己写也行,也可以用插件

原理-刷新页面-所有的东西都销毁重来,Vuex 都会重来, 要保证重新初始化的时候能够读取到之前存储的数据-从缓存中读取数据

  • 初始化的时候从缓存中读取
  • 修改的状态的时候存入缓存

登录逻辑

  1. 点击登录后
  2. 可以选择性的添加(人机判断)
  3. 判断用户名/密码的前端验证
    • 判断是否符合规则否则按钮无法点击
    • 请求接口登录
      • 成功则跳转页面保存对应的用户数据
      • 失败则弹出对应的错误提示
  4. 验证成功后将对应的Token保存
    • 分别保存到浏览器和vuex
      • 保存到浏览器是防止刷新数据丢失
      • 保存到vuex是为了更好的性能
  5. 选择性的在封装的axios的请求拦截中携带Token
  6. 通过保存的Token判断用户的权限
    • 获取用户权限对应的路由权限
    • 获取用户权限对应的按钮权限
    • ……
  7. 通过路由和按钮的权限进行菜单和按钮渲染
    • 菜单组件
      • 如果确定了几层就可以直接写几层
      • 如果不确定多少层就可以使用递归
    • 按钮组件 也可以不封装
      • 通过v-if,直接不渲染
      • 通过disabled使按钮无法点击

账号密码登录

  1. 表单验证
    • 显示提示文字
    • 登录是否可以点击
  2. 同意协议 可选
  3. 调用账号密码登录的API
    • 成功:跳转页面 + 消息提示
    • 失败:消息提示

短信验证登录

  1. 表单验证
    • 显示提示文字
    • 验证码是否可以发送
    • 验证码的倒计时
  2. 同意协议 可选
  3. 调用验证验证码的API
    • 成功:跳转页面 + 消息提示
    • 失败:消息提示

QQ登录

使用QQ登录的前提

  1. 有一个已经备案的网站
  2. 进入QQ互联,登录注册
  3. 创建应用,提供域名、备案号、回调地址
  4. 审核通过后获取到应用的id和key

登录逻辑

  1. 点击QQ登录
  2. 打开登录弹窗
  3. 用户登录
  4. 登录成功返回页面

是否注册过

  • 注册过、已经绑定:跳转首页
  • 注册过、没有绑定:进行手机号/邮箱绑定
  • 未注册:进行信息的完善、跳转首页

使用QQ登录

按钮

  1. 引用JS SDK的JavaScript文件

    • ```
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      - 填入APPID和回调地址

      2. 在webpack中配置

      ```javascript
      configureWebpack:{
      externals:{
      qc:'QC' // 包名称:全局变量名称
      }
      }
  2. 在使用的地方导入

  3. 放置QQ登录按钮

    1
    2
    3
    4
    5
    6
    <span id="qqLoginBtn"></span>
    <script type="text/javascript">
    QC.Login({
    btnId:"qqLoginBtn" //插入按钮的节点id
    });
    </script>
  4. 默认是小窗口

    • 通过审查元素复制小窗口地址
    • 手动写a标签和img标签、填入地址
  5. 可以选择性的删除qc和span的代码

  6. 但是webpack的qc还是有用的,后续获取QQ信息要使用

登录

  1. 判断是否登录成功

  2. 调用获取openId的方法

  3. 请求登录的API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     // 确保QQ已经登录
    if (QC.Login.check()) {
    // 第三方唯一标识QQ唯一标识
    QC.Login.getMe((openId) => {
    unionId.value = openId
    // 请求后台,做QQ登录
    userQQLogin(openId).then(data => {
    // 登录成功:data.result 用户信息
    // 1. 存储用户信息
    const { id, account, avatar, mobile, nickname, token } = data.result
    store.commit('user/setUser', { id, account, avatar, mobile, nickname, token })
    store.dispatch('cart/mergeCart').then(() => {
    // 2. 跳转到来源页或者首页
    router.push(store.state.user.redirectUrl)
    // 3. 成功提示
    Message({ type: 'success', text: 'QQ登录成功' })
    })
    }).catch(e => {
    // 登录失败:没有绑定
    isBind.value = false
    })
    })
    }
    • 成功:获取用户信息等后续业务

    • 失败:没有绑定平台

      • 有账号未绑定

        1. 获取QQ的头像昵称等信息

          1
          2
          3
          4
          QC.api('get_user_info').success(res => {
          avatar.value = res.data.figureurl_qq_1
          nickname.value = res.data.nickname
          })
        2. 表单校验手机号/发送验证码

        3. 绑定账号,跳转页面

      • 没有账号没有绑定

        1. 需要进行对应的表单验证
        2. 判断用户是否存在/两次密码是否一致
        3. 绑定账号

退出登录

  1. 清除Token的信息
  2. 删除本地的用户信息
  3. 跳转到登录页面

支付逻辑

  1. 点击去支付

  2. 判断是否选择商品

  3. 选择收货地址 电商类

  4. 订单页面(内容的选择和确认订单)外卖等

  5. 选择支付的方式

    • 发送请求

    • 获取url,展示二维码

    • 弹出二维码/或跳转预支付页面

    • 短轮询(最简单粗暴就是定时器反复请求接口) 判断用户是否支付

  6. 判断是否支付成功或者超时

    • 成功
      • 停止定时器
      • 删除购物车对应的数据
    • 超时
      • 停止定时器
      • 提示用户支付超时
      • 返回购物车/跳转超时页面
  • 前端发送信息(不要包含金额等信息,前端是不安全的,交个后端处理)
    • 后台服务的标准地址+支付页面地址+订单信息+回跳地址
  • 通过后端处理后返回支付宝/微信的二维码
  • 同步支付结果到后台、防止支付过程中断(如果支付出现异常可以进场对账)

权限

路由权限和按钮权限

路由权限

  • 创建路由时区分好需要权限的路由,不要一次性添加,后面匹配对应的权限筛选后添加

  • 创建路由时可以考虑使用要加一个属性用于配合后端判断权限等级

  • 创建路由时保存好静态路由,用于退出登录后切换账号清空路由;也可以手动刷新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const createRouter = () => new VueRouter({
    mode: 'hash',// hash带# history不带#
    routes
    })
    // 初次创建的路由,在最后有导出
    const router = createRouter()
    //写一个重置路由的方法,切换用户后,或者退出时清除动态加载的路由
    export function resetRouter() {
    const newRouter = createRouter()
    router.matcher = newRouter.matcher // 最开始的路由赋值给最新的路由
    }
  • 创建路由守卫对需要权限的路由进行拦截

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    router.beforeEach((to, from, next) => {
    // 进入的路由对象 通常判断不是login就要判断权限
    if (to.path !== '/login') {
    const token = localStorage.getItem('token')
    !token ? next({ path: '/home' }) : next()
    }
    else {
    next()
    }
    })
  • 登录后请求后端返回对应账号可使用的路由,持久化保存后进行筛选

  • 筛选好对应账号权限的路由,遍历使用this.$router.addRoute("Router", item)添加

  • 最后添加404路由

按钮权限

  • 获取后端传递的权限等级,根据对应的规则判断权限
  • 使用v-if/自定义属性/disabled等方法来设置需要权限的按钮

打印

1.使用步骤

安装

1
npm install vue-print-nb --save

在main.js中引入

1
2
import Print from 'vue-print-nb'
Vue.use(Print)

在组件的打印区域标签上加 id=”printArea”

1
<div id="printArea"> 打印区域 <div>

在组件的打印按钮标签上使用指令 v-print=”print”,print是配置对象

1
<el-button v-print="print" type="primary">打印</el-button>

在组件的data中定义print配置对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print: {
id: 'printArea',
popTitle: '打印', // 打印配置页上方标题
extraHead: '', //最上方的头部文字,附加在head标签上的额外标签,使用逗号分隔
preview: '', // 是否启动预览模式,默认是false(开启预览模式,可以先预览后打印)
previewTitle: '', // 打印预览的标题(开启预览模式后出现),
previewPrintBtnLabel: '', // 打印预览的标题的下方按钮文本,点击可进入打印(开启预览模式后出现)
zIndex: '', // 预览的窗口的z-index,默认是 20002(此值要高一些,这涉及到预览模式是否显示在最上面)
previewBeforeOpenCallback() {}, //预览窗口打开之前的callback(开启预览模式调用)
previewOpenCallback() {}, // 预览窗口打开之后的callback(开启预览模式调用)
beforeOpenCallback() {}, // 开启打印前的回调事件
openCallback() {}, // 调用打印之后的回调事件
closeCallback() {}, //关闭打印的回调事件(无法确定点击的是确认还是取消)
url: '',
standard: '',
extraCss: '',
},

2. 打印过滤(隐藏打印区域不需要打印的内容)

只需要在组件的打印区域里,给需要隐藏的内容的标签上添加

1
class="noprint"

3. 配置打印的样式

在全局样式中,新增标签,里面是打印时才生效的样式

1
2
3
4
5
6
7
8
9
10
11
12
<style media="print">
@page {
size: auto;
margin: 3mm;
}

html {
background-color: #ffffff;
height: auto;
margin: 0px;
}
</style>

4.处理常见的打印bug

1. 全局打印生效

  1. 解决打印出现空白页的问题
  2. 解决el-table表格内容过多,打印不全问题
  3. 解决作用域污染问题导致el-table序号错位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<style media="print" lang="scss">
@page {
size: auto;
margin: 3mm;
}
@media print {
html {
background-color: #ffffff;
height: auto;
margin: 0px;
}

body {
border: solid 1px #ffffff;
margin: 10mm 15mm 10mm 15mm;
}
table {
table-layout: auto !important;
}

.el-table__header-wrapper .el-table__header {
width: 100% !important;
border: solid 1px #f2f2f2;
}
.el-table__body-wrapper .el-table__body {
width: 100% !important;
}
#pagetable table {
table-layout: fixed !important;
}
}
</style>

2. 局部打印,局限在当前组件里

在组件标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<style media="print" lang="scss">
@page {
size: auto;
margin: 3mm;
}
@media print {
html {
background-color: #ffffff;
height: auto;
margin: 0px;
}

body {
border: solid 1px #ffffff;
margin: 10mm 15mm 10mm 15mm;
}
#printArea table {
table-layout: auto !important;
}

#printArea .el-table__header-wrapper .el-table__header {
width: 100% !important;
border: solid 1px #f2f2f2;
}
#printArea .el-table__body-wrapper .el-table__body {
width: 100% !important;
}
#printArea #pagetable table {
table-layout: fixed !important;
}
}
</style>

注意

  1. 启动打印后可以通过调整设置的缩放来调整显示比例
  2. 可以通过设置背景图形来控制页面是否使用彩色背景
本文作者:辰風依恛
本文链接:https://766187397.github.io/2025/07/10/Vue%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E5%B8%B8%E7%94%A8%E5%8A%9F%E8%83%BD/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×