Vue2最佳实践
本文主要是Vue2的最佳实践,Vue3特有的在单独的文章中列出
环境相关
node
环境搭建
单独安装nodejs
linux
环境
下载压缩包,若是tar.xz
,以tar -xf filename.tar.xz
命令解压
配置环境变量:
1 | vim /etc/profile |
windows
环境
win8
及以上- 直接去官网上下载安装:https://nodejs.org/en/
win7
- 有的
nodejs
版本不支持win7
,在win7
系统中执行npm -v
时会有以下提示This application is only supported on Windows 8.1, Windows Server 2012 R2, or higher.
下载v12.16.2
及之前的版本即可:https://nodejs.org/dist/v12.16.2/ - 建议升级系统
- 有的
nvm
管理node
NVM
是一个非常方便的node
包管理工具,可以实现在NodeJS
各个不同版本之间自由的进行切换。
下面,介绍用root
权限安装NVM
工具。到2022年6月,nvm
的最新版本为v0.39
。
vite
依赖的node
版本 >=
12.0.0
nuxt3
依赖的node
版本>=
14.16.0
安装nvm
下载,可以打开链接查看
1 | wget https://github.com/nvm-sh/nvm/archive/refs/tags/v0.39.1.tar.gz |
解压
1 | mkdir -p /root/.nvm |
配置环境,打开~/.bashrc
1 | vim ~/.bashrc |
在末尾添加
1 | # nvm path env |
保存退出并使配置生效
1 | source ~/.bashrc |
使用nvm
管理node
列出已经安装的版本
1 | nvm ls |
安装指定版本nodejs
,可以去官网看一下最新的Node.js (nodejs.org)
1 | nvm install 16.14.0 |
卸载指定版本nodejs
1 | nvm uninstall 16.14.0 |
切换到其他版本nodejs
1 | nvm use 14.17.3 |
切换到iojs
1 | nvm use iojs-v3.2.0 |
ubuntu环境安装nvm
(11条消息) Ubuntu 安装 nvm_hey laosha的博客-CSDN博客
(11条消息) ubuntu系统添加环境变量3种方法_ubuntu添加环境变量_prz0590的博客-CSDN博客
ubuntu-设置系统环境变量 - 简书 (jianshu.com)
最后改~/.bashrc
,使用前需要到目录下手动. nvm.sh
运行下(已经配了环境变量,估计是杀掉了),只能在一个shell
如果nvm ls报权限问题,参看:使用NVM安装节点v4.1 - 权限被拒绝 - VoidCC
或者关掉shell重新开一个即可
Vue/cli
脚手架
创建新项目
使用nvm
装完node
环境后,单独新建project
文件夹安装@vue/cli
包,然后在这文件夹下,使用npx vue create project-name
进行每个项目的开发
- 接口文档和
UI
设计稿 - 前端项目搭建
nodejs
- 创建新目录并进入,习惯性的
npm init -y
一下,创建一个空的package.json
- 安装
vue-cli
:npm i @vue/cli
20220511
默认安装的脚手架版本为5.0.4
- 这个默认是安装在本地的,调用指令时需要使用
npx
前缀,如果是全局安装的则不需要
- 安装
- 创建
vue
项目:npx vue create project-name
或vue create project-name
- 然后选择是
vue2
还是vue3
的项目
- 然后选择是
新项目技术选型
每个的写法(语法、配置)都不太一样
- vue2+vueRouter+vueX+elementUI/vantUI
- vue3+vueRouter+vueX+elementPlus/vantUI
- vue3+Ts+vueRouter+Pinia+elementPlus/vantUI
接手老项目
接手其他项目时,如果自己的npm
版本,和创建package-lock.json
的版本不一致,npm i
可能会有问题。
最好是先安装npx
,在了解接手的项目的node版本(node和npm绑定)是多少后,切换到一致的node版本,或者再尝试安装。
当然也存在node版本一致,但npm版本不一致的情况,可以新建node
分支,在新建node
分支上升级或降级npm
版本,所以两者都要先了解清楚。
配置
配置代理
问题描述
使用ajax
技术,发送请求可能会存在跨域问题
本机的浏览器是8080,直接向服务器5000请求数据是不行的,可以通过本机的服务器8080这个代理服务器发请求
服务器和服务器之间,发送请求是不存在跨域问题的,跨域问题只存在于浏览器端
那么怎么开启代理服务器呢?
通过
nginx
配置代理服务器通过
vue-cli
的配置项方式一:
vue.config.js
新增配置项
devServer
1
2
3devServer: {
proxy: "http://localhost:5000" //配置成最终要请求的服务器
}填写该配置项后,
vue-cli
会生成一个8080的服务器(和浏览器当前页面的端口一直),并且开发的public
目录,对应着该服务器的资源目录,ajax
的请求路径,要修改成代理服务的优点:
- 配置简单,请求资源直接发给前端即可
存在的问题:
- 不能配置多个代理服务器,不能灵活的控制请求是否走代理
- 如果请求路径和
public
目录资源存在重复,则会返回代理服务的的资源,请求不会转发
方式二:
vue.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18devServer: {
proxy: {
'/api': { // 表示请求前缀,发送`ajax`请求时,在端口号后添加该前缀,表示要通过代理服务器进行转发,并且该前缀,会作为请求路径发送给目标服务器
target: '<url>', // 代理服务器的路径
pathRewrite: {
'^/api': '' // 对路径进行匹配替换,将前缀替换为空字符串,如果真实的服务器有这个路径头,就不需要重写
},
ws: true, // 用于支持websocket
changeOrigin: true // true配置项,会伪装代理服务的主机端口号,是localhost:5000。false配置项,代理服务器会如实告诉目标服务器自己的主机端口号,是localhost:8080
},
'/foo': {
target: '<other_url>',
pathRewrite: {
'^/foo': ''
}
}
}
}优点:
- 可以配置多个代理,且可以灵活的控制请求是否走代理
缺点:
- 配置略微繁琐,请求资源时必须加前缀
实际应用
前端的页面是:http://218.94.82.249:8091/#/analysisfiles
后台接口是:http://218.94.82.249:8093/file_handing/upload
直接请求会报错跨域:
1 | this.$axios |
vue.config.js
配置proxy
1 | const { defineConfig } = require('@vue/cli-service') |
然后将请求的接口改成8091
(也可以直接只加路径)
1 | this.$axios |
如果是设置了隧道,前后端环境需要都在目标服务器上才行
配置eslint
前端项目目录结构
参照:前端项目目录结构演变 - 掘金 (juejin.cn)
目录结构一
概览
1 | root |
src/main.js
结构
vue
相关依赖
1 | import Vue from 'vue' // 引入的是node_moudules下的vue.runtime.js,去除了模板解析功能模块的() |
其他依赖
1 | // serviceworker的配置文件,pwa 离线缓存,会生成manifes + sw.js |
src/utils
loadable.js
1 | import LoadingComponent from '@/components/loading.vue' |
使用第三库的loading
,并封装成一个公共组件
component/loading.vue
异步组件加载处理
_debounce.js
、_throttles.js
防抖节流处理
什么是防抖节流
函数防抖(debounce) 是指在一定时间内,在动作被连续频繁触发的情况下,动作只会被执行一次,也就是说当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间,所以短时间内的连续动作永远只会触发一次。
函数节流 是指一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期,一个比较形象的例子是如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出。
函数节流(throttle)与 函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
区别:
防抖是将多次执行变成最后一次执行;而节流是将多次执行变为每隔一段时间执行一次。
那么它们各自的使用场景有哪些呢?
防抖
- 短信验证码
- 提交表单
- resize 事件
- input 事件(当然也可以用节流,实现实时关键字查找)
1 |
|
结果:输入很多次,在结束输入后,1秒钟打印(发起ajax
请求)
节流
- scroll 事件,单位时间后计算一次滚动位置
- input 事件(上面提到过)
- 播放事件,计算进度条
1 |
|
点的再快,也只会1秒执行一次
如果要自定义封装,需要了解闭包和定时器
vue中自定义防抖节流
函数封装
写在utils文件夹下
1 | // 防抖 |
在需要使用的组件中引用
1 | import { _debounce } from "..." |
使用
1 | methods: { |
防抖节流的第三方库
lodash
注意:使用的时候不用再安装了,因为
vue-cli
等一些库都依赖lodash
,之前都装过了,可以使用npm ls lodash
查看,在pnpm
中使用方法是pnpm why lodash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21[root@VM-4-12-centos atguigu_gmall_frontend]# npm ls lodash
atguigu_gmall_frontend@0.1.0 /root/hh_git/code/atguigu_gmall/atguigu_gmall_frontend
├─┬ @vue/cli-service@5.0.6
│ ├─┬ html-webpack-plugin@5.5.0
│ │ ├── lodash@4.17.21 deduped
│ │ └─┬ pretty-error@4.0.0
│ │ ├── lodash@4.17.21 deduped
│ │ └─┬ renderkid@3.0.0
│ │ └── lodash@4.17.21 deduped
│ ├─┬ portfinder@1.0.28
│ │ └─┬ async@2.6.4
│ │ └── lodash@4.17.21 deduped
│ └─┬ webpack-bundle-analyzer@4.5.0
│ └── lodash@4.17.21 deduped
├─┬ eslint-plugin-vue@8.7.1
│ └─┬ vue-eslint-parser@8.3.0
│ └── lodash@4.17.21 deduped
└─┬ json-server@0.17.0
├── lodash@4.17.21
└─┬ lowdb@1.0.0
└── lodash@4.17.21 deduped使用
全量导入:
1
2
3
4
5
6
7
8
9import _ from 'lodash'
// ...
methods: {
changeIndex: _.throttle(function (index) {
this.currentIndex = index
}, 50)
}
// ...按需导入:
1
import throttle from 'lodash/throttle' // 底层是module.exports默认暴露的,就不用加小括号了(咋不是require咧)
注意点:查看
node_modules
,throttle.js
文件用到了debounce.js
里面是有保存上下文
this
的,在使用的时候不能用箭头函数debounce.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// ...
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this; // 注意throttle在使用时,不能用箭头函数,否则this指向会出问题
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
// ...
src/styles
/deep
的用法
src/router
router.js
hooks.js
1 | impor store from '@/store' |
permission.js
1 | export default [ // 权限管理,要和后台一一对应 |
资源导入
导入静态资源
导入第三方UI
库
elementUI
vue2
项目- 见《
vue2
基本使用》
elementPlus
vue3
项目
vantUI
目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。
安装
1
2
3
4
5# Vue 3 项目,安装最新版 Vant:
npm i vant -S
# Vue 2 项目,安装 Vant 2:
npm i vant@latest-v2 -S引入
自动按需引入(推荐)
babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 的写法自动转换为按需引入的方式。
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# 安装插件
npm i babel-plugin-import -D
// 在.babelrc 中添加配置
// 注意:webpack 1 无需设置 libraryDirectory
{
"plugins": [
["import", {
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}]
]
}
// 对于使用 babel7 的用户,可以在 babel.config.js 中配置
module.exports = {
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
};
// 接着你可以在代码中直接引入 Vant 组件
// 插件会自动将代码转化为方式二中的按需引入形式
import { Button } from 'vant';Tips: 如果你在使用 TypeScript,可以使用 ts-import-plugin 实现按需引入。
在不使用插件的情况下,可以手动引入需要的组件。
main.js
1
2import Button from 'vant/lib/button';
import 'vant/lib/button/style';导入所有组件(不推荐)
Vant 支持一次性导入所有组件,引入所有组件会增加代码包体积,因此不推荐这种做法。
main.js
1
2
3
4
5import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);Tips: 配置按需引入后,将不允许直接导入所有组件。
可以在
App.vue
的#app
中覆盖vant
的样式,也可以单独写个覆盖样式的文件在main.js
中引入
导入图标库
- 阿里图标字体,选好需要的字体后,可以直接要css中
@import url()
导入,并在入口文件中引入资源;商用时要注意 font awesome
- 自研图标
导入自定义工具库
第三方库的自定义封装
导入动态资源
导入scss
变量
src/assets/common.scss
1 | $color: #fff; |
无法在main.js
入口文件中导入动态资源,如src/assets/common.scss
中的scss
变量,需要使用插件动态注入
vue.config.js
1 | const path = require('path') |
Vuex
最佳实践
目录:root/src/store
vuex
模块化编码最佳实践
定义模块
不同模块都放在
store/modules
文件夹下不同的模块每个
state
、action
、getters
和mutation
都单独抽成一个js
文件(放在一起代码太多时不好维护)为了便于识别,每个
state
或其它,命名前可以加模块名,如homeState
如下形式,分别定义4个
1
2
3
4
5const homeState = {
}
export default homeState后面使用
.default
属性获取文件导出内容时时,并不会用到homeState
这个名字,也可以这样写home/state.js
1
2
3export default {
}这时只能根据文件名和路径名,才能判断这个文件是干嘛的了
在
modules
同级目录,定义index.js
整合所有的模块利用
webpack
中require.context()
方法获取文件路径、利用default
属性获取模块导出内容moudles/index.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
33
34
35
36
37// require.context()方法时webpack内置方法,直接用
const files = require.context('.', true, /\.js$/) // 搜索当前文件夹,及子文件夹中,以.js结尾的文件
const modules = {}
// keys()方法返回匹配的每个文件的相对路径,是个数组
files.keys().forEach(key => {
const path = key.replace(/\.\/|\.js/g, '') // 使用正则,把每个相对路径的 ./ 和.js 都替换成空字符串
// path只剩下 home/state 这种和自身 index 了
if (path == 'index') return // 路径是自己则不做任何处理
let [moduleName, type] = path.split('/')
if (!modules[moduleName]) {
modules[moduleName] = {// 开始构造导出对象
namespaced: true // 构建命名空间配置 {namespaced: true}
}
}
// 构建state/getters/actions/mutations配置项
// files()方法的返回值一个Module类型的对象,其default属性值,就是每个文件导出的结果值
modules[moduleName][type] = files(key).default //最终的modules对象,就是包含了每个配置项的模块对象
console.log(modules)
// moudles的打印结果 = {
// home: {
// namespaced: true,
// state:{},
// getters:{},
// actions:{},
// mutations:{}
// },
// search: {
// namespaced: true,
// state:{},
// getters:{},
// actions:{},
// mutations:{}
// }
// }
})
// 这个结果,和我们一开始学vuex模块化编码是一致的
export default modules;
在公共的
index.js
中,解构赋值上面的modules
对象,这样就不用一个一个导入一大堆模块了- 另外,
vue-router
中不建议使用使用批量导入的方式,因为没有一个明确的规范
1
2
3
4
5
6
7
8
9
10
11
12import Vue from 'vue'
import Vuex from 'vuex'
import modules from './modules/index.js'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
...modules
}
})- 另外,
公共的状态在
store/index.js
中维护,如websocket
等
使用createNamespacedHelpers
简化map*
写法
取数的数量越多,这种写法越清爽,提交actions
和mutations
同理
test.vue
1 | <script> |
store/modules/home/actions.js
1 | import {reqCategoryList} from '@/api/home.js' |
actions
和mutaion
s的types
的统一管理
上一小节我们是直接提交的action
,如果数据太多的话,不太好管理命名
-根据需求创建action-types.js
和mutation-types.js
数据少的的话,都放在mutation-types.js
里也不是不行
store/mutation-types.js
1 | // 所有的名字都列在这里 |
然后在用到的子模块的action.js
或mutation.js
中导入
home/action.js
1 | import {reqCategoryList} from '@/api/home.js' |
home/mutation.js
1 | import * as Types from '@/store/action-types' |
home/state.js
1 | const homeState = { |
在组件中使用
test.vue
1 | <template> |
看另外一个案例
index.vue
1 | <template> |
VueRouter
最佳实践
路由不建议自动生成,但可配置性太低(如批注、钩子)
基本配置
见《vue2基本使用》
异步组件
除了首页用到的组件,其他组件都可以按需加载
1 | const routes = [ |
异步组件未加载时,会出现空白页
虽然可以使用prefetch
让后续页面用到的先加载,但还是不太好,一般会加个loading
在router/index.js
中使用src/utils/loadable.js
1 | // ... |
路由权限
src/api/user.js
1 | import axios from '@/utils/axios' |
src/store/modules/user/state.js
1 | const userState = { |
src/store/action-types.js
1 | // 设置用户信息 登录需要的 |
src/store/modules/user/actions.js
1 | import * as Types from '@/store/action-types.js' |
src/store/modules/user/mutations.js
1 | import * as Types from '@/store/action-types.js' |
src/router/hooks.js
1 | impor store from '@/store' |
有些页面不用登录也能访问,有些页面必须需要登录
src/router/index.js
1 | // ... |
登录页面
1 | <script> |
views/profile/index.vue
1 | <template> |
菜单权限
后端有一个根据username
返回不同path
的接口
后台返回权限信息的接口:
1 | validate() { |
上述返回的path
对应前台的动态路由
我们在views/others
文件夹中创建这四个vue
文件:lesson-manager.vue
、student-manager.vue
、points.vue
、collect.vue
由于是动态添加的,我们不能直接写到router/index.js
中
新建src/rotuer/permission.js
,该文件中专门放一些权限相关的路由
1 | export default [ // 权限管理,要和后台一一对应 |
在路由前置钩子里,还需要做一件事:根据菜单权限显示不同的动态组件
src/router/hooks.js
1 | impor store from '@/store' |
src/store/action-types.js
1 | // ... |
src/store/modules/user/mutations.js
1 | import * as Types from '@/store/action-types.js' |
src/store/modules/user/actions.js
1 | import * as Types from '@/store/action-types.js' |
profile.vue
1 | <template> |
按钮权限
一般也是用户登录的一瞬间,把按钮权限字段,放在vuex
中
页面中使用指令控制
src/store/modules/user/state.js
1 | const userState = { |
profile.vue
1 | <template> |
src/utils/directives.js
1 | export default { |
main.js
中引入指令
1 | // 指令 |
axios
最佳实践
函数封装
utils/request.js
1 | import axios from "axios"; |
api/target.js
1 | import { request } from "@/utils/request"; |
类封装01
封装
utils/axios.js
1 | import axios from 'axios' |
src/store/action-types.js
1 | // 设置token |
src/store/index.js
这个功能是所有路由组件都需要的,可以放在根文件,当然也可以单独抽成一个模块
1 | import Vue from 'vue' |
src/router/hooks.js
1 | import store from '@/store' |
src/router/index.js
1 | import Vue from 'vue' |
使用
新建src/api/home.js
文件,对应每个页面的接口文件
如果一个页面的接口有很多,可以新建src/api/config/config.js
,该文件单独维护各个路径
home/js
1 | import axois from '@/utils/axios' |
vue
的走vuex
的开发流程:state -> action-types -> api -> actions -> mutations
发请求的页面
1 | <script> |
注意点
避免路由组件一切换就发请求,可以加一层判断
1
2
3
4
5
6
7mounted() {
// this[Types.ACTION_CATEGORYLIST]() // 注意写法
// 加一层判断,避免每次切换路由组件时都请求一遍接口
if(this.categoryList.length == 0) { // 如果vuex中没有数据,再去重新请求(用户刷新页面会重新请求)
this[Types.ACTION_CATEGORYLIST]()
}
},刷新页面,立刻切换到其他路由组件时,之前的请求应该取消掉
之前我们在全局的请求拦截器中,加了
cancelToken
,相当于xhr.abort()
类封装02
封装
抽离baseURL
https://www.cnblogs.com/jdWu-d/p/12687396.html
在public目录下,新建一个文件,我命名为serverConfig.json,具体如图所示,里面配了一个baseURL。
1 | { |
新建初始化方法utils/init.js
1 | import axios from "axios"; |
然后,在main.js里面定义一个读取这个文件的方法,在初始化的时候读取这个文件。
1 | import {getServerConfig} from "@/utils/init.js"; |
如果封装了axios
,在封装的js
文件中,读取配置
1 | import 'Vue' from 'vue'; |
持久化最佳实践
登录持久化
localStorage
cookie
sessionStorage
记录滚动条位置
indexDB
移动端最佳实践
em
适配
rem
适配
备注:见《移动端开发基础》
postcss-plugin-px2rem
-
lib-flexible
- 在页面中注入一段脚本,可获取设备
DPR
和屏幕宽度,自动计算font-size
并添加至html
节点属性上
安装:
1 | npm i postcss-plugin-px2rem lib-flexible |
main.js
中引入
1 | import 'lib/flexible' // 对应设置根的字体(这个插件也可以自己手写) |
vue.config.js
新增配置
1 | module.exports = { |
组件最佳实践
业务组件
路由组件
公共组件
组件库的样式穿透
https://www.bbsmax.com/A/amd026aWJg/
scoped看起来很好用,当时在Vue项目中,当我们引入第三方组件库时(如使用element-ui),需要在局部组件中修改第三方组件库样式,而又不想去除scoped属性造成组件之间的样式覆盖。这时我们可以通过特殊的方式穿透scoped
stylus的样式穿透 使用 >>>
1 | .wrapper >>> .swiper-pagination-bullet-active |
sass和less的样式穿透 使用 /deep/
1 | // 语法 |
vue-cli5.0
中使用/deep/
报错,默认安装的sass
版本为1.32.7
,sass-loader
版本为12.0.0
报错信息:
1 | Error: Module build failed (from ./node_modules/sass-loader/dist/cjs.js): |
解决方案:
把/deep/
写成::v-deep
控制台警告
Added non-passive event listener...
在使用第三方库时,可能控制台报错如下:
1 | [Violation] Added non-passive event listener to a [scroll](https://so.csdn.net/so/search?q=scroll&spm=1001.2101.3001.7020)-blocking 'mousewheel' event. Consider marking event handler as 'passive' to make the page more responsive. |
[违规]添加非被动事件监听器到滚动阻塞’鼠标滚轮’事件。 考虑将事件处理程序标记为“被动”,以使页面响应更快。
解决方案:
1、npm i default-passive-events -S
2、main.js中加入:import ‘default-passive-events’
原因是 Chrome51 版本以后,Chrome 增加了新的事件捕获机制-Passive Event Listeners;
Passive Event Listeners:就是告诉前页面内的事件监听器内部是否会调用preventDefault函数来阻止事件的默认行为,以便浏览器根据这个信息更好地做出决策来优化页面性能。当属性passive的值为true的时候,代表该监听器内部不会调用preventDefault函数来阻止默认滑动行为,Chrome浏览器称这类型的监听器为被动(passive)监听器。目前Chrome主要利用该特性来优化页面的滑动性能,所以Passive Event Listeners特性当前仅支持mousewheel/touch相关事件。
来源:https://blog.csdn.net/Wildness_/article/details/123190078