Запись и чтение файлов. Nodejs

Всем привет! В этой статье мы рассмотрим, как записывать и читать файлы в NodeJS .

Платформа NodeJS позволяет записывать и читать файлы в операционной системе. Для этого нам потребуется использовать модуль FS (file system ).

Var fs = require("fs");

Для демонстрации считывания содержимого файлов давайте создадим файлик с названием readme.txt .

// содержимое файла readme.txt
Здесь какое-нибудь содержимое файла

Var text = fs.readFileSync("readme.txt", "utf8");
console.log(text);

Мы используем метод, в который передаем первым параметром имя файла, а вторым – кодировку. Как понятно из названия, этот метод является синхронным. Это значит, что весь код, который идет ниже, выполнится только тогда, когда весь файл будет прочитан. Дальше мы просто записываем полученные данные в переменную, которую потом выводим на экран.

Теперь давайте попробуем считанное нами содержание файла записать в новый файл. Для этого напишем следующее:

Fs.writeFileSync("writeme.txt", text);

Теперь после запуска кода вы увидите, что создался новый файлик с названием writeme.txt , в котором будет содержимое, записанное в переменную text из файла readme.txt .

Давайте рассмотрим, как использовать методы асинхронно. Например, считаем файлик readme.txt :


console.log(data);
});

Console.log("выведется раньше, чем данные из файла");

Использование почти такое же, но теперь мы также третьим параметром передаем функцию, где в качестве аргументов первым идет ошибка, а вторым содержимое файла, которое мы потом и выводим. Ниже я написал еще один вывод текста, чтобы показать вам, что метод действительно асинхронный, поэтому, пока идет считывание файла, выполнится код ниже, а только потом выведется текст из файлика.

Теперь давайте снова считаем содержимое файла readme.txt и запишем его в файл writeme.txt , но только теперь асинхронно.

Fs.readFile("readme.txt", "utf8", function(error, data) {
fs.writeFile("writeme.txt", data);
});

А на этом у меня сегодня все. Спасибо за внимание!

  • Перевод

Сегодня, в девятой части перевода руководства по Node.js, мы поговорим о работе с файлами. В частности, речь пойдёт о модулях fs и path - о файловых дескрипторах, о путях к файлам, о получении информации о файлах, об их чтении и записи, о работе с директориями.

Работа с файловыми дескрипторами в Node.js

Прежде чем вы сможете взаимодействовать с файлами, находящимися в файловой системе вашего сервера, вам необходимо получить дескриптор файла.

Дескриптор можно получить, воспользовавшись для открытия файла асинхронным методом open() из модуля fs:

Const fs = require("fs") fs.open("/Users/flavio/test.txt", "r", (err, fd) => { //fd - это дескриптор файла })
Обратите внимание на второй параметр, r , использованный при вызове метода fs.open() . Это - флаг, который сообщает системе о том, что файл открывают для чтения. Вот ещё некоторые флаги, которые часто используются при работе с этим и некоторыми другими методами:

  • r+ - открыть файл для чтения и для записи.
  • w+ - открыть файл для чтения и для записи, установив указатель потока в начало файла. Если файл не существует - он создаётся.
  • a - открыть файл для записи, установив указатель потока в конец файла. Если файл не существует - он создаётся.
  • a+ - открыть файл для чтения и записи, установив указатель потока в конец файла. Если файл не существует - он создаётся.
Файлы можно открывать и пользуясь синхронным методом fs.openSync() , который, вместо того, чтобы предоставить дескриптор файла в коллбэке, возвращает его:

Const fs = require("fs") try { const fd = fs.openSync("/Users/flavio/test.txt", "r") } catch (err) { console.error(err) }
После получения дескриптора любым из вышеописанных способов вы можете производить с ним необходимые операции.

Данные о файлах

С каждым файлом связан набор данных о нём, исследовать эти данные можно средствами Node.js. В частности, сделать это можно, используя метод stat() из модуля fs .

Вызывают этот метод, передавая ему путь к файлу, и, после того, как Node.js получит необходимые сведения о файле, он вызовет коллбэк, переданный методу stat() . Вот как это выглядит:

Const fs = require("fs") fs.stat("/Users/flavio/test.txt", (err, stats) => { if (err) { console.error(err) return } //сведения о файле содержатся в аргументе `stats` })
В Node.js имеется возможность синхронного получения сведений о файлах. При таком подходе главный поток блокируется до получения свойств файла:

Const fs = require("fs") try { const stats = fs.statSync ("/Users/flavio/test.txt") } catch (err) { console.error(err) }
Информация о файле попадёт в константу stats . Что это за информация? На самом деле, соответствующий объект предоставляет нам большое количество полезных свойств и методов:

  • Методы.isFile() и.isDirectory() позволяют, соответственно, узнать, является ли исследуемый файл обычным файлом или директорией.
  • Метод.isSymbolicLink() позволяет узнать, является ли файл символической ссылкой.
  • Размер файла можно узнать, воспользовавшись свойством.size .
Тут имеются и другие методы, но эти - самые употребимые. Вот как ими пользоваться:

Const fs = require("fs") fs.stat("/Users/flavio/test.txt", (err, stats) => { if (err) { console.error(err) return } stats.isFile() //true stats.isDirectory() //false stats.isSymbolicLink() //false stats.size //1024000 //= 1MB })

Пути к файлам в Node.js и модуль path

Путь к файлу - это адрес того места в файловой системе, где он расположен.

В Linux и macOS путь может выглядеть так:

/users/flavio/file.txt
В Windows пути выглядят немного иначе:

C:\users\flavio\file.txt
На различия в форматах записи путей при использовании разных операционных систем следует обращать внимание, учитывая операционную систему, используемую для развёртывания Node.js-сервера.

В Node.js есть стандартный модуль path , предназначенный для работы с путями к файлам. Перед использованием этого модуля в программе его надо подключить:

▍Получение информации о пути к файлу

Если у вас есть путь к файлу, то, используя возможности модуля path , вы можете, в удобном для восприятия и дальнейшей обработки виде, узнать подробности об этом пути. Выглядит это так:

Const notes = "/users/flavio/notes.txt" path.dirname(notes) // /users/flavio path.basename(notes) // notes.txt path.extname(notes) // .txt
Здесь, в строке notes , хранится путь к файлу. Для разбора пути использованы следующие методы модуля path:

  • dirname() - возвращает родительскую директорию файла.
  • basename() - возвращает имя файла.
  • extname() - возвращает расширение файла.
Узнать имя файла без расширения можно, вызвав метод.basename() и передав ему второй аргумент, представляющий расширение:

Path.basename(notes, path.extname(notes)) //notes

▍Работа с путями к файлам

Несколько частей пути можно объединить, используя метод path.join() :

Const name = "flavio" path.join("/", "users", name, "notes.txt") //"/users/flavio/notes.txt"
Найти абсолютный путь к файлу на основе относительного пути к нему можно с использованием метода path.resolve() :

Path.resolve("flavio.txt") //"/Users/flavio/flavio.txt" при запуске из моей домашней папки
В данном случае Node.js просто добавляет /flavio.txt к пути, ведущем к текущей рабочей директории. Если при вызове этого метода передать ещё один параметр, представляющий путь к папке, метод использует его в качестве базы для определения абсолютного пути:

Path.resolve("tmp", "flavio.txt") // "/Users/flavio/tmp/flavio.txt" при запуске из моей домашней папки
Если путь, переданный в качестве первого параметра, начинается с косой черты - это означает, что он представляет собой абсолютный путь.

Path.resolve("/etc", "flavio.txt") // "/etc/flavio.txt"
Вот ещё один полезный метод - path.normalize() . Он позволяет найти реальный путь к файлу, используя путь, в котором содержатся спецификаторы относительного пути вроде точки (.), двух точек (..), или двух косых черт:

Path.normalize("/users/flavio/..//test.txt") // /users/test.txt
Методы resolve() и normalize() не проверяют существование директории. Они просто находят путь, основываясь на переданным им данным.

Чтение файлов в Node.js

Самый простой способ чтения файлов в Node.js заключается в использовании метода fs.readFile() с передачей ему пути к файлу и коллбэка, который будет вызван с передачей ему данных файла (или объекта ошибки):

Fs.readFile("/Users/flavio/test.txt", (err, data) => { if (err) { console.error(err) return } console.log(data) })
Если надо, можно воспользоваться синхронной версией этого метода - fs.readFileSync() :

Const fs = require("fs") try { const data = fs.readFileSync("/Users/flavio/test.txt") console.log(data) } catch (err) { console.error(err) }
По умолчанию при чтении файлов используется кодировка utf8 , но кодировку можно задать и самостоятельно, передав методу соответствующий параметр.

Методы fs.readFile() и fs.readFileSync() считывают в память всё содержимое файла. Это означает, что работа с большими файлами с применением этих методов серьёзно отразится на потреблении памяти вашим приложением и окажет влияние на его производительность. Если с такими файлами нужно работать, лучше всего воспользоваться потоками.

Запись файлов в Node.js

В Node.js легче всего записывать файлы с использованием метода fs.writeFile() :

Const fs = require("fs") const content = "Some content!" fs.writeFile("/Users/flavio/test.txt", content, (err) => { if (err) { console.error(err) return } //файл записан успешно })
Есть и синхронная версия того же метода - fs.writeFileSync() :

Const fs = require("fs") const content = "Some content!" try { const data = fs.writeFileSync("/Users/flavio/test.txt", content) //файл записан успешно } catch (err) { console.error(err) }
Эти методы, по умолчанию, заменяют содержимое существующих файлов. Изменить их стандартное поведение можно, воспользовавшись соответствующим флагом:

Fs.writeFile("/Users/flavio/test.txt", content, { flag: "a+" }, (err) => {})
Тут могут использоваться флаги, которые мы уже перечисляли в разделе, посвящённом дескрипторам. Подробности о флагах можно узнать .

Присоединение данных к файлу

Метод fs.appendFile() (и его синхронную версию - fs.appendFileSync()) удобно использовать для присоединения данных к концу файла:

Const content = "Some content!" fs.appendFile("file.log", content, (err) => { if (err) { console.error(err) return } //готово! })

Об использовании потоков

Выше мы описывали методы, которые, выполняя запись в файл, пишут в него весь объём переданных им данных, после чего, если используются их синхронные версии, возвращают управление программе, а если применяются асинхронные версии - вызывают коллбэки. Если вас такое состояние дел не устраивает - лучше будет воспользоваться потоками.

Работа с директориями в Node.js

Модуль fs предоставляет в распоряжение разработчика много удобных методов, которые можно использовать для работы с директориями.

▍Проверка существования папки

Для того чтобы проверить, существует ли директория и может ли Node.js получить к ней доступ, учитывая разрешения, можно использовать метод fs.access() .

▍Создание новой папки

Для того чтобы создавать новые папки, можно воспользоваться методами fs.mkdir() и fs.mkdirSync() :

Const fs = require("fs") const folderName = "/Users/flavio/test" try { if (!fs.existsSync(dir)){ fs.mkdirSync(dir) } } catch (err) { console.error(err) }

▍Чтение содержимого папки

Для того чтобы прочесть содержимое папки, можно воспользоваться методами fs.readdir() и fs.readdirSync() . В этом примере осуществляется чтение содержимого папки - то есть - сведений о том, какие файлы и поддиректории в ней имеются, и возврат их относительных путей:

Const fs = require("fs") const path = require("path") const folderPath = "/Users/flavio" fs.readdirSync(folderPath)
Вот так можно получить полный путь к файлу:

Fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName) }
Результаты можно отфильтровать для того, чтобы получить только файлы и исключить из вывода директории:

Const isFile = fileName => { return fs.lstatSync(fileName).isFile() } fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName)).filter(isFile) }

▍Переименование папки

Для переименования папки можно воспользоваться методами fs.rename() и fs.renameSync() . Первый параметр - это текущий путь к папке, второй - новый:

Const fs = require("fs") fs.rename("/Users/flavio", "/Users/roger", (err) => { if (err) { console.error(err) return } //готово })
Переименовать папку можно и с помощью синхронного метода fs.renameSync() :

Const fs = require("fs") try { fs.renameSync("/Users/flavio", "/Users/roger") } catch (err) { console.error(err) }

▍Удаление папки

Для того чтобы удалить папку, можно воспользоваться методами fs.rmdir() или fs.rmdirSync() . Надо отметить, что удаление папки, в которой что-то есть, задача несколько более сложная, чем удаление пустой папки. Если вам нужно удалять такие папки, воспользуйтесь пакетом fs-extra , который весьма популярен и хорошо поддерживается. Он представляет собой замену модуля fs , расширяющую его возможности.

Метод remove() из пакета fs-extra умеет удалять папки, в которых уже что-то есть.

Установить этот модуль можно так:

Npm install fs-extra
Вот пример его использования:

Const fs = require("fs-extra") const folder = "/Users/flavio" fs.remove(folder, err => { console.error(err) })
Его методами можно пользоваться в виде промисов:

Fs.remove(folder).then(() => { //готово }).catch(err => { console.error(err) })
Допустимо и применение конструкции async/await:

Async function removeFolder(folder) { try { await fs.remove(folder) //готово } catch (err) { console.error(err) } } const folder = "/Users/flavio" removeFolder(folder)

Модуль fs

Выше мы уже сталкивались с некоторыми методами модуля fs , применяемыми при работе с файловой системой. На самом деле, он содержит ещё много полезного. Напомним, что он не нуждается в установке, для того, чтобы воспользоваться им в программе, его достаточно подключить:

Const fs = require("fs")
После этого у вас будет доступ к его методам, среди которых отметим следующие, некоторые из которых вам уже знакомы:

  • fs.access() : проверяет существование файла и возможность доступа к нему с учётом разрешений.
  • fs.appendFile() : присоединяет данные к файлу. Если файл не существует - он будет создан.
  • fs.chmod() : изменяет разрешения для заданного файла. Похожие методы: fs.lchmod() , fs.fchmod() .
  • fs.chown() : изменяет владельца и группу для заданного файла. Похожие методы: fs.fchown() , fs.lchown() .
  • fs.close() : закрывает дескриптор файла.
  • fs.copyFile() : копирует файл.
  • fs.createReadStream() : создаёт поток чтения файла.
  • fs.createWriteStream() : создаёт поток записи файла.
  • fs.link() : создаёт новую жёсткую ссылку на файл.
  • fs.mkdir() : создаёт новую директорию.
  • fs.mkdtemp() : создаёт временную директорию.
  • fs.open() : открывает файл.
  • fs.readdir() : читает содержимое директории.
  • fs.readFile() : считывает содержимое файла. Похожий метод: fs.read() .
  • fs.readlink() : считывает значение символической ссылки.
  • fs.realpath() : разрешает относительный путь к файлу, построенный с использованием символов. и.. , в полный путь.
  • fs.rename() : переименовывает файл или папку.
  • fs.rmdir() : удаляет папку.
  • fs.stat() : возвращает сведения о файле. Похожие методы: fs.fstat() , fs.lstat() .
  • fs.symlink() : создаёт новую символическую ссылку на файл.
  • fs.truncate() : обрезает файл до заданной длины. Похожий метод: fs.ftruncate() .
  • fs.unlink() : удаляет файл или символическую ссылку.
  • fs.unwatchFile() : отключает наблюдение за изменениями файла.
  • fs.utimes() : изменяет временную отметку файла. Похожий метод: fs.futimes() .
  • fs.watchFile() : включает наблюдение за изменениями файла. Похожий метод: fs.watch() .
  • fs.writeFile() : записывает данные в файл. Похожий метод: fs.write() .
Интересной особенностью модуля fs является тот факт, что все его методы, по умолчанию, являются асинхронными, но существуют и их синхронные версии, имена которых получаются путём добавления слова Sync к именам асинхронных методов.

Например:

  • fs.rename()
  • fs.renameSync()
  • fs.write()
  • fs.writeSync()
Использование синхронных методов серьёзно влияет на то, как работает программа.

В Node.js 10 имеется экспериментальная поддержка этих API , основанных на промисах.

Исследуем метод fs.rename() . Вот асинхронная версия этого метода, использующая коллбэки:

Const fs = require("fs") fs.rename("before.json", "after.json", (err) => { if (err) { return console.error(err) } //готово })
При использовании его синхронной версии для обработки ошибок используется конструкция try/catch:

Const fs = require("fs") try { fs.renameSync("before.json", "after.json") //готово } catch (err) { console.error(err) }
Основное различие между этими вариантами использования данного метода заключается в том, что во втором случае выполнение скрипта будет заблокировано до завершения файловой операции.

Модуль path

Модуль path, о некоторых возможностях которого мы тоже уже говорили, содержит множество полезных инструментов, позволяющих взаимодействовать с файловой системой. Как уже было сказано, устанавливать его не нужно, так как он является частью Node.js. Для того чтобы пользоваться им, его достаточно подключить:

Const path = require("path")
Свойство path.sep этого модуля предоставляет символ, использующийся для разделения сегментов пути (\ в Windows и / в Linux и macOS), а свойство path.delimiter даёт символ, используемый для отделения друг от друга нескольких путей (; в Windows и: в Linux и macOS).

Рассмотрим и проиллюстрируем примерами некоторые методы модуля path .

▍path.basename()

Возвращает последний фрагмент пути. Передав второй параметр этому методу можно убрать расширение файла.

Require("path").basename("/test/something") //something require("path").basename("/test/something.txt") //something.txt require("path").basename("/test/something.txt", ".txt") //something

▍path.dirname()

Возвращает ту часть пути, которая представляет имя директории:

Require("path").dirname("/test/something") // /test require("path").dirname("/test/something/file.txt") // /test/something

▍path.extname()

Возвращает ту часть пути, которая представляет расширение файла:

Require("path").extname("/test/something") // "" require("path").extname("/test/something/file.txt") // ".txt"

▍path.isAbsolute()

Возвращает истинное значение если путь является абсолютным:

Require("path").isAbsolute("/test/something") // true require("path").isAbsolute("./test/something") // false

▍path.join()

Соединяет несколько частей пути:

Const name = "flavio" require("path").join("/", "users", name, "notes.txt") //"/users/flavio/notes.txt"

▍path.normalize()

Пытается выяснить реальный путь на основе пути, который содержит символы, использующиеся при построении относительных путей вроде. , .. и // :

Require("path").normalize("/users/flavio/..//test.txt") ///users/test.txt

▍path.parse()

Преобразует путь в объект, свойства которого представляют отдельные части пути:
  • root: корневая директория.
  • dir: путь к файлу, начиная от корневой директории
  • base: имя файла и расширение.
  • name: имя файла.
  • ext: расширение файла.
Вот пример использования этого метода:

Require("path").parse("/users/test.txt")
В результате его работы получается такой объект:

{ root: "/", dir: "/users", base: "test.txt", ext: ".txt", name: "test" }

▍path.relative()

Принимает, в качестве аргументов, 2 пути. Возвращает относительный путь из первого пути ко второму, основываясь на текущей рабочей директории:

Require("path").relative("/Users/flavio", "/Users/flavio/test.txt") //"test.txt" require("path").relative("/Users/flavio", "/Users/flavio/something/test.txt") //"something/test.txt"

▍path.resolve()

Находит абсолютный путь на основе переданного ему относительного пути:

Path.resolve("flavio.txt") //"/Users/flavio/flavio.txt" при запуске из моей домашней папки.

Итоги

Сегодня мы рассмотрели модули Node.js fs и path , которые используются для работы с файловой системой. В следующей части этой серии, на которой она завершается, мы обсудим модули os , events , http , поговорим о работе с потоками и с системами управления базами данных в Node.js.

Уважаемые читатели! Какими npm-пакетами вы пользуетесь при работе с файловой системой в Node.js?

Ученик спросил: «Программисты встарь использовали только простые компьютеры и программировали без языков, но они делали прекрасные программы. Почему мы используем сложные компьютеры и языки программирования?». Фу-Тзу ответил: «Строители встарь использовали только палки и глину, но они делали прекрасные хижины».

Мастер Юан-Ма, «Книга программирования»

На текущий момент вы учили язык JavaScript и использовали его в единственном окружении: в браузере. В этой и следующей главе мы кратко представим вам Node.js, программу, которая позволяет применять навыки JavaScript вне браузера. С ней вы можете написать всё, от утилит командной строки до динамических HTTP серверов.

Эти главы посвящены обучению важным идеям, составляющим Node.js и предназначены для передачи вам достаточного количества информации, чтобы вы могли писать полезные программы в этой среде. Они не пытаются быть всеобъемлющими справочниками по Node.

Код из предыдущих глав вы могли писать и исполнять прямо в браузере, но код из этой главы написан для Node и в браузере работать не будет.

Если вы хотите сразу запускать код из этой главы, начните с установки Node с сайта nodejs.org для вашей операционки. Также на этом сайте вы найдёте документацию по Node и его встроенным модулям.

Вступление

Одна из наиболее сложных проблем при написании систем, общающихся по сети – обработка ввода и вывода. Чтение и запись данных в сеть и из сети, на диск, и другие устройства. Перемещение данных требует времени, и грамотное планирование этих действий может сильно повлиять на время отклика системы для пользователя или сетевых запросов.

В традиционном методе обработки ввода и вывода принято, что функция, к примеру, readFile, начинает читать файл и возвращается только когда файл полностью прочитан. Это называется синхронным вводом-выводом (synchronous I/O, input/output).

Node был задуман с целью облегчить и упростить использование асинхронного I/O. Мы уже встречались с асинхронными интерфейсами, такими, как объект браузера XMLHttpRequest, обсуждавшийся в главе 17. Такой интерфейс позволяет скрипту продолжать работу, пока интерфейс делает свою, и вызывает функцию обратного вызова по окончанию работы. Таким образом в Node работает весь I/O.

JavaScript легко вписывается в систему типа Node. Это один из немногих языков, в которые не встроена система I/O. Поэтому JavaScript легко встраивается в довольно эксцентричный подход к I/O в Node и в результате не порождает две разных системы ввода и вывода. В 2009 году при разработке Node люди уже использовали I/O в браузере, основанный на обратных вызовах, поэтому сообщество вокруг языка было привычно к асинхронному стилю программирования.

Асинхронность

Попробую проиллюстрировать разницу в синхронном и асинхронном подходах в I/O на небольшом примере, где программа должна получить два ресурса из интернета, и затем сделать что-то с данными.

В синхронном окружении очевидным способом решения задачи будет сделать запросы последовательно. У этого метода есть минус – второй запрос начнётся только после окончания первого. Общее время будет не меньше, чем сумма времени на обработку двух запросов. Это неэффективное использование компьютера, который большую часть времени будет простаивать, пока происходит передача данных по сети.

Решение проблемы в синхронной системе – запуск дополнительных потоков контроля исполнения программы (в главе 14 мы их уже обсуждали). Второй поток может запустить второй запрос, и затем оба потока будут ждать возврата результата, после чего они заново будут синхронизированы для сведения работы в один результат.

На диаграмме жирные линии обозначают время нормальной работы программы, а тонкие – время ожидания I/O. В синхронной модели время, затраченное на I/O, входит во временной график каждого из потоков. В асинхронной, запуск действия по I/O приводит к разветвлению временной линии. Поток, запустивший I/O, продолжает выполнение, а I/O выполняется параллельно ему, по окончанию работы делая обратный вызов функции.

Поток выполнения программы для синхронного и асинхронного I/O

Ещё один способ выразить эту разницу: в синхронной модели ожидание окончания I/O неявное, а в асинхронной – явное, и находится под нашим непосредственным контролем. Но асинхронность работает в обе стороны. С её помощью выражать программы, не работающие по принципу прямой линии, проще, но выражать прямолинейные программы становится сложнее.

В главе 17 я уже касался того факта, что обратные вызовы привносят кучу шума и делают программу менее упорядоченной. Является ли такой подход в общем хорошей идеей – спорный вопрос. В любом случае, требуется время, чтобы привыкнуть к нему.

Но для системы, основанной на JavaScript, я бы сказал, что использование асинхронности с обратными вызовами имеет смысл. Одна из сильных сторон JavaScript – простота, и попытки добавить в программу несколько потоков привели бы к сильному усложнению. Хотя обратные вызовы не делают код простым, их идея очень проста и в то же время достаточно сильна для того, чтобы писать высокопроизводительные веб-серверы.

Команда node

Когда в вашей системе установлен Node.js, у вас появляется программа под названием node, которая запускает файлы JavaScript. Допустим, у вас есть файл hello.js со следующим кодом:

Var message = "Hello world"; console.log(message);

Вы можете выполнить свою программу из командной строки:

$ node hello.js Hello world

Метод console.log в Node действует так же, как в браузере. Выводит кусок текста. Но в Node текст выводится на стандартный вывод, а не в консоль JavaScript в браузере.

Если запустить node без файла, он выдаст вам строку запроса, в которой можно писать код на JavaScript и получать результат.

$ node > 1 + 1 2 > [-1, -2, -3].map(Math.abs) > process.exit(0) $

Переменная process, так же как и console, доступна в Node глобально. Она обеспечивает несколько способов для инспектирования и манипулирования программой. Метод exit заканчивает процесс, и ему можно передать код статуса окончания программы, который сообщает программе, запустившей node (в данном случае, программной оболочке), завершилась ли программа удачно (нулевой код) или с ошибкой (любое другое число).

Для доступа к аргументам командной строки, переданным программе, можно читать массив строк process.argv. В него также включены имя команды node и имя вашего скрипта, поэтому список аргументов начинается с индекса 2. Если файл showargv.js содержит только инструкцию console.log(process.argv), его можно запустить так:

$ node showargv.js one --and two ["node", "/home/marijn/showargv.js", "one", "--and", "two"]

Все стандартные глобальные переменные JavaScript - Array, Math, JSON, также есть в окружении Node. Но там отсутствует функционал, связанный с работой браузера, например document или alert.

Объект глобальной области видимости, который в браузере называется window, в Node имеет более осмысленное название global.

Модули

Кроме нескольких упомянутых переменных, вроде console и process, Node держит мало функционала в глобальной области видимости. Для доступа к остальным встроенным возможностям вам надо обращаться к системе модулей.

Система CommonJS, основанная на функции require, была описана в главе 10. Такая система встроена в Node и используется для загрузки всего, от встроенных модулей и скачанных библиотек до файлов, являющихся частями вашей программы.

При вызове require Node нужно преобразовать заданную строку в имя файла. Пути, начинающиеся с "/", "./" или "../", преобразуются в пути относительно текущего. "./" означает текущую директорию, "../" – директорию выше, а "/" – корневую директорию файловой системы. Если вы запросите "./world/world" из файла /home/marijn/elife/run.js, Node попробует загрузить файл /home/marijn/elife/world/world.js. Расширение.js можно опускать.

Когда передаётся строка, которая не выглядит как относительный или абсолютный путь, то предполагается, что это либо встроенный модуль, или модуль, установленный в директории node_modules. К примеру, require(«fs») выдаст вам встроенный модуль для работы с файловой системой, а require(«elife») попробует загрузить библиотеку из node_modules/elife/. Типичный метод установки библиотек – при помощи NPM, к которому я вернусь позже.

Для демонстрации давайте сделаем простой проект из двух файлов. Первый назовём main.js, и в нём будет определён скрипт, вызываемый из командной строки, предназначенный для искажения строк.

Var garble = require("./garble"); // По индексу 2 содержится первый аргумент программы из командной строки var argument = process.argv; console.log(garble(argument));

Файл garble.js определяет библиотеку искажения строк, которая может использоваться как заданной ранее программой для командной строки, так и другими скриптами, которым нужен прямой доступ к функции garble.

Module.exports = function(string) { return string.split("").map(function(ch) { return String.fromCharCode(ch.charCodeAt(0) + 5); }).join(""); };

Замена module.exports вместо добавления к нему свойств позволяет нам экспортировать определённое значение из модуля. В данном случае, результатом запроса нашего модуля получится сама функция искажения.

Функция разбивает строку на символы, используя split с пустой строкой, и затем заменяет все символы на другие, с кодом на 5 единиц выше. Затем она соединяет результат обратно в строку.

Теперь мы можем вызвать наш инструмент:

$ node main.js JavaScript Of{fXhwnuy

Установка через NPM

NPM, вскользь упомянутый в главе 10, это онлайн-хранилище модулей JavaScript, многие из которых написаны специально для Node. Когда вы ставите Node на компьютер, вы получаете программу npm, которая даёт удобный интерфейс к этому хранилищу.

К примеру, один из модулей NPM зовётся figlet, и он преобразует текст в “ASCII art”, рисунки, составленные из текстовых символов. Вот как его установить:

$ npm install figlet npm GET https://registry.npmjs.org/figlet npm 200 https://registry.npmjs.org/figlet npm GET https://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz npm 200 https://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz [email protected] node_modules/figlet $ node > var figlet = require("figlet"); > figlet.text("Hello world!", function(error, data) { if (error) console.error(error); else console.log(data); }); _ _ _ _ _ _ _ | | | | ___| | | ___ __ _____ _ __| | __| | | | |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| "__| |/ _` | | | _ | __/ | | (_) | \ V V / (_) | | | | (_| |_| |_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)

После запуска npm install NPM создаст директорию node_modules. Внутри неё будет директория figlet, содержащий библиотеку. Когда мы запускаем node и вызываем require(«figlet»), библиотека загружается и мы можем вызвать её метод text, чтобы вывести большие красивые буквы.

Что интересно, вместо простого возврата строки, в которой содержатся большие буквы, figlet.text принимает функцию для обратного вызова, которой он передаёт результат. Также он передаёт туда ещё один аргумент, error, который в случае ошибки будет содержать объект error, а в случае успеха – null.

Такой принцип работы принят в Node. Для создания букв figlet должен прочесть файл с диска, содержащий буквы. Чтение файла – асинхронная операция в Node, поэтому figlet.text не может вернуть результат немедленно. Асинхронность заразительна – любая функция, вызывающая асинхронную, сама становится асинхронной.

NPM – это больше, чем просто npm install. Он читает файлы package.json, содержащие информацию в формате JSON про программу или библиотеку, в частности, на каких библиотеках она основана. Выполнение npm install в директории, содержащей такой файл, автоматически приводит к установке всех зависимостей, и в свою очередь их зависимостей. Также инструмент npm используется для размещения библиотек в онлайновом хранилище NPM, чтобы другие люди могли их находить, скачивать и использовать.

Больше мы не будем углубляться в детали использования NPM. Обращайтесь на npmjs.org за документацией поиску библиотек.

Модуль file system

Один из самых востребованных встроенных модулей Node – модуль “fs”, что означает «файловая система». Модуль обеспечивает функционал для работы с файлами и директориями.

К примеру, есть функция readFile, читающая файл и делающая обратный вызов с содержимым файла.

Var fs = require("fs"); fs.readFile("file.txt", "utf8", function(error, text) { if (error) throw error; console.log("А в файле том было:", text); });

Второй аргумент readFile задаёт кодировку символов, в которой нужно преобразовывать содержимое файла в строку. Текст можно преобразовать в двоичные данные разными способами, но самым новым из них является UTF-8. Если у вас нет оснований полагать, что в файле содержится текст в другой кодировке, можно смело передавать параметр «utf8». Если вы не задали кодировку, Node выдаст вам данные в двоичной кодировке в виде объекта Buffer, а не строки. Это массивоподобный объект, содержащий байты из файла.

Var fs = require("fs"); fs.readFile("file.txt", function(error, buffer) { if (error) throw error; console.log("В файле было ", buffer.length, " байт.", "Первый байт:", buffer); });

Схожая функция, writeFile, используется для записи файла на диск.

Var fs = require("fs"); fs.writeFile("graffiti.txt", "Здесь был Node ", function(err) { if (err) console.log("Ничего не вышло, и вот почему:", err); else console.log("Запись успешна. Все свободны."); });

Здесь задавать кодировку не нужно, потому что writeFile полагает, что если ей на запись дали строку, а не объект Buffer, то её надо выводить в виде текста с кодировкой по умолчанию UTF-8.

Модуль “fs” содержит много полезного: функция readdir возвращает список файлов директории в виде массива строк, stat вернёт информацию о файле, rename переименовывает файл, unlink удаляет, и т.п. См. документацию на nodejs.org

Многие функции “fs” имеют как синхронный, так и асинхронный вариант. К примеру, есть синхронный вариант функции readFile под названием readFileSync.

Var fs = require("fs"); console.log(fs.readFileSync("file.txt", "utf8"));

Синхронные функции использовать проще и полезнее для простых скриптов, где дополнительная скорость асинхронного метода не важна. Но заметьте – на время выполнения синхронного действия ваша программа полностью останавливается. Если ей надо отвечать на ввод пользователя или другим программам по сети, затыки ожидания синхронного I/O приводят к раздражающим задержкам.

Модуль HTTP

Ещё один основной модуль - «http». Он даёт функционал для создания HTTP серверов и HTTP запросов.

Вот всё, что нужно для запуска простейшего HTTP сервера:

Var http = require("http"); var server = http.createServer(function(request, response) { response.writeHead(200, {"Content-Type": "text/html"}); response.write("

Привет!

Вы запросили " + request.url + "

"); response.end(); }); server.listen(8000);

Запустив скрипт на своей машины, вы можете направить браузер по адресу localhost :8000/hello, таким образом создав запрос к серверу. Он ответит небольшой HTML-страницей.

Функция, передаваемая как аргумент к createServer, вызывается при каждой попытке соединения с сервером. Переменные request и response – объекты, представляющие входные и выходные данные. Первый содержит информацию по запросу, например свойство url содержит URL запроса.

Чтобы отправить что-то назад, используются методы объекта response. Первый, writeHead, пишет заголовки ответа (см. главу 17). Вы даёте ему код статуса (в этом случае 200 для “OK”) и объект, содержащий значения заголовков. Здесь мы сообщаем клиенту, что он должен ждать документ HTML.

Затем идёт тело ответа (сам документ), отправляемое через response.write. Этот метод можно вызывать несколько раз, если хотите отправлять ответ по кускам, к примеру, передавая потоковые данные по мере их поступления. Наконец, response.end сигнализирует конец ответа.

Вызов server.listen заставляет сервер слушать запросы на порту 8000. Поэтому вам надо в браузере заходить на localhost:8000, а не просто на localhost (где портом по умолчанию будет 80).

Для остановки такого скрипта Node, который не завершается автоматически, потому что ожидает следующих событий (в данном случае, соединений), надо нажать Ctrl-C.

Настоящий веб-сервер делает гораздо больше того, что описано в примере. Он смотрит на метод запроса (свойство method), чтобы понять, какое действие пытается выполнить клиент, и на URL запроса, чтобы понять, на каком ресурсе это действие должно выполняться. Далее вы увидите более продвинутую версию сервера.

Чтобы сделать HTTP-клиент, мы можем использовать функцию модуля “http” request.

Var http = require("http"); var request = http.request({ hostname: "eloquentjavascript.net", path: "/20_node.html", method: "GET", headers: {Accept: "text/html"} }, function(response) { console.log("Сервис ответил с кодом ", response.statusCode); }); request.end();

Первый аргумент request настраивает запрос, объясняя Node, с каким сервером будем общаться, какой путь будет у запроса, какой метод использовать, и т.д. Второй – функция. которую надо будет вызвать по окончанию запроса. Ей передаётся объект response, в котором содержится вся информация по ответу – к примеру, код статуса.

Как и объект response сервера, объект, возвращаемый request, позволяет передавать данные методом write и заканчивать запрос методом end. В примере не используется write, потому что запросы GET не должны содержать данных в теле.

Для запросов на безопасные URL (HTTPS), Node предлагает модуль https, в котором есть своя функция запроса, схожая с http.request.

Потоки

Мы видели два примера потоков в примерах HTTP – объект response, в который сервер может вести запись, и объект request, который возвращается из http.request

Потоки с возможностью записи – популярная концепция в интерфейсах Node. У всех потоков есть метод write, которому можно передать строку или объект Buffer. Метод end закрывает поток, а при наличии аргумента, выведет перед закрытием кусочек данных. Обоим методам можно задать функцию обратного вызова через дополнительный аргумент, которую они вызовут по окончанию записи или закрытию потока.

Возможно создать поток, показывающий на файл, при помощи функции fs.createWriteStream. Затем можно использовать метод write для записи в файл по кусочкам, а не целиком, как в fs.writeFile.

Потоки с возможностью чтения будут чуть сложнее. Как переменная request, переданная функции для обратного вызова в сервере HTTP, так и переменная response, переданная в HTTP-клиенте, являются потоками с возможностью чтения. (Сервер читает запрос и потом пишет ответы, а клиент пишет запрос и читает ответа). Чтение из потока осуществляется через обработчики событий, а не через методы.

У объектов, создающих события в Node, есть метод on, схожий с методом браузера addEventListener. Вы даёте ему имя события и функцию, и он регистрирует эту функцию, чтоб её вызвали сразу, когда произойдёт событие.

У потоков с возможностью чтения есть события «data» и «end». Первое происходит при поступлении данных, второе – по окончанию. Эта модель подходит к потоковым данным, которые можно сразу обработать, даже если получен не весь документ. Файл можно прочесть в виде потока через fs.createReadStream.

Следующий код создаёт сервер, читающий тела запросов и отправляющий их в ответ потоком в виде текста из заглавных букв.

Var http = require("http"); http.createServer(function(request, response) { response.writeHead(200, {"Content-Type": "text/plain"}); request.on("data", function(chunk) { response.write(chunk.toString().toUpperCase()); }); request.on("end", function() { response.end(); }); }).listen(8000);

Переменная chunk, передаваемая обработчику данных, будет бинарным Buffer, который можно преобразовать в строку, вызвав его метод toString, который декодирует его из кодировки по умолчанию (UTF-8).

Следующий код, будучи запущенным одновременно с сервером, отправит запрос на сервер и выведет полученный ответ:

Var http = require("http"); var request = http.request({ hostname: "localhost", port: 8000, method: "POST" }, function(response) { response.on("data", function(chunk) { process.stdout.write(chunk.toString()); }); }); request.end("Hello server");

Пример пишет в process.stdout (стандартный вывод процесса, являющийся потоком с возможностью записи), а не в console.log. Мы не можем использовать console.log, так как он добавляет лишний перевод строки после каждого куска кода – это здесь не нужно.

Простой файловый сервер

Давайте скомбинируем наши новые знания о серверах HTTP и работе с файловой системой, и наведём мостик между ними: HTTP-сервер, предоставляющий удалённый доступ к файлам. У такого сервера много вариантов использования. Он позволяет веб-приложениям хранить и делиться данными, или может дать группе людей доступ к набору файлов.

Когда мы относимся к файлам, как к ресурсам HTTP, методы GET, PUT и DELETE можно использовать для чтения, записи и удаления файлов. Мы будем интерпретировать путь в запросе как путь к файлу.

Нам не надо открывать доступ ко всей файловой системе, поэтому мы будем интерпретировать эти пути как заданные относительно корневого каталога, и это будет каталог запуска скрипта. Если я запущу сервер из /home/marijn/public/ (или C:\Users\marijn\public\ на Windows), то запрос на /file.txt должен указать на /home/marijn/public/file.txt (или C:\Users\marijn\public\file.txt).

Программу мы будем строить постепенно, используя объект methods для хранения функций, обрабатывающих разные методы HTTP.

Var http = require("http"), fs = require("fs"); var methods = Object.create(null); http.createServer(function(request, response) { function respond(code, body, type) { if (!type) type = "text/plain"; response.writeHead(code, {"Content-Type": type}); if (body && body.pipe) body.pipe(response); else response.end(body); } if (request.method in methods) methods(urlToPath(request.url), respond, request); else respond(405, "Method " + request.method + " not allowed."); }).listen(8000);

Этот код запустит сервер, возвращающий ошибки 405 – этот код используется для обозначения того, что запрошенный метод сервером не поддерживается.

Функция respond передаётся функциям, обрабатывающим разные методы, и работает как обратный вызов для окончания запроса. Она принимает код статуса HTTP, тело, и, возможно, тип содержимого. Если переданное тело – поток с возможностью чтения, у него будет метод pipe, который используется для передачи читаемого потока в записываемый. Если нет – предполагается, что это либо null (тело пустое), или строка, и тогда она передаётся напрямую в метод ответа end.

Чтобы получить путь из URL в запросе, функция urlToPath, используя встроенный модуль Node “url”, разбирает URL. Она принимает имя пути, нечто вроде /file.txt, декодирует, чтобы убрать экранирующие коды %20, и вставляет в начале точку, чтобы получить путь относительно текущего каталога.

Вам кажется, что функция urlToPath небезопасна? Вы правы. Вернёмся к этому вопросу в упражнениях.

Мы устроим метод GET так, чтобы он возвращал список файлов при чтении директории, и содержимое файла при чтении файла.

Вопрос на засыпку – какой тип заголовка Content-Type мы должны возвращать, читая файл. Поскольку в файле может быть всё, что угодно, сервер не может просто вернуть один и тот же тип для всех. Но NPM с этим может помочь. Модуль mime (индикаторы типа содержимого файла вроде text/plain также называются MIME types) знает правильный тип для огромного количества расширений файлов.

Запустив следующую команду npm в директории, где живёт скрипт сервера, вы сможете использовать require(«mime») для запросов к библиотеке типов.

$ npm install mime npm http GET https://registry.npmjs.org/mime npm http 304 https://registry.npmjs.org/mime [email protected] node_modules/mime

Когда запрошенного файла не существует, правильным кодом ошибки для этого случая будет 404. Мы будем использовать fs.stat для возврата информации по файлу, чтобы выяснить, есть ли такой файл, и не директория ли это.

Methods.GET = function(path, respond) { fs.stat(path, function(error, stats) { if (error && error.code == "ENOENT") respond(404, "File not found"); else if (error) respond(500, error.toString()); else if (stats.isDirectory()) fs.readdir(path, function(error, files) { if (error) respond(500, error.toString()); else respond(200, files.join("\n")); }); else respond(200, fs.createReadStream(path), require("mime").lookup(path)); }); };

Поскольку запросы к диску занимают время, fs.stat работает асинхронно. Когда файла не существует, fs.stat передаст объект error с кодовым свойством «ENOENT» в функцию обратного вызова. Было бы здорово, если бы Node определил разные типы ошибок для разных ошибок, но такого нет. Вместо этого он выдаёт запутанные коды в стиле Unix.

Все неожиданные ошибки мы будем выдавать с кодом 500, обозначающим, что на сервере проблема – в отличие от кодов, начинающихся на 4, говорящих о проблеме с запросом. В некоторых ситуациях это будет не совсем аккуратно, но для небольшой примерной программы этого будет достаточно.

Объект stats возвращаемый fs.stat, рассказывает нам о файле всё. Например, size – размер файла, mtime – дата модификации. Здесь нам нужно узнать, директория это или обычный файл – это нам сообщит метод isDirectory.

Для чтения списка файлов в директории мы используем fs.readdir, и через ещё один обратный вызов, возвращаем его пользователю. Для обычных файлов мы создаём читаемый поток через fs.createReadStream и передаём его в ответ, вместе с типом содержимого, который модуль “mime” выдал для этого файла.

Код обработки DELETE будет проще:

Methods.DELETE = function(path, respond) { fs.stat(path, function(error, stats) { if (error && error.code == "ENOENT") respond(204); else if (error) respond(500, error.toString()); else if (stats.isDirectory()) fs.rmdir(path, respondErrorOrNothing(respond)); else fs.unlink(path, respondErrorOrNothing(respond)); }); };

Возможно, вам интересно, почему попытка удаления несуществующего файла возвращает статус 204 вместо ошибки. Можно сказать, что при попытке удалить несуществующий файл, так как файла там уже нет, то запрос уже исполнен. Стандарт HTTP поощряет людей делать идемпотентные запросы – то есть такие, при которых многократный повтор одного и того же действия не приводит к разным результатам.

Function respondErrorOrNothing(respond) { return function(error) { if (error) respond(500, error.toString()); else respond(204); }; }

Когда ответ HTTP не содержит данных, можно использовать код статуса 204 (“no content”). Так как нам нужно обеспечить функции обратного вызова, которые либо сообщают об ошибки, или возвращают ответ 204 в разных ситуациях, я написал специальную функцию respondErrorOrNothing, которая создаёт такой обратный вызов.

Вот обработчик запросов PUT:

Methods.PUT = function(path, respond, request) { var outStream = fs.createWriteStream(path); outStream.on("error", function(error) { respond(500, error.toString()); }); outStream.on("finish", function() { respond(204); }); request.pipe(outStream); };

Здесь нам не нужно проверять существование файла – если он есть, мы его просто перезапишем. Опять мы используем pipe для передачи данных из читаемого потока в записываемый, в нашем случае – из запроса в файл. Если создать поток не удаётся, создаётся событие “error”, о чём мы сообщаем в ответе. Когда данные переданы успешно, pipe закроет оба потока, что приведёт к запуску события “finish”. А после этого мы можем отчитаться об успехе с кодом 204.

Полный скрипт сервера лежит тут: eloquentjavascript.net/code/file_server.js. Его можно скачать и запустить через Node. Конечно, его можно менять и дополнять для решения упражнений или экспериментов.

Утилита командной строки curl, общедоступная на unix-системах, может использоваться для создания HTTP запросов. Следующий фрагмент тестирует наш сервер. –X используется для задания метода запроса, а –d для включения тела запроса.

$ curl http://localhost:8000/file.txt File not found $ curl -X PUT -d hello http://localhost:8000/file.txt $ curl http://localhost:8000/file.txt hello $ curl -X DELETE http://localhost:8000/file.txt $ curl http://localhost:8000/file.txt File not found

Первый запрос к file.txt завершается с ошибкой, поскольку файла ещё нет. Запрос PUT создаёт файл, и глядите-ка – следующий запрос его успешно получает. После его удаления через DELETE файл снова отсутствует.

Обработка ошибок

В коде файлового сервера есть шесть мест, где мы перенаправляем исключения, когда мы не знаем, как обрабатывать ошибки. Поскольку исключения не передаются автоматически в функции обратного вызова, но передаются им как аргументы, их надо каждый раз обрабатывать персонально. Это сводит на нет преимущество обработки исключений, а именно, возможность централизованно обрабатывать ошибки.

Что будет, когда что-то реально выбросит исключение в системе? Мы не используем блоки try, потому оно будет передано на самый верх стека вызовов. В Node это приводит к прекращению выполнения программы и выводу информации об исключении (вместе с отслеживанием стека) на стандартный вывод.

Поэтому наш сервер будет падать при возникновении проблем в коде – в отличие от проблем с асинхронностью, которые будут переданы как аргументы в функции вызова. Если нам надо обрабатывать все исключения, возникающие при обработке запроса, чтобы мы точно отправили ответ, нам надо добавлять блоки try/catch в каждом обратном вызове.

Это плохо. Много программ для Node написаны так, чтобы использовать как можно меньше работы с исключениями, подразумевая что в случае возникновения исключения программа не может его обработать, и поэтому надо падать.

Ещё один подход – использование обещаний, которые были описаны в главе 17. Они ловят исключения, выброшенные функциями обратного вызова и передают их как ошибки. В Node можно загрузить библиотеку promise и использовать её для обработки асинхронных вызовов. Немногие библиотеки Node интегрируют обещания, но обычно их довольно просто обернуть. Отличный модуль “promise” с NPM содержит функцию denodeify, которая берёт асинхронную функцию вроде fs.readFile и преобразовывает её в функцию, возвращающую обещание.

Var Promise = require("promise"); var fs = require("fs"); var readFile = Promise.denodeify(fs.readFile); readFile("file.txt", "utf8").then(function(content) { console.log("The file contained: " + content); }, function(error) { console.log("Failed to read file: " + error); });

Для сравнения, я написал ещё одну версию файлового сервера с использованием обещаний, которую можно найти на eloquentjavascript.net/code/file_server_promises.js . Она почище, потому что функции теперь могут возвращать результаты, а не назначать обратные вызовы, и исключения передаются неявно.

Приведу несколько строк оттуда, чтобы продемонстрировать разницу в стилях.

Объект fsp, использующийся в коде, содержит варианты функций fs с обещаниями, обёрнутыми при помощи Promise.denodeify. Возвращаемый из обработчика метода объект, со свойствами code и body, становится окончательным результатом цепочки обещаний, и он используется для определения того, какой ответ отправить клиенту.

Methods.GET = function(path) { return inspectPath(path).then(function(stats) { if (!stats) // Does not exist return {code: 404, body: "File not found"}; else if (stats.isDirectory()) return fsp.readdir(path).then(function(files) { return {code: 200, body: files.join("\n")}; }); else return {code: 200, type: require("mime").lookup(path), body: fs.createReadStream(path)}; }); }; function inspectPath(path) { return fsp.stat(path).then(null, function(error) { if (error.code == "ENOENT") return null; else throw error; }); }

Функция inspectPath – простая обёртка вокруг fs.stat, обрабатывающая случай, когда файл не найден. В этом случае мы заменяем ошибку на успех, возвращающий null. Все остальные ошибки можно передавать. Когда обещание, возвращаемое из этих обработчиков, обламывается, сервер отвечает кодом 500.

Итог

Node – отличная простая система, позволяющая запускать JavaScript вне браузера. Изначально она разрабатывалась для работы по сети, чтобы играть роль узла в сети. Но она позволяет делать много всего, и если вы наслаждаетесь программированием на JavaScript, автоматизация ежедневных задач с Node работает отлично.

NPM предоставляет библиотеки для всего, что вам может прийти в голову (и даже для кое-чего, что вам не придёт в голову), и она позволяет скачивать и устанавливать их простой командой. Node также поставляется с набором встроенных модулей, включая “fs” для работы с файловой системой, и “http” для запуска HTTP серверов и создания HTTP запросов.

Весь ввод и вывод в Node делается асинхронно, если только вы не используете явно синхронный вариант функции, например fs.readFileSync. Вы предоставляете функции обратного вызова, а Node их вызывает в нужное время, когда операции I/O заканчивают работу.

Упражнения

И снова согласование содержания
В главе 17 первое упражнение было посвящено созданию запросов к eloquentjavascript.net/author, спрашивавших разные типы содержимого путём передачи разных заголовков Accept.

Сделайте это снова, используя функцию Node http.request. Запросите, по крайней мере, типы text/plain, text/html и application/json. Помните, что заголовки запроса можно передавать как объект в свойстве headers, первым аргументом http.request.

Выведите содержимое каждого ответа.

Устранение утечек

Для упрощения доступа к файлам я оставил работать сервер у себя на комьпютере, в директории /home/marijn/public. Однажды я обнаружил, что кто-то получил доступ ко всем моим паролям, которые я хранил в браузере. Что случилось?

Если вам это непонятно, вспомните функцию urlToPath, которая определялась так:

Function urlToPath(url) { var path = require("url").parse(url).pathname; return "." + decodeURIComponent(path); }

Теперь вспомните, что пути, передаваемые в функцию “fs”, могут быть относительными. Они могут содержать путь “../” в верхний каталог. Что будет, если клиент отправит запросы на URL вроде следующих:

myhostname :8000/../.config/config/google-chrome/Default/Web%20Data
myhostname :8000/../.ssh/id_dsa
myhostname :8000/../../../etc/passwd

Поменяйте функцию urlToPath для устранения подобной проблемы. Примите во внимание, что на Windows Node разрешает как прямые так и обратные слеши для задания путей.

Кроме этого, помедитируйте над тем фактом, что как только вы выставляете сырую систему в интернет, ошибки в системе могут быть использованы против вас и вашего компьютера.

Создание директорий
Хотя метод DELETE работает и при удалении директорий (через fs.rmdir), пока сервер не предоставляет возможности создания директорий.

Добавьте поддержку метода MKCOL, который должен создавать директорию через fs.mkdir. MKCOL не является основным методом HTTP, но он существует, именно для этого, в стандарте WebDAV, который содержит расширения HTTP, чтобы использовать его для записи ресурсов, а не только для их чтения.

Общественное место в сети
Так как файловый сервер выдаёт любые файлы и даже возвращает правильный заголовок Content-Type, его можно использовать для обслуживания веб-сайта. Так как он разрешает всем удалять и заменять файлы, это был бы интересный сайт – который можно изменять, портить и удалять всем, кто может создать правильный HTTP-запрос. Но это всё равно был бы веб-сайт.

Напишите простую HTML страницу с простым файлом JavaScript. Разместите их в директории, обслуживаемой сервером и откройте в браузере.

Затем, в качестве продвинутого упражнения, скомбинируйте все полученные знания из книги, чтобы построить более дружественный интерфейс для модификации веб-сайта изнутри самого сайта.

Используйте форму HTML (глава 18) для редактирования файлов, составляющих сайт, позволяя пользователю обновлять их на сервере через HTTP-запросы, как описано в главе 17.

Начните с одного файла, редактирование которого разрешено. Затем сделайте так, чтобы можно было выбирать файл для редактирования. Используйте тот факт, что наш файловый сервер возвращает списки файлов по запросу директории.

Не меняйте файлы непосредственно в коде файлового сервера – если вы сделаете ошибку, вы скорее всего испортите те файлы. Работайте в директории, недоступной снаружи, и копируйте их туда после тестирования.

Если ваш компьютер соединяется с интернетом напрямую, без firewall, роутера или других устройств, вы сможете пригласить друга на свой сайт. Для проверки сходите на whatismyip.com, скопируйте IP адрес в адресную строку и добавьте:8000 для выбора нужного порта. Если вы попали на свой сайт, то он доступен для просмотра всем.

Был рассмотрен переход от синхронного кода к асинхронному. Во второй части я рассмотрю переход от побайтового чтения из файла к построчному. Поскольку операция кодирования из одной кодировки в другую довольно тривиальна, я чуть больше расскажу про теоретическую сторону вопроса.

Часть 2: Как прочитать строку из файла

Fs.readFile("large.txt", { encoding: "utf8" }, (err, data) => { if (err) throw err; data.split("\n").forEach(line => { doSomethingWithLine(line); }); });

Он же, пожалуй, самый быстрый. Но он же требует больше всего памяти — от 100% до 200% от размера файла. 200% — это одновременно и самый распространенный случай, так как в памяти кодировка у строки UTF-16 и поэтому размер требуемой памяти умножается на два если файл содержит в основном символы из однобайтного диапазона UTF-8.

Кроме того, разработчики Node.js не рекомендуют загружать много данных в Node.js процесс (см. What is the memory limit on a node process?) . Сделано это не очень элегантно — даже если физической памяти хватает, то при попытке загрузить файл больше 1Gb бросается исключение:

This.parent = new SlowBuffer(this.length); ^ RangeError: length > kMaxLength

Если же файл поменьше, то можно получить и такое:

FATAL ERROR: CALL_AND_RETRY_0 Allocation failed - process out of memory

Остается только обрабатывать файл по частям. Для этого нужно его по частям прочитать и Node.js предоставляет для этого минимум 5 способов:

  1. Использовать «старые» потоки — открыть поток и подписаться на событие «data».
  2. Использовать «новые» потоки — подписаться на событие «readable» и использовать метод read().
  3. Создать свой WritableStream и направить в него файловый поток методом «pipe()».
  4. Использовать файловые дескрипторы и набор методов open(), read(), close().
  5. Использовать синхронные варианты — openSync(), readSync(), closeSync().

Варианты 1-3 являются более элегантными, так как оперируют удобной абстракцией — потоком. Это позволяет рассматривать программу как диаграмму потоков данных (data flow diagram) и при дизайне архитектуры оперировать такими терминами как слияние, разделение и трансформация.

Также варианты 1 и 2 отличаются возможностью чтения символов из файла. В вариантах 3 и 4 данные из файла записываются в буфер и затем их надо конвертировать в текст.

// Вариант #1 - "старые" потоки var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("data", (_, data) => processData(data)); stream.on("end", done); // Вариант #2 - "новые" потоки var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("readable", () => processData(stream.read())); stream.on("end", done); // Вариант #3 - pipe var stream = fs.createReadStream(file, { encoding: "utf8" }); var writeStream = new Writable(); writeStream._write = (chunk, encoding, callback) => { processData(chunk); callback(); }; writeStream.on("end", done); stream.pipe(writeStream); // Вариант #4 - асинхронные методы fs fs.open(file, "r", (err, fd) => { var buffer = new Buffer(1000*1000); (function next() { fs.read(fd, buffer, 0, buffer.length, null, (err, bytesRead, buffer) => { if (bytesRead === 0) { fs.close(fd, done); } else { processData(buffer); next(); } }); }()); });

Более концептуальным является отличие с точки зрения получения данных из файла. Варианты 1-2 получают следующий фрагмент как только завершается обработчик события текущего фрагмента. В случае асинхронного кода в обработчике последовательность его выполнения непредсказуема:

Function processData(chunk) { console.log("first") setImmediate(() => { console.log("second"); setImmediate(() => console.log("third")); }); } var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("readable", () => processData(stream.read())); ... first third second third first second ...

Ситуацию можно поправить используя методы pause()/resume().

Function processData(chunk, done) { console.log("first") setImmediate(() => { console.log("second"); setImmediate(() => { console.log("third"); done(); }); }); } var stream = fs.createReadStream(file, { encoding: "utf8" }); stream.on("readable", () => { stream.pause(); processData(stream.read(), () => stream.resume()); }); ... first second third first second third ...

В вариантах 3 и 4 следующий фрагмент будет получен только после передачи управления (вариант 3) или запроса (вариант 4).

Думаю, что информации достаточно для реализации функции createTextReader() из первой части статьи. Из всех вариантов наиболее соответствующим является четвертый, поскольку поток управления у него аналогичен интерфейсу (request-callback).

Function createTextReader(file, options, done) { var length, encoding, separator; if ("function" === typeof options) { done = options; options = { }; } length = 4 * 1024; encoding = options.encoding || "utf8"; separator = (options.separator || "\n"); fs.open(file, "r", (err, fd) => { var eof, tail, buffer, decoder, lines; if (err) { done(err); return; } eof = false; buffer = new Buffer(length); tail = ""; lines = ; decoder = new StringDecoder(encoding); done(null, { readLine: done => { var line; if (lines.length > 0) { line = lines.shift(); done(null, line); } else if (eof) { done(null, null); } else { (function read() { fs.read(fd, buffer, 0, length, null, function (err, bytesRead, buffer) { var index, position; if (bytesRead === 0) { eof = true; done(null, tail); } else { tail = tail + decoder.write(buffer.slice(0, bytesRead)); index = -1; while (-1 !== (position = tail.indexOf(separator, index))) { lines.push(tail.substring(index, position)); index = position + separator.length; } tail = tail.substring(index); if (lines.length === 0) { read(); } else { line = lines.shift(); done(null, line); } } }); }()); } }, close: done => { fs.close(fd, () => { if (done) { done(err || null); } }); } }); }); }

Послесловие

В двух частях этой статьи я постарался изложить все, что мне пригодилось при создании модуля https://github.com/AlexAtNet/async-read-lines . К сожалению, многое осталось за рамками, не на все хватило времени. Так что если нашли ошибку или опечатку — пишите в личные сообщения. Если у вас есть вопросы по теме статьи — буду рад ответить в комментариях. Если увидите баги в модуле — создавайте запрос в github issues . Связаться со мной лично можно через сайт alexatnet.com .

Успехов в программировании!

Об авторе: Александр Неткачев — старший разработчик на С# и F#. Поддерживает сайт alexatnet.com , проводит вебинары (Code&Coffe), помогает с кодом начинающим разработчикам (CodeReview4U).

Операции с файлами языку JavaScript не в новинку — в JScript, встроенном в Windows, доступен полный набор функций для работы с диском. Node, в силу своей асинхронной природы, несколько усложняет эти в общем то тривиальные задачи.

Сразу хочу предупредить об одной возможной ошибке. Если Вы, как и я, запускаете Node в виртуальной машине из общей папки, помните — VM в эту папку писать не может. Попытки создать или дополнить файлы в ней закончатся только Error: Permission denied

Открытие файла

Обращение к файлам на диске — операция небыстрая. Она в среднем в десятки тысяч раз дольше чем обращение к оперативной памяти. Поэтому, большинство операций с файлами асинхронные. Все операции с файловой системой собраны во встроенном модуле fs , стало быть начнём с его подключения.

Var fs = require("fs"), sys = require("sys");

Модуль sys нам нужен для вывода информации в консоль. В последующих примерах я эти строки буду опускать, чтобы не повторяться.

Открытие файла делается так:

Fs.open(<путь> , <флаги> , <режим доступа> , <функция-обработчик> )

  • Путь к файлу. Относительно запущенного скрипта либо абсолютный.
  • Флаг — режим доступа к файлу. Может принимать следующие значения:
    • r — только чтение, указатель в начале файла
    • r+ — чтение и запись, указатель в начале файла
    • w — только запись, указатель в начале файла
    • w+ — запись и чтение, указатель в начале файла
    • a — запись, указатель в конце файла
    • a+ — запись и чтение, указатель в конце файла
  • Режим доступа используется если открываемый файл не существует. В таком случае будет создан новый пустой файл с заданным режимом. Нотация стандартная для UNIX — например 0664
  • Обработчик — функция, которая будет выполнена при открытии/создании файла. В качестве аргументов передаются флаг ошибки и дескриптор файла

Например:

Fs.open("readme.txt", "r+", 0644, function(err, file_handle) { if (!err) { // Операции с открытым файлом } else { // Обработка ошибок } });

Запись в файл

Для записи в файл используется метод fs.write:

Fs.write(<дескриптор> , <данные> , <позиция> , <кодировка> , <обработчик> )

  • Дескриптор файла, полученный в fs.open .
  • Данные , которые мы записываем. Объекты здесь будут приведены к строковому типу.
  • Позиция , с которой начинается запись. Null означает запись с текущей позиции.
  • Кодировка , в которой будут записаны данные. Может быть «ascii «, «utf8 » и «raw «
  • Обработчик — функция, которая будет выполнена после записи. Аргументы — флаг ошибки и количество записанных байт

Расширим предыдущий пример записью строки 🙂

Fs.open("readme.txt", "a", 0644, function(err, file_handle) { if (!err) { // Записываем в конец файла readme.txt фразу "Copyrighted by Me" // при открытии в режиме "a" указатель уже в конце файла, и мы передаём null // в качестве позиции fs.write(file_handle, "Copyrighted by Me", null, "ascii", function(err, written) { if (!err) { // Всё прошло хорошо } else { // Произошла ошибка при записи } }); } else { // Обработка ошибок при открытии } });

Чтение из файла

Чтение делается так:

Fs.read(<дескриптор> , <длина> , <позиция> , <кодировка> , <обработчик> )

Здесь всё почти так же, как в fs.write .

  • Дескриптор файла, полученный в fs.open
  • Длина данных, которые мы планируем прочитать
  • Позиция , с которой начинаем читать. Null — с текущей позиции
  • Кодировка , в которой читаются данные. Может быть «ascii «, «utf8 » и «raw «. Здесь лучше не ошибаться)
  • Обработчик — функция, которая будет выполнена после чтения. Аргументы — флаг ошибки,данные, количество прочитанных байт

Чтение из файла — совсем несложный процесс:

Fs.open("readme.txt", "r", 0644, function(err, file_handle) { if (!err) { // Читаем 10 килобайт с начала файла, в ascii fs.read(file_handle, 10000, null, "ascii", function(err, data) { if (!err) { // Всё прошло хорошо, выводим прочитанное в консоль sys.puts(data); } else { // Произошла ошибка при чтении } }); } else { // Обработка ошибок при открытии файла } });

После того как чтение/запись завершены, файл надо закрыть. Node может обслуживать одновременно очень много клиентов, поэтому ресурсы лучше освобождать сразу когда они становятся не нужны.

Fs.open("readme.txt", "r", 0644, function(err, file_handle) { if (!err) { // Читаем 10 килобайт с начала файла, в ascii fs.read(file_handle, 10000, null, "ascii", function(err, data) { if (!err) { // Всё прошло хорошо, выводим прочитанное в консоль sys.puts(data); fs.close(file_handle); } else { // Произошла ошибка при чтении } }); } else { // Обработка ошибок при открытии файла } });

Вторым аргументом fs.close может принимать функцию-callback, которой передаётся исключение в случае ошибки.

У всех перечисленных функций есть синхронные варианты. К их названию добавлено Sync и они не принимают последним аргументом функцию-обработчик, а просто возвращают соответствующее значение (или бросают исключение). Обратите внимание, readSync возвращает массив из данных и количества прочитанных байт.

Var file_handle = fs.openSync("readme.txt", "r", 0644); var data = fs.readSync(file_handle, 10000, null, "ascii"); sys.puts(data); fs.closeSync(file_handle);