Vue - vue 前端项目基础框架搭建流程
1. 初始化项目
// 创建vue基础代码框架, 可选择支持typescript,vue-router, pinia, jsx, Eslint, prettier
pnpm create vue
// 添加路由支持 (创建基础代码框架时可选)
pnpm add vue-router
// 添加数据状态管理支持 (创建基础代码框架时可选)
pnpm add pinia
// 添加UI模块支持
pnpm add element-plus
// 添加图标支持
pnpm add @element-plus/icons-vue
// 增加网络请求模块支持
pnpm add axios
// 添加国际化支持
pnpm add vue-i18n
// 添加css预处理器支持
pnpm add -D sass
// 引入数据处理支持
pnpm add lodash-es
// 引入日期处理支持
pnpm add dayjs
// 增加数据模拟模块支持
pnpm add -D mockjs
2. 编译配置 (vite.config.js)
// 安装自动引入组件开发插件
pnpm add -D unplugin-vue-components
pnpm add -D unplugin-auto-import
// 安装插件支持jsx语法 (创建基础代码框架时可选)
pnpm add -D @vitejs/plugin-vue-jsx
vite.config.js
import { defineConfig } from 'vite';
import vueJsx from '@vitejs/plugin-vue-jsx';
import vue from '@vitejs/plugin-vue';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import vueDevTools from 'vite-plugin-vue-devtools'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [
vueJsx(), // 使vue项目支持jsx语法
vue(), // 使vite支持vue组件的编译和解析
vueDevTools(), // vue开发者工具
/**支持自动导入element-plus组件,vue-router, vue-i18n, pinia */
AutoImport({
imports: ['vue', 'vue-router', 'pinia', 'vue-i18n'],
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
});
3. 引入normal.css 进行样式重置
-
安装normalize.css
pnpm add normalize.css
-
项目入口引入样式重置文件
src/main.js
// 使用normalize.css 重置样式
import 'normalize.css';
4. 国际化配置
- 创建语言包目录及语言文件 src/i18n/language, src/i18n/language/en.js src/i18n/language/zh-cn.js
language/en.js
export default {
logout: "Exit",
login: "Login",
nodata: "No results.",
}
language/zh-cn.js
export default {
logout: '退出',
login: '登录',
nodata: '暂无数据',
}
- 创建语言包入口文件 src/i18n/index.js
import { createI18n } from 'vue-i18n';
// 项目语言包文件
import localEn from './language/en';
import localZhCn from './language/zh-cn';
// element-plus 语言包文件
import en from 'element-plus/es/locale/lang/en';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
// project i18n
const i18n = createI18n({
legacy: false, // vue组合式api设为false
locale: 'zh-cn', // 默认语言
fallbackLocale: 'en', // 回退语言
messages: {
'zh-cn': localZhCn,
'en': localEn
}
});
// element i18n
export const elementLocales = {
'zh-cn': zhCn,
en
};
export default i18n;
- 项目入口引入i18n
src/main.js
import { createApp } from 'vue';
import i18n from './i18n';
const app = createApp(App);
app.use(i18n).mount('#app');
- 项目中设置element-plus组件国际化
src/App.vue
<script setup>
// 导入elementUI 的国际化内容
import { elementLocales } from '@/i18n';
const { locale } = useI18n();
locale.value = localStorage.getItem('locale') || 'zh-cn';
</script>
<template>
<el-config-provider :locale="elementLocales[locale]">
<router-view></router-view>
</el-config-provider>
</template>
- 在项目中使用国际化相关方法
<template>
<div class="font-sm">{{ t('tips404') }}</div>
</template>
<script setup>
const { locale, t } = useI18n();
function changeLanguage(lang){
locale.value = lang;
localStorage.setItem('locale', lang);
}
</script>
5. 配置路由
- 创建路由配置文件
src/router/router.js
// 组件采用懒加载形式引入,打包时会进行分块
export default [
{
path: '/datacenter',
name: 'Datacenter',
component: () => import('@/views/datacenter/index.vue'),
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/404.vue'),
},
]
- 创建路由入口文件
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 布局文件
import Layout from '@/layout/index.vue'
// 路由配置文件
import subRoutes from './router';
/**
* 配置路由
*/
const routes = [
{
path: '/',
name: 'Home',
component: Layout,
children: subRoutes,
}
];
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
- 创建布局文件
src/layout/components/PageHeader.vue
src/layout/components/PageSidebar.vue
src/layout/index.vue
<template>
<div class="page-container">
<main>
<page-sidebar></page-sidebar>
<div class="right">
<page-header />
<router-view></router-view>
</div>
</main>
</div>
</template>
<script setup>
import { computed } from 'vue';
import PageHeader from './components/PageHeader.vue';
import PageSidebar from './components/PageSidebar.vue';
const route = useRoute();
</script>
<style lang="scss">
.page-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
</style>
- 在项目入口引入路由
src/main.js
import { createApp } from 'vue';
import router from './router';
const app = createApp(App);
app.use(router).mount('#app');
6. 创建路由对应的视图文件
- 404 页面视图
src/views/404.vue
- 数据中心页面视图
src/views/datacenter/index.vue
7. 创建布局文件
- 页头
src/layout/components/PageHeader.vue
- 侧边栏
src/layout/components/PageSidebar.vue
- 布局入口文件
src/layout/index.vue
8. 创建store
- 在项目入口引入pinia
src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
const pinia = createPinia();
const app = createApp(App);
app.use(pinia).mount('#app');
- 创建store
src/store/dict.js
// 字典数据
import { defineStore } from 'pinia';
import { getDict } from '@/apis/dict';
export const DICT = {
vehicleType: '1819698002348158977',
plateColor: '1819698002348158978',
}
export const useDictStore = defineStore('dict', {
// 类似于data
state: () => ({
data: {}
}),
// 类似于computed计算属性
getters: {
dict(state, type) {
return state.data[type] || [];
}
},
// 类似于vue2中的methods
actions: {
setDict(type, data) {
this.data[type] = data;
},
format(type, value) {
const data = this.data[type];
if (data) {
const obj = data.find((item) => value == item.value);
return obj?.label || '';
}
},
async getDict(type, callback) {
const id = DICT[type];
if (!id) throw new Error('字典类型不存在');
// 如果store中存在,则从store中直接获取
if(this.data[type]) {
// 执行回调
callback && callback();
return this.data[type];
}
// 获取该类型字典数据
const res = await getDict(id);
const resData = res?.data || [];
// 提取关键数据转换为{label: '', value: ''}
const data = resData.map((item) => ({ label: item.name, value: item.value }));
// 存储数据
this.setDict(type, data);
// 执行回调
callback && callback();
// 返回数据
return data;
}
}
});
- 在项目中使用创建的store
<script setup>
import { useDictStore } from '@/store/dict';
const dictStore = useDictStore();
onMounted(() => {
dictStore.getDict('vehicleType');
});
</script>
9. 封装mock模拟数据模块
- 创建mock入口文件
src/mock/index.js
import Mock from 'mockjs';
import config from './config';
// 引入模块模拟数据
import * as login from './modules/login';
import * as app from './modules/app';
const { baseURL, timeout } = config;
Mock.setup({ timeout });
// 1. 开启/关闭所有模块拦截,通过openMock开关设置
// 2. 开启/关闭单个模块拦截,通过调用mock方法isOpen参数设置
// 3. 开启/关闭模块中某个请求拦截,通过函数返回对象中的isOpen属性设置
const openMock = false;
mockAll([
login,
app,
], openMock);
function mockAll(modules, isOpen=true){
for(const k in modules){
mock(modules[k], isOpen);
}
}
// 模拟单个模块
// mock(login, true);
// mock(app, true);
function mock(mod, isOpen = true) {
if(isOpen){
for (var key in mod){
((res) => {
if(res.isOpen !== false){
let url = baseURL;
if(!url.endsWith('/')){
url = url + '/';
}
url = url + res.url;
Mock.mock(new RegExp(url), res.method, (opts) =>{
opts.data = opts.body ? JSON.parse(opts.body) : null;
const resData = Mock.mock(typeof res.response === 'function' ? res.response(opts):res.response);
console.log('%cmock拦截, 请求:', 'color: blue', opts);
console.log('%cmock拦截,响应:', 'color: blue', resData);
return resData;
});
}
})(mod[key]() || {});
}
}
}
- 创建数据模块文件
src/mock/modules/app.js
// 应用管理模块 分页查询
export function listPage() {
return {
url: "app/listPage",
method: "get",
response: (opts) => {
const { pageNum, pageSize } = opts.data;
const totalSize = 24;
const content =
pageNum * pageSize < totalSize
? `content|${pageSize}`
: `content|${totalSize % pageSize}`;
return {
code: 200,
msg: null,
data: {
pageNum,
pageSize,
totalSize,
[content]: [
{
appCode: "@increment",
appName: "@word",
"versionName": "15",
versionNumber: "@float(1, 100, 2)",
"isForce|1": ["是", "否"],
buildTime: "@date @time",
commitTime: "@date @time",
"versionStatus|1": [0, 1],
},
],
},
};
},
};
}
- 项目入口引入mock
src/main.js
// 使用mockjs进行数据模拟
import './mock';
10. 封装网络请求模块
- 配置请求参数设置
src/request/config.js
export default {
// 默认请求方法
method: 'get',
// 请求url前缀
baseURL: 'http://your.domain.com/api',
// 请求头设置
headers: {
'Content-type': 'application/json;charset=UTF-8'
},
// 设置超时时间
timeout: 10000,
// 携带凭证
withCredentials: true,
// 返回数据类型
responseType: 'json'
};
- 封装统一的api请求
src/request/index.js
import axios from 'axios';
import config from './config';
import router from '../router';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/store/user';
import i18n from '@/i18n';
const { t } = i18n.global;
export default function request(options) {
return new Promise((resolve, reject) => {
// 创建请求实例
const instance = axios.create({ ...config });
const userStore = useUserStore();
// 增加请求拦截处理
instance.interceptors.request.use(
(config) => {
let token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `bearer ${token}`;
} else {
router.push('/login');
}
return config;
},
(error) => {
console.log('request:', error);
if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
ElMessage({ message: '请求超时', type: 'error', showClose: true });
}
return Promise.reject(error);
}
);
// 增加响应拦截处理
instance.interceptors.response.use(
(response) => {
return response.data;
},
(err) => {
const codeMap = {
400: '请求错误',
401: '未授权,请登录',
403: '拒绝访问',
404: '请求地址出错',
408: '请求超时',
500: '服务器内部错误',
501: '服务未实现',
502: '网关错误',
503: '服务不可用',
504: '网关超时',
505: 'HTTP版本不受支持'
};
if (err && err.response) {
const status = err.response.status;
const errMsg = codeMap[status] || '';
ElMessage({ type: 'error', message: errMsg, showClose: true });
if (status === 401) {
userStore.clearToken();
router.push('/login');
}
}
return Promise.reject(err);
}
);
// 处理正常返回的数据
instance(options)
.then((res) => {
if (res || res.code === 200) {
resolve(res);
} else {
if (res.code === -2) {
router.push('/login');
}
ElMessage({ type: 'error', message: res.msg || '操作失败', showClose: true });
reject(res);
}
})
.catch((error) => {
reject(error);
});
});
}
- 添加业务模块所需的请求接口
src/apis/login.js
import request from '@/request';
import config from '@/request/config';
const baseURL = `${config.baseURL}/auth/oauth/token`
// 登录
export const login = (data) => {
return request({
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
url: baseURL,
method: 'post',
data
});
};
// 退出登录
export const logout = (data) => {
return request({
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=utf-8'
},
url: `${config.baseURL}/auth/logout2`,
method: 'post',
data,
});
};
src/apis/params.js
import request from '@/request';
import config from '@/request/config';
const baseURL = `${config.baseURL}/admin/baseparam`
// 基础参数查询
export const listPage = (data) => {
const url = `${baseURL}/queryList`;
return request({ url, method: 'post', data });
};
// 添加
export const save = (data) => {
const url = `${baseURL}/add`;
return request({ url, method: 'post', data });
};
// 更新
export const update = (data) => {
const url = `${baseURL}/update`;
return request({ url, method: 'post', data });
}
// 删除
export const remove = (id) => {
const url = `${baseURL}/delete/${id}`;
return request({ url, method: 'post'});
}
11. 定义项目中用到常量
src/consts/index.js
// 设置统计数据 5 min 刷新一次
export const STATS_DATA_FRESH_TIME = 5 * 60 * 1000;
// 设置实时检测记录数据 30s 刷新一次
export const RECORD_DATA_FRESH_TIME = 30 * 1000;
12. 封装自定义组件
- 创建自定义组件
src/components/CmVideo.vue
<template>
<el-dialog v-model="dialogVisible" :draggable="true" :close-on-click-modal="false" @close="closeDialog"
class="videoDialog">
<video :src="videoUrl" controls autoplay width="100%"></video>
</el-dialog>
</template>
<script setup>
const dialogVisible = ref(false);
const videoUrl = ref(null);
const props = defineProps(['url']);
defineExpose({
display: openDialog,
close: closeDialog,
});
function openDialog() {
dialogVisible.value = true;
videoUrl.value = props.url;
}
function closeDialog() {
videoUrl.value = null;
dialogVisible.value = false;
}
</script>
<style lang="scss" scoped>
</style>
- 在项目中使用自定义组件
<template>
<CmVideo ref="videoRef" :url="videoUrl" />
</template>
<script>
import { getFile } from '@/apis/file';
import CmVideo from '@/components/CmVideo.vue';
const videoRef = ref(null);
const videoUrl = ref(null);
const isVideoLoading = ref(false);
// 播放视频
function playVideo() {
if (isVideoLoading.value || videoUrl.value == null) return;
videoRef.value?.display();
}
// 获取视频资源
async function getVideo(truckVedio) {
if (truckVedio == 'null' || truckVedio == '0') return;
isVideoLoading.value = true;
return getFile(truckVedio).then((res) => {
const url = window.URL.createObjectURL(res);
videoUrl.value = url;
}).finally(() => {
isVideoLoading.value = false;
});
}
onMounted(async () => {
const { detectImages, truckVedio } = await getData();
getVideo(truckVedio);
});
</script>
13 封装组合式函数
src/hooks/use-table-handlers.js
- 创建组合式函数
export default function useTableHandlers(form) {
const { t } = useI18n();
const tableRef = ref();
const formRef = ref();
const formLoading = ref(false);
const dialogVisible = ref(false);
const isEdit = ref(false);
const _formOld = { ...form };
// 查询
const doSearch = () => {
tableRef.value.reload();
};
// 增加
const doAdd = () => {
dialogVisible.value = true;
isEdit.value = false;
formRef.value && formRef.value.clearValidate();
};
// 编辑
const doEdit = (row) => {
if (!form) return;
isEdit.value = true;
dialogVisible.value = true;
for (const key in form) {
if (key in row) {
form[key] = row[key];
}
}
};
// 删除
const doRemove = (api, ids, callback) => {
api(ids).then((res) => {
callback && callback(res);
});
};
// 获取参数
function getParams() {
const params = { ...form };
if (!isEdit.value) {
delete params.id;
}
return params;
}
// 提交数据
function doSubmit(apis, callback) {
if (!form || !apis) return;
formRef.value.validate((valid) => {
if (!valid) return;
formLoading.value = true;
let promise;
const params = apis.getParams ? apis.getParams() : getParams();
if (isEdit.value) {
promise = apis.update(params);
} else {
promise = apis.save(params);
}
promise
.then((res) => {
if (callback) {
callback(res);
} else {
ElMessage({
message: t('tips.success'),
type: 'success',
showClose: true
});
}
doClose();
if (isEdit.value) {
tableRef.value.refresh();
} else {
tableRef.value.reload();
}
})
.finally(() => {
formLoading.value = false;
});
});
}
//重置表单
function resetForm() {
if (!form) return;
for (const key in _formOld) {
form[key] = _formOld[key];
}
}
// 关闭新增弹窗
function doClose() {
dialogVisible.value = false;
resetForm();
}
return {
t,
tableRef,
dialogVisible,
isEdit,
formLoading,
formRef,
doSearch,
doAdd,
doEdit,
doRemove,
doSubmit,
doClose
};
}
- 在项目中使用组合式函数
<template>
<cm-table ref="tableRef" :header-cell-style="{ background: '#F6F7FB' }" :oprWidth="150" :get-page="listPage"
:filters="filters" :columns="columns" :showBatchDelete="false" :showAdd="false" :operations="operations"
:showPagination="true" @handleEdit="doEdit" @handleAdd="doAdd" @handleDelete="handleDelete"></cm-table>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="right" label-suffix=": ">
<el-row>
<el-col :span="24">
<el-form-item prop="name" label="检测点名称">
<el-input v-model="form.name" placeholder="检测点名称"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import useTableHandlers from '@/hooks/use-table-handlers';
// form数据模型
const form = reactive({
id: null,
name: null,
pointNo: null,
deviceType: null,
lat: null,
lng: null,
location: null,
});
// 使用表格处理器
const {
t,
tableRef,
formRef,
formLoading,
dialogVisible,
isEdit,
doSearch,
doAdd,
doEdit,
doRemove,
doSubmit,
doClose
} = useTableHandlers(form);
// 处理删除
function handleDelete(ids, callback) {
doRemove(remove, ids, callback);
}
</script>
14. 封装自定义指令 [可选]
src/directives/
15. 封装自定义插件 [可选]
src/plugins/
16. 封装工具函数
src/utils/index.js
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration'
dayjs.extend(duration);
// 转换时长
export function formatDuration(duration) {
return dayjs.duration(duration, 'seconds').format('HH:mm:ss');
}
17. 进行业务开发
- 开发api请求
在src/apis/ 目录下建立以业务命名的接口请求文件 - 开发数据状态业务
在src/store/ 目录下建立以业务命名的进行数据请求或者数据转换的文件(如果有需要进行数据状态管理的话) - 构建业务相关UI
在src/views/ 目录下建立以模块命名的视图目录,并建立相关单文件组件视图页面