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

1年前 (2022) 程序员胖胖胖虎阿
264 0 0

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

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

  • 对小程序端分模块重构

  • 使用 tabbar 实现多 Tab 切换

  • switchTab 和 navigateTo

  • weui.wxss 引入

  • form 表单提交

  • css 优先级

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

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

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

小程序端

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

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

如图,登录成功以后进入主页,区分为四个选项卡,首页、通知、礼品和我几个选项卡,所以对前端小程序进行了重构。把 index.js 从 question 启动到最外层,然后分别创建了 gift、notification、profile和question 文件夹用于存放相对应的选项卡的页面内容,同时记得修改 /app.json 里面的 pages 内容。
这个地方值得讲的是,小程序组件默认支持这种选项卡的方式,使用方式也是非常简单。
app.json 里面添加如下文件,就可以自动定义页面的选项卡。

  
  
  
  1. {

  2.  "tabBar": {

  3.    "selectedColor": "#3c506f",

  4.    "list": [

  5.      {

  6.        "navigationBarTitleText": "首页",

  7.        "pagePath": "pages/question/list",

  8.        "text": "首页",

  9.        "iconPath": "images/index-default.png",

  10.        "selectedIconPath": "images/index-selected.png"

  11.      },

  12.      {

  13.        "navigationBarTitleText": "通知",

  14.        "pagePath": "pages/notification/list",

  15.        "text": "通知",

  16.        "iconPath": "images/notification-default.png",

  17.        "selectedIconPath": "images/notification-selected.png"

  18.      },

  19.      {

  20.        "navigationBarTitleText": "礼品",

  21.        "pagePath": "pages/gift/list",

  22.        "text": "礼品",

  23.        "iconPath": "images/gift-default.png",

  24.        "selectedIconPath": "images/gift-selected.png"

  25.      },

  26.      {

  27.        "navigationBarTitleText": "我",

  28.        "pagePath": "pages/profile/index",

  29.        "text": "我",

  30.        "iconPath": "images/me-default.png",

  31.        "selectedIconPath": "images/me-selected.png"

  32.      }

  33.    ]

  34.  }

  35. }

需要注意的是上面的 pagePath 对应的页面路径一定要存在, navigationBarTitleText 是跳转以后头部显示的名称。 iconPathselectedIconPath 分别是选中前后展示的图标,小编特意选择了一些对应的图片,这个图片直接在
http://www.iconfont.cn
下载,并且可以免费试用,如果你想换成自己的图标,可以去里面碰碰运气。

这一讲用了两种跳转的方式 switchTabnavigateTo,其中 switchTab 是跳转选项卡的时候用,并且只能用这个方法跳转,而 navigateTo是让页面导航到页面,同时这个方法会记录历史,也就是说你会发现左上角会有一个后退按钮,点击可以回退到历史浏览的页面。如果你不想有这个后退按钮可以使用 redirectTo 进行跳转,这样会覆盖掉之前的访问堆栈。

weui.wxss 是微信官方默认的样式库,没有第三方的漂亮,但是够用即可,直接下载下来放到 /lib/weui.wxss 下面,在需要使用的地方用如下语句引入即可。

  
  
  
  1. @import "../../lib/weui.wxss";

接下来就是小程序端关键的一步,提交表单。这个被小程序组件优化的还是比较简单。直接在 wxml 里面添加 form标签,然后定义 bindsubmit属性指定点击提交绑定的方法即可。同时定义一个 button 绑定提交属性 form-type='submit',这样点击这个按钮的时候,就会自动调用 bindsubmit绑定的方法了,具体代码如下。里面用的 weui-cells__title便是 weui 提供的一些样式,这个直接对着 css 找就可以了。

  
  
  
  1. <form bindsubmit='post'>

  2.    <view class="page-section">

  3.      <view class="weui-cells__title">输入标题</view>

  4.      <view class="weui-cells weui-cells_after-title">

  5.        <view class="weui-cell weui-cell_input">

  6.          <input class="weui-input" name="title" auto-focus placeholder="请输入提问标题" />

  7.        </view>

  8.      </view>

  9.    </view>

  10.    <view class="page-section">

  11.      <view class="weui-cells__title">输入提问内容</view>

  12.      <view class="textarea-wrp">

  13.        <textarea  auto-height name="content"/>

  14.      </view>

  15.    </view>

  16.    <view class='page-section'>

  17.      <button form-type='submit' bindtap="primary" class='weui-btn'>提问</button>

  18.    </view>

  19. </form>


点击 提问按钮以后,触发了定义在 post.jspost,这个时候我们可以通过 e.detail.value 获取到绑定到 form 上面的所有对象,可以做简单的校验,然后传递给服务器端。

  
  
  
  1. post: function(e) {

  2.    console.log("submit")

  3.    console.log(e.detail.value)

  4.    if (!e.detail.value.title) {

  5.      wx.showToast({

  6.        title: '请输入标题',

  7.      });

  8.      return;

  9.    }


  10.    if (!e.detail.value.content) {

  11.      wx.showToast({

  12.        title: '请输入内容',

  13.      });

  14.      return;

  15.    }

  16.    // 调用服务端 API

  17.    wx.showLoading({

  18.      title: '提交中'

  19.    });

  20. }

有读者问过,怎么样封装一个好的 API 工具,答案是没有的。你觉得好用就时好的封装。这里小编简单对 API 工具进行了封装。

为什么在这一章节封装呢?因为只有两个地方调用的时候才需要封装,如果调用 API我们只有一个地方需要,其实不封装也是可以的,封装是为了抽象、公用,所以对于封装我们还是要做到恰如其分。

我直接独立出来一个 service.js 用于专门调用服务端的 API 代码如下。

  
  
  
  1. const service = options => {

  2.  wx.showNavigationBarLoading();


  3.  options = {

  4.    dataType: "json",

  5.    ...options,

  6.    method: options.method ? options.method.toUpperCase() : "GET",

  7.    header: {

  8.      "token": wx.getStorageSync("token") || ""

  9.    },

  10.  };

  11.  const result = new Promise(function(resolve, reject) {

  12.    //做一些异步操作

  13.    const optionsData = {

  14.      success: res => {

  15.        wx.hideNavigationBarLoading();

  16.        if (res.data.status == 1005){

  17.          wx.showModal({

  18.            title: '请登陆',

  19.            content: '您还未登录,请授权登陆',

  20.            success: res => {

  21.              app.reLogin();

  22.              wx.redirectTo({

  23.                url: '/pages/index',

  24.              });

  25.            }

  26.          });

  27.        }

  28.        resolve(res.data);

  29.      },

  30.      fail: error => {

  31.        wx.hideNavigationBarLoading();

  32.        reject(error);

  33.      },

  34.      ...options

  35.    };


  36.    let token = wx.getStorageSync("token") || "";

  37.    if (!token) {

  38.      if (optionsData.url.indexOf('api/login') == -1) {

  39.        wx.showModal({

  40.          title: '请登陆',

  41.          content: '您还未登录,请授权登陆',

  42.          success: res => {

  43.            app.reLogin();

  44.            wx.redirectTo({

  45.              url: '/index',

  46.            });

  47.          }

  48.        });

  49.        reject(error);

  50.        return;

  51.      }

  52.    }


  53.    wx.request(optionsData);

  54.  });

  55.  return result;

  56. };


  57. export default service;

如上我们简单进行讲解,封装主要涉及两个地方,一个是对于 API 的封装,我们把 API 统一定义到了 api.js 格式如下

  
  
  
  1. const Login = {

  2.  url: config.serverHost + "/api/login",

  3.  method: "post"

  4. };

这样在使用的地方直接引用即可,

  
  
  
  1. service({

  2.  ...Question,

  3.    data: {

  4.      title: e.detail.value.title,

  5.      content: e.detail.value.content

  6.    }

  7.  })

  8.  .then(response => {

  9.    wx.hideLoading();

  10.    console.log(response);

  11.    if (response.status == 200) {

  12.      // 展示 登录成功 提示框

  13.      wx.showToast({

  14.        title: '发布成功',

  15.        icon: "success",

  16.        duration: 2000,

  17.        success: res => {

  18.          wx.switchTab({

  19.            url: "list"

  20.          });

  21.        }

  22.      });

  23.    } else {

  24.      // 展示 错误信息

  25.      wx.showToast({

  26.        title: response.message,

  27.        icon: "none",

  28.        duration: 1000

  29.      });

  30.    }

  31.  })

  32.  .catch(error => {

  33.    console.log(error);

  34.    wx.showToast({

  35.      title: '提交失败'

  36.    });

  37.  });

同时直接传入 JSON 数据然后通过 Primose 返回的回调处理正确和失败即可,这样把调用 API 的处理全部封装起来,便于使用和管理。
另一个方便的地方是,统一处理了一下登录状态,简单点说就是每次调用服务端接口的时候都需要检查一下是否登录。检查分为三个部分,
第一部分是检查本地是否存储了 token,否则提示需要登录。这个在《第五讲:登录的原理和实现》中有讲解怎么存 token
第二部分是把本地存储的 token 通过 header 传递给服务器端,这样是最关键的地方,不然服务器端怎么校验你的登录态?
第三部分如果调用服务端接口检测登录态过期会提示登录异常并跳转到登录页面。

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

服务器端

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

登录校验

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

  
  
  
  1. <mvc:interceptors>

  2.    <mvc:interceptor>

  3.        <mvc:mapping path="/api/**"/>

  4.        <mvc:exclude-mapping path="/api/login"/>

  5.        <bean class="com.codedrinker.interceptor.LoginInterceptor"></bean>

  6.    </mvc:interceptor>

  7. </mvc:interceptors>

如上代码,指定了具体的拦截器,拦截 /api/**地址, **代表任意,但是不可以拦截 /api/login,因为它是登录接口肯定没有 token。其次编写拦截器。

  
  
  
  1. public class LoginInterceptor implements HandlerInterceptor {


  2.    @Autowired

  3.    private UserService userService;


  4.    @Override

  5.    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

  6.        //请求之前,验证通过返回true,验证失败返回false

  7.        String token = request.getHeader("token");

  8.        if (StringUtils.isBlank(token)) {

  9.            makeFail(response);

  10.            return false;

  11.        }


  12.        // 通过 token 从数据库中获取信息,如果没有验证失败

  13.        // 如果通过一台设备登录,再通过另一台设备登录,第一台设备会自动登出

  14.        User user = userService.getByToken(token);

  15.        if (user == null) {

  16.            makeFail(response);

  17.            return false;

  18.        }


  19.        //把获取到的user信息暂存到 ThreadLocal 里面,以便上线文中方便的使用

  20.        SessionUtil.setUser(user);

  21.        return true;

  22.    }


  23.    private void makeFail(HttpServletResponse response) {

  24.        ResultDTO resultDTO = ResultDTO.fail(CommonErrorCode.NO_USER);

  25.        response.setCharacterEncoding("UTF-8");

  26.        response.setContentType("application/json; charset=utf-8");

  27.        try {

  28.            PrintWriter out = response.getWriter();

  29.            out.print(JSON.toJSONString(resultDTO));

  30.            out.close();

  31.        } catch (Exception e) {

  32.            log.error("LoginInterceptor preHandle", e);

  33.        }

  34.    }


  35.    @Override

  36.    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

  37.        //请求结束

  38.        //请求结束以后移除 user

  39.        SessionUtil.removeUser();

  40.    }

  41. }

拦截器的实现比较简单,直接实现 HandlerInterceptor 接口,在 preHandle里面处理访问拦截并存入 ThreadLocal即可,需要注意的是在 postHandle里面需要把 ThreadLocal移除。拦截器的如果需要返回数据给小程序端,需要使用 response,这里不能像 RestController 那么简洁了。

接口

接口就相对比较简单了,直接上代码。

  
  
  
  1. @RestController

  2. @Slf4j

  3. public class QuestionController {

  4.    @Autowired

  5.    private QuestionService questionService;


  6.    @RequestMapping(value = "api/question", method = RequestMethod.POST)

  7.    public ResultDTO post(@RequestBody Question question) {

  8.        try {

  9.            questionService.createQuestion(question);

  10.            return ResultDTO.ok(null);

  11.        } catch (Exception e) {

  12.            log.error("QuestionController post error, question : {}", question, e);

  13.            return ResultDTO.fail(CommonErrorCode.UNKOWN_ERROR);

  14.        }

  15.    }

  16. }

上面的内容在《第五讲:登录原理和实现》 里面已经讲解,不在累述。另外还需要注意的是创建一个名为 V2__的数据库脚本,在运行的时候会自动帮你创建数据库表,为什么呢?《第六讲:数据的验证和存储》已经讲解。
到此已经全部结束,回看上文是不是编写一个小程序也是很简单呢?

相关资料

小程序组件示例
https://developers.weixin.qq.com/miniprogram/dev/component/textarea.html

源码

小程序源码地址,Tag V7
https://github.com/codedrinker/jiuask

服务端源码地址,Tag V7
https://github.com/codedrinker/jiuask-server

登相关文章

小程序申请和注意事项

客户端代码准备和基础功能讲解

服务端搭建和免费部署

Heroku 绑定 Github 自动部署

登录的原理和实现

登录优化和存储用户信息


我是浪漫的分割线


问答

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

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

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

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

本文分享自微信公众号 - Java后端(web_resource)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

版权声明:程序员胖胖胖虎阿 发表于 2022年11月13日 下午4:24。
转载请注明:三周学会小程序第七讲:提交问题 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...