【譯】使用 AngularJS 和 Electron 構建桌面應用

原文:Creating Desktop Applications With AngularJS and GitHub Electronjavascript

angular-electron-cover.png

GitHub 的 Electron 框架(之前叫作 Atom Shell)容許你使用 HTML, CSS 和 JavaScript 編寫跨平臺的桌面應用。它是 io.js 運行時的衍生,專一於桌面應用而不是 web 服務端。css

Electron 豐富的原生 API 使咱們可以在頁面中直接使用 JavaScript 獲取原生的內容。html

這個教程向咱們展現瞭如何使用 Angular 和 Electron 構建一個桌面應用。下面是本教程的全部步驟:java

  1. 建立一個簡單的 Electron 應用node

  2. 使用 Visual Studio Code 編輯器管理咱們的項目和任務mysql

  3. 使用 Electron 開發(原文爲 Integrate)一個 Angular 顧客管理應用(Angular Customer Manager App)react

  4. 使用 Gulp 任務構建咱們的應用,並生成安裝包linux

建立你的 Electron 應用

起初,若是你的系統中尚未安裝 Node,你須要先安裝它。咱們應用的結構以下所示:git

project-structure.png

這個項目中有兩個 package.json 文件。angularjs

  • 開發使用
    項目根目錄下的 package.json 包含你的配置,開發環境的依賴和構建腳本。這些依賴和 package.json 文件不會被打包到生產環境構建中。

  • 應用使用
    app 目錄下的 package.json 是你應用的清單文件。所以每當在你須要爲你項目安裝 npm 依賴的時候,你應該依照這個 package.json 來進行安裝。

package.json 的格式和 Node 模塊中的徹底一致。你應用的啓動腳本(的路徑)須要在 app/package.json 中的 main 屬性中指定。

app/package.json 看起來是這樣的:

{
  name: "AngularElectron", 
  version: "0.0.0", 
  main: "main.js" 
}

過執行 npm init 命令分別建立這兩個 package.json 文件,也能夠手動建立它們。經過在命令提示行裏鍵入如下命令來安裝項目打包必要的 npm 依賴:

npm install --save-dev electron-prebuilt fs-jetpack asar rcedit Q

建立啓動腳本

app/main.js 是咱們應用的入口。它負責建立主窗口和處理系統事件。main.js 應該以下所示:

// 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 文件:

<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.htmlapp/main.jsapp/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 編輯器相結合,咱們會在下一部分展現。

// 獲取依賴
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。咱們的應用看起來會是這個樣子:

electron-app

配置 Visual Studio Code 開發環境

Visual Studio Code 是微軟的一款跨平臺代碼編輯器。VS Code 是基於 Electron 和 微軟自身的 Monaco Code Editor 開發的。你能夠在這裏下載到 Visual Studio Code。

在 VS Code 中打開你的 electron 應用。

open-application.png

配置 Visual Studio Code Task Runner

有不少自動化的工具,像構建、打包和測試等。咱們大多從命令行中運行這些工具。VS Code task runner 使你可以將你自定義的任務集成到項目中。你能夠在你的項目中直接運行 grunt,、gulp,、MsBuild 或者其餘任務,這並不須要移步到命令行。

VS Code 可以自動檢測你的 grunt 和 gulp 任務。按下 ctrl + shift + p 而後鍵入 Run Task 敲擊回車即可。

run-task.png

你將從 gulpfile.jsgruntfile.js 文件中獲取全部有效的任務。

注意:你須要確保 gulpfile.js 文件存在於你應用的根目錄下。

run-task-gulp.png

ctrl + shift + b 會從你任務執行器(task runner)中執行 build 任務。你可使用 task.json 文件來覆蓋任務集成。按下 ctrl + shift + p 而後鍵入 Configure Task 敲擊回車。這將會在你項目中建立一個 .setting 的文件夾和 task.json 文件。要是你不止想要執行簡單的任務,你須要在 task.json 中進行配置。例如你或許想要經過按下 Ctrl + Shift + B 來運行應用,你能夠這樣編輯 task.json 文件:

{ 
  "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 + Bgulp run 將會被執行。

你能夠在這裏閱讀到更多關於 visual studio code 任務的信息。

調試 Electron 應用

打開調試面板點擊配置按鈕就會在 .settings 文件夾內建立一個 launch.json 文件,包含了調試的配置。

debug.png

咱們不須要啓動 app.js 的配置,因此移除它。

如今,你的 launch.json 應該以下所示:

{ 
  "version": "0.1.0", 
  // 配置列表。添加新的配置或更改已存在的配置。
  // 僅支持 "node" 和 "mono",能夠改變 "type" 來進行切換。
  "configurations": [
    { 
      "name": "Attach", 
      "type": "node", 
      // TCP/IP 地址. 默認是 "localhost"
      "address": "localhost", 
      // 創建鏈接的端口.
      "port": 5858, 
      "sourceMaps": false 
     } 
   ] 
}

按照下面所示更改以前建立的 gulp run 任務,這樣咱們的 electron 將會採用調試模式運行,5858 端口也會被監聽。

gulp.task('run', function () { 
  childProcess.spawn(electron, ['--debug=5858','./app'], { stdio: 'inherit' }); 
});

在調試面板中選擇 「Attach」 配置項,點擊開始(run)或者按下 F5。稍等片刻後你應該就能在上部看到調試命令面板。

debug-star.png

建立 AngularJS 應用

第一次接觸 AngularJS?瀏覽官方網站或一些 Scotch Angular 教程

這一部分會講解如何使用 AngularJS 和 MySQL 數據庫建立一個顧客管理(Customer Manager)應用。這個應用的目的不是爲了強調 AngularJS 的核心概念,而是展現如何在 GiHub 的 Electron 中同時使用 AngularJS 和 NodeJS 以及 MySQL 。

咱們的顧客管理應用正以下面這樣簡單:

  • 顧客列表

  • 添加新顧客

  • 選擇刪除一個顧客

  • 搜索指定的顧客

項目結構

咱們的應用在 app 文件夾下,目錄結構以下所示:

angular-project-structure.png

主頁是 app/index.html 文件。app/scripts 文件夾包含全部用在該應用中的關鍵腳本和視圖。有許多方法能夠用來組織應用的文件。

這裏我更喜歡按照功能來組織腳本文件。每一個功能都有它本身的文件夾,文件夾中有模板和控制器。獲取更多關於目錄結構的信息,能夠閱讀 AngularJS 最佳實踐: 目錄結構

在開始 AngularJS 應用以前,咱們將使用 bower 安裝客戶端方面的依賴。若是你尚未 Bower 先要安裝它。在命令提示行中將當前工做目錄切換至你應用的根目錄,而後依照下面的命令安裝依賴。

bower install angular angular-route angular-material --save

設置數據庫

在這個例子中,我將使用一個名字爲 customer-manager 的數據庫和一張名字爲 customers 的表。下面是數據庫的導出文件,你能夠依照這個快速開始。

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 以下所示:

(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 以下所示:

(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,以下所示:

<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 包含模塊初始化腳本和應用的路由配置,以下所示:

(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

<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 來啓動你的應用。

angular-app.png

構建 AngularJS 應用

爲了構建咱們的 Angular 應用,須要安裝 gulp-uglify, gulp-minify-cssgulp-usemin 依賴包。

npm install --save gulp-uglify gulp-minify-css gulp-usemin

打開你的 gulpfile.js 而且引入必要的模塊。

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');

若是構建目錄已經存在的話,清理一下它。

gulp.task('clean', function (callback) { 
  return destDir.dirAsync('.', { empty: true }); 
});

複製文件到構建目錄。咱們並不須要使用複製功能來複制 angular 應用的代碼,在下一部分中 usemin 將會爲咱們作這件事請:

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 塊:

<!-- 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 -->

構建任務以下所示:

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

接下來,初始化咱們的構建腳本,以下所示:

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-jetpack node 模塊進行文件操做。它提供了更靈活的文件操做。

複製 Electron Distribution

electron-prebuilt/dist 複製默認的 electron distribution 到咱們的 dist 目錄

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

function cleanupRuntime() { 
     return buildDir.removeAsync('resources/default_app'); 
}

建立 asar 包

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 替換成你本身的,更新產品的信息而後重命名應用。

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 腳本

!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 的函數,以下所示:

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 任務能夠獲取到而後輸出它:

function build() { 
    return init()
            .then(copyElectron) 
            .then(cleanupRuntime) 
            .then(createAsar) 
            .then(updateResources) 
            .then(rename) 
            .then(createInstaller); 
}
module.exports = { build: build };

接着,在 gulpfile.js 中建立 gulp 任務來執行這個構建腳本:

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 建立,包括聊天應用、數據庫管理器、地圖設計器、協做設計工具和手機原型等。

下面是 Github Electron 的一些有用的資源:

相關文章
相關標籤/搜索