├── .nojekyll ├── 01~语法基础 ├── 快速开始 │ ├── 开发环境.md │ ├── README.md │ ├── HTTP 服务器.md │ ├── HTTP2.md │ └── 命令行应用.md ├── 包管理器 │ ├── Yarn │ │ ├── Yarn 1.md │ │ ├── Yarn 3.md │ │ ├── Yarn 2.md │ │ └── README.md │ ├── pnpm │ │ ├── Monorepo.md │ │ ├── README.md │ │ └── 依赖管理结构.md │ └── npm │ │ ├── Npm.md │ │ └── 依赖管理.md ├── .DS_Store ├── 事件 IO │ ├── 异步 IO.md │ └── Thread.md ├── Streams │ ├── Writable Stream.md │ ├── README.md │ ├── Readable Stream.md │ ├── 其他 Stream.md │ └── Pipe.md └── 文件存储 │ ├── 文件写入.md │ ├── 文件系统.md │ └── 文件读取.md ├── 02~工程实践 ├── 实时通信 │ ├── 聊天室.md │ ├── SocketIO.md │ ├── README.md │ └── 实时通信.md ├── 生产调优 │ ├── 性能优化.md │ └── 安全加固.md ├── SocketIO │ └── README.md ├── WebSocket │ └── README.md ├── 日志 │ ├── README.md │ └── winston.md ├── 权限认证 │ ├── Node.js 原生权限认证 │ │ ├── JWT.md │ │ └── RBAC.md │ └── Better Auth 库 │ │ └── README.md ├── .DS_Store └── 系统进程 │ ├── README.md │ ├── 多进程架构.md │ └── 命令执行.md ├── 05~数据库 ├── Sequelize │ └── README.md ├── Prisma │ ├── 01~快速开始 │ │ └── README.md │ ├── 02~数据查询 │ │ └── README.md │ ├── 88~examples │ │ └── prisma-nextjs-starter │ │ │ ├── .eslintrc.json │ │ │ ├── next.config.js │ │ │ ├── postcss.config.js │ │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── layout.tsx │ │ │ ├── globals.css │ │ │ ├── page.tsx │ │ │ └── api │ │ │ │ └── route.ts │ │ │ ├── prisma │ │ │ ├── migrations │ │ │ │ ├── migration_lock.toml │ │ │ │ └── 20240515102404_init_project │ │ │ │ │ └── migration.sql │ │ │ ├── schema.prisma │ │ │ └── seed.ts │ │ │ ├── lib │ │ │ ├── utils │ │ │ │ ├── helpers.ts │ │ │ │ └── query.ts │ │ │ ├── db.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ │ ├── tailwind.config.ts │ │ │ ├── .gitignore │ │ │ ├── public │ │ │ ├── vercel.svg │ │ │ └── next.svg │ │ │ ├── tsconfig.json │ │ │ ├── components │ │ │ └── Quote │ │ │ │ ├── QuoteWrapper.tsx │ │ │ │ ├── Quotes.tsx │ │ │ │ ├── AddQuote.tsx │ │ │ │ ├── Quote.tsx │ │ │ │ └── QuotesLoading.tsx │ │ │ ├── package.json │ │ │ ├── LICENSE │ │ │ └── README.md │ └── 99~参考资料 │ │ └── 2021~Prisma:下一代 ORM,不仅仅是 ORM.md └── TypeORM │ ├── README.md │ ├── 实体 │ ├── 索引.md │ └── 监听器和订阅者.md │ ├── 配置 │ ├── 日志.md │ ├── 快速开始.md │ ├── 缓存.md │ └── Connection.md │ └── 数据操作 │ ├── 事务.md │ └── Query Builder.md ├── 20~Deno ├── 调试.md ├── 99~参考资料 │ └── 2022-Roll your own JavaScript runtime.md ├── Hello World.md ├── README.md └── 特性与优缺点.md ├── INTRODUCTION.md ├── 03~Web 框架 ├── 10~Nest.js │ ├── 微服务 │ │ └── README.md │ ├── 数据访问 │ │ ├── Redis.md │ │ ├── README.md │ │ └── TypeORM.md │ ├── .DS_Store │ ├── 中间件 │ │ ├── 99~参考资料 │ │ │ └── 2023~一篇文章带你了解 NestJS 中的 AOP 架构(中间件,拦截器,守卫,异常过滤器,管道).md │ │ ├── 过滤器与异常处理.md │ │ ├── 拦截器.md │ │ └── 中间件与管道.md │ ├── MVC │ │ ├── 快速开始.md │ │ ├── 权限与校验.md │ │ └── 依赖注入与模块.md │ ├── 工程实践 │ │ ├── 性能优化.md │ │ ├── 安全与日志.md │ │ └── 测试与发布.md │ └── README.md ├── Egg │ ├── README.md │ ├── 请求处理.md │ └── 架构机制.md ├── Express │ ├── README.md │ ├── 中间件.md │ ├── 快速开始.md │ └── 路由与请求处理.md ├── Koa │ ├── README.md │ └── 中间件.md ├── .DS_Store ├── trpc │ └── README.md └── Fastify │ └── README.md ├── 04~全栈与 GraphQL ├── 同构应用 │ └── README.md ├── 技术栈 │ ├── MEAN Stack.md │ └── JAM Stack.md ├── .DS_Store ├── GraphQL │ ├── Schema │ │ ├── TypeGraphQL.md │ │ └── 快速开始.md │ ├── 客户端 │ │ └── 客户端接入.md │ ├── README.md │ └── 服务端 │ │ └── 服务端开发.md ├── BFF │ ├── README.md │ └── BFF 与 BIF.md ├── README.md └── 前后端分离 │ └── README.md ├── .DS_Store ├── .gitattributes ├── .gitignore ├── README.md └── index.html /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01~语法基础/快速开始/开发环境.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~工程实践/实时通信/聊天室.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~工程实践/生产调优/性能优化.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/Yarn/Yarn 1.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~工程实践/SocketIO/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~工程实践/WebSocket/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /05~数据库/Sequelize/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /20~Deno/调试.md: -------------------------------------------------------------------------------- 1 | # Deno 调试 2 | -------------------------------------------------------------------------------- /INTRODUCTION.md: -------------------------------------------------------------------------------- 1 | # 本篇导读 2 | -------------------------------------------------------------------------------- /05~数据库/Prisma/01~快速开始/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /05~数据库/Prisma/02~数据查询/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01~语法基础/快速开始/README.md: -------------------------------------------------------------------------------- 1 | # Node 快速开始 2 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/微服务/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /03~Web 框架/Egg/README.md: -------------------------------------------------------------------------------- 1 | # Egg.js 2 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/Yarn/Yarn 3.md: -------------------------------------------------------------------------------- 1 | # Yarn 3 2 | -------------------------------------------------------------------------------- /02~工程实践/日志/README.md: -------------------------------------------------------------------------------- 1 | # Node.js 中日志处理 2 | -------------------------------------------------------------------------------- /03~Web 框架/Express/README.md: -------------------------------------------------------------------------------- 1 | # Express 2 | -------------------------------------------------------------------------------- /03~Web 框架/Koa/README.md: -------------------------------------------------------------------------------- 1 | # Koa 介绍与基础使用 2 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/同构应用/README.md: -------------------------------------------------------------------------------- 1 | # 同构应用 2 | -------------------------------------------------------------------------------- /02~工程实践/权限认证/Node.js 原生权限认证/JWT.md: -------------------------------------------------------------------------------- 1 | # JWT 2 | -------------------------------------------------------------------------------- /03~Web 框架/Express/中间件.md: -------------------------------------------------------------------------------- 1 | # Express 中间件使用与开发 2 | -------------------------------------------------------------------------------- /03~Web 框架/Express/快速开始.md: -------------------------------------------------------------------------------- 1 | # Express 快速开始 2 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/技术栈/MEAN Stack.md: -------------------------------------------------------------------------------- 1 | # MEANStack 2 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/数据访问/Redis.md: -------------------------------------------------------------------------------- 1 | # Node & Redis 2 | -------------------------------------------------------------------------------- /02~工程实践/实时通信/SocketIO.md: -------------------------------------------------------------------------------- 1 | # SocketIO 2 | 3 | # Links 4 | -------------------------------------------------------------------------------- /02~工程实践/权限认证/Node.js 原生权限认证/RBAC.md: -------------------------------------------------------------------------------- 1 | # Node.js 中的 RBAC 实现 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Node.js-Notes/HEAD/.DS_Store -------------------------------------------------------------------------------- /01~语法基础/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Node.js-Notes/HEAD/01~语法基础/.DS_Store -------------------------------------------------------------------------------- /02~工程实践/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Node.js-Notes/HEAD/02~工程实践/.DS_Store -------------------------------------------------------------------------------- /02~工程实践/实时通信/README.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | - https://mp.weixin.qq.com/s/23unZJrMP9sVe5PTCApzGQ 4 | -------------------------------------------------------------------------------- /03~Web 框架/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Node.js-Notes/HEAD/03~Web 框架/.DS_Store -------------------------------------------------------------------------------- /02~工程实践/系统进程/README.md: -------------------------------------------------------------------------------- 1 | # 系统进程 2 | 3 | # Links 4 | 5 | - https://segmentfault.com/a/1190000020077274 6 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Node.js-Notes/HEAD/04~全栈与 GraphQL/.DS_Store -------------------------------------------------------------------------------- /01~语法基础/包管理器/Yarn/Yarn 2.md: -------------------------------------------------------------------------------- 1 | # Yarn 2 2 | 3 | # Links 4 | 5 | - https://loveky.github.io/2019/02/11/yarn-pnp/ 6 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /02~工程实践/权限认证/Better Auth 库/README.md: -------------------------------------------------------------------------------- 1 | > [Better Auth](https://github.com/better-auth/better-auth) 2 | 3 | # Better Auth 4 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Node.js-Notes/HEAD/03~Web 框架/10~Nest.js/.DS_Store -------------------------------------------------------------------------------- /01~语法基础/事件 IO/异步 IO.md: -------------------------------------------------------------------------------- 1 | # Node 中异步 IO 的实现 2 | 3 | # Links 4 | 5 | - https://www.kancloud.cn/thinkphp/nodejs-mini-book/43660 6 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/GraphQL/Schema/TypeGraphQL.md: -------------------------------------------------------------------------------- 1 | # TypeGraphQL 2 | 3 | # Links 4 | 5 | - https://zhuanlan.zhihu.com/p/56516614 6 | -------------------------------------------------------------------------------- /01~语法基础/事件 IO/Thread.md: -------------------------------------------------------------------------------- 1 | # Thread 2 | 3 | # Links 4 | 5 | - https://blog.logrocket.com/a-complete-guide-to-threads-in-node-js-4fa3898fe74f 6 | -------------------------------------------------------------------------------- /03~Web 框架/trpc/README.md: -------------------------------------------------------------------------------- 1 | # trpc 2 | 3 | tRPC allows you to easily build & consume fully typesafe APIs without schemas or code generation. 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.xmind filter=lfs diff=lfs merge=lfs -text 2 | *.zip filter=lfs diff=lfs merge=lfs -text 3 | *.pdf filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /20~Deno/99~参考资料/2022-Roll your own JavaScript runtime.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://deno.com/blog/roll-your-own-javascript-runtime) 2 | 3 | # Roll your own JavaScript runtime 4 | -------------------------------------------------------------------------------- /20~Deno/Hello World.md: -------------------------------------------------------------------------------- 1 | # Deno Hello World 2 | 3 | # Links 4 | 5 | - https://mp.weixin.qq.com/s/iTOz3R4rN_2CB9tNr9PLew 6 | - https://dev.to/kryz/write-a-small-api-using-deno-1cl0 7 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/Node.js-Notes/HEAD/05~数据库/Prisma/88~examples/prisma-nextjs-starter/app/favicon.ico -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /03~Web 框架/Fastify/README.md: -------------------------------------------------------------------------------- 1 | # Fastify 2 | 3 | 高效的服务器意味着更低的基础设施成本、更好的负载响应能力和用户满意度。在不牺牲安全验证和便捷开发的前提下,如何知道服务器正在处理尽可能多的请求,又如何有效地处理服务器资源?用 Fastify 吧。Fastify 是一个 web 开发框架,其设计灵感来自 Hapi 和 Express,致力于以最少的开销和强大的插件结构提供最佳的开发体验。据我们所知,它是这个领域里速度最快的 web 框架之一。 4 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/BFF/README.md: -------------------------------------------------------------------------------- 1 | # BFF 2 | 3 | One of the basic problems with conventional REST is the failure of the client to demand a personalized data set. In addition to that, running and controlling multiple endpoints is another difficulty as clients are mostly needed to request data from diversified endpoints. 4 | 5 | # Links 6 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/prisma/migrations/20240515102404_init_project/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Quotes" ( 3 | "id" SERIAL NOT NULL, 4 | "quote" TEXT NOT NULL, 5 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | 7 | CONSTRAINT "Quotes_pkey" PRIMARY KEY ("id") 8 | ); 9 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/lib/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export async function delay(ms: number): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, ms); 6 | }); 7 | } 8 | 9 | export const HOST = process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"; 10 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: {}, 10 | plugins: [], 11 | } 12 | export default config 13 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/README.md: -------------------------------------------------------------------------------- 1 | # TypeORM 2 | 3 | TypeORM 是一个 ORM 框架,它可以运行在 NodeJS、Browser、Cordova、PhoneGap、Ionic、React Native、Expo 和 Electron 平台上,可以与 TypeScript 和 JavaScript (ES5,ES6,ES7,ES8)一起使用。它的目标是始终支持最新的 JavaScript 特性并提供额外的特性以帮助你开发任何使用数据库的(不管是只有几张表的小型应用还是拥有多数据库的大型企业应用)应用程序。 4 | 5 | 不同于现有的所有其他 JavaScript ORM 框架,TypeORM 支持 Active Record 和 Data Mapper 模式,这意味着你可以以最高效的方式编写高质量的、松耦合的、可扩展的、可维护的应用程序。 6 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/数据访问/README.md: -------------------------------------------------------------------------------- 1 | # 数据访问 2 | 3 | Nest 与数据库无关,可让您轻松地与任何 SQL 或 NoSQL 数据库集成。您可以根据自己的喜好选择多种选择。在最一般的级别上,将 Nest 连接到数据库仅是为数据库加载适当的 Node.js 驱动程序,就像使用 Express 或 Fastify 一样。您还可以直接使用任何通用的 Node.js 数据库集成库或 ORM(例如 Sequelize,Knex.js(教程)和 TypeORM 来进行更高级别的抽象。 4 | 5 | 为了方便起见,Nest 还使用 `@nestjs/typeorm` 与现成的 TypeORM 紧密集成,这将在本章中介绍,而 Mongoose 与 `@nestjs/mongoose` 进行了本章介绍。这些集成提供了其他 NestJS 特定的功能,例如模型/存储库注入,可测试性和异步配置,以使访问所选数据库更加容易。 6 | -------------------------------------------------------------------------------- /03~Web 框架/Koa/中间件.md: -------------------------------------------------------------------------------- 1 | # Koa 中间件 2 | 3 | 所有的请求经过一个中间件的时候都会执行两次,对比 express 形式的中间件,koa 的模型可以非常方便的实现后置处理逻辑,对比 koa 和 express 的 compress 中间件就可以明显的感受到 koa 中间件模型的优势。 4 | 5 | ![](https://camo.githubusercontent.com/d80cf3b511ef4898bcde9a464de491fa15a50d06/68747470733a2f2f7261772e6769746875622e636f6d2f66656e676d6b322f6b6f612d67756964652f6d61737465722f6f6e696f6e2e706e67) 6 | 7 | ![](https://raw.githubusercontent.com/koajs/koa/a7b6ed0529a58112bac4171e4729b8760a34ab8b/docs/middleware.gif) 8 | -------------------------------------------------------------------------------- /02~工程实践/系统进程/多进程架构.md: -------------------------------------------------------------------------------- 1 | # Node.js 多进程架构 2 | 3 | # Links 4 | 5 | - [How To Use Multithreading in Node.js](https://www.digitalocean.com/community/tutorials/how-to-use-multithreading-in-node-js): In this tutorial, you’ll create a Node.js app with a CPU-intensive task that blocks the main thread. Next, you will use the worker-threads module to offload the CPU-intensive task to another thread to avoid blocking the main thread. Finally, you will divide the CPU-bound task and have four threads work on it in parallel to speed up the task. 6 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | directUrl = env("DIRECT_URL") 12 | } 13 | 14 | model Quotes { 15 | id Int @id @default(autoincrement()) 16 | quote String 17 | createdAt DateTime @default(now()) 18 | } 19 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Accelerated Quotes", 9 | description: "Accelerate starter", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | .env 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | package-lock.json 39 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/pnpm/Monorepo.md: -------------------------------------------------------------------------------- 1 | # Monorepo 2 | 3 | monorepo 是把多个项目的所有代码放到一个 git 仓库中进行管理,多个项目中会有共享的代码则可以分包引用。整个项目就是有 root 管理的 dependencies 加上多个 packages,每个 package 也可以在自己的作用域引入自己的 dependencies。 4 | 5 | 项目结构如下: 6 | 7 | ```s 8 | . 9 | ├── node_modules 10 | ├── package.json 11 | ├── packages 12 | │ ├── ui 13 | │ ├── utils 14 | │ └── web 15 | ├── pnpm-lock.yaml 16 | ├── pnpm-workspace.yaml 17 | ├── readme.md 18 | └── tsconfig.json 19 | ``` 20 | 21 | packages 文件夹中的就是原本每个独立的项目(下文称之为 package )了,现在放在一起用 workspace 去管理。最外层路径称之为 root。在 root package.json 中的 deps 是所有子 package 共用的。 22 | 23 | > 源码参考《[web-examples/pnpm](https://github.com/wx-chevalier/web-examples?q=)》 24 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~工程实践/实时通信/实时通信.md: -------------------------------------------------------------------------------- 1 | | Feature | **HTTP/2** | **WebSocket** | 2 | | ------------------ | --------------------------- | ----------------- | 3 | | **Headers** | Compressed (HPACK) | None | 4 | | **Binary** | Yes | Binary or Textual | 5 | | **Multiplexing** | Yes | Yes | 6 | | **Prioritization** | Yes | No | 7 | | **Compression** | Yes | Yes | 8 | | **Direction** | Client/Server + Server Push | Bidirectional | 9 | | **Full-duplex** | Yes | Yes | 10 | -------------------------------------------------------------------------------- /03~Web 框架/Egg/请求处理.md: -------------------------------------------------------------------------------- 1 | # 文件上传 2 | 3 | ```js 4 | const awaitWriteStream = require('await-stream-ready').write; 5 | const sendToWormhole = require('stream-wormhole'); 6 | ... 7 | const stream = await ctx.getFileStream(); 8 | 9 | // 文件生成绝对路径 10 | const filename = 11 | md5(stream.filename) + path.extname(stream.filename).toLocaleLowerCase(); 12 | 13 | const target = path.join(this.config.baseDir, 'app/public/uploads', filename); 14 | 15 | // 生成一个文件写入文件流 16 | const writeStream = fs.createWriteStream(target); 17 | try { 18 | // 异步把文件流写入 19 | await awaitWriteStream(stream.pipe(writeStream)); 20 | } catch (err) { 21 | // 如果出现错误,关闭管道 22 | await sendToWormhole(stream); 23 | throw err; 24 | } 25 | ... 26 | ``` 27 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /01~语法基础/Streams/Writable Stream.md: -------------------------------------------------------------------------------- 1 | # Writable Stream 2 | 3 | ```js 4 | readableStream.on("data", function (chunk) { 5 | writableStream.write(chunk); 6 | }); 7 | 8 | writableStream.end(); 9 | ``` 10 | 11 | 当 `end()` 被调用时,所有数据会被写入,然后流会触发一个 `finish` 事件。注意在调用 `end()` 之后,你就不能再往可写流中写入数据了。 12 | 13 | ```js 14 | const { Writable } = require("stream"); 15 | 16 | const outStream = new Writable({ 17 | write(chunk, encoding, callback) { 18 | console.log(chunk.toString()); 19 | callback(); 20 | }, 21 | }); 22 | 23 | process.stdin.pipe(outStream); 24 | ``` 25 | 26 | Writable Stream 中同样包含一些与 Readable Stream 相关的重要事件: 27 | 28 | - error: 在写入或链接发生错误时触发 29 | - pipe: 当可读流链接到可写流时,这个事件会触发 30 | - unpipe: 在可读流调用 unpipe 时会触发 31 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client/edge"; 2 | import { withAccelerate } from "@prisma/extension-accelerate"; 3 | 4 | // Learn more about instantiating PrismaClient in Next.js here: https://www.prisma.io/docs/data-platform/accelerate/getting-started 5 | 6 | const prismaClientSingleton = () => { 7 | return new PrismaClient().$extends(withAccelerate()); 8 | }; 9 | 10 | declare const globalThis: { 11 | prismaGlobal: ReturnType; 12 | } & typeof global; 13 | 14 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); 15 | 16 | export default prisma; 17 | 18 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; 19 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export type CacheStrategy = 2 | | { 3 | ttl: number; 4 | swr: number; 5 | } 6 | | { 7 | ttl: number; 8 | } 9 | | { 10 | swr: number; 11 | }; 12 | 13 | export type AccelerateInfo = { 14 | cacheStatus: "ttl" | "swr" | "miss" | "none"; 15 | lastModified: Date; 16 | region: string; 17 | requestId: string; 18 | signature: string; 19 | }; 20 | 21 | export type Quote = { 22 | id: number; 23 | quote: string; 24 | createdAt: string; 25 | }; 26 | 27 | export type QuoteResult = { 28 | data: Quote; 29 | info: AccelerateInfo; 30 | time: number; 31 | }; 32 | 33 | export type QuoteCacheType = "SWR" | "TTL" | "No caching" | "TTL + SWR"; 34 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "ts-node": { 26 | "compilerOptions": { 27 | "module": "commonjs" 28 | } 29 | }, 30 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/GraphQL/客户端/客户端接入.md: -------------------------------------------------------------------------------- 1 | # 客户端接入 2 | 3 | # 客户端接入 4 | 5 | 目前最为著名的 Apollo 客户端框架当属 [Apollo Client](https://github.com/apollographql/apollo-client),其也可以很方便地与 React、Angular、Vue 等常见的前端框架集成使用。 6 | 7 | 如果我们使用 Apollo 技术栈,那么还可以使用 [graphql-tag](https://github.com/apollographql/graphql-tag) 8 | 9 | ## 基础客户端 10 | 11 | ```js 12 | import ApolloClient from "apollo-boost"; 13 | 14 | const client = new ApolloClient({ 15 | uri: "https://graphql.example.com" 16 | }); 17 | ``` 18 | 19 | [graphql-tag](https://github.com/apollographql/graphql-tag) 20 | 21 | ```js 22 | import gql from "graphql-tag"; 23 | 24 | client 25 | .query({ 26 | query: gql` 27 | query TodoApp { 28 | todos { 29 | id 30 | text 31 | completed 32 | } 33 | } 34 | ` 35 | }) 36 | .then(data => console.log(data)) 37 | .catch(error => console.error(error)); 38 | ``` 39 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/components/Quote/QuoteWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { QuoteCacheType } from "@/lib/types"; 2 | import { ReactNode } from "react"; 3 | 4 | export const QuoteWrapper: React.FC<{ 5 | title: string; 6 | type: QuoteCacheType; 7 | children: ReactNode; 8 | }> = ({ title, type, children }) => { 9 | return ( 10 |
11 |
12 |
13 | {title} 14 |
15 |
16 |

17 | {type} 18 |

19 |
20 |
{children}
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /01~语法基础/Streams/README.md: -------------------------------------------------------------------------------- 1 | # Node.js Streams 2 | 3 | Stream(流)是 Node.js 中的基础概念,类似于 EventEmitter,专注于 IO 管道中事件驱动的数据处理方式;类比于数组或者映射,Stream 也是数据的集合,只不过其代表了不一定正在内存中的数据。流从最早的 Unix 时代就来到了我们身边,几十年来已经证明了自己是一种可靠的方式,可以将做得好的小组件组成大型系统。在 Unix 中,流是由 shell 用|管道实现的。在 node 中,内置的流模块被核心库使用,也可以被用户空间模块使用。与 Unix 类似,node 流模块的主要组成操作符叫做 .pipe(),你可以得到一个免费的背压机制,以节制慢速消费者的写入。 4 | 5 | 流可以帮助分离你的关注点,因为它们将实现表面的面积限制在一个可以重用的一致接口中。然后,你可以将一个流的输出插入到另一个流的输入,并使用对流进行抽象操作的库来建立更高级别的流控制。流是程序设计和 Unix 哲学的重要组成部分,但还有许多其他重要的抽象值得考虑。只要记住技术债务是敌人,并为眼前的问题寻求最好的抽象。 6 | 7 | Node.js 的 Stream 分为以下类型: 8 | 9 | - Readable Stream: 可读流,数据的产生者,譬如 process.stdin 10 | - Writable Stream: 可写流,数据的消费者,譬如 process.stdout 或者 process.stderr 11 | - Duplex Stream: 双向流,即可读也可写 12 | - Transform Stream: 转化流,数据的转化者 13 | 14 | Stream 本身提供了一套接口规范,很多 Node.js 中的内建模块都遵循了该规范,譬如著名的 `fs` 模块,即是使用 Stream 接口来进行文件读写;同样的,每个 HTTP 请求是可读流,而 HTTP 响应则是可写流。 15 | 16 | ![Node.js Streams 示意](https://s1.ax1x.com/2020/10/14/04qO5d.png) 17 | -------------------------------------------------------------------------------- /01~语法基础/文件存储/文件写入.md: -------------------------------------------------------------------------------- 1 | # 文件写入 2 | 3 | ```js 4 | const fs = require("fs"); 5 | fs.writeFile("/tmp/test", "Hey there!", function (err) { 6 | if (err) { 7 | return console.log(err); 8 | } 9 | 10 | console.log("The file was saved!"); 11 | }); 12 | ``` 13 | 14 | 我们也可以直接使用 `fs-extra` 提供的 outputFile 函数来自动创建不存在的文件: 15 | 16 | ```js 17 | const fs = require("fs-extra"); 18 | 19 | const file = "/tmp/this/path/does/not/exist/file.txt"; 20 | 21 | fs.outputFile(file, "hello!", (err) => { 22 | console.log(err); // => null 23 | 24 | fs.readFile(file, "utf8", (err, data) => { 25 | if (err) return console.error(err); 26 | console.log(data); // => hello! 27 | }); 28 | }); 29 | 30 | // With Promises: 31 | fs.outputFile(file, "hello!") 32 | .then(() => fs.readFile(file, "utf8")) 33 | .then((data) => { 34 | console.log(data); // => hello! 35 | }) 36 | .catch((err) => { 37 | console.error(err); 38 | }); 39 | ``` 40 | -------------------------------------------------------------------------------- /20~Deno/README.md: -------------------------------------------------------------------------------- 1 | # Deno 2 | 3 | Deno 是一款通用的 JavaScript/TypeScript 编程环境。它汇集了许多最出色的开源技术,并使用一个很小的可执行文件提供了全面的解决方案。Deno 由 Ryan Dahl 创建,他最出名的头衔是 Node.js 的幕后策划者。Deno 充分利用了自 2009 年 Node.js 发布以来不断加强的 JavaScript 特性。它还解决了 Ryan 在他的“Node.js 令我感到遗憾的 10 件事”中谈到的设计缺陷。有些人称其为 Node.js 的续作,尽管作者本人并未提出这种主张。 4 | 5 | 与用 C++ 编写的 Node.js 不同,Deno 是用 Rust 编写的。它建立在 Tokio 平台之上,并且像 Node.js 一样使用 V8 引擎执行 JavaScript 代码。 6 | 7 | 8 | - Rust 是由 Mozilla 主导开发的通用、编译型编程语言。设计准则为 “安全、并发、实用”,支持函数式、并发式、过程式以及面向对象的编程风格。Deno 使用 Rust 语言来封装 V8 引擎,通过 libdeno 绑定,我们就可以在 JavaScript 中调用隔离的功能。 9 | 10 | - Tokio 是 Rust 编程语言的异步运行时,提供异步事件驱动平台,构建快速,可靠和轻量级网络应用。利用 Rust 的所有权和并发模型确保线程安全。Tokio 构建于 Rust 之上,提供极快的性能,使其成为高性能服务器应用程序的理想选择。在 Deno 中 Tokio 用于并行执行所有的异步 IO 任务。 11 | 12 | - V8 是一个由 Google 开发的开源 JavaScript 引擎,用于 Google Chrome 及 Chromium 中。V8 在运行之前将JavaScript 编译成了机器代码,而非字节码或是解释执行它,以此提升性能。更进一步,使用了如内联缓存(inline caching)等方法来提高性能。有了这些功能,JavaScript 程序与 V8 引擎的速度媲美二进制编译。在 Deno 中,V8 引擎用于执行 JavaScript 代码。 13 | 14 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/中间件/99~参考资料/2023~一篇文章带你了解 NestJS 中的 AOP 架构(中间件,拦截器,守卫,异常过滤器,管道).md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://blog.csdn.net/xdididi/article/details/134932593) 2 | 3 | # 一篇文章带你了解 NestJS 中的 AOP 架构(中间件,拦截器,守卫,异常过滤器,管道) 4 | 5 | 在介绍 AOP 架构之前我们需要先了解一下 NestJS 对一个请求的处理过程。在 NestJS 中,一个请求首先会先经过控制器(Controller),然后 Controller 调用服务 (Service)中的方法,在 Service 中可能还会进行数据库的访问(Repository)等操作,最后返回结果。但是如果我们想在这个过程中加入一些通用逻辑,比如打印日志,权限控制等该如何做呢?这时候就需要用到 AOP(Aspect-Oriented Programming,面向切面编程)了,它允许开发者通过定义切面(Aspects)来对应用程序的各个部分添加横切关注点(Cross-Cutting Concerns)。横切关注点是那些不属于应用程序核心业务逻辑,但在整个应用程序中多处重复出现的功能或行为。这样可以让我们在不侵入业务逻辑的情况下来加入一些通用逻辑。 6 | 7 | 本篇文章将介绍 NestJS 中的五种实现 AOP 的方式(Middleware、Guard、Pipe、Interceptor、ExceptionFilter) 8 | 9 | # Middleware(中间件) 10 | 11 | Middleware(中间件)这个大家应该不陌生,在 express 中经常会用到,Middleware 在 NestJS 中与 Express 类似,它是用于处理 HTTP 请求和响应的功能模块。中间件可以在请求进入控制器之前或响应返回给客户端之前执行一些操作。 12 | 13 | 我们先创建一个 Nest 项目来演示 Middleware 的用法: 14 | 15 | ```sh 16 | nest new test -p npm 17 | ``` 18 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/lib/utils/query.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { CacheStrategy } from "@/lib/types"; 3 | 4 | export const getQuotes = async (strategy?: CacheStrategy) => { 5 | const start = Date.now(); 6 | 7 | const result = await prisma.quotes 8 | .findMany({ 9 | // You can find the `cacheStrategy` options [here](https://www.prisma.io/docs/accelerate/caching#cache-strategies). The `cacheStrategy` can also be undefined, which would mean only connection pooling is being used. 10 | cacheStrategy: strategy, 11 | orderBy: { 12 | id: "desc", 13 | }, 14 | take: 1, 15 | }) 16 | .withAccelerateInfo(); 17 | 18 | return { 19 | data: result?.data?.[0], 20 | info: result.info, 21 | time: Date.now() - start, 22 | }; 23 | }; 24 | 25 | export const addQuote = async (quote: string) => { 26 | return await prisma.quotes.create({ 27 | data: { 28 | quote, 29 | }, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/pnpm/README.md: -------------------------------------------------------------------------------- 1 | # pnpm 2 | 3 | pnpm 的官方文档(https://pnpm.js.org/en/)是这样说的: Fast, disk space efficient package manager. pnpm 本质上就是一个包管理器,这一点跟 npm/yarn 没有区别,但它作为杀手锏的两个优势在于: 4 | 5 | - 包安装速度极快; 6 | - 磁盘空间利用非常高效。 7 | 8 | # 特性 9 | 10 | ## 高磁盘利用率 11 | 12 | 对 yarn 比较熟悉的同学可能会说,yarn 不是有 PnP 安装模式吗?直接去掉 node_modules,将依赖包内容写在磁盘,节省了 node 文件 I/O 的开销,这样也能提升安装速度。pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于: 13 | 14 | - 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink。 15 | 16 | - 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件。 17 | 18 | ## 支持 Monorepo 19 | 20 | 随着前端工程的日益复杂,越来越多的项目开始使用 monorepo。之前对于多个项目的管理,我们一般都是使用多个 git 仓库,但 monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的 packages 目录下,那么一个子项目就代表一个 package。 21 | 22 | pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,体现在各个子命令的功能上,比如在根目录下 pnpm add A -r, 那么所有的 package 中都会被添加 A 这个依赖,当然也支持 --filter 字段来对 package 进行过滤。 23 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/技术栈/JAM Stack.md: -------------------------------------------------------------------------------- 1 | # JAMStack 2 | 3 | JAMStack 即基于客户端 JavaScript,可重用 API 和预先构建 Markup 的现代 Web 开发架构。当我们谈论“堆栈”时,我们不再谈论操作系统,特定 Web JAMStack 与特定技术无关。这是一种构建网站和应用程序的新方法,可提供更好的性能,更高的安全性,更低的扩展成本以及更好的开发人员体验。 4 | 5 | - J(JavaScript):请求/响应周期中的任何动态编程都由 JavaScript 处理,完全在客户端上运行。这可以是任何前端框架,库,甚至是 vanilla JavaScript。 6 | 7 | - A(APIs):所有服务器端进程或数据库操作都被抽象为可重用的 API,通过 HTTPS 使用 JavaScript 进行访问。这些可以是定制的或利用第三方服务。 8 | 9 | - M(Markup):模板化标记应该在部署时预先构建,通常使用内容站点的站点生成器或 Web 应用程序的构建工具。 10 | 11 | 依赖于客户端和服务器之间紧密耦合的任何项目都不是使用 JAMStack 构建的。这将包括: 12 | 13 | - 使用服务器端 CMS 构建的站点,如 WordPress,Drupal,Joomla 或 Squarespace。 14 | 15 | - 一个单片服务器运行的 Web 应用程序,它依赖于 Ruby,Node 或其他后端语言。 16 | 17 | - 单页应用程序,使用同构呈现在运行时在服务器上构建视图。 18 | 19 | # 背景特性 20 | 21 | 为何选择 JAMStack? 22 | 23 | - 更好的性能:为什么要在部署时生成页面,等待页面动态构建?当谈到最小化第一个字节的时间时,没有什么能比通过 CDN 提供的预构建文件更好。 24 | 25 | - 安全性更高:将服务器端进程抽象为微服务 API,可以减少攻击的表面区域。您还可以利用专业第三方服务的专业知识。 26 | 27 | - 更便宜,更容易扩展:当您的部署相当于可以在任何地方提供服务的一堆文件时,扩展就是在更多地方提供这些文件的问题。CDN 是完美的,通常包括扩展他们的所有计划。 28 | 29 | - 更好的开发者体验:松散的耦合和控制分离允许更有针对性的开发和调试,并且为站点生成器扩展选择 CMS 选项消除了为内容和营销维护单独堆栈的需要。 30 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accelerate-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "npx prisma generate --no-engine && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "5.16.1", 13 | "@prisma/extension-accelerate": "1.1.0", 14 | "@types/node": "20.14.10", 15 | "@types/react": "18.3.3", 16 | "@types/react-dom": "18.3.0", 17 | "autoprefixer": "10.4.19", 18 | "eslint": "8.57.0", 19 | "eslint-config-next": "14.2.5", 20 | "next": "14.2.5", 21 | "node-fetch": "3.3.2", 22 | "openflights-cached": "1.3.15", 23 | "postcss": "8.4.39", 24 | "react": "18.3.1", 25 | "react-dom": "18.3.1", 26 | "react-hot-toast": "2.4.1", 27 | "tailwindcss": "3.4.5", 28 | "typescript": "5.5.3", 29 | "zod": "3.23.8" 30 | }, 31 | "devDependencies": { 32 | "prisma": "5.16.1", 33 | "ts-node": "10.9.2" 34 | }, 35 | "prisma": { 36 | "seed": "ts-node prisma/seed.ts" 37 | }, 38 | "license": "MIT" 39 | } -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { AddQuote } from '@/components/Quote/AddQuote' 2 | import { QuotesLoading } from '@/components/Quote/QuotesLoading' 3 | import { Quotes } from '@/components/Quote/Quotes' 4 | import { Suspense } from 'react' 5 | 6 | // Disable caching. If 'force-dynamic' is not used, stale data can be returned from Prisma Client. 7 | // Learn more here: https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dynamic. 8 | export const dynamic = 'force-dynamic' 9 | 10 | export default async function Home() { 11 | return ( 12 |
13 |

14 | Accelerated Quotes 15 |

16 |
17 |

18 | Retrieves the most recently added quote with and without caching enabled 19 |

20 |
21 | 22 |
23 | }> 24 | 25 | 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2024 ankur 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /04~全栈与 GraphQL/GraphQL/Schema/快速开始.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | 值得一提的是,[Github Explorer](https://developer.github.com/v4/explorer/) 是非常不错的在 Github GraphQL API 中实践 GraphQL 的在线练习场,也可以在 [Backend-Boilerplate/graphql](https://github.com/wx-chevalier/Backend-Boilerplate/blob/master/node/graphql) 中了解笔者的 GraphQL 相关模板。 4 | 5 | ```gql 6 | query { 7 | user(login: "wx-chevalier") { 8 | starredRepositories { 9 | totalCount 10 | } 11 | followers { 12 | totalCount 13 | } 14 | repositories(first: 35) { 15 | nodes { 16 | id 17 | name 18 | descriptionHTML 19 | forkCount 20 | stargazers { 21 | totalCount 22 | } 23 | } 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | # Links 30 | 31 | - 典型案例,参考 [howtographql](https://www.howtographql.com/basics/3-big-picture/) 中的介绍, 32 | 33 | - https://github.com/sogko/graphql-schema-language-cheat-sheet 34 | 35 | - https://gist.github.com/jbritton/1f60ef440686b51ee37b708e6376b26e 36 | 37 | - https://medium.com/graphql-mastery/graphql-quick-tip-how-to-pass-variables-into-a-mutation-in-graphiql-23ecff4add57 38 | 39 | - https://mp.weixin.qq.com/s/YjLuf3Oebmw0Zo6Xx-0qKg 40 | 41 | - https://mp.weixin.qq.com/s/8FgP1LeO6eDd8xuIMGzIIA 42 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/README.md: -------------------------------------------------------------------------------- 1 | # 前后端分离与全栈架构 2 | 3 | 好的程序员能够独立的解决某个技术难题,主动的关心项目进度与潜在瓶颈,能够负责模块小组,合理地分配任务,与项目经理、产品经理、美工、测试、服务端的同事高效包容地沟通。而衡量好的架构师的标准则是整个项目的工程化程度。众所周知,现在前端进入了一种爆炸期,各种技术框架百花齐放,各种应用场景天差地别,前端工程化个人感觉不仅仅是选定某个技术框架、选定代码规范、选定测试方案等等,工程化的根本目标即是以尽可能快的速度实现可信赖的产品。尽可能短的时间包括开发速度、部署速度与重构速度,而可信赖又在于产品的可测试性、可变性以及 Bug 的重现与定位。笔者感觉遇见的最大的问题在于需求的不明确、接口的不稳定与开发人员素质的参差不齐。先不论技术层面,项目开发中我们在组织层面的希望能让每个参与的人无论水平高低都能最大限度的发挥其价值,每个人都会写组件,都会写实体类,但是他们不一定能写出合适的优质的代码。另一方面,好的架构都是衍化而来,不同的行业领域、应用场景、界面交互的需求都会引发架构的衍化。我们需要抱着开放的心态,不断地提取公共代码,保证合适的复用程度。同时也要避免过度抽象而带来的一系列问题。当我们落地到前端时,笔者在历年的实践中感受到以下几个突出的问题: 4 | 5 | - 前后端业务逻辑衔接:在前后端分离的情况下,前后端是各成体系与团队,那么前后端的沟通也就成了项目开发中的主要矛盾之一。前端在开发的时候往往是根据界面来划分模块,命名变量,而后端是习惯根据抽象的业务逻辑来划分模块,根据数据库定义来命名变量。最简单而是最常见的问题譬如二者可能对于同意义的变量命名不同,并且考虑到业务需求的经常变更,后台接口也会发生频繁变动。此时就需要前端能够建立专门的接口层对上屏蔽这种变化,保证界面层的稳定性。 6 | - 多业务系统的组件复用:当我们面临新的开发需求,或者具有多个业务系统时,我们希望能够尽量复用已有代码,不仅是为了提高开发效率,还是为了能够保证公司内部应用风格的一致性。 7 | 8 | - 多平台适配与代码复用:在移动化浪潮面前,我们的应用不仅需要考虑到 PC 端的支持,还需要考虑微信小程序、微信内 H5、WAP、ReactNative、Weex、Cordova 等等平台内的支持。这里我们希望能够尽量的复用代码来保证开发速度与重构速度,这里需要强调的是,笔者觉得移动端和 PC 端本身是不同的设计风格,笔者不赞同过多的考虑所谓的响应式开发来复用界面组件,更多的应该是着眼于逻辑代码的复用,虽然这样不可避免的会影响效率。鱼与熊掌,不可兼得,这一点需要因地制宜,也是不能一概而论。 9 | 10 | # 全栈工程师 11 | 12 | - [如何识别牛逼的前端工程师](https://zhuanlan.zhihu.com/p/22026860) 13 | 14 | ![](https://pic2.zhimg.com/713774d5383d726b1cb8e6b6ef48fd0d_b.png) 15 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/components/Quote/Quotes.tsx: -------------------------------------------------------------------------------- 1 | import { Quote } from "@/components/Quote/Quote"; 2 | import { QuoteResult } from "@/lib/types"; 3 | import { HOST } from "@/lib/utils/helpers"; 4 | 5 | const loadData = async (url: string): Promise => { 6 | const { signal } = new AbortController(); 7 | const result = await fetch(url, { 8 | cache: "no-cache", 9 | signal: signal, 10 | }); 11 | const data = await result.json(); 12 | return data; 13 | }; 14 | 15 | const getQuotes = async () => { 16 | const data = await Promise.all([ 17 | loadData(`${HOST}/api?cache=TTL`), 18 | loadData(`${HOST}/api?cache=SWR`), 19 | loadData(`${HOST}/api?cache=BOTH`), 20 | loadData(`${HOST}/api?cache=NONE`), 21 | ]); 22 | 23 | return data; 24 | }; 25 | 26 | export async function Quotes() { 27 | const [ttl, swr, both, none] = await getQuotes(); 28 | 29 | return ( 30 |
31 | 32 | 33 | 34 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | .DS_Store 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/MVC/快速开始.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | 本部分主要介绍 Nest.js 项目的基本搭建与请求处理相关内容,建议是直接下载官方的 TypeScript 模板作为项目初始化模板: 4 | 5 | ```sh 6 | $ git clone https://github.com/nestjs/typescript-starter.git project 7 | 8 | $ cd project 9 | 10 | $ npm install 11 | 12 | $ npm run start 13 | ``` 14 | 15 | ```ts 16 | import { NestFactory } from "@nestjs/core"; 17 | 18 | import { ApplicationModule } from "./modules/ApplicationModule"; 19 | 20 | async function bootstrap() { 21 | const app = await NestFactory.create(ApplicationModule); 22 | 23 | await app.listen(3000); 24 | } 25 | 26 | bootstrap(); 27 | ``` 28 | 29 | # 控制器 30 | 31 | ![Nest Controller](https://docs.nestjs.com/assets/Controllers_1.png) 32 | 33 | ```ts 34 | // ApplicationModule.ts 35 | 36 | import { Module } from "@nestjs/common"; 37 | 38 | import { HelloController } from "../controller/HelloController"; 39 | 40 | @Module({ 41 | modules: [], 42 | 43 | controllers: [HelloController] 44 | }) 45 | export class ApplicationModule {} 46 | ``` 47 | 48 | ```ts 49 | // HelloController.ts 50 | 51 | import { Controller, Get } from "@nestjs/common"; 52 | 53 | @Controller("/") 54 | export class HelloController { 55 | @Get() 56 | hello() { 57 | return "Next.js Boilerplate @ 王下邀月熊"; 58 | } 59 | } 60 | ``` 61 | 62 | ```ts 63 | import { Controller, Get, Post } from "@nestjs/common"; 64 | 65 | @Controller("cats") 66 | export class CatsController { 67 | @Post() 68 | create() { 69 | // TODO: Add some logic here 70 | } 71 | 72 | @Get() 73 | findAll() { 74 | return []; 75 | } 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/components/Quote/AddQuote.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { HOST, delay } from "@/lib/utils/helpers"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | import toast, { Toaster } from "react-hot-toast"; 7 | 8 | export const AddQuote = () => { 9 | const router = useRouter(); 10 | 11 | const addQuote = async () => { 12 | try { 13 | const response = await fetch(`${HOST}/api`, { 14 | method: "POST", 15 | }); 16 | const data = await response.json(); 17 | const toastId = toast.success(`New quote added - "${data.quote}"`); 18 | await delay(2000); 19 | toast.remove(toastId); 20 | router.refresh(); 21 | } catch (error) { 22 | toast.error("Something went wrong"); 23 | } 24 | }; 25 | 26 | return ( 27 | <> 28 | 29 | 36 | 37 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /05~数据库/Prisma/99~参考资料/2021~Prisma:下一代 ORM,不仅仅是 ORM.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://juejin.cn/post/6973277530996342798),你提供的链接是一篇关于 Prisma 的文章,标题为“Prisma:下一代 ORM,不仅仅是 ORM(上篇)”。以下是文章的主要内容概>述: 2 | > 3 | > ### 前言 4 | > 5 | > - 文章介绍了 Prisma,一个在 NodeJS 社区中备受关注的 ORM(对象关系映射)工具。 6 | > - 作者对 Prisma 的未来发展充满信心,并计划将文章分为两部分:基础介绍和进阶使用。 7 | > 8 | > ### NodeJS 社区中的 ORM 9 | > 10 | > - 介绍了 NodeJS 社区中常用的 ORM 工具,包括 Sequelize、TypeORM、MikroORM、Mongoose、Typegoose 和 Bookshelf。 11 | > - 每个 ORM 工具都有其独特的特点和使用方式。 12 | > 13 | > ### ORM 的 Data Mapper 与 Active Record 模式 14 | > 15 | > - 解释了 Data Mapper 和 Active Record 两种模式的区别。 16 | > - 展示了 TypeORM 中如何使用这两种模式。 17 | > 18 | > ### Query Builder 19 | > 20 | > - 介绍了 Query Builder 的概念及其与传统 ORM 的不同之处。 21 | > - 展示了 TypeORM 和 knex 的 Query Builder 的使用示例。 22 | > 23 | > ### Prisma 24 | > 25 | > - Prisma 被定义为 NodeJS 的 ORM,但其功能比普通 ORM 更强大。 26 | > - Prisma 使用独特的 Schema 定义方式,生成的类型定义比 TypeORM 更全面。 27 | > - Prisma 提供了更全面的操作符,如字符串的 contains、startsWith、endsWith 等。 28 | > 29 | > ### Prisma 的使用流程 30 | > 31 | > - 创建一个名为`prisma`的文件夹,并在其中创建`schema.prisma`文件。 32 | > - 定义数据库类型、路径以及数据库表结构。 33 | > - 运行`prisma generate`命令生成 Prisma Client。 34 | > - 在项目中导入并使用 Prisma Client。 35 | > 36 | > ### 项目初始化 37 | > 38 | > - 创建空文件夹并初始化 npm 项目。 39 | > - 全局安装`@prisma/cli`。 40 | > - 安装必要的依赖,如`@prisma/client`、`sqlite3`、`prisma`等。 41 | > 42 | > ### 创建数据库 43 | > 44 | > - 修改 Prisma Schema 中的数据库类型为 SQLite。 45 | > - 运行`prisma db push`创建数据库文件。 46 | > 47 | > ### 客户端生成与使用 48 | > 49 | > - 执行`prisma generate`生成 Prisma Client。 50 | > - 创建`index.ts`文件并使用 Prisma Client 进行数据库操作。 51 | > 52 | > ### Prisma 单表初体验 53 | > 54 | > - 介绍了如何在 Prisma 中进行 CRUD 操作(创建、读取、更新、删除)。 55 | > 56 | > ### 尾声 & 下篇预告 57 | > 58 | > - 文章结尾预告了下篇内容,将介绍 Prisma 的多表级联关系处理、多个 Prisma Client 协作、Prisma 与其他 ORM 的协作、Prisma 与 GraphQL 的实战以及 Prisma 的展望。 59 | > 60 | > 这篇文章详细介绍了 Prisma 的基本概念、使用方式以及与其他 ORM 工具的比较,适合对 Prisma 感兴趣的开发者阅读。 61 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { addQuote, getQuotes } from "@/lib/utils/query"; 2 | import { z } from "zod"; 3 | 4 | export const runtime = "edge"; 5 | 6 | // disabling caching 7 | export const fetchCache = "force-no-store"; 8 | export const revalidate = 0; 9 | 10 | export async function GET(request: Request) { 11 | const url = new URL(request.url); 12 | const searchParams = new URLSearchParams(url.search); 13 | 14 | const parser = z.enum(["TTL", "SWR", "BOTH", "NONE"]); 15 | const parsedOutput = await parser.safeParseAsync(searchParams.get("cache")); 16 | 17 | if (!parsedOutput.success) { 18 | return new Response(JSON.stringify("Invalid search parameter."), { 19 | status: 400, 20 | }); 21 | } 22 | 23 | let map = new Map(); 24 | 25 | // When TTL is selected 26 | map.set("TTL", { 27 | ttl: 30, 28 | }); 29 | 30 | // When SWR is selected 31 | map.set("SWR", { 32 | swr: 30, 33 | }); 34 | 35 | // When TTL + SWR is selected 36 | map.set("BOTH", { 37 | ttl: 30, 38 | swr: 60, 39 | }); 40 | 41 | // This ensures no caching is performed and only the Accelerate connection pool is used 42 | map.set("NONE", undefined); 43 | 44 | const data = await getQuotes(map.get(parsedOutput.data)); 45 | 46 | return new Response(JSON.stringify(data), { 47 | headers: { "Content-Type": "application/json" }, 48 | status: 200, 49 | }); 50 | } 51 | 52 | export async function POST(request: Request) { 53 | const response = await fetch("https://api.quotable.io/random", { 54 | cache: "no-cache", 55 | }); 56 | 57 | const data = await response.json(); 58 | 59 | await addQuote(data.content); 60 | 61 | return new Response(JSON.stringify({ quote: data.content }), { 62 | headers: { "Content-Type": "application/json" }, 63 | status: 201, 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /01~语法基础/文件存储/文件系统.md: -------------------------------------------------------------------------------- 1 | # Node.js 文件系统 2 | 3 | ```sh 4 | $ npm install --save walk-sync 5 | ``` 6 | 7 | ```js 8 | const walkSync = require("walk-sync"); 9 | const paths = walkSync("project"); 10 | 11 | // 输出结果 12 | // ['one.txt', 'subdir/', 'subdir/two.txt'] 13 | ``` 14 | 15 | # 文件寻址 16 | 17 | Node 中的文件路径大概有 `__dirname`, `__filename`, `process.cwd()`, `./` 或者 `../`,前三个都是绝对路径,为了便于比较,`./` 和 `../` 我们通过 `path.resolve('./')` 来转换为绝对路径。 18 | 19 | - `__dirname`: 总是返回被执行的 js 所在文件夹的绝对路径 20 | - `__filename`: 总是返回被执行的 js 的绝对路径 21 | - `process.cwd()`: 总是返回运行 node 命令时所在的文件夹的绝对路径 22 | 23 | ```s 24 | app/ 25 | -lib/ 26 | -common.js 27 | -model 28 | -task.js 29 | -test.js 30 | ``` 31 | 32 | 在 task.js 里编写如下的代码: 33 | 34 | ```js 35 | var path = require("path"); 36 | 37 | console.log(__dirname); 38 | console.log(__filename); 39 | console.log(process.cwd()); 40 | console.log(path.resolve("./")); 41 | ``` 42 | 43 | 在 `model` 目录下运行 `node task.js` 得到的输出是: 44 | 45 | ```js 46 | /Users/guo/Sites/learn/app/model 47 | /Users/guo/Sites/learn/app/model/task.js 48 | /Users/guo/Sites/learn/app/model 49 | /Users/guo/Sites/learn/app/model 50 | ``` 51 | 52 | 然后在 `app` 目录下运行 `node model/task.js`,得到的输出是: 53 | 54 | ```js 55 | /Users/guo/Sites/learn/app/model 56 | /Users/guo/Sites/learn/app/model/task.js 57 | /Users/guo/Sites/learn/app 58 | /Users/guo/Sites/learn/app 59 | ``` 60 | 61 | 只有在 require() 时才使用相对路径(./, ../) 的写法,其他地方一律使用绝对路径,如下: 62 | 63 | ```js 64 | // 当前目录下 65 | path.dirname(__filename) + "/test.js"; 66 | // 相邻目录下 67 | path.resolve(__dirname, "../lib/common.js"); 68 | ``` 69 | 70 | ```js 71 | fs.readFile(path.resolve(__dirname, "settings.json"), "UTF-8", callback); 72 | ``` 73 | 74 | `path.sep` 表示了平台相关的路径分隔符,即  `'\\'` 或者 `'/'`;在类 Unix 系统中,我们可以进行如下测试: `'foo/bar/baz'.split(path.sep)// returns ['foo', 'bar', 'baz']`而在 Windows 系统中,则是用的双斜杠分隔:```'foo\\bar\\baz'.split(path.sep)// returns ['foo', 'bar', 'baz'] 75 | -------------------------------------------------------------------------------- /01~语法基础/Streams/Readable Stream.md: -------------------------------------------------------------------------------- 1 | # Readable Stream 2 | 3 | ```js 4 | const stream = require("stream"); 5 | const fs = require("fs"); 6 | 7 | const readableStream = fs.createReadStream(process.argv[2], { 8 | encoding: "utf8", 9 | }); 10 | 11 | // 手动设置流数据编码 12 | // readableStream.setEncoding('utf8'); 13 | 14 | let wordCount = 0; 15 | 16 | readableStream.on("data", function (data) { 17 | wordCount += data.split(/\s{1,}/).length; 18 | }); 19 | 20 | readableStream.on("end", function () { 21 | // Don't count the end of the file. 22 | console.log("%d %s", --wordCount, process.argv[2]); 23 | }); 24 | ``` 25 | 26 | 当我们创建某个可读流时,其还并未开始进行数据流动;添加了 data 的事件监听器,它才会变成流动态的。在这之后,它就会读取一小块数据,然后传到我们的回调函数里面。`data` 事件的触发频次同样是由实现者决定,譬如在进行文件读取时,可能每行都会触发一次;而在 HTTP 请求处理时,可能数 KB 的数据才会触发一次。可以参考 [`nodejs/readable-stream/_stream_readable`](https://github.com/nodejs/readable-stream/blob/master/lib/_stream_readable.js) 中的相关实现,发现 on 函数会触发 resume 方法,该方法又会调用 flow 函数进行流读取: 27 | 28 | ```js 29 | // function on 30 | if (ev === 'data') { 31 | // Start flowing on next tick if stream isn't explicitly paused 32 | if (this._readableState.flowing !== false) this.resume(); 33 | } 34 | ... 35 | // function flow 36 | while (state.flowing && stream.read() !== null) {} 37 | ``` 38 | 39 | 我们还可以监听 `readable` 事件,然后手动地进行数据读取: 40 | 41 | ```js 42 | let data = ""; 43 | let chunk; 44 | readableStream.on("readable", function () { 45 | while ((chunk = readableStream.read()) != null) { 46 | data += chunk; 47 | } 48 | }); 49 | readableStream.on("end", function () { 50 | console.log(data); 51 | }); 52 | ``` 53 | 54 | Readable Stream 还包括如下常用的方法: 55 | 56 | - Readable.pause(): 这个方法会暂停流的流动。换句话说就是它不会再触发 data 事件。 57 | - Readable.resume(): 这个方法和上面的相反,会让暂停流恢复流动。 58 | - Readable.unpipe(): 这个方法会把目的地移除。如果有参数传入,它会让可读流停止流向某个特定的目的地,否则,它会移除所有目的地。 59 | 60 | 在日常开发中,我们可以用 [stream-wormhole](https://github.com/node-modules/stream-wormhole) 来模拟消耗可读流: 61 | 62 | ```js 63 | sendToWormhole(readStream, true); 64 | ``` 65 | -------------------------------------------------------------------------------- /01~语法基础/快速开始/HTTP 服务器.md: -------------------------------------------------------------------------------- 1 | # 简单 HTTP 服务器 2 | 3 | # createServer 4 | 5 | ```js 6 | const http = require("http"); 7 | const url = require("url"); 8 | const path = require("path"); 9 | const fs = require("fs"); 10 | const port = process.argv[2] || 8033; 11 | 12 | const server = http 13 | .createServer(function (request, response) { 14 | const uri = url.parse(request.url).pathname; 15 | let filename = path.join(process.cwd(), uri); 16 | 17 | fs.exists(filename, function (exists) { 18 | if (!exists) { 19 | response.writeHead(404, { "Content-Type": "text/plain" }); 20 | console.log("node.js: 404 Not Found"); 21 | response.write("404 Not Found\n"); 22 | response.end(); 23 | return; 24 | } 25 | 26 | if (fs.statSync(filename).isDirectory()) filename += "/index.html"; 27 | 28 | fs.readFile(filename, "binary", function (err, file) { 29 | if (err) { 30 | response.writeHead(500, { "Content-Type": "text/plain" }); 31 | response.write(err + "\n"); 32 | response.end(); 33 | return; 34 | } 35 | 36 | response.writeHead(200); 37 | response.write(file, "binary"); 38 | response.end(); 39 | }); 40 | }); 41 | }) 42 | .listen(parseInt(port, 10)); 43 | 44 | console.log( 45 | "Static file server running at\n => http://localhost:" + port + "/" 46 | ); 47 | ``` 48 | 49 | # 在 Chrome 中调试 NodeJS 应用 50 | 51 | NodeJS 在 6.3.0 版本之后允许使用 Chrome 来调试 NodeJS 应用,从而方便了开发者进行断点调试与单步运行,以及对堆栈信息进行查看。安装好 node 之后我们可以使用`--inspect`选项来运行应用: 52 | 53 | ``` 54 | node --inspect index.js 55 | ``` 56 | 57 | 我们也可以选择直接从第一行代码开始进行断点调试: 58 | 59 | ``` 60 | node --inspect --debug-brk index.js 61 | ``` 62 | 63 | 运行上述命令之后,控制台中会返回该应用对应的 Chrome 开发工具链接,譬如: 64 | 65 | ``` 66 | chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/69beb5d3-2b1c-4513-aa4b-78d1eb1865ea 67 | ``` 68 | 69 | 在 Chrome 中直接打开该链接,即可开始调试: 70 | -------------------------------------------------------------------------------- /01~语法基础/快速开始/HTTP2.md: -------------------------------------------------------------------------------- 1 | # HTTP/2 2 | 3 | In HTTP/1 the client sends a request to the server, which replies with the requested content, usually with an HTML file that contains links to many assets (.js, .css, etc. files). As the browser processes this initial HTML file, it starts to resolve these links and makes separate requests to fetch them. The problem with the current approach is that the user has to wait while the browser parses responses, discovers links and fetches assets. This delays rendering and increases load times. There are workarounds like inlining some assets, but it also makes the initial response bigger and slower. 4 | 5 | ![](https://blog-assets.risingstack.com/2017/08/http_1-in-nodejs.png) 6 | 7 | This is where HTTP/2 Server Push capabilities come into the picture as the server can send assets to the browser before it has even asked for them. 8 | 9 | ![](https://blog-assets.risingstack.com/2017/08/http2-in-nodejs.png) 10 | 11 | ```js 12 | const http2 = require("http2"); 13 | 14 | const server = http2.createSecureServer( 15 | { cert, key }, 16 | 17 | onRequest 18 | ); 19 | 20 | function push(stream, filePath) { 21 | const { file, headers } = getFile(filePath); 22 | 23 | const pushHeaders = { [HTTP2_HEADER_PATH]: filePath }; 24 | 25 | stream.pushStream(pushHeaders, pushStream => { 26 | pushStream.respondWithFD(file, headers); 27 | }); 28 | } 29 | 30 | function onRequest(req, res) { 31 | // Push files with index.html 32 | 33 | if (reqPath === "/index.html") { 34 | push(res.stream, "bundle1.js"); 35 | 36 | push(res.stream, "bundle2.js"); 37 | } // Serve file 38 | 39 | res.stream.respondWithFD(file.fileDescriptor, file.headers); 40 | } 41 | ``` 42 | 43 | ```html 44 | 45 | 46 |  

HTTP2 Push!

47 | 48 |   49 |   50 | 51 | ``` 52 | 53 | [http2-push-example](https://github.com/RisingStack/http2-push-example) 54 | -------------------------------------------------------------------------------- /02~工程实践/系统进程/命令执行.md: -------------------------------------------------------------------------------- 1 | # SysProc 2 | 3 | # System 4 | 5 | ## 内置的 process 对象 6 | 7 | - [process 对象](http://javascript.ruanyifeng.com/nodejs/process.html) 8 | 9 | `process`对象是 Node 的一个全局对象,提供当前 Node 进程的信息。它可以在脚本的任意位置使用,不必通过`require`命令加载。该对象部署了`EventEmitter`接口。进程退出时,会返回一个整数值,表示退出时的状态。这个整数值就叫做退出码。下面是常见的 Node 进程退出码。 10 | 11 | - 0,正常退出 12 | - 1,发生未捕获错误 13 | - 5,V8 执行错误 14 | - 8,不正确的参数 15 | - 128 + 信号值,如果 Node 接受到退出信号(比如 SIGKILL 或 SIGHUP),它的退出码就是 128 加上信号值。由于 128 的二进制形式是 10000000, 所以退出码的后七位就是信号值。 16 | 17 | process 对象提供一系列属性,用于返回系统信息。 18 | 19 | - **process.argv**:返回当前进程的命令行参数数组。 20 | - **process.env**:返回一个对象,成员为当前 Shell 的环境变量,比如`process.env.HOME`。 21 | - **process.installPrefix**:node 的安装路径的前缀,比如`/usr/local`,则 node 的执行文件目录为`/usr/local/bin/node`。 22 | - **process.pid**:当前进程的进程号。 23 | - **process.platform**:当前系统平台,比如 Linux。 24 | - **process.title**:默认值为“node”,可以自定义该值。 25 | - **process.version**:Node 的版本,比如 v0.10.18。 26 | 27 | process 对象提供以下方法: 28 | 29 | - **process.chdir()**:切换工作目录到指定目录。 30 | - **process.cwd()**:返回运行当前脚本的工作目录的路径。 31 | - **process.exit()**:退出当前进程。 32 | - **process.getgid()**:返回当前进程的组 ID(数值)。 33 | - **process.getuid()**:返回当前进程的用户 ID(数值)。 34 | - **process.nextTick()**:指定回调函数在当前执行栈的尾部、下一次 Event Loop 之前执行。 35 | - **process.on()**:监听事件。 36 | - **process.setgid()**:指定当前进程的组,可以使用数字 ID,也可以使用字符串 ID。 37 | - **process.setuid()**:指定当前进程的用户,可以使用数字 ID,也可以使用字符串 ID。 38 | 39 | ## Shell Commands(系统命令执行) 40 | 41 | ### Windows 42 | 43 | ``` 44 | var theJobType = 'FOO'; 45 | var exec = require('child_process').exec; 46 | var child = exec('Test.exe ' + theJobType, function( error, stdout, stderr) 47 | { 48 | if ( error != null ) { 49 | console.log(stderr); 50 | // error handling & exit 51 | } 52 | 53 | // normal 54 | 55 | }); 56 | ``` 57 | 58 | ### Unix-[shelljs](https://github.com/shelljs/shelljs) 59 | 60 | # Links 61 | 62 | - [Nodejs 进阶:如何玩转子进程(child_process)](http://www.cnblogs.com/chyingp/p/node-learning-guide-child_process.html) 63 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/npm/Npm.md: -------------------------------------------------------------------------------- 1 | # Npm 2 | 3 | # npx 4 | 5 | 近日发布的 npm 5.2.0 版本中内置了伴生命令:npx,类似于 npm 简化了项目开发中的依赖安装与管理,该工具致力于提升开发者使用包提供的命令行的体验。npx 允许我们使用本地安装的命令行工具而不需要再定义 npm run-script,并且允许我们仅执行一次脚本而不需要再将其实际安装到本地;同时 npx 还允许我们以不同的 node 版本来运行指定命令、允许我们交互式地开发 node 命令行工具以及便捷地安装来自于 gist 的脚本。 6 | 7 | 在传统的命令执行中,我们需要将工具添加到 package.json 的 `scripts` 配置中,这种方式还需要我们以 `--` 方式传递参数;我们也可以使用 `alias npmx=PATH=$(npm bin):$PATH,` 或者 `./node_modules/.bin/mocha` 方式来执行命令,虽然都能达到目标,但不免繁杂了许多。而 npx 允许我们以 `npx mocha` 这样的方式直接运行本地安装的 mocha 命令。 8 | 9 | 完整的 npx 命令提示如下: 10 | 11 | ```sh 12 | 从 npm 的可执行包执行命令 13 |   npx [选项] <命令>[@版本] [命令的参数]... 14 |   npx [选项] [-p|--package <包>]... <命令> [命令的参数]... 15 |   npx [选项] -c '<命令的字符串>' 16 |   npx --shell-auto-fallback [命令行解释器] 17 | 18 | 选项: 19 |   --package, -p包安装的路径 [字符串] 20 |   --cachenpm 缓存路径 [字符串] 21 |   --install如果有包缺失,跳过安装[布尔] [默认值: true] 22 |   --userconfig 当前用户的 npmrc 路径[字符串] 23 |   --call, -c 像执行 `npm run-script` 一样执行一个字符串 [字符串] 24 |   --shell, -s执行命令用到的解释器,可选 [字符串] [默认值: false] 25 |   --shell-auto-fallback产生“找不到命令”的错误码 26 |   [字符串] [可选值: "", "bash", "fish", "zsh"] 27 |   --ignore-existing忽略 $PATH 或工程里已有的可执行文件,这会强制使 npx 28 |  临时安装一次,并且使用其最新的版本 [布尔] 29 |   --quiet, -q隐藏 npx 的输出,子命令不会受到影响[布尔] 30 |   --npm为了执行内部操作的 npm 可执行文件 [字符串] [默认值: 31 |  "/Users/apple/.nvm/versions/node/v8.1.3/lib/node_modules/npm/bin/npm-cli.js"] 32 |   --version, -v显示版本号 [布尔] 33 |   --help, -h 显示帮助信息 [布尔] 34 | ``` 35 | 36 | npx 还允许我们单次执行命令而不需要安装;在某些场景下有可能我们安装了某个全局命令行工具之后一直忘了更新,导致以后使用的时候误用了老版本。而使用 `npx create-react-app my-cool-new-app` 来执行 create-react-app 命令时,它会正常地帮我们创建 React 应用而不会实际安装 create-react-app 命令行。 37 | 我们还可以使用类似于 `$ npx -p node-bin@6 npm it` 的格式来指定 Node 版本,或者使用 `npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32` 方式直接运行来自于 Gist 的脚本。 38 | 39 | # 运行小技巧 40 | 41 | ## 多个任务并发执行 42 | 43 | 我们可以利用 concurrently 库来并发执行多个任务: 44 | 45 | ```sh 46 | $ npm i concurrently --save-dev 47 | ``` 48 | 49 | 然后在 Npm 脚本中使用 concurrent 来运行多个命令: 50 | 51 | ```json 52 | { 53 | "dev": "concurrently --kill-others \"npm run start-watch\" \"npm run wp-server\"" 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /02~工程实践/日志/winston.md: -------------------------------------------------------------------------------- 1 | # winston 2 | 3 | ```js 4 | const logger = createLogger({ 5 | level: "debug", 6 | format: format.combine( 7 | // Use this function to create a label for additional text to display 8 | format.label({ label: path.basename(module.parent.filename) }), 9 | format.colorize(), 10 | format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 11 | format.printf( 12 | // We display the label text between square brackets using ${info.label} on the next line 13 | info => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}` 14 | ) 15 | ), 16 | transports: [new transports.Console()] 17 | }); 18 | ``` 19 | 20 | # 生产环境配置 21 | 22 | ```ts 23 | import * as path from "path"; 24 | import * as winston from "winston"; 25 | 26 | const { createLogger, format, transports } = winston; 27 | 28 | const errorStackFormat = format(info => { 29 | if (info instanceof Error) { 30 | return Object.assign({}, info, { 31 | stack: info.stack, 32 | message: info.message 33 | }); 34 | } 35 | return info; 36 | }); 37 | 38 | const formats = [ 39 | format.label({ 40 | label: path.basename(`${module.parent}/${module.filename}`) 41 | }), 42 | format.timestamp({ 43 | format: "YYYY-MM-DD HH:mm:ss" 44 | }), 45 | format.printf( 46 | info => 47 | `${info.label}[${info.level}]: ${info.timestamp} --- ${info.message}` 48 | ) 49 | ]; 50 | 51 | export const logger = createLogger({ 52 | level: "info", 53 | format: format.combine(...formats), 54 | exitOnError: false, 55 | transports: [ 56 | // 生产环境下区分 Error 与其他 57 | new transports.File({ 58 | filename: path.resolve("logs/error.log"), 59 | level: "error", 60 | handleExceptions: true, 61 | maxsize: 5242880, // 5MB 62 | maxFiles: 5 63 | }), 64 | new transports.File({ 65 | filename: path.resolve("logs/info.log"), 66 | handleExceptions: true, 67 | maxsize: 5242880, // 5MB 68 | maxFiles: 5 69 | }) 70 | ] 71 | }); 72 | 73 | logger.add( 74 | new winston.transports.Console({ 75 | handleExceptions: true, 76 | format: format.combine(errorStackFormat(), ...formats, format.colorize()) 77 | }) 78 | ); 79 | ``` 80 | -------------------------------------------------------------------------------- /01~语法基础/快速开始/命令行应用.md: -------------------------------------------------------------------------------- 1 | # Node.js 构建命令行程序 2 | 3 | ```js 4 | #!/usr/bin/env node 5 | 6 | const chalk = require("chalk"); 7 | 8 | const program = require("commander"); 9 | 10 | program 11 | .version("0.1.0") 12 | .option("-p, --peppers", "Add peppers") 13 | .option("-P, --pineapple", "Add pineapple") 14 | .option("-b, --bbq-sauce", "Add bbq sauce") 15 | .option( 16 | "-c, --cheese [type]", 17 | "Add the specified type of cheese [marble]", 18 | "marble" 19 | ) 20 | .parse(process.argv); 21 | 22 | console.log("you ordered a pizza with:"); 23 | if (program.peppers) console.log(chalk.blue(" - peppers")); 24 | if (program.pineapple) console.log(" - pineapple"); 25 | if (program.bbqSauce) console.log(" - bbq"); 26 | console.log(" - %s cheese", program.cheese); 27 | ``` 28 | 29 | 使用 [pkg](https://github.com/zeit/pkg) 将命令行程序打包为不依赖与外部 Node.js 的本地程序: 30 | 31 | ```sh 32 | cross-env PKG_CACHE_PATH=D:\SDK\Node\pkg\ pkg -t node8-win -o index.exe index.js 33 | ``` 34 | 35 | 可以在 [pkg-fetch](https://github.com/zeit/pkg-fetch/releases) 下载本地依赖。 36 | 37 | ```js 38 | const init = require("./server/init"); 39 | 40 | require("yargs") // eslint-disable-line no-unused-expressions 41 | .usage("Usage: node $0 [args]") 42 | .command({ 43 | command: "start", 44 | alias: ["boot", "init"], 45 | desc: "Start Wiki.js process", 46 | handler: argv => { 47 | init.startDetect(); 48 | } 49 | }) 50 | .command({ 51 | command: "stop", 52 | alias: ["quit", "exit"], 53 | desc: "Stop Wiki.js process", 54 | handler: argv => { 55 | init.stop(); 56 | } 57 | }) 58 | .command({ 59 | command: "restart", 60 | alias: ["reload"], 61 | desc: "Restart Wiki.js process", 62 | handler: argv => { 63 | init.restart(); 64 | } 65 | }) 66 | .command({ 67 | command: "configure [port]", 68 | alias: ["config", "conf", "cfg", "setup"], 69 | desc: "Configure Wiki.js using the web-based setup wizard", 70 | builder: yargs => yargs.default("port", 3000), 71 | handler: argv => { 72 | init.configure(argv.port); 73 | } 74 | }) 75 | .recommendCommands() 76 | .demandCommand(1, "You must provide one of the accepted commands above.") 77 | .help() 78 | .version() 79 | .epilogue("Read the docs at https://wiki.requarks.io").argv; 80 | ``` 81 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | const main = async () => { 6 | console.time("Seeding complete 🌱"); 7 | 8 | await prisma.quotes.createMany({ 9 | skipDuplicates: true, 10 | data: [ 11 | { quote: "The only way to do great work is to love what you do." }, 12 | { 13 | quote: 14 | "Success is not final, failure is not fatal: It is the courage to continue that counts.", 15 | }, 16 | { quote: "In the middle of every difficulty lies opportunity." }, 17 | { quote: "Believe you can and you're halfway there." }, 18 | { quote: "The best way to predict the future is to create it." }, 19 | { quote: "Don't watch the clock; do what it does. Keep going." }, 20 | { quote: "The only thing we have to fear is fear itself." }, 21 | { quote: "The journey of a thousand miles begins with a single step." }, 22 | { quote: "If you can dream it, you can achieve it." }, 23 | { quote: "Innovation distinguishes between a leader and a follower." }, 24 | { 25 | quote: 26 | "The greatest glory in living lies not in never falling, but in rising every time we fall.", 27 | }, 28 | { quote: "You miss 100% of the shots you don't take." }, 29 | { 30 | quote: 31 | "The only limit to our realization of tomorrow will be our doubts of today.", 32 | }, 33 | { quote: "Change your thoughts and you change your world." }, 34 | { 35 | quote: 36 | "To be yourself in a world that is constantly trying to make you something else is the greatest accomplishment.", 37 | }, 38 | { 39 | quote: 40 | "The only thing standing between you and your goal is the story you keep telling yourself as to why you can't achieve it.", 41 | }, 42 | { quote: "Life is 10% what happens to us and 90% how we react to it." }, 43 | { 44 | quote: 45 | "The future belongs to those who believe in the beauty of their dreams.", 46 | }, 47 | { 48 | quote: 49 | "Do not wait for the perfect moment, take the moment and make it perfect.", 50 | }, 51 | { quote: "The only source of knowledge is experience." }, 52 | ], 53 | }); 54 | 55 | console.timeEnd("Seeding complete 🌱"); 56 | }; 57 | 58 | main() 59 | .then(() => { 60 | console.log("Process completed"); 61 | }) 62 | .catch((e) => console.log(e)); 63 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/工程实践/性能优化.md: -------------------------------------------------------------------------------- 1 | # 接口缓存 2 | 3 | 缓存是一种很棒且简单的技术,可帮助您提高应用程序的性能。它充当提供高性能数据访问的临时数据存储。 4 | 5 | ## In-memory cache 6 | 7 | Nest 为各种缓存存储提供商提供了统一的 API。内置的一个是内存中的数据存储。但是,您可以轻松地切换到更全面的解决方案,例如 Redis。为了启用缓存,请首先导入 CacheModule 并调用其 register() 方法。 8 | 9 | ```ts 10 | import { CacheModule, Module } from "@nestjs/common"; 11 | import { AppController } from "./app.controller"; 12 | 13 | @Module({ 14 | imports: [CacheModule.register()], 15 | controllers: [AppController] 16 | }) 17 | export class ApplicationModule {} 18 | ``` 19 | 20 | 然后,只需将 CacheInterceptor 绑定到要缓存数据的位置即可。 21 | 22 | ```ts 23 | @Controller() 24 | @UseInterceptors(CacheInterceptor) 25 | export class AppController { 26 | @Get() 27 | findAll(): string[] { 28 | return []; 29 | } 30 | } 31 | ``` 32 | 33 | ## 全局缓存 34 | 35 | 为了减少所需的样板数量,可以将 CacheInterceptor 全局绑定到所有端点: 36 | 37 | ```ts 38 | import { CacheModule, Module, CacheInterceptor } from "@nestjs/common"; 39 | import { AppController } from "./app.controller"; 40 | import { APP_INTERCEPTOR } from "@nestjs/core"; 41 | 42 | @Module({ 43 | imports: [CacheModule.register()], 44 | controllers: [AppController], 45 | providers: [ 46 | { 47 | provide: APP_INTERCEPTOR, 48 | useClass: CacheInterceptor 49 | } 50 | ] 51 | }) 52 | export class ApplicationModule {} 53 | ``` 54 | 55 | ## 自定义缓存 56 | 57 | 所有缓存的数据都有其自己的到期时间(TTL)。要自定义默认值,请将 options 对象传递给 register()方法。 58 | 59 | ```ts 60 | CacheModule.register({ 61 | ttl: 5, // seconds 62 | max: 10 // maximum number of items in cache 63 | }); 64 | ``` 65 | 66 | 启用全局高速缓存后,高速缓存条目存储在基于路由路径自动生成的 CacheKey 下。您可以逐个方法覆盖某些缓存设置(@CacheKey()和@CacheTTL()),从而允许为各个控制器方法定制缓存策略。使用不同的缓存存储区时,这可能是最相关的。 67 | 68 | ```ts 69 | @Controller() 70 | export class AppController { 71 | @CacheKey("custom_key") 72 | @CacheTTL(20) 73 | findAll(): string[] { 74 | return []; 75 | } 76 | } 77 | ``` 78 | 79 | @CacheKey()装饰器可以与或不与相应的@CacheTTL()装饰器一起使用,反之亦然。可以选择仅覆盖@CacheKey()或仅覆盖@CacheTTL()。未用装饰器覆盖的设置将使用默认值(全局注册)。 80 | 81 | 我们也可以选择使用不同的缓存数据源: 82 | 83 | ```ts 84 | import * as redisStore from "cache-manager-redis-store"; 85 | import { CacheModule, Module } from "@nestjs/common"; 86 | import { AppController } from "./app.controller"; 87 | 88 | @Module({ 89 | imports: [ 90 | CacheModule.register({ 91 | store: redisStore, 92 | host: "localhost", 93 | port: 6379 94 | }) 95 | ], 96 | controllers: [AppController] 97 | }) 98 | export class ApplicationModule {} 99 | ``` 100 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/components/Quote/Quote.tsx: -------------------------------------------------------------------------------- 1 | import { QuoteWrapper } from "./QuoteWrapper"; 2 | import { QuoteCacheType, QuoteResult } from "@/lib/types"; 3 | import { findIATA } from "openflights-cached"; 4 | 5 | export const Quote: React.FC<{ 6 | title: string; 7 | type: QuoteCacheType; 8 | result: QuoteResult; 9 | }> = ({ title, type, result }) => { 10 | const [{ id, quote, createdAt }, { cacheStatus, region, lastModified }] = [ 11 | result.data, 12 | result.info, 13 | ]; 14 | 15 | return ( 16 | 17 |
18 |

19 | {" "} 20 | ID {id} ⸺ {'"'} 21 | {quote} 22 | {'"'} 23 |

24 |
25 |

26 | Created At ⸺ 27 | {new Date(createdAt).toLocaleString("en-US")} 28 |

29 | 30 |
31 | 32 |
33 |
34 |

35 | Cache Node Region ⸺ 36 | 37 | {" "} 38 | {findIATA(region)?.city ?? region} 39 | 40 |

41 |
42 |

43 | Cached Modified at ⸺{" "} 44 | 45 | {" "} 46 | {new Date(lastModified).toLocaleString("en-US")} 47 | 48 |

49 |
50 |

51 | Cache status ⸺{" "} 52 | 59 | {" "} 60 | {cacheStatus 61 | .toUpperCase() 62 | .concat( 63 | cacheStatus === "swr" || cacheStatus === "ttl" 64 | ? " CACHE HIT" 65 | : "" 66 | )} 67 | 68 |

69 |
70 |

71 | Time taken: {result.time}ms 72 |

73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/前后端分离/README.md: -------------------------------------------------------------------------------- 1 | # 前后端分离 2 | 3 | 前后端分离的问题,不仅仅是技术上的选型问题,还涉及到整个团队在认知、职责、流程上面重新定义的问题,这也是为什么前后端分离概念看起来简单易懂,但真正团队在落地的时候却有诸多水土不服。笔者认为前后端分离最早可以追溯到 Roy Thomas Fielding 在博士论文 [Architectural Styles and the Design of Network-based Software Architectures](https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm)([架构风格与基于网络的软件架构设计](http://o6v08w541.bkt.clouddn.com/REST-article.pdf))中提出的所谓表述性状态转移(Representational State Transfer, REST )架构风格;该论文描述了指导 REST 的软件工程原则和选择用来支持这些原则的交互约束: 4 | 5 | - CS 架构:客户 - 服务器约束背后的原则即是关注点分离(Separation of concerns,SOC),通过分离用户接口和数据存储这两个关注点,我们改善了用户接口跨多个平台的可移植性;同时通过简化服务器组件,改善了系统 的可伸缩性。然而对于 Web 来说,最重要的是这种关注点的分离允许组件独立地进化,从而支持多个组织领域的 Internet 规模的需求。 6 | 7 | * 无状态:从客户到服务器的每个请求都必须包含理解该请求所必需的所有信息,不能利用任何存储在服务器上的上下文,会话状态因此要全部保存在客户端。其优势在于不必为了确定一个请求的全部性质而去查看该请求之外的多个请求,减轻了从局部故障中恢复的任务量,并且允许服务器组件迅速释放资源,在云端部署时能够完成弹性伸缩。其缺陷在于增加了在一系列请求中发送的重复数据(每次交互的开销),可能会降低网络性能;并且将应用状态放在客户端还降低了服务器对于一致的应用行为的控制。 8 | 9 | * 缓存:缓存约束要求一个请求的响应中的数据被隐式地或显式地标记为可缓存的或不可缓存的。如果响应是可缓存的,那么客户端缓存就可以为以后的相同请求重用这个响应的数据。 10 | 11 | - 统一接口:REST 架构区别于其他基于网络的架构风格的核心特征即是强调组件之间要有一个统一的接口,通过在组件接口上应用通用性的软件工程原则,整体的系统架构得到了简化,交互的可见性也得到了改善。实现与它们所提供的服务是解耦的,这促进了独立的可进化性。 12 | 13 | * 分层系统:分层系统风格通过限制组件的行为(即,每个组件只能 “ 看到 ” 与其交互的紧邻层),将架构分解为若干等级的层。通过将组件对系统的知识限制在单一层内,为整个系统的复杂性设置了边界,并且提高了底层独立性。 14 | 15 | - 按需代码:通过下载并执行 applet 形式或脚本形式的代码,REST 允许对客户端的功能进行扩展。这样,通过减少必须被预先实现的功能的数目,简化了客户端的开发。允许在部署之后下载功能代码也改善了系统的可扩展性。通过在组件接口上应用通用性的软件工程原则,整体的系统架构得到了简化,交互的可见性也得到了改善。实现与它们所提供的服务是解耦的,这促进了独立的可进化性。 16 | 17 | 笔者认为对于前后端分离的理念有重要启发意义的即是 CS 架构与无状态这两个约束,传统的 BS 开发中因为使用了 Cookie-Session 这种存储模式,将数据的传输在服务端与客户端透明化;虽然方便了我们在服务端使用,却也导致了强耦合 18 | 19 | 服务端模板渲染(Aspx、JSP、PHP )、客户端渲染(ExtJS )与服务端渲染(ServerSide Rendering )这三类 20 | 21 | 笔者认为的前后端分离架构下的团队组织方式如下: 22 | 23 | ![](https://github.com/wx-chevalier/OSS/blob/master/ProcessOn/%E5%9B%A2%E9%98%9F%E6%9E%B6%E6%9E%84.png?raw=true) 24 | 25 | ![](https://mmbiz.qpic.cn/mmbiz_jpg/uMh5nccSicmKVK1CdCUhqxSgR9xtQliasRFfDVaWL8TZpsfNvVQzNAuRaqEVb3csY7YECwnh6So2HjmnyPTb2ILg/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1) 26 | 27 | 在[《状态管理》]()一节中我们讨论了分离前后端状态的意义,这也是笔者的一贯看法,在 JavaScript 中,纯粹的同构的前后端是个非常酷的概念,不过就如笔者在文首所讲,在 CS 架构中还是保持客户端与服务端的某个边界还是很有意义的。而在实践中,这个边界往往就是所谓的 Public API。从前端开发者的角度而言,对于服务端我只需要了解 API 相关的内容,自然也希望 API 能够稳定的保证一种向后兼容性,不向后兼容的 API 会强制前端开发者及时地做出响应。一般来说,客户端需要维护两种状态,用户的交互以及需要持久化存储到某个域中的结果。因此,我们希望能够以一种清晰可预测的方式来管理我们的状态。以 API 的方式分隔客户端状态与服务端状态即是为了保证所写代码的职责分离,你肯定不希望模块对于外部系统有太多的依赖。在具体的实现 API 的时候,我们的目标兼括了稳定性与性能本身,即使你自己作为全栈开发者同时负责了前后端的代码,我也是不建议在开发服务端业务逻辑的代码时过多的考虑到 UI State。我使用 Redux 来管理应用前端的 UI 状态,它提倡的模式与文档支持都非常好,社区本身也非常地活跃。GraphQL 也是值得推荐的用于分割状态的工具,在 GraphQL 中,客户端能够以查询地方式从服务端请求状态,GraphQL 可以将多个 Endpoint 的状态合并返回给前端,从而使前端不需要去关心后端到底是如何实现的。综上所述,随着应用的日渐增长,状态管理的复杂度也与日俱增。而能够掌控这种复杂的情形,也是软件工程师的必备能力之一。 28 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/BFF/BFF 与 BIF.md: -------------------------------------------------------------------------------- 1 | # Backend for Frontend,性能与效率的平衡 2 | 3 | BIF, Backend in Frontend 的概念 4 | 5 | 在 [Api CheatSheet]() 中,标准的 RESTful 接口是为,而在 [PayPal]() 的实践中又引入了所谓 Complex Operation 的概念,来处理跨资源的逻辑操作。在业务极速变化的情况下,前端往往需要从 6 | 7 | 这时候 BFF 层就起到了接口聚合的作用,正如 GraphQL 的思想一样,让前端从单一接口请求,减少数据交互的频次。 8 | 9 | 数据组装一个是从数据源上,针对多源异构数据的组装;另一个是在于逻辑关系上,将原本数据库联查的压力转移到逻辑层或者前端。 10 | 11 | GraphQL 与 BFF 并没有必然的联系,BFF 更多是对于关注点分离原则的实践 12 | 13 | 我们希望能够在开发效率与性能之间达成平衡,性能包括后台的数据库组装 14 | 15 | student 以 JSON 字段的方式存放了 course 相关。我们的课程数据是相对静态数据,可以认为在某次 Session 周期内,其是可静态缓存的。student_profile 表中获取 16 | 17 | 如果我们在标准后台中,依靠数据库查询的方式进行数据组装,那么不可避免地要进行多次查询,或者相对性能损耗较大地模糊查询。 18 | 19 | 在所谓的 BFF 层去进行数据组装 20 | 21 | # BFF 22 | 23 | 随着无线技术的发展和各种智能设备的兴起,互联网应用已经从单一 Web 浏览器时代演进到以 API 驱动的无线优先(Mobile First)和面向全渠道体验(omni-channel experience oriented)的时代。 24 | 25 | 应用架构的新挑战是: 26 | 27 | 用户接入形式的多样性,无线(手机、Pad),Web,互联网电视,第三方合作应用等等,各种用户设备的屏幕大小,操控体验方式各不相同,例如,手机设备的屏幕较小,能够展示的数据量小,交互方式以触控为主,也可通过条形码扫描器; 28 | 29 | 有些用户设备的带宽受限,同时应用的 UI 一般宿主在客户端,有些页面需要组合好几个后台业务服务的数据和功能,如果直接在客户端发起对多个后台服务的调用,势必造成大量网络开销影响性能,这个有点类似数据库查询中的 n+1 问题。 30 | 31 | BFF(Backend For Frontend)是应对上述应用架构挑战的一种模式和最佳实践,2015 年底,ThoughtWorks 在其网站上刊登了一篇称为 BFF@SoundCloud(SoundCloud 是一个类似音频 YouTube 的网站)的文章[附录 1],讲述 SoundCloud 如何利用 BFF 模式逐步将其单块 Rails 应用迁移改造为支持无线等多种用户体验的微服务架构。同期,ThoughtWorks 的顾问 Sam Newman,也就是《Building Microservices》那本书的作者,在 SoundCloud 等业界实践的基础上,写了一篇博客总结了 BFF 模式的原理、场景和用法[附录 2],建议大家阅读。 32 | 33 | ![640](https://user-images.githubusercontent.com/5803001/39958394-d90d8f42-5634-11e8-9bd7-61925f210d36.png) 34 | 35 | BFF 本质上是一个后端中间层,但是它的作用主要是适配前端不同用户体验(无线,桌面,智能终端等等),所以称为用户体验适配层,它的适配作用主要是: 36 | 37 | 裁剪和格式化,对后台的通用数据模型进行适当的裁剪和格式化,以适应不同的用户体验展示的需要; 38 | 39 | 聚合编排,对后台服务数据进行编排和预聚合,这样可以有效简化客户端逻辑和减少网络调用开销。 40 | 41 | Sam 推荐理想情况下针对每种用户体验类型需要一个 BFF(one BFF per user experience),例如 Mobile BFF,Desktop BFF,这可以做到职责单一和关注分离(遵循有界上下文原则),但是 BFF 过多也会造成代码逻辑重复冗余的问题,需要权衡。UI 和 BFF 理想是同一个团队负责,这样可以减少沟通协调成本,加快变更迭代速度,这是遵循康威定律的体现。下图展示了一种 BFF 和团队职责边界划分方案。 42 | 43 | ![default](https://user-images.githubusercontent.com/5803001/39958388-bfbe68ae-5634-11e8-97e4-fa4186183522.png) 44 | 45 | Sam 还指出,对于一些跨横切面的关注点(cross cutting concerns),例如路由,安全认证,日志监控分析,限流等等,通常可由独立的网关(Gateway)层负责(如 Fig 6 所示),由独立基础设施团队运维,置于 BFF 之前,这样在架构上可以做到职责单一和关注分离,让 BFF 开发人员专注于聚合裁剪等业务功能,无需考虑跨横切面功能。但是如果对运维成本和调用性能有额外考虑,跨横切面的功能也可以直接做在 BFF 一层。一般来说,开发者只需要关注蓝色(functions)部分,而至于红色部分(stub 句柄)和黄色部分(socket 网络)部分呢,框架层面会把它解决掉。蓝色部分,服务端开发者要做的事情就是定义某个接口,客户端开发者要做的事情就是调用某个接口,一切开发模式都跟本地调用无差别。 46 | 47 | # GraphQL 48 | 49 | GraphQL 最有趣的特性之一,是它本质上是面向消费者的,响应体的结构不取决于服务端,而是完全由客户端驱动。直观的理解就是 GraphQL 规定客户端在发起请求的时候必须显示的标注所需要获得的原子资源的类型以及在返回体中的位置,这一点和 Immutable.js 中的`update`函数很类似,你都需要显示指明到叶子节点。这样确实使得整个请求需要很多额外的数据参数与编码工作,但是它就将消费者解耦了,并且强迫服务端遵守 Postel 法则 ( 对自己严格,对他人宽容 )。 50 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/components/Quote/QuotesLoading.tsx: -------------------------------------------------------------------------------- 1 | import { title } from "process"; 2 | import { QuoteWrapper } from "./QuoteWrapper"; 3 | import type { QuoteCacheType } from "@/lib/types"; 4 | 5 | const loaders: { title: string; type: QuoteCacheType }[] = [ 6 | { 7 | title: "Cached Quote", 8 | type: "TTL", 9 | }, 10 | { 11 | title: "Cached Quote", 12 | type: "SWR", 13 | }, 14 | { 15 | title: "Cached Quote", 16 | type: "TTL + SWR", 17 | }, 18 | { 19 | title: "Quote", 20 | type: "No caching", 21 | }, 22 | ]; 23 | 24 | export const QuotesLoading: React.FC = () => { 25 | return ( 26 |
27 | {loaders.map((value) => { 28 | return ( 29 | 30 |
34 | 50 | Loading... 51 |
52 |
53 | ); 54 | })} 55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /01~语法基础/Streams/其他 Stream.md: -------------------------------------------------------------------------------- 1 | # Duplex Stream 2 | 3 | Duplex Stream 可以看做读写流的聚合体,其包含了相互独立、拥有独立内部缓存的两个读写流,读取与写入操作也可以异步进行: 4 | 5 | ``` 6 | Duplex Stream 7 | ------------------| 8 | Read <----- External Source 9 | You ------------------| 10 | Write -----> External Sink 11 | ------------------| 12 | ``` 13 | 14 | 我们可以使用 Duplex 模拟简单的套接字操作: 15 | 16 | ```js 17 | const { Duplex } = require("stream"); 18 | 19 | class Duplexer extends Duplex { 20 | constructor(props) { 21 | super(props); 22 | this.data = []; 23 | } 24 | 25 | _read(size) { 26 | const chunk = this.data.shift(); 27 | if (chunk == "stop") { 28 | this.push(null); 29 | } else { 30 | if (chunk) { 31 | this.push(chunk); 32 | } 33 | } 34 | } 35 | 36 | _write(chunk, encoding, cb) { 37 | this.data.push(chunk); 38 | cb(); 39 | } 40 | } 41 | 42 | const d = new Duplexer({ allowHalfOpen: true }); 43 | d.on("data", function (chunk) { 44 | console.log("read: ", chunk.toString()); 45 | }); 46 | d.on("readable", function () { 47 | console.log("readable"); 48 | }); 49 | d.on("end", function () { 50 | console.log("Message Complete"); 51 | }); 52 | d.write("...."); 53 | ``` 54 | 55 | 在开发中我们也经常需要直接将某个可读流输出到可写流中,此时也可以在其中引入 PassThrough,以方便进行额外地监听: 56 | 57 | ```js 58 | const { PassThrough } = require("stream"); 59 | const fs = require("fs"); 60 | 61 | const duplexStream = new PassThrough(); 62 | 63 | // can be piped from reaable stream 64 | fs.createReadStream("tmp.md").pipe(duplexStream); 65 | 66 | // can pipe to writable stream 67 | duplexStream.pipe(process.stdout); 68 | 69 | // 监听数据,这里直接输出的是 Buffer 70 | duplexStream.on("data", console.log); 71 | ``` 72 | 73 | # Transform Stream 74 | 75 | Transform Stream 则是实现了 `_transform` 方法的 Duplex Stream,其在兼具读写功能的同时,还可以对流进行转换: 76 | 77 | ``` 78 | Transform Stream 79 | --------------|-------------- 80 | You Write ----> ----> Read You 81 | --------------|-------------- 82 | ``` 83 | 84 | 这里我们实现简单的 Base64 编码器: 85 | 86 | ```js 87 | const util = require("util"); 88 | const Transform = require("stream").Transform; 89 | 90 | function Base64Encoder(options) { 91 | Transform.call(this, options); 92 | } 93 | 94 | util.inherits(Base64Encoder, Transform); 95 | 96 | Base64Encoder.prototype._transform = function (data, encoding, callback) { 97 | callback(null, data.toString("base64")); 98 | }; 99 | 100 | process.stdin.pipe(new Base64Encoder()).pipe(process.stdout); 101 | ``` 102 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/README.md: -------------------------------------------------------------------------------- 1 | # Nest 2 | 3 | 近些年来,随着 Node.js 的发展,JavaScript 日渐成为 Web 开发中前后端皆可用的最流行语言,也为 Angular、React、Vue.js 这些优秀的前端项目的崛起提供了基石。而 Next.js 则致力于提供开箱即用的应用架构,允许开发者快速构建高可测试、可扩展、松耦合、易维护的 Node.js Web 应用。 4 | 5 | [Nest.js](https://docs.nestjs.com/) 基于 TypeScript 构建,其和 TypeScript 无缝衔接,同时允许开发者使用现代 JavaScript 进行开发。Nest.js 融合了面向对象编程 OOP、函数式编程 FP、函数响应式编程 FRP 的优秀思想,为开发者提供了完善的功能特性与使用体验;其底层使用了 Express,也方便了开发者集成 Express 生态圈相关的第三方插件。Next.js 的主要特性包括:依赖注入、WebSockets、模块化、响应式微服务、异常处理层、用于校验的 Pipe、用于角色化权限控制的 Guards、Interceptors、单元测试与集成测试等。 6 | 7 | > 注:本系列是对于官网文档和示例,以及笔者自身实践过程中的代码总结。 8 | 9 | # 快速开始 10 | 11 | 本部分主要介绍 Nest.js 项目的基本搭建与请求处理相关内容,建议是直接下载官方的 TypeScript 模板作为项目初始化模板: 12 | 13 | ```sh 14 | $ git clone https://github.com/nestjs/typescript-starter.git project 15 | $ cd project 16 | $ npm install 17 | $ npm run start 18 | ``` 19 | 20 | 典型的 Nest 项目会包含如下的模块: 21 | 22 | ```s 23 | src 24 | ├── app.controller.spec.ts 25 | ├── app.controller.ts 26 | ├── app.module.ts 27 | ├── app.service.ts 28 | └── main.ts 29 | ``` 30 | 31 | ```ts 32 | import { NestFactory } from "@nestjs/core"; 33 | 34 | import { ApplicationModule } from "./modules/ApplicationModule"; 35 | 36 | async function bootstrap() { 37 | const app = await NestFactory.create(ApplicationModule); 38 | 39 | await app.listen(3000); 40 | } 41 | 42 | bootstrap(); 43 | ``` 44 | 45 | ## 控制器 46 | 47 | ```ts 48 | // ApplicationModule.ts 49 | 50 | import { Module } from "@nestjs/common"; 51 | 52 | import { HelloController } from "../controller/HelloController"; 53 | 54 | @Module({ 55 | modules: [], 56 | 57 | controllers: [HelloController], 58 | }) 59 | export class ApplicationModule {} 60 | ``` 61 | 62 | ```ts 63 | // HelloController.ts 64 | 65 | import { Controller, Get } from "@nestjs/common"; 66 | 67 | @Controller("/") 68 | export class HelloController { 69 | @Get() 70 | hello() { 71 | return "Next.js Boilerplate @ 王下邀月熊"; 72 | } 73 | } 74 | ``` 75 | 76 | ```ts 77 | import { Controller, Get, Post } from "@nestjs/common"; 78 | 79 | @Controller("cats") 80 | export class CatsController { 81 | @Post() 82 | create() { 83 | // TODO: Add some logic here 84 | } 85 | 86 | @Get() 87 | findAll() { 88 | return []; 89 | } 90 | } 91 | ``` 92 | 93 | ## 平台 94 | 95 | Nest 的目标是一个平台无关的框架。这个意思就是说 Nest 本身并不造某个细分领域的轮子,他只构建一套构架体系,然后把一些好用的库或者平台融合进来。所以 Nest 可以衔接任何 HTTP 框架,默认支持 express 和 fastify 两个 web 框架。 96 | 97 | - platform-express: Express 是一个 Node web 框架,有很多社区成熟的资源。@nestjs/platform-express 默认会被引入,大家都很熟悉了,用起来会容易上手 98 | 99 | - platform-fastify: Fastify 是一个高能低耗的框架,致力于最大化效率与速度 100 | 101 | 无论使用哪个平台,都要暴露自己的应用接口。上面两个平台暴露了对应的两个变量 NestExpressApplication and NestFastifyApplication。如下的代码会创建一个 app 对象,并且指定了使用 NestExpressApplication 平台: 102 | 103 | ```ts 104 | const app = await NestFactory.create(ApplicationModule); 105 | ``` 106 | 107 | 不过一般情况下不需要指定这个类型。 108 | 109 | # Links 110 | 111 | - https://keelii.com/2019/07/03/nestjs-framework-tutorial-3/ 112 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/实体/索引.md: -------------------------------------------------------------------------------- 1 | # 索引 2 | 3 | ## 单列索引 4 | 5 | 你可以在要创建索引的列上使用`@Index`为特定列创建数据库索引也可以为实体的任何列创建索引例如: 6 | 7 | ```typescript 8 | import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm"; 9 | 10 | @Entity() 11 | export class User { 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @Index() 16 | @Column() 17 | firstName: string; 18 | 19 | @Column() 20 | @Index() 21 | lastName: string; 22 | } 23 | ``` 24 | 25 | 还可以指定索引名称: 26 | 27 | ```typescript 28 | import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm"; 29 | 30 | @Entity() 31 | export class User { 32 | @PrimaryGeneratedColumn() 33 | id: number; 34 | 35 | @Index("name1-idx") 36 | @Column() 37 | firstName: string; 38 | 39 | @Column() 40 | @Index("name2-idx") 41 | lastName: string; 42 | } 43 | ``` 44 | 45 | ## 唯一索引 46 | 47 | 要创建唯一索引,需要在索引选项中指定`{unique:true}`: 48 | 49 | ```typescript 50 | import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm"; 51 | 52 | @Entity() 53 | export class User { 54 | @PrimaryGeneratedColumn() 55 | id: number; 56 | 57 | @Index({ unique: true }) 58 | @Column() 59 | firstName: string; 60 | 61 | @Column() 62 | @Index({ unique: true }) 63 | lastName: string; 64 | } 65 | ``` 66 | 67 | ## 联合索引 68 | 69 | 要创建具有多个列的索引,需要将`@Index`放在实体本身上,并指定应包含在索引中的所有列属性名称例如: 70 | 71 | ```typescript 72 | import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm"; 73 | 74 | @Entity() 75 | @Index(["firstName", "lastName"]) 76 | @Index(["firstName", "middleName", "lastName"], { unique: true }) 77 | export class User { 78 | @PrimaryGeneratedColumn() 79 | id: number; 80 | 81 | @Column() 82 | firstName: string; 83 | 84 | @Column() 85 | middleName: string; 86 | 87 | @Column() 88 | lastName: string; 89 | } 90 | ``` 91 | 92 | ## 空间索引 93 | 94 | MySQL 和 PostgreSQL(当 PostGIS 可用时)都支持空间索引。 95 | 96 | 要在 MySQL 中的列上创建空间索引,请在使用空间类型的列(`geometry`,`point`,`linestring`,`polygon`,`multipoint`,`multilinestring`,`multipolygon`,`geometrycollection`)上添加`index`,其中`spatial:true`): 97 | 98 | ```typescript 99 | @Entity() 100 | export class Thing { 101 | @Column("point") 102 | @Index({ spatial: true }) 103 | point: string; 104 | } 105 | ``` 106 | 107 | 要在 PostgreSQL 中的列上创建空间索引,请在使用空间类型(`geometry`,`geography`)的列上添加带有`spatial:true`的`Index`: 108 | 109 | ```typescript 110 | @Entity() 111 | export class Thing { 112 | @Column("geometry", { 113 | spatialFeatureType: "Point", 114 | srid: 4326 115 | }) 116 | @Index({ spatial: true }) 117 | point: Geometry; 118 | } 119 | ``` 120 | 121 | ## 禁用同步 122 | 123 | TypeORM 不支持某些索引选项和定义(例如`lower`,`pg_trgm`),因为它们具有许多不同的数据库细节以及获取有关现有数据库索引的信息并自动同步的多个问题在这种情况下,你应该使用所需的任何索引签名手动创建索引(例如在迁移中)要使 TypeORM 在同步期间忽略这些索引,请在`@Index`装饰器上使用`synchronize:false`选项。 124 | 125 | 例如,使用不区分大小写的比较创建索引: 126 | 127 | ```sql 128 | CREATE INDEX "POST_NAME_INDEX" ON "post" (lower("name")) 129 | ``` 130 | 131 | 之后,应该禁用此索引的同步,以避免在下一个架构同步时删除: 132 | 133 | ```ts 134 | @Entity() 135 | @Index("POST_NAME_INDEX", { synchronize: false }) 136 | export class Post { 137 | @PrimaryGeneratedColumn() 138 | id: number; 139 | 140 | @Column() 141 | name: string; 142 | } 143 | ``` 144 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/MVC/权限与校验.md: -------------------------------------------------------------------------------- 1 | # 权限与校验 2 | 3 | # Guards 4 | 5 | 守卫(Guards)是一个使用 @Injectable() 装饰的类,它必须实现 CanActivate 接口。 6 | 7 | ![守卫](https://i.loli.net/2019/07/05/5d1ec2a0e2ae056395.png) 8 | 9 | 守卫只有一个职责,就是决定请求是否需要被控制器处理。一般用在权限、角色的场景中。守卫和中间件的区别在于:中间件很简单,next 方法调用后中间的任务就完成了。但是守卫需要关心上下游,它需要鉴别请求与控制器之间的关系。守卫会在中间件逻辑之后、拦截器/管道之前执行。 10 | 11 | ## 绑定守卫 12 | 13 | 就像过滤器一样,守卫可以是控制器作用域的,也可以是方法作用域或者全局作用域。我们使用 @UseGuards 来引用一个控制器作用域的守卫。 14 | 15 | ```ts 16 | @Controller("cats") 17 | @UseGuards(RolesGuard) 18 | export class CatsController {} 19 | ``` 20 | 21 | 如果想引用到全局作用域可以调用 useGlobalGuards 方法。 22 | 23 | ```ts 24 | const app = await NestFactory.create(ApplicationModule); 25 | app.useGlobalGuards(new RolesGuard()); 26 | ``` 27 | 28 | 由于我们在根模块外层引用了全局守卫,这时守卫无法注入依赖。所以我们还需要在要模块上引入。 29 | 30 | ```ts 31 | import { Module } from "@nestjs/common"; 32 | import { APP_GUARD } from "@nestjs/core"; 33 | 34 | @Module({ 35 | providers: [ 36 | { 37 | provide: APP_GUARD, 38 | useClass: RolesGuard 39 | } 40 | ] 41 | }) 42 | export class ApplicationModule {} 43 | ``` 44 | 45 | ## 授权守卫 46 | 47 | ```ts 48 | @Injectable() 49 | export class AuthGuard implements CanActivate { 50 | canActivate( 51 | context: ExecutionContext 52 | ): boolean | Promise | Observable { 53 | const request = context.switchToHttp().getRequest(); 54 | return validateRequest(request); 55 | } 56 | } 57 | ``` 58 | 59 | canActivate 返回 true,控制器正常执行,false 请求会被 deny。 60 | 61 | ## 执行上下文 62 | 63 | ExecutionContext 不但继承了 ArgumentsHost,还有两个额外方法: 64 | 65 | ```ts 66 | export interface ExecutionContext extends ArgumentsHost { 67 | getClass(): Type; 68 | getHandler(): Function; 69 | } 70 | ``` 71 | 72 | getHandler() 方法会返回一个将被调用的方法处理器,getClass() 返回处理器对应的控制器类。我们来实现一个小型的基于角色的认证系统。 73 | 74 | ```ts 75 | @Injectable() 76 | export class RolesGuard implements CanActivate { 77 | constructor(private readonly reflector: Reflector) {} 78 | 79 | canActivate(context: ExecutionContext): boolean { 80 | const roles = this.reflector.get("roles", context.getHandler()); 81 | if (!roles) { 82 | return true; 83 | } 84 | const request = context.switchToHttp().getRequest(); 85 | const user = request.user; 86 | const hasRole = () => user.roles.some(role => roles.includes(role)); 87 | return user && user.roles && hasRole(); 88 | } 89 | } 90 | ``` 91 | 92 | 虽然现在已经有了守卫,但是它还没有执行上下文。CatsController 应该有一些需要访问到的权限类型。比如:管理员(admin)角色可以访问、其它角色不可以。这时我们需要对控制器(或方法)添加一些元数据,用来标记这个控制器的权限类型。在 Nest 中我们通常使用 @SetMetadata() 装饰器来完成这个工作。 93 | 94 | ```ts 95 | @Post() 96 | @SetMetadata('roles', ['admin']) 97 | async create(@Body() createCatDto: CreateCatDto) { 98 | this.catsService.create(createCatDto); 99 | } 100 | ``` 101 | 102 | 上面的代码表示给 create 方法设置角色的元数据,用来标识 create 方法只能是 roles 关联的一些角色(admin…)才能访问到的。如果你觉得 SetMetadata 这个装饰器看着不是那么见名知意,也可以实现一个自定义的装饰器。 103 | 104 | ```ts 105 | import { SetMetadata } from "@nestjs/common"; 106 | 107 | export const Roles = (...roles: string[]) => SetMetadata("roles", roles); 108 | ``` 109 | 110 | 这样就可以用更简洁的方式来声明角色权限关系了: 111 | 112 | ```ts 113 | @Post() 114 | @Roles('admin') 115 | async create(@Body() createCatDto: CreateCatDto) { 116 | this.catsService.create(createCatDto); 117 | } 118 | ``` 119 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/实体/监听器和订阅者.md: -------------------------------------------------------------------------------- 1 | ## 监听器 2 | 3 | 任何实体都可以使用具有侦听特定实体事件的自定义逻辑的方法你必须使用特殊装饰器标记这些方法,具体取决于要侦听的事件。 4 | 5 | ### `@AfterLoad` 6 | 7 | 你可以在实体中定义具有任何名称的方法,并使用`@AfterLoad`标记它,TypeORM 将在每次实体时调用它 使用`QueryBuilder`或 repository/manager 查找方法加载例如: 8 | 9 | ```typescript 10 | @Entity() 11 | export class Post { 12 | @AfterLoad() 13 | updateCounters() { 14 | if (this.likesCount === undefined) this.likesCount = 0; 15 | } 16 | } 17 | ``` 18 | 19 | ### `@BeforeInsert` 20 | 21 | 你可以在实体中定义具有任何名称的方法,并使用`@BeforeInsert`标记它,并且 TypeORM 将在使用 repository/manager `save`插入实体之前调用它例如: 22 | 23 | ```typescript 24 | @Entity() 25 | export class Post { 26 | @BeforeInsert() 27 | updateDates() { 28 | this.createdDate = new Date(); 29 | } 30 | } 31 | ``` 32 | 33 | ### `@AfterInsert` 34 | 35 | 你可以在实体中定义具有任何名称的方法,并使用`@AfterInsert`标记它,并且在使用 repository/manager `save`插入实体后,TypeORM 将调用它。 36 | 37 | ```typescript 38 | @Entity() 39 | export class Post { 40 | @AfterInsert() 41 | resetCounters() { 42 | this.counters = 0; 43 | } 44 | } 45 | ``` 46 | 47 | ### `@BeforeUpdate` 48 | 49 | 你可以在实体中定义具有任何名称的方法,并使用`@BeforeUpdate`标记它,并且 TypeORM 将在使用 repository/manager `save`更新现有实体之前调用它但请记住,只有在模型中更改信息时才会出现这种情况如果运行`save`而不修改模型中的任何内容,`@BeforeUpdate`和`@AfterUpdate`将不会运行例如: 50 | 51 | ```typescript 52 | @Entity() 53 | export class Post { 54 | @BeforeUpdate() 55 | updateDates() { 56 | this.updatedDate = new Date(); 57 | } 58 | } 59 | ``` 60 | 61 | ### `@AfterUpdate` 62 | 63 | 你可以在实体中定义具有任何名称的方法,并使用`@AfterUpdate`标记它,并且在使用 repository/manager `save`更新现有实体后,TypeORM 将调用它例如: 64 | 65 | ```typescript 66 | @Entity() 67 | export class Post { 68 | @AfterUpdate() 69 | updateCounters() { 70 | this.counter = 0; 71 | } 72 | } 73 | ``` 74 | 75 | ### `@BeforeRemove` 76 | 77 | 你可以在实体中定义具有任何名称的方法,并使用`@BeforeRemove`标记它,并且 TypeORM 将在使用 repository/manager `remove`删除实体之前调用它例如: 78 | 79 | ```typescript 80 | @Entity() 81 | export class Post { 82 | @BeforeRemove() 83 | updateStatus() { 84 | this.status = "removed"; 85 | } 86 | } 87 | ``` 88 | 89 | ### [#](https://typeorm.io/#listeners-and-subscribers/afterremove) `@AfterRemove` 90 | 91 | 你可以在实体中定义一个具有任何名称的方法,并使用`@ AfterRemove`标记它,TypeORM 将在使用 repository/manager `remove`删除实体后调用它例如: 92 | 93 | ```typescript 94 | @Entity() 95 | export class Post { 96 | @AfterRemove() 97 | updateStatus() { 98 | this.status = "removed"; 99 | } 100 | } 101 | ``` 102 | 103 | ## 订阅者 104 | 105 | 将类标记为可以侦听特定实体事件或任何实体事件的事件订阅者使用`QueryBuilder`和存储库/管理器方法触发事件例如: 106 | 107 | ```typescript 108 | @EventSubscriber() 109 | export class PostSubscriber implements EntitySubscriberInterface { 110 | /** 111 | * 表示此订阅者仅侦听Post事件。 112 | */ 113 | listenTo() { 114 | return Post; 115 | } 116 | 117 | /** 118 | * 插入post之前调用。 119 | */ 120 | beforeInsert(event: InsertEvent) { 121 | console.log(`BEFORE POST INSERTED: `, event.entity); 122 | } 123 | } 124 | ``` 125 | 126 | 你可以从`EntitySubscriberInterface`实现任何方法要监听任何实体,你只需省略`listenTo`方法并使用`any`: 127 | 128 | ```typescript 129 | @EventSubscriber() 130 | export class PostSubscriber implements EntitySubscriberInterface { 131 | /** 132 | * 在实体插入之前调用。 133 | */ 134 | beforeInsert(event: InsertEvent) { 135 | console.log(`BEFORE ENTITY INSERTED: `, event.entity); 136 | } 137 | } 138 | ``` 139 | -------------------------------------------------------------------------------- /05~数据库/Prisma/88~examples/prisma-nextjs-starter/README.md: -------------------------------------------------------------------------------- 1 | # Prisma Accelerate Example: Next.js Starter 2 | 3 | This project showcases how to use Prisma ORM with Prisma Accelerate in a Next.js application. It [demonstrates](./app/api/route.ts#L15-46) every available [caching strategy in Accelerate](https://www.prisma.io/docs/data-platform/accelerate/concepts#cache-strategies). 4 | 5 | ## Prerequisites 6 | 7 | To successfully run the project, you will need the following: 8 | 9 | - The **connection string** of a publicly accessible database 10 | - Your **Accelerate connection string** (containing your **Accelerate API key**) which you can get by enabling Accelerate in a project in your [Prisma Data Platform](https://pris.ly/pdp) account (learn more in the [docs](https://www.prisma.io/docs/platform/concepts/environments#api-keys)) 11 | 12 | ## Getting started 13 | 14 | ### 1. Clone the repository 15 | 16 | Clone the repository, navigate into it and install dependencies: 17 | 18 | ``` 19 | git clone git@github.com:prisma/prisma-examples.git --depth=1 20 | cd prisma-examples/accelerate/nextjs-starter 21 | npm install 22 | ``` 23 | 24 | ### 2. Configure environment variables 25 | 26 | Create a `.env` in the root of the project directory: 27 | 28 | ```bash 29 | touch .env 30 | ``` 31 | 32 | Now, open the `.env` file and set the `DATABASE_URL` and `DIRECT_URL` environment variables with the values of your connection string and your Accelerate connection string: 33 | 34 | ```bash 35 | # .env 36 | 37 | # Accelerate connection string (used for queries by Prisma Client) 38 | DATABASE_URL="__YOUR_ACCELERATE_CONNECTION_STRING__" 39 | 40 | # Database connection string (used for migrations by Prisma Migrate) 41 | DIRECT_URL="__YOUR_DATABASE_CONNECTION_STRING__" 42 | 43 | NEXT_PUBLIC_URL="http://localhost:3000" 44 | ``` 45 | 46 | Note that `__YOUR_DATABASE_CONNECTION_STRING__` and `__YOUR_ACCELERATE_CONNECTION_STRING__` are placeholder values that you need to replace with the values of your database and Accelerate connection strings. Notice that the Accelerate connection string has the following structure: `prisma://accelerate.prisma-data.net/?api_key=__YOUR_ACCELERATE_API_KEY__`. 47 | 48 | ### 3. Run a migration to create the `Quotes` table and seed the database 49 | 50 | The Prisma schema file contains a single `Quotes` model. You can map this model to the database and create the corresponding `Quotes` table using the following command: 51 | 52 | ``` 53 | npx prisma migrate dev --name init 54 | ``` 55 | 56 | You now have an empty `Quotes` table in your database. Next, run the [seed script](./prisma/seed.ts) to create some sample records in the table: 57 | 58 | ``` 59 | npx prisma db seed 60 | ``` 61 | 62 | ### 4. Generate Prisma Client for Accelerate 63 | 64 | When using Accelerate, Prisma Client doesn't need a query engine. That's why you should generate it as follows: 65 | 66 | ``` 67 | npx prisma generate --no-engine 68 | ``` 69 | 70 | ### 5. Start the app 71 | 72 | You can run the app with the following command: 73 | 74 | ``` 75 | npm run dev 76 | ``` 77 | 78 | You can see the performancen and other stats (e.g. cache/hit) for the different Accelerate cache strategies at the bottom of the UI: 79 | 80 | ![Demo](./demo.gif) 81 | 82 | ## Resources 83 | 84 | - [Accelerate Speed Test](https://accelerate-speed-test.vercel.app/) 85 | - [Accelerate documentation](https://www.prisma.io/docs/accelerate) 86 | - [Prisma Discord](https://pris.ly/discord) 87 | -------------------------------------------------------------------------------- /01~语法基础/文件存储/文件读取.md: -------------------------------------------------------------------------------- 1 | # Node.js 文件读写 2 | 3 | 推荐使用 [node-fs-extra](https://github.com/jprichardson/node-fs-extra) 来扩展原生的 fs 模块尚未提供的功能, 4 | 5 | # 读取为流 6 | 7 | 读取文件并将其输出到 stdout 的简单例子。 8 | 9 | ```js 10 | const fs = require("fs"); 11 | const readStream = fs.createReadStream("myfile.txt"); 12 | readStream.pipe(process.stdout); 13 | ``` 14 | 15 | 创建一个文件的 sha1 摘要,并将结果回传到 stdout(类似于 shasum): 16 | 17 | ```js 18 | const crypto = require("crypto"); 19 | const fs = require("fs"); 20 | 21 | const readStream = fs.createReadStream("myfile.txt"); 22 | const hash = crypto.createHash("sha1"); 23 | readStream 24 | .on("data", function (chunk) { 25 | hash.update(chunk); 26 | }) 27 | .on("end", function () { 28 | console.log(hash.digest("hex")); 29 | }); 30 | ``` 31 | 32 | data 事件是在可读数据流上为每块数据发射的,所以你可以边走边更新每块数据的摘要,最后当数据流结束时,结束事件被发射,这样你就可以输出最终结果。请注意,每次你调用.on() 来注册一个监听器时,它都会返回原始流,所以你可以很容易地连锁方法。在 Node.js 0.10+中,有一个更好的方法来消耗流。Readable 接口使流的工作变得更容易,特别是在创建流和使用流之间你想做其他事情的流。这些较新的 Readable 流是拉流,当你被读取数据时,你会请求数据,而不是让数据推送给你。 33 | 34 | ```js 35 | const crypto = require("crypto"); 36 | const fs = require("fs"); 37 | const readStream = fs.createReadStream("myfile.txt"); 38 | const hash = crypto.createHash("sha1"); 39 | 40 | readStream 41 | .on("readable", function () { 42 | const chunk; 43 | while (null !== (chunk = readStream.read())) { 44 | hash.update(chunk); 45 | } 46 | }) 47 | .on("end", function () { 48 | console.log(hash.digest("hex")); 49 | }); 50 | ``` 51 | 52 | # 文本读取 53 | 54 | ```js 55 | const { promisify } = require("util"); 56 | const fs = require("fs"); 57 | const readFileAsync = promisify(fs.readFile); // (A) 58 | const filePath = process.argv[2]; 59 | 60 | readFileAsync(filePath, { encoding: "utf8" }) 61 | .then((text) => { 62 | console.log("CONTENT:", text); 63 | }) 64 | .catch((err) => { 65 | console.log("ERROR:", err); 66 | }); 67 | ``` 68 | 69 | 如果文件不存在的话则会报错,有时候我们需要首先判断文件是否存在: 70 | 71 | ```js 72 | fs.exists(path, callback); 73 | fs.existsSync(path); 74 | ``` 75 | 76 | # JSON 77 | 78 | ```js 79 | const fs = require("fs-extra"); 80 | 81 | const file = "/tmp/this/path/does/not/exist/file.json"; 82 | 83 | fs.outputJson(file, { name: "JP" }, (err) => { 84 | console.log(err); // => null 85 | 86 | fs.readJson(file, (err, data) => { 87 | if (err) return console.error(err); 88 | 89 | console.log(data.name); // => JP 90 | }); 91 | }); 92 | 93 | // With Promises: 94 | 95 | fs.outputJson(file, { name: "JP" }) 96 | .then(() => fs.readJson(file)) 97 | .then((data) => { 98 | console.log(data.name); // => JP 99 | }) 100 | .catch((err) => { 101 | console.error(err); 102 | }); 103 | ``` 104 | 105 | ```js 106 | function processFile(inputFile) { 107 | const fs = require("fs"), 108 | readline = require("readline"), 109 | instream = fs.createReadStream(inputFile), 110 | outstream = new (require("stream"))(), 111 | rl = readline.createInterface(instream, outstream); 112 | 113 | rl.on("line", function (line) { 114 | console.log(line); 115 | }); 116 | 117 | rl.on("close", function (line) { 118 | console.log(line); 119 | console.log("done reading file."); 120 | }); 121 | } 122 | 123 | processFile("/path/to/a/input/file.txt"); 124 | ``` 125 | 126 | ```js 127 | fs.readFile("input.txt", "utf8", function (err, data) { 128 | if (err) throw err; 129 | 130 | console.log(data); 131 | }); 132 | ``` 133 | -------------------------------------------------------------------------------- /03~Web 框架/Express/路由与请求处理.md: -------------------------------------------------------------------------------- 1 | # Express 中路由与请求处理 2 | 3 | ```js 4 | const fs = require("fs"); 5 | const http = require("http"); 6 | const https = require("https"); 7 | const privateKey = fs.readFileSync("sslcert/server.key", "utf8"); 8 | const certificate = fs.readFileSync("sslcert/server.crt", "utf8"); 9 | 10 | const credentials = { key: privateKey, cert: certificate }; 11 | const express = require("express"); 12 | const app = express(); 13 | 14 | app.get("/", function(req, res) { 15 | res.send("hello"); 16 | }); 17 | 18 | // 其他的 Express 配置 19 | 20 | const httpServer = http.createServer(app); 21 | const httpsServer = https.createServer(credentials, app); 22 | 23 | httpServer.listen(8080); 24 | httpsServer.listen(8443); 25 | ``` 26 | 27 | # Hello World 28 | 29 | 首先假定你已经安装了 [Node.js](https://nodejs.org/),接下来为你的应用创建一个目录,然后进入此目录并将其作为当前工作目录。 30 | 31 | ``` 32 | $ mkdir myapp 33 | $ cd myapp 34 | ``` 35 | 36 | 通过 `npm init` 命令为你的应用创建一个 `package.json` 文件。欲了解 `package.json` 是如何起作用的,请参考 [Specifics of npm’s package.json handling](https://docs.npmjs.com/files/package.json)。 37 | 38 | ``` 39 | $ npm init 40 | ``` 41 | 42 | 此命令将要求你输入几个参数,例如此应用的名称和版本。你可以直接按“回车”键接受默认设置即可,下面这个除外: 43 | 44 | ``` 45 | entry point: (index.js) 46 | ``` 47 | 48 | 键入 `app.js` 或者你所希望的名称,这是当前应用的入口文件。如果你希望采用默认的 `index.js` 文件名,只需按“回车”键即可。接下来安装 Express 并将其保存到依赖列表中: 49 | 50 | ``` 51 | $ npm install express --save 52 | ``` 53 | 54 | 如果只是临时安装 Express,不想将它添加到依赖列表中,只需略去 `--save` 参数即可: 55 | 56 | ## HelloWorld 57 | 58 | ``` 59 | var express = require('express'); 60 | var app = express(); 61 | 62 | app.get('/', function (req, res) { 63 | res.send('Hello World!'); 64 | }); 65 | 66 | var server = app.listen(3000, function () { 67 | var host = server.address().address; 68 | var port = server.address().port; 69 | 70 | console.log('Example app listening at http://%s:%s', host, port); 71 | }); 72 | ``` 73 | 74 | 上面的代码启动一个服务并监听从 3000 端口进入的所有连接请求。他将对所有 (/) URL 或路由 返回 “Hello World!” 字符串。对于其他所有路径全部返回 404 Not Found。 75 | 76 | ## Hot Reload 77 | 78 | ``` 79 | var express = require('express') 80 | var app = express() 81 | var chokidar = require('chokidar') 82 | var watcher = chokidar.watch('./app') 83 | watcher.on('ready', function() { 84 | watcher.on('all', function() { 85 | console.log("Clearing /dist/ module cache from server") 86 | Object.keys(require.cache).forEach(function(id) { 87 | if (/[\/\\]app[\/\\]/.test(id)) delete require.cache[id] 88 | }) 89 | }) 90 | }) 91 | app.use(function (req, res, next) { 92 | require('./app/index')(req, res, next) 93 | }) 94 | app.listen(9000) 95 | ``` 96 | 97 | 如果你希望在生产环境下不进行热加载,那么可以通过判断 NODE_ENV 的方式: 98 | 99 | ``` 100 | var production = process.env.NODE_ENV === 'production' 101 | if(!production) { 102 | var chokidar = require('chokidar') 103 | var watcher = chokidar.watch('./dist') 104 | watcher.on('ready', function() { 105 | watcher.on('all', function() { 106 | console.log("Clearing /dist/ module cache from server") 107 | Object.keys(require.cache).forEach(function(id) { 108 | if (/[\/\\]dist[\/\\]/.test(id)) delete require.cache[id] 109 | }) 110 | }) 111 | }) 112 | } 113 | ``` 114 | 115 | # 路由 116 | 117 | ## 静态路由 118 | 119 | # 中间件 120 | 121 | # 调试与发布 122 | 123 | ## 应用调试 124 | 125 | ## 进程管理 126 | 127 | ## 状态监控 128 | 129 | 借鉴了[express-status-monitor](https://github.com/RafalWilinski/express-status-monitor): 130 | 131 | ![](https://camo.githubusercontent.com/1920f24ec0da485299d076cacc5a2606d0c6a7e3/687474703a2f2f692e696d6775722e636f6d2f4148697a4557712e676966) 132 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/pnpm/依赖管理结构.md: -------------------------------------------------------------------------------- 1 | # 依赖管理结构 2 | 3 | ## npm/yarn 的依赖管理结构 4 | 5 | 主要分为两个部分, 首先,执行 npm/yarn install 之后,包如何到达项目 node_modules 当中。其次,node_modules 内部如何管理依赖。执行命令后,首先会构建依赖树,然后针对每个节点下的包,会经历下面四个步骤: 6 | 7 | - 1. 将依赖包的版本区间解析为某个具体的版本号 8 | - 2. 下载对应版本依赖的 tar 包到本地离线镜像 9 | - 3. 将依赖从离线镜像解压到本地缓存 10 | - 4. 将依赖从缓存拷贝到当前目录的 node_modules 目录 11 | 12 | 然后,对应的包就会到达项目的 node_modules 当中。那么,这些依赖在 node_modules 内部是什么样的目录结构呢,换句话说,项目的依赖树是什么样的呢?在 npm1、npm2 中呈现出的是嵌套结构,比如下面这样: 13 | 14 | ```sh 15 | node_modules 16 | └─ foo 17 | ├─ index.js 18 | ├─ package.json 19 | └─ node_modules 20 | └─ bar 21 | ├─ index.js 22 | └─ package.json 23 | ``` 24 | 25 | 如果 bar 当中又有依赖,那么又会继续嵌套下去。试想一下这样的设计存在什么问题: 26 | 27 | - 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下。 28 | - 大量重复的包被安装,文件体积超级大。比如跟 foo 同级目录下有一个 baz,两者都依赖于同一个版本的 lodash,那么 lodash 分别在两者的 node_modules 中被安装,也就是重复安装。 29 | - 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。 30 | 31 | 接着,从 npm3 开始,包括 yarn,都着手来通过扁平化依赖的方式来解决这个问题。相比之前的嵌套结构,现在的目录结构类似下面这样: 32 | 33 | ```sh 34 | node_modules 35 | ├─ foo 36 | | ├─ index.js 37 | | └─ package.json 38 | └─ bar 39 | ├─ index.js 40 | └─ package.json 41 | ``` 42 | 43 | 所有的依赖都被拍平到 node_modules 目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的 node_modules 当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。之前的问题是解决了,但仔细想想这种扁平化的处理方式,它真的就是无懈可击吗?并不是。它照样存在诸多问题,梳理一下: 44 | 45 | - 依赖结构的不确定性。 46 | - 扁平化算法本身的复杂性很高,耗时较长。 47 | - 项目中仍然可以非法访问没有声明过依赖的包 48 | 49 | 后面两个都好理解,那第一点中的不确定性是什么意思?这里来详细解释一下。假如现在项目依赖两个包 foo 和 bar,这两个包的依赖又是这样的: 50 | 51 | ![混合依赖](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/superbed/2021/08/01/610659935132923bf810cb51.jpg) 52 | 53 | 那么 npm/yarn install 的时候,通过扁平化处理之后,可能是以下任一方式: 54 | 55 | ![扁平化方式](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/superbed/2021/08/01/61065a465132923bf8142bf8.jpg) 56 | 57 | 取决于 foo 和 bar 在 package.json 中的位置,如果 foo 声明在前面,那么就是前面的结构,否则是后面的结构。这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是 package-lock.json(npm 5.x 才出现)还是 yarn.lock,都是为了保证 install 之后都产生确定的 node_modules 结构。 58 | 59 | 尽管如此,npm/yarn 本身还是存在扁平化算法复杂和 package 非法访问的问题,影响性能和安全。 60 | 61 | ## pnpm 依赖管理 62 | 63 | pnpm 的作者 Zoltan Kochan 发现 yarn 并没有打算去解决上述的这些问题,于是另起炉灶,写了全新的包管理器,开创了一套新的依赖管理机制,现在就让我们去一探究竟。 64 | 65 | ```s 66 | ▾ node_modules 67 | ▾ .pnpm 68 | ▸ accepts@1.3.7 69 | ▸ array-flatten@1.1.1 70 | ... 71 | ▾ express@4.17.1 72 | ▾ node_modules 73 | ▸ accepts -> ../accepts@1.3.7/node_modules/accepts 74 | ▸ array-flatten -> ../array-flatten@1.1.1/node_modules/array-flatten 75 | ... 76 | ▾ express 77 | ▸ lib 78 | History.md 79 | index.js 80 | LICENSE 81 | package.json 82 | Readme.md 83 | ``` 84 | 85 | 将包本身和依赖放在同一个 node_module 下面,与原生 Node 完全兼容,又能将 package 与相关的依赖很好地组织到一起,设计十分精妙。现在我们回过头来看,根目录下的 node_modules 下面不再是眼花缭乱的依赖,而是跟 package.json 声明的依赖基本保持一致。即使 pnpm 内部会有一些包会设置依赖提升,会被提升到根目录 node_modules 当中,但整体上,根目录的 node_modules 比以前还是清晰和规范了许多。 86 | 87 | pnpm 这种依赖管理的方式也很巧妙地规避了非法访问依赖的问题,也就是只要一个包未在 package.json 中声明依赖,那么在项目中是无法访问的。但在 npm/yarn 当中是做不到的,那你可能会问了,如果 A 依赖 B,B 依赖 C,那么 A 就算没有声明 C 的依赖,由于有依赖提升的存在,C 被装到了 A 的 node_modules 里面,那我在 A 里面用 C,跑起来没有问题呀,我上线了之后,也能正常运行啊。不是挺安全的吗? 88 | 89 | - 第一,你要知道 B 的版本是可能随时变化的,假如之前依赖的是C@1.0.1,现在发了新版,新版本的 B 依赖 C@2.0.1,那么在项目 A 当中 npm/yarn install 之后,装上的是 2.0.1 版本的 C,而 A 当中用的还是 C 当中旧版的 API,可能就直接报错了。 90 | - 第二,如果 B 更新之后,可能不需要 C 了,那么安装依赖的时候,C 都不会装到 node_modules 里面,A 当中引用 C 的代码直接报错。 91 | 92 | 还有一种情况,在 monorepo 项目中,如果 A 依赖 X,B 依赖 X,还有一个 C,它不依赖 X,但它代码里面用到了 X。由于依赖提升的存在,npm/yarn 会把 X 放到根目录的 node_modules 中,这样 C 在本地是能够跑起来的,因为根据 node 的包加载机制,它能够加载到 monorepo 项目根目录下的 node_modules 中的 X。但试想一下,一旦 C 单独发包出去,用户单独安装 C,那么就找不到 X 了,执行到引用 X 的代码时就直接报错了。 93 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/配置/日志.md: -------------------------------------------------------------------------------- 1 | ## 开启日志 2 | 3 | 你只需在连接选项中设置`logging:true`即可启用所有查询和错误的记录: 4 | 5 | ```typescript 6 | { 7 | name: "mysql", 8 | type: "mysql", 9 | host: "localhost", 10 | port: 3306, 11 | username: "test", 12 | password: "test", 13 | database: "test", 14 | ... 15 | logging: true 16 | } 17 | ``` 18 | 19 | ## 日志选项 20 | 21 | 可以在连接选项中启用不同类型的日志记录: 22 | 23 | ```typescript 24 | { 25 | host: "localhost", 26 | ... 27 | logging: ["query", "error"] 28 | } 29 | ``` 30 | 31 | 如果要启用失败查询的日志记录,则只添加`error`: 32 | 33 | ```typescript 34 | { 35 | host: "localhost", 36 | ... 37 | logging: ["error"] 38 | } 39 | ``` 40 | 41 | 还可以使用其他选项: 42 | 43 | - `query` - 记录所有查询。 44 | - `error` - 记录所有失败的查询和错误。 45 | - `schema` - 记录架构构建过程。 46 | - `warn` - 记录内部 orm 警告。 47 | - `info` - 记录内部 orm 信息性消息。 48 | - `log` - 记录内部 orm 日志消息。 49 | 50 | 你可以根据需要指定任意数量的选项如果要启用所有日志记录,只需指定`logging:“all”`: 51 | 52 | ```typescript 53 | { 54 | host: "localhost", 55 | ... 56 | logging: "all" 57 | } 58 | ``` 59 | 60 | ## 记录耗时长的查询 61 | 62 | 如果遇到性能问题,可以通过在连接选项中设置`maxQueryExecutionTime`来记录执行时间过长的查询: 63 | 64 | ```typescript 65 | { 66 | host: "localhost", 67 | ... 68 | maxQueryExecutionTime: 1000 69 | } 70 | ``` 71 | 72 | 此代码将记录所有运行超过`1秒`的查询。 73 | 74 | ## 更改默认记录器 75 | 76 | TypeORM 附带 4 种不同类型的记录器: 77 | 78 | - `advanced-console` - 默认记录器,它将使用颜色和 sql 语法高亮显示所有记录到控制台中的消息(使用[chalk](https://github.com/chalk/chalk))。 79 | - `simple-console` - 简单的控制台记录器,与高级记录器完全相同,但它不使用任何颜色突出显示如果你不喜欢/或者使用彩色日志有问题,可以使用此记录器。 80 | - `file` - 这个记录器将所有日志写入项目根文件夹中的`ormlogs.log`(靠近`package.json`和`ormconfig.json`)。 81 | - `debug` - 此记录器使用[debug package](https://github.com/visionmedia/debug)打开日志记录设置你的 env 变量`DEBUG = typeorm:*`(注意记录选项对此记录器没有影响)。 82 | 83 | 你可以在连接选项中启用其中任何一个: 84 | 85 | ```typescript 86 | { 87 | host: "localhost", 88 | ... 89 | logging: true, 90 | logger: "file" 91 | } 92 | ``` 93 | 94 | ## 使用自定义记录器 95 | 96 | 你可以通过实现`Logger`接口来创建自己的记录器类: 97 | 98 | ```typescript 99 | import { Logger } from "typeorm"; 100 | 101 | export class MyCustomLogger implements Logger { 102 | // 实现logger类的所有方法 103 | } 104 | ``` 105 | 106 | 并在连接选项中指定它: 107 | 108 | ```typescript 109 | import { createConnection } from "typeorm"; 110 | import { MyCustomLogger } from "./logger/MyCustomLogger"; 111 | 112 | createConnection({ 113 | name: "mysql", 114 | type: "mysql", 115 | host: "localhost", 116 | port: 3306, 117 | username: "test", 118 | password: "test", 119 | database: "test", 120 | logger: new MyCustomLogger() 121 | }); 122 | ``` 123 | 124 | 如果在`ormconfig`文件中定义了 然后你可以使用它并以下面的方式覆盖它: 125 | 126 | ```typescript 127 | import { createConnection, getConnectionOptions } from "typeorm"; 128 | import { MyCustomLogger } from "./logger/MyCustomLogger"; 129 | 130 | // getConnectionOptions将从ormconfig文件中读取选项并将其返回到connectionOptions对象中, 131 | // 然后你只需向其附加其他属性 132 | getConnectionOptions().then(connectionOptions => { 133 | return createConnection( 134 | Object.assign(connectionOptions, { 135 | logger: new MyCustomLogger() 136 | }) 137 | ); 138 | }); 139 | ``` 140 | 141 | 记录器方法可接受`QueryRunner`如果要记录其他数据将会很有帮助此外,通过查询运行程序,你可以访问在持久/删除期间传递的其他数据例如: 142 | 143 | ```typescript 144 | // 用户在实体保存期间发送请求 145 | postRepository.save(post, { data: { request: request } }); 146 | 147 | // 在logger中你可以这样访问它: 148 | logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { 149 | const requestUrl = queryRunner && queryRunner.data["request"] ? "(" + queryRunner.data["request"].url + ") " : ""; 150 | console.log(requestUrl + "executing query: " + sql); 151 | } 152 | ``` 153 | -------------------------------------------------------------------------------- /20~Deno/特性与优缺点.md: -------------------------------------------------------------------------------- 1 | # 特性与优缺点 2 | 3 | # 特性 4 | 5 | Deno 含有以下功能亮点: 6 | 7 | - 默认安全。外部代码没有文件系统、网络、环境的访问权限,除非显式开启。 8 | 9 | - 支持开箱即用的 TypeScript 的环境,尽管它仍需要编译为 JavaScript 才能运行,但这是在内部完成的,因此对用户来说 TypeScript 的行为就好像它是原生支持的一样。 10 | 11 | - 只分发一个独立的可执行文件(deno)。 12 | 13 | - 有着内建的工具箱,比如一个依赖信息查看器(deno info)和一个代码格式化工具(deno fmt)。 14 | 15 | - 有一组经过审计的标准模块,保证能在 Deno 上工作。 16 | 17 | - 脚本代码能被打包为一个单独的 JavaScript 文件。 18 | 19 | ## 安全性 20 | 21 | 默认情况下,Deno 是安全的。相比之下,Node.js 拥有对文件系统和网络的完全访问权限。要在没有权限的情况下运行程序,请使用: 22 | 23 | deno run file-needing-to-run-a-subprocess.ts 24 | 25 | 如果代码需要权限设置,则会提醒你。 26 | 27 | error: Uncaught PermissionDenied: access to run a subprocess, run again with the --allow-run flag 28 | 29 | Deno 使用命令行选项来显式许可访问系统的各个部分。最常用的包括:环境访问 30 | 网络访问,文件系统读 / 写访问,运行一个子进程,要查看权限示例的完整列表,请输入 deno run -h。 31 | 32 | 这里的最佳实践是在 read、write 和 net 上使用权限白名单。这样你就可以更清楚地了解 Deno 被允许访问哪些内容。例如,要允许 Deno 在 /etc 目录中只读文件,请使用:deno --allow-read=/etc 33 | 34 | ## 标准库 35 | 36 | Deno 标准库是由 Deno 项目维护,并保证可用于 Deno 的常用模块集合。它涵盖了用户最常用的常见任务代码,并且是基于 Go 编程语言提供的标准库。JavaScript 一直以来困扰用户的一个问题就是缺乏标准库。用户被迫一次又一次地重新发明轮子,开发人员不得不经常在 npm 上搜索第三方模块,以解决本应由平台制造商解决的常见问题。 37 | 38 | React 之类的库可以解决很多复杂问题,这些第三方包很好用。但是对于 UUID 生成等简单的事情来说,我们最好使用官方的标准库。这些小型库可作为大型库的构建块,从而加快了开发速度并减少了开发人员的负担。曾一度流行的库被遗弃,留给用户自己维护或者需要用户找一个替代品的事情时有发生。 39 | 40 | ## 内置 TypeScript 41 | 42 | TypeScript 是添加了显式类型的 JavaScript。任何有效的 JavaScript 代码也是有效的 TypeScript 代码,因这个将你的代码转换为 TypeScript 是非常容易的。只需将扩展名更改为.ts 并开始添加类型即可。 43 | 44 | 要在 Deno 中使用 TypeScript 无需执行任何操作。没有 Deno 时,必须将 TypeScript 编译为 JavaScript 才能运行。Deno 会在内部为你完成这个步骤,让 TypeScript 更容易上手。 45 | 46 | ## 尽可能使用 Web 标准 47 | 48 | 创建一个 Web 标准需要很长时间,但一旦标准被确定下来,我们就不应该忽略它。虽然框架来来去去,但 Web 标准是会长期存在的。花费在学习标准化 API 上的时间永远不会白费,因为没有人敢于破坏 Web;它可能已经使用了数十年,甚至可能在你剩下的职业生涯中一直发光发热。 49 | 50 | fetch 这个 Web API 提供了用于提取资源的接口。浏览器中有一个 JavaScript fetch() 方法。如果你想在 Node.js 中使用这个标准,则需要使用第三方库 Node Fetch。在 Deno 中它是内置的,并且就像浏览器版本一样开箱即用。 51 | 52 | ## ECMAScript 模块 53 | 54 | 相比 Node.js,Deno 的一项主要进步是它使用了官方的 ECMAScript 模块标准,而不是老式的 CommonJS。Node.js 直到 2019 年底才在 13.2.0 版本中启用 ECMAScript 模块,但支持还是不够成熟,并且仍然包含有争议的.mjs 文件扩展名。 55 | 56 | Deno 在模块系统中使用了现代 Web 标准,从而避免了旧时代的影响。模块使用 URL 或文件路径引用,并包含必需的文件扩展名。例如: 57 | 58 | ```ts 59 | import * as log from "https://deno.land/std/log/mod.ts"; 60 | import { outputToConsole } from "./view.ts"; 61 | ``` 62 | 63 | # 缺点 64 | 65 | ## 包管理器 66 | 67 | Deno 包管理器的主要问题是,它在 CI/CD 中很难用。我的意思是,如果你需要快速执行 CI/CD,则必须将所有软件包手动加载到你的应用 Git 中(否则,每次 CI/CD 启动后开发人员和测试人员都需要等待从网络加载所有内容——这只是在浪费开发时间和预算)。如果选择 Git 这个选项,那么你的开发人员需要像上世纪那样手动进行所有升级(或者干脆换成带有 NPM 的 Node.js)。 68 | 69 | ```js 70 | import { connect } from "https://deno.land/x/amqp/mod.ts"; 71 | import * as base64 from "https://denopkg.com/chiefbiiko/base64/mod.ts"; 72 | import { createLogger } from "https://denolib.com/yamboy1/deno-structured-logging/mod.ts"; 73 | ... 74 | ``` 75 | 76 | 另外,我不喜欢 Deno 不在一个文件中指示所有包的做法。在大型企业级应用程序中这是会造成混乱的。试想一下,超过 20 位开发人员无需任何系统化方法即可导入软件包。而且根本没有版本控制(仅在某些 URL 中或手动创建的带有依赖项文件中才有,这不是严格的默认设置)。我认为这是不对的。 77 | 78 | ## 包 79 | 80 | 我希望 Deno 具有所有 Express 功能,这样就不用任何框架了。但 Deno 并没有这样做,而是引入了那个 Oak 框架(“Oak”是它的名字): 81 | 82 | https://deno.land/x/oak 83 | 84 | 为了记录日志,我们需要再导入一个包: 85 | 86 | https://deno.land/x/deno_structured_logging 87 | 88 | 为了其他一些简单的开发功能——我们需要越来越多的包: 89 | 90 | https://deno.land/x/ 91 | 92 | 奇怪的是,Deno 默认("开箱即用")几乎没有集成其中的任何功能,要知道它的使用场景是非常清晰的。至少应该集成最基本的功能,例如路由、日志记录和调试吧。 93 | 94 | ## 安全策略 95 | 96 | Deno 的安全策略仅适用于相对较小的应用程序。大型企业应用程序需要大量权限,因此在我看来,我们必须为每个软件包指定策略。这就是为什么我们需要一个带有包的根文件或生成器(用于单体应用、服务等)的原因所在。使用现在的方法时,如果一个包被感染,并且在应用级别为另一个包提供了权限(所以被感染的程序包将具有该权限),那么整个应用的这个权限都会失效。 97 | 98 | ```yaml 99 | deno run --allow-net .\main.ts 100 | # it allows running the server via the net 101 | # basically, there is one permission for the whole app right now 102 | ``` 103 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/npm/依赖管理.md: -------------------------------------------------------------------------------- 1 | # 依赖管理 2 | 3 | # peerDependencies 4 | 5 | ## 引言 6 | 7 | 想必前端同学对 npm 的 devDependencies 和 dependencies 都比较熟悉,但是对 peerDependencies 可能就有点陌生,尤其是没有写过 npm 包插件的同学,比如之前使用 grunt 自动化工具的相关插件(如 grunt-contrib-jasmine 等)或者目前基于某个框架的 ui 组件库等等,这些都是需要对 peerDependencies 有一定了解的。下面我们就来说说 peerDependencies。 8 | 9 | ## npm2 中 dependencies 与 peerDependencies 区别 10 | 11 | 假设我们当前的项目是 MyProject,项目中有一些依赖,比方其中有一个依赖包**PackageA**,该包的**package.json**文件指定了对**PackageB**的依赖: 12 | 13 | ``` 14 | { 15 | "dependencies": { 16 | "PackageB": "1.0.0" 17 | } 18 | } 19 | ``` 20 | 21 | 如果我们在我们的 MyProject 项目中执行`npm install PackageA`, 我们会发现我们项目的目录结构会是如下形式: 22 | 23 | ``` 24 | MyProject 25 | |- node_modules 26 | |- PackageA 27 | |- node_modules 28 | |- PackageB 29 | ``` 30 | 31 | 那么在我们的项目中,我们能通过下面语句引入"PackageA": 32 | 33 | ``` 34 | var packageA = require('PackageA') 35 | ``` 36 | 37 | 但是,如果你想在项目中直接引用 PackageB: 38 | 39 | ``` 40 | var packageA = require('PackageA') 41 | var packageB = require('PackageB') 42 | ``` 43 | 44 | 这是不行的,即使 PackageB 被安装过;因为 Node 只会在“MyProject/node_modules”目录下查找 PackageB,它不会在进入 PackageA 模块下的 node_modules 下查找。 45 | 46 | 所以,为了解决这个问题,在 MyProject 项目 package.json 中我们必须直接声明对 PackageB 的依赖并安装。 47 | 48 | 但是,有时我们不用在当前项目中声明对 PackageB 的依赖就可以直接引用,尤其是,PackageA 是一个类似于**grunt**的插件,例如 grunt-contrib-jshint。 49 | 50 | 为什么在项目中不用声明就可以直接使用呢?这就不得不说说 peerDependencies 的作用了。 51 | 52 | ## peerDependencies 的引入 53 | 54 | 为了解决这种问题: 55 | 56 | ``` 57 | 如果你安装我,那么你最好也安装X,Y和Z. 58 | ``` 59 | 60 | 于是`peerDependencies`就被引入了。例如上面 PackageA 的**package.json**文件如果是下面这样: 61 | 62 | ``` 63 | { 64 | "peerDependencies": { 65 | "PackageB": "1.0.0" 66 | } 67 | } 68 | ``` 69 | 70 | 那么,它会告诉 npm:如果某个 package 把我列为依赖的话,那么那个 package 也必需应该有对 PackageB 的依赖。 71 | 72 | 也就是说,如果你`npm install PackageA`,你将会得到下面的如下的目录结构: 73 | 74 | ``` 75 | MyProject 76 | |- node_modules 77 | |- PackageA 78 | |- PackageB 79 | ``` 80 | 81 | 你可能注意到: 82 | 83 | > 在 npm2 中,即使当前项目 MyProject 中没有直接依赖 PackageB,该 PackageB 包依然会安装到当前项目的 node_modules 文件夹中。 84 | 85 | 下面的代码现在可以正常工作了,因为两个包在"MyProject/node_modules"中被安装了: 86 | 87 | ``` 88 | var packageA = require('PackageA') 89 | var packageB = require('PackageB') 90 | ``` 91 | 92 | 总结一句话,`peerDependencies`的具体作用: 93 | 94 | > `peerDependencies`的目的是提示宿主环境去安装满足插件 peerDependencies 所指定依赖的包,然后在插件 import 或者 require 所依赖的包的时候,永远都是引用宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。 95 | 96 | 举个例子,就拿目前基于 react 的 ui 组件库**ant-design@3.x**来说,因该 ui 组件库只是提供一套 react 组件库,它要求宿主环境需要安装指定的 react 版本。具体可以看它[package.json](https://github.com/ant-design/ant-design/blob/master/package.json#L37)中的配置: 97 | 98 | ``` 99 | "peerDependencies": { 100 | "react": ">=16.0.0", 101 | "react-dom": ">=16.0.0" 102 | } 103 | ``` 104 | 105 | 它要求宿主环境安装 react@>=16.0.0 和 react-dom@>=16.0.0 的版本,而在每个 antd 组件的定义文件顶部: 106 | 107 | ``` 108 | import * as React from 'react'; 109 | import * as ReactDOM from 'react-dom'; 110 | ``` 111 | 112 | 组件中引入的 react 和 react-dom 包其实都是宿主环境提供的依赖包。 113 | 114 | ## npm2 和 npm3 中 peerDependencies 的区别 115 | 116 | 正如上一节谈论的,在 npm2 中,PackageA 包中`peerDependencies`所指定的依赖会随着`npm install PackageA`一起被强制安装,所以不需要在宿主环境的 package.json 文件中指定对 PackageA 中`peerDependencies`内容的依赖。 117 | 118 | 但是在 npm3 中,`peerDependencies`的表现与 npm2 不同: 119 | 120 | > npm3 中不会再要求 peerDependencies 所指定的依赖包被强制安装,相反 npm3 会在安装结束后检查本次安装是否正确,如果不正确会给用户打印警告提示。 121 | 122 | 就拿上面的例子来说,如果我们 npm install PackageA 安装 PackageA 时,你会得到一个警告提示说: 123 | 124 | ``` 125 | PackageB是一个需要的依赖,但是没有被安装。 126 | ``` 127 | 128 | 这时,你需要手动的在 MyProject 项目的 package.json 文件指定 PackageB 的依赖。 129 | 130 | 另外,在 npm3 的项目中,可能存在一个问题就是你所依赖的一个 package 包更新了它 peerDependencies 的版本,那么你可能也需要在项目的 package.json 文件中手动更新到正确的版本。否则会出现类似下图所示的警告信息: 131 | 132 | ![img](https://s2.ax1x.com/2019/11/13/MGF5kj.png) 133 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/数据操作/事务.md: -------------------------------------------------------------------------------- 1 | ## 创建和使用事务 2 | 3 | 事务是使用`Connection`或`EntityManager`创建的例如: 4 | 5 | ```typescript 6 | import { getConnection } from "typeorm"; 7 | 8 | await getConnection().transaction(transactionalEntityManager => {}); 9 | ``` 10 | 11 | or 12 | 13 | ```typescript 14 | import { getManager } from "typeorm"; 15 | 16 | await getManager().transaction(transactionalEntityManager => {}); 17 | ``` 18 | 19 | 你想要在事务中运行的所有内容都必须在回调中执行: 20 | 21 | ```typescript 22 | import { getManager } from "typeorm"; 23 | 24 | await getManager().transaction(async transactionalEntityManager => { 25 | await transactionalEntityManager.save(users); 26 | await transactionalEntityManager.save(photos); 27 | // ... 28 | }); 29 | ``` 30 | 31 | 当在事务中工作时需要**总是**使用提供的实体管理器实例 - `transactionalEntityManager`如果你使用全局管理器(来自`getManager`或来自连接的 manager 可能会遇到一些问题你也不能使用使用全局管理器或连接的类来执行查询。**必须**使用提供的事务实体管理器执行所有操作。 32 | 33 | ### 指定隔离级别 34 | 35 | 指定事务的隔离级别可以通过将其作为第一个参数提供来完成: 36 | 37 | ```typescript 38 | import { getManager } from "typeorm"; 39 | 40 | await getManager().transaction( 41 | "SERIALIZABLE", 42 | transactionalEntityManager => {} 43 | ); 44 | ``` 45 | 46 | 隔离级别实现与所有数据库不相关。 47 | 48 | 以下数据库驱动程序支持标准隔离级别(`READ UNCOMMITTED`,`READ COMMITTED`,`REPEATABLE READ`,`SERIALIZABLE`): 49 | 50 | - MySQL 51 | - Postgres 52 | - SQL Server 53 | 54 | **SQlite**将事务默认为`SERIALIZABLE`,但如果启用了*shared cache mode*,则事务可以使用`READ UNCOMMITTED`隔离级别。 55 | 56 | **Oracle**仅支持`READ COMMITTED`和`SERIALIZABLE`隔离级别。 57 | 58 | ## 事务装饰器 59 | 60 | 以下装饰器可以帮助你组织事务操作 - `@Transaction`, `@TransactionManager` 和 `@TransactionRepository`。 61 | 62 | `@Transaction`将其所有执行包装到一个数据库事务中,`@TransportManager`提供了一个事务实体管理器,它必须用于在该事务中执行查询: 63 | 64 | ```typescript 65 | @Transaction() 66 | save(@TransactionManager() manager: EntityManager, user: User) { 67 | return manager.save(user); 68 | } 69 | ``` 70 | 71 | 隔离级别: 72 | 73 | ```typescript 74 | @Transaction({ isolation: "SERIALIZABLE" }) 75 | save(@TransactionManager() manager: EntityManager, user: User) { 76 | return manager.save(user); 77 | } 78 | ``` 79 | 80 | 你**必须**使用`@TransportManager`提供的管理器。 81 | 82 | 但是,你也可以使用`@TransactionRepository`注入事务存储库(使用事务实体管理器): 83 | 84 | ```typescript 85 | @Transaction() 86 | save(user: User, @TransactionRepository(User) userRepository: Repository) { 87 | return userRepository.save(user); 88 | } 89 | ``` 90 | 91 | 你可以注入内置的 TypeORM 的存储库,如`Repository`,`TreeRepository`和`MongoRepository`(使用`@TransactionRepository(Entity)entityRepository:Repository`)或自定义存储库(扩展内置 TypeORM 的存储库类并用`@ EntityRepository`装饰的类)使用`@TransactionRepository() customRepository:CustomRepository`。 92 | 93 | ## 使用 `QueryRunner` 创建和控制单个数据库连接的状态 94 | 95 | `QueryRunner`提供单个数据库连接使用查询运行程序组织事务单个事务只能在单个查询运行器上建立你可以手动创建查询运行程序实例,并使用它来手动控制事务状态例如: 96 | 97 | ```typescript 98 | import { getConnection } from "typeorm"; 99 | 100 | // 获取连接并创建新的queryRunner 101 | const connection = getConnection(); 102 | const queryRunner = connection.createQueryRunner(); 103 | 104 | // 使用我们的新queryRunner建立真正的数据库连 105 | await queryRunner.connect(); 106 | 107 | // 现在我们可以在queryRunner上执行任何查询,例如: 108 | await queryRunner.query("SELECT * FROM users"); 109 | 110 | // 我们还可以访问与queryRunner创建的连接一起使用的实体管理器: 111 | const users = await queryRunner.manager.find(User); 112 | 113 | // 开始事务: 114 | await queryRunner.startTransaction(); 115 | 116 | try { 117 | // 对此事务执行一些操作: 118 | await queryRunner.manager.save(user1); 119 | await queryRunner.manager.save(user2); 120 | await queryRunner.manager.save(photos); 121 | 122 | // 提交事务: 123 | await queryRunner.commitTransaction(); 124 | } catch (err) { 125 | // 有错误做出回滚更改 126 | await queryRunner.rollbackTransaction(); 127 | } 128 | ``` 129 | 130 | 在`QueryRunner`中有 3 种控制事务的方法: 131 | 132 | - `startTransaction` - 启动一个新事务。 133 | - `commitTransaction` - 提交所有更改。 134 | - `rollbackTransaction` - 回滚所有更改。 135 | 136 | 了解有关[Query Runner](https://typeorm.io/#/query-runner/)的更多信息。 137 | -------------------------------------------------------------------------------- /03~Web 框架/Egg/架构机制.md: -------------------------------------------------------------------------------- 1 | # Egg.js 源码探秘 2 | 3 | ## 代码加载与依赖注入 4 | 5 | ## 集群模式的启动逻辑 6 | 7 | 首先是 egg-scripts/start.js 中封装启动参数,并且 Fork 新的进程来启动: 8 | 9 | ```js 10 | // 封装参数 11 | this.serverBin = path.join(__dirname, '../start-cluster'); 12 | ... 13 | const eggArgs = [ 14 | ...(execArgv || []), 15 | this.serverBin, 16 | clusterOptions, 17 | `--title=${argv.title}` 18 | ]; 19 | ... 20 | // 启动执行进程 21 | spawn('node', eggArgs, options); 22 | ``` 23 | 24 | 然后 start-cluster 中加载 egg-cluster 的启动代码: 25 | 26 | ```js 27 | // start-cluster 28 | require(options.framework).startCluster(options); 29 | ``` 30 | 31 | egg-cluster/index.js 中暴露了 startCluster 方法,其会构造 Master 对象实例: 32 | 33 | ```js 34 | exports.startCluster = function(options, callback) { 35 | new Master(options).ready(callback); 36 | }; 37 | ``` 38 | 39 | Master 本身继承自 EventEmitter,并且包含了 Manager 与 Messenger 这两个消息组件的实例以及 AgentWorker 与 AppWorker 两类具体的工作进程。messenger.js 则是负责在 Master、Agent 与 App Worker 之间的通信: 40 | 41 | ``` 42 | ┌────────┐ 43 | │ parent │ 44 | /└────────┘\ 45 | / | \ 46 | / ┌────────┐ \ 47 | / │ master │ \ 48 | / └────────┘ \ 49 | / / \ \ 50 | ┌───────┐ ┌───────┐ 51 | │ agent │ ------- │ app │ 52 | └───────┘ └───────┘ 53 | ``` 54 | 55 | 在 Master 的构造函数中,其会使用 detectPort 检测端口是否可用;若可用则调用 forkAgentWorker 创建 AgentWorker: 56 | 57 | ```js 58 | // AgentWorker 创建完毕后创建 AppWorkers 59 | this.once("agent-start", this.forkAppWorkers.bind(this)); 60 | 61 | // 使用 fork 创建新的 AgentWorker 62 | const agentWorker = childprocess.fork(agentWorkerFile, args, opt); 63 | ``` 64 | 65 | 在 forkAppWorkers 中调用 cfork 来创建多个 AppWorker,并且将它们注册到管理中心: 66 | 67 | ```js 68 | cfork({ 69 | exec: appWorkerFile, 70 | args, 71 | silent: false, 72 | count: this.options.workers, 73 | // don't refork in local env 74 | refork: this.isProduction 75 | }); 76 | 77 | cluster.on('fork', worker => { 78 | ... 79 | this.workerManager.setWorker(worker); 80 | ... 81 | }); 82 | 83 | // 子进程开始监听后,发送 app-start 消息 84 | cluster.on('listening', (worker, address) => { 85 | this.messenger.send({ 86 | action: 'app-start', 87 | ... 88 | }); 89 | }); 90 | ``` 91 | 92 | app-start 事件会触发 onAppStart,并且启动所有的 AppWorker: 93 | 94 | ```js 95 | // enable all workers when app started 96 | for (const id in cluster.workers) { 97 | const worker = cluster.workers[id]; 98 | worker.disableRefork = false; 99 | } 100 | ``` 101 | 102 | egg-cluster 允许传入 sticky 属性,来控制是否启动门面服务器;如果传入,则会调用 startMasterSocketServer 方法: 103 | 104 | ```js 105 | // manager.js 106 | startMasterSocketServer(cb) { 107 | // Create the outside facing server listening on our port. 108 | require('net').createServer({ pauseOnConnect: true }, connection => { 109 | if (!connection.remoteAddress) { 110 | connection.close(); 111 | } else { 112 | const worker = this.stickyWorker(connection.remoteAddress); 113 | worker.send('sticky-session:connection', connection); 114 | } 115 | }).listen(this[REALPORT], cb); 116 | } 117 | ``` 118 | 119 | 这里的 stickyWorker 会根据 IP 地址动态分配具体的 Worker 实例,每个 Worker 是完整的 Node.js 应用: 120 | 121 | ```js 122 | // app_worker.js 123 | const Application = require(options.framework).Application; 124 | const app = new Application(options); 125 | ``` 126 | 127 | 应用创建完毕后会调用 startServer 方法来启动本地服务器: 128 | 129 | ```js 130 | // app_worker.js 131 | if (options.https) { 132 | const httpsOptions = Object.assign({}, options.https, { 133 | key: fs.readFileSync(options.https.key), 134 | cert: fs.readFileSync(options.https.cert) 135 | }); 136 | server = require("https").createServer(httpsOptions, app.callback()); 137 | } else { 138 | server = require("http").createServer(app.callback()); 139 | } 140 | ``` 141 | 142 | 如果是 Sticky 模式,则监听 127.0.0.1,否则共享连接: 143 | 144 | ```js 145 | // app_worker.js 146 | if (options.sticky) { 147 | server.listen(0, "127.0.0.1"); 148 | } else { 149 | server.listen(...args); 150 | } 151 | ``` 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Forks][forks-shield]][forks-url] 3 | [![Stargazers][stars-shield]][stars-url] 4 | [![Issues][issues-shield]][issues-url] 5 | [![MIT License][license-shield]][license-url] 6 | 7 | 8 |
9 |

10 | 11 | Logo 12 | 13 | 14 |

深入浅出 Node.js 全栈架构

15 | 16 |

17 |
18 | 在线阅读 >> 19 |
20 |
21 | 速览手册 22 | · 23 | Report Bug 24 | · 25 | 参考资料 26 | 27 |

28 |

29 | 30 | # Introduction 31 | 32 | Node.js 为前端开发者开拓了更广阔的应用空间,前端和后端开发的一种语言和技术,以及出色的性能; 但是没有并发和繁重的 CPU 处理问题,以及高级数据库抽象。 33 | 34 | ## NodeJS 的潜在缺陷探讨 35 | 36 | - 在虚拟机层面 node.js 基于的 v8 VM 看起来很不错,但和 Java 的 VM 一比,差距甚远。在服务器领域,特别是拥有众多 CPU 和大量内存的环境下,Java 的 VM 几乎是你能在地球上找到的最好的 VM。而 v8 既不能充分利用多 CPU 的性能,也不能将内存充分利用。你唯一能做的事情就是开很多个 node.js 实例来缓解,但这进一步带来了更多的问题。 37 | 38 | - 在语言层面 JavaScript 本身的设计让你感觉很灵活,因为它基本上是不对类型进行约束的,只有当运行过程中发生了错误才会提示你,毕竟在浏览器环境内,这算不上什么问题;但在一个团队内进行协作时,你会深刻的明白类型系统如果能在运行前就帮你找出那些低级的类型错误问题,将会节省你多少的时间和金钱。特别是别忘了,系统总是在演进的,一个稍微复杂些的业务系统就拥有几十个乃至上百个类型,而类型修改又往往很频繁,想想这个过程里会发生些什么你就明白了。 39 | 40 | - 在领域应用层面 node.js 在 Web 开发领域特别是其中的前端部分已经达到了惊人的繁荣程度,甚至有不少重要系统的后端部分也基于 node.js 完成。但如果仅凭这些就轻易的认为 node.js 将会一统全栈打败包括 Java 在内的其它语言是很幼稚的。首先,在一个大型的系统架构中,整个系统是拆分成很多很小的业务系统的,这些系统往往通过消息队列(如 RabbitMQ、Kafka 等等)相互连接起来。也许在小型 Web 站点中,你从来没用过这些。但相信我,在但凡稍微大一些的业务系统中,都是这么干的。这些消息队列服务存在的理由就是将各个子系统解耦。这样一来,你可以在前端部分应用 node.js 进行快速开发,在业务处理部分使用 Java 来完成。数据分析系统却可以使用 Python/Scala (例如基于 Spark)实现。大型业务系统的架构者们都是些经验丰富的老手,他们知道每个语言/系统的利弊,也知道世界总在变,今天是 node.js、明天也许就是另一个新秀,因此在整个业务系统中,你要做的根本不是“统一”,反而是“分离”。这样的设计才能够预留出扩展和变更的机会。 41 | 42 | # Nav | 关联导航 43 | 44 | 本书的精排目录导航版请参考 [https://ng-tech.icu/books/Node-Notes](https://ng-tech.icu/books/Node-Notes),更多 Web 开发相关请参阅[《现代 Web 全栈开发与工程架构](https://ngte-web.gitbook.io/i/)》。 45 | 46 | # About | 关于 47 | 48 | 49 | 50 | ## Contributing 51 | 52 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 53 | 54 | 1. Fork the Project 55 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 56 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 57 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 58 | 5. Open a Pull Request 59 | 60 | ## Copyright & More | 延伸阅读 61 | 62 | 笔者所有文章遵循[知识共享 署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh),欢迎转载,尊重版权。您还可以前往 [NGTE Books](https://ng-tech.icu/books-gallery/) 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表: 63 | 64 | [![NGTE Books](https://s2.ax1x.com/2020/01/18/19uXtI.png)](https://ng-tech.icu/books-gallery/) 65 | 66 | 67 | 68 | 69 | [contributors-shield]: https://img.shields.io/github/contributors/wx-chevalier/Node-Notes.svg?style=flat-square 70 | [contributors-url]: https://github.com/wx-chevalier/Node-Notes/graphs/contributors 71 | [forks-shield]: https://img.shields.io/github/forks/wx-chevalier/Node-Notes.svg?style=flat-square 72 | [forks-url]: https://github.com/wx-chevalier/Node-Notes/network/members 73 | [stars-shield]: https://img.shields.io/github/stars/wx-chevalier/Node-Notes.svg?style=flat-square 74 | [stars-url]: https://github.com/wx-chevalier/Node-Notes/stargazers 75 | [issues-shield]: https://img.shields.io/github/issues/wx-chevalier/Node-Notes.svg?style=flat-square 76 | [issues-url]: https://github.com/wx-chevalier/Node-Notes/issues 77 | [license-shield]: https://img.shields.io/github/license/wx-chevalier/Node-Notes.svg?style=flat-square 78 | [license-url]: https://github.com/wx-chevalier/Node-Notes/blob/master/LICENSE.txt 79 | -------------------------------------------------------------------------------- /01~语法基础/Streams/Pipe.md: -------------------------------------------------------------------------------- 1 | # Pipe | 管道 2 | 3 | ```js 4 | const fs = require("fs"); 5 | 6 | const inputFile = fs.createReadStream("REALLY_BIG_FILE.x"); 7 | const outputFile = fs.createWriteStream("REALLY_BIG_FILE_DEST.x"); 8 | 9 | // 当建立管道时,才发生了流的流动 10 | inputFile.pipe(outputFile); 11 | ``` 12 | 13 | 多个管道顺序调用,即是构建了链接(Chaining): 14 | 15 | ```js 16 | const fs = require("fs"); 17 | const zlib = require("zlib"); 18 | fs.createReadStream("input.txt.gz") 19 | .pipe(zlib.createGunzip()) 20 | .pipe(fs.createWriteStream("output.txt")); 21 | ``` 22 | 23 | 管道也常用于 Web 服务器中的文件处理,以 Egg.js 中的应用为例,我们可以从 Context 中获取到文件流并将其传入到可写文件流中: 24 | 25 | > 📎 完整代码参考 [Backend Boilerplate/egg](https://parg.co/A24) 26 | 27 | ```js 28 | const awaitWriteStream = require('await-stream-ready').write; 29 | const sendToWormhole = require('stream-wormhole'); 30 | ... 31 | const stream = await ctx.getFileStream(); 32 | 33 | const filename = 34 | md5(stream.filename) + path.extname(stream.filename).toLocaleLowerCase(); 35 | //文件生成绝对路径 36 | 37 | const target = path.join(this.config.baseDir, 'app/public/uploads', filename); 38 | 39 | //生成一个文件写入文件流 40 | const writeStream = fs.createWriteStream(target); 41 | try { 42 | //异步把文件流写入 43 | await awaitWriteStream(stream.pipe(writeStream)); 44 | } catch (err) { 45 | //如果出现错误,关闭管道 46 | await sendToWormhole(stream); 47 | throw err; 48 | } 49 | ... 50 | ``` 51 | 52 | 参照[分布式系统导论](https://parg.co/Uxo),可知在典型的流处理场景中,我们不可以避免地要处理所谓的背压(Backpressure)问题。无论是 Writable Stream 还是 Readable Stream,实际上都是将数据存储在内部的 Buffer 中,可以通过 `writable.writableBuffer` 或者 `readable.readableBuffer` 来读取。当要处理的数据存储超过了 `highWaterMark` 或者当前写入流处于繁忙状态时,write 函数都会返回 `false`。`pipe` 函数即会自动地帮我们启用背压机制: 53 | 54 | ![image](https://user-images.githubusercontent.com/5803001/45255876-99c94f80-b3c0-11e8-93f2-3ae0474426fa.png) 55 | 56 | 当 Node.js 的流机制监测到 write 函数返回了 `false`,背压系统会自动介入;其会暂停当前 Readable Stream 的数据传递操作,直到消费者准备完毕。 57 | 58 | ``` 59 | +===============+ 60 | | Your_Data | 61 | +=======+=======+ 62 | | 63 | +-------v-----------+ +-------------------+ +=================+ 64 | | Readable Stream | | Writable Stream +---------> .write(chunk) | 65 | +-------+-----------+ +---------^---------+ +=======+=========+ 66 | | | | 67 | | +======================+ | +------------------v---------+ 68 | +-----> .pipe(destination) >---+ | Is this chunk too big? | 69 | +==^=======^========^==+ | Is the queue busy? | 70 | ^ ^ ^ +----------+-------------+---+ 71 | | | | | | 72 | | | | > if (!chunk) | | 73 | ^ | | emit .end(); | | 74 | ^ ^ | > else | | 75 | | ^ | emit .write(); +---v---+ +---v---+ 76 | | | ^----^-----------------< No | | Yes | 77 | ^ | +-------+ +---v---+ 78 | ^ | | 79 | | ^ emit .pause(); +=================+ | 80 | | ^---^---------------------+ return false; <-----+---+ 81 | | +=================+ | 82 | | | 83 | ^ when queue is empty +============+ | 84 | ^---^-----------------^---< Buffering | | 85 | | |============| | 86 | +> emit .drain(); | | | 87 | +> emit .resume(); +------------+ | 88 | | | | 89 | +------------+ add chunk to queue | 90 | | <--^-------------------< 91 | +============+ 92 | ``` 93 | -------------------------------------------------------------------------------- /01~语法基础/包管理器/Yarn/README.md: -------------------------------------------------------------------------------- 1 | # [Yarn](https://github.com/yarnpkg/yarn) 2 | 3 | Yarn 是一个新的快速安全可信赖的可以替代 NPM 的依赖管理工具,笔者在自己过去无论是本机还是 CI 中经常会碰到 NPM 安装依赖失败的情形,防不胜防啊。Yarn 正式发布没几天已经迅速达到了数万赞,就可以知道大家苦 NPM 久已。笔者最早是在 [Facebook 的这篇吐槽文](https://code.facebook.com/posts/1840075619545360/yarn-a-new-package-manager-for-javascript/)中了解到 Yarn。Facebook 使用 NPM 与 npm.js 存放管理大量的依赖项目,不过随着依赖项数目与复杂度的增加,NPM 本身在一致性、安全性以及性能方面的弊端逐渐暴露。因此忍无可忍的 Facebook 重构了 Yarn 这个新型的可替换 NPM 客户端的依赖管理工具。Yarn 仍然基于 NPM Registry 作为主要的仓库,不过其提供了更快的安装速度与不同环境下的一致性保证。 4 | 5 | ## Features 6 | 7 | - **Consistency:** Yarn 允许使用某个 lockfile 来保证团队中的所有人使用相同版本的 npm 依赖包,这一点会大大减少因为某个人系统本身问题而导致的 Bug。 8 | - **Versatile Archives:** Yarn 还允许用户将 npm 包以*tar.gz*形式打包上传到版本控制系统中,这一点能够利用 NPM 包本身已经对不同版本的 Node 或者操作系统做了容错这一特性。 9 | - **Offline:** Yarn 允许离线安装某些依赖,这点对于 CI 系统特别适用。CI 系统就不需要保证有稳定的网络连接,特别是在有墙的地方。 10 | - **Speed:** Yarn 采用了新的算法来保证速度,[比 NPM 快到 2~7 倍](https://yarnpkg.com/en/compare),同时也允许使用离线包的方式本地安装依赖。 11 | 12 | # Quick Start 13 | 14 | 直接使用 `npm i yarn -g` 全局安装即可,这是笔者本机的运行结果图,速度与稳定性确实都快了不少: 15 | 16 | ## Cheat 17 | 18 | | NPM | YARN | 说明 | 19 | | -------------------------- | ------------------------- | ---------------------------------------- | 20 | | npm init | yarn init | 初始化某个项目 | 21 | | npm install/link | yarn install/link | 默认的安装依赖操作 | 22 | | npm install taco —save | yarn add taco | 安装某个依赖,并且默认保存到 package. | 23 | | npm uninstall taco —save | yarn remove taco | 移除某个依赖项目 | 24 | | npm install taco —save-dev | yarn add taco —dev | 安装某个开发时依赖项目 | 25 | | npm update taco —save | yarn upgrade taco | 更新某个依赖项目 | 26 | | npm install taco --global | yarn global add taco | 安装某个全局依赖项目 | 27 | | npm publish/login/logout | yarn publish/login/logout | 发布/登录/登出,一系列 NPM Registry 操作 | 28 | | npm run/test | yarn run/test | 运行某个命令 | 29 | 30 | # Yarn Workspaces 31 | 32 | 工作区是设置你的软件包体系结构的一种新方式,默认情况下从 Yarn 1.0 开始使用。它允许你可以使用这种方式安装多个软件包,就是只需要运行一次 `yarn install` 便可将所有依赖包全部安装。 33 | 34 | - 你的依赖包可以链接在一起,这意味着你的工作区可以相互依赖,同时始终使用最新的可用代码。这也是一个比 `yarn link` 更好的机制,因为它只影响你工作区的依赖树,而不会影响整个系统。 35 | 36 | - 所有的项目依赖将被安装在一起,这样可以让 Yarn 来更好地优化它们。 37 | 38 | - Yarn 将使用一个单一的 lock 文件,而不是每个项目多有一个,这意味着更少的冲突和更容易进行代码检查。 39 | 40 | Workspaces 的使用方式也非常简单,在 package.json 文件中添加以下内容,从现在开始,我们将此目录称为 “工作区根目录”: 41 | 42 | ```json 43 | { 44 | "private": true, 45 | "workspaces": ["workspace-a", "workspace-b"] 46 | } 47 | ``` 48 | 49 | 请注意,`private: true` 是必需的!工作区本身不应当被发布出去,所以我们添加了这个安全措施以确保它不会被意外暴露。创建这个文件后,再创建两个名为 `workspace-a` 和 `workspace-b` 的子文件夹。在每个文件夹里面,创建一个具有以下内容的 `package. json` 文件: 50 | 51 | - workspace-a/package.json: 52 | 53 | ```json 54 | { 55 | "name": "workspace-a", 56 | "version": "1.0.0", 57 | 58 | "dependencies": { 59 | "cross-env": "5.0.5" 60 | } 61 | } 62 | ``` 63 | 64 | - workspace-b/package.json: 65 | 66 | ```json 67 | { 68 | "name": "workspace-b", 69 | "version": "1.0.0", 70 | 71 | "dependencies": { 72 | "cross-env": "5.0.5", 73 | "workspace-a": "1.0.0" 74 | } 75 | } 76 | ``` 77 | 78 | 最后,在某个地方运行 `yarn install`,当然最好是在工作区根目录里面。如果一切正常,你现在应该有一个类似这样的文件层次结构: 79 | 80 | ``` 81 | /package.json 82 | /yarn.lock 83 | 84 | /node_modules 85 | /node_modules/cross-env 86 | /node_modules/workspace-a -> /workspace-a 87 | 88 | /workspace-a/package.json 89 | /workspace-b/package.json 90 | ``` 91 | 92 | Yarn 的工作区是诸如 Lerna 这样的工具可以(并且正在)利用的底层机制。它们将永远不会试图提供像 Lerna 那么高级的功能,但通过实现该解决方案的核心逻辑和 Yarn 内部的连接步骤,我们希望能够提供新的用法并提高性能。 93 | 94 | ```sh 95 | # 为某个子模块添加本地依赖 96 | $ yarn workspace x add y@^1.0.0 97 | 98 | # 在所有子项目下运行 Build 命令 99 | $ yarn workspaces run build 100 | ``` 101 | 102 | # Links 103 | 104 | - [yarn-a-new-package-manager-for-javascript](https://code.facebook.com/posts/1840075619545360/yarn-a-new-package-manager-for-javascript/) 105 | 106 | - [yarn-a-new-program-for-installing-javascript-dependencies](https://blog.getexponent.com/yarn-a-new-program-for-installing-javascript-dependencies-44961956e728#.qf8fmeg4g) 107 | 108 | - [npm-vs-yarn-cheat-sheet](https://shift.infinite.red/npm-vs-yarn-cheat-sheet-8755b092e5cc#.dcd5qeolm) 109 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/配置/快速开始.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## DataMapper 4 | 5 | 使用 Data Mapper 方法,你可以在名为"repositories"的单独类中定义所有查询方法,并使用存储库保存、删除和加载对象在数据映射器中,你的实体非常笨,它们只是定义了相应的属性,并且可能有一些很笨的方法。 6 | 7 | 简单来说,数据映射器是一种在存储库而不是模型中访问数据库的方法你可以在[Wikipedia](https://en.wikipedia.org/wiki/Data_mapper_pattern)上查看更多关于 data mapper 的信息。 8 | 9 | 例如: 10 | 11 | ```typescript 12 | import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 13 | 14 | @Entity() 15 | export class User { 16 | @PrimaryGeneratedColumn() 17 | id: number; 18 | 19 | @Column() 20 | firstName: string; 21 | 22 | @Column() 23 | lastName: string; 24 | 25 | @Column() 26 | isActive: boolean; 27 | } 28 | ``` 29 | 30 | 以下示例展示如何使用此类实体: 31 | 32 | ```typescript 33 | const userRepository = connection.getRepository(User); 34 | 35 | // 示例如何保存DM实体 36 | const user = new User(); 37 | user.firstName = "Timber"; 38 | user.lastName = "Saw"; 39 | user.isActive = true; 40 | await userRepository.save(user); 41 | 42 | // 示例如何删除DM实体 43 | await userRepository.remove(user); 44 | 45 | // 示例如何加载DM实体 46 | const users = await userRepository.find({ skip: 2, take: 5 }); 47 | const newUsers = await userRepository.find({ isActive: true }); 48 | const timber = await userRepository.findOne({ 49 | firstName: "Timber", 50 | lastName: "Saw" 51 | }); 52 | ``` 53 | 54 | 现在假设我们要创建一个按 first name 和 last name 返回用户的函数我们可以在"custom repository"中创建这样的功能。 55 | 56 | ```typescript 57 | import { EntityRepository, Repository } from "typeorm"; 58 | import { User } from "../entity/User"; 59 | 60 | @EntityRepository() 61 | export class UserRepository extends Repository { 62 | findByName(firstName: string, lastName: string) { 63 | return this.createQueryBuilder("user") 64 | .where("user.firstName = :firstName", { firstName }) 65 | .andWhere("user.lastName = :lastName", { lastName }) 66 | .getMany(); 67 | } 68 | } 69 | ``` 70 | 71 | 并以这种方式使用它: 72 | 73 | ```typescript 74 | const userRepository = connection.getCustomRepository(UserRepository); 75 | const timber = await userRepository.findByName("Timber", "Saw"); 76 | ``` 77 | 78 | 了解有关[custom repositories](https://typeorm.io/#/working-with-entity-manager/custom-repositories)的更多信息。 79 | 80 | ## Active Record 81 | 82 | 使用 Active Record 方法,你可以在模型本身内定义所有查询方法,并使用模型方法保存、删除和加载对象。 83 | 84 | 简单来说,Active Record 模式是一种在模型中访问数据库的方法你可以在[Wikipedia](https://en.wikipedia.org/wiki/Active_record_pattern)上查看有关 Active Record 模式的更多信息。 85 | 86 | 例如: 87 | 88 | ```typescript 89 | import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 90 | 91 | @Entity() 92 | export class User extends BaseEntity { 93 | @PrimaryGeneratedColumn() 94 | id: number; 95 | 96 | @Column() 97 | firstName: string; 98 | 99 | @Column() 100 | lastName: string; 101 | 102 | @Column() 103 | isActive: boolean; 104 | } 105 | ``` 106 | 107 | 所有 active-record 实体都必须扩展`BaseEntity`类,它提供了与实体一起使用的方法。 108 | 109 | 以下示例展示如何使用此类实体: 110 | 111 | ```typescript 112 | // 示例如何保存AR实体 113 | const user = new User(); 114 | user.firstName = "Timber"; 115 | user.lastName = "Saw"; 116 | user.isActive = true; 117 | await user.save(); 118 | 119 | // 示例如何删除AR实体 120 | await user.remove(); 121 | 122 | // 示例如何加载AR实体 123 | const users = await User.find({ skip: 2, take: 5 }); 124 | const newUsers = await User.find({ isActive: true }); 125 | const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" }); 126 | ``` 127 | 128 | `BaseEntity`具有标准`Repository`的大部分方法大多数情况下,你不需要将`Repository`或`EntityManager`与 active record 实体一起使用。 129 | 130 | 现在假设我们要创建一个按 first name 和 last name 返回用户的函数我们可以在`User`类中创建静态方法等函数: 131 | 132 | ```typescript 133 | import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm"; 134 | 135 | @Entity() 136 | export class User extends BaseEntity { 137 | @PrimaryGeneratedColumn() 138 | id: number; 139 | 140 | @Column() 141 | firstName: string; 142 | 143 | @Column() 144 | lastName: string; 145 | 146 | @Column() 147 | isActive: boolean; 148 | 149 | static findByName(firstName: string, lastName: string) { 150 | return this.createQueryBuilder("user") 151 | .where("user.firstName = :firstName", { firstName }) 152 | .andWhere("user.lastName = :lastName", { lastName }) 153 | .getMany(); 154 | } 155 | } 156 | ``` 157 | 158 | 并像使用其他方法一样使用它: 159 | 160 | ```typescript 161 | const timber = await User.findByName("Timber", "Saw"); 162 | ``` 163 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/配置/缓存.md: -------------------------------------------------------------------------------- 1 | # 缓存查询 2 | 3 | 你可以缓存`getMany`,`getOne`,`getRawMany`,`getRawOne`和`getCount`这些`QueryBuilder`方法的查询结果。 4 | 5 | 还可以缓存`find`,`findAndCount`,`findByIds`和`count`这些`Repository`方法查询的结果。 6 | 7 | 要启用缓存,需要在连接选项中明确启用它: 8 | 9 | ```typescript 10 | { 11 | type: "mysql", 12 | host: "localhost", 13 | username: "test", 14 | ... 15 | cache: true 16 | } 17 | ``` 18 | 19 | 首次启用缓存时,你必须同步数据库架构(使用 CLI,migrations 或`synchronize`连接选项)。 20 | 21 | 然后在`QueryBuilder`中,你可以为任何查询启用查询缓存: 22 | 23 | ```typescript 24 | const users = await connection 25 | .createQueryBuilder(User, "user") 26 | .where("user.isAdmin = :isAdmin", { isAdmin: true }) 27 | .cache(true) 28 | .getMany(); 29 | ``` 30 | 31 | 等同于`Repository`查询: 32 | 33 | ```typescript 34 | const users = await connection.getRepository(User).find({ 35 | where: { isAdmin: true }, 36 | cache: true 37 | }); 38 | ``` 39 | 40 | 这将执行查询以获取所有 admin users 并缓存结果下次执行相同的代码时,它将从缓存中获取所有 admin users 默认缓存生存期为`1000 ms`,例如 1 秒这意味着在调用查询构建器代码后 1 秒内缓存将无效实际上,这也意味着如果用户在 3 秒内打开用户页面 150 次,则在此期间只会执行三次查询在 1 秒缓存窗口期间插入的任何 users 都不会返回到 user。 41 | 42 | 你可以通过`QueryBuilder`手动更改缓存时间: 43 | 44 | ```typescript 45 | const users = await connection 46 | .createQueryBuilder(User, "user") 47 | .where("user.isAdmin = :isAdmin", { isAdmin: true }) 48 | .cache(60000) // 1 分钟 49 | .getMany(); 50 | ``` 51 | 52 | 或者通过 `Repository`: 53 | 54 | ```typescript 55 | const users = await connection.getRepository(User).find({ 56 | where: { isAdmin: true }, 57 | cache: 60000 58 | }); 59 | ``` 60 | 61 | 或者通过全局连接选项: 62 | 63 | ```typescript 64 | { 65 | type: "mysql", 66 | host: "localhost", 67 | username: "test", 68 | ... 69 | cache: { 70 | duration: 30000 // 30 seconds 71 | } 72 | } 73 | ``` 74 | 75 | 此外,你可以通过`QueryBuilder`设置"cache id": 76 | 77 | ```typescript 78 | const users = await connection 79 | .createQueryBuilder(User, "user") 80 | .where("user.isAdmin = :isAdmin", { isAdmin: true }) 81 | .cache("users_admins", 25000) 82 | .getMany(); 83 | ``` 84 | 85 | 或者通过 `Repository`: 86 | 87 | ```typescript 88 | const users = await connection.getRepository(User).find({ 89 | where: { isAdmin: true }, 90 | cache: { 91 | id: "users_admins", 92 | milisseconds: 25000 93 | } 94 | }); 95 | ``` 96 | 97 | 这使你可以精确控制缓存,例如,在插入新用户时清除缓存的结果: 98 | 99 | ```typescript 100 | await connection.queryResultCache.remove(["users_admins"]); 101 | ``` 102 | 103 | 默认情况下,TypeORM 使用一个名为`query-result-cache`的单独表,并在那里存储所有查询和结果表名是可配置的,因此您可以通过在 tableName 属性中给出值来更改它 例如: 104 | 105 | ```typescript 106 | { 107 | type: "mysql", 108 | host: "localhost", 109 | username: "test", 110 | ... 111 | cache: { 112 | type: "database", 113 | tableName: "configurable-table-query-result-cache" 114 | } 115 | } 116 | ``` 117 | 118 | 如果在单个数据库表中存储缓存对你无效,则可以将缓存类型更改为"redis"或者"ioredis",而 TypeORM 将以 redis 形式存储所有缓存的记录例如: 119 | 120 | ```typescript 121 | { 122 | type: "mysql", 123 | host: "localhost", 124 | username: "test", 125 | ... 126 | cache: { 127 | type: "redis", 128 | options: { 129 | host: "localhost", 130 | port: 6379 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | "options" 可以是[node_redis specific options](https://github.com/NodeRedis/node_redis#options-object-properties) 或者 [ioredis specific options](https://typeorm.io/#/https://github.com/luin/ioredis/blob/master/API/new-redisport-host-options),具体取决于你使用的类型。 137 | 138 | 如果你想使用 IORedis 的集群功能连接到 redis-cluster,则可以通过方式下操作来执行此操作: 139 | 140 | ```json 141 | { 142 | "type": "mysql", 143 | "host": "localhost", 144 | "username": "test", 145 | "cache": { 146 | "type": "ioredis/cluster", 147 | "options": { 148 | "startupNodes": [ 149 | { "host": "localhost", "port": 7000 }, 150 | { "host": "localhost", "port": 7001 }, 151 | { "host": "localhost", "port": 7002 } 152 | ], 153 | "options": { 154 | "scaleReads": "all", 155 | "clusterRetryStrategy": function(times) { 156 | return null; 157 | }, 158 | "redisOptions": { "maxRetriesPerRequest": 1 } 159 | } 160 | } 161 | } 162 | } 163 | ``` 164 | 165 | 请注意,你仍然可以使用选项作为 IORedis 的集群构造函数的第一个参数。 166 | 167 | ```json 168 | { ...cache: { type: "ioredis/cluster", options: [ { host: 'localhost', port: 7000, }, { host: 'localhost', port: 7001, }, { host: 'localhost', port: 7002, } ] }, ... } 169 | ``` 170 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/工程实践/安全与日志.md: -------------------------------------------------------------------------------- 1 | # 安全与日志 2 | 3 | # 系统安全 4 | 5 | ## Helmet 6 | 7 | Helmet 可以通过适当设置 HTTP 标头来帮助保护您的应用程序免受某些知名的网络漏洞的侵害。通常,Helmet 只是 12 个较小的中间件功能的集合,这些功能设置了与安全性相关的 HTTP 响应头。首先,安装所需的软件包: 8 | 9 | ```sh 10 | $ npm i --save helmet 11 | 12 | import * as helmet from 'helmet'; 13 | // somewhere in your initialization file 14 | app.use(helmet()); 15 | ``` 16 | 17 | ## CORS 18 | 19 | 跨域资源共享(CORS)是一种允许从另一个域请求资源的机制。在底层,Nest 利用了 cors 软件包,该软件包提供了许多选项,您可以根据自己的需求进行自定义。为了启用 CORS,您必须调用 enableCors()方法。 20 | 21 | ```ts 22 | const app = await NestFactory.create(ApplicationModule); 23 | app.enableCors(); 24 | await app.listen(3000); 25 | 26 | const app = await NestFactory.create(ApplicationModule, { cors: true }); 27 | await app.listen(3000); 28 | ``` 29 | 30 | ## CSRF 31 | 32 | 跨站点请求伪造(称为 CSRF 或 XSRF)是一种网站的恶意利用,其中从 Web 应用程序信任的用户发送未经授权的命令。为了减轻这种攻击,您可以使用 csurf 软件包。首先,安装所需的软件包: 33 | 34 | ```sh 35 | $ npm i --save csurf 36 | 37 | import * as csurf from 'csurf'; 38 | // somewhere in your initialization file 39 | app.use(csurf()); 40 | 41 | ``` 42 | 43 | ## Rate limiting 44 | 45 | 为了保护您的应用程序免受暴力攻击,您必须实施某种速率限制。幸运的是,NPM 上已经有很多中间件可用。其中之一是 express-rate-limit。 46 | 47 | ```ts 48 | import * as rateLimit from "express-rate-limit"; 49 | // somewhere in your initialization file 50 | app.use( 51 | rateLimit({ 52 | windowMs: 15 * 60 * 1000, // 15 minutes 53 | max: 100 // limit each IP to 100 requests per windowMs 54 | }) 55 | ); 56 | ``` 57 | 58 | # 日志 59 | 60 | Nest 随附了一个内置的基于文本的记录器,该记录器可在应用程序引导过程中以及其他几种情况下使用,例如显示捕获的异常(即系统日志记录)。通过 `@nestjs/common` 包中的 Logger 类提供此功能。您可以完全控制日志记录系统的行为,包括以下任何一项: 61 | 62 | - 完全禁用日志记录 63 | - 详细说明日志级别(例如,显示错误,警告,调试信息等) 64 | - 完全覆盖默认记录器 65 | - 通过扩展默认自定义记录器 66 | - 利用依赖注入简化应用程序的编写和测试 67 | 68 | 您还可以使用内置记录器,或创建自己的自定义实现,以记录自己的应用程序级事件和消息。 69 | 70 | ## 内置 Logger 71 | 72 | 要禁用日志记录,请在(可选)作为第二个参数传递给 NestFactory.create()方法的 Nest 应用程序选项对象中将 logger 属性设置为 false,也可以自定义日志级别。 73 | 74 | ```ts 75 | const app = await NestFactory.create(ApplicationModule, { 76 | logger: false 77 | }); 78 | 79 | const app = await NestFactory.create(ApplicationModule, { 80 | logger: ["error", "warn"] 81 | }); 82 | await app.listen(3000); 83 | ``` 84 | 85 | 我们也可以自定义日志类: 86 | 87 | ```ts 88 | // 使用 console 89 | const app = await NestFactory.create(ApplicationModule, { 90 | logger: console 91 | }); 92 | 93 | // 自定义日志类 94 | import { LoggerService } from "@nestjs/common"; 95 | 96 | export class MyLogger implements LoggerService { 97 | log(message: string) { 98 | /* your implementation */ 99 | } 100 | error(message: string, trace: string) { 101 | /* your implementation */ 102 | } 103 | warn(message: string) { 104 | /* your implementation */ 105 | } 106 | debug(message: string) { 107 | /* your implementation */ 108 | } 109 | verbose(message: string) { 110 | /* your implementation */ 111 | } 112 | } 113 | 114 | const app = await NestFactory.create(ApplicationModule, { 115 | logger: new MyLogger() 116 | }); 117 | ``` 118 | 119 | ## Winston 120 | 121 | 首先安装依赖: 122 | 123 | ```ts 124 | $ npm install --save nest-winston winston 125 | ``` 126 | 127 | 将 WinstonModule 导入到根 AppModule 中,并使用 forRoot()方法对其进行配置。此方法接受与 winston 包中的 createLogger()函数相同的选项对象: 128 | 129 | ```ts 130 | import { Module } from "@nestjs/common"; 131 | import { WinstonModule } from "nest-winston"; 132 | import * as winston from "winston"; 133 | 134 | @Module({ 135 | imports: [ 136 | WinstonModule.forRoot({ 137 | // options 138 | }) 139 | ] 140 | }) 141 | export class AppModule {} 142 | ``` 143 | 144 | 之后,可以使用 winston 注入令牌将 winston 实例注入整个项目: 145 | 146 | ```ts 147 | import { Controller, Inject } from "@nestjs/common"; 148 | import { Logger } from "winston"; 149 | 150 | @Controller("cats") 151 | export class CatsController { 152 | constructor(@Inject("winston") private readonly logger: Logger) {} 153 | } 154 | ``` 155 | 156 | 请注意,WinstonModule 是全局模块,将在您所有的功能模块中使用。 157 | 158 | ## 异步配置 159 | 160 | 也许您需要异步传递模块选项,例如在需要配置服务时。在这种情况下,请使用 forRootAsync()方法,并从 useFactory 方法返回一个 options 对象: 161 | 162 | ```ts 163 | import { Module } from "@nestjs/common"; 164 | import { WinstonModule } from "nest-winston"; 165 | import * as winston from "winston"; 166 | 167 | @Module({ 168 | imports: [ 169 | WinstonModule.forRootAsync({ 170 | useFactory: () => ({ 171 | // options 172 | }), 173 | inject: [] 174 | }) 175 | ] 176 | }) 177 | export class AppModule {} 178 | ``` 179 | 180 | 工厂可能是异步的,可以使用 inject 选项注入依赖项,并可以使用 imports 选项导入其他模块。另外,您可以使用 useClass 语法: 181 | 182 | ```ts 183 | WinstonModule.forRootAsync({ 184 | useClass: WinstonConfigService 185 | }); 186 | ``` 187 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/GraphQL/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL 2 | 3 | > GraphQL is Facebook’s new query language for fetching application data in a uniform way. 4 | 5 | GraphQL 是由 Facebook 开源的查询语言标准,其并非具体的后端编程框架,而是一个包含了数据格式、数据关联、查询方式定义与实现等等一揽子东西的数据抽象层。GraphQL 也并不是一个具体的后端编程框架,如果将 REST 看做适合于简单逻辑的查询标准,那么 GraphQL 可以做一个独立的抽象层,通过对于多个 REST 风格的简单的接口的排列组合提供更多复杂多变的查询方式。与 REST 相比,GraphQL 定义了更严格、可扩展、可维护的数据查询方式。 6 | 7 | ![default](https://user-images.githubusercontent.com/5803001/39741543-ef8d4c50-52cc-11e8-9d16-c3f71329290a.jpg) 8 | 9 | GraphQL 解决了现代分布式架构(如微服务)中的一些共性问题:当开发人员把系统分解成很多小块时,他们通常还要把信息重新聚合起来才能解决业务需求。它并不能消融业务内在的复杂度,而是通过引入灵活的数据抽象层,尽量解耦前后端之间的直接关联或者阻塞;在满足日益增长不断变化的 Web/Mobile 端复杂的数据需求的同时,尽可能避免服务端内部逻辑复杂度的无序增加。GraphQL 能够用于实践 [BFF](https://www.thoughtworks.com/radar/techniques/bff-backend-for-frontends) 理念,其允许将部分数据组装/聚合地逻辑交于前端完成,即给予了前端灵活变更、快速迭代的空间,也能保证后端的相对中立性,不会疲于应付不同端或者不同界面设计的差异化数据格式要求。 10 | 11 | 但是就像所有强大的抽象一样,它提供的是一种折衷方案,团队要认真考虑,以避免长线上的负面影响。比如,我们已经看到有团队通过聚合工具暴露了过多的底层实现细节,导致架构出现了不必要的脆弱性。当团队试图借助聚合工具来创建规范化的、通用的、中心化的数据模型时,就会把短线上的便利变成长线上的麻烦。我们鼓励团队使用 GraphQL 及其迅速成长的周边工具,但是,要小心别过度追求技术通用性,不要试图用一项技术解决很多问题。 12 | 13 | # GraphQL 与 REST 14 | 15 | 经典的 REST 架构模式立足资源,规范了基础的增删改查操作,却未能很好地处理资源之间的关联,及其衍生的一系列接口命名、代码分层等问题。接口名是对于某个逻辑的抽象描述,其往往会关联到某个特定的服务以及特定的多表查询语句,这就导致了接口、服务、SQL 语句与某个逻辑的强耦合性,而无法动态地应对业务逻辑的快速变化。笔者在早年间提出的 [RARF](https://parg.co/AvR) 架构模式中也探讨过,将请求再资源间响应式地流动与转换,单个资源仅需要关心与其他邻接资源的转换而不需要耦合于某个接口的返回,这也是典型的图模式。 16 | 17 | GraphQL 与之前 Netflix 出品的 Falcor,都是致力于解决相同的问题:如何有效处理日益增长不断变化的 Web/Mobile 端复杂的数据需求。与 REST 相比,GraphQL 为我们提供了声明式(Declarative)、分层可组合的(Hiearchial)、强类型控制(Static Type)的接口声明与交互方式;即保证了单一的查询端点,也提供了更严格、可扩展、可维护的数据查询方式(详见下文-数据模型层)。单一的查询端点能够让开发者免于考虑大量复杂的接口命名,直接使用图查询语言,也能更好地描述资源之间的关系;同时像 GraphiQL 这样的工具也能够帮我们快速生成交互式地接口文档。 18 | 19 | GraphQL 本质上是面向消费者的,客户端驱动的开发模式;它允许请求方(即客户端)而非响应方(即服务器端)决定查询的结果格式,从而返回可预测(Predictable)的结果类型,省去了客户端很多的异常情况处理与向后兼容的操作(Backwards Compatible),提升了产品整体的健壮性。这样确实使得整个请求需要很多额外的数据参数与编码工作,但是它就将消费者与服务端解耦,并且强迫服务端遵守 Postel 法则(对自己严格,对他人宽容)。 20 | 21 | 不过 GraphQL 并非银弹,其并未缓解业务逻辑本身的复杂度,反而图查询方式在弱化各模块间的耦合的同时带来多次查询的性能损耗,在代码规范、模块划分不当的情况下可能导致渐进式地微服务切分割离也变得麻烦。于前端,直接用 Apollo Graphql React 这样的框架替代原有的状态管理,将组件直接绑定于接口,就是在破坏前端应用的自治性,与 SOLID 背道而驰,将独立的前端应用强耦合于后端;单一的端点在网络调试时也是不甚方便。Graphql 作为前端编写,维护的数据聚合层是非常好的选择,但是小型项目也可以在前端完成自聚合;将服务端的计算查询压力,传导给分布式的,性能日渐强大的客户端,也不失一个选择。REST 与 GraphQL 并非取舍关系,而是渐进融合,REST 项目在自身迭代衍化的过程中也可以不断地借鉴 GraphQL 中的一些思想或者理念,来解决譬如模型分层与界定等问题。 22 | 23 | 还是需要强调一点,引入 GraphQL 并不意味着要像之前从 Struts 迁移到 SpringBoot 一样需要去修改你的真实的后端代码,因此 GraphQL 可以看做一个业务逻辑层灵活有效地辅助工具。这一点也是 GraphQL 与原来的 REST API 最大的差别,举例而言: 24 | 25 | ```gql 26 | { 27 | latestPost { 28 | _id 29 | title 30 | content 31 | author { 32 | name 33 | } 34 | comments { 35 | content 36 | author { 37 | name 38 | } 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | 这是一个很典型的 GraphQL 查询,在查询中指明了需要返回某个 Blog 的评论与作者信息,一个典型的返回结果譬如: 45 | 46 | ```json 47 | { 48 | "data": { 49 | "latestPost": { 50 | "_id": "03390abb5570ce03ae524397d215713b", 51 | "title": "New Feature: Tracking Error Status with Kadira", 52 | "content": "Here is a common feedback we received from our users ...", 53 | "author": { 54 | "name": "Pahan Sarathchandra" 55 | }, 56 | "comments": [ 57 | { 58 | "content": "This is a very good blog post", 59 | "author": { 60 | "name": "Arunoda Susiripala" 61 | } 62 | }, 63 | { 64 | "content": "Keep up the good work", 65 | "author": { 66 | "name": "Kasun Indi" 67 | } 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | 而如果采用 REST API 方式,要么需要前端查询多次,要么需要去添加一个新的接口,专门针对前端这种较为特殊的请求进行响应,而这样又不可避免地导致后端代码的冗余,毕竟很有可能这个特殊的请求与返回哪天就被废了。 76 | 77 | ```json 78 | { 79 | user(id:1) { 80 | name 81 | title 82 | avatarUrl 83 | timezone 84 | locale 85 | lastSeenOnline 86 | email 87 | phone 88 | location 89 | 90 | accountOwner { 91 | name 92 | avatarUrl 93 | } 94 | 95 | tags { 96 | edges { 97 | node { 98 | label 99 | color 100 | } 101 | } 102 | } 103 | 104 | accountUsers(first:10) { 105 | edges { 106 | node { 107 | id 108 | avatarUrl 109 | } 110 | } 111 | pageInfo { 112 | totalAccountUsers 113 | } 114 | } 115 | 116 | recentConversations(first:10) { 117 | edges { 118 | node { 119 | lastMessage 120 | updatedAt 121 | status 122 | } 123 | pageInfo { 124 | totalConversationCount 125 | } 126 | } 127 | } 128 | } 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /04~全栈与 GraphQL/GraphQL/服务端/服务端开发.md: -------------------------------------------------------------------------------- 1 | # 服务端开发 2 | 3 | # 服务端开发 4 | 5 | ## 基础服务 6 | 7 | ### express-graphql 8 | 9 | 最简单的构建 GraphQL API 服务器的方式就是基于 Express,添加自定义的处理器: 10 | 11 | ```js 12 | import express from "express"; 13 | import graphqlHTTP from "express-graphql"; 14 | import { buildSchema } from "graphql"; 15 | 16 | // Construct a schema, using GraphQL schema language 17 | const schema = buildSchema(` 18 | type Query { 19 | hello: String 20 | } 21 | `); 22 | 23 | // The root provides a resolver function for each API endpoint 24 | const root = { 25 | hello: () => "Hello world!" 26 | }; 27 | 28 | const app = express(); 29 | 30 | app.use( 31 | "/graphql", 32 | graphqlHTTP({ 33 | schema, 34 | rootValue: root, 35 | graphiql: true 36 | }) 37 | ); 38 | 39 | // http://localhost:4000/graphql 进入 GraphiQL 交互查询工具 40 | app.listen(4000); 41 | ``` 42 | 43 | [Prisma](https://github.com/graphcool/prisma) 是非常不错的全栈架构,开发者只需要定义好数据结构,Prisma 即能够为我们自动构建包含数据库(Docker)的 GraphQL API,Prisma 也为我们提供了便捷的云化部署方案,较为适合个人项目。 44 | 45 | ### Apollo Server 46 | 47 | ```js 48 | const schema = ` 49 | type Todo { 50 | ... 51 | } 52 | 53 | type TodoList { 54 | todos: [Todo] 55 | } 56 | 57 | type Query { 58 | todoList: Todo List 59 | } 60 | 61 | type Mutation { 62 | addTodo( 63 | text: String! 64 | ): Todo, 65 | toggleTodo( 66 | id: String! 67 | ): Todo 68 | } 69 | 70 | type Subscription { 71 | todoUpdated: Todo 72 | } 73 | 74 | schema { 75 | query: Query 76 | mutation: Mutation 77 | subscription: Subscription 78 | } 79 | `; 80 | 81 | const resolvers = { 82 | TodoList: { 83 | todos() { 84 | return todos; 85 | } 86 | }, 87 | Query: { 88 | todoList() { 89 | return true; 90 | } 91 | }, 92 | Mutation: { 93 | addTodo(_, { text }) { 94 | ... 95 | }, 96 | toggleTodo(_, { id }, { ctx }) { 97 | ... 98 | } 99 | }, 100 | Subscription: { 101 | todoUpdated: { 102 | ... 103 | } 104 | } 105 | }; 106 | 107 | const executableSchema = makeExecutableSchema({ 108 | typeDefs: schema, 109 | resolvers 110 | }); 111 | 112 | router.post( 113 | '/graphql', 114 | graphqlKoa(ctx => ({ 115 | schema: executableSchema, 116 | context: { ctx } 117 | })) 118 | ) 119 | ``` 120 | 121 | ## 数据模型层 122 | 123 | GraphQL 的一大优势就是避免了单个业务逻辑与查询语句的强绑定,SQL 具备良好的声明式可读性,但是其编程可组合性较差。在 GraphQL 的图模型中,无法确定单次查询涉及的资源数目,自然也就无法预先编写出完整的 SQL 语句,而需要在程序里进行资源的获取与组合。REST 架构开发模式下,我们往往会为某个涉及多资源的查询编写复杂的关联语句: 124 | 125 | ```sql 126 | select * from user left join asset on user.asset_id = asset.id; 127 | ``` 128 | 129 | 而 GraphQL 中则是将复杂的逻辑划分为多个原子查询,并在编程语言中完成数据的聚合,譬如我们需要查询用户关联的资产时,其 Schema 定义如下: 130 | 131 | ```gql 132 | type User { 133 | asset: Asset 134 | } 135 | ``` 136 | 137 | 此时对于 Asset 资源,其并不需要了解完整的业务逻辑,只需要根据输入的 userId 获取到 asset 对象,对于数据的封装则是由 GraphQL 自动完成: 138 | 139 | ```js 140 | const user = getUserById(userId); 141 | const asset = getAssetById(user.assetId); 142 | 143 | user.asset = asset; 144 | ``` 145 | 146 | 显而易见地,这种方式可能导致单次请求处理中对于某个表的多次查询,[dataloader](https://github.com/facebook/dataloader) 即是 Facebook 开源的数据访问层辅助工具,其能够将多次对于数据库或者外部服务查询的语句合并处理。dataloader 的核心理念在于接收用户定义的批次加载函数: 147 | 148 | ```js 149 | const DataLoader = require("dataloader"); 150 | 151 | const userLoader = new DataLoader(keys => myBatchGetUsers(keys)); 152 | ``` 153 | 154 | 当我们在业务逻辑中进行多次查询时,譬如: 155 | 156 | ```js 157 | userLoader 158 | .load(1) 159 | .then(user => userLoader.load(user.invitedByID)) 160 | .then(invitedBy => console.log(`User 1 was invited by ${invitedBy}`)); 161 | 162 | // Elsewhere in your application 163 | userLoader 164 | .load(2) 165 | .then(user => userLoader.load(user.lastInvitedID)) 166 | .then(lastInvited => console.log(`User 2 last invited ${lastInvited}`)); 167 | 168 | // 也可以同时加载多个数据 169 | const [a, b] = await myLoader.loadMany(["a", "b"]); 170 | ``` 171 | 172 | dataloader 会把某个执行时间片(参考 [EventLoop](https://parg.co/AzO))内的独立查询合并处理,调用批量查询函数来进行单次查询;dataloader 还提供了缓存机制,当我们在某个执行时间片内查询了相同键的数据,dataloader 会自动返回缓存的 Promise 对象: 173 | 174 | ```js 175 | const userLoader = new DataLoader(...) 176 | const promise1A = userLoader.load(1) 177 | const promise1B = userLoader.load(1) 178 | assert(promise1A === promise1B) 179 | ``` 180 | 181 | ## 分页与搜索 182 | 183 | GitHub GraphQL API,可以在 [Explorer](https://developer.github.com/v4/explorer/) 184 | 185 | ```gql 186 | query { 187 | viewer { 188 | login 189 | name 190 | starredRepositories(first: 3, after: "put_in_a_cursor_value_here") { 191 | edges { 192 | cursor 193 | node { 194 | id 195 | name 196 | primaryLanguage { 197 | id 198 | name 199 | color 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | ``` 207 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/数据操作/Query Builder.md: -------------------------------------------------------------------------------- 1 | # Query Builder 2 | 3 | QueryBuilder 是 TypeORM 最强大的功能之一,它允许你使用优雅便捷的语法构建 SQL 查询,执行并获得自动转换的实体。QueryBuilder 的简单示例: 4 | 5 | ```ts 6 | const firstUser = await connection 7 | .getRepository(User) 8 | .createQueryBuilder("user") 9 | .where("user.id = :id", { id: 1 }) 10 | .getOne(); 11 | ``` 12 | 13 | 它将生成以下 SQL 查询: 14 | 15 | ```sql 16 | SELECT 17 | user.id as userId, 18 | user.firstName as userFirstName, 19 | user.lastName as userLastName 20 | FROM users user 21 | WHERE user.id = 1 22 | ``` 23 | 24 | 然后返回一个 `User` 实例: 25 | 26 | ```s 27 | User { 28 | id: 1, 29 | firstName: "Timber", 30 | lastName: "Saw" 31 | } 32 | ``` 33 | 34 | # 创建与操作 35 | 36 | ## 创建 QueryBuilder 37 | 38 | 有几种方法可以创建 `Query Builder`: 39 | 40 | - 使用 connection: 41 | 42 | ```typescript 43 | import { getConnection } from "typeorm"; 44 | 45 | const user = await getConnection() 46 | .createQueryBuilder() 47 | .select("user") 48 | .from(User, "user") 49 | .where("user.id = :id", { id: 1 }) 50 | .getOne(); 51 | ``` 52 | 53 | - 使用 entity manager: 54 | 55 | ```typescript 56 | import { getManager } from "typeorm"; 57 | 58 | const user = await getManager() 59 | .createQueryBuilder(User, "user") 60 | .where("user.id = :id", { id: 1 }) 61 | .getOne(); 62 | ``` 63 | 64 | - 使用 repository: 65 | 66 | ```typescript 67 | import { getRepository } from "typeorm"; 68 | 69 | const user = await getRepository(User) 70 | .createQueryBuilder("user") 71 | .where("user.id = :id", { id: 1 }) 72 | .getOne(); 73 | ``` 74 | 75 | 有 5 种不同的`QueryBuilder`类型可用: 76 | 77 | - `SelectQueryBuilder` - 用于构建和执行`SELECT`查询例如: 78 | 79 | ```typescript 80 | import { getConnection } from "typeorm"; 81 | 82 | const user = await getConnection() 83 | .createQueryBuilder() 84 | .select("user") 85 | .from(User, "user") 86 | .where("user.id = :id", { id: 1 }) 87 | .getOne(); 88 | ``` 89 | 90 | - `InsertQueryBuilder` - 用于构建和执行 `INSERT` 查询例如: 91 | 92 | ```typescript 93 | import { getConnection } from "typeorm"; 94 | 95 | await getConnection() 96 | .createQueryBuilder() 97 | .insert() 98 | .into(User) 99 | .values([ 100 | { firstName: "Timber", lastName: "Saw" }, 101 | { firstName: "Phantom", lastName: "Lancer" } 102 | ]) 103 | .execute(); 104 | ``` 105 | 106 | - `UpdateQueryBuilder` - 用于构建和执行 `UPDATE` 查询例如: 107 | 108 | ```typescript 109 | import { getConnection } from "typeorm"; 110 | 111 | await getConnection() 112 | .createQueryBuilder() 113 | .update(User) 114 | .set({ firstName: "Timber", lastName: "Saw" }) 115 | .where("id = :id", { id: 1 }) 116 | .execute(); 117 | ``` 118 | 119 | - `DeleteQueryBuilder` - 用于构建和执行`DELETE`查询例如: 120 | 121 | ```typescript 122 | import { getConnection } from "typeorm"; 123 | 124 | await getConnection() 125 | .createQueryBuilder() 126 | .delete() 127 | .from(User) 128 | .where("id = :id", { id: 1 }) 129 | .execute(); 130 | ``` 131 | 132 | - `RelationQueryBuilder` - 用于构建和执行特定于关系的操作[TBD]。 133 | 134 | 你可以在其中切换任何不同类型的查询构建器,一旦执行,则将获得一个新的查询构建器实例(与所有其他方法不同)。 135 | 136 | ## 插入 137 | 138 | 你可以使用`QueryBuilder`创建`INSERT`查询例如: 139 | 140 | ```typescript 141 | import { getConnection } from "typeorm"; 142 | 143 | await getConnection() 144 | .createQueryBuilder() 145 | .insert() 146 | .into(User) 147 | .values([ 148 | { firstName: "Timber", lastName: "Saw" }, 149 | { firstName: "Phantom", lastName: "Lancer" } 150 | ]) 151 | .execute(); 152 | ``` 153 | 154 | 就性能而言,这是向数据库中插入实体的最有效方法你也可以通过这种方式执行批量插入。在某些情况下需要执行函数 SQL 查询时: 155 | 156 | ```typescript 157 | import { getConnection } from "typeorm"; 158 | 159 | await getConnection() 160 | .createQueryBuilder() 161 | .insert() 162 | .into(User) 163 | .values({ 164 | firstName: "Timber", 165 | lastName: () => "CONCAT('S', 'A', 'W')" 166 | }) 167 | .execute(); 168 | ``` 169 | 170 | 此语法不会对值进行转义,你需要自己处理转义。 171 | 172 | ## 更新 173 | 174 | 你可以使用 `QueryBuilder` 创建 `UPDATE` 查询例如: 175 | 176 | ```typescript 177 | import { getConnection } from "typeorm"; 178 | 179 | await getConnection() 180 | .createQueryBuilder() 181 | .update(User) 182 | .set({ firstName: "Timber", lastName: "Saw" }) 183 | .where("id = :id", { id: 1 }) 184 | .execute(); 185 | ``` 186 | 187 | 就性能而言,这是更新数据库中的实体的最有效方法。在某些情况下需要执行函数 SQL 查询时: 188 | 189 | ``` 190 | typescript import {getConnection} from "typeorm"; await getConnection() .createQueryBuilder() .update(User) .set({ firstName: "Timber", lastName: "Saw", age: () => "'age' + 1" }) .where("id = :id", { id: 1 }) .execute(); 191 | ``` 192 | 193 | 此语法不会对值进行转义,你需要自己处理转义。 194 | 195 | ## 删除 196 | 197 | 你可以使用 `QueryBuilder` 创建 `DELETE` 查询例如: 198 | 199 | ```typescript 200 | import { getConnection } from "typeorm"; 201 | 202 | await getConnection() 203 | .createQueryBuilder() 204 | .delete() 205 | .from(User) 206 | .where("id = :id", { id: 1 }) 207 | .execute(); 208 | ``` 209 | 210 | 就性能而言,这是删除数据库中的实体的最有效方法。 211 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/中间件/过滤器与异常处理.md: -------------------------------------------------------------------------------- 1 | # 异常处理 2 | 3 | Nest 框架内部实现了一个异常处理层,专门用来负责应用程序中未处理的异常。 4 | 5 | ![Nest Pipe](https://i.loli.net/2019/07/02/5d1b66de9370734231.png) 6 | 7 | 默认情况未处理的异常会被全局过滤异常器 HttpException 或者它的子类处理。如果一个未识别的异常(非 HttpException 或未继承自 HttpException)被抛出,下面的信息将被返回给客户端: 8 | 9 | ```json 10 | { 11 | "statusCode": 500, 12 | "message": "Internal server error" 13 | } 14 | ``` 15 | 16 | # 基础异常 17 | 18 | 我们可以从控制器的方法中手动抛出一个异常: 19 | 20 | ```ts 21 | @Get() 22 | async findAll() { 23 | throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); 24 | } 25 | ``` 26 | 27 | 客户端将收到如下信息: 28 | 29 | ```json 30 | { 31 | "statusCode": 403, 32 | "message": "Forbidden" 33 | } 34 | ``` 35 | 36 | 当然你也可以自定义返回状态值和错误信息: 37 | 38 | ```ts 39 | @Get() 40 | async findAll() { 41 | throw new HttpException({ 42 | status: HttpStatus.FORBIDDEN, 43 | error: 'This is a custom message', 44 | }, 403); 45 | } 46 | ``` 47 | 48 | # 异常级别 49 | 50 | 比较好的做法是实现你自己想要的异常类。 51 | 52 | ```ts 53 | export class ForbiddenException extends HttpException { 54 | constructor() { 55 | super("Forbidden", HttpStatus.FORBIDDEN); 56 | } 57 | } 58 | ``` 59 | 60 | 然后你就可以手动在需要的地方抛出它。 61 | 62 | ```ts 63 | @Get() 64 | async findAll() { 65 | throw new ForbiddenException(); 66 | } 67 | ``` 68 | 69 | ## HTTP 异常 70 | 71 | Nest 内置了以下集成自 HttpException 的异常类: 72 | 73 | - BadRequestException 74 | - UnauthorizedException 75 | - NotFoundException 76 | - ForbiddenException 77 | - NotAcceptableException 78 | - RequestTimeoutException 79 | - ConflictException 80 | - GoneException 81 | - PayloadTooLargeException 82 | - UnsupportedMediaTypeException 83 | - UnprocessableEntityException 84 | - InternalServerErrorException 85 | - NotImplementedException 86 | - BadGatewayException 87 | - ServiceUnavailableException 88 | - GatewayTimeoutException 89 | 90 | # 异常过滤器 91 | 92 | 如果你想给异常返回值加一些动态的参数,可以使用异常过滤器来实现。例如下面的异常过滤器将会给 HttpException 添加额外的时间缀和路径参数: 93 | 94 | ```ts 95 | @Catch(HttpException) 96 | export class HttpExceptionFilter implements ExceptionFilter { 97 | catch(exception: HttpException, host: ArgumentsHost) { 98 | const ctx = host.switchToHttp(); 99 | const response = ctx.getResponse(); 100 | const request = ctx.getRequest(); 101 | const status = exception.getStatus(); 102 | 103 | response.status(status).json({ 104 | statusCode: status, 105 | timestamp: new Date().toISOString(), 106 | path: request.url 107 | }); 108 | } 109 | } 110 | ``` 111 | 112 | 注意:所有的异常过滤器都必须实现泛型接口 ExceptionFilter。就是说你必须要提供一个 catch(exception: T, host: ArgumentsHost) 方法 113 | 114 | ## 参数宿主 115 | 116 | 上面代码中的 host 参数是一个类型为 ArgumentsHost 的原生请求处理器包装对象。根据应用程序的不同它具有不同的接口。 117 | 118 | ```ts 119 | export interface ArgumentsHost { 120 | getArgs = any[]>(): T; 121 | getArgByIndex(index: number): T; 122 | switchToRpc(): RpcArgumentsHost; 123 | switchToHttp(): HttpArgumentsHost; 124 | switchToWs(): WsArgumentsHost; 125 | } 126 | ``` 127 | 128 | ## 绑定过滤器 129 | 130 | 可以使用 @UseFilters 装饰器让一个控制器方法具有过滤器处理逻辑。 131 | 132 | ```ts 133 | @Post() 134 | @UseFilters(HttpExceptionFilter) 135 | async create(@Body() createCatDto: CreateCatDto) { 136 | throw new ForbiddenException(); 137 | } 138 | ``` 139 | 140 | 当然过滤器可以被使用在不同的作用域上:**方法作用域、控制器作用域、全局作用域**。比如应用一个控制器作用域的过滤器,可以这么写: 141 | 142 | ```ts 143 | @UseFilters(new HttpExceptionFilter()) 144 | export class CatsController {} 145 | ``` 146 | 147 | 全局过滤器可以通过如下代码实现: 148 | 149 | ```ts 150 | async function bootstrap() { 151 | const app = await NestFactory.create(ApplicationModule); 152 | app.useGlobalFilters(new HttpExceptionFilter()); 153 | await app.listen(3000); 154 | } 155 | bootstrap(); 156 | ``` 157 | 158 | 不过这样注册的全局过滤器无法进入依赖注入,因为它在模块作用域之外。为了解决这个问题,你可以在根模块上面注册一个全局作用域的过滤器。 159 | 160 | ```ts 161 | @Module({ 162 | providers: [ 163 | { 164 | provide: APP_FILTER, 165 | useClass: HttpExceptionFilter 166 | } 167 | ] 168 | }) 169 | export class ApplicationModule {} 170 | ``` 171 | 172 | ## 捕获所有异常 173 | 174 | @Catch() 装饰器不传入参数就默认捕获所有的异常: 175 | 176 | ```ts 177 | @Catch() 178 | export class AllExceptionsFilter implements ExceptionFilter { 179 | catch(exception: unknown, host: ArgumentsHost) { 180 | const ctx = host.switchToHttp(); 181 | const response = ctx.getResponse(); 182 | const request = ctx.getRequest(); 183 | 184 | const status = 185 | exception instanceof HttpException 186 | ? exception.getStatus() 187 | : HttpStatus.INTERNAL_SERVER_ERROR; 188 | 189 | response.status(status).json({ 190 | statusCode: status, 191 | timestamp: new Date().toISOString(), 192 | path: request.url 193 | }); 194 | } 195 | } 196 | ``` 197 | 198 | ## 继承 199 | 200 | 通常你可能并不需要自己实现完全定制化的异常过滤器,可以继承自 BaseExceptionFilter 即可复用内置的过滤器逻辑。 201 | 202 | ```ts 203 | @Catch() 204 | export class AllExceptionsFilter extends BaseExceptionFilter { 205 | catch(exception: unknown, host: ArgumentsHost) { 206 | super.catch(exception, host); 207 | } 208 | } 209 | ``` 210 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/中间件/拦截器.md: -------------------------------------------------------------------------------- 1 | # 拦截器 2 | 3 | 拦截器(Interceptors)是一个使用 @Injectable() 装饰的类,它必须实现 NestInterceptor 接口。 4 | 5 | ![拦截器示意图](https://i.loli.net/2019/07/05/5d1edebfa26f771380.png) 6 | 7 | 拦截器有一系列的功能,这些功能的设计灵感都来自于面向切面的编程(AOP)技术。这使得下面这些功能成为可能: 8 | 9 | - 在函数执行前/后绑定**额外的逻辑** 10 | - **转换**一个函数的返回值 11 | - **转换**函数抛出的异常 12 | - **扩展**基础函数的行为 13 | - 根据特定的条件**完全的重写**一个函数(比如:缓存) 14 | 15 | # 基础 16 | 17 | 每个拦截器都要实现 intercept() 方法,此方法有两个参数。第一个是 ExecutionContext 实例(这和守卫中的对象一样)。ExecutionContext 继承自 ArgumentsHost。上一节中我们见过,它是一个包装了传递向原始处理器而且根据应用的不同包含不同的参数数组的类。 18 | 19 | ## 执行上下文 20 | 21 | ExecutionContext 通过继承 ArgumentsHost,提供了更多的执行过种中的更多细节,它看起来长这样: 22 | 23 | ```ts 24 | export interface ExecutionContext extends ArgumentsHost { 25 | getClass(): Type; 26 | getHandler(): Function; 27 | } 28 | ``` 29 | 30 | getHandler() 方法返回一个将会被调用的路由处理器的引用。getClass() 方法返回控制器类的类型。例如,如果当前进行着一个 POST 请求,假定它会由 CatsController 的 create() 方法处理,那么 getHandler() 方法将返回 create() 方法的引用,而 getClass() 则会返回 CatsController 的类型(非实例)。 31 | 32 | ## 调用处理器 33 | 34 | 第二个参数是一个 CallHandler。CallHandler 接口实现了 handle() 方法,这个方法就是你可以在你拦截器的某个地方调用的路由处理器。如果你的 intercept() 方法中没调用 handle() 方法,那么路由处理器将不会被执行。 35 | 36 | 不像守卫与过滤器,拦截器对于一次请求响应有完全的控制权与责任。这样的方式意味着 intercept() 方法可以高效地包装请求/响应流。因此,你可以在最终的路由处理器执行前/后实现自己的逻辑。显然,你已经可以通过在 intercept() 方法中的 handle() 调用之前写自己的代码,但是后续的逻辑应该如何处理?因为 handle() 方法返回的是一个 Observable,我们可以使用 RxJS 做到修改后来的响应。使用 AOP 技术,路由处理器的调用被称做一个 切点(Pointcut),这表示一个我们的自定义的逻辑插入的地方。 37 | 38 | 假如有一个 POST /cats 的请求,这个请求将被 CatsController 中的 create() 方法处理。如果一个没调用 handle() 方法的拦截器在某处被调用,create() 方法将不会被执行。一但 handle() 方法被调用(它的 Observable 已返回),create() 处理器将被触发。一但响应流通过 Observable 接收到,附加的操作可以在注上被执行,最后的结果将返回给调用方。 39 | 40 | ## 切面拦截 41 | 42 | 我们将要研究的第一个例子就是用户登录的交互。下面展示了一个简单的日志拦截器: 43 | 44 | ```ts 45 | @Injectable() 46 | export class LoggingInterceptor implements NestInterceptor { 47 | intercept(context: ExecutionContext, next: CallHandler): Observable { 48 | console.log("Before..."); 49 | 50 | const now = Date.now(); 51 | return next 52 | .handle() 53 | .pipe(tap(() => console.log(`After... ${Date.now() - now}ms`))); 54 | } 55 | } 56 | ``` 57 | 58 | 由于 handle() 方法返回了一个 RxJS 的 Observable 对象,对于修改流我们将有更多的选择。上面的示例中我们使用了 tap() 操作符。它在 Observable 流的正常或异常终止时调用我们的匿名日志记录函数,但不会干扰到响应周期。 59 | 60 | ## 绑定拦截器 61 | 62 | 我们可以使用 @UseInterceptors() 装饰器来绑定一个拦截器,和管道、守卫一样,它即可以是控制器作用域的,也可以是方法作用域的,或者是全局的。 63 | 64 | ``` 65 | @UseInterceptors(LoggingInterceptor) 66 | export class CatsController {} 67 | ``` 68 | 69 | 上面的实现,在请求进入 CatsController 后,你将看到下面的日志输出。 70 | 71 | ``` 72 | Before... 73 | After... 1ms 74 | ``` 75 | 76 | # 案例 77 | 78 | ## 响应映射 79 | 80 | 我们已经知道了 handle() 方法返回一个 Observable。流包含路由处理器返回的值,因此,我们可以很容易的使用 RxJS 的 map() 操作符改变它。让我们新建一个 TransformInterceptor,它可以修改每个响应。它将使用 map() 操作符来给响应对象符加 data 属性,并且将这个新的响应返回给客户端。 81 | 82 | ```ts 83 | export interface Response { 84 | data: T; 85 | } 86 | 87 | @Injectable() 88 | export class TransformInterceptor 89 | implements NestInterceptor> { 90 | intercept( 91 | context: ExecutionContext, 92 | next: CallHandler 93 | ): Observable> { 94 | return next.handle().pipe(map(data => ({ data }))); 95 | } 96 | } 97 | ``` 98 | 99 | 当有请求进入时,响应看起来将会是下面这样: 100 | 101 | ```json 102 | { 103 | "data": [] 104 | } 105 | ``` 106 | 107 | 拦截器对于创建整个应用层面的可复用方案有非常大的意义。比如说,我们需要将所有响应中出现的 null 值改成空字符串 ““。我们可以使用拦截器功能仅用下面一行代码就可以实现 108 | 109 | ```ts 110 | @Injectable() 111 | export class ExcludeNullInterceptor implements NestInterceptor { 112 | intercept(context: ExecutionContext, next: CallHandler): Observable { 113 | return next.handle().pipe(map(value => (value === null ? "" : value))); 114 | } 115 | } 116 | ``` 117 | 118 | ## 异常映射 119 | 120 | 另外一个有趣的用例是使用 RxJS 的 catchError() 操作符来重写异常捕获: 121 | 122 | ```ts 123 | @Injectable() 124 | export class ErrorsInterceptor implements NestInterceptor { 125 | intercept(context: ExecutionContext, next: CallHandler): Observable { 126 | return next 127 | .handle() 128 | .pipe(catchError(err => throwError(new BadGatewayException()))); 129 | } 130 | } 131 | ``` 132 | 133 | ## 流重写 134 | 135 | 有一些情况下我们希望完全阻止处理器的调用并返回一个不同的值。比如缓存的实现。让我们来试试使用缓存拦截器来实现它。当然真正的缓存实现还包含 TTL,缓存验证,缓存大小等问题,我们这个例子只是一个简单的示意。 136 | 137 | ```ts 138 | @Injectable() 139 | export class CacheInterceptor implements NestInterceptor { 140 | intercept(context: ExecutionContext, next: CallHandler): Observable { 141 | const isCached = true; 142 | if (isCached) { 143 | return of([]); 144 | } 145 | return next.handle(); 146 | } 147 | } 148 | ``` 149 | 150 | 上面的代码中我们硬编码了 isCached 变量,以及返回的缓存数据 []。关键点在于我们返回了一个新的流,使用了 RxJS 的 of() 操作符。因此路由处理器永远不会被调用。为了实现一个更完整的解决方案,你可以通过使用 Reflector 创建一个自定义的装饰器来实现缓存功能。 151 | 152 | # 更多的操作符 153 | 154 | RxJS 的操作符有很多种能力,我们可以考虑下面这种用例。你需要处理路由请求的超时问题。当你的响应很久都没正常返回时,你会想把它关闭并返回一个错误的响应。 155 | 156 | ```ts 157 | @Injectable() 158 | export class TimeoutInterceptor implements NestInterceptor { 159 | intercept(context: ExecutionContext, next: CallHandler): Observable { 160 | return next.handle().pipe(timeout(5000)); 161 | } 162 | } 163 | ``` 164 | -------------------------------------------------------------------------------- /05~数据库/TypeORM/配置/Connection.md: -------------------------------------------------------------------------------- 1 | # Connection 2 | 3 | 只有在建立连接后才能与数据库进行交互。TypeORM 的 Connection 不会像看起来那样设置单个数据库连接,而是设置连接池。QueryRunner 的每个实例都是一个独立的数据库连接。一旦调用 Connection 的 connect 方法,就建立连接池设置。如果使用 createConnection 函数设置连接,则会自动调用 connect 方法。调用 close 时会断开连接(关闭池中的所有连接)。通常情况下,你只能在应用程序启动时创建一次连接,并在完全使用数据库后关闭它。实际上,如果要为站点构建后端,并且后端服务器始终保持运行,则不需要关闭连接。 4 | 5 | # 连接创建 6 | 7 | ## 手动操作 8 | 9 | 有多种方法可以创建连接。但是最简单和最常用的方法是使用 createConnection 和 createConnections 函数。createConnection 创建单个连接: 10 | 11 | ```ts 12 | import { createConnection, Connection } from "typeorm"; 13 | 14 | const connection = await createConnection({ 15 | type: "mysql", 16 | host: "localhost", 17 | port: 3306, 18 | username: "test", 19 | password: "test", 20 | database: "test" 21 | }); 22 | 23 | // 只使用 url 和 type 也可以进行连接。 24 | 25 | createConnection({ 26 | type: "postgres", 27 | url: "postgres://test:test@localhost/test" 28 | }); 29 | ``` 30 | 31 | createConnections 创建多个连接: 32 | 33 | ```ts 34 | import { createConnections, Connection } from "typeorm"; 35 | 36 | const connections = await createConnections([ 37 | { 38 | name: "default", 39 | type: "mysql", 40 | host: "localhost", 41 | port: 3306, 42 | username: "test", 43 | password: "test", 44 | database: "test" 45 | }, 46 | { 47 | name: "test2-connection", 48 | type: "mysql", 49 | host: "localhost", 50 | port: 3306, 51 | username: "test", 52 | password: "test", 53 | database: "test2" 54 | } 55 | ]); 56 | ``` 57 | 58 | 创建连接后,你可以使用 getConnection 函数从应用程序中的任何位置使用它: 59 | 60 | ```ts 61 | import { getConnection } from "typeorm"; 62 | 63 | // 可以在调用 createConnection 后使用并解析 64 | const connection = getConnection(); 65 | 66 | // 如果你有多个连接,则可以按名称获取连接 67 | const secondConnection = getConnection("test2-connection"); 68 | ``` 69 | 70 | 应避免额外创建 classes/services 来存储和管理连接。此功能已嵌入到 TypeORM 中,无需过度工程并创建无用的抽象。 71 | 72 | ## ormconfig.json 73 | 74 | 大多数情况下,我们希望将连接选项存储在单独的配置文件中,因为此方式使管理变得更方便和容易 TypeORM 支持多个配置源。你只需要在应用程序的根目录(package.json 附近)中创建一个 `ormconfig.[format]` 文件存放连接配置,并在应用程序中调用 createConnection(),而不传递任何参数配置: 75 | 76 | ```ts 77 | import { createConnection } from "typeorm"; 78 | 79 | // createConnection方法会自动读取来自ormconfig文件或环境变量中的连接选项 80 | const connection = await createConnection(); 81 | ``` 82 | 83 | 支持的 ormconfig 文件格式有:.json, .js, .env, .yml 和 .xml。在项目根目录(package.json 附近)中创建 ormconfig.json,并包含以下内容: 84 | 85 | ```json 86 | { 87 | "type": "mysql", 88 | "host": "localhost", 89 | "port": 3306, 90 | "username": "test", 91 | "password": "test", 92 | "database": "test" 93 | } 94 | ``` 95 | 96 | 如果要创建多个连接,则只需在数组中添加多个连接: 97 | 98 | ```json 99 | [ 100 | { 101 | "name": "default", 102 | "type": "mysql", 103 | "host": "localhost", 104 | "port": 3306, 105 | "username": "test", 106 | "password": "test", 107 | "database": "test" 108 | }, 109 | { 110 | "name": "second-connection", 111 | "type": "mysql", 112 | "host": "localhost", 113 | "port": 3306, 114 | "username": "test", 115 | "password": "test", 116 | "database": "test" 117 | } 118 | ] 119 | ``` 120 | 121 | 有时你希望覆盖 ormconfig 文件中定义的值,或者可能会在配置中附加一些 TypeScript/JavaScript 逻辑在这种情况下,你可以从 ormconfig 加载选项并构建 ConnectionOptions,然后在将它们传递给 createConnection 函数之前,使用这些选项执行任何操作: 122 | 123 | ```ts 124 | // 从ormconfig文件(或ENV变量)读取连接选项 125 | const connectionOptions = await getConnectionOptions(); 126 | 127 | // 使用connectionOptions做一些事情, 128 | // 例如,附加自定义命名策略或自定义记录器 129 | Object.assign(connectionOptions, { namingStrategy: new MyNamingStrategy() }); 130 | 131 | // 使用覆盖后的连接选项创建连接 132 | const connection = await createConnection(connectionOptions); 133 | ``` 134 | 135 | # Connection 使用 136 | 137 | ## 多个 Connection 138 | 139 | 使用多个数据库的最简单方法是创建不同的连接: 140 | 141 | ```ts 142 | import { createConnections } from "typeorm"; 143 | 144 | const connections = await createConnections([ 145 | { 146 | name: "db1Connection", 147 | type: "mysql", 148 | host: "localhost", 149 | port: 3306, 150 | username: "root", 151 | password: "admin", 152 | database: "db1", 153 | entities: [__dirname + "/entity/*{.js,.ts}"], 154 | synchronize: true 155 | }, 156 | { 157 | name: "db2Connection", 158 | type: "mysql", 159 | host: "localhost", 160 | port: 3306, 161 | username: "root", 162 | password: "admin", 163 | database: "db2", 164 | entities: [__dirname + "/entity/*{.js,.ts}"], 165 | synchronize: true 166 | } 167 | ]); 168 | ``` 169 | 170 | 此方法允许你连接到已拥有的任意数量的数据库,每个数据库都有自己的配置,自己的实体和整体 ORM 范围和设置。对于每个连接,将创建一个新的 Connection 实例你必须为创建的每个连接指定唯一的名称。 171 | 也可以从 ormconfig 文件加载所有连接选项: 172 | 173 | ```typescript 174 | import { createConnections } from "typeorm"; 175 | 176 | const connections = await createConnections(); 177 | ``` 178 | 179 | 指定要按名称创建的连接: 180 | 181 | ```typescript 182 | import { createConnection } from "typeorm"; 183 | 184 | const connection = await createConnection("db2Connection"); 185 | ``` 186 | 187 | 使用连接时,必须指定连接名称以获取特定连接: 188 | 189 | ```typescript 190 | import { getConnection } from "typeorm"; 191 | 192 | const db1Connection = getConnection("db1Connection"); 193 | // 现在可以使用"db1"数据库... 194 | 195 | const db2Connection = getConnection("db2Connection"); 196 | // 现在可以使用"db2"数据库... 197 | ``` 198 | 199 | 使用此方法的好处是你可以使用不同的登录凭据,主机,端口甚至数据库类型来配置多个连接。但是缺点可能是需要管理和使用多个连接实例。 200 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/中间件/中间件与管道.md: -------------------------------------------------------------------------------- 1 | # Nest 中间件 2 | 3 | 中间件就是一个函数,在路由处理器之前调用。这就表示中间件函数可以访问到请求和响应对象以及应用的请求响应周期中的 next() 中间间函数。 4 | 5 | ![Nest 中间件](https://i.loli.net/2019/07/01/5d19e2f1938ef39341.png) 6 | 7 | Nest 中间件实际上和 Express 的中间件是一样的,Express 文档中对中间件的描述如下: 8 | 9 | - 执行任意的代码 10 | - 对请求/响应做操作 11 | - 终结请求-响应周期 12 | - 调用下一个栈中的中间件函数 13 | - 如果当前的中间间函数没有终结请求响应周期,那么它必须调用 next() 方法将控制权传递给下一个中间件函数。否则请求将被挂起 14 | 15 | # 中间件定义 16 | 17 | Nest 允许你使用函数或者类来实现自己的中间件。如果用类实现,则需要使用 @Injectable() 装饰,并且实现 NestMiddleware 接口。 18 | 19 | ```ts 20 | import { Injectable, NestMiddleware } from "@nestjs/common"; 21 | import { Request, Response } from "express"; 22 | 23 | @Injectable() 24 | export class LoggerMiddleware implements NestMiddleware { 25 | use(req: Request, res: Response, next: Function) { 26 | console.log("Request..."); 27 | next(); 28 | } 29 | } 30 | ``` 31 | 32 | 或者我们也可以使用函数式中间件,函数式的中间件可以用一个简单无依赖函数来实现: 33 | 34 | ```ts 35 | export function logger(req, res, next) { 36 | console.log(`Request...`); 37 | next(); 38 | } 39 | ``` 40 | 41 | # 应用中间件 42 | 43 | @Module() 装饰器中并不能指定中间件参数,我们可以在模块类的构 configure() 方法中应用中间件,下面的代码会应用一个 ApplicationModule 级别的日志中间件 LoggerMiddleware。 44 | 45 | ```ts 46 | @Module({ 47 | imports: [CatsModule] 48 | }) 49 | export class ApplicationModule implements NestModule { 50 | configure(consumer: MiddlewareConsumer) { 51 | consumer.apply(LoggerMiddleware).forRoutes("cats"); 52 | } 53 | } 54 | ``` 55 | 56 | 上面的代码 forRoutes 方法表示只将中间件应用在 cats 路由上,还可以是指定的 HTTP 方法,甚至是路由通配符: 57 | 58 | ```ts 59 | .forRoutes({ path: 'cats', method: RequestMethod.GET }); 60 | .forRoutes({ path: 'ab*cd', method: RequestMethod.ALL }); 61 | ``` 62 | 63 | 当然,你也可以指定不包括某些路由规则: 64 | 65 | ```ts 66 | consumer 67 | .apply(LoggerMiddleware) 68 | .exclude( 69 | { path: "cats", method: RequestMethod.GET }, 70 | { path: "cats", method: RequestMethod.POST } 71 | ) 72 | .forRoutes(CatsController); 73 | ``` 74 | 75 | 不过请注意 exclude 方法不能运用在函数式的中间件上,而且这里指定的 path 也不支持通配符,这只是个快捷方法,如果你真的需要某种路由级别的控制,那完全可以把逻辑写在一个单独的中间件中。我们也可以用 apply 方法传入多个中间件参数即可: 76 | 77 | ```ts 78 | consumer.apply(cors(), helmet(), logger).forRoutes(CatsController); 79 | ``` 80 | 81 | # 管道 82 | 83 | 管道(Pipes)是一个用 @Injectable() 装饰过的类,它必须实现 PipeTransform 接口。 84 | 85 | ![管道](https://i.loli.net/2019/07/04/5d1dc90b3606425009.png) 86 | 87 | 从官方的示意图中我们可以看出来管道 pipe 和过滤器 filter 之间的关系:管道偏向于服务端控制器逻辑,过滤器则更适合用客户端逻辑。过滤器在客户端发送请求后处理,管道则在控制器接收请求前处理。管道通常有两种作用: 88 | 89 | - 转换/变形:转换输入数据为目标格式 90 | - 验证:对输入数据时行验证,如果合法让数据通过管道,否则抛出异常。 91 | 92 | 管道会处理控制器路由的参数,Nest 会在方法调用前插入管道,管道接收发往该方法的参数,此时就会触发上面两种情况。然后路由处理器会接收转换过的参数数据并处理后续逻辑。Nest 内置了两种管道:ValidationPipe 和 ParseIntPipe。 93 | 94 | ```ts 95 | @Injectable() 96 | export class ValidationPipe implements PipeTransform { 97 | transform(value: any, metadata: ArgumentMetadata) { 98 | return value; 99 | } 100 | } 101 | ``` 102 | 103 | ## 类验证器 104 | 105 | 因为我们前面已经在控制器参数上使用了 @body 装饰器,并且使用 TypeScript 的类型声明它为 CreateCatDto,如下: 106 | 107 | ```ts 108 | async create(@Body() createCatDto: CreateCatDto) { 109 | this.catsService.create(createCatDto); 110 | } 111 | ``` 112 | 113 | 要使用类验证器,你需要先安装 class-validator 库。class-validator 可以让你使用给类变量加装饰器的写法给类添加额外的验证功能。这样以来我们就可以直接在原始的 CreateCatDto 类上添加验证装饰器了,这样看起来就整洁多了,而且还没有重复代码: 114 | 115 | ```ts 116 | import { IsString, IsInt } from "class-validator"; 117 | 118 | export class CreateCatDto { 119 | @IsString() 120 | readonly name: string; 121 | 122 | @IsInt() 123 | readonly age: number; 124 | 125 | @IsString() 126 | readonly breed: string; 127 | } 128 | ``` 129 | 130 | 不过管道验证器中的代码也需要适配一下: 131 | 132 | ```ts 133 | @Injectable() 134 | export class ValidationPipe implements PipeTransform { 135 | async transform(value: any, { metatype }: ArgumentMetadata) { 136 | if (!metatype || !this.toValidate(metatype)) { 137 | return value; 138 | } 139 | const object = plainToClass(metatype, value); 140 | const errors = await validate(object); 141 | if (errors.length > 0) { 142 | throw new BadRequestException("Validation failed"); 143 | } 144 | return value; 145 | } 146 | 147 | private toValidate(metatype: Function): boolean { 148 | const types: Function[] = [String, Boolean, Number, Array, Object]; 149 | return !types.includes(metatype); 150 | } 151 | } 152 | ``` 153 | 154 | 注意这次的 transform 是 async 异步的,因为内部需要用到异步验证方法。Nest 是支持你这么做的,因为管道可以是异步的。然后我们可以插入这个管道,位置可以是方法级别的,也可以是参数级别的。 155 | 156 | - 参数作用域 157 | 158 | ```ts 159 | @Post() 160 | async create( 161 | @Body(new ValidationPipe()) createCatDto: CreateCatDto, 162 | ) { 163 | this.catsService.create(createCatDto); 164 | } 165 | ``` 166 | 167 | - 方法作用域 168 | 169 | ```ts 170 | @Post() 171 | @UsePipes(new ValidationPipe()) 172 | async create(@Body() createCatDto: CreateCatDto) { 173 | this.catsService.create(createCatDto); 174 | } 175 | ``` 176 | 177 | 管道修饰器入参可以是类而不必是管道实例: 178 | 179 | ```ts 180 | @Post() 181 | @UsePipes(ValidationPipe) 182 | async create(@Body() createCatDto: CreateCatDto) { 183 | this.catsService.create(createCatDto); 184 | } 185 | ``` 186 | 187 | 这样以来将实例化过程留给框架去做并肝启用依赖注入。由于 ValidationPipe 被尽可能的泛化,所以它可以直接使用在全局作用域上。 188 | 189 | ```ts 190 | async function bootstrap() { 191 | const app = await NestFactory.create(ApplicationModule); 192 | app.useGlobalPipes(new ValidationPipe()); 193 | await app.listen(3000); 194 | } 195 | bootstrap(); 196 | ``` 197 | 198 | ## 转换 199 | 200 | 我们还可以用管道来进行数据转换,比如说上面的例子中 age 虽然声明的是 int 类型,但是我们知道 HTTP 请求传递的都是纯字符流,所以通常我们还要把期望传进行类型转换。 201 | 202 | ```ts 203 | @Injectable() 204 | export class ParseIntPipe implements PipeTransform { 205 | transform(value: string, metadata: ArgumentMetadata): number { 206 | const val = parseInt(value, 10); 207 | if (isNaN(val)) { 208 | throw new BadRequestException("Validation failed"); 209 | } 210 | return val; 211 | } 212 | } 213 | ``` 214 | 215 | 上面这个管道的功能就是强制转换成 Int 类型,如果转换不成功就抛出异常。我们可以针对性的对传入控制器的**某个**参数插入这个管道: 216 | 217 | ```ts 218 | @Get(':id') 219 | async findOne(@Param('id', new ParseIntPipe()) id) { 220 | return await this.catsService.findOne(id); 221 | } 222 | ``` 223 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Node-Series 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 34 | 38 | 40 | 45 | 46 |
47 | 64 | 97 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 143 | 144 | 145 | 146 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/数据访问/TypeORM.md: -------------------------------------------------------------------------------- 1 | # Nest.js 中的数据库操作 2 | 3 | 为了与 SQL 和 NoSQL 数据库集成,Nest 提供了@ nestjs / typeorm 软件包。Nest 使用 TypeORM,因为它是 TypeScript 可用的最成熟的对象关系映射器(ORM)。由于它是用 TypeScript 编写的,因此可以与 Nest 框架很好地集成。 4 | 5 | 要开始使用它,我们首先安装所需的依赖项。在本章中,我们将演示如何使用流行的 MySQL Relational DBMS,但是 TypeORM 提供了对许多关系数据库的支持,例如 PostgreSQL,Oracle,Microsoft SQL Server,SQLite,甚至是 NoSQL 数据库,例如 MongoDB。对于 TypeORM 支持的任何数据库。 6 | 7 | ```ts 8 | import { Module } from "@nestjs/common"; 9 | import { TypeOrmModule } from "@nestjs/typeorm"; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forRoot({ 14 | type: "mysql", 15 | host: "localhost", 16 | port: 3306, 17 | username: "root", 18 | password: "root", 19 | database: "test", 20 | entities: [], 21 | synchronize: true 22 | }) 23 | ] 24 | }) 25 | export class AppModule {} 26 | ``` 27 | 28 | forRoot() 方法从 TypeORM 包接受与 createConnection() 相同的配置对象。另外,我们可以在项目根目录中创建 ormconfig.json 文件,而不是将配置对象传递给 forRoot()。 29 | 30 | ```json 31 | { 32 | "type": "mysql", 33 | "host": "localhost", 34 | "port": 3306, 35 | "username": "root", 36 | "password": "root", 37 | "database": "test", 38 | "entities": ["dist/**/*.entity{.ts,.js}"], 39 | "synchronize": true 40 | } 41 | ``` 42 | 43 | 然后在 forRoot 中不传入任何参数: 44 | 45 | ```ts 46 | import { Module } from "@nestjs/common"; 47 | import { TypeOrmModule } from "@nestjs/typeorm"; 48 | 49 | @Module({ 50 | imports: [TypeOrmModule.forRoot()] 51 | }) 52 | export class AppModule {} 53 | ``` 54 | 55 | TyperOrm 也支持异步配置: 56 | 57 | ```ts 58 | TypeOrmModule.forRootAsync({ 59 | imports: [ConfigModule], 60 | useFactory: async (configService: ConfigService) => ({ 61 | type: "mysql", 62 | host: configService.getString("HOST"), 63 | port: configService.getString("PORT"), 64 | username: configService.getString("USERNAME"), 65 | password: configService.getString("PASSWORD"), 66 | database: configService.getString("DATABASE"), 67 | entities: [__dirname + "/**/*.entity{.ts,.js}"], 68 | synchronize: true 69 | }), 70 | inject: [ConfigService] 71 | }); 72 | ``` 73 | 74 | # 数据查询 75 | 76 | 完成此操作后,TypeORM Connection 和 EntityManager 对象将可用于在整个项目中注入(而无需导入任何模块),例如: 77 | 78 | ```ts 79 | import { Connection } from "typeorm"; 80 | 81 | @Module({ 82 | imports: [TypeOrmModule.forRoot(), PhotoModule] 83 | }) 84 | export class AppModule { 85 | constructor(private readonly connection: Connection) {} 86 | } 87 | ``` 88 | 89 | ## Repository 90 | 91 | TypeORM 支持存储库设计模式,因此每个实体都有自己的存储库。这些存储库可以从数据库连接中获取,首先在 Module 中需要声明依赖: 92 | 93 | ```ts 94 | @Module({ 95 | imports: [TypeOrmModule.forFeature([Photo])], 96 | providers: [PhotoService], 97 | controllers: [PhotoController] 98 | }) 99 | export class PhotoModule {} 100 | ``` 101 | 102 | 然后在 Service 中引入 Repository: 103 | 104 | ```ts 105 | @Injectable() 106 | export class PhotoService { 107 | constructor( 108 | @InjectRepository(Photo) 109 | private readonly photoRepository: Repository 110 | ) {} 111 | 112 | findAll(): Promise { 113 | return this.photoRepository.find(); 114 | } 115 | } 116 | ``` 117 | 118 | 如果要在导入 TypeOrmModule.forFeature 的模块之外使用存储库,则需要重新导出由其生成的提供程序。您可以通过导出整个模块来做到这一点,如下所示: 119 | 120 | ```ts 121 | @Module({ 122 | imports: [TypeOrmModule.forFeature([Photo])], 123 | exports: [TypeOrmModule] 124 | }) 125 | export class PhotoModule {} 126 | ``` 127 | 128 | # 多数据库 129 | 130 | 一些项目需要多个数据库连接。这也可以通过该模块来实现。要使用多个连接,请首先创建连接。在这种情况下,连接命名成为强制性的。假设您有一个 Person 实体和一个 Album 实体,它们分别存储在各自的数据库中。 131 | 132 | ```ts 133 | const defaultOptions = { 134 | type: "postgres", 135 | port: 5432, 136 | username: "user", 137 | password: "password", 138 | database: "db", 139 | synchronize: true 140 | }; 141 | 142 | @Module({ 143 | imports: [ 144 | TypeOrmModule.forRoot({ 145 | ...defaultOptions, 146 | host: "photo_db_host", 147 | entities: [Photo] 148 | }), 149 | TypeOrmModule.forRoot({ 150 | ...defaultOptions, 151 | name: "personsConnection", 152 | host: "person_db_host", 153 | entities: [Person] 154 | }), 155 | TypeOrmModule.forRoot({ 156 | ...defaultOptions, 157 | name: "albumsConnection", 158 | host: "album_db_host", 159 | entities: [Album] 160 | }) 161 | ] 162 | }) 163 | export class AppModule {} 164 | ``` 165 | 166 | 此时,您已将每个 Photo,Person 和 Album 实体注册为各自的连接。使用此设置,您必须告诉 TypeOrmModule.forFeature()函数和@InjectRepository()装饰器应使用哪个连接。如果未传递任何连接名称,则使用默认连接。 167 | 168 | ```ts 169 | @Module({ 170 | imports: [ 171 | TypeOrmModule.forFeature([Photo]), 172 | TypeOrmModule.forFeature([Person], "personsConnection"), 173 | TypeOrmModule.forFeature([Album], "albumsConnection") 174 | ] 175 | }) 176 | export class AppModule {} 177 | ``` 178 | 179 | 然后可以注入 Connection 或 EntityManager: 180 | 181 | ```ts 182 | @Injectable() 183 | export class PersonService { 184 | constructor( 185 | @InjectConnection("personsConnection") 186 | private readonly connection: Connection, 187 | @InjectEntityManager("personsConnection") 188 | private readonly entityManager: EntityManager 189 | ) {} 190 | } 191 | ``` 192 | 193 | # Testing 194 | 195 | 在对应用程序进行单元测试时,我们通常希望避免建立数据库连接,使我们的测试套件保持独立,并尽可能快地执行它们。但是我们的类可能取决于从连接实例中拉出的存储库。我们该如何处理?解决方案是创建模拟存储库。为此,我们设置了自定义提供程序。每个注册的存储库都由 `Repository` 标记自动表示,其中 EntityName 是您的实体类的名称。 196 | 197 | ```ts 198 | @Module({ 199 | providers: [ 200 | PhotoService, 201 | { 202 | provide: getRepositoryToken(Photo), 203 | useValue: mockRepository 204 | } 205 | ] 206 | }) 207 | export class PhotoModule {} 208 | ``` 209 | 210 | 现在,替代的嘲讽存储库将用作 PhotoRepository。每当任何类使用@InjectRepository() 装饰器要求提供 PhotoRepository 时,Nest 都会使用已注册的 mockRepository 对象。 211 | 212 | # 自定义 Repository 213 | 214 | TypeORM 提供了一种称为自定义存储库的功能。自定义存储库允许您扩展基本存储库类,并使用几种特殊方法来丰富它。 215 | 216 | ```ts 217 | @EntityRepository(Author) 218 | export class AuthorRepository extends Repository {} 219 | ``` 220 | 221 | 创建类后,下一步就是将实例化责任委托给 Nest。为此,我们必须将 AuthororRepository 类传递给 TypeOrm.forFeature()方法。 222 | 223 | ```ts 224 | @Module({ 225 | imports: [TypeOrmModule.forFeature([AuthorRepository])], 226 | controller: [AuthorController], 227 | providers: [AuthorService] 228 | }) 229 | export class AuthorModule {} 230 | ``` 231 | 232 | 然后用如下方式注入: 233 | 234 | ```ts 235 | @Injectable() 236 | export class AuthorService { 237 | constructor(private readonly authorRepository: AuthorRepository) {} 238 | } 239 | ``` 240 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/MVC/依赖注入与模块.md: -------------------------------------------------------------------------------- 1 | # 依赖注入与模块 2 | 3 | Nest 和传统的 MVC 框架的区别在于它更注重于后端部分(控制器、服务与数据)的架构,视图层相对比较独立,完全可以由用户自定义配置。Nest 的分层借鉴自 Spring,更细化。随着代码库的增长 MVC 模式中 Modal 和 Controller 会变得含糊不清,导致难于维护。 4 | 5 | ![Nest vs MVC](https://s2.ax1x.com/2020/01/07/lcp9oD.png) 6 | 7 | Provider 主要的设计理念来自于控制反转(Inversion of Control,简称 IOC1)模式中的依赖注入(Dependency Injection)特性。使用 @Injectable() 装饰的类就是一个 Provider,装饰器方法会优先于类被解析执行。 8 | 9 | # Service 10 | 11 | 我们可以自己实现一个名叫 `CatsService` 的 Service,得益于 TypeScript 类型,Nest 可以通过 CatsService 类型查找到 catsService,依赖被查找并传入到控制器的构造函数中。 12 | 13 | ```ts 14 | export interface Cat { 15 | name: string; 16 | age: number; 17 | breed: string; 18 | } 19 | 20 | @Injectable() 21 | export class CatsService { 22 | private readonly cats: Cat[] = []; 23 | 24 | create(cat: Cat) { 25 | this.cats.push(cat); 26 | } 27 | 28 | findAll(): Cat[] { 29 | return this.cats; 30 | } 31 | } 32 | ``` 33 | 34 | 有了 Service 我们就可以在控制器中注入并引用到它了: 35 | 36 | ```ts 37 | @Controller("cats") 38 | export class CatsController { 39 | constructor(private readonly catsService: CatsService) {} 40 | // 等同于 41 | private readonly catsService: CatsService; 42 | constructor(catsService: CatsService) { 43 | this.catsService = catsService; 44 | } 45 | 46 | @Post() 47 | async create(@Body() createCatDto: CreateCatDto) { 48 | this.catsService.create(createCatDto); 49 | } 50 | 51 | @Get() 52 | async findAll(): Promise { 53 | return this.catsService.findAll(); 54 | } 55 | } 56 | ``` 57 | 58 | 你能发现 Controller 和 Service 处于完全解耦的状态:Controller 做的事情仅仅是接收请求,并在合适的时候调用到 Service,至于 Service 内部怎么实现的 Controller 完全不在乎。这样以来有两个好处: 59 | 60 | - 其一,Controller 和 Service 的职责边界很清晰,不存在灰色地带; 61 | - 其二,各自只关注自身职责涉及的功能,比方说 Service 通常来写业务逻辑,但它也仅仅只与业务相关。 62 | 63 | 当然你可能会觉得这很理想,时间长了增加了诸如缓存、验证等逻辑后,代码最终会变得无比庞大而难于维护。事实上这也是一个框架应该考虑和抽象出来的,后续 Nest 会有一系列的解决方法,但目前为至我们只需要了解到 Controller 和 Service 的设计原理即可。 64 | 65 | # Providers 66 | 67 | ## 注入作用域 68 | 69 | Providers 有一个和应用程序一样的生命周期。当应用启动,每个依赖都必须被获取到。 70 | 71 | ## 自定义的 Providers 72 | 73 | Nest 有一个内置的 IOC 容器,用来解析 Providers 之间的关系。这个功能相对于 DI 来讲更底层,但是功能却异常强大,@Injectable() 只是冰山一角。事实上,你可以使用值,类和同步或者异步的工厂。 74 | 75 | ### 可选的 Providers 76 | 77 | 有时候,你可以会需要一个依赖,但是这个依赖并不需要一定被容器解析出来。比如我们通常会传入一个配置对象,但是如果不传会使用一个默认值代替。可以使用 @Optional() 来装饰一个非必选的参数。 78 | 79 | ```ts 80 | @Injectable() 81 | export class HttpService { 82 | constructor( 83 | @Optional() 84 | @Inject("HTTP_OPTIONS") 85 | private readonly httpClient: T 86 | ) {} 87 | } 88 | ``` 89 | 90 | ### 基于属性的注入 91 | 92 | 前面我们提过了 Nest 实现注入是基于类的构造函数的,但是在一些特殊情况下,基于属性的注入会特别有用。比如一个顶层的类依赖一个或多个 Providers 时,通过在子类的构造函数中调用 super() 方法并不是很优雅,为了避免这种情况我们可以在属性上使用 @Inject() 装饰器。 93 | 94 | ```ts 95 | @Injectable() 96 | export class HttpService { 97 | @Inject("HTTP_OPTIONS") 98 | private readonly httpClient: T; 99 | } 100 | ``` 101 | 102 | ## 注册 Provider 103 | 104 | 一般来讲控制器就是 Service 的消费(使用)者,我们需要将这些 Service 注册到 Nest 上,这样就可以让 Nest 帮你完成注入操作。通常我们会使用 @Module 装饰器来完成注册的过程。 105 | 106 | ```ts 107 | @Module({ 108 | controllers: [CatsController], 109 | providers: [CatsService] 110 | }) 111 | export class ApplicationModule {} 112 | ``` 113 | 114 | # 模块 115 | 116 | 模块(Module)是一个使用了 @Module() 装饰的类。@Module() 装饰器提供了一些 Nest 需要使用的元数据,用来组织应用程序的结构。 117 | 118 | ![Module 示意图](https://s2.ax1x.com/2020/01/07/lciwHx.md.png) 119 | 120 | 每个应用都至少有一个根模块,根模块就是 Nest 应用的入口。Nest 会从这里查找出整个应用的依赖/调用图。@Module() 装饰器接收一个参数对象,有以下取值: 121 | 122 | | 名称 | | 功能 | 123 | | ------------- | ------------------------------------------------------------ | ---- | 124 | | `providers` | 可以被 Nest 的注入器初始化的 providers,至少会在此模块中共享 | 125 | | `controllers` | 这个模块需要用到的控制器集合 | 126 | | `imports` | 引入的其它模块集合 | 127 | | `exports` | 此模块提供的 providers 的子集,其它模块引入此模块时可用 | 128 | 129 | 模块默认会封装 providers,如果要在不同模块之间共享 provider 可以在 exports 参数中指定。 130 | 131 | ## 功能模块 132 | 133 | 使用下面的代码可以将相关的控制器和 Service 包装成一个模块: 134 | 135 | ```ts 136 | @Module({ 137 | controllers: [CatsController], 138 | providers: [CatsService] 139 | }) 140 | export class CatsModule {} 141 | ``` 142 | 143 | ## 共享的模块 144 | 145 | 在 Nest 中模块默认是单例的,因此你可在不同的模块之间共享任意 Provider 实例。 146 | 147 | ![共享的模块](https://s2.ax1x.com/2020/01/07/lciLKs.png) 148 | 149 | 模块都是共享的,我们可以通过导出当前模块的指定 Service 来实现其它模块对 Service 的复用。 150 | 151 | ```ts 152 | @Module({ 153 | controllers: [CatsController], 154 | providers: [CatsService], 155 | exports: [CatsService] // 导出 156 | }) 157 | export class CatsModule {} 158 | ``` 159 | 160 | ## 全局模块 161 | 162 | 当一些模块在你的应用频繁使用时,可以使用全局模块来避免每次都要调用的问题。Angular 会把 provider 注册到全局作用域上,然而 Nest 会默认将 provider 注册到模块作用域上。如果你没有显式地导出模块的 provider,那么其它地方就无法使用它。如果你想让一个模块随处可见,那就使用 @Global() 装饰器来装饰这个模块。 163 | 164 | ```ts 165 | @Global() 166 | @Module({ 167 | controllers: [CatsController], 168 | providers: [CatsService], 169 | exports: [CatsService] 170 | }) 171 | export class CatsModule {} 172 | ``` 173 | 174 | @Global() 装饰器可以让模块获得全局作用域,譬如我们有全局的配置模块,其关联的服务于模块声明如下: 175 | 176 | ```ts 177 | @Injectable() 178 | export class ConfigService {} 179 | 180 | @Global() 181 | @Module({ 182 | providers: [ 183 | { 184 | provide: ConfigService, 185 | useValue: new ConfigService() 186 | } 187 | ], 188 | exports: [ConfigService] 189 | }) 190 | export class ConfigModule {} 191 | ``` 192 | 193 | App Module 定义如下: 194 | 195 | ```ts 196 | @Module({ 197 | imports: [ConfigModule, FeatureModule], 198 | controllers: [AppController], 199 | providers: [AppService] 200 | }) 201 | export class AppModule {} 202 | ``` 203 | 204 | 在其他的模块服务中,我们可以直接依赖于该服务: 205 | 206 | ```ts 207 | @Injectable() 208 | export class FeatureService { 209 | constructor(private readonly configService: ConfigService) {} 210 | } 211 | ``` 212 | 213 | ## 动态模块 214 | 215 | Nest 模块系统支持动态模块的功能,这将让自定义模块的开发变得容易。 216 | 217 | ```ts 218 | import { Module, DynamicModule } from "@nestjs/common"; 219 | import { createDatabaseProviders } from "./database.providers"; 220 | import { Connection } from "./connection.provider"; 221 | 222 | @Module({ 223 | providers: [Connection] 224 | }) 225 | export class DatabaseModule { 226 | static forRoot(entities = [], options?): DynamicModule { 227 | const providers = createDatabaseProviders(options, entities); 228 | return { 229 | module: DatabaseModule, 230 | providers: providers, 231 | exports: providers 232 | }; 233 | } 234 | } 235 | ``` 236 | 237 | 模块的静态方法 forRoot 返回一个动态模块,可以是同步或者异步模块。 238 | -------------------------------------------------------------------------------- /03~Web 框架/10~Nest.js/工程实践/测试与发布.md: -------------------------------------------------------------------------------- 1 | # 测试与发布 2 | 3 | 自动化测试被视为任何认真的软件开发工作的重要组成部分。自动化使得在开发过程中快速,轻松地重复单个测试或测试套件变得容易。这有助于确保发行版符合质量和性能目标。自动化有助于扩大覆盖范围,并为开发人员提供更快的反馈循环。自动化既可以提高单个开发人员的生产率,又可以确保测试在关键的开发生命周期关头运行,例如源代码控制检入,功能集成和版本发布。 4 | 5 | 此类测试通常涉及多种类型,包括单元测试,端到端(e2e)测试,集成测试等等。虽然好处是毋庸置疑的,但设置这些好处可能很繁琐。Nest 致力于促进开发最佳实践,包括有效的测试,因此它包含以下功能,以帮助开发人员和团队构建和自动化测试。巢: 6 | 7 | - 自动为组件提供默认的单元测试和为应用程序提供端到端测试。 8 | - 提供默认工具(例如构建独立模块/应用程序加载器的测试运行器)。 9 | - 开箱即用地提供与 Jest 和 Supertest 的集成,同时与测试工具无关。 10 | - 使 Nest 依赖项注入系统可在测试环境中使用,以轻松模拟组件。 11 | 12 | 如前所述,您可以使用自己喜欢的任何测试框架,因为 Nest 不会强制使用任何特定工具。只需替换所需的元素(例如测试运行程序),您仍将享受 Nest 的现成测试设施的好处。 13 | 14 | # 单元测试 15 | 16 | 在以下示例中,我们测试了两个类:CatsController 和 CatsService。如前所述,Jest 被提供为默认测试框架。它充当测试运行器,还提供断言函数和双重测试实用程序,以帮助进行模拟,监视等。在下面的基本测试中,我们手动实例化这些类,并确保控制器和服务履行其 API 约定。 17 | 18 | ```ts 19 | describe("CatsController", () => { 20 | let catsController: CatsController; 21 | let catsService: CatsService; 22 | 23 | beforeEach(() => { 24 | catsService = new CatsService(); 25 | catsController = new CatsController(catsService); 26 | }); 27 | 28 | describe("findAll", () => { 29 | it("should return an array of cats", async () => { 30 | const result = ["test"]; 31 | jest.spyOn(catsService, "findAll").mockImplementation(() => result); 32 | 33 | expect(await catsController.findAll()).toBe(result); 34 | }); 35 | }); 36 | }); 37 | ``` 38 | 39 | 由于上面的示例是微不足道的,因此我们并未真正测试任何特定于 Nest 的内容。实际上,我们甚至没有使用依赖注入(注意,我们将 CatsService 的实例传递给我们的 catsController)。这种测试形式-我们手动实例化要测试的类-通常被称为隔离测试,因为它独立于框架。让我们介绍一些更高级的功能,这些功能可以帮助您测试更广泛使用 Nest 功能的应用程序。 40 | 41 | # 测试工具 42 | 43 | `@nestjs/testing` 软件包提供了一组实用程序,这些实用程序可实现更强大的测试过程。让我们使用内置的 Test 类重写前面的示例: 44 | 45 | ```ts 46 | describe("CatsController", () => { 47 | let catsController: CatsController; 48 | let catsService: CatsService; 49 | 50 | beforeEach(async () => { 51 | const module = await Test.createTestingModule({ 52 | controllers: [CatsController], 53 | providers: [CatsService] 54 | }).compile(); 55 | 56 | catsService = module.get(CatsService); 57 | catsController = module.get(CatsController); 58 | }); 59 | 60 | describe("findAll", () => { 61 | it("should return an array of cats", async () => { 62 | const result = ["test"]; 63 | jest.spyOn(catsService, "findAll").mockImplementation(() => result); 64 | 65 | expect(await catsController.findAll()).toBe(result); 66 | }); 67 | }); 68 | }); 69 | ``` 70 | 71 | Test 类对于提供实质上模拟整个 Nest 运行时的应用程序执行上下文很有用,但为您提供了易于管理类实例(包括模拟和覆盖)的挂钩。Test 类具有 `createTestingModule()` 方法,该方法将模块元数据对象作为其参数(与传递给 `@Module()` 装饰器的对象相同)。此方法返回 TestingModule 实例,该实例又提供了一些方法。对于单元测试,重要的是 compile() 方法。此方法使用其依赖项引导模块(类似于使用 `NestFactory.create()` 在常规 main.ts 文件中引导应用程序的方式),然后返回准备进行测试的模块。 72 | 73 | 除了使用任何提供程序的生产版本,您还可以使用自定义提供程序覆盖它以进行测试。例如,您可以模拟数据库服务而不是连接到实时数据库。我们将在下一部分中介绍替代,但是它们也可用于单元测试。 74 | 75 | # 端到端测试 76 | 77 | 与侧重于单个模块和类的单元测试不同,端到端(e2e)测试涵盖了更汇总级别的类和模块的交互-更接近最终用户与产品之间的交互类型。系统。随着应用程序的增长,很难手动测试每个 API 端点的端到端行为。自动化的端到端测试可帮助我们确保系统的整体行为正确并符合项目要求。为了执行端到端测试,我们使用与刚才在单元测试中介绍的配置类似的配置。此外,Nest 可以轻松使用 Supertest 库来模拟 HTTP 请求。 78 | 79 | ```ts 80 | describe("Cats", () => { 81 | let app: INestApplication; 82 | let catsService = { findAll: () => ["test"] }; 83 | 84 | beforeAll(async () => { 85 | const module = await Test.createTestingModule({ 86 | imports: [CatsModule] 87 | }) 88 | .overrideProvider(CatsService) 89 | .useValue(catsService) 90 | .compile(); 91 | 92 | app = module.createNestApplication(); 93 | await app.init(); 94 | }); 95 | 96 | it(`/GET cats`, () => { 97 | return request(app.getHttpServer()) 98 | .get("/cats") 99 | .expect(200) 100 | .expect({ 101 | data: catsService.findAll() 102 | }); 103 | }); 104 | 105 | afterAll(async () => { 106 | await app.close(); 107 | }); 108 | }); 109 | ``` 110 | 111 | 在此示例中,我们以前面描述的一些概念为基础。除了我们之前使用的 compile() 方法之外,我们现在还使用 createNestApplication() 方法实例化完整的 Nest 运行时环境。我们在应用程序变量中保存了对正在运行的应用程序的引用,因此我们可以使用它来模拟 HTTP 请求。 112 | 113 | 我们使用 Supertest 的 request() 函数模拟 HTTP 测试。我们希望这些 HTTP 请求路由到运行中的 Nest 应用,因此我们将 request() 函数传递给嵌套 Nest 的 HTTP 侦听器的引用(而后者又可能由 Express 平台提供)。因此,构造请求(app.getHttpServer())。对 request() 的调用将为我们提供一个包装好的 HTTP 服务器,该服务器现已连接到 Nest 应用,该应用公开了模拟实际 HTTP 请求的方法。例如,使用 request(...)。`get('/ cats')` 将向 Nest 应用发起一个请求,该请求与通过网络传入的实际 HTTP 请求(如 get/cats)相同。 114 | 115 | 在此示例中,我们还提供了 CatsService 的备用(测试两倍)实现,该实现仅返回我们可以测试的硬编码值。使用 overrideProvider() 提供此类替代实现。同样,Nest 提供的方法分别使用 overrideGuard(),overrideInterceptor(),overrideFilter() 和 overridePipe() 方法来覆盖防护,拦截器,过滤器和管道。 116 | 117 | 每个重写方法都会使用 3 种不同的方法返回一个对象,这些方法与针对自定义提供程序描述的方法类似: 118 | 119 | - useClass:您提供了一个将实例化的类,以提供实例来覆盖对象(提供者,保护者等)。 120 | - useValue:您提供一个将覆盖该对象的实例。 121 | - useFactory:提供一个函数,该函数返回将覆盖该对象的实例。 122 | 123 | ## 数据库测试 124 | 125 | 我们也可以在集成测试中对数据库进行测试: 126 | 127 | ```ts 128 | let app: INestApplication; 129 | 130 | beforeAll(async () => { 131 | const module = await Test.createTestingModule({ 132 | imports: [ 133 | UserModule, 134 | // Use the e2e_test database to run the tests 135 | TypeOrmModule.forRoot({ 136 | type: "postgres", 137 | host: "localhost", 138 | port: 5432, 139 | username: "username", 140 | password: "", 141 | database: "e2e_test", 142 | entities: ["./**/*.entity.ts"], 143 | synchronize: false 144 | }) 145 | ] 146 | }).compile(); 147 | app = module.createNestApplication(); 148 | await app.init(); 149 | }); 150 | 151 | afterAll(async () => { 152 | await app.close(); 153 | }); 154 | ``` 155 | 156 | 具体的测试用例可以编写如下: 157 | 158 | ```ts 159 | let repository: Repository; 160 | beforeAll(async () => { 161 | // ... 162 | 163 | // Add this line at the end of the beforeAll method 164 | repository = module.get("UserRepository"); 165 | }); 166 | 167 | afterEach(async () => { 168 | await repository.query(`DELETE FROM users;`); 169 | }); 170 | 171 | describe("GET /users", () => { 172 | it("should return an array of users", async () => { 173 | // Pre-populate the DB with some dummy users 174 | await repository.save([{ name: "test-name-0" }, { name: "test-name-1" }]); 175 | 176 | // Run your end-to-end test 177 | const { body } = await supertest 178 | .agent(app.getHttpServer()) 179 | .get("/users") 180 | .set("Accept", "application/json") 181 | .expect("Content-Type", /json/) 182 | .expect(200); 183 | 184 | expect(body).toEqual([ 185 | { id: expect.any(Number), name: "test-name-0" }, 186 | { id: expect.any(Number), name: "test-name-1" } 187 | ]); 188 | }); 189 | }); 190 | ``` 191 | -------------------------------------------------------------------------------- /02~工程实践/生产调优/安全加固.md: -------------------------------------------------------------------------------- 1 | # HTTP Headers 2 | 3 | ```js 4 | const express = require("express"); 5 | const PORT = process.env.PORT || 3000; 6 | const app = express(); 7 | 8 | app.get("/", (req, res) => { 9 | res.send(`

Hello World

`); 10 | }); 11 | 12 | app.listen(PORT, () => { 13 | console.log(`Listening on http://localhost:${PORT}`); 14 | }); 15 | ``` 16 | 17 | ```js 18 | ... 19 | const helmet = require('helmet'); 20 | ... 21 | 22 | app.use(helmet()); 23 | 24 | ... 25 | ``` 26 | 27 | ```js 28 | const express = require("express"); 29 | const PORT = process.env.PORT || 3000; 30 | const app = express(); 31 | 32 | app.get("/", (req, res) => { 33 | res.send(`

Hello World

`); 34 | }); 35 | 36 | app.listen(PORT, () => { 37 | console.log(`Listening on http://localhost:${PORT}`); 38 | }); 39 | ``` 40 | 41 | We’ll start by adding and removing a few HTTP headers that will help improve our security. To inspect these headers you can use a tool like curl by running: 42 | 43 | ```js 44 | curl http://localhost:3000 --include 45 | 46 | HTTP/1.1 200 OK 47 | X-Powered-By: Express 48 | Content-Type: text/html; charset=utf-8 49 | Content-Length: 20 50 | ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU" 51 | Date: Wed, 01 Nov 2017 13:36:10 GMT 52 | Connection: keep-alive 53 | ``` 54 | 55 | One header that you should keep an eye on is X-Powered-By. Generally speaking headers that start with X- are non-standard headers. This one gives away which framework you are using. For an attacker, this cuts the attack space down for them as they can concentrate on the known vulnerabilities in that framework. 56 | 57 | ```js 58 | ... 59 | const helmet = require('helmet'); 60 | ... 61 | 62 | app.use(helmet()); 63 | 64 | ... 65 | ``` 66 | 67 | Now add the helmet middleware to your application. Modify the index.js accordingly: 68 | 69 | ```sh 70 | curl http://localhost:3000 --inspect 71 | 72 | HTTP/1.1 200 OK 73 | X-DNS-Prefetch-Control: off 74 | X-Frame-Options: SAMEORIGIN 75 | Strict-Transport-Security: max-age=15552000; includeSubDomains 76 | X-Download-Options: noopen 77 | X-Content-Type-Options: nosniff 78 | X-XSS-Protection: 1; mode=block 79 | Content-Type: text/html; charset=utf-8 80 | Content-Length: 20 81 | ETag: W/"14-SsoazAISF4H46953FT6rSL7/tvU" 82 | Date: Wed, 01 Nov 2017 13:50:42 GMT 83 | Connection: keep-alive 84 | ``` 85 | 86 | The `X-Powered-By` header is gone, which is a good start, but now we have a bunch of new headers. 87 | 88 | ## X-DNS-Prefetch-Control 89 | 90 | This header isn’t much of an added security benefit as much as an added privacy benefit. By setting the value to `off` it turns off the DNS lookups that the browser does for URLs in the page. The DNS lookups are done for performance improvements and according to MDN they can [improve the performance of loading images by 5% or more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control). However this look up can make it appear like the user visited things they never visited. 91 | 92 | The default for this is `off` but if you care about the added performance benefit you can enable it by passing `{ dnsPrefetchControl: { allow: true }}` to the `helmet()` call. 93 | 94 | ## X-Frame-Options 95 | 96 | `X-Frame-Options` allows you to control whether the page can be loaded in a frame like `` `