一、概述

   再过三个月 jfinal 将走过 10 年的迭代时期,经过如此长时间的打磨 jfinal 现今已十分完善、稳定。

   jfinal 起步时就定下的开发效率高、学习成本低的核心目标,一直不忘初心,坚持至今。

   jfinal 在开发效率方向越来越逼近极限,如果还要进一步提升开发效率,唯一的道路就是入场前端。

   jfinal 社区经常有同学反馈,用上 jfinal 以后,90% 以上的时间都在折腾前端,强烈希望 jfinal 官方能出手前端,推出一个 jfinal 风格的前端框架。

   虽然我个人对前端没有兴趣,但为了进一步提升广大 jfinal 用户的开发效率,决定这次在前端先小试牛刀。

   本次为大家带来是 jfinal 极简风格的前端交互工具箱:jfinal-kit.js。jfinal-kit.js 主要特色如下:

  1. 学习成本:不引入 vue react angular 这类前端技术栈,核心用法 10 分钟掌握,极大降低学习成本

  2. 开发效率:尽可能避免编写 js 代码就能实现前端功能,极大提升开发效率

  3. 用户体验:交互全程 ajax,交互过程 UI 尽可能及时反馈

  4. 前后分离:只在必要之处使用前后分离,其它地方使用模板引擎,结合前后分离与模板引擎优势

    仍是熟悉的味道:学习成本低、开发效率高

二、五种交互抽象

   jfinal-kit.js 将前端交互由轻到重抽象为:msg、switch、confirm、open、fill 五种模式(未来还将增加tab抽象),以下分别介绍。

1、msg 交互

    msg 用于最轻量级的交互,当用户点击页面中某个组件(如按钮)时立即向后端发起 ajax 请求,然后将后端响应输出到页面。要用该功能,第一步通过 jfinal-kit.js 中的 kit.bindMsg(...) 绑定需要 msg 交互的页面元素:

kit.bindMsg('#content-box', 'button[msg],a[msg]', '正在处理, 请稍候 .....');

   以上一行代码就可以为带有 msg 属性的标签 button 与标签 a 添加 msg 交互功能。

    注意,bindMsg 方法中的前两个参数在底层实际上就是用的 jquery 的事件绑定方法 on,尽可能用上开发者已有的技术只累,降低学习成本。第三个参数是在交互过程中的提示信息,用于提升用户体验,该参数可以省略。

   第二步在 html 中使用第一步中绑定所带来的功能:

<button msg url="/func/clearCache">
   清除缓存
</button>

   上面的 button 标签中的 url 指向了后端的 action。由于第一步中第二个参数的选择器同时也绑定了 a 标签,所以 button 改为 a 也可以。

    第三步,在后端添加第二步 url 指向的 action 即可:

public void clearCache() {
   cacheService.clearCache();
   renderJson(Ret.ok("msg", "缓存清除完成"));
}

    整个过程的代码量极少,前端交互功能的实现也像后端一样快了,开发效率得到极大提升。

2、switch 交互

   switch 交互是指类似于手机设置中心开关控件功能,点击 switch 可在两种状态间来回切换,使用方法:

kit.bindSwitch('#content-box', 'div.custom-switch input[url]');

   与 msg 交互类似,同样也是一行代码。参数用法也一样:将 switch 交互功能绑定到带有 url 的 input 控件上(div.custom-switch是jquery选择器的一部分)。功能绑定后,就可以在 html 中使用了:

<div class="custom-control custom-switch">
   <input #(x.state == 1 ? 'checked':'') url='/blog/publish?id=#(x.id)' type="checkbox">
   <label class="custom-control-label" for="id-#(x.id)"></label>
</div>

   上面代码中的 div、lable 仅仅为 bootstrap 4 的 switch 组件所要求的内容,不必关注,重点关注 input 标签,其 url 指向了后端 action,在后端添加即可:

public void publish() {
   Ret ret = srv.publish(getInt("id"), getBoolean("checked"));
   renderJson(ret);
}

   switch 交互与 msg 在本质上是完全相同的。

3、confirm 交互

    confirm 交互与 msg 交互基本一样,只不过在与后端交互之前会弹出对话框进行确认,使用方法:

kit.bindConfirm('#content-box', 'a[confirm],button[confirm]');

   与 msg、switch 本质上一样,将 confirm 交互绑定到具有 confirm 属性的 a 标签与 button 标签上。在 html 中使用:

<button confirm="确定重启项目 ?" url="/admin/func/restart">
   重启项目
</button>

   最后是添加后端 action:

public void restart() {
   renderJson(srv.restart());
}

   以上 msg、switch、confirm 三种交互方式,使用模式完全一样:绑定、添加 html(url指向后端action)、添加 action。

4、open 交互

    open 交互方式与前面三种交互方式基本相同,不同之处在于前三种交互方式参与的元素就在当前页面,而 open 交互方式的参与元素是一个独立的 html 文件,第一步仍然是绑定:

kit.bindOpen('#content-box', 'a[open],button[open]', '正在加载, 请稍候 .....');

    以上代码的含义与 msg 类似,将 open 交互功能绑定到带有 open 属性的 a 标签与 button 标签之上。

    第二步仍然是在 html 中使用:

<button open url="/account/add">
   创建
</button>

    第三步仍然是创建 url 指向的 action:

public void add() {
   render("add.html");
}

    第三步与 msg、switch、confirm 交互不同之处在于,这里是返回一个独立的页面,而非返回 json 数据。注意,如果页面并没有动态内容,无需模板引擎渲染的话,无需创建该 action,而是让 url 直接指向它就可以了:

<button open url="/这里是一个静态页面文件.html">
   创建
</button>

    第四步是创建页面 "add.html" 单独用于交互,页面的主要内容如下:

<!-- 弹出层主体 -->
<div class="open-box">
  <form id="open-form" action="/account/save">	
    <div class="row">
      <label class="col-2 col-form-label">昵称</label>
      <input name="nickName">
    </div>
    <div class="row">
      <label class="col-2 col-form-label">账号</label>
      <input name="userName">
    </div>
    <div class="row">
      <label class="col-2 col-form-label">密码</label>
      <input name="password" type="password">
    </div>
    <div class="row">
      <button onclick="submitAccount();">提交</button>
    </div>
   </form>
</div>

<!-- 弹出层样式 -->
<style>
  .open-box {padding: 20px 30px 0 35px;}
</style>

<!-- 弹出层 js 脚本 -->
<script>
  function submitAccount() {
    $form = $('#open-form');
    kit.post($form.attr('action'), $form.serialize(), function(ret) {
      kit.msg(ret);
    });
  }
</script>

    以上 add.html 页面是用于交互的 html 内容,该内容将会显示在一个弹出的对话框之中。该文件的内容分为 html、css、js 三个部分,从而可以实现功能的模块化。

    第五步,针对 add.html 中 form 表单的 action="/account/save" 创建相应的 action:

public void save() {
  Ret ret = srv.save(getBean(Account.class));
  renderJson(ret);
}

   action 代码十分简单,与 msg 交互模式代码风格一样。

    open 交互需要一个独立的页面作为载体,而 msg、switch、confirm 没有这个载体。

5、fill 交互

   fill 交互与前面四种交互很不一样,它是向当前页面的指定容器填充 html 内容,从而在当前页面中进行交互。

   第一步仍然是绑定:

kit.bindFill('#content-box', 'a[fill],button[fill],ul.pagination a[href]', '#content-box');

   前两个参数与前面四种交互模式完全一样,最后一个参数 '#content-box' 表示从后端被加载的 html 内容 fill 到的容器。

   第二步与前面四种交互模式的用法完全一样,不再详述。

   第三步与 open 模式的第四步创建页面 "add.html" 单独用于交互完全一样,也不再详述。

   fill 与 open 在本质上是一样的,只不过前者是将交互用的 html 文件内容直接 fill 到当前页面,后者是用弹出层来承载 html 文件内容,仅此而已。

   所以,学会了 open,相当于就学会了 fill。

   最后,fill 交互是实现前后分离模式的基础,后续章节将深入介绍。

三、前后端半分离方案

   最近几年前后分离技术很热,前后分离有很多优点,但对于全栈开发者和中小企业也有一定的缺点。

   首先,前后分离不利于 SEO,不利于搜索引擎收录。搜索引擎仍是巨大的流量入口,如果辛辛苦苦创建的内容没有被搜索引擎收录,将是巨大的损失。

   其次,前后分离通常要引入其整个技术栈,会带来一定的学习成本。jfinal 社区多数开发者主要面向后端开发,如果再引入前后分离技术栈,很多同学并没有多少时间与动力。有兴趣原因也有专注度原因,前端也是一片汪洋大海。

   再次,前后分离通常要设置前端与后端两种工作岗位,对于小企业有成本压力。维护前后分离项目的成本也有所增加。

   最后,前后分离减轻了后端工作负担,加重了前端工作负担,但对于 jfinal 社区的全栈开发者来说,相当程度上是工作负担的转移,总体上并没有消除多少工作量。对于后端包打天下,未设置前端职位的中小企业带来的是成本提升与效率降低。

   jfinal-kit.js 希望能得到前后分离的优点,并同时能消除它的缺点。

   jfinal-kit.js 的采用前后端 "半分离" 方案:只在必要的地方前后分离拿走前后分离的好处,其它地方使用模板引擎扔掉前后分离的坏处。并且不必引入 vue、react 等前端技术栈,消除学习成本。

   jfinal-kit.js 的前后半分离具体是下面这样的,需要前后分离的 "xxx.html" 页面内容如下:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
     内容省略...
  </head>

  <body class="home-template">
     内容省略...
     
     <!-- 下面 div 内的内容通过 ajax 获取,实现前后分离 -->
     <div id='article'>
     </div>
   
  </body>
  
  <script>
     $(function() {
        kit.fill('/article/123', null, '#article');
     });   
  </script>
</html>

   以上 "xxx.html" 文件只有静态内容,动态内容通过 kit.fill(...) 异步加载,其第一个参数指向的 action 后端代码如下:

public void article() {
   set("article", srv.getById(getPara()));
   render("_article.html");
}

   以上代码中的 "_article.html" 是与传统前的分离方案不同的地方,传统前后分离返回的是 json 数据,而这里返回的是 html 片段,其代码结构如下:

<div class="article-box">
  <div class="title">
    <span>#(article.title)</span>
    <span>#date(article.createTime)</span>
  </div>
  
  <div class="content">
    #(article.content)
  </div>
</div>

    上面的 html 片段内容以模板方式展现,可读性、可维护性比传统前后分离要好。并且 html 片将由模板引擎渲染,客户端没有计算压力。

    以上仅仅示范了静态页面的一处动态加载方式,也可以使用任意多处动态加载,并且动态部分的粒度可以极细。例如假定静态部分如下:

其它地方与前面的 xxx.html 一样,省去....

<table class="table table-hover">			
  <thead>
    <tr>
      <th>ID</th>
      <th>昵称</th>
      <th>登录名</th>
      <th>创建</th>
    </tr>
  </thead>			
  <tbody id='account-table'>
  </tbody>
</table>

<script>
  $(function() {
    kit.fill('/account/list', null, '#account-table');
  });   
</script>

其它地方与前面的 xxx.html 一样,省去....

   然后创建一个 action 响应上面代码中的 "/account/list":

public void list() {
  set("accountList", srv.getAccountList());
  render("_account_table.html");
}

   以上代码中的 _account_table.html 如下:

#for (x : accountList)
  <tr>
    <td>#(x.id)</td>
    <td>#(x.nickName)</td>
    <td>#(x.userName)</td>
    <td>#date(x.created)</td>
  </tr>
#end

    以上 _account.html 以模板形式展示,用 enjoy 进行渲染,使用简单,可读性高。

    从本质上来说传统前后分离与 jfinal-kit.js 前后分离几乎一样,都是先向客户端响应静态 html + css + js,然后通过 ajax 向后端获取数据并渲染出动态内容,区别就在于前者是获取 json 并在客户端进行渲染,而后者是直接获取后端渲染好的 html 片进行简单的填充,仅此而已。

    即便在底层技术实现上是如此的相似,但 jfinal-kit.js 无需引入复杂的技术栈,极大降低了学习成本。

四、jfinal blog 实践

    jfinal-kit.js 以 jfinal blog 为载体,演示了 jfinal-kit.js 中的功能用法,实现了一些常见功能:账户管理、文章管理、图片管理、功能管理、登录等功能。

    jfinal blog 后台管理 UI 面向实际项目精心设计,可以作为项目起步的蓝本。以下是部分界面截图:

    补充截图

    实践证明,开发效率极大提升,学习成本极低,几乎不用写 js 代码就轻松实现前后交互。学习成本、开发效率两个方向符合预期目标,符合 jfinal 极简设计思想。

五、咖啡授权

    app & coffee 频道所有 application 采用咖啡授权模式,意在请作者喝一杯咖啡即可获得授权。