硬核!全能 Deno 高手篇

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 6 天,点击查看活动详情


在上一篇文章中我们学习了 Deno 的组成、Deno 的基础功能和核心功能、Deno 的官方库和第三方库以及 Deno 的测试。

如果你对 Deno 还不够熟悉,推荐你去读我的上一篇文章:一文读懂 Deno


这一篇我们将会更进一步,学习 Deno 的 打包、编译、安装、Web API、REST API、调试和部署。最后会使用 fresh 框架开发一个 TodoList 应用,并部署上线。

打包、编译和安装

Deno 具有一些内置函数,这些函数能够帮助我们将多个文件分组到单个包或者可执行脚本中。这些功能在我们的开发过程中和准备部署到生产环境时非常有用。

打包

打包是一种非常典型的 Web 开发优化技术,可以减少 JavaScript、CSS 以及其他资源的请求数量和请求体积。无论是 JavaScript、CSS,还是其他资源,打包过程中都会尽可能尝试将多个文件合并到一个文件中。
Deno 通过 bundle 命令进行打包。

deno bundle [OPTIONS] <source_file> [<output>]
复制代码

1.5 版本以后,添加了 Tree Shaking 功能,可以帮我们删除没有使用到的代码,最终生成只包含我们实际使用的代码,以此来减小包的体积。
我们使用官方库中的 cat.ts 进行简单的打包测试。
源代码如下:

// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { copy } from "../streams/conversion.ts";
const filenames = Deno.args;
for (const filename of filenames) {
  const file = await Deno.open(filename);
  await copy(file, Deno.stdout);
  file.close();
}
复制代码

其中它导入了 ../streams/conversion.ts 文件。
我们使用 bundle 命令进行打包后,会将这个文件以及这个文件所依赖的文件全部打包到最终生成的文件中。
我们运行以下命令进行测试:

deno bundle https://deno.land/[email protected]/examples/cat.ts bundle-cat-example.js
复制代码

它会生成一个 bundle-cat-example.js 文件,我们可以把这个文件当作常规的脚本文件来运行。
运行它的命令是 deno run。

deno run bundle-cat-example.js test.txt
复制代码

我们打包后的文件除了可以独立运行以外,还可以在浏览器中导入或者被其他模块导入。
在浏览器中导入:

<script type="module" src="bundle.js"></script>
复制代码

在其他模块中导入:

import * as lib from 'bundle.js'
复制代码

编译

我们使用 compile 命令将脚本编译为独立的可执行文件。

deno compile [--output <OUT>] <SRC>
复制代码

在编译时,我们需要设置脚本运行时所需要的权限 flag。
下面是一个脚本代码,作用是读取相同目录下的 README.md 文件并将它的内容打印到控制台。

try {
  const decoder = new TextDecoder("utf-8");
  const data = await Deno.readFile("README.md");
  console.log(decoder.decode(data));
} catch (e) {
  console.error("An error occurrred while reading the file: ", e);
}
复制代码

我们运行以下命令对它进行编译:

deno compile --allow-read main.ts
复制代码

我们没有使用 --output 指定编译后的文件名,Deno 会创建一个叫做 usercode 的可执行文件。
现在我们就可以执行这个 usercode 文件了。

./usercode
复制代码

我们可以把这个可执行文件发给任何人,他不需要安装 Deno 环境就可以直接运行这个可执行文件。
这看上去非常灵活,但这种灵活性是有代价的,因为它会把 Deno 也集成到可执行文件中,即使对于这么小的文件来说,文件体积依旧超过了 50 M。

安装

Deno 还提供了一种安装脚本的方法。它可以创建一个可执行的 shell 脚本。
安装脚本的命令是 install,下面是示例。

deno install -n copyText --root ./ --allow-read main.ts
复制代码

我们来解释上面的命令。
我们使用 -n 或者 --name 来指定可执行文件的名字。
使用 --root 指定根路径,如果不指定,它会使用环境变量 DENO_INSTALL_ROOT 作为默认值。
install 命令会创建一个 bin 文件夹,其中包含一个 copyText 的文件。
我们可以进入到 bin 文件夹中,查看 copyText 文件。
运行 copyText 文件也非常简单,因为它是 shell 文件。

./copyText
复制代码

对比

我们已经了解了 Deno 的打包(bundle)、编译(build)和安装(install),下面是它们之间的区别。

命令 生成的文件 Deno 嵌入式 独立运行
bundle JS
build 二进制
install shell

调试

调试是一项非常重要的功能。
对 Deno 程序进行调试,需要在运行命令行中添加 --inspect 或者 --inspect-brk。这和 Node.js 是一致的。

  • inspect 允许我们在任何时间点设置调试器。
  • inspect-brk 会等待调试器,并暂停第一行代码的执行。

如果我们的脚本内容很短,那么使用 inspect-brk 是更合适的选择,因为它可以让我们有时间暂停并继续执行代码,否则程序可能会在我们开始调试之前就执行完了。
Deno 支持 V8 的 Inspector 协议,它提供了丰富的调试功能,我们可以使用任何 Chromium 开发工具(不仅仅是 Chrome)或任何支持该协议的 IDE(比如 VSCode)来调试我们的脚本。

使用 VSCode 进行调试

首先要为 VSCode 安装 Deno 扩展。
扩展下载地址:marketplace.visualstudio.com/items?itemN…
接下来要修改 Deno 的设置,建议设置工作区配置,这样不会影响其它项目。
Mac 系统使用 Cmd+Shift+p 来打开 setting.json,Windows 使用 Ctrl 替换 Cmd。
添加下面的配置。

{
    "deno.enable": true,
    "deno.lint": true,
    "deno.unstable": true
}
复制代码

这样 Deno 的关键字将不再显示错误。
接下来我们要创建启动配置文件。
创建启动配置文件有两种方式,第一种是手动创建 .vscode/launch.json 文件。第二种是利用 VSCode 扩展来创建。这里讲一下第二种情况。
选择左侧菜单的 Run and Debug 菜单,选择 create a launch.json file。
image.png
在弹出的选择框中选择 Deno。
image.png
这样就创建好了 .vscode/launch.json 文件,下面是创建出来的默认内容。

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "request": "launch",
      "name": "Launch Program",
      "type": "pwa-node",
      "program": "${workspaceFolder}/main.ts",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "/opt/homebrew/bin/deno",
      "runtimeArgs": [
        "run",
        "--inspect",
        "--allow-all"
      ],
      "attachSimplePort": 9229
    }
  ]
}
复制代码

其中 runtimeArgs 可以在运行时传递一些参数给 Deno,默认的参数有:

  • --inspect:允许在代码任意位置中设置断点。
  • --allow-all:允许所有权限。

现在就可以按下 F5 进行调试了。

使用 Chrome 进行调试

简单起见,我们可以使用 Deno 标准库中的聊天服务器程序。
运行以下命令,启动聊天服务器。

deno run --inspect-brk --allow-net https://deno.land/[email protected]/examples/chat/server.ts
复制代码

在浏览器中输入:chrome://inspect。可以看到聊天服务器。
image.png
点击 inspect,弹出 DevTools。
image.png
现在就可以进行调试了。
不过现在只是显示了一个文件,我们可以按住 cmd+p/ctrl+p 来现实所有的文件。

Web API

Deno 提供了一些 Web API,帮助我们处理一些常见的任务。

Base64 编解码

atob 和 btoa 函数可以对 base64 格式字符串进行编码/解码。

const encoded = btoa('Hello,Deno!')
console.log('编码: ', encoded)
const decoded = atob(encoded)
console.log('解码: ', decoded)
复制代码

二进制编解码

和 Base64 的编解码类似,Deno 提供了 TextEncoder 和 TextDecoder 两个 API 来实现二进制的编解码。

const stringToEncode = "Hello Deno!";

const textEncoder = new TextEncoder();
const encodedBytes = textEncoder.encode(stringToEncode);
console.log("编码: ", encodedBytes);

const textDecoder = new TextDecoder();
const plainText = textDecoder.decode(encodedBytes);
console.log("解码: ", plainText);
复制代码

密码学加密

Deno 还实现了 Web Cryptography API,它是标准的 JS API,用于加密和解密。

const uuid = crypto.randomUUID();
console.log('UUID: ', uuid);
console.log('\n---------------------------------------------------------------')


const bytes = await crypto.getRandomValues(new Uint8Array(16));
console.log('随机整数数组: ', bytes);
console.log('\n---------------------------------------------------------------');

const privateKey = await crypto.subtle.generateKey(
    { name: "HMAC", "hash": "SHA-256" }, // 算法
    true,                                // 是否可提取
    ["sign", "verify"]                   // 用途
);
console.log('密钥: ', privateKey);

const exportedKey = await crypto.subtle.exportKey(
    "raw",
    privateKey
);

// 将密钥导出为 ArrayBuffer
const exportedKeyBuffer = new Uint8Array(exportedKey);

const keyAsString = String.fromCharCode.apply(null, exportedKeyBuffer as any);
const exportedBase64 = btoa(keyAsString);
const pem = `
-----BEGIN PRIVATE KEY-----
${exportedBase64}
-----END PRIVATE KEY-----`;

console.log('导出的 PEM 密钥: ', pem);
复制代码

性能评估

在 Web 中,我们有很多种方式来测试完成某项个函数或某项功能所需要的时间。但最精准、最实用的莫过于 Performance API。
performance.now 可以获取自程序执行以来到现在的时间,单位是毫秒。
还可以通过 performance.mark 和 performance.measure 来使用标记的方式创建时间戳,从而更轻松的进行性能评估。

const tartgetSite = "https://meta.com";

const start = performance.now();
await fetch(tartgetSite);
const end = performance.now();

console.log(`访问网址: ${tartgetSite} 耗费时间: ${end - start}  ms`);


// ----------------------------------------------------------------------------

// 创建一个标记
const task1 = performance.mark('task1');

// 执行一些任务
for(let i=0; i<10000000; i++){};

// 创建第二个标记
const task2 = performance.mark('task2');

// 计算两个标记之间的差距
const measure = performance.measure('Task_Measure', 'task1', 'task2');

const res = performance.getEntriesByName("Task_Measure", "measure");
console.log('性能指标:', res)
复制代码

深度克隆

在不使用第三方库的情况下,JavaScript 中最常见的深度克隆方式是使用 JSON API。

JSON.parse(JSON.stringify(object))
复制代码

它很简单有效。但是它也不是完美的,当我们的对象中具有正则表达式、函数、日期、Map、Set 和 Blob 等复杂类型时,它会丢失数据。
Deno 使用了和 Web API 一致的 Structured Clone API,在最新版本的 Chrome 浏览器中已经可以使用。通过这种方式进行深度克隆不会丢失任何数据。

const user = {
  name: "章三",
  age: 45,
  codes: new Map().set('code', 1),
};

console.log('原始对象: ', user);

const jsonClone = JSON.parse(JSON.stringify(user));
// 使用 JSON API 会丢失 codes
console.log('\nJSON Clone: ', jsonClone);

const deepClone = structuredClone(user);
console.log('\ndeep Clone: ', deepClone); 
console.log('\n比较: ', user === deepClone); 

deepClone.age = 18;
console.log('\nuser.age: ', user.age);
console.log('\nstructuredClone.age: ', deepClone.age);
复制代码

REST API 和 oak、fresh 框架

到这里,关于 Deno 的理论部分基本上已经学完了。
接下来我们将使用 Deno 设计并实现一个具有增删改查操作的 REST API。

使用标准库创建 HTTP 服务器

Deno 在标准库中提供了 http 模块,用于创建 HTTP 客户端和服务器。
这个模块的 API 非常简单,几行代码就可以轻松创建一个 HTTP 服务器。
下面就是利用 Deno 的 HTTP 模块创建服务器的代码,它监听了 5000 端口,并在接收到请求时返回字符串“你好,我是使用 Deno 构建的服务器”。

import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { Status } from "https://deno.land/[email protected]/http/http_status.ts";

const port = 5000;

function reqHandler(req: Request): Response {
  console.log("\n接收到一个请求");
  const body = JSON.stringify({ message: "你好,我是使用 Deno 构建的服务器" });
  return new Response(body, {
    status: Status.OK, // 200
    headers: {
      "content-type": "application/json; charset=utf-8",
    },
  });
}

// 默认端口是 8000
serve(reqHandler, { port })

console.log("服务器已启动,端口: ", port);
复制代码

其中 deno.land/[email protected] 模块是 Deno 官方维护的 HTTP Status 枚举。这些枚举值是具有描述性的,比数字代码更容易阅读。
下面是它的部分源码。

export enum Status {
  OK = 200,
  /** RFC 7231, 6.3.2 */
  Created = 201,
  /** RFC 7231, 6.3.3 */
  Accepted = 202,
  /** RFC 7231, 6.3.4 */
  NonAuthoritativeInfo = 203,
  /** RFC 7231, 6.3.5 */
  NoContent = 204,
  /** RFC 7231, 6.5.1 */
  BadRequest = 400,
  /** RFC 7231, 6.6.1 */
  InternalServerError = 500,
  /** RFC 7231, 6.6.2 */
  NotImplemented = 501,
  /** RFC 7231, 6.6.3 */
  BadGateway = 502

// ...
}
复制代码

Deno 团队从 1.13 版本开始投入了大量精力来改进 HTTP 模块,不断调整它的性能和稳定性,目前已经非常成熟。
但是标准库中的 HTTP Server API 的功能比较有限,它只提供了最基础的 API,我们直接用它很难开发出大型的 HTTP 服务器程序。所以在实际情况中我们通常会使用第三方的 HTTP 模块进行开发。
第三方 HTTP 模块会基于标准库模块进行封装,增加路由、中间件、日志记录等功能。
目前 Deno 生态中最受欢迎的第三方 HTTP 模块有 Oakfresh

使用 Oak 创建 HTTP 服务器

Oak 是受 Node.js 中著名的 HTTP 框架 Koa 启发而创建的框架,对很多 Node.js 的开发者来说,它应该很熟悉。
Oak 提供了 Application 类作为根目录来管理 HTTP 服务器的请求。使用这个类,我们可以使用 use 方法来注册中间件,使用 listen 方法启动我们的服务器。
下面是使用 Oak 的示例。

import { Application } from "https://deno.land/x/[email protected]/mod.ts";

const app = new Application();
const port = 5000;

app.use((context) => {
  context.response.body = "你好,我是使用 Oak 构建的服务器";
});

await app.listen({ port });

console.log('服务器已启动,端口: ', port)
复制代码

路由

Oak 还提供了 Router 类,它可以创建出一个中间件,我们使用 use 方法将它注入到 Application 中,就可以把路由功能添加到我们服务器上。

import { Application, Router } from "https://deno.land/x/[email protected]/mod.ts";

const router = new Router();
router
  .get("/", (context) => {
   // 服务器基础路由: localhost:5400
    context.response.body = "服务根路径";
  })
  .get("/hello", (context) => {
   // 命名路由: localhost:5400/hello
  })
  .get("/hello/:id", (context) => {
   // 动态路由: localhost:5400/hello/abc
  });

const app = new Application();

// 注册中间件
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 5000 });
复制代码

allowedMethods 返回一个控制注册路由请求的中间件。
如果客户端发送了 DELETE 请求,但是我们不想在路由中实现 DELETE 请求,那这个中间件就会返回状态码:405: Not Allowed。
如果我们实现了删除方法,但是匹配的路由不支持,那么它会返回状态码:501: Not Implemented。
当然这些都属于默认行为,我们可以根据需求自定义返回内容。

CORS

我们要想让不同域名的客户端连接服务器,就需要允许跨域请求,我们可以在服务器上定义 CORS 规则,最简单的方式是使用 CORS 库:deno.land/x/cors
和 Router 类似,CORS 库也返回一个中间件,我们可以在 Application 的实例上注册这个中间件,来开启跨域请求。

import { Application } from "https://deno.land/x/[email protected]/mod.ts";
import { oakCors } from "https://deno.land/x/[email protected]/mod.ts";

const app = new Application();

//....

app.use(oakCors());

app.listen({ port: 5000 });
复制代码

如果不给 oakCors 传递参数,那么会有一个默认参数:

{
  "origin": "*",
  "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
  "preflightContinue": false,
  "optionsSuccessStatus": 204
}
复制代码

使用 fresh 实现 Todo List

Oak 是一个专注于 REST API 开发的 HTTP 框架。与 Oak 不同的是,fresh 是一个全栈框架,它最重要的两个功能是路由和模板引擎。而 Oak 的功能主要是路由。
整体来看,Oak 更像是 Node.js 中的 Koa,而 fresh 更像 Node.js 中的 Next.js。但 fresh 的前端框架是 Preact。
fresh 虽然是第三方模块,但它是由 Deno 官方团队开发和维护的。fresh 发布的非常晚,但很受欢迎。从 7 月 1 日发布至今短短一个月时间,已经在 github 上面收获了 10k star,超过 oak 4k 左右了。
现在我们要使用 fresh 开发一个完整的 CRUD 应用:Todo List。
所谓的 CRUD,C 是指 Create、R 是指 Read、U 是指 Update、D 是指 Delete。一个具备 CRUD 的项目可以说是麻雀虽小,五脏俱全了。
使用 Fresh 提供的初始化脚本可以创建新项目,唯一的要求是 Deno CLI 的版本要大于等于 1.23.0。

deno run -A -r https://fresh.deno.dev todolist
复制代码

安装过程它会询问是否使用 twind,这是一种样式框架,我们选择是。
之后它会询问是否使用 VSCode,我们同样选择是。

封装数据层

我们使用数据层来处理我们的数据。
在根目录下创建 data 文件夹,这里面将放置我们的数据层代码。
首先定义数据结构和存储层的 Schema。

export interface ITodo {
  id?: string;
  text?: string;
  completed?: boolean;
}

export type ITodos = ITodo[];

export interface ITodoListStore {
  _get(): ITodos;
  _set(todos: ITodos): void;
  get(): ITodos;
  create(todo: ITodo): ITodo;
  update(todo: ITodo): boolean;
  remove(id: string): boolean;
}
复制代码

数据层是独立的,它不需要知道底层的存储是采用什么技术,它只是提供了操作数据的接口。这么设计是为了松耦合。
Deno 实现了和 Web API 中类似的存储 API。sessionStorage 和 localStorage。这两个 API 都可以存储字符串格式的数据。存储的上限是 10 MB。
我们可以使用 sessionStore 来实现这个存储接口。
创建 todo-list.session.ts 文件。

import { ITodo, ITodoListStore, ITodos } from "./todo-list.ts";

export class TodoListStore implements ITodoListStore {
  store: Storage;
  key: "todolist";
  constructor() {
    this.store = sessionStorage;
    this.key = "todolist";
  }

  _get() {
    return JSON.parse(this.store.getItem(this.key) || "[]");
  }
  _set(todos: ITodos): void {
    this.store.setItem(this.key, JSON.stringify(todos));
  }

  get() {
    return this._get();
  }
  create(todo: ITodo) {
    todo.id = crypto.randomUUID();
    todo.completed = false;
    const todos = this._get();
    todos.push(todo);
    this._set(todos);
    return todo;
  }
  update(todo: ITodo) {
    const todos = this._get();
    const findTodo = todos.find((t: ITodo) => t.id === todo.id);
    if (!findTodo) {
      return false;
    }
    Object.keys(todo).forEach((key) => {
      findTodo[key] = todo[key as keyof ITodo];
    });
    this._set(todos);
    return true;
  }
  remove(id: string): boolean {
    const todos = this._get();
    const idx = todos.findIndex((t: ITodo) => t.id === id);
    if (idx >= 0) {
      todos.splice(idx, 1);
      this._set(todos);
      return true;
    }
    return false;
  }
}
复制代码

在全局注入数据层

我们要把数据存储的实例在程序初始化之前注入到全局,方便我们后续使用。
编写 setup.ts,在其中向 Deno 的全局对象 window 注入存储实例。

import { ITodoListStore } from "./data/todo-list.ts";
import { TodoListStore } from "./data/todo-list.memo.ts";

declare global {
  const todoListStore: ITodoListStore;
  interface Window {
    todoListStore: ITodoListStore;
  }
}

window.todoListStore = new TodoListStore();
复制代码

然后在程序入口,也就是根目录中的 main.ts 的最顶部导入 setup.ts 文件。

import './setup.ts'
复制代码

编写接口

和 Next.js 类似,routes/api 下面是约定好的接口目录。
我们需要创建 5 个接口:

  • /api/todo-list/ Get 获取所有任务列表
  • /api/todo-list/ Post 创建任务
  • /api/todo-list/:id Delete 删除任务
  • /api/todo-list/complete Put 完成任务
  • /api/todo-list/not-complete Put 未完成任务

创建 /routes/api/todo-list/index.ts 文件,编写获取所有任务列表和创建任务接口。

import { Handlers } from "$fresh/server.ts";
import { ITodo } from "../../../data/todo-list.ts";
import { getData } from "../../../utils/getData.ts";

const store = window.todoListStore

// deno-lint-ignore no-explicit-any
export function JSONtoString(json: any) {
  return JSON.stringify(json);
}

export const handler: Handlers = {
  GET(_) {
    return new Response(JSONtoString(store.get()), {
      headers: { "Content-Type": "application/json" },
    });
  },
  async POST(req) {
    const data = await getData(req.body);
    let response = null;
    if (data) {
      response = store.create(JSON.parse(data) as ITodo);
    }
    return new Response(JSONtoString(response), {
      headers: { "Content-Type": "application/json" },
    });
  },
};

复制代码

创建 /routes/api/todo-list/[id].ts 文件,中括号命名的文件是动态路由,其中 id 是动态路由参数。这个文件中是删除任务的接口。

import { Handlers } from "$fresh/server.ts";
import { JSONtoString } from "./index.ts";

const store = window.todoListStore

export const handler: Handlers = {
  DELETE(_, ctx) {
    return new Response(
      JSONtoString({ ok: store.remove(ctx.params.id as string) }),
      {
        headers: { "Content-Type": "application/json" },
      }
    );
  },
};
复制代码

创建 /routes/api/todo-list/complete.ts 文件,编写完成任务接口。

import { Handlers } from "$fresh/server.ts";
import { ITodo } from "../../../data/todo-list.ts";
import { getData } from "../../../utils/getData.ts";
import { JSONtoString } from "./index.ts";

const store = window.todoListStore

export const handler: Handlers = {
  async PUT(req) {
    const data = await getData(req.body);
    let response = null;
    if (data) {
      const todo = JSON.parse(data) as ITodo;
      todo.completed = true;
      response = store.update(todo);
    }
    return new Response(JSONtoString({ ok: response }), {
      headers: { "Content-Type": "application/json" },
    });
  },
};
复制代码

创建 /routes/api/todo-list/not-complete.ts 文件,编写未完成任务接口。

import { Handlers } from "$fresh/server.ts";
import { ITodo } from "../../../data/todo-list.ts";
import { getData } from "../../../utils/getData.ts";
import { JSONtoString } from "./index.ts";

const store = window.todoListStore

export const handler: Handlers = {
  async PUT(req) {
    const data = await getData(req.body);
    let response = null;
    if (data) {
      const todo = JSON.parse(data) as ITodo;
      todo.completed = false;
      response = store.update(todo);
    }
    return new Response(JSONtoString({ ok: response }), {
      headers: { "Content-Type": "application/json" },
    });
  },
};
复制代码

编写前端数据请求函数

在根目录下创建 apis 文件夹,再创建 todolist.ts 文件,编写前端的接口请求函数。

import { ITodo } from "../data/todo-list.ts";

export const get = async () =>
  await fetch("/api/todo-list").then((res) => res.json());

export const add = async (body: ITodo) =>
  await fetch("/api/todo-list", {
    method: "POST",
    body: JSON.stringify(body),
  }).then((res) => res.json());

export const remove = async (id: string) =>
  await fetch(`/api/todo-list/${id}`, {
    method: "DELETE",
  }).then((res) => res.json());

export const complete = async (id: string) =>
  await fetch(`/api/todo-list/complete`, {
    method: "PUT",
    body: JSON.stringify({ id }),
  }).then((res) => res.json());

export const notComplete = async (id: string) =>
  await fetch(`/api/todo-list/not-complete`, {
    method: "PUT",
    body: JSON.stringify({ id }),
  }).then((res) => res.json());
复制代码

封装按钮组件

首先封装一下按钮组件,在 components 文件夹下修改 Button.tsx 文件。

/** @jsx h */
import { h } from "preact";

export function Button(
props: h.JSX.HTMLAttributes<HTMLButtonElement> & {
 class?: string;
 }
) {
  return (
    <button
      {...props}
      class={`
      whitespace-nowrap
      py-1
      px-2
      my-1/2
      mx-1
      border-2
      rounded
      outline-none
      hover:outline-none
      focus:outline-none
      ${props.class}
      `}
      />
  );
}
复制代码

封装 TodoList 组件

在 islands 目录下创建 TodoList.tsx,这是我们主要的组件。

/** @jsx h */
import { h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import "twind/shim";
import { get, add, complete, notComplete, remove } from "../apis/todolist.ts";
import { Button } from "../components/Button.tsx";
import { ITodo, ITodos } from "../data/todo-list.ts";

const AddButton = (props: any) => (
  <Button
    {...props}
    class="text-green-600 border-green-600 hover:text-white hover:bg-green-600"
  />
);

const CompleteButton = (props: any) => (
  <Button
    {...props}
    class="text-green-300 border-green-300 hover:text-white hover:bg-green-300"
  />
);

const NotCompleteButton = (props: any) => (
  <Button
    {...props}
    class="hover:text-white text-gray-400 border-gray-400 hover:bg-gray-400"
  />
);

const RemoveButton = (props: any) => (
  <Button
    {...props}
    class="text-red-400 border-red-400 hover:text-white hover:bg-red-400"
  />
);

const TodoItem = (props: ITodo & { refresh: Function }) => (
  <div
    class={`flex  py-2 px-3 items-center border-b-4 border-slate-400
  ${props.completed ? " bg-green-100 " : " bg-red-100 "}`}
  >
    <p class="w-full text-grey-darkest">{props.text}</p>
    {props.completed ? (
      <NotCompleteButton
        onClick={async () => {
          if (props.id) {
            const res = await notComplete(props.id);
            res.ok && props.refresh();
          }
        }}
      >
        未完成
      </NotCompleteButton>
    ) : (
      <CompleteButton
        onClick={async () => {
          if (props.id) {
            const res = await complete(props.id);
            res.ok && props.refresh();
          }
        }}
      >
        完成
      </CompleteButton>
    )}
    <RemoveButton
      onClick={async () => {
        if (props.id) {
          const res = await remove(props.id);
          res.ok && props.refresh();
        }
      }}
    >
      移除
    </RemoveButton>
  </div>
);

export default function TodoList(props: any) {
  const [todoList, setTodoList] = useState<ITodos>([]);
  const inputRef = useRef<HTMLInputElement>(null);
  const refresh = () => {
    get().then((todoList) => {
      setTodoList(todoList);
      console.log(todoList, "todoList");
    });
  };
  useEffect(refresh, []);

  const _add = () => {
    const inputEl = inputRef.current;
    if (inputEl) {
      const body = {
        text: inputEl.value,
      };
      add(body)
        .then(refresh)
        .finally(() => {
          inputEl.value = "";
        });
    }
  };

  return (
    <div class="h-100 w-full flex items-center justify-center bg-teal-lightest font-sans">
      <div class="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
        <div class="mb-4">
          <h1 class="text-grey-darkest">任务列表</h1>
          <div class="flex items-center mt-4">
            <input
              class="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker"
              placeholder="添加任务"
              ref={inputRef}
              onKeyPress={(evt) => {
                evt.key === "Enter" && _add();
              }}
            ></input>
            <AddButton onClick={_add}>添加</AddButton>
          </div>
        </div>
        <div>
          {todoList.map((todo) => (
            <TodoItem {...todo} refresh={refresh} />
          ))}
        </div>
      </div>
    </div>
  );
}
复制代码

最后在 routes/index.ts 中导入 TodoList.tsx 组件即可。

/** @jsx h */
import { h } from "preact";
import TodoList from "../islands/TodoList.tsx";

export default function Page() {
  return (<TodoList />);
}
复制代码

运行

到这一步,TodoList 应用已经大功告成。
在命令行运行下列命令启动项目:

deno task start
复制代码

项目默认地址是:http://localhost:8000/

部署

在上面我们已经使用 fresh 开发了一个 TodoList 应用,现在我们来学习如何将它部署到线上。
只要我们将应用部署上线,就可以让全世界任何一个人都去使用它。
但是通常来说,部署是一个比较麻烦的过程。所以涌现了非常多的部署工具来帮助我们简化这一过程。比如 Github Actions、Vercel 等。
想象一下,如果我们只需要在 Github 上面提交代码,应用就可以自动部署,那不是非常令人舒服的事情吗?这种理想的部署方式,正是 Deno Deploy 在做的事。
Deno 的创造者曾经说过(已翻译):

Deno Deploy 是一个分布式系统,在全球范围内运行 JavaScript、TypeScript 和 WebAssembly。 该服务将 V8 JavaScript 运行时和高性能异步 Web 服务器深度集成,来提供最佳性能,而不需要不必要的中间抽象。

Deno Deploy 在全球多个数据中心运行。目前在欧洲、美洲、亚洲和澳大利亚等共 25 个地区和城市都有服务器,并且还在不断增长。
Deno Deploy 使用和 Deno CLI 相同的系统,我们不需要做其他任何事情就可以使用它。它的目标是让部署变得容易。
我们可以在 Deno Deploy 的官网(deno.com/deploy)通过 Github 账号进行注册。
登录成功后就可以创建项目了。
项目是 Deno Deploy 的核心概念,它对应的就是我们的应用程序。
我们点击 new project 就可以创建项目了。
image.png
创建项目时,我们有两种选择。

  • 从 Github 仓库部署项目
  • 创建 playground 项目

从 Github 仓库部署项目

我们只需要将项目提交到 Github,然后选择仓库、分支;输入项目名就可以创建项目了。
我们可以设置特定分支作为生产分支,这样只有提交到生产分支后才会触发自动部署。
部署模式也有两种:

  • Automatic:我们每次推送到仓库后,Deno Deploy 都会进行自动部署。它的缺点是不允许自定义构建步骤。但优点是简单。它适合大多数用户。
  • Github Action:这种模式适合我们需要自定义构建步骤的情况。我们可以将自定义操作加入到构建流中,它比自动模式更加复杂,但是它让我们对整个构建流程有了更多控制能力。

在选择生产分支和部署模式后,我们还要选择部署的文件,最后 Deno Deploy 会生成预览链接和生产链接。

创建 playground 项目

当我们需要快速部署和测试某个想法时,playground 是最快的一种方式。
playground 可以只处理一个文件,我们可以在 Deno Deploy 提供的 Web IDE 中快速修改并部署项目,以此来快速验证自己的想法。
image.png
playground 同样具备完整的自定义域名、环境变量和崩溃报告等功能。
在顶部菜单栏中,我们还可以选择自己使用的语言,比如 JavaScript、TypeScript、JSX 和 TSX。在创建项目时,默认会使用 TypeScript。
为了提高体验,我们甚至在按下保存快捷键时,它就会自动部署项目,我们根本不需要点击 Save&Deploy 按钮。只需要几秒钟,新版本的代码就上线了。
默认情况下,playground 项目是不公开的,没有人可以查看我们的代码。可以在项目设置中将它设置为公开,这样就可以共享给其他人了。
image.png

将 Todo List 应用部署上线

我们首先要将 TodoList 的源代码推送到 Github,然后就可以选择从 Github 创建项目,选择仓库、分支、入口文件(main.ts)、项目名,就可以创建成功。
项目名我使用的是 fresh-todolist,这个名字会作为子域名,子域名 URL 的格式是 project_name.deno.dev。所以项目的访问地址是:fresh-todolist.deno.dev/
但是我们访问时发现了一个 502 Bad Gateway 错误。

image.png
Deno Deploy 提供了一个 Logs 功能,可以排查线上错误。
image.png
通过排查,发现 Deno Deploy 的 Deno 版本不支持 sessionStorage API。
因为我们已经创建了接口,所以只需要换一种技术来实现接口即可。
我们可以使用内存模型来存储数据,创建 /data/todo-list.memo.ts 文件,实现 ITodoListStore 接口。

import { ITodo, ITodoListStore, ITodos } from "./todo-list.ts";

let store: ITodos = [];

export class TodoListStore implements ITodoListStore {
  _get() {
    return store;
  }
  _set(todos: ITodos): void {
    store = todos;
  }

  get() {
    return this._get();
  }
  create(todo: ITodo) {
    todo.id = crypto.randomUUID();
    todo.completed = false;
    const todos = this._get();
    todos.push(todo);
    this._set(todos);
    return todo;
  }
  update(todo: ITodo) {
    const todos = this._get();
    const findTodo = todos.find((t: ITodo) => t.id === todo.id);
    if (!findTodo) {
      return false;
    }
    Object.keys(todo).forEach((key) => {
      if (key in findTodo) {
        (findTodo as any)[key] = todo[key as keyof ITodo];
      }
    });
    this._set(todos);
    return true;
  }
  remove(id: string): boolean {
    const todos = this._get();
    const idx = todos.findIndex((t: ITodo) => t.id === id);
    if (idx >= 0) {
      todos.splice(idx, 1);
      this._set(todos);
      return true;
    }
    return false;
  }
}
复制代码

然后把 setup.ts 中的数据层改为内存模型的存储。

import { TodoListStore } from "./data/todo-list.memo.ts";
// ...
复制代码

提交代码,Deno Deploy 会重新部署项目。现在一切正常了。
可以通过 fresh-todolist.deno.dev/ 访问线上项目。

你是否需要 Deno?

通过两篇文章的学习,相比你已经能够熟练使用 Deno 了。
Deno 的学习曲线相对平坦,如果你熟悉 JavaScript 和 Node.js。那么学习 Deno 应该非常轻松。
虽然 Deno 被很多人寄予厚望,但现在断言 Deno 将来可以取代 Node.js 还为之过早。
Deno 的潜力是巨大的,但也不能太过于盲目崇拜。起码在目前来说,Node.js 更加成熟稳定。
如果现在我们已经使用 Node.js 开发了一个应用,我们不应该过于着急的将它迁移到 Deno。Node.js 虽然有很多不完美的地方,但是它的生态是非常完善的,有着非常多的用户和第三方库。毕竟在生态方面,Node.js 比 Deno 有着十几年的优势,这是在短时间内无法改变的。
所以,在未来相当长的一段时间内,Node.js 依然是最好的选择。

什么时候选择 Node.js?

Deno 的开发速度和开发体验是明显优于 Node.js 的。当我们需要开发一些小型项目、或者验证某些概念性项目、MVP 版本的项目,都可以在一开始就使用 Deno。
现在的技术发展迅速,在 JavaScript 运行时中,除了 Node.js 和 Deno 以外,还出现了 Bun。
我打算在未来一段时间来深入研究 Bun,如果你对 Deno 和 Bun 感兴趣的话,欢迎点赞留言与我进行交流。

猜你喜欢

转载自juejin.im/post/7128001813801861127