
> Contents
- 前言
- 開(kāi)發(fā)環(huán)境搭建
- 引入Webpack4.0前端打包工具
- Electron代碼結(jié)構(gòu)和代碼熱更新
- 前端界面React + Mobx 代碼結(jié)構(gòu)和熱更新
- Linux桌面客戶(hù)端開(kāi)發(fā)遇到的問(wèn)題
前言
最近桌面系統(tǒng)從Ubuntu18.04切換到了Manjaro Linux 17,之前聽(tīng)說(shuō)Manjaro的軟件豐富,倉(cāng)庫(kù)更新及時(shí),很多常用軟件都能一鍵安裝(比如QQ,微信),同時(shí)也支持主流的Linux桌面環(huán)境:Gnome、KDE、Cinnamon、Mate、Deepin等等,安裝了Gnome版本的Manjaro之后發(fā)現(xiàn)果然還不錯(cuò)。系統(tǒng)安裝好后配置比較繁瑣,就想給Manjaro寫(xiě)一個(gè)GUI客戶(hù)端工具用于安裝常用軟件和作為簡(jiǎn)單的系統(tǒng)管理工具 - electronux
作為一名正直的前端開(kāi)發(fā)人員,理所應(yīng)當(dāng)?shù)鼐蜏?zhǔn)備使用Electron + Node.js + React + Mobx + Webpack + Shell 來(lái)進(jìn)行開(kāi)發(fā)啦 ~ 目前仍然在開(kāi)發(fā)中,這篇文章用于記錄自己的環(huán)境搭建過(guò)程、一些對(duì)Electron+React開(kāi)發(fā)的理解以及談?wù)勛约河龅降囊恍㎜inux桌面軟件開(kāi)發(fā)時(shí)遇到的問(wèn)題和解決辦法。








開(kāi)發(fā)環(huán)境搭建
代碼目錄結(jié)構(gòu)
electronux
|---- [dir ] app ( 主代碼目錄 )
|----------- [dir ] app/configure ( 應(yīng)用配置更新 )
|----------- [dir ] app/runtime ( 運(yùn)行數(shù)據(jù)文件 )
|
|----------- [dir ] app/services ( 后臺(tái)服務(wù)存放目錄 )
|------------------------ [dir ] app/services/middleware ( 一些中間處理件 )
|------------------------ [dir ] app/services/shell ( shell腳本存放目錄 )
|------------------------ [dir ] app/services/main-serv ( 主進(jìn)程服務(wù) )
|------------------------ [dir ] app/services/render-serv ( 渲染進(jìn)程服務(wù) )
|
|----------- [dir ] app/stores ( 前端狀態(tài)管理文件目錄 )
|----------- [dir ] app/styles ( 公用樣式表文件 )
|----------- [dir ] app/utils ( 公用工具函數(shù) )
|
|----------- [dir ] app/views ( UI界面代碼 )
|------------------------ [dir ] app/views/module1 ( 界面模塊1 )
|------------------------ [dir ] app/views/module2 ( 界面模塊2)
|------------------------ [dir ] app/views/module3 ( 界面模塊3 )
|
|----------- [file] app/App.js ( 前端應(yīng)用入口文件 )
|----------- [file] app/index.js ( 前端應(yīng)用熱加載文件 )
|
|---- [dir ] dist ( 前端代碼編譯打包文件存放目錄 )
|---- [dir ] resources ( 前端靜態(tài)資源存放目錄 )
|
|---- [file] .babelrc ( babel配置文件 )
|---- [file] .editorconfig (編輯器編碼規(guī)范文件)
|---- [file] .eslintrc ( 代碼格式檢查配置文件 )
|---- [file] .gitignore ( git忽略追蹤配置文件 )
|---- [file] electron-builder.json ( electron-builder打包配置文件 )
|---- [file] index.html ( 應(yīng)用渲染入口頁(yè)面 )
|---- [file] index.js ( 應(yīng)用主進(jìn)程入口文件 )
|---- [file] package.json (前端模塊和框架配置文件)
|---- [file] webpack.config.js (webpack開(kāi)發(fā)環(huán)境配置文件)
|---- [file] webpack.prod.config.js ( webpack生產(chǎn)環(huán)境配置文件 )
項(xiàng)目環(huán)境依賴(lài)配置文件
{
"name": "electronux",
"description": "linux manager-software powered by electron & react & Mobx ",
"version": "1.0.0",
"author": {
"name": "nojsja",
"email": "yangwei020154@gmail.com"
},
"scripts": {
"start": "concurrently \"npm run start-dev\" \"npm run start-electron\"",
"start-dev": "cross-env NODE_ENV=development webpack-dev-server",
"start-electron": "nodemon --exec 'cross-env NODE_ENV=development electron --inspect=5858 index'",
"start-production": "cross-env NODE_ENV=production electron --inspect=5858 index",
"build-all": "npm run dist && npm run build",
"dist": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js",
"build": "electron-builder -l"
},
"keywords": [
"electron",
"react",
"mobx",
"react-router",
"webpack4"
],
"license": "",
"nodemonConfig": {
"ignore": [
"resources/*",
"node_modules/*",
"dist/*",
"build/*",
"app/stores/*",
"app/styles/*",
"app/services/shell/*",
"app/configure/view.conf",
"app/views/*",
"app/App.js",
"app/main.js",
"app/index.js",
"electron-builder.yml"
],
"delay": "1000"
},
"dependencies": {
"semantic-ui-css": "^2.4.0",
"semantic-ui-react": "^0.82.5",
"mobx": "^4.4.1",
"mobx-react": "^5.2.8",
"prop-types": "^15.6.2",
"react": "^16.5.1",
"react-dom": "^16.5.1",
"react-hot-loader": "^4.3.8",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"history": "^4.7.2"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"clean-webpack-plugin": "^0.1.19",
"concurrently": "^3.6.1",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"electron": "^2.0.9",
"electron-builder": "^20.28.4",
"eslint": "^5.6.1",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-react": "^7.11.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.9.4",
"nodemon": "^1.18.4",
"sass-loader": "^7.1.0",
"source-map-support": "^0.5.9",
"style-loader": "^0.21.0",
"url-loader": "^1.1.2",
"webpack": "^4.19.0",
"webpack-cli": "^2.1.5",
"webpack-dev-server": "^3.1.8"
}
}
引入Webpack4.0前端打包工具
webpack開(kāi)發(fā)環(huán)境配置文件
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// 拆分樣式文件
const extractSass = new ExtractTextPlugin({
filename: 'style.scss.css',
});
const extractCss = new ExtractTextPlugin({
filename: 'style.css',
});
module.exports = {
devtool: 'source-map',
entry: [
'react-hot-loader/patch',
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./app/index',
],
mode: 'development',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
resolve: {
alias: {
resources: path.resolve(__dirname, 'resources'),
app: path.resolve(__dirname, 'app'),
},
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
},
{
test: /\.css$/,
use: extractCss.extract({
fallback: 'style-loader',
use: 'css-loader',
publicPath: '/',
}),
},
{
test: /\.scss$/,
use: extractSass.extract({
use: [{
loader: 'css-loader',
}, {
loader: 'sass-loader',
}],
fallback: 'style-loader', // 在開(kāi)發(fā)環(huán)境使用 style-loader
publicPath: '/',
}),
},
{
test: /\.html$/,
use: {
loader: 'html-loader',
},
},
{
test: /\.(png|jpg|gif|svg|ico|woff|eot|ttf|woff2)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
},
},
],
},
],
},
plugins: [
extractSass,
extractCss,
new webpack.HotModuleReplacementPlugin(),
new CleanWebpackPlugin(['dist']),
new webpack.NamedModulesPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
],
devServer: {
host: 'localhost',
port: 3000,
historyApiFallback: true,
hot: true,
},
target: 'electron-renderer',
};
Electron基本原理和代碼熱更新
Electron 運(yùn)行 package.json 的 main 腳本的進(jìn)程被稱(chēng)為主進(jìn)程。 在主進(jìn)程中運(yùn)行的腳本通過(guò)創(chuàng)建web頁(yè)面來(lái)展示用戶(hù)界面。 一個(gè) Electron 應(yīng)用總是有且只有一個(gè)主進(jìn)程。
由于 Electron 使用了 Chromium 來(lái)展示 web 頁(yè)面,所以 Chromium 的多進(jìn)程架構(gòu)也被使用到。 每個(gè) Electron 中的 web 頁(yè)面運(yùn)行在它自己的渲染進(jìn)程中。
在普通的瀏覽器中,web頁(yè)面通常在一個(gè)沙盒環(huán)境中運(yùn)行,不被允許去接觸原生的資源。 然而 Electron 的用戶(hù)在 Node.js 的 API 支持下可以在頁(yè)面中和操作系統(tǒng)進(jìn)行一些底層交互。
進(jìn)程使用 BrowserWindow 實(shí)例創(chuàng)建頁(yè)面。 每個(gè) BrowserWindow 實(shí)例都在自己的渲染進(jìn)程里運(yùn)行頁(yè)面。 當(dāng)一個(gè) BrowserWindow 實(shí)例被銷(xiāo)毀后,相應(yīng)的渲染進(jìn)程也會(huì)被終止。
主進(jìn)程管理所有的web頁(yè)面和它們對(duì)應(yīng)的渲染進(jìn)程。 每個(gè)渲染進(jìn)程都是獨(dú)立的,它只關(guān)心它所運(yùn)行的 web 頁(yè)面。
在頁(yè)面中調(diào)用與 GUI 相關(guān)的原生 API 是不被允許的,因?yàn)樵?web 頁(yè)面里操作原生的 GUI 資源是非常危險(xiǎn)的,而且容易造成資源泄露。 如果你想在 web 頁(yè)面里使用 GUI 操作,其對(duì)應(yīng)的渲染進(jìn)程必須與主進(jìn)程進(jìn)行通訊,請(qǐng)求主進(jìn)程進(jìn)行相關(guān)的 GUI 操作。
創(chuàng)建主進(jìn)程
在index.js文件中我們引入electron和所有的自定義模塊文件,并根據(jù)開(kāi)發(fā)環(huán)境或是生產(chǎn)環(huán)境來(lái)進(jìn)行主進(jìn)程窗口加載,開(kāi)發(fā)環(huán)境下使用http協(xié)議加載由webpack-dev-server啟動(dòng)的http服務(wù),生產(chǎn)環(huán)境下使用file協(xié)議加載本地由webpack打包好的前端bundle.js文件,所以開(kāi)發(fā)環(huán)境下npm start指令其實(shí)主要是執(zhí)行了兩步操作,一是啟動(dòng)webpack-dev-server,此時(shí)已經(jīng)可以通過(guò)外部瀏覽器訪(fǎng)問(wèn)到localhost:3000的http服務(wù),只不過(guò)我們實(shí)際是用electron之中的chromium瀏覽器來(lái)加載的,它與node.js主進(jìn)程共享同一個(gè)chrome v8引擎,所以理論上,在頁(yè)面加載后,你同樣可以在渲染進(jìn)程中使用node.js API,比如用使用fs模塊訪(fǎng)問(wèn)文件系統(tǒng)。
主進(jìn)程代碼熱更新
我用了nodemon工具實(shí)現(xiàn)了主進(jìn)程代碼熱更新,如果不用nodemon工具那么 npm start-electron命令實(shí)際是執(zhí)行cross-env NODE_ENV=development electron index,就是簡(jiǎn)單的用electron啟動(dòng)主進(jìn)程文件,使用nodemon之后npm start-electron實(shí)際上是執(zhí)行nodemon --exec 'cross-env NODE_ENV=development electron index',最后在package.json文件中增加一個(gè)nodemonConfig字段用于指定哪些文件需要納入nodemon監(jiān)聽(tīng)即可。
=> package.json中定義的啟動(dòng)腳本:
"scripts": {
"start": "concurrently \"npm run start-dev\" \"npm run start-electron\"",
"start-dev": "cross-env NODE_ENV=development webpack-dev-server",
"start-electron": "nodemon --exec 'cross-env NODE_ENV=development electron index'",
"build": "npm run dist && npm run build-all",
"dist": "cross-env NODE_ENV=production webpack --config webpack.production.config.js",
"build-all": "build -lmw"
},
=> package.json中nodemonConfig字段
"nodemonConfig": {
"ignore": [
"resources/*",
"node_modules/*",
"dist/*",
"app/stores/*",
"app/styles/*",
"app/services/shell/*",
"app/configure/view.conf",
"app/views/*",
"app/App.js",
"app/main.js",
"app/index.js"
],
"delay": "1000"
},
=> 項(xiàng)目啟動(dòng)文件index.js:
...
// 根據(jù)運(yùn)行環(huán)境加載窗口 //
function loadWindow(window, env) {
if (env === 'development') {
// wait for webpack-dev-server start
setTimeout(() => {
window.loadURL(url.format({
pathname: 'localhost:3000',
protocol: 'http:',
slashes: true,
}));
// window.webContents.openDevTools();
}, 1e3);
} else {
window.loadURL(url.format({
pathname: path.join(path.resolve(__dirname, './dist'), 'index.html'),
protocol: 'file:',
slashes: true,
}));
}
}
/* ------------------- main window ------------------- */
function createWindow() {
const { width, height } = getAppConf();
win = new BrowserWindow({
width,
height,
title: 'electronux',
autoHideMenuBar: true,
});
win.on('resize', () => {
const [_width, _height] = win.getContentSize();
viewConf.set({
width: _width,
height: _height,
});
});
loadWindow(win, nodeEnv);
}
/* ------------------- electron event ------------------- */
app.on('ready', () => {
if (nodeEnv === 'development') {
sourceMapSupport.install();
}
createWindow();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('will-quit', () => {
viewConf.write().then(() => 0, (err) => {
console.error(err);
throw new Error('App quit: view-conf write error !');
});
});
app.on('activate', () => {
if (win === null) {
createWindow();
}
});
前端界面React + Mobx 代碼結(jié)構(gòu)和熱更新
代碼結(jié)構(gòu)
- App.js前端入口文件
入口文件基本是整個(gè)前端應(yīng)用的關(guān)鍵點(diǎn),我們使用mobx-react包提供的Provider組件加載整個(gè)應(yīng)用,并把各個(gè)應(yīng)用模塊(按功能劃分)的mobx store示例作為props屬性傳入Provider,在各個(gè)組建中使用修飾器@inject就能直接使用store實(shí)例了,頁(yè)面層次比較多的話(huà)最好使用React Router進(jìn)行路由管理,值得注意的是React Router V4版本跟之前版本的理念和使用方式有很大區(qū)別,可以去官網(wǎng)查閱相關(guān)文檔react-router4
/* ------------------- export global history ------------------- */
export const history = createHistory();
const stores = {
install: new InstallState(),
startup: new StartupState(),
info: new InfoState(),
clean: new CleanState(),
pub: new PublicState(),
};
function App() {
return (
<Provider {...stores}>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
</Provider>
);
}
/* ------------------- export provider ------------------- */
export default App;
- mobx store 存儲(chǔ)
這是項(xiàng)目其中一個(gè)系統(tǒng)清理模塊的mobx store,在store中被mobx監(jiān)聽(tīng)的屬性最好結(jié)構(gòu)層次簡(jiǎn)單、只有單一的功能劃分,不要把一個(gè)屬性對(duì)象的嵌套寫(xiě)得太深。開(kāi)發(fā)時(shí)我們把UI界面的數(shù)據(jù)抽象成store中的數(shù)據(jù)時(shí)可能會(huì)下意識(shí)地根據(jù)頁(yè)面顯示狀態(tài)而把單個(gè)屬性對(duì)象寫(xiě)得過(guò)于復(fù)雜,但其實(shí)頁(yè)面顯示狀態(tài)只是邏輯的數(shù)據(jù)結(jié)構(gòu),我們?cè)趕tore中存儲(chǔ)的時(shí)候應(yīng)該盡量將這種邏輯數(shù)據(jù)結(jié)構(gòu)翻譯成扁平化的數(shù)據(jù)結(jié)構(gòu),然后再在各個(gè)屬性對(duì)象之間建立映射關(guān)系。
并且使用了mobx之后請(qǐng)盡量依賴(lài)mobx的數(shù)據(jù)引用監(jiān)聽(tīng)自動(dòng)更新特性,多寫(xiě)computed、autorun來(lái)自動(dòng)生成數(shù)據(jù),使用action修飾一些需要更改store屬性的方法。
class Clean {
constructor() { }
/* ------------------- observable ------------------- */
// 所有檢查項(xiàng)目 //
@observable items = {
appCache: false,
appLog: false,
trash: false,
packageCache: false,
};
// 主界面加載 //
@observable loadingMain = false;
// 清理路徑 //
cleanPaths = {
appCache: [`/home/${this.userinfo.username}/.cache`],
appLog: ['/var/log/'],
trash: [`/home/${this.userinfo.username}/.local/share/Trash/files`],
packageCache: ['/var/cache/pacman/pkg'],
}
// 路徑模塊映射 //
@observable cleanPathMap = {
appCache: [], // '/var/log/pacman.log'
appLog: [],
trash: [],
packageCache: [],
}
// 清理內(nèi)容 //
@observable cleanContents = observable.map({})
// 清理大小 //
cleanSizes = {
// '/var/log//pacman.log': '10kb',
}
// ---- 清理選項(xiàng)細(xì)節(jié)-數(shù)據(jù)對(duì)象邏輯樹(shù)結(jié)構(gòu) ---- //
// @observable cleanDetails = {
// appCache: {
// url: [`/home/${this.userinfo.username}/.cache`], // 指定掃描路徑多個(gè)
// contents: { // 絕對(duì)路徑
// // '/var/cache/pacman/pkg/zsh-5.6.2-1-x86_64.pkg.tar.xz': false,
// },
// size: {
// // '/var/cache/pacman/pkg/zsh-5.6.2-1-x86_64.pkg.tar.xz': '10kb',
// },
// },
// appLog: {
// url: ['/var/log/'],
// contents: {
// // '/var/log//pacman.log': false,
// },
// size: {
// // '/var/log//pacman.log': '10kb',
// },
// }
// }
/* ------------------- static ------------------- */
/* ------------------- computed ------------------- */
// 獲取所有被選中的detail item //
@computed get allCheckedDetail() {
const a = [];
this.cleanContents.forEach((v, k) => {
if (v) a.push(k);
});
return a;
}
// 清理路徑詳細(xì)信息 //
@computed get cleanDetail() {
const result = [];
Object.keys(this.cleanPathMap).forEach((item) => {
if (this.items[item]) {
const oneResult = {
label: item,
contents: [],
};
this.cleanPathMap[item].forEach((it) => {
oneResult.contents.push({
content: it,
size: this.cleanSizes[it] || 0,
});
});
result.push(oneResult);
}
});
return result;
}
}
export default Clean;
頁(yè)面組件劃分
在views目錄下創(chuàng)建的各個(gè)目錄都是一個(gè)單獨(dú)的組件目錄,組件目錄下有一個(gè)組件入口文件和css樣式表文件以及其它子組件,入口文件載入css文件和子組件,使用@inject修飾器后各個(gè)組件都可以獨(dú)立訪(fǎng)問(wèn)mobx store實(shí)例,不必在父和子組件之間通過(guò)props進(jìn)行逐級(jí)參數(shù)傳遞,但是如果一個(gè)子組件依賴(lài)父組件來(lái)加工原始數(shù)據(jù)的話(huà)也可以使用props傳遞參數(shù)。
使用了mobx之后,并不是說(shuō)每個(gè)頁(yè)面需要使用的數(shù)據(jù)都有必要納入mobx store的管理,在我的代碼中只是把關(guān)鍵性數(shù)據(jù)以及關(guān)鍵性數(shù)據(jù)加工方法存入了store中,每個(gè)組件拿到store傳遞下來(lái)的數(shù)據(jù)后一些頁(yè)面狀態(tài)可能需要依賴(lài)組件各自的數(shù)據(jù)處理函數(shù)進(jìn)行數(shù)據(jù)二次加工,我覺(jué)得這樣應(yīng)該會(huì)減輕store實(shí)例的負(fù)載壓力,非絕對(duì)中心化。比如在一個(gè)列表菜單組件中,這個(gè)組件的列表數(shù)據(jù)可以切換顯示和隱藏,但是控制這個(gè)列表顯示/隱藏的參數(shù)狀態(tài)visible沒(méi)有必要納入store實(shí)例管理,相對(duì)的管理這個(gè)列表組件的store實(shí)例只是存儲(chǔ)了列表數(shù)據(jù)的數(shù)組,以及一些必要的數(shù)據(jù)加工方法。渲染進(jìn)程和主進(jìn)程ipc通信的問(wèn)題
頁(yè)面的每個(gè)渲染進(jìn)程(ipcRender),雖然說(shuō)可以直接使用node.js原生模塊和api,但是不建議在渲染進(jìn)程中過(guò)度使用原生模塊,一是因?yàn)橐恍﹏ode.js原生模塊并沒(méi)有考慮到進(jìn)程安全的問(wèn)題,第二個(gè)原因是渲染進(jìn)程應(yīng)該專(zhuān)注處理頁(yè)面交互和數(shù)據(jù)處理問(wèn)題,劃清代碼的功能區(qū)域,把和系統(tǒng)交互的問(wèn)題交由主進(jìn)程(ipcMain)處理,把網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求也交由各自的service服務(wù),減少不必要的模塊和數(shù)據(jù)耦合。渲染進(jìn)程通過(guò)ipc通信向主進(jìn)程發(fā)送處理請(qǐng)求,主進(jìn)程和service負(fù)責(zé)原始數(shù)據(jù)的獲取和網(wǎng)絡(luò)數(shù)據(jù)的傳輸,最后主進(jìn)程通過(guò)ipc通信向?qū)?yīng)的渲染進(jìn)程返回處理結(jié)果,service拿到的網(wǎng)絡(luò)數(shù)據(jù)也通過(guò)回調(diào)事件發(fā)送給渲染進(jìn)程。項(xiàng)目中我把mobx store作為和主進(jìn)程通信的橋梁,mobx store向主進(jìn)程發(fā)送信號(hào),同時(shí)也在接收到主進(jìn)程的ipc通信事件后再把主進(jìn)程發(fā)回來(lái)的數(shù)據(jù)更新到各個(gè)observer??傊鬟M(jìn)程和service服務(wù)負(fù)責(zé)系統(tǒng)交互、原始數(shù)據(jù)獲取和傳輸,渲染進(jìn)程mobx store負(fù)責(zé)響應(yīng)信號(hào)和事件進(jìn)行業(yè)務(wù)數(shù)據(jù)更新,各個(gè)view子組件只負(fù)責(zé)頁(yè)面渲染和用戶(hù)交互。
前端代碼熱更新
- webpack.config.js中啟動(dòng)webpack-dev-server的熱更新功能
devServer: {
host: 'localhost',
port: 3000,
historyApiFallback: true,
hot: true,
},
- 使用
react-hot-loader的AppContainer組件
import { AppContainer } from 'react-hot-loader';
import 'semantic-ui-css/semantic.min.css';
import './styles/public.css';
import App from './App';
render(
<AppContainer>
<App />
</AppContainer>,
document.getElementById('root')
);
Linux桌面客戶(hù)端開(kāi)發(fā)遇到的問(wèn)題
使用node.js子進(jìn)程child_process執(zhí)行shell腳本時(shí)無(wú)法取得系統(tǒng)root權(quán)限
項(xiàng)目中有的腳本需要使用root權(quán)限,比如安裝和卸載軟件、掃描系統(tǒng)關(guān)鍵路徑,node.js里執(zhí)行shell腳本可以使用child_process模塊(node.js子進(jìn)程),child_process有幾個(gè)方法,spawn、exec、execFile、fork,它們都能創(chuàng)建子進(jìn)程以執(zhí)行指定文件或命令,具體的使用方法見(jiàn)Node API,如果我們的腳本或指令需要使用root權(quán)限那可就麻煩了,桌面應(yīng)用又不是終端,不可能用著用著讓用戶(hù)去終端輸入密碼吧,況且只是在開(kāi)發(fā)環(huán)境下能看到終端輸出,應(yīng)用打包安裝運(yùn)行起來(lái)后就是一個(gè)獨(dú)立的應(yīng)用程序了,根本沒(méi)法輸入終端密碼,仔細(xì)查閱了Electron官網(wǎng)API發(fā)現(xiàn)electron官方并沒(méi)有集成一個(gè)什么系統(tǒng)權(quán)限調(diào)用窗口之類(lèi)的組件。沒(méi)辦法了,這種情況下手動(dòng)寫(xiě)出了兩種方法:
- 調(diào)用獲取系統(tǒng)權(quán)限的系統(tǒng)自帶組件來(lái)執(zhí)行自定義命令和腳本
- 封裝一個(gè)彈窗組件來(lái)獲取用戶(hù)首次輸入的密碼,然后手動(dòng)把密碼記錄到文件中,應(yīng)用啟動(dòng)的時(shí)候從文件中讀出密碼,在使用child_process創(chuàng)建子進(jìn)程的時(shí)候再監(jiān)聽(tīng)子進(jìn)程的輸出事件和錯(cuò)誤事件,然后把讀取到的保存在內(nèi)存中的密碼以輸入流(input stream)的形式發(fā)送給child_process創(chuàng)建的子進(jìn)程,子進(jìn)程讀取到輸入流傳入的密碼后就能繼續(xù)執(zhí)行了。


具體代碼見(jiàn):github/nojsja/electronux/app/utils/sudo-prompt.js
感謝閱讀,文章中出現(xiàn)的錯(cuò)誤之處還請(qǐng)多原諒~