0. 引子

每年总有那么两周。

明明不用工作,但却不能休息,宅在家还得爆肝。

没错,国庆&春节。

春节假期的第一天,我就入手了最近一个很火的国产修仙游戏《鬼谷八荒》,无数网友为之肝疼,就下面这条评论,我已经在B站、微博、A社、QQ、微信多个渠道看到了。

为了体验一下修仙的感觉,我也入坑了这个游戏,两天玩了差不多9个小时,今天起床,一阵不以人的主观意志为转移的猛烈肝痛,让我意识到作者是真的打算让我修仙。

总的来说,这个游戏体系很丰富,但还处于内测阶段,玩法剧情上还有很多缺陷,现阶段只是单纯的肝,还没有自动刷怪的功能,每次打材料的过程中,现实世界的你都有极小概率直接爆肝飞升。

这种现象最常见的是在日本,过马路被卡车撞能穿越,玩游戏肝爆能穿越,下班路上也能穿越。

代表人物就是《転生したらスライムだった件》中的三上悟,下班路上被人刺死,穿越到了异世界。

这也是最近我第二肝的事情,把第一部从头看了一遍。

24集看完,我的肝还没有感觉到异常,但是天有不测风云,一条半剧透的弹幕,出现在了我的视野里,我身体躺在床上,而我唯物主义的大脑一直闪过被剧透的画面。

垂死病中惊坐起,绝知此事要躬行。

我搜出了史莱姆正在连载的漫画,快速补完了已经更新的剧情,但是内容和动漫相比,故事线还是很接近,等于没看,所以我准备去补小说。

百度搜索了几个史莱姆小说的资源,但是网站内容过于拉胯,我的肝已经很不舒服了,不能让我眼睛也不舒服,所以我决定自己搭一个史莱姆小说在线阅读的网站!!!

1. 进入正题

1.1 epub介绍

自己搭一个小说阅读网站,仔细理一下思路其实实现很简单。

一个介绍页,展示小说相关信息。

一个阅读页,用于阅读小说。

介绍页很简单,阅读页的话为了方便使用已有的阅读器引擎epub.js。

小说的格式通常有很多,txt、pdf、epub、mobi等等,为了有更优的阅读体验,我选择了epub的格式,因为epub其实本身就是一种电子书的格式协议,有这非常完善的电子书信息可以让我们程序去获取。

我在网上找到了史莱姆的epub资源,将epub后缀改为zip,就可以发现epub到底是怎么存储的了。

小说需要阅读的部分其实都在OPS文件中,以html的形式存在,当然也就支持小说插图。

1.2 epubjs入门

epubjs是一个专门用来解析epub文件的库。

https://github.com/futurepress/epub.js/blob/master/documentation/README.md

利用其api就可以快速完成解析。

  • 安装
npm install epubjs
  • 创建一个book对象
this.book = new Epub('01.epub')
  • 通过Book.renderTo生成Rendition对象
  this.rendition = this.book.renderTo('read', {
    width: window.innerWidth,
    height: window.innerHeight,
    method: 'default'
  })
  • 通过Rendtion.display渲染电子书
this.rendition.display()
  • 翻页
 function prevPage() {
    if (this.rendition) {
      this.rendition.prev()
    }
  }

  function nextPage() {
    if (this.rendition) {
      this.rendition.next()
    }
  }

核心api就这几个,只需要根据路由来控制需要读取的epub文件即可。

1.3 手动引入

引入如下文件即可,可以下载到本地。

<script src="https://github.com/futurepress/epub.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jszip/3.5.0/jszip.js"></script>

1.4 菜单栏

我们需要在阅读过程中对界面进行一些配置,比如字体、背景、亮度等,这也是epubjs所开放api支持的,我们需要一个菜单栏来操作它。

代码如下,因为过长,样式部分省略。

<template>
  <div class="menu-bar">
    <transition name="slide-up">
      <div class="menu-wrapper" :class="{'hide-box-shadow': ifSettingShow || !ifTitleAndMenuShow}" v-show="ifTitleAndMenuShow">
        <div class="icon-wrapper">
          <span class="icon-menu icon" @click="showSetting(3)"></span>
        </div>
        <div class="icon-wrapper">
          <span class="icon-progress icon" @click="showSetting(2)"></span>
        </div>
        <div class="icon-wrapper">
          <span class="icon-bright icon" @click="showSetting(1)"></span>
        </div>
        <div class="icon-wrapper">
          <span class="icon-a icon" @click="showSetting(0)">A</span>
        </div>
      </div>
    </transition>
    <transition name="slide-up">
    <div class="setting-wrapper" v-show="ifSettingShow">
      <div class="setting-font-size" v-if="showTag === 0">
        <div class="preview" :style="{fontSize: fontSizeList[0].fontSize + 'px'}">A</div>
        <div class="select">
          <div class="select-wrapper" v-for="(item, index) in fontSizeList" :key="index" @click="setFontSize(item.fontSize)">
            <div class="line"></div>
            <div class="point-wrapper">
              <div class="point" v-show="defaultFontSize === item.fontSize">
                <div class="small-point"></div>
              </div>
            </div>
            <div class="line"></div>
          </div>
        </div>
        <div class="preview" :style="{fontSize: fontSizeList[fontSizeList.length - 1].fontSize + 'px'}">A</div>
      </div>
      <div class="setting-theme" v-else-if="showTag === 1">
        <div class="setting-theme-item" v-for="(item, index) in themeList" :key="index" @click="setTheme(index)">
          <div class="preview" :style="{background: item.style.body.background}" :class="{'no-border': item.style.body.background !== '#fff'}"></div>
          <div class="text" :class="{'selected': index === defaultTheme}">{{item.name}}</div>
        </div>
      </div>
      <div class="setting-progress" v-else-if="showTag === 2">
        <div class="progress-wrapper">
          <input class="progress" type="range"
                 max="100"
                 min="0"
                 step="1"
                 @change="onProgressChange($event.target.value)" @input="onProgressInput($event.target.value)"
                 :value="progress"
                 :disabled="!bookAvailable"
                 ref="progress">
        </div>
        <div class="text-wrapper">
          <span>{{bookAvailable ? progress + '%' : '加载中...'}}</span>
        </div>
      </div>
    </div>
  </transition>
    <content-view :ifShowContent="ifShowContent"
                    v-show="ifShowContent"
                    :navigation="navigation"
                    :bookAvailable="bookAvailable"
                    @jumpTo="jumpTo"></content-view>
    <transition name="fade">
      <div class="content-mask"
            v-show="ifShowContent"
            @click="hideContent"></div>
    </transition>
  </div>
</template>

<script>
import ContentView from '@/components/Content'
export default {
  components: {
    ContentView
  },
  props: {
    ifTitleAndMenuShow: {
      type: Boolean,
      default: false
    },
    fontSizeList: Array,
    defaultFontSize: Number,
    themeList: Array,
    defaultTheme: Number,
    bookAvailable: {
      type: Boolean,
      default: false
    },
    navigation: Object
  },
  data() {
    return {
      ifSettingShow: false,
      showTag: 0,
      progress: 0,
      ifShowContent: false
    }
  },
  methods: {
    hideContent() {
      this.ifShowContent = false
    },
    jumpTo(target) {
      this.$emit('jumpTo', target)
    },
    onProgressInput(progress) {
      this.progress = progress
      this.$refs.progress.style.backgroundSize = `${this.progress}% 100%`
    },
    onProgressChange(progress) {
      this.$emit('onProgressChange', progress)
    },
    setTheme(index) {
      this.$emit('setTheme', index)
    },
    setFontSize(fontSize) {
      this.$emit('setFontSize', fontSize)
    },
    showSetting(tag) {
      this.showTag = tag
      if (this.showTag === 3) {
        this.ifSettingShow = false
        this.ifShowContent = true
      } else {
        this.ifSettingShow = true
      }
    },
    hideSetting() {
      this.ifSettingShow = false
    }
  }
}
</script>

3. 效果

章节信息直接硬编码在了介绍页,点击跳转按照epub的编号做了,打包好代码,直接在局域网的一个树莓派上部署,起飞~

image.png

谨以此文。

纪念我坚强无比,最后却因为被剧透而快爆掉的肝。

Q.E.D.