最近在做一类广告配置后台页面。它不像普通的内容展示页,页面上通常同时存在筛选条件、投放列表、素材信息、时间区间、上下线操作以及编辑表单。
如果用零散的 DOM 操作处理,页面状态很快会变得难以追踪:筛选以后列表是否重新加载,编辑完成以后哪一行需要更新,时间字段如何校验,重复点击保存怎样避免二次提交。
这类后台页面比较适合用 AngularJS 来组织。数据变化驱动视图更新,控制器处理页面行为,服务封装请求,指令处理重复出现的交互组件。
本文使用的是 AngularJS 1.x,下文简称 AngularJS。
一个广告配置页面包含哪些状态
先不急着写代码,可以先把页面中需要管理的信息分出来:
页面状态 -> 查询条件:广告位、投放状态、关键词、时间范围 -> 列表数据:当前页、总数、加载状态 -> 编辑数据:素材、链接、开始时间、结束时间 -> 操作状态:保存中、上下线中、错误提示
如果这些数据全部散落在点击回调中,任何需求变化都会影响多个位置。AngularJS 的价值之一,就是把页面展示所依赖的状态集中放在一个清晰的数据模型中。
使用 Controller 组织页面行为
列表页可以由一个控制器负责查询条件和交互入口:
<div ng-controller="AdListController as vm"> <form class="filter-bar" ng-submit="vm.search()"> <input type="text" ng-model="vm.query.keyword" placeholder="广告名称或素材关键词"> <select ng-model="vm.query.status"> <option value="">全部状态</option> <option value="online">投放中</option> <option value="offline">已下线</option> </select> <button type="submit">查询</button> </form> <p ng-if="vm.loading">加载中...</p> <p ng-if="!vm.loading && vm.items.length === 0">暂无广告配置</p> <table ng-if="!vm.loading && vm.items.length > 0"> <tr ng-repeat="item in vm.items track by item.id"> <td>{{ item.name }}</td> <td>{{ item.positionName }}</td> <td>{{ item.startTime }} - {{ item.endTime }}</td> <td>{{ item.statusText }}</td> <td> <button ng-click="vm.edit(item)">编辑</button> <button ng-click="vm.toggleStatus(item)"> {{ item.status === 'online' ? '下线' : '上线' }} </button> </td> </tr> </table></div>
对应的控制器只维护与页面交互相关的状态:
angular.module('adConsole').controller('AdListController', function (AdService) { var vm = this; vm.query = { keyword: '', status: '', page: 1, pageSize: 20 }; vm.items = []; vm.loading = false; vm.search = function () { vm.query.page = 1; vm.load(); }; vm.load = function () { vm.loading = true; AdService.query(vm.query).then(function (result) { vm.items = result.list; vm.total = result.total; }).finally(function () { vm.loading = false; }); }; vm.load();});
使用 controller as 的写法以后,模板中能够明确看出数据属于 vm,比在大页面里不断向 $scope 增加字段更容易维护。
请求逻辑放入 Service
后台页面往往有多个操作:查询列表、读取详情、保存编辑、上线、下线。请求地址和数据格式如果全部写在控制器中,控制器很快会变成接口与交互混在一起的大文件。
可以通过服务将请求能力集中封装:
angular.module('adConsole').factory('AdService', function ($http) { return { query: function (params) { return $http.get('/api/ad/list', { params: params }).then(function (response) { return response.data; }); }, detail: function (id) { return $http.get('/api/ad/detail', { params: { id: id } }).then(function (response) { return response.data; }); }, save: function (formData) { return $http.post('/api/ad/save', formData).then(function (response) { return response.data; }); }, toggleStatus: function (id, status) { return $http.post('/api/ad/status', { id: id, status: status }).then(function (response) { return response.data; }); } };});
控制器关心的是“用户发起了什么操作,以及页面如何反馈”,服务则关心“如何和接口交互”。当接口路径或返回结构变化时,不需要把整个页面翻一遍。
编辑表单要把校验放在提交之前
广告配置里,时间和素材是比较容易出错的部分。例如结束时间必须晚于开始时间,跳转地址不能为空,素材尺寸必须符合广告位要求。
AngularJS 表单可以先处理基础必填校验:
<form name="adForm" ng-submit="vm.save(adForm)" novalidate> <label> 广告名称 <input name="name" ng-model="vm.form.name" required> </label> <span ng-if="adForm.name.$touched && adForm.name.$error.required"> 请输入广告名称 </span> <label> 跳转地址 <input name="link" ng-model="vm.form.link" required> </label> <label> 开始时间 <input ng-model="vm.form.startTime" required> </label> <label> 结束时间 <input ng-model="vm.form.endTime" required> </label> <button type="submit" ng-disabled="vm.saving"> {{ vm.saving ? '保存中...' : '保存' }} </button></form>
需要结合业务判断的规则,则在提交入口中统一处理:
vm.save = function (form) { if (form.$invalid) { return; } if (vm.form.endTime <= vm.form.startTime) { vm.errorMessage = '结束时间需要晚于开始时间'; return; } vm.saving = true; AdService.save(vm.form).then(function () { vm.closeEditor(); vm.load(); }).finally(function () { vm.saving = false; });};
保存按钮在请求完成前禁用,可以避免用户连续提交产生重复配置。
重复交互适合封装为 Directive
广告后台中常见的一类重复控件是状态标签、图片预览和时间范围选择。如果每个页面各自处理 DOM 与事件,交互细节会越来越不一致。
例如,一个素材预览指令可以封装图片展示和加载失败处理:
angular.module('adConsole').directive('adPreview', function () { return { restrict: 'E', scope: { src: '=', width: '=', height: '=' }, template: '<div class="ad-preview">' + '<img ng-src="{{ src }}" alt="广告素材">' + '<p>{{ width }} x {{ height }}</p>' + '</div>' };});
页面使用时只传入数据:
<ad-preview src="vm.form.imageUrl" width="vm.form.imageWidth" height="vm.form.imageHeight"></ad-preview>
指令不应该承担保存广告或者请求列表这样的业务动作,它更适合封装可复用的展示和输入交互。
列表操作需要保留稳定反馈
后台中的上线、下线操作通常会直接影响当前列表行。操作成功后可以局部更新状态,也可以重新请求列表。
如果状态更新会影响排序、筛选结果或者多个字段,重新加载当前查询条件往往更稳妥:
vm.toggleStatus = function (item) { var nextStatus = item.status === 'online' ? 'offline' : 'online'; item.operating = true; AdService.toggleStatus(item.id, nextStatus).then(function () { vm.load(); }).finally(function () { item.operating = false; });};
如果只是非常简单的字段变更,可以直接修改当前行,减少一次列表请求。这里不一定有唯一正确的答案,关键是页面展示不能与服务端最终状态产生长时间偏差。
小结
AngularJS 很适合这类状态密集的管理后台页面:
Controller组织筛选、编辑和列表操作。Service统一接口访问与数据转换。- 表单能力处理基础校验与提交状态。
Directive封装重复交互组件。
后台页面并不追求复杂动画,但它非常在意信息是否清楚、操作是否可预期、状态是否一致。把这些职责拆开以后,广告配置需求继续增长时,页面也不会很快陷入一堆难以整理的事件回调中。