Using React, Electron, Dva, Webpack, Node.js, Websocket to quickly build cross-platform applications.

  css, electron, html5, javascript, node.js

图片描述

At presentElectronIngithubAbovestarThe volume is already approachingReact-nativeThere are as many

Under the spit here,webpackI feel secretly updated every week, which is very sad, andAngularIt’s updated8,VueThere will be a new official version soon.5GThis year will be commercial, Huawei’s system will also come out.RNIt has not been updated to formal yet.1Version, and technology that claims to make front-end developers unemployedflutterAlso in the crazy update, front-end is really can’t finish learning

图片描述
图片描述

To get back to business, there is no denying that the current big front end is really amazing.PCThe terminal can be developed across three platforms, and the mobile terminal can write at one time to generate various small programs andReact-nativeApply, and then run iniosAnd Android and the web page, here have to say-jingdong’sTaroThe framework these people have putNode.jsAndwebpackIt took days.

YeswebpackNot familiar with, look at my previous articles, today does not focus onwebpack

Welcome to pay attention to my column “Front End Advanced” which is full of articles praised by hundreds of stars.

Let’s talk first.ElectronIntroduction to the official website:

UseJavaScript, HTML and CSSBuild cross-platform desktop applications. If you can build a website, you can build a desktop application.ElectronIs a useJavaScript, HTML and CSSTechnology creates the framework of native programs. It is responsible for the more difficult parts. You just need to focus on the core of your application.

  • What do you mean?
  • Electron = Node.js+Google Browser+Common JS Code Generation Application, finally packed into an installation package, is a complete application
  • ElectronThere are two processes, the main process is responsible for the more difficult part, rendering process (normalJSCode) part, responsible forUIInterface display
  • Between the two processes can be throughremoteModules, andIPCRenderAndIPCMainCommunication between two processes, the former is similar to communication mounted on global attributes (much like the earliest namespace modularization scheme), while the latter is based on publish-subscribe mechanism, which implements communication between two processes by monitoring and triggering custom events.
  • ElectronIt is equivalent to givingReactThe generated single-page application is covered with a layer of shells, and if complex functions such as file operation are involved, then it is necessary to rely onElectronBecause the main process can call directlyNode.jsTheAPI, you can also use theC++Plug-in, hereNode.jsThe cow force degree is highlighted, can write backstageCRUD, and can do middleware, now can write the front end.

Talking about Technical Selection

  • UseReactTo do the bottomUIDrawing is preferred for large projectsReact+TS
  • Best practices for state management are definitely notRedux, currently preferreddva, orredux-saga.
  • Building tool selectionwebpackIf notwebpackIt is really a disadvantage and will severely restrict your front-end development, so it is recommended to study hard.Node.jsAndwebpack
  • I chose the ordinary one.RestfulArchitecture, notGraphQLPerhaps I am rightGraphQLI don’t understand deeply, I don’t understand the essence.
  • In the area of communication protocol, I chosewebsoketAnd ordinaryhttpcommunication mode
  • Because it isdemo, many places are not detailed, later will be aimed atelectronA Netease cloud music open source project, this must be done

First, the formal environmental construction should be started.

clipboard.png

  • configFile placementwebpackConfiguration file
  • serverFolder placementNode.jsThe backend server code for
  • srcPlace source code below
  • main.jsYesElectronThe entry file for
  • jsonThe file is a script entry file and also a package management file ~

Ideas for Starting Development Model Projects;

  • Start firstwebpackPackage the code into memory for hot update
  • restartElectronRead the correspondingurlThe address of the file content, also realize hot update

Set upwebpackEntrance

app: ['babel-polyfill', './src/index.js', './index.html'],
 vendor: ['react']
 }

IgnoreElectronThe code in, no.webpackPacking (becauseElectronIf there is background module code in, the package will report an error)

externals: [
(function () {
var IGNORES = [
'electron'
];
return function (context, request, callback) {
if (IGNORES.indexOf(request) >= 0) {
return callback(null, "require('" + request + "')");
}
return callback();
};
})()
]

Add code division

optimization: {
 runtimeChunk: true,
 splitChunks: {
 chunks: 'all'
 }
 },

Setting up thermal updates, etc

plugins: [
new HtmlWebpackPlugin({
template: './index.html'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
],
mode: 'development',
devServer: {
contentBase: '../build',
open: true,
port: 5000,
hot: true
},

# # # # Joinbabel

{
 loader: 'babel-loader',
 Options: {//jsx syntax
 presets: ["@babel/preset-react",
 //tree shaking loads babel-politfillpresets from back to front as needed.
 ["@babel/preset-env", {
 "modules": false,
 "useBuiltIns": "false", "corejs": 2,
 }],
 ],
 
 plugins: [
 //Support import lazy loading plugin from front to back
 "@babel/plugin-syntax-dynamic-import",
 //andt-mobile Loading on Demand true is less, and' css' can be written if the value of less style is not used.
 ["import", { libraryName: "antd-mobile", style: true }],
 //Identify class Components
 ["@babel/plugin-proposal-class-properties", { "loose": true }],
 //
 ],
 cacheDirectory: true
 },
 }

Look at the configuration file of the main processmain.js

// Modules to control application life and create native browser window
 const { app, BrowserWindow, ipcMain, Tray, Menu } = require('electron')
 const path = require('path')
 // Keep a global reference of the window object, if you don't, the window will
 // be closed automatically when the JavaScript object is garbage collected.
 let mainWindow
 app.disableHardwareAcceleration()
 // ipcMain.on('sync-message', (event, arg) => {
 //   console.log("sync - message")
 //   // event.returnValue('message', 'tanjinjie hello')
 // })
 function createWindow() {
 // Create the browser window.
 tray = new Tray(path.join(__dirname, './src/assets/bg.jpg'));
 tray.setToolTip('wechart');
 tray.on('click', () => {
 mainWindow.isVisible() ?  mainWindow.hide() : mainWindow.show()
 });
 const contextMenu = Menu.buildFromTemplate([
 {label:' quit', click: () => mainWindow.quit()},
 ]);
 tray.setContextMenu(contextMenu);
 mainWindow = new BrowserWindow({
 width: 805,
 height: 500,
 webPreferences: {
 nodeIntegration: true
 },
 // titleBarStyle: 'hidden'
 frame: false
 })
 
 //Custom Zoom In and Out Tray Function
 ipcMain.on('changeWindow', (event, arg) => {
 if (arg === 'min') {
 console.log('min')
 mainWindow.minimize()
 } else if (arg === 'max') {
 console.log('max')
 if (mainWindow.isMaximized()) {
 mainWindow.unmaximize()
 } else {
 mainWindow.maximize()
 }
 } else if (arg === "hide") {
 console.log('hide')
 mainWindow.hide()
 }
 })
 // and load the  index.html of the app.
 // mainWindow.loadFile('index.html')
 mainWindow.loadURL('http://localhost:5000');
 BrowserWindow.addDevToolsExtension(
 path.join(__dirname, './src/extensions/react-dev-tool'),
 );
 
 
 // Open the DevTools.
 // mainWindow.webContents.openDevTools()
 
 // Emitted when the window is closed.
 mainWindow.on('closed', function () {
 // Dereference the window object, usually you would store windows
 // in an array if your app supports multi windows, this is the time
 // when you should delete the corresponding element.
 mainWindow = null
 BrowserWindow.removeDevToolsExtension(
 path.join(__dirname, './src/extensions/react-dev-tool'),
 );
 })
 }
 
 // This method will be called when Electron has finished
 // initialization and is ready to create browser windows.
 // Some APIs can only be used after this event occurs.
 app.on('ready', createWindow)
 
 // Quit when all windows are closed.
 app.on('window-all-closed', function () {
 // On macOS it is common for applications and their menu bar
 // to stay active until the user quits explicitly with Cmd + Q
 if (process.platform !  == 'darwin') app.quit()
 })
 
 app.on('activate', function () {
 // On macOS it's common to re-create a window in the app when the
 // dock icon is clicked and there are no other windows open.
 if (mainWindow === null) createWindow()
 })
 
 
 // In this file you can include the rest of your app's specific main process
 // code. You can also put them in separate files and require them here.

Today, I will only talk about the configuration under the development mode, because there are too many. I have written two articles to write about the remaining configuration.gitLook at the warehouse

Start the project in development mode:

  • Use"dev": "webpack-dev-server --config ./config/webpack.dev.js",Package code into memory
  • Use"start": "electron ."OpenelectronRead the resources in the corresponding memory address to realize hot update

After the project starts, at the entranceindex.jsFile, injectdva

import React from 'react'
import App from './App'
import dva from 'dva'
import Homes from './model/Homes'
import main from './model/main'
const app = dva()
app.router(({ history, app: store }) => (
<App
history={history}
getState={store._store.getState}
dispatch={store._store.dispatch}
/>
));
app.model(Homes)
app.model(main)
app.start('#root')

I have to say hereredux,redux-sage,dvaThe difference between directly look at the picture

First of allRedux

  • ReactOnly responsible for page rendering, but not for page logic, page logic can be extracted from it and changed intostore, status and page logic from<App/>It is extracted from the inside and becomes independent.store,
  • Page logic isReducer, < TodoList/ > and < AddTodoBtn/ >Are allPure ComponentThroughconnectThe method can easily add a layer to themwrapperSo as to establish a relationship withstoreCan be reached throughdispatchTostoreInjectionaction, promptingstoreThe status of the change, and at the same time subscribed tostoreThe state of change, once the state changes, isconnectThe components of the are also refreshed, using thedispatchtostoreSendactionThis process of can be intercepted, naturally can add a variety of hereMiddleware, to achieve a variety of custom functions, eg: logging in this way, each part performs its duties, coupling degree is lower, reuse degree is higher, better scalability

这个是Redux

Then there is injectionRedux-sage

  • As mentioned above, Middleware can be used to intercept action, thus asynchronous network operation is also very convenient. Just make a Middleware. Here, use redux-saga as a class library, for example, chestnut:
  • Click CreateTodoTo initiate an action of type == addTodo
  • sagaIntercept thisactionInitiatehttpRequest, if the request is successful, continue toreducerSend atype == addTodoSuccTheaction, prompt successful creation, otherwise sendtype == addTodoFailTheactionJust do it

Finally: Dva

  • With the three steps ahead,DvaThe emergence of also follow, asDvaAccording to the official website,DvaBased onReact + Redux + SagaThe best practice of precipitation, did 3 very important things, greatly improve the coding experience:
  • ThestoreAndsagaUnite into onemodelThe concept of writing in ajsIn the file
  • Add oneSubscriptionsFor collecting from other sourcesaction,Eg: keyboard operation
  • modelWriting is very simple, similar toDSLOr ..RoR,codingFly fast
  • Agreement is better than configuration. It is always good.

clipboard.png

clipboard.png

At the entranceAPPAssembly, injectionpropsTo manage the state tree

import React from 'react'
 import { HashRouter, Route, Redirect, Switch } from 'dva/router';
 import Home from './pages/home'
 const Router = (props) => {
 return (
 <HashRouter>
 <Switch>
 <Route path="/home" component={Home}></Route>
 <Redirect to="/home"></Redirect>
 </Switch>
 </HashRouter>
 )
 }
 export default Router

In the assemblyconnectConnect the status tree

import React from 'react'
import { ipcRenderer } from 'electron'
import { NavLink, Switch, Route, Redirect } from 'dva/router'
import Title from '../../components/title'
import Main from '../main'
import Friend from '../firend'
import More from '../more'
import { connect } from 'dva'
import './index.less'
class App extends React.Component {
componentDidMount() {
ipcRenderer.send('message', 'hello electron')
ipcRenderer.on('message', (event, arg) => {
console.log(arg, new Date(Date.now()))
})
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = function () {
ws.send('123')
console.log('open')
}
ws.onmessage = function () {
console.log('onmessage')
}
ws.onerror = function () {
console.log('onerror')
}
ws.onclose = function () {
console.log('onclose')
}
}
componentWillUnmount() {
ipcRenderer.removeAllListeners()
}
render() {
console.log(this.props)
return (
<div className="wrap">
<div className="nav">
<NavLink to="/home/main">Home</NavLink>
<NavLink to="/home/firend">Friend</NavLink>
<NavLink to="/home/more">More</NavLink>
</div>
<div className="content">
<Title></Title>
<Switch>
<Route path="/home/main" component={Main}></Route>
<Route path="/home/firend" component={Friend}></Route>
<Route path="/home/more" component={More}></Route>
<Redirect to="/home/main"></Redirect>
</Switch>
</div>
</div>
)
}
}
export default connect(
({ main }) => ({
test: main.main
})
)(App)
// ipcRenderer.sendSync('sync-message','sync-message')

What did you do with the above components

  • In the component-mounted lifecycle function, thewebsocketConnection, and mount the response of event monitoring, sent a message to the main thread, and triggered the main thread’smessageEvents.
  • When the component is about to be unloaded, all event monitoring for cross-process communication is removed.
  • UseddvaCarry out route jump
  • Connected state tree, read state treemainmodularmainStatus data

Enter the subcomponent of the previous component

import React from 'react'
import { connect } from 'dva'
class App extends React.Component {
handleAdd = () => {
this.props.dispatch({
type: 'home/add',
val: 5,
res: 1
})
}
handleDel = () => {
}
render() {
const { homes } = this.props
console.log(this.props)
return (
<div>
<button onClick={this.handleAdd}>add</button>
<button onClick={this.handleDel}>{homes}</button>
</div>
)
}
}
export default connect(
({ home, main }) => ({
homes: home.num,
mains: main.main
})
)(App)

Also look at what this component does

  • Connection status tree, readhome,mainThe status data of the module is converted toprops
  • The event is bound, and if the button is clicked,dispatchTo the correspondingeffects, update the status tree data, and then update the page

Finally, we will look at how to control the window display of the main process through the rendering process.

import React from 'react'
import { ipcRenderer } from 'electron'
import './index.less'
export default class App extends React.Component {
handle = (type) => {
return () => {
if (type === 'min') {
console.log('min')
ipcRenderer.send('changeWindow', 'min')
} else if (type === 'max') {
console.log('max')
ipcRenderer.send('changeWindow', 'max')
} else {
console.log('hide')
ipcRenderer.send('changeWindow', 'hide')
}
}
}
render() {
return (
<div className="title-container">
< divclassname = "title" style = {{"webkitappregion": "drag"}} > draggable area < /div >
< button onclick = {this.handle ('min')} > minimize < /button >
< button onclick = {this.handle ('max')} > maximize < /button >
< button onclick = {this.handle ('hide')} > tray < /button >
</div>
)
}
}
  • viaIPCRenderCommunicate with the main process and control the display and hiding of windows.

Let’s go together.dvaManaged bymodelLook

  • homemodule
export default {
namespace: 'home',
state: {
homes: [1, 2, 3],
num: 0
},
reducers: {
adds(state, { newNum }) {
return {
...state,
num: newNum
}
}
},
effects: {
* add({ res, val }, { put, select, call }) {
const { home } = yield select()
console.log(home.num)
yield console.log(res, val)
const newNum = home.num + 1
yield put({ type: 'adds', newNum })
}
},
}

dvaIt can really save us a lot of code, and it is better maintained and easier to read.

  • Its general flow

clipboard.png

  • If not, it is suggested to go to the official website to see examples. Generally speaking, it is not likeRXJSThe learning route is so steep

Node.jsMedium code

const express = require('express')
 const { Server } = require("ws");
 const app = express()
 const wsServer = new Server({ port: 8080 })
 wsServer.on('connection', (ws) => {
 ws.onopen = function () {
 console.log('open')
 }
 ws.onmessage = function (data) {
 console.log(data)
 ws.send('234')
 console.log('onmessage' + data)
 }
 ws.onerror = function () {
 console.log('onerror')
 }
 ws.onclose = function () {
 console.log('onclose')
 }
 });
 
 app.listen(8000, (err) => {
 if (!  Err) {console.log ('listen OK')} else {.
 Log ('listening failed')
 }
 })

Come up and give one first.websocket 8080Port monitor, bind events, and useexpressMonitor native port8000

  • This has the advantage that not all applications need real-time communication, and when to conduct real-time communication is decided according to requirements.
  • RestfulThe architecture still exists.Node.jsAs middleware orIOThe output is more than the bottom serverCRUDit’s ok

Today, I’ll write here first. The routine is the same. The basic framework has been set up and the code can be transferredcloneGo down and play slowly, add function. Give me one if you canstarAnd praise, thank you

This article git warehouse source code address, welcome star