14. Renderizado del lado del cliente

Versión para imprimir.

A. Introducción

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/rendercli?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Usa o crea una cuenta de Google.

  3. Crea una cuenta de Replit usando la cuenta de Google.

  4. Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.

  5. Depura el proyecto.

  6. Crea la cover page o página de spotlight del proyecto.

D. Archivos

Haz clic en los triángulos para expandir las carpetas

E. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Render en el cliente</title>
10
11</head>
12
13<body>
14
15 <h1>Render en el cliente</h1>
16
17 <dl id="lista">
18 <dt>Cargando…</dt>
19 <dd><progress max="100">Cargando…</progress></dd>
20 </dl>
21
22 <script>
23
24 // Crea y pone en funcionamiento el worker del archivo "render.js".
25 const worker = new Worker("render.js", { type: "module" })
26 // Se invoca cuando el worker envía un mensaje a la página.
27 worker.onmessage = event => {
28 const respuesta = event.data
29 if (respuesta.resultado !== undefined) {
30 lista.innerHTML = respuesta.resultado
31 } else if (respuesta.error !== undefined) {
32 lista.innerHTML = ""
33 alert(respuesta.error)
34 }
35 }
36
37 </script>
38
39</body>
40
41</html>

F. render.js

1/* Ejemplo de render en el cliente. No se usa import
2 * porque Firefox no lo soporta en los web workers. */
3
4invocaServicio("srv/lista.php")
5 .then(respuesta => {
6
7 const lista = respuesta.body
8
9 if (Array.isArray(lista)) {
10 // Genera el código HTML de la lista.
11 let render = ""
12 for (const modelo of lista) {
13 /* Codifica nombre y color para que cambie los caracteres especiales y
14 * el texto no se pueda interpretar como HTML. Esta técnica evita la
15 * inyección de código. */
16 const nombre =
17 typeof modelo.nombre === "string" ? htmlentities(modelo.nombre) : ""
18 const color =
19 typeof modelo.color === "string" ? htmlentities(modelo.color) : ""
20 render += /* html */
21 `<dt>${nombre}</dt>
22 <dd>${color}</dd>`
23 }
24
25 // Verifica si el código corre dentro de un web worker.
26 if (self instanceof WorkerGlobalScope) {
27 // Envía el render a la página que invocó este web worker.
28 self.postMessage({ resultado: render })
29 }
30 }
31 })
32 .catch(muestraErrorEnWorker)
33
34const ProblemDetails_InternalServerError = 500
35
36class ProblemDetails extends Error {
37
38 /**
39 * @param {number} status
40 * @param {string} title
41 * @param {string} [detail]
42 * @param {string} [type]
43 */
44 constructor(status, title, detail, type) {
45 super(title)
46 /** @readonly */
47 this.status = status
48 /** @readonly */
49 this.type = type
50 /** @readonly */
51 this.title = title
52 /** @readonly */
53 this.detail = detail
54 }
55
56}
57
58/**
59 * Muestra un error en la consola y en un cuadro de
60 * alerta el mensaje de una excepción.
61 * @param { ProblemDetails | Error | null } error descripción del error.
62 */
63function muestraErrorEnWorker(error) {
64 if (error === null) {
65 console.log("Error")
66 self.postMessage({ error: "Error" })
67 } else if (error instanceof ProblemDetails) {
68 let mensaje = error.title
69 if (error.detail) {
70 mensaje += `\n\n${error.detail}`
71 }
72 mensaje += `\n\nCódigo: ${error.status}`
73 if (error.type) {
74 mensaje += ` ${error.type}`
75 }
76 console.error(mensaje)
77 console.error(error)
78 self.postMessage({ error: mensaje })
79 } else {
80 console.error(error)
81 self.postMessage({ error: error.message })
82 }
83}
84
85const JsonResponse_OK = 200
86const JsonResponse_Created = 201
87const JsonResponse_NoContent = 204
88
89class JsonResponse {
90
91 /**
92 * @param {number} status
93 * @param {any} [body]
94 * @param {string} [location]
95 */
96 constructor(status, body, location) {
97 /** @readonly */
98 this.status = status
99 /** @readonly */
100 this.body = body
101 /** @readonly */
102 this.location = location
103 }
104
105}
106
107/**
108 * Espera a que la promesa de un fetch termine. Si
109 * hay error, lanza una excepción. Si no hay error,
110 * interpreta la respuesta del servidor como JSON y
111 * la convierte en una literal de objeto.
112 * @param { string | Promise<Response> } servicio
113 */
114export async function invocaServicio(servicio) {
115 let f = servicio
116 if (typeof servicio === "string") {
117 f = fetch(servicio, {
118 headers: { "Accept": "application/json, application/problem+json" }
119 })
120 } else if (!(f instanceof Promise)) {
121 throw new Error("Servicio de tipo incorrecto.")
122 }
123 const respuesta = await f
124 if (respuesta.ok) {
125 if (respuesta.status === JsonResponse_NoContent) {
126 return new JsonResponse(JsonResponse_NoContent)
127 }
128 const texto = await respuesta.text()
129 try {
130 const body = JSON.parse(texto)
131 if (respuesta.status === JsonResponse_Created) {
132 const location = respuesta.headers.get("location")
133 return new JsonResponse(JsonResponse_Created, body,
134 location === null ? undefined : location)
135 } else {
136 return new JsonResponse(JsonResponse_OK, body)
137 }
138 } catch (error) {
139 // El contenido no es JSON. Probablemente sea texto.
140 throw new ProblemDetails(ProblemDetails_InternalServerError,
141 "Problema interno en el servidor.", texto)
142 }
143 } else {
144 const texto = await respuesta.text()
145 try {
146 const { type, title, detail } = JSON.parse(texto)
147 throw new ProblemDetails(respuesta.status,
148 typeof title === "string" ? title : "",
149 typeof detail === "string" ? detail : undefined,
150 typeof type === "string" ? type : undefined)
151 } catch (error) {
152 if (error instanceof ProblemDetails) {
153 throw error
154 } else {
155 // El contenido no es JSON. Probablemente sea texto.
156 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
157 }
158 }
159 }
160}
161
162/**
163 * Codifica un texto para que cambie los caracteres
164 * especiales y no se pueda interpretar como
165 * etiiqueta HTML. Esta técnica evita la inyección
166 * de código.
167 * @param { string } texto
168*/
169function htmlentities(texto) {
170 return texto.replace(/[<>"']/g, textoDetectado => {
171 switch (textoDetectado) {
172 case "<": return "<"
173 case ">": return ">"
174 case '"': return "&quot;"
175 case "'": return "&#039;"
176 default: return textoDetectado
177 }
178 })
179}

G. Carpeta « srv »

Versión para imprimir.

A. srv / lista.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4
5ejecutaServicio(function () {
6 return [
7 [
8 "nombre" => "pp",
9 "color" => "azul"
10 ],
11 [
12 "nombre" => "kq",
13 "color" => "rojo"
14 ],
15 [
16 "nombre" => "tt",
17 "color" => "rosa"
18 ],
19 [
20 "nombre" => "bb",
21 "color" => "azul"
22 ]
23 ];
24});
25

H. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

I. Carpeta « error »

Versión para imprimir.

A. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

B. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

J. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

K. Resumen