使用 AngularJS 和 Electron 构建桌面应用
Electron 丰富的原生 API 使我们能够在页面中直接使用 javascript 获取原生的内容。 这个教程向我们展示了如何使用 Angular 和 Electron 构建一个桌面应用。下面是本教程的所有步骤:
创建你的 Electron 应用起初,如果你的系统中还没有安装 Node,你需要先安装它。我们应用的结构如下所示: 这个项目中有两个 package.json 文件。
package.json 的格式和 Node 模块中的完全一致。你应用的启动脚本(的路径)需要在 app/package.json 中的
{ name: "AngularElectron",version: "0.0.0",main: "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。你可以这样创建 <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) 模块来读取 运行应用一旦你创建好了项目结构、 如果你已经在系统中全局安装了 electron app
在这里, "node_modules/.bin/electron" "./app"
尽管你可以这样来运行应用,但是我还是建议你在 // 获取依赖 var gulp = require('gulp'),childProcess = require('child_process'),electron = require('electron-prebuilt'); // 创建 gulp 任务 gulp.task('run',function () { childProcess.spawn(electron,['./app'],{ stdio: 'inherit' }); });
运行你的 gulp 任务: 配置 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 任务。按下 你将从
{ "version": "0.1.0","command": "gulp","isShellCommand": true,"args": [ "--no-color" ],"tasks": [ { "taskName": "run","args": [],"isBuildCommand": true } ] }
根部分声明命令为 现在,如果你按下 你可以在这里阅读到更多关于 visual studio code 任务的信息。 调试 Electron 应用打开调试面板点击配置按钮就会在 我们不需要启动 app.js 的配置,所以移除它。 现在,你的 { "version": "0.1.0",// 配置列表。添加新的配置或更改已存在的配置。 // 仅支持 "node" 和 "mono",可以改变 "type" 来进行切换。 "configurations": [ { "name": "Attach","type": "node",// TCP/IP 地址. 默认是 "localhost" "address": "localhost",// 建立连接的端口. "port": 5858,"sourceMaps": false } ] }
按照下面所示更改之前创建的 gulp gulp.task('run',['--debug=5858','./app'],{ stdio: 'inherit' }); });
在调试面板中选择 “Attach” 配置项,点击开始(run)或者按下 F5。稍等片刻后你应该就能在上部看到调试命令面板。 创建 AngularJS 应用第一次接触 AngularJS?浏览官方网站或一些 Scotch Angular 教程。 这一部分会讲解如何使用 angularjs 和 MySQL 数据库创建一个顾客管理(Customer Manager)应用。这个应用的目的不是为了强调 AngularJS 的核心概念,而是展示如何在 GiHub 的 Electron 中同时使用 angularjs 和 NodeJS 以及 mysql 。 我们的顾客管理应用正如下面这样简单:
项目结构我们的应用在 app 文件夹下,目录结构如下所示: 主页是 这里我更喜欢按照功能来组织脚本文件。每个功能都有它自己的文件夹,文件夹中有模板和控制器。获取更多关于目录结构的信息,可以阅读 AngularJS 最佳实践: 目录结构 在开始 AngularJS 应用之前,我们将使用 bower 安装客户端方面的依赖。如果你还没有 Bower 先要安装它。在命令提示行中将当前工作目录切换至你应用的根目录,然后依照下面的命令安装依赖。
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 来直接从数据库中获取数据。使用
在命令提示行中切换工作目录至 app 文件夹然后按照下面所示安装模块:
(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],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],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,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,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],res) { if (err) deferred.reject(err); deferred.resolve(res); }); return deferred.promise; } } })();
控制器 & 模板app/scripts/customer/customerController 中的
<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 包含模块初始化脚本和应用的路由配置,如下所示:
<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 的话,使用 构建 AngularJS 应用为了构建我们的 Angular 应用,需要安装
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('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 中的引用用优化版代码替换掉 。
<!-- 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 -->
构建任务如下所示:
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(); }
这里我们使用 复制 Electron Distribution从
function cleanupRuntime() { return buildDir.removeAsync('resources/default_app'); }
创建 asar 包
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 "SoftwareMicrosoftWindowsCurrentVersionUninstall${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=$PLUGINSDIRbanner.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 $PLUGINSDIRbanner.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" '"$INSTDIRicon.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}" "" "$INSTDIRicon.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
在
function build() { return init() .then(copyElectron) .then(cleanupRuntime) .then(createAsar) .then(updateResources) .then(rename) .then(createInstaller); } module.exports = { build: build };
接着,在
gulp build-electron
你最终的 electron 应用应该在 总结Electron 不仅仅是一个支持打包 web 应用成为桌面应用的原生 web view。它现在包含 app 的自动升级、Windows 安装包、崩溃报告、通知和一些其它有用的原生 app 功能——所有的这些都通过 javascript API 调用。 到目前为止,很大范围的应用使用 electron 创建,包括聊天应用、数据库管理器、地图设计器、协作设计工具和手机原型等。 下面是 Github Electron 的一些有用的资源:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |