一个小框架,基于rx_retrofit2_mvp
离职在即,也没什么事情做,就鼓捣了一下。任意搭建了一个小框架,看看以后能不能搞出自己的一个model,好了。不说别的,上代码
1,先上依赖库
compile 'io.reactivex:rxandroid:1.2.1'
compile 'com.squareup.okhttp3:okhttp:3.3.1'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'
compile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2'
compile 'com.android.support:design:24.2.1'
compile 'com.android.support:recyclerview-v7:24.2.1'
compile 'com.android.support:cardview-v7:24.2.1'
compile 'com.jakewharton:butterknife:7.0.1'
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.github.chrisbanes.photoview:library:1.2.3'
2。 依赖retrolambda
在app.build依赖
apply plugin: 'me.tatarka.retrolambda'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
然后在项目的app.build依赖
classpath 'me.tatarka:gradle-retrolambda:3.2.5'
OK,到这里我们的环境搭建就完毕了
3。搭建Http请求模块
- 搭建工具类RetrofitUtils
package mvpmaster.lht.com.lht.utils; import com.squareup.okhttp.OkHttpClient; import java.util.concurrent.TimeUnit; import retrofit.GsonConverterFactory;
import retrofit.Retrofit;
import retrofit.RxJavaCallAdapterFactory; /**
* Created by Ly on 2016/10/14.
*/ public class RetrofitUtils {
private static final int READ_TIMEOUT = 60;//读取超时时间 单位 秒
private static final int CONN_TIMEOUT = 60;//连接超时时间 单位 秒
private static Retrofit retrofit; public RetrofitUtils() {
} public static Retrofit getInstance(String url) {
retrofit = null;
// 初始化一个okhttpClicent的对象 不然ref会自己加入一个
OkHttpClient client = new OkHttpClient();
// 设置读取时间为1分钟
client.setReadTimeout(READ_TIMEOUT, TimeUnit.MINUTES);
// 设置链接时间为12s
client.setConnectTimeout(CONN_TIMEOUT, TimeUnit.SECONDS);
retrofit = new Retrofit.Builder()
.client(client)
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
return retrofit;
}
}Ps,我发现这里并不能做到session的保持,在某些公司的后台并不能支持这种请求库,我稍候会上传一个更新后的版本号
- 这里写上我的一个interface(为什么我用了rx还要有callback???黑人问号脸)
package mvpmaster.lht.com.lht.conf; /**
* Created by Ly on 2016/10/13.
*/ public interface OkHttpCallBack<T> {
void onSuccess(T t);//成功的回调 void onFaild(Throwable e);//失败的回调 void onFinish();
} - 然后是一些比較通用的api文件了,我就不写凝视了
package mvpmaster.lht.com.lht.conf; /**
* Created by Ly on 2016/11/2.
*/ public class HttpConf {
private static final String ZHIHU_BASE_URL = "http://news-at.zhihu.com/api/4/";
private static final String GANK_BASE_URL = "http://gank.io/api/";
private static final String DAILY_BASE_URL = "http://app3.qdaily.com/app3/"; public static String getZhihuBaseUrl() {
return ZHIHU_BASE_URL;
} public static String getGankBaseUrl() {
return GANK_BASE_URL;
} public static String getDailyBaseUrl() {
return DAILY_BASE_URL;
}
}package mvpmaster.lht.com.lht.conf; /**
* Created by Ly on 2016/11/2.
*/ public class HttpStatusConf {
private static final int SUCCESS = 200; public static int getSUCCESS() {
return SUCCESS;
}
}package mvpmaster.lht.com.lht.utils; import mvpmaster.lht.com.lht.ui.beanIml.DailyBean;
import mvpmaster.lht.com.lht.ui.beanIml.NewsDetailBean;
import mvpmaster.lht.com.lht.ui.beanIml.NewsTimeLine;
import retrofit.http.GET;
import retrofit.http.Path;
import rx.Observable; /**
* Created by Ly on 2016/10/14.
*/ public interface APIService { @GET("news/latest")
Observable<NewsTimeLine> getZhiHuList(); @GET("news/before/{time}")
Observable<NewsTimeLine> getBeforetNews(@Path("time") String time); @GET("news/{id}")
Observable<NewsDetailBean> getDetailNews(@Path("id") String id); // for daily
@GET("homes/index/{num}.json")
Observable<DailyBean> getDailyTimeLine(@Path("num") String num); }
- 我们的baseActivity
package mvpmaster.lht.com.lht.base; import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.TypedValue;
import android.view.MenuItem; import butterknife.ButterKnife;
import mvpmaster.lht.com.lht.R; /**
* Created by Ly on 2016/11/2.
*/ public abstract class BaseActivity<V, T extends BasePresenter<V>> extends AppCompatActivity { protected T mPresenter;
private AppBarLayout mAppBar;
private Toolbar mToolbar;
private SwipeRefreshLayout mRefreshLayout; public Context mContext;
private boolean mIsRequestDataRefresh = false; @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
// 同意为空 不是全部的都要实现这个模式
if (createPresenter() != null) {
mPresenter = createPresenter();
mPresenter.attachView((V) this);
}
setContentView(provideContentViewId());
ButterKnife.bind(this); mAppBar = (AppBarLayout) findViewById(R.id.app_bar_layout);
mToolbar = (Toolbar) findViewById(R.id.toolbar);
if (mToolbar != null && mAppBar != null) {
setSupportActionBar(mToolbar); //把Toolbar当做ActionBar给设置 if (canBack()) {
ActionBar actionBar = getSupportActionBar();
if (null != actionBar) {
//设置ActionBar一个返回箭头。主界面没有,次级界面有
actionBar.setDisplayHomeAsUpEnabled(true);
}
if (Build.VERSION.SDK_INT >= 21) {
//Z轴浮动
mAppBar.setElevation(10.6F);
}
}
}
if (isSetRefresh()) {
setupSwipeRefresh();
} } public static void toIntent(Context context, String... str) {
} @Override
public boolean onOptionsItemSelected(MenuItem item) {
// 此时android.R.id.home即为返回箭头
if (item.getItemId() == android.R.id.home) {
onBackPressed();
finish();
return true;
} else {
return super.onOptionsItemSelected(item);
}
} @Override
protected void onDestroy() {
super.onDestroy();
if (mPresenter != null) {
mPresenter.detachView();
}
} /**
* 生成下拉刷新
*/
private void setupSwipeRefresh() {
mRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
if (null != mRefreshLayout) {
mRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary);
mRefreshLayout.setProgressViewOffset(true,
0,
(int) TypedValue.applyDimension
(TypedValue.COMPLEX_UNIT_DIP, 24, getResources()
.getDisplayMetrics()));
}
} /**
* 设置刷新
*
* @param requestDataRefresh
*/
public void setRefresh(boolean requestDataRefresh) {
if (mRefreshLayout == null) {
return;
}
if (!requestDataRefresh) {
mIsRequestDataRefresh = false;
mRefreshLayout.postDelayed(() -> {
if (mRefreshLayout != null) {
mRefreshLayout.setRefreshing(false);
}
}, 1000);
} else {
mRefreshLayout.setRefreshing(true);
}
} /**
* 数据刷新
*/
public void requestDataRefresh() {
mIsRequestDataRefresh = true;
} /**
* 推断当前 Activity 是否同意返回
* 主界面不同意返回,次级界面同意返回
*
* @return false
*/
public boolean canBack() {
return false;
} /**
* 推断子Activity是否须要刷新功能
*
* @return false
*/
public Boolean isSetRefresh() {
return false;
} /**
* 创建P
*
* @return T
*/
protected abstract T createPresenter(); /**
* 用于引入布局文件
*
* @return
*/
abstract protected int provideContentViewId();
}package mvpmaster.lht.com.lht.base; import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import butterknife.ButterKnife;
import mvpmaster.lht.com.lht.R; /**
* Created by Ly on 2016/11/2.
*/ public abstract class BaseFragment<V, T extends BasePresenter<V>> extends Fragment {
protected Context mContext;
protected T mPresenter; private boolean mIsRequestDataRefresh = false;
private SwipeRefreshLayout mRefreshLayout; @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
mPresenter = createPresenter();
mPresenter.attachView((V) this);
} @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(createViewLayoutId(), container, false);
ButterKnife.bind(this, rootView);
initView(rootView);
if (isSetRefresh()) {
setupSwipeRefresh(rootView);
}
return rootView;
} @Override
public void onDestroy() {
super.onDestroy();
mPresenter.detachView();
} private void setupSwipeRefresh(View view) {
mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh);
if (mRefreshLayout != null) {
mRefreshLayout.setColorSchemeResources(R.color.refresh_progress_1,
R.color.refresh_progress_2, R.color.refresh_progress_3);
mRefreshLayout.setProgressViewOffset(true, 0, (int) TypedValue
.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, getResources().getDisplayMetrics()));
mRefreshLayout.setOnRefreshListener(this::requestDataRefresh);
}
} public void requestDataRefresh() {
mIsRequestDataRefresh = true;
} public void setRefresh(boolean requestDataRefresh) {
if (mRefreshLayout == null) {
return;
}
if (!requestDataRefresh) {
mIsRequestDataRefresh = false;
mRefreshLayout.postDelayed(() -> {
if (mRefreshLayout != null) {
mRefreshLayout.setRefreshing(false);
}
}, 1000);
} else {
mRefreshLayout.setRefreshing(true);
}
} protected abstract T createPresenter(); protected abstract int createViewLayoutId(); protected void initView(View rootView) {
} public Boolean isSetRefresh() {
return true;
}
}还有其它的一些base,我就不一一上传了。
- 说一下我理解的mvp。在我的看法中。mvp,m是要做一些耗时操作的,像读取网络数据,数据库数据。sp数据啊...这些脏活苦活所有都丢给它去做,我们在contract中给它定义好了interface,model类在自己本身去实现它。然后按着上层的要求去做那些苦活累活;而View呢?我认为通常是指我们的activity或者fragment巴,他们就负责一些比較轻松的东西了。像显示个toast啊,show一下dialog啊,拿一下editext的数据啊。最苦力也就是设置个适配器啊。监听一下滑动啊之类的,反正最轻松的那个就是它了;然后就是presenter了,这个类我认为挺难弄的,类似于红娘吧,它也要实现Contract的interface,并且要持有model和view的引用。在interface的回调里面去操控model类去做耗时操作,然后在对应的callback(怎么又是callback?)去操控view去实现各种交互。(不要喷我说得那么模糊,可是这样的东西写不出来,用了就会有这样的想法,并且。用了一次mvp,你就不会再想去用mvc了)
- 我们举一个样例,首页那里拿取了知乎的信息,用了一个recyclerview去显示拿到的数据。我们就用它来讲,我先上传一波项目文件夹,不然太懵逼了。
- 我们先看我们的fragment
package mvpmaster.lht.com.lht.ui.fragment.zhuhu; import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Toast; import butterknife.Bind;
import mvpmaster.lht.com.lht.R;
import mvpmaster.lht.com.lht.base.BaseFragment;
import mvpmaster.lht.com.lht.ui.adapter.ZhiHuAdapter;
import mvpmaster.lht.com.lht.ui.beanIml.NewsTimeLine; /**
* Created by Ly on 2016/11/2.
*/ public class ZhiHuFragment extends BaseFragment<ZhiHuContract.ZhiHuView, ZhiHuPresenter> implements ZhiHuContract.ZhiHuView {
@Bind(R.id.content_list)
RecyclerView mRlvZhiHu; private LinearLayoutManager mLayoutManager;
private ZhiHuAdapter zhiHuAdapter; // 最后一个可见的视图
private int lastVisibleItem;
// 是否载入过很多其它
private boolean isLoadMore = false;
// 知乎日报须要的下一个參数
private String time; /**
* 初始化配置
*/
private void initConf() {
// 适配器
zhiHuAdapter = new ZhiHuAdapter(getActivity());
// manager
mLayoutManager = new LinearLayoutManager(getActivity());
mRlvZhiHu.setLayoutManager(mLayoutManager);
mRlvZhiHu.setAdapter(zhiHuAdapter);
// 启动自己主动刷新配置
setDataRefresh(true);
// 获取第一次的数据
mPresenter.getDataList();
// 检測recView的滑动状态
scrollRecycleView();
} /**
* recyclerView Scroll listener , maybe in here is wrong ?
*/
public void scrollRecycleView() {
mRlvZhiHu.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
lastVisibleItem = mLayoutManager
.findLastVisibleItemPosition();
if (mLayoutManager.getItemCount() == 1) {
zhiHuAdapter.updateLoadStatus(zhiHuAdapter.getLOAD_MORE());
return;
}
if (lastVisibleItem + 1 == mLayoutManager
.getItemCount()) {
zhiHuAdapter.updateLoadStatus(zhiHuAdapter.getLOAD_PULL_TO());
isLoadMore = true;
zhiHuAdapter.updateLoadStatus(zhiHuAdapter.getLOAD_MORE());
mPresenter.getBeforeDateList(time);
}
}
} @Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();
}
});
} @Override
public void requestDataRefresh() {
super.requestDataRefresh();
setDataRefresh(true);
mPresenter.getDataList();
} @Override
protected ZhiHuPresenter createPresenter() {
return new ZhiHuPresenter(this);
} @Override
protected int createViewLayoutId() {
return R.layout.fragment_zhihu;
} @Override
public void setDataRefresh(boolean refresh) {
setRefresh(refresh);
} public static ZhiHuFragment newInstance() {
Bundle args = new Bundle();
ZhiHuFragment fragment = new ZhiHuFragment();
fragment.setArguments(args);
return fragment;
} @Override
public void loadFinishAndReset(NewsTimeLine newsTimeLine, String time) {
zhiHuAdapter.resetData(newsTimeLine);
setDataRefresh(false);
this.time = time;
} @Override
public void loadFinishAndAdd(NewsTimeLine newsTimeLine, String time) {
zhiHuAdapter.addData(newsTimeLine);
this.time = time;
} @Override
public void loadFailure() {
setDataRefresh(false);
zhiHuAdapter.updateLoadStatus(zhiHuAdapter.getLOAD_FAILURE());
} @Override
protected void initView(View rootView) {
super.initView(rootView);
initConf();
} @Override
public void TsShow(String msg) {
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}我们在这个看到一个mPresenter。这个是什么鬼?是从哪里来的?事实上这个就是Presenter的对象了。也是BaseActivity的泛型里面的那个
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">我们点取查看它的引用,发如今这个fragment里面一共被引用了3次。第一次是刚进去页面的时候,第一次读取数据。第二个是下拉载入的时候,读取了下一波的数据。再有一次是上拉刷新的时候。我们又一次刷新了一次页面拿取了最新的一波数据,可是?拿数据的在哪里呢?网络请求呢?在哪里??
- 我们看Presenter里面做的操作吧:
package mvpmaster.lht.com.lht.ui.fragment.zhuhu; import mvpmaster.lht.com.lht.base.BasePresenter;
import mvpmaster.lht.com.lht.conf.OkHttpCallBack;
import mvpmaster.lht.com.lht.ui.beanIml.NewsTimeLine; /**
* Created by Ly on 2016/11/2.
*/ public class ZhiHuPresenter extends BasePresenter<ZhiHuContract.ZhiHuView> implements ZhiHuContract.ZhiHuPresenter {
private ZhiHuContract.ZhiHuView zhiHuView;
private ZhiHuContract.ZhiHuModel zhiHuModel; public ZhiHuPresenter(ZhiHuContract.ZhiHuView zhiHuView) {
this.zhiHuView = zhiHuView;
zhiHuModel = new ZhiHuModel();
} @Override
public void getDataList() {
zhiHuModel.getDataList(new OkHttpCallBack<NewsTimeLine>() {
@Override
public void onSuccess(NewsTimeLine newsTimeLine) {
zhiHuView.loadFinishAndReset(newsTimeLine, newsTimeLine.getDate());
} @Override
public void onFaild(Throwable e) {
loadError(e);
zhiHuView.loadFailure();
} @Override
public void onFinish() { }
});
} @Override
public void getBeforeDateList(String time) {
zhiHuModel.getBeforeDateList(time, new OkHttpCallBack<NewsTimeLine>() {
@Override
public void onSuccess(NewsTimeLine newsTimeLine) {
zhiHuView.loadFinishAndAdd(newsTimeLine, newsTimeLine.getDate());
} @Override
public void onFaild(Throwable e) {
loadError(e);
zhiHuView.loadFailure();
} @Override
public void onFinish() { }
});
} private void loadError(Throwable throwable) {
throwable.printStackTrace();
zhiHuView.TsShow(throwable.getMessage());
}
}我们看到有三个ovver的方法。各自是刷新(第一次读取),载入。读取失败 三种情况:我们能够看到,这个类持有了View和Model两个模块,在方法体里面,我们调用了model的方法去做耗时,在结果方法体里面我们调用了view的方法去改动UI,同一时候presenter这个模块又被view持有了,view能够在声明周期里面去调用特定的方法,view和presenter相互沟通。view和model全然隔离,presenter调控model,presenter沟通全局。
一个小框架,基于rx_retrofit2_mvp的更多相关文章
- 利用jdbc简单封装一个小框架(类似DBUtils)
利用jdbc写的一个类似DBUtils的框架 package com.jdbc.orm.dbutils; import java.io.IOException; import java.io.Inpu ...
- mpvue最佳实践 , 美团出的一个小程序框架
看手机微信,看到说美团出了1个小程序框架, mpvue 搜下来试试,看了网上的一个对比 ----------------- 以下为引用 我们对微信小程序.mpvue.WePY 这三个开发框架的主要能 ...
- 通过Dapr实现一个简单的基于.net的微服务电商系统(十四)——开发环境容器调试小技巧
之前有很多同学提到如何做容器调试,特别是k8s环境下的容器调试,今天就讲讲我是如何调试的.大家都知道在vs自带的创建项目模板里勾选docker即可通过F5启动docker容器调试.但是对于启动在k8s ...
- 通过Dapr实现一个简单的基于.net的微服务电商系统(二十)——Saga框架实现思路分享
今天这篇博文的主要目的是分享一下我设计Saga的实现思路来抛砖引玉,其实Saga本身非常的类似于一个简单的工作流体系,相比工作流不一样的部分在于它没有工作流的复杂逻辑处理机制(比如会签),没有条件分支 ...
- 分析一个类似于jquery的小框架
在网上下了一个类似于jQuery的小框架,分析源码,看看怎么写框架. 选择器Select //用沙箱闭包其整个代码,只有itcast和I暴漏在全局作用域 (function( window , und ...
- 一个可以代替冗长switch-case的消息分发小框架
在项目中,我需要维护一个应用层的字节流协议.这个协议的每条报文都是一个字节数组,数组的头两个字节表示消息的传送方向,第三.四个字节表示消息ID,也就是消息种类,再往后是消息内容.时间戳.校验码等……整 ...
- 2、基于wsgiref模块DIY一个web框架
一 web框架 Web框架(Web framework)是一种开发框架,用来支持动态网站.网络应用和网络服务的开发.这大多数的web框架提供了一套开发和部署网站的方式,也为web行为提供了一套通用的方 ...
- spring security 一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中 配置的Bean,充分利用了Spring ...
- 任务驱动,对比式学习.NET开发系列之开篇------开源2个小框架(一个Winform框架,一个Web框架)
一 源码位置 1. Winform框架 2. web框架 二 高效学习编程的办法 1 任务驱动方式学习软件开发 大部分人学习软件开发技术是通过看书,看视频,听老师上课的方式.这些方式有一个共同点即按知 ...
随机推荐
- Mac 10.13安装telnet
狗日的Mac 10.13默认不自带telnet!!!苹果你以为你的操作系统真的那么平民吗,别做梦,用你只不过是为了开发!!! 安装: brew install telnet 如果你用上述方法安装不上, ...
- STM32 Seminar 2007 -- Timer
- Mui --- app与服务器之间的交互原理、mui ajax使用
1.APP与服务器之间的交互原理 app端(客户端)与服务端的交互其实理解起来和容易,客户端想服务器端发送请求,服务器端进行数据运算后返回最终结果.结果可以是多种格式: 1.text 文本格式 2.x ...
- [转]Twemproxy 介绍与使用
Twemproxy是一种代理分片机制,由Twitter开源.Twemproxy作为代理,可接受来自多个程序的访问,按照路由规则,转发给后台的各个Redis服务器,再原路返回.该方案很好的解决了单个Re ...
- 如何给Windows Server 2012 R2 添加“磁盘清理”选项
最近想做一个试验,把我的Windows Server 2008 R2 升级为Server 2012 R2,因为手头没有Raid卡和网卡的驱动,所以做了升级安装,于是那个讨厌的Windows.old出现 ...
- .Net Discovery 系列之二--string从入门到精通(下)
前两节我们介绍了string的两个基本特性,如果你觉得你已经比较全面的了解了string,那么就来看看这第3.4两节吧. 三.有趣的比较操作 在第一节与第二节中,我们分别介绍了字符串的恒定性与与驻留 ...
- C#编程(四十二)----------委托和事件
委托和事件 委托是C#总比较重要的概念,学习C#爱这里最容易产生迷惑. 有些时候,犹豫我们在开发程序时对后续可能出现的要求及变化考虑不足而导致麻烦,这些新变化可能导致程序的重新编写,那能不能改变这种情 ...
- SurfaceFlinger( 226): Permission Denial: can't access SurfaceFlinger
MODIFY_PHONE_STATE permission is granted to system apps only. For your information, there are 2 type ...
- Exchange2003/2010共存模式环境迁移
一.我司的exchange2010架构设计基于中心的模式进行.而且基于exchange2010sp3进行. 基于dag三台架构设计进行,截止到5月14日,北京局基于2台dag进行,大连局基于excha ...
- JQuery攻略(四)事件
jQuery事件处理,鼠标的单击,双击,悬停,键盘按键,文本动画..... 此章节有 1.1被点击的按钮查找 1.2事件的自动触发 1.3点击之后禁用按钮 1.4鼠标事件 1.5焦点事件 1.6CSS ...