零基础快速搭建rxjava框架
零基础快速搭建rxjava框架
基本概念
定义
RxJava 是一个 基于事件流、实现异步操作的库
原理
角色 | 作用 | 类比 |
---|---|---|
被观察者(Observable) | 产生事件 | 顾客 |
观察者(Observer) | 接收事件,并给出响应动作 | 厨房 |
订阅(Subscribe) | 连接 被观察者 & 观察者 | 服务员 |
事件(Event) | 被观察者 & 观察者 沟通的载体 | 菜式 |
即RxJava原理可总结为:被观察者 (Observable) 通过 订阅(Subscribe) 按顺序发送事件 给观察者 (Observer), 观察者(Observer) 按顺序接收事件 & 作出对应的响应动作。具体如下图:
(图片和表格来自Carson_Ho博客)
集成
在build中添加
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'implementation 'io.reactivex.rxjava2:rxandroid:2.0.0'
最简单的形式
/*** 最基础的订阅*/private void test1() {Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {emitter.onNext(1);emitter.onNext(2);emitter.onNext(3);emitter.onComplete();}}).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Integer integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});}
- 在这个函数中首先通过Observable.create创建了一个被观察者,然后被观察者发送了四个event事件,包含三个onNext和一个onComplete事件。
- 之后通过new Observer
- 最后通过subscribe将二者连接到了一起。
常用基本操作符
fromArray和fromIterable
作用
直接发送数组(fromArray)和列表(fromIterable)
使用场景
可以实现数组或者列表的便利,可以替代for循环
实例代码:
Observable.fromArray(new Integer[]{0,1,2,3,4}).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Integer integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});ArrayList<Integer> list=new ArrayList();for (int i=0;i<10;i++){list.add(i);}Observable.fromIterable(list).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Integer integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});
interval
作用
定时发送事件,类型timer
使用场景
可以替代timer等定时任务,例如每隔一段时间去重新拉取数据
实例代码:
/**参数1 = 第1次延迟时间;参数2 = 间隔时间数字;参数3 = 时间单位;*/Observable.interval(3,1, TimeUnit.SECONDS).subscribe(new Observer<Long>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Long integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});
map和flatmap
作用
将获取到的事件转换成新的事件(map)或者转换成新的观察者(flatmap)
使用场景
- 数据类型转换,例如将罗马数字1234转换成中文数字一二三四。或者从json中取出特定的数据。
- 可以将两个观察者串在一起。例如我在第一个接口中获取了用户信息,但是只包含userid用户姓名需要第二个接口去根据userid去查。就可以使用flatmap去实现串联操作
- 可以结合上面的interval操作符,实现定时拉取数据
实例代码:
Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {emitter.onNext(1);emitter.onNext(2);emitter.onNext(3);}}).map(new Function<Integer, String>() {@Overridepublic String apply(Integer integer) throws Exception {return "使用 Map变换操作符 将事件" + integer + "的参数从 整型" + integer + " 变换成 字符串类型" + integer;}}).subscribe(new Consumer<String>() {@Overridepublic void accept(String s) throws Exception {Log.d("qin", s);}});Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {emitter.onNext(1);emitter.onNext(2);emitter.onNext(3);}}).flatMap(new Function<Integer, ObservableSource<String>>() {@Overridepublic ObservableSource<String> apply(Integer integer) throws Exception {final List<String> list = new ArrayList<>();for (int i = 0; i < 3; i++) {list.add("我是事件" + integer + "拆分后的子事件" + i);}return Observable.fromIterable(list);}}).subscribe(new Consumer<String>() {@Overridepublic void accept(String s) throws Exception {Log.d("qin", s);}});
zip
作用
合并 多个被观察者(Observable)发送的事件,生成一个新的事件序列(即组合过后的事件序列),并最终发送
使用场景
- 两个接口没有关联关系,但是都需要调用。那么就可以使用zip来进行并发操作。例如我拥有用户的id,需要查询用户的姓名(第一个接口)和用户的家庭住址(第二个接口)。按照原来的程序逻辑我们需要先查询用户姓名,成功以后再查询用户地址,最后显示。如果使用zip,我们就可以同时创建两个被观察者一个获取姓名一个获取地址,并发操作,最后再zip中进行合并,在观察者中显示。
实例代码:
Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {Log.d(TAG, "被观察者发送了事件1");emitter.onNext(1);Thread.sleep(1000);Log.d(TAG, "被观察者发送了事件2");emitter.onNext(2);Thread.sleep(1000);Log.d(TAG, "被观察者发送了事件3");emitter.onNext(3);Thread.sleep(1000);emitter.onComplete();}}).subscribeOn(Schedulers.io());Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() {@Overridepublic void subscribe(ObservableEmitter<String> emitter) throws Exception {Log.d(TAG, "被观察者2发送了事件1");emitter.onNext("A");Thread.sleep(1000);Log.d(TAG, "被观察者2发送了事件2");emitter.onNext("B");Thread.sleep(1000);Log.d(TAG, "被观察者2发送了事件3");emitter.onNext("C");Thread.sleep(1000);Log.d(TAG, "被观察者2发送了事件4");emitter.onNext("D");Thread.sleep(1000);emitter.onComplete();}}).subscribeOn(Schedulers.newThread());Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() {@Overridepublic String apply(Integer integer, String s) throws Exception {return integer + s;}}).subscribe(new Observer<String>() {@Overridepublic void onSubscribe(Disposable d) {Log.d(TAG, "onSubscribe");}@Overridepublic void onNext(String value) {Log.d(TAG, "最终接收到的事件 = " + value);}@Overridepublic void onError(Throwable e) {Log.d(TAG, "onError");}@Overridepublic void onComplete() {Log.d(TAG, "onComplete");}});
subscribeOn和observeOn
作用
subscribeOn :指定Observable自身在哪个调度器上执行,也就是指定被观察者运行的线程
observeOn :指定一个观察者在哪个调度器上观察这个Observable,也就是指定观察者运行的线程
使用场景
- 在程序中,我们经常需要在非主线程进行网络操作,在主线程更新ui,就可以使用这两个操作符
实例代码:
Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Thread.sleep(2000);emitter.onNext(1);emitter.onComplete();}}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {}@Overridepublic void onNext(Integer value) {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Log.d(TAG, "onNext: " + value);}@Overridepublic void onError(Throwable e) {Log.d(TAG, "对Error事件作出响应");}// 接收合并事件后,统一展示@Overridepublic void onComplete() {Log.d(TAG, "获取数据完成");Log.d(TAG, result);}});
compose
作用
可以将一种类型的Observable转换成另一种类型的Observable
使用场景
- 可以将一组操作符重用于多个数据流中。例如,因为希望在工作线程中处理数据,然后在主线程中处理结果,所以我会频繁使用subscribeOn()和observeOn(),但是这是非常繁琐的,我们就可以使用compose来处理
实例代码:
//我们可以把上个例子的代码变为Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Thread.sleep(2000);emitter.onNext(1);emitter.onComplete();}}).compose(rxSchedulerHelper()).subscribe(new Observer<Object>() {@Overridepublic void onSubscribe(Disposable d) {}@Overridepublic void onNext(Object value) {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Log.d(TAG, "onNext: " + value);}@Overridepublic void onError(Throwable e) {Log.d(TAG, "对Error事件作出响应");}// 接收合并事件后,统一展示@Overridepublic void onComplete() {Log.d(TAG, "获取数据完成");Log.d(TAG, result);}});/*** 统一线程处理* @param <T> 指定的泛型类型* @return ObservableTransformer*/public static <T> ObservableTransformer<T, T> rxSchedulerHelper() {return new ObservableTransformer<T, T>() {@Overridepublic ObservableSource<T> apply(Observable<T> upstream) {return upstream.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());}};}
retrofit和rxjava的结合
依赖包
如果让retrofit支持rxjava的调用需要依赖下面两个包
// 衔接 Retrofit & RxJava
// 此处一定要注意使用RxJava2的版本compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'// 支持Gson解析compile 'com.squareup.retrofit2:converter-gson:2.1.0'
最基本网络访问
首先是新建retrofit,这里封装了一个方法。
/*** 新建需要进行网络访问retrofit*/public <T> T getRx(Class<T> service) {String baseUrl = HttpURLConstant.getURL(0);LogUtil.i("provideRetrofit() retrofit will new Retrofit.Builder() baseUrl:" + baseUrl);retrofit = new Retrofit.Builder() //每次请求都要求新建.baseUrl(baseUrl).addConverterFactory(ScalarsConverterFactory.create()).addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(AppModule.provideOkHttpClient()).build();return retrofit.create(service);}
可以看到与不使用rxjava的retrofit相比多了一句" .addCallAdapterFactory(RxJava2CallAdapterFactory.create())" 这句是必须的,否则rxjava会创建报错
接下来我们需要编写retrofit的访问方法
//不使用rxjava的时候retrofit的写法@POST(HttpURLConstant.URL_CHECK_REGISTED)Call<String> checkRegisted(@Body Map<String,Object> jsonString);
//使用rxjava的时候retrofit的写法@POST(HttpURLConstant.URL_CHECK_REGISTED)Observable<String> checkObservableRegisted(@Body Map<String,Object> jsonString);
很明显就可以发现,二者最大的不同就是返回的类型由call变成了Observable。这样联系上面的知识,我们就可以知道,我们获取了retrofit的observable之后,就可以去订阅,获取访问结果.
所以,最后我们获取到retrofit的Observable的完整的方法是
/*** 校验是否已注册** @param phoneNum* @param* @return*/public Observable<String> checkRegistedObservableString(String phoneNum) {Map<String, Object> obj = new HashMap<>();if (!TextUtils.isEmpty(phoneNum)) {obj.put("telephone", phoneNum);}Observable observable = getRx(HttpTalkForRetrofit.UserApi.class).checkRegistedObservableString(obj);return observable;}
然后我们在调用的地方这样写
new HttpManagerUser().checkRegistedObservableString(phone).compose(RxUtils.rxSchedulerHelper()).subscribe(new ResourceObserver<String>() {@Overridepublic void onNext(String value) {Log.d("qin","value"+value);}@Overridepublic void onError(Throwable e) {e.printStackTrace();}@Overridepublic void onComplete() {}});
这样我们就可以在onNext中获取访问的结果,如果访问出错那么就会onError的回调。这样我们就完成了一个最基本的网络访问的请求。
对网络返回结果进行过滤
我们在网络请求的时候经常会出现,服务端返回的数据虽然走的是正确的接口,但是里面的数据是没有的。所以我们希望滤掉这样的信息,只保留获取成功的数据,在我们的项目中也就是state=0的数据。
如果要实现这样的功能,那么我们就需要和服务端约定一个基类。例如在我们现在的工程中基类如下
public class HttpResponseBase<T> {private int status;private String description;private int total;private T data;/**这里是get,set方法,就不列出来了**/
}
然后在例子中,data的类是
public class CheckUserExistsBean {/*** registered : true*/private boolean registered;public boolean isRegistered() {return registered;}public void setRegistered(boolean registered) {this.registered = registered;}
}
接下来,我们就可以改造之前的获取retrofit的Observable的方法。由string改成具体的bean。
/*** 校验是否已注册** @param phoneNum* @param* @return*/public Observable<HttpResponseBase<CheckUserExistsBean>> checkObservableRegisted(String phoneNum) {Map<String, Object> obj = new HashMap<>();if (!TextUtils.isEmpty(phoneNum)) {obj.put("telephone", phoneNum);}Observable observable = getRx(HttpTalkForRetrofit.UserApi.class).checkObservableRegisted(obj);return observable;}@POST(HttpURLConstant.URL_CHECK_REGISTED)Observable<HttpResponseBase<CheckUserExistsBean>> checkObservableRegisted(@Body Map<String,Object> jsonString);
这样,只要接口返回的数据正确,我们就可以直接获取到对应bean。而不是string。接下来,我们只需要在最后的观察者收到数据直接拦截state不为0的数据即可,这里我们就可以使用前面讲过flatmap方法。所以调用接口方法可以改成这样
new HttpManagerUser().checkObservableRegisted(phone).compose(RxUtils.rxSchedulerHelper()).flatMap(new Function<HttpResponseBase<CheckUserExistsBean>, ObservableSource<CheckUserExistsBean>>() {@Overridepublic ObservableSource<CheckUserExistsBean> apply(HttpResponseBase<CheckUserExistsBean> httpResponseBase) throws Exception {if(httpResponseBase.getStatus() == 0) {return Observable.create(new ObservableOnSubscribe<CheckUserExistsBean>() {@Overridepublic void subscribe(ObservableEmitter<CheckUserExistsBean> emitter) throws Exception {try {emitter.onNext(httpResponseBase.getData());emitter.onComplete();} catch (Exception e) {emitter.onError(e);}}});} else {return Observable.error(new ServerException(httpResponseBase.getStatus(),httpResponseBase.getDescription()));}}}).subscribe(new ResourceObserver<CheckUserExistsBean>() {@Overridepublic void onNext(CheckUserExistsBean value) {}@Overridepublic void onError(Throwable e) {}@Overridepublic void onComplete() {}});
但是这样功能虽然实现了,很明显代码很臃肿,而且不能复用,每个接口都需要写这么一大坨。所以这时候compose就派上用处了。我们可以把代码拆分成这样
new HttpManagerUser().checkObservableRegisted(phone).compose(RxUtils.rxSchedulerHelper()).compose(RxUtils.handleResult()).subscribe(new ResourceObserver<CheckUserExistsBean>() {@Overridepublic void onNext(CheckUserExistsBean value) {}@Overridepublic void onError(Throwable e) {}@Overridepublic void onComplete() {}});//RxUtils中/*** 统一返回结果处理* @param <T> 指定的泛型类型* @return ObservableTransformer*/public static <T> ObservableTransformer<HttpResponseBase<T>, T> handleResult() {ObservableTransformer observableTransformer= new ObservableTransformer<HttpResponseBase<T>, T>() {@Overridepublic Observable apply(Observable<HttpResponseBase<T>> httpResponseObservable) {return httpResponseObservable.flatMap(new Function<HttpResponseBase<T>, Observable<T>>() {@Overridepublic Observable<T> apply(HttpResponseBase<T> httpResponseBase) throws Exception {if(httpResponseBase.getStatus() == 0) {return createData(httpResponseBase.getData());} else {return Observable.error(new ServerException(httpResponseBase.getStatus(),httpResponseBase.getDescription()));}}});}};return observableTransformer;}/*** 得到 Observable* @param <T> 指定的泛型类型* @return Observable*/private static <T> Observable<T> createData(final T t) {return Observable.create(emitter -> {try {emitter.onNext(t);emitter.onComplete();} catch (Exception e) {emitter.onError(e);}});}
这样我们就成功的筛选出了stata为0的数据,并且无论哪个接口只需要添加一句
.compose(RxUtils.handleResult())
就可以实现数据的过滤
对网络访问错误进行处理
同样的,如果网络访问出错,例如网络错误,服务器返回了错误的数据,而且在上面我们把stata不为0的数据也抛到错误中了,如果我们分别在每个错误的回调中处理,这样的处理方法不仅低效,而且臃肿了。所以和上面一样,我们也使用compose来进行统一的处理。把网络访问的错误信息直接转换成用户可以理解的。
在RxUtils中添加错误的处理方法
/*** 统一返回结果处理* @param <T> 指定的泛型类型* @return ObservableTransformer*/public static <T> ObservableTransformer<T,T> handleError() {ObservableTransformer observableTransformer= new ObservableTransformer<T, T>() {@Overridepublic ObservableSource<T> apply(Observable<T> upstream) {return upstream.onErrorResumeNext(new Function<Throwable, ObservableSource<? extends T>>() {@Overridepublic ObservableSource<? extends T> apply(Throwable throwable) throws Exception {return Observable.error(ExceptionEngine.handleException(throwable));}});}};return observableTransformer;}
新建一个错误处理类ExceptionEngine(这里仅仅用作例子,需要根据实际情况更改)
public class ExceptionEngine {public static final int UNKNOWN = 1000;public static ApiException handleException(Throwable e){ApiException ex;if (e instanceof HttpException){ //HTTP错误org.xutils.ex.HttpException httpException = (org.xutils.ex.HttpException) e;ex = new ApiException(e, httpException.getCode());ex.message=ErrorManager.getToastErrorMsg(httpException.getCode());return ex;} else if (e instanceof ServerException){ //服务器返回的错误ServerException resultException = (ServerException) e;ex = new ApiException(resultException, resultException.code);ex.message = ErrorManager.getToastErrorMsg(resultException.code);return ex;} else if (e instanceof JsonParseException|| e instanceof JSONException|| e instanceof ParseException){ex = new ApiException(e, HttpListener.ERROR_EXCEPTION);
// ex.message = "解析错误"; //均视为解析错误ex.message = ErrorManager.getToastErrorMsg(HttpListener.ERROR_EXCEPTION);return ex;}else if(e instanceof ConnectException){ex = new ApiException(e, HttpListener.ERROR_GATEWAY_TIMEOUT);
// ex.message = "连接失败"; //均视为网络错误ex.message = ErrorManager.getToastErrorMsg(HttpListener.ERROR_GATEWAY_TIMEOUT);return ex;}else {ex = new ApiException(e, UNKNOWN);
// ex.message = "未知错误"; //未知错误ex.message = ErrorManager.getToastErrorMsg(UNKNOWN);return ex;}}
}
同时新建一种错误类型ApiException
public class ApiException extends Exception {public int code;public String message;public ApiException(Throwable throwable, int code) {super(throwable);this.code = code;}
}
好了现在万事具备,我们只需要在我们的获取数据方法中使用这个方法就行了
new HttpManagerUser().checkObservableRegisted(phone).compose(RxUtils.rxSchedulerHelper()).compose(RxUtils.handleResult()).compose(RxUtils.handleError()).subscribeWith(new ResourceObserver<CheckUserExistsBean>() {@Overridepublic void onNext(CheckUserExistsBean value) {if (value.isRegistered()) {//如果是已经注册的手机号view.toVelidateActivity();} else {//如果是新用户view.toRegisterctivity();}}@Overridepublic void onError(Throwable e) {showErrorMsg(e);}@Overridepublic void onComplete() {view.hindDialog();}})
这样我们就基本上完成了比较简单和完善的rxjava访问的框架了
零基础快速搭建rxjava框架相关推荐
- 零基础快速搭建私人影音媒体平台
目录 1. 前言 2. Jellyfin服务网站搭建 2.1. Jellyfin下载和安装 2.2. Jellyfin网页测试 3.本地网页发布 3.1 cpolar的安装和注册 3.2 Cpolar ...
- 零基础快速搭建K歌应用
点击观看大咖分享 玩法开天辟地,体验不留缝隙.K歌不遗余力,应用解决效益.总是羡慕别人家的"歌房"苦叹自家"茅草房"消除不了回音和混音?这次就将带你实战K歌功能 ...
- 零基础快速学习Java技术的方法整理
在学习java技术这条道路上,有很多都是零基础学员,他们对于java的学习有着很多的不解,不知怎么学习也不知道如何下手,其实Java编程涉及到的知识点还是非常多的,我们需要制定java学习路线图这样才 ...
- 零基础快速开发全栈后台管理系统(Vue3+ElementPlus+Koa2)—项目概述篇(一)
零基础快速开发全栈后台管理系统(Vue3+ElementPlus+Koa2)-项目概述篇(一) 一.项目开发总体框架 二.项目开发流程 三.项目技术选型
- 零基础快速入门web学习路线(含视频教程)
下面小编专门为广大web学习爱好者汇总了一条完整的自学线路:零基础快速入门web学习路线(含视频教程)(绝对纯干货)适合初学者的最新WEB前端学习路线汇总! 在当下来说web前端开发工程师可谓是高福利 ...
- 【Python零基础快速入门系列 | 03】AI数据容器底层核心之Python列表
• 这是机器未来的第7篇文章 原文首发地址:https://blog.csdn.net/RobotFutures/article/details/124957520 <Python零基础快速入门 ...
- 【Python零基础快速入门系列 | 07】浪漫的数据容器:成双成对之字典
这是机器未来的第11篇文章 原文首发链接:https://blog.csdn.net/RobotFutures/article/details/125038890 <Python零基础快速入门系 ...
- 零基础快速打造一个属于自己的微信聊天工具
" 零基础快速打造一个属于自己的微信聊天工具" 打开微信,我们可以和别人进行聊天,发送消息.非常方便,那微信是怎么来的呢?这个本质的问题让人突发奇想,我们能不能做一个属于自己的微 ...
- 零基础快速入门SpringBoot2.0教程 (三)
一.SpringBoot Starter讲解 简介:介绍什么是SpringBoot Starter和主要作用 1.官网地址:https://docs.spring.io/spring-boot/doc ...
最新文章
- 神了,无意中发现一位1500道的2021LeetCode算法刷题pdf笔记
- linux性能调优原创翻译系列
- 崩溃重启_semi-sync插件崩溃导致MySQL重启的故障分析-爱可生
- python控制台输出到文件_Python print 立即打印内容到重定向的文件
- 用until编写一段shell程序,计算1~10的平方和
- Zookeeper架构及FastLeaderElection机制
- JavaScript基本类型值和引用类型值的复制问题
- nlp基础—10.结巴分词的应用及底层原理剖析
- bdm导入mysql_MySQL数据库导入教程
- Go 设计模式(Go patterns)
- VMware workstation 16 pro下载、安装(官网)
- js 大地坐标转经纬度
- 怎么查看微信收藏功能的剩余可用空间
- Asio Threads and Asio
- 华为薪资等级结构表_2018华为等级工资表一览
- mysql group_concat去重_mysql GROUP_CONCAT 函数 将相同的键的多个单元格合并到一个单元格...
- php date函数 在哪里,PHP date函数
- 微信小程序用echarts引入中国地图
- 两台ubantu搭linux集群,如何使用运行Ubuntu 14.04的firewire在两台Linux...
- leetcode_885. 螺旋矩阵 III