一个基于Vue.js的小说阅读器

6,833 阅读5分钟

需求说明,在uni-app环境中打包到APP的一个功能,高仿原生APP的一个小说阅读器

网上这个功能貌似还没有人开源过,要么就是C++那边才有的实现分享,所以觉得有必要开源给前端社区。注意这里不能用canvas去实现,因为uni-app打包成半原生APP的时候不兼容

效果图

这里有个细节,就是要实现swiper.js那样完善的手势操作和物理回弹效果

思路说明

1. 首先整个功能我将它分为三个页面,也就是上、中、下三个页面,中间的则是显示用户观看的内容,其他两个则是作为左右滑动斜街显示的副内容。然后用一个pageIndex值作为滑动切换的索引,在这三个页面中准确地更新对应的文章。

滑动页面切换的思路我不说了,因为我做过很多这类功能,直接看我代码注释即可,这里我只说怎么把这些用户操作和数据联动起来

data() {
    return {
        /** 
         * 页面切换索引 `[0,1,2]` 因为初始是中间的显示内容,所以初始值为`1`
         * 主要这的 pageIndex 是当前中间页面显示的索引,后面更新内容也是根据这个值为标准
        */
        pageIndex: 1,
        /** 三个页面的滑动样式列表 */
        styles: [
                {
                    transform: '-102%',
                    transition: '0s all',
                    zIndex: '3'
                }, {
                    transform: '0px',
                    transition: '0s all',
                    zIndex: '2'
                }, {
                    transform: '0px',
                    transition: '0s all',
                    zIndex: '1'
                }
            ],
    }
}

2. 然后来说一下一个页面的数据格式

因为后台返回给我的数据是这样的

const data = {
    /** 当前章节标题 */
    title: '第二章',
    /** 当前章节内容 */
    content: '内容内容内容内容内容< /br>内容内容内容内容内容< /br>内容内容内容内容内容< /br>'
}

很明显这样的数据在我现在的项目中是无法做到自适应并调整每一页的内容的,所以我要做的就是以</ br>分为一段一段,然后以数组存储起来,这个时候就要通过字体的大小和行高,边距计算好然后再存到一个二维数组里面渲染出来,这里我计算的公式思路为

/** 当前章节的段落列表 */
const contents = data.content.split('<br />');
/** 下边距 */
const margin = this.AppOption.sizeInfo.margin;
/** 内容实际宽度 */
const width = this.pageWidth - 32;
/** 计算的页数 */
let page = 0;
/** 是否第一页 */
let firstPage = true;
/** 一页的字体容器高度 */
let height = 0;
/** 一页的字体列表 */
let list = [];
/** 下一页超出的数据 */
let nextPageText = {
    height: 0,
    text: ''
};
// `chapterList`在最后一个步骤中介绍
// 先重置为一个空的数组,因为二维数组的值初始化是`undefined`
this.chapterList[chapter] = [];
for (let i = 0; i < contents.length; i++) {
    /** 单个字体的宽度 */
    const sizeWidth = 1;
    /** 当前段的内容 */
    const item = contents[i];
    /** 一段字体的总宽度 */
    const fontWidth = item.length * (this.AppOption.sizeInfo.p * sizeWidth) + 24; // 24 是段落缩进像素
    /** 在页面中的行数 */
    const row = Math.ceil(fontWidth / width);
    /** 一段字的高度 */
    const itemHeight = row * this.AppOption.sizeInfo.pLineHeight + margin;
    // console.log(`第${i + 1}段`, row + '行', fontWidth, item);
    // console.log(`第${i + 1}段`, row + '行');
    
    if (firstPage) {
        // 标题的行高 + 下边距
        height += this.AppOption.sizeInfo.tLineHeight + margin;
        firstPage = false;
    } 
    
    // 把上一页超出的内容加到当前页中去
    if (nextPageText.height) {
        height += nextPageText.height;
        list.push(nextPageText.text);
        // 用完拼接好的页面记得清除
        nextPageText.height = 0; 
        nextPageText.text = '';
    }

    list.push(item);
    height += itemHeight;
    
    // 46是 .info + .page padding的上下距离 
    if (height - margin > this.pageHeight - 46) {
        list.pop();
        nextPageText.height = itemHeight;
        nextPageText.text = item;
        // console.log('页数:', page, 'height:', height - itemHeight, '段数:', list.length);
        // 下一页
        this.chapterList[chapter][page] = {
            title: '',
            content: list
        }
        list = [];
        height = 0;
        page ++;
    } 
    
}   
this.chapterList[chapter][0].title = data.title;

因为我以一段一段为换页标准,所以这里的计算不算很复杂,所以存在一些小瑕疵,有做过类似算法思路的老哥欢迎纠正我。

3. 最主要的一个小说内容列表数据整合并联动(难点!!!)

这里要注意几个操作:

当滑动到第一章第一页的时候(检测提示并处理前一个页面的内容)

最后一章最后一页的时候(检测提示并处理下一个页面的内容)

某一章第一页的时候(加载上一章的内容,并重新计算上一章的最后一页索引,最后缓存到chapterList中)

某一章最后一页的时候(加载下一章的内容,最后缓存到chapterList中)

点击&拖动操作栏上/下章的切换,判断加载上/下章节的内容

// 文章的数据格式 
// 以上操作我都会先通过一个叫`getChapterData`的方法去获取下一章的原始数据,
// 如果有则从`cacheData[要获取的章节索引]`中返回,没有则从服务端请求回来,然后缓存到`cacheData`中
// 下次用到直接从缓存拿,最后再传入`updateChapterList`方法中去计算并缓存到`chapterList`中,最后传入
// `updateContent`更新三个页面的内容

data() {
    return {
        /** 最大章节数 */
        chapterMax: 10,
        /** 章节位置(0开始) */
        chapterIndex: 0,
        /** 章节的页数位置 */
        chapterPage: 0,
        /** 
         * 章节阅读器数据 
         * @type {Array<Array<{title: string, content: Array<string>}>>}
        */
        chapterList: [],
        /** 
         * 请求回来的章节阅读器数据 | 下载的`json`也是这个 
         * @type {Array<{title: string, content: string}>}
        */
        cacheData: [],
        /** 页面内容列表 */
        pageTextList: [
            {
                title: '', // 'page-1',
                content: []
            }, {
                title: '', // 'page-2',
                content: []
            }, {
                title: '', // 'page-3',
                content: []
            }
        ],
    }
}

这里我加载新的篇章用的是假数据setTimeout去模拟网络请求回来的功能

其他说明

  1. 运行环境,因为我是针对APP环境下的,所以我用的是uni-app的兼容写法,需要复制到web项目中则把对应标签改一下即可。
  2. 因为涉及到大量的变量在一个页面中,所以后面换成typescript,理由就不说了,实现思路是不变的
  3. 项目模板引用的是uni-app-template,理由是可以一套代码适配多端

代码地址

有用记得star噢 github地址