Vue项目搭建与实战:从vue-cli到vue-admin-template完整指南
手把手教你搭建Vue项目,集成后台管理框架,实现客户管理模块
一、为什么需要vue-cli?
1.1 Webpack打包工具
Webpack是一个现代JavaScript应用程序的静态模块打包器。它能够将js、image、css等资源视为模块,递归构建依赖关系图,最终打包成一个或多个bundle。
为什么需要打包?
- ✅ 将碎片文件整合,减少HTTP请求,提升性能
- ✅ 编译ES6等高阶语法,兼容老版本浏览器
- ✅ 代码混淆压缩,提高安全性
入口文件 main.js
Webpack
模块解析
CSS
图片
字体
bundle.js
1.2 vue-cli简介
vue-cli 是Vue官方提供的脚手架工具,能够快速搭建标准化的Vue工程模板,内置了Webpack配置、开发服务器、热更新等开箱即用的功能。
二、搭建第一个Vue项目
2.1 安装vue-cli
bash
# 全局安装最新版vue-cli
npm install -g @vue/cli
# 检查版本
vue --version
💡 如果网络慢,可以配置淘宝镜像:
bashnpm config set registry https://registry.npmmirror.com
2.2 创建项目
bash
# 使用vue-cli 3/4 创建项目
vue create vue-demo
# 或使用vue-cli 2.x 风格(适用于老项目)
npm install -g @vue/cli-init
vue init webpack vue-demo
创建过程中可手动选择特性:Babel、Router、Vuex、CSS预处理器等。
2.3 项目结构解析
vue-demo/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 图片、字体等
│ ├── components/ # 公共组件
│ ├── views/ # 页面级组件
│ ├── router/ # 路由配置
│ ├── store/ # Vuex状态管理
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── package.json # 项目依赖
└── vue.config.js # 自定义webpack配置(需手动创建)
package.json关键脚本:
json
"scripts": {
"serve": "vue-cli-service serve", // 启动开发服务器
"build": "vue-cli-service build", // 打包生产环境
"lint": "vue-cli-service lint" // 代码检查
}
2.4 启动与打包
bash
# 启动开发服务器(默认 http://localhost:8080)
npm run serve
# 打包生产代码(生成dist目录)
npm run build
打包后的dist目录可直接部署到Nginx/Apache等静态服务器。
三、更强大的后台框架:vue-admin-template
手动从零搭建后台管理系统工作量巨大,我们可以直接使用开源的vue-admin-template(基于Vue + Element UI)。
3.1 下载与安装
bash
# 克隆项目
git clone https://github.com/PanJiaChen/vue-admin-template.git
# 进入目录
cd vue-admin-template
# 安装依赖(推荐使用npm,避免cnpm诡异bug)
npm install --registry=https://registry.npmmirror.com
# 启动项目
npm run dev
3.2 项目核心文件说明
App.vue ------ 根组件
包含<router-view>,所有页面路由渲染于此。
main.js ------ 入口文件
javascript
import Vue from 'vue'
import ElementUI from 'element-ui'
import locale from 'element-ui/lib/locale/lang/zh-CN' // 切换中文
import router from './router'
import store from './store'
Vue.use(ElementUI, { locale })
new Vue({ el: '#app', router, store, render: h => h(App) })
路由配置(router/index.js)
javascript
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/layout'
Vue.use(Router)
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true // 不在侧边栏菜单显示
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: '首页', icon: 'dashboard' }
}]
},
{
path: '/system',
component: Layout,
redirect: '/system/user',
name: 'System',
meta: { title: '系统管理', icon: 'el-icon-s-help' },
children: [
{
path: 'admin',
name: 'Admin',
component: () => import('@/views/table/index'),
meta: { title: '管理员管理', icon: 'table' }
},
{
path: 'role',
name: 'Role',
component: () => import('@/views/tree/index'),
meta: { title: '角色管理', icon: 'tree' }
}
]
}
]
const router = new Router({ mode: 'hash', routes: constantRoutes })
export default router
3.3 登录流程与路由守卫
项目通过路由守卫实现登录拦截,流程如下:
有
是
否
有
无
成功
失败
无
是
否
用户访问任何页面
router.beforeEach
是否有token?
是否在登录页?
重定向到首页
是否有用户信息?
放行 next
调用getInfo接口获取用户信息
清除token并跳转登录
是否在白名单?
跳转登录页 login?redirect=原路径
核心代码位于 src/permission.js:
javascript
router.beforeEach(async (to, from, next) => {
NProgress.start()
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
await store.dispatch('user/getInfo')
next()
} catch (error) {
await store.dispatch('user/resetToken')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
3.4 跨域代理配置
由于前后端分离,前端开发时需配置代理解决跨域问题。在项目根目录创建 vue.config.js:
javascript
module.exports = {
devServer: {
port: 9528,
open: true,
proxy: {
'/api': {
target: 'http://localhost:80/ssm', // 后端服务器地址
changeOrigin: true,
pathRewrite: { '^/api': '' } // 将/api前缀重写为空
}
}
}
}
原理说明:
- 前端请求
/api/user/login→ 代理转发到http://localhost:80/ssm/user/login - 浏览器无跨域困扰,因为请求的是同源前端服务器
3.5 请求封装与拦截器
项目在 src/utils/request.js 中封装了axios,并添加了请求/响应拦截器。
javascript
import axios from 'axios'
import { getToken } from '@/utils/auth'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
// 请求拦截器:携带token
service.interceptors.request.use(config => {
if (getToken()) {
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
})
// 响应拦截器:统一处理错误
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 20000) {
Message({ message: res.message || 'Error', type: 'error' })
return Promise.reject(new Error(res.message))
}
return res
},
error => {
console.log('err' + error)
Message({ message: error.message, type: 'error' })
return Promise.reject(error)
}
)
四、实战:客户管理模块
下面我们通过一个完整的客户管理模块(增删改查+分页+条件查询)来串联所学知识。
4.1 后端接口设计(SpringBoot + MyBatis)
为方便理解,给出后端关键代码:
实体类 Customer
java
@Data
public class Customer {
private Integer id;
private String name;
private String address;
private String phone;
private Date createTime;
private Integer status; // 0禁用 1正常
}
查询DTO(含分页)
java
@Data
public class CustomerQueryDto extends Page {
private String name;
private String phone;
}
Mapper接口与XML
java
public interface CustomerMapper {
List<Customer> query(CustomerQueryDto dto);
}
xml
<select id="query" resultType="com.gxa.entity.Customer">
SELECT * FROM t_customer
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="phone != null and phone != ''">
AND phone = #{phone}
</if>
</where>
</select>
Service实现分页
java
@Service
public class CustomerServiceImpl implements CustomerService {
@Resource
private CustomerMapper customerMapper;
public PageInfo<Customer> query(CustomerQueryDto dto) {
PageHelper.startPage(dto.getPage(), dto.getLimit());
List<Customer> list = customerMapper.query(dto);
return new PageInfo<>(list);
}
}
Controller接口
java
@PostMapping("/list")
public Result list(@RequestBody CustomerQueryDto dto) {
PageInfo<Customer> pageInfo = customerService.query(dto);
return ResultUtils.buildSuccess(pageInfo.getList(), pageInfo.getTotal());
}
4.2 前端实现
4.2.1 API接口封装(src/api/customer.js)
javascript
import request from '@/utils/request'
const api_name = '/customer'
export default {
// 分页+条件查询
search(searchMap) {
return request({
url: `${api_name}/list`,
method: 'post',
data: searchMap
})
},
// 根据id查询
findById(id) {
return request({
url: `${api_name}/findById`,
method: 'get',
params: { id }
})
},
// 新增或修改
save(pojo) {
return request({
url: `${api_name}/save`,
method: 'post',
data: pojo
})
},
// 删除
deleteById(id) {
return request({
url: `${api_name}/delete`,
method: 'get',
params: { id }
})
}
}
4.2.2 客户列表页面(views/customer/index.vue)
vue
<template>
<div class="app-main">
<!-- 查询表单 -->
<el-form :inline="true">
<el-form-item label="客户名称">
<el-input v-model="searchMap.name" placeholder="请输入"></el-input>
</el-form-item>
<el-form-item label="客户电话">
<el-input v-model="searchMap.phone" placeholder="请输入"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData">查询</el-button>
<el-button type="primary" @click="handleEdit('')">新增</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="list" border stripe>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
<el-table-column prop="phone" label="电话"></el-table-column>
<el-table-column prop="createTime" label="创建时间" :formatter="dateFormat" width="180"></el-table-column>
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.status === 1">正常</el-tag>
<el-tag type="warning" v-else>禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button type="text" @click="handleEdit(scope.row.id)">修改</el-button>
<el-button type="text" style="color: #F56C6C;" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="searchMap.page"
:page-sizes="[5,10,20,30]"
:page-size="searchMap.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px">
<el-form :model="pojo" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="姓名">
<el-input v-model="pojo.name"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电话">
<el-input v-model="pojo.phone"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="地址">
<el-input v-model="pojo.address"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="pojo.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="handleSave">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import customerApi from '@/api/customer'
import moment from 'moment'
export default {
data() {
return {
list: [],
total: 0,
searchMap: {
page: 1,
limit: 10,
name: '',
phone: ''
},
pojo: {
id: '',
name: '',
phone: '',
address: '',
status: 1
},
dialogVisible: false,
dialogTitle: ''
}
},
created() {
this.fetchData()
},
methods: {
// 获取列表数据
fetchData() {
customerApi.search(this.searchMap).then(response => {
this.list = response.data
this.total = response.count
})
},
// 日期格式化
dateFormat(row, column, cellValue) {
return cellValue ? moment(cellValue).format('YYYY-MM-DD HH:mm:ss') : ''
},
// 分页:每页条数变化
handleSizeChange(val) {
this.searchMap.limit = val
this.fetchData()
},
// 分页:页码变化
handleCurrentChange(val) {
this.searchMap.page = val
this.fetchData()
},
// 打开新增/编辑弹窗
handleEdit(id) {
this.dialogVisible = true
if (id) {
this.dialogTitle = '编辑客户'
customerApi.findById(id).then(res => {
this.pojo = res.data
})
} else {
this.dialogTitle = '新增客户'
this.pojo = { id: '', name: '', phone: '', address: '', status: 1 }
}
},
// 保存(新增或修改)
handleSave() {
customerApi.save(this.pojo).then(response => {
this.$message.success(response.msg || '保存成功')
this.dialogVisible = false
this.fetchData() // 刷新列表
})
},
// 删除
handleDelete(id) {
this.$confirm('确定删除该客户吗?', '提示', { type: 'warning' }).then(() => {
customerApi.deleteById(id).then(res => {
this.$message.success('删除成功')
this.fetchData()
})
}).catch(() => {})
}
}
}
</script>
<style scoped>
.app-main {
padding: 20px;
}
.el-pagination {
text-align: center;
margin-top: 20px;
}
</style>
4.3 添加moment.js格式化日期
bash
npm install moment --save
4.4 国际化切换为中文
修改 main.js 中的element-ui语言配置:
javascript
import locale from 'element-ui/lib/locale/lang/zh-CN' // 英文版为'en'
Vue.use(ElementUI, { locale })
五、总结
通过本文,你已经学会:
| 知识点 | 内容 |
|---|---|
| vue-cli | 脚手架安装、项目创建、启动与打包 |
| vue-admin-template | 目录结构、路由配置、登录流程、拦截器 |
| 跨域代理 | vue.config.js 配置devServer.proxy |
| 客户管理CRUD | 分页查询、条件筛选、新增、修改、删除 |
| Element UI | 表格、表单、弹窗、分页、消息提示 |
后续学习方向:
- 掌握Vuex状态管理
- 学习更多Element UI高级组件
- 研究路由权限控制(动态添加路由)
- 使用TypeScript重构项目