目前Electron
在github
上面的star
量已經快要跟React-native
一樣多了
這裡吐槽下,webpack
感覺每週都在偷偷更新,很糟心啊,還有Angular
更新到了8
,Vue
馬上又要出正式新版本了,5G
今年就要商用,華為的系統也要出來了,RN
還沒有更新到正式的1
版本,還有號稱讓前端開發者失業的技術flutter
也在瘋狂更新,前端真的是學不完的
回到正題,不能否認,現在的大前端,真的太牛了,PC
端可以跨三種平臺開發,移動端可以一次編寫,生成各種小程式以及React-native
應用,然後跑在ios
和安卓以及網頁中 , 這裡不得不說-------京東的Taro
框架 這些人 已經把Node.js
和webpack
用上了天對
webpack
不熟悉的,看我之前的文章 ,今天不把重點放在webpack
歡迎關注我的專欄 《前端進階》 都是百星高贊文章
先說說Electron
官網介紹:
使用 JavaScript, HTML 和 CSS
構建跨平臺的桌面應用 ,如果你可以建一個網站,你就可以建一個桌面應用程式。 Electron
是一個使用 JavaScript, HTML 和 CSS 等 Web
技術建立原生程式的框架,它負責比較難搞的部分,你只需把精力放在你的應用的核心上即可。
- 什麼意思呢?
-
Electron = Node.js + 谷歌瀏覽器 + 平常的JS程式碼生成的應用
,最終打包成安裝包,就是一個完整的應用 -
Electron
分兩個程式,主程式負責比較難搞的那部分,渲染程式(平常的JS
程式碼)部分,負責UI
介面展示 - 兩個程式之間可以通過
remote
模組,以及IPCRender
和IPCMain
之間通訊,前者類似於掛載在全域性的屬性上進行通訊(很像最早的名稱空間模組化方案),後者是基於釋出訂閱機制,自定義事件的監聽和觸發實現兩個程式的通訊。 -
Electron
相當於給React
生成的單頁面應用套了一層殼,如果涉及到檔案操作這類的複雜功能,那麼就要依靠Electron
的主程式,因為主程式可以直接呼叫Node.js
的API
,還可以使用C++
外掛,這裡Node.js
的牛逼程度就凸顯出來了,既可以寫後臺的CRUD
,又可以做中介軟體,現在又可以寫前端。
談談技術選型
- 使用
React
去做底層的UI
繪製,大專案首選React+TS
- 狀態管理的最佳實踐肯定不是
Redux
,目前首選dva
,或者redux-saga
。 - 構建工具選擇
webpack
,如果不會webpack
真的很吃虧,會嚴重限制你的前端發展,所以建議好好學習Node.js
和webpack
- 選擇了普通的
Restful
架構,而不是GraphQL
,可能我對GraphQL
理解不深,沒有領悟到精髓 - 在通訊協議這塊,選擇了
websoket
和普通的http
通訊方式 - 因為是
demo
,很多地方並沒有細化,後期會針對electron
出一個網易雲音樂的開源專案,這是一定要做到的
先開始正式的環境搭建
-
config
檔案放置webpack
配置檔案 -
server
資料夾放置Node.js
的後端伺服器程式碼 -
src
下放置原始碼 -
main.js
是Electron
的入口檔案 -
json
檔案是指令碼入口檔案,也是包管理的檔案~
開發模式專案啟動思路:
- 先啟動
webpack
將程式碼打包到記憶體中,實現熱更新 - 再啟動
Electron
讀取對應的url
地址的檔案內容,也實現熱更新
設定webpack
入口
app: ['babel-polyfill', './src/index.js', './index.html'],
vendor: ['react']
}
忽略Electron
中的程式碼,不用webpack
打包(因為Electron
中有後臺模組程式碼,打包就會報錯)
externals: [
(function () {
var IGNORES = [
'electron'
];
return function (context, request, callback) {
if (IGNORES.indexOf(request) >= 0) {
return callback(null, "require('" + request + "')");
}
return callback();
};
})()
]
加入程式碼分割
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all'
}
},
設定熱更新等
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
],
mode: 'development',
devServer: {
contentBase: '../build',
open: true,
port: 5000,
hot: true
},
#### 加入babel
{
loader: 'babel-loader',
options: { //jsx語法
presets: ["@babel/preset-react",
//tree shaking 按需載入babel-polifill presets從後到前執行
["@babel/preset-env", {
"modules": false,
"useBuiltIns": "false", "corejs": 2,
}],
],
plugins: [
//支援import 懶載入 plugin從前到後
"@babel/plugin-syntax-dynamic-import",
//andt-mobile按需載入 true是less,如果不用less style的值可以寫'css'
["import", { libraryName: "antd-mobile", style: true }],
//識別class元件
["@babel/plugin-proposal-class-properties", { "loose": true }],
//
],
cacheDirectory: true
},
}
看看主程式的配置檔案main.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: '退出', click: () => mainWindow.quit() },
]);
tray.setContextMenu(contextMenu);
mainWindow = new BrowserWindow({
width: 805,
height: 500,
webPreferences: {
nodeIntegration: true
},
// titleBarStyle: 'hidden'
frame: false
})
//自定義放大縮小托盤功能
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.
今天只講開發模式下的配置,因為實在太多,得分兩篇文章寫了~ 剩下的配置去git
倉庫看
在開發模式下啟動專案:
- 使用
"dev": "webpack-dev-server --config ./config/webpack.dev.js",
將程式碼打包到記憶體中 - 使用
"start": "electron ."
開啟electron
,讀取對應的記憶體地址中的資源,實現熱更新
專案起來後,在入口處index.js
檔案中,注入dva
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')
這裡不得不說redux,redux-sage,dva
的區別 直接看圖
首先是Redux
-
React
只負責頁面渲染, 而不負責頁面邏輯, 頁面邏輯可以從中單獨抽取出來, 變成store
,狀態及頁面邏輯從<App/>
裡面抽取出來, 成為獨立的store
, - 頁面邏輯就是
reducer,<TodoList/> 及<AddTodoBtn/>
都是Pure Component
, 通過connect
方法可以很方便地給它倆加一層wrapper
從而建立起與store
的聯絡: 可以通過dispatch
向store
注入action
, 促使store
的狀態進行變化, 同時又訂閱了store
的狀態變化, 一旦狀態有變, 被connect
的元件也隨之重新整理,使用dispatch
往store
傳送action
的這個過程是可以被攔截的, 自然而然地就可以在這裡增加各種Middleware
, 實現各種自定義功能, eg: logging這樣一來, 各個部分各司其職, 耦合度更低, 複用度更高, 擴充套件性更好
然後是注入Redux-sage
- 上面說了, 可以使用 Middleware 攔截 action, 這樣一來非同步的網路操作也就很方便了, 做成一個 Middleware 就行了, 這裡使用 redux-saga 這個類庫, 舉個栗子:
- 點選建立
Todo
的按鈕, 發起一個 type == addTodo 的 action -
saga
攔截這個action
, 發起http
請求, 如果請求成功, 則繼續向reducer
發一個type == addTodoSucc
的action
, 提示建立成功, 反之則傳送type == addTodoFail
的action
即可
最後是: Dva
- 有了前面的三步鋪墊,
Dva
的出現也就水到渠成了, 正如Dva
官網所言,Dva
是基於React + Redux + Saga
的最佳實踐沉澱, 做了 3 件很重要的事情, 大大提升了編碼體驗: - 把
store
及saga
統一為一個model
的概念, 寫在一個js
檔案裡面 - 增加了一個
Subscriptions
, 用於收集其他來源的action,
eg: 鍵盤操作 -
model
寫法很簡約, 類似於DSL
或者RoR
,coding
快得飛起✈️ - 約定優於配置, 總是好的?
在入口APP
元件中,注入props
,實現狀態樹的管理
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
在元件中connect
連線狀態樹即可
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')
捋一捋上面的元件做了什麼
- 上來在元件掛載的生命週期函式中,啟動了
websocket
連線,並且掛載了響應的事件監聽,對主執行緒傳送了訊息,並且觸發了主執行緒的message
事件。 - 在元件即將解除安裝的時候,移除了所有的跨程式通訊的事件監聽
- 使用了
dva
進行路由跳轉 - 連線了狀態樹,讀取了狀態樹
main
模組的main
狀態資料
進入上一個元件的子元件
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)
同樣看看,這個元件做了什麼
- 連線狀態樹,讀取了
home,main
模組的狀態資料,並且轉換成了props
- 繫結了事件,如果點選按鈕,
dispatch
給對應的effects
,更新狀態樹的資料,進而更新頁面
最後我們看下如何通過渲染程式控制主程式的視窗顯示
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">
<div className="title" style={{ "WebkitAppRegion": "drag" }}>可以拖拽的區域</div>
<button onClick={this.handle('min')}>最小化</button>
<button onClick={this.handle('max')}>最大化</button>
<button onClick={this.handle('hide')}>托盤</button>
</div>
)
}
}
- 通過
IPCRender
與主程式通訊,控制視窗的顯示和隱藏
我們一起去dva
中管理的model
看看
-
home
模組
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 })
}
},
}
dva
真的可以給我們省掉很多很多程式碼,而且更好維護,也更容易閱讀
- 它的大概流程
- 如果不會的話建議去官網看例子,一般來說不會像
RXJS
學習路線那麼陡峭
Node.js
中程式碼
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('監聽OK') } else {
console.log('監聽失敗')
}
})
上來先給一個websocket 8080
埠監聽,繫結事件,並且使用express
監聽原生埠8000
- 這樣好處,一個應用並不一定全部需要實時通訊,根據需求來決定什麼時候進行實時通訊
-
Restful
架構依然存在,Node.js
作為中介軟體或者IO
輸出比較多的底層伺服器進行CRUD
都可以