背景:vue3在2022年2月7日成为新的默认版本。而vite原生 ES 模块导入方式,可以实现闪电般的冷服务器启动,所以优先使用Vite 快速构建 Vue 项目。 构建方式可能随时更新,这里推荐根据官网最新的构建流程来做,传送门。
搭建后最原始的样子(vscode记得安装Vetur)
vue3 + vuex + vue-router + vuex-persistedstate(持久化缓存) 等基配
1.package.json
{
"name": "yc-ui",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"element-plus": "^1.2.0-beta.4",
"vite-plugin-style-import": "^1.4.0",
"vite-plugin-svg-icons": "^1.0.5",
"vue": "^3.2.16",
"vue-router": "^4.0.12",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.9.3",
"sass": "^1.43.5",
"unplugin-vue-components": "^0.17.2",
"vite": "^2.6.4"
}
}
2.router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const Login = () => import('@/views/login.vue');
const p404 = () => import('@/views/error-page/p404.vue');
import views from '@/router/views';
import MenuLayOut from '@/components/MenuLayOut.vue'; // 这个是管理系统左侧菜单
import Store from '@/store/index';
const routes = [
{
path: '/login',
component: Login,
name: 'login',
meta: {
title: 'login',
icon: '#login',
},
},
{
path: '/',
component: MenuLayOut,
redirect: 'devops',
children: [...views],
},
{
path: '/404',
name: '404',
component: p404,
meta: {
title: '404',
},
},
{
path: '/:pathMatch(.*)',
redirect: '/404',
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
router.beforeEach(async (to, from, next) => {
// const token = Cookies.get('token')
// if (token) {
if (to.path === '/login') {
next({ path: '/' });
} else {
// if (!store.state.authorized) {
// // set authority
// await store.dispatch('setAuthority')
// // it's a hack func,avoid bug
// next({ ...to, replace: true })
// } else {
next();
// }
}
// } else {
// if (to.path !== '/login') {
// next({ path: '/login' })
// } else {
// next(true)
// }
// }
});
router.afterEach((to, from) => {
// ...
Store.commit('common/changeMenu', to.meta.activeMenu);
});
export default router;
3.view.js
const Devops = () =>
import(/* webpackChunkName: "devops" */ '../views/devops/list.vue');
const Quality = () =>
import(/* webpackChunkName: "quality" */ '../views/quality/list.vue');
const QualityDetail = () =>
import(/* webpackChunkName: "quality" */ '../views/quality/detail.vue');
const Performance = () =>
import(/* webpackChunkName: "performance" */ '../views/performance/list.vue');
const PerformanceDetail = () =>
import(/* webpackChunkName: "quality" */ '../views/performance/detail.vue');
const router = [
{
path: 'devops',
component: Devops,
name: 'devops',
meta: {
title: 'DevOps看板',
icon: 'icon_devops', // 看后面的svg配置后,记得创建对应的svg文件
activeMenu: 'devops',
},
},
{
path: 'quality',
component: Quality,
name: 'quality',
meta: {
title: '应用代码质量报告',
icon: 'icon_quality',
activeMenu: 'quality',
},
},
{
path: 'quality/:name',
component: QualityDetail,
name: 'quality.detail',
meta: {
title: '应用代码质量报告',
icon: 'icon_quality',
activeMenu: 'quality',
hidden: true,
},
},
{
path: 'performance',
component: Performance,
name: 'performance',
meta: {
title: '应用性能测试报告',
icon: 'icon_microsoft',
activeMenu: 'performance',
},
},
{
path: 'performance/:name',
component: PerformanceDetail,
name: 'performance.detail',
meta: {
title: '应用性能测试报告',
activeMenu: 'performance',
hidden: true,
},
},
];
export default router;
添加eslint
eslint相关依赖
npm i eslint -s
npm i -d @typescript-eslint/parser @vue/eslint-config-prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue eslint-plugin-vuejs-accessibility prettier vue-eslint-parser
创建 eslintignore.js
/build/
/config/
/dist/
**/*.ts
/tests/
创建 eslint.js
//.eslintrc.js
module.exports = {
parser: 'vue-eslint-parser',
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended',
],
parserOptions: {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true,
},
},
rules: {
'import/extensions': ['.js', '.vue', '.json'],
// allow optionalDependencies
// 'import/no-extraneous-dependencies': [
// 'error',
// {
// optionalDependencies: ['test/unit/index.js'],
// sourceType: 'module',
// allowImportExportEverywhere: true,
// },
// ],
'import/extensions': ['error', 'always', { ignorePackages: false }],
'implicit-arrow-linebreak': ['error', 'beside'],
'no-shadow': 0,
// 禁止覆盖受限制的标识符
'no-shadow-restricted-names': 2,
// "no-shadow": ["error", { "allow": ["done"] }],
'no-unused-expressions': [
'off',
{ allowShortCircuit: true, allowTernary: true },
],
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// https://eslint.org/docs/rules/arrow-parens
'arrow-parens': ['warn', 'as-needed'],
// https://eslint.org/docs/rules/arrow-body-style
'arrow-body-style': ['off', 'as-needed'],
// https://eslint.org/docs/rules/arrow-spacing
'arrow-spacing': 'error',
// https://eslint.org/docs/rules/class-methods-use-this
'class-methods-use-this': ['off'],
// https://eslint.org/docs/rules/no-param-reassign
'no-param-reassign': ['off'],
'no-mixed-operators': ['off', { allowSamePrecedence: false }],
// https://eslint.org/docs/rules/no-console
'no-console': ['warn', { allow: ['warn', 'error', 'log'] }],
'prefer-template': 'off',
'linebreak-style': [2, 'unix'],
// vue lint
// https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/html-self-closing.md
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'never',
},
svg: 'any',
math: 'always',
},
],
// https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/max-attributes-per-line.md
'vue/max-attributes-per-line': [
0,
{
singleline: 2,
multiline: {
max: 1,
allowFirstLine: true,
},
},
],
'object-curly-newline': ['off'],
'function-paren-newline': ['off'],
camelcase: ['off'],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
mocha: true,
},
},
],
};
添加 .prettierrc.json
{
"semi": true,
"eslintIntegration": true,
"singleQuote": true,
"endOfLine": "lf",
"tabWidth": 2,
"bracketSpacing": true
}
.setting.json + jsconfig.json 配置 vscode 自动保存elint/prettier格式化
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"vue",
"typescript",
"typescriptreact",
"json"
]
}
配置 模块化store module
这里可以有大体两种,本人更喜欢下面这个
1.index.js
import { createStore, createLogger } from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import auth from 'store/modules/auth';
import common from 'store/modules/common';
const debug = process.env.NODE_ENV !== 'production';
const plugins = [createPersistedState()];
if (debug) {
plugins.push(createLogger());
}
export default createStore({
modules: {
auth,
common,
},
strict: debug,
plugins,
});
common.js
配置alias
vite.config
resolve: {
alias: [
{
find: /^@\//,
replacement: pathResolve('src') + '/',
},
{
find: /^store\//,
replacement: pathResolve('src/store') + '/',
},
{
find: /^~\//,
replacement: pathResolve('node_modules') + '/',
},
],
},
jsconfig.json 是告诉vscode知道
配置自动批量导入自定义组件
这个也是一直用一直爽,每次在指定的文件夹(src/components/*.vue)新增组件,不用在全局逐个引入,就能页面里面直接引用
src/components/index.js
const files = import.meta.glob('./*.vue');
const filesJs = import.meta.glob('./*.js');
const componentsGlob = (app) => {
Object.keys(files).forEach((key) => {
files[key]().then((res) => {
const start = key.lastIndexOf('.');
const end = key.lastIndexOf('/') + 1;
const cName = key.slice(end, start);
app.component(cName, res.default);
});
});
};
export { componentsGlob };
src/components/MenuLayOut.vue
<template>
<div class="dashboard-con">
<!-- <div class="status-bar"></div> -->
<div :class="{ content: true, opened: isCollapse }">
<div class="nav">
<el-menu
:default-active="defaultActiveMenu"
class="menu"
:collapse="isCollapse"
:default-openeds="defaultOpeneds"
router
background-color="#373B41"
text-color="#e4e7ed"
>
<template v-for="e in views">
<el-sub-menu :index="e.name" :key="e.name" v-if="e.children">
<template #title>
<SvgIcon class="icon" :name="e.meta.icon"></SvgIcon>
<template>
<span>{{ e.meta.title }}</span>
</template>
<template v-for="c in e.children">
<el-menu-item
v-if="!c.meta.hidden"
:key="c.name"
:index="c.name"
:route="{
name: c.name,
params: { scope: c.meta.title },
}"
>
<SvgIcon class="icon" :name="c.meta.icon"></SvgIcon>
<span> {{ c.meta.title }}</span>
</el-menu-item>
</template>
</template>
</el-sub-menu>
<el-menu-item
v-if="!e.children && !e.meta.hidden"
:key="e.name"
:index="e.name"
:route="{ name: e.name }"
>
<SvgIcon class="icon" :name="e.meta.icon"></SvgIcon>
<template #title>
<span>{{ e.meta.title }}</span>
</template>
</el-menu-item>
</template>
</el-menu>
<p class="collapse-btn" @click="toogleCollapse">
<el-icon class="arrow"> <arrow-left-bold /> </el-icon>
</p>
</div>
<div class="dashboard-content">
<router-view></router-view>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import views from '@/router/views.js';
export default {
components: {},
data() {
return {
views,
};
},
computed: {
...mapState({
defaultActiveMenu: (state) => state.common.defaultActiveMenu,
isCollapse: (state) => state.common.isCollapse,
}),
defaultOpeneds() {
const temp = [];
this.views.forEach((e) => {
e.children && temp.push(e.name);
});
return temp;
},
},
methods: {
toogleCollapse() {
this.$store.commit('common/changeIsCollapse', !this.isCollapse);
},
},
};
</script>
<style lang="scss" scoped>
$icon-default: #909399;
.el-menu {
border-right: none !important;
}
.icon {
vertical-align: middle;
margin-right: 5px;
text-align: center;
font-size: 20px;
width: 24px;
height: 18px;
align-self: center;
color: $icon-default;
fill: $icon-default;
}
.el-menu--vertical .icon {
color: #909399;
}
.el-sub-menu__title {
&:hover {
background-color: generate-color($blue, -6);
}
&.is-active {
background-color: generate-color($blue, -7);
}
}
.el-menu-item {
height: 50px;
line-height: 50px;
&:hover {
background-color: $dark-menu-item-bg;
}
&.is-active {
background-color: $dark-menu-item-bg;
color: $blue;
.icon {
color: $blue;
fill: $blue;
}
}
}
.dashboard-con {
height: 100%;
position: relative;
.status-bar {
height: $nav-height;
width: 100%;
background-color: $dark-menu-bg;
}
.content {
width: 100%;
height: 100%;
.nav {
color: #e4e7ed;
background-color: $dark-menu-bg;
width: $nav-width;
min-width: $nav-width;
height: calc(100% - 57px);
padding-bottom: 57px;
position: absolute;
overflow-y: scroll;
transition: all 0.3s;
.menu {
width: 100%;
overflow-y: scroll;
// height: calc(100% - #{$nav-height});
height: 100%;
overflow-y: scroll;
}
.collapse-btn {
border-top: 1px solid #2d3034;
background-color: $dark-menu-bg;
width: $nav-width;
text-align: center;
height: 56px;
line-height: 56px;
position: absolute;
bottom: 0;
}
}
.dashboard-content {
width: calc(100% - $nav-width);
min-width: calc(1280px - $nav-width);
box-sizing: border-box;
height: 100%;
overflow: scroll;
margin-left: $nav-width;
position: relative;
transition: all 0.3s;
}
&.opened {
.nav {
width: 64px;
min-width: 64px;
}
.dashboard-content {
margin-left: 64px;
}
.collapse-btn {
width: 64px;
}
}
}
}
</style>
配置svg
svg 在一些菜单栏或者操作按钮上都很常用,你不会还在用图片格式的吧。使用svg加上依稀图标网站上例如icon-font获取一些必要的图表,没有ui也不方,像我现在的广州团队没有ui,我一个前端搞定了。
1.引入依赖
npm i vite-plugin-svg-icons -d
2.vite.config 加上这个配置
import viteSvgIcons from 'vite-plugin-svg-icons';
plugins: [
vue(),
viteSvgIcons({
// 指定需要缓存的图标文件夹
iconDirs: [resolve(root, 'src/assets/svg')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
}),
]
一直用一直爽,每次到icon-font复制svg代码到src/assets/svg下,就可以直接在组件中使用了,不用再逐个引入svg文件了
3.svg 组件
<template>
<svg class="svg" aria-hidden="true">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script>
import { computed } from 'vue';
export default {
name: 'SvgIcon',
props: {
prefix: {
type: String,
default: 'icon',
},
name: {
type: String,
required: true,
},
color: {
type: String,
},
},
setup(props) {
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
return { symbolId };
},
};
</script>
<style lang="scss" scoped>
.svg {
display: inline-block;
vertical-align: middle;
}
</style>
4.使用方式
<SvgIcon class="svg-icon" name="icon_name"></SvgIcon>
5.可以class改变样式或者大小
.svg-icon{fill:cyan;background-color:blue;}
使用ui组件库 element-plus
1.element-pus 安装
npm i element-plus
2.使用unplugin-vue-components/resolvers按使用情况自动导入对应组件依赖
npm i unplugin-vue-components/resolvers
配置vite.config
Components({
resolvers: [ElementPlusResolver()],
}),
styleImport({
libs: [
{
libraryName: "element-plus",
ensureStyleFile: true,
resolveStyle: (name) => {
if (name === "locale") return "";
return `element-plus/packages/theme-chalk/src/${name}.scss`;
},
resolveComponent: (name) => {
return `element-plus/lib/${name}`;
},
},
],
}),
3.配置组件为中文版本
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import zhCn from 'element-plus/lib/locale/lang/zh-cn';
import { ref } from 'vue';
const locale = ref(zhCn);
</script>
<template>
<div id="app">
<ElConfigProvider :locale="locale">
<router-view></router-view>
</ElConfigProvider>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
background-color: #f1f3f6;
width: 100%;
height: fit-content;
}
</style>
添加env 信息
.env
# port
VITE_PORT = 5000
# spa-title
VITE_GLOB_APP_TITLE = Daoclou DCS
# spa shortname
VITE_GLOB_APP_SHORT_NAME = DCS
.env.production
# 只在生产模式中被载入
# 网站标题
VITE_APP_TITLE = peoduction-ui
# 网站前缀
VITE_BASE_URL = './'
VITE_OUTPUT_DIR = dist
# 是否删除console
VITE_DROP_CONSOLE = true
# API
VITE_APP_API_URL = http://10.23.12.108:34203
.env.development
# 只在开发模式中被载入
# 网站标题
VITE_APP_TITLE = dev-ui
# 网站前缀
VITE_BASE_URL = '/'
VITE_OUTPUT_DIR = dist
# 是否删除console
VITE_DROP_CONSOLE = true
# API
VITE_APP_API_URL = http://10.23.12.108:34203
整个vite.config
import { defineConfig, loadEnv } from 'vite';
import { resolve } from 'path';
import dayjs from 'dayjs';
import vue from '@vitejs/plugin-vue';
import viteSvgIcons from 'vite-plugin-svg-icons';
import Components from 'unplugin-vue-components/vite';
// 动态导入用到的element组件
import styleImport from 'vite-plugin-style-import';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import pkg from './package.json';
// https://vitejs.dev/config/
const root = process.cwd();
const { dependencies, devDependencies, name, version } = pkg;
const __APP_INFO__ = {
pkg: { dependencies, devDependencies, name, version },
lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
};
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production';
const { VITE_BASE_URL, VITE_APP_API_URL, VITE_PORT, VITE_OUTPUT_DIR } =
loadEnv(mode, root);
function pathResolve(dir) {
return resolve(root, VITE_BASE_URL, dir);
}
return {
base: VITE_BASE_URL,
root,
hmr: { overlay: false },
resolve: {
alias: [
{
find: /^@\//,
replacement: pathResolve('src') + '/',
},
{
find: /^store\//,
replacement: pathResolve('src/store') + '/',
},
{
find: /^~\//,
replacement: pathResolve('node_modules') + '/',
},
],
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import './src/style/variables.scss';@import './src/style/element.scss';`,
},
},
},
build: {
outDir: VITE_OUTPUT_DIR, //打包后文件的存放路径
cssCodeSplit: true,
sourcemap: false,
target: 'modules',
chunkSizeWarningLimit: 2000,
assetsInlineLimit: 4096,
minify: 'terser',
brotliSize: false, // => brotli压缩大小报告
terserOptions: {
compress: {
keep_infinity: true,
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
},
},
},
define: {
// setting vue-i18-next
// Suppress warning
// __INTLIFY_PROD_DEVTOOLS__: false,
__APP_INFO__: JSON.stringify(__APP_INFO__),
},
optimizeDeps: {
include: [
'@vueuse/core',
'@vue/runtime-core',
'element-plus',
'lodash',
'jspdf',
'html2canvas',
'axios',
'vuex',
],
},
server: {
port: VITE_PORT,
open: true,
cors: true,
proxy: {
'/v1': {
target: VITE_APP_API_URL,
changeOrigin: true,
},
},
},
plugins: [
vue(),
viteSvgIcons({
// 指定需要缓存的图标文件夹
iconDirs: [resolve(root, 'src/assets/svg')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
}),
Components({
resolvers: [ElementPlusResolver()],
}),
styleImport({
libs: [
{
libraryName: 'element-plus',
ensureStyleFile: true,
resolveStyle: (name) => {
if (name === 'locale') return '';
return `element-plus/theme-chalk/${name}.css`;
},
},
],
}),
],
};
});
引入axios
1.npm i axios
2. src/service/index.js
import axios from 'axios';
import { localGet } from '@/core/storage';
import { ElMessage } from 'element-plus';
const showMsg = (
message = '请求出错,请联系管理员',
type = 'error',
duration = 5 * 1000
) => {
ElMessage({
message,
type,
duration,
});
};
const service = axios.create({
// baseURL: process.env.BASE_API, // api 的 base_url
timeout: 50000, // 请求超时时间
});
service.interceptors.request.use(
(config) => {
// if (store.getters.token) {
// config.headers['X-Token'] = getToken(); // TODO:
// }
return config;
},
(error) => {
console.log(error); // for debug
Promise.reject(error);
}
);
// response 拦截器
service.interceptors.response.use(
(response) => {
const { silent } = response.config;
const res = response.data;
const codeReg = /^20\d+/;
if (codeReg.test(response.status)) {
return Promise.resolve(res);
} else {
return Promise.reject(res);
}
// !silent && showMsg(res.msg);
// return Promise.reject(response.data);
},
(error) => {
const errorItems = error?.response?.data;
const { status } = error.response;
const { silent } = error.config;
if (silent) {
return Promise.reject(error);
}
let msg = '';
if (status === 500) {
const { error_type = '错误', error_message = '系统错误' } =
errorItems || {};
msg = `${error_type} ${error_message}`;
}
!silent && showMsg(msg || error.message);
return Promise.reject(error);
}
);
export default service;
3. src/service/namespace.js
import api from '@/service/index';
class NameSpaceService {
constructor() {
this.api = api;
}
nameSpaceList(namespace_id = '') {
return this.api.get('/api/dcs/namespace', { params: namespace_id });
}
}
export default new NameSpaceService();
关于vue2的mixins在vue3下使用的语法糖
优化的原因
主要是避免了命名冲突,解决了变量追踪不明确的问题。
使用方法
-例如:src/hooks/TableHook.js
import { reactive, ref } from 'vue';
export default function () {
const total = ref(0);
const page = ref(1);
const pageSizes = [10, 30, 50, 100];
const pageSize = ref(10);
const query = reactive({ page, pageSize });
function handleSizeChange(val) {
query.pageSize = val;
}
return {
total,
query,
pageSizes,
handleSizeChange,
};
}
然后用的地方
import TableHook from '@/hooks/TableHook';
const { total, query, pageSizes } = TableHook();
使用lodash并自动引入
npm i lodash
npm i @types/lodash -d
支持js新语法可选链「?.」以及逻辑空分配(双问号)「??」
npm i @babel/plugin-proposal-optional-chaining -s
// .babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
...
}