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

拉取商店列表

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

  1. const pluginsPath = store.get(
  2. 'PLUGIN_PATH',
  3. resolve(app.getPath('home'), '.reader-app-scripts')
  4. )
  5. ensureDirSync(pluginsPath)

修改 tray.ts

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

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

  1. const fetchAllPlugins = (send: any) => {
  2. // 从远端获取所有新插件
  3. promisified(storeURL).then((res: any) => {
  4. let db = []
  5. try {
  6. db = JSON.parse(res.body.toString())
  7. } catch (e) {}
  8. send('all_plugins', db) // 发送到渲染进程
  9. store.set('ALL_PLUGINS', db) // 保存到 store
  10. })
  11. }
  12. on('reload_all_plugins').subscribe(async ({ event, args }) => {
  13. await fetchAllPlugins(event.sender.send)
  14. ipcMain.emit('get_all_plugins', event, args) // 触发自己的 get_all_plugins 事件
  15. })
  16. on('get_all_plugins').subscribe(({ event, args }) => {
  17. const db = store.get('ALL_PLUGINS', [])
  18. if (db.length > 0) {
  19. return event.sender.send('all_plugins', db)
  20. }
  21. fetchAllPlugins(event.sender.send)
  22. })

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

  1. const load = () => {
  2. let [_, temPlugins] = loadPlugins()
  3. plugins = temPlugins
  4. }
  5. load()
  6. on('reload_local_plugins').subscribe(load)

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

  1. const loadPlugins = () => {
  2. let files = readdirSync(pluginsPath)
  3. return [
  4. files, // 文件名,因为要判断本地已安装插件
  5. files.map(filename =>
  6. requireFoolWebpack(resolve(pluginsPath, filename))(requireFoolWebpack)
  7. )
  8. ]
  9. }
  10. const saveToSetting = (all?: any) => {
  11. const { event, args } = all
  12. const [files, _] = loadPlugins()
  13. store.set('LOACAL_PLUGINS', files) // 将已安装插件,存入 store
  14. event && ipcMain.emit('get_local_plugins', event, args) // 发送到渲染进程
  15. }
  16. saveToSetting()
  17. on('reload_local_plugins').subscribe(saveToSetting)
  18. on('get_local_plugins').subscribe(({ event, args }) => {
  19. event.sender.send('local_plugins', store.get('LOACAL_PLUGINS', []))
  20. })

完成前端

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

  1. npm install marked --save

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

  1. "electronWebpack": {
  2. "whiteListedModules": ["marked"]
  3. }

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

  1. <h2>插件列表</h2>
  2. <ul ref:list>
  3. {#each plugins as plugin}
  4. <li>
  5. <a target="_blank" href={plugin.repository}>{plugin.filename} - {plugin.author} { plugin.downloaded ? 'downloaed' : ''}</a>
  6. <p>{@html marked(plugin.description || '')}</p>
  7. </li>
  8. {/each}
  9. </ul>
  10. <script>
  11. import {
  12. ipcRenderer as ipc,
  13. shell
  14. } from 'electron'
  15. import marked from 'marked'
  16. function addDownloaed(plugins, local_plugins) {
  17. return plugins.map(plugin => {
  18. plugin.downloaded = local_plugins.findIndex(local_plugin => local_plugin == plugin.filename) >= 0
  19. return plugin
  20. })
  21. }
  22. export default {
  23. data() {
  24. return {
  25. local_plugins: [],
  26. all_plugins: []
  27. }
  28. },
  29. helpers: {
  30. marked,
  31. },
  32. computed: {
  33. plugins: ({
  34. all_plugins,
  35. local_plugins
  36. }) => addDownloaed(all_plugins, local_plugins)
  37. },
  38. oncreate() {
  39. ipc.send('get_all_plugins')
  40. ipc.send('get_local_plugins')
  41. ipc.on('all_plugins', (event, all_plugins) => {
  42. this.set({
  43. all_plugins
  44. })
  45. })
  46. ipc.on('local_plugins', (event, local_plugins) => {
  47. this.set({
  48. local_plugins
  49. })
  50. })
  51. this.refs.list.addEventListener("click", (e) => { // 处理插件的打开链接
  52. if (e.target.tagName.toLowerCase() == 'a') {
  53. e.preventDefault()
  54. shell.openExternal(e.target.href)
  55. }
  56. }, true)
  57. }
  58. }
  59. </script>

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

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

store-list

实现路由与全局状态

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

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

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

创建 store.js

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

  1. import { Store } from 'svelte/store.js'
  2. import Status from './pages/Status.svelte'
  3. import Download from './pages/Download.svelte'
  4. import CrawlStore from './pages/CrawlStore.svelte'
  5. const store = new Store({
  6. currentPage: Status // 当前页面
  7. })
  8. store.changePage = changePage.bind(store)
  9. function changePage(pageName) {
  10. const map = {
  11. Status,
  12. Download,
  13. CrawlStore
  14. }
  15. const current = store.get().currentPage //当前页
  16. store.set({ currentPage: map[pageName] || current })
  17. }
  18. export { changePage }
  19. export default store

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

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

修改 App.svelte

  1. <Link className="Hello" to="Download">下载页面</Link>
  2. <Link className="Hello" to="Status">状态</Link>
  3. <Link className="Hello" to="CrawlStore">爬虫商店</Link>
  4. <svelte:component this="{$currentPage}" />
  5. <script>
  6. export default {
  7. components: {
  8. Link: './components/Link.svelte'
  9. }
  10. };
  11. </script>

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

跳转组件

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

  1. <a class={className} href={to} ref:link>
  2. <slot/>
  3. </a>
  4. <script>
  5. export default {
  6. oncreate() {
  7. this.refs.link.addEventListener('click', (e) => {
  8. e.preventDefault(); // 阻止默认事件
  9. this.store.changePage(this.get().to)
  10. }, false)
  11. }
  12. }
  13. </script>