新建一个vue2项目并加入基础建设
1.前置环境准备:
node环境:下载并安装node
vue-cli的安装:npm i vue-cli -g
2.使用vue create 项目名生成一个vue2项目,具体配置如下:
根据以上的配置,你会得到一个vue + vue-router + vuex + less + eslint的vue2项目模板
3.加入UI组件库,以element-ui为例子
安装命令:
npm i element-ui -S
如果安装不上,是因为npm版本问题报错**,**则使用
npm i element-ui -S --legacy-peer-deps
打开项目的main.js,添加以下代码(全部引入,需要优化为按需导入请看element官网)即可
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
在页面中使用element的组件没有问题则引入成功
4.建立一些必须要用到的文件夹
5.消灭浏览器差异以及盒子原有样式
(1)normalize.css保证我们的代码在各浏览器的一致性
- 安装 npm install --save normalize.css(报错的话,跟上面一样加上–legacy-peer-deps)
- main.js引入 import ‘normalize.css/normalize.css’
(2)消除盒子原有样式(简化版)
1.建立rest.css,写入以下样式:
* {
padding: 0;
margin: 0;
list-style: none;
box-sizing: border-box;
}
html,
body {
height: 100%;
}
2.在main.js中引入:import ‘./assets/css/reset.css’
6.配置别名
在vue.config.js中添加:
const path = require('path')
function resolve(dir) {
// 配置别名
return path.join(__dirname, dir)
}
const {
defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
configureWebpack: {
resolve: {
alias: {
'@': resolve('src')
}
}
},
})
7.配置代理
在vue.config.js中添加:
// 样例
devServer: {
//开启代理服务器 (方式1) 配置多个代理
proxy: {
"/api": {
//'/api'是自行设置的请求前缀
target: "http://localhost:5000",
// ws: true,//用于支持websocket
changeOrigin: true, //用于控制请求头中的host值
pathRewrite: {
"^/api": "" }, //路径重写,(正则)匹配以api开头的路径为空(将请求前缀删除)
},
},
},
8.vuex的store改造
因为在项目中,一般是分仓库进行存储数据,所以我们需要对store进行修改一下,把它改成分仓库存储数据的模式。这里可以参考vue-element-admin的vuex仓库
创建这几个文件
index.js:
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
Vue.use(Vuex)
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)
// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {
})
const store = new Vuex.Store({
modules,
getters
})
export default store
getters.js(示例):
const getters = {
token: state => state.user.token,
name: state => state.user.username,
}
export default getters
user.js中(示例):
const state = {
token: '',
username: '',
avatar:'',
desc:''
}
const mutations = {
setName(state, data){
state.username = data
},
saveUserInfo(state, data) {
state.token = data.token
state.username = data.username
state.avatar = data.avatar
state.desc = data.desc
},
removeUserInfo(state) {
state.token = ''
state.username = ''
state.avatar = ''
state.desc = ''
},
}
export default {
namespaced: true,
state,
mutations,
}
使用示例:
import {
mapState,mapMutations } from 'vuex';
computed: {
...mapState('user', {
username: 'username'
})
},
created () {
console.log(this.username)
console.log(this.$store.state.user.username);
this.setName('李四')
this.generateRoutes()
this.$store.dispatch('permission/generateRoutes')
},
methods: {
...mapMutations('user', {
setName: 'setName'
}),
...mapActions('permission', {
generateRoutes: 'generateRoutes'
})
},
加入持久化插件:vuex-persistedstate
安装插件:
npm i vuex-persistedstate 或者 npm i vuex-persistedstate --legacy-peer-deps
在store中使用(这里是把user仓库的数据存储到了sessionStorage中):
import createPersistedState from 'vuex-persistedstate'
const store = new Vuex.Store({
modules,
getters,
plugins: [
createPersistedState({
storage: window.sessionStorage,
reducer(val) {
return {
User: val.user,
}
},
}),
],
})
9.添加axios并封装axios
安装axios:
npm i axios
在utils文件夹下创建auth.js,加入以下代码:
function getLocal(key = "token") {
return localStorage.getItem(key);
}
// 删除
function removeLocal(key = "token") {
window.localStorage.removeItem(key);
}
// 保存
function setLocal(value, key = "token") {
window.localStorage.setItem(key, value);
}
export {
getLocal, removeLocal, setLocal };
在utils文件夹下创建request.js,加入以下代码:
详细的封装过程请移步这篇博客(这个代码是通用的,可直接copy):
https://blog.csdn.net/weixin_43239880/article/details/130143352?spm=1001.2014.3001.5501
import axios from 'axios'
import {
Message, Loading, MessageBox } from 'element-ui'
import store from '@/store'
import {
getLocal } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_URL, // api的base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
config => {
removePending(config)
// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
!config.repeatRequest && addPending(config)
// 打开loading
if (config.loading) {
LoadingInstance._count++
if (LoadingInstance._count === 1) {
openLoading(config.loadingDom)
}
}
// 如果登录了,有token,则请求携带token
// Do something before request is sent
if (store.state.user.token) {
config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
}
return config
},
error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// respone拦截器
service.interceptors.response.use(
// response => response,
/**
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
response => {
// 已完成请求的删除请求中数组
removePending(response.config)
// 关闭loading
if (response.config.loading) {
closeLoading()
}
const res = response.data
// 处理异常的情况
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
// 403:非法的token; 50012:其他客户端登录了; 401:Token 过期了;
if (res.code === 403 || res.code === 50012 || res.code === 401) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/FedLogOut').then(() => {
location.reload() // 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject(new Error('error'))
} else {
// 默认只返回data,不返回状态码和message
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
const isbackAll = response.config.meta && response.config.meta.responseAll
if (isbackAll) {
return res
} else {
return res.data
}
}
},
error => {
error.config && removePending(error.config)
// 关闭loading
if (error.config.loading) {
closeLoading()
}
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
// --------------------------------取消接口重复请求的函数-----------------------------------
// axios.js
const pendingMap = new Map()
/**
* 生成每个请求唯一的键
* @param {*} config
* @returns string
*/
function getPendingKey (config) {
let {
url, method, params, data } = config
if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}
/**
* 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
* @param {*} config
*/
function addPending (config) {
const pendingKey = getPendingKey(config)
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel)
}
})
}
/**
* 删除重复的请求
* @param {*} config
*/
function removePending (config) {
const pendingKey = getPendingKey(config)
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey)
cancelToken(pendingKey)
pendingMap.delete(pendingKey)
}
}
// ----------------------------------loading的函数-------------------------------
const LoadingInstance = {
_target: null, // 保存Loading实例
_count: 0
}
function openLoading (loadingDom) {
LoadingInstance._target = Loading.service({
lock: true,
text: '数据正在加载中',
spinner: 'el-icon-loading',
background: 'rgba(25, 32, 53, 1)',
target: loadingDom || 'body'
})
}
function closeLoading () {
if (LoadingInstance._count > 0) LoadingInstance._count--
if (LoadingInstance._count === 0) {
LoadingInstance._target.close()
LoadingInstance._target = null
}
}
export default service
axios的使用:在api文件夹中建立home.js,加入以下代码(示例):
import request from "@/utils/request";
//使用封装好的request
export function getInfo(params) {
return request({
url: "/user/info",
method: "get",
params,
});
}
//返回所有的res信息
export function login(data) {
return request({
url: "/user/login",
method: "post",
data,
meta: {
responseAll: true, // 返回所有的信息,包括状态码和message和data
},
});
}
//使用repeatRequest参数让接口 可以同一时间多次调用
export function getList(params) {
return request({
url: "/message/list",
method: "get",
params,
repeatRequest: true, // 配置为true,则可以同一时间多次调用
});
}
//使用loading
export function getListById(loading) {
return request({
url: "/message/listbyId",
method: "get",
...loading,
});
}
export function gettableList() {
return request({
url: "/user/tableData",
method: "get",
});
}
10.区分生产环境和开发环境
在src目录建立两个文件夹(名字固定),定义变量VUE_APP_URL
如何使用该变量?使用process.env.VUE_APP_URL来调用
11.封装路由拦截器
router文件夹的index.js中(示例可用)
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '@/views/HomeView.vue'
Vue.use(VueRouter)
// 解决重复点击路由报错的BUG
// 下面这段代码主要解决这个问题 :Uncaught (in promise) Error: Redirected when going from "/login" to "/index" via a navigation guard.
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '@/views/AboutView.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.VUE_APP_URL,
routes
})
export default router
在router文件夹下创建router-config.js,并在main.js中导入:
import "./router/router-config" // 路由守卫,做动态路由的地方
router-config.js中的代码: 由于封装路由拦截器需要和项目选中前端路由还是后端路由有关系,所以这里就简单的写了样例。
import router from './index'
// 白名单页面直接进入
const whiteList = ['/login']
console.log(whiteList)
router.beforeEach((to, from, next) => {
next()
})
router.afterEach(() => {
// finish progress bar
// NProgress.done()
})
需要使用前端路由的,可参考我的这篇文章:
https://blog.csdn.net/weixin_43239880/article/details/129943035?spm=1001.2014.3001.5501
需要使用后端路由的,可参考我的这篇文章:
https://blog.csdn.net/weixin_43239880/article/details/129922664?spm=1001.2014.3001.5501
其中主要的逻辑为:
是否为白名单页面
是:直接进入
不是:判断是否有token
无token:重置到login页面
有token: 判断用户的路由状态(指的是用户是否有路由)
有路由:直接进入
没有路由:调接口去获取路由
递归处理后台返回的路由
将异步路由存储到vuex,并设置用户的路由状态为true
使用route.addRouters将路由添加进去
12.适配处理
适配处理参考这篇文章https://blog.csdn.net/weixin_43239880/article/details/130135971?spm=1001.2014.3001.5501,因为不知道是什么项目,所以这里就不加进去了。
各类需求适合适配的主方案:
移动端: rem(lib-flexible+postcss-pxtorem) 或者 vw方案 参考vant官方文档:https://vant-ui.github.io/vant/#/zh-CN/advanced-usage#liu-lan-qi-gua-pei
PC大屏: scale优先
PC端(官网主页/购物网站/类似知乎,淘宝,小米等主页):使用版心布局
PC端(有地图或不允许留白(设计稿完全占满了1920*1080)):rem(lib-flexible+postcss-pxtorem)+其他响应式方法
10.配置prettier(可选)
1.安装必须的包
- prettier - prettier 本体
- eslint-config-prettier - 关闭 ESLint 中与 Prettier 中发生冲突的规则
- eslint-plugin-prettier - 将 Prettier 的规则设置到 ESLint 的规则中
npm i prettier eslint-config-prettier@6 eslint-plugin-prettier@3 -D
安装时的报错解决:指定某个大版本就ok了,这个报错很明显是因为eslint和eslint-plugin-prettier的版本搭配不正确导致的
2.在根目录添加一个 .prettierrc.js 文件,内容如下:
module.exports = {
tabWidth: 2, // 指定每个缩进级别的空格数<int>,默认2
useTabs: false, // 用制表符而不是空格缩进行<bool>,默认false
printWidth: 300, //一行的字符数,如果超过会进行换行,默认为80
singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号
endOfLine: 'auto', //避免报错delete (cr)的错
proseWrap: 'always',
semi: false, // 不加分号
trailingComma: 'none', // 结尾处不加逗号
htmlWhitespaceSensitivity: 'ignore', // 忽略'>'下落问题
}
3.修改 ESLint 配置,使 Eslint 兼容 Prettier 规则
plugin:prettier/recommended 的配置需要注意的是,一定要放在最后。因为extends中后引入的规则会覆盖前面的规则。也就是说你可以在.prettierrc.js 中定义自己的风格代码。到时候,本地的prettier插件会根据这个文件来格式化,项目安装的prettier也会根据该文件来格式化。且eslint的风格与prettier风格冲突的地方会以prettier为主
const {
defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
/// ...
extends: [
'plugin:vue/essential',
'@vue/standard'
'plugin:prettier/recommended'
],
// ...
})