JavaScript学习总结(二)——延迟对象、跨域、模板引擎、弹出层、AJAX示例
发布日期:2021-05-09 00:51:56 浏览次数:23 分类:博客文章

本文共 47732 字,大约阅读时间需要 159 分钟。

一、AJAX示例

AJAX全称为“Asynchronous JavaScript And XML”(异步JavaScript和XML) 是指一种创建交互式网页应用的开发技术、改善用户体验,实现无刷新效果。

1.1、优点

不需要插件支持

优秀的用户体验

提高Web程序的性能

减轻服务器和带宽的负担

1.2、缺点

浏览器对XMLHttpRequest对象的支持度不足,几乎所有浏览器现在都支持

破坏浏览器“前进”、“后退”按钮的正常功能,可以通过简单的插件弥补

对搜索引擎的支持不足

1.3、jQuery AJAX示例

在HTML5中对原生的AJAX核心对象XMLHttpRequest进行升级,也就是XHR2,功能更加强大。 

jQuery对AJAX封装的非常好,这里以简单的商品管理为示例使用jQuery完成AJAX应用。

Product.java bean:

package com.gomall.bean;/*** * 产品 *  * @author Administrator * */public class Product {    /** 编号 */    private int id;    /** 名称 */    private String name;    /** 价格 */    private double price;    /** 图片 */    private String picture;    /** 详细 */    private String detail;    @Override    public String toString() {        return "Product [id=" + id + ", name=" + name + ", price=" + price + ", picture=" + picture + ", detail="                + detail + "]";    }    public Product(int id, String name, double price, String picture) {        super();        this.id = id;        this.name = name;        this.price = price;        this.picture = picture;    }    public Product(int id, String name, double price, String picture, String detail) {        super();        this.id = id;        this.name = name;        this.price = price;        this.picture = picture;        this.detail = detail;    }    public int getId() {        return id;    }    public void setId(int id) {        this.id = id;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public double getPrice() {        return price;    }    public void setPrice(double price) {        this.price = price;    }    public String getPicture() {        return picture;    }    public void setPicture(String picture) {        this.picture = picture;    }    public String getDetail() {        return detail;    }    public void setDetail(String detail) {        this.detail = detail;    }}

IProductService.java:

package com.gomall.service;import java.util.List;import com.gomall.bean.Product;public interface IProductService {    /**获得所有*/    List
getAll(); /**添加 * @return */ boolean add(Product entity); /**根据编号获得产品对象*/ Product findById(int id); /**根据编号获得产品对象 * @return */ boolean deleteById(int id);}

ProductService.java:

package com.gomall.service;import java.util.ArrayList;import java.util.List;import java.util.Random;import com.gomall.bean.Product;public class ProductService implements IProductService {    public static ArrayList
products; static { products = new ArrayList<>(); Random random = new Random(); for (int i = 1; i <= 10; i++) { Product product = new Product(i, "华为Mate9MHA-AL00/4GB RAM/全网通华为超级闪充技术双后摄设计" + random.nextInt(999), random.nextDouble() * 1000, "pic(" + i + ").jpg", "产品详细"); products.add(product); } } /* * (non-Javadoc) * * @see com.gomall.service.IProductService#getAll() */ @Override public List
getAll() { return products; } /* * (non-Javadoc) * * @see com.gomall.service.IProductService#add(com.gomall.bean.Product) */ @Override public boolean add(Product entity) { try { entity.setId(products.size() + 1); entity.setPicture("pic(" + entity.getId() + ").jpg"); // uploadify // 上传图片 products.add(entity); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /* * (non-Javadoc) * * @see com.gomall.service.IProductService#findById(int) */ @Override public Product findById(int id) { for (Product product : products) { if (product.getId() == id) { return product; } } return null; } /* * (non-Javadoc) * * @see com.gomall.service.IProductService#deleteById(int) */ @Override public boolean deleteById(int id) { try { Product product = findById(id); if (product != null) { products.remove(product); } } catch (Exception e) { e.printStackTrace(); return false; } return true; }}

ProductAction.java:

package com.gomall.action;import java.io.IOException;import java.io.PrintWriter;import java.util.Date;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.codehaus.jackson.map.ObjectMapper;import com.gomall.bean.Product;import com.gomall.service.IProductService;import com.gomall.service.ProductService;@WebServlet("/Product")public class ProductAction extends HttpServlet {    private static final long serialVersionUID = 1L;    public ProductAction() {        super();    }    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        /*模拟网络延时*/        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        request.setCharacterEncoding("utf-8");        response.setCharacterEncoding("utf-8");        String act = request.getParameter("act");        IProductService productService = new ProductService();        /**用于序列化json*/        ObjectMapper mapper = new ObjectMapper();        PrintWriter out=response.getWriter();                if (act.equals("getAll")) {            String json = mapper.writeValueAsString(productService.getAll());            out.append(json);        } else if (act.equals("area")) {            String callback=request.getParameter("callback");            out.append(callback+"('"+new Date()+"')");        } else if (act.equals("getJSONP")) {            String callback=request.getParameter("callback");            String json = mapper.writeValueAsString(productService.getAll());            out.append(callback+"("+json+")");        } else if (act.equals("getAllCORS")) {            /**向响应的头部中添加CORS信息*/            response.addHeader("Access-Control-Allow-Origin", "*");            response.addHeader("Access-Control-Allow-Methods", "GET,POST");                        String json = mapper.writeValueAsString(productService.getAll());            out.append(json);        } else if(act.equals("del")){            /**向响应的头部中添加CORS信息*/            response.addHeader("Access-Control-Allow-Origin", "*");            response.addHeader("Access-Control-Allow-Methods", "GET,POST");            int id=Integer.parseInt(request.getParameter("id"));            String json = mapper.writeValueAsString(productService.deleteById(id));            out.append(json);        }        else if(act.equals("add")){            /**向响应的头部中添加CORS信息*/            response.addHeader("Access-Control-Allow-Origin", "*");            response.addHeader("Access-Control-Allow-Methods", "GET,POST");            String name=request.getParameter("name");            double price=Double.parseDouble(request.getParameter("price"));            String detail=request.getParameter("detail");            Product entity=new Product(0, name, price, "",detail);            String json = mapper.writeValueAsString(productService.add(entity));            out.append(json);        }    }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

客户端跨域调用:

            
AJAX

商品管理

编号 图片 商品名 价格 详细 操作

添加

添加商品

 

运行结果:

删除:

二、延迟对象(Deferred)

deferred对象就是jQuery1.5版以后新增加的回调函数解决方案。

2.0、方法作为参数传递

在静态语言如C#中可以使用委托将方法作为参数传递,C++中可以使用方法指针,因为JavaScript是动态语言,方法作为参数传递非常便捷,示例如下:

示例1:

            

结果:

示例2:

            

结果:

函数自动提升

在Javascript中定义一个函数,有两种写法:

  function foo() { }

  var foo = function () { }

两种写法完全等价。但是在解析的时候,前一种写法会被解析器自动提升到代码的头部,因此违背了函数应该先定义后使用的要求,所以建议定义函数时,全部采用后一种写法。

示例:

            

结果:

当函数作为参数传递时应该在调用前判断,避免异常,示例:

            

结果:

回调方法在异步请求中的使用:

            

结果:

2.1、回调函数

先看一个示例:

首先,为什么要使用Deferred?

            
回调

student.json文件:{"name":"tom","id":"01"}

运行结果:

因为AJAX是异步执行的,类似高级语言中的多线程,当发起ajax请求时会有网络延迟,而代码并没有在$.get的位置被阻塞,alert先执行,但数据并没有从远程获取到,所以结果是undefined。

其实初学者经常会犯这种错误,如:

function getStudentById(id){                $.get("students.do",{"id":id},function(stu){                    return stu;                },"json");            }

上面的代码是有问题的,原因如前面的示例是一样的。怎么解决,如果你认为是异步带来的问题,当然通过同步是可以解决的,如:

$.ajax({                type:"get",                url:"student.json",                async:false,  /*非异步,同步*/                success:function(data){                    student=data;                }            });

结果:

如果将所有的ajax请求修改为同步的,则ajax的好处就大打折扣了,如果即要异步又要解决上面的问题,可以使用回调方法。

示例:

            
回调

结果:

 

从这里看回调很完美,其实不然,实际开发中要复杂得多,如当第一个ajax请求完成才可以完成第二个,当第二个完成才可以完成第三个,可能最一个请求要等前面的所有请求都成功时才允许执行或才有条件执行,如

使用ajax编辑用户信息,先加载用户对象,再加载省,加载市,加县,可能代码会这样写:

$.get("url1",function(){                $.get("url2",function(){                    $.get("url3",function(){                      ...                   });                });            });

当回调越来越多,嵌套越深,代码可读性就会越来越差。如果注册了多个回调,那更是一场噩梦,幸好从jQuery1.5开始出现了延迟对象(deferred),可以解决这个问题。

2.2、deferred.done

$.ajax()操作完成后,如果使用的是低于1.5.0版本的jQuery,返回的是XHR对象,你没法进行链式操作;如果高于1.5版,返回的是deferred对象,可以进行链式操作。

 

当延迟成功时调用一个函数或者数组函数,功能与原success类似。

语法:deferred.done(doneCallbacks[,doneCallbacks]) 

返回值:Deferred Object

该参数可以是一个函数或一个函数的数组。当延迟成功时,doneCallbacks被调用。回调执行是依照他们添加的顺序。一旦deferred.done()返回延迟对象,延迟对象的其它方法也可以链接到了这里,包括增加.done()方法。当延迟解决,doneCallbacks执行使用参数提供给 或 方法依照添加的顺序调用。

示例代码:

            
延迟对象(deferred)

运行结果:

2.3、deferred.fail

语法:deferred.fail(failCallbacks[,failCallbacks])

返回值:Deferred Object

当延迟失败时调用一个函数或者数组函数,功能与原回调方法error类似。

该参数可以是一个函数或一个函数的数组。当延迟失败时,doneCallbacks被调用。回调执行是依照他们添加的顺序。一旦deferred.fail()返回延迟对象,延迟对象的其它方法也可以链接到了这里,包括增加.done()方法。当延迟解决,doneCallbacks执行使用参数提供给 或 方法依照添加的顺序调用。

示例:

            
延迟对象(deferred)

运行结果:

2.4、deferred.always

语法:deferred.always(alwaysCallbacks,[alwaysCallbacks])

返回值:Deferred Object

当递延对象是解决(成功, )或拒绝(失败,rejected)时被调用添加处理程序,与回调方法complete类似。

示例:

            
延迟对象(deferred)

运行结果

成功时:

 

失败时:

2.5、deferred.then

deferred.then(doneFilter [, failFilter ] [, progressFilter ])

添加处理程序被调用时,递延对象得到解决或者拒绝,一次指定多个事件。

所有三个参数(包括progressCallbacks ,在jQuery的1.7 )可以是一个单独的函数或一个函数的数组。 其中一个参数,也可以为空,如果没有该类型的回调是需要的。或者,使用.done()或.fail()仅设置doneCallbacks或failCallbacks。当递延解决,doneCallbacks被调用。若递延代替拒绝,failCallbacks被调用。回调按他们添加的顺序执行。一旦deferred.then返回延迟对象,延迟对象的其它方法也可以链接到了这里,包括增加.then()方法。

示例:

            
延迟对象(deferred)

结果:

2.6、应用延迟对象

前面的示例中我们都是使用jQuery ajax返回的deferred对象,其实我们也可以在自定义的代码中使用deferred对象,恰当的使用deferred对象或以优雅的解决不少问题。

示例:

            
延迟对象(deferred)

失败时:

成功时:

promise()在原来的deferred对象上返回另一个deferred对象,后者只开放与改变执行状态无关的方法(比如done()方法和fail()方法),屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),从而使得执行状态不能被改变。

示例2:

            
Deferred
View Code

结果2:

2.7、总结

(1) $.Deferred() 生成一个deferred对象。

(2) deferred.done() 指定操作成功时的回调函数

(3) deferred.fail() 指定操作失败时的回调函数

(4) deferred.promise() 没有参数时,返回一个新的deferred对象,该对象的运行状态无法被改变;接受参数时,作用为在参数对象上部署deferred接口。

(5) deferred.resolve() 手动改变deferred对象的运行状态为"已完成",从而立即触发done()方法。

(6)deferred.reject() 这个方法与deferred.resolve()正好相反,调用后将deferred对象的运行状态变为"已失败",从而立即触发fail()方法。

(7) $.when() 为多个操作指定回调函数。

(8)deferred.then() 有时为了省事,可以把done()和fail()合在一起写,这就是then()方法。如果then()有两个参数,那么第一个参数是done()方法的回调函数,第二个参数是fail()方法的回调方法。如果then()只有一个参数,那么等同于done()。

(9)deferred.always() 这个方法也是用来指定回调函数的,它的作用是,不管调用的是deferred.resolve()还是deferred.reject(),最后总是执行。

三、跨域

互联网上的主机由IP来标识,为了方便记忆,创建了域名系统.域名与IP对应,域名的作用是不用让你记复杂的IP地址,能唯一定位资源,URL的格式是协议://主机名.公司名称.机构类型.地域类型:端口/路径,如http://www.zhangguo.com.cn:8080/products/list.do?id=1#a1 

3.1、什么是跨域

JavaScript同源策略的限制,A域名下的JavaScript无法操作B或是C域名下的对象,如下所示:

假设页面:http://store.company.com/index.html

 

客户端代码d05.html,http://localhost:8087/jQuery601_JAVA/d05.html

6.4.9、跨域AJAX请求

6.4.9、跨域AJAX请求

另一个域下面一般处理程序,http://localhost:12833/Action/FindUserById.ashx:

using System;using System.Collections.Generic;using System.Linq;using System.Web;namespace jQuery601_DotNet.Action{    ///     /// 根据用户编号获得用户    ///     public class FindUserById : IHttpHandler    {        public void ProcessRequest(HttpContext context)        {            context.Response.ContentType = "text/plain";            String name = "";            int id = Convert.ToInt32(context.Request.Params["id"]);            if (id == 1001)            {                name = "Mark";            }            else if (id == 1002)            {                name = "Jack";            }            context.Response.Write(name);        }        public bool IsReusable        {            get            {                return false;            }        }    }}

运行结果:

3.2、JSONP跨域

JSONP跨域是利用script脚本允许引用不同域下的js实现的,将回调方法带入服务器,返回结果时回调。

2.1、JSONP跨域原理

客户端:

                    

服务器:

String callback=request.getParameter("callback");            out.append(callback+"('"+new Date()+"')");

结果:

 

服务器返回一段javascript,通过指定的方法名调用。从图中可以看出,使用JSONP的形式调用已经不再是通过XMLHTTPRequest对象,而是同步调用。

3.3、jQuery使用JSONP跨域

在jQuery中内置了实现JSONP跨域的功能,如果指定为json类型,则会把获取到的数据作为一个JavaScript对象来解析,并且把构建好的对象作为结果返回。为了实现这个目的,他首先尝试使用JSON.parse()。如果浏览器不支持,则使用一个函数来构建。JSON数据是一种能很方便通过JavaScript解析的结构化数据。如果获取的数据文件存放在远程服务器上(域名不同,也就是跨域获取数据),则需要使用jsonp类型。使用这种类型的话,会创建一个查询字符串参数 callback=? ,这个参数会加在请求的URL后面。服务器端应当在JSON数据前加上回调函数名,以便完成一个有效的JSONP请求。如果要指定回调函数的参数名来取代默认的callback,可以通过设置$.ajax()的jsonp参数。

页面脚本:

6.4.9、跨域AJAX请求

6.4.9、跨域AJAX请求

服务器一般处理程序:

using System;using System.Collections.Generic;using System.Linq;using System.Web;namespace jQuery601_DotNet.Action{    ///     /// FindUserById 的摘要说明    ///     public class FindUserById : IHttpHandler    {        public void ProcessRequest(HttpContext context)        {            context.Response.ContentType = "text/plain";            String name = "";            int id = Convert.ToInt32(context.Request.Params["id"]);            if (id == 1001)            {                name = "Mark";            }            else if (id == 1002)            {                name = "Jack";            }            String callback = context.Request["callback"];            context.Response.Write(callback+"('"+name+"')");        }        public bool IsReusable        {            get            {                return false;            }        }    }}

运行结果:

服务器Servlet: 

package com.gomall.action;import java.io.IOException;import java.io.PrintWriter;import java.util.Date;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.codehaus.jackson.map.ObjectMapper;import com.gomall.service.IProductService;import com.gomall.service.ProductService;@WebServlet("/Product")public class Product extends HttpServlet {    private static final long serialVersionUID = 1L;    public Product() {        super();    }    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        request.setCharacterEncoding("utf-8");        response.setCharacterEncoding("utf-8");        String act = request.getParameter("act");        IProductService productService = new ProductService();        ObjectMapper mapper = new ObjectMapper();        PrintWriter out=response.getWriter();                if (act.equals("getAll")) {            String json = mapper.writeValueAsString(productService.getAll());            out.append(json);        } else if (act.equals("area")) {            String callback=request.getParameter("callback");            out.append(callback+"('"+new Date()+"')");        } else if (act.equals("getJSONP")) {            String callback=request.getParameter("callback");            String json = mapper.writeValueAsString(productService.getAll());            out.append(callback+"("+json+")");        }     }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

客户端:

            
AJAX

运行结果:

在jQuery中如果使用JSONP只需要将返回数据类型设置为jsonp就可以了,但是这种方法只支持get请求,不支持post请求;请求是同步的;服务器返回数据要处理,要添加回调函数,麻烦。

3.4、跨域资源共享(CORS)

同源策略(same origin policy)的限制下非同源的网站之间不能发送 ajax 请求的。

w3c 提出了跨源资源共享CORS即Cross Origin Resource Sharing(跨域源资源共享),就是我们所熟知的跨域请求。

跨域资源共享(CORS)是一种网络浏览器的技术规范,它为Web服务器定义了一种方式,允许网页从不同的域访问其资源。

CORS与JSONP相比:
1、 JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。
2、 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。
3、 JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS。

CORS 将请求分为两类:简单请求和非简单请求:

3.4.1.简单请求

支持get/post/put/delete请求,例如返回Access-Control-Allow-Origin:*,但是不允许自定义header且会忽略cookies,且post数据格式有限制,只支持‘text/plain','application/x-www-urlencoded'and'multipart/form-data',其中’text/plain'默认支持,后面两种需要下面的预检请求和服务器协商。

简单请求对应该规则,因此对简单请求的定义为:

(1) 请求方法是以下三种方法之一:

HEADGETPOST

(2)HTTP的头信息不超出以下几种字段:

AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

简单请求的部分响应头及解释如下:

Access-Control-Allow-Origin必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写"*"。

Access-Control-Allow-Credentials(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest2对象当中的withCredentials属性应保持一致,即withCredentials为true时该项也为true;withCredentials为false时,省略该项不写。反之则导致请求失败。

Access-Control-Expose-Headers(可选) – 该项确定XmlHttpRequest2对象当中getResponseHeader()方法所能获得的额外信息。通常情况下,getResponseHeader()方法只能获得如下的信息:

Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma

当你需要访问额外的信息时,就需要在这一项当中填写并以逗号进行分隔

示例:

服务器端:

package com.zhangguo.springmvc08.action;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet(name = "ProductServlet",value = "/pdt")public class ProductServlet extends HttpServlet {    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        doGet(request,response);    }    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        //设置允许CORS的域名,如果是所有则使用*        response.addHeader("Access-Control-Allow-Origin","*");        response.getWriter().write("{\"name\":\"Book\"}");    }}

客户端:

            
跨域

结果:

3.4.2.复杂请求

如说你需要发送PUT、DELETE等HTTP动作,或者发送Content-Type: application/json的内容就需要使用复杂请求了。

最先发送的是一种"预请求",此时作为服务端,也需要返回"预回应"作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行。预请求以OPTIONS形式发送,当中同样包含域,并且还包含了两项CORS特有的内容

代码:

结果:

Access-Control-Allow-Origin(必含) – 和简单请求一样的,必须包含一个域,不能是*号

Access-Control-Allow-Methods(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。

Access-Control-Allow-Headers(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。

Access-Control-Allow-Credentials(可选) – 和简单请求当中作用相同。

Access-Control-Max-Age(可选) – 以秒为单位的缓存时间,允许时应当尽可能缓存。

3.4.3、CORSFilter

CORSFilter是Apache官方提供一个支持CORS跨域的过滤器,详细说明: 

依赖:

4.0.0
com.zhangguo.cors
TestCors
1.0-SNAPSHOT
war
TestCors Maven Webapp
http://www.example.com
UTF-8
1.7
1.7
junit
junit
4.11
test
javax.servlet
jstl
1.2
javax.servlet
javax.servlet-api
3.0.1
provided
javax.servlet.jsp
jsp-api
2.1
provided
org.apache.tomcat
tomcat-catalina
8.5.33
com.fasterxml.jackson.core
jackson-core
2.5.2
com.fasterxml.jackson.core
jackson-databind
2.5.2
TestCors
maven-clean-plugin
3.0.0
maven-resources-plugin
3.0.2
maven-compiler-plugin
3.7.0
maven-surefire-plugin
2.20.1
maven-war-plugin
3.2.0
maven-install-plugin
2.5.2
maven-deploy-plugin
2.8.2

添加过滤器,尽量添加在最前面:

CorsFilter
org.apache.catalina.filters.CorsFilter
cors.allowed.origins
https://www.apache.org
cors.allowed.methods
GET,POST,HEAD,OPTIONS,PUT
cors.allowed.headers
Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers
cors.exposed.headers
Access-Control-Allow-Origin,Access-Control-Allow-Credentials
cors.support.credentials
true
cors.preflight.maxage
10
CorsFilter
/*

 

如果使用了Spring MVC,请开启Spring对OPTIONS的支持:

dispatchOptionsRequest
true

客户端:

            
跨域

服务器:

package com.zhangguo.springmvc08.action;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.Enumeration;@WebServlet(name = "UserServlet", value = "/u")public class UserServlet extends HttpServlet {    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        doGet(request, response);    }    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        response.getWriter().write("{\"name\":\"Book\"}");    }    @Override    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        doGet(req, resp);    }    @Override    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        doGet(req, resp);    }    @Override    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        doGet(req, resp);    }}

结果:

web.xml

users/tom
springMVC
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:spring/spring-mvc.xml
dispatchOptionsRequest
true
1
true
springMVC
/
CORS
com.thetransactioncompany.cors.CORSFilter
cors.allowOrigin
http://127.0.0.1:8020
cors.supportedMethods
POST,GET,OPTIONS,DELETE,PUT
cors.supportedHeaders
Content-Type,Accept,Origin,XRequestedWith,ContentType,LastModified
cors.exposedHeaders
SetCookie
cors.supportsCredentials
true
CORS
/*
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
encodingFilter
/*
View Code

3.4.4、Servlet支持CORS

通过修改请求头部门信息可以实现Servlet完成复杂跨域功能,示例如下:

后台:

package com.zhangguo.springmvc08.action;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.Enumeration;@WebServlet(name = "ProductServlet", value = "/pdt")public class ProductServlet extends HttpServlet {    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        doGet(request, response);    }    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        //获得所有头部信息        Enumeration
items=request.getHeaderNames(); String headers="Content-Type,Accept,Origin,XRequestedWith,ContentType,LastModified,Content-Type,ContentType,content-type"; while(items.hasMoreElements()){ headers+=","+items.nextElement(); } //设置允许CORS的域名,如果是所有则使用* response.addHeader("Access-Control-Allow-Origin", "http://127.0.0.1:8020"); response.addHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, TRACE, OPTIONS,PUT,DELETE"); response.addHeader("Access-Control-Request-Headers", "Origin,X-Requested-With,Content-Type,Accept"); response.addHeader("Access-Control-Allow-Credentials", "true"); response.getWriter().write("{\"name\":\"Book\"}"); } @Override protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.addHeader("Access-Control-Allow-Headers", "Content-type"); doGet(req, resp); } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); }}

这里在实际使用中有遇到,所有支持的头部一时可能不能完全写出来,而又不想在这一层做过多的判断,没关系,事实上通过request的header可以直接取到Access-Control-Request-Headers,直接把对应的value设置到Access-Control-Allow-Headers即可

前台:

            
跨域

结果:

3.4.5、Spring MVC4.2+ CORS注解

Spring MVC4.2 及以上增加了对CORS的支持

一个应用可能会有多个 CORS 配置,并且可以设置每个 CORS 配置针对一个接口或一系列接口或者对所有接口生效。

对第一种情况,如果想要对某一接口配置 CORS,可以在方法上添加 CrossOrigin 注解:

@CrossOrigin(origins = {"http://localhost:9000", "null"})@RequestMapping(value = "/test", method = RequestMethod.GET)public String greetings() {    return "{\"project\":\"just a test\"}";}

第二种情况,如果想对一系列接口添加 CORS 配置,可以在类上添加注解,对该类声明所有接口都有效:

@CrossOrigin(origins = {"http://localhost:8080", "null"})@RestControllerpublic class HomeController{}

第三种情况,添加全局配置,则需要添加一个配置类:

@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter {    @Override    public void addCorsMappings(CorsRegistry registry) {        registry.addMapping("/**")                .allowedOrigins("http://localhost:9000", "null")                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")                .maxAge(3600)                .allowCredentials(true);    }}

可以通过添加 Filter 的方式,配置 CORS 规则,并手动指定对哪些接口有效。

@Beanpublic FilterRegistrationBean corsFilter() {    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();    CorsConfiguration config = new CorsConfiguration();    config.setAllowCredentials(true);    config.addAllowedOrigin("http://localhost:9000");    config.addAllowedOrigin("null");    config.addAllowedHeader("*");    config.addAllowedMethod("*");    source.registerCorsConfiguration("/**", config); // CORS 配置对所有接口都有效    FilterRegistrationBean bean = newFilterRegistrationBean(new CorsFilter(source));    bean.setOrder(0);    return bean;}

也可以修改配置文件:

员工管理的跨域综合示例

后台REST服务:

package com.zhangguo.springmvc08.controller;import com.zhangguo.springmvc08.entity.User;import com.zhangguo.springmvc08.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.List;@RestController@RequestMapping(path = "/emps")public class EmpController extends BaseController {    @Autowired    UserService userService;    @RequestMapping(path = "")    public AjaxState getAllEmps(HttpServletRequest request, HttpServletResponse response) {        List
users=userService.queryAllUsers(); boolean result=users!=null; return new AjaxState(result?"success":"error",users,result?"获得数据成功!":"获得数据失败!"); } @RequestMapping(path = "/{id}", method = RequestMethod.GET) public AjaxState getEmpById(@PathVariable int id) { User user=userService.getUserById(id); boolean result=user!=null; return new AjaxState(result?"success":"error",user,result?"获得数据成功!":"获得数据失败!"); } @RequestMapping(path = "", method = RequestMethod.POST) public AjaxState addEmp(@RequestBody User user) { boolean result=userService.addUser(user); return new AjaxState(result?"success":"error",user,result?"添加成功!":"添加失败"); } @RequestMapping(path = "", method = RequestMethod.PUT) public AjaxState updateEmp(@RequestBody User user) { boolean result=userService.editUser(user); return new AjaxState(result?"success":"error",user,result?"修改成功!":"修改失败"); } @RequestMapping(path = "/{id}", method = RequestMethod.DELETE) public AjaxState deleteEmpById(@PathVariable int id) { Boolean result=userService.deleteUser(id); return new AjaxState(result?"success":"error",id,result?"删除成功!":"删除失败"); }}class AjaxState{ public String state; public Object data; public String message; public AjaxState(String state, Object data, String message) { this.state = state; this.data = data; this.message = message; } public AjaxState(){}}
View Code

Spring配置文件:

View Code

前端:

    
员工管理

员工管理

编号 姓名 生日 地址 电话 操作
用户信息

View Code

运行结果:

 其它跨域示例:

.Net服务器一般处理程序代码:

using System;using System.Collections.Generic;using System.Linq;using System.Web;namespace jQuery601_DotNet.Action{    ///     /// FindUserById 的摘要说明    ///     public class FindUserById : IHttpHandler    {        public void ProcessRequest(HttpContext context)        {            context.Response.ContentType = "text/plain";            context.Response.Headers.Add("Access-Control-Allow-Origin","*");            context.Response.Headers.Add("Access-Control-Allow-Methods", "GET,POST");            String name = "";            int id = Convert.ToInt32(context.Request.Params["id"]);            if (id == 1001)            {                name = "Mark";            }            else if (id == 1002)            {                name = "Jack";            }            context.Response.Write(name);        }        public bool IsReusable        {            get            {                return false;            }        }    }}

 客户端脚本:

6.4.9、跨域AJAX请求

6.4.9、跨域AJAX请求

运行结果:

从上图可以看到实现跨域且为异步请求。

Java Servlet后台脚本:

package com.gomall.action;import java.io.IOException;import java.io.PrintWriter;import java.util.Date;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.codehaus.jackson.map.ObjectMapper;import com.gomall.service.IProductService;import com.gomall.service.ProductService;@WebServlet("/Product")public class Product extends HttpServlet {    private static final long serialVersionUID = 1L;    public Product() {        super();    }    protected void doGet(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        request.setCharacterEncoding("utf-8");        response.setCharacterEncoding("utf-8");        String act = request.getParameter("act");        IProductService productService = new ProductService();        ObjectMapper mapper = new ObjectMapper();        PrintWriter out=response.getWriter();                if (act.equals("getAll")) {            String json = mapper.writeValueAsString(productService.getAll());            out.append(json);        } else if (act.equals("area")) {            String callback=request.getParameter("callback");            out.append(callback+"('"+new Date()+"')");        } else if (act.equals("getJSONP")) {            String callback=request.getParameter("callback");            String json = mapper.writeValueAsString(productService.getAll());            out.append(callback+"("+json+")");        } else if (act.equals("getAllCORS")) {            /**向响应的头部中添加内容*/            response.addHeader("Access-Control-Allow-Origin", "*");            response.addHeader("Access-Control-Allow-Methods", "GET,POST");                        String json = mapper.writeValueAsString(productService.getAll());            out.append(json);        }     }    protected void doPost(HttpServletRequest request, HttpServletResponse response)            throws ServletException, IOException {        doGet(request, response);    }}

客户端代码:

            
AJAX

运行结果:

 

 

3.4.6、IE8实现CORS跨域的问题

a)、如果认为每次需要修改HTTP头部比较麻烦,在java中可以使用过滤器,.Net可以使用Module或HttpHandler全局注册(注册到Web.Config中,部署时还需要注意)。

b)、如果需要考虑IE8实现CORS则要插件支持,因为IE8并没有完全支持CORS。

插件名称:javascript-jquery-transport-xdr

github:

示例代码:

            
AJAX

运行结果:

3.4.7、.Net Core跨域

方法一、修改Web.config文件

  
  
    
    
    
    
    
    
    
    
 
View Code

方法二、手动修改HttpResponse对象的头部信息,与Java一样

方法三、使用Microsoft.AspNetCore.Cors

#region 程序集 Microsoft.AspNetCore.Cors, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60// C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.cors\2.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Cors.dll#endregionusing System;using Microsoft.AspNetCore.Cors.Infrastructure;namespace Microsoft.AspNetCore.Cors{    //    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]    public class EnableCorsAttribute : Attribute, IEnableCorsAttribute    {        //        // 摘要:        //     Creates a new instance of the Microsoft.AspNetCore.Cors.EnableCorsAttribute with        //     the default policy name defined by Microsoft.AspNetCore.Cors.Infrastructure.CorsOptions.DefaultPolicyName.        public EnableCorsAttribute();        //        // 摘要:        //     Creates a new instance of the Microsoft.AspNetCore.Cors.EnableCorsAttribute with        //     the supplied policy name.        //        // 参数:        //   policyName:        //     The name of the policy to be applied.        public EnableCorsAttribute(string policyName);        //        public string PolicyName { get; set; }    }}

1)、using Microsoft.AspNetCore.Cors;

2)、使用特性    [EnableCors]

using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using Microsoft.AspNetCore.Cors;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;namespace WebApplication1.Controllers{    [Route("api/[controller]")]    [ApiController]    [EnableCors]    public class TaskController : ControllerBase    {}}
View Code

配置Startup.cs文件:

public void ConfigureServices(IServiceCollection services)        {            services.AddCors(options =>            {                options.AddDefaultPolicy(                    builder =>                    {                        builder.WithOrigins("http://www.163.com",                                            "http://www.cnblogs.com");                    });                options.AddPolicy("AnotherPolicy",                    builder =>                    {                        builder.WithOrigins("http://www.baidu.com")                                            .AllowAnyHeader()                                            .AllowAnyMethod();                    });            });            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);        }
services.AddCors(p => p.AddDefaultPolicy(                adp => adp.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials())                );
View Code

 

3.5、小结

当然除了兼容老浏览器的jsonp跨域与HTML5中的CORS跨域还有很多其它办法如利用iframe和location.hash、window.name实现的跨域数据传输、使用HTML5 postMessage、利用flash等办法。个人认为CORS应该才是未来主要的跨域选择,其它的方法都只是hack。

四、弹出层

前面AJAX示例中添加功能如果放在一个弹出层中布局会更加紧凑一些,像登录,提示信息经常会需要弹出层。

常见的弹出层有:FancyBox,LightBox,colorBox,artDialog,BlockUI,Layer等,这里介绍腾讯开源的artDialog,轻量,实用。

artDialog是一个设计得十分巧妙的对话框组件,小巧身材却拥有丰富的接口与漂亮的外观。

特点是自适应内容、优雅的接口、细致的体验、跨平台兼容、轻量实用。

项目源码:

帮助信息:

文档与示例:

AngularJS 版本:

使用方法:

            
artDialog

运行结果:

属性:

// 对齐方式    //align: 'bottom left',        // 是否固定定位    //fixed: false,        // 对话框叠加高度值(重要:此值不能超过浏览器最大限制)    //zIndex: 1024,    // 设置遮罩背景颜色    backdropBackground: '#000',    // 设置遮罩透明度    backdropOpacity: 0.7,    // 消息内容    content: 'Loading..',        // 标题    title: '',    // 对话框状态栏区域 HTML 代码    statusbar: '',        // 自定义按钮    button: null,        // 确定按钮回调函数    ok: null,        // 取消按钮回调函数    cancel: null,    // 确定按钮文本    okValue: 'ok',        // 取消按钮文本    cancelValue: 'cancel',    cancelDisplay: true,        // 内容宽度    width: '',        // 内容高度    height: '',        // 内容与边界填充距离    padding: '',        // 对话框自定义 className    skin: '',    // 是否支持快捷关闭(点击遮罩层自动关闭)    quickClose: false,    // css 文件路径,留空则不会使用 js 自动加载样式    // 注意:css 只允许加载一个    cssUri: '../css/ui-dialog.css',

事件:

/**     * 显示对话框     * @name artDialog.prototype.show     * @param   {HTMLElement Object, Event Object}  指定位置(可选)     */        /**     * 显示对话框(模态)     * @name artDialog.prototype.showModal     * @param   {HTMLElement Object, Event Object}  指定位置(可选)     */    /**     * 关闭对话框     * @name artDialog.prototype.close     * @param   {String, Number}    返回值,可被 onclose 事件收取(可选)     */    /**     * 销毁对话框     * @name artDialog.prototype.remove     */    /**     * 重置对话框位置     * @name artDialog.prototype.reset     */    /**     * 让对话框聚焦(同时置顶)     * @name artDialog.prototype.focus     */    /**     * 让对话框失焦(同时置顶)     * @name artDialog.prototype.blur     */    /**     * 添加事件     * @param   {String}    事件类型     * @param   {Function}  监听函数     * @name artDialog.prototype.addEventListener     */    /**     * 删除事件     * @param   {String}    事件类型     * @param   {Function}  监听函数     * @name artDialog.prototype.removeEventListener     */    /**     * 对话框显示事件,在 show()、showModal() 执行     * @name artDialog.prototype.onshow     * @event     */    /**     * 关闭事件,在 close() 执行     * @name artDialog.prototype.onclose     * @event     */    /**     * 销毁前事件,在 remove() 前执行     * @name artDialog.prototype.onbeforeremove     * @event     */    /**     * 销毁事件,在 remove() 执行     * @name artDialog.prototype.onremove     * @event     */    /**     * 重置事件,在 reset() 执行     * @name artDialog.prototype.onreset     * @event     */    /**     * 焦点事件,在 foucs() 执行     * @name artDialog.prototype.onfocus     * @event     */    /**     * 失焦事件,在 blur() 执行     * @name artDialog.prototype.onblur     * @event     */

该插件使用比较简单,可以看示例与源代码。

五、模板引擎

在AJAX示例中javascript中有大量的html字符串,html中有一些像onclick样的javascript,这样javascript中有html,html中有javascript,代码的偶合度很高,不便于修改与维护,使用模板引擎可以解决问题。

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。前后端都有模板引擎,比如T4、FreeMarker、Velocity,这里主要讲前端模板引擎:

上图是常见的一些前端模板引擎,速度相对快的是artTemplate,与artDialog是同一个作者,当然一个好的模板引擎不仅是速度还有很多方面都关键。

源码与帮助:

5.1、Hello World

示例代码:

            
artTemplate

运行结果:

生成的代码:

姓名:Tom

  • 1 看书
  • 2 上网
  • 3 运动
  • 4 电影
  • 5 购物

5.2、方法

当前最新的版本是4.2与3.x版本有一些区别,4.x版本的方法与选项如下:

// 模板名filename: null,// 模板语法规则列表rules: [nativeRule, artRule],// 是否开启对模板输出语句自动编码功能。为 false 则关闭编码输出功能// escape 可以防范 XSS 攻击escape: true,// 启动模板引擎调试模式。如果为 true: {cache:false, minimize:false, compileDebug:true}debug: detectNode ? process.env.NODE_ENV !== 'production' : false,// bail 如果为 true,编译错误与运行时错误都会抛出异常bail: true,// 是否开启缓存cache: true,// 是否开启压缩。它会运行 htmlMinifier,将页面 HTML、CSS、CSS 进行压缩输出// 如果模板包含没有闭合的 HTML 标签,请不要打开 minimize,否则可能被 htmlMinifier 修复或过滤minimize: true,// 是否编译调试版compileDebug: false,// 模板路径转换器resolveFilename: resolveFilename,// 子模板编译适配器include: include,// HTML 压缩器。仅在 NodeJS 环境下有效htmlMinifier: htmlMinifier,// HTML 压缩器配置。参见 https://github.com/kangax/html-minifierhtmlMinifierOptions: {    collapseWhitespace: true,    minifyCSS: true,    minifyJS: true,    // 运行时自动合并:rules.map(rule => rule.test)    ignoreCustomFragments: []},// 错误事件。仅在 bail 为 false 时生效onerror: onerror,// 模板文件加载器loader: loader,// 缓存中心适配器(依赖 filename 字段)caches: caches,// 模板根目录。如果 filename 字段不是本地路径,则在 root 查找模板root: '/',// 默认后缀名。如果没有后缀名,则会自动添加 extnameextname: '.art',// 忽略的变量。被模板编译器忽略的模板变量列表ignore: [],// 导入的模板变量imports: runtime

方法:

与3.x版本类似,但config方法被取消,更多新查看如下链接

方法:

1)、template(id, data)

根据 id 渲染模板。内部会根据document.getElementById(id)查找模板。

如果没有 data 参数,那么将返回一渲染函数。

2)、template.compile(source, options)

将返回一个渲染函数。演示

3)、template.render(source, options)

将返回渲染结果。

4)、template.helper(name, callback)

添加辅助方法,让模板引擎调用自定义的javascript方法。

5)、template.config(name, value)

示例代码:

            
artTemplate

运行结果:

 

4.x示例:

            
模板引擎
View Code

结果:

5.3、与AJAX结合应用

示例脚本:

            
商品管理

商品管理

编号 图片 商品名 价格 详细 操作

添加

添加商品

运行结果:

六、示例下载

coding:

服务器:

github:   

第二版:

前端:

后台:

七、视频

八、作业

8.1、请完成一个简单的分布式应用,使用Java作为服务器对外发布服务,PC客户端实现“品牌或商品”的管理,移动端实现如下列表:

  • a)、分析出数据库的设计,建库、建表 (MySQL)
  • b)、创建后台项目,实现5个服务,可以使用RETSFul (IDEA)
  • c)、创建PC Web项目(HBuilder),使用AJAX消费后台提供的5个服务 ,完成增加、删除、修改、查询功能
  • d)、创建App项目(HBuilder),先完成页面的静态布局,使用AJAX调用服务
  • e)、注意跨域、可以选择三方的UI框架,但界面需完全一样
  • f)、在PC Web中,添加,删除,编辑、详细功能请使用artDialog弹出层
  • g)、在PC Web与App中请使用artTemplate渲染页面HTML

系统结构如下:

 参考网站:

 

上一篇:Node.js开发Web后台服务
下一篇:CSS3与页面布局学习总结(八)——浏览器兼容与前端性能优化

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2025年04月14日 14时25分24秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

Nmap渗透测试指南之探索网络 2023-01-23
Nmap渗透测试指南之防火墙/IDS逃逸、信息搜集 2023-01-23
Nmap端口服务 之 CentOS7 关于启动Apache(httpd)服务、telnet服务、smtp服务、ftp服务、sftp服务、snmp服务 2023-01-23
PHP系列:PHP 基础编程 2(时间函数、数组---实现登录&注册&修改) 2023-01-23
PHP系列:使用PHP实现登录注册功能的完整指南 2023-01-23
Python&aconda系列:cmd/powershell/anaconda prompt提示“系统找不到指定的路径”(亲测有效) 2023-01-23
Python&aconda系列:conda踩坑记录2.UnsatisfiableError: The following specifications were found to be incompa 2023-01-23
Python&aconda系列:Jupyter Notebook快速上手、深度学习库PyTorch安装 2023-01-23
Python&aconda系列:(W&L)Conda使用faiss-gpu报错及解决办法、安装numpy的坑、cmd执行Python脚本找不到第三方库、安装tensorflow-gpu时遇到的from 2023-01-23
python&anconda 系列:Pycharm在debug问题的N种解决方案(一般程序、web方向、人工智能方向) 2023-01-23
python&anconda系列(亲测有效):tensorflow AttributeError: ‘str’ object has no attribute ‘decode’ 2023-01-23
python&anconda系列:tf.keras.backend.get_session()和keras.backend.get_会话()返回不同的会话对象(待解答) 2023-01-23
"WARNING: Increasing RAM size to 1GB" and "Cannot set up guest memory 'xxx.ram': Invalid argument". 2023-01-23
#if 0 #elif 1 #else #endif 用法 2023-01-23
#include <gdiplus.h>出错 2023-01-23
$ajax({}).done 和 $ajax({}) success 区别 2023-01-23
'ascii' codec can't encode characters in position 0-4: ordinal not in range(128) 2023-01-23
(反射+内省机制的运用)处理jdbc的结果集 2023-01-23
(反射+内省机制的运用)简单模拟spring IoC容器的操作 2023-01-23
#C8# UVM中的factory机制 #S8.2.3# 重载 component 哪些情形 2023-01-23