截止到上一讲可以支持数据库存储了,所以这一讲开始讲解怎么从小程序发布一个问题并存储到服务器端。下面简单罗列一下本讲的知识点。对了老规矩,文末附源码。
小程序端
先用一张图描绘一下这一讲的工作
如图,登录成功以后进入主页,区分为四个选项卡,首页、通知、礼品和我几个选项卡,所以对前端小程序进行了重构。把 index.js 从 question 启动到最外层,然后分别创建了 gift、notification、profile和question 文件夹用于存放相对应的选项卡的页面内容,同时记得修改 /app.json 里面的 pages 内容。app.json
里面添加如下文件,就可以自动定义页面的选项卡。
{ "tabBar": { "selectedColor": "#3c506f", "list": [ { "navigationBarTitleText": "首页", "pagePath": "pages/question/list", "text": "首页", "iconPath": "images/index-default.png", "selectedIconPath": "images/index-selected.png" }, { "navigationBarTitleText": "通知", "pagePath": "pages/notification/list", "text": "通知", "iconPath": "images/notification-default.png", "selectedIconPath": "images/notification-selected.png" }, { "navigationBarTitleText": "礼品", "pagePath": "pages/gift/list", "text": "礼品", "iconPath": "images/gift-default.png", "selectedIconPath": "images/gift-selected.png" }, { "navigationBarTitleText": "我", "pagePath": "pages/profile/index", "text": "我", "iconPath": "images/me-default.png", "selectedIconPath": "images/me-selected.png" } ] }
}
需要注意的是上面的 pagePath
对应的页面路径一定要存在, navigationBarTitleText
是跳转以后头部显示的名称。 iconPath
和 selectedIconPath
分别是选中前后展示的图标,小编特意选择了一些对应的图片,这个图片直接在http://www.iconfont.cn
这一讲用了两种跳转的方式 switchTab
和 navigateTo
,其中 switchTab
是跳转选项卡的时候用,并且只能用这个方法跳转,而 navigateTo
是让页面导航到页面,同时这个方法会记录历史,也就是说你会发现左上角会有一个后退按钮,点击可以回退到历史浏览的页面。如果你不想有这个后退按钮可以使用 redirectTo
进行跳转,这样会覆盖掉之前的访问堆栈。
weui.wxss
是微信官方默认的样式库,没有第三方的漂亮,但是够用即可,直接下载下来放到 /lib/weui.wxss
下面,在需要使用的地方用如下语句引入即可。
@import "../../lib/weui.wxss";
接下来就是小程序端关键的一步,提交表单。这个被小程序组件优化的还是比较简单。直接在 wxml
里面添加 form
标签,然后定义 bindsubmit
属性指定点击提交绑定的方法即可。同时定义一个 button
绑定提交属性 form-type='submit'
,这样点击这个按钮的时候,就会自动调用 bindsubmit
绑定的方法了,具体代码如下。里面用的 weui-cells__title
便是 weui
提供的一些样式,这个直接对着 css 找就可以了。
输入标题 输入提问内容
点击 提问
按钮以后,触发了定义在 post.js
的 post
,这个时候我们可以通过 e.detail.value
获取到绑定到 form
上面的所有对象,可以做简单的校验,然后传递给服务器端。
post: function(e) { console.log("submit") console.log(e.detail.value) if (!e.detail.value.title) { wx.showToast({ title: '请输入标题', }); return; } if (!e.detail.value.content) { wx.showToast({ title: '请输入内容', }); return; } // 调用服务端 API wx.showLoading({ title: '提交中' });
}
有读者问过,怎么样封装一个好的 API
工具,答案是没有的。你觉得好用就时好的封装。这里小编简单对 API
工具进行了封装。
为什么在这一章节封装呢?因为只有两个地方调用的时候才需要封装,如果调用 API
我们只有一个地方需要,其实不封装也是可以的,封装是为了抽象、公用,所以对于封装我们还是要做到恰如其分。
我直接独立出来一个 service.js
用于专门调用服务端的 API
代码如下。
const service = options => { wx.showNavigationBarLoading(); options = { dataType: "json", ...options, method: options.method ? options.method.toUpperCase() : "GET", header: { "token": wx.getStorageSync("token") || "" }, }; const result = new Promise(function(resolve, reject) { //做一些异步操作 const optionsData = { success: res => { wx.hideNavigationBarLoading(); if (res.data.status == 1005){ wx.showModal({ title: '请登陆', content: '您还未登录,请授权登陆', success: res => { app.reLogin(); wx.redirectTo({ url: '/pages/index', }); } }); } resolve(res.data); }, fail: error => { wx.hideNavigationBarLoading(); reject(error); }, ...options }; let token = wx.getStorageSync("token") || ""; if (!token) { if (optionsData.url.indexOf('api/login') == -1) { wx.showModal({ title: '请登陆', content: '您还未登录,请授权登陆', success: res => { app.reLogin(); wx.redirectTo({ url: '/index', }); } }); reject(error); return; } } wx.request(optionsData); }); return result;
};
export default service;
如上我们简单进行讲解,封装主要涉及两个地方,一个是对于 API
的封装,我们把 API
统一定义到了 api.js
格式如下
const Login = { url: config.serverHost + "/api/login", method: "post"
};
这样在使用的地方直接引用即可,
service({ ...Question, data: { title: e.detail.value.title, content: e.detail.value.content } }) .then(response => { wx.hideLoading(); console.log(response); if (response.status == 200) { // 展示 登录成功 提示框 wx.showToast({ title: '发布成功', icon: "success", duration: 2000, success: res => { wx.switchTab({ url: "list" }); } }); } else { // 展示 错误信息 wx.showToast({ title: response.message, icon: "none", duration: 1000 }); } }) .catch(error => { console.log(error); wx.showToast({ title: '提交失败' }); });
同时直接传入 JSON
数据然后通过 Primose
返回的回调处理正确和失败即可,这样把调用 API
的处理全部封装起来,便于使用和管理。token
,否则提示需要登录。这个在《第五讲:登录的原理和实现》中有讲解怎么存 token
。token
通过 header
传递给服务器端,这样是最关键的地方,不然服务器端怎么校验你的登录态?
到此小程序端逻辑已经全部完成,现在默认返回正确以后会跳转到列表也没,现在是空白没关系,下一讲就会展示出一个列表。
服务器端
因为上一讲已经把基础的服务器端处理好,这一讲就比较简单,主要就需要做两件事情:登录校验和存储问题。
登录校验
和小程序的思路类似,我们不能每一个请求过来都写一段逻辑校验一下是否有传递 token
,然后再获取一下用户信息看是否正确。于是服务端引入了 Interceptor
的概念,它可以在请求开始和结束的时候做拦截处理,这样每次请求来的时候先校验是否传递 token
,然后通过 token
到数据库里面查询是否有用户资料,如果没有返回错误,如果验证全部通过把查询出来的用户信息存储到 ThreadLocal
里面,供下文使用。关于 ThreadLocal
使用有不理解的可以查看一下小编之前的文章《如何优雅的使用 ThreadLocal》。具体实现如下。applicationContext.xml
配置一下拦截器。
如上代码,指定了具体的拦截器,拦截 /api/**
地址, **
代表任意,但是不可以拦截 /api/login
,因为它是登录接口肯定没有 token
。其次编写拦截器。
public class LoginInterceptor implements HandlerInterceptor { @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //请求之前,验证通过返回true,验证失败返回false String token = request.getHeader("token"); if (StringUtils.isBlank(token)) { makeFail(response); return false; } // 通过 token 从数据库中获取信息,如果没有验证失败 // 如果通过一台设备登录,再通过另一台设备登录,第一台设备会自动登出 User user = userService.getByToken(token); if (user == null) { makeFail(response); return false; } //把获取到的user信息暂存到 ThreadLocal 里面,以便上线文中方便的使用 SessionUtil.setUser(user); return true; } private void makeFail(HttpServletResponse response) { ResultDTO resultDTO = ResultDTO.fail(CommonErrorCode.NO_USER); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); try { PrintWriter out = response.getWriter(); out.print(JSON.toJSONString(resultDTO)); out.close(); } catch (Exception e) { log.error("LoginInterceptor preHandle", e); } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //请求结束 //请求结束以后移除 user SessionUtil.removeUser(); }
}
拦截器的实现比较简单,直接实现 HandlerInterceptor
接口,在 preHandle
里面处理访问拦截并存入 ThreadLocal
即可,需要注意的是在 postHandle
里面需要把 ThreadLocal
移除。拦截器的如果需要返回数据给小程序端,需要使用 response
,这里不能像 RestController
那么简洁了。
接口
接口就相对比较简单了,直接上代码。
@RestController
@Slf4j
public class QuestionController { @Autowired private QuestionService questionService; @RequestMapping(value = "api/question", method = RequestMethod.POST) public ResultDTO post(@RequestBody Question question) { try { questionService.createQuestion(question); return ResultDTO.ok(null); } catch (Exception e) { log.error("QuestionController post error, question : {}", question, e); return ResultDTO.fail(CommonErrorCode.UNKOWN_ERROR); } }
}
上面的内容在《第五讲:登录原理和实现》 里面已经讲解,不在累述。另外还需要注意的是创建一个名为 V2__
的数据库脚本,在运行的时候会自动帮你创建数据库表,为什么呢?《第六讲:数据的验证和存储》已经讲解。
相关资料
小程序组件示例
源码
小程序源码地址,Tag V7
服务端源码地址,Tag V7
登相关文章
问答
如果您对本系列文章有兴趣,欢迎置顶本订阅号,第一时间获取更新。
如果有任何问题,欢迎留言,小编很热衷和大家一起讨论技术问题。
另外小编创建了一个技术交流群,请添加小编微信,切记备注“小程序”,小编拉你进去。【只讨论技术,非诚勿扰】