nojsja.gitee.io/blogs 更多內(nèi)容已經(jīng)在個人博客發(fā)布,請知悉
> Contents
- 前言
- 開發(fā)環(huán)境搭建
- 引入Webpack4.0前端打包工具
- Electron代碼結(jié)構(gòu)和代碼熱更新
- 前端界面React + Mobx 代碼結(jié)構(gòu)和熱更新
- Linux桌面客戶端開發(fā)遇到的問題
前言
最近桌面系統(tǒng)從Ubuntu18.04切換到了Manjaro Linux 17癌蚁,之前聽說Manjaro的軟件豐富幻梯,倉庫更新及時兜畸,很多常用軟件都能一鍵安裝(比如QQ,微信)碘梢,同時也支持主流的Linux桌面環(huán)境:Gnome咬摇、KDE、Cinnamon煞躬、Mate肛鹏、Deepin等等,安裝了Gnome版本的Manjaro之后發(fā)現(xiàn)果然還不錯恩沛。系統(tǒng)安裝好后配置比較繁瑣在扰,就想給Manjaro寫一個GUI客戶端工具用于安裝常用軟件和作為簡單的系統(tǒng)管理工具 - electronux
作為一名正直的前端開發(fā)人員,理所應(yīng)當(dāng)?shù)鼐蜏?zhǔn)備使用Electron + Node.js + React + Mobx + Webpack + Shell 來進行開發(fā)啦 ~ 目前仍然在開發(fā)中雷客,這篇文章用于記錄自己的環(huán)境搭建過程芒珠、一些對Electron+React開發(fā)的理解以及談?wù)勛约河龅降囊恍㎜inux桌面軟件開發(fā)時遇到的問題和解決辦法。
開發(fā)環(huán)境搭建
代碼目錄結(jié)構(gòu)
electronux
|---- [dir ] app ( 主代碼目錄 )
|----------- [dir ] app/configure ( 應(yīng)用配置更新 )
|----------- [dir ] app/runtime ( 運行數(shù)據(jù)文件 )
|
|----------- [dir ] app/services ( 后臺服務(wù)存放目錄 )
|------------------------ [dir ] app/services/middleware ( 一些中間處理件 )
|------------------------ [dir ] app/services/shell ( shell腳本存放目錄 )
|------------------------ [dir ] app/services/main-serv ( 主進程服務(wù) )
|------------------------ [dir ] app/services/render-serv ( 渲染進程服務(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)用渲染入口頁面 )
|---- [file] index.js ( 應(yīng)用主進程入口文件 )
|---- [file] package.json (前端模塊和框架配置文件)
|---- [file] webpack.config.js (webpack開發(fā)環(huán)境配置文件)
|---- [file] webpack.prod.config.js ( webpack生產(chǎn)環(huán)境配置文件 )
項目環(huán)境依賴配置文件
{
"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開發(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', // 在開發(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 運行 package.json 的 main 腳本的進程被稱為主進程搅裙。 在主進程中運行的腳本通過創(chuàng)建web頁面來展示用戶界面皱卓。 一個 Electron 應(yīng)用總是有且只有一個主進程。
由于 Electron 使用了 Chromium 來展示 web 頁面呈宇,所以 Chromium 的多進程架構(gòu)也被使用到好爬。 每個 Electron 中的 web 頁面運行在它自己的渲染進程中局雄。
在普通的瀏覽器中甥啄,web頁面通常在一個沙盒環(huán)境中運行,不被允許去接觸原生的資源炬搭。 然而 Electron 的用戶在 Node.js 的 API 支持下可以在頁面中和操作系統(tǒng)進行一些底層交互蜈漓。
進程使用 BrowserWindow 實例創(chuàng)建頁面。 每個 BrowserWindow 實例都在自己的渲染進程里運行頁面宫盔。 當(dāng)一個 BrowserWindow 實例被銷毀后融虽,相應(yīng)的渲染進程也會被終止。
主進程管理所有的web頁面和它們對應(yīng)的渲染進程灼芭。 每個渲染進程都是獨立的有额,它只關(guān)心它所運行的 web 頁面。
在頁面中調(diào)用與 GUI 相關(guān)的原生 API 是不被允許的彼绷,因為在 web 頁面里操作原生的 GUI 資源是非常危險的巍佑,而且容易造成資源泄露。 如果你想在 web 頁面里使用 GUI 操作寄悯,其對應(yīng)的渲染進程必須與主進程進行通訊萤衰,請求主進程進行相關(guān)的 GUI 操作。
創(chuàng)建主進程
在index.js文件中我們引入electron和所有的自定義模塊文件猜旬,并根據(jù)開發(fā)環(huán)境或是生產(chǎn)環(huán)境來進行主進程窗口加載脆栋,開發(fā)環(huán)境下使用http協(xié)議
加載由webpack-dev-server啟動的http服務(wù)倦卖,生產(chǎn)環(huán)境下使用file協(xié)議
加載本地由webpack打包好的前端bundle.js文件,所以開發(fā)環(huán)境下npm start
指令其實主要是執(zhí)行了兩步操作椿争,一是啟動webpack-dev-server怕膛,此時已經(jīng)可以通過外部瀏覽器訪問到localhost:3000的http服務(wù),只不過我們實際是用electron之中的chromium瀏覽器來加載的秦踪,它與node.js主進程共享同一個chrome v8引擎嘉竟,所以理論上,在頁面加載后洋侨,你同樣可以在渲染進程中使用node.js API舍扰,比如用使用fs模塊訪問文件系統(tǒng)。
主進程代碼熱更新
我用了nodemon工具實現(xiàn)了主進程代碼熱更新希坚,如果不用nodemon工具那么 npm start-electron
命令實際是執(zhí)行cross-env NODE_ENV=development electron index
边苹,就是簡單的用electron啟動主進程文件,使用nodemon之后npm start-electron
實際上是執(zhí)行nodemon --exec 'cross-env NODE_ENV=development electron index'
裁僧,最后在package.json文件中增加一個nodemonConfig字段用于指定哪些文件需要納入nodemon監(jiān)聽即可个束。
=> package.json中定義的啟動腳本:
"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"
},
=> 項目啟動文件index.js:
...
// 根據(jù)運行環(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前端入口文件
入口文件基本是整個前端應(yīng)用的關(guān)鍵點,我們使用mobx-react
包提供的Provider組件加載整個應(yīng)用聊疲,并把各個應(yīng)用模塊(按功能劃分)的mobx store示例作為props屬性傳入Provider茬底,在各個組建中使用修飾器@inject
就能直接使用store實例了,頁面層次比較多的話最好使用React Router進行路由管理获洲,值得注意的是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 存儲
這是項目其中一個系統(tǒng)清理模塊的mobx store,在store中被mobx監(jiān)聽的屬性最好結(jié)構(gòu)層次簡單贡珊、只有單一的功能劃分最爬,不要把一個屬性對象的嵌套寫得太深。開發(fā)時我們把UI界面的數(shù)據(jù)抽象成store中的數(shù)據(jù)時可能會下意識地根據(jù)頁面顯示狀態(tài)而把單個屬性對象寫得過于復(fù)雜门岔,但其實頁面顯示狀態(tài)只是邏輯的數(shù)據(jù)結(jié)構(gòu)爱致,我們在store中存儲的時候應(yīng)該盡量將這種邏輯數(shù)據(jù)結(jié)構(gòu)翻譯
成扁平化的數(shù)據(jù)結(jié)構(gòu),然后再在各個屬性對象之間建立映射關(guān)系寒随。
并且使用了mobx之后請盡量依賴mobx的數(shù)據(jù)引用監(jiān)聽自動更新特性糠悯,多寫computed
、autorun
來自動生成數(shù)據(jù)妻往,使用action
修飾一些需要更改store屬性的方法互艾。
class Clean {
constructor() { }
/* ------------------- observable ------------------- */
// 所有檢查項目 //
@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',
}
// ---- 清理選項細(xì)節(jié)-數(shù)據(jù)對象邏輯樹結(jié)構(gòu) ---- //
// @observable cleanDetails = {
// appCache: {
// url: [`/home/${this.userinfo.username}/.cache`], // 指定掃描路徑多個
// contents: { // 絕對路徑
// // '/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;
頁面組件劃分
在views目錄下創(chuàng)建的各個目錄都是一個單獨的組件目錄,組件目錄下有一個組件入口文件和css樣式表文件以及其它子組件蒲讯,入口文件載入css文件和子組件忘朝,使用@inject
修飾器后各個組件都可以獨立訪問mobx store實例,不必在父和子組件之間通過props進行逐級參數(shù)傳遞判帮,但是如果一個子組件依賴父組件來加工原始數(shù)據(jù)的話也可以使用props傳遞參數(shù)局嘁。
使用了mobx之后溉箕,并不是說每個頁面需要使用的數(shù)據(jù)都有必要納入mobx store的管理,在我的代碼中只是把關(guān)鍵性數(shù)據(jù)
以及關(guān)鍵性數(shù)據(jù)加工方法
存入了store中悦昵,每個組件拿到store傳遞下來的數(shù)據(jù)后一些頁面狀態(tài)可能需要依賴組件各自的數(shù)據(jù)處理函數(shù)進行數(shù)據(jù)二次加工肴茄,我覺得這樣應(yīng)該會減輕store實例的負(fù)載壓力,非絕對中心化但指。比如在一個列表菜單組件中寡痰,這個組件的列表數(shù)據(jù)可以切換顯示和隱藏,但是控制這個列表顯示/隱藏的參數(shù)狀態(tài)visible
沒有必要納入store實例管理棋凳,相對的管理這個列表組件的store實例只是存儲了列表數(shù)據(jù)的數(shù)組拦坠,以及一些必要的數(shù)據(jù)加工方法。渲染進程和主進程ipc通信的問題
頁面的每個渲染進程(ipcRender)剩岳,雖然說可以直接使用node.js原生模塊和api贞滨,但是不建議在渲染進程中過度使用原生模塊,一是因為一些node.js原生模塊并沒有考慮到進程安全的問題拍棕,第二個原因是渲染進程應(yīng)該專注處理頁面交互和數(shù)據(jù)處理問題晓铆,劃清代碼的功能區(qū)域,把和系統(tǒng)交互的問題交由主進程(ipcMain)處理绰播,把網(wǎng)絡(luò)數(shù)據(jù)請求也交由各自的service服務(wù)骄噪,減少不必要的模塊和數(shù)據(jù)耦合。渲染進程通過ipc通信向主進程發(fā)送處理請求蠢箩,主進程和service負(fù)責(zé)原始數(shù)據(jù)的獲取和網(wǎng)絡(luò)數(shù)據(jù)的傳輸链蕊,最后主進程通過ipc通信向?qū)?yīng)的渲染進程返回處理結(jié)果,service拿到的網(wǎng)絡(luò)數(shù)據(jù)也通過回調(diào)事件發(fā)送給渲染進程忙芒。項目中我把mobx store作為和主進程通信的橋梁示弓,mobx store向主進程發(fā)送信號讳侨,同時也在接收到主進程的ipc通信事件后再把主進程發(fā)回來的數(shù)據(jù)更新到各個observer呵萨。總之主進程和service服務(wù)負(fù)責(zé)系統(tǒng)交互跨跨、原始數(shù)據(jù)獲取和傳輸潮峦,渲染進程mobx store負(fù)責(zé)響應(yīng)信號和事件進行業(yè)務(wù)數(shù)據(jù)更新,各個view子組件只負(fù)責(zé)頁面渲染和用戶交互勇婴。
前端代碼熱更新
- webpack.config.js中啟動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桌面客戶端開發(fā)遇到的問題
使用node.js子進程child_process執(zhí)行shell腳本時無法取得系統(tǒng)root權(quán)限
項目中有的腳本需要使用root權(quán)限忱嘹,比如安裝和卸載軟件、掃描系統(tǒng)關(guān)鍵路徑耕渴,node.js里執(zhí)行shell腳本可以使用child_process模塊(node.js子進程)拘悦,child_process有幾個方法,spawn
橱脸、exec
础米、execFile
分苇、fork
,它們都能創(chuàng)建子進程以執(zhí)行指定文件或命令屁桑,具體的使用方法見Node API医寿,如果我們的腳本或指令需要使用root權(quán)限那可就麻煩了,桌面應(yīng)用又不是終端蘑斧,不可能用著用著讓用戶去終端輸入密碼吧靖秩,況且只是在開發(fā)環(huán)境下能看到終端輸出,應(yīng)用打包安裝運行起來后就是一個獨立的應(yīng)用程序了竖瘾,根本沒法輸入終端密碼沟突,仔細(xì)查閱了Electron官網(wǎng)API發(fā)現(xiàn)electron官方并沒有集成一個什么系統(tǒng)權(quán)限調(diào)用窗口之類的組件。沒辦法了捕传,這種情況下手動寫出了兩種方法:
- 調(diào)用獲取系統(tǒng)權(quán)限的系統(tǒng)自帶組件來執(zhí)行自定義命令和腳本
- 封裝一個彈窗組件來獲取用戶首次輸入的密碼事扭,然后手動把密碼記錄到文件中,應(yīng)用啟動的時候從文件中讀出密碼乐横,在使用child_process創(chuàng)建子進程的時候再監(jiān)聽子進程的輸出事件和錯誤事件求橄,然后把讀取到的保存在內(nèi)存中的密碼以輸入流(input stream)的形式發(fā)送給child_process創(chuàng)建的子進程,子進程讀取到輸入流傳入的密碼后就能繼續(xù)執(zhí)行了葡公。
具體代碼見:github/nojsja/electronux/app/utils/sudo-prompt.js
感謝閱讀罐农,文章中出現(xiàn)的錯誤之處還請指正~