nw.js 调用驱动程序_使用NW.js创建照片发现应用程序(第2部分)

nw.js 调用驱动程序

NW.js (formerly known as Node Webkit) is a framework for creating cross-platform desktop applications using HTML, CSS, and JavaScript. This is part 2 of our tutorial on building a Photo Discovery App with NW.js.

NW.js (以前称为Node Webkit)是用于使用HTML,CSS和JavaScript创建跨平台桌面应用程序的框架。 这是我们使用NW.js构建Photo Discovery应用程序的教程的第2部分。

In the previous tutorial, we covered:

在上一教程中 ,我们介绍了:

  • Prerequisites

    先决条件
  • Getting your computer setup (like installing Node)

    进行计算机设置(例如安装Node)
  • Getting Started

    入门
  • Finding photos

    查找照片

In this tutorial, we're cover some more cool stuff and wrap-up with packaging the application into a native one!

在本教程中,我们将介绍一些更酷的东西,并通过将应用程序打包到一个本地应用程序中来总结!

以全屏模式显示照片 (Displaying a photo in full-view mode)

From the photos being displayed in grid mode, we want to allow a user to be able to click on a photo, and have that photo be displayed in full-view mode, where the entire app screen shows the photo (as much as it can whilst respecting the photo's aspect ratio).

通过以网格模式显示的照片,我们希望允许用户能够单击照片,并以全视图模式显示该照片,在该模式下,整个应用程序屏幕都会显示照片(同时尊重照片的长宽比)。

This feature will require a combination of HTML, CSS, and JavaScript. We will begin with the first action in the user journey - clicking on an image, and then work through to displaying the photo in full view mode.

此功能需要HTML,CSS和JavaScript的组合。 我们将从用户旅程中的第一个动作开始-单击图像,然后逐步以全屏模式显示照片。

First, we need to modify the "window.onload" function so that once the photos are in the DOM of the HTML, we can call a function to listen to when a photo div element is clicked on, and trigger another function. Alter the code of the window.onload function to look like this:

首先,我们需要修改“ window.onload”函数,以便一旦照片进入HTML的DOM中,我们就可以调用一个函数来监听单击div元素时的声音,并触发另一个函数。 更改window.onload函数的代码,如下所示:

// Runs when the browser has loaded the page
    window.onload = function () {
        bindSelectFolderClick(function (folderPath) {
            hideSelectFolderButton();
            findAllFiles(folderPath, function (err, files) {
                if (!err) {
                    findImageFiles(files, folderPath, function (imageFiles) {
                        imageFiles.forEach(function (file, index) {
                            addImageToPhotosArea(file);
                            if (index === imageFiles.length-1) {
                                bindClickingOnAllPhotos();
                            }
                        });
                    });
                }
            });
        });
    };

We check for when the last imageFile is added to the DOM, and then call a new function called bindClickingOnAllPhotos. Before the window.onload function is defined, add the following function:

我们检查何时将最后一个imageFile添加到DOM中,然后调用一个名为bindClickingOnAllPhotos的新函数。 在定义window.onload函数之前,添加以下函数:

function bindClickingOnAllPhotos () {
        var photos = document.querySelectorAll('.photo');
        for (var i=0;i<photos.length;i++) {
            var photo = photos[i];
            bindClickingOnAPhoto(photo);
        }
    }

This function finds all of the photo div elements, loops through them, and then calls another function that will listen for clicking on that individual photo (as you shouldn't define functions within a loop), a function called bindClickingOnAPhoto. Before the bindClickingOnAllPhotos function, add the following function:

此函数查找所有照片div元素,循环遍历它们,然后调用另一个函数,该函数将监听单击该单独的照片(因为您不应该在循环中定义函数),该函数称为bindClickingOnAPhoto 。 在bindClickingOnAllPhotos函数之前,添加以下函数:

function bindClickingOnAPhoto (photo) {
        photo.onclick = function () {
            console.log(this); // A placeholder until we load full view mode here
        };
    }

The bindClickingOnAPhoto function receives the photo div element from the bindClickingOnAllPhotos function, and attaches a function to execute on it (in this a simple console.log statement as a placeholder for now).

bindClickingOnAPhoto函数接收从照片div元素bindClickingOnAllPhotos功能,并连接到它执行功能(在这个简单的console.log语句作为预留位置)。

Next, we want to focus on displaying the clicked photo in full view mode. This feature requires a tiny bit of HTML, some CSS, and some JavaScript. Like the photo display feature before, we'll start with the HTML. In the index.html file, add the following snippet of HTML after the div element with the id attribute "photos":

接下来,我们要专注于以全屏模式显示单击的照片。 此功能需要一点HTML,一些CSS和一些JavaScript。 像以前的照片显示功能一样,我们将从HTML开始。 在index.html文件中,将以下HTML代码段添加到具有id属性“ photos”的div元素之后:

<div id="fullViewPhoto">
    <img />
</div>

This snippet of HTML will handle displaying the photo in full view mode. Next, we'll add some CSS. In the app.css file, add the following rule for the ".photo" element:

该HTML代码段将以全屏模式显示照片。 接下来,我们将添加一些CSS。 在app.css文件中,为“ .photo”元素添加以下规则:

cursor: pointer;

This will make sure that the cursor will turn to a hand when it appears over a photo in the grid view, indicating to the user that they can click on it. At the bottom of the CSS file, we'll add these rules for the #fullPhotoView element:

这样可以确保当光标出现在网格视图中的照片上方时,它会变成手形,向用户指示可以单击它。 在CSS文件的底部,我们将为#fullPhotoView元素添加以下规则:

#fullViewPhoto {
        text-align: center;
        position: fixed;
        top: 0px;
        left: 0px;
        width: 100%;
        height: 100%;
        background-color: #222;
        z-index: 2;
        display: none;
    }

Finally, we'll reuse the CSS rules for the .photo img element, and append the #fullViewPhoto img element to it, like so:

最后,我们将重用CSS规则.photo img元素,并添加#fullViewPhoto img元素它,就像这样:

.photo img, #fullViewPhoto img {
        max-width: 100%;
        max-height: 100%;
    }

This ensures that the photo scales up inside of the full photo view, whilst also respecting the photo's aspect ratio and avoiding distortion. Finally, we add some JavaScript to handle displaying the photo in full view mode. In the app.js file, add the following function above the bindClickingOnAPhoto function:

这样可以确保照片在整个照片视图中按比例放大,同时还应注意照片的长宽比并避免失真。 最后,我们添加一些JavaScript以处理以全屏模式显示照片。 在app.js文件中,在bindClickingOnAPhoto函数上方添加以下函数:

function displayPhotoInFullView (photo) {
        var filePath = photo.querySelector('img').src;
        var fileName = photo.querySelector('img').attributes[1].value;
        document.querySelector('#fullViewPhoto > img').src = filePath;
        document.querySelector('#fullViewPhoto > img').setAttribute('data-name', fileName);
        document.querySelector('#fullViewPhoto').style.display = 'block';
    }

This function will receive the photo element that is clicked on, extract the image tag's src and data-name attributes, insert them into the image tag that is inside of the #fullViewPhoto div element, and then alter the styling of that element to make it visible.

此函数将接收被单击的photo元素,提取图像标签的src和data-name属性,将它们插入#fullViewPhoto div元素内部的image标签,然后更改该元素的样式以使其可见。

We then need to modify the bindClickingOnAPhoto function so that it passes the photo to the new function, like this:

然后,我们需要修改bindClickingOnAPhoto函数,以便将照片传递给新函数,如下所示:

function bindClickingOnAPhoto (photo) {
        photo.onclick = function () {
            displayPhotoInFullView(photo);
        };
    }

With these changes saved and the app reloaded, we can now try to click on a photo displayed in the grid view, and see the photo be displayed in full view mode, just like in the screenshot below:

保存这些更改并重新加载应用程序之后,我们现在可以尝试单击在网格视图中显示的照片,然后以全视图模式查看该照片,就像下面的屏幕截图所示:

Step 10 Full View Photo

Nice, now we need to think about how users are going to get back to the grid view from there. We need to implement a toolbar containing a button to click on that will take the user back. In the index.html file, add the following snippet of HTML inside of the fullViewPhoto element, after the image tag:

很好,现在我们需要考虑用户将如何从那里回到网格视图。 我们需要实现一个工具栏,其中包含一个单击按钮,它将使用户返回。 在index.html文件中,在image标记之后,在fullViewPhoto元素内添加以下HTML片段:

<div id="toolbar">
    <div id="back" onclick="backToGridView();">Back</div>
</div>

This bit of HTML contains the toolbar, a button with the text "Back", and when it is clicked it will call a JavaScript function called "backToGridView", that will do what the name suggests. Next, we will add some CSS for the layout, styling, and user interactivity. At the bottom of the app.css file, add the following CSS:

HTML的这一部分包含工具栏,一个带有文本“ Back”的按钮,单击该按钮将调用一个名为“ backToGridView”JavaScript函数,该函数将按照其名称进行提示。 接下来,我们将为布局,样式和用户交互性添加一些CSS。 在app.css文件的底部,添加以下CSS:

#toolbar {
        width: 96%;
        z-index: 2;
        position: absolute;
        top: 0;
        padding: 2%;
        background: rgba(0,0,0,0.7);
        color: white;
        opacity: 0;
    }

    #toolbar:hover {
        opacity: 1;
    }

    #back {
        text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
        background: rgba(255,255,255,0.2);
        border: solid 2px white;
        border-radius: 2em;
        padding: 5px 15px;
        margin: 5px;
        float: right;
    }

    #back:hover {
        background: rgba(255,255,255,0.3);
        box-shadow: 0px 0px 20px rgba(255,255,255,0.6);
        cursor: pointer;
    }

For the last step, we will insert the backToGridView function into the app.js file, before the bindClickingOnAPhoto function:

对于最后一步,我们将在backToGridView函数之前将bindClickingOnAPhoto函数插入到app.js文件中:

function backToGridView () {
        document.querySelector('#fullViewPhoto').style.display = 'none';
    }

This bit of JavaScript simply changes the display attribute of the #fullViewPhoto div element, giving the visual impression of it being taken out of site, and revealing the grid view that has always been there, just not in direct view.

#fullViewPhoto JavaScript只是简单地更改了#fullViewPhoto div元素的显示属性,给人以离开现场的视觉印象,并揭示了一直存在的网格视图,只是不在直接视图中。

With these changes saved and the app reloaded, when we click on a photo and hover our cursor towards the top of the screen, we can see the toolbar appear, as shown in the screenshot below:

保存这些更改并重新加载应用程序后,当我们单击照片并将光标悬停在屏幕顶部时,我们可以看到出现了工具栏,如以下屏幕截图所示:

Step 11 Toolbar

We'll also notice that when you click on the back button, you are take back to the grid view, allowing users to navigate around. At this stage we've got the exploration and navigation parts of the app covered, but the next thing that we want to achieve is being able to apply Instagram-like filters to the photos, and saving those altered photos back to the computer.

我们还将注意到,当您单击“后退”按钮时,您将返回到网格视图,从而允许用户四处浏览。 在此阶段,我们已经覆盖了应用程序的探索和导航部分,但是我们接下来要实现的是能够将类似于Instagram的滤镜应用于照片,并将更改后的照片保存回计算机。

将视觉滤镜应用于照片 (Applying visual filters to the photos)

Now for the fun bit - making some Instagram-esque filters to apply to the photos. For this we're going to make use of a really good library called Caman. It's a browser-based library that allows you to alter different visual aspects of an image (brightness, hue, saturation, contrast, sepia tint, and others). It can enable all kinds of filter effects to be applied to photos, and helps us to not try and re-invent the wheel on such a requirement.

现在为您带来一些乐趣-制作一些类似Instagram的滤镜以应用于照片。 为此,我们将使用一个叫做Caman的非常好的库。 这是一个基于浏览器的库,可让您更改图像的不同视觉外观(亮度,色相,饱和度,对比度,棕褐色调等)。 它可以将各种滤镜效果应用到照片,并帮助我们不要根据这种要求尝试重新发明轮子。

We should grab a copy of the library first. Although it suggests either installing it via npm or bower, I'm going to suggest an alternative approach, and grab a copy of the minified file for distribution. I'll explain a bit later. First, make a new folder inside of the "lens" folder called "scripts", and then download a copy of the minified javascript file listed below to inside that folder:

我们应该首先获取该库的副本。 尽管它建议通过npm或bower安装它,但我将建议一种替代方法,并获取缩小文件的副本以进行分发。 我待会儿解释。 首先,在“ lens”文件夹中创建一个名为“ scripts”的新文件夹,然后将下面列出的缩小的javascript文件的副本下载到该文件夹​​中:

https://raw.githubusercontent.com/meltingice/CamanJS/master/dist/caman.full.min.js

The reason why I suggested this is for 2 reasons. The first is that installing it via npm requires some other system libraries to be installed beforehand, and that's a bit of overkill in this case (as we're going to be rendering the photo filter effects in the browser rather than the server side). The second reason is that if we had installed it via bower, then there's a chance that we could run into an issue getting the app to work on Windows. For some weird reason, Windows does not like file paths that are longer than 255 characters, and given the way that dependencies are nested in npm (as bower is an npm module), then we would possibly run into this issue, along with the fact that any files present in the node_modules folder would be loaded by NW.js, and for some large dependencies that can have a noticeable impact on performance.

我之所以建议这样做,有两个原因。 首先是通过npm安装它需要事先安装一些其他系统库,在这种情况下,这有点过头了(因为我们将在浏览器而不是服务器端渲染照片滤镜效果)。 第二个原因是,如果我们是通过Bower安装的,则可能会遇到使该应用程序在Windows上运行的问题。 由于某些奇怪的原因,Windows不喜欢长度超过255个字符的文件路径,并且考虑到依赖项嵌套在npm中的方式(因为bower是一个npm模块),那么我们很可能会遇到这个问题, NW.js会加载node_modules文件夹中存在的所有文件,并且对于某些较大的依赖项可能会对性能产生明显影响。

Now that we've created the script folder and downloaded the CamanJS file to it, let's start playing with the library so we can get a feel for it. In the index.html file, let's include the file in the head section, above the app.js script tag:

现在,我们已经创建了脚本文件夹并将CamanJS文件下载到其中,让我们开始使用该库,以便对其有所了解。 在index.html文件中,让我们将该文件包括在app.js脚本标签上方的头部中:

<script src="scripts/caman.full.js"></script>

Next, we turn our attention to the toolbar, the location for where the filter buttons will be. For now, we're just going to create a text button and implement a simple filter, so that we can verify that it all works as we would like. In the index.html file, alter the HTML inside of the "#fullViewPhoto" div element, so that it looks like this:

接下来,我们将注意力转向工具栏,即筛选器按钮所在的位置。 现在,我们只是要创建一个文本按钮并实现一个简单的过滤器,以便我们可以验证所有按钮是否都可以正常工作。 在index.html文件中,更改“ #fullViewPhoto” div元素内部HTML,使其如下所示:

<div id="fullViewPhoto">
    <img id="image"/>
    <div id="toolbar">
        <div id="applyFilter" class="button" onclick="applyFilter();">Apply filter</div>
        <div id="back" class="button" onclick="backToGridView();">Back</div>
    </div>
</div>

We've made a couple of changes. Firstly, we've added an id attribute to the image tag, so that CamanJS will be able to easily attach to it and apply the photo effects there. Secondly, we've modified the #back div element that was the back button, so that it now has a CSS class of "button", and finally, we've added a new div element for the "Apply filter" button, using the CSS class of "button" as well.

我们进行了一些更改。 首先,我们在图片标签中添加了id属性,以便CamanJS可以轻松地附加到该标签并在其中应用照片效果。 其次,我们修改了#back div元素(即后退按钮),使其现在具有CSS类“ button”,最后,我们为“ Apply filter”按钮添加了一个新的div元素,使用CSS类的“按钮”也是如此。

In the app.css file, we're going to make some alterations there to accommodate for the new button in the toolbar, and also to handle the way that CamanJS manipulates images using the HTML5 Canvas API. The first change to make is to append another CSS matching rule onto an existing rule for the canvas tag inside of the #fullViewPhoto div element, like so:

在app.css文件中,我们将在那里进行一些更改以适应工具栏中的新按钮,并处理CamanJS使用HTML5 Canvas API处理图像的方式。 首先要进行的更改是将另一个CSS匹配规则附加到#fullViewPhoto div元素内部的canvas标签的现有规则上,如下所示:

.photo img, #fullViewPhoto img, #fullViewPhoto canvas {
    max-width: 100%;
    max-height: 100%;
}

When CamanJS manipulates an image, it does it using the HTML5 Canvas API, and converts the img element into a canvas element, so we need to apply the same CSS layout rules for it.

CamanJS处理图像时,它使用HTML5 Canvas API进行处理,并将img元素转换为canvas元素,因此我们需要为其应用相同CSS布局规则。

Next, with the introduction of the .button class we need to copy most of the properties of the "#back" CSS rule over to a new rule for the ".button" class. Add the following CSS:

接下来,通过引入.button类,我们需要将“ #back” CSS规则的大多数属性复制到“ .button”类的新规则中。 添加以下CSS:

.button {
        text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
        background: rgba(255,255,255,0.2);
        border: solid 2px white;
        border-radius: 2em;
        padding: 5px 15px;
        margin: 5px;
        cursor: pointer;
    }

We can then shrink the CSS for the #back rule down to just this:

然后,我们可以将#back规则CSS缩减为:

#back {
        float: right;
    }

Above that CSS rule, we'd also like to add another rule for the #applyFilter div element:

在该CSS规则之上,我们还要为#applyFilter div元素添加另一条规则:

#applyFilter {
        float: left;
    }

Then, we remove the cursor:pointer property from the #back:hover CSS rule, as it is now handled by the button class, and is therefore redundant. Now for the JavaScript. We will need to add a new function called applyFilter, and modify the backToGridView function so that it will reset the modified DOM of the #fullViewPhoto div element back to how it was before any filters were applied.

然后,我们从#back:hover CSS规则中删除cursor:pointer属性,因为该规则现在由按钮类处理,因此是多余的。 现在使用JavaScript。 我们将需要添加一个名为applyFilter的新函数,并修改backToGridView函数,以便它将#fullViewPhoto div元素的修改后的DOM重置为应用任何过滤器之前的状态。

Before the backToGridView function in the app.js file, add the following function:

在app.js文件中的backToGridView函数之前,添加以下函数:

function applyFilter () {
        Caman('#image', function () {
            this.brightness(10);
          this.sepia(20);
          this.saturation(30);
          this.render();
        }); 
    }

This function (which is called when the apply filter button is clicked) will call CamanJS on the img element with the id attribute "image", apply some image aspect changes, and render the final result to the DOM. Next, we need to alter the backToGridView function to this:

此函数(单击“应用过滤器”按钮时将调用此函数)将使用id属性“ image”调用img元素上的CamanJS,应用某些图像纵横比更改,并将最终结果呈现给DOM。 接下来,我们需要将backToGridView函数更改为此:

function backToGridView () {
        var canvas  = document.querySelector('canvas');
        var image   = document.createElement('img');
        image.setAttribute('id','image');
        canvas.parentNode.removeChild(canvas);
        var fullViewPhoto = document.querySelector('#fullViewPhoto');
        fullViewPhoto.insertBefore(image, fullViewPhoto.firstChild);
        document.querySelector('#fullViewPhoto').style.display = 'none';
    }

The backToGridView function resets any changes made to the DOM when applying filters to the photo. If you save the changes to the JS file and reload the application, select a folder of photos, click on one, and then click on the "apply filter" button in the top left, then you should see something like this:

将滤镜应用于照片时, backToGridView函数将重置对DOM所做的任何更改。 如果将更改保存到JS文件并重新加载应用程序,请选择一个照片文件夹,单击一个,然后单击左上方的“应用过滤器”按钮,那么您应该会看到类似以下内容的内容:

Step 12 Apply Filter

This is where the value of the app comes through - being able to browse photos, apply filters to them, and then save those modified photos. Before we get onto saving the photos, we want to be able to apply other filter styles.

这就是应用程序的价值所在-能够浏览照片,对其应用滤镜,然后保存这些修改过的照片。 在保存照片之前,我们希望能够应用其他滤镜样式。

创建过滤器 (Creating the filters)

For the app, we're going to allow the user to apply a small number of filters to their photos. These filters will be displayed in the toolbar as images that represent a preview of what the filter's effect is on the original photo. When a user clicks on an image of a filter effect, it is applied to the photo.

对于该应用程序,我们将允许用户对其照片应用少量滤镜。 这些滤镜将在工具栏中显示为图像,这些图像代表滤镜对原始照片的效果的预览。 当用户单击具有滤镜效果的图像时,该图像将应用于照片。

We're going to need to create a folder called "images" inside of the Lens folder, and then inside of the new images folder, we need to save some PNG images inside of it for the filter effect buttons. Download the images listed below:

我们将需要在Lens文件夹内创建一个名为“ images”的文件夹,然后在新的images文件夹内,我们需要在其中保存一些PNG图像作为滤镜效果按钮。 下载下面列出的图像:

https://raw.githubusercontent.com/paulbjensen/lens-photo-app/master/images/original.png

https://raw.githubusercontent.com/paulbjensen/lens-photo-app/master/images/grayscale.png

https://raw.githubusercontent.com/paulbjensen/lens-photo-app/master/images/sepia.png

https://raw.githubusercontent.com/paulbjensen/lens-photo-app/master/images/sunburst.png

https://raw.githubusercontent.com/paulbjensen/lens-photo-app/master/images/port.png

Once those files are saved in the images folder, we can move onto modifying the HTML to incorporate the new buttons. Inside the #toolbar div element, we'll remove the #applyFilter div element, and replace it with the following HTML:

将这些文件保存在images文件夹中后,我们可以继续修改HTML以合并新按钮。 在#toolbar div元素内,我们将删除#applyFilter div元素,并将其替换为以下HTML:

<div id="filterLabel">Filters</div>
<div class="filter" onclick="applyFilter('original');" title="original">
    <img src="images/original.png" alt="original" />
</div>
<div class="filter" onclick="applyFilter('grayscale');" title="grayscale">
    <img src="images/grayscale.png" alt="grayscale"/>
</div>
<div class="filter" onclick="applyFilter('sepia');" title="sepia">
    <img src="images/sepia.png" alt="sepia"/>
</div>
<div class="filter" onclick="applyFilter('sunburst');" title="sunburst">
    <img src="images/sunburst.png" alt="sunburst"/>
</div>
<div class="filter" onclick="applyFilter('port');" title="port">
    <img src="images/port.png" alt="port"/>
</div>

The toolbar contains a set of div element with images inside representing the various photo filters, and when each div element is clicked on, it will call the applyFilter function, along with the name of the filter effect that we want to apply. Next, we'll make some layout and styling changes in the CSS file. First, remove the now redundant CSS rule for the #applyFilter div element, and insert the following CSS in its place:

工具栏包含一组div元素,其中的图像表示各种照片滤镜,单击每个div元素时,它将调用applyFilter函数以及我们要应用的滤镜效果的名称。 接下来,我们将在CSS文件中进行一些布局和样式更改。 首先,删除#applyFilter div元素现在多余CSS规则,并在其位置插入以下CSS:

#filterLabel {
        padding: 0.5em;
        float: left;
    }

    .filter img {
        float: left;
        width: 45px;
        height: 30px;
        cursor: pointer;
        border: solid 1px white;
        margin: 0em 0.25em;
    }

The CSS rules above help to position the filter label and the filter effect images in the position that we want. Finally, we change the "#back:hover" CSS rule to this instead:

上面CSS规则有助于将滤镜标签和滤镜效果图像定位在我们想要的位置。 最后,我们改为将“ #back:hover” CSS规则改为:

.button:hover, .filter img:hover {

This way, we can give a visual cue of interactivity to the filter effect buttons as well as any other buttons. Now for the JavaScript that handles applying the filter effects. Above the "applyFilter" function, insert the following JavaScript in the app.js file:

这样,我们可以为滤镜效果按钮以及任何其他按钮提供直观的交互提示。 现在针对处理滤镜效果JavaScript。 在“ applyFilter”函数上方,在app.js文件中插入以下JavaScript:

var filters = {
        original: function () {},

        grayscale: function (item) {
            item.saturation(-100);
            item.render();
        },
        sepia: function (item) {
            item.saturation(-100);
            item.vibrance(100);
            item.sepia(100);
            item.render();
        }, 
        sunburst: function (item) {
            item.brightness(21);
            item.vibrance(22);
            item.contrast(11);
            item.saturation(-18);
            item.exposure(18);
            item.sepia(17);
            item.render();
        },
        port: function (item) {
            item.vibrance(49);
            item.hue(6);
            item.gamma(0.6);
            item.stackBlur(2);
            item.contrast(11);
            item.saturation(19);
            item.exposure(2);
            item.noise(2);
            item.render();
        }
    };

We have a filters Object, containing a list of named functions that execute the filter effects. They are passed the CamanJS instance, and run a number of image manipulation commands on them. We then use these in the applyFilter function, which is modified to call them like so:

我们有一个过滤器对象,其中包含执行过滤器效果的命名函数列表。 它们被传递给CamanJS实例,并在它们上运行许多图像处理命令。 然后,我们在applyFilter函数中使用这些函数,该函数经过修改后可以像这样调用它们:

function applyFilter (filterName) {
        Caman('#image', function () {
            this.reset();
            filters[filterName](this);
        });
    }

The CamanJS instance will reset any previously-applied effects for a start, and then name of the filter passed by the div element in the html corresponds with the name of the filter in the filters object.

CamanJS实例将重新设置任何先前应用的效果,然后html中div元素传递的过滤器名称与filter对象中的过滤器名称相对应。

With those changes saved and and the app reloaded, we should now be able to try out different filter effects on our photos, like in the screenshot below:

保存这些更改并重新加载应用程序之后,我们现在应该能够对照片尝试不同的滤镜效果,如以下屏幕截图所示:

Step 13 Multiple Photo Filters

This is where we get oh so close to the final product, but for one more feature - being able to save the modified photo back to the computer.

这是我们接近最终产品的地方,但还有另一个功能-能够将修改后的照片保存回计算机。

保存更改后的照片 (Saving the altered photo)

The final feature to add to the app before the final touches is to enable the user to save the altered photo onto their computer. Although CamanJS does have a API method for saving the file to your computer, unfortunately it does not work in combination with NW.js, so we're going to have to do a bit more work to implement this feature.

在最后触摸之前添加到应用程序中的最后一项功能是使用户能够将更改后的照片保存到他们的计算机上。 尽管CamanJS确实具有用于将文件保存到计算机的API方法,但是不幸的是,它无法与NW.js结合使用,因此,我们将需要做更多的工作才能实现此功能。

What we're going to do to implement this feature instead is the following:

我们将要做的是实现以下功能:

  1. Use an input tag with the attribute "nwsaveas" to bring up a save file dialog, and populate that dialog with the name of the file.

    使用属性为“ nwsaveas”的输入标签将弹出一个保存文件对话框,并使用文件名填充该对话框。
  2. Use the HTML5 Canvas API to extract the photo's data, and prepare for being saved to a file.

    使用HTML5 Canvas API提取照片的数据,并准备将其保存到文件中。
  3. Use Node.js' FileSystem API to receive the file name and file path chosen by the user, and save the data of the photo to there.

    使用Node.js的FileSystem API来接收用户选择的文件名和文件路径,然后将照片数据保存在那里。

We will start by adding the HTML needed to trigger the file save dialog as well as the HTML for the "Save as" button in the toolbar. In the index.html file, add the following line of HTML before the input tag with the id attribute of "folderSelector":

我们将从添加触发文件保存对话框所需HTML以及工具栏中“另存为”按钮HTML开始。 在index.html文件中,将以下行HTML添加到id为“ folderSelector”的输入标签之前:

<input type="file" nwsaveas id="photoSaver"/>

NW.js provides some custom HTML input tag attributes for opening files/folders, as well as saving files. As we need to save a file, we'll use the "nwsaveas" attribute. Next, for the "Save as" button, add the following line of HTML after the div element with the id attribute "back":

NW.js提供了一些自定义HTML输入标签属性,用于打开文件/文件夹以及保存文件。 由于需要保存文件,因此将使用“ nwsaveas”属性。 接下来,对于“另存为”按钮,在具有id属性“ back”的div元素之后添加以下HTML行:

<div id="save" class="button" onclick="saveToDisk();">Save as</div>

The "Save as" button will call a JavaScript function called saveToDisk(), which we will implement later on. Next, we need to make sure that these new HTML elements are styled properly. The input tag needs to be hidden (as users won't click on it directly), and the "Save as" button needs to be displayed to the left of the back button. For the #photoSaver input tag, add the #photoSaver rule to the existing rule for the #folderSelector element, like this:

“另存为”按钮将调用一个名为saveToDisk()JavaScript函数,稍后我们将实现该函数。 接下来,我们需要确保这些新HTML元素的样式正确。 输入标签需要隐藏(因为用户不会直接单击它),并且“另存为”按钮需要显示在后退按钮的左侧。 对于#photoSaver输入标签,将#photoSaver规则添加到#folderSelector元素的现有规则中,如下所示:

#folderSelector, #photoSaver {
        display: none;
    }

This ensures that the #photoSaver element is hidden from the user interface. For the #save div element, we will do the same thing and append its rule to the existing rule for the "#back" element:

这样可以确保#photoSaver元素从用户界面中隐藏。 对于#save div元素,我们将做同样的事情,并将其规则附加到“ #back”元素的现有规则中:

#back, #save {
        float: right;
    }

Even though the #save element comes after the #back element in the HTML, because both are floating to the right, the "Save as" button will be displayed to the left of the "Back" button. Now, we need to implement the saveToDisk function in the app.js file.

即使#save元素位于HTML中的#back元素之后,但由于两者都在右侧浮动,因此“另存为”按钮将显示在“ Back”按钮的左侧。 现在,我们需要在app.js文件中实现saveToDisk函数。

The way that the JavaScript will work is this:

JavaScript的工作方式如下:

  • We add an event listener on when the input tag with the id attribute "photoSaver" changes its value (when the user choose a file name and location for the photo to save from the save as dialog). This will need to be called once when the app is initialised, and the function inside will handle saving the photo data with the file name to the computer.

    当具有id属性“ photoSaver”的输入标签更改其值时(当用户从​​“另存为”对话框中选择要保存的照片的文件名和位置时),我们将添加一个事件侦听器。 初始化应用程序后,将需要调用此函数一次,并且内部函数将处理将带有文件名的照片数据保存到计算机的情况。
  • We add an empty variable called "photoData" that will allow us to dynamically set the photo data that needs to be saved to the computer.

    我们添加了一个名为“ photoData”的空变量,该变量将允许我们动态设置需要保存到计算机的照片数据。
  • We add a function called "saveToDisk" that will get the photo data that is currently loaded, and set the photoData variable to store that data. It will then trigger the save as dialog box by finding the input dialog, and programmatically triggering a click event on it.

    我们添加了一个名为“ saveToDisk”的函数,该函数将获取当前加载的照片数据,并设置photoData变量以存储该数据。 然后,它将通过找到输入对话框并以编程方式触发其上的click事件来触发另存为对话框。

After the dependencies at the top of the app.js file, add the following line of JS:

在app.js文件顶部的依赖项之后,添加以下JS行:

var photoData = null;

This variable will help to store the raw base64 data of the photo for saving to the computer. Next, we want to add a function for binding on the #photoSaver input tag being clicked. Add the following JS after the applyFilter function:

此变量将有助于存储照片的原始base64数据以保存到计算机。 接下来,我们要添加一个用于绑定被单击的#photoSaver输入标签的函数。 在applyFilter函数之后添加以下JS:

function bindSavingToDisk () {
        var photoSaver  = document.querySelector('#photoSaver');
        photoSaver.addEventListener('change', function () {
            var filePath = this.value;
            fs.writeFile(filePath, photoData, 'base64', function (err) {
                if (err) { alert('There was an error saving the photo:',err.message); }
                photoData = null;
            });
        });
    }

This binding function listens on when the "save as" dialog receives a file name and path to save the photo to (filePath). Using Node.js' File System API, it accesses the photo data, and writes the photo's data to the file with the given name and path. It then does a bit of memory cleanup afterwards.

当“另存为”对话框接收到将照片保存到(filePath)的文件名和路径时,此绑定函数会侦听。 使用Node.js的文件系统API,它可以访问照片数据,并将照片数据以给定的名称和路径写入文件中。 然后,它随后会进行一些内存清理。

We then need to modify the window.onload function so that this function is bound on startup. Add the following line after the bindClickingOnAllPhotos(); line inside of the window.onload function:

然后,我们需要修改window.onload函数,以便在启动时绑定此函数。 在bindClickingOnAllPhotos();之后添加以下行bindClickingOnAllPhotos(); window.onload函数内部的行:

bindSavingToDisk();

Now, after the bindSavingToDisk function back higher up in the code, add the following function:

现在,在代码中将bindSavingToDisk函数返回到更高位置之后,添加以下函数:

function saveToDisk () {
        var photoSaver  = document.querySelector('#photoSaver');
        var canvas      = document.querySelector('canvas');
        photoSaver.setAttribute('nwsaveas','Copy of ' + canvas.attributes['data-name'].value);
        photoData       = canvas.toDataURL('image/png').replace(/^data:image\/(png|jpg|jpeg);base64,/, '');
        photoSaver.click();
    }

This function does 2 things; first, it sets the name of the nwsaveas attribute to a "Copy of" plus the photo's name, so that the user has a pre-filled name suggestion for the name of the file in the save as dialog. It then sets the photoData to store the base64-encoded data for the photo, so that the bound function can access it. Finally, it triggers the "save as" dialog programmatically by calling "click" on the #photoSaver input tag.

此功能有两件事: 首先,它将nwsaveas属性的名称设置为“副本的”加上照片的名称,以便用户在“另存为”对话框中预先填写文件名称的建议。 然后,它将photoData设置为存储照片的base64编码数据,以便绑定函数可以访问它。 最后,它通过在#photoSaver输入标签上调用“点击”,以编程方式触发“另存为”对话框。

After all that, give it a spin. You'll see now when you go to a photo, apply a filter, and click "Save as" in the toolbar, you'll be presented with a "save as" dialog and the name of the file to save to your computer, like in the screenshot below:

毕竟,旋转一下。 现在,当您转到照片,应用滤镜并单击工具栏中的“另存为”时,会看到一个“另存为”对话框以及要保存到计算机的文件名,如下面的屏幕截图所示:

Step 14 Saving File to Disk

Now that we've got here, there's a bit of tidying up to do before we package the app for distribution:

现在我们已经到了这里,在打包应用程序以进行分发之前,需要进行一些整理:

整理 (Tidying up)

修正错误 (Fixing a bug)

Before we put the bow and ribbon on the app, we need to do a few things to get it polished. Firstly, I noticed in playing around with the app that a little bug had crept in where if I clicked on a photo, made no filter changes, and clicked "back", then it wouldn't work. The issue lies with how we assume that there will always be a filter applied in the backToGridView function inside of the app.js file. Alter the function to look like this:

在将弓箭和丝带放到应用程序上之前,我们需要做一些事情来完善它。 首先,我在玩该应用程序时发现一个小漏洞爬到了哪里,如果我单击照片,不更改滤镜并单击“返回”,那么它将无法正常工作。 问题在于我们假设如何始终在app.js文件内的backToGridView函数中应用一个过滤器。 将函数更改为如下所示:

function backToGridView () {
        var canvas  = document.querySelector('canvas');
        if (canvas) {
            var image   = document.createElement('img');
            image.setAttribute('id','image');
            canvas.parentNode.removeChild(canvas);
            var fullViewPhoto = document.querySelector('#fullViewPhoto');
            fullViewPhoto.insertBefore(image, fullViewPhoto.firstChild);        
        }
        document.querySelector('#fullViewPhoto').style.display = 'none';
    }

We wrap the code that would cleanup after applying any CamanJS filter effects inside of an if function. If a filter effect has been applied, then there will be a canvas tag to clean up, otherwise, we can simply continue on.

我们在if函数内部应用任何CamanJS滤镜效果后包装将清除的代码。 如果已应用滤镜效果,则将有一个canvas标签需要清除,否则,我们可以继续。

改善应用的视觉效果 (Improving the app's visual performance)

You might notice that when you load the a folder with many photos, the images can take a long time to load. That is because all of the photos are being loaded at the same time, and if you have a large number of photos to load, then this problem becomes noticeable.

您可能会注意到,在加载包含许多照片的文件夹时,图像可能需要很长时间才能加载。 这是因为所有照片都是同时加载的,并且如果要加载大量照片,则此问题将变得很明显。

One of the options for tackling this is to use lazy loading for the images. The idea is that those images which are in the visible area of the application will be loaded, and the rest won't be until they appear in the visual area. This means that even if we have a folder containing thousands of photos, it will initially load the 12 photos that are visible in the area, and then start to show the rest as they become visible.

解决此问题的一种方法是对图像使用延迟加载。 想法是将加载应用程序可见区域中的那些图像,其余图像将不会出现,直到它们出现在视觉区域中为止。 这意味着即使我们有一个包含数千张照片的文件夹,它也将首先加载该区域中可见的12张照片,然后开始显示其余的照片。

For this, we're going to use a JavaScript library called "Echo" by Todd Motto. This library handles image lazy loading for us, and all we need to do is to integrate it into the right places in our app's code.

为此,我们将使用Todd MottoJavaScript库“ Echo”。 该库为我们处理图像延迟加载,我们要做的就是将其集成到应用程序代码中的正确位置。

First, get a copy of Echo's minified javascript echo.min.js from the github repository, and put the file in the scripts folder. Next, we want to include the script in our index.html file, by placing the following script tag in our head section (before the app.js script include):

首先,从github存储库中获得Echo的缩小的javascript echo.min.js的副本,并将该文件放在scripts文件夹中。 接下来,我们希望通过在我们的head部分(在包含app.js脚本之前)放置以下script标签,将脚本包含在index.html文件中:

<script src="scripts/echo.min.js"></script>

With the library included, we can now modify the app.js file in 2 places to make use of this library. Echo works by inspecting image tags that have a 'data-echo' attribute in them, and loads the value of that attribute in place of the image tag's src attribute. In the addImageToPhotosArea of the app.js file, change the function so that it looks like this:

有了包含的库,我们现在可以在2个地方修改app.js文件以使用此库。 Echo通过检查其中具有“数据回显”属性的图像标签来工作,并加载该属性的值来代替图像标签的src属性。 在app.js文件的addImageToPhotosArea中,更改函数,使其如下所示:

function addImageToPhotosArea (file) {
        var photosArea = document.getElementById('photos');
        var template = document.querySelector('#photo-template');
        template.content.querySelector('img').src = 'images/blank.png';
        template.content.querySelector('img').setAttribute('data-echo', file.path);
        template.content.querySelector('img').setAttribute('data-name',file.name);
        var clone = window.document.importNode(template.content, true);
        photosArea.appendChild(clone);
    }

We've changed the function so that instead of setting the image's src attribute to the file path for the photo, we set the 'data-echo' attribute instead, and make the src attribute load a blank png image. Echo will identify the data-echo attributes in the image tags, and load the images when they appear in the visible viewport of the application.

我们更改了功能,以代替将图像的src属性设置为照片的文件路径,而是设置了“ data-echo”属性,并使src属性加载空白的png图像。 回声将识别图像标签中的数据回声属性,并在图像出现在应用程序的可视视口中时加载图像。

In order to make Echo identify these image tags and lazy-load them, it will need to be initialised. In the app.js file's window.onload function, we add a call to Echo's init function, and it's render function, like so:

为了使Echo识别这些图像标签并延迟加载它们,需要对其进行初始化。 在app.js文件的window.onload函数中,我们添加了对Echo的init函数的调用,它是render函数,如下所示:

window.onload = function () {

        echo.init({
            offset: 0,
            throttle: 0,
            unload: false
        });

        bindSelectFolderClick(function (folderPath) {
            hideSelectFolderButton();
            findAllFiles(folderPath, function (err, files) {
                if (!err) {
                    findImageFiles(files, folderPath, function (imageFiles) {
                        imageFiles.forEach(function (file, index) {
                            addImageToPhotosArea(file);
                            if (index === imageFiles.length-1) {
                                echo.render();
                                bindClickingOnAllPhotos();
                                bindSavingToDisk();
                            }
                        });
                    });
                }
            });
        });
    };

The Echo library is initialised when the DOM is loaded, and once all of the photo image have been added into the DOM, we call Echo's render function to automatically trigger the lazy-loading of images that are already in the visible area of the application. We also add a 1x1 PNG to the images folder, called blank, which is there to be loaded by the image initially.

Echo库在加载DOM时初始化,并且一旦将所有照片图像添加到DOM中,我们就调用Echo的render函数自动触发已经在应用程序可见区域中的图像的延迟加载。 我们还将1x1 PNG添加到图像文件夹中,称为空白,图像最初将在该文件夹中加载。

With the changes saved, reload the application, and you should now notice that the images load quicker in the visible area, and that the app is that bit snappier for it.

保存更改后,重新加载应用程序,您现在应该注意到,图像在可见区域的加载速度更快,并且该应用程序对它来说有点敏捷。

查看其他照片文件夹 (Viewing other photo folders)

When you play with the application enough, you begin to realise that there is one remaining flaw in the user experience - you can't browse another folder without closing the application down and reopening it. Luckily we can resolve this pretty easily, and do it by using one of NW.js UI APIs - the application window menu. What we want to do is implement a single application window menu item that will allow the user to load another folder containing photos. This will repurpose some of the code we created for loading the app when a folder is selected, and will use some more code to take care of the new functionality.

当您充分利用该应用程序时,您会开始意识到用户体验中还存在一个缺陷-您必须先关闭应用程序然后重新打开它,才能浏览另一个文件夹。 幸运的是,我们可以很容易地解决此问题,并且可以使用NW.js UI API之一(应用程序窗口菜单)来解决。 我们要做的是实现一个应用程序窗口菜单项,该菜单项将允许用户加载包含照片的另一个文件夹。 这将重新利用我们在选择文件夹时创建的用于加载应用程序的一些代码,并将使用更多代码来维护新功能。

The first thing that we will need to do is to make sure that NW.js' GUI module is loaded into the app. In the dependencies section of the app.js file, add the following line of code:

我们需要做的第一件事是确保将NW.js的GUI模块加载到应用程序中。 在app.js文件的依赖项部分中,添加以下代码行:

var gui     = require('nw.gui');

This loads NW.js' GUI module, so that we can load the Menu API. Next, we want to write some code that will be able to handle the way that NW.js handle Menus for application windows. Menus in NW.js have different API methods for Windows/Linux and Mac OS X - this is because application window menus in Mac OS X are displayed at the top of the screen, and not nested under each application window, unlike Windows and Linux applications.

这将加载NW.js的GUI模块,以便我们可以加载Menu API。 接下来,我们要编写一些代码,这些代码将能够处理NW.js处理应用程序窗口菜单的方式。 NW.js中的菜单针对Windows / Linux和Mac OS X具有不同的API方法-这是因为Mac OS X中的应用程序窗口菜单显示在屏幕顶部,而不是嵌套在每个应用程序窗口下,这与Windows和Linux应用程序不同。

For this, we're going to write a function called "loadMenu", which will take care of handling which menu type to load, based on the user's Operating System. We will call this function when the user first loads a folder. In the window.onload function at the end of the app.js file, adjust it to this:

为此,我们将编写一个名为“ loadMenu”的函数,该函数将根据用户的操作系统来处理要加载的菜单类型。 用户首次加载文件夹时,我们将调用此函数。 在app.js文件末尾的window.onload函数中,将其调整为:

window.onload = function () {

        echo.init({
            offset: 0,
            throttle: 0,
            unload: false
        });

        bindSelectFolderClick(function (folderPath) {
            loadMenu();
            hideSelectFolderButton();
            findAllFiles(folderPath, function (err, files) {
                if (!err) {
                    findImageFiles(files, folderPath, function (imageFiles) {
                        imageFiles.forEach(function (file, index) {
                            addImageToPhotosArea(file);
                            if (index === imageFiles.length-1) {
                                echo.render();
                                bindClickingOnAllPhotos();
                                bindSavingToDisk();
                            }
                        });
                    });
                }
            });
        });
    };

We've added a new function called "loadMenu", and now we need to flesh it out. Above the window.onload function, add a new function called "loadMenu", with the following code:

我们添加了一个名为“ loadMenu”的新函数,现在我们需要对其进行充实。 在window.onload函数上方,添加一个名为“ loadMenu”的新函数,其代码如下:

function loadMenu () {
        var menuBar     = new gui.Menu({type:'menubar'});
        var menuItems   = new gui.Menu();

        menuItems.append(new gui.MenuItem({ label: 'Load another folder', click: loadAnotherFolder }));

        var fileMenu = new gui.MenuItem({
            label: 'File',
            submenu: menuItems
        });

        if (process.platform === 'darwin') {

            // Load Mac OS X application menu
            menuBar.createMacBuiltin('Lens');
            menuBar.insert(fileMenu, 1);

        } else {

            // Load Windows/Linux application menu
            menuBar.append(fileMenu, 1);

        }

        gui.Window.get().menu = menuBar;

    }

This code does a number of things; firstly, it instantiates some objects that are needed for constructing an application window menu, such as the menubar, the menu containing the action items (in this case just one action item for loading another folder), and the File Menu for the menu bar, to contain the 1 action item.

这段代码可以做很多事情。 首先,它实例化了构建应用程序窗口菜单所需的一些对象,例如菜单栏,包含动作项目的菜单(在这种情况下,该动作项目仅是用于加载另一个文件夹的一个动作项目),以及菜单栏的“文件菜单”,包含1个操作项。

We then use Node.js' process.platform API to determine what Operating System the user is running. In this case, we want to identify if the user is running Mac OS X or not. If they are, then we call NW.js' API for loading Mac OS X's own menu and insert the File menu into it, and if they're running something else, then we call NW.js' API and append the File menu to the menu bar. After this, we attach the menu bar to the application window.

然后,我们使用Node.js的process.platform API来确定用户正在运行的操作系统。 在这种情况下,我们要确定用户是否正在运行Mac OSX。 如果是这样,则我们调用NW.js的API来加载Mac OS X自己的菜单,然后将File菜单插入其中,如果它们正在运行其他内容,则我们调用NW.js的API并将File菜单附加到菜单栏。 之后,我们将菜单栏附加到应用程序窗口。

You'll also notice in the code that one of the menu items (Load another folder) will call a function called "loadAnotherFolder" when it is clicked on. We haven't yet fleshed out this function, so we will need to do that next. Above the loadMenu function that we added, we will create a function called loadAnotherFolder, and put the following code inside of it:

您还将在代码中注意到,单击菜单项之一(“加载另一个文件夹”)将调用一个名为“ loadAnotherFolder”的函数。 我们尚未充实此功能,因此我们下一步需要做。 在我们添加的loadMenu函数上方,我们将创建一个名为loadAnotherFolder的函数,并将以下代码放入其中:

function loadAnotherFolder () {
        openFolderDialog(function (folderPath) {        
            findAllFiles(folderPath, function (err, files) {
                if (!err) {
                    clearArea();
                    echo.init({
                        offset: 0,
                        throttle: 0,
                        unload: false
                    });
                    findImageFiles(files, folderPath, function (imageFiles) {
                        imageFiles.forEach(function (file, index) {
                            addImageToPhotosArea(file);
                            if (index === imageFiles.length-1) {
                                echo.render();
                                bindClickingOnAllPhotos();
                                bindSavingToDisk();
                            }
                        });
                    });
                }
            });
        });
    }

This function essentially repurposes some of the code that exists in the window.onload function, but with some subtle differences. The function will trigger the selector folder dialog, but when it has a folder and has scanned the files that exist in that folder, what it will do instead is clear the photos that were displayed in the previously-selected folder away, make sure that echo is initialised for when the photos from the newly-selected folder are added in place, and then adds the new photos to the application. Isolate the differences between this function and the window.onload function, and there is a small refactoring opportunity to be had.

此函数实质上是重新利用window.onload函数中存在的某些代码,但有一些细微的差别。 该功能将触发选择器文件夹对话框,但是当它有一个文件夹并扫描了该文件夹中存在的文件时,它将执行的操作是清除先前选择的文件夹中显示的照片,并确保回显当将新选择的文件夹中的照片添加到位,然后将新照片添加到应用程序时,将初始化。 隔离此函数和window.onload函数之间的差异,然后会有一个小的重构机会。

In order to make the function clear the photos of the previously-selected folder, we need to implement another function to handle clearing them, called "clearArea". Above the "loadAnotherFolder" function, add the following code:

为了使该功能清除以前选择的文件夹中的照片,我们需要实现另一个功能来清除它们,称为“ clearArea”。 在“ loadAnotherFolder”函数上方,添加以下代码:

function clearArea () {
        document.getElementById('photos').innerHTML = '';
    }

This function finds the #photos div, and simply sets the innerHTML to an empty string, removing all of the nested div elements inside of it, which are the photos from the previously-selected folder.

此函数查找#photos div,然后将innerHTML设置为空字符串,并删除其中的所有嵌套div元素,这些元素是先前选择的文件夹中的照片。

Reload the app, and you should now see that when you select a folder, the app should display an application menu, with the option under the File menu to load another folder, as shown in the screenshot below:

重新加载该应用程序,现在您应该看到选择一个文件夹时,该应用程序应显示一个应用程序菜单,其中“文件”菜单下的选项可以加载另一个文件夹,如以下屏幕截图所示:

App with Menu

Voila, you can now load another folder within the app, helping to close off an annoying UX fail that the app previously had.

瞧,您现在可以在应用程序中加载另一个文件夹,以帮助解决该应用程序以前遇到的烦人的UX故障。

隐藏开发人员工具工具栏 (Hiding the developer tools toolbar)

Next, we want to hide the developer tools toolbar at the top of the app window. In the package.json file, we can add a bit of JSON to handle the display of that toolbar. After the "dependencies" section, add the following snippet of JSON:

接下来,我们要隐藏应用程序窗口顶部的开发人员工具工具栏。 在package.json文件中,我们可以添加一些JSON来处理该工具栏的显示。 在“依赖项”部分之后,添加以下JSON代码段:

"window": {
        "toolbar": false
    }

NW.js is able to configure the behaviour of the window based on the properties in the package.json file. Here, we are telling the window to not load the toolbar. Save this file, reload the app from the command line, and now we should see this:

NW.js能够根据package.json文件中的属性配置窗口的行为。 在这里,我们告诉窗口不要加载工具栏。 保存此文件,从命令行重新加载应用程序,现在我们应该看到以下内容:

Step 15 Window Toolbar

With these changes in place, we can now think about how to get the app packaged as binaries for multiple Operating Systems.

完成这些更改之后,我们现在可以考虑如何将应用打包为多个操作系统的二进制文件。

打包应用 (Packaging the app)

In order to package the app, we also need to create an icon for the app as well, and package it so that the apps builds will have it. In this case, I've created one for the app, shown below:

为了打包该应用程序,我们还需要为该应用程序创建一个图标,并对其进行打包,以便构建应用程序时可以使用它。 在这种情况下,我为该应用程序创建了一个,如下所示:

Lens App

Take a copy of the file, and save it as "lens.png" inside of the lens folder. Next, we want to reference the icon in the package.json file. Add the following snippet of JSON to the package.json file, after the "toolbar: false" setting:

取得该文件的副本,并将其另存为lens文件夹内的“ lens.png”。 接下来,我们要引用package.json文件中的图标。 在“工具栏:false”设置之后,将以下JSON代码片段添加到package.json文件:

"icon":"lens.png"

With that saved, we can now package the app for multiple operating systems. For this, we will use a tool called "nwbuilder", which simplifies the task. Run the following command on your command line terminal:

保存后,我们现在可以将应用程序打包为多个操作系统。 为此,我们将使用一个名为“ nwbuilder”的工具,它可以简化任务。 在命令行终端上运行以下命令:

npm install -g nw-builder

This will install the nw-builder tool as a global dependency on your computer. Next, we want to run the tool to build the app. Next, change to the parent directory that contains the lens folder, and run the following command on your terminal:

这会将nw-builder工具安装为您计算机上的全局依赖项。 接下来,我们要运行该工具来构建应用程序。 接下来,转到包含lens文件夹的父目录,然后在终端上运行以下命令:

nwbuild -p win32,win64,osx32,osx64,linux32,linux64 lens

This will go ahead and build binaries of the lens app for Windows, Mac and Linux, with both 32-bit and 64-bit compatible versions for each. Once the process has finished, there will be a build folder, and inside of it there will be another folder with the app name "Lens", and inside of that will be 6 folders, two each for Windows, Mac and Linux (with the 32-bit and 64-bit versions).

这将继续并为Windows,Mac和Linux构建lens应用程序的二进制文件,并分别提供32位和64位兼容版本。 该过程完成后,将有一个build文件夹,在其中将有另一个名为“ Lens”的应用程序文件夹,其中将有6个文件夹,每个文件夹分别用于Windows,Mac和Linux(带有32位和64位版本)。

If you go ahead and browse for the application that works on our operating system, we can double-click on it, and it will run as a standalone application.

如果您继续浏览并找到适用于我们操作系统的应用程序,则可以双击它,它将作为独立的应用程序运行。

应用程序图标 (Application Icons)

Unfortunately nw-builder does not set the application icon for the Mac OS X applications, and so in this case we have to do it manually. That said, the good news is that changing an application icon in Mac OS X is very easy to do, but we need to make sure that the application icon is in the right file format.

不幸的是nw-builder没有为Mac OS X应用程序设置应用程序图标,因此在这种情况下,我们必须手动进行操作。 也就是说,好消息是,在Mac OS X中更改应用程序图标非常容易,但是我们需要确保该应用程序图标的文件格式正确。

On Mac OS X, application icons use the .icns file format. There are a number of applications out there for creating .icns files, and the one that I recommend using is iconvert icons. You can either use the website, or purchase the desktop app from either the Mac App Store or Windows App Store (if you're using a PC for this tutorial).

在Mac OS X上,应用程序图标使用.icns文件格式。 有许多用于创建.icns文件的应用程序,而我建议使用的应用程序是iconvert icons 。 您可以使用该网站,也可以从Mac App Store或Windows App Store购买桌面应用程序(如果本教程使用的是PC)。

We'll need to create a Mac OS icns file as opposed to a Mac OS folder icon. Once we've created this, we can change the application icon for the Mac OS X builds of the Lens app.

我们需要创建一个Mac OS icns文件,而不是Mac OS文件夹图标。 创建此选项后,我们可以更改Lens应用程序的Mac OS X版本的应用程序图标。

If you navigate to one of the Mac OS X apps inside of the build folder, right click on the app to bring up the context menu, and select "Get Info". This will bring up a window displaying some of the details about the application (when it was created, how big the file size is, and other details). In the top left of the window you will see the application's icon (as per the screenshot).

如果导航到build文件夹内的Mac OS X应用程序之一,请右键单击该应用程序以打开上下文菜单,然后选择“获取信息”。 这将打开一个窗口,显示有关该应用程序的一些详细信息(创建时,文件大小以及其他详细信息)。 在窗口的左上方,您将看到应用程序的图标(如屏幕截图所示)。

Step 16 Change App Icon

Drag the lens.icns file that we generated from iConvert Icons into the area displaying the icon in the "Get Info" window, and this will change the application's icon to use the lens.icns file. If we now double-click on the app and run it, we will see that the application uses the new app icon (as opposed to NW.js' icon).

将我们从iConvert Icons生成的lens.icns文件拖到“获取信息”窗口中显示图标的区域中,这将更改应用程序的图标以使用lens.icns文件。 如果现在双击应用程序并运行它,我们将看到该应用程序使用新的应用程序图标(而不是NW.js的图标)。

If you run the application on Windows, you should see something like this:

如果您在Windows上运行该应用程序,则应该看到类似以下内容:

Lens on Windows v2

We now have an app that we can be given to people to use on their computers.

现在,我们有了一个可以供人们在其计算机上使用的应用程序。

结论 (Conclusion)

The major takeaway from this tutorial is to show you that if you have web design skills and know your way around JavaScript, then it won't take much for you to use those skills to create a desktop application, especially one that can be built for Mac OS X, Windows, and Linux. Not only that, but as a desktop app that can access the user's file system and interact with files, you have a greater range of possible apps that you can build, beyond what can be achieved with just a web application.

本教程的主要内容是向您显示,如果您具有网页设计技能并且了解JavaScript的使用方法,那么您不需要花太多时间即可使用这些技能来创建桌面应用程序,尤其是可以为Mac OS X,Windows和Linux。 不仅如此,而且作为可以访问用户文件系统并与文件交互的桌面应用程序,您可以构建的应用程序范围更大,而不仅仅是Web应用程序可以实现。

NW.JS In Action

If you are interested in finding out more about NW.js, you can checkout the web site. Also, I'm writing a book for Manning called "NW.js in Action", where you get to find out not just how NW.js works and how to build desktop applications with it, but also how to test those applications, debug performance issues, and prepare your app for selling on App Stores. It's available now via the MEAP program, and will be published Spring 2016. Scotch.io readers can get 40% off of the book with discount code: scotch

如果您想了解有关NW.js的更多信息,可以访问该网站 。 另外,我正在为曼宁写一本书,名为《 NW.js in Action》 ,您不仅可以找到NW.js的工作方式以及如何使用它构建桌面应用程序,还可以找到如何测试这些应用程序,调试性能问题,并准备好要在App Store上出售的应用。 它现在可以通过MEAP程序获得,并将于2016年Spring发布。Scotch.io读者可以使用折扣码scotch享受40%的折扣。

If you have any queries about the app or the article, my email is paulbjensen@gmail.com. Hope you enjoyed the tutorial, and thanks for reading.

如果您对应用程序或文章有任何疑问,我的电子邮件是paulbjensen@gmail.com 。 希望您喜欢本教程,并感谢您的阅读。

附录 (Appendix)

*In the old days it was using Webkit and Node.js, but switched to the forks of those projects, hence the renaming of the project from Node Webkit to NW.js.

*在过去,它使用Webkit和Node.js,但是切换到那些项目的分支,因此将项目从Node Webkit重命名为NW.js。

翻译自: https://scotch.io/tutorials/creating-a-photo-discovery-app-with-nw-js-part-2

nw.js 调用驱动程序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值