打造web版epub阅读器(阅读设计)

写在前面的话

实现本阅读器需要进行以下几个步骤:

  1. 设计书架。(可添加图书,删除图书等)
  2. 打开并阅读epub图书。(可做高亮、笔记、书签,可显示目录并通过目录跳转)

上一篇文章中,我们实现了书架设计,本篇将实现epub阅读器的阅读部分。
作者在实现时采用了vue + vue-loader来进行编码,直接使用js实现时原理都是一样的。

实现效果图如下:
<img src="http://upload-images.jianshu.io/upload_images/6376000-25dce08b67f1ea2e.gif?imageMogr2/auto-orient/strip" width="40" height="40" alt="123"/>

在此主要有以下几个问题:

  1. 如何打开图书,并实现阅读功能。
  2. 如何生成目录。
  3. 如何给书籍添加笔记。

打开图书、阅读图书

我们使用epub.js开源库来实现epub图书的阅读,如果要自行实现epub的解析,请到参考epub规范。详细的使用方式请到此处查阅。在此只贴出作者使用时的相关代码:

//参考上篇文章,使用localForage加载图书。
this.store.getItem(this.editFile.dname, function(err, file) {
    if (file) {
        //读取图书
        var reader = new FileReader();
        reader.onload = function() {
            var arrayBuffer = reader.result;
            //参考epub.js读取epub图书
            self.Book = ePub(arrayBuffer, {
                restore: true,
                gap: 80
            });
            //检测是否保存上次读取页
            if (self.editFile.lastreadurl) {
                self.Book.spinePos = self.editFile.lastreadurl;
            }
            //将图书渲染到html中。
            self.Book.renderTo("viewer");
            self.Book.setStyle("font-family", "微软雅黑,宋体");
            self.Book.setStyle("color", self.mainStyle.color);
            self.Book.on('renderer:locationChanged', function(locationCfi) {
                self.editFile.lastreadurl = locationCfi;
                if (self.onLine == '0') {
                    localStorage.setItem("filesInfo", JSON.stringify(self.files));
                }
            });
        }
        reader.readAsArrayBuffer(file);
    }
});

生成目录

我在制作此项目时,采用的是elementui框架,用树形控件来实现目录的展示。

使用如下代码:

<el-tree :data="toc" :props="tocprop" @node-click="selectChapter" class="toc"></el-tree>
self.Book.getToc().then(function(toc) {
    //遍历目录树,并修改部分内容
    self.transitionToc(toc);
    self.toc = toc;
});
//真正编写代码时,可调式查看toc(epub.js所生成的目录格式)
//会发现其与elementui树形控件所需数据格式并不相同,所以需要使用translitionToc函数对格式进行转换。
//递归
transitionToc: function(toc) {
    var self = this;
    $.each(toc, function(index, val) {
        if (val.nodes.length == 0) {
            //val.nodes = null;
        } else {
            self.transitionToc(val.nodes);
        }
    });
},

添加笔记

添加笔记需要注意以下几点:

  • 检测用户选中文字
  • 弹出用户操作框
  • 用户笔记输入框
  • 保存用户选中文字及范围
  • 高亮
检测用户选中文字

当用户选中文字时,会使得slef.selected=true,之后下面界面部分将显示。

    //epub.js能捕获用户在书籍上的鼠标释放事件,使用self.selected是为了防止用户重复选中。
    self.Book.on('renderer:mouseup', function(event) {
        //释放后检测用户选中的文字 
        var render = self.Book.renderer.render;
        var selectedContent = render.window.getSelection();
        self.selection = selectedContent;
        //若当前用户不在选中状态,并且选中文字不为空
        if (self.selected == false) {
            if (selectedContent.toString() && (selectedContent.toString() != "")) {
                self.selected = true;
            }
        }
    });
弹出用户操作框

用户操作框界面设计

用户操作框
//html
<el-card v-if="selected" class="box-card colorcontent">
    <div slot="header" class="clearfix">
        <div class="colorBs" id="theme1" style="background-color: #A4B401"></div>
        <div class="colorBs" id="theme2" style="background-color: #D32802"></div>
        <div class="colorBs" id="theme3" style="background-color: #0383B3"></div>
        <div class="colorBs" id="theme4" style="background-color: #04B91E"></div>
        <div class="colorBs" id="theme5" style="background-color: #F634F8"></div>
    </div>
    <div class="addnote">
        <div class="colorb" v-bind:style="colorB"></div>添加笔记
    </div>
    <div class="selectitem">
        翻译
    </div>
    <div class="selectitem">
        网络搜索
    </div>
    <div class="selectitem">
        复制内容
    </div>
</el-card>
//css
.colorcontent {
    width: 200px;
    height: 205px;
    position: absolute;
    font-size: 17px;
    color: #7E7E7E;
}
.colorBs {
    float: left;
    margin-top: 0px;
    margin-left: 15px;
    width: 20px;
    height: 20px;
    -moz-border-radius: 50%;
    -webkit-border-radius: 50%;
    border-radius: 50%;
    border: 1px solid #D4D0D0;
    cursor: pointer;
}
.colorBs:hover {
    border: 1px solid #ffffff;
}
.selectitem {
    height: 40px;
    line-height: 40px;
    margin-left: -20px;
    margin-right: -20px;
    padding-left: 20px;
    cursor: pointer;
}

用户操作框目前实现的包括高亮和添加笔记,因为添加笔记的同时也会高亮,下面将重点讲添加笔记部分。

用户笔记输入框

外观部分代码

输入笔记
//html
<div v-if="noteselected" class="noteinput">
    <div class="noteheader2">
        ┆┆ 添加笔记<i class="fa fa-close noteinput-close" @click="noteselected=!noteselected"></i>
    </div>
    <div contenteditable="true" class="notecontainer" placeholder="请输入笔记">
    </div>
    <div class="notefooter">
        <div class="savebut" @click="savenote">保存</div>
        <div class="giveupbut" @click="noteselected=!noteselected">放弃</div>
    </div>
</div>
//css
.noteinput {
    position: absolute;
    left: 20px;
    top: 20px;
    width: 350px;
    height: 230px;
    background-color: #ffffff;
    z-index: 1000;
}
.noteheader2 {
    padding-left: 10px;
    color: #ffffff;
    line-height: 30px;
    font-size: 10px;
    background-color: #858585;
    height: 30px;
    cursor: move;
    //不被选中
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.noteheader2:hover {
    background-color: #707070;
}
.noteinput-close {
    font-size: 14px;
    margin-left: 250px;
    cursor: pointer;
}
.noteinput-close:hover {
    color: #A7A7A7;
}
.notecontainer {
    border: 1px solid #A0A0A0;
    margin-top: 10px;
    margin-left: 15px;
    margin-right: 15px;
    height: 130px;
    padding: 10px;
    font-size: 13px;
    line-height: 20px;
    overflow-y: auto;
}
.notecontainer:empty:before {
    content: attr(placeholder);
    color: #989898;
}
.notecontainer:focus {
    content: none;
    color: #464646;
}
/**
 * 自定义滚动条
 * @type {[type]}
 */
.notecontainer::-webkit-scrollbar-track {
    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
    border-radius: 10px;
    background-color: #ffffff;
}
.notecontainer::-webkit-scrollbar {
    width: 4px;
    background-color: #ffffff;
}
.notecontainer::-webkit-scrollbar-thumb {
    border-radius: 10px;
    -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
    background-color: #8F8E8E;
}
.notefooter {
    margin-top: 8px;
    height: 30px;
    line-height: 30px;
}
.savebut {
    float: left;
    border: 1px solid #00C4BE;
    color: #00C4BE;
    margin-left: 240px;
    text-align: center;
    width: 50px;
    height: 23px;
    line-height: 23px;
    cursor: pointer;
}
.giveupbut {
    float: left;
    text-align: center;
    width: 50px;
    height: 23px;
    line-height: 23px;
    cursor: pointer;
}

用户操作部分代码

$('.addnote').click(function(event) {
    //记录标记,使得用户在编写笔记时,无法再次选中其他文字。
    self.noteselected = true;
    self.highlightAndSaveSelected(); ////高亮(下面将贴出代码)
    selectedContent.empty(); //清空选中文字
    self.$nextTick(function() {/////一下代码使得用户笔记部分为可拖动的。
        var pageW = $(window).width();
        var pageH = $(window).height();
        var dialogW = $('.noteinput').width();
        var dialogH = $('.noteinput').height();
        var maxX = pageW - dialogW; //X轴可拖动最大值
        var maxY = pageH - dialogH; //Y轴可拖动最大值
        var moveX = event.pageX - 50;
        var moveY = event.pageY;
        moveX = Math.min(Math.max(0, moveX), maxX); //X轴可拖动范围
        moveY = Math.min(Math.max(0, moveY), maxY); //Y轴可拖动范围
        $('.noteinput').css({
            top: moveY,
            left: moveX
        });
        var mx, my, dx, dy, isDraging;
        //鼠标按下
        $(".noteheader2").mousedown(function(e) {
            e = e || window.event;
            mx = e.pageX; //点击时鼠标X坐标
            my = e.pageY; //点击时鼠标Y坐标
            dx = $('.noteinput').offset().left;
            dy = $('.noteinput').offset().top;
            isDraging = true; //标记对话框可拖动                
        });
        var moveNote = function(e) {
            var e = e || window.event;
            var x = e.pageX; //移动时鼠标X坐标
            var y = e.pageY; //移动时鼠标Y坐标
            if (isDraging) { //判断对话框能否拖动
                var moveX = dx + x - mx; //移动后对话框新的left值
                var moveY = dy + y - my; //移动后对话框新的top值
                //设置拖动范围
                var pageW = $(window).width();
                var pageH = $(window).height();
                var dialogW = $('.noteinput').width();
                var dialogH = $('.noteinput').height();
                var maxX = pageW - dialogW; //X轴可拖动最大值
                var maxY = pageH - dialogH; //Y轴可拖动最大值
                moveX = Math.min(Math.max(0, moveX), maxX); //X轴可拖动范围
                moveY = Math.min(Math.max(0, moveY), maxY); //Y轴可拖动范围
                //重新设置对话框的left、top
                $('.noteinput').css({
                    "left": moveX + 'px',
                    "top": moveY + 'px'
                });
            };
        };
        //鼠标移动更新窗口位置
        $(self.Book.renderer.render.document).mousemove(function(e) {
            moveNote(e);
        });
        $(document).mousemove(function(e) {
            moveNote(e);
        });

        $(document).mouseup(function() {
            isDraging = false;
        });
    });
});
保存用户操作信息以及高亮

有关Range操作请参考此篇文章

  1. 对用户选中的文字和范围进行保存,以便实现用户笔记的记录。
    文字可以使用this.selection.toString();来生成。
    范围可以用var epubcfi = new EPUBJS.EpubCFI();来生成。
  2. 高亮用户所选信息
    包括两部分,一是用户选中后的高亮,二是从用户记录的epubcfi信息来高亮。
    两者都是要先转换成Range对象,转换方式分别为:
    var range = this.selection.getRangeAt(0);//从用户选中的selection对象来转换
    var doc = self.Book.renderer.doc;
    var range = epubcfi.generateRangeFromCfi(cfi, doc);//从epubcfi转换
    高亮Range使用github库dom-highlight-range来实现。
highlightAndSaveSelected: function() {
    this.selected = false;
    var range = this.selection.getRangeAt(0);
    var epubcfi = new EPUBJS.EpubCFI();
    var chapter = this.Book.currentChapter;
    var cfiBase = chapter.cfiBase;
    var cfi = epubcfi.generateCfiFromRange(range, cfiBase);
    //对CFI进行存储
    var note = new Object();
    note.color = this.colorB['background-color'];
    note.cfi = cfi;
    note.text = this.selection.toString();
    note.tagtime = Date.parse(new Date());
    note.tagtimedis = ''; //标记标签时间
    note.dishead = false; //显示标签跳转图标
    note.note = ""; //标记注释
    this.editFile.notes.push(note);
    this.editNote = note;
    localStorage.setItem("filesInfo", JSON.stringify(this.files));
    this.selection.empty();
    highlightRange(range, this.editNote.color);
},

总结

epub阅读器的阅读设计效果就如文章开头的效果图,作者开发时使用的是elementui框架,如果未使用框架或使用的其他框架,将代码稍作修改即可,因为作者文笔有限,很多东西想要写出来但下笔时顿觉灵感被抽空。。。。有什么问题可以在下方留言交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容