-
Notifications
You must be signed in to change notification settings - Fork 76
Expand file tree
/
Copy path20_node.html
More file actions
509 lines (328 loc) · 63.2 KB
/
Copy path20_node.html
File metadata and controls
509 lines (328 loc) · 63.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
<!doctype html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Node.js :: Eloquent JavaScript</title>
<link rel=stylesheet href="css/ejs.css"><script>
var page = {"type":"chapter","number":20}</script></head>
<article>
<nav><a href="19_paint.html" title="previous chapter" aria-label="previous chapter">◂</a> <a href="index.html" title="cover" aria-label="cover">●</a> <a href="21_skillsharing.html" title="next chapter" aria-label="next chapter">▸</a> <button class=help title="help" aria-label="help"><strong>?</strong></button>
</nav>
<h1>Node.js</h1>
<blockquote>
<p><a class="p_ident" id="p-i9iqRkQ4RC" href="#p-i9iqRkQ4RC" tabindex="-1" role="presentation"></a>Un estudiante preguntó: “Los programadores de antaño solo usaban máquinas simples y ningún lenguaje de programación, sin embargo, creaban programas hermosos. ¿Por qué nosotros usamos máquinas complicadas y lenguajes de programación?” Fu-Tzu respondió: “Los constructores de antaño solo usaban palos y arcilla y, sin embargo, creaban hermosas chozas.”</p>
<footer>Master Yuan-Ma, <cite>The Book of Programming</cite></footer>
</blockquote><figure class="chapter framed"><img src="img/chapter_picture_20.jpg" alt="Ilustración que muestra un poste telefónico con un enredo de cables en todas direcciones"></figure>
<p><a class="p_ident" id="p-ZVNZPnz/ZC" href="#p-ZVNZPnz/ZC" tabindex="-1" role="presentation"></a>Hasta ahora, hemos utilizado el lenguaje JavaScript en un solo entorno: el navegador. Este capítulo y el <a href="21_skillsharing.html">siguiente</a> introducirán brevemente Node.js, un programa que te permite aplicar tus habilidades con JavaScript fuera del navegador. Con él, puedes construir desde pequeñas herramientas de línea de comandos hasta servidores HTTP para sitios web dinámicos.</p>
<p><a class="p_ident" id="p-tavS0sF06x" href="#p-tavS0sF06x" tabindex="-1" role="presentation"></a>Estos capítulos tienen como objetivo enseñarte los conceptos principales que Node.js utiliza y darte información suficiente para escribir programas útiles para él. No intentan ser un tratamiento completo, ni siquiera exhaustivo, de la plataforma.</p>
<p><a class="p_ident" id="p-GpZPVzYr6C" href="#p-GpZPVzYr6C" tabindex="-1" role="presentation"></a>Si bien podrías ejecutar el código en los capítulos anteriores directamente en estas páginas (por tratarse de JavaScript puro o escrito para el navegador), los ejemplos de código en este capítulo están escritos para Node y a menudo no se ejecutarán en el navegador.</p>
<p><a class="p_ident" id="p-u232WdUbYW" href="#p-u232WdUbYW" tabindex="-1" role="presentation"></a>Si deseas seguir y ejecutar el código en este capítulo, necesitarás instalar Node.js versión 18 o superior. Para hacerlo, ve a <a href="https://nodejs.org"><em>https://nodejs.org</em></a> y sigue las instrucciones de instalación para tu sistema operativo. También puedes encontrar más documentación para Node.js allí.</p>
<h2><a class="h_ident" id="h-bv9WrnaS3g" href="#h-bv9WrnaS3g" tabindex="-1" role="presentation"></a>Antecedentes</h2>
<p><a class="p_ident" id="p-b5C83jVV2T" href="#p-b5C83jVV2T" tabindex="-1" role="presentation"></a>Cuando se construyen sistemas que se comunican a través de la red, la forma en que gestionas la entrada y el output —es decir, la lectura y escritura de datos desde y hacia la red y el disco duro— puede marcar una gran diferencia en cuán rápido responde un sistema al usuario o a las solicitudes de red.</p>
<p><a class="p_ident" id="p-EQnhnuy5FO" href="#p-EQnhnuy5FO" tabindex="-1" role="presentation"></a>En tales programas, la programación asíncrona a menudo es útil. Permite que el programa envíe y reciba datos desde y hacia múltiples dispositivos al mismo tiempo sin una complicada gestión de hilos y sincronización.</p>
<p><a class="p_ident" id="p-Hm/D4MsCjI" href="#p-Hm/D4MsCjI" tabindex="-1" role="presentation"></a>Node fue concebido inicialmente con el propósito de hacer que la programación asíncrona sea fácil y conveniente. JavaScript se presta bien a un sistema como Node. Es uno de los pocos lenguajes de programación que no tiene una forma incorporada de manejar la entrada y salida. Por lo tanto, JavaScript podría adaptarse al enfoque algo excéntrico de Node para la programación de red y sistemas de archivos sin terminar con dos interfaces inconsistentes. En 2009, cuando se diseñaba Node, la gente ya estaba realizando programación basada en callbacks en el navegador, por lo que la comunidad alrededor del lenguaje estaba acostumbrada a un estilo de programación asíncrona.</p>
<h2><a class="h_ident" id="h-qzhakKB9xU" href="#h-qzhakKB9xU" tabindex="-1" role="presentation"></a>El comando node</h2>
<p><a class="p_ident" id="p-qa9aNXTUus" href="#p-qa9aNXTUus" tabindex="-1" role="presentation"></a>Cuando Node.js está instalado en un sistema, proporciona un programa llamado <code>node</code>, que se utiliza para ejecutar archivos de JavaScript. Supongamos que tienes un archivo <code>hello.js</code>, que contiene este código:</p>
<pre tabindex="0" class="snippet" data-language="javascript" ><a class="c_ident" id="c-mGmtX8ZFbB" href="#c-mGmtX8ZFbB" tabindex="-1" role="presentation"></a><span class="tok-keyword">let</span> <span class="tok-definition">message</span> = <span class="tok-string">"Hola mundo"</span>;
console.log(message);</pre>
<p><a class="p_ident" id="p-/uXlGGpFk+" href="#p-/uXlGGpFk+" tabindex="-1" role="presentation"></a>Luego puedes ejecutar <code>node</code> desde la línea de comandos de la siguiente manera para ejecutar el programa:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-KwqkOzuFaq" href="#c-KwqkOzuFaq" tabindex="-1" role="presentation"></a>$ node hello.js
Hola mundo</pre>
<p><a class="p_ident" id="p-9eFTrEoqAN" href="#p-9eFTrEoqAN" tabindex="-1" role="presentation"></a>El método <code>console.log</code> en Node hace algo similar a lo que hace en el navegador. Imprime un texto. Pero en Node, el texto irá al flujo de salida estándar del proceso, en lugar de ir a la consola de JavaScript de un navegador. Al ejecutar <code>node</code> desde la línea de comandos, significa que verás los valores registrados en tu terminal.</p>
<p><a class="p_ident" id="p-s+WcE2oW8u" href="#p-s+WcE2oW8u" tabindex="-1" role="presentation"></a>Si ejecutas <code>node</code> sin proporcionarle un archivo, te proporcionará un indicador de línea de comandos (o <em>prompt</em>) en el que puedes escribir código JavaScript y ver inmediatamente el resultado.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-d/9k6S5oD+" href="#c-d/9k6S5oD+" tabindex="-1" role="presentation"></a>$ node
> 1 + 1
2
> [-1, -2, -3].map(Math.abs)
[1, 2, 3]
> process.exit(0)
$</pre>
<p><a class="p_ident" id="p-EmDKlpimWX" href="#p-EmDKlpimWX" tabindex="-1" role="presentation"></a>La asociación <code>process</code>, al igual que la asociación <code>console</code>, está disponible globalmente en Node. Proporciona varias formas de inspeccionar y manipular el programa actual. El método <code>exit</code> finaliza el proceso y puede recibir un código de estado de salida, que le indica al programa que inició <code>node</code> (en este caso, la shell de línea de comandos) si el programa se completó correctamente (código cero) o si se encontró un error (cualquier otro código).</p>
<p><a class="p_ident" id="p-W7/OzhvC4c" href="#p-W7/OzhvC4c" tabindex="-1" role="presentation"></a>Para encontrar los argumentos de línea de comandos dados a tu script, puedes leer <code>process.argv</code>, que es un array de cadenas. Ten en cuenta que también incluye el nombre del comando <code>node</code> y el nombre de tu script, por lo que los argumentos reales comienzan en el índice 2. Si <code>showargv.js</code> contiene la instrucción <code>console.<wbr>log(process.<wbr>argv)</code>, podrías ejecutarlo de la siguiente manera:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-WCisDZr57g" href="#c-WCisDZr57g" tabindex="-1" role="presentation"></a>$ node showargv.js one --and two
["node", "/tmp/showargv.js", "one", "--and", "two"]</pre>
<p><a class="p_ident" id="p-qz0/vmtwFP" href="#p-qz0/vmtwFP" tabindex="-1" role="presentation"></a>Todas las asociaciones globales de JavaScript estándar, como <code>Array</code>, <code>Math</code> y <code>JSON</code>, también están presentes en el entorno de Node. Las funcionalidades relacionadas con el navegador, como <code>document</code> o <code>prompt</code>, no lo están.</p>
<h2><a class="h_ident" id="h-45kWH4U4Bd" href="#h-45kWH4U4Bd" tabindex="-1" role="presentation"></a>Módulos</h2>
<p><a class="p_ident" id="p-EJ7yQIM8FP" href="#p-EJ7yQIM8FP" tabindex="-1" role="presentation"></a>Además de las asociaciones que mencioné, como <code>console</code> y <code>process</code>, Node agrega pocas asociaciones adicionales en el ámbito global. Si deseas acceder a funcionalidades integradas, debes solicitarlas al sistema de módulos.</p>
<p><a class="p_ident" id="p-MYYWnlIMIS" href="#p-MYYWnlIMIS" tabindex="-1" role="presentation"></a>Node comenzó utilizando el sistema de módulos CommonJS, basado en la función <code>require</code>, que vimos en el <a href="10_modules.html#commonjs">Capítulo 10</a>. Aún utilizará este sistema de forma predeterminada cuando cargas un archivo <code>.js</code>.</p>
<p><a class="p_ident" id="p-JOqjzoG84a" href="#p-JOqjzoG84a" tabindex="-1" role="presentation"></a>Pero también soporta el sistema de módulos ES más moderno. Cuando el nombre de un script termina en <code>.mjs</code>, se considera que es un módulo de este tipo, y puedes usar <code>import</code> y <code>export</code> en él (en lugar de <code>require</code>). Utilizaremos módulos ES en este capítulo.</p>
<p><a class="p_ident" id="p-cp71LSk3dO" href="#p-cp71LSk3dO" tabindex="-1" role="presentation"></a>Cuando se importa un módulo, ya sea con <code>require</code> o <code>import</code>, Node debe resolver la cadena proporcionada a un archivo real que pueda cargar. Los nombres que comienzan con <code>/</code>, <code>./</code> o <code>../</code> se resuelven como archivos, relativos a la ruta del módulo actual. Aquí, <code>.</code> representa el directorio actual, <code>../</code> para un directorio arriba, y <code>/</code> se usa para la raíz del sistema de archivos. Por lo tanto, si solicitas <code>"./<wbr>graph.<wbr>mjs"</code> desde el archivo <code>/<wbr>tmp/<wbr>robot/<wbr>robot.<wbr>mjs</code>, Node intentará cargar el archivo <code>/<wbr>tmp/<wbr>robot/<wbr>graph.<wbr>mjs</code>.</p>
<p><a class="p_ident" id="p-+iwf8vlfx1" href="#p-+iwf8vlfx1" tabindex="-1" role="presentation"></a>Cuando se importa una cadena que no parece una ruta relativa o absoluta, se asume que se refiere a un módulo integrado o un módulo instalado en un directorio <code>node_modules</code>. Por ejemplo, importar desde <code>"node:fs"</code> te dará el módulo integrado de sistema de archivos de Node. Importar <code>"robot"</code> podría intentar cargar la biblioteca encontrada en <code>node_modules/<wbr>robot/<wbr></code>. Una forma común de instalar estas bibliotecas es usando NPM, como volveremos en un momento.</p>
<p><a class="p_ident" id="p-xlrDnOEgFK" href="#p-xlrDnOEgFK" tabindex="-1" role="presentation"></a>Configuremos un proyecto pequeño que consta de dos archivos. El primero, llamado <code>main.mjs</code>, define un script que puede ser llamado desde la línea de comandos para revertir una cadena.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-Q+j+qsUhhG" href="#c-Q+j+qsUhhG" tabindex="-1" role="presentation"></a>import {reverse} from "./reverse.mjs";
// El índice 2 contiene el primer
// argumento real de la línea de comandos
let argument = process.argv[2];
console.log(reverse(argument));</pre>
<p><a class="p_ident" id="p-XA7d0ksImw" href="#p-XA7d0ksImw" tabindex="-1" role="presentation"></a>El archivo <code>reverse.mjs</code> define una biblioteca para revertir cadenas, que puede ser utilizada tanto por esta herramienta de línea de comandos como por otros scripts que necesiten acceso directo a una función para revertir cadenas.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-p8FJa0H1D4" href="#c-p8FJa0H1D4" tabindex="-1" role="presentation"></a>export function reverse(string) {
return Array.from(string).reverse().join("");
}</pre>
<p><a class="p_ident" id="p-HHZf0gHVDZ" href="#p-HHZf0gHVDZ" tabindex="-1" role="presentation"></a>Recuerda que <code>export</code> se utiliza para declarar que una asociación es parte de la interfaz del módulo. Eso permite que <code>main.mjs</code> importe y utilice la función.</p>
<p><a class="p_ident" id="p-kFzP7q7ejQ" href="#p-kFzP7q7ejQ" tabindex="-1" role="presentation"></a>Ahora podemos llamar a nuestra herramienta de esta manera:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-LYQo5Arp8G" href="#c-LYQo5Arp8G" tabindex="-1" role="presentation"></a>$ node main.mjs JavaScript
tpircSavaJ</pre>
<h2><a class="h_ident" id="h-MoxrdWcLlT" href="#h-MoxrdWcLlT" tabindex="-1" role="presentation"></a>Instalando con NPM</h2>
<p><a class="p_ident" id="p-EdwkPKYJjz" href="#p-EdwkPKYJjz" tabindex="-1" role="presentation"></a>NPM, que fue introducido en el <a href="10_modules.html#modules_npm">Capítulo 10</a>, es un repositorio en línea de módulos de JavaScript, muchos de los cuales están escritos específicamente para Node. Cuando instalas Node en tu computadora, también obtienes el comando <code>npm</code>, que puedes usar para interactuar con este repositorio.</p>
<p><a class="p_ident" id="p-eiIjy/CDcA" href="#p-eiIjy/CDcA" tabindex="-1" role="presentation"></a>El uso principal de NPM es descargar paquetes. Vimos el paquete <code>ini</code> en el <a href="10_modules.html#modulos_ini">Capítulo 10</a>. Podemos usar NPM para buscar e instalar ese paquete en nuestra computadora.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-/RND8raC5h" href="#c-/RND8raC5h" tabindex="-1" role="presentation"></a>$ npm install ini
agregado 1 paquete en 723ms
$ node
> const {parse} = require("ini");
> parse("x = 1\ny = 2");
{ x: '1', y: '2' }</pre>
<p><a class="p_ident" id="p-HIbiUlanJ3" href="#p-HIbiUlanJ3" tabindex="-1" role="presentation"></a>Después de ejecutar <code>npm install</code>, NPM habrá creado un directorio llamado <code>node_modules</code>. Dentro de ese directorio estará un directorio <code>ini</code> que contiene la biblioteca. Puedes abrirlo y ver el código. Cuando importamos <code>"ini"</code>, esta biblioteca se carga, y podemos llamar a su propiedad <code>parse</code> para analizar un archivo de configuración.</p>
<p><a class="p_ident" id="p-Ed4D4eUL/o" href="#p-Ed4D4eUL/o" tabindex="-1" role="presentation"></a>Por defecto, NPM instala paquetes en el directorio actual, en lugar de en un lugar centralizado. Si estás acostumbrado a otros gestores de paquetes, esto puede parecer inusual, pero tiene ventajas: pone a cada aplicación en control total de los paquetes que instala y facilita la gestión de versiones y limpieza al eliminar una aplicación.</p>
<h3><a class="i_ident" id="i-cQf3FV14Fd" href="#i-cQf3FV14Fd" tabindex="-1" role="presentation"></a>Archivos de paquete</h3>
<p><a class="p_ident" id="p-FfYB7hx8VS" href="#p-FfYB7hx8VS" tabindex="-1" role="presentation"></a>Después de ejecutar <code>npm install</code> para instalar algún paquete, encontrarás no solo un directorio <code>node_modules</code>, sino también un archivo llamado <code>package.json</code> en tu directorio actual. Se recomienda tener tal archivo para cada proyecto. Puedes crearlo manualmente o ejecutar <code>npm init</code>. Este archivo contiene información sobre el proyecto, como su nombre y versión, y enumera sus dependencias.</p>
<p><a class="p_ident" id="p-wjvaCZSoLl" href="#p-wjvaCZSoLl" tabindex="-1" role="presentation"></a>La simulación del robot de <a href="07_robot.html">Capítulo 7</a>, modularizada en el ejercicio del <a href="10_modules.html#modular_robot">Capítulo 10</a>, podría tener un archivo <code>package.json</code> como este:</p>
<pre class="snippet" data-language="json" ><a class="c_ident" id="c-w5ItHEwYvj" href="#c-w5ItHEwYvj" tabindex="-1" role="presentation"></a>{
<span class="tok-string">"author"</span>: <span class="tok-string">"Marijn Haverbeke"</span>,
<span class="tok-string">"name"</span>: <span class="tok-string">"eloquent-javascript-robot"</span>,
<span class="tok-string">"description"</span>: <span class="tok-string">"Simulación de un robot de entrega de paquetes"</span>,
<span class="tok-string">"version"</span>: <span class="tok-string">"1.0.0"</span>,
<span class="tok-string">"main"</span>: <span class="tok-string">"run.mjs"</span>,
<span class="tok-string">"dependencies"</span>: {
<span class="tok-string">"dijkstrajs"</span>: <span class="tok-string">"^1.0.1"</span>,
<span class="tok-string">"random-item"</span>: <span class="tok-string">"^1.0.0"</span>
},
<span class="tok-string">"license"</span>: <span class="tok-string">"ISC"</span>
}</pre>
<p><a class="p_ident" id="p-a5gRcLiGid" href="#p-a5gRcLiGid" tabindex="-1" role="presentation"></a>Cuando ejecutas <code>npm install</code> sin especificar un paquete para instalar, NPM instalará las dependencias enumeradas en <code>package.json</code>. Cuando instalas un paquete específico que no está listado como una dependencia, NPM lo añadirá a <code>package.json</code>.</p>
<h3><a class="i_ident" id="i-dPYCknJANY" href="#i-dPYCknJANY" tabindex="-1" role="presentation"></a>Versiones</h3>
<p><a class="p_ident" id="p-ppapKEIy7c" href="#p-ppapKEIy7c" tabindex="-1" role="presentation"></a>Un archivo <code>package.json</code> lista tanto la versión del propio programa como las versiones de sus dependencias. Las versiones son una forma de manejar el hecho de que los paquetes evolucionan por separado, y el código escrito para funcionar con un paquete tal como existía en un momento dado puede no funcionar con una versión posterior y modificada del paquete.</p>
<p><a class="p_ident" id="p-hXBNL5Yk0x" href="#p-hXBNL5Yk0x" tabindex="-1" role="presentation"></a>NPM exige que sus paquetes sigan un esquema llamado <em>semantic versioning</em>, que codifica información sobre qué versiones son <em>compatibles</em> (no rompen la antigua interfaz) en el número de versión. Una versión semántica consiste en tres números, separados por puntos, como <code>2.3.0</code>. Cada vez que se añade nueva funcionalidad, el número del medio debe incrementarse. Cada vez que se rompe la compatibilidad, de modo que el código existente que utiliza el paquete puede que no funcione con la nueva versión, el primer número debe incrementarse.</p>
<p><a class="p_ident" id="p-oNddowX5th" href="#p-oNddowX5th" tabindex="-1" role="presentation"></a>Un acento circunflejo (<code>^</code>) delante del número de versión para una dependencia en <code>package.json</code> indica que se puede instalar cualquier versión compatible con el número dado. Por ejemplo, <code>"^2.<wbr>3.<wbr>0"</code> significaría que se permite cualquier versión mayor o igual a 2.3.0 y menor que 3.0.0.</p>
<p><a class="p_ident" id="p-bj5Vm8XuzQ" href="#p-bj5Vm8XuzQ" tabindex="-1" role="presentation"></a>El comando <code>npm</code> también se utiliza para publicar nuevos paquetes o nuevas versiones de paquetes. Si ejecutas <code>npm publish</code> en un directorio que tiene un archivo <code>package.json</code>, se publicará en el registro un paquete con el nombre y versión listados en el archivo JSON. Cualquiera puede publicar paquetes en NPM, aunque solo bajo un nombre de paquete que aún no esté en uso, ya que no sería bueno que personas aleatorias pudieran actualizar paquetes existentes.</p>
<p><a class="p_ident" id="p-XRV5Pi4c1o" href="#p-XRV5Pi4c1o" tabindex="-1" role="presentation"></a>Este libro no profundizará más en los detalles del uso de NPM. Consulta <a href="https://npmjs.org"><em>https://npmjs.org</em></a> para obtener más documentación y una forma de buscar paquetes.</p>
<h2><a class="h_ident" id="h-4FFpIoHRzB" href="#h-4FFpIoHRzB" tabindex="-1" role="presentation"></a>El módulo del sistema de archivos</h2>
<p><a class="p_ident" id="p-C+k/JVrGOf" href="#p-C+k/JVrGOf" tabindex="-1" role="presentation"></a>Uno de los módulos integrados más utilizados en Node es el módulo <code>node:fs</code>, que significa <em>sistema de archivos</em> (en inglés, <em>file system</em>). Exporta funciones para trabajar con archivos y directorios.</p>
<p><a class="p_ident" id="p-0fez9kQsMK" href="#p-0fez9kQsMK" tabindex="-1" role="presentation"></a>Por ejemplo, la función llamada <code>readFile</code> lee un archivo y luego llama a una función de callback con el contenido del archivo.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-VsqkE3ANgm" href="#c-VsqkE3ANgm" tabindex="-1" role="presentation"></a>import {readFile} from "node:fs";
readFile("archivo.txt", "utf8", (error, texto) => {
if (error) throw error;
console.log("El archivo contiene:", texto);
});</pre>
<p><a class="p_ident" id="p-Tr9Tr55xIA" href="#p-Tr9Tr55xIA" tabindex="-1" role="presentation"></a>El segundo argumento de <code>readFile</code> indica la <em>codificación de caracteres</em> utilizada para decodificar el archivo en una cadena. Existen varias formas en las que el texto puede ser codificado en datos binarios, pero la mayoría de los sistemas modernos utilizan UTF-8. Entonces, a menos que tengas razones para creer que se utiliza otra codificación, pasa <code>"utf8"</code> al leer un archivo de texto. Si no pasas una codificación, Node asumirá que estás interesado en los datos binarios y te dará un objeto <code>Buffer</code> en lugar de una cadena. Este es un objeto similar a un array que contiene números que representan los bytes (trozos de datos de 8 bits) en los archivos.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-nnbBYaleDu" href="#c-nnbBYaleDu" tabindex="-1" role="presentation"></a>import {readFile} from "node:fs";
readFile("archivo.txt", (error, buffer) => {
if (error) throw error;
console.log("El archivo contenía", buffer.length, "bytes.",
"El primer byte es:", buffer[0]);
});</pre>
<p><a class="p_ident" id="p-om2+u0JXVt" href="#p-om2+u0JXVt" tabindex="-1" role="presentation"></a>Una función similar, <code>writeFile</code>, se utiliza para escribir un archivo en el disco.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-eMus/6Pliv" href="#c-eMus/6Pliv" tabindex="-1" role="presentation"></a>import {writeFile} from "node:fs";
writeFile("graffiti.txt", "Node estuvo aquí", err => {
if (err) console.log(`Error al escribir el archivo: ${err}`);
else console.log("Archivo escrito.");
});</pre>
<p><a class="p_ident" id="p-gUnVPgRs1A" href="#p-gUnVPgRs1A" tabindex="-1" role="presentation"></a>Aquí no fue necesario especificar la codificación: <code>writeFile</code> asumirá que cuando se le da una cadena para escribir, en lugar de un objeto <code>Buffer</code>, debe escribirla como texto utilizando su codificación de caracteres predeterminada, que es UTF-8.</p>
<p><a class="p_ident" id="p-2Dle5H5be+" href="#p-2Dle5H5be+" tabindex="-1" role="presentation"></a>El módulo <code>node:fs</code> contiene muchas otras funciones útiles: <code>readdir</code> te dará los archivos en un directorio como un array de cadenas, <code>stat</code> recuperará información sobre un archivo, <code>rename</code> cambiará el nombre de un archivo, <code>unlink</code> lo eliminará, etc. Consulta la documentación en <a href="https://nodejs.org"><em>https://nodejs.org</em></a> para obtener detalles específicos.</p>
<p><a class="p_ident" id="p-kpvwRcuKUy" href="#p-kpvwRcuKUy" tabindex="-1" role="presentation"></a>La mayoría de estas funciones toman una función de callback como último parámetro, a la que llaman ya sea con un error (el primer argumento) o con un resultado exitoso (el segundo). Como vimos en el <a href="11_async.html">Capítulo 11</a>, hay desventajas en este estilo de programación, siendo la mayor que el manejo de errores se vuelve verboso y propenso a errores.</p>
<p><a class="p_ident" id="p-mjIQmHwkIo" href="#p-mjIQmHwkIo" tabindex="-1" role="presentation"></a>El módulo <code>node:fs/promises</code> exporta la mayoría de las mismas funciones que el antiguo módulo <code>node:fs</code>, pero utiliza promesas en lugar de funciones de callback.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-Zh54f68FoC" href="#c-Zh54f68FoC" tabindex="-1" role="presentation"></a>import {readFile} from "node:fs/promises";
readFile("file.txt", "utf8")
.then(text => console.log("El archivo contiene:", text));</pre>
<p><a class="p_ident" id="p-eHp/9t5Scg" href="#p-eHp/9t5Scg" tabindex="-1" role="presentation"></a>A veces no necesitas asincronía y simplemente te estorba. Muchas de las funciones en <code>node:fs</code> también tienen una variante sincrónica, que tiene el mismo nombre con <code>Sync</code> agregado al final. Por ejemplo, la versión sincrónica de <code>readFile</code> se llama <code>readFileSync</code>.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-F1N+amKpIz" href="#c-F1N+amKpIz" tabindex="-1" role="presentation"></a>import {readFileSync} from "node:fs";
console.log("El archivo contiene:",
readFileSync("file.txt", "utf8"));</pre>
<p><a class="p_ident" id="p-UBT8FAnlTu" href="#p-UBT8FAnlTu" tabindex="-1" role="presentation"></a>Cabe destacar que mientras se realiza una operación sincrónica de este tipo, tu programa se detiene por completo. Si debería estar respondiendo al usuario o a otras máquinas en la red, quedarse atrapado en una acción sincrónica podría producir retrasos molestos.</p>
<h2><a class="h_ident" id="h-dabppseVCT" href="#h-dabppseVCT" tabindex="-1" role="presentation"></a>El módulo HTTP</h2>
<p><a class="p_ident" id="p-af0Pfzl8K0" href="#p-af0Pfzl8K0" tabindex="-1" role="presentation"></a>Otro módulo central se llama <code>node:http</code>. Proporciona funcionalidad para ejecutar un servidor HTTP.</p>
<p><a class="p_ident" id="p-k3MuCWd5Dq" href="#p-k3MuCWd5Dq" tabindex="-1" role="presentation"></a>Esto es todo lo que se necesita para iniciar un servidor HTTP:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-dxKrKAofVd" href="#c-dxKrKAofVd" tabindex="-1" role="presentation"></a>import {createServer} from "node:http";
let server = createServer((solicitud, respuesta) => {
respuesta.writeHead(200, {"Content-Type": "text/html"});
respuesta.write(`
<h1>¡Hola!</h1>
<p>Has pedido <code>${solicitud.url}</code></p>`);
respuesta.end();
});
server.listen(8000);
console.log("¡Escuchando! (puerto 8000)");</pre>
<p><a class="p_ident" id="p-nrNhyBCrAg" href="#p-nrNhyBCrAg" tabindex="-1" role="presentation"></a>Si ejecutas este script en tu propia máquina, puedes apuntar tu navegador web a <a href="http://localhost:8000/hello">_http://localhost:8000/hello</a> para hacer una solicitud a tu servidor. Responderá con una pequeña página HTML.</p>
<p><a class="p_ident" id="p-INGY/PNK5X" href="#p-INGY/PNK5X" tabindex="-1" role="presentation"></a>La función pasada como argumento a <code>createServer</code> se llama cada vez que un cliente se conecta al servidor. Los enlaces <code>solicitud</code> y <code>respuesta</code> son objetos que representan los datos de entrada y salida. El primero contiene información sobre la solicitud, como su propiedad <code>url</code>, que nos dice a qué URL se hizo la solicitud.</p>
<p><a class="p_ident" id="p-9kr8owkFll" href="#p-9kr8owkFll" tabindex="-1" role="presentation"></a>Así que, cuando abres esa página en tu navegador, envía una solicitud a tu propia computadora. Esto hace que la función del servidor se ejecute y envíe una respuesta, que luego puedes ver en el navegador.</p>
<p><a class="p_ident" id="p-pM6WTnWBJW" href="#p-pM6WTnWBJW" tabindex="-1" role="presentation"></a>Para enviar algo al cliente, llamas a métodos en el objeto <code>respuesta</code>. El primero, <code>writeHead</code>, escribirá los encabezados de respuesta (ver <a href="18_http.html#headers">Capítulo 18</a>). Le das el código de estado (200 para “OK” en este caso) y un objeto que contiene valores de encabezado. El ejemplo establece el encabezado <code>Content-Type</code> para informar al cliente que estaremos enviando de vuelta un documento HTML.</p>
<p><a class="p_ident" id="p-8ynV8UjqtW" href="#p-8ynV8UjqtW" tabindex="-1" role="presentation"></a>A continuación, el cuerpo de la respuesta (el documento en sí) se envía con <code>response.write</code>. Se permite llamar a este método varias veces si deseas enviar la respuesta pieza por pieza, por ejemplo para transmitir datos al cliente a medida que estén disponibles. Por último, <code>response.end</code> señala el fin de la respuesta.</p>
<p><a class="p_ident" id="p-QpKSlbY/vo" href="#p-QpKSlbY/vo" tabindex="-1" role="presentation"></a>La llamada a <code>server.listen</code> hace que el servidor comience a esperar conexiones en el puerto 8000. Por eso debes conectarte a <em>localhost:8000</em> para comunicarte con este servidor, en lugar de simplemente a <em>localhost</em>, que usaría el puerto predeterminado 80.</p>
<p><a class="p_ident" id="p-939YnPvrTt" href="#p-939YnPvrTt" tabindex="-1" role="presentation"></a>Cuando ejecutas este script, el proceso se queda esperando. Cuando un script está escuchando eventos —en este caso, conexiones de red—, <code>node</code> no se cerrará automáticamente al llegar al final del script. Para cerrarlo, presiona <span class="keyname">control</span>-C.</p>
<p><a class="p_ident" id="p-WmCudMYcSj" href="#p-WmCudMYcSj" tabindex="-1" role="presentation"></a>Un verdadero servidor web server usualmente hace más cosas que el del ejemplo; examina el método de la solicitud (la propiedad <code>method</code>) para ver qué acción está intentando realizar el cliente y mira la URL de la solicitud para descubrir sobre qué recurso se está realizando esta acción. Veremos un servidor más avanzado <a href="20_node.html#file_server">más adelante en este capítulo</a>.</p>
<p><a class="p_ident" id="p-btbFVE7L41" href="#p-btbFVE7L41" tabindex="-1" role="presentation"></a>El módulo <code>node:http</code> también provee una función <code>request</code>, que se puede usar para hacer solicitudes HTTP. Sin embargo, es mucho más engorroso de usar que <code>fetch</code>, que vimos en el <a href="18_http.html">Capítulo 18</a>. Afortunadamente, <code>fetch</code> también está disponible en Node, como una asociación global. A menos que desees hacer algo muy específico, como procesar el documento de respuesta pieza por pieza a medida que llegan los datos a través de la red, recomiendo usar <code>fetch</code>.</p>
<h2><a class="h_ident" id="h-/tdw6BShQX" href="#h-/tdw6BShQX" tabindex="-1" role="presentation"></a>Flujos</h2>
<p><a class="p_ident" id="p-8QZY0sXowQ" href="#p-8QZY0sXowQ" tabindex="-1" role="presentation"></a>El objeto de respuesta al que el servidor HTTP podría escribir es un ejemplo de un objeto de <em>flujo de escritura</em>, que es un concepto ampliamente usado en Node. Estos objetos tienen un método <code>write</code> al que se puede pasar una cadena o un objeto <code>Buffer</code> para escribir algo en el flujo. Su método <code>end</code> cierra el flujo y opcionalmente toma un valor para escribir en el flujo antes de cerrarlo. Ambos métodos también pueden recibir una función de callback como argumento adicional, que se llamará cuando la escritura o el cierre hayan finalizado.</p>
<p><a class="p_ident" id="p-3tXq6zb5kH" href="#p-3tXq6zb5kH" tabindex="-1" role="presentation"></a>Es posible crear un flujo de escritura que apunte a un archivo con la función <code>createWriteStream</code> del módulo <code>node:fs</code>. Luego puedes usar el método <code>write</code> en el objeto resultante para escribir el archivo pieza por pieza, en lugar de hacerlo de una sola vez como con <code>writeFile</code>.</p>
<p><a class="p_ident" id="p-agbkP1opi0" href="#p-agbkP1opi0" tabindex="-1" role="presentation"></a>Los <em>flujos legibles</em> son un poco más complejos. El argumento <code>request</code> para la función de callback del servidor HTTP es un flujo legible. Leer de un flujo se hace utilizando manejadores de eventos, en lugar de métodos.</p>
<p><a class="p_ident" id="p-o2U33sZhdp" href="#p-o2U33sZhdp" tabindex="-1" role="presentation"></a>Los objetos que emiten eventos en Node tienen un método llamado <code>on</code> que es similar al método <code>addEventListener</code> en el navegador. Le das un nombre de evento y luego una función, y registrará esa función para que se llame cada vez que ocurra el evento dado.</p>
<p><a class="p_ident" id="p-F0B4A4f9Iq" href="#p-F0B4A4f9Iq" tabindex="-1" role="presentation"></a>Los flujos legibles tienen eventos <code>"data"</code> y <code>"end"</code>. El primero se dispara cada vez que llegan datos, y el segundo se llama cuando el flujo llega a su fin. Este modelo es más adecuado para <em>flujos</em> de datos que pueden procesarse de inmediato, incluso cuando todo el documento aún no está disponible. Un archivo se puede leer como un flujo legible utilizando la función <code>createReadStream</code> de <code>node:fs</code>.</p>
<p><a class="p_ident" id="p-KSoYRgbMl2" href="#p-KSoYRgbMl2" tabindex="-1" role="presentation"></a>Este código crea un servidor que lee los cuerpos de las solicitudes y los reenvía al cliente como texto en mayúsculas:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-dncpo30aqE" href="#c-dncpo30aqE" tabindex="-1" role="presentation"></a>import {createServer} from "node:http";
createServer((solicitud, respuesta) => {
respuesta.writeHead(200, {"Content-Type": "text/plain"});
solicitud.on("data", fragmento =>
respuesta.write(fragmento.toString().toUpperCase()));
solicitud.on("end", () => respuesta.end());
}).listen(8000);</pre>
<p><a class="p_ident" id="p-uGKPxOs3On" href="#p-uGKPxOs3On" tabindex="-1" role="presentation"></a>El valor <code>fragmento</code> pasado al manejador de datos será un <code>Buffer</code> binario. Podemos convertir esto a una cadena decodificándolo como caracteres codificados en UTF-8 con su método <code>toString</code>.</p>
<p><a class="p_ident" id="p-raOCi6Y4Ip" href="#p-raOCi6Y4Ip" tabindex="-1" role="presentation"></a>El siguiente fragmento de código, cuando se ejecuta con el servidor de mayúsculas activo, enviará una solicitud a ese servidor y escribirá la respuesta que recibe:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-oHkRAhUdbQ" href="#c-oHkRAhUdbQ" tabindex="-1" role="presentation"></a>fetch("http://localhost:8000/", {
method: "POST",
body: "Hola servidor"
}).then(resp => resp.text()).then(console.log);
// → HOLA SERVIDOR</pre>
<h2 id="file_server"><a class="h_ident" id="h-uA++Rty3OD" href="#h-uA++Rty3OD" tabindex="-1" role="presentation"></a>Un servidor de archivos</h2>
<p><a class="p_ident" id="p-jU7doWE9wG" href="#p-jU7doWE9wG" tabindex="-1" role="presentation"></a>Combina nuestro nuevo conocimiento sobre los servidores HTTP y el trabajo con el sistema de archivos para crear un puente entre ambos: un servidor HTTP que permite el acceso remoto a un sistema de archivos. Este tipo de servidor tiene todo tipo de usos, como permitir que las aplicaciones web almacenen y compartan datos, o dar acceso compartido a un grupo de personas a un montón de archivos.</p>
<p><a class="p_ident" id="p-Zhrg6JSTjX" href="#p-Zhrg6JSTjX" tabindex="-1" role="presentation"></a>Cuando tratamos los archivos como recursos de HTTP, los métodos HTTP <code>GET</code>, <code>PUT</code> y <code>DELETE</code> se pueden usar para leer, escribir y eliminar los archivos, respectivamente. Interpretaremos la ruta en la solicitud como la ruta del archivo al que se refiere la solicitud.</p>
<p><a class="p_ident" id="p-y3l0EFr6+q" href="#p-y3l0EFr6+q" tabindex="-1" role="presentation"></a>Probablemente no queramos compartir todo nuestro sistema de archivos, por lo que interpretaremos estas rutas como comenzando en el directorio de trabajo del servidor, que es el directorio en el que se inició. Si ejecuté el servidor desde <code>/tmp/public/</code> (o <code>C:\tmp\public\</code> en Windows), entonces una solicitud para <code>/file.txt</code> debería referirse a <code>/<wbr>tmp/<wbr>public/<wbr>file.<wbr>txt</code> (o <code>C:\tmp\public\file.<wbr>txt</code>, respectivamente).</p>
<p><a class="p_ident" id="p-oUNTEGADKj" href="#p-oUNTEGADKj" tabindex="-1" role="presentation"></a>Construiremos el programa paso a paso, utilizando un objeto llamado <code>methods</code> para almacenar las funciones que manejan los diferentes métodos HTTP. Los manejadores de métodos son funciones <code>async</code> que reciben el objeto de solicitud como argumento y devuelven una promesa que se resuelve a un objeto que describe la respuesta.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-t1jsLbJd7n" href="#c-t1jsLbJd7n" tabindex="-1" role="presentation"></a>import {createServer} from "node:http";
const methods = Object.create(null);
createServer((request, response) => {
let handler = methods[request.method] || notAllowed;
handler(request).catch(error => {
if (error.status != null) return error;
return {body: String(error), status: 500};
}).then(({body, status = 200, type = "text/plain"}) => {
response.writeHead(status, {"Content-Type": type});
if (body && body.pipe) body.pipe(response);
else response.end(body);
});
}).listen(8000);
async function notAllowed(request) {
return {
status: 405,
body: `Método ${request.method} no permitido.`
};
}</pre>
<p><a class="p_ident" id="p-+zXJyjiv+x" href="#p-+zXJyjiv+x" tabindex="-1" role="presentation"></a>Esto inicia un servidor que simplemente devuelve respuestas de error 405, que es el código utilizado para indicar que el servidor se niega a manejar un método determinado.</p>
<p><a class="p_ident" id="p-LQFPuDuYA9" href="#p-LQFPuDuYA9" tabindex="-1" role="presentation"></a>Cuando la promesa de un manejador de solicitud es rechazada, la llamada a <code>catch</code> traduce el error en un objeto de respuesta, si aún no lo es, para que el servidor pueda enviar una respuesta de error para informar al cliente que no pudo manejar la solicitud.</p>
<p><a class="p_ident" id="p-1mh3Zz7i88" href="#p-1mh3Zz7i88" tabindex="-1" role="presentation"></a>El campo <code>status</code> de la descripción de la respuesta puede omitirse, en cuyo caso se establece en 200 (OK) por defecto. El tipo de contenido, en la propiedad <code>type</code>, también puede omitirse, en cuyo caso se asume que la respuesta es texto plano.</p>
<p><a class="p_ident" id="p-Lrm7n6XIp1" href="#p-Lrm7n6XIp1" tabindex="-1" role="presentation"></a>Cuando el valor de <code>body</code> es un readable stream, este tendrá un método <code>pipe</code> que se utiliza para reenviar todo el contenido de un flujo de lectura a un flujo de escritura. Si no es así, se asume que es <code>null</code> (sin cuerpo), una cadena o un búfer, y se pasa directamente al método <code>end</code> del response.</p>
<p><a class="p_ident" id="p-dslBfnWhvL" href="#p-dslBfnWhvL" tabindex="-1" role="presentation"></a>Para determinar qué ruta de archivo corresponde a una URL de solicitud, la función <code>urlPath</code> utiliza la clase integrada <code>URL</code> (que también existe en el navegador) para analizar la URL. Este constructor espera una URL completa, no solo la parte que comienza con la barra diagonal que obtenemos de <code>request.url</code>, por lo que le proporcionamos un nombre de dominio falso para completar. Extrae su ruta, que será algo como <code>"/<wbr>archivo.<wbr>txt"</code>, la decodifica para eliminar los códigos de escape estilo <code>%20</code>, y la resuelve en relación con el directorio de trabajo del programa.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-qUo0ijb1sS" href="#c-qUo0ijb1sS" tabindex="-1" role="presentation"></a>import {parse} from "node:url";
import {resolve, sep} from "node:path";
const baseDirectory = process.cwd();
function urlPath(url) {
let {pathname} = new URL(url, "http://d");
let path = resolve(decodeURIComponent(pathname).slice(1));
if (path != baseDirectory &&
!path.startsWith(baseDirectory + sep)) {
throw {status: 403, body: "Prohibido"};
}
return path;
}</pre>
<p><a class="p_ident" id="p-dFcFdVohrh" href="#p-dFcFdVohrh" tabindex="-1" role="presentation"></a>Tan pronto como configuras un programa para aceptar solicitudes de red, debes empezar a preocuparte por la seguridad. En este caso, si no tenemos cuidado, es probable que terminemos exponiendo accidentalmente todo nuestro sistema de archivos a la red.</p>
<p><a class="p_ident" id="p-xDAA5DC9ZB" href="#p-xDAA5DC9ZB" tabindex="-1" role="presentation"></a>Las rutas de archivos son cadenas en Node. Para mapear dicha cadena a un archivo real, hay una cantidad no trivial de interpretación en juego. Las rutas pueden, por ejemplo, incluir <code>../</code> para hacer referencia a un directorio padre. Así que una fuente obvia de problemas serían las solicitudes de rutas como <code>/<wbr>.<wbr>./<wbr>archivo_secreto</code>.</p>
<p><a class="p_ident" id="p-0MnFEqoNvg" href="#p-0MnFEqoNvg" tabindex="-1" role="presentation"></a>Para evitar tales problemas, <code>urlPath</code> utiliza la función <code>resolve</code> del módulo <code>node:path</code>, que resuelve rutas relativas. Luego verifica que el resultado esté <em>debajo</em> del directorio de trabajo. La función <code>process.cwd</code> (donde <code>cwd</code> significa “directorio de trabajo actual”) se puede usar para encontrar este directorio de trabajo. El vínculo <code>sep</code> del paquete <code>node:path</code> es el separador de ruta del sistema: una barra invertida en Windows y una barra diagonal en la mayoría de otros sistemas. Cuando la ruta no comienza con el directorio base, la función arroja un objeto de respuesta de error, usando el código de estado HTTP que indica que el acceso al recurso está prohibido.</p>
<p><a class="p_ident" id="p-ixwH011iiN" href="#p-ixwH011iiN" tabindex="-1" role="presentation"></a>Configuraremos el método <code>GET</code> para devolver una lista de archivos al leer un directorio y para devolver el contenido del archivo al leer un archivo normal.</p>
<p><a class="p_ident" id="p-bXBiG1wVg7" href="#p-bXBiG1wVg7" tabindex="-1" role="presentation"></a>Una pregunta complicada es qué tipo de encabezado <code>Content-Type</code> debemos establecer al devolver el contenido de un archivo. Dado que estos archivos podrían ser cualquier cosa, nuestro servidor no puede simplemente devolver el mismo tipo de contenido para todos ellos. El gestor npm puede ayudarnos nuevamente aquí. El paquete <code>mime-types</code> (los indicadores de tipo de contenido como <code>text/plain</code> también se llaman <em>tipos MIME</em>) conoce el tipo correcto para una gran cantidad de extensiones de archivo.</p>
<p><a class="p_ident" id="p-MSwHYukw8J" href="#p-MSwHYukw8J" tabindex="-1" role="presentation"></a>El siguiente comando de <code>npm</code>, en el directorio donde reside el script del servidor, instala una versión específica de <code>mime</code>:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-EObiWUZetK" href="#c-EObiWUZetK" tabindex="-1" role="presentation"></a>$ npm install mime-types@2.1.0</pre>
<p><a class="p_ident" id="p-oX7svPGvOz" href="#p-oX7svPGvOz" tabindex="-1" role="presentation"></a>Cuando un archivo solicitado no existe, el código de estado HTTP correcto a devolver es 404. Utilizaremos la función <code>stat</code>, que busca información sobre un archivo, para averiguar tanto si el archivo existe como si es un directorio.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-HNsvbqDhZq" href="#c-HNsvbqDhZq" tabindex="-1" role="presentation"></a>import {createReadStream} from "node:fs";
import {stat, readdir} from "node:fs/promises";
import {lookup} from "mime-types";
methods.GET = async function(request) {
let path = urlPath(request.url);
let stats;
try {
stats = await stat(path);
} catch (error) {
if (error.code != "ENOENT") throw error;
else return {status: 404, body: "Archivo no encontrado"};
}
if (stats.isDirectory()) {
return {body: (await readdir(path)).join("\n")};
} else {
return {body: createReadStream(path),
type: lookup(path)};
}
};</pre>
<p><a class="p_ident" id="p-fhmsDfHQpL" href="#p-fhmsDfHQpL" tabindex="-1" role="presentation"></a>Como debe acceder al disco, lo que podría llevar algún tiempo, <code>stat</code> es asíncrono. Dado que estamos utilizando promesas en lugar del estilo de funcinoes de callback, hay que importar desde <code>node:fs/promises</code> en lugar de directamente desde <code>node:fs</code>.</p>
<p><a class="p_ident" id="p-JZjlGZPnMe" href="#p-JZjlGZPnMe" tabindex="-1" role="presentation"></a>Cuando el archivo no existe, <code>stat</code> lanzará un objeto de error con una propiedad <code>code</code> de <code>"ENOENT"</code>. Estos códigos extraños, inspirados en Unix, son la forma en que se reconocen los tipos de error en Node.</p>
<p><a class="p_ident" id="p-3QdcZTE5z7" href="#p-3QdcZTE5z7" tabindex="-1" role="presentation"></a>El objeto <code>stats</code> devuelto por <code>stat</code> nos indica varias cosas sobre un archivo, como su tamaño (propiedad <code>size</code>) y su fecha de modificación (<code>mtime</code>). Aquí nos interesa saber si se trata de un directorio o de un archivo sin más. Esto nos lo dice el método <code>isDirectory</code>.</p>
<p><a class="p_ident" id="p-h0xB1EOSYW" href="#p-h0xB1EOSYW" tabindex="-1" role="presentation"></a>Usamos <code>readdir</code> para leer el array de archivos en un directorio y devolverlo al cliente. Para archivos normales, creamos un flujo de lectura con <code>createReadStream</code> y lo devolvemos como cuerpo, junto con el tipo de contenido que nos proporciona el paquete <code>mime</code> para el nombre del archivo.</p>
<p><a class="p_ident" id="p-y4nquA3rgd" href="#p-y4nquA3rgd" tabindex="-1" role="presentation"></a>El código para manejar las solicitudes <code>DELETE</code> es ligeramente más sencillo.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-JlCZDm9eg3" href="#c-JlCZDm9eg3" tabindex="-1" role="presentation"></a>import {rmdir, unlink} from "node:fs/promises";
methods.DELETE = async function(request) {
let path = urlPath(request.url);
let stats;
try {
stats = await stat(path);
} catch (error) {
if (error.code != "ENOENT") throw error;
else return {status: 204};
}
if (stats.isDirectory()) await rmdir(path);
else await unlink(path);
return {status: 204};
};</pre>
<p><a class="p_ident" id="p-i19kOGm2ta" href="#p-i19kOGm2ta" tabindex="-1" role="presentation"></a>Cuando una respuesta HTTP no contiene datos, se puede usar el código de estado 204 (“sin contenido”) para indicarlo. Dado que la respuesta a la eliminación no necesita transmitir ninguna información más allá de si la operación tuvo éxito, es sensato devolver eso en este caso.</p>
<p><a class="p_ident" id="p-SU+s9xoEsL" href="#p-SU+s9xoEsL" tabindex="-1" role="presentation"></a>Es posible que te preguntes por qué intentar eliminar un archivo inexistente devuelve un código de estado de éxito en lugar de un error. Cuando el archivo que se está eliminando no está presente, se podría decir que el objetivo de la solicitud ya se ha cumplido. El estándar HTTP nos anima a hacer solicitudes <em>idempotentes</em>, lo que significa que hacer la misma solicitud varias veces produce el mismo resultado que hacerla una vez. De cierta manera, si intentas eliminar una cosa que ya no está, el efecto que intentabas lograr se ha alcanzado: la cosa ya no está allí.</p>
<p><a class="p_ident" id="p-ARSP8HNYDA" href="#p-ARSP8HNYDA" tabindex="-1" role="presentation"></a>Este es el manejador para las solicitudes <code>PUT</code>:</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-ChtmXskNos" href="#c-ChtmXskNos" tabindex="-1" role="presentation"></a>import {createWriteStream} from "node:fs";
function pipeStream(from, to) {
return new Promise((resolve, reject) => {
from.on("error", reject);
to.on("error", reject);
to.on("finish", resolve);
from.pipe(to);
});
}
methods.PUT = async function(request) {
let path = urlPath(request.url);
await pipeStream(request, createWriteStream(path));
return {status: 204};
};</pre>
<p><a class="p_ident" id="p-rQWta+cFGd" href="#p-rQWta+cFGd" tabindex="-1" role="presentation"></a>Esta vez no necesitamos verificar si el archivo existe; si lo hace, simplemente lo sobrescribiremos. Nuevamente usamos <code>pipe</code> para mover datos de un flujo legible a uno escribible, en este caso de la request al archivo. Pero como <code>pipe</code> no está diseñado para devolver una promesa, debemos escribir un contenedor, <code>pipeStream</code>, que cree una promesa alrededor del resultado de llamar a <code>pipe</code>.</p>
<p><a class="p_ident" id="p-JjAdEY1T6W" href="#p-JjAdEY1T6W" tabindex="-1" role="presentation"></a>Cuando algo sale mal al abrir el archivo, <code>createWriteStream</code> seguirá devolviendo un flujo, pero ese flujo lanzará un evento de <code>"error"</code>. El flujo de la request también puede fallar, por ejemplo si la red falla. Por lo tanto, conectamos los eventos de <code>"error"</code> de ambos flujos para rechazar la promesa. Cuando <code>pipe</code> haya terminado, cerrará el flujo de salida, lo que hará que lance un evento de <code>"finalización"</code>. En ese momento podemos resolver la promesa con éxito (devolviendo nada).</p>
<p><a class="p_ident" id="p-8kKGPbzzF1" href="#p-8kKGPbzzF1" tabindex="-1" role="presentation"></a>El script completo del servidor está disponible en el siguiente enlace: <a href="https://eloquentjavascript.net/code/file_server.mjs"><em>https://eloquentjavascript.net/code/file_server.mjs</em></a>. Puedes descargarlo y, después de instalar sus dependencias, ejecutarlo con Node para iniciar tu propio servidor de archivos. Y, por supuesto, puedes modificarlo y ampliarlo para resolver los ejercicios de este capítulo o para experimentar.</p>
<p><a class="p_ident" id="p-hd8NXvgo0U" href="#p-hd8NXvgo0U" tabindex="-1" role="presentation"></a>La herramienta de línea de comandos <code>curl</code>, ampliamente disponible en sistemas Unix (como macOS y Linux), se puede utilizar para hacer solicitudes HTTP. La siguiente sesión prueba brevemente nuestro servidor. La opción <code>-X</code> se usa para establecer el método de la solicitud, y <code>-d</code> se utiliza para incluir un cuerpo de solicitud.</p>
<pre class="snippet" data-language="null" ><a class="c_ident" id="c-9YZyNAzvD4" href="#c-9YZyNAzvD4" tabindex="-1" role="presentation"></a>$ curl http://localhost:8000/file.txt
Archivo no encontrado
$ curl -X PUT -d CONTENIDO http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
CONTENIDO
$ curl -X DELETE http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
Archivo no encontrado</pre>
<p><a class="p_ident" id="p-mLKdgFKylg" href="#p-mLKdgFKylg" tabindex="-1" role="presentation"></a>La primera solicitud para <code>file.txt</code> falla ya que el archivo aún no existe. La solicitud <code>PUT</code> crea el archivo y, <em>voilà</em>, la siguiente solicitud lo recupera con éxito. Después de eliminarlo con una solicitud <code>DELETE</code>, el archivo vuelve a desaparecer.</p>
<h2><a class="h_ident" id="h-NUFOUyK+lw" href="#h-NUFOUyK+lw" tabindex="-1" role="presentation"></a>Resumen</h2>
<p><a class="p_ident" id="p-ZkCAhCPqn3" href="#p-ZkCAhCPqn3" tabindex="-1" role="presentation"></a>Node es un sistema pequeño interesante que nos permite ejecutar JavaScript en un contexto no de navegador. Originalmente fue diseñado para tareas de red para desempeñar el papel de un <em>nodo</em> en una red. Sin embargo, se presta para todo tipo de tareas de script y, si disfrutas escribiendo en JavaScript, automatizar tareas con Node funciona bien.</p>
<p><a class="p_ident" id="p-mDqNwoa/J2" href="#p-mDqNwoa/J2" tabindex="-1" role="presentation"></a>NPM proporciona paquetes para todo lo que puedas imaginar (y varias cosas que probablemente nunca se te ocurrirían), y te permite descargar e instalar esos paquetes con el programa <code>npm</code>. Node viene con varios módulos integrados, incluido el módulo <code>node:fs</code> para trabajar con el sistema de archivos y el módulo <code>node:http</code> para ejecutar servidores HTTP.</p>
<p><a class="p_ident" id="p-CV2G85CEjY" href="#p-CV2G85CEjY" tabindex="-1" role="presentation"></a>Todo input y output en Node se hace de forma asíncrona, a menos que uses explícitamente una variante sincrónica de una función, como <code>readFileSync</code>. Originalmente, Node usaba funciones de callback para funcionalidades asíncronas, pero el paquete <code>node:fs/promises</code> proporciona una interfaz basada en promesas para el sistema de archivos.</p>
<h2><a class="h_ident" id="h-tkm7ntLto1" href="#h-tkm7ntLto1" tabindex="-1" role="presentation"></a>Ejercicios</h2>
<h3><a class="i_ident" id="i-F/Hf/Ag4yl" href="#i-F/Hf/Ag4yl" tabindex="-1" role="presentation"></a>Herramienta de búsqueda</h3>
<p><a class="p_ident" id="p-IXlWk51Q1o" href="#p-IXlWk51Q1o" tabindex="-1" role="presentation"></a>En los sistemas Unix, existe una herramienta de línea de comandos llamada <code>grep</code> que se puede utilizar para buscar rápidamente archivos según una expresión regular.</p>
<p><a class="p_ident" id="p-nw3FaVfQZY" href="#p-nw3FaVfQZY" tabindex="-1" role="presentation"></a>Escribe un script de Node que se pueda ejecutar desde la línea de comandos y funcione de manera similar a <code>grep</code>. Trata el primer argumento de la línea de comandos como una expresión regular y trata cualquier argumento adicional como archivos a buscar. Debería mostrar los nombres de los archivos cuyo contenido coincide con la expresión regular.</p>
<p><a class="p_ident" id="p-pdXy9ZIXbz" href="#p-pdXy9ZIXbz" tabindex="-1" role="presentation"></a>Una vez que eso funcione, extiéndelo para que cuando uno de los argumentos sea un directorio, busque en todos los archivos de ese directorio y sus subdirectorios.</p>
<p><a class="p_ident" id="p-qnRKogZMPC" href="#p-qnRKogZMPC" tabindex="-1" role="presentation"></a>Utiliza funciones asíncronas o sincrónicas del sistema de archivos según consideres adecuado. Configurar las cosas para que se soliciten múltiples acciones asíncronas al mismo tiempo podría acelerar un poco las cosas, pero no demasiado, ya que la mayoría de los sistemas de archivos solo pueden leer una cosa a la vez.</p>
<details class="solution"><summary>Mostrar pistas...</summary><div class="solution-text">
<p><a class="p_ident" id="p-HJCQsGtLBH" href="#p-HJCQsGtLBH" tabindex="-1" role="presentation"></a>Tu primer argumento de línea de comandos, la expresión regular, se puede encontrar en <code>process.argv[2]</code>. Los archivos de entrada vienen después de eso. Puedes usar el constructor <code>RegExp</code> para convertir una cadena en un objeto de expresión regular.</p>
<p><a class="p_ident" id="p-ExC1m50vRM" href="#p-ExC1m50vRM" tabindex="-1" role="presentation"></a>Hacer esto de forma sincrónica, con <code>readFileSync</code>, es más sencillo, pero si usas <code>node:fs/promises</code> para obtener funciones que devuelven promesas y escribes una función <code>async</code>, el código será parecido.</p>
<p><a class="p_ident" id="p-xmMSoqqpPB" href="#p-xmMSoqqpPB" tabindex="-1" role="presentation"></a>Para averiguar si algo es un directorio, nuevamente puedes usar <code>stat</code> (o <code>statSync</code>) y el método <code>isDirectory</code> del objeto de estadísticas.</p>
<p><a class="p_ident" id="p-bDecTy5JM0" href="#p-bDecTy5JM0" tabindex="-1" role="presentation"></a>Explorar un directorio es un proceso ramificado. Puedes hacerlo usando una función recursiva o manteniendo un array de tareas pendientes (archivos que aún deben ser explorados). Para encontrar los archivos en un directorio, puedes llamar a <code>readdir</code> o <code>readdirSync</code>. Observa la extraña manera de escribir estos nombres: el nombrado de funciones del sistema de archivos de Node se basa vagamente en las funciones estándar de Unix, como <code>readdir</code>, que son todas en minúsculas, pero luego agrega <code>Sync</code> con una letra mayúscula, como haríamos con una función de JavaScript.</p>
<p><a class="p_ident" id="p-nak1lIuwWy" href="#p-nak1lIuwWy" tabindex="-1" role="presentation"></a>Para obtener el nombre completo de un archivo leído con <code>readdir</code>, debes combinarlo con el nombre del directorio, ya sea añadiendo <code>sep</code> de <code>node:path</code> entre ellos, o utilizando la función <code>join</code> de ese mismo paquete.</p>
</div></details>
<h3><a class="i_ident" id="i-o/SovGzlc3" href="#i-o/SovGzlc3" tabindex="-1" role="presentation"></a>Creación de directorios</h3>
<p><a class="p_ident" id="p-noPcKPskXH" href="#p-noPcKPskXH" tabindex="-1" role="presentation"></a>Aunque el método <code>DELETE</code> en nuestro servidor de archivos es capaz de eliminar directorios (usando <code>rmdir</code>), actualmente el servidor no proporciona ninguna forma de <em>crear</em> un directorio.</p>
<p><a class="p_ident" id="p-t0vs3VLgD4" href="#p-t0vs3VLgD4" tabindex="-1" role="presentation"></a>Añade soporte para el método <code>MKCOL</code> (“make collection”), que debería crear un directorio llamando a <code>mkdir</code> desde el módulo <code>node:fs</code>. <code>MKCOL</code> no es un método HTTP ampliamente utilizado, pero sí existe con este mismo propósito en el estándar <em>WebDAV</em>, el cual especifica un conjunto de convenciones sobre HTTP que lo hacen adecuado para crear documentos.</p>
<details class="solution"><summary>Mostrar pistas...</summary><div class="solution-text">
<p><a class="p_ident" id="p-AVf93pz/4b" href="#p-AVf93pz/4b" tabindex="-1" role="presentation"></a>Puedes usar la función que implementa el método <code>DELETE</code> como base para el método <code>MKCOL</code>. Cuando no se encuentra ningún archivo, intenta crear un directorio con <code>mkdir</code>. Cuando existe un directorio en esa ruta, puedes devolver una respuesta 204 para que las solicitudes de creación de directorios sean idempotentes. Si existe un archivo que no es un directorio en esta ruta, devuelve un código de error. El código 400 (“solicitud incorrecta”) sería apropiado.</p>
</div></details>
<h3><a class="i_ident" id="i-Txw3NflFh8" href="#i-Txw3NflFh8" tabindex="-1" role="presentation"></a>Un espacio público en la web</h3>
<p><a class="p_ident" id="p-cVdLVfzQ+z" href="#p-cVdLVfzQ+z" tabindex="-1" role="presentation"></a>Dado que el servidor de archivos sirve cualquier tipo de archivo e incluso incluye la cabecera <code>Content-Type</code> correcta, puedes usarlo para servir un sitio web. Dado que permite a todos eliminar y reemplazar archivos, sería un tipo interesante de sitio web: uno que puede ser modificado, mejorado y vandalizado por todos aquellos que se tomen el tiempo de hacer la solicitud HTTP adecuada.</p>
<p><a class="p_ident" id="p-w5d/xudUZ/" href="#p-w5d/xudUZ/" tabindex="-1" role="presentation"></a>Escribe una página HTML básica que incluya un archivo JavaScript sencillo. Coloca los archivos en un directorio servido por el servidor de archivos y ábrelos en tu navegador.</p>
<p><a class="p_ident" id="p-dGMy9Ug18P" href="#p-dGMy9Ug18P" tabindex="-1" role="presentation"></a>Luego, como ejercicio avanzado o incluso como un proyecto de fin de semana, combina todo el conocimiento que has adquirido de este libro para construir una interfaz más amigable para modificar el sitio web —desde <em>dentro</em> del sitio web.</p>
<p><a class="p_ident" id="p-ZKY31OAsrV" href="#p-ZKY31OAsrV" tabindex="-1" role="presentation"></a>Utiliza un formulario HTML para editar el contenido de los archivos que conforman el sitio web, permitiendo al usuario actualizarlos en el servidor mediante solicitudes HTTP, como se describe en el <a href="18_http.html">Capítulo 18</a>.</p>
<p><a class="p_ident" id="p-Ogr7GZ3+wK" href="#p-Ogr7GZ3+wK" tabindex="-1" role="presentation"></a>Comienza permitiendo que solo un archivo sea editable. Luego haz que el usuario pueda seleccionar qué archivo editar. Aprovecha el hecho de que nuestro servidor de archivos devuelve listas de archivos al leer un directorio.</p>
<p><a class="p_ident" id="p-PII294GUqX" href="#p-PII294GUqX" tabindex="-1" role="presentation"></a>No trabajes directamente en el código expuesto por el servidor de archivos ya que si cometes un error, es probable que dañes los archivos que hay allí. En vez de eso, mantén tu trabajo fuera del directorio accesible al público y cópialo allí al hacer pruebas.</p>
<details class="solution"><summary>Mostrar pistas...</summary><div class="solution-text">
<p><a class="p_ident" id="p-cMkx7R4x8e" href="#p-cMkx7R4x8e" tabindex="-1" role="presentation"></a>Puedes crear un elemento <code><textarea></code> para contener el contenido del archivo que se está editando. Una solicitud <code>GET</code>, utilizando <code>fetch</code>, puede recuperar el contenido actual del archivo. Puedes usar URLs relativas como <em>index.html</em>, en lugar de <a href="http://localhost:8000/index.html"><em>http://localhost:8000/index.html</em></a>, para hacer referencia a archivos en el mismo servidor que el script en ejecución.</p>
<p><a class="p_ident" id="p-ORpeth54Uz" href="#p-ORpeth54Uz" tabindex="-1" role="presentation"></a>Luego, cuando el usuario haga clic en un botón (puedes usar un elemento <code><form></code> y el evento <code>"submit"</code>), realiza una solicitud <code>PUT</code> a la misma URL, con el contenido del <code><textarea></code> como cuerpo de la solicitud, para guardar el archivo.</p>
<p><a class="p_ident" id="p-3BlItWwtjj" href="#p-3BlItWwtjj" tabindex="-1" role="presentation"></a>Puedes luego agregar un elemento <code><select></code> que contenga todos los archivos en el directorio principal del servidor mediante la adición de elementos <code><option></code> que contengan las líneas devueltas por una solicitud <code>GET</code> a la URL <code>/</code>. Cuando el usuario seleccione otro archivo (un evento <code>"change"</code> en el campo), el script debe recuperar y mostrar ese archivo. Al guardar un archivo, utiliza el nombre de archivo actualmente seleccionado.</p>
</div></details><nav><a href="19_paint.html" title="previous chapter" aria-label="previous chapter">◂</a> <a href="index.html" title="cover" aria-label="cover">●</a> <a href="21_skillsharing.html" title="next chapter" aria-label="next chapter">▸</a> <button class=help title="help" aria-label="help"><strong>?</strong></button>
</nav>
</article>
<script src="ejs.js"></script>