前言
相信大家在工作中都会遇到过,需要自己从0-1去开始搭建一个新项目的框架吧,一个项目好的基础架构也在很大程度上影响了项目的可维护性和可扩展性以及稳定性
本篇以作者个人角度给大家分享一下自己在工作中如何实现一个项目从0-1的搭建
技术选型
首先我们第一步要确定的就是技术栈的选型是Vue2,Vue3,React还是Angular?
这里我用的是 Vue3+Element-Plus
安装项目脚手架
使用命令安装
js
vue create my-vue-project
这里选择 Manually select features(手动选择)
选择vue3
选择sass
选择ESLint+Prettier 来规范代码格式(这个很重要)
之后的选项就一路回车键确认就行了
安装一些项目中常用的插件和UI框架
在项目项目创建成功之后根据项目的情况安装一些合适的插件 和UI框架,这对你以后的开发过程中会很有帮助
以下就是我安装的一些插件内容
1.ElementPlus
js
yarn add element-plus
2.Axios
js
yarn add axios
3.Echarts
js
yarn add echarts
4.Moment
js
yarn add moment
5.Vue-cookies
js
yarn add vue-cookies
6.Pinia(在Vue3项目中我更推荐使用pinia比Vuex更轻量化的状态管理)
csharp
yarn add pinia
新建utils文件夹
1.将一些常用的工具函数.js写在该文件夹下面(utils.js)
如:防抖,节流,生成唯一标识,深拷贝,获取文件格式,数组对象方法去重等工具函数方法的封装
js
/**
* 节流原理:在一定时间内,只能触发一次
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
export function throttle(func, wait = 500, immediate = false) {
let timer; let
flag
if (immediate) {
if (!flag) {
flag = true
// 如果是立即执行,则在wait毫秒内开始时执行
typeof func === 'function' && func()
timer = setTimeout(() => {
flag = false
}, wait)
}
} else if (!flag) {
flag = true
// 如果是非立即执行,则在wait毫秒内的结束处执行
timer = setTimeout(() => {
flag = false
typeof func === 'function' && func()
}, wait)
}
}
/**
* 防抖原理:一定时间内,只有最后一次操作,再过wait毫秒后才执行函数
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
export function debounce(func, wait = 500, immediate = false) {
let timeout = null
// 清除定时器
if (timeout !== null) clearTimeout(timeout)
// 立即执行,此类情况一般用不到
if (immediate) {
const callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) typeof func === 'function' && func()
} else {
// 设置定时器,当最后一次操作后,timeout不会再被清除,所以在延时wait毫秒后执行func回调方法
timeout = setTimeout(() => {
typeof func === 'function' && func()
}, wait)
}
}
/**
* @description 生成唯一标识符方法函数
* @param {Number} len 长度
* @param {Number} radix 基数
* @return {String} 唯一标识符字符串
*/
export function onlyKey(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
var r;
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
/**
@description 对象数组深拷贝方法 函数
* @param {Array | Object} source 需要拷贝的数据源
* @return {Array | Object} 拷贝后的新值
*/
export function deepCopy(source) {
if (typeof source !== 'object' || source == null) {
return source;
}
const target = Array.isArray(source) ? [] : {};
for (const key in source) {
// 检查属性是否存在对象中
if (Object.hasOwn(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = deepCopy(source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
/**
@description 获取文件格式
* @param {String} fileName 文件名称字符串
* @return {String} fileType 文件格式
*/
export function getFileType(fileName) {
const fileExtension = fileName.split('.').pop().toLowerCase();
return fileExtension
}
/**
* @description 数组对象方法去重函数
* @param {Array} arr 原数组
* @param {String} key 需要去重的对象 key
* @return {Array} 去重后的数组
*/
export function arrayDuplicateRemoval(arr, key) {
let map = new Map();
for (let item of arr) {
if (!map.has(item[key])) {
map.set(item[key], item);
}
}
return [...map.values()];
}
2.axios的二次封装(request.js)
axios的二次封装相信大家也一定不陌生吧,在实际的项目开发过程中我们需要对后端请求接口的一些内容进行全局处理减少代码和耦合度如:请求头TOKEN的统一下处理 ,请求前缀地址 ,请求超时时间 , 请求拦截 , 响应参数和响应状态码统一拦截 等
js
// request.js
import axios from 'axios'
import { ACCESS_TOKEN } from '@/config/constant'
import { message } from 'ant-design-vue'
import VueCookies from 'vue-cookies'
import { loginStore } from '@/store/loginStore'
const store = loginStore();
const request = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL,
timeout: 6000,
})
//请求错误处理
const errorHandler = (error) => {
return message.error(error.message)
}
//请求头同意处理
request.interceptors.request.use(config => {
const token = VueCookies.get(ACCESS_TOKEN)
if (token) {
config.headers['authorization'] = token
}
return config
}, errorHandler)
//请求返回数据格式同意处理
request.interceptors.response.use((response) => {
const { code } = response.data
if (code == 500) {
message.warning(response.data.message)
return Promise.reject(response.data)
}
//登录失效,异常处理
if (code == 403) {
store.logout().then(() => {
setTimeout(() => {
message.warning('登录异常')
}, 1000)
})
}
return response.data
}, errorHandler)
export default request
3.sessionStorage 和 localStorage 的二次封装(storage.js) 在实际项目开发过程也会经常用到本地浏览器缓存,因此个人也习惯将这两个函数进行二次封装
js
// storage.js
/**
* @function clear
@description 清除所有存储 sessionStorage | localStorage
*
* @function set
@description 设置存储 sessionStorage | localStorage
@param {Object | Array | String | Number} value value
@param {String} key
@return {Object | Array | String | Number}
*
* @function get
* @description 获取存储 sessionStorage | localStorage
* @param {String} key
*
* @function remove
@description 清除存储 sessionStorage | localStorage
@param {String} key
*/
const session = {
set: (key, value) => {
if (!key || !value) { return null }
sessionStorage.setItem(key, JSON.stringify(value))
},
get: (key) => {
if (!key) { return null }
var obj = JSON.parse(sessionStorage.getItem(key))
return obj
},
remove: (key) => {
sessionStorage.removeItem(key)
},
clear: () => {
sessionStorage.clear()
}
}
const local = {
set: (key, value) => {
if (!key || !value) { return null }
localStorage.setItem(key, JSON.stringify(value))
},
get: (key) => {
if (!key) { return null }
var obj = JSON.parse(localStorage.getItem(key))
return obj
},
remove: (key) => {
localStorage.removeItem(key)
},
clear: () => {
localStorage.clear()
}
}
export {
session,
local
}
4.Vue3全局函数的挂载(globalProperties.js)
在vue3项目中对于一些全局函数的挂载需要通过 Vue.config.globalProperties 上去实现,对于项目中的一些全局函数或者变量这里也需要用一个js文件进行单独管理
以下是作者在项目中经常挂载的一些函数和常量
js
// globalProperties.js
import {session,local} from './storage' //会话和本地缓存全局方法
import url from '@/config/routerPath' //全局静态路由
import { messageListener, messageSend } from './webScoket'; //webscoket 全局方法
import { drawAssetsImage } from '@/utils/utils.js' //获取本地图片方法
import mitt from 'mitt' //全局总事件方法
const globalProperties = {
install(Vue) {
Vue.config.globalProperties.$session = session
Vue.config.globalProperties.$local = local
Vue.config.globalProperties.$urls = url
Vue.config.globalProperties.$drawAssetsImage = drawAssetsImage
Vue.config.globalProperties.$bus = mitt()
Vue.config.globalProperties.$scoketEvent = {
messageSend,
messageListener,
}
}
}
export default {
...globalProperties
}
在main.js中去注册使用
js
import { createApp } from 'vue'
import App from './App.vue'
import GlobalProperties from "@/utils/globalProperties.js"; //自定义封装全局方法
const app = createApp(App)
app.use(GlobalProperties)
app.mount('#app')
5.全局组件的注册(globalComponent.js)
在项目中对一些复用性很高的组件,会把其封装成全局组件并用一个统一的模块管理
js
// globalComponent.js
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { ZTable, ZModal, ZTextTooltip, ZUpload } from '@/components'
const components = {
install(Vue) {
// 表格table组件二次封装
Vue.component('ZTable', ZTable)
// 文字超出提示组件
Vue.component('ZTextTooltip', ZTextTooltip)
// 文件上传组件二次封装
Vue.component('ZUpload', ZUpload)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
Vue.component(key, component)
}
Vue.config.productionTip = false
}
}
export default components
在main.js中去注册使用
js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
import GlobalComponent from '@/utils/globalComponent' //全局组件
app.use(GlobalComponent)
app.mount('#app')
6.自定义指令的封装(directive.js)
在项目开发可能会遇到这样一些场景:通过一个标识控制按钮的显隐或者自定义一个全局的loading加载对于这些场景,个人比较推荐用vue的自定义指令去实现
封装一个全局loading的指令
js
// directive.js
import { createApp } from "vue"
import Loading from '@/components/Loading'
/**
* @description loading 加载状态
* @params {Vue} vue实列操作对象
*/
const directiveLoading = (Vue) => {
Vue.directive('zLoading', {
mounted(el, binding, vnode) {
const app = createApp(Loading)
const vNode = app.mount(document.createElement('div'))
el.style.position = 'relative'
vNode.$el.style.display = 'none'
el.appendChild(vNode.$el)
},
updated(el, binding, vnode) {
const loadingNode = el.parentElement.querySelector('#loading-mark')
const { value } = binding
if (!value) {
el.style.position = ''
loadingNode.style.display = 'none'
} else {
el.style.position = 'relative'
loadingNode.style.display = 'block'
}
},
unmounted(el, binding, vnode) {
const loadingNode = el.children[0]
el.removeChild(loadingNode)
},
})
}
const directive = {
install(Vue) {
directiveLoading(Vue)
}
}
export default directive
在main.js中去注册使用
js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
import Directive from "@/utils/directive.js";
app.use(Directive)
app.mount('#app')
在页面中去使用
js
<template>
<div class="edit-box" v-zLoading="loading">
<div>内容</div>
</div>
</template>
系统常量配置管理:config文件夹
在项目开发过程会有很多系统常量值,或者数据字典字段以及枚举值这些,因此也需要将这一块的内容单独抽离出来进行管理
项目中的一些常量值 constant.js
js
// 请求TOKEN
export const ACCESS_TOKEN = 'ANT_DESIGN_TOKEN'
// 页面路由
export const commdityCenterUrl = {
Add_Edit_Commodity_Library: '/commoditycenter/commoditylibrary/addeditcommoditylibrary',
Set_Goods_Detail: '/commoditycenter/commoditylibrary/setgoodsdetail'
}
// 用户类型枚举值
export const userTypeMap ={
0:'家长',
1:'老师',
2:'学生',
3:'管理员'
}
// 本地缓存key
export const MODEL_PRIVEW_CONFIG = 'MODEL_PRIVEW_CONFIG'
数据状态共享管理 store 文件夹
在项目开发过程中数据状态共享永远是无法避免的一个话题,在传统的vue2项目中是用Vuex去实现的
而在Vue3项目中作者更推荐使用Pinia
为什么要使用Pinia?
这个链接是关于pinia和Vuex的比较说明pinia.web3doc.top/introductio...
作者对于pinia的看法则是:pinia能够实现Vuex所有的功能,且比Vuex更加轻量和灵活
1.新建pinia.js和 loginStore.js
js
// pinia.js
import { createPinia } from 'pinia';
const piniaStore = createPinia();
export default piniaStore
js
// loginStore.js
import { defineStore } from 'pinia'
export const useMeshEditStore = defineStore('useMeshEditStore', {
state: () => ({
//用户信息
userInfo: {},
}),
getters: {
userName: (state) => userInfo.userName
},
actions: {
login() {
},
loginOut() {
},
}
})
export const useIndexedDBStore = defineStore('useIndexedDBStore', {
state: () => ({
db: {}
}),
getters: {
},
actions: {
setDbApi(db) {
this.db = db
}
}
})
在main.js中去注册
js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import piniaStore from './store/pinia'
const app = createApp(App)
app.use(piniaStore)
app.mount('#app')
全局样式管理文件夹 style
为了提高代码的复用性,也需要对项目中的css进行统一管理,定义出一些常用的css样式类
如一些全局样式和sass变量,以及css样式重置
css样式重置 result.scss
css
html {
overflow: auto;
}
body,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
input,
p,
blockquote,
th,
td {
font-weight: 400;
margin: 0;
padding: 0;
}
h1,
h2,
h3,
h4,
h4,
h5 {
margin: 0;
padding: 0;
}
select {
font-size: 12px;
}
table {
border-collapse: collapse;
}
fieldset,
img {
border: 0 none;
}
fieldset {
margin: 0;
padding: 0;
}
fieldset p {
margin: 0;
padding: 0 0 0 8px;
}
legend {
display: none;
}
address,
caption,
em,
strong,
th,
i {
font-style: normal;
font-weight: 400;
}
table caption {
margin-left: -1px;
}
hr {
border-bottom: 1px solid #FFFFFF;
border-top: 1px solid #E4E4E4;
border-width: 1px 0;
clear: both;
height: 2px;
margin: 5px 0;
overflow: hidden;
}
ol,
ul {
list-style-image: none;
list-style-position: outside;
list-style-type: none;
}
caption,
th {
text-align: left;
}
q:before,
q:after,
blockquote:before,
blockquote:after {
content: "";
}
sass常用变量 public.scss
css
/* 页面左右间距 */
$page-row-spacing: 30upx;
$page-color-base: #f8f8f8;
$page-color-light: #f8f6fc;
$base-color: #fa436a;
/* 文字尺寸 */
$font-sm: 24upx;
$font-base: 28upx;
$font-lg: 32upx;
/*文字颜色*/
$font-color-dark: #303133;
$font-color-base: #606266;
$font-color-light: #909399;
$font-color-disabled: #C0C4CC;
$font-color-spec: #4399fc;
/* 边框颜色 */
$border-color-dark: #DCDFE6;
$border-color-base: #E4E7ED;
$border-color-light: #EBEEF5;
/* 图片加载中颜色 */
$image-bg-color: #eee;
页面的总体部件模型 layouts 文件夹
在传统管理系统项目中页面部件都是采用左侧菜单+头部header 和中间内容区组成的,在这种布局模式左侧菜单和头部header都是固定不变的只有中间的内容区是根据当前路由地址动态变化。
但在一些特殊的页面中又不需要显示左侧的菜单和头部的header
因此我们需要我们需要针对项目中的不同布局模式进行layout统一处理
新建BasicLayouts.vue 用于展示 左侧菜单+头部header+中间内容区的布局模型
html
<template>
<div class="layout-containter">
<div class="left-menu">左侧菜单</div>
<div class="right-content">
<div class="header">头部</div>
<div class="content">内容区
<router-view></router-view>
</div>
</div>
</div>
</template>
新建RouteView.vue 用于展示其子路由内容页面
html
<template>
<router-view> </router-view>
</template>
在 router.js文件中根据不同的情况去使用对于的布局模式
js
import { BasicLayouts, RouteView } from '@/layouts'
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
name: 'layout',
path: '/',
component:BasicLayouts,
children: [
{
path: '/',
name: 'modelEdit',
component: () => import('@/views/modelEdit/index.vue')
}
]
},
{
path: '/loginview',
component: RouteView,
redirect: '/loginview/login',
children: [
{
path: '/loginview/login',
name: 'login',
component: () => import('@/views/Login/Login'),
},
]
},
{
hide: true,
path: '/:pathMatch(.*)*',
component: () => import('@/views/Exception/404'),
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
base: process.env.BASE_URL,
model: 'hash',
routes
})
export default router
全局拦截 permission.js
一般来讲企业级的项目开发(公司官网除外)基本上都要涉及登录以及未登录全局拦截
让用户只有登录成功的状态下或者是有指定的权限下才可以去某一些页面
作者通常会新建一个 permission.js 文件对这些内容进行处理
如下是一个后管理系统的全局权限拦截
大致流程是:
1.首先判断当前浏览器是否有TOKEN
2.如果有TOKEN则通过验证
3.如果没有并且当前访问path 不在系统白名单内容则强行让其跳转登录页面
4.TOKEN验证通过后则可访问当前页面
js
import router from './router'
import VueCookies from 'vue-cookies'
import { ACCESS_TOKEN } from '@/config/constant'
import NProgress from 'nprogress' // progress bar
import '@/components/NProgress/nprogress.less' // progress bar custom style
NProgress.configure({ showSpinner: false })
const whiteList = ['login', 'NoRole'] //免登录白名单
router.beforeEach(async (to, from, next) => {
NProgress.start()
//判断是否登录
if (VueCookies.get(ACCESS_TOKEN)) {
if (to.path == '/loginview/login') {
next({ path: from.path || '/loginview/login' })
} else {
try {
router.replace({ ...to, replace: true })
} catch {
next({ path: '/loginview/login' })
}
}
} else {
//免登录菜单
if (whiteList.includes(to.name)) {
next()
} else {
next({ path: '/loginview/login', query: { redirect: to.fullPath } })
}
}
})
router.afterEach((to) => {
NProgress.done()
})
配置不同的环境.env文件
在实际的项目开发过程中,一般都会有3套数据环境既:本地环境(development ),测试环境(test ),生产环境(product)
1.新建本地环境env文件 .env.development
js
NODE_ENV=development
# 服务端接口 url
VUE_APP_API_BASE_URL=http://192.168.3.18:99/
#scoket 连接地址url
VUE_APP_WEBSCOKET_URL=ws://192.168.3.18:8080/ws
2.新建测试环境env文件 .env.test
js
NODE_ENV=test
# 服务端接口 url
VUE_APP_API_BASE_URL=http://dev.duobihouse.cn:9000/
#scoket 连接地址url
VUE_APP_WEBSCOKET_URL=ws://192.168.3.18:8080/ws
4.新建生产环境env文件 .env.product
js
NODE_ENV=product
# 服务端接口 url
VUE_APP_API_BASE_URL=http://www.duobihouse.cn/
#scoket 连接地址url
VUE_APP_WEBSCOKET_URL=ws://192.168.3.18:8080/ws
5.package.json 文件新增对应的启动项命令和打包项命令
js
"serve": "vue-cli-service serve",
"serve:test": "vue-cli-service serve --mode test",
"serve:product": "vue-cli-service serve --mode product",
"build": "vue-cli-service build",
"build:test:": "vue-cli-service --mode test build",
"build:product": "vue-cli-service --mode product build",
项目结构截图
结语
在实际工作开发过程中我们难免会接触到新的项目开发,一个好的项目项目框架搭建不仅能有效的提高项目的扩展性,也更能加深个人对于项目总体熟悉情况
虽然github上面已经有了很多成熟的前端项目框架模板,或者有之前老项目的框架模板。但作者本人更推荐大家自己手动去搭建一个项目框架
因为在搭建的过程中也能让你对前端技术有新的领悟