😉 你好呀,我是爱编程的Sherry,很高兴在这里遇见你!我是一名拥有十多年开发经验的前端工程师。这一路走来,面对困难时也曾感到迷茫,凭借不懈的努力和坚持,重新找到了前进的方向。我的人生格言是——认准方向,坚持不懈,你终将迎来辉煌!欢迎关注我😁,我将在这里分享工作中积累的经验和心得,希望我的分享能够给你带来帮助。
👉 关于【Vue3项目实战系列】专栏,本专栏旨在帮助新人粉丝全面快速的掌握vue3开发技术,在接下来的日子里我带着大家从零开始一个vue3项目,项目包含了框架搭建,用户登录,用户权限,表格增删改查,表单验证,等一套完整的用户管理系统的实践开发,学完可以帮助前端新人至少拥有一年工作经验,本专栏完全免费,欢迎关注并订阅!🎈
🎯如果你有任何困惑或疑问欢迎评论区给我留言哦,我收到后会第一时间为你答疑解惑😉
项目初始化完成了,本篇给大家讲讲,初始目录的结构以及他们的作用。
项目目录
/vue3-js-demo
├── node_modules/ # 第三方包存储目录,存放项目依赖的npm包
├── public/ # 静态资源目录,任何放置在 public 文件夹的静态资源都会被简单复制,而不经过构建工具处理
│ └── favicon.ico # 网站图标
├── src/ # 源代码目录
│ ├── api/ # 接口文件
│ ├── assets/ # 公共资源目录,放图片、样式表等资源
│ ├── components/ # 公共组件目录,存放可复用的Vue组件
│ ├── router/ # 路由相关模块,配置路由规则和导航逻辑
│ ├── stores/ # 状态管理容器相关模块,使用Pinia或Vuex管理应用状态(这里假设使用的是Pinia)
│ ├── views/ # 路由页面组件存储目录,通常存放对应不同路由的页面级组件
│ ├── App.vue # 根组件,最终被替换渲染到index.html页面中的#app入口节点
│ └── main.js # 整个项目的启动入口模块,负责创建Vue实例并挂载到DOM
├── .gitignore # Git 的忽略配置文件,告诉Git项目中要忽略的文件或文件夹
├── index.html # HTML 模板文件,作为应用的入口点
├── package.json # 包说明文件,记录项目中使用的第三方包依赖信息等内容
│ ├── .editorconfig # Editorconfig 帮助开发人员定义和维护跨编辑器(或IDE)的统一代码风格
│ ├── eslint.config.js # ESLint 的配置文件,用于定义代码检查规则
│ ├── package-lock.json # 记录安装时的包版本号,以保证自己或其他人在 npm install 时依赖的一致性
├── README.md # 项目说明文档,描述项目的基本信息和使用方法
├── vite.config.js # Vite 配置文件,配置Vite构建工具的行为
│ └── jsconfig.json # JavaScript 语言服务配置文件,提供智能感知、转到定义等功能支持
接下来会详细讲解,每个文件的作用与用法,并结合实际项目给出示例代码,欢迎学习交流。
public 静态资源目录
它是一个静态资源目录,任何放置在 public 文件夹的静态资源都会被简单复制,而不经过构建工具处理。比如你可以存放图片、字体、html文件、favicon.ico和其他静态资源(任何你想要直接通过URL访问而不需要经过打包过程的文件,比如视频文件、PDF文档等)
src 源代码目录
用来存放项目源代码,这里面的代码都会被打包。
api 接口文件
将所有接口文件统一放在这个目录下,方便管理。
import request from '@/plugins/request';
// 树形列表
export function getTreeList (params) {
return request({
url: '/dataVisualization/treeList',
method: 'get',
params
});
}
export function deleteTable (data) {
return request({
url: `/dataModel/deleteModel`,
method: 'post',
data
});
}
这段代码就是文件里的内容,request是一个公共的封装方法,在这里我们只需要调用它就可以了。这样我们在调用接口的时候可以很简洁。
request代码
import axios from 'axios';
import { merge } from 'lodash';
import store from '@/store';
import util from '@/libs/util';
import { tansParams, blobValidate } from '@/libs';
import { errorCode } from '@/libs/config';
import Setting from '@/setting';
import Router from '@/router';
import ViewUIPlus, { Message, Notice } from 'view-ui-plus';
import { saveAs } from 'file-saver';
// 创建一个错误
function errorCreate (msg) {
const err = new Error(msg);
errorLog(err);
throw err;
}
// 记录和显示错误
function errorLog (err) {
// 添加到日志
store.dispatch('admin/log/push', {
message: '数据请求异常',
type: 'error',
meta: {
error: err
}
});
// 打印到控制台
if (process.env.NODE_ENV === 'development') {
util.log.error('>>>>>> Error >>>>>>');
console.log(err);
}
// 显示提示,可配置使用 View UI Plus 的 $Message 还是 $Notice 组件来显示
if (Setting.request.errorModalType === 'Message') {
Message.error({
content: err.message,
duration: Setting.request.modalDuration
});
} else if (Setting.request.errorModalType === 'Notice') {
Notice.error({
title: '提示',
desc: err.message,
duration: Setting.request.modalDuration
});
}
}
// 默认content-type
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
// 创建一个 axios 实例
const service = axios.create({
baseURL: Setting.request.apiBaseURL,
timeout: Setting.request.timeout,
withCredentials: Setting.request.withCredentials
});
// 不需携带token的接口地址
const noTokenUrl = [ '/code/captchaInfo', '/system/tenant/list', '/code', '/code/get', '/code/check', '/auth/oauth/token', '/register' ];
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
// 除白名单外,其余接口请求附加token
const token = util.cookies.get('token');
if (
token &&
!noTokenUrl.includes(config.url) &&
!isToken
) {
config.headers['Authorization'] = 'Bearer ' + token;
}
if (config.isRefreshToken) {
const refreshToken = util.cookies.get('refresh_token');
config.headers['Authorization'] = 'Bearer ' + refreshToken;
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
return merge(config, Setting.request.requestConfig(config, util));
},
(error) => {
// 发送失败
console.log(error);
Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
// dataAxios 是 axios 返回数据中的 data
const dataAxios = response.data;
// 这个状态码是和后端约定的
const { code } = dataAxios;
// 根据 code 进行判断
if (code === undefined) {
// 如果没有 code 代表这不是项目后端开发的接口
return dataAxios;
} else {
// 有 code 代表这是一个后端接口 可以进行进一步的判断
switch (code) {
case 200:
store.dispatch('admin/account/refreshAccessToken')
return dataAxios.data;
case 401:
Message.error({
content: '登录凭证过期',
duration: Setting.request.modalDuration
});
store.dispatch('admin/account/logout', {
confirm: false,
expired: true,
vm: {}
});
break;
case 404:
Router.replace({ name: '404' });
break;
// case 'xxx':
// // [ 示例 ] 其它和后台约定的 code
// errorCreate(`[ code: xxx ] ${dataAxios.msg}: ${response.config.url}`);
// break;
case 500:
// Router.replace({ name: '500' });
if (dataAxios.msg === '验证码错误') {
Router.replace({ name: 'login' });
Message.error({
content: dataAxios.msg,
duration: Setting.request.modalDuration
});
break;
}
errorCreate(`${dataAxios.msg}`);
break;
// 针对二维转一维的编辑接口返回的错误信息回显
case 5001:
return dataAxios
default:
// 不是正确的 code
errorCreate(`${dataAxios.msg}`);
break;
}
}
},
(error) => {
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = '请求错误';
break;
case 401:
error.message = '未授权,请登录';
break;
case 403:
error.message = '拒绝访问';
break;
case 404:
error.message = `请求地址出错: ${error.response.config.url}`;
break;
case 408:
error.message = '请求超时';
break;
case 500:
error.message = '服务器内部错误';
break;
case 501:
error.message = '服务未实现';
break;
case 502:
error.message = '网关错误';
break;
case 503:
error.message = '服务不可用';
break;
case 504:
error.message = '网关超时';
break;
case 505:
error.message = 'HTTP版本不受支持';
break;
default:
break;
}
}
errorLog(error);
return Promise.reject(error);
}
);
export default service;
assets 公共资源目录
放图片、字体、样式表等资源
components 公共组件目录
存放可复用的Vue组件,我们一般将与业务无关的组件放到这个目录下,不与业务挂钩可以无限制复用。
router路由相关模块
路由相关模块,配置路由规则和导航逻辑。
index.js
import {
createRouter,
createWebHistory,
createWebHashHistory,
createMemoryHistory
} from 'vue-router'
import ViewUIPlus from 'view-ui-plus'
import util from '@/libs/util'
import Setting from '@/setting'
import store from '@/store/index'
// 路由数据
import routes from './routes'
import { frameInRoutes } from '@/router/routes'
// 导出路由 在 main.js 里使用
let router = createRouter({
history:
Setting.routerMode === 'history'
? createWebHistory(Setting.routerBase)
: Setting.routerMode === 'hash'
? createWebHashHistory(Setting.routerBase)
: createMemoryHistory(Setting.routerBase),
routes
})
// 登录后,无需比对 access 的路由
const whiteList = [
'/login',
'/user/profile',
'/register',
'/register/result',
'/forget'
]
/**
* 路由拦截
* 权限验证
*/
router.beforeEach(async (to, from, next) => {
if (Setting.showProgressBar) ViewUIPlus.LoadingBar.start()
// 这里依据 token 判断是否登录,可视情况修改
const token = util.cookies.get('token')
if (token && token !== 'undefined') {
if (to.path === '/login') {
next({
path: '/'
})
} else if (
!store.state.admin.user.roles ||
store.state.admin.user.roles.length === 0
) {
// 没有用户信息,则获取用户信息
await store.dispatch('admin/account/getUserInfo')
const newRoutes = store.state.admin.menu.routes
newRoutes.forEach((item) => {
router.addRoute(item)
})
// 最后添加404
router.addRoute({
path: '/:pathMatch(.*)',
name: '404',
meta: {
title: '404'
},
component: () => import('@/pages/system/error/404')
})
const allRoutes = frameInRoutes.concat(newRoutes)
store.commit('admin/page/init', allRoutes, { root: true })
next({ ...to, replace: true })
} else {
next()
}
} else {
const isPermission = whiteList.includes(to.path)
if (isPermission) {
next()
} else {
next({
name: 'login',
query: {
redirect: to.fullPath
}
})
}
}
})
router.afterEach((to) => {
if (Setting.showProgressBar) ViewUIPlus.LoadingBar.finish()
// 多页控制 打开新的页面
if (
!('meta' in to) ||
(to.meta && !('tabs' in to.meta)) ||
(to.meta && to.meta.tabs)
) {
store.dispatch('admin/page/open', to)
}
// 更改标题
util.title({
title: to.meta.title
})
// 返回页面顶端
window.scrollTo(0, 0)
})
export default router
routes.js
import { h } from 'vue'
import account from './modules/account'
import dashboard from './modules/dashboard'
import workplace from './modules/workplace'
import datamodel from './modules/datamodel'
import datatable from './modules/datatable'
import merged from './modules/merged'
import examine from './modules/examine'
import system from './modules/system'
import BasicLayout from '@/layouts/basic-layout'
/**
* 在主框架内显示
*/
const frameIn = [
{
path: '/',
redirect: {
name: 'dashboard'
},
component: BasicLayout,
children: [
// 刷新页面 必须保留
{
path: 'refresh',
name: 'refresh',
hidden: true,
component: {
beforeRouteEnter (to, from, next) {
next((instance) => instance.$router.replace(from.fullPath))
},
render: () => h()
}
},
// 页面重定向 必须保留
{
path: 'redirect/:route*',
name: 'redirect',
hidden: true,
component: {
beforeRouteEnter (to, from, next) {
next((instance) =>
instance.$router.replace(JSON.parse(from.params.route))
)
},
render: () => h()
}
}
]
},
dashboard,
workplace,
datamodel,
datatable,
merged,
examine,
account,
system
]
/**
* 在主框架之外显示
*/
const frameOut = [
// 登录
{
path: '/login',
name: 'login',
meta: {
title: '$t:page.login.title'
},
component: () => import('@/pages/account/login')
},
{
path: '/dashboard/create',
name: 'dashboard-create',
meta: {
title: '新建仪表板',
closable: false
},
component: () => import('@/pages/dashboard/components/dataease-create')
}
// // 注册
// {
// path: '/register',
// name: 'register',
// meta: {
// title: '$t:page.register.title'
// },
// component: () => import('@/pages/account/register')
// },
// // 注册结果
// {
// path: '/register/result',
// name: 'register-result',
// meta: {
// title: '注册结果'
// },
// component: () => import('@/pages/account/register/result')
// },
// // 找回密码
// {
// path: '/forget',
// name: 'forget',
// meta: {
// title: '找回密码'
// },
// component: () => import('@/pages/account/forget')
// }
]
/**
* 错误页面
*/
const errorPage = [
{
path: '/403',
name: '403',
meta: {
title: '403'
},
component: () => import('@/pages/system/error/403')
},
{
path: '/500',
name: '500',
meta: {
title: '500'
},
component: () => import('@/pages/system/error/500')
}
// {
// path: '/:pathMatch(.*)',
// name: '404',
// meta: {
// title: '404'
// },
// component: () => import('@/pages/system/error/404')
// }
]
// 导出需要显示菜单的
export const frameInRoutes = frameIn
// 重新组织后导出
export default [...frameIn, ...frameOut, ...errorPage]
示例代码仅用来参考,根据自己需要删减。
stores 状态管理容器相关模块
主要用于集中管理和共享应用的全局状态。
App.vue
根组件,最终被替换渲染到index.html页面中的#app入口节点
main.js
整个项目的启动入口模块,负责创建Vue实例并挂载到DOM
以上的示例代码仅用来参考,具体代码应该根据项目需要进行增减。