FreeBook

Project Url: 80945540/FreeBook
Introduction: 基于 MVP 模式开发的带缓存网络爬虫,采用最流行框架搭建,干货多多
More: Author   ReportBugs   
Tags:

若您对技术有着狂热的追求,我期待您能加入我的团队

无论你是高手还是菜鸟,只要你想在技术深耕我都欢迎您

坐标:上海 宝山区

以下是岗位需求,若您感兴趣欢迎投递简历到邮箱:shengqq@eagersoft.cn (记得备注来源是 github 开源项目 )

Android 岗位

1、熟练掌握 Java 开发技术,具备扎实的编程基础;
2、熟练掌握 Android 各类 UI 组件、图像动画、网络、文件系统等模块的使用和开发;
3、熟练掌握 Android 的 API,理解 Android 的体系结构;
4、熟练运用市面上主流的开源库,如 Glide、RxJava 等;
5、熟悉 Android 中界面绘制、后台运行、数据存储等的原理;
6、了解 Jectpack 相关组件;
7、了解 Android 内存管理机制,并能进行优化;
8、拥有一定的 Android 自绘技能;
9、拥有良好的代码规范,文档能力,Git 使用经验;

IOS 岗位

1、有扎实的 Swift 基础,了解应用基本算法/数据结构,了解基础设计模式;
2、熟悉 iOS SDK 开发环境及相关开发工具;
3、熟悉 RxSwift、Sqlite;
4、熟练掌握 Autolayout、xib、storyboard;
5、具有良好编码风格,有较强的独立工作能力和团队合作精神;

前端-小程序方向

1、掌握 HTML5、CSS3、ES6,JS 基础扎实;
2、掌握微信小程序开发原理、开发流程、接口组件、验证授权等,能独立开发微信小程序;
3、熟练使用 canvas;
4、有完整的微信小程序项目经验;
5、有字节跳动/百度/支付宝/快应用等小程序开发经验者优先。

前端-web 方向

1、熟练 Ajax、Node.js、NPM 常用的开发体系原理,完成数据绑定、业务交互等;
2、熟练 VUE、Bootstrap、jQuery 等框架,懂得灵活运用;
3、对 JS 模块化和通用化有一定设计经验的;
4、有良好的团队合作精神;沟通能力强、有责任心、做事细致;

FreeBook v2.0 去除网络爬虫 使用 api 请求数据

引言

基于 MVP 模式开发的免费下载小说 app,采用最流行框架搭建,干货多多

集中最近流行的框架和思维以一种简单易理解的方式实现,方便大家学习

至于需求什么的我就不说了,对于准备毕业项目的同学学会这个你会觉得毕业项目非常 easy

api 帮助文档

APK 下载 免费下载小说 学习的同时 还能拿到不少的干货

由于使用的美国服务器 国内访问会比较慢 所有 api 仅使用于学习 当然也欢迎大家赞助 换个国内好点的服务器啦

这里有那么一群志同道合的人在等你加入 QQ 群:173999252

首先让大家看看效果 No picture you say a jb? gif 录制效果不好 将就点看

首页 书库 搜索 下载 下载

还有就是项目的结构 看看我是怎么玩转 MVP 的

项目结构

用到的主流框架

  • RxJava+Retrofit2+Okhttp+RxCache 实现 API 数据请求以及缓存(缓存不区分 GET&POST 缓存策略可根据自己要求修改)

  • RxJava+jsoup+RxCache 实现 HTMl 页面爬虫数据的请求以及缓存 缓存实现与 API 一致 不需要另写逻辑

  • glide 加载图片

  • LCRapidDevelop 下拉刷新 状态页 RecyclerView 适配器 RecyclerView 加载动画 等等感兴趣的自行了解 传送门

  • bga-banner 首页的 Banner 实现无限循环 还不错 集成简单

功能点

  • 首页 banner 以及推荐数据 根据后台接口更新(总要有点自己可控的元素嘛 比如加个广告什么的 哈哈 比如说)

  • 书库类别 以及类别的 HTML 地址等数据 通过后台接口控制 (如果哪天我觉得这个网站的资源不是很丰富 我可以很任性的直接在后台换一个)

  • 数据缓存 请求 HTML 网页再从网页上抓取想要的数据其实相对 API 来说耗时会比较大 缓存就显得非常重要了

  • 文件下载统一管理 并且调用系统支持的程序打开文件

首先详细讲解一下 RxJava+Retrofit2+Okhttp+RxCache 的使用 五部曲

第一步:导包

    compile 'io.reactivex:rxjava:1.1.8'
    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'
    compile 'com.github.VictorAlbertos.RxCache:core:1.4.6'

第二步:新建 API 接口

/**
 * API 接口 
 * 因为使用 RxCache 作为缓存策略 所以这里不需要写缓存信息
 */
public interface MovieService {

    //获取书库分类信息
    @GET("freebook/typeconfigs.json")
    Observable<List<BookTypeDto>> getBookTypes();

    //获得首页 banner 以及书籍数据
    @GET("freebook/home.json")
    Observable<HomeDto> getHomeInfo();

    //获得搜索标签
    @GET("freebook/search_lable.json")
    Observable<List<String>> getSearchLable();
}

第三步:新建缓存接口(Html 爬虫共用)

/**
 * 缓存 API 接口
 * @LifeCache 设置缓存过期时间. 如果没有设置@LifeCache , 数据将被永久缓存理除非你使用了 EvictProvider, EvictDynamicKey or EvictDynamicKeyGroup .
 * EvictProvider 可以明确地清理清理所有缓存数据.
 * EvictDynamicKey 可以明确地清理指定的数据 DynamicKey.
 * EvictDynamicKeyGroup 允许明确地清理一组特定的数据. DynamicKeyGroup.
 * DynamicKey 驱逐与一个特定的键使用 EvictDynamicKey 相关的数据。比如分页,排序或筛选要求
 * DynamicKeyGroup。驱逐一组与 key 关联的数据,使用 EvictDynamicKeyGroup。比如分页,排序或筛选要求
 */
public interface CacheProviders {
    //获取书库对应类别列表  缓存时间 1 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<List<BookInfoListDto>>> getStackTypeList(Observable<List<BookInfoListDto>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //获取书库分类信息缓存数据 缓存时间 永久
    Observable<Reply<List<BookTypeDto>>> getBookTypes(Observable<List<BookTypeDto>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //获取首页配置数据 banner 最热 最新  缓存时间 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<HomeDto>> getHomeInfo(Observable<HomeDto> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //获取搜索标签  缓存时间 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<List<String>>> getSearchLable(Observable<List<String>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    //获取书籍详情  缓存时间 7 天
    @LifeCache(duration = 7, timeUnit = TimeUnit.DAYS)
    Observable<Reply<BookInfoDto>> getBookInfo(Observable<BookInfoDto> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);
}

第四步:新建 retrofit 抽象类


/**
 *封装一个 retrofit 集成 0kHttp3 的抽象基类
 */
public abstract class RetrofitUtils {

    private static Retrofit mRetrofit;
    private static OkHttpClient mOkHttpClient;
    /**
     * 获取 Retrofit 对象
     *
     * @return
     */
    protected static Retrofit getRetrofit() {

        if (null == mRetrofit) {

            if (null == mOkHttpClient) {
                mOkHttpClient = new OkHttpClient.Builder().build();
            }

            //Retrofit2 后使用 build 设计模式
            mRetrofit = new Retrofit.Builder()
                    //设置服务器路径
                    .baseUrl(Constant.API_SERVER + "/")
                    //添加转化库,默认是 Gson
                    .addConverterFactory(GsonConverterFactory.create())
                    //添加回调库,采用 RxJava
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    //设置使用 okhttp 网络请求
                    .client(mOkHttpClient)
                    .build();
        }

        return mRetrofit;
    }

}

第五步:新建 HttpData 类 用于统一管理请求

/*
 *所有的请求数据的方法集中地
 * 根据 MovieService 的定义编写合适的方法
 * 其中 observable 是获取 API 数据
 * observableCahce 获取缓存数据
 * new EvictDynamicKey(false) false 使用缓存  true 加载数据不使用缓存
 */
public class HttpData extends RetrofitUtils {

    private static File cacheDirectory = FileUtil.getcacheDirectory();
    private static final CacheProviders providers = new RxCache.Builder()
            .persistence(cacheDirectory)
            .using(CacheProviders.class);
    protected static final MovieService service = getRetrofit().create(MovieService.class);

    //在访问 HttpMethods 时创建单例
    private static class SingletonHolder {
        private static final HttpData INSTANCE = new HttpData();
    }

    //获取单例
    public static HttpData getInstance() {
        return SingletonHolder.INSTANCE;
    }

    //获取 app 书本类别
    public void getBookTypes(Observer<List<BookTypeDto>> observer){
        Observable observable=service.getBookTypes();
        Observable observableCahce=providers.getBookTypes(observable,new DynamicKey("书本类别"),new EvictDynamicKey(false)).map(new HttpResultFuncCcche<List<BookTypeDto>>());
        setSubscribe(observableCahce,observer);
    }
    //获取 app 首页配置信息  banner  最新 最热
    public void getHomeInfo(Observer<HomeDto> observer){
        Observable observable=service.getHomeInfo();
        Observable observableCache=providers.getHomeInfo(observable,new DynamicKey("首页配置"),new EvictDynamicKey(false)).map(new HttpResultFuncCcche<HomeDto>());
        setSubscribe(observableCache,observer);
    }
    //获得搜索热门标签
    public void getSearchLable(Observer<List<String>> observer){
        Observable observable=service.getSearchLable();
        Observable observableCache=providers.getSearchLable(observable,new DynamicKey("搜索热门标签"), new EvictDynamicKey(false)).map(new HttpResultFuncCcche<List<String>>());
        setSubscribe(observableCache,observer);
    }
    /**
     * 插入观察者
     *
     * @param observable
     * @param observer
     * @param <T>
     */
    public static <T> void setSubscribe(Observable<T> observable, Observer<T> observer) {
        observable.subscribeOn(Schedulers.io())
                .subscribeOn(Schedulers.newThread())//子线程访问网络
                .observeOn(AndroidSchedulers.mainThread())//回调到主线程
                .subscribe(observer);
    }

    /**
     * 用来统一处理 RxCacha 的结果
     */
    private  class HttpResultFuncCcche<T> implements Func1<Reply<T>, T> {

        @Override
        public T call(Reply<T> httpResult) {
            return httpResult.getData();
        }
    }

}

RxJava+Retrofit2+Okhttp+RxCache 的搭建就是这么简单的五步就完成了,剩下的就是怎么去使用了 我来举个栗子 像这样请求数据肯定是需要写到 Model 里面的

/**
 * 获得类别数据
 */
public class HomeStackFragmentModel {

    public void LoadData(final OnLoadDataListListener listener){
        HttpData.getInstance().getBookTypes(new Observer<List<BookTypeDto>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                listener.onFailure(e);
            }

            @Override
            public void onNext(List<BookTypeDto> bookTypeDtos) {
                listener.onSuccess(bookTypeDtos);
            }
        });
    }
}

想要的数据已经拿到了,故事到这里结束了,但是新的故事又开始了,吃瓜群众们你们准备好瓜子了吗?

RxJava+jsoup+RxCache 实现 HTMl 页面爬虫数据的请求以及缓存 四部曲

第一步:导包 还是熟悉的套路

compile 'org.jsoup:jsoup:1.9.2'

第二步:其实就是 RxJava+Retrofit2+Okhttp+RxCache 的第三步 新建缓存接口 第三步:新建自定义 OnSubscribe 用于解析 Html 获得自己数据

/**
 * 其实这里面的玩法还很多
 * 这是 jsop 的中文文档 http://www.open-open.com/jsoup/  再牛逼的数据都能抓取
 * 其实 doc.select(".bookcover h1:eq(1)");  ()里面的数据完全可以通过接口定义  达到完全控制的效果
 * 我是懒得写了  但是这个需求还是提一下  很 nice 的  装逼必备啊
 */
public class BookInfoHtmlOnSubscribe<T> implements Observable.OnSubscribe<T> {
    private String url;

    public BookInfoHtmlOnSubscribe(String url) {
        //获取到需要解析 html 地址
        this.url = url;
    }

    @Override
    public void call(Subscriber<? super T> subscriber) {
        try {
            //开始疯狂的数据抓取啦 这个我就不解释了  大家去看看文档  http://www.open-open.com/jsoup/
            Document doc = Jsoup.connect(url).get();
            Elements bookIntroduction = doc.select(".con");
            Elements bookname = doc.select(".bookcover h1:eq(1)");
            Elements bookImageUrl = doc.select(".bookcover img");
            Elements bookAuthor = doc.select(".bookcover p:eq(2)");
            Elements bookType = doc.select(".bookcover p:eq(3)");
            Elements bookLength = doc.select(".bookcover p:eq(4)");
            Elements bookProgress = doc.select(".bookcover p:eq(5)");
            Elements bookUpdateTime = doc.select(".bookcover p:eq(6)");
            String[] strs=url.split("/");
            String bookDownload="http://www.txt99.cc/home/down/txt/id/"+((strs[strs.length-1]));
            T bookInfoDto= (T) new BookInfoDto(bookImageUrl.attr("src"),bookname.text(),bookAuthor.text(),bookType.text(),bookLength.text(),bookProgress.text(),bookUpdateTime.text(),bookDownload,bookIntroduction.html());
            subscriber.onNext(bookInfoDto);
            subscriber.onCompleted();
        } catch (IOException e) {
            throw new ApiException("ERROR:数据解析错误");
        }
    }
}

第四步:新建 HtmlData 类 和上面的非常相似 哎 就不解释了 就是这么 666

/**
 * Created by Administrator on 2016/9/14. */
public class HtmlData {
    //这里是设置一个缓存地址 如果地址不存在就新建一个
    private static File cacheDirectory = FileUtil.getcacheDirectory();
    //添加缓存提供者
    private static final CacheProviders providers = new RxCache.Builder()
            .persistence(cacheDirectory)
            .using(CacheProviders.class);

    //在访问 HttpMethods 时创建单例
    private static class SingletonHolder {
        private static final HtmlData INSTANCE = new HtmlData();
    }

    //获取单例
    public static HtmlData getInstance() {
        return SingletonHolder.INSTANCE;
    }

    //根据类型获取书籍集合
    public void getStackTypeHtml(BookTypeDto bookType, int pageIndex, Observer<List<BookInfoListDto>> observer) {
        Observable observable = Observable.create(new StackTypeHtmlOnSubscribe<BookInfoListDto>(bookType.getBookTypeUrl().replace("{Page}",pageIndex+"")));
        Observable observableCache=providers.getStackTypeList(observable,new DynamicKey("getStackTypeHtml"+bookType.getBookTypeName()+pageIndex), new EvictDynamicKey(false)).map(new HttpResultFuncCache<List<BookInfoListDto>>());
        setSubscribe(observableCache, observer);
    }
    //根据关键字搜索书籍
    public void getSearchList(String key,Observer<List<BookInfoListDto>> observer){
        try {
            //中文记得转码  不然会乱码  搜索不出想要的效果
            key = URLEncoder.encode(key, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        Observable observable=Observable.create(new StackTypeHtmlOnSubscribe<BookInfoListDto>(Constant.API_SEARCH.replace("{Key}",key)));
        Observable observableCache=providers.getStackTypeList(observable,new DynamicKey("getSearchList&"+key), new EvictDynamicKey(false)).map(new HttpResultFuncCache<List<BookInfoListDto>>());
        setSubscribe(observableCache, observer);
    }
    //获得书籍的详情
    public void getBookInfo(String bookUrl,String bookName, Observer<BookInfoDto> observer){
        Observable observable=Observable.create(new BookInfoHtmlOnSubscribe<BookInfoDto>(bookUrl));
        Observable observableCache=providers.getBookInfo(observable,new DynamicKey(bookName),new EvictDynamicKey(false)).map(new HttpResultFuncCache<BookInfoDto>());
        setSubscribe(observableCache, observer);
    }

    /**
     * 插入观察者
     *
     * @param observable
     * @param observer
     * @param <T>
     */
    public static <T> void setSubscribe(Observable<T> observable, Observer<T> observer) {
        observable.subscribeOn(Schedulers.io())
                .subscribeOn(Schedulers.newThread())//子线程访问网络
                .observeOn(AndroidSchedulers.mainThread())//回调到主线程
                .subscribe(observer);
    }
    private  class HttpResultFuncCache<T> implements Func1<Reply<T>, T> {

        @Override
        public T call(Reply<T> httpResult) {
            return httpResult.getData();
        }
    }
}

使用方式和 RxJava+Retrofit2+Okhttp+RxCache 一致 我也举个栗子好了

/**
 * 获取书籍详情数据
 */
public class BookInfoModel {
    public void loadData(String bookUrl,String bookName, final OnLoadDataListListener listener){
        HtmlData.getInstance().getBookInfo(bookUrl,bookName, new Observer<BookInfoDto>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                listener.onFailure(e);
            }

            @Override
            public void onNext(BookInfoDto bookInfoDto) {
                listener.onSuccess(bookInfoDto);
            }
        });
    }
}

好了是不是觉得特别简单 当然我只是带你们入门 真正想玩转想拓展 还是要好好的多了解了解 有吃瓜群众要问了 这个缓存是以什么形式缓存的啊 不说话 看图 缓存数据

接下来就是 MVP 了

最近 mvp 真的是火的不行啊,了解后确实觉得非常不错,解决了 View 和 Model 的耦合,对于后期的维护以及逻辑的清晰度的来说还是非常不错的

有吃瓜群众要说了 为毛一步可以搞定的事情非要分三步

MVP 的优点:
  • 模型与视图完全分离,我们可以修改视图而不影响模型

  • 可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter 内部

  • 我们可以将一个 Presenter 用于多个视图,而不需要改变 Presenter 的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。

  • 如果我们把逻辑放在 Presenter 中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

MVP 一种编程模式 没有必要照搬 按照自己的理解去写就好 下面我例举这个 APP 里的书籍详情页面 来帮助大家理解

首页是 Model 层:业务逻辑和实体模型 由于我不是很关注实体模型 所以 model 层我只放业务逻辑 实体统一丢 entity

/**
 * 获取书籍详情数据
 */
public class BookInfoModel {
    public void loadData(String bookUrl,String bookName, final OnLoadDataListListener listener){
        HtmlData.getInstance().getBookInfo(bookUrl,bookName, new Observer<BookInfoDto>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                listener.onFailure(e);
            }

            @Override
            public void onNext(BookInfoDto bookInfoDto) {
                listener.onSuccess(bookInfoDto);
            }
        });
    }
    public interface OnLoadDataListListener<T> {
    void onSuccess(T data);
    void onFailure(Throwable e);
}
}

然后是我们的 view 层: View 对应于 Activity||fragment,负责 View 的绘制以及与用户交互

/**
 * 定义一个接口来控制 Activity 相关操作
 */
public interface BookInfoView {
    //显示加载页
    void showProgress();
    //关闭加载页
    void hideProgress();
    //数据加载成功
    void newData(BookInfoDto data);
    //显示加载失败
    void showLoadFailMsg();
}

最后就是 Presenter 负责完成 View 于 Model 间的交互 一个正宗的媒婆 不应该说中介 大哥你三环的房子卖吗?

public class BookInfoPresenter implements OnLoadDataListListener<BookInfoDto>{
    private BookInfoView mView;
    private BookInfoModel mModel;

    public BookInfoPresenter(BookInfoView mView) {
        this.mView = mView;
        mModel=new BookInfoModel();
    }

    public void loadData(String bookUrl,String bookName){
        mModel.loadData(bookUrl,bookName,this);
        mView.showProgress();
    }

    @Override
    public void onSuccess(BookInfoDto data) {
        if(data.getBookName().equals("")){
            mView.showLoadFailMsg();
        }else{
            mView.newData(data);
            mView.hideProgress();
        }
    }

    @Override
    public void onFailure(Throwable e) {
        mView.showLoadFailMsg();
    }
}

然而不管我们套路有多深 最终我们还是得回到 Activity

public class BookInfoActivity extends BaseActivity implements BookInfoView {

    @BindView
    ......
    @BindView(R.id.book_info_textview_download)
    TextView bookInfoTextviewDownload;
    private String bookurl, bookname;
    private BookInfoDto bookInfoDto;
    private BookInfoPresenter presenter;

    @Override
    protected void loadViewLayout() {
        setContentView(R.layout.activity_book_info);
    }

    @Override
    protected void findViewById() {
        Intent intent = getIntent();
        bookurl = intent.getStringExtra("bookurl");
        bookname = intent.getStringExtra("bookname");
    }

    public void initview(BookInfoDto data) {
        bookInfoDto = data;
        Glide.with(mContext)
                .load(data.getBookImageUrl())
                .crossFade()
                .placeholder(R.mipmap.image_error)
                .into(bookInfoImageviewBookurl);
        bookInfoTextviewName.setText(data.getBookName());
        bookInfoTextviewAuthor.setText(data.getBookAuthor());
        bookInfoTextviewType.setText(data.getBookType());
        bookInfoTextviewLength.setText(data.getBookLength());
        bookInfoTextviewProgress.setText(data.getBookProgress());
        bookInfoTextviewUpdatetime.setText(data.getBookUpdateTime());
        bookInfoTextviewIntroduction.setText(Html.fromHtml(data.getBookIntroduction()));
    }

    @Override
    protected void setListener() {

    }

    @Override
    protected void processLogic() {
        presenter = new BookInfoPresenter(this);
        presenter.loadData(bookurl, bookname);
    }

    @Override
    protected Context getActivityContext() {
        return this;
    }

    @Override
    public void showProgress() {
        bookInfoProgress.showLoading();
    }

    @Override
    public void hideProgress() {
        bookInfoProgress.showContent();
    }

    @Override
    public void newData(BookInfoDto data) {
        initview(data);
    }

    @Override
    public void showLoadFailMsg() {
        toError();
    }

    public void toError() {
        bookInfoProgress.showError(getResources().getDrawable(R.mipmap.load_error), Constant.ERROR_TITLE, Constant.ERROR_CONTEXT, Constant.ERROR_BUTTON, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                bookInfoProgress.showLoading();
                //重试
                presenter.loadData(bookurl, bookname);
            }
        });
    }
}

在 APP 里出现的加载页面 无数据页面 网络错误页面 下拉刷新 自动加载 加载动画 viewpage 的头部动画等等 看似很高级 其实就是很轻量级的框架啦 LCRapidDevelop是我前段时间整合的一个快速开发框架 欢迎大家 star

网络上形形色色的教程非常之多 讲的也非常细致 细致到 看着看着我都不想看了 我呢 只是教大家怎么去入门

先把东西玩起来再去细致的了解 会比你先详细的了解在开发要轻松的多

目前在线阅读还没有开发 感兴趣的朋友可以一起

联系邮箱 mychinalance@gmail.com

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools