Vue项目搭建与实战:从vue-cli到vue-admin-template完整指南

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

💡 如果网络慢,可以配置淘宝镜像:

bash 复制代码
npm 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重构项目
相关推荐
yqcoder2 小时前
前端性能优化基石:深入解析 CSS 雪碧图 (CSS Sprites)
前端·css·性能优化
最后一只小白2 小时前
封装form表单
前端·javascript·vue.js
魔士于安2 小时前
Unity类似博物馆场景
前端·unity·游戏引擎·贴图·模型
喜欢吃鱿鱼2 小时前
vue 数字转千分位js
前端·javascript·vue.js
吴声子夜歌2 小时前
Vue3——组件进阶
前端·javascript·vue.js
鸽芷咕2 小时前
KingbaseES NFS部署实战:环境变量缺失与权限报错排查指南
前端·chrome
Fighting_p2 小时前
【FileShowCom 组件】文件预览:图片预览 el-image,其余文件预览打开新窗口或者下载
开发语言·前端·javascript
Ting.~2 小时前
从 0 到 1 搭建 Vue 项目
vue.js·前端框架
a1117762 小时前
Web3D 在线3D模型骨骼动画编辑器(开源 Reze Studio)
前端·3d·开源·html