EchoCow

念念不忘,必有回响

念念不忘,必有回响
  menu
99 文章
101 评论
111909 浏览
2 当前访客
ღゝ◡╹)ノ❤️

saplat 开发笔记

前言

写这个开发笔记记录的原因是因为我们这个小团队不可能一直伴随着这个项目一直走下去,在第一期项目中,我们踩了无数的坑,走了无数的弯路,在不断尝试,不断试验过程中也吸取了很多经验,但是有许多耗时耗力但是却只是一些小问题的事儿我们浪费了太多时间。所以希望用这个笔记来记录下来,然后在以后这个项目的开发工程中可以让后面的开发者少走一点弯路。当初我选择框架的时候并没有考虑到这个问题,按理来说应该在项目启动时就开始记录,但是昨夜无眠思考的时候发现似乎少了这个非常重要的环节,幸运的是现在还不晚,但愿这个笔记能够帮助你在对这个项目的开发或者二次开发中能够带来些许帮助。

EchoLZY 2018-7-28

目录

[TOC]

后端篇

技术选型

  • 核心框架:jboot 1.5.3/jfinal 3.4
  • 模版引擎:jfinal enjoy、layui
  • 注册中心:consul/zookeeper
  • RPC:motan/dubbo
  • RPC治理:motan-manager
  • 安全框架:shiro/jwt
  • 缓存框架:ehcache/redis
  • 容错隔离:hystrix
  • 调用监控:hystrix-dashboard
  • 链路跟踪:zipkin
  • 页面ui:layui v2.3.0

资料参考

概述

在我们这个项目中,我们使用的是基于 jfinal 的 jboot 微服务架构,使用 GitHub 上的开源项目 jboot-admin 来进行的开发。但是在我们后来的开发的过程中发现存在大量的问题,有些地方或许是因为技术选型的问题造成项目开发难度逐渐加大,后面会提到。如果你是新的开发人员,那么请按照此篇笔记 saplat 部署 对项目进行环境部署,在部署成功的前提下来进行了解此项目。

服务发布

在你搭建完项目后,你应该会运行两个 Application,其中

SaPlat\SaPlat-service\SaPlat-service-provider\src\main\java\io\jboot\admin\service\provider\app\Application.java

的启动器,我们将它命名为 service ,也即为服务。他即是微服务的核心,向 consul 注册中心注册服务,将服务发布出去,可以在其他地方调用已经发布的服务。

消费服务

而另外一个 Application

SaPlat\SaPlat\src\main\java\io\jboot\admin\Application.java

我们将它命名为 client,即为客户端,是我们 web 应用的核心,同时也是对前端的请求接受,调用相应的服务进行处理业务逻辑,然后将结果返回给前端。


所以可理解为我们项目主要由两个部分组成:web应用方 (简称为 client) 和 服务提供方 (简称为 service),所以我们在后面的谈论中主要分为下面两个部分进行讨论,

client

他主要位于 SaPlat\SaPlat\src\main\java\io\jboot\admin 下,下面详细说一说每个文件夹的作用

├─src
   └─main
       ├─java
       │  └─io
       │      └─jboot
       │          └─admin                   // 后台管理根目录
       │              ├─config              // 客户端启动配置
       │              ├─controller          // 控制器,用来接收前端请求以及逻辑处理
       │              │  ├─app              // 应用功能模块
       │              │  ├─b2c              // b2c 模块,暂时无需管
       │              │  └─system           // 系统基础功能模块
       │              ├─support             // 第三方支持
       │              │  ├─auth             // 认证、权限管理
       │              │  ├─enjoy            // 模板引擎自定义扩展
       │              │  │  └─directive     // 模板引擎指令扩展
       │              │  └─log              // 日志配置
       │              └─validator           // 前端参数验证拦截器
       │                  ├─app             // 应用功能验证拦截器
       │                  └─system          // 系统功能验证拦截器
       └─resources                          // 包括配置文件、前端页面,后面会提到

而对于开发人员来说,我们最常用的一个目录便是 controller ,因为他是用来接收我们前端请求处理的主要控制器。而 validator 目录则是用来进行辅助参数检验,防止不合法的参数传递的。对于 support 目录,我们在需要的时候进行扩展即可。

controller

我们来看看 controller 的基础结构,我们以 日志管理 为例,位于 io.jboot.admin.controller.system.LogController

package io.jboot.admin.controller.system;

import com.jfinal.plugin.activerecord.Page;
import io.jboot.admin.base.rest.datatable.DataTable;
import io.jboot.admin.base.web.base.BaseController;
import io.jboot.admin.service.api.LogService;
import io.jboot.admin.service.entity.model.Log;
import io.jboot.core.rpc.annotation.JbootrpcService;
import io.jboot.web.controller.annotation.RequestMapping;

/**
 * 日志管理
 * @author Rlax
 *
 */
@RequestMapping("/system/log")      // (1)设置 url 映射
// 继承 BaseController
public class LogController extends BaseController {
    
    // 依赖注入,获取到服务提供者提供的 logservice
    @JbootrpcService
    private LogService logService;

    /**
     * index
     * 这是一个 controller 中的最小单元,也即是他的子路径
     * 当我们访问 /system/log/index 的时候,也就会进入这个方法
     */
    public void index() { // (2)
        //(3) 不做任何操作,将它转发到页面中
        render("main.html");  
    }

    /**
     * 表格数据,同理,访问 /system/log/tableData 的时候进入到这个方法
     */
    public void tableData() {
        // 获取参数的方法,前端传来数据为json
        // (4)
        int pageNumber = getParaToInt("pageNumber", 1);
        int pageSize = getParaToInt("pageSize", 30);

        Log log = new Log();
        log.setIp(getPara("ip"));
        log.setUrl(getPara("url"));
        log.setLastUpdAcct(getPara("userName"));
        
        // 调用服务提供的方法
        Page<Log> logPage = logService.findPage(log, pageNumber, pageSize);
        // 以 json 的方式进行回复
        renderJson(new DataTable<Log>(logPage));
    }
    
}

JbootController 是扩展了JFinal Controller,在Jboot应用中,所有的控制器都应该继承至 JbootController,即 BaseController。

  1. @RequestMapping("/system/log") 是对 url 的映射,也即请求映射,我们发起请求的时候,是需要对某个 url 发送请求,这个 url 就取决于此注释。
  2. 在 Controller 之中定义的 public 方法称为 Action。Action 是请求的最小单位。Action 方法 必须在 Controller 中定义,且必须是 public 可见性。每个Action对应一个URL地址的映射:
  3. render,他是渲染器,负责把内容输出到浏览器,每一个 Action ,都应该有一个 render 来进行返回,否则,他会默认去寻找当前方法名的 html 页面。在Controller中,提供了如下一些列render方法。
指令描述
render("test.html")渲染名为 test.html 的视图,该视图的全路径为”/path/test.html”
render("/other_path/test.html")渲染名为 test.html 的视图,该视图的全路径为”/other_path/test.html”,即当参数以”/”开头时将采用绝对路径。
renderTemplate("test.html")渲染名为 test.html 的视图,且视图类型为 JFinalTemplate。
renderFreeMarker("test.html")渲 染 名 为 test.html 的视图 , 且 视图类型为FreeMarker。
renderJsp("test.jsp")渲染名为 test.jsp 的视图,且视图类型为 Jsp。
renderVelocity("test.html")渲染名为 test.html 的视图,且视图类型为 Velocity。
renderJson()将所有通过 Controller.setAttr(String, Object)设置的变量转换成 json 数据并渲染。
renderJson("users", userList)以”users”为根,仅将 userList 中的数据转换成 json数据并渲染。
renderJson(user)将 user 对象转换成 json 数据并渲染。
renderJson("{"age":18}"" )直接渲染 json 字符串。
renderJson(new String[]{"user", "blog"})仅将 setAttr("user", user)与 setAttr("blog", blog)设置的属性转换成 json 并渲染。使用 setAttr 设置的其它属性并不转换为 json。
renderFile("test.zip");渲染名为 test.zip 的文件,一般用于文件下载
renderText("Hello Jboot")渲染纯文本内容”Hello Jboot”。
renderHtml("Hello Html")渲染 Html 内容”Hello Html”。
renderError (404 , "test.html")渲染名为 test.html 的文件,且状态为 404。
renderError (500 , "test.html")渲染名为 test.html 的文件,且状态为 500。
renderNull()不渲染,即不向客户端返回数据。
render(new MyRender())使用自定义渲染器 MyRender 来渲染。
  1. Controller 供了 getPara 系列方法用来从请求中获取参数。getPara 系列方法分为两种类型。

第一种类型为第一个形参为 String 的 getPara 系列方法。该系列方法是对 HttpServletRequest.getParameter(String name) 的 封 装 , 这 类 方 法 都 是 转 调 了 HttpServletRequest.getParameter(String name)。

第二种类型为第一个形参为 int 或无形参的 getPara 系列方法。该系列方法是去获取 urlPara 中所带的参数值。getParaMap 与 getParaNames 分别对应 HttpServletRequest 的 getParameterMap 与 getParameterNames。

提供了如下的获取参数的方法:

方法调用返回值
getPara("title")返回页面表单域名为“title”参数值
getParaToInt("age")返回页面表单域名为“age”的参数值并转为 int 型
getPara(0)返回 url 请求中的 urlPara 参数的第一个值,如 http://localhost/controllerKey/method/v0-v1-v2 这个请求将 返回”v0”
getParaToInt(1)返回 url 请求中的 urlPara 参数的第二个值并转换成 int 型,如 http://localhost/controllerKey/method/2-5-9 这个请求将返回 5
getParaToInt(2)http://localhost/controllerKey/method/2-5-N8 这个 请求将返回 -8。注意:约定字母 N 与 n 可以表示负 号,这对 urlParaSeparator 为 “-” 时非常有用。
getPara()返回 url 请求中的 urlPara 参数的整体值,如 http://localhost/controllerKey/method/v0-v1-v2 这个 请求将返回”v0-v1-v2”

当然,为了方便,还提供了直接通过 bean 或者 model 来获取参数的方法,getModel 用来接收页面表单域传递过来的 model 对象,表单域名称以”modelName.attrName”方式命名,getModel 使用的 attrName 必须与数据表字段名完全一样。getBean 方法用于支持传统 Java Bean,包括支持使用 jfnal 生成器生成了 getter、setter 方法的 Model,页面表单传参时使用与 setter 方法相一致的 attrName,而非数据表字段名。 getModel 与 getBean 区别在于前者使用数表字段名而后者使用与 setter 方法一致的属性名进行数据注入。建议优先使用 getBean 方法。


// 定义Model,在此为Blog
public class Blog extends JbootModel<Blog> {
	
}

// 在页面表单中采用modelName.attrName形式为作为表单域的name 
<form action="/blog/save" method="post">
	<input name="blog.title" type="text"> 
	<input name="blog.content" type="text"> 
	<input value=" 交" type="submit">
</form>

@RequestMapping("/blog")
public class BlogController extends JbootController { 

	public void save() {
		// 页面的modelName正好是Blog类名的首字母小写 
		Blog blog = getModel(Blog.class);
		
		//如果表单域的名称为 "otherName.title"可加上一个参数来获取
		Blog blog = getModel(Blog.class, "otherName");
		
		//如果表单域的名称为 "title" 和 "content" 
		Blog blog = getModel(Blog.class, "");
	}
	
	// 或者 也可以写如下代码,但是注意,只能写一个save方法
	public void save(Blog blog) {
		// do your something
	}
	
}

当你书写完一个 controller 后,你就能对请求进行处理了,而调用服务我们后面会说到。

使用模板引擎初始化页面

我们的项目并不是前后端分离的,我们后端人员依旧要去前端写业务逻辑,这个时候就需要模板引擎来帮忙了。在 jboot 中我们如何在页面初始化的时候传递数据呢?

注意:这里提到的模板引擎,是 jfinal 的 enjoy,并不是 layui 的

使用 setAttr 即可

    setAttr("user", loginUser);
    // 也支持链式的
    setAttr("user", loginUser)
        .setAttr("user1", loginUser1)
        .setAttr("user2", loginUser2);

前端如何获取呢?

    #(loginUser1.name) 即可获取到他的对应属性

其他更多操作,参见 模板引擎(enjoy)

自定义返回 json 格式数据

jboot 给我们提供了默认两种返回 json 数据的方式

  1. 返回成功
    // 默认
    renderJson(RestResult.buildSuccess());
    // 自定义消息
    renderJson(RestResult.buildSuccess("OK"));

前端收到的 json 为

    // 默认
    {
        code : 0,
        msg : "请求成功"
    }
    // 自定义消息
    {
        code : 0,
        msg : "ok"
    }
  1. 返回失败
    // 默认
    renderJson(RestResult.buildError());
    // 自定义消息
    renderJson(RestResult.buildError("用户更新失败"));

前端收到的 json 为

    // 默认
    {
        code : 1,
        msg : "请求失败"
    }
    // 自定义消息
    {
        code : 1,
        msg : "用户更新失败"
    }

但是很多时候我们需要自定义消息,比如我希望返回如下数据

    {
        code : 0,
        msg : "请求成功",
        data : {
            src : "123123123",
            fileId : 1,
            title : "name"
        }
    }

那么我们可以通过如下方式解决

    // 构建一个线程安全的 hashMap
    ConcurrentHashMap<String, Object> data = new ConcurrentHashMap<String, Object>();
    // 存放数据
    data.put("src","123123123");
    data.put("fileId",1);
    data.put("title","name");
    // 回复
    renderJson(RestResult.buildSuccess(data));

错误的同理。

validator

这是帮助 controller 进行参数验证,当请求一个 url 携带参数的时候,我们不需要在 Action 中进行手动验证,可以让他独立于此,简化我们业务逻辑的书写。

下面看一个登录参数检验的例子 io.jboot.admin.validator.LoginValidator


package io.jboot.admin.validator;

import com.jfinal.core.Controller;
import io.jboot.admin.base.web.base.JsonValidator;

/**
 * 登录校验
 * @author Rlax
 *
 */
public class LoginValidator extends JsonValidator {

    @Override
    protected void validate(Controller c) {
        validateString("loginName", 4, 16, "用户名格式不正确");
        validateString("password", 6, 16, "密码格式不正确");
        validateString("capval", 4, 4, "验证码格式不正确");
        validateCaptcha("capval", "验证码不正确");
        
        // 可以书写更复杂的业务逻辑
    }
}

较为复杂的检验 io.jboot.admin.validator.system.ChangePwdValidator

package io.jboot.admin.validator.system;

import com.jfinal.core.Controller;
import io.jboot.admin.base.web.base.JsonValidator;
import io.jboot.admin.service.entity.model.User;
import io.jboot.admin.support.auth.AuthUtils;


/**
 * 修改密码校验器
 * @author Rlax
 *
 */
public class ChangePwdValidator extends JsonValidator {

    @Override
    protected void validate(Controller c) {
        String pwd =  c.getPara("user.pwd");
        String newPwd =  c.getPara("newPwd");
        String rePwd =  c.getPara("rePwd");

        validateRequiredString("user.pwd", "旧密码不能为空");
        validateRequiredString("newPwd", "新密码不能为空");
        validateRequiredString("rePwd", "确认密码不能为空");

        if(!newPwd.equals(rePwd)){
            addError("两次输入密码不一致,请重新输入!");
        }

        User user = AuthUtils.getLoginUser();

        if(!AuthUtils.checkPwd(pwd, user.getPwd(), user.getSalt())){
            // 错误返回
            addError("原密码不正确!");
        }
    }
}

然后我们只需要在需要检验参数的方法上面加上 @Before 注解即可

// 只接受 POST 提交,LoginValidator 进行参数检验
@Before( {POST.class, LoginValidator.class} )
public void postLogin() {
    // 省略代码
}

你也可以自定义检验的 validateRequired 方法,也即在他的父类 SaPlat\SaPlat-base\src\main\java\io\jboot\admin\base\web\base\JsonValidator.java 中添加方法,不难,请自行研究。

注意事项

  1. 服务调用后一定要检查其结果,当结果与预期的时候不符时,务必结束方法

例如:

    // 当保存不成功的时候,提前返回
    if (!filesService.save(files)) {
        renderJson(RestResult.buildError("文件上传失败,请重新尝试!503"));
        throw new BusinessException("文件上传失败,请重新尝试!503");
    }
  1. 方法名上写清楚注释,此方法是干嘛的,接受什么参数,做什么。

service

同样我们来看看目录层次结构

├─SaPlat-service-api
│  └─src
│     └─main
│         ├─java
│         │  └─io
│         │      └─jboot
│         │          └─admin
│         │              └─service
│         │                  └─api                  // 定义服务的接口 api,对于微服务,我们使用面向接口开发,所以在对一个服务写实现之前一定先写接口
│         │                      └─ge               // 根据数据库自动生成空的
│         └─resources                               // 自动生成的配置文件
├─SaPlat-service-entity
│  └─src
│     └─main
│         ├─java
│         │  └─io
│         │      └─jboot
│         │          └─admin
│         │              └─service
│         │                  └─entity
│         │                      ├─model            // 模型,与表对应,可以自定义
│         │                      │  └─base          // 基础模型,封装了表字段一一对应的基础
│         │                      └─status           // 状态,用于前端模板指令识别
│         │                          └─system       // 系统级别的状态
│         └─resources                               // 自动生成的配置文件
└─SaPlat-service-provider
    └─src
       └─main
           ├─java
           │  └─io
           │      └─jboot
           │          └─admin
           │              └─service
           │                  └─provider            // 服务提供类,实现了 api 的接口的服务
           │                      ├─app             // 启动器
           │                      └─ge              // 代码生成,用于生成基础的服务
           └─resources                              // 自动生成的配置文件
               └─sql                                // 复杂sql语句
                   └─system                         // 复杂sql语句

我们这里主要关心 SaPlat-service-provider 的服务实现

service-provider

在代码生成的时候,会根据数据库,为我们生成一个基础的服务实现类,你打开的时候,他应当是空的

如下

package io.jboot.admin.service.provider;

import io.jboot.aop.annotation.Bean;
import io.jboot.admin.service.api.EvaSchemeService;
import io.jboot.admin.service.entity.model.EvaScheme;
import io.jboot.core.rpc.annotation.JbootrpcService;
import io.jboot.service.JbootServiceBase;

import javax.inject.Singleton;

@Bean
@Singleton
public class EvaSchemeServiceImpl extends JbootServiceBase<EvaScheme> implements EvaSchemeService {

}

注意:我们需要手动添加 @JbootrpcService 注解,后面会提到为什么

他为我们实现基本的 增删改查分页 方法,可以直接调用,具体请查看他的父类 JbootServiceBase。

但是在他父类提供的方法之外,可以 JbootServiceBase 类的 DAO 进行处理

下面我会不定期的加一些例子,如果不懒的话=-=

根据某列值查询一个

    DAO.findFirstByColumn("structID", orgStructureId);

根据某列值查询列表

    DAO.findListByColumn("structID", orgStructureId);

根据多列值查询一个

    Columns columns = Columns.create();
    columns.eq("user_id",userId);
    columns.eq("role_id",roleId);
    // Columns 有其他的条件
    //      eq      相等        ==
    //      ne      不等        !=
    //      like    模糊查询    like
    //      gt      大于        >
    //      ge      大于等于    >=
    //      lt      小于        <
    //      le      小于等于    <=
    return DAO.findFirstByColumns(columns);

根据多列值查询列表

    Columns columns = Columns.create();
    columns.eq("user_id",userId);
    columns.eq("role_id",roleId);
    // Columns 有其他的条件
    //      eq      相等        ==
    //      ne      不等        !=
    //      like    模糊查询    like
    //      gt      大于        >
    //      ge      大于等于    >=
    //      lt      小于        <
    //      le      小于等于    <=
    return DAO.findListByColumns(columns);

自定义分页查询语句

当我们想自定义其他的语句查询如何解决呢?例如冯鑫遇到的根据多个 id 分页查询,

即:SELECT * FROM t_name WHERE 'id' IN (?,?,?) 其中 为数量不定,根据传过来的 id 进行复制,比如传过来 id 数组为 [1,2,3] 那么 sql 语句为:

SELECT * FROM t_name WHERE `id` IN (1,2,3)

亦或是使用 or 语句

SELECT * FROM t_name WHERE `id` = 1 OR `id` = 2 OR `id` = 3

我去查看他的 MysqlDialect ,发现他的 sql 语句只有 AND 拼接,原本想添加个源码的方法用于 OR 凭借,但是由于是 jar 包并且使用 maven 下载下来的,所以改 jar 包方法明显是不行的。

在我帮他处理这个问题的时候尝试多种方法,包括了在文件中自定义 sql,和在代码中拼接 sql

我在源码中查询到了他进行分页的逻辑,使用 limit ?,? 查询,第一个参数为偏移量,计算方式为 pageSize * (pageNumber - 1) ,第二个参数就是pageSize

但在文件中自定义 sql 的有个问题,后面参数数量不定的,所以也就是说 sql 语句不固定,将 sql 存放到文件中中去提取是没办法完成的,但是可以完成分页。

然后尝试在代码中拼接 sql ,通过 find() 方法,但是发现参数不好传递,虽然也可以通过某些方法完成,但是过于繁琐。

并且我不希望使用这种需要我们手动计算的方式,我去查看源码,找到一个方法

/**
 * 指定分页 sql 最外层以是否含有 group by 语句
 * <pre>
 * 举例:
 * paginate(1, 10, true, "select *", "from user where id>? group by age", 123);
 * </pre>
 */
public Page<M> paginate(int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras) {
	return doPaginate(pageNumber, pageSize, isGroupBySql, select, sqlExceptSelect, paras);
}

所以我将 sql 语句重新拼接,分成如下

public Page<QuestionnaireContent> findPageById(Long[] ids, int pageNumber, int pageSize){
        String select = "SELECT * ";
        StringBuilder sql = new StringBuilder("FROM questionnaire_content WHERE `id` IN(");
        for (int i = 0; i < ids.length; i++) {
            if (i != ids.length - 1){
               sql.append("?,");
            } else {
                sql.append("?)");
            }
        }
        return DAO.paginate(pageNumber, pageSize, false, select, sql.toString(), ids);
    }

这样也就完成了分页的 or 多条件查询。当然你也可以直接使用 doPaginateByFullSql 方法,参数相同,paginate 不过是对他的一种多态的封装。

服务调用过程

当你书写一个 controller,即可对前端的请求进行处理了。然后你书写一个服务就是对数据库进行某项操作了,但是如何在 controller 调用 service 提供的服务?

在 jboot 中,使用的是 rpc 远程调用的方式,他可以RPC通过新浪的 motan、或阿里的 dubbo 来完成的,在我们项目中使用的是 motan。

如何使用呢?

  1. 通过 @JbootrpcService 注解暴露服务到注册中心(代码生成没有这个注解,我们需要手动添加)
package io.jboot.admin.service.provider;

import io.jboot.aop.annotation.Bean;
import io.jboot.admin.service.api.EvaSchemeService;
import io.jboot.admin.service.entity.model.EvaScheme;
import io.jboot.core.rpc.annotation.JbootrpcService;
import io.jboot.service.JbootServiceBase;

import javax.inject.Singleton;

@Bean
@Singleton
@JbootrpcService
public class EvaSchemeServiceImpl extends JbootServiceBase<EvaScheme> implements EvaSchemeService {

}
  1. 在Controller中,也可以通过 @JbootrpcService 注解来获取服务,代码如下:
public class MyController extends JbootController{
    
    @JbootrpcService
    EvaSchemeServiceImpl rvaSchemeServiceImpl ;
    
    public void index(){
        renderJson("rvaScheme" ,rvaSchemeServiceImpl.findAll());
    }
    
}

这就是 rpc 调用的方式

常见问题

1. Java.lang.NullPointerException

我相信,这个异常可能会伴随你很久,时不时的就跳出来=-=常见的解决方式有

  1. 检查 controller 调用的 service 是否已经加上 rpc 注解
    @JbootrpcService
    EvaSchemeServiceImpl rvaSchemeServiceImpl ;
  1. 检查 serviceImp 是否加了 rpc 注解
@Bean
@Singleton
@JbootrpcService \\ 这里
public class EvaSchemeServiceImpl extends JbootServiceBase<EvaScheme> implements EvaSchemeService {

}
  1. 检查参数传递是否正确

  2. 查看实体类是否被初始化

    // 未初始化
    Modal modal;
    modal.setName("123"); 
    // 初始化
    Modal modal = new Modal();
    modal.setName("123"); 
  1. 检查 service 查询出来的数据是否为空
    Role role = roleService.findById(1);
    role.getName();
    // 当查询出来没有数据的时候,role 为 null,调用 getName 方法会报异常

一些笔记

常用工具类

获取当前登录用户

    User user = AuthUtils.getLoginUser();

获取项目运行路径

    String path = PathKit.getWebRootPath();

前端篇

其实相比来说,前端比后端简单太多,前端人员除了写页面基本没有什么要做的。项目本身不是前后端分离的,基本上前端的逻辑都需要后端人员来写,所以这个就造成了一个问题,除了前端的模板引擎以外,又引入了后端的模板引擎,这个问题实在让人头痛。不仅让前后端耦合太可怕,还使得前端逻辑处理过于复杂,但是又只能由后端人员来写,所以对后端人员来说是极其繁琐的。大多时候逻辑和页面的元素处理都是由后端人员解决,前端基本只提供了静态的页面的,并且在整合中问题也太多。目前没有想到什么更好的办法去解决这个问题。这里的前端篇只是记录我们后端人员在使用的过程中的一些处理问题的方式。

layui

我曾经给前端人员写了一个简单的开发笔记

saplat 前端开发文档 —— layui

这是最基础的入门,你必须在本地能够脱离的前提下(务必使用模板框架的那种方式啊!!!)才能去书写页面。

添加行、删除行

这是我写给谢印的一个示例,他演示了添加行、删除行、获取数据的问题。其中用到的 js、css 请在 saplat 前端开发文档 —— layui 下载模板框架,将它复制到更目录即可

<!DOCTYPE html>
<html>

	<head>
		<meta charset="utf-8" />
		<title></title>
		<link rel="stylesheet" href="static/js/layui/css/layui.css" />
		<link rel="stylesheet" href="static/css/style.css">
		<link rel="stylesheet" href="static/css/x-admin.css" type="text/css">
		<link rel="stylesheet" href="static/css/iconfont.css">

		<!--css start-->
		<style>

		</style>
		<!--css end-->
	</head>

	<body class="body">
		<!--content start-->
		<div class="layui-row">
			<div class="layui-btn-group">
				<button id="add" class="layui-btn layui-btn-small">添加</button>
				<button id="see" class="layui-btn layui-btn-small">点击在控制台查看表格json数据</button>
			</div>
			<table id="dateTable" lay-filter="dateTable"></table>
			
		</div>
		<!--content end-->
		<script type="text/javascript" src="static/js/layui/layui.all.js"></script>
		<script type="text/javascript" src="static/js/comm_notbar.js"></script>
		<script type="text/javascript" src="static/js/utils.js"></script>
		<script type="text/javascript" src="static/js/ztree/jquery-1.4.4.min.js"></script>
		<!--js start-->

		<script type="text/html" id="barDemo">
			<!--<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del" id="del">删除</a>-->
		</script>

		<script type="text/javascript">
			layui.use(['form', 'layer', 'element', 'table'], function() {
				var table = layui.table,
					layer = layui.layer,
					$ = layui.jquery;
				// 表格数据初始化
				table.cache.dateTable = new Array();
				layui.use(['table', 'layer'], function() {
					var table = layui.table;
					var tableIns = table.render({
						elem: '#dateTable' //指定原始表格元素选择器(推荐id选择器)
							,
						id: 'dateTable',
						even: true //开启隔行背景
							,
						size: 'sm' //小尺寸的表格
							,
						height: '300' //容器高度
							,
						contentType: 'application/json; charset=UTF-8',
						cols: [
								[{
									type: 'numbers',
									unresize: 'true'
								}, {
									field: 'name',
									title: '计划名称',
									width: 200,
									edit: true
								}, {
									field: 'startDate',
									title: '起始时间',
									width: 150,
									edit: true
								}, {
									field: 'endDate',
									title: '结束时间',
									width: 150,
									edit: true
								}, {
									field: 'content',
									title: '工作内容',
									width: 200,
									edit: true
								}, {
									field: 'action',
									title: '操作',
									align: 'center',
									unresize: true,
									rowspan: 1,
									toolbar: '#barDemo'
								}]
							]
							// , url: '#(ctxPath)/app/project/verfedSuccess'
							,
						loading: true,
						// 默认存在第一行
						data:[
							{
								name : "",
								startDate : "",
								endDate:"",
								content:""
							}
						]
					});
				});

				
				var index = table.cache.dateTable.length;
				$('#add').click(function(e) {
					var tr = $('<tr data-index="' + index + '"></tr>');
					var td1 = $('<td data-field="0" align="center"><div class="layui-table-cell laytable-cell-1-0 layuitable-cell-numbers">' + (index + 1) + '</div></td>');
					var td2 = $('<td data-field="name" data-edit="true" align="center"><div class="layui-table-cell laytable-cell-1-fa"></div></td>');
					var td3 = $('<td data-field="startDate" data-edit="true" align="center"><div class="layui-table-cell laytable-cell-1-fb"></div></td>');
					var td4 = $('<td data-field="endDate" data-edit="true" align="center"><div class="layui-table-cell laytable-cell-1-fc"></div></td>');
					var td5 = $('<td data-field="content" data-edit="true" align="center"><div class="layui-table-cell laytable-cell-1-fd"></div></td>');
					var td6 = $('<td data-field="action" align="center" data-off="true"><div class="layui-table-cell laytable-cell-1-action">' +
							'<a class="layui-btn layui-btn-danger layui-btn-xs" id="del">删除</a></div></td>')
					tr.append(td1).append(td2).append(td3).append(td4).append(td5).append(td6).appendTo($('tbody').first());
					table.cache.dateTable.push({
						"LAY_TABLE_INDEX": index,
						"name": "",
						"startDate": "",
						"endDate": ""
					});
					index++;
				});
				// 删除事件
				/**
				 * 防止出现生成的行高宽不对应的问题,所以务必要有一行起始行
				 */
				$(document).on("click","#del",function(e){
					var tr = $(this).parents("tr")
					var id =  parseInt(tr.attr("data-index"));
					if(id === 0){
						layer.msg("至少保留一项!");
						return;
					}
					tr.remove();
				    $.each(table.cache.dateTable,function(index,value){
				    	if(value.LAY_TABLE_INDEX === id){
				    		value.LAY_TABLE_INDEX = -1;
						}
				    });
				});
				// 查看数据
				$('#see').click(function(e){
					var res = new Array();
				    $.each(table.cache.dateTable,function(index,value){
				    	if(value.LAY_TABLE_INDEX != -1){
				    		res.push(value)
						}
				    });
					console.log(res);
				});
			});
		</script>
		<!--js end-->

	</body>

</html>

增加删除行

检验可编辑表格中的数据

曾经做表的时候遇到一个要求,表格可编辑,但是只能输入数字和小数点,所以写了这个案例

当输入的不合法的时候,会修改为上一次输入的数字,并且给出提示。

    /**
     * 表格使用方法渲染,见上一个例子,id 为 dateTable
     */
	table.on('edit(dateTable)', function (obj) {
        var value = obj.value,
            oldvalue = $(this).prev().text();
        var reg = /^\d+(?=\.{0,1}\d+$|$)/;
        if (!reg.test(value)) {
        	layer.msg("请输入合法的数字");
            obj.data[obj.field] = oldvalue; 
            $(this).val(oldvalue);
        }
    });
				

检验1
检验2

右下角固定按钮,点击右侧列表出现

需要有一个 json 数据,放在 json 文件夹下,文件名 list.html

{
	"list": [
		{
			"id": 1,
			"name": "aaaaa",
			"spell": "",
			"tableName": "某个html页面",
			"roleID": 1
		}, {
			"id": 2,
			"name": "bbbb",
			"spell": "",
			"tableName": "某个html页面",
			"roleID": 2
		}
	]
}

<!DOCTYPE html>
<html>

	<head>
		<meta charset="utf-8" />
		<title></title>
		<link rel="stylesheet" href="static/js/layui/css/layui.css" />
		<link rel="stylesheet" href="static/css/style.css">
		<link rel="stylesheet" href="static/css/x-admin.css" type="text/css">
		<link rel="stylesheet" href="static/css/iconfont.css">

		<!--css start-->
		<style>
		    html,body{
		        height: 98%;
		        width: 98%;
		    }
		
		    .layui-text p {
		        cursor: pointer;
		    }
		
		    .layui-text p:hover {
		        color: red;
		    }
		
		    #list{
		        padding: 10px;
		    }
		
		    #content {
		        width: 100%;
		        height: 100%;
		        border: none;
		        outline: none;
		    }
		</style>
		<!--css end-->
	</head>

	<body class="body">
		<!--content start-->
		<iframe style="display: none;" id="content">
		</iframe>
		<!--content end-->
		<script type="text/javascript" src="static/js/layui/layui.all.js"></script>
		<script type="text/javascript" src="static/js/comm_notbar.js"></script>
		<script type="text/javascript" src="static/js/utils.js"></script>
		<script type="text/javascript" src="static/js/ztree/jquery-1.4.4.min.js"></script>
		<!--js start-->
		<script type="text/javascript">
			layui.use(['form', 'layer', 'element'], function() {
				var layer = layui.layer,
					$ = layui.jquery,
					util = layui.util;

				var ul
				$.get("json/list.json", function(res) {
					var data = res.list;
					ul = $('<ul class="layui-timeline"></ul>');
					data.forEach(function(ele) {
						var li = $('<li class="layui-timeline-item"></li>');
						var i = $('<i class="layui-icon layui-timeline-axis" data-tableName="' + ele.tableName + '">&#xe63f;</i>');
						var lt = $('<div class="layui-timeline-content layui-text"></div>');
						var h = $('<h3 class="layui-timeline-title tableName" data-tableName="' + ele.tableName + '">' + ele.name + '</h3>');
						h.appendTo(lt);
						li.append(i).append(lt);
						ul.append(li);
					});
				});

				$(document).on('click','.tableName',function(e){
					console.log($(this).attr("data-tableName"));
					var s = $(this).attr("data-tableName")
					$('#content').css("display","").prop("src",s);
					
				});

				util.fixbar({
					bar1: "&#xe60a;",
					bar2: "&#xe604;",
					click: function(type) {
						if(type === 'bar1') {
							layer.open({
								title: '在线调试',
								id: 'list',
								type: 1,
								content: ul.html(),
								skin: 'layui-layer-molv',
								closeBtn: 1,
								offset: 'r',
								shadeClose: true,
								anim: 1,
								resize: false,
								area: ['20%', '100%']
							});
						} else if(type === 'bar2') {
							layer.msg("回到顶部");
						}
					}
				});
			    $(function (e) {
			       layer.tips("点击这里查看资料哦",'li[lay-type="bar1"]',{
			           tipsMore: true,
			           time:3000
			       });
			       layer.tips("点击这里回到顶部哈",'li[lay-type="bar2"]',{
			           tipsMore: true,
			           time:3000
			       });
			    });
							
			});
		</script>
		<!--js end-->

	</body>

</html>

表格自动计算

使用 jq 不断地进行选取即可,不难就不上源码了

念念不忘,必有回响。

如果觉得文章不错或者帮到了您,帮忙点点下面广告呗~谢谢啦~

评论