文章目录
四. tomcat服务器(web项目代码的容器)CS和BS的异同点新建项目-部署-运行-访问新建web项目,在tomcat部署运行和访问
在idea新建java web项目web项目的配置lib文件夹和artifact的关系部署进一步说明什么叫web项目
四:JDBC和DAOjava se + 数据库项目实战一些概念
项目架构项目需求
五. servelet一个简单的例子xxxServlet类xxxServlet类实现第一个功能-获取来自客户端的数据http request(http请求)获取数据重写继承的哪一个方法以及实例化哪一个xxxServlet类xxxServlet类的设计对请求-响应执行顺序的理解
xxxServlet类实现第二个功能-调用DAO完成对数据库的操作xxxServlet类实现第三个功能-在控制台打印相应的操作成功
处理http request中的中文乱码Servlet的继承树和service方法继承树service方法
servlet的生命周期servlet的初始化时机tomcat容器与servlet
http协议会话-session-http是无状态的http是无状态的会话跟踪session保存作用域
服务器端转发和客户端重定向服务器端转发客户端重定向
thymeleaf为什么使用thymeleaf配置thymeleaf的步骤如何实现大量统一格式的数据在html页面的展示
回顾servlet保存作用域作用域解释HttpServletRequest容器HttpSession容器ServletContext容器
绝对路径和相对路径区分超链接和超文本引用
正式进入后端
四. tomcat服务器(web项目代码的容器)
CS和BS的异同点
CS:client-server架构模式 充分利用客户端机器的资源,减轻服务器的负荷(将一部分安全要求不高额计算或者存储任务放在客户端,减轻服务器压力以及减轻网络负荷)
但是,客户端需要单独安装,也需要升级维护(成本较高,以前的升级很麻烦,打补丁包,可能有多个)
BS:browser-server架构模式 不需要单独安装客户端
但是所有的计算、存储任务都在服务器吨,服务器负荷重。同时,计算结果要通过网络传输给用户,因此浏览器和服务器会进行频繁的网络通信,网络负荷重
新建项目-部署-运行-访问
tomcat是服务器端的一个webcontainer,免费,体积小,性能还行
可以在tomcat中安装项目,这个过程又叫做“部署”deploy,也就是把写好的程序拷贝到相应的项目目录下
项目名称又叫做context root
bin:可执行文件目录 conf:配置文件目录 lib:tomcat使用java和C开发,依赖 logs:日志 webapps:部署项目的空间,早期做法,现在可以自定义,workspace work:项目的目录
配置环境变量 tomcat中的bin/start.bat需要jvm,因此需要告诉tomcat服务器jdk的位置(jre包含在jdk中)。也就是在环境变量中创建JDK_HOME
启动tomcat服务(就像mysql服务器),访问主页 先启动bin/start.bat,然后输入url,http://localhost:8080/ 注意不是https,而是http,如果是前者,报错 启动start.bat之后,可以看到一个进程msedge.exe占用了8080端口 使用netstat -ano|findstr 8080命令可以查看
新建web项目,在tomcat部署
在webapps文件夹下,新建文件夹baidu,作为web项目目录
在baidu目录下新建文件夹WEB-INF,目录名全部大写
一个web项目基本结构就是这样
运行和访问
在baidu目录下导入项目文件 然后重启start.bat,也就是tomcat服务,再在浏览器输入http://localhost:8080/baidu/demo09.html就可以看到 这是很激动人心的时刻,就是说我们从客户端访问到了服务器的资源 曾经的我们打开.html文件是在本地,打开页面之后,其地址是 现在资源的地址变成了http://localhost:8080/baidu/demo09.html
localhost:8080通过ip地址和端口号定位到某台计算机上的tomcat服务器 baidu/demo09.html通过路径定位到服务器上的项目
舒服了,这才是互联网
在idea新建java web项目
(1)在idea中配置tomcat服务器。就是首先得让idea找到tomcat
(2)添加框架支持。自动将我们的java module变成web application。帮我们创建了web项目的文件目录 相比于前面我们直接在tomcat安装路径下创建的目录还是有区别 tomcat/webapps/baidu目录下默认会有一个WEB-INF文件夹,我们的项目和WEB-INF在同一级目录 (3)编辑web工程的结构(edit configuration) 也就是加上本地或者远程的tomcat服务器 左起第二个框,是select run/debug configuration,这个地方要选对,如果是java se程序,那么就是程序名;如果是java web项目,就是你配置的服务器名
点击下拉菜单,可以编辑当前选中的configuration 然后就可以进行部署等相关设置
(4)部署web项目 也是在edit configuration中完成部署
web项目的配置
有几个比较重要的按键: (1)右键module,add framework support, 添加框架支持,比如说让一个java se module变成一个java web module (2)file->project structure,对整个项目的设置进行调整 挺恶心的,全是英文
facets:本意是方面,部分。可以用于添加框架支持。比如说将一个java se module变成一个java web module web项目还需要部署到tomcat服务器中。更准确地说,一个项目要能够部署到服务器中,就必须按照规范先变成一个web项目(创建标准格式的目录),然后再部署到服务器。
之前我们是在tomcat安装目录下的webapps目录下去创建了一个项目文件夹baidu,然后在baidu目录下创建了一个WEB-INF文件夹,接着将项目源码放在和WEB-INF同一级目录下。完成以后,先启动tomcat服务器,然后就可以通过url=http://localhost:8888/baidu/hello.html在浏览器访问到相应的资源
在idea中,web项目的目录结构都不一样了。并不是将源代码部署到服务器,而是将一个部署包部署到tomcat。这个部署包叫做artifact,本意是指人工制品。
artifacts:web项目部署包。
lib文件夹和artifact的关系
lib是我们自定义的依赖,artifact是部署包,artifact中包含了引用lib的信息
先有artifact,如果之后有了新的依赖,必须手动更新artifact。要么删掉重建,要么fix
为了写DAO,或者引入数据库连接池,我们都写过依赖。不管是idea还是eclipse,都由我们自己创建了一个叫做lib的文件夹,然后将相应的驱动直接复制到这个lib文件夹下
接着就有区别了: 对于eclipse,右键驱动,选择build path 对于idea,右键lib文件夹,选择add as library,直接将这个lib文件夹变成依赖
对于idea,module之前是不共享依赖的。因此需要对每一个module添加这个依赖 还是project stucture,选择module,选中module,找右边的dependencies来添加依赖
部署
准备好web项目,依赖,部署包之后,就准备开始部署到tomcat服务器了
这个时候就可以edit configuration了
先添加tomcat服务器 再将artifact部署包部署,也就是添加artifact 下面有个application context,他的值就是一个路径,这个路径会被添加到server选项卡的URL的值得后面
URL的值默认是http://localhost:8080,默认访问的顺序是 这是在tomcat服务器中的默认设置,在config/web.xml中设置好的
假设 application context=/pro,那么URL=http://localhost:8080/pro
进一步说明
什么叫web项目
第一印象里,web项目是说项目里边必须包含web相关的固定格式的目录。在project structure里,我们可以看到web项目的module里边包含web 具体来说,有了web意味着什么呢??有什么变化??? 位置1是WEB-INF中web.xml文件的地址,叫做deployment description 位置2是web文件夹的地址,叫做web resource directory 位置3是src文件夹的地址,叫做source roots 这样看下来,不就是把web项目重要的三类资源的起始目录给记录下来了吗
四:JDBC和DAO
虽然前面已经学习过这两部分内容,但是我们需要在java web的背景下再回顾
JDBC:实现通过java控制数据库 DAO:管理代码的一种方式。将操作数据库的原子操作封装在BaseDAO中 然后针对每一张数据表设计对应的映射类,接口,DAO
在引入java web以前,我们学习到的java se的知识已经可以支撑应用软件的基本开发
SSM:spring, springmvc, mabatis
java se + 数据库项目实战
一些概念
之前,我们是将数据表的映射类放在了java bean里边,但是我们对java bean的元素有相当严格的界定。现在是将映射类放在pojo里边
POJO是Plain OrdinaryJava Object的缩写,实质上可以理解为简单的实体类,顾名思义POJO类的作用是方便程序员使用数据库中的数据表,对于广大的程序员,可以很方便的将POJO类当做对象来进行使用,当然也是可以方便的调用其get,set方法。POJO类也给我们在struts框架中的配置带来了很大的方便
JavaBean则比 POJO复杂很多, Java Bean 是可复用的组件,对 Java Bean 并没有严格的规范,理论上讲,任何一个 Java 类都可以是一个 Bean。但是,javabean还是有一些必要的结构: (1)通常情况下,由于 Java Bean 是被容器所创建(如 Tomcat) 的,所以 Java Bean 应具有一个无参的构造器 (2)实现 Serializable 接口用于实现 Bean 的持久性 (3)不能被跨进程访问
一般在web应用程序中建立一个数据库的映射对象时,我们只能称它为POJO。POJO(Plain Old Java Object)这个名字用来强调它是一个普通java对象,而不是一个特殊的对象,其主要用来指代那些没有遵从特定的Java对象模型、约定或框架(如EJB)的Java对象。理想地讲,一个POJO是一个不受任何限制的Java对象(除了Java语言规范)
项目架构
BaseDAO类:通用的增删改查操作 xxxDAO接口:针对某一张数据表的原子操作,使用继承与BaseDAO的方法实现 xxxDAOImpl类:重写xxxDAO接口中的所有方法
然后在具体的业务模块,我们会设计大量的业务方法,这些业务方法由xxxDAO接口定义的原子操作来实现
总结:业务方法由xxxDAOImpl类实现类中的原子操作实现,xxxDAOImpl实现类中的原子操作由BaseDAO中的原子操作实现
项目需求
有这样一个场景:在下单的时候,订单表中显示谁在什么时间买了什么东西 而具体买了什么在另外一张表 订单详情表 呈现: 这样一来就产生了一个新的需求:需要获取订单表的id来访问订单详情表的某一条记录。而BaseDAO中通用的增删改这一原子操作是不返回id的
需求:在增加数据完成后返回自增列的值 之前我都忽略了一点,conn.preparedStatemetn是有返回值的,返回int
一个新知识,conn.preparedStatemetn的重载方法返回一个ResultSet实例,但是方法的参数要增加 对于删和改没有上面这一需求,只针对增这一操作。因此,我们需要解析字符串sql,看是不是以insert开头。调用下面的方法进行解析sql.trim().toUpperCase().startsWith("INSERT") trim方法,去掉字符串首尾空格;toUpperCase方法,将字符串全转为大写;startsWith方法,从首字母开始匹配,看是否能匹配到目标字符串
五. servelet
在后端编程中,我们需要一些逻辑结构,比如servlet(server applet,服务器连接器,applet的本意是小程序),JSP
HTML:页面的结构 CSS: 页面的美化 JS:页面的行为 JQuery:封装了JS VUE:封装了JS(简单) React:封装了JS
TOMCAT:服务器 xml:可以自定义标签,用来写配置文件 JSON:服务器向浏览器传数据以JSON的格式,轻量级,更易解析 servlet:用于和浏览器交互,tomcat中最重要的组件获取请求,处理请求,响应请求 filter:过滤器,比如说购物车点击购买,会先看是否登录等等 listener:监听器,监听用户的操作 HTTP:浏览器和服务器交互需要遵循的协议 JSP:java服务器页面,响应请求显示的页面,动态页面 EL,JSTL:用于提高JSP的开发效率
会话控制:服务器无法区分请求的来源,通过会话控制区分请求来自于哪个服务器
Ajax:实现异步请求。比如说网站注册,输入完用户名,后面就会判断是否通过,此时我们并没有主动点击注册
一个简单的例子
经过前面对于tomcat服务器的学习,我们知道,当启动tomcat服务器且服务器中有相应的资源时,我们可以通过浏览器输入url(统一资源定位符)打开相应的文件 比如说有这样一个.html文件
(1)通过url,向服务器发起请求 (2)服务器响应请求,返回响应的数据 (3)当我们点击添加按钮,客户端又向服务器发起请求.同时传给服务器的还包括此次进行的操作action,填写好的数据 (4)服务器需要有组件来获取请求,处理请求,响应请求。这个组件就是servlet。从实际代码角度看,我们需要专门设计一个类,用以处理action。比如action = add,那就设计一个叫做AddServlet的类(添加服务器端小程序)
这个类的作用是: 获取来自客户端的数据 调用DAO完成对数据库的操作 在控制台打印相应的操作成功
(5)这个组件可以调用DAO,向数据库服务器中添加数据
xxxServlet类
一个普通的xxxServlet类是无法完成以下功能的: 获取来自客户端的数据 调用DAO完成对数据库的操作 在控制台打印相应的操作成功
服务器厂商,比如说tomcat,会提供一个父类HttpServlet,自定义的xxxServlet类去继承这个父类。这个父类的声明为:public abstract class HttpServlet extends javax.servlet.GenericServlet ,是一个抽象父类
实际上服务器厂商会提供JSP和servlet等许多结构的相关API,都在安装路径的lib文件夹中,我们在使用的时候需要手动导入需要的依赖(dependency)
xxxServlet类实现第一个功能-获取来自客户端的数据
http request(http请求)
万事万物皆对象,http请求也被封装为对象,封装到HttpServletRequest类,这个类来自javax.servlet.http,由服务器厂商提供
获取数据
以前学习计算机网络的时候,知道http报文中携带了数据,他们按照固定的格式排列,服务器提供的API中的抽象类HttpServletRequest提供了getParameter方法来解析http报文
传入getParameter方法的参数就来自于.html文件中的name属性的值
重写继承的哪一个方法以及实例化哪一个xxxServlet类
在上面的.html文件中,我们是这样声明表单的
会话-session-http是无状态的
物联网淑慧试用:传输层,会话层,表示层,应用层
万物皆对象,会话也是一个类,HttpSession类
http是无状态的
- HTTP 无状态:服务器无法判断这两次请求是同一个客户端发过来的,还是不同的客户端发过来的
- 无状态带来的现实问题:第一次请求是添加商品到购物车,第二次请求是结账;如果这两次请求服务器无法区分是同一个用户的,那么就会导致混乱
- 通过会话跟踪技术来解决无状态的问题。
会话跟踪
客户端请求,服务器响应,这就像谈话一样,这种场景被定义为一个会话
会话跟踪技术
- 客户端第一次发请求给服务器,服务器获取session,获取不到,则创建新的,然后响应给客户端
- 下次客户端给服务器发请求时,会把sessionID带给服务器,那么服务器就能获取到了,那么服务器就判断这一次请求和上次某次请求是同一个客户端,从而能够区分开客户端
- 常用的API:
request.getSession() -> 获取当前的会话,没有则创建一个新的会话
request.getSession(true) -> 效果和不带参数相同
request.getSession(false) -> 获取当前会话,没有则返回null,不会创建新的
session.getId() -> 获取sessionID
session.isNew() -> 判断当前session是否是新的
session.getMaxInactiveInterval() -> session的非激活间隔时长,默认1800秒
session.setMaxInactiveInterval()
session.invalidate() -> 强制性让会话立即失效
....
session保存作用域
前面说了,会话就是HttpSession类,那么类里边有属性很正常吧
每个客户端实例化了一个HttpSession类,同时呢,也实例化了一个xxxServlet类
- session保存作用域是和具体的某一个session对应的
- 常用的API:
void session.setAttribute(k,v)
Object session.getAttribute(k)
void removeAttribute(k)
服务器端转发和客户端重定向
两种资源跳转的方式
服务器端转发
上面说了,服务器端只会实例化一个servlet,由这个实例来为所有的请求提供服务。如果是这样,从哪里来的服务器端转发呢????
这里说的是两个组件,不是两个servlet;但是处理请求的不都是servlet吗??
服务器内部转发 : request.getRequestDispatcher("...").forward(request,response);
- 一次请求响应的过程,对于客户端而言,内部经过了多少次转发,客户端是不知道的
- 地址栏没有变化
客户端重定向
服务器的组件让客户端立即向指定的组件发起请求
客户端重定向: response.sendRedirect("....");
- 两次请求响应的过程。客户端肯定知道请求URL有变化
- 地址栏有变化
thymeleaf
thyme:百里香,有吉祥如意的意思
一种视图模板技术,可以实现在html中显示数据库中的真实信息,和springboot完美整合
在静态页面.html文件上加载java内存中的数据这一过程被称之为渲染
thymeleaf就是用来帮助进行渲染的一个技术
渲染的英文是render,在底层源码中设计了一个render方法
为什么使用thymeleaf
背景:假设要将数据库中的数据展示在html页面
我们肯定是不希望将取出的记录一条一条的写到静态页面上,而是说,我取出一百条记录,通过一些操作,可以让他直接显示在页面上
配置thymeleaf的步骤
添加thymeleaf的jar包:有一大堆jar包 注意,新建的依赖文件夹,也就是lib,需要右键->add as library
新建一个Servlet类ViewBaseServlet:这个类继承了HttpServlet类
public class ViewBaseServlet extends HttpServlet {
private TemplateEngine templateEngine;
@Override
public void init() throws ServletException {
// 1.获取ServletContext对象
ServletContext servletContext = this.getServletContext();
// 2.创建Thymeleaf解析器对象
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);
// 3.给解析器对象设置参数
// ①HTML是默认模式,明确设置是为了代码更容易理解
templateResolver.setTemplateMode(TemplateMode.HTML);
// ②设置前缀
String viewPrefix = servletContext.getInitParameter("view-prefix");
templateResolver.setPrefix(viewPrefix);
// ③设置后缀
String viewSuffix = servletContext.getInitParameter("view-suffix");
templateResolver.setSuffix(viewSuffix);
// ④设置缓存过期时间(毫秒)
templateResolver.setCacheTTLMs(60000L);
// ⑤设置是否缓存
templateResolver.setCacheable(true);
// ⑥设置服务器端编码方式
templateResolver.setCharacterEncoding("utf-8");
// 4.创建模板引擎对象
templateEngine = new TemplateEngine();
// 5.给模板引擎对象设置模板解析器
templateEngine.setTemplateResolver(templateResolver);
}
protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1.设置响应体内容类型和字符集
resp.setContentType("text/html;charset=UTF-8");
// 2.创建WebContext对象
WebContext webContext = new WebContext(req, resp, getServletContext());
// 3.处理模板数据
templateEngine.process(templateName, webContext, resp.getWriter());
}
}
在web.xml文件中添加配置,ViewBaseServlet类中的init方法会使用这两个参数 - 配置前缀 view-prefix - 配置后缀 view-suffix
//前缀就是web这个根目录
//后缀就是.html,相当于说去找这个.html文件
使得我们的Servlet继承ViewBaseServlet,在这个例子中,我们设计了一个IndexServlet类
//servlet从3.0版本开始支持使用注解完成在xml文件中的注册
@WebServlet("/index")
public class IndexServlet extends ViewBaseServlet {
@Override
public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
FruitDAO fruitDAO = new FruitDAOImpl();
List
//保存到session作用域
HttpSession session = request.getSession() ;
session.setAttribute("fruitList",fruitList);
//此处的视图名称是 index
//那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
//逻辑视图名称 : index
//物理视图名称 : view-prefix + 逻辑视图名称 + view-suffix
//所以真实的视图名称是: / index .html
super.processTemplate("index",request,response);
}
}
根据逻辑视图名称 得到 物理视图名称:说白了就是将一个不带后缀的文件名变成一个相对路径,这个路径必须定位到一个文件 //此处的视图名称是 index //那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去 //逻辑视图名称 : index //物理视图名称 : view-prefix + 逻辑视图名称 + view-suffix //所以真实的视图名称是: / index .html super.processTemplate(“index”,request,response);
使用thymeleaf的标签 th:if , th:unless , th:each , th:text
//为了使用thymeleaf,需要添加这么一个表头
欢迎使用水果库存后台管理系统
名称 | 单价 | 库存 | 操作 |
---|---|---|---|
对不起,库存为空! | |||
苹果 | 5 | 20 |
如何实现大量统一格式的数据在html页面的展示
如果是一般的高级程序语言,我想肯定能脱口而出,就是用循环嘛
在这就懵逼了。html不同于之前遇到的高级程序语言,我们没有看到经典的流程控制相关的语法
但在前面说明使用thymeleaf的背景时,我们提到已经提前获取到sql查询的结果,需要通过一些操作将查询结果一条一条的展示出来
从上面的html代码可以看到,他妈的,居然用的是循环,意料之外又在情理之中
th:each="fruit : ${session.fruitList}"
session.fruitList,我们将装查询结果的容器储存到会话中 fruit,每次从容器中取出一条记录储存到变量fruit
回顾
review:
1. post提交方式下的设置编码,防止中文乱码
request.setCharacterEncoding("utf-8");
get提交方式,tomcat8开始,编码不需要设置
tomcat8之前,get方式设置比较麻烦:
String fname = request.getParameter("fname");
byte[] bytes = fname.getBytes("iso-8859-1");
fname = new String(bytes,"UTF-8");
2. Servlet继承关系以及生命周期
1) Servlet接口 : init() , service() , destroy()
GenericServlet抽象子类: abstract service();
HttpServlet抽象子类:实现了service方法,在service方法内部通过request.getMethod()来判断请求的方式,
然后根据请求的方式去调用内部的do方法。每一个do方法进行了简单实现,主要是如果请求方式不符合,则报405错误。
目的是让我们的Servlet子类去重写对应的方法(如果重写的不对,则使用父类的405错误实现)
2) 生命周期:实例化、初始化、服务、销毁
- Tomcat负责维护Servlet实例的生命周期
- 每个Servlet在Tomcat容器中只有一个实例,它是线程不安全的
- Servlet的启动时机:
- Servlet3.0开始支持注解: @WebServlet
3. HTTP协议:
1) 由 Request 和 Response 两部分组成
2) 请求包含了三部分:请求行、请求消息头、请求主体: 普通的get方式请求-query string;post方式- form data ; json格式 - request payload
3) 响应包含了三部分:响应行、响应消息头、响应主体
4. HttpSession
1) HttpSession :表示 会话
2) 为什么需要HttpSession , 原因是因为 Http协议是无状态的
3) Session保存作用域 :一次会话范围都有效 ; void session.setAttribute(k,v) ,Object session.getAttribute(k)
4) 其他的API: session.getId() , session.isNew() , session.getCreationTime() , session.invalidate() 等等
5. 服务器端转发和客户端重定向
1) 服务器端转发 : request.getRequestDispatcher("index.html").forward(request,response);
2) 客户端重定向: response.sendRedirect("index.html");
6. thymeleaf的部分标签
1) 使用步骤: 添加jar , 新建ViewBaseServlet(有两个方法) , 配置两个
2) 部分标签:
servlet保存作用域
从我最开始学习会话的时候,我就没明白这个保存作用域是什么意思。从代码层面来看,有一个HttpSession类,这个类的实例可以存储其他类型的实例,感觉上HttpSession类有点像是一个容器
但你要说这是个作用域,听起来就很奇怪,很反直觉
作用域解释
于是,我搜索到了这样一样说法, 几乎所有web应用容器都提供了四种类似Map的结构:application session request page,通过向着这四个对象放入数据,从而实现数据的共享
这些说法真的是越来越抽象,不说人话啊!!! web容器:就是一个程序。特指服务器程序,比如说tomcat也可以叫tomcat容器
因此,上面那个说法翻译一下就是,tomcat服务器提供了四种类似于哈希表的类: application:整个应用 对应servlet中ServletContext类
session:会话 对应servlet中HttpSession类
request:一次请求 对应servlet中的HttpServletRequest类
page:当前页面 对应servlet中的PageContext类
pageContext:当前页面有效 (页面跳转后无效)
request:同一次请求有效(请求转发后有效;重定向后无效)
session:同一次会话有效(关闭/切换浏览器后无效 ; 默认30分钟有效期)
appliation:全局有效 (切换浏览器 仍然有效) 服务器开着就有效,切换客户端
结论:所以说,作用域指的是,web容器提供的四个类Map容器类实例的作用范围
HttpServletRequest容器
HttpServletRequest实例的作用范围在一次请求之内,因此,上面的示例中,第二次请求是无法取到数据的
模拟不同请求:可以使用客户端重定向
HttpSession容器
模拟不同会话:
ServletContext容器
模拟不同的客户端:换一台IP地址不同的设备或者直接换一个浏览器
绝对路径和相对路径
href:hyper reference超文本引用
区分超链接和超文本引用
超链接:将网页的选中部分作为跳转到另一个网页的入口,通过点击实现跳转 超文本引用:用超链接的方法,将各种不同空间的文字信息组织在一起的网状文本
从实际使用来看,超链接需要用户主动点击才能跳转,超文本引用写好以后,在运行时会自动引用链接的内容,用户不能主动点击
好文推荐
发表评论