Hi, Habr. At the beginning of the winter of 2016, I became lonely again. After some time, I decided to get a profile in Tinder. All would be nothing, but gradually fatigue began to accumulate due to the impossibility of typing normally on the physical keyboard. I saw several solutions to this problem:
The first version did not suit me because of the fundamental superiority of the real keyboard over the screen. The second option was not suitable due to the fact that after all this would be an application that was not optimized for the desktop. The third option was all good except design, bugs, and low activity in the repository. Later, Tinder ++ received a letter from Tinder lawyers and the project was completely folded. Thus, for me personally, the choice was obvious.
First of all, it should be noted that Tinder does not have an open API, but it was opened in 2014 using MITM. That was enough for writing a client.
Perhaps the only thing that almost did not change during the difficult fate of the project was Electron .
I was impatient to play around with React , so the standard combination react + redux + redux-saga + immutable was chosen. By May, the first version was written, but there were problems with my crooked hands architecture. It turned out that in order to make redux fast, a lot of manual work is required: memoization, shouldComponentUpdate, selector factories, and the like.
Also, perhaps, it was not worth every time to request the whole story and merge it with the existing store using Immutable.Map.mergeDeep
In any case, the verbosity of redux and redux-saga began to bore me. My constant impression was that the library was struggling with me instead of helping.
I do not want to say that redux is a bad library. From an aesthetic point of view, it is very elegant. And if it allows you to write good code, then this is the most important thing. However, it cannot be denied that it is usually required to create a lot of auxiliary code along the way, even for simple things.
So, the redux style did not suit me, but in the logs I blamed it and react. I had only one thing left.
Of course, the title is slightly provocative. In fact, the diploma protection period, the session, the collection of documents for the magistracy, military training, and moving to another country approached. But as soon as everything got better, I moved on to the next item.
New stack included Inferno and MobX . Both of these libraries promised good performance with a minimum of hands-on work (as it turned out later, not quite). In general, it was pleasant to work with them, thanks to MobX, the code became much more concise, but at the same time three problems grew.
The first obvious solution was to use localStorage. For this, I used the wonderful localForage library. However, JSON.stringify and JSON.parse with each saving and retrieving of history (and the history was saved anew each time completely with each update) did not add joy. Even the fact that now I requested only updates from the server and merged them with history did not allow achieving the desired performance.
The next solution was to use IndexedDB, and for maximum performance, the Dexie.js library was chosen. It quickly became clear that updating only the changed data significantly adds speed, but the interface lags were still noticeable. Then I made all the work with IndexedDB in WebWorker and everything seemed to be working out.
To request the Tinder API, you need to install special headers for mimicry under their Android client. For security reasons, browser-based JS does not support such tricks, so all requests were made from the main process of Electron.
Thus, the data passed the following way:
This made it possible to achieve acceptable performance, but the stores expanded and each time carefully merging the data into IndexedDB, and then in MobX meant doing the same job twice with your hands. In addition, there was a third problem.
Inferno defeats competitors in speed in almost all benchmarks, but developer productivity is just as important. Despite the existence of inferno-compat, many React libraries still did not work. It was hardly possible to start the material-ui , did not load react-vistualized .
Of course, most of the missing things were pretty simple and easy to write on your own. In some places it was possible to start a React library using a pair of dirty hacks. But in general, this situation began to bore me, as did the manual synchronization of the database and reactive storage. I tried to contribute to the Inferno repository, but for a long time I did not have enough. Three processes for such a simple application also seemed brute force. I wanted something declarative and not requiring a heap of code for support.
This time the decision was more balanced. Compatibility with React is needed - we just use React, such benchmarks are important only if you display thousands of elements. Do not like too many processes - it means data should be stored in the same place where they come from, in the main process. In general, I like MobX and its advantages, but it becomes not very convenient to work with large storages - hence, MobX remains as a manager of the local state of components, and for global data something else is used.
If you read the title of the article, then something else will be obvious to you. Of course, this is GraphQL. Apollo is used as a client. At first, the solution will seem unusual, but with a little thought, you will find many advantages:
Of course, in Apollo, by default there is no support for IPC, but it is possible to create your own network interface. It is very simple:
import { ipcRenderer } from 'electron' import { GRAPHQL } from 'shared/constants' import uuid from 'uuid' import { print } from 'graphql/language/printer' export class ElectronInterface { ipc listeners = new Map() constructor(ipc = ipcRenderer) { this.ipc = ipc this.ipc.on(GRAPHQL, this.listener) } listener = (event, args) => { const { id, payload } = args if (!id) { throw new Error('Listener ID is not present!') } const resolve = this.listeners.get(id) if (!resolve) { throw new Error(`Listener with id ${id} does not exist!`) } resolve(payload) this.listeners.delete(id) } printRequest(request) { return { ...request, query: print(request.query) } } generateMessage(id, request) { return { id, payload: this.printRequest(request) } } setListener(request, resolve) { const id = uuid.v1() this.listeners.set(id, resolve) const message = this.generateMessage(id, request) this.ipc.send(GRAPHQL, message) } query = request => { return new Promise(this.setListener.bind(this, request)) } }
The following is the request processing code in the main process. All factories create ServerAPI class methods.
Code to execute GraphQL query:
// @flow import { ServerAPI } from './ServerAPI' import { graphql } from 'graphql' export default function callGraphQLFactory(instance: ServerAPI) { return function callGraphQL(payload: any) { const { query, variables, operationName } = payload return graphql( instance.schema, query, null, instance, variables, operationName ) } }
Code to create a response message:
// @flow export default function generateMessage(id: string, res: any) { return { id, payload: res } }
The code that processes the request and returns the data:
// @flow import { ServerAPI } from './ServerAPI' import { GRAPHQL } from 'shared/constants' type RequestArguments = { id: string, payload: any } export default function processRequestFactory(instance: ServerAPI) { return async function processRequest(event: Event, args: RequestArguments) { const { id, payload } = args const res = await instance.callGraphQL(payload) const message = instance.generateMessage(id, res) if (instance.app.window !== null) { instance.app.window.webContents.send(GRAPHQL, message) } } }
And finally, in the constructor, create a subscriber to the message:
import { ipcMain } from 'electron' ipcMain.on(GRAPHQL, instance.processRequest)
Now, when each update is received, it is recorded in the NeDB database, then the main process sends a message to the renderer process about the need to re-request the actual data using IPC.
I didn’t want to use react-router for a long time. The fact is that I found their large-scale rewriting of the API and was not eager to step on the same rake again. Therefore, first I connected a router5 + self-written middleware that syncs in MobX. Inside Electron, de facto, there are no URLs in the usual sense, so the idea of ​​storing the state of navigation in a jet vault was excellent. However, despite the fact that such a bundle gives you complete control over navigation, sometimes it requires too much extra code.
I combined the transition to react-router @ v4 with a partial transition from Flexbox to a CSS Grid. These things seem to be made for each other. It seems that this time the react-router command really worked!
At first I used webpack and electron-packager , but during the last major change I switched to electron-forge . As far as I understand, in the future this package will become a standard solution for building and distributing applications on Electron. It includes an electron-packager for assembly and electron-compile , which allows JS / TS transpiling, compiling other formats (Less, Stylus, SCSS), and much more with almost no configuration.
With GraphQL, I got rid of a large amount of my code (and hence my bugs). Adding new features to the code has become much easier. I and the application began to work faster.
I hope that this approach will help someone in creating his applications on Electron. I plan to allocate the implementation of GraphQL-over-IPC in a separate npm package, so that it can be conveniently used.
For version 2.0, I would like to
Thanks to Julia Kurdi for the wonderful illustrations!
Source: https://habr.com/ru/post/331446/