使用 NW.js 跨平台開發

越來越多的應用開始藉助於 Web 技術。比如,Brackets、Peppermint 和Pinegrow 都是基於 HTML 、JavaScript 和 CSS 實現的程序編輯器。這樣不但可以使用熟悉的工具,應用還是天然跨平台的。在本教程中,我們為你展示如何使用 NW.js 開發一個程序編輯器,可以跨 Windows Mac OS X 和 Linux 使用。

NW.js 介紹及下載

NW.js 就是原來的 Node Webkit,即融合了 Node.js 和 Webkit HTML 渲染器來運行本地應用。新版的 NW.js 基於 io.js ,後者比起 Node.js 採用了更新版的 JavaScript 引擎 V8,對 ES6 支持得更好。既然 io.js 對最新的 Node.js 是百分百兼容的,因此所有是使用 Node.js 的類庫和程序也可以使用在 io.js 中。

行動起來,下載三個不同 OS 平台的 NW.js,也可以下載你想運行應用平台對應的版本。我將在我的 MacBook Air 上進行開發,但是你可以使用你想想用的系統。項目是 Fun Editor:一個易用的單文件代碼編輯器。傳承 Linux 的思想:每次只做好一件事!

安裝組件

要開始,系統必須安裝 node 或者 io.js。一旦裝好了 node 或者 io.js,你的系統中就包含了 npm 命令。這個命令可以用來安裝其他 JavaScript 類庫。

第一個類庫就是 Bower。在命令行中輸入如下命令:

npm install -g bower

在某些系統你,你可能需要使用 sudo 命令,以超級用戶的身份來運行 npm 命令。

bower 命令作為絕大多數 Web 類庫的包管理器,為 Web 項目安裝常用類庫提供了一種簡單的方式。

為了進行 DOM 操作,編輯器採用了 Zepto.js 來代替 jQuery。既然 Fun Editor 就只是使用類庫來操作 DOM,小巧的 Zepto 優勢就體現出來了。

為項目創建一個新的目錄,在命令行的新目錄中,輸入如下命令:

bower install lessbower install zepto

在目錄中多了一個 bower_components 的子目錄,在這個目錄中,bower 安裝了 less 和 zepto 這兩個類庫。這比起找到它們網站在下載容易多了。

Ace JavaScript 類庫是這個編輯器應用的基礎。它是一個靈活易用的編輯器類庫,基於 JavaScript,為 Web 站點設計而開發。可以在命令行中輸入如下命令安裝:

git clone git://github.com/ajaxorg/ace.git

現在又多了一個新目錄 ace。這個類庫所需要的資源都在這個目錄中。需要把這個類庫編譯成合併的模式來加快它的載入速度。在命令行中,輸入:

cd acenpm installnode Makefile.dryice.js full -m --target ./build

運行這些命令,進入 ace 目錄,安裝所有 ace 編輯器依賴的類庫,然後在子目錄 build 中生成壓縮有的 ace 類庫文件。

這個項目將使用 node-watch 類庫來監聽文件變化。文件變化後這個類庫會激活一個回調函數。為了把這個模塊安裝到項目中,項目首先必須是一個 node 包目錄。因此在命令行的根目錄中,輸入:

npm init

npm 程序會詢問幾個關於項目的問題。你一一作答之後,就會在目錄中生成一個 package.json 文件。npm 命令會把所有用到的類庫名和版本保存在文件中。這樣有個好處,項目到別人手裡可以很快搭建起一樣的工作環境。

輸入如下命令,安裝 node-watch 類庫:

npm install node-watch --save

安裝完成,增加了一個 node_modules 目錄,node-watch 就放在裡面。—save標識是為了告訴 npm 將項目隊 node-watch 的依賴寫入到 package.json 中。

最後一定要安裝的就是 Emmet。沒有它就沒有這個代碼編輯器。從 GitHub 上獲取 Emmet 的源碼,保存到 js 目錄的 emmet.js 文件中。

好了,現在所有組件都已經就位,接下來就是把它們組合到一起!

組裝

對於 NW.js 項目來說第一要素來說就是 project 文件。但不幸的是與 node 的項目文件是重名的。既然在開發過程中 node 項目文件不是必須的,可以把它複製到別的地方去,直到需要在改回來。在命令行中運行:

mv package.json node.package.jsontouch package.json

命令把原始的 package.json 文件變成 node.package.json 文件,並創建了一個內容為空的 package.json。在這個文件中,輸入:

{ "description": "A very small code editor.", "main": "main.html", "name": "Fun Editor", "version": "1.0", "window": { "height": 600, "width": 650, "show": false, "title": "Fun Editor", "toolbar": false, "icon": "icon.png" }}

這個文件會告訴 NW.js 如何啟動這個程序。每個欄位的作用如下:

description

簡短描述應用是什麼。

main

入口 HTML 文件。裡面包含了應用入口頁面所需要的全部 HTML。

name

應用名。

version

應用的版本號。

window

一個 json 對象用來描述這個程序的用戶界面。包含以下屬性:

height

程序啟動時窗口的高度。

width

程序啟動時窗口的寬度。

show

Boolean 值,設置 NW.js 在載入時是否顯示主窗口。我設置的是 true,如果要設置為 false,必須確保之後在某個地方激活窗口。

title

默認程序名。在程序載入好之後顯示。除非應用中有代碼修改了它。

toolbar

Boolean 值,設置 NW.js 是否包含一個工具欄。因為 FunEditor 是一個極簡的編輯器,所以這裡設置為 false。但是,如果需要使用 Dev Tools 來調試編輯器,可以將其設置為 true,這樣工具欄上就有一個用來打開 Dev Tools 的圖標。

icon

相對路徑指向作為應用圖標的圖片文件,相對於項目的根目錄。

在配置中有一個 main.html 文件,自然就是下一個要加入的文件。在項目根目錄,創建 main.html ,添加如下代碼:

<!DOCTYPE html><html> <head> <title>Fun Editor</title> <script type="text/javascript" src="bower_components/zepto/zepto.min.js"></script> <script type="text/javascript" src="js/emmet.js"></script> <script type="text/javascript" src="ace/build/src-min-noconflict/ace.js"></script> <script type="text/javascript" src="ace/build/src-min-noconflict/ext-emmet.js"></script> <script type="text/javascript" src="ace/build/src-min-noconflict/ext-language_tools.js"></script> <script type="text/javascript" src="ace/build/src-min-noconflict/ext-spellcheck.js"></script> <script type="text/javascript" src="ace/build/src-min-noconflict/keybinding-vim.js"></script> <script type="text/javascript" src="js/FunEditor.js"></script> <link rel="stylesheet/less" type="text/css" href="less/default.less"> <script type="text/javascript" src="bower_components/less/dist/less.min.js"></script> </head> <body> <div id="editor"></div> <div class="info"> <span id="editMode" class="statuslineitem">Normal</span> <span class="title statuslineitem"> <span class="arrow-right-editMode statuslineitem"></span> <span id="title">No file</span> </span> <span class="mode statuslineitem"> <span class="arrow-right-title statuslineitem"></span> <span id="mode">JavaScript</span> </span> <span class="linenum statuslineitem"> <span class="arrow-right-mode statuslineitem"></span> <span id="linenum">1</span> </span> <span class="colnum statuslineitem"> <span class="arrow-right-linenum statuslineitem"></span> <span id="colnum">1</span> </span> <span class="arrow-right-colnum statuslineitem"></span> </div> <input style="display:none;" id="openFile" type="file" /> <input style="display:none;" id="saveFile" type="file" /> </body></html>

這是應用的主入口,就是一個簡單的頁面,引入了多個 JavaScript 文件,一個定義所有樣式的 Less 文件,一個 id 為 editor 的 div 用作編輯器,一個 id 為info 的 div ,包含數個 span 元素作為狀態欄。狀態欄的樣式是從我之前的一個教程 Getting Spiffy With Powerline 移植過來的。只是長得像 Powerline,但並沒有使用它。在底部還有兩個隱藏的輸入框,給 NW.js 用來打開去讀文件和保存文件的對話框。

最佳實踐告訴我們 JavaScript 必須放在頁面的底部,但既然頁面窗口的顯示是由程序來控制的,關係就不大了。

要讓 HTML 可以看,樣式是下一個需要添加的。在項目目錄新建一個 less 文件夾,添加 default.less 文件,添加如下樣式:

@StatusLineEditMode: rgb(98, 105, 255);@StatusLineTitle: rgb(98, 247, 255);@StatusLineMode: rgb(98, 255, 149);@StatusLineLineNum: rgb(224, 217, 87);@StatusLineColNum: rgb(87, 212, 224);@StatusLineBackground: white;body { margin: 0; padding: 0; margin-top: 0; overflow: hidden;}#editor { position: absolute; top: 0px; bottom: 25px; left: 0; right: 0; margin: 0px; padding: 0px;}.info { position: absolute; display: inline; bottom: 0px; font-family: monospace; white-space: nowrap; bottom: 0; background: @StatusLineBackground; padding: 0px; height: 25px; left: 0; width: 100%;}.statuslineitem { height: 25px; line-height: 25px; text-align: center; vertical-align: middle; margin: 0px; padding-top: 0px; padding-bottom: 0px; padding-right: 3px; padding-left: 0px; border: 0px; float: left;}.arrow-right-editMode { width: 0; height: 0; border-top: 13px solid transparent; border-bottom: 13px solid transparent; border-left: 13px solid @StatusLineEditMode; margin: 0px; padding: 0px; margin-left: 0px;}.arrow-right-title { width: 0; height: 0; border-top: 13px solid transparent; border-bottom: 13px solid transparent; border-left: 13px solid @StatusLineTitle; margin: 0px; padding: 0px; margin-left: 0px;}.arrow-right-mode { width: 0; height: 0; border-top: 13px solid transparent; border-bottom: 13px solid transparent; border-left: 13px solid @StatusLineMode; margin: 0px; padding: 0px; margin-left: 0px;}.arrow-right-linenum { width: 0; height: 0; border-top: 13px solid transparent; border-bottom: 13px solid transparent; border-left: 13px solid @StatusLineLineNum; margin: 0px; padding: 0px; margin-left: 0px;}.arrow-right-colnum { width: 0; height: 0; border-top: 13px solid transparent; border-bottom: 13px solid transparent; border-left: 13px solid @StatusLineColNum; margin: 0px; padding: 0px;}#editMode { background: @StatusLineEditMode; padding-left: 5px;}#title { background: @StatusLineTitle; padding-left: 5px;}#mode { background: @StatusLineMode; padding-left: 5px;}#linenum { background: @StatusLineLineNum; padding-left: 5px;}#colnum { background: @StatusLineColNum; padding-left: 5px;}#editMode { background: @StatusLineEditMode; padding-left: 5px;}.title { background: @StatusLineTitle;}.mode { background: @StatusLineMode;}.linenum { background: @StatusLineLineNum;}.colnum { background: @StatusLineColNum;}

這段樣式讓 editor div 絕對定位,佔滿整個瀏覽器窗口,處理在底部留出 25px 像素的間隙給狀態欄。

在這個文件的最上面,發現定義了幾個 Less 變數。既然同樣的顏色值會用在不同的地方(例如顯示行號的 span,包括後面顯示「箭頭」的 span),所以我把他們定義成了一個 Less 變數。這樣寫便於就修改一個地方,每個地方都生效。Stylus 或者 SASS 都可以,只是 Less 允許在代碼中動態地對值進行修改,便於隨後定製主題的修改。

現在輪到了主程序文件。在 js 目錄下,新建 FunEditor.js 添加如下代碼:

//// Program: Fun Editor//// Description: This is a basic editor built using NW.js.// This is more of a project to learn how to use// NW.js, but I am finding that I really like// the editor!//// Author: Richard Guay (raguay@customct.com)// License: MIT////// Class: FunEditor//// Description: This class contains the information and functionality// of the Fun Editor. There should be only one instance// of this class per editor.//// Class Variables:// editor Keeps the Editor object// menu keeps the menu object for the pop-up// menu.// menuEdit Edit menu for the popup menu// menuEditMain The Edit menu for the main menu// menuFile File menu for the popup menu// menuFileMain File menu for the main menu// nativeMenuBar The menu bar for OSX Main menu// hasWriteAccess boolean for if the file is// writable or not.// origFileName Last file name opened.// watch The node-watch library object.// osenv The osenv library object.// gui The NW.js gui library// object.// fs The fs library object.// clipboard The clipboard library object.//function FunEditor() {}FunEditor.prototype.filesOpened = 0;FunEditor.prototype.saving = false;FunEditor.prototype.editor = null;FunEditor.prototype.menu = null;FunEditor.prototype.menuEdit = null;FunEditor.prototype.menuFile = null;FunEditor.prototype.menuEditMain = null;FunEditor.prototype.menuFileMain = null;FunEditor.prototype.nativeMenuBar = null;FunEditor.prototype.fileEntry = null;FunEditor.prototype.hasWriteAccess = false;FunEditor.prototype.origFileName = "";FunEditor.prototype.theme = {};FunEditor.prototype.lastCursor = { line: 0, col: 0 };FunEditor.prototype.watch = require("node-watch");FunEditor.prototype.gui = require("nw.gui");FunEditor.prototype.fs = require("fs");FunEditor.prototype.osenv = require("osenv");FunEditor.prototype.os = require("os");var FE = new FunEditor();FunEditor.prototype.clipboard = FE.gui.Clipboard.get();FunEditor.prototype.win = FE.gui.Window.get();//// Function: handleDocumentChange//// Description: This function is called whenever the document// is changed. This function will get the title set,// remove the old document name from the window// list, set the syntax highlighting based on the// file extension, and update the status line.//// Inputs:// title Title of the new document//FunEditor.prototype.handleDocumentChange = function(title) { // // Setup the default syntax highlighting mode. // var mode = "ace/mode/javascript"; var modeName = "JavaScript"; // // Set the new file name. If the title is blank, reflect that for // setting a new one later. // this.fileEntry = title; // // Set the syntax highlighting based on the file ending. // if (title) { // // Set up file watching with node-watch. The file being edited is // watched for changes. If the file changes, then reload the file // into the editor. // this.watch(this.fileEntry, function() { // // The file changed. Load it into the editor. Needs implemented: // ask the user if they want the file to be reloaded. // if(! FE.saving ) { FE.readFileIntoEditor(FE.fileEntry); } }); // // If there is a title, then setup everything by that title. // The title will be the file name. // if(this.os.platform() == "win32") { title = title.match(/[^\]+$/)[0]; } else { title = title.match(/[^/]+$/)[0]; } if (this.origFileName.indexOf(title) == -1) { // // Remove whatever the old file was loaded and put in // the new file. // this.origFileName = title; } // // Check for OS permissions for writing. NOTE: Not implemented. // this.hasWriteAccess = true; // // Set the document"s title to the file name. // document.getElementById("title").innerHTML = title; document.title = title; // // Set the syntax highlighting mode based on extension of the file. // if (title.match(/.js$/)) { mode = "ace/mode/javascript"; modeName = "JavaScript"; } else if (title.match(/.html$/)) { mode = "ace/mode/html"; modeName = "HTML"; } else if (title.match(/.css$/)) { mode = "ace/mode/css"; modeName = "CSS"; } else if (title.match(/.less$/)) { mode = "ace/mode/less"; modeName = "LESS"; } else if (title.match(/.md$/)) { mode = "ace/mode/markdown"; modeName = "Markdown"; } else if (title.match(/.ft$/)) { mode = "ace/mode/markdown"; modeName = "FoldingText"; } else if (title.match(/.markdown$/)) { mode = "ace/mode/markdown"; modeName = "Markdown"; } else if (title.match(/.php$/)) { mode = "ace/mode/php"; modeName = "PHP"; } } else { // // Setting an empty document. Leave syntax highlighting as the last // file. // document.getElementById("title").innerHTML = "[no document loaded]"; this.origFileName = ""; } // // Tell the Editor and setup the status bar with the syntax highlight mode. // this.editor.getSession().setMode(mode); document.getElementById("mode").innerHTML = modeName;};//// Function: setCursorLast//// Description: Set the cursor to the last stored state.//FunEditor.prototype.setCursorLast = function() { this.editor.moveCursorTo(this.lastCursor.line, this.lastCursor.col);}//// Function: newFile//// Description: This function is called to set the global// variables properly for a new empty file.//// Inputs://FunEditor.prototype.newFile = function() { this.fileEntry = null; this.hasWriteAccess = false; this.handleDocumentChange(null); this.editor.setValue("");};//// Function: readFileIntoEditor//// Description: This function handles the reading of the file// contents into the editor. If reading fails, a// log entry is created.//// Inputs:// theFileEntry The path and file name//FunEditor.prototype.readFileIntoEditor = function(theFileEntry) { this.fs.readFile(theFileEntry, function(err, data) { if (err) { console.log("Error Reading file."); } // // Set the file properties. // FE.handleDocumentChange(theFileEntry); // // Set the file contents. // FE.editor.setValue(String(data)); // // Remove the selection. // FE.editor.session.selection.clearSelection(); // // Put the cursor to the last know position. // FE.setCursorLast(); });};//// Function: writeEditorToFile//// Description: This function takes what is in the editor// and writes it out to the file.//// Inputs:// theFileEntry File to write the contents to.//FunEditor.prototype.writeEditorToFile = function(theFileEntry) { var str = this.editor.getValue(); this.fs.writeFile(theFileEntry, str, function(err) { if (err) { console.log("Error Writing file."); return; } });};//// Function: copyFunction//// Description: This function takes the current selection and copies it// to the clipboard.//FunEditor.prototype.copyFunction = function() { this.clipboard.set(this.editor.getCopyText());};//// Function: cutFunction//// Description: This function cuts out the current selection and copies it// to the clipboard.//FunEditor.prototype.cutFunction = function() { this.clipboard.set(this.editor.getCopyText()); this.editor.session.replace(this.editor.selection.getRange(),"");};//// Function: pasteFunction//// Description: This function takes the clipboard and pastes it to the// current location.//FunEditor.prototype.pasteFunction = function() { this.editor.session.replace(this.editor.selection.getRange(), this.clipboard.get());};//// Function: openFile//// Description: This function opens the select a file dialog for opening// into the editor.//FunEditor.prototype.openFile = function() { $("#openFile").trigger("click");};//// Function: saveFile//// Description: This function saves to the currently open file or// opens the save file dialog.//FunEditor.prototype.saveFile = function() { this.saving = true; if (this.fileEntry && this.hasWriteAccess) { this.writeEditorToFile(this.fileEntry); } else { $("#saveFile").trigger("click"); } this.saving = false;};//// Function: initMenus//// Description: This function setups the right click menu and system used// in the editor.//// Inputs://FunEditor.prototype.initMenus = function() { this.menu = new this.gui.Menu(); this.menuFile = new this.gui.Menu(); this.menuEdit = new this.gui.Menu(); this.menuFile.append(new this.gui.MenuItem({ label: "New", click: function() { FE.newFile(); } })); this.menuFile.append(new this.gui.MenuItem({ label: "Open", click: function() { FE.openFile(); } })); this.menuFile.append(new this.gui.MenuItem({ label: "Save", click: function() { FE.saveFile(); } })); this.menuEdit.append(new this.gui.MenuItem({ label: "Copy", click: function() { FE.copyFunction(); } })); this.menuEdit.append(new this.gui.MenuItem({ label: "Cut", click: function() { FE.cutFunction(); } })); this.menuEdit.append(new this.gui.MenuItem({ label: "Paste", click: function() { FE.pasteFunction(); } })); this.menuFileMain = new this.gui.Menu(); this.menuEditMain = new this.gui.Menu(); this.menuFileMain.append(new this.gui.MenuItem({ label: "New", click: function() { FE.newFile(); } })); this.menuFileMain.append(new this.gui.MenuItem({ label: "Open", click: function() { FE.openFile(); } })); this.menuFileMain.append(new this.gui.MenuItem({ label: "Save", click: function() { FE.saveFile(); } })); this.menuEditMain.append(new this.gui.MenuItem({ label: "Copy", click: function() { FE.copyFunction(); } })); this.menuEditMain.append(new this.gui.MenuItem({ label: "Cut", click: function() { FE.cutFunction(); } })); this.menuEditMain.append(new this.gui.MenuItem({ label: "Paste", click: function() { FE.pasteFunction(); } })); this.menu.append(new this.gui.MenuItem({ label: "File", submenu: FE.menuFile })); this.menu.append(new this.gui.MenuItem({ label: "Edit", submenu: FE.menuEdit })); // // Create the main Mac menu also. // this.nativeMenuBar = new this.gui.Menu({ type: "menubar" }); if(this.os.platform() == "darwin") { this.nativeMenuBar.createMacBuiltin("Fun Editor", { hideEdit: true, hideWindow: true }); } this.nativeMenuBar.append(new this.gui.MenuItem({ label: "File", submenu: FE.menuFileMain })); this.nativeMenuBar.append(new this.gui.MenuItem({ label: "Edit", submenu: FE.menuEditMain })); this.win.menu = this.nativeMenuBar; // // Add the menu to the contextmenu event listener. // document.getElementById("editor").addEventListener("contextmenu", function(ev) { ev.preventDefault(); FE.menu.popup(ev.x, ev.y); return false; } );};//// Function: onChosenFileToOpen//// Description: This function is called whenever a open// file dialog is closed with a file selection.// This is an automatically made function in// NW.js that needs to be set by your app.//// Inputs:// theFileEntry The path to the file selected.//onChosenFileToOpen = function(theFileEntry) { FE.readFileIntoEditor(theFileEntry);};//// Function: onChosenFileToSave//// Description: When a file is selected to save into, this// function is called. It is originally set by// NW.js.//// Inputs:// theFileEntry The path to the file selected.//onChosenFileToSave = function(theFileEntry) { FE.writeEditorToFile(theFileEntry);};//// Function: onload//// Description: This function is setup by NW.js to be// called when the page representing the application// is loaded. The application overrides this by// assigning it"s own function.//// Here, we initialize everything needed for the// Editor. It also loads the initial document for// the editor, any plugins, and theme.//// Inputs://onload = function() { // // Initialize the context menu. // FE.initMenus(); // // Set the change function for saveFile and openFile. // $("#saveFile").change(function(evt) { onChosenFileToSave($(this).val()); }); $("#openFile").change(function(evt) { onChosenFileToOpen($(this).val()); }); FE.editor = ace.edit("editor"); FE.editor.$blockScrolling = Infinity; FE.editor.setTheme("ace/theme/solarized_dark"); FE.editor.getSession().setMode("ace/mode/javascript"); FE.editor.setKeyboardHandler("ace/keyboard/vim"); FE.editor.setOption("enableEmmet", true); FE.editor.setOption("selectionStyle","text"); FE.editor.setOption("highlightActiveLine",true); FE.editor.setOption("cursorStyle","slim"); FE.editor.setOption("autoScrollEditorIntoView",true); FE.editor.setOption("tabSize",4); FE.editor.setOption("enableSnippets",true); FE.editor.setOption("spellcheck",true); FE.editor.setOption("wrap",true); FE.editor.setOption("enableBasicAutocompletion",true); FE.editor.setOption("enableLiveAutocompletion",false); FE.editor.commands.addCommand({ name: "myCopy", bindKey: {win: "Ctrl-C", mac: "Command-C"}, exec: function(editor) { FE.copyFunction(); }, readOnly: false }); FE.editor.commands.addCommand({ name: "myPaste", bindKey: {win: "Ctrl-V", mac: "Command-V"}, exec: function(editor) { FE.pasteFunction(); }, readOnly: false }); FE.editor.commands.addCommand({ name: "myCut", bindKey: {win: "Ctrl-X", mac: "Command-X"}, exec: function(editor) { FE.cutFunction(); }, readOnly: false }); FE.editor.commands.addCommand({ name: "mySave", bindKey: {win: "Ctrl-S", mac: "Command-S"}, exec: function(editor) { FE.saveFile(); }, readOnly: false }); // // Tie into the Vim mode save function. FE one took some digging to find! // FE.editor.state.cm.save = function() { FE.saveFile(); } // // Setup on events listeners. The first one is listen for cursor // movements to update the position in the file in the status line. // Next, setup the listener for Vim mode changing to update the // status line. Lastly, run function on window closing to remove // the current file from the open file list. // FE.editor.on("changeStatus", function() { // // Get the current cursor to set the row and column. // var cursor = FE.editor.selection.lead; document.getElementById("linenum").innerHTML = cursor.row + 1; document.getElementById("colnum").innerHTML = cursor.column + 1; // // Save a copy of the cursor location. // FE.lastCursor.line = cursor.row; FE.lastCursor.col = cursor.column; // // Get the text mode to set the Normal, Visual, or Insert vim // modes in the status line. // var mode = FE.editor.keyBinding.getStatusText(editor); if (mode == "") { document.getElementById("editMode").innerHTML = "Normal"; } else if (mode == "VISUAL") { document.getElementById("editMode").innerHTML = "Visual"; } else if (mode == "INSERT") { document.getElementById("editMode").innerHTML = "Insert"; } }); // // Capture the Ace editor"s copy and paste signals to get // or put to the system clipboard. // FE.editor.on("copy",function(text) { FE.clipboard.set(text); }); FE.editor.on("paste", function(e) { e.text = FE.clipboard.get(); }); // // Capture the window close and make // sure the file has been saved. // FE.win.on("close", function() { // // Make sure the contents are saved. // if (this.fileEntry && this.hasWriteAccess) { FE.saveFile(); } // // Close the program. // this.close(true); }); // // Setup for having a new empty file loaded. // FE.newFile(); onresize(); // // Show the program and set the focus (focus does not work!). // FE.win.show(); FE.win.focus();};//// Function: onresize//// Description: Another NW.js function that is called every time// the application is resized.//// Inputs://onresize = function() {};

在文件的一開始,定義了 FunEditor 對象以及其他一些變數。變數定義完之後,是三個 require 語句。這些語句首先是載入 node-watch 類庫監聽文件的變化,nw.gui 類庫可以用來與圖形的用戶界面進行交互,fs 類庫用來訪問文件系統,os 類庫則是用來與操作系統交互。gui、fs 和 os 都是 NW.js 程序的一部分,而 node-watch 則是使用 npm 下載的類庫。

通過 gui,FunEditor.clipboard 變數包含了一個 clipboard 對象,可以訪問系統剪切板。同理, FunEditor.win 變數包含 window 對象,即 NW.js 為當前應用創建的主窗口。

在這些類庫載入完,添加了一些全局變數之後,又增加了一些函數。FunEditor 開頭的函數是運行編輯器必須的,其他的函數則是 NW.js 所必須的。

下面描述每個函數的作用:

FunEditor.handleDocumentChange

這個函數基於載入的文檔載入具備正確高亮的編輯器。它還會把窗口標題和狀態欄設置成文檔的名稱。當設置窗口標題時,使用 FunEditor.os.platform()檢測用戶使用什麼系統,如果是 Windows,搜索文檔所在文件夾的操作符就有點區別,因為 Windows 系統使用的是 ,但 OSX 和 Linux 使用的是 /。

這個函數還使用 FunEditor.watch 來做文件變更的監聽。但給定的文件發生了變化,回調函數就會把文件讀到編輯器中。

FunEditor.setCursorLast

該函數把游標上一次記錄的位置。每次游標移動都會把它的位置記錄在狀態欄上。當文檔變化重新被載入時,可以用這個函數把放在最後一次記錄的位置上。

FunEditor.newFile

使用這個函數創建一個新的空文檔。

FunEditor.readFileIntoEditor

該函數利用 FunEditor 上引用的 fs 類庫從文件系統中讀取文件,加入到編輯器中。並會清除選中,將游標加入到最近的一次出現的位置。

FunEditor.writeEditorToFile

將編輯器目前的狀態通過 FuncEditor.fs 保存到文件系統中。

FunEditor.copyFunction

使用 NW.js 把當前選中的文本加入到系統剪切板中。

FunEditor.cutFunction

與 FunEditor.copyFunction 一致,只是該函數會刪除編輯器中被選中的文本。

FunEditor.pasteFunction

通過 NW.js 類庫從剪切板拷貝文本,粘貼到編輯器中目前游標的位置中。

FunEditor.openFile

通過出發隱藏文件輸入框的方式打開系統文件對話框,這個是 NW.js 的一部分。

FunEditor.saveFile

如果打開的文件有寫入許可權的話,這個函數會把編輯器的內容寫入到打開的文件中。否則,它將會彈出一個文件對話框,讓用戶選擇一個文件。這也是通過觸發隱藏的 saveFile 元素來實現的。

FunEditor.initMenus

為右鍵菜單和系統菜單創建圖形化菜單元素。這基於 FunEditor.gui 類庫。

Mac OS X 的主菜單還添加了一些更多的功能。在這部分代碼中, FunEditor.os類庫可以獲取到軟體運行的平台,如果是 Mac OS X ,為主菜單添額外的功能。

onChosenFileToOpen

NW.js 定義文件選中對話框關閉時的回調函數,這函數把文件載入到編輯器中,聽過 FunEditor.readFileIntoEditor 這個函數。

onChosenFileToSave

當文件對話框關閉獲取到選擇要存儲到的文件時,會調用這個函數。這函數接著通過 FunEditor.writeEditorToFile 函數把編輯器的內容存儲到指定的文件中。

Onload

這個 NW.js 函數會在 main.html 中所有的資源被載入到 NW.js 中後調用。它相當與 document.onload = function(){}; 或者 jQuery 的$(document).ready(function(){})。

這個函數按照既定的配置初始化編輯器。主題採用 solarized dark,鍵盤綁定為 vim 布局,高亮正在編輯的行,等等。編輯器複製、粘貼、剪切以及保存文件的快捷方式都按照 Windows 和 Mac 系統默認的方式進行設置。其他跨平台的問題 ace 編輯器會處理好。

當編輯器初始化好之後,綁定changeStatus、copy 、paste、save 等編輯器事件。這使得編輯器讓 ace editor 使用系統的剪切板,還可以在狀態欄中更新行列信息。

我還碰到了一個 vim 保存功能(:w)的問題,花了點時間,最終還是搞定了。Ace 從 Code Mirror 上借取了部分代碼來實現 Vim 鍵盤布局,可以工作,但並不是一個魯棒的方案。

Onresize

當窗口尺寸變化時,就會調用這個 NW.js 提供的方法。對於 FunEditor 來說,並不需要做什麼。

在不同的平台上運行

這些就是全部編輯器代碼。現在,讓它在各個平台上運行。如果你還沒下載 NW.js 針對每個平台的包,下載之,並根據下面的介紹操作。把項目文件夾的所有文件全部打包成一個zip文件,打包後,將文件名修改為 FunEditor.nw。

對於每一個平台,你都可以在命令中,在項目文件夾下運行一遍 nw 命令。不過,普通的安裝過程,並不會在系統路徑中添加可執行的文件。因此,針對不同的shell,你可以 alias 一個 nw 的可執行文件。在開發的時候,我在項目目錄中使用下面的命名來進行測試:

nw .

創建了 FunEditor.nw 之後,我使用:

nw FunEditor.nw

一旦測試通過,就可以開始為其他平台打包了。

Mac

一旦在 Mac 上安裝了 NW.js,在 Applications 目錄中可以找到 nwjs.app 文件。在進行壓縮應用的目錄中,你可以使用下面的命令運行程序:

/Applications/nwjs.app/Contents/MacOS/nwjs FunEditor.nw

為了製作一個可點擊運行的包,將 FunEditor.nw 更名為 app.nw。複製Applications/nwjs.app 到 /Applications/FunEditor.app, 在 Finder 中,右鍵點擊應用,選擇 Show Contents。將 app.nw 文件放入到Resources 目錄下,並將 icon 替換成隨便什麼你喜歡的。我在下載包中包含了一個圖標,記得保留圖標的文件名為 nw.icns。

為了讓菜單顯示正確的名字,你要修改 info.plist 文件。將它打開,將內容修改為:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>BuildMachineOSBuild</key> <string>12F45</string> <key>CFBundleDevelopmentRegion</key> <string>en</string> <key>CFBundleDisplayName</key> <string>FunEditor</string> <key>CFBundleExecutable</key> <string>nwjs</string> <key>CFBundleIconFile</key> <string>nw.icns</string> <key>CFBundleIdentifier</key> <string>io.nwjs.nw</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>FunEditor</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>1.0</string> <key>CFBundleVersion</key> <string>1.0</string> <key>DTSDKBuild</key> <string>12F37</string> <key>DTSDKName</key> <string>macosx10.8</string> <key>DTXcode</key> <string>0511</string> <key>DTXcodeBuild</key> <string>5B1008</string> <key>LSFileQuarantineEnabled</key> <false/> <key>LSMinimumSystemVersion</key> <string>10.6.0</string> <key>NSPrincipalClass</key> <string>NSApplication</string> <key>NSSupportsAutomaticGraphicsSwitching</key> <true/> <key>SCMRevision</key> <string>df30fb73b312044486237d93cf96f3606862f2a3</string></dict></plist>

這是我從 NW.js 中拿到的 info.plist 文件,移除了所有和啟動 nw 擴展名文件相關的配置信息,將版本號設置為 1.0,將名字換成 FunEditor:

在 Mac OS X 上的 FunEditor

搞定後,你就有一個可以點擊打開的 FunEditor 了。

Windows

在 Windows 上運行編輯器最簡單的方案就是使用批處理文件。在系統目錄中創建一個 FunEditor.bat 文件。添加如下內容:

<path to nw.exe>
w.exe <path to FunEditor>FunEdit.nw

Windows 7 上的 FunEditor

當你雙擊批處理文件時,FunEditor 就打開了。

Linux

下載 NW.js 程序,確保程序目錄包含在你的系統目錄中。創建一個腳本文件,通過的程序調用 NW.js 程序。在你的系統目中添加一個文件,通過下面的代碼調用 FunEditor:

#!/usr/bin/bashnwjs="<path to nwjs directory>/nwjs";fe="<path to the FunEditor.nw file>";$nwjs $fe

保存文件,添加可運行的許可權:

chmod a+x FunEditor

運行該命令,它就在 Linux 上跑起來了!

在 Arch Linux 上的 FunEditor

在我的 Arch Linux,FunEditor 用起來感覺不錯!

總結

通過這個項目,你學習到了如何利用 NW.js,創建一個可運行在 Windows、Linux 和 Mac OS X 的編輯器。這個編輯器還有很多提升的空間。你大可以按照自己的興趣將它提升成你夢想中的編輯器,在每個平台都使用它寫程序!

原文:http://code.tutsplus.com/tutorials/cross-platform-development-with-nwjs–cms-23281

外刊君推薦閱讀:

  1. NW.js

  2. StrongLoop | Creating Desktop Applications With node-webkit

  3. Introduction to HTML5 Desktop Apps With Node-Webkit

  4. 用node-webkit開發多平台的桌面客戶端_Alien的筆記

關注微博:前端外刊評論


推薦閱讀:

macbook貼膜後,合不上蓋,有縫隙是怎麼回事?設計缺陷?
Thunderbolt 能接的顯示器只有蘋果的么?
有沒有辦法讓iPad做Mac的外接顯示器?
MacBook Pro retina 外接顯示器如何放大窗口內容?
MacBook 為什麼不自帶滑鼠?

TAG:Nodejs | iojs | JavaScript | WebKit | Mac | Linux | MicrosoftWindows |