一、概述
再过三个月 jfinal 将走过 10 年的迭代时期,经过如此长时间的打磨 jfinal 现今已十分完善、稳定。
jfinal 起步时就定下的开发效率高、学习成本低的核心目标,一直不忘初心,坚持至今。
jfinal 在开发效率方向越来越逼近极限,如果还要进一步提升开发效率,唯一的道路就是入场前端。
jfinal 社区经常有同学反馈,用上 jfinal 以后,90% 以上的时间都在折腾前端,强烈希望 jfinal 官方能出手前端,推出一个 jfinal 风格的前端框架。
虽然我个人对前端没有兴趣,但为了进一步提升广大 jfinal 用户的开发效率,决定这次在前端先小试牛刀。
本次为大家带来是 jfinal 极简风格的前端交互工具箱:jfinal-kit.js。jfinal-kit.js 主要特色如下:
学习成本:不引入 vue react angular 这类前端技术栈,核心用法 10 分钟掌握,极大降低学习成本
开发效率:尽可能避免编写 js 代码就能实现前端功能,极大提升开发效率
用户体验:交互全程 ajax,交互过程 UI 尽可能及时反馈
前后分离:只在必要之处使用前后分离,其它地方使用模板引擎,结合前后分离与模板引擎优势
仍是熟悉的味道:学习成本低、开发效率高
二、五种交互抽象
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 采用咖啡授权模式,意在请作者喝一杯咖啡即可获得授权。