一 需求
服务端设计已经准备完毕,各类数据访问接口设计都已经实现,前端的页面模板、Js模板也已就绪,剩下最后的Html设计及Js编写工作,这部分没有太多的新鲜技术,而且我个人对前端技术不是很熟练,所以剩下部分我仅针对部分Bootstrap和服务端接口设计进行介绍。
登录页我们需要提供两个基础功能,登陆和注册,并且通过服务端进行账户验证。这里我偷了个懒,登陆和注册用同一个页面,仅提供两个不同的按钮来做功能上的区分。
用户注册时可以选填一个昵称,用以登陆后做显示用的用户名,登陆时不需要填写。
注册时需要账号格式为手机号码,以后有时间我会再提供一个验证码功能,正确填写验证图片中的字符才能获取手机验证码,并且仅手机验证码通过校验才能注册成功,手机验证码通过第三方短信服务实现。这里用到手机号,各位懂得。
注册时密码格式暂不做太多限制,只要不为空就好,各位可按自己的口味酌情处理。
登陆时要求账户(手机号码)及密码正确,登陆成功后服务端对用户会话状态进行保存,如果是移动端APP登陆的话有效会话时间为30天,主要不主动退出则会话状态在每次登陆应用时更新,这些都是后话了。
二 页头设计
第一部分做一个通用的页头,这里使用Bootstrap提供的一个布局组件PageHeader来实现,如下:
<!-- 页头 -->
<div>
<div class="container">
<div class="row clearfix">
<div class="col-sm-0 col-md-1 col-lg-2"></div>
<div class="col-sm-12 col-md-10 col-lg-8">
<div class="page-header">
<h1>
简字 <small>简单文字,你的心声</small>
</h1>
</div>
</div>
<div class="col-sm-0 col-md-1 col-lg-2"></div>
</div>
</div>
</div>
注意上述代码中的container,因为我们的整体布局是通过Bootstrap的栅格系统实现的,只有将内容置入container内,每行内容才能获得适当的缩放。
另外,clearfix样式是为了消除在小屏幕浏览时网格错乱而设置的,读者可自行调试下不加入clearfix的缩放区别。
最后每一列的宽占比通过col-*-*来实现不同屏幕尺寸设备的分辨率来实现不同的展示效果,这部分知识如果读者不是很清楚可以自行百度。
三 主内容区设计
我习惯把网页除开页头页尾侧边栏等部分,剩余的主体显示区域叫做主内容区。
主内容区是一个简单的表单,三行输入框,两个功能按钮,先上代码:
<!-- 主内容区 -->
<div>
<div class="container">
<!--内容-->
<div class="row clearfix">
<div class="col-sm-0 col-md-1 col-lg-2"></div>
<div class="col-sm-12 col-md-10 col-lg-8">
<form class="form-horizontal">
<div class="form-group">
<input type="tel" class="form-control" id="inputPhone" placeholder="手机号码(必输)">
</div>
<div class="form-group">
<input type="password" class="form-control" id="inputPassword" placeholder="密码(必输)">
</div>
<div class="form-group">
<input type="text" class="form-control" id="inputNickname" placeholder="昵称(选输)">
</div>
<div class="form-group">
<button type="button" class="btn btn-default" onclick="LoginPageMVC.Controller.login()">登录</button>
<button type="button" class="btn btn-default" onclick="LoginPageMVC.Controller.register()">注册</button>
</div>
</form>
</div>
<div class="col-sm-0 col-md-1 col-lg-2"></div>
</div>
</div>
</div>
整个表单我采用Bootstrap的一个css组件form-horizontal,这是一个水平排列的表单,使用很简单,在父元素上加入form-horizontal样式,把表单组件加入一个带有form-group样式的div中即可,所有的表单控件需要指定样式form-control。
另外有一点小细节,每一个input的有一个属性叫placeholder,它会以灰底颜色显示提示信息,账户密码的必输提示我没有通过Js的表单验证来处理,一个是自己懒,一个是服务端做了处理,虽然消耗了服务端资源,但是对于这样一个Demo项目来说无可厚非,读者如果有兴趣可以自己尝试一下表单验证,Jquery也提供了很多这方面的插件。
最后注意一下两个功能按钮的onclick事件,它们直接调用Login.js提供的方法,这部分实现后文介绍。
四 页尾设计
页尾是固定在页面底部的,不会随着屏幕滚动而发生位置的变化,为了实现这样的需求,我使用了<footer class=“navbar-fixed-bottom”>标签,如下:
<!-- 页尾 -->
<footer class="navbar-fixed-bottom">
<div class="container">
<div class="row clearfix">
<div class="col-sm-0 col-md-1 col-lg-2"></div>
<div class="col-sm-12 col-md-10 col-lg-8">
<address contenteditable="true">
<strong>冒泡工作室, 大福楠</strong><br /> <abbr title="Phone">Email:</abbr>
983950935@qq.com
</address>
</div>
<div class="col-sm-0 col-md-1 col-lg-2"></div>
</div>
</div>
</footer>
完整的效果图参考第九部分,完整的html代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>简字-登陆</title>
<link href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="css/your-style.css" rel="stylesheet">
<!-- 以下两个插件用于在IE8以及以下版本浏览器支持HTML5元素和媒体查询,如果不需要用可以移除 -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<!-- 页头 -->
<div>
<div class="container">
<div class="row clearfix">
<div class="col-sm-0 col-md-1 col-lg-2"></div>
<div class="col-sm-12 col-md-10 col-lg-8">
<div class="page-header">
<h1>
简字 <small>简单文字,你的心声</small>
</h1>
</div>
</div>
<div class="col-sm-0 col-md-1 col-lg-2"></div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div>
<div class="container">
<!--内容-->
<div class="row clearfix">
<div class="col-sm-0 col-md-1 col-lg-2"></div>
<div class="col-sm-12 col-md-10 col-lg-8">
<form class="form-horizontal">
<div class="form-group">
<input type="tel" class="form-control" id="inputPhone" placeholder="手机号码(必输)">
</div>
<div class="form-group">
<input type="password" class="form-control" id="inputPassword" placeholder="密码(必输)">
</div>
<div class="form-group">
<input type="text" class="form-control" id="inputNickname" placeholder="昵称(选输)">
</div>
<div class="form-group">
<button type="button" class="btn btn-default" onclick="LoginPageMVC.Controller.login()">登录</button>
<button type="button" class="btn btn-default" onclick="LoginPageMVC.Controller.register()">注册</button>
</div>
</form>
</div>
<div class="col-sm-0 col-md-1 col-lg-2"></div>
</div>
</div>
</div>
<!-- 页尾 -->
<footer class="navbar-fixed-bottom">
<div class="container">
<div class="row clearfix">
<div class="col-sm-0 col-md-1 col-lg-2"></div>
<div class="col-sm-12 col-md-10 col-lg-8">
<address contenteditable="true">
<strong>冒泡工作室, 大福楠</strong><br /> <abbr title="Phone">Email:</abbr>
983950935@qq.com
</address>
</div>
<div class="col-sm-0 col-md-1 col-lg-2"></div>
</div>
</div>
</footer>
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="js/login.js"></script>
</body>
</html>
五 Js文件引用
我在整个html页得最后引入需要使用Bootstrap的js文件,以及登录页功能实现的js,那么为什么在页面的最后引入,这里涉及到一个看似很常见但是没多少人能说的清楚的问题——性能分析。
如果读者有兴趣了解前端性能知识,我贴一个链接大家自己看看,放这里细说不合适:
https://blog.csdn.net/ywb201314/article/details/53170298
六 Login.js的实现
第十部分我贴了自己常写的Js模式,模板以登录页的Js为例,第十部分还没有实现任何逻辑,这部分逐步的将其补充完整。
其实Js并没有什么很特殊的地方,更多的是Ajax的使用,因为页面没有数据是不完整的,而数据处理都是通过Ajax来和服务端进行交互实现的,所以这部分我主要将请求及应答数据的处理进行一个很简略的介绍。
以用户注册功能举例:
$(function () {
LoginPage.initial();
});
var LoginPage = {
initial: function () {
LoginPageMVC.View.initial();
},
URLs: {
base: 'http://localhost:8080/JianZi',
login: {
// TODO
},
register: {
url: LoginPage.URLs.base + '/user',
method: 'POST',
params: {
'action': 'register',
'phone': '',
'password': '',
'nickname': ''
}
}
}
};
var LoginPageMVC = {
Model: {
user: {
'phone': '',
'password': '',
'nickname': '',
'token': ''
}
},
View: {
initial: function () {
$("#inputPhone").val(LoginPageMVC.Model.user.phone);
$("#inputPassword").val(LoginPageMVC.Model.user.password);
$("#inputNickname").val(LoginPageMVC.Model.user.nickname);
},
refresh: function () {
// TODO
}
},
Controller: {
login: function () {
// TODO
},
register: function () {
LoginPageMVC.Model.user.phone = $("#inputPhone").val();
LoginPageMVC.Model.user.password = $("#inputPassword").val();
LoginPageMVC.Model.user.nickname = $("#inputNickname").val();
LoginPage.URLs.register.params.phone = LoginPageMVC.Model.user.phone;
LoginPage.URLs.register.params.password = LoginPageMVC.Model.user.password;
LoginPage.URLs.register.params.nickname = LoginPageMVC.Model.user.nickname;
$.ajax({
async: false,
type: LoginPage.URLs.register.method,
url: LoginPage.URLs.register.url,
data: LoginPage.URLs.register.params,
dataType: 'jsonp',
success: function (result) {
if (result.code != '000000') {
alert(result.message);
LoginPageMVC.View.initial();
} else {
alert("注册成功");
}
},
error: function () {
alert("注册请求发送失败");
}
});
}
}
};
初始化页面的时候,将model绑定在表单上,而后就是注册方法LoginPage.Controller.register()的实现,一个很简单的Ajax,以Jsonp方式请求,应答则是前半部分的服务端设计中提到的IResponse对象的JSON序列化格式,如果code值为000000则说明请求成功,否则请求失败,如果因网络问题或者其他情况导致的请求发送失败,则通过绑定的error回调函数弹出提示框,提示用户请求发送失败。
七 服务端对请求的处理
服务端接收到请求后,首先根据请求Url的Servlet映射值确定具体的请求处理Servlet实例,然后通过参数action的值来确定处理该请求的方法:
<servlet>
<servlet-name>user</servlet-name>
<servlet-class>com.bubbling.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>user</servlet-name>
<url-pattern>/user</url-pattern>
</servlet-mapping>
public class UserServlet extends IServlet {
private static final long serialVersionUID = 1L;
private static UserService service = UserService.getService();
@Override
protected Map<String, String> getMethodMap() {
return new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("register", "register");
put("login", "login");
}
};
}
/**
* 用户注册请求
* <p>
* action:register
* <p>
* 参数:phone(必填)、password(必填)、nickname(选填)
*/
public void register() {
try {
if (service.register(getParam("phone"), getParam("password"),
getParam("nickname"))) {
setWebResponse(Constant.STR_ERROR_CODE_SUCCESS);
} else {
setWebResponse(Constant.STR_ERROR_CODE_REGISTER_FAILUER);
}
} catch (ServiceException e) {
setWebResponse(e.getCode());
}
}
……
}
UserServlet.register()方法通过UserService来处理注册相关的业务逻辑,其中getParam()方法被封装在IServlet中,作为所有IServlet派生类公用的方法,这部分可以查阅之前服务端设计部分。
这里我对所有请求处理的结果做了一个code码整理,其静态值保存在Constant类中,这部分设计在前面的章节也有提到。并且所有业务层的设计都是通过抛出ServiceException来作为业务处理结果的,其设计如下:
package com.bubbling.common;
/**
* 公用服务异常,仅供业务层处理使用 <br>
* 当业务逻辑处理失败,包括参数校验失败、数据访问失败、文件操作失败等各种场景下抛出 <br>
* 该异常在应答前结束作用域,对应答拼装提供错误码及错误信息
*
* @author 胡楠
*
*/
public class ServiceException extends Exception
{
private static final long serialVersionUID = 1L;
private String code;
private String info;
public ServiceException(String code)
{
this(code, null);
}
public ServiceException(String code, String info)
{
this.code = code;
this.info = info;
}
public String getCode()
{
return code;
}
public void setCode(String code)
{
this.code = code;
}
public String getInfo()
{
return info;
}
public void setInfo(String info)
{
this.info = info;
}
@Override
public String getMessage()
{
return toString();
}
@Override
public String toString()
{
return "ServiceException [code=" + code + ", info=" + info + "]";
}
}
如果Service处理失败,则抛出ServiceException,异常中包括了错误码code值,和错误信息info值,这样Servlet在得到异常后会根据code值在Constant类中取得相应的应答数据,然后拼装IReponse对象,并响应客户端请求:
/**
* @author 胡楠
*
* 所有Servlet均需要自该类派生,并且需实现处理请求映射的获取方法,派生类的所有方法访问类型按规范必须声明为public,
* 且返回值为void
*
*/
public abstract class IServlet extends HttpServlet
{
……
/**
* @return kEY值为请求参数action值,VALUE值为处理该请求的方法名
*/
protected abstract Map<String, String> getMethodMap();
……
private void process() throws IOException
{
……
try
{
processAction();
}
catch (Throwable t)
{
result.setCode(Constant.STR_ERROR_CODE_SYSTEM_ERROR);
result.setMessage(Constant.MAP_ERROR.get(Constant.STR_ERROR_CODE_SYSTEM_ERROR));
}
……
try
{
PrintWriter out = response.getWriter();
……
out.flush();
out.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
……
而Service层不仅需要处理请求参数的校验,还需要通过Dao层与数据库进行交互,如果用户注册成功,需要向用户表插入一条记录,并且向用户会话表插入一条记录,用户会话记录则随着每次用户的登陆、登出等操作进行更新操作:
public boolean register(String phone, String password, String nickname) throws ServiceException {
verifyPhone(phone);
verifyPassword(password);
if (StringUtil.isEmpty(nickname)) {
nickname = StringUtil.getUUID().substring(0, 6);
}
User user = new User();
user.setPhone(phone);
user.setPassword(password);
user.setNickname(nickname);
return dao.add(user);
}
这里的verifyPhone()方法不仅需要校验手机号是否为空,还需要对号码格式进行验证,通过简单的正则判断可以实现,这些公用的校验方法我们额外进行封装,以便于重用:
private void verifyPhone(String phone) throws ServiceException {
if (StringUtil.isEmpty(phone)) {
throw new ServiceException(Constant.STR_ERROR_CODE_PHONE_EMPTY);
}
if (!RegexUtil.IsCellphone(phone)) {
throw new ServiceException(Constant.STR_ERROR_CODE_PHONE_FORMAT);
}
}
public static boolean isEmpty(String value) {
return value == null ? true : "".equals(value.trim());
}
/**
* 验证手机号码
*
* @param str
* @return
*/
public static boolean IsCellphone(String str) {
String regex = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";
return match(regex, str);
}
最后UserService通过UserDao来进行数据库交互,UserDao本身是一个接口定义,在UserService中被UserDaoMysqlImpl实例化,UserDaoMysqlImpl中实现了UserDao的所定义的数据交互方法,这里不细细列举了,因为都是简单的JDBC操作,个人认为将前后端的逻辑串联起来是最重要的,其实现细节毫无意义。
八 结语
到此为止,我从服务端一路高歌猛进到前端设计,将整个设计流程通过登录页这个案例串联了起来,再往后我觉得已经没有进行介绍的意义了,因为都是重复性的东西。
这里做一个设计的总结:
-
数据模型设计
数据模型设计是一个项目的根基,所有程序设计都应该是围绕数据模型而展开,没有数据模型则没有产品概念,所有的产品设计最终都是为了那些存于文件、存于数据库中的记录。数据模型设计可通过ER模型图或者其他工具设计,表结构设计时需要对字段类型、存储方式进行预先优化设计,尤其是热点表(经常被访问的数据)、热点数据,如何设置其索引、唯一键等很关键,这对服务端的性能而言极其重要。 -
服务端结构设计
在进行服务设计之初需要大量的基础设施设计,包括共有处理部分(各工具类),或是使用第三方Web框架,或是想我一样自己编写一个简单的Web框架,核心都在于如何对设计进行层次化处理,良好的层次结构对于代码的维护、延展至关重要。服务端设计主要考虑实现如何拆分请求处理、业务处理以及数据交互,所有的实现尽量以接口的方式设计,这是模式化设计的基础。 -
前端设计
前端设计其实是最复杂的,因为这是产品的门面,整体风格、样式这些偏产品设计的理念不细讲,站在程序员的角度来说,仅仅关心如何将请求发送至服务端,如何根据服务端的应答数据来更新页面。
从前端到服务端的一个完整处理思路进行一个整理:
- 客户端初始化Html页
- 通过Ajax向服务端发起请求
- 服务端通过Servlet处理请求,并通过业务层进行请求的业务逻辑处理
- 业务层对请求进行业务相关处理,包括参数校验、业务逻辑实现,并通过数据访问层与数据库进行交互
- 数据访问层与DB、文件进行交互,并返回交互结果
- 业务层根据处理结果向Servlet汇报应答数据
- Servlet将应答数据进行拼装,并响应给客户端
- 客户端获取Ajax应答数据将其绑定到页面元素中,进行页面的绘制更新
整个Demo从设计到实现我花了将近两周,因为平时工作的原因,只能周末找点时间做,勉强及格,虽然只有很简单的一些功能,但是对于一个常规Web项目来说,应有的设计也没差很多,当然达到上线产品级是远远不够的。
一共十一部分从服务端到前端,实现的代码我并没有贴上很多,因为确实没有什么技术含量。
后面如果有时间我会慢慢完善它,争取能让它早日部署在阿里的云服务器上。大吼一声:完结!