前端进击笔记 - 腾讯高级前端工程师 - 拉勾教育

前面几讲我介绍了前端应用开发中常见的工具原理,包括前端框架、前端路由等,还介绍了前端编程的一些开发技巧,比如使用数据驱动、组件化和模块化设计、数据抽象等。这些内容到底该怎么在实际工作中使用呢?

今天,我会以最常见的管理端系统为例,带你用这些编程技巧来提升开发效率,三天实现一个管理端系统。

搭建一个管理端系统包括以下工作:

  1. 管理端路由与功能设计;
  2. 页面功能设计与实现。

由于技术选型并不是本节内容的重点,这里我将直接选择入门简单的 Vue、配套 Vue 热门 UI 框架,使用 Vue CLI + Vue + ElementUI + vue-router 来搭建管理端。关于技术选型的内容,你可以关注后续第 22 讲的内容。

本文中我不会花过多的篇幅去描述 Vue 的一些基本概念和工具,包括 Vue 组件、vue-router、ElementUI 组件等,这些内容在官方网站上有很详细的介绍,因此我们专注于介绍搭建思路。

下面,我们先来设计管理端功能吧!

管理端路由与功能设计

常见的管理端系统长这个样子:

16 | 实战:三天实现管理端系统 - 图1

一般来说,管理端系统包括这些功能页面结构。

16 | 实战:三天实现管理端系统 - 图2

我们先来设计管理端左侧的菜单,将 2-4 的页面结构都整合进去:

16 | 实战:三天实现管理端系统 - 图3

页面路由设计

菜单内容以及对应的页面结构都确定之后,我们可以先来设计管理端的路由。这时,我们需要将应用进行适当的模块化和组件化拆分。

为了更直观地进行说明,我给你梳理了页面路由和页面组件嵌套的关系图(使用框框框起来的代表一个 Vue 组件):

16 | 实战:三天实现管理端系统 - 图4

将页面和路由的关系梳理清楚,是很关键的一个步骤。

根据上述的页面路由和嵌套关系,下面我们以/作为根路由,进行路由和页面组件的设计。

16 | 实战:三天实现管理端系统 - 图5

应用的各个页面和路由嵌套关系梳理完成之后,我们可以对项目文件结构进行划分。

目录结构划分

项目设计的时候,我们需要初步约定项目代码的组织结构。

对于前端项目来说,项目根目录会区分开发代码(src)、编译后代码(dist)和常见的配置(Babel 配置、Eslint 配置、项目配置等)。对于开发代码(src),也会划分为静态资源(assets)、页面(page)、组件库(component)、公共库(util)等。如下述目录结构所示:

  1. ├─dist
  2. ├─src
  3. ├─assets
  4. ├─less
  5. ├─img
  6. ├─components
  7. ├─pages
  8. ├─utils
  9. ├─App.vue
  10. ├─main.js
  11. ├─babel.config.js
  12. ├─vue.config.js
  13. ├─package.json
  14. ├─README.md

项目的目录结构是否有规范约束、是否结构清晰,对一个项目的可维护性非常重要。通过目录结构,我们可以直观地看到项目中包括了哪些代码、分别都放在哪里。

好了,项目目录和路由结构我们划分好了,我们来看看怎么根据上面的设计来配置路由,以及实现相互跳转。

路由配置与开发

Vue 框架本身的定位是核心关注视图层,所以路由配置、状态管理、测试工具等都不是自带的,我们需要自己找到对应的开源库配合使用。Vue 官方推荐的工具是vue-router

根据以上的嵌套关系,我们可以设置最外层的根路由为"/",并加上其他嵌套子路由配置,比如登录页面、列表页和详情页等。

  1. const routes = [
  2. {
  3. path: "/",
  4. component: App,
  5. name: "App",
  6. children: [
  7. {
  8. path: "login",
  9. component: Login,
  10. name: "Login",
  11. },
  12. {
  13. path: "home",
  14. component: Home,
  15. name: "Home",
  16. children: [
  17. { path: "service", component: Service, name: "Service" },
  18. {
  19. path: "product",
  20. component: Product,
  21. name: "Product",
  22. children: [
  23. { path: "list", component: ProductList, name: "ProductList" },
  24. { path: "add/0", component: ProductEdit, name: "ProductAdd" },
  25. { path: "edit/:id", component: ProductEdit, name: "ProductEdit" },
  26. ],
  27. },
  28. ],
  29. },
  30. ],
  31. },
  32. ];

上述的路由配置中涵盖了页面的路由嵌套关系,在 Vue 中可以使用<router-view>组件,将渲染路径匹配到具体的视图组件,视图组件还可以内嵌自己的<router-view>匹配子路由路径,从而渲染嵌套组件。

比如,Home.vue页面中包括左侧菜单和右侧内容,在右侧内容中可以使用<router-view>来嵌套子路由的组件进行渲染。

  1. <!-- 这里是 /app/home 路由的组件,Home.vue -->
  2. <template>
  3. <el-container>
  4. <!-- 左侧菜单, Menu.vue -->
  5. <menu></menu>
  6. <!-- 右侧内容 -->
  7. <el-container>
  8. <!-- 上边的头部栏 -->
  9. <el-header></el-header>
  10. <!-- 子路由页面的内容 -->
  11. <router-view></router-view>
  12. </el-container>
  13. </el-container>
  14. </template>

其中,在左侧的在菜单<menu></menu>中,可以使用 Vue 中的<router-link>组件,来绑定路由的跳转能力,进行页面的导航。

路由的配置完成后,我们可以将路由的能力添加到应用中。

在第 12 讲中,我们介绍了前端路由库都会支持两种路由模式:Hash 和 History。由于 History 模式需要后台配合,因此这里使用 Hash 模式来加载路由。

在 Vue 中可以通过将router配置参数注入路由,给应用添加上路由功能,比如<router-link tag="div" :to="{name: 路由名字}"></router-link>

  1. const router = new VueRouter({
  2. routes,
  3. });
  4. new Vue({
  5. el: "#app",
  6. router,
  7. render: (h) => h(App),
  8. });

通过新建VueRouter,并在Vue中传入该路由示例,便可以给应用添加路由能力,这就是 vue-router 的基本功能。

除此之外,vue-router 还提供了路由监控能力、鉴权能力等,可以结合实现非登录页的登录状态鉴权,比如使用 vue-router 的router.beforeEach导航守卫能力,当用户未登录时,则拒绝进入其他路由页面里:

  1. router.beforeEach((to, from, next) => {
  2. if (to.name !== "Login") {
  3. if (!isUserLogin) {
  4. next({ name: "Login" });
  5. }
  6. }
  7. next();
  8. });

这样,我们就可以在用户未登录时,拦截所有通往内页的操作,并重新定向到登录页面。

到这里,我们应用基本具备了登录、导航的能力。当然,这只是个静态页面,距离真正上线,我们还需要进行接口的联调、代码打包、发布上线等工作。

管理端路由与功能设计过程中,我们分别使用了前端框架、前端路由以及组件化和模块化的设计。

下面我们会结合数据抽象与数据驱动的方式,来进行页面内功能的具体设计与实现。

页面功能设计与实现

前面我们已经将管理端划分成多个模块和页面,接下来我会对页面进行组件的拆分,结合数据抽象的方式进行组件的设计。

管理端页面的主要包括左侧菜单、列表和表单,我们可以按照这样的结构进行组件设计。

下面我们先来分别对它们进行数据抽象和设计。

菜单设计

菜单的最终效果如图,这里会包括父菜单和子菜单两层结构。

16 | 实战:三天实现管理端系统 - 图6

其中,每个父菜单需要展示以下的内容:

  • 图标icon
  • 菜单名字text
  • (可选)子菜单列表subMenus,以及子菜单名字text

列表可以用数组来表示,因此我们可以将菜单组件的数据抽象为以下的数据结构(数组 + 对象):

  1. const menus = [
  2. {
  3. text: "服务管理",
  4. icon: "el-icon-setting",
  5. subMenus: [{ text: "服务信息" }, { text: "新增" }],
  6. },
  7. {
  8. text: "产品管理",
  9. icon: "el-icon-menu",
  10. subMenus: [{ text: "产品信息" }],
  11. },
  12. {
  13. text: "日志信息",
  14. icon: "el-icon-message",
  15. },
  16. ];

当我们将菜单用这样的数据结构表示以后,实现 UI 的时候就可以轻松地通过数据绑定的方式来进行。此处使用了 Elmenet 的菜单组件<el-menu><el-submenu><el-menu-item>如下所示:

  1. <!-- 顺便调整了下颜色 -->
  2. <el-menu
  3. :default-openeds="['0', '1']"
  4. class="el-menu-vertical-demo"
  5. background-color="#545c64"
  6. text-color="#fff"
  7. active-text-color="#ffd04b"
  8. >
  9. <!-- 遍历生成父菜单选项 -->
  10. <template v-for="menu in menus">
  11. <!-- 有子菜单的时候,就用 el-submenu,再绑个序号 index -->
  12. <el-submenu
  13. v-if="menu.subMenus && menu.subMenus.length"
  14. :index="menu.index"
  15. :key="menu.index"
  16. >
  17. <template slot="title">
  18. <!-- 绑个父菜单的 icon -->
  19. <i :class="menu.icon"></i>
  20. <!-- 再绑个父菜单的名称 text -->
  21. <!-- slot 其实类似于占位符,可以去 Vue 官方文档了解一下插槽 -->
  22. <span slot="title">{{menu.text}}</span>
  23. </template>
  24. <el-menu-item-group>
  25. <!-- 子菜单也要遍历,同时绑上子菜单名称 text,也要绑个序号 index -->
  26. <el-menu-item
  27. v-for="subMenu in menu.subMenus"
  28. :key="subMenu.index"
  29. :index="subMenu.index"
  30. >{{subMenu.text}}</el-menu-item
  31. >
  32. </el-menu-item-group>
  33. </el-submenu>
  34. <!-- 没子菜单的时候,就用 el-menu-item,也要绑个序号 index -->
  35. <el-menu-item v-else :index="menu.index" :key="menu.index">
  36. <!-- 绑个父菜单的 icon -->
  37. <i :class="menu.icon"></i>
  38. <!-- 再绑个父菜单的名称 text -->
  39. <span slot="title">{{menu.text}}</span>
  40. </el-menu-item>
  41. </template>
  42. </el-menu>

如果需要绑定路由,我们还可以添加上一个路由的绑定信息。

前面我们说过 vue-router 中可以使用<router-link>组件来实现路由跳转,你可以试试看要怎么做,文末会有源码地址和最终页面效果参考哦。

下面我们来看看列表和表单的设计。

列表和表单设计

我们看看列表的最终效果:

16 | 实战:三天实现管理端系统 - 图7

我们能看到,列表里每行内容包括这些信息:

  • 日期:date
  • 姓名:name
  • 电话:phone
  • 地址:address

在列表中的每一个数据项,还需要使用一个唯一标识来作为标记(id),便于用户增删查改时进行标识。

我们同样可以使用对象的方式来描述列表中的数据项:

  1. const tableItem = {
  2. id: 123,
  3. date: "2019-05-20",
  4. name: "被删",
  5. phone: "13888888888",
  6. address: "深圳市南山区滨海大道 888 号",
  7. };

那么列表则是由以上对象结构的数据组成的数组,我们可以使用 Element-Table 组件来绑定列表的 UI 展示。

  1. <!-- data 里绑定表格数据,同时这里调整了下样式 -->
  2. <el-table
  3. stripe
  4. :data="tableData"
  5. style="border: 1px solid #ebebeb;border-radius: 3px;margin-top: 10px;"
  6. >
  7. <!-- prop 传绑定 tableData 的数据 id,表头名称 id,同时设了下宽度 -->
  8. <el-table-column prop="id" label="id" width="100"></el-table-column>
  9. <!-- prop 传绑定 tableData 的数据 date,表头名称日期 -->
  10. <el-table-column prop="date" label="日期" width="200"></el-table-column>
  11. <!-- prop 传绑定 tableData 的数据 name,表头名称姓名 -->
  12. <el-table-column prop="name" label="姓名" width="200"></el-table-column>
  13. <!-- prop 传绑定 tableData 的数据 phone,表头名称电话 -->
  14. <el-table-column prop="phone" label="电话" width="200"></el-table-column>
  15. <!-- prop 传绑定 tableData 的数据 address,表头名称地址 -->
  16. <el-table-column prop="address" label="地址"></el-table-column>
  17. <!-- 该列固定在右侧,表头名称操作 -->
  18. <el-table-column fixed="right" label="操作" width="300">
  19. <template slot-scope="scope">
  20. <!-- 添加了个删除按钮,绑定了前面定义的删除事件 deleteTableItem,传入参数 id -->
  21. <el-button
  22. @click="deleteTableItem(scope.row.id)"
  23. type="danger"
  24. size="small"
  25. >删除</el-button
  26. >
  27. <!-- 分别添加了上移和下移按钮,绑定了前面定义的移动事件 moveTableItem,传入参数 id 和移动方向 -->
  28. <el-button @click="moveTableItem(scope.row.id, 'up')" size="small"
  29. >上移</el-button
  30. >
  31. <el-button @click="moveTableItem(scope.row.id, 'down')" size="small"
  32. >下移</el-button
  33. >
  34. </template>
  35. </el-table-column>
  36. </el-table>

可以看到,列表中支持了选项的删除、上下移动操作,当我们将页面抽象为数据之后,页面的功能可以对应于数据的如下操作。

  • 删除:删除数组中的某个对象
  • 位置移动:改变数组中对象的排序

这些操作会改变并更新页面中的数据,使用 Vue 可以直接绑定数据操作的方法:

  1. export default {
  2. data() {
  3. return {
  4. menus: menus,
  5. tableData: tableData,
  6. };
  7. },
  8. methods: {
  9. addTableItem(item = {}) {
  10. this.tableData.push({ ...item, id: this.tableData.length + 1 });
  11. },
  12. deleteTableItem(id) {
  13. const index = this.tableData.findIndex((x) => x.id === id);
  14. this.tableData.splice(index, 1);
  15. },
  16. moveTableItem(id, direction) {
  17. const dataLength = this.tableData.length;
  18. const index = this.tableData.findIndex((x) => x.id === id);
  19. switch (direction) {
  20. case "up":
  21. if (index > 0) {
  22. const item = this.tableData.splice(index, 1)[0];
  23. this.tableData.splice(index - 1, 0, item);
  24. }
  25. break;
  26. case "down":
  27. if (index < dataLength - 1) {
  28. const item = this.tableData.splice(index, 1)[0];
  29. this.tableData.splice(index + 1, 0, item);
  30. }
  31. break;
  32. }
  33. },
  34. },
  35. };

由于使用了 Vue 框架,当我们绑定的数据发生变化的时候,框架会自动帮我们更新到页面里(具体实现可以参考第 10 讲)。

列表常用于数据项的查看,而表单则通常用于对数据项的编辑和修改。对于同一个数据项来说,表单的数据结构与列表中数据项的结构是一致的,同样可以使用上述的对象结构来表达。

使用 Element-Form 组件将表单的数据进行绑定之后,就可以直接进行编辑了:

16 | 实战:三天实现管理端系统 - 图8

到这里,我们已经实现了一个带菜单、列表和表单的页面了,单列表和单表单的页面同样可以通过数据抽象设计 + UI 组件绑定的方式来实现。

这个页面也有挺多可以完善的地方,例如:

  • 左侧菜单可以支持收起;
  • 列表支持修改;
  • 列表支持批量删除;
  • 表单支持校验手机号和其他选项不为空。

这些就当作课后作业来完成,最终的实现可以参考以下链接:

小结

今天我带你使用开源前端框架、前端路由库,通过模块划分、组件设计、数据抽象的方式来快速搭建实现管理端。

虽然文章标题是三天实现管理端,实际上如果熟练之后,这些工作一天就能完成。

或许你会觉得,这管理端也太简单了吧。在实际工作中,大家也都会对管理端系统感到苦恼:管理端多半是增删改查的东西,做多了就会变成重复性的工作。

其实管理端的开发也可以不只是复制粘贴,我们还可以对管理端的主要功能进行抽象,然后通过配置化的方式来配置完成。如果实现了管理端的配置化,我们就可以从重复烦琐的工作中解放出来,去做更多有意思的事情。

在日常工作中,对于前端应用的实现,是否可以使用更好的方式、又可以怎样去在重复的工作中提升自己呢?我认为这些思考才是最重要的,这决定了我们只是一个会用工具的工具人,还是一个可以用工具去改变工作方式的思考者。