electron-vue架構解析3-開發環境啟動流程分析(原)
上一節我們介紹了生產環境的打包流程,這一節我們來看開發環境的啟動流程。
該框架主要修改是對開發環境的優化,包括了於開發環境的配置檔案隔離,主程序和渲染程序配置檔案隔離,編譯過程提示等功能,因此這一節內容才是整個框架的核心。
我們從開發人員用到的啟動命令說起。
從package中我們看到啟動命令就是:
"dev": "node .electron-vue/dev-runner.js",
也就是在終端使用npm run dev
就可以了,而這個命令對應的指令碼就是dev-runner.js。
function init() {
greeting();
Promise.all([startRenderer(), startMain()])
.then(() => {
startElectron();
})
.catch(err => {
console.error(err);
});
}
這是指令碼執行時執行的唯一方法,其中的greeting()就是在終端輸出一個Log,如圖所示:
然後做了三個操作分別啟動了渲染程序、主程序和Electron:
- startRenderer()
- startMain()
- startElectron()
下面我們逐個來看具體的啟動流程。
1.渲染程序啟動過程分析
function startRenderer() {
return new Promise((resolve, reject) => {
//載入webpack配置檔案
rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client' )].concat(rendererConfig.entry.renderer);
//建立webpack
const compiler = webpack(rendererConfig);
//建立webpack-hot-middleware
hotMiddleware = webpackHotMiddleware(compiler, {
log: false,
heartbeat: 2500
});
//編譯狀態監控
compiler.plugin('compilation' , compilation => {
compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({action: 'reload'});
cb();
});
});
compiler.plugin('done', stats => {
logStats('Renderer', stats);
});
//建立webpack-dev-server
const server = new WebpackDevServer(
compiler,
{
contentBase: path.join(__dirname, '../'),
quiet: true,
before(app, ctx) {
app.use(hotMiddleware);
ctx.middleware.waitUntilValid(() => {
resolve();
});
}
}
);
server.listen(9080);
});
}
在這個方法裡,共完成了三個操作:
- 建立webpack物件
- 利用webpack物件來建立W**ebpackDevServer物件
- 監聽webpack編譯過程
我們分別來看這三個操作的具體情況。
1.1.渲染程序建立webpack物件
webpack物件建立時使用的配置檔案是我們分析的重點。我們來看webpack的配置檔案,也就是rendererConfig變數:
rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer);
這說明webpack的配置檔案來自於兩個檔案:dev-client模組和rendererConfig.entry.renderer變數。
這裡看一下原始碼就知道,concat方法連線而成的陣列中包含了兩個元素,一個是”dev-client”,另一個是根目錄的”../src/renderer/main.js”檔案,也就是說,webpack的entry引數實際上是這種形式:
entry: {
renderer: ['dev-client', path.join(__dirname, '../src/renderer/main.js')]
},
再來看一下output引數內容(在webpack.renderer.config.js):
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron')
},
這種用法包含了三個資訊:
- webpack將會把dev-client 模組 和main.js 檔案同時打包進output指定的檔案中
- dev-client是一個模組,根據原始碼查詢,這個模組就是dev-client.js檔案
- entry內容以key–value形式定義,那麼output中的name變數就是entry中的key
綜合來說,這種用法的作用就是:同時把dev-client.js和main.js檔案打包,輸出到根目錄下的/dist/electron/render.js檔案中。
這個資訊非常重要,後面要用到。
接下來我們先來解決這兩個檔案的作用。
渲染程序的dev-client配置檔案
這個檔案內容很簡單,直接貼出原始碼:
const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true');
//註冊webpack-hot-middleware監聽器
hotClient.subscribe(event => {
//這裡只處理了Main程序傳送的"compiling"的事件,實際上在Render程序中還發送了"reload"的訊息
if (event.action === 'compiling') {
...
<div id="dev-client">
Compiling Main Process...
</div>
`;
}
});
我們直接來說作用,他負責在編譯過程中,在介面上顯示“Compiling Main Process…”的提示語。
效果如下:
至於他如何檢測編譯過程,我們稍後來說。
渲染程序的webpack.renderer.config配置檔案
接下來我們來看渲染程序的主配置檔案的主要內容:
//將vue模組列為白名單
let whiteListedModules = ['vue'];
let rendererConfig = {
//指定sourcemap方式
devtool: '#cheap-module-eval-source-map',
entry: {
renderer: path.join(__dirname, '../src/renderer/main.js')
},
externals: [
//編譯白名單
...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
],
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
},
{
test: /\.html$/,
use: 'vue-html-loader'
},
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.node$/,
use: 'node-loader'
},
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: process.env.NODE_ENV === 'production',
loaders: {
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
scss: 'vue-style-loader!css-loader!sass-loader'
}
}
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]'
}
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name]--[folder].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]'
}
}
}
]
},
node: {
//根據版本資訊確定__dirname和__filename的行為
__dirname: process.env.NODE_ENV !== 'production',
__filename: process.env.NODE_ENV !== 'production'
},
plugins: [
//css檔案分離
new ExtractTextPlugin('styles.css'),
//自動生成html首頁
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true
},
nodeModules: process.env.NODE_ENV !== 'production'
? path.resolve(__dirname, '../node_modules')
: false
}),
//熱更新模組
new webpack.HotModuleReplacementPlugin(),
//在編譯出現錯誤時,使用 NoEmitOnErrorsPlugin 來跳過輸出階段
new webpack.NoEmitOnErrorsPlugin()
],
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron')
},
resolve: {
alias: {
//在程式碼中使用@代表renderer目錄
'@': path.join(__dirname, '../src/renderer'),
//精確指定vue特指vue.esm.js檔案
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['.js', '.vue', '.json', '.css', '.node']
},
//指定編譯為 Electron 渲染程序
target: 'electron-renderer'
};
這個配置檔案的主要配置項都給出了註釋,作用很容易理解,這裡不再詳細介紹,webpack更多配置可以檢視這裡。
1.2.渲染程序利用webpack物件來建立WebpackDevServer物件
我們先來區分一下三個概念:
-
webpack本身只是打包工具,不具備熱更新功能
-
An express-style development middleware for use with webpack bundles and allows for serving of the files emitted from webpack. This should be used for development only.
The middleware installs itself as a webpack plugin, and listens for compiler events.
他是一個運行於記憶體中的(非常重要的概念,作用稍後會提到)一個檔案系統,可以檢測檔案改動自動編譯(到記憶體中)。 -
Use webpack with a development server that provides live reloading. This should be used for development only. It uses webpack-dev-middleware under the hood, which provides fast in-memory access to the webpack assets.
他是利用webpack-hot-middleware搭建了Express的伺服器,從而實現實時重新整理的功能。
瞭解了這三個概念之後,我們來看具體如何建立WebpackDevServer物件,其實很簡單,把我們剛才建立的webpack作為引數來初始化WebpackDevServer即可:
//建立webpackHotMiddleware
hotMiddleware = webpackHotMiddleware(compiler, {
log: false,
heartbeat: 2500
});
//建立WebpackDevServer
const server = new WebpackDevServer(
//以webpack物件作為引數
compiler,
{
contentBase: path.join(__dirname, '../'),
quiet: true,
before(app, ctx) {
//使用webpackHotMiddleware
app.use(hotMiddleware);
ctx.middleware.waitUntilValid(() => {
resolve();
});
}
}
);
//伺服器執行在9080埠
server.listen(9080);
這個建立過程很清晰,需要注意的是,webpackHotMiddleware的heartbeat的引數作用並不是檢測檔案的頻率,而是保持伺服器連結存活的心跳頻率:
heartbeat - How often to send heartbeat updates to the client to keep the connection alive. Should be less than the client’s timeout setting - usually set to half its value.
1.3.渲染程序監聽webpack編譯過程
經過以上的步驟,渲染程序的初始化就基本上結束了,但是我們看到在建立過程中有兩個“小插曲”:
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({action: 'reload'});
cb();
});
});
compiler.plugin('done', stats => {
logStats('Renderer', stats);
});
其中第二個logStats作用就是在終端螢幕上輸出編譯過程(後面我們在看main程序編譯過程也會輸出到終端中),如圖所示:
而第一個hotMiddleware.publish()作用是什麼呢?
這其實是一個鉤子函式,檢測webpack的編譯狀態,把其中的html-webpack-plugin-after-emit
狀態,釋出到webpackHotMiddleware中。
我們可以檢測多種狀態,比如
- html-webpack-plugin-before-html-processing
- html-webpack-plugin-after-html-processing
- html-webpack-plugin-after-emit
- …
然後我們就可以在渲染程序中檢測到這一狀態。
然後還記得之前的dev-client模組嗎?我們再來看一下原始碼:
const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true');
//註冊webpack-hot-middleware監聽器
hotClient.subscribe(event => {
//這裡只處理了Main程序傳送的"compiling"的事件,實際上在Render程序中還發送了"reload"的訊息
if (event.action === 'compiling') {
...
<div id="dev-client">
Compiling Main Process...
</div>
`;
}
});
也就是說,在整個webpack-hot-middleware編譯過程中傳送的編譯訊息,將會在介面展示一個提示框提示開發者,編譯器正在進行的工作。
從這一點來看也發現,該框架的作者對細節的把握多麼的重視。
2.主程序過程分析
前面分析了渲染程序的啟動過程,現在來看主程序的啟動過程。
function startMain() {
return new Promise((resolve, reject) => {
mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main);
//建立主程序的webpack
const compiler = webpack(mainConfig);
compiler.plugin('watch-run', (compilation, done) => {
//向webpack-hot-middleware釋出"compiling"的訊息,用於頁面顯示
hotMiddleware.publish({action: 'compiling'});
done();
});
compiler.watch({}, (err, stats) => {
if (err) {
return;
}
...
if (electronProcess && electronProcess.kill) {
//主程序檔案發生改變,重啟Electron
manualRestart = true;
process.kill(electronProcess.pid);
electronProcess = null;
startElectron();
setTimeout(() => {
manualRestart = false;
}, 5000);
}
resolve();
});
});
}
主程序的啟動和渲染程序非常相似,共經歷了三個步驟:
- 建立webpack
- 實時釋出hotMiddleware狀態(用於頁面展示編譯過程)
- 主程序的程式碼”熱更新”
下面我們也來分別介紹這三個過程。
2.1.主程序的webpack建立過程
webpack建立的關鍵在於配置檔案,和渲染程序一樣,主程序也有兩個配置檔案:
- /src/main/index.dev.js
- /.electron-vue/webpack.main.config.js
下面分別介紹他們內容。
主程序的index.dev.js配置檔案
我們直接來看這個檔案內容。
process.env.NODE_ENV = 'development'
//安裝`electron-debug`工具
require('electron-debug')({ showDevTools: true })
//安裝Vue的一個chrome開發工具`vue-devtools`
require('electron').app.on('ready', () => {
let installExtension = require('electron-devtools-installer')
installExtension.default(installExtension.VUEJS_DEVTOOLS)
.then(() => {})
.catch(err => {
console.log('Unable to install `vue-devtools`: \n', err)
})
})
...
這個配置檔案的作用就是安裝了electron-debug
和vue-devtools
兩個工具,其中vue-devtools
工具因為網路原因無法安裝,可以自己手動安裝。
主程序的webpack.main.config.js配置檔案
這個檔案主要配置項如下:
let mainConfig = {
entry: {
main: path.join(__dirname, '../src/main/index.js')
},
externals: [
...Object.keys(dependencies || {})
],
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.node$/,
use: 'node-loader'
}
]
},
node: {
__dirname: process.env.NODE_ENV !== 'production',
__filename: process.env.NODE_ENV !== 'production'
},
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron')
},
plugins: [
new webpack.NoEmitOnErrorsPlugin()
],
resolve: {
extensions: ['.js', '.json', '.node']
},
//編譯為 Electron 主程序
target: 'electron-main'
};
他和渲染程序最大不同就是主程序處理的檔案很有限,他不會(不需要)處理vue、圖片、css、html等檔案型別。
2.2.主程序的編譯過程跟蹤
這裡和渲染程序的跟蹤稍有不同,我們對比一下:
渲染程序
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({action: 'reload'});
cb();
});
});
主程序
compiler.plugin('watch-run', (compilation, done) => {
logStats('Main', chalk.white.bold('compiling...'));
hotMiddleware.publish({action: 'compiling'});
done();
});
從這裡發現,他們分別監聽了編譯的不同事件。渲染程序監聽的是compilation
,而主程序監聽的是watch-run
。
這裡的”watch-run”只會出現在webpack3.X版本中,在webpack4.x版本上,”watch-run”事件被修改為了”watchRun”事件。
webpack還支援的一些事件包括:
- afterPlugins
- afterResolvers
- environment
- afterEnvironment
- beforeRun
- run
- beforeCompile
- afterCompile
- …
從webpack說明文件看出,”compilation”會在編譯器建立時觸發:
Runs a plugin after a compilation has been created
“watchRun”也是會在編譯器建立之後被觸發,不同的是,這個這個事件只有在”watch mode”模式下才會生效:
Executes a plugin during watch mode after a new compilation is triggered but before the compilation is actually started.
由於渲染程序使用了WebpackDevServer
的熱更新,因此可以檢測compilation
事件來跟蹤事件。
主程序在初始化過程中,由於沒有使用WebpackDevServer
,而是開啟了watch
模式,所以可以檢測到這個事件。
2.3.主程序的程式碼”熱更新”
主程序沒有使用WebpackDevServer的方式自動更新介面,而是通過webpack的watch模式,不斷重啟Electron實現的:
compiler.watch({}, (err, stats) => {
if (err) {
return;
}
if (electronProcess && electronProcess.kill) {
manualRestart = true;
process.kill(electronProcess.pid);
electronProcess = null;
//重啟Electron
startElectron();
setTimeout(() => {
manualRestart = false;
}, 5000);
}
resolve();
});
3.Electron過程分析
前面兩節分析了主程序和渲染程序的建立流程,在除錯模式命令的最後一步就是開啟Electron:
function init() {
Promise.all([startRenderer(), startMain()])
.then(() => {
startElectron();
})
.catch(err => {
});
}
下面我們來看如何開啟Electron:
function startElectron() {
electronProcess = spawn(electron, ['--inspect=5858', '.']);
...
}
原來是通過node的spawn方法運行了electron,並傳遞了兩個引數。
兩個引數分別代表開啟5858的除錯埠和electron的執行目錄(也就是當前目錄)。
至此,除錯環境執行時的流程就介紹完了,我們用一張流程圖來歸納一下這個過程:
下一節我們介紹原始介面的渲染過程。