三周学会小程序第七讲:提交问题

640?wx_fmt=png

截止到上一讲可以支持数据库存储了,所以这一讲开始讲解怎么从小程序发布一个问题并存储到服务器端。下面简单罗列一下本讲的知识点。对了老规矩,文末附源码。

  • 对小程序端分模块重构

  • 使用 tabbar 实现多 Tab 切换

  • switchTab 和 navigateTo

  • weui.wxss 引入

  • form 表单提交

  • css 优先级

  • API 工具的封装和校验逻辑

  • 添加 LoginInterceptor 用户校验登录状态和获取用户信息

  • 添加 api/question API 提交问题到服务器端并存储

小程序端

先用一张图描绘一下这一讲的工作

640?wx_fmt=png

如图,登录成功以后进入主页,区分为四个选项卡,首页、通知、礼品和我几个选项卡,所以对前端小程序进行了重构。把 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 是跳转以后头部显示的名称。 iconPathselectedIconPath 分别是选中前后展示的图标,小编特意选择了一些对应的图片,这个图片直接在http://www.iconfont.cn

这一讲用了两种跳转的方式 switchTabnavigateTo,其中 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 找就可以了。

<form bindsubmit='post'>	
    <view class="page-section">	
      <view class="weui-cells__title">输入标题</view>	
      <view class="weui-cells weui-cells_after-title">	
        <view class="weui-cell weui-cell_input">	
          <input class="weui-input" name="title" auto-focus placeholder="请输入提问标题" />	
        </view>	
      </view>	
    </view>	
    <view class="page-section">	
      <view class="weui-cells__title">输入提问内容</view>	
      <view class="textarea-wrp">	
        <textarea  auto-height name="content"/>	
      </view>	
    </view>	
    <view class='page-section'>	
      <button form-type='submit' bindtap="primary" class='weui-btn'>提问</button>	
    </view>	
</form>

点击 提问按钮以后,触发了定义在 post.jspost,这个时候我们可以通过 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,否则提示需要登录。这个在《第五讲:登录的原理和实现》中有讲解怎么存 tokentoken 通过 header 传递给服务器端,这样是最关键的地方,不然服务器端怎么校验你的登录态?

到此小程序端逻辑已经全部完成,现在默认返回正确以后会跳转到列表也没,现在是空白没关系,下一讲就会展示出一个列表。

服务器端

因为上一讲已经把基础的服务器端处理好,这一讲就比较简单,主要就需要做两件事情:登录校验和存储问题。

登录校验

和小程序的思路类似,我们不能每一个请求过来都写一段逻辑校验一下是否有传递 token,然后再获取一下用户信息看是否正确。于是服务端引入了 Interceptor 的概念,它可以在请求开始和结束的时候做拦截处理,这样每次请求来的时候先校验是否传递 token,然后通过 token 到数据库里面查询是否有用户资料,如果没有返回错误,如果验证全部通过把查询出来的用户信息存储到 ThreadLocal 里面,供下文使用。关于 ThreadLocal 使用有不理解的可以查看一下小编之前的文章《如何优雅的使用 ThreadLocal》。具体实现如下。applicationContext.xml 配置一下拦截器。

 <mvc:interceptors>	
    <mvc:interceptor>	
        <mvc:mapping path="/api/**"/>	
        <mvc:exclude-mapping path="/api/login"/>	
        <bean class="com.codedrinker.interceptor.LoginInterceptor"></bean>	
    </mvc:interceptor>	
</mvc:interceptors>

如上代码,指定了具体的拦截器,拦截 /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

登相关文章

我是浪漫的分割线

问答

如果您对本系列文章有兴趣,欢迎置顶本订阅号,第一时间获取更新。

如果有任何问题,欢迎留言,小编很热衷和大家一起讨论技术问题。

另外小编创建了一个技术交流群,请添加小编微信,切记备注“小程序”,小编拉你进去。【只讨论技术,非诚勿扰】

640?wx_fmt=jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值