GitHub 的 Electron 框架(以前叫做 Atom Shell)允許你使用 HTML, CSS 和 JavaScript 編寫跨平台的桌面應用。它是io.js 運行時的衍生,專注於桌面應用而不是 web 服務端。
Electron 豐富的原生 API 使我們能夠在頁面中直接使用 JavaScript 獲取原生的內容。
這個教程向我們展示了如何使用 Angular 和 Electron 構建一個桌面應用。下面是本教程的所有步驟:
-
創建一個簡單的 Electron 應用
-
使用 Visual Studio Code 編輯器管理我們的項目和任務
-
使用 Electron 開發(原文為 Integrate)一個 Angular 顧客管理應用(Angular Customer Manager App)
-
使用 Gulp 任務構建我們的應用,並生成安裝包
創建你的 Electron 應用
起初,如果你的系統中還沒有安裝 Node,你需要先安裝它。我們應用的結構如下所示:
這個項目中有兩個 package.json 文件。
-
開發使用項目根目錄下的 package.json 包含你的配置,開發環境的依賴和構建腳本。這些依賴和 package.json 文件不會被打包到生產環境構建中。
-
應用使用app 目錄下的 package.json 是你應用的清單文件。因此每當在你需要為你項目安裝 npm 依賴的時候,你應該依照這個 package.json 來進行安裝。
package.json 的格式和 Node 模塊中的完全一致。你應用的啟動腳本(的路徑)需要在 app/package.json 中的main屬性中指定。
app/package.json看起來是這樣的:
|
1
2
3
4
5
|
{
name:
"AngularElectron"
,
version:
"0.0.0"
,
main:
"main.js"
}
|
過執行npm init命令分別創建這兩個package.json文件,也可以手動創建它們。通過在命令提示行里鍵入以下命令來安裝項目打包必要的 npm 依賴:
|
1
|
npm install --save-dev electron-prebuilt fs-jetpack asar rcedit Q
|
創建啟動腳本
app/main.js是我們應用的入口。它負責創建主窗口和處理系統事件。 main.js 應該如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// app/main.js
// 應用的控制模塊
var
app = require(
'app'
);
// 創建原生瀏覽器窗口的模塊
var
BrowserWindow = require(
'browser-window'
);
var
mainWindow =
null
;
// 當所有窗口都關閉的時候退出應用
app.on(
'window-all-closed'
,
function
() {
if
(process.platform !=
'darwin'
) {
app.quit();
}
});
// 當 Electron 結束的時候,這個方法將會生效
// 初始化並准備創建瀏覽器窗口
app.on(
'ready'
,
function
() {
// 創建瀏覽器窗口.
mainWindow =
new
BrowserWindow({ width: 800, height: 600 });
// 載入應用的 index.html
mainWindow.loadUrl(
'file://'
+ __dirname +
'/index.html'
);
// 打開開發工具
// mainWindow.openDevTools();
// 窗口關閉時觸發
mainWindow.on(
'closed'
,
function
() {
// 想要取消窗口對象的引用,如果你的應用支持多窗口,
// 通常你需要將所有的窗口對象存儲到一個數組中,
// 在這個時候你應該刪除相應的元素
mainWindow =
null
;
});
});
|
通過 DOM 訪問原生
正如我上面提到的那樣,Electron 使你能夠直接在 web 頁面中訪問本地 npm 模塊和原生 API。你可以這樣創建app/index.html文件:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<html>
<body>
<h1>Hello World!</h1>
We are using Electron
<script> document.write(process.versions[
'electron'
]) </script>
<script> document.write(process.platform) </script>
<script type=
"text/javascript"
>
var
fs = require(
'fs'
);
var
file = fs.readFileSync(
'app/package.json'
);
document.write(file);
</script>
</body>
</html>
|
app/index.html是一個簡單的 HTML 頁面。在這里,它通過使用 Node’s fs (file system) 模塊來讀取package.json文件並將其內容寫入到 document body 中。
運行應用
一旦你創建好了項目結構、app/index.html、app/main.js和app/package.json,你很可能想要嘗試去運行初始的 Electron 應用來測試並確保它正常工作。
如果你已經在系統中全局安裝了electron-prebuilt,就可以通過下面的命令啟動應用:
electron app
在這里,electron是運行 electron shell 的命令,app是我們應用的目錄名。如果你不想將 Election 安裝到你全局的 npm 模塊中,可以在命令提示行中通過下面命令使用本地npm_modules文件夾下的 electron 來啟動應用。
"node_modules/.bin/electron" "./app"
盡管你可以這樣來運行應用,但是我還是建議你在gulpfile.js中創建一個 gulp task ,這樣你就可以將你的任務和 Visual Studio Code 編輯器相結合,我們會在下一部分展示。
|
1
2
3
4
5
6
7
8
9
|
// 獲取依賴
var
gulp = require(
'gulp'
),
childProcess = require(
'child_process'
),
electron = require(
'electron-prebuilt'
);
// 創建 gulp 任務
gulp.task(
'run'
,
function
() {
childProcess.spawn(electron, [
'./app'
], { stdio:
'inherit'
});
});
|
運行你的 gulp 任務:gulp run。我們的應用看起來會是這個樣子:
配置 Visual Studio Code 開發環境
Visual Studio Code 是微軟的一款跨平台代碼編輯器。VS Code 是基於 Electron 和 微軟自身的 Monaco Code Editor 開發的。你可以在 這里 下載到 Visual Studio Code。
在 VS Code 中打開你的 electron 應用。
配置 Visual Studio Code Task Runner
有很多自動化的工具,像構建、打包和測試等。我們大多從命令行中運行這些工具。VS Code task runner 使你能夠將你自定義的任務集成到項目中。你可以在你的項目中直接運行 grunt,、gulp,、MsBuild 或者其他任務,這並不需要移步到命令行。
VS Code 能夠自動檢測你的 grunt 和 gulp 任務。按下ctrl + shift + p然后鍵入Run Task敲擊回車便可。
你將從gulpfile.js或gruntfile.js文件中獲取所有有效的任務。
注意:你需要確保gulpfile.js文件存在於你應用的根目錄下。
ctrl + shift + b會從你任務執行器(task runner)中執行build任務。你可以使用task.json文件來覆蓋任務集成。按下ctrl + shift + p然后鍵入Configure Task敲擊回車。這將會在你項目中創建一個.setting的文件夾和task.json文件。要是你不止想要執行簡單的任務,你需要在task.json中進行配置。例如你或許想要通過按下Ctrl + Shift + B來運行應用,你可以這樣編輯task.json文件:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
{
"version"
:
"0.1.0"
,
"command"
:
"gulp"
,
"isShellCommand"
:
true
,
"args"
: [
"--no-color"
],
"tasks"
: [
{
"taskName"
:
"run"
,
"args"
: [],
"isBuildCommand"
:
true
}
]
}
|
根部分聲明命令為gulp。你可以在tasks部分寫入你想要的更多任務。將一個任務的isBuildCommand設置為 true 意味着它和Ctrl + Shift + B進行了綁定。目前 VS Code 只支持一個頂級任務。
現在,如果你按下Ctrl + Shift + B,gulp run將會被執行。
你可以在 這里 閱讀到更多關於 visual studio code 任務的信息。
調試 Electron 應用
打開調試面板點擊配置按鈕就會在.settings文件夾內創建一個launch.json文件,包含了調試的配置。
我們不需要啟動 app.js 的配置,所以移除它。
現在,你的launch.json應該如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{
"version"
:
"0.1.0"
,
// 配置列表。添加新的配置或更改已存在的配置。
// 僅支持 "node" 和 "mono",可以改變 "type" 來進行切換。
"configurations"
: [
{
"name"
:
"Attach"
,
"type"
:
"node"
,
// TCP/IP 地址. 默認是 "localhost"
"address"
:
"localhost"
,
// 建立連接的端口.
"port"
: 5858,
"sourceMaps"
:
false
}
]
}
|
按照下面所示更改之前創建的 gulprun任務,這樣我們的 electron 將會采用調試模式運行,5858 端口也會被監聽。
|
1
2
3
|
gulp.task(
'run'
,
function
() {
childProcess.spawn(electron, [
'--debug=5858'
,
'./app'
], { stdio:
'inherit'
});
});
|
在調試面板中選擇 “Attach” 配置項,點擊開始(run)或者按下 F5。稍等片刻后你應該就能在上部看到調試命令面板。
創建 AngularJS 應用
第一次接觸 AngularJS?瀏覽 官方網站 或一些 Scotch Angular 教程 。
這一部分會講解如何使用 AngularJS 和 MySQL 數據庫創建一個顧客管理(Customer Manager)應用。這個應用的目的不是為了強調 AngularJS 的核心概念,而是展示如何在 GiHub 的 Electron 中同時使用 AngularJS 和 NodeJS 以及 MySQL 。
我們的顧客管理應用正如下面這樣簡單:
-
顧客列表
-
添加新顧客
-
選擇刪除一個顧客
-
搜索指定的顧客
項目結構
我們的應用在 app 文件夾下,目錄結構如下所示:
主頁是app/index.html文件。app/scripts文件夾包含所有用在該應用中的關鍵腳本和視圖。有許多方法可以用來組織應用的文件。
這里我更喜歡按照功能來組織腳本文件。每個功能都有它自己的文件夾,文件夾中有模板和控制器。獲取更多關於目錄結構的信息,可以閱讀 AngularJS 最佳實踐: 目錄結構
在開始 AngularJS 應用之前,我們將使用 bower 安裝客戶端方面的依賴。如果你還沒有 Bower 先要安裝它。在命令提示行中將當前工作目錄切換至你應用的根目錄,然后依照下面的命令安裝依賴。
|
1
|
bower install angular angular-route angular-material --save
|
設置數據庫
在這個例子中,我將使用一個名字為customer-manager的數據庫和一張名字為customers的表。下面是數據庫的導出文件,你可以依照這個快速開始。
|
1
2
3
4
5
6
7
8
9
|
CREATE TABLE `customer_manager`.`customers` (
`customer_id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL,
`address` VARCHAR(450) NULL,
`city` VARCHAR(45) NULL,
`country` VARCHAR(45) NULL,
`phone` VARCHAR(45) NULL,
`remarks` VARCHAR(500) NULL, PRIMARY KEY (`customer_id`)
);
|
創建一個 Angular Service 和 MySQL 進行交互
一旦你的數據庫和表都准備好了,就可以開始創建一個 AngularJS service 來直接從數據庫中獲取數據。使用node-mysql這個 npm 模塊使 service 連接數據庫——一個使用 JavaScript 為 NodeJs 編寫的 MySQL 驅動。在你 Angular 應用的app/ 目錄下安裝node-mysql模塊。
注意:我們將 node-mysql 模塊安裝到 app 目錄下而不是應用的根目錄,是因為我們需要在最終的 distribution 中包含這個模塊。
在命令提示行中切換工作目錄至 app 文件夾然后按照下面所示安裝模塊:
npm install --save mysql
我們的 angular service —— app/scripts/customer/customerService.js 如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
(
function
() {
'use strict'
;
var
mysql = require(
'mysql'
);
// 創建 MySql 數據庫連接
var
connection = mysql.createConnection({
host:
"localhost"
,
user:
"root"
,
password:
"password"
,
database:
"customer_manager"
});
angular.module(
'app'
)
.service(
'customerService'
, [
'$q'
, CustomerService]);
function
CustomerService($q) {
return
{
getCustomers: getCustomers,
getById: getCustomerById,
getByName: getCustomerByName,
create: createCustomer,
destroy: deleteCustomer,
update: updateCustomer
};
function
getCustomers() {
var
deferred = $q.defer();
var
query =
"SELECT * FROM customers"
;
connection.query(query,
function
(err, rows) {
if
(err) deferred.reject(err);
deferred.resolve(rows);
});
return
deferred.promise;
}
function
getCustomerById(id) {
var
deferred = $q.defer();
var
query =
"SELECT * FROM customers WHERE customer_id = ?"
;
connection.query(query, [id],
function
(err, rows) {
if
(err) deferred.reject(err);
deferred.resolve(rows);
});
return
deferred.promise;
}
function
getCustomerByName(name) {
var
deferred = $q.defer();
var
query =
"SELECT * FROM customers WHERE name LIKE '"
+ name +
"%'"
;
connection.query(query, [name],
function
(err, rows) {
if
(err) deferred.reject(err);
deferred.resolve(rows);
});
return
deferred.promise;
}
function
createCustomer(customer) {
var
deferred = $q.defer();
var
query =
"INSERT INTO customers SET ?"
;
connection.query(query, customer,
function
(err, res)
if
(err) deferred.reject(err);
deferred.resolve(res.insertId);
});
return
deferred.promise;
}
function
deleteCustomer(id) {
var
deferred = $q.defer();
var
query =
"DELETE FROM customers WHERE customer_id = ?"
;
connection.query(query, [id],
function
(err, res) {
if
(err) deferred.reject(err);
deferred.resolve(res.affectedRows);
});
return
deferred.promise;
}
function
updateCustomer(customer) {
var
deferred = $q.defer();
var
query =
"UPDATE customers SET name = ? WHERE customer_id = ?"
;
connection.query(query, [customer.name, customer.customer_id],
function
(err, res) {
if
(err) deferred.reject(err);
deferred.resolve(res);
});
return
deferred.promise;
}
}
})();
|
customerService是一個簡單的自定義 angular service,它提供了對表customers的基礎 CRUD 操作。直接在 service 中使用了 node 模塊mysql。如果你已經擁有了一個遠程的數據服務,你也可以使用它來替代之。
控制器 & 模板
app/scripts/customer/customerController中的customerController如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
(
function
() {
'use strict'
;
angular.module(
'app'
)
.controller(
'customerController'
, [
'customerService'
,
'$q'
,
'$mdDialog'
, CustomerController]);
function
CustomerController(customerService, $q, $mdDialog) {
var
self =
this
;
self.selected =
null
;
self.customers = [];
self.selectedIndex = 0;
self.filterText =
null
;
self.selectCustomer = selectCustomer;
self.deleteCustomer = deleteCustomer;
self.saveCustomer = saveCustomer;
self.createCustomer = createCustomer;
self.filter = filterCustomer;
// 載入初始數據
getAllCustomers();
//----------------------
// 內部方法
//----------------------
function
selectCustomer(customer, index) {
self.selected = angular.isNumber(customer) ? self.customers[customer] : customer;
self.selectedIndex = angular.isNumber(customer) ? customer: index;
}
function
deleteCustomer($event) {
var
confirm = $mdDialog.confirm()
.title(
'Are you sure?'
)
.content(
'Are you sure want to delete this customer?'
)
.ok(
'Yes'
)
.cancel(
'No'
)
.targetEvent($event);
$mdDialog.show(confirm).then(
function
() {
customerService.destroy(self.selected.customer_id).then(
function
(affectedRows) {
self.customers.splice(self.selectedIndex, 1);
});
},
function
() { });
}
function
saveCustomer($event) {
if
(self.selected !=
null
&& self.selected.customer_id !=
null
) {
customerService.update(self.selected).then(
function
(affectedRows) {
$mdDialog.show(
$mdDialog
.alert()
.clickOutsideToClose(
true
)
.title(
'Success'
)
.content(
'Data Updated Successfully!'
)
.ok(
'Ok'
)
.targetEvent($event)
);
});
}
else
{
//self.selected.customer_id = new Date().getSeconds();
customerService.create(self.selected).then(
function
(affectedRows) {
$mdDialog.show(
$mdDialog
.alert()
.clickOutsideToClose(
true
)
.title(
'Success'
)
.content(
'Data Added Successfully!'
)
.ok(
'Ok'
)
.targetEvent($event)
);
});
}
}
function
createCustomer() {
self.selected = {};
self.selectedIndex =
null
;
}
function
getAllCustomers() {
customerService.getCustomers().then(
function
(customers) {
self.customers = [].concat(customers);
self.selected = customers[0];
});
}
function
filterCustomer() {
if
(self.filterText ==
null
|| self.filterText ==
""
) {
getAllCustomers();
}
else
{
customerService.getByName(self.filterText).then(
function
(customers) {
self.customers = [].concat(customers);
self.selected = customers[0];
});
}
}
}
})();
|
我們的顧客模板( app/scripts/customer/customer.html )使用了 angular material 組件來構建 UI,如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
<div style=
"width:100%"
layout=
"row"
>
<md-sidenav class=
"site-sidenav md-sidenav-left md-whiteframe-z2"
md-component-id=
"left"
md-is-locked-open=
"$mdMedia('gt-sm')"
>
<md-toolbar layout=
"row"
class=
"md-whiteframe-z1"
>
<h1>Customers</h1>
</md-toolbar>
<md-input-container style=
"margin-bottom:0"
>
<label>Customer Name</label>
<input required name=
"customerName"
ng-model=
"_ctrl.filterText"
ng-change=
"_ctrl.filter()"
>
</md-input-container>
<md-list>
<md-list-item ng-repeat=
"it in _ctrl.customers"
>
<md-button ng-click=
"_ctrl.selectCustomer(it, $index)"
ng-class=
"{'selected' : it === _ctrl.selected }"
>
{{it.name}}
</md-button>
</md-list-item>
</md-list>
</md-sidenav>
<div flex layout=
"column"
tabIndex=
"-1"
role=
"main"
class=
"md-whiteframe-z2"
>
<md-toolbar layout=
"row"
class=
"md-whiteframe-z1"
>
<md-button class=
"menu"
hide-gt-sm ng-click=
"ul.toggleList()"
aria-label=
"Show User List"
>
<md-icon md-svg-icon=
"menu"
></md-icon>
</md-button>
<h1>{{ _ctrl.selected.name }}</h1>
</md-toolbar>
<md-content flex id=
"content"
>
<div layout=
"column"
style=
"width:50%"
>
<br />
<md-content layout-padding class=
"autoScroll"
>
<md-input-container>
<label>Name</label>
<input ng-model=
"_ctrl.selected.name"
type=
"text"
>
</md-input-container>
<md-input-container md-no-float>
<label>Email</label>
<input ng-model=
"_ctrl.selected.email"
type=
"text"
>
</md-input-container>
<md-input-container>
<label>Address</label>
<input ng-model=
"_ctrl.selected.address"
ng-required=
"true"
>
</md-input-container>
<md-input-container md-no-float>
<label>City</label>
<input ng-model=
"_ctrl.selected.city"
type=
"text"
>
</md-input-container>
<md-input-container md-no-float>
<label>Phone</label>
<input ng-model=
"_ctrl.selected.phone"
type=
"text"
>
</md-input-container>
</md-content>
<section layout=
"row"
layout-sm=
"column"
layout-align=
"center center"
layout-wrap>
<md-button class=
"md-raised md-info"
ng-click=
"_ctrl.createCustomer()"
>Add</md-button>
<md-button class=
"md-raised md-primary"
ng-click=
"_ctrl.saveCustomer()"
>Save</md-button>
<md-button class=
"md-raised md-danger"
ng-click=
"_ctrl.cancelEdit()"
>Cancel</md-button>
<md-button class=
"md-raised md-warn"
ng-click=
"_ctrl.deleteCustomer()"
>Delete</md-button>
</section>
</div>
</md-content>
</div>
</div>
|
app.js 包含模塊初始化腳本和應用的路由配置,如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
(
function
() {
'use strict'
;
var
_templateBase =
'./scripts'
;
angular.module(
'app'
, [
'ngRoute'
,
'ngMaterial'
,
'ngAnimate'
])
.config([
'$routeProvider'
,
function
($routeProvider) {
$routeProvider.when(
'/'
, {
templateUrl: _templateBase +
'/customer/customer.html'
,
controller:
'customerController'
,
controllerAs:
'_ctrl'
});
$routeProvider.otherwise({ redirectTo:
'/'
});
}
]);
})();
|
最后是我們的首頁 app/index.html
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<html lang=
"en"
ng-app=
"app"
>
<title>Customer Manager</title>
<meta charset=
"utf-8"
>
<meta http-equiv=
"X-UA-Compatible"
content=
"IE=edge"
gt;
<meta name=
"description"
content=
""
>
<meta name=
"viewport"
content=
"initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<!-- build:css assets/css/app.css -->
<link rel=
"stylesheet"
href=
"../bower_components/angular-material/angular-material.css"
/>
<link rel=
"stylesheet"
href=
"assets/css/style.css"
/>
<!-- endbuild -->
<body>
<ng-view></ng-view>
<!-- build:js scripts/vendor.js -->
<script src=
"../bower_components/angular/angular.js"
></script>
<script src=
"../bower_components/angular-route/angular-route.js"
></script>
<script src=
"../bower_components/angular-animate/angular-animate.js"
></script>
<script src=
"../bower_components/angular-aria/angular-aria.js"
></script>
<script src=
"../bower_components/angular-material/angular-material.js"
></script>
<!-- endbuild -->
<!-- build:app scripts/app.js -->
<script src=
"./scripts/app.js"
></script>
<script src=
"./scripts/customer/customerService.js"
></script>
<script src=
"./scripts/customer/customerController.js"
></script>
<!-- endbuild -->
</body>
</html>
|
如果你已經如上面那樣配置過 VS Code task runner 的話,使用gulp run命令或者按下Ctrl + Shif + B來啟動你的應用。
構建 AngularJS 應用
為了構建我們的 Angular 應用,需要安裝gulp-uglify,gulp-minify-css和gulp-usemin依賴包。
|
1
|
npm install --save gulp-uglify gulp-minify-css gulp-usemin
|
打開你的gulpfile.js並且引入必要的模塊。
|
1
2
3
4
5
6
7
8
9
10
|
var
childProcess = require(
'child_process'
);
var
electron = require(
'electron-prebuilt'
);
var
gulp = require(
'gulp'
);
var
jetpack = require(
'fs-jetpack'
);
var
usemin = require(
'gulp-usemin'
);
var
uglify = require(
'gulp-uglify'
);
var
projectDir = jetpack;
var
srcDir = projectDir.cwd(
'./app'
);
var
destDir = projectDir.cwd(
'./build'
);
|
如果構建目錄已經存在的話,清理一下它。
|
1
2
3
|
gulp.task(
'clean'
,
function
(callback) {
return
destDir.dirAsync(
'.'
, { empty:
true
});
});
|
復制文件到構建目錄。我們並不需要使用復制功能來復制 angular 應用的代碼,在下一部分中usemin將會為我們做這件事請:
|
1
2
3
4
5
6
7
8
9
10
11
|
gulp.task(
'copy'
, [
'clean'
],
function
() {
return
projectDir.copyAsync(
'app'
, destDir.path(), {
overwrite:
true
, matching: [
'./node_modules/**/*'
,
'*.html'
,
'*.css'
,
'main.js'
,
'package.json'
]
});
});
|
我們的構建任務將使用 gulp.src() 獲取 app/index.html 然后傳遞給 usemin。然后它會將輸出寫入到構建目錄並且把 index.html 中的引用用優化版代碼替換掉 。
注意: 千萬不要忘記在 app/index.html 像這樣定義 usemin 塊:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!-- build:js scripts/vendor.js -->
<script src=
"../bower_components/angular/angular.js"
></script>
<script src=
"../bower_components/angular-route/angular-route.js"
></script>
<script src=
"../bower_components/angular-animate/angular-animate.js"
></script>
<script src=
"../bower_components/angular-aria/angular-aria.js"
></script>
<script src=
"../bower_components/angular-material/angular-material.js"
></script>
<!-- endbuild -->
<!-- build:app scripts/app.js -->
<script src=
"./scripts/app.js"
></script>
<script src=
"./scripts/customer/customerService.js"
></script>
<script src=
"./scripts/customer/customerController.js"
></script>
<!-- endbuild -->
|
構建任務如下所示:
|
1
2
3
4
5
6
7
|
gulp.task(
'build'
, [
'copy'
],
function
() {
return
gulp.src(
'./app/index.html'
)
.pipe(usemin({
js: [uglify()]
}))
.pipe(gulp.dest(
'build/'
));
});
|
為發行(distribution)做准備
在這一部分我們將把 Electron 應用打包至生產環境。在根目錄創建構建腳本build.windows.js。這個腳本用於 Windows 上。對於其他平台來說,你應該創建那個平台特定的腳本並且根據平台來運行。
可以在node_modules/electron-prebuilt/dist目錄中找到一個典型的 electron distribution。這里是構建 electron 應用的步驟:
-
我們首要的任務是復制 electron distribution 到我們的dist目錄。
-
每一個 electron distribution 都包含一個默認的應用在dist/resources/default_app中 。我們需要用我們最終構建的應用來替換它。
-
為了保護我們的應用源碼和資源,你可以選擇將你的應用打包成一個 asar 歸檔,這會改變一點你的源碼。一個 asar 歸檔是一個簡單的類似 tar 的格式,它會將你所有的文件拼接成單個文件,Electron 可以在不解壓整個文件的情況下從中讀取任意文件。
注意:這一部分描述的是 windows 平台下的打包。其他平台中的步驟是一樣的,只是路徑和使用的文件不一樣而已。你可以在 github 中獲取 OSx 和 linux 的完整構建腳本。
安裝構建 electron 必要的依賴:npm install --save q asar fs-jetpack recedit
接下來,初始化我們的構建腳本,如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
var
Q = require(
'q'
);
var
childProcess = require(
'child_process'
);
var
asar = require(
'asar'
);
var
jetpack = require(
'fs-jetpack'
);
var
projectDir;
var
buildDir;
var
manifest;
var
appDir;
function
init() {
// 項目路徑是應用的根目錄
projectDir = jetpack;
// 構建目錄是最終應用被構建后放置的目錄
buildDir = projectDir.dir(
'./dist'
, { empty:
true
});
// angular 應用目錄
appDir = projectDir.dir(
'./build'
);
// angular 應用的 package.json 文件
manifest = appDir.read(
'./package.json'
,
'json'
);
return
Q();
}
|
這里我們使用fs-jetpacknode 模塊進行文件操作。它提供了更靈活的文件操作。
復制 Electron Distribution
從electron-prebuilt/dist復制默認的 electron distribution 到我們的 dist 目錄
|
1
2
3
|
function
copyElectron() {
return
projectDir.copyAsync(
'./node_modules/electron-prebuilt/dist'
, buildDir.path(), { overwrite:
true
});
}
|
清理默認應用
你可以在resources/default_app文件夾內找到一個默認的 HTML 應用。我們需要用我們自己的 angular 應用來替換它。按照下面所示移除它:
注意:這里的路徑是針對 windows 平台的。對於其他平台過程是一致的,只是路徑不一樣而已。在 OSX 中路徑應該是 Contents/Resources/default_app
|
1
2
3
|
function
cleanupRuntime() {
return
buildDir.removeAsync(
'resources/default_app'
);
}
|
創建 asar 包
|
1
2
3
4
5
6
7
|
function
createAsar() {
var
deferred = Q.defer();
asar.createPackage(appDir.path(), buildDir.path(
'resources/app.asar'
),
function
() {
deferred.resolve();
});
return
deferred.promise;
}
|
這將會把你 angular 應用的所有文件打包到一個 asar 包文件里。你可以在dist/resources/目錄中找到 asar 文件。
替換為自己的應用資源
下一步是將默認的 electron icon 替換成你自己的,更新產品的信息然后重命名應用。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
function
updateResources() {
var
deferred = Q.defer();
// 將你的 icon 從 resource 文件夾復制到構建文件夾下
projectDir.copy(
'resources/windows/icon.ico'
, buildDir.path(
'icon.ico'
));
// 將 Electron icon 替換成你自己的
var
rcedit = require(
'rcedit'
);
rcedit(buildDir.path(
'electron.exe'
), {
'icon'
: projectDir.path(
'resources/windows/icon.ico'
),
'version-string'
: {
'ProductName'
: manifest.name,
'FileDescription'
: manifest.description,
}
},
function
(err) {
if
(!err) {
deferred.resolve();
}
});
return
deferred.promise;
}
// 重命名 electron exe
function
rename() {
return
buildDir.renameAsync(
'electron.exe'
, manifest.name +
'.exe'
);
}
|
創建原生安裝包
你可以使用 wix 或 NSIS 創建 windows 安裝包。這里我們盡可能使用更小更靈活的 NSIS,它很適合網絡應用。使用 NSIS 可以創建支持應用安裝時需要的任何事情的安裝包。
在 resources/windows/installer.nsis 中創建 NSIS 腳本
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
!include LogicLib.nsh
!include nsDialogs.nsh
; --------------------------------
; Variables
; --------------------------------
!define dest
"{{dest}}"
!define src
"{{src}}"
!define name
"{{name}}"
!define productName
"{{productName}}"
!define version
"{{version}}"
!define icon
"{{icon}}"
!define banner
"{{banner}}"
!define exec
"{{productName}}.exe"
!define regkey
"Software\${productName}"
!define uninstkey
"Software\Microsoft\Windows\CurrentVersion\Uninstall\${productName}"
!define uninstaller
"uninstall.exe"
; --------------------------------
; Installation
; --------------------------------
SetCompressor lzma
Name
"${productName}"
Icon
"${icon}"
OutFile
"${dest}"
InstallDir
"$PROGRAMFILES\${productName}"
InstallDirRegKey HKLM
"${regkey}"
""
CRCCheck on
SilentInstall normal
XPStyle on
ShowInstDetails nevershow
AutoCloseWindow
false
WindowIcon off
Caption
"${productName} Setup"
; Don
't add sub-captions to title bar
SubCaption 3 " "
SubCaption 4 " "
Page custom welcome
Page instfiles
Var Image
Var ImageHandle
Function .onInit
; Extract banner image for welcome page
InitPluginsDir
ReserveFile "${banner}"
File /oname=$PLUGINSDIR\banner.bmp "${banner}"
FunctionEnd
; Custom welcome page
Function welcome
nsDialogs::Create 1018
${NSD_CreateLabel} 185 1u 210 100% "Welcome to ${productName} version ${version} installer.$\r$\n$\r$\nClick install to begin."
${NSD_CreateBitmap} 0 0 170 210 ""
Pop $Image
${NSD_SetImage} $Image $PLUGINSDIR\banner.bmp $ImageHandle
nsDialogs::Show
${NSD_FreeImage} $ImageHandle
FunctionEnd
; Installation declarations
Section "Install"
WriteRegStr HKLM "${regkey}" "Install_Dir" "$INSTDIR"
WriteRegStr HKLM "${uninstkey}" "DisplayName" "${productName}"
WriteRegStr HKLM "${uninstkey}" "DisplayIcon" '
"$INSTDIR\icon.ico"
'
WriteRegStr HKLM "${uninstkey}" "UninstallString" '
"$INSTDIR\${uninstaller}"
'
; Remove all application files copied by previous installation
RMDir /r "$INSTDIR"
SetOutPath $INSTDIR
; Include all files from /build directory
File /r "${src}\*"
; Create start menu shortcut
CreateShortCut "$SMPROGRAMS\${productName}.lnk" "$INSTDIR\${exec}" "" "$INSTDIR\icon.ico"
WriteUninstaller "${uninstaller}"
SectionEnd
; --------------------------------
; Uninstaller
; --------------------------------
ShowUninstDetails nevershow
UninstallCaption "Uninstall ${productName}"
UninstallText "Don'
t like ${productName} anymore? Hit uninstall button."
UninstallIcon
"${icon}"
UninstPage custom un.confirm un.confirmOnLeave
UninstPage instfiles
Var RemoveAppDataCheckbox
Var RemoveAppDataCheckbox_State
; Custom uninstall confirm page
Function un.confirm
nsDialogs::Create 1018
${NSD_CreateLabel} 1u 1u 100% 24u
"If you really want to remove ${productName} from your computer press uninstall button."
${NSD_CreateCheckbox} 1u 35u 100% 10u
"Remove also my ${productName} personal data"
Pop $RemoveAppDataCheckbox
nsDialogs::Show
FunctionEnd
Function un.confirmOnLeave
; Save checkbox state on page leave
${NSD_GetState} $RemoveAppDataCheckbox $RemoveAppDataCheckbox_State
FunctionEnd
; Uninstall declarations
Section
"Uninstall"
DeleteRegKey HKLM
"${uninstkey}"
DeleteRegKey HKLM
"${regkey}"
Delete
"$SMPROGRAMS\${productName}.lnk"
; Remove whole directory from Program Files
RMDir /r
"$INSTDIR"
; Remove also appData directory generated by your app
if
user checked
this
option
${If} $RemoveAppDataCheckbox_State == ${BST_CHECKED}
RMDir /r
"$LOCALAPPDATA\${name}"
${EndIf}
SectionEnd
|
在build.windows.js文件中創建一個叫做createInstaller的函數,如下所示:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
function
createInstaller() {
var
deferred = Q.defer();
function
replace(str, patterns) {
Object.keys(patterns).forEach(
function
(pattern) {
console.log(pattern)
var
matcher =
new
RegExp(
'{{'
+ pattern +
'}}'
,
'g'
);
str = str.replace(matcher, patterns[pattern]);
});
return
str;
}
var
installScript = projectDir.read(
'resources/windows/installer.nsi'
);
installScript = replace(installScript, {
name: manifest.name,
productName: manifest.name,
version: manifest.version,
src: buildDir.path(),
dest: projectDir.path(),
icon: buildDir.path(
'icon.ico'
),
setupIcon: buildDir.path(
'icon.ico'
),
banner: projectDir.path(
'resources/windows/banner.bmp'
),
});
buildDir.write(
'installer.nsi'
, installScript);
var
nsis = childProcess.spawn(
'makensis'
, [buildDir.path(
'installer.nsi'
)], {
stdio:
'inherit'
});
nsis.on(
'error'
,
function
(err) {
if
(err.message ===
'spawn makensis ENOENT'
) {
throw
"Can't find NSIS. Are you sure you've installed it and"
+
" added to PATH environment variable?"
;
}
else
{
throw
err;
}
});
nsis.on(
'close'
,
function
() {
deferred.resolve();
});
return
deferred.promise;
}
|
你應該安裝了 NSIS,並且確保它在你的路徑中是可用的。creaeInstaller函數會讀取安裝包腳本並且依照 NSIS 運行時使用makensis命令來執行。
將他們組合到一起
創建一個函數把所有的片段放在一起,為了使 gulp 任務可以獲取到然后輸出它:
|
1
2
3
4
5
6
7
8
9
10
|
function
build() {
return
init()
.then(copyElectron)
.then(cleanupRuntime)
.then(createAsar)
.then(updateResources)
.then(rename)
.then(createInstaller);
}
module.exports = { build: build };
|
接着,在gulpfile.js中創建 gulp 任務來執行這個構建腳本:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var
release_windows = require(
'./build.windows'
);
var
os = require(
'os'
);
gulp.task(
'build-electron'
, [
'build'
],
function
() {
switch
(os.platform()) {
case
'darwin'
:
// 執行 build.osx.js
break
;
case
'linux'
:
//執行 build.linux.js
break
;
case
'win32'
:
return
release_windows.build();
}
});
|
運行下面命令,你應該就會得到最終的產品:
gulp build-electron
你最終的 electron 應用應該在dist目錄中,並且目錄結構應該和下面是相似的:

總結
Electron 不僅僅是一個支持打包 web 應用成為桌面應用的原生 web view。它現在包含 app 的自動升級、Windows 安裝包、崩潰報告、通知和一些其它有用的原生 app 功能——所有的這些都通過 JavaScript API 調用。
到目前為止,很大范圍的應用使用 electron 創建,包括聊天應用、數據庫管理器、地圖設計器、協作設計工具和手機原型等。
