章节内容
历史介绍
开发环境
代码封装
函数式
获取小说命令行版(上)
获取小说命令行版(中)
获取小说命令行版(下)
嵌入到应用中
插件机制
文字转语音
设置与信号中断
插件商店
下载与删除(上)
下载与删除(下)
通知栏程序
播放功能
美化界面
数据可视化(上)
数据可视化(中)
数据可视化(下)
嵌入到应用中
打包应用

插件商店的实现原理其实跟脚手架差不多,比如 egg-init 、vue-cli ,首先有一个仓库存储了所有的模板与地址列表,所以我先创建了一个存储的仓库 ,然后我又把爬取规则创建了一个仓库, ybdy_downloader 。

# 拉取商店列表

首先我们将 loadPlugins 改成 plugins , 这样我们就可以把 plugin 的所有逻辑都放到这个里面来。然后我们将插件保存的目录也改成可配置的。修改 plugins.ts

const pluginsPath = store.get(
  'PLUGIN_PATH',
  resolve(app.getPath('home'), '.reader-app-scripts')
)
ensureDirSync(pluginsPath)

修改 tray.ts

PLUGIN_PATH: {
  type: 'path',
  label: ' 插件路径',
  defaultValue: resolve(app.getPath('home'), '.reader-app-scripts')
},

修改 setting.ts , 我们需要添加一个刷新的逻辑。

const fetchAllPlugins = (send: any) => {
  // 从远端获取所有新插件
  promisified(storeURL).then((res: any) => {
    let db = []
    try {
      db = JSON.parse(res.body.toString())
    } catch (e) {}
    send('all_plugins', db) // 发送到渲染进程
    store.set('ALL_PLUGINS', db) // 保存到 store
  })
}

on('reload_all_plugins').subscribe(async ({ event, args }) => {
  await fetchAllPlugins(event.sender.send)
  ipcMain.emit('get_all_plugins', event, args) // 触发自己的 get_all_plugins 事件
})

on('get_all_plugins').subscribe(({ event, args }) => {
  const db = store.get('ALL_PLUGINS', [])
  if (db.length > 0) {
    return event.sender.send('all_plugins', db)
  }
  fetchAllPlugins(event.sender.send)
})

为了让插件支持热更新,需要通过事件触发更新,假设把 plugin 丢到 store 里面去,由于序列化,会导致爬取方法丢失,所以只能存一下文件名做一下缓存。修改 index.ts 的 ready 方法,当然也可以用一个方法,比如 pluginSetUp 把下面的逻辑包裹一下。

const load = () => {
  let [_, temPlugins] = loadPlugins()
  plugins = temPlugins
}

load()

on('reload_local_plugins').subscribe(load)

再次修改 plugins.ts , 我们需要 filename 来判断该插件是否已经下载过了, 当接收到 reload_local_plugins 信号更新配置,这不仅会触发 index.ts 的事件监听,还会触发 plugins.ts 里的事件监听。

const loadPlugins = () => {
  let files = readdirSync(pluginsPath)
  return [
    files, // 文件名,因为要判断本地已安装插件
    files.map(filename =>
      requireFoolWebpack(resolve(pluginsPath, filename))(requireFoolWebpack)
    )
  ]
}
const saveToSetting = (all?: any) => {
  const { event, args } = all
  const [files, _] = loadPlugins()
  store.set('LOACAL_PLUGINS', files) // 将已安装插件,存入 store
  event && ipcMain.emit('get_local_plugins', event, args) // 发送到渲染进程
}
saveToSetting()
on('reload_local_plugins').subscribe(saveToSetting)
on('get_local_plugins').subscribe(({ event, args }) => {
  event.sender.send('local_plugins', store.get('LOACAL_PLUGINS', []))
})

# 完成前端

首先安装以下依赖,来支持简介的 markdown 渲染支持。

npm install marked --save

修改 package.json,将 markd 打包进去,要不然会找不到 marked

"electronWebpack": {
    "whiteListedModules": ["marked"]
}

新建 pages/CrawlStore.svelte , 大多数东西跟 Vue 比较类似,helpers 是在模板里面访问的方法,它是纯函数,不具备 this 的访问能力。在 oncreate 的时候通过事件捕获 a 标签的点击,使用浏览器打开链接,以及拉取数据。

<h2>插件列表</h2>

<ul ref:list>
    {#each plugins as plugin}
    <li>
        <a target="_blank" href={plugin.repository}>{plugin.filename} - {plugin.author} { plugin.downloaded ? 'downloaed' : ''}</a>
        <p>{@html marked(plugin.description || '')}</p>
    </li>
    {/each}
</ul>

<script>
    import {
        ipcRenderer as ipc,
        shell
    } from 'electron'
    import marked from 'marked'

    function addDownloaed(plugins, local_plugins) {
        return plugins.map(plugin => {
            plugin.downloaded = local_plugins.findIndex(local_plugin => local_plugin == plugin.filename) >= 0
            return plugin
        })
    }
    export default {
        data() {
            return {
                local_plugins: [],
                all_plugins: []
            }
        },
        helpers: {
            marked,
        },
        computed: {
            plugins: ({
                all_plugins,
                local_plugins
            }) => addDownloaed(all_plugins, local_plugins)
        },
        oncreate() {
            ipc.send('get_all_plugins')
            ipc.send('get_local_plugins')
            ipc.on('all_plugins', (event, all_plugins) => {
                this.set({
                    all_plugins
                })
            })
            ipc.on('local_plugins', (event, local_plugins) => {
                this.set({
                    local_plugins
                })
            })
            this.refs.list.addEventListener("click", (e) => { // 处理插件的打开链接
                if (e.target.tagName.toLowerCase() == 'a') {
                    e.preventDefault()
                    shell.openExternal(e.target.href)
                }
            }, true)
        }
    }
</script>

最后别忘记在 App.svelte 里面引入并装载它。

现在的结果看起来会像下面这样,因为还没有任何样式,所以可能看起来比较简陋。

store-list

# 实现路由与全局状态

我们来为 svelte 添加路由支持,svelte 自身是不带路由组件的, 因为作者更希望开发者自己去控制,所以也导致了难度有一些提升。

由于我们是 Electron 环境,没必要在意浏览器的 url 是什么,所以我们根本就不需要关心浏览器前进、后退的兼容,与参数解析。

下面是几个 svlte 路由的库和例子,大家想研究的话可以去看一下

  • https://github.com/krasimir/navigo
  • https://github.com/ilog2000/svelte-navigo
  • https://github.com/EmilTholin/svelte-routing
  • https://github.com/Rich-Harris/roadtrip

# 创建 store.js

为了持久化存储,Svelte 自带了全局状态管理,为了更好的项目结构,我们将之前写好的页面都放到 pages 文件下。

import { Store } from 'svelte/store.js'
import Status from './pages/Status.svelte'
import Download from './pages/Download.svelte'
import CrawlStore from './pages/CrawlStore.svelte'

const store = new Store({
  currentPage: Status // 当前页面
})

store.changePage = changePage.bind(store)

function changePage(pageName) {
  const map = {
    Status,
    Download,
    CrawlStore
  }
  const current = store.get().currentPage //当前页
  store.set({ currentPage: map[pageName] || current })
}

export { changePage }

export default store

我是直接通过  添加到实例上面添加的方法,大家也可以参考官方的文档,按照官方的例子去做。

这里我们通过 changePage 来修改 currentPage 变量,然后我们把 currentPage 绑定到动态组件上面去,这样我们就实现了路由。

# 修改 App.svelte

<Link className="Hello" to="Download">下载页面</Link>
<Link className="Hello" to="Status">状态</Link>
<Link className="Hello" to="CrawlStore">爬虫商店</Link>
<svelte:component this="{$currentPage}" />

<script>
    export default {
        components: {
            Link: './components/Link.svelte'
        }
    };
</script>

这样我们就实现了路由,但是我们还需要切换,所有我们构建一个 Link 组件。

# 跳转组件

新建 components/Link.svelte ,阻止默认事件,修改全局状态即可。

<a class={className} href={to} ref:link>
    <slot/>
</a>

<script>
    export default {
        oncreate() {
            this.refs.link.addEventListener('click', (e) => {
                e.preventDefault(); // 阻止默认事件
                this.store.changePage(this.get().to)
            }, false)
        }
    }
</script>