2022/01/24

Electron Bookmarker

這是 Electron in Action chap2 的 github sample bookmarker github,該章節是第一個 Electron applicaiton sample,裡面使用了 localStorage 及 shell,因為測試的關係,修改使用 IPC,將 shell 呼叫移到 main process。

Electron API 有列出 API 項目,以及該 API 可以使用在 Main/Renderer process 的資訊

package.json

  • 將 electron 版本改為目前的 stable 版
    • "electron": "^13.1.9"
{
  "name": "bookmarker",
  "version": "1.0.0",
  "description": "An example Electron application",
  "main": "app/main.js",
  "scripts": {
    "start": "electron .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/electron-in-action/bookmarker.git"
  },
  "author": "Steve Kinney <hello@stevekinney.net> (http://stevekinney.net)",
  "license": "MIT",
  "private": true,
  "dependencies": {},
  "bugs": {
    "url": "https://github.com/electron-in-action/bookmarker/issues"
  },
  "homepage": "https://github.com/electron-in-action/bookmarker#readme",
  "devDependencies": {
    "electron": "^13.1.9"
  }
}

main.js

  • 把原本放在 renderer process 的 shell 呼叫,移到 main process

  • 增加 IPC main 的部分,處理 renderer process 傳送的訊息及 href 參數,然後呼叫 shell.openExternal(href);

  • 因為新版的 electron 的限制,renderer process 使用了 localStorage,故 BrowserWindow 必須增加兩個 webPreferences 設定

    mainWindow = new BrowserWindow({
          webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
          }
      });
  • 用以下 code 查詢 localStorage 儲存的硬碟路徑

    // 查詢 application 將 localStorage 儲存在哪一個 folder
      let userdata = app.getPath('userData');
      console.log('userdata='+userdata);
const {
  app,
  shell,
  BrowserWindow,
  ipcMain
} = require('electron');

let mainWindow = null; // #A

app.on('ready', () => {
  console.log('Hello from Electron.');

  // 查詢 application 將 localStorage 儲存在哪一個 folder
  let userdata = app.getPath('userData');
  console.log('userdata='+userdata);

  mainWindow = new BrowserWindow({
      webPreferences: {
        nodeIntegration: true,
        contextIsolation: false
      }
  });
  mainWindow.webContents.loadURL(`file://${__dirname}/index.html`); // #A
});

ipcMain.on('shell-openExternal', (event, data) => {
  var href = data.href;
  console.log("shell-openExternal href="+href);
  shell.openExternal(href);
});

index.html

  • 修改 include js 的部分

    <!--     <script>
          require('./renderer');
        </script> -->
        <script src="renderer.js"></script>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="stylesheet" href="style.css" type="text/css">
    <title>Bookmarker</title>
  </head>
  <body>

    <h1>Bookmarker</h1>

    <div class="error-message"></div>

    <section class="add-new-link">
      <form class="new-link-form">
        <input type="url" class="new-link-url" placeholder="URL" required>
        <input type="submit" class="new-link-submit" value="Submit" disabled>
      </form>
    </section>

    <section class="links"></section>

    <section class="controls">
      <button class="clear-storage">Clear Storage</button>
    </section>

<!--     <script>
      require('./renderer');
    </script> -->
    <script src="renderer.js"></script>

  </body>

</html>

style.css

html {
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body, input {
  font: menu; // #A
}

renderer.js

  • 刪除 shell 呼叫的部分,改用 IPC 傳送到 main process 處理

  • 注意處理 Submit 的寫法

    • 在填寫 newLinkUrl 時,用 validity 檢查是否為正確的 URL 字串,然後將結果,轉為指定 newLinkSubmit enable/disable

    • submit form 的時候,event.preventDefault(); 可避免 Chromium 呼叫原本 form submit 的動作,不會 trigger HTTP request

    • fetch 可取得 URL 的 response,在這邊只需要 title,用來作為 bookmark 的 title。實際上是用了 DOMParser parsing HTTP response data

      newLinkUrl.addEventListener('keyup', () => {
      newLinkSubmit.disabled = !newLinkUrl.validity.valid;
      });
      
      newLinkForm.addEventListener('submit', (event) => {
      event.preventDefault();
      
      const url = newLinkUrl.value;
      
      fetch(url)
        .then(response => response.text())
        .then(parseResponse)
        .then(findTitle)
        .then(title => storeLink(title, url))
        .then(clearForm)
        .then(renderLinks)
        .catch(error => handleError(error, url));
      });
  • localStorage 是 key-value 的形式

    localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
let ipcRenderer = require('electron').ipcRenderer;

// const {shell} = require('electron');

const parser = new DOMParser();

const linksSection = document.querySelector('.links');
const errorMessage = document.querySelector('.error-message');
const newLinkForm = document.querySelector('.new-link-form');
const newLinkUrl = document.querySelector('.new-link-url');
const newLinkSubmit = document.querySelector('.new-link-submit');
const clearStorageButton = document.querySelector('.clear-storage');

newLinkUrl.addEventListener('keyup', () => {
  newLinkSubmit.disabled = !newLinkUrl.validity.valid;
});

newLinkForm.addEventListener('submit', (event) => {
  event.preventDefault();

  const url = newLinkUrl.value;

  fetch(url)
    .then(response => response.text())
    .then(parseResponse)
    .then(findTitle)
    .then(title => storeLink(title, url))
    .then(clearForm)
    .then(renderLinks)
    .catch(error => handleError(error, url));
});

clearStorageButton.addEventListener('click', () => {
  localStorage.clear();
  linksSection.innerHTML = '';
});

linksSection.addEventListener('click', (event) => {
  if (event.target.href) {
    event.preventDefault();
    var data = {
      href: event.target.href
    };
    // shell.openExternal(event.target.href);
    ipcRenderer.send('shell-openExternal', data);

  }
});


const clearForm = () => {
  newLinkUrl.value = null;
}

const parseResponse = (text) => {
  return parser.parseFromString(text, 'text/html');
}

const findTitle = (nodes) => {
  return nodes.querySelector('title').innerText;
}

const storeLink = (title, url) => {
  localStorage.setItem(url, JSON.stringify({ title: title, url: url }));
}

const getLinks = () => {
  return Object.keys(localStorage)
               .map(key => JSON.parse(localStorage.getItem(key)));
}

const convertToElement = (link) => {
  return `<div class="link"><h3>${link.title}</h3>
          <p><a href="${link.url}">${link.url}</a></p></div>`;
}

const renderLinks = () => {
  const linkElements = getLinks().map(convertToElement).join('');
  linksSection.innerHTML = linkElements;
}

const handleError = (error, url) => {
  errorMessage.innerHTML = `
    There was an issue adding "${url}": ${error.message}
  `.trim();
  setTimeout(() => errorMessage.innerText = null, 5000);
}

const validateResponse = (response) => {
  if (response.ok) { return response; }
  throw new Error(`Status code of ${response.status} ${response.statusText}`);
}

renderLinks();

執行畫面

References

Electron in Action

Electron in Action repositories

Electron in Action source code

require is not defined

Where an electron application's sessionStorage and localStorage stored?

electron-localStorage,如何在主进程和渲染进程中使用?

沒有留言:

張貼留言