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

jopen 8年前發布 | 114K 次閱讀 Web框架 angularjs

原文: Creating Desktop Applications With AngularJS and GitHub Electron

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

Electron 豐富的原生 API 使我們能夠在頁面中直接使用 JavaScript 獲取原生的內容。

這個教程向我們展示了如何使用 Angular 和 Electron 構建一個桌面應用。下面是本教程的所有步驟:

  1. 創建一個簡單的 Electron 應用

    </li>

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

    </li>

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

    </li>

  4. 使用 Gulp 任務構建我們的應用,并生成安裝包

    </li> </ol>

    創建你的 Electron 應用

    起初,如果你的系統中還沒有安裝 Node,你需要先安裝它。我們應用的結構如下所示:

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

    • 開發使用項目根目錄下的 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.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 編輯器相結合,我們會在下一部分展示。

    // 獲取依賴
    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文件:

    { 
      "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應該如下所示:

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

    按照下面所示更改之前創建的 gulprun任務,這樣我們的 electron 將會采用調試模式運行,5858 端口也會被監聽。

    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 先要安裝它。在命令提示行中將當前工作目錄切換至你應用的根目錄,然后依照下面的命令安裝依賴。

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

    構建 AngularJS 應用

    為了構建我們的 Angular 應用,需要安裝gulp-uglify,gulp-minify-css和gulp-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-jetpacknode 模塊進行文件操作。它提供了更靈活的文件操作。

    復制 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目錄中,并且目錄結構應該和下面是相似的:

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

    總結

    Electron 不僅僅是一個支持打包 web 應用成為桌面應用的原生 web view。它現在包含 app 的自動升級、Windows 安裝包、崩潰報告、通知和一些其它有用的原生 app 功能——所有的這些都通過 JavaScript API 調用。

    到目前為止,很大范圍的應用使用 electron 創建,包括聊天應用、數據庫管理器、地圖設計器、協作設計工具和手機原型等。

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!