node.js 自定义类库

The first thing I do on any new device is customize the home screen. This has been a tradition of mine since my first non-DOS computer, when I used my Mario Paint manual to help me make some Legend of Zelda pixel art in MS Paint, and set it as the Desktop wallpaper.

我在任何新设备上要做的第一件事就是自定义主屏幕。 自从我的第一台非DOS计算机以来,这一直是我的传统,当时我使用了Mario Paint手册来帮助我在MS Paint中制作一些《塞尔达传说》像素艺术并将其设置为“桌面”墙纸。

Now that I'm a husband and a father, I prefer to put pictures of my family on my Desktop, and for years I've been doing so manually with the help of GeekTool, but I waste a lot of time on that manual process: Choosing the newest images, exporting them, resizing them, moving them around in GeekTool, and repeating the whole process every time I import a new batch.

现在,我已经是丈夫和父亲了,我更喜欢将家人的照片放在桌面上,多年来,我一直在GeekTool的帮助下进行手动操作,但是我在手动操作上浪费了很多时间:选择最新的图像,导出它们,调整它们的大小,在GeekTool中移动它们,并在每次导入新批次时重复整个过程。

I use Adobe Lightroom to manage my photos, and one day it occurred to me that the Catalogue itself must be searchable outside of the application. Looking further into it, I learned that it is merely an SQLite Database. With that knowledge, I began sketching a plan to ease my desktop woes. My goals were:

我使用Adobe Lightroom管理照片,有一天我发现目录本身必须在应用程序外部可以搜索。 进一步研究它,我了解到它只是一个SQLite数据库。 有了这些知识,我就开始草拟一个计划来缓解台式机的麻烦。 我的目标是:

  • Use a keyword to find the latest flagged/picked image from a Lightroom Catalog.使用关键字从Lightroom目录中查找最新标记/挑选的图像。
  • Take that image and make a smaller copy.拍摄该图像并制作较小的副本。
  • Do this for a number of keywords.对许多关键字执行此操作。
  • Show those images dynamically on the Desktop.在桌面上动态显示这些图像。

In this article, we'll learn how to do all of the above using our own Node module. So let's dig in!

在本文中,我们将学习如何使用自己的Node模块完成上述所有操作。 因此,让我们深入研究吧!

总览 ( Overview )

Its important to start with a plan, so here are the tasks we'll need to accomplish in order to reach our goal.

从计划开始很重要,因此,这是我们要实现目标所需完成的任务。

  1. Setup our Node module and add dependencies设置我们的Node模块并添加依赖项
  2. Write our SQLite queries to find the image file we want.编写我们SQLite查询以查找所需的图像文件。
  3. Copy file locally.在本地复制文件。
  4. Allow for multiple keywords.允许多个关键字。
  5. Shrink the image to a reasonable size.将图像缩小到合理的尺寸。
  6. Setup the Desktop to use our generated images.设置桌面以使用我们生成的图像。

先决条件 (Prerequisites)

There are a number of required apps and scripts you'll need to follow along. Not everyone will have them, and I am coming from a decidedly Mac point of view, but my hope is that you'll learn something of what Node can accomplish "server-side", even when that server is your laptop. Windows users will have to chime in with workarounds if something doesn't make sense, as the scripts we're using are meant for Unix systems.

您需要遵循许多必需的应用程序和脚本。 并不是每个人都会拥有它们,而且我从Mac的角度出发,但是我希望您能学到Node可以在“服务器端”完成的工作,即使该服务器是您的笔记本电脑。 如果没有意义,Windows用户将不得不采用变通办法,因为我们使用的脚本适用于Unix系统。

You'll certainly have to have a copy of Adobe Lightroom installed. You'll also need an Sqlite browser, Homebrew, Node & npm, some image manipulation libraries, and a tool like GeekTool or Übersicht. But we'll go through all those as we come to them in the session.

当然,您必须安装Adobe Lightroom的副本。 您还需要Sqlite浏览器,Homebrew,Node&npm,一些图像处理库以及类似GeekTool或Übersicht的工具。 但是,我们将在本次会议中介绍所有这些内容。

设置我们的Node模块并添加依赖项 ( Setup our Node module and add dependencies )

To begin, we'll need to scaffold out a Node module. This is quite easy, so long as you already have Node and npm installed. Use the official installer from Nodejs.org, or install via Homebrew. (If you don't have Homebrew installed, you'll need it later on. Get things started here: Homebrew Setup)

首先,我们需要搭建一个Node模块。 只要已经安装了Node和npm,这就非常容易。 使用Nodejs.org上的官方安装程序,或通过Homebrew安装。 (如果您没有安装Homebrew,则稍后将需要它。从此处开始: Homebrew Setup )

Once you're ready, we'll create our project. Open up your Terminal app, and mkdir a folder wherever you like (I prefer to place mine in ~/Code/). We'll call it lr-latest, then cd into it.

准备就绪后,我们将创建我们的项目。 打开您的终端应用程序,然后在任意位置添加mkdir文件夹(我更喜欢将我的文件夹放置在~/Code/ )。 我们将其称为lr-latest ,然后将其插入cd。

@media (max-width: 1280px) { .go-go-gadget-react img:first-child { display: none; } }@media (max-width: 780px) {.go-go-gadget-react { flex-direction: column; }.go-go-gadget-react img { margin-left: 0 !important; margin-bottom: 12px !important; }.header-thingy { margin-top: 20px; }.button-thingy { margin-left: 0 !important; margin-top: 12px !important; }} @media (max-width: 1280px) { .go-go-gadget-react img:first-child { display: none; } }@media (max-width: 780px) {.go-go-gadget-react { flex-direction: column; }.go-go-gadget-react img { margin-left: 0 !important; margin-bottom: 12px !important; }.header-thingy { margin-top: 20px; }.button-thingy { margin-left: 0 !important; margin-top: 12px !important; }}

$mkdir lr-latest && cd lr-latest

Now we'll create the Node module. Step one is to run the init command by npm:

现在,我们将创建Node模块。 第一步是通过npm运行init命令:

$npm init

By default it uses the name of the folder you're in for the module name. That sounds good to me. In fact, just hit Enter/Return at all of the prompts. When its done, you'll have a new file in your directory named package.json, that looks like this:

默认情况下,它使用您所在文件夹的名称作为模块名称。 对我来说听起来不错。 实际上,只需在所有提示下按Enter / Return。 完成后,您的目录中将有一个名为package.json的新文件,如下所示:

{"name": "lr-latest","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "","license": "ISC"
}

Lets start there and get things cleaned up. The most important field is "main", as it tells Node what file to run when you use the module. Let's change that to a more specific filename:

让我们从那里开始,进行清理。 最重要的字段是"main" ,它告诉Node使用模块时要运行什么文件。 让我们将其更改为更特定的文件名:

"main": "lr-latest.js",

Also, we're going to add two more lines manually to make things run how we want. Above "author" add these lines:

另外,我们将手动添加两行,以使事情按预期运行。 在"author"上方添加以下行:

"preferGlobal": true,
"bin": {"lr-latest": "lr-latest.js"
},

Don't forget the commas! Only the last line can be left without a comma. What the above is doing is first telling Node that we prefer our command to be installed via the npm install -g tag, as this is a system-wide function we're building.

不要忘记逗号! 只能保留最后一行而不带逗号。 上面的操作是首先告诉Node我们希望通过npm install -g标记安装命令,因为这是我们正在构建的系统级功能。

More importantly is the bin line, which tells Node that typing lr-latest on the command line should run node lr-latest.js within this module. Much easier to remember and use.

更重要的是bin行,它告诉Node在命令行上键入lr-latest应该在该模块内运行node lr-latest.js 。 容易记住和使用。

So, before we move on, we'll need to create lr-latest.js!

因此,在继续之前,我们需要创建lr-latest.js!

$touch lr-latest.js

For now, let's put something simple in here to test it out:

现在,让我们在这里进行一些简单的测试:

#!/usr/bin/env Node
console.log('Well hi there!');

We begin with a hash-bang (is that what its called?) to tell the system we're using Node, and then we'll spit out a bit of friendly text.

我们从一个哈希爆炸开始(它叫它什么?)来告诉系统我们正在使用Node,然后吐出一些友好的文本。

To finish, we'll run a command in the Terminal to make Node think that we've installed our module globally, but actually leave it where it is so we can keep editing it.

最后,我们将在终端中运行命令,以使Node认为我们已经在全局安装了模块,但实际上将其保留在原处,以便我们可以继续对其进行编辑。

$npm link

You may see a few errors here, npm WARN EPACKAGEJSON lightroom-latest@1.0.0 No description and npm WARN EPACKAGEJSON lightroom-latest@1.0.0 No repository field. That's totally ok, and only a warning. If you were to publish on NPMjs.org, you'd want to have those fields filled out. But for now, let's see if it worked!

您可能会在这里看到一些错误, npm WARN EPACKAGEJSON lightroom-latest@1.0.0 No descriptionnpm WARN EPACKAGEJSON lightroom-latest@1.0.0 No repository field. 没关系,只有警告。 如果要在NPMjs.org上发布,则需要填写这些字段。 但是现在,让我们看看它是否有效!

$    lr-latest

If you saw "Well hi there!", we're good to go, you can now run your command anywhere on your system.

如果您看到“ Well hi there!”,那就好了,您现在可以在系统上的任何位置运行命令。

添加依赖项 (Add Dependencies)

One of my favorite things about npm is how easy it is to piggyback off the amazing work of others. For this project we're going to need some functions that we really shouldn't have to write out by hand ourselves. Doing a search on Github or npmjs.com will point you in the right direction to find well-maintained modules whenever you need additional functionality.

关于npm我最喜欢的事情之一是背负他人的出色工作是多么容易。 对于这个项目,我们将需要一些我们不需要亲自编写的功能。 在Github或npmjs.com上进行搜索将为您指明正确的方向,以便在需要其他功能时找到维护良好的模块。

For us, we will want to grab information from the Lightroom Catalog, which is an sqlite database. So I've found a nice module called sqlite3 that we simply cannot do without.

对于我们来说,我们将要从Lightroom目录中获取信息,该目录是一个sqlite数据库。 因此,我找到了一个很好的名为sqlite3的模块,我们无法没有这个模块。

To install it and use it, we first have to include it in our package.json, which we can add manually, but which npm will do for us if we type the following:

要安装和使用它,我们首先必须将其包含在package.json中,我们可以手动添加它,但是如果键入以下内容,哪个npm可以为我们做:

$npm install sqlite3 --save

The --save part will automatically add the module as a dependency inside our package.json file. Open it up, and you should see this new section:

--save部分将自动将模块作为依赖项添加到我们的package.json文件中。 打开它,您将看到以下新部分:

"dependencies": {"sqlite3": "^3.1.1"
}

So, all that's left is to actually use it.

因此,剩下的就是实际使用它了。

编写我们SQLite查询以查找所需的图像文件。 ( Write our SQLite queries to find the image file we want. )

准备Lightroom (Prepare Lightroom)

First, you'll need to have some photos in your Lightroom Catalog. What we said earlier is that we want to show the "latest" photo that's been "picked" for a given "keyword". So, for now, choose a few photos you think are good and give them a Keyword of 'Fantastic'.

首先,您需要在Lightroom目录中有一些照片。 前面我们说的是,我们想显示给定的“关键字”被“挑选”的“最新”照片。 因此,就目前而言,选择几张您认为不错的照片,并为它们指定“神奇”关键字。

Also, choose two and add a picked Flag. Leave Lightroom open for reference as we proceed.

另外,选择两个并添加一个拾取的标志。 继续进行操作时,请保持Lightroom处于打开状态以供参考。

What we want to find with our Node command is the Absolute location of the most recently captured photo in our catalog with the keyword "Fantastic" and its flag set to "Picked".

我们想通过Node命令找到的是目录中最近捕获的照片的绝对位置,其中关键字为“ Fantastic”,其标志设置为“ Picked”。

探索数据库 (Exploring the Database)

We can't really write much until we understand what we're trying to do. For this next part, you'll probably prefer to use a GUI, and I like sqlitebrowser. Install through their website if you wish, but bonus points if you use the Homebrew extension Cask, and install with brew cask install sqlitebrowser.

在我们了解自己要做什么之前,我们不能真正写很多东西。 在下一部分中,您可能更喜欢使用GUI,我喜欢sqlitebrowser 。 如果愿意,可以通过他们的网站进行安装,但是如果您使用Homebrew扩展名Cask ,则可以加分,并使用brew cask install sqlitebrowser

In the app, click on Open database, and browse to your Lightroom Catalog.

在应用程序中,单击“打开数据库”,然后浏览到您的Lightroom目录。

In the current version, I get the error message invalid file format. That's ok, we'll just make a copy of the catalog and change its extension to .db instead of .lrcat (Keep in mind, this is only for exploration-mode, in our module, we'll use the actual database in read-only mode).

在当前版本中,我收到错误消息无效的文件格式。 没关系,我们只需要复制目录并将其扩展名更改为.db而不是.lrcat(请注意,这仅适用于探索模式,在我们的模块中,我们将使用实际数据库读取-only模式)。

You should now be able to see the structure of the database.

现在,您应该能够看到数据库的结构。

测试查询 (Test a query)

Before digging into this project, I'd never had the "pleasure" of writing an sqlite query. It gets very complex, very fast, and this one was a doozy.

在深入研究这个项目之前,我从未有过编写sqlite查询的“乐趣”。 它变得非常复杂,非常快,而且这是一个笨拙的过程。

Lets start with finding the photo we want. It took quite a bit of guess work, and just sticking with it, to find the tables I needed. What you need to understand is that there is no full path in the database, so we'll need to build several queries to get the parts we need, and put them together later.

让我们开始寻找我们想要的照片。 花费了大量的猜测工作,并且坚持下去才能找到我需要的表。 您需要了解的是数据库中没有完整的路径,因此我们需要构建几个查询来获取所需的部分,然后将它们放在一起。

I'll save you some time, and just tell you what we need:

我会为您节省一些时间,只是告诉您我们需要什么:

  • fileAbsolutePathfileAbsolutePath
  • filePathFromRootfilePathFromRoot
  • fileBaseNamefileBaseName
  • fileExtension文件扩展名

Lets start with the BaseName.

让我们以BaseName开头。

First, we need to know the id_loca of our tag/keyword. I found this in the agLibrarykeywordimage table. To get the id, we'll use this:

首先,我们需要知道标签/关键字的id_loca 。 我在agLibrarykeywordimage表中找到了agLibrarykeywordimage 。 要获取ID,我们将使用以下代码:

SELECT id_local from aglibrarykeyword where lc_name='fantastic'

If you run that in the Execute SQL section of the app, you should see a number, in my case 124.

如果您在应用程序的“执行SQL”部分中运行该代码,则应该看到一个数字,就我而言,是124

Next, we want to use that number to find any rows in the table aglibrarkeywordimage that have that number in the 'tag' column. Specifically, we want a list of the values of the image column. Like so:

接下来,我们要使用该数字在表aglibrarkeywordimage中查找在“标签”列中具有该数字的任何行。 具体来说,我们想要一个image列值的列表。 像这样:

SELECT image from aglibrarykeywordimage where tag=(124)

The best part, though its complex, is that we can nest our queries, so 124 can be replaced with the previous query:

最好的部分尽管很复杂,但是我们可以嵌套查询,因此可以用先前的查询替换124:

SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='fantastic'
)

That should return a handful of numbers.

那应该返回一些数字。

Next, we want to find all of those images that are picked, and only keep the one with the most recent capture time. the adobe_images column has a most of the information we want about the actual image. You'll see there's a captureTime column, as well as a pick column. Here's the query that will get what we want:

接下来,我们要查找所有已拾取的图像,并仅保留最近捕获时间的图像。 adobe_images列包含我们想要的有关实际图像的大多数信息。 您将看到有一个captureTime列和一个pick列。 这是将得到我们想要的查询:

SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='fantastic')
) AS B ON A.id_local=B.image where pick=1.0

Yeesh, that was beyond what I felt comfortable doing, but logically it makes sense. We're joining results from the adobe_images table, and our previous image results, and only keeping the ones where the pick column from adobe_images has a value of 1.0. This works because the id_local column in adobe_images contains the same values as the image column in id_local.

是的,这超出了我的感觉,但是从逻辑上讲这是有道理的。 我们将adobe_images table结果与先前的image结果结合adobe_images table ,并且仅保留adobe_images中的pick列值为1.0 。 这工作,因为id_localadobe_images包含为同一值imageid_local

Then, we're taking those results and only keeping the one with the highest value of in its captureTime column. If you run that, you should see a single result, the latest captureTime value.

然后,我们将获得这些结果,并仅将其captureTime结果保留在其captureTime列中。 如果运行该命令,则应该看到一个结果,即最新的captureTime值。

Ok, so what we actually want is the rootFile value from that row, so we add another wrapper around our query, like so:

好的,所以我们真正想要的是该行中的rootFile值,因此我们在查询周围添加了另一个包装器,如下所示:

SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='fantastic')) AS B ON A.id_local=B.image where pick=1.0
)

That gets us the rootFile number, which is the same as the id_local column in the AgLibraryFile table. That table also has a column called baseName, which finally (finally!) has the actual file name we're looking for. So our final query is:

这样便获得了rootFile编号,该编号与AgLibraryFile表中的id_local列相同。 该表还有一个名为baseName的列,最后(最终!)具有我们正在寻找的实际文件名。 因此,我们的最终查询是:

SELECT baseName from AgLibraryFile where id_local=(SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='fantastic')) AS B ON A.id_local=B.image where pick=1.0)
)

That should return a filename, like IMG_2012 or such. If you get an error, be careful that your test photos don't all have the same capture time, as that confuses matters and we have no error protection built in here.

那应该返回一个文件名,例如IMG_2012等。 如果您遇到错误,请注意您的测试照片的拍摄时间并不相同,这会引起混淆,并且我们没有内置错误保护功能。

Ok, so we can move forward now, right? Wrong! That's only one part of what we need. We're looking for the absolute path, after all. But rather than walk you through them all, you can find them in the sections below as we build our actual Node module.

好吧,现在我们可以继续前进,对吗? 错误! 那只是我们需要的一部分。 毕竟,我们正在寻找绝对的道路。 但是,与其逐步介绍所有内容,您还可以在构建实际的Node模块时在以下各节中找到它们。

On a side note, if you were finding this all yourself, you'd have to start with the filename, then dig through the tables until you traced back to the tag. It takes a while, but the relationships are all in there! Now, on to the module.

顺便说一句,如果您自己都找到了它,则必须从文件名开始,然后浏览表,直到追溯到标签为止。 需要一些时间,但所有关系都在里面! 现在,进入模块。

在Node中构建查询 (Building the queries in Node)

Lets change our lr-latest.js file so it looks like this:

让我们更改lr-latest.js文件,使其如下所示:

#!/usr/bin/env Node
var tag = "fantastic";
getFile(tag);function getFile(tag) {console.log(tag);
}

This creates a function that takes a tag we give it and lets us do things with it. We'll want it wrapped up in a function so we can make it take multiple tags later on. The tag variable is, for now, being set manually to the keyword we want to retrieve from the catalog.

这将创建一个函数,该函数接受我们为其提供的tag ,并让我们对其进行处理。 我们希望将其包装在一个函数中,以便稍后使它带有多个标签。 暂时将tag变量设置为我们要从目录中检索的关键字。

Next, we'll take the tag and try and get the filename from our database. We'll be using the sqlite3 module, so lets add in the includes just below the hash-bang (really though, is that what its called?) the following:

接下来,我们将获取标签并尝试从数据库中获取文件名。 我们将使用sqlite3模块 ,因此让我们在hash-bang下方添加(包括(实际上,这是它的名字吗?))以下内容:

var catpath = '/Users/evanpayne/Pictures/Lightroom/Lightroom\ Catalog.lrcat';
var sqlite3 = require('sqlite3');
var db = new sqlite3.Database(catpath, 'OPEN_READONLY');

You'll need to change catpath to the absolute location of your own Lightroom catalog. The easiest way to do this is to browse to the catalog in the Finder, drag-and-drop the catalog onto your Terminal window, then copy and paste the line that appears.

您需要将catpath更改为您自己的Lightroom目录的绝对位置。 最简单的方法是浏览到Finder中的目录,将目录拖放到“终端”窗口中,然后复制并粘贴出现的行。

What db is doing is creating a new sqlite3 Database object, using our catalog as the database. We're also specifying that we want it to be readonly, so we don't have to worry about accidentally deleting anything.

db所做的是使用我们的目录作为数据库来创建一个新的sqlite3数据库对象。 我们还指定我们希望它是只读的,因此我们不必担心意外删除任何内容。

Next, we'll replace what we had in our getFile function as follows:

接下来,我们将替换在getFile函数中的内容,如下所示:

function getFile(tag) {db.parallelize(function(){db.get("SELECT baseName from AgLibraryFile where id_local=(SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='"+tag+"')) AS B ON A.id_local=B.image where pick=1.0))", function(err, row) {if (err){console.log(err);process.exit(2);}else{console.log(row.baseName);}});});
}

In this function, we're using sqlite3's parallelize function to perform actions on the db. This is because trying to do it other ways caused a lot of trouble, mostly because we'll be doing a number of such calls, and Node will try and run them all at the same time.

在此函数中,我们使用sqlite3的parallelize函数对数据库执行操作。 这是因为尝试以其他方式进行操作会造成很多麻烦,这主要是因为我们将进行许多此类调用,而Node将尝试同时运行所有这些调用。

So, we've seen how our tag variable gets passed into the function, and now we'll use it with db.get() within the sqlite query we built before, all on one line. Please note the distinction between single and double quotes here. Single is used to surround the tag, and would still be there if we hand-coded it, double quotes allow us to use a variable, much like in php, only joining with plus signs instead of dots.

因此,我们已经看到了如何将tag变量传递到函数中,现在,我们将它与我们之前构建的sqlite查询中的db.get()一起使用,全部在一行上。 请注意此处单引号和双引号之间的区别。 单引号用于包围标签,如果我们对其进行手工编码,它将仍然存在,双引号允许我们使用变量,就像在php中一样,仅使用加号而不是点来连接。

We could skip logging the error, but its nice to know what's happening in case it breaks. If all goes well, we'll echo the baseName property of the returned object we called row. If you save the file and run lr-latest now, you should see the filename we want.

我们可以跳过记录该错误的步骤,但是很高兴知道发生了什么情况,以防发生错误。 如果一切顺利,我们将回显我们称为row的返回对象的baseName属性。 如果保存文件并立即运行lr-latest ,则应该看到我们想要的文件名。

Our module will just stop when its finished, but it would be best practise to make sure the database we opened is closed as well, so we'll add this at the end of the file:

我们的模块将在完成时停止,但是最好的方法是确保打开的数据库也已关闭,因此我们将其添加到文件末尾:

db.close();

添加更多查询 (Adding more queries)

Ok, before we go further, we have to understand something about Node. It tries to do everything as fast as it can, so you can't specify order. Point in case, try adding this after the db.parallelize() function:

好的,在继续之前,我们必须了解有关Node的一些知识。 它尝试尽快完成所有操作,因此您无法指定顺序。 以防万一,请尝试在db.parallelize()函数之后添加此代码:

console.log('Hi.');

If you run you're code, you'll probably see Hi. before the filename. That's normal with Node, because it takes longer to get a result from the db query.

如果您运行的是代码,则可能会看到Hi. 文件名之前。 这对于Node是很正常的,因为从db查询中获取结果花费的时间更长。

A lot of the time, that's great, but for our particular use case, we'll want a little more control over the order of things. So we'll use the easy-to-grasp async module. On the command line, run:

很多时候,这很棒,但是对于我们的特定用例,我们希望对事物的顺序有更多的控制。 因此,我们将使用易于掌握的异步模块 。 在命令行上,运行:

$npm install async --save

Then, under the hash-bang in the lr-latest.js file, add this line:

然后,在lr-latest.js文件中的hash-bang下,添加以下行:

var async    = require('async');

Now we're cooking. We're going to use the series method, so we can control the order.

现在我们在做饭。 我们将使用series方法,因此我们可以控制顺序。

Change the beginning of our getFile function so it looks like this:

更改我们的getFile函数的开头,如下所示:

function getFile(tag) {var absolutePath, pathFromRoot, baseName, extension, filename;var tasks = [fileAbsolutePath, filePathFromRoot, fileBaseName, fileExtension, buildFilename];async.series(tasks, finish);// this is where the db.paralellize() function is.// you can remove the console.log('Hi.'); line now.
}

What we're doing here is setting up the variables we'll need, then telling async that we want to run a series of functions in order (the names are in the tasks variable), as well as one to finish.

我们在这里要做的是设置所需的变量,然后告诉async我们要按顺序运行一系列功能(名称在tasks变量中),以及要完成的功能。

This will require us to change how we build things though. In this case, the query we already wrote should look a little different:

这将需要我们改变构建事物的方式。 在这种情况下,我们已经编写的查询应该看起来有些不同:

function fileBaseName(cb) {db.parallelize(function(){db.get("SELECT baseName from AgLibraryFile where id_local=(SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='"+tag+"')) AS B ON A.id_local=B.image where pick=1.0))", function(err, row) {if (err){console.log(err);process.exit(2);}else{baseName = row.baseName;cb();}});});
}

The most important change to notice is that we've wrapped our call in a function with a callback. Async functions need to return a callback, in this case we name it cb. If we were really clever, we could use promises here (with a library like bluebird or q), but for now, this works and will pass the baseName variable along the series, and let us use it in our buildFilename function.

需要注意的最重要更改是,我们已将调用包装在带有回调的函数中。 异步函数需要返回一个回调,在这种情况下,我们将其命名为cb 。 如果我们真的很聪明,我们可以在这里使用promise(使用诸如bluebird或q之类的库),但是现在,它可以正常工作,并将在该系列中传递baseName变量,并让我们在buildFilename函数中使用它。

Lets add in the other calls now, since we've covered most of it. The complete getFile() function should look like this:

现在,让我们添加其他呼叫,因为我们已经介绍了大多数呼叫。 完整的getFile()函数应如下所示:

function getFile(tag) {var absolutePath, pathFromRoot, baseName, extension, filename;var tasks = [fileAbsolutePath, filePathFromRoot, fileBaseName, fileExtension, buildFilename];// Each function below is executed in orderasync.series(tasks, finish);function fileAbsolutePath(cb) {db.parallelize(function(){db.get("SELECT absolutePath from AgLibraryRootFolder where id_local=(SELECT rootFolder from AgLibraryFolder where id_local=(SELECT folder from AgLibraryFile where id_local=(SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='"+tag+"')) AS B ON A.id_local=B.image where pick=1.0))))", function(err, row) {if (err){console.log(err);process.exit(2);}else{absolutePath = row.absolutePath;cb();}});});}function filePathFromRoot(cb) {db.parallelize(function(){db.get("SELECT pathFromRoot from AgLibraryFolder where id_local=(SELECT folder from AgLibraryFile where id_local=(SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='"+tag+"')) AS B ON A.id_local=B.image where pick=1.0)))", function(err, row) {if (err){console.log(err);process.exit(2);}else{pathFromRoot = row.pathFromRoot;cb();}});});}function fileBaseName(cb) {db.parallelize(function(){db.get("SELECT baseName from AgLibraryFile where id_local=(SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='"+tag+"')) AS B ON A.id_local=B.image where pick=1.0))", function(err, row) {if (err){console.log(err);process.exit(2);}else{baseName = row.baseName;cb();}});});}function fileExtension(cb) {db.parallelize(function(){db.get("SELECT lc_idx_filenameExtension from AgLibraryFile where id_local=(SELECT rootFile from adobe_images where captureTime=(SELECT max(captureTime) FROM adobe_images AS A JOIN (SELECT image from aglibrarykeywordimage where tag=(SELECT id_local from aglibrarykeyword where lc_name='"+tag+"')) AS B ON A.id_local=B.image where pick=1.0))", function(err, row) {if (err){console.log(err);process.exit(2);}else{extension = row.lc_idx_filenameExtension;cb();}});});}function buildFilename(cb) {filename = absolutePath+pathFromRoot+baseName+'.'+extension;cb();}function finish(err, results){console.log(filename);}};

If you follow along (and trust that the db queries actually work), what should happen is that we find the values of the absolutePath, the pathFromRoot, the baseName, and the file extension, then combine them all into single string and log that string to the console. If you save and run our command, you should see the absolute path of the file we're looking for!

如果您遵循(并相信数据库查询实际上可以工作),应该发生的是我们找到absolutePathpathFromRootbaseName和文件extension ,然后将它们全部组合成单个字符串并记录该字符串到控制台。 如果保存并运行我们的命令,则应该看到我们正在寻找的文件的绝对路径!

本地复制文件 ( Copy File Locally )

Our final step will be to copy the file we found to the directory we're in. To do this we need the exec/child_process function, which is pre-built into Node, so all we have to do is call it.

我们的最后一步是将找到的文件复制到我们所在的目录中。为此,我们需要exec / child_process函数,该函数已预先内置在Node中,因此我们要做的就是调用它。

First, at the top of the file, make sure to include child_process so we can use it further on:

首先,请确保在文件顶部包含child_process以便我们可以在以下位置进一步使用它:

var child_process = require('child_process');

Then, change our finish function so it looks like this:

然后,更改finish函数,使其如下所示:

function finish(err, results){console.log(filename);child_process.exec(['cp '+ filename + ' ' + process.cwd()+'/'+tag+'-latest.jpg'], function(err) {if (err instanceof Error) {console.log(err);process.exit(1);}});
}

exec is Node's way of running simple command-line commands. In our case, that's cp /Absolute/Path/to/Original.jpg /Absolute/Path/to/Current/Directory/tag_name-latest.jpg So, we'll use variables we already have. Make sure you add your own spacing in, such as +' '+. The exec function used to take an array, but it seems like that method is being deprecated, and it prefers to use child_process.exec now.

exec是Node运行简单命令行命令的方式。 在我们的例子中,这就是cp /Absolute/Path/to/Original.jpg /Absolute/Path/to/Current/Directory/tag_name-latest.jpg因此,我们将使用已有的变量。 确保添加自己的间距,例如+' '+exec函数曾经使用过一个数组,但是似乎该方法已被弃用,它现在更喜欢使用child_process.exec

A quick note: Since we're dealing with the async module, we're just going to use exec, but if we were using a promise-based method, we would look into something like spawn.

快速说明:由于我们正在处理async模块,因此我们将只使用exec ,但是如果使用的是基于Promise的方法,我们将研究类似于spawn的东西。

允许多个关键字 ( Allow for Multiple Keywords )

Just knowing where the file lives gives us the ability to do a lot of other things. But before we go too much further, we should rework things to satisfy some of our earlier requirements. Namely, we want to find multiple tags, and also, we should be able to specify those tags with our command, not hard-code them into the module.

只需知道文件在哪里,便可以执行许多其他操作。 但是,在进一步深入之前,我们应该重新做一些事情,以满足我们之前的一些要求。 即,我们要查找多个标签,而且,我们应该能够使用我们的命令指定这些标签,而不是将其硬编码到模块中。

添加命令行参数 (Adding command line arguments)

Thankfully, pulling arguments from the command line is something built into Node, and its quite simple. Replace the var tag = "fantastic"; line with this:

幸运的是,从命令行提取参数是Node内置的功能,而且非常简单。 替换var tag = "fantastic"; 符合此:

var userArgs = process.argv.slice(2);

The process object is the command that was executed, and it contains the argv object, which is an array of the single words that were typed. If we had typed lr-latest big awesome cool, the process.argv object would return:

process对象是已执行的命令,它包含argv对象,该对象是键入的单个单词的数组。 如果我们输入了lr-latest big awesome cool ,则process.argv对象将返回:

['/path/to/Node',
'path/to/lr-latest',
'big',
'awesome',
'cool']

So, we're using slice to pull out everything from the array, starting with the number 2 element (counting from 0).

因此,我们使用slice从数组2中提取所有内容(从0开始)。

So, just like that, we can have user input tags in our module. We'll integrate them below as we also allow for multiple tags.

因此,就像这样,我们可以在模块中包含用户输入标签。 我们将在下面集成它们,因为我们还允许多个标签。

添加多个标签 (Adding Multiple tags)

Just below setting the userArgs variable, replace the getFile(tag) line with this:

在设置userArgs变量的正下方,将getFile(tag)行替换为:

async.each(userArgs, function(tag){getFile(tag);
}, function(err){if( err ) {console.log(err);}
});

This will break down whatever tags were available and run our getFile function with them all. We'll also throw in an error message, just in case.

这将破坏可用的任何标签,并全部使用我们的getFile函数。 为了以防万一,我们还将抛出一条错误消息。

So, if you type lr-latest fantastic into the command line, you should see the filename. If you tried adding another, you'll probably see a gnarly error message. That's because we just logged the error, and because the tag we asked for doesn't exist. Ideally, we'd add a kinder message, but for now, lets fix the error by opening our Lightroom Catalog and adding a new keyword to any of the photos, and setting its flag to Picked.

因此,如果在命令行中键入lr-latest fantastic ,则应该看到文件名。 如果您尝试添加另一个,则可能会看到一则粗糙的错误消息。 那是因为我们只是记录了错误,并且因为我们要求的标签不存在。 理想情况下,我们将添加一条提示消息,但现在,通过打开Lightroom目录并为任何照片添加新关键字,并将其标志设置为Picked来修复错误。

I recommend the keyword 'cat'.

我建议使用关键字“猫”。

Now, if you type lr-latest fantastic cat, you should get back two files (or perhaps the same file twice, in which case, just one).

现在,如果您键入lr-latest fantastic cat ,则应该取回两个文件(或者相同的文件两次,在这种情况下,只有一个)。

One interesting thing you might notice is an error telling you the database is locked. This happens if Lightroom is open in the background, and it is an excellent idea to avoid messing with the database (even read only) while the app that owns it is running.

您可能会注意到的一件有趣的事是告诉您数据库已锁定的错误。 如果Lightroom在后台打开,则会发生这种情况,并且最好避免在拥有它的应用程序运行时弄乱数据库(甚至是只读的)。

Ok! We're amazing! We've got files! But what should we do with them? I suggest doing awesome things.

好! 我们太神奇了! 我们有文件! 但是我们该怎么办呢? 我建议做些很棒的事情。

将图像缩小到合理的尺寸 ( Shrink the image to a reasonable size )

This next part isn't actually using Node. In fact, in our script, we'll just be changing our exec command to run another local command. For this, we'll need to use ImageMagick, which will allow us to convert one image to another through the command line.

下一部分实际上并没有使用Node。 实际上,在脚本中,我们只是将exec命令更改为运行另一个本地命令。 为此,我们需要使用ImageMagick,它将允许我们通过命令行将一个图像convert为另一个图像。

Let's install our tools!

让我们安装我们的工具!

Earlier on I said we'd need Homebrew, and this is why. With a few commands, we're going to make sure we have everything we need. Those commands are:

前面我说过,我们需要Homebrew ,这就是原因。 通过一些命令,我​​们将确保我们拥有所需的一切。 这些命令是:

$    brewinstall libpng jpeg libtiff dcraw little-cms exiv2 freetype webp
--
$    brew install ufraw --with-exiv2
--
$    brew install imagemagick --with-webp

Those get you most of what you need to work with images on the command line. Before we add it to our script, we'll check if it works by running a command directly via the command line:

这些使您获得了在命令行上处理图像所需的大部分功能。 在将其添加到脚本之前,我们将通过直接通过命令行运行命令来检查其是否有效:

convert cat-latest.jpg -resize 500x500 cat-latest-sm.jpg

Now, run the convert command from above once again, and you should end up with a resized image, 500 pixels on its longest side. Let's add the command into our lr-latest.js script and break down what its doing. First, we have to add a new function into our async series called convertImage. Our tasks section should now read:

现在,再次从上方运行convert命令,您应该得到调整后的图像,最长边500像素。 让我们将命令添加到我们的lr-latest.js脚本中,并分解其作用。 首先,我们必须在异步系列中添加一个称为convertImage的新函数。 我们的任务部分现在应显示为:

var tasks = [fileAbsolutePath, filePathFromRoot, fileBaseName, fileExtension, buildFilename, convertImage
];

We will place our new function between buildFilename and finish:

我们将新函数放置在buildFilenamefinish之间:

function convertImage(cb) {child_process.exec(['convert ' + filename + ' -units PixelsPerInch -colorspace sRGB -density 72 -format JPG -quality 80 -resize 500x500 -auto-orient ' + process.cwd()+'/'+tag+'-latest.jpg'], function(err) {if (err instanceof Error) {console.log(err);process.exit(1);}cb();});
}

We've added a few things here, firstly telling ImageMagick that we want to convert our file using Pixels per Inch, using the sRGB colorspace, set at 72dpi, as a jpeg, with 80% quality.

我们在这里添加了一些内容,首先告诉ImageMagick,我们要使用设置为72dpi的sRGB色彩空间(为jpeg)以80%的质量使用每英寸像素数转换文件。

The -resize modifier will resize, not crop, so we're telling it to make it 500 pixels wide, or tall, whichever is larger, and to resize the other dimension proportionally. (so, a 1500x500 image would be resized to 500x100, while a 1000x2000 image would be resized to 250x500).

-resize修饰符将调整大小,而不是裁切,因此我们告诉它使其宽度为500像素或更高(以较大者为准),并按比例调整其他尺寸。 (因此,将1500x500的图像调整为500x100,而将1000x2000的图像调整为250x500)。

We're also tacking on the -auto-orient modifier, because I found that a lot of images taken in portrait (as opposed to landscape), come out sideways. This fixes that, and seems to have no effect on landscape images.

我们还增加了-auto-orient修饰符,因为我发现很多以人像(相对于风景)拍摄的图像都是横向出现的。 这样可以解决此问题,并且似乎对风景图像没有影响。

Take the time to look through ImageMagick's website, there's a lot of amazing things it can do.

花点时间浏览ImageMagick的网站 ,它可以做很多令人惊奇的事情。

Now that that's all set we'll just need to change our finish function so it simply tells us everything is ready:

现在已经准备好了,我们只需要更改finish函数,这样就可以告诉我们一切准备就绪:

function finish(err, results){console.log(filename + ' is ready!');
}

Keep in mind that running the command will overwrite without warning if the file is already in our working directory, but that should be alright, since we're always looking for the latest image. Another benefit of using the convert command is that it will also convert Adobe DNG files (special preview images created for use in a Lightroom Catalog). To do this, we need just needed a few plugins, which we already installed above when we used Homebrew. Raw files like CR2s should be converted ok as well.

请记住,如果文件已经在我们的工作目录中,则运行该命令将覆盖而不发出警告,但这应该没问题,因为我们一直在寻找最新的映像。 使用convert命令的另一个好处是它还将转换Adobe DNG文件(为在Lightroom目录中使用而创建的特殊预览图像)。 为此,我们只需要一些插件,使用Homebrew时我们已经在上面安装了它们。 CR2之类的原始文件也应转换为OK。

设置桌面以使用我们生成的图像 ( Setup the Desktop to use our generated images )

So, the very last thing we wanted to do was set the images dynamically on our Desktop. I mentioned GeekTool previously, which works fine, but I recently found an equally brilliant tool called Übersicht, which does much the same as GeekTool, but with an easier way to build your own widgets.

因此,我们要做的最后一件事是在桌面上动态设置图像。 我之前提到过GeekTool ,它工作正常,但是最近我找到了一个同样出色的工具Übersicht ,它与GeekTool的功能大致相同,但是构建自己的小部件的方法更简单。

In practise, every widget in Übersicht is a simple Coffeescript file with some html and css rules to place the output of the a shell command on the desktop. For our inital attempt, we won't use much of the functionality available at all, just a few simple rules.

实际上,Übersicht中的每个小部件都是一个简单的Coffeescript文件,带有一些html和css规则,可将shell命令的输出放置在桌面上。 对于我们的初始尝试,我们将仅使用一些简单的规则,而不会使用所有可用的功能。

Begin by installing Übersicht, and once you do, click on the icon in your menu-bar and choose Open Widgets Folder. We'll create our widget inside this folder, and it will be automatically loaded without our having to do anything else.

首先安装Übersicht,然后单击菜单栏中的图标,然后选择Open Widgets Folder 。 我们将在此文件夹中创建窗口小部件,它将自动加载,而无需执行任何其他操作。

Create a folder called lr-latest.widget. Inside, we should have a single file called index.coffee. Inside that file, we'll write our code in three sections:

创建一个名为lr-latest.widget的文件夹。 在内部,我们应该有一个名为index.coffee文件。 在该文件中,我们将分三部分编写代码:

# Execute the shell command.
command: ""# Set the refresh frequency (milliseconds).
refreshFrequency: 86400000

Ideally, we would use this first section to run our lr-latest command with the keywords we want, but we're just going to leave it blank for now, so we can see some results right away.

理想情况下,我们将使用第一部分来运行带有所需关键字的lr-latest命令,但现在暂时将其保留为空白,以便立即看到一些结果。

# Render the output.
render: (output) -> """<div><img src="./lr-latest.widget/fantastic-latest.jpg"><img src="./lr-latest.widget/cat-latest.jpg"></div>
"""

In the next section, we're rendering the output. This is where we would make a fancy loop in coffescript to allow for any number of images.

在下一节中,我们将呈现输出。 在这里,我们可以在咖啡中制作精美的循环,以容纳任意数量的图像。

/* CSS Style */
style: """margin:0padding:0pxwidth: 100%text-align: centertop: 60px;imgwidth: 500pxheight: automargin: 0 20pxh1font-size: 24px
"""

Finally, we set up some simple styling with Compass, placing our two images next to each other near the top of the desktop. One of the things I love about Übersicht is how easy it is to move your widgets around with simple css rules.

最后,我们使用Compass设置了一些简单的样式,将我们的两个图像在桌面顶部附近彼此相邻放置。 我最喜欢Übersicht的一件事是,使用简单CSS规则移动小部件非常容易。

The widget will refresh every 86400000 miliseconds, also known as 1 day, but you can just save the index.coffee file, and it wil reload right away. Finally, to test things, just run 'lr-latest fantastic cat' from inside the lr-latest.widget folder (remember, you can type cd [space] and drag-and-drop the folder into the terminal to quickly navigate to it).

该小部件将每86400000毫秒刷新一次,也称为1天,但您只需保存index.coffee文件,即可立即重新加载。 最后,要进行测试,只需从lr-latest.widget文件夹中运行“ lr-latest lr-latest.widget cat”(请记住,您可以键入cd [space]并将该文件夹拖放到终端中以快速导航至该文件夹)。

And there we have it, two nicely positioned images fixed to our desktop. Congratulations!

在那里,我们将两个定位良好的图像固定在我们的桌面上。 恭喜你!

奖金回合 ( Bonus Round )

Ok, I don't feel right leaving it like that, so I took the time to learn a bit of Coffeescript and get things working correctly.

好的,我不喜欢这样离开,所以我花时间学习一些Coffeescript并使事情正常进行。

We'll start by slightly changing things around with the assumption that our node module will be called via Übersicht. The only things we have to do is be sure we're logging the filenames, and that the convert command is put in using its absolute path. This is how our final two functions inside getFile look now:

我们将从稍微改变一些情况开始,假设我们的节点模块将通过Übersicht调用。 我们要做的唯一一件事情就是确保我们记录了文件名,并且使用其绝对路径放入了convert命令。 这就是getFile最后两个函数的外观:

function convertImage(cb) {child_process.exec(['/usr/local/bin/convert ' + filename + ' -units PixelsPerInch -colorspace sRGB -density 72 -format JPG -quality 80 -resize 500x500 -auto-orient "' + process.cwd()+'/lr-latest.widget/'+tag+'-latest.jpg"'], function(err) {if (err instanceof Error) {console.log(err);process.exit(1);}cb();});
}function finish(err){console.log('./lr-latest.widget/'+ tag +'-latest.jpg,');
}

First, we changed convert to /usr/local/bin/convert, and you can easily find the path for that using the command which convert. This is also how the absolute paths should be found for node and lr-latest command in the widget file below.

首先,我们将convert更改为/usr/local/bin/convert ,您可以使用which convert命令轻松找到该路径。 这也是在下面的小部件文件中应为nodelr-latest命令找到绝对路径的方式。

We changed the location of the output file slightly, since Übersicht runs its files from the base of its widget folder. We also added /lr-latest.widget to store our images inside our widget's folder, for cleanliness sake.

由于Übersicht从其小部件文件夹的底部运行其文件,因此我们略微更改了输出文件的位置。 为了/lr-latest.widget ,我们还添加了/lr-latest.widget将图像存储在小部件的文件夹中。

Finally, we changed our finish function to simply log the relative path to the image from the Übersicht base widget folder. Notice that there is a trailing comma at the end of the filename: ...-latest.jpg,. This way, we can trick Übersicht into thinking our output is an array.

最后,我们更改了finish函数,以简单地记录Übersicht基本窗口小部件文件夹中图像的相对路径。 请注意,文件名末尾带有逗号: ...-latest.jpg, 这样,我们可以欺骗Übersicht认为我们的输出是一个数组。

Lastly, here's our new index.coffee file inside lr-latest.widget:

最后,这是lr-latest.widgetindex.coffee文件:

# Execute the shell command.
command: """/usr/local/bin/node /usr/local/bin/lr-latest fantastic cat"""# Set the refresh frequency (milliseconds).
refreshFrequency: 86400000# Render the output.
render: (output) -> """<div id='images'><div>
"""
update: (output, domEl) ->container = $(domEl).find('#images')$(container).html("")images = output.toString().split(",")wrap = (image) -> $(container).append """<img src="#{image}" />"""wrap image for image in images# CSS Style
style: """margin:0padding:0pxwidth: 100%text-align: centertop: 60pximgwidth: 500pxheight: automargin: 0 20pxh1font-size: 24px
"""

With those simple changes, you shouldn't have to manually type out the commands anymore, just keep Übersicht running, and it will refresh the photos every day, automatically. Success and Joy!

通过这些简单的更改,您不必再手动键入命令,只需保持Übersicht运行,它就会每天自动刷新照片。 成功与喜悦!

Thanks for following along, I hope this encourages you to explore the software you use, and to push the boundaries of what you think you're comfortable doing with code!

感谢您的关注,我希望这能鼓励您探索使用的软件,并突破您认为对代码的理解的界限!

翻译自: https://scotch.io/tutorials/build-a-custom-photo-widget-for-your-desktop-with-node-js

node.js 自定义类库

node.js 自定义类库_使用Node.js为您的桌面构建自定义照片小部件相关推荐

  1. iphone 屏幕_如何在iPhone的主屏幕上添加和删除小部件

    iphone 屏幕 Khamosh Pathak Khamosh Pathak Apple brought widgets to the iPhone's Home screen with iOS 1 ...

  2. node.js编写网页_为Node.js编写可扩展架构

    node.js编写网页 by Zafar Saleem 通过Zafar Saleem 为Node.js编写可扩展架构 (Writing Scalable Architecture For Nodejs ...

  3. node.js中模块_在Node.js中需要模块:您需要知道的一切

    node.js中模块 by Samer Buna 通过Samer Buna 在Node.js中需要模块:您需要知道的一切 (Requiring modules in Node.js: Everythi ...

  4. js list操作_使用 Node.js 实现一个命令行 todo-list(1)- 基本功能

    功能介绍 为了熟悉 Node.js,使用 Node.js 制作一个命令行小工具,项目仓库:https://github.com/FuZhouJohn/node-todo,先来介绍一下功能: 添加任务: ...

  5. Node.js Web开发_设置Node.js(1)

    电子书推荐 Multithreaded JavaScript: Concurrency Beyond the Event Loop Computers For Seniors For Dummies, ...

  6. js模板字符串自定义类名_【Vue.js 入门到实战教程】07Vue 组件注册 | 基本使用和组件嵌套...

    来源 | https://xueyuanjun.com/post/21929除了前面介绍的基本语法之外,Vue.js 还支持通过组件构建复杂的功能模块,组件可以称得上是 Vue.js 的灵魂,是 Vu ...

  7. node开启子线程_真Node多线程

    本文测试使用环境: 系统:macOS Mojave 10.14.2 CPU:4 核 2.3 GHz Node: 10.15.1 从 Node 线程说起 一般人理解 Node 是单线程的,所以 Node ...

  8. js input 自动换行_深入Slate.js - 拯救 ContentEditble

    我们是钉钉的文档协同团队,我们在做一些很有意义的事情,其中之一就是自研的文字编辑器.为了把自研文字编辑器做好,我们调研了开源社区各种优秀编辑器,Slate.js 是其中之一(实际上,自研文字编辑器前, ...

  9. 桌面小部件_适用于您网站的25个最有用的WordPress小部件

    桌面小部件 Are you looking for useful WordPress widgets for your website? 您是否正在为您的网站寻找有用的WordPress小部件? Wi ...

  10. electron 打包把node代理服务打包进去_用 Node.js 官方镜像打包一个 express 服务

    最近感觉确实有必要了解一下 Docker,在 CI/CD 这个阶段很有用,所以记录一下. 先放上 Node.js 的官方 Docker 镜像. Docker Hub​hub.docker.com 需要 ...

最新文章

  1. C# 获取电脑的网络连接状态
  2. Python通过urllib访问网页
  3. 黄聪:visual studio 2017编译运行出现脚本发生错误等问题如何解决?
  4. SAP Spartacus 自定义 theme 实现思路
  5. ios 如何获得系统时间和日期
  6. 龙族幻想服务器维护中怎么办,龙族幻想遇到无法连接服务器?两招教你轻松解决...
  7. Flask 区域块简单原理实现
  8. 实战:自定义脚本启动mysql-mysql 5.6.19
  9. IP记录Linux所有用户操作日志的方法(附脚本)
  10. 删除顽固node_modules
  11. css如何设置div中的内容垂直居中?
  12. 数学建模论文写作要求
  13. 电视剧的收视排行榜(Python)
  14. throw与throws的区别
  15. 什么是软件测试,软件测试的目的?
  16. 新版HCIE考试有什么题型?各题型具体怎么考?
  17. 云桌面和云计算之间的区别是什么
  18. 视频直播源码 直播软件搭建进行直播推流时,对声音进行音质优化
  19. mysql error 1044
  20. 微信扫码支付功能(2)---用户扫码支付成功,微信异步回调商户接口

热门文章

  1. 松下伺服电机uvw接线图_松下Panasonic伺服电机电源线选择与接线方法
  2. 新路由3 newifi3 官方固件与离线插件合集
  3. laxTex 手写体
  4. 泛函分析 04.03 有界线性算子 - 一致有界原则
  5. 实在小店在B2C平台具有一定的知名度
  6. 尚学堂马士兵Oracle学习笔记之一:基本select语句
  7. 三极管开关电路_利用三极管设计开关电路
  8. PCBLayout相关注意事项和常见问题
  9. 网络战武器——震网(Stuxnet)病毒
  10. Android逆向Unity3D——XXX快跑破解