Writing in the front

Electron is an open source library for building cross-platform desktop applications using front-end languages, developed by Github. In Electron, we can easily build multi-terminal desktop applications using the Node API, Chromium API and Native API. About the basic concept of Electron, I wrote an article a few days ago, which briefly gave a general popularization of Electron. Students who do not know much about Electron can go to have a look.

Portal: Electron – Electron principle and simple practice

Cause & Value

In daily life and work, notes and browser bookmarks are the main means for us to use the commonly used web sites on mobile phones. I have always followed this method and have found many solutions, including but not limited to browser plug-ins and desktop notes. However, when Kaola was still at netease, I felt that there were too many internal platforms, and different platforms had different environments, which were not as fixed and easy to remember as the websites needed in daily life. When I moved to Ali, there were a lot of new platforms in the new environment.

Do you know the website of XX platform? .

B: I don’t know, if you look in xx files/chat logs, it might come up.

However, there are some problems with bookmarks or notes that can be used as records, which makes me feel inconvenient

  • Link length – you need to open your notes/browser first to find the bookmark bar address
  • Interrupt the current screen
  • Bookmark library sharing trouble, there is no base bookmark library, the same URL everyone needs to favorites

As a programmer, it is natural to want a tool to optimize this operation. So what does this tool have to do? Less free time is available due to more daily development demands.


According to the Electron?

Why do you choose Electron? When I started to have an idea, I basically searched some other relevant technologies on the market:

PyQt

PyQt is mainly built based on Python code. It is simple, easy to learn, free, open source, and has a mature community. Although Python is relatively easy to learn, it is not my familiar field at this stage

Nw.js

The author of Electron is the same person, but Electron is maintained by a dedicated team at Github and officially has a technical comparison: There are technical differences between Electron and Nw.js (formerly nod-Webkit). To put it simply, Nw.js is a combination of desktop version and online version. Chinese documents are relatively lacking, and the community is not active

Ps: can concern on zhihu: www.zhihu.com/question/38…

Flutter

Fultter is a cross-terminal tool developed and maintained by Google, but Dart has a learning cost. Although I would love to learn, the desktop of Flutter is not mature, and Google did not commit to supporting it, just an “exploratory experiment”, so abandoned it

Electron

The introduction of electron will not be described in detail. The main reason for choosing electron is that the community is active, the documentation is complete, and there are many successful cases. It is also helpful for understanding the development of VScode plug-in

What to achieve

With a preliminary idea and tool selection, I will divide the functions I want to implement into two categories:

  • Basic functions (short – term and small range can be used
  • Advanced functions (long-term planning, which can be popularized)

Basic functions:

  1. Basic operation interface, simple and convenient
  2. Quick call up, can be in different scenes through the shortcut keys quickly call up the search bar
  3. Supports local entry of items, builds persistent local database, can quickly retrieve items and give the expected match results
  4. Read chrome bookmarks and import chrome bookmarks into your local database
  5. Provides a navigation interface to facilitate direct access to commonly used web sites

Advanced functions:

  1. Chrome plugin, companion tool, quickly synchronize chrome pages and bookmarks to the application
  2. Update server, support automatic update and even hot update
  3. The deployment server can optionally share data with others


The basic idea

I have combed out some functions and outlined the basic idea of implementation. You can click open to see the big picture.


Implement it while stepping on pits

In view of the daily development of the technology stack is mainly Vue, so the choice of Electron- Vue into this scaffold to reduce the early configuration work, attached: Electron- Vue official documentation

Kneel in step 1: Install failed

According to the official steps, the project of electron- Vue has been initialized,

vue init simulatedgreg/electron-vue my-project
Install dependencies and run your program
cd my-project
yarn Or NPM install
yarn run dev Or NPM run devCopy the code

However, in the heart of install, found that the download speed of electron is very slow, often download progress 50% hang because it is a personal computer can not be accelerated on the company’s network, the first time more Taobao source mirror, thought that the speed problem will be solved, however.

Check and find that the compressed release package will be downloaded from Github during the electron download. The download speed in China fluctuates greatly, and download failure often occurs. Therefore, you need a good ladder and a good agent.

Note: ss global proxy does not work on the command line, you need privoxy on MAC, you can Google:”MAC command line privoxy”


With these ready, I can finally implement my desired functionality with pleasure ~~

A frameless interface that can be invoked at any time

Stripping out the extraneous parts of the sample code, the running code has this interface:

Compared to Spotlight, it’s not so beautiful

So how do you implement a frameless interface where you need a property

new BrowserWindow({
        height: 400,
        width: windowWidth,
        frame: false,})Copy the code

So we can have a frameless interface

So how to achieve fast arousal anytime and anywhere? It’s natural to think of global shortcuts

. new BrowserWindow({ height: 400, width: windowWidth, frame:false,
          show: false, // Initially hide the window})... // Register global shortcuts and display window globalshortcut.register ('Alt+A', () => {
        mainWindow.show();
    })Copy the code

In general, the simple shortcut works for us, but in full screen, it works like this


Quick evoke question 1. Mov


In the case of full screen, simply registering the shortcut keys cannot achieve the effect of integrating spotlight with the background. I searched many issues and articles and finally found two solutions:

  1. Using MACOS’s PanelWindow, Electron native does not support it and needs to be implemented manually. I am not the developer of THE OS, but there is a big guy on Github who implemented it: Electron – panel-Window
  2. Use app.dock.hide(), which is intended to hide the dock at the bottom of the MAC, but has the added bonus of being able to evoke a fusion in full screen
// Improved shortcut to register callback globalshortcut.register ('Alt+A', () = > {if(! mainWindow.isVisible()) { app.dock.hide(); mainWindow.show(); }else{ mainWindow.hide(); app.dock.show(); }})Copy the code

The search box didn’t follow when you cut to another screen, although it was evoked in full screen:


Multi-screen problem 1.mov


Check the official documentation and you’ll find a property under BrowserWindow:

AlwaysOnTop: Boolean (optional) – whether a window is alwaysOnTop of another window. The default value is false.

We added this property, along with the APP’s out-of-focus event, and finally got the performance we were looking for


. mainWindow = new BrowserWindow({ height: 50, width: windowWidth, alwaysOnTop:true,
  show: false,
  frame: false,})... // Listen for app out-of-focus events app.on('browser-window-blur', () => {
  mainWindow.hide();
  app.dock.show();
})Copy the code

Effect:


Evoke the final effect 1.mov


The implementation of search function

Basic window display solved, now to achieve the basic search function, the search is based on data ~

Local databases and data structures

To store data, you need a persistent local database. The official recommended database is Nedb

The JavaScript Database, for Node.js, nw.js, electron and the browser

This is enough for me to use it, Nedb is smaller and lighter than Mysql, MongoDB, Redis, etc. Written in JavaScript with natural support for Electron, it can be thought of as a lite version of MongoDB.

Choose a good database and then design a basic data structure for the basic information storage and display, Nedb document structure can be very convenient for the subsequent expansion of data.

{
  url: 'http://www.baidu.com'// The URL name of the URL:"Baidu"// Search is the title of the entry display keyWords:'search', // Keywordstype: 'insert'// Whether to open desc in the program or in the browser:'Baidu's search products', // description icon:'http://www.google.com/s2/favicons?domain=www.baidu.com'// Use the display icon}Copy the code


Word segmentation & similarity comparison

The data is already available, and the next step is how to search for the items we expect. In order to search for the items we expect in a wider range, a simple search algorithm is designed:

1, the word segmentation

Stutter participle is a widely used participle bank in Chinese word segmentation. The node version of “stutter participle” I choose this time is: nodejieba:

  • Jieba Chinese word segmentation series performance evaluation
  • Online demonstration of word segmentation effect

It should be noted that the default word segmentation algorithm: nodejieba.cut() will cut English words into letters during the actual operation. HMM algorithm is recommended:

nodejieba.cutHMM(searchKey);Copy the code

2, retrieval,

After word segmentation, our search terms are sliced into an array, for example:

searchKey = "Baidu baidu 1"console.log(nodejieba.cutHMM(searchKey)); / / /"Baidu".'1'.'baidu' ]Copy the code

We need according to the string array, the array containing all the data filtered, based query to realize the matching disorderly array containing all the items as a substring of string is more complicated, straight-tempered Nedb support for regular expressions for queries, we use positive first asserts that regular expressions to match strings containing all of the substring function. Sample code is as follows

letreg = /(? = (. *? aa)(? = (. *? bb)(? = (. *? cc)/ reg.test('aabbcc') / /true
reg.test('ccbbaa') / /true
reg.test('bbvvaaffcc') / /true
reg.test('bbvvaaff') / /falseCopy the code


The complete search function

let search = function({searchKey}) {// participleletsearchTokens = nodejieba.cutHMM(searchKey); // The search term must contain all the segmentation results to be found, can be unorderedletregString = searchTokens.map(ele => `(? = (. *?${ele})`).join(' ')
    let reg = new RegExp(regString);
    return new Promise((reslove, reject) => {
        db.find({
            $or: [{
                url: { $regex: reg }
            }, {
                name: { $regex: reg }
            }, {
                keyWords: { $regex: reg }
            }, {
                desc: { $regex: reg }
            }]
        }, function (err, docs) {
            if (err) {
                reject(err)
            } else{ reslove(docs) } }); })}Copy the code


3, sorting,

The data is matched, but there may be many pieces of data, so which one is the most consistent with our expectations? At this time, we need to sort the retrieval results:

Calculates the similarity of two strings:

The third-party package String-Similarity is selected in this paper to calculate the similarity of two strings. The string-Similarity calculation algorithm is based on Dice’s Coefficient. If you are interested, you can refer to the introduction of Dice Coefficient in Wikipedia. The overall code is divided into

  • Obtain the similarity of title, description, keyword and URL of the search term and the item in the search result
  • Weight the similarity by => URL: Name: Keyword: Description 10:10:5:3
  • Sort search results by weighted value

The specific code logic is as follows:

let compareTwoStrings = function (first, second) {
    if (first == second) {
        return 1;
    }
    if (first.length === 1 || second.length === 1) {
        let max = Math.max(first.length, second.length);
        let min = Math.min(first.length, second.length);
        return min/max;
    } else {
        returnstringSimilarity.compareTwoStrings(first, second); }}let sort = function (searchResult, searchKey) {
    // console.log('sort', searchResult, searchKey)
    searchResult = searchResult.map((ele) => {
        let { url, name, keyWords, desc } = ele;
        let urlRate = compareTwoStrings(url, searchKey);
        let nameRate = compareTwoStrings(name, searchKey);
        // console.log(name, searchKey, nameRate)
        let keyWordsRate = compareTwoStrings(keyWords, searchKey);
        letdescRate = compareTwoStrings(desc, searchKey); // console.log(ele, urlRate, nameRate, keyWordsRate, Rate = 10 * urlRate + 10 * nameRate + 5 * keyWordsRate + 3 * descRate;return ele;
    })

    return searchResult.sort((a, b) => b.rate - a.rate);

}Copy the code


PS: hit the pit

NODE_MODULE_VERSION encountered an error using nodeJieba as follows:

error:: Error: The module '<project>/node_modules/electron/node_modules/ref/build/Release/binding.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 57. This version of Node.js requires
NODE_MODULE_VERSION 54. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or`npm install`).Copy the code

The solution

yarn add electron-rebuild --dev
./node_modules/.bin/electron-rebuildCopy the code

The module to be rebuilt in electron rebuild must be in dependencies, not in devDependencies. Because electron-rebuild only builds to dependencies.

Rendering process display

With the basic data and search in place, it’s time for presentation, which this example divides into four presentation modes

  • Single bar: Has only one search box
  • Bar + List: indicates the list of search boxes and search results
  • Bar + Navigation: Search box + navigation list
  • Bar + IFrame: Search box + embedded page

A single search bar

The main functions of searching bar are:

  • Enter search terms
  • The Enter&Input event triggers the search
  • Resize the window

The triggering of search events mainly depends on process communication and event listening, which is the same as listening for input events and keyboard events in ordinary browsers.

ipcRenderer.on('search-result', (event, arg) => {
  this.list = arg;
  this.$forceUpdate(a); // this.handleReset(['iframeUrl'])
  // console.log(arg) // prints "pong"})... ipcRenderer.send('search', {
        searchKey: $event
 });Copy the code

The ability to resize a window is based on

BrowserWindow setSize function, core code

    ipcMain.on('change-window', (event, arg) => {
        if (arg === 'small') {
            mainWindow.setSize(windowWidth, minWindowHeight, true)}else {
            mainWindow.setSize(windowWidth, maxWindowHeight, true)}})Copy the code

Effect:


Window change.mov


Navigation bar +

Navigation is mainly used to show users’ fixed or frequently used pages. Related table structure and frequency algorithm have not been completed yet. The following is the display effect of navigation, which can be deleted, edited and opened inside and outside.


Navigation 720. Mov


bar+list

The functions of list are as follows:

  • Display search results
  • Click or Enter to jump to an inline or browser page, and the Up & Down control list activates the up and down entries.

Core point 1: Get the site icon:

The main idea of obtaining an Icon is

  1. Obtain the HTML file of the web page through request
  2. Load HTML data through Cheerio and read link[rel~=”icon”] or link[rel~=”icon”] data
  3. Modify the URL for different situations
  4. Read failed: use the default ‘${parseurl.origin}/favicon.ico’

Core code:

const $ = cheerio.load(body);
let icon = ($('link[rel~="icon"]') [0] && $('link[rel~="icon"]')[0].attribs.href) ||
    ($('link[rel~="ICON"]') [0] && $('link[rel~="ICON"]')[0].attribs.href);
const parseUrl = new URL(url);
if (icon) {
  if(! /^ HTTP /.test(icon)) {// Not HTTPif (icon[0] === '/') {
      icon = `${parseUrl.origin}${icon}`}else {
      icon = `${parseUrl.origin}/${icon}`}}}else {
  icon = `${parseUrl.origin}/favicon.ico`
}
if (url[url.length - 1] === '/') {
  url = url.slice(0, url.length - 1);
}Copy the code


Core point 2: The default browser opens the link, mainly relying on the shell module’s openExternal function

ipcMain.on('goto', (event, arg) => {
  shell.openExternal(arg.url)
})Copy the code


List 720. Mov


bar+iframe

Simple Iframe embedding is relatively easy, just embed an Iframe element in the Vue component, but there are some pitfalls:

Pit 1: the display window is small, many pages need to slide, if embedded with a text to edit the page, the operation is more uncomfortable

Solution: Widen the width and height of iframe by a certain ratio, reduce it by transform, and then calculate and locate it according to scale again

<iframe
        :src="iframeUrl"
        :width="iframe.width * scale"
        :style="{transform: `scale(${1/scale})`, position: 'absolute', top: `${-top}px`,left: `${-left}px`}"
        :height="iframe.height * scale"
        frameborder="0"
        name="test"
        ref="iframe"
        @load="handleLoad"
        />
...
  computed: {
    top() {
      return (
        (this.iframe.height * (this.scale - 1)) / 2 - this.iframe.minHeight
      );
    },
    left() {
      return(this.iframe.width * (this.scale - 1)) / 2; }},Copy the code

Pit 2: redirects the page embedded in the Iframe

Ifram embedded page, jump again, such as Baidu jump to a specific page, language sparrow jump edit page, brought some other pit points,

  1. When the page favorites function is implemented, the dynamic address of the actual page in the IFrame cannot be obtained due to cross-domain problems
  2. A new window opens in the form of a third _blank

When jumping to a page twice or multiple times, such as using Baidu search results, and clicking on a specific edit on the language platform, the current page is often recorded and stored locally. However, due to the cross-domain security limitation of IFrame, the actual content of dynamic IFrame cannot be read. Embedded third-party pages are not controlled by the program and cannot be pulled by PostMessage. After checking the documents, WE find that the Electron WebView tag can solve this problem.

Replace iframe with webView

  • The cursor for editing a page is often not found
  • The display is inconsistent with the iframe presentation
  • Electron official warning

Electron’s WebView TAB is based on Chromium WebView
, which is undergoing a huge architectural change. This affects the stability of the WebView, including rendering, navigation, and event routing. We currently recommend not using webView tags and consider alternatives such as Iframe, Electron’s BrowserView, or an architecture that avoids embedding content entirely.

So we had to find another way. Flip through the Electron file and find: Bro’w’er’ViewwebContents

It can happen when the window.location object is changed or a user clicks a link in the page.

Practice found that it could not detect the second jump in Iframe, continued to browse the document, found several other events in BrowerView.webContents, after testing the following events can detect the jump action of Iframe:

  • did-start-navigation
  • did-frame-navigate

Although these events can monitor the change of iframe, they will also be detected when the third party page in which our iframe is embedded has embedded the sub-level IFrame. We cannot confirm which event is triggered corresponding to the current display page. Did-frame-navigate can detect events, but the loading time and order of the embedded iframe is uncertain, while the did-start-navigation ensures that the target URl is the first to jump to. So here’s the solution:

  1. Listen for the did-start-navigation event and maintain a global array. In the did-start-navigation event, Push the target URL in the array
  2. Listen for the onload event of the iframe. The onload event is not affected by the loading of the child IFrame. Send a message in the onload to the main process
  3. The main process listens for the onLoad message of the iframe and retrieves the first of the array as the target URL
  4. Obtain Title, desc, keywords, URL, and other parameters based on the URL using a similar method to obtain Icon above
  5. Emptying the global array

PS: To improve accuracy and reduce the impact of asynchronous iframe loading on reading current IFrame information, a domain name blacklist is maintained to filter common embedded IFrames, for example:

  'pagead2.googlesyndication.com'.'googleads.g.doubleclick.net'Copy the code

The report mechanism will be formed later for maintenance. The specific code is long, you can check the GitHub link

Another problem is that page jumps open a new window in the form of a third-party _blank. The solution is to listen for the new-window event, block the default and change the URL when opened in the browser.

webContents.on('new-window', async (event, navigationUrl) => {
  // In this example, we'll ask the operating system // to open this event's url in the default browser.
  const parsedUrl = new URL(navigationUrl);
  // console.log(parsedUrl)
  event.preventDefault();
  await shell.openExternal(navigationUrl)
})Copy the code


PS: Step on the pit, icon load 403.

Fix: add to HTML:

   <meta name="referrer" content="no-referrer">Copy the code


iframe720.mov


Debug the electron – vue

The above has realized the basic display, search, storage and other functions of the search tool, the basic function can be used, but there are still some complex logic on the way of development and the follow-up GUI hair, writing complex logic is indispensable debugging, In the previous article Electron- Electron principle and simple practice has been briefly introduced the Electron debugging, here we mainly talk about the Electron- VUE debugging.

Rendering process

Electronic-debug has been integrated in Electorn vue, and VueTools has been integrated in the window, so it is very convenient to debug the rendering process.

require('electron-debug')({ isEnabled: true })
// Install `vue-devtools`
require('electron').app.on('ready', () = > {let installExtension = require('electron-devtools-installer')
  installExtension.default(installExtension.VUEJS_DEVTOOLS)
    .then(() => { })
    .catch(err => {
      console.log('Unable to install `vue-devtools`: \n', err)
    })
})
// Require `main` process to boot app
require('./index')Copy the code

Install () can be enabled by typing require(‘devtron’).install() in the console of the window. In Devtron, we can observe dependency topology, event monitoring, IPC monitoring, etc. For details, see: devtron

The main process

The rendering process is relatively easy to debug, but the main process is more complicated. Electron -debug prints a Ws address. We can debug the application using Chrome inspect, but there is a drawback

  • Difficult operation
  • The code has some compilation

The ideal mode is to use Vscode for debugging. According to the official Vscode debugging code, it is found that the main process can be started, but there is no rendering process content, and the main process can not break the point. After observation, it is found that the Electron vue has several features:

  1. Babel and Webpack are used
  2. The development mode entry is index.dev.j.

To do this, we add DEBUG_ENV, NODE_END,BABEL_ENV to launch.json and add babel-register to get the following configuration file, and we can happily interrupt the main process

{
    "version": "0.2.0"."configurations": [{"name": "Launch"."type": "node"."request": "launch"."program": "${workspaceRoot}/src/main/index.dev.js"."stopOnEntry": false."args": []."cwd": "${workspaceRoot}"."runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron"."runtimeArgs": [
                "--nolazy"."-r"."babel-register"]."env": {
                "DEBUG_ENV": "debug"."BABEL_ENV": "main"."NODE_ENV":"development"
            },
            "sourceMaps": false,]}}Copy the code

The main process is up, but the interface is blank. The reason is that we used debugging directly to start the main process, not the rendering process. We added the following script to package.json:

"render": "webpack-dev-server --hot --colors --config .electron-vue/webpack.renderer.config.js --port 9080 --content-base ./"Copy the code

Start the script, and then start debugging, and there it is,

The road to packaging

The Electron- Vue integrates the Electron- Builder and Electron- Packager. The initial project can choose either of them. This project chooses the Electron- Builder. The electric-Builder and electric-Packager have greatly simplified the complex packaging process, with a few caveats here

asar

By default, autoelectron-Builder is enabled for ASAR, which can avoid source code exposure. However, in some cases, the volume of package will be increased. In this project, the volume of package is 140M+ before ASAR is enabled, but only 60M+ after it is closed. And Nodejieba will shut down ASar in this project because ASar cannot read files correctly

Static resource packaging

Before packaging, I added the assets folder under the Renderer process as usual. The performance of development was normal. After packaging, I found that static resources could not be found.

Iconfont packaging

Iconfont needs to be downloaded and imported locally, and introducing online links in the ejS header does not take effect at packaging time


The future planning

The current function is still relatively basic, the follow-up planning is mainly in the server level chrome bookmarks, improve the experience and efficiency, please look forward to ~

  • Server: Software updates & data sharing
  • Client: Chrome plug-in, error reporting and performance optimization, data export and export (with the device as the unique ID, but data can be exported and imported), Chrome bookmarks import, navigation functions are complete

At the end

Github link of this project (client only) : github.com/ZhenyuCheng…

PS: In the function update, the early code is rough and can barely be seen. After that, we are ready to add state management and re-split the module