From 6a894b22663123da5e2fff9c5ee9cefff6394586 Mon Sep 17 00:00:00 2001 From: liuyi Date: Sat, 21 Jun 2025 19:40:04 +0800 Subject: [PATCH] add db seed handler --- nest-cli.json | 1 + src/assets/posts/docker-introduce.md | 64 + src/assets/posts/goroutings.md | 197 +++ src/assets/posts/lerna.md | 221 ++++ src/assets/posts/php-di.md | 320 +++++ src/assets/posts/rbac.md | 372 ++++++ src/assets/posts/react-hooks.md | 326 +++++ src/assets/posts/typeorm-fixtures-cli.md | 442 +++++++ src/assets/posts/typescript-decorator.md | 892 ++++++++++++++ src/assets/posts/yargs.md | 1066 +++++++++++++++++ src/config/database.config.ts | 4 + src/modules/core/helpers/utils.ts | 37 + .../database/factories/content.data.ts | 134 +++ .../database/factories/content.factory.ts | 44 + src/modules/database/resolver/data.factory.ts | 5 +- .../database/seeders/content.seeder.ts | 128 ++ src/modules/database/types.ts | 2 +- src/modules/database/utils.ts | 20 + 18 files changed, 4272 insertions(+), 3 deletions(-) create mode 100644 src/assets/posts/docker-introduce.md create mode 100644 src/assets/posts/goroutings.md create mode 100644 src/assets/posts/lerna.md create mode 100644 src/assets/posts/php-di.md create mode 100644 src/assets/posts/rbac.md create mode 100644 src/assets/posts/react-hooks.md create mode 100644 src/assets/posts/typeorm-fixtures-cli.md create mode 100644 src/assets/posts/typescript-decorator.md create mode 100644 src/assets/posts/yargs.md create mode 100644 src/modules/database/factories/content.data.ts create mode 100644 src/modules/database/factories/content.factory.ts create mode 100644 src/modules/database/seeders/content.seeder.ts diff --git a/nest-cli.json b/nest-cli.json index a7b8d10..c5b3b3e 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -6,6 +6,7 @@ "assets": [ "assets/**/*" ], + "watchAssets": true, "deleteOutDir": true, "builder": "swc", "typeCheck": true, diff --git a/src/assets/posts/docker-introduce.md b/src/assets/posts/docker-introduce.md new file mode 100644 index 0000000..a39d89a --- /dev/null +++ b/src/assets/posts/docker-introduce.md @@ -0,0 +1,64 @@ +# 什么是 Docker + +**Docker** 最初是 `dotCloud` 公司创始人 [Solomon Hykes](https://github.com/shykes) 在法国期间发起的一个公司内部项目,它是基于 `dotCloud` 公司多年云服务技术的一次革新,并于 [2013 年 3 月以 Apache 2.0 授权协议开源][docker-soft],主要项目代码在 [GitHub](https://github.com/moby/moby) 上进行维护。`Docker` 项目后来还加入了 Linux 基金会,并成立推动 [开放容器联盟(OCI)](https://www.opencontainers.org/)。 + +**Docker** 自开源后受到广泛的关注和讨论,至今其 [GitHub 项目](https://github.com/moby/moby) 已经超过 5 万 4 千个星标和一万多个 `fork`。甚至由于 `Docker` 项目的火爆,在 `2013` 年底,[dotCloud 公司决定改名为 Docker](https://blog.docker.com/2013/10/dotcloud-is-becoming-docker-inc/)。`Docker` 最初是在 `Ubuntu 12.04` 上开发实现的;`Red Hat` 则从 `RHEL 6.5` 开始对 `Docker` 进行支持;`Google` 也在其 `PaaS` 产品中广泛应用 `Docker`。 + +**Docker** 使用 `Google` 公司推出的 [Go 语言](https://golang.org/) 进行开发实现,基于 `Linux` 内核的 [cgroup](https://zh.wikipedia.org/wiki/Cgroups),[namespace](https://en.wikipedia.org/wiki/Linux_namespaces),以及 [AUFS](https://en.wikipedia.org/wiki/Aufs) 类的 [Union FS](https://en.wikipedia.org/wiki/Union_mount) 等技术,对进程进行封装隔离,属于 [操作系统层面的虚拟化技术](https://en.wikipedia.org/wiki/Operating-system-level_virtualization)。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 [LXC](https://linuxcontainers.org/lxc/introduction/),从 0.7 版本以后开始去除 `LXC`,转而使用自行开发的 [libcontainer](https://github.com/docker/libcontainer),从 1.11 开始,则进一步演进为使用 [runC](https://github.com/opencontainers/runc) 和 [containerd](https://github.com/containerd/containerd)。 + +![Docker 架构](https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/media/docker-on-linux.png) + +> `runc` 是一个 Linux 命令行工具,用于根据 [OCI容器运行时规范](https://github.com/opencontainers/runtime-spec) 创建和运行容器。 + +> `containerd` 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。 + +**Docker** 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 `Docker` 技术比虚拟机技术更为轻便、快捷。 + +下面的图片比较了 **Docker** 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。 + +![传统虚拟化](_images/virtualization.png) + +![Docker](_images/docker.png) + +[docker-soft]:https://en.wikipedia.org/wiki/Docker_(software) + +# 为什么要使用 Docker? + +作为一种新兴的虚拟化方式,`Docker` 跟传统的虚拟化方式相比具有众多的优势。 + +## 更高效的利用系统资源 + +由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,`Docker` 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。 + +## 更快速的启动时间 + +传统的虚拟机技术启动应用服务往往需要数分钟,而 `Docker` 容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。 + +## 一致的运行环境 + +开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 `Docker` 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 *「这段代码在我机器上没问题啊」* 这类问题。 + +## 持续交付和部署 + +对开发和运维([DevOps](https://zh.wikipedia.org/wiki/DevOps))人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。 + +使用 `Docker` 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 [Dockerfile](../image/dockerfile/) 来进行镜像构建,并结合 [持续集成(Continuous Integration)](https://en.wikipedia.org/wiki/Continuous_integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 [持续部署(Continuous Delivery/Deployment)](https://en.wikipedia.org/wiki/Continuous_delivery) 系统进行自动部署。 + +而且使用 [`Dockerfile`](../image/build.md) 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。 + +## 更轻松的迁移 + +由于 `Docker` 确保了执行环境的一致性,使得应用的迁移更加容易。`Docker` 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。因此用户可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。 + +## 更轻松的维护和扩展 + +`Docker` 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,`Docker` 团队同各个开源项目团队一起维护了一大批高质量的 [官方镜像](https://hub.docker.com/search/?type=image&image_filter=official),既可以直接在生产环境使用,又可以作为基础进一步定制,大大的降低了应用服务的镜像制作成本。 + +## 对比传统虚拟机总结 + +| 特性 | 容器 | 虚拟机 | +| :--------- | :----------------- | :---------- | +| 启动 | 秒级 | 分钟级 | +| 硬盘使用 | 一般为 `MB` | 一般为 `GB` | +| 性能 | 接近原生 | 弱于 | +| 系统支持量 | 单机支持上千个容器 | 一般几十个 | \ No newline at end of file diff --git a/src/assets/posts/goroutings.md b/src/assets/posts/goroutings.md new file mode 100644 index 0000000..2a76f58 --- /dev/null +++ b/src/assets/posts/goroutings.md @@ -0,0 +1,197 @@ +## 概念 + +下面简单介绍一下,进程,线程,锁,协程,通道,并发,并行的概念。 + +### 进程 + +一个程序一般会开一个进程(像phpfpm这种是开多进程的),一个进程可以开N多线程。 + +### 线程 + +线程在单核计算机上是抢占执行的,也就是说同一时间单核计算机上一个进程内只有一个线程列队(线程池),但可以有多条线程在线程池,它们等待时机执行,但是同一时间段运行着的只有一个线程。这样做的好处就是异步执行,比如一些耗时的任务可以先放入线程池,等其余的线程不忙碌的时候再拿出来执行。而多核计算机则可以有多个线程池,把不同的线程池分配到不同的核心上,但是同样的一个核心上的每个线程池同时只有一个线程在运行,所以多核心上同时运行的线程数量就等于核心的数量。 + +### 协程 + +> 这里的协程直接使用goroutine概念,跟coroutine不相干 + +每个线程又可以有非常多的协程,协程在线程上成列队排序,就跟每个内核的线程池一样,并且随机执行,当某个线程上一个协程在执行过程中卡死的时候go会自动把此线程上其余协程分配到空闲的线程上执行。 + +### 通道与锁 + +一个线程池的线程列队是随机执行的,这会导致共享的数据无法确定。比如一个线程列队共享一个数据`count`,但是其中一部分线程设置`count`值,另一部分线程又需要读取`count`值,而我们又无法确定哪些线程先执行哪些后执行,这时候在一个设置`count`的线程里会导致`count`值被随意覆盖,而读取的线程里无法获得准确值。所以我们就需要**线程锁**来控制线程的执行顺序,以及锁定共享数据了,通过这种**共享内存**的方法虽然解决了问题,但也造成了不必要的风险,比如锁住后忘记解锁会导致整个程序崩溃。因为一个线程上的协程也是随机抢占(就是不自觉排队,看能力插队)执行的,这就导致了与线程同样的问题-数据不准确。 + +go直接放弃操作线程,只操作协程,并且使用简单易用的通道来解决这个问题。通道的目标就是在一部分协程间建立起一座数据通信的桥梁,使各个协程可以关联,从而通过执行顺序来控制共享数据。 + +### 并发并行 + +多个任务在同一时间片执行是为并行,多个任务在同一时间异步抢占执行是为并发。由于一个核心同时只能有一个线程运行,一个线程同时只有一个协程处于运行状态,所以协程的并行需要线程的并行,线程的并行需要多核心。所以go并行的首要条件就是多核心计算机。其中并行的每个核心其实包含了并发的协程。但是重要的是,对于单核计算机来说,协程也远比线程轻量,协程的切换和控制更加方便,所以即使单核计算机上goroutine的性能也是无可比拟的。 + +### 线程设置 + +Go默认设置的线程数等于执行go程序的计算机的核心数,但是这个数字是可以读取也可以修改的。如下: + +> 当GOMAXPROCS方法没有参数或参数<=0时为读取线程数 + +```go +// 获取核心数 +fmt.Println(runtime.NumCPU()) +// 读取当前线程数 +fmt.Println(runtime.GOMAXPROCS(0)) +// 设置线程数 +runtime.GOMAXPROCS(16) +``` + +### 协程优势 + +类似PHP(swoole等除外)这种语言是最传统的,每次执行一个单位需要过一遍整个进程。而像Java,Node(新版本),Python等编程语言对并发的处理一般使用多线程的方式解决,而一个线程是非常重量的单位,一个进程开的线程数是非常有限的,开多了很容易造成程序假死。而go协程(区别于python,kotlin等coroutines)是非常轻量的单位,一个进程就可以管理上百万个协程,所以这就使得go的并发量特别高,进一步是go程序的执行性能非常优秀。协程主要用于API请求,消息推送,聊天会话等,每次请求开一个协程,开个几十万都不是问题。 + +## 协程 + +### 初步 + +> go的入口函数`main`是所有协程的父协程 + +Go语言里使用协程非常简单,只要在调用一个函数的时候在其前面加上`go`关键字即可。比如: + +```go +package main + +import ( + "time" + "fmt" +) + +func outputName(name string) { + fmt.Println(name) +} + +func main() { + go outputName("Jack") + go outputName("Rose") + go outputName("Lily") + // 保证其它的协程执行完毕后(1秒的时间足够执行三次输出)再继续执行主协程 + time.Sleep(time.Second) + fmt.Println("do in main goroutine") +} +``` + +结果: + +> 每次执行输出的结果都是不一样的除了最后一条,因为输出人名的是放到线程里异步抢占执行的协程 + +```shell +Jack +Lily +Rose +do in main goroutine +``` + +协程也可以使用立即执行的*匿名函数*,与JS里的用法类似,比如: + +> 立即执行的意思,是立即加入到协程列队,而不是同步当场执行 + +```go +func main() { + go func() { + fmt.Println("Jack") + }() + // 保证其它的协程执行完毕后(1秒的时间足够执行三次输出)再继续执行主协程 + time.Sleep(time.Second) + fmt.Println("do in main goroutine") +} +``` + +为了不会让因子协程执行错误而导致主协程甚至整个程序挂掉,可以在有风险的子协程函数内加入一个`defer`关键字的异常处理,并在异常处理的时候使用`recover()`函数恢复异常状态为正常状态,这样可以确保在子协程执行遇到错误的时候立即结束这个协程而不至于冒泡到主协程 + +```go +func outputName(name string) { + defer func() { + if err := recover(); err != nil { + // log error + } + }() +} +``` + +### 通道 + +通道(channels)是协程间的数据通信机制。可以用通道对协程进行执行顺序和数据的控制。 + +#### 创建通道 + +通道是引用类型数据,使用`make`关键字创建,如下: + +```go +ch1 := make(chan 类型,缓冲数量) // 类型可以是int,string等基础类型,也可以是结构体指针或接口等引用类型 +``` + +#### 无缓冲通道 + +无缓冲通道就是创建通道的时候把`缓冲数量`参数的值设置为`<=0`的数值或者不设置。一个无缓冲通道要求发送goroutine和接受gorouine同时就绪才能执行。如果发送者没有准备好,则接受者会一直处于阻塞状态,反之亦然。 + +> 阻塞是某个协程处于等待数据的状态而无法向下执行 + +```go +package main + +import ( + "fmt" + "math/rand" + "time" +) + +var groups = []string{ + "jack", "rose", "Lily", "jobs", "tim", "jordan", +} + +func init() { + // 设置一个取货随机数的基准为unix时间戳 + rand.Seed(time.Now().UnixNano()) +} + +func main() { + // 设置一个判断是否全歼敌军的通道变量 + alldied := make(chan bool) + go func() { + for { + // 随机判断是否击杀 + killed := rand.Intn(2) != 0 + if !killed { + fmt.Println("本轮没有击杀敌人") + } else { + // 如果击杀则pop最后一个敌人 + groups = groups[0 : len(groups)-1] + fmt.Printf("击杀1人,剩余%d个敌人\n", len(groups)) + } + // 全部歼灭则发送通道值通知父routine并跳出循环 + if len(groups) <= 0 { + alldied <- true + break + } + // 每轮射击后休息1秒 + time.Sleep(time.Second) + } + + }() + // 阻塞到全部击杀 + <-alldied + fmt.Println("敌人全部被歼灭") +} +``` +执行结果如下 +```shell +本轮没有击杀敌人 +击杀1人,剩余5个敌人 +击杀1人,剩余4个敌人 +本轮没有击杀敌人 +击杀1人,剩余3个敌人 +本轮没有击杀敌人 +击杀1人,剩余2个敌人 +击杀1人,剩余1个敌人 +本轮没有击杀敌人 +击杀1人,剩余0个敌人 +敌人全部被歼灭 +``` + +#### 缓冲通道 + diff --git a/src/assets/posts/lerna.md b/src/assets/posts/lerna.md new file mode 100644 index 0000000..5f17e90 --- /dev/null +++ b/src/assets/posts/lerna.md @@ -0,0 +1,221 @@ +## 关于 + +将大型代码库分成单独的独立版本的packages对于代码共享非常有用。但是,跨许多仓库进行更改很麻烦且难以跟踪,并且跨仓库的测试变得非常复杂。 + +为了解决这些(以及许多其他)问题,一些项目会将其代码库组织到单个的多packages仓库中(有时称为[monorepos](https://github.com/babel/babel/blob/master/doc/design/monorepo.md))。比如像Babel,React,Angular,Ember,Meteor,Jest等以及许多其他项目都选择把所有代码包放在一个仓库中进行开发。 + +Lerna就是一种用于优化使用git和npm管理多应用包仓库工作流程的工具。 + +Lerna还可以减少开发和构建环境中复制大量的重复npm依赖包的时间和空间,这通常是将项目分成许多单独的NPM依赖包的缺点。有关详细信息,请参见 [hoist documentation](https://github.com/lerna/lerna/blob/master/doc/hoist.md)。 + +### Lerna项目的结构是怎样的? + +实际上非常轻量。您的文件结构如下所示: + +```shell +my-lerna-repo/ + package.json + packages/ + package-1/ + package.json + package-2/ + package.json +``` + +### Lerna可以做什么? + +`bootstrap`将连接仓库中的依赖。`publish`用于发布任何有更新的包。 + +### Lerna不能做什么? + +Lerna并不是 serverless monorepos的部署工具。Hoisting可能与传统的serverless monorepo部署技术不兼容。 + +## 入门 + +> 以下说明适用于Lerna3.x。对于新的Lerna项目,建议使用它而不是2.x。 + +首先,使用[npm](https://www.npmjs.com/)将Lerna安装为项目的开发依赖。 + +```shell +$ mkdir lerna-repo && cd $_ +$ npx lerna init +``` + +这将创建一个`lerna.json`配置文件以及一个`packages`文件夹,因此您的文件夹现在应如下所示: + +```shell +lerna-repo/ + packages/ + package.json + lerna.json +``` + +## 如何运行 + +Lerna允许您使用以下两种模式之一来管理项目:固定或独立。 + +### 固定/锁定模式(默认) + +> 译者注:这种模式适用于多个目录模块属于同一个仓库的情形,比如一个React/Vue组件库 + +固定模式的Lerna项目的所有包在单个版本库上操作。版本号通过项目根目录下`lerna.json`文件中`version`字段配置。当您运行时`lerna publish`,如果某个模块自上次发布以来已被更新,它将被更新为您要发布的新版本。这意味着您仅在需要时才发布packages的新版本。 + +这是[Babel](https://github.com/babel/babel)当前使用的模式。如果要自动将所有packages版本捆绑在一起,请使用此选项。这种方法的一个问题是,对任何packages进行重大更改都会导致所有packages都具有新的主要版本。 + +### 独立模式 + +> 译者注:这种模式比较适用于一个包含前后端以及多端同构的应用,比如koa+react admin+taro小程序... + +``` +lerna init --independent +``` + +独立模式Lerna项目允许维护者彼此独立地增加packages的版本。每次发布时,都会提示您已更改的每个packages,以指定是补丁,次要,主要还是自定义更改。 + +独立模式使您可以更具体地更新每个packages的版本,并使一组组件变得有意义。将该模式与[语义版本](https://github.com/semantic-release/semantic-release)类的东西相结合来解决痛点。(这个任务已经在[atlassian / lerna-semantic-release](https://github.com/atlassian/lerna-semantic-release)这个仓库上完成)。 + +> 将`lerna.json`中的`version`字段设置为`independent`,以确保独立模式运行。 + +## 故障排除 + +如果您在使用Lerna时遇到任何问题,请查阅我们的[故障排除](https://github.com/lerna/lerna/blob/master/doc/troubleshooting.md) 文档,在这里您可以找到问题的答案。 + +## 经常问的问题 + +见 [FAQ.md](https://github.com/lerna/lerna/blob/master/FAQ.md). + +## 概念 + +当Lerna 在运行命令遇到错误时,它将会记录到`lerna-debug.log`文件(与`npm-debug.log`类似)。 + +Lerna还支持 [scoped packages](https://docs.npmjs.com/misc/scope)。 + +运行`lerna --help`以查看所有可用的命令和选项。 + +### lerna.json + +```json +{ + "version": "1.1.3", + "npmClient": "npm", + "command": { + "publish": { + "ignoreChanges": ["ignored-file", "*.md"], + "message": "chore(release): publish", + "registry": "https://npm.pkg.github.com" + }, + "bootstrap": { + "ignore": "component-*", + "npmClientArgs": ["--no-package-lock"] + } + }, + "packages": ["packages/*"] +} +``` + +- `version`: 当前仓库的版本。 +- `npmClient`:指定运行命令的客户端 (也可以为每个命令指定). 如果想使用yarn来运行所有命令则改为 `"yarn"` 。默认为 "npm". +- `command.publish.ignoreChanges`:不会包含在 `lerna changed/publish`命令中的globs数组。使用此功能可以防止不必要的更改发布在新版本中,例如修正`README.md`错字。 +- `command.publish.message`:发布更新版本时的自定义提交消息。有关更多详细信息,请参见[@ lerna / version](https://github.com/lerna/lerna/blob/master/commands/version#--message-msg)。 +- `command.publish.registry`:使用它来设置要发布到的自定义registry url来替代默认的npmjs.org,如果需要身份认证,请确保您能通过认证。 +- `command.bootstrap.ignore`:运行`lerna bootstrap`命令时不会被引导的glob数组。 +- `command.bootstrap.npmClientArgs`:在运行 `lerna bootstrap`的时候,将会被作为参数直接传递给`npm install`的字符串数组。 +- `command.bootstrap.scope`:指定在运行`lerna bootstrap`命令时将会引导的packages的globs数组。 +- `packages`: 存放package位置的globs数组。 + +`lerna.json`中的packages配置是匹配到包含`package.json`文件的目录的globs列表,这个就是lerna识别“leaf” packages的方式(注意:根目录下的`package.json`用于管理整个仓库的dev依赖和脚本)。 + +默认情况下,lerna将packages列表初始化为`["packages/*"]`,但是您也可以使用其他目录,例如`["modules/*"]`或`["package1", "package2"]`。定义好的globs变量是相对于`lerna.json`文件所在目录的,该目录`lerna.json`通常是仓库的根目录。唯一的限制是您不能直接嵌套packages位置,但这也是“正常的” npm packages所共有的一个限制。 + +例如,`["packages/*", "src/**"]`匹配文件树: + +```shell +packages/ +├── foo-pkg +│ └── package.json +├── bar-pkg +│ └── package.json +├── baz-pkg +│ └── package.json +└── qux-pkg + └── package.json +src/ +├── admin +│ ├── my-app +│ │ └── package.json +│ ├── stuff +│ │ └── package.json +│ └── things +│ └── package.json +├── profile +│ └── more-things +│ └── package.json +├── property +│ ├── more-stuff +│ │ └── package.json +│ └── other-things +│ └── package.json +└── upload + └── other-stuff + └── package.json +``` + +将leaf packages放在`packages/*`下被认为是“最佳实践”,但不是使用Lerna的要求。 + +### 共享的 `devDependencies` + +大多数`devDependencies`可以通过`lerna link convert`命令推送到Lerna仓库的根目录 + +上面的命令将自动吊装并使用相对目录的`file:`说明符。 + +吊装有一些好处: + +- 所有软件包都使用给定依赖项的相同版本 +- 可以通过自动化工具(例如[GreenKeeper)](https://greenkeeper.io/)使根目录的依赖关系保持最新 +- 依赖包安装时间减少 +- 需要更少的存储空间 + +请注意,`devDependencies`仍然需要将npm脚本使用的“二进制”可执行文件直接安装在使用它们的每个软件包中。 + +请注意,`devDependencies`依赖包中如果有被npm scripts使用的”二进制“可执行文件的话,则仍然需要在需要它们的包中各自安装这些依赖。 + +例如,在以下场景下`nsp`依赖对于`lerna run nsp` 的正常运行来说是必需的(`npm run nsp`在包目录中)。 + +```json +{ + "scripts": { + "nsp": "nsp" + }, + "devDependencies": { + "nsp": "^2.3.3" + } +} +``` + +### Git主机的依赖项 + +Lerna允许将本地独立包的目标版本写成`committish`格式的[git remote url](https://docs.npmjs.com/cli/install),而不仅仅是普通的数字版本范围。当包必须是私有的并且[不使用私有的npm registry](https://www.dotconferences.com/2016/05/fabien-potencier-monolithic-repositories-vs-many-repositories)时,这允许包通过git仓库获取。`committish``#v1.0.0``#semver:^1.0.0` + +请注意,lerna并*没有*执行把git history实际分离到独立的只读存储库。这是仓库管理者的责任。(有关实现的详细信息,请参[见此评论](https://github.com/lerna/lerna/pull/1033#issuecomment-335894690)) + +```json +// packages/pkg-1/package.json +{ + name: "pkg-1", + version: "1.0.0", + dependencies: { + "pkg-2": "github:example-user/pkg-2#v1.0.0" + } +} + +// packages/pkg-2/package.json +{ + name: "pkg-2", + version: "1.0.0" +} +``` + +在上面的示例中, + +- `lerna bootstrap`将正确链接`pkg-2`到`pkg-1`。 +- `lerna publishd`当`pkg-2`更改时,将会在`pkg-1`中更新committish(`#v1.0.0`) \ No newline at end of file diff --git a/src/assets/posts/php-di.md b/src/assets/posts/php-di.md new file mode 100644 index 0000000..061bba8 --- /dev/null +++ b/src/assets/posts/php-di.md @@ -0,0 +1,320 @@ +### 基本概念 + +#### 依赖注入 + +> 依赖注入是一种允许我们从硬编码的依赖中解耦出来,从而在运行时或者编译时能够修改的软件设计模式。 + +以下代码解释了依赖注入的方便性 + +```php +// SendMessage.php +sdk = $sdk; + } + public function send($content) + { + $this->sdk->Xsend($this->phone,$content); + } + ... +} +``` + +```php +//SendController.php +... +public function sendMessage(SendMessageInterface $send) +{ + $content = 'Hello My God!'; + $send->setPhone('+86.xxxxxxxxxxx')->send($content); +} +``` + +```php +//index.php +$controller = new SendController(); +$controller->sendMessage($container->make('send.message')); +``` + +依赖注入的优点: + +- 如果所有的类和函数直接类名或函数名来获取对象,那么由于高度的耦合度测试将显得相当困难。 +- 注入接口的好处是在类内部实现有所改变时我们将只需更改类本身而不需关心调用他的其它类或者代码 + +#### 服务容器 + +服务容器包含了2个东西。 + +1. 用于实现依赖注入的服务解析器 +2. 用于管理依赖注入的容器 + +由于PHP原生并不支持依赖注入,所以我们要实现依赖注入则首先必须实现服务的解析,可以解析后即可实现用于管理所有服务的容器。为了方便,一般在PHP框架及类库中一体化服务与容器. + +### 轻松实现 + +> 具体代码请查看本课时源代码中的step1 + +我们自己可以仿造Laravel或者Symfony的模式来轻松的实现一个自己的服务容器 + +#### 创建容器 + +整个服务容器的所有服务皆存储与Container容器类中的$app变量中 + +```php +// Container.php + ... + public function bind($name, $definition){} //绑定服务 + public function make($name){} //解析服务 + public function alias($serviceName,$alias){} //设置服务的别名 + public function all(){} //获取所有服务 + public function has($name){} //判断服务是否存在在容器内 +``` + +#### 实现容器 + +> Reflection类库用于PHP的反转控制,是实现DI的核心 + +在实现过程中,我们用的最多的是ReflectionClass类和ReflectionFunction类,这两个类用于处理PHP的类和函数的反射机制. + +它们都是Reflection类的子集,具体使用请查看[官方API](http://php.net/manual/zh/book.reflection.php),这里就不再赘述。。。 + +我们假定服务有三种模式 + +- 闭包函数服务 +- 对象绑定服务 +- 类解析服务 + +那么我们要在bind(绑定方法)和make(解析方法)中分别对着三种模式进行处理 + +比如我们添加了factory方法与getArguments方法 + +factroy方法用于在解析的时候分别处理这三种模式 + +1. 对象绑定模式则直接返回对象即可 +2. 闭包函数模式,则可能会在函数的参数中注入其它服务,所以要单独处理 +3. 类解析服务则可能会在类的构造函数中注入其它服务,所以要单独处理 + +最后我们单独编写一个getArguments方法用于处理参数中含有其它服务的方法用于获取解析后的参数用于factory方法,还有一个值得注意的是alias,我们给一个处理绑定了服务名之后,我们添加一个alias方法,就可以为这个服务设置别名了.我们这里模仿laravel使alias可以为数组亦可为单个字符串别名(如果设置接口或者类名,在数组中会自动转换为字符串) + +#### 测试容器 + +在实现服务容器后,我们编写一些代码用于测试服务容器是否有效 + +> 我们这里的服务容器是参照Laravel的模式编写,但是建议大家尝试改进,可以模仿Symfony的方法.Symfony是用service.yaml的方式来标准化服务容器,代码较Laravel更加优雅亦读 + +```php +// services.php +... +$container = new Container(); +$container->bind('onedriver',function (){ + return new OneDriver(); +}); +$container->bind('send.message',SendMessage::class); +$container->alias('onedriver',OneDriver::class); +$container->alias('send.message',[ + SendMessageInterface::class +]); +``` + +绑定所有服务和设置别名之后即可访问index.php看到效果 + +上面我们自己写的服务容器为了简便起见很多没有实现,有兴趣的朋友可以结合Composer的自动加载类模式,参照Laravel和Symfony的方案自己扩展一下,就可以实现完整的服务容器方案咯 + +### Laravel与依赖注入 + +#### 容器方法 + +在Laravel中,依赖注入也是采用服务容器的设计模式,以下是它的一些常用容器方法,具体可以去看他的[API文档](https://laravel-china.org/api/5.4/Illuminate/Container/Container.html) + +> 我们上面的实现为了方便简易的完成了,大家可以看到下面Laravel就做的比较细致了,比如闭包函数处理模式和普通的类绑定就用一个bind方法,而对象绑定就单独一个instance,设置还设置了singleton这种一次性解析方法,还有tag模式等,大家可以参考一下 + +- $app->bound(判断服务是否存在) +- $app->resolved(服务解析事件) +- $app->bind(普通的绑定服务模式,当然通过最后一个shared参数就可以设置成共享服务) +- $app->bindIf(按条件绑定) +- $app->singleton(只解析一次,产生的对象后面就一直使用了,可以理解为共享服务) +- $app->instance(直接以对象做为服务来绑定,那当然也是共享服务了) +- $app->make(解析服务) +- $app->tag(创建服务标签,用于归类服务) +- $app->tagged(解析服务标签得到服务数组,并按绑定时候顺序可以通过整数键值获取服务) +- $app->offsetXXX(用于设置,读取,判断一些全局变量,比如你要设置一个系统变量,那么就用他) +- $app->alias(老朋友了,我们上面就自己实现了,用于设置服务别名) +- $app->isAlias和$app->isShared(这就不用解释了) +- $app->when,$app->needs,$app->give(画蛇添足的地方,其实按条件解析如果实现同一个接口注入后,在依赖类中使用一个give方法就解析不同服务不是更好吗?用when,needs不仅没解决问题还增加了耦合度。。。) +- 。。。其它的自己看一下就明白了 + +#### 具体使用 + +因为Laravel是使用Composer机制,所以不需要像我们上面写的一样,自己require文件,设置好命名空间就能加载类了,这里重要讲解的是Laravel的Provider机制 + +> 类似Facades这种样子货大家自己看一下[文档](https://laravel-china.org/docs/5.4/facades)就明白了 + +首先大家要明白一点,Laravel中的Constract,Provider,Facades这些概念并不是什么高端的东西,非常好理解,我们还是回到我们上面自己写的服务容器案例。 + +对比一下Laravel的文档可以清楚的知道,所谓Constract不过是interface而已-接口,用于提供一套公有的方法,让具体实现的类来继承,然后通过服务绑定就可以解析了 + +##### 创建服务 + +$app->bind(服务名,"具体实现") + +> "具体实现"可以是类,对象或者闭包什么的,绑定方法可以按自己的需要选择bind,singleton以及instance, +> +> 也可以根据条件绑定(bindIf),或者根据依赖类绑定(when,needs,give) + +接着$app->alias(服务名,[一些Contract接口]) + +> 如果"具体实现"是一个类最好是继承这些接口,显得标准点,如果不是就别理会了 + +```php +/** + * 首先定义一个接口和一个类,这里省略命名空间 + */ +namespace App\Services\Message\Contracts; +interface SendMessage +{ + public function say(); +} + +namespace App\Services\Message; +class SendMessage implements SendMessageInterface +{ + public function say() + { + echo 'Laravel is good!'; + } +} +``` + +##### 创建提供者 + +在你的AppServiceProvider或者自定义的Provider中的register方法中这样做 + +```php +... +use App\Services\Message\Contracts\SendMessage as SendMessageInterface; +class MessageServiceProvider extends ServiceProvider +{ + public function register() + { + $this->app->singleton('message.send',SendMessage::class); + $this->app->alias('message.send',SendMessageInterface::class); + } +} +``` + +##### 注册提供者 + +> 如果你的绑定是放在AppServiceProvider或者其它Laravel应用自动生成的提供者里面则不用管这一步了,因为这些提供者已经注册好了 + +有两种方法可以绑定注册提供者 + +1. 可以选择在config/app.php的'providers'数组中加载你的MessageServiceProvider +2. 在AppServiceProvider中的register方法里面用$this->app->register(MessageServiceProvider::class)注册 + +```php +//config/app.php +... + 'providers' => [ + App\Services\Message\MessageServiceProvider::class, + ] +``` + +##### 使用门面 + +如果你想要个Facades(门面)就文档自己弄一个Facades类就可以了,当然也可以在MessageServiceProvider的boot方法中使用 + +```php +public function boot() +{ + AliasLoader::getInstance +} +``` + +> Laravel5.4可以自动让任何类变成门面 + +你用5.4版本并且你的服务仅仅是一个类的话的话直接可以像这样调用门面,而不需要任何其它附加的注册 + +```php + use Facades\ { + App\Services\Message\SendMessage; + }; +``` + +##### 解析服务 + +> 当然也可以使用门面,但是不建议多使用,这东西很废(在blade或twig模板里除外) + +在控制器中通过Constract接口注入请求方法 + +```php +use App\Services\Message\Contracts\SendMessage; +class ArticleController extends Controller +{ + public function create(SendMessage $message) + { + $hello->say(); + } +} +``` + +在控制器中通过构造方法注入Application接口调用或者在构造方法中直接注入Contract接口 + +```php + +class ArticleController extends Controller +{ + protected $app; + public function __(Application $app) + { + $this->app = $app; + } + public function say() + { + $hello = $this->app->make('message.send'); //或者 + $hello = $this->app['message.send']; //或者 + $hello = app('message.send'); + // 链式写法 + $this->app->make('hmessage.send')->say(); + } +} +``` + +在其它非控制器类中使用 + +> 如果SendMessage使用的地方不方便注入Application的话,可以丢弃这个构造函数,直接使用app('message.send')获取服务 + +```php +/** + * 在其它非控制器类中使用 + */ + +class CustomClass +{ + public function say(message.send $hello) + { + $hello->say(); + } +} + +class UserSay +{ + protected $app; + public function __construct(Application $app) + { + $this->app = $app; + } + public function say() + { + $custom = new CustomClass(); + $custom->say($this->app->make('message.send')); + } +} +``` +closure \ No newline at end of file diff --git a/src/assets/posts/rbac.md b/src/assets/posts/rbac.md new file mode 100644 index 0000000..1a191d1 --- /dev/null +++ b/src/assets/posts/rbac.md @@ -0,0 +1,372 @@ +

+ AccessControl.js +

+ + + +### 基于角色和属性的Node.js访问控制 + +虽然许多[RBAC][rbac](基于角色的访问控制)实现上有所不同,但基础知识都是一样的,这种模式也被广泛采用,因为它模拟了真实生活角色(任务)分配。 但是数据变得越来越复杂; 您需要在资源,功能甚至环境中定义策略。 这称为 [ABAC][abac] (基于属性的访问控制)。 + +我们需要合并以上两者的最佳特性(见[NIST paper][nist-paper]); 这个node库不仅实现了RBAC基础知识,并且还关注* resource *和* action *属性。 + +## 核心功能 + +- 链式的,友好的API。 + 例如`ac.can(role).create(resource)` +- 角色分层**继承**。 +- 可**一次**定义授权(例如从数据库结果)也可以**逐个**定义授权。 +- 通过**glob表示法**定义的属性授予/拒绝权限(支持嵌套对象)。 +- 能够设置可允许的属性**过滤**数据(模型)实例。 +- 能够控制**自己创建**的或**任何**资源的访问。 +- 能够**锁定**基础授权模型。 +- 没有**静默**错误。 +- **快**。(授权存储在内存中,没有数据库查询。) +- **经过**严格**测试**。 +- TypeScript支持。 + +*为了构建更加健壮的应用,这个库(v1.5.0 +)完全用TypeScript重写* + +## 安装 + +使用 [**npm**](https://www.npmjs.com/package/accesscontrol): `npm i accesscontrol --save` +使用 [**yarn**](https://yarn.pm/accesscontrol): `yarn add accesscontrol` + +## 使用指南 + +```js +const AccessControl = require('accesscontrol'); +// or: +// import { AccessControl } from 'accesscontrol'; +``` + +### 基础示例 + +逐个定义 roles(权限) 和grants(角色) +```js +const ac = new AccessControl(); +ac.grant('user') // 定义新角色或修改现有角色。也可以是一个数组。 + .createOwn('video') // 与.createOwn('video', ['*'])相同 ['*']为默认参数 + .deleteOwn('video') + .readAny('video') + .grant('admin') // 切换到另一个角色而不破坏操作链 + .extend('user') // 继承角色功能。一样,也可以是一个数组 + .updateAny('video', ['title']) // 明确定义可操作的属性 + .deleteAny('video'); + +const permission = ac.can('user').createOwn('video'); +console.log(permission.granted); // —> true +console.log(permission.attributes); // —> ['*'] (所有属性) + +permission = ac.can('admin').updateAny('video'); +console.log(permission.granted); // —> true +console.log(permission.attributes); // —> ['title'] +``` + +### Express.js 示例 + +检查所请求资源和操作的角色权限,如果已授权则返回权限筛选出的属性进行响应 + +```js +const ac = new AccessControl(grants); +// ... +router.get('/videos/:title', function (req, res, next) { + const permission = ac.can(req.user.role).readAny('video'); + if (permission.granted) { + Video.find(req.params.title, function (err, data) { + if (err || !data) return res.status(404).end(); + // filter data by permission attributes and send. + res.json(permission.filter(data)); + }); + } else { + // resource is forbidden for this user/role + res.status(403).end(); + } +}); +``` + +## 角色 + +您可以通过轻松地调用`AccessControl`实例上的方法`.grant()`或`.deny()`方法来创建/定义角色。 + +- 角色也可以继承自其它角色. + +```js +// 用户角色继承查看者角色权限 +ac.grant('user').extend('viewer'); +// 管理员角色继承普通用户和编辑员的角色权限 +ac.grant('admin').extend(['user', 'editor']); +// 管理员和超级管理员角色都继承了版主权限 +ac.grant(['admin', 'superadmin']).extend('moderator'); +``` + +- 继承是通过引用完成的,因此您可以在继承角色之前或之后授予资源权限。 + +```js +// 案例 #1 +ac.grant('admin').extend('user') // 假设用户角色已经存在 + .grant('user').createOwn('video'); + +// 案例 #2 +ac.grant('user').createOwn('video') + .grant('admin').extend('user'); + +// 以下结果对于两种情况都是相同的 +const permission = ac.can('admin').createOwn('video'); +console.log(permission.granted); // true +``` + + +继承说明: + +- 角色不能自我继承。 +- 不允许交叉继承。 + 例如`ac.grant('user').extend('admin').grant('admin').extend('user')`将抛出异常。 +- 角色不能(预)继承不存在的角色。换句话说,您应该首先创建基本角色。例如`ac.grant('baseRole').grant('role').extend('baseRole')` + +## 动作和动作属性 + +[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)操作是您可以对资源执行的操作。有两个动作属性定义了资源的**所有权**:*own*和*any*。 + +例如,一个`admin`角色可以`create`,`read`,`update`或`delete`(CRUD)**任何** `account`资源。但是,一个`user`角色可能只`read`或`update`它**自己的** `account`资源。 + + + + + + + + + + + + + + + + + + + +
操作所有权
+ Create
+ Read
+ Update
+ Delete
+
自己的资源可以(或不可以)对当前请求的自己的资源执行C|R|U|D 操作。
任何资源可以(或不可以)对任何资源执行C|R|U|D 操作,包括自己的资源。
+ +```js +ac.grant('role').readOwn('resource'); +ac.deny('role').deleteAny('resource'); +``` + +*请注意,操作**自己的资源**也要求您检查实际所有权。[查看此处](https://github.com/onury/accesscontrol/issues/14#issuecomment-328316670) 获取更多信息。* + +## 资源和资源属性 + +多个角色可以访问特定资源。但是根据上下文,您可能需要限制特定角色可访问的资源内容。 + +这可以通过资源属性实现。您可以使用Glob表示法来定义允许或拒绝的资源属性。 + +例如,我们有一个`video`具有以下属性的资源:`id`,`title`和`runtime`。`admin`角色可以读取*任何* `video`资源的所有属性: + +```js +ac.grant('admin').readAny('video', ['*']); +// 也可以这样写: +// ac.grant('admin').readAny('video'); +``` +但是`id`属性不应该被`user`角色读取。 + +```js +ac.grant('user').readOwn('video', ['*', '!id']); +// 也可以这样写: +// ac.grant('user').readOwn('video', ['title', 'runtime']); +``` + +你也可以嵌套对象 (属性). +```js +ac.grant('user').readOwn('account', ['*', '!record.id']); +``` + +## 检查权限和过滤属性 + +你可以调用`AccessControl`实例的`.can().()`方法来检查对指定资源和行为的权限。 + +```js +const permission = ac.can('user').readOwn('account'); +permission.granted; // true +permission.attributes; // ['*', '!record.id'] +permission.filter(data); // filtered data (without record.id) +``` +见 [express.js 示例](#expressjs-example). + +## 一次定义所有授权 + +你可以一次把所有授权传递给 `AccessControl` 的构造方法. +它也可以接受一个对象 `Object`: + +```js +// 这实际上是如何在内部维护授权的方案 +let grantsObject = { + admin: { + video: { + 'create:any': ['*', '!views'], + 'read:any': ['*'], + 'update:any': ['*', '!views'], + 'delete:any': ['*'] + } + }, + user: { + video: { + 'create:own': ['*', '!rating', '!views'], + 'read:own': ['*'], + 'update:own': ['*', '!rating', '!views'], + 'delete:own': ['*'] + } + } +}; +const ac = new AccessControl(grantsObject); +``` +... 也可以传递一个 数组 (从数据库中获取时很有用): +```js +// 从数据库获取的授权列表 (必须在内部转换为如下格式的有效的grants对象) +let grantList = [ + { role: 'admin', resource: 'video', action: 'create:any', attributes: '*, !views' }, + { role: 'admin', resource: 'video', action: 'read:any', attributes: '*' }, + { role: 'admin', resource: 'video', action: 'update:any', attributes: '*, !views' }, + { role: 'admin', resource: 'video', action: 'delete:any', attributes: '*' }, + + { role: 'user', resource: 'video', action: 'create:own', attributes: '*, !rating, !views' }, + { role: 'user', resource: 'video', action: 'read:any', attributes: '*' }, + { role: 'user', resource: 'video', action: 'update:own', attributes: '*, !rating, !views' }, + { role: 'user', resource: 'video', action: 'delete:own', attributes: '*' } +]; +const ac = new AccessControl(grantList); +``` +你可以随时设置授权... +```js +const ac = new AccessControl(); +ac.setGrants(grantsObject); +console.log(ac.getGrants()); +``` +...除非你锁定它: +```js +ac.lock().setGrants({}); // throws after locked +``` + +## 适配nest.js + +#### 一个基于[onury/accesscontrol](https://github.com/onury/accesscontrol)实现的Nestjs权限控制模块 + +#### 本模块提供什么 ? + +在这个模块中,您将拥有开箱即用的以下所有功能(只用于Nest.js)。 + +* 它是**基于装饰器的,**因为大多数时候你会在你的路由中使用装饰器。 +* 内置**ACGuard**,您可以直接使用它。 +* 从任何地方都可以访问底层的**AccessControl**对象。 + +## 安装 + +* NPM: + +```bash +npm install nest-access-control --save +``` + +* Yarn: + +```bash +yarn add nest-access-control +``` + +--- + +#### 示例 + +> 查看示例目录以获取更多代码; + +加入我们需要构建视频服务,以便用户可以与他人分享视频,但我们需要一些`admins`来控制这些视频。 + +1. 首先让我们定义角色: + +为了构建我们的角色,我们需要这个`RolesBuilder`类,它继承自`accesscontrol`包的`AccessControl`类。 + +```ts +// app.roles.ts + +export enum AppRoles { + USER_CREATE_ANY_VIDEO = 'USER_CREATE_ANY_VIDEO', + ADMIN_UPDATE_OWN_VIDEO = 'ADMIN_UPDATE_OWN_VIDEO', +} + +export const roles: RolesBuilder = new RolesBuilder(); + +roles + .grant(AppRoles.USER_CREATE_ANY_VIDEO) // define new or modify existing role. also takes an array. + .createOwn('video') // equivalent to .createOwn('video', ['*']) + .deleteOwn('video') + .readAny('video') + .grant(AppRoles.ADMIN_UPDATE_OWN_VIDEO) // switch to another role without breaking the chain + .extend(AppRoles.USER_CREATE_ANY_VIDEO) // inherit role capabilities. also takes an array + .updateAny('video', ['title']) // explicitly defined attributes + .deleteAny('video'); +``` + +> 专家提示 👍 :请将所有角色组织在一个文件中,例如: `app.roles.ts`。 + +2. 接着让我们在跟模块中使用`AccessControlModule`注册角色: + +```ts +// app.module.ts + +import { roles } from './app.roles'; + +@Module({ + imports: [AccessControlModule.forRoles(roles)], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} +``` + +直到现在一切都很好,现在让我们构建我们的应用程序,假设我们有视频名称列表,用户可以 - *根据我们的角色* - `create:own`新视频或`read:any`视频,好的,让我们开始构建它 + +```ts +// app.controller.ts +... +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + @UseGuards(AuthGuard, ACGuard) + @UseRoles({ + resource: 'video', + action: 'read', + possession: 'any', + }) + @Get() + root(@UserRoles() userRoles: any) { + return this.appService.root(userRoles); + } +} +``` + +那么让我们讨论一下发生了什么! + +首先我们介绍了两个新装饰器,实际上它们是三个,但让我们看看它们能做什么: + +- `@UseRoles({ ... })`:这是最常用的装饰器,它定义用户允许访问此路由的角色。它可以设置一个或多个角色,但请记住,**必须**满足所有角色。角色的结构非常简单,例如,我们在这里定义了我们拥有的资源,以及**ACGuard \*** - 将检查用户角色,然后如果用户角色具有访问此资源的权限,则守卫将返回`true`,否则将抛出一个`ForbiddenException`。关于角色的结构的更多信息请参阅`roles.interface.ts`文件或读取`accesscontrol`库的[原始文档](https://onury.io/accesscontrol/)。 +- `UserRoles()`:如果你想直接访问用户角色,也许你就想手动检查它的角色,而不是让`ACGuard`为你做这些,然后你就会寻找`ACGuard`这个装饰器。这个装饰器它其实很简单,它只是获取`req.user.roles`从`request`对象返回值,但是等等,如果用户的角色中不存在`prop: role`,我们知道你会问这个问题,这样你就可以将一个可选的属性键传递给装饰器了以便从用户对象获取它,例如`@UserRoles('permissions')`将返回`req.user.permissions`。 +- `@InjectRolesBuilder()`: If you hate the `ACGuard` - _imo it's a good guard_ - and want to build your own Guard instead, you will likely need to access to the underlying `RolesBuilder` Object , then that decorator for you, it will inject the `Roles` you have defined before, i.e the object passed to the `AccessControlModule.forRoles(roles)`. +- `@InjectRolesBuilder()`:如果你不喜欢的`ACGuard`- *这是一个很好的守卫* -如果你想建立自己的守卫代替它,您可能需要访问底层`RolesBuilder`对象,那么这个装饰器将会注入你之前已定义的`Roles`,即传递给`AccessControlModule.forRoles(roles)`的对象。 + +#### 限制 + +首先,这个模块假设以下情况 + +1. 在 `req.user`存在用户对象 +2. 您可以自己构建`AuthGuard`将`user`对象附加到`req`对象的内容,[查看详细方法](https://docs.nestjs.com/guards) +3. the `AuthGuard` must be registered before roles guard, in this case it's `ACGuard`, and of course you can combine the `AuthGuard` and `ACGuard` in one guard, and use it everywhere. +4. `AuthGuard`必须在roles守卫之前注册,在这个案例中roles守卫就是`ACGuard`,当然你可以把`AuthGuard`和`ACGuard`放在一个守卫中,并在任何地方使用它。 + +其次,我不认为这些是限制,因为你可以轻松地建立自己的守卫,而不再需要内置的了。 \ No newline at end of file diff --git a/src/assets/posts/react-hooks.md b/src/assets/posts/react-hooks.md new file mode 100644 index 0000000..21f6183 --- /dev/null +++ b/src/assets/posts/react-hooks.md @@ -0,0 +1,326 @@ +## 内置hooks + + + +### UseState + +```typescript +const App: FC = () => { + const [count, setCount] = useState(0); + const [isShow, toggleShow] = useState(true); + return ( + <> +

{count}

+ +

{isShow ? I'm show now : null}

+ + + ); +}; +``` + + + +![](https://pic.phpna.com/react-notes/20190920010500.gif) + +### UseEffect + +函数体用于注册事件,返回值用于注销事件 + +#### 没有第二个参数 + +1. 组件挂载(首次渲染)只执行useEffect函数体 +2. 组件更新先执行返回值函数,再执行函数体 +3. 组件卸载只执行返回值函数 + +#### 第二个参数为空数组 + +1. 组件挂载(首次渲染)只执行函数体 +2. 组件更新不执行,直接跳过 +3. 组件卸载只执行返回值函数 + +#### 第二个参数为可变值 + +1. 组件挂载(可变值初始化后)只执行函数体 +2. 只当可变值变化先执行返回值函数,再执行函数体 +3. 组件卸载只执行返回值函数 + +![](https://pic.phpna.com/react-notes/20190920023556.png) + +示例: 写一个拖动浏览器实时显示窗口宽度的组件,注意不要忘记把这个组件放入`App`组件中 + +```tsx +const EffectComponent: FC = () => { + const [width, setWidth] = useState(window.innerWidth); + const resizeHandle = () => { + console.log(window.innerWidth); + setWidth(window.innerWidth); + }; + useEffect(() => { + window.addEventListener("resize", resizeHandle); + return () => { + window.removeEventListener("resize", resizeHandle); + }; + }, [width]); + return ( + <> +

{width}

+ + ); +}; +``` + +看一下效果 + +![](https://pic.phpna.com/react-notes/20190920034745.gif) + + + +### useContext + +创建演示组件 + +```tsx +// components/ContextDemo.tsx +... +import { LocalProps, LangType } from "../typing"; + +const langs: LangType[] = [ + { name: "en", label: "english" }, + { name: "zh-CN", label: "简体中文" } +]; + +const localContext = createContext(langs[0]); + +const LocalContextProvider: FC = props => { + const [localLang, setLocalLang] = useState( + !props.lang ? props.lang! : langs[0] + ); + useEffect(() => { + setLocalLang(props.lang!); + }, [props.lang]); + return ( + <> + + {props.children} + + + ); +}; + +const LocalSelector: FC<{ setLang: Function }> = props => { + const currentLang = useContext(localContext); + const changeCurrentLang = (event: ChangeEvent) => { + props.setLang(langs.find(lang => lang.name === event.target.value)); + }; + return ( + <> + + + ); +}; + +const contextDemo = { + langs, + localContext, + LocalContextProvider, + LocalSelector +}; +export default contextDemo; + +``` + +在根组件导入组件 + +```tsx +// index.tsx +... +const App: FC = () => { + const [lang, setLang] = useState(langs[0]); + return ( + + ... +

useContext Demo

+ setLang(lang)} /> +
+ ); +}; + +``` + +在`EffectDemo`组件使用`useContext`显示当前语言 + +```tsx +... +const currentLang = useContext(ContextDemo.localContext); +

+ current lang is{" "} + {currentLang.label ? currentLang.label! : currentLang.name} +

+``` + +看一下效果 + +![](https://pic.phpna.com/react-notes/20190922195237.gif) + + + +### useReducer + +```tsx +// components/ReducerDemo.tsx +... +// 初始化时默认主题名称 +const defaultThemeName: string = "dark"; + +// 主题列表 +const themes: ThemeType[] = [ + { name: "dark", label: "暗黑" }, + { name: "light", label: "明亮" } +]; + +// 过滤主题列表(删除非默认主题的isDefault属性) +const filterThemes = (data: ThemeType[]) => + data.map(theme => { + if (theme.isDefault !== undefined && !theme.isDefault) { + delete theme.isDefault; + } + return theme; + }); + +// 获取初始化主题列表(设置初始化默认主题为当前默认主题),此函数用于初始化reducer的值 +const getInitThemes = (data: ThemeType[]) => { + return filterThemes( + data.map(theme => { + theme.isDefault = theme.name === defaultThemeName; + return theme; + }) + ); +}; + +// 主题操作,为了便于演示这里我们只添加了一个切换默认主题的操作 +const ThemeReducer = (state: ThemeType[], action: ThemeActionType) => { + switch (action.type) { + case "CHANGE_THEME": + return filterThemes( + state.map(theme => { + if (theme.name === action.value.name) { + return { ...theme, isDefault: true }; + } + return { ...theme, isDefault: false }; + }) + ); + default: + return filterThemes(state); + } +}; + +// 获取默认当前主题 +const getDefaultTheme = (data: ThemeType[]) => + filterThemes(data).find(theme => theme.isDefault!); + +// 创建主题列表的全局变量 +const themesContext = createContext< + [ThemeType[], Dispatch] | null +>(null); + +// 创建当前默认主题的全局变量 +const defaultThemeContext = createContext( + getInitThemes(themes).find(theme => theme.name === defaultThemeName) +); + +// 创建一个Provider高阶组件 +const ThemeContextProvider: FC<{ + children: ReactElement[] | ReactElement; +}> = props => { + // 把{主题列表状态,触发状态改变的dispatch}包装为全局变量 + const contextValue = useReducer(ThemeReducer, themes, getInitThemes); + // 根据主题列表状态的变化使用getDefaultTheme动态的获取当前默认主题 + const [currentThemes] = contextValue; + return ( + <> + + + {props.children} + + + + ); +}; + +// 自定义一个hooks用于其它组件便捷地通过themesContext全局变量来获取主题列表的state和dispatch +const useThemesReducer = () => { + const contextValue = useContext(themesContext); + return contextValue; +}; + +// 自定义一个Hooks通过全局变量defaultThemeContext来快捷地获取当前默认主题 +const useTheme = (): ThemeType => { + const contextValue = useContext(defaultThemeContext); + return contextValue!; +}; + +// 根据主题对象获取主题的显示名 +const getThemeText = (theme: ThemeType): string => { + return theme.label ? theme.label : theme.name; +}; + +// 主题选择器组件 +const ThemeSelector: FC = () => { + const [themes, dispatch] = useThemesReducer()!; + const defaultTheme = useTheme(); + const changeDefaultTheme = (event: ChangeEvent) => { + dispatch({ + type: ThemeActionName.CHANGE_THEME, + value: themes.find(theme => theme.name === event.target.value)! + }); + }; + return ( + <> +

{defaultTheme.name}

+ + + ); +}; + +export default { + useTheme, + useThemesReducer, + getThemeText, + ThemeContextProvider, + ThemeSelector +}; +``` + +```tsx +// index.tsx + + ... + + +``` + +在`EffectDemo`组件测试当前默认主题切换 + +```tsx +... +

current theme is {ReducerDemo.getThemeText(currentTheme)}

+``` + +![](https://pic.phpna.com/react-notes/20190923005620.gif) diff --git a/src/assets/posts/typeorm-fixtures-cli.md b/src/assets/posts/typeorm-fixtures-cli.md new file mode 100644 index 0000000..e97a755 --- /dev/null +++ b/src/assets/posts/typeorm-fixtures-cli.md @@ -0,0 +1,442 @@ +依赖于[faker.js](https://github.com/marak/Faker.js/),typeorm-fixtures-cli允许您在开发或测试项目时创建大量的数据填充/假数据。它为您提供了一些基本工具,使您可以轻松地以编写用于生成复杂数据的易于读写的规则,以便团队中的每个人都可以根据需要生成自己的测试数据。 + +## 安装 + +#### NPM + +```bash +npm install typeorm-fixtures-cli --save-dev +``` + +#### Yarn + +```bash +yarn add typeorm-fixtures-cli --dev +``` + +## 开发步骤 + +```bash +# 安装依赖 +npm install + +# 构建编译文件 +npm run build +``` + +## 示例 + +`fixtures/Comment.yml` + +```yaml +entity: Comment +items: + comment{1..10}: + fullName: '{{name.firstName}} {{name.lastName}}' + email: '{{internet.email}}' + text: '{{lorem.paragraphs}}' + post: '@post*' +``` + +`fixtures/Post.yml` + +```yaml +entity: Post +items: + post1: + title: '{{name.title}}' + description: '{{lorem.paragraphs}}' + user: '@user($current)' + post2: + title: '{{name.title}}' + description: '{{lorem.paragraphs}}' + user: '@user($current)' +``` + +`fixtures/User.yml` + +```yaml +entity: User +items: + user1: + firstName: '{{name.firstName}}' + lastName: '{{name.lastName}}' + email: '{{internet.email}}' + profile: '@profile1' + __call: + setPassword: + - foo + user2: + firstName: '{{name.firstName}}' + lastName: '{{name.lastName}}' + email: '{{internet.email}}' + profile: '@profile2' + __call: + setPassword: + - foo +``` + +`fixtures/Profile.yml` + +```yaml +entity: Profile +items: + profile1: + aboutMe: <%= ['about string', 'about string 2', 'about string 3'].join(", ") %> + skype: skype-account> + language: english + profile2: + aboutMe: <%= ['about string', 'about string 2', 'about string 3'].join(", ") %> + skype: skype-account + language: english +``` + +## 创建填充 + +此库的最基本功能是将扁平的yaml文件转换为对象 + +```yaml +entity: User +items: + user0: + username: bob + fullname: Bob + birthDate: 1980-10-10 + email: bob@example.org + favoriteNumber: 42 + + user1: + username: alice + fullname: Alice + birthDate: 1978-07-12 + email: alice@example.org + favoriteNumber: 27 +``` + +### Fixture范围 + +第一步是创建一个对象的多个副本,以便从yaml文件中删除重复的规则。 + +您可以通过在fixture名称中定义范围来实现: + +```yaml +entity: User +items: + user{1..10}: + username: bob + fullname: Bob + birthDate: 1980-10-10 + email: bob@example.org + favoriteNumber: 42 +``` + +现在它将生成十个用户,ID为user1到user10。相当不错,但我们只有10条相同的名字,用户名和电子邮件的数据,这还不是很花哨。 + +### Fixture引用 + +您还可以指定对先前创建的fixture列表的引用: + +```yaml +entity: Post +items: + post1: + title: 'Post title' + description: 'Post description' + user: '@user1' +``` + +### Fixture列表 + +您也可以指定值列表而不是范围: + +```yaml +entity: Post +items: + post{1..10}: + title: 'Post title' + description: 'Post description' + user: '@user($current)' +``` + +在一个设置了范围的案例中(例如,用户{1..10}),`($current)`将为user1返回1,为user2返回2。。。 + +current在迭代中也可以用作字符串值: + +```yaml +entity: Post +items: + post{1..10}: + title: 'Post($current)' + description: 'Post description' +``` + +``Post($current)` 将返回Post1为post1,Post2为post2。。。 + +您可以使用基本数学运算符来改变此输出: + +```yaml +entity: Post +items: + post{1..10}: + title: 'Post($current*100)' + description: 'Post description' +``` + +``Post($current*100)` 将返回post100 for post1,Post200 for post2。。。 + +### 调用方法 + +有时如果你想要调用一个方法来初始化更多数据,你可以像使用属性一样执行此操作,使用方法名称并为其提供一组参数。 + +```yaml +entity: User +items: + user{1..10}: + username: bob + fullname: Bob + birthDate: 1980-10-10 + email: bob@example.org + favoriteNumber: 42 + _call: + setPassword: + - foo +``` + +## 处理关联 + +```yaml +entity: User +items: + user1: + # ... + +entity: Group +items: + group1: + name: '<{names.admin}>' + owner: '@user1' + members: + - '@user2' + - '@user3' + +``` + +如果要创建10个user和10个group并让每个用户拥有一个组,则可以在使用fixture ranges时,使用`($current)`将其替换为每次迭代的当前ID: + +```yaml +entity: User +items: + user1: + # ... + +entity: Group +items: + group{1..10}: + name: 'name' + owner: '@user($current)' + members: + - '@user2' + - '@user3' + +``` + +如果您想要一个随机用户而不是一个固定用户,您可以使用通配符定义一个引用: + +```yaml +entity: User +items: + user1: + # ... + +entity: Group +items: + group{1..10}: + name: 'name' + owner: '@user*' + members: + - '@user2' + - '@user3' + +``` + +或者 + +```yaml +entity: User +items: + user1: + # ... + +entity: Group +items: + group{1..10}: + name: 'name' + owner: '@user{1..2}' # @user1 or @user2 + members: + - '@user2' + - '@user3' + +``` + +## 高级指南 + +### Parameters + +You can set global parameters that will be inserted everywhere those values are used to help with readability. For example: + +```yaml +entity: Group +parameters: + names: + admin: Admin +items: + group1: + name: '<{names.admin}>' # <--- set Admin + owner: '@user1' + members: + - '@user2' + - '@user3' +``` + +### Faker数据 + +本库整合了[faker.js](https://github.com/marak/Faker.js/)库。使用{{foo}},您可以调用Faker数据提供者来生成随机数据。 + +```yaml +entity: User +items: + user{1..10}: + username: '{{internet.userName}}' + fullname: '{{name.firstName}} {{name.lastName}}' + birthDate: '{{date.past}}' + email: '{{internet.email}}' + favoriteNumber: '{{random.number}}' + _call: + setPassword: + - foo +``` + +### EJS模板 + +本库与[EJS](https://github.com/mde/ejs)整合 + +```yaml +entity: Profile +items: + profile1: + aboutMe: <%= ['about string', 'about string 2', 'about string 3'].join(", ") %> + skype: skype-account> + language: english +``` + +### 加载处理器 + +处理器允许您在持久化之前和(或)之后处理对象。处理器必须实现:`IProcessor` + +```typescript +import { IProcessor } from 'typeorm-fixtures-cli'; +``` + +下面是一个示例: + +`processor/UserProcessor.ts` + +```typescript +import { IProcessor } from 'typeorm-fixtures-cli'; +import { User } from '../entity/User'; + +export default class UserProcessor implements IProcessor { + preProcess(name: string, object: any): any { + return { ...object, firstName: 'foo' }; + } + + postProcess(name: string, object: { [key: string]: any }): void { + object.name = `${object.firstName} ${object.lastName}`; + } +} +``` + +fixture 配置 `fixtures/user.yml` + +```yaml +entity: User +processor: ../processor/UserProcessor +items: + user1: + firstName: '{{name.firstName}}' + lastName: '{{name.lastName}}' + email: '{{internet.email}}' +``` + +## 用法 + +``` +Usage: fixtures [options] Fixtures folder/file path + +Options: + -v, --version output the version number + -c, --config TypeORM config path (default: "ormconfig.yml") + --require A list of additional modules. e.g. ts-node/register + -cn, --connection [value] TypeORM connection name (default: "default") + -s --sync Database schema sync + -d --debug Enable debug + -h, --help output usage information + --no-color Disable color +``` + +##### 需要多个附加模块 + +如果您一次使用多个模块(例如ts-node和tsconfig-paths),则可以提供多个require参数。例如: + +``` +fixtures ./fixtures --config ./typeorm.config.ts --sync --require=ts-node/register --require=tsconfig-paths/register +``` + +### 以编程方式加载fixtures + +虽然typeorm-fixtures-cli旨在用作CLI,但您仍然可以通过程序中的API加载fixture。 + +例如,下面的代码片段将加载`./fixtures`目录中存在的所有fixture : + +```typescript +import * as path from 'path'; +import { Builder, fixturesIterator, Loader, Parser, Resolver } from 'typeorm-fixtures-cli/dist'; +import { createConnection, getRepository } from 'typeorm'; + +const loadFixtures = async (fixturesPath: string) => { + let connection; + + try { + connection = await createConnection(); + await connection.synchronize(true); + + const loader = new Loader(); + loader.load(path.resolve(fixturesPath)); + + const resolver = new Resolver(); + const fixtures = resolver.resolve(loader.fixtureConfigs); + const builder = new Builder(connection, new Parser()); + + for (const fixture of fixturesIterator(fixtures)) { + const entity = await builder.build(fixture); + await getRepository(entity.constructor.name).save(entity); + } + } catch (err) { + throw err; + } finally { + if (connection) { + await connection.close(); + } + } +}; + +loadFixtures('./fixtures') + .then(() => { + console.log('Fixtures are successfully loaded.'); + }) + .catch(err => console.log(err)); +``` + +## 简单示例 + +- [typeorm-fixtures-sample](https://github.com/RobinCK/typeorm-fixtures-sample) diff --git a/src/assets/posts/typescript-decorator.md b/src/assets/posts/typescript-decorator.md new file mode 100644 index 0000000..9c5da28 --- /dev/null +++ b/src/assets/posts/typescript-decorator.md @@ -0,0 +1,892 @@ +> 在看本文前最好先看一下[《阮一峰-es6中的装饰器》](http://es6.ruanyifeng.com/#docs/decorator) + +装饰器用于给类,方法,属性以及方法参数等增加一些附属功能而不影响其原有特性。其在Typescript应用中的主要作用类似于Java中的注解,在AOP(面向切面编程)使用场景下非常有用。 + +> **面向切面编程(AOP)** 是一种编程范式,它允许我们分离[横切关注点](https://zh.wikipedia.org/wiki/横切关注点),藉此达到增加模块化程度的目标。它可以在不修改代码自身的前提下,给已有代码增加额外的行为(通知) + +**装饰器一般用于处理一些与类以及类属性本身无关的逻辑**,例如: 一个类方法的执行耗时统计或者记录日志,可以单独拿出来写成装饰器。 + +看一下官方的解释更加清晰明了 + +> 装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 `@expression`这种形式,`expression`求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。 + +如果有使用过spring boot或者php的symfony框架的话,就基本知道装饰器的作用分别类似于以上两者注解和annotation,而node中装饰器用的比较好的框架是nest.js。不过不了解也没关系,接下来我就按我的理解讲解一下装饰器的使用。 + +不过目前装饰器还不属于标准,还在建议征集的第二阶段,但这并不妨碍我们在ts中的使用。只要在 `tsconfig.json`中开启 `experimentalDecorators`编译器选项就可以愉快地使用啦^^ + +```json +{ + "compilerOptions": { + "target": "ES5", + "experimentalDecorators": true + } +} +``` + +## 基本原理 + +可能有些时候,我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。 + +比如,我们要在用户登录的时候记录一下登录时间 + +```typescript + +const logger = (now: number) => console.log(`lasted logged in ${now}`); + +class User { + async login() { + await setTimeout(() => console.log('login success'), 100); + logger(new Date().valueOf()); + } +} + +``` + +以上代码把记录日志的代码强行写入登录的逻辑处理,这样代码量越高则代码越冗余。我们需要把日志逻辑单独拿出来,使login方法更专注于处理登录的逻辑,接下去我们用**高阶函数**模拟一下装饰器的原理,以便于后面更好的理解装饰器。 + +```typescript +type decoratorFunc = ( + target: any, + key: string, + descriptor: PropertyDescriptor, +) => void; + +// 模拟的装饰器工厂函数 +const createDecorator = (decorator: decoratorFunc) => { + return (Model: any, key: string) => { + // 获取即将使用装饰器的类原型 + const target = Model.prototype; + // 获取这个原型上某个方法的描述 + const descriptor = Object.getOwnPropertyDescriptor(target, key); + // 更改描述,生成新的方法 + decorator(target, key, descriptor); + }; +}; + +const logger: decoratorFunc = (target, key, descriptor) => { + // 将修改后的函数重新定义到原型链上 + Object.defineProperty(target, key, { + ...descriptor, + value: async (...arg) => { + try { + return await descriptor.value.apply(this, arg); // 调用之前的函数 + } finally { + const now = new Date().valueOf(); + console.log(`lasted logged in ${now}`); + } + }, + }); +}; + +class User { + async login() { + console.log('login success'); + await new Promise(resolve => setTimeout(resolve, 100)); + } +} + +const loggerDecorator = createDecorator(logger); +loggerDecorator(User, 'login'); +const user = new User(); +user.login(); + +// 控制台输出 +// login success +// 停顿100ms +// lasted logged in 1571771681793 + +``` + +了解了以上概念,接下去让我们学习真正的装饰器。 + +## 装饰器类型 + +TS中的装饰器有几种类型,如下: + +- 参数装饰器 +- 方法装饰器 +- 访问符装饰器 +- 属性装饰器 +- 类装饰器 + +以上每中装饰器分别可以作用于类原型(*prototype*属性)和类本身 + +### 类装饰器 + +[TS官方文档](https://www.typescriptlang.org/docs/handbook/decorators.html)中举了一个类装饰器的例子,也可以看一下。类装饰器其实就是把我们本身的类传入装饰器注解中,并对这个类或类的原型进行一些处理,仅此而已。例如: + +```typescript +const UserDerorator = {}>( + constructor: T, +) => { + return class extends constructor { + newProperty = 'new property'; + hello = 'override'; + sayHello() { + return this.hello; + } + }; +}; + +@HelloDerorator +export class UserService { + [key: string]: any; //此处用于防止eslint提示sayHello方法不存在 + hello: string; + constructor() { + this.hello = 'test'; + } +} + +const user = new UserService() +console.log(user.sayHello()) + +// 控制台打印 override +``` + +#### 装饰器工厂 + +上面的方法我们为`UserService`添加了一个`HelloDerorator`装饰器,这个装饰器的属性将覆盖`UserService`的默认属性。为了方便给装饰器添加其它参数,我们把`HelloDerorator`改造成为一个装饰器工厂,如下: + +```typescript +const WhoAmIDerorator = (firstname: string, lastname: string) => { + const name = `${firstname}.${lastname}`; + return {}>(target: T) => { + return class extends target { + _name: string = name; + getMyName() { + return this._name; + } + }; + }; +}; + +@WhoAmI('gkr', 'lichnow') +export class UserService {...} + +const user = new UserService() +console.log(user.getMyName()) + +// 控制台打印 gkr.lichnow +``` + +#### 其它用法 + +我们还可以对类原型链`property`上的属性/方法和类本身的静态属性/方法进行赋值或重载操作,还可以重载构造函数,如下: + +```typescript +interface UserProfile { + phone?: number; + address?: string; +} + +const ProfileDerorator = (profile: UserProfile) => { + return (target: any) => { + const original = target; + let userinfo: string = ''; + Object.keys(profile).forEach(key => { + userinfo = `${userinfo}.${profile[key].toString()}`; + }); + // 添加一个原型属性 + original.prototype.userinfo = userinfo; + // 使用函数创建一个新的类(类构造器),返回值为传入类的对象,这样就重载了构造函数 + function constructor(...args: any[]) { + console.log('contruct has been changed'); + return new original(...args); + } + // 赋值原型链 + constructor.prototype = original.prototype; + // 添加一个静态属性 + constructor.myinfo = `myinfo ${userinfo}`; + return constructor as typeof original; + }; +}; + +// 因为静态属性是无法通过[key: string]: any;获取类型提示的,所以这里添加一个接口用于动态各类添加静态属性 +interface StaticUser { + new (): UserService; + myinfo: string; +} + +@ProfileDerorator({ phone: 133, address: 'zhejiang' }) +class UserService {...} + +console.log(((UserService as unknown) as StaticUser).myinfo); +// 控制台输出 myinfo .133.zhejiang +// 控制台输出 contruct has been changed + +const user = new UserService() +console.log(user.userinfo) +// 控制台输出 .133.zhejiang +``` + +### 属性装饰器 + +属性装饰器一般不单独使用,主要用于配合类或方法装饰器进行组合装饰 + +#### 参数 + +属性装饰器函数有两个参数: + +**target** + +对于普通属性,target就是当前对象的原型,也就是说,假设 Employee 是对象,那么 target 就是 `Employee.prototype` + +对于静态属性,target就是当前对象的类 + +**propertyKey** + +属性的名称 + +#### 使用示例 + +```typescript +const userRoles = []; + +// 通过属性装饰器把角色赋值给userRoles +const RoleDerorator = (roles: string[]) => { + return (target: any, propertyName: string) => { + roles.forEach(role => userRoles.push(role)); + }; +}; + +// 根据userRoles生成Roles对象并赋值给类原型的roles属性 +const SetRoleDerorator = < + T extends new (...args: any[]) => { + [key: string]: any; + } +>( + constructor: T, +) => { + const roles = [ + { name: 'super-admin', desc: '超级管理员' }, + { name: 'admin', desc: '管理员' }, + { name: 'user', desc: '普通用户' }, + ]; + return class extends constructor { + constructor(...args) { + super(...args); + this.roles = roles.filter(role => userRoles.includes(role.name)); + } + }; +}; + +@SetRoleDerorator +export class UserService { + @RoleDerorator(['admin', 'user']) + roles: string[] = []; +} + +// 控制台输出 [ { name: 'admin', desc: '管理员' }, { name: 'user', desc: '普通用户' } ] +``` + +### 方法装饰器 + +在一开始我们介绍了装饰器的原理,其实这就是方法装饰器的原始实现。与属性装饰器不同的是,方法装饰器接受三个参数 + +> 方法装饰器重载的时候需要注意的一点是定义value务必使用function,而不是箭头函数,因为我们在调用原始的旧方法使用会使用到this,如:method.apply(this, args),这里的this指向需要function来定义,具体原因可参考我的另一篇文章[apply,bind,call使用](https://lichnow.com) + +#### 参数 + +**target** + +对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 + +**key** + +方法名称 + +**descriptor: PropertyDescriptor** + +方法的属性描述符(最重要的参数) + +#### 属性描述符 + +属性描述包含以下几个属性 + +- configurable?: boolean; // 能否使用delete、能否修改方法特性或修改访问器属性 +- enumerable?: boolean; 是否在遍历对象的时候存在 +- value?: any; 用于定义新的方法代替旧方法 +- writable?: boolean; 是否可写 +- get?(): any; // 访问器 +- set?(v: any): void; // 访问器 + +接下来我们使用方法装饰器修改一开始的[装饰器原理中的登录日志记录器](#基本原理) + +```typescript +const loggerDecorator = () => { + return function logMethod( + target: Object, + propertyName: string, + propertyDescriptor: PropertyDescriptor, + ): PropertyDescriptor { + const method = propertyDescriptor.value; + + // 重载方法 + propertyDescriptor.value = function async (...args: any[]) { + try { + return await method.apply(this, args); // 调用之前的函数 + } finally { + const now = new Date().valueOf(); + console.log(`lasted logged in ${now}`); + } + }; + return propertyDescriptor; + }; +}; + +class UserService { + ... + @loggerDecorator() + async login() { + console.log('login success'); + await new Promise(resolve => setTimeout(resolve, 100)); + } +} + +const user = new UserService(); +user.login(); +// 控制台输出结果与前面的例子相同 +``` + +### 参数装饰器 + +一个类中每个方法的参数也可以有自己的装饰器。 + +> 与属性装饰器类似,参数装饰器一般不单独使用,而是配合类或方法装饰器组合使用 + +#### 参数 + +1. **target**: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 +2. **key**:方法名称 +3. **index**: 参数数组中的位置 + +比如我们需要格式化一个方法的参数,那么可以创建一个转么用于格式化的装饰器 + +```typescript +// 参数格式化配置 +const parseConf: Function[] = []; + +const parse = (parseTo: Function) => { + return (target: any, propertyName: string, index: number) => { + parseConf[index] = parseTo; + }; +}; + +// 在函数调用前执行格式化操作 +const parseFunc = ( + target: Object, + propertyName: string, + descriptor: PropertyDescriptor, +): PropertyDescriptor => { + return { + ...descriptor, + value(...args: any[]) { + // 获取格式化后的参数列表 + const newArgs = parseConf.map((toParse, index) => toParse(args[index])); + + return descriptor.value.apply(this, newArgs); + }, + }; +}; + +interface UserEntity { + id: number; + username: string; +} + +class User { + private users: UserEntity[] = [ + { id: 1, username: 'admin' }, + { id: 2, username: 'lichnow' }, + ]; + + getUsers() { + return this.users; + } + + @parseDecorator + delete(@parse((arg: any) => Number(arg)) id): UserService { + this.users = this.users.filter(userObj => userObj.id !== id); + return this; + } +} + + +const user = new User(); +user.delete('1'); +console.log(user.getUsers()); + +// 控制台输出: [ { id: 2, username: 'lichnow' } ] +``` + +### 访问器装饰器 + +访问器其实只是那些添加了`get`,`set`前缀的方法,用于使用调用属性的方式获取和设置一些属性的方法,类似于PHP中的魔术方法`__get`,`__set`。其装饰器使用方法与普通方法并无差异,只是在获取值的时候是调用描述符的`get`和`set`来替代`value`而已。 + +例如,我们添加一个*nickname*字段,给设置*nickname*添加一个自定义前缀,并禁止在遍历*user*对象时出现*nickname*的值,添加一个*fullname*字段,在设置*nickname*时添加一个字符串后缀生成。 + +```typescript +export const HiddenDecorator = () => { + return ( + target: any, + propertyName: string, + descriptor: PropertyDescriptor, + ) => { + descriptor.enumerable = false; + }; +}; + +export const PrefixDecorator = (prefix: string) => { + return ( + target: any, + propertyName: string, + descriptor: PropertyDescriptor, + ) => { + return { + ...descriptor, + set(value: string) { + descriptor.set.apply(this, [`${prefix}_${value}`]); + }, + }; + }; +}; + +export class UserService { + ... + private _nickname: string; + private fullname: string; + @HiddenDecorator() + @PrefixDecorator('gkr_') + get nickname() { + return this._nickname; + } + + set nickname(value: string) { + this._nickname = value; + this.fullname = `${value}_fullname`; + } +} + +const user = new UserService(); +user.password = '123456'; +user.nickname = 'lichnow'; +console.log(user); +console.log(user.nickname); + +// 第一个console.log控制台输出,可以看到遍历对象后并没有nickname字段的值 +// UserService { +// users: [ { id: 1, username: 'admin' }, { id: 2, username: 'lichnow' } ], +// roles: [], +// hello: 'test', +// password: '123456', +// _nickname: 'gkr__lichnow', +// fullname: 'gkr__lichnow_fullname' +//} +// 第二个console.log控制台输出 +// gkr__lichnow +``` + +## 装饰器写法 + +通过装饰器重载方法有许多写法,可以根据自己的喜好来,以下举例几种 + +### 继承法 + +一般用于类装饰器中添加属性或方法,例如: + +```typescript + return {}>(target: T) => { + return class extends target { + getMyName() { + return this._name; + } + }; + }; +``` + +### 原型法 + +一般用于类装饰器上重载构造函数以及添加属性或方法,例如: + +```typescript +const ProfileDerorator = (profile: UserProfile) => { + return (target: any) => { + const original = target; + function constructor(...args: any[]) { + console.log('contruct has been changed'); + return new original(...args); + } + // 赋值原型链 + constructor.prototype = original.prototype; + // 添加一个静态属性 + constructor.myinfo = `myinfo ${userinfo}`; + return constructor as typeof original; + }; +}; + +``` + +### 赋值法 + +一般用于方法装饰器上修改某个描述符,例如 + +```typescript +const loggerDecorator = () => { + return function logMethod( + target: Object, + propertyName: string, + propertyDescriptor: PropertyDescriptor, + ): PropertyDescriptor { + const method = propertyDescriptor.value; + // 重载方法 + propertyDescriptor.value = function async (...args: any[]) {...}; + return propertyDescriptor; + }; +}; +``` + +### 展开法 + +与赋值法类似,只不过使用ES6+的展开语法,更容易理解和使用,例如 + +```typescript +const parseFunc = ( + target: Object, + propertyName: string, + descriptor: PropertyDescriptor, +): PropertyDescriptor => { + return { + ...descriptor, + value(...args: any[]) { + // 获取格式化后的参数列表 + const newArgs = parseConf.map((toParse, index) => toParse(args[index])); + + return descriptor.value.apply(this, newArgs); + }, + }; +}; +``` + +## 元信息反射 API + +元信息反射 API (例如 `Reflect`)能够用来以标准方式组织元信息。而装饰器中的*元信息反射*使用非常简单,外观上仅仅可以看做在类的某个方法上附加一些随时可以获取的信息而已。 + +使用之前我们必须先安装`reflect-metadata`这个库 + +```typescript +npm i reflect-metadata --save +``` + +并且在`tsconfig.json`中启用原信息配置 + +```json +{ + "compilerOptions": { + "target": "ES5", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} +``` + +### 基本使用 + +我们看一下TS官方的[示例](https://https://www.typescriptlang.org/docs/handbook/decorators.html#metadata)是如何通过反射API获取属性设计阶段的类型信息的。 + +**需要注意的是目前预定义的元信息只有三种** + +- **类型元信息**: `design:type`。 +- **参数类型元信息**: `design:paramtypes`。 +- **返回类型元信息**: `design:returntype`。 + +```typescript +import "reflect-metadata"; + +class Point { + x: number; + y: number; +} + +class Line { + private _p0: Point; + private _p1: Point; + + @validate + // 这句可以省略,因为design:type是预订属性 + // @Reflect.metadata('design:type', Point) + set p0(value: Point) { + this._p0 = value; + } + get p0() { + return this._p0; + } + + @validate + // @Reflect.metadata("design:type", Point) + set p1(value: Point) { + this._p1 = value; + } + get p1() { + return this._p1; + } +} + +function validate( + target: any, + propertyKey: string, + descriptor: TypedPropertyDescriptor, +) { + const set = descriptor.set; + descriptor.set = function(value: T) { + const type = Reflect.getMetadata('design:type', target, propertyKey); + if (!(value instanceof type)) { + throw new TypeError('Invalid type.'); + } + set.apply(this, [value]); + }; + return descriptor; +} + +const line = new Line(); +const p0 = new Point(); +p0.x = 1; +p0.y = 2; +line.p1 = p0; +console.log(line); + +// 控制台输出: Line { _p1: Point { x: 1, y: 2 } } +``` + +### 自定义元信息 + +除了使用类似`design:type`这种预定义的原信息外,我们也可以自定义信息,因为一般我们都是用`reflect-metadata`来自定义原信息的。比如我们可以在**删除用户**的方法上添加一个**角色判断**,只有拥有我们设定角色的用户才能删除用户,比如**管理员角色**,具体可参考以下代码: + +```typescript +// 角色守卫 +export const RoleGuardDecorator = (roles: string[]) => { + return function roleGuard( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + // 根据传入的参数定义守卫所需的角色 + Reflect.defineMetadata('roles', roles, target, propertyKey); + const method = descriptor.value; + descriptor.value = function(...args: any[]) { + // 获取当前用户的角色 + const currentRoles = target.getRoles(); + // 获取我们定义的操作此方法所需的角色 + const needRoles = Reflect.getMetadata('roles', target, propertyKey); + // 判断当前用户是否拥有所需的角色,没有则抛出异常 + for (const role of needRoles) { + if (!currentRoles.includes(role)) { + throw new Error(`you have not permission to run ${propertyKey}`); + } + } + return method.apply(this, args); + }; + return descriptor; + }; +}; + +class UserService { + ... + // 设定当前用户的角色 + getRoles() { + return ['user']; + } + + @RoleGuardDecorator(['admin']) + // 在装饰器中使用Reflect.defineMetadata()放定义roles只是为了方便封装 + // 当然,我们也可以在方法上直接定义roles,如下 + // Reflect.metadata('roles',['admin']) + @parseDecorator + delete(@parse((arg: any) => Number(arg)) id): UserService { + this.users = this.getUsers().filter(userObj => userObj.id !== id); + return this; + } +} + +const user = new UserService(); +user.delete(1); +console.log(user.getUsers()); + +// 控制台将输出异常 +// Error: you have not permission to run delete +``` + +## 组合与顺序 + +每一个属性,参数或方法都可以使用多组装饰器。每个类型的装饰器的调用顺序也是不同的。 + +### 组合使用 + +我们可以对任意一个被装饰者调用多组装饰器,多组装饰器一般书写在多行上(当然你也可以写在一行上,多行书写只不过是个约定俗成的惯例),比如 + +```typescript +@RoleGuardDecorator +@parseDecorator +delete(@parse((arg: any) => Number(arg)) id): UserService +``` + +当多个装饰器应用于一个声明上,它们求值方式与[高阶函数](http://en.wikipedia.org/wiki/Function_composition)相似。在这个模型下,当复合*RoleGuardDecorator*和*parseDecorator*时,复合的结果(*RoleGuardDecorator* ∘ *parseDecorator*)(*delete*)等同于*RoleGuardDecorator*(*parseDecorator*(*delete*))。 + +同时,我们可以参考react中的[高阶](https://zh-hans.reactjs.org/docs/higher-order-components.html),原理相似 + +它们的调用步骤类似剥洋葱法,即: + +1. 由上至下依次对装饰器表达式求值。 +2. 求值的结果会被当作函数,由下至上依次调用。 + +我们使用装饰器工厂来包装一下`@parseDecorator`(`@RoleGuardDecorator`已经在上面的代码中包装为工厂),并且在调用和求值阶段`console.log`来测试一下 + +```typescript +export const parseDecorator = () => { + console.log('开始格式化数据'); + return ( + target: Object, + propertyName: string, + descriptor: PropertyDescriptor, + ): PropertyDescriptor => { + return { + ...descriptor, + value(...args: any[]) { + const newArgs = parseConf.map((toParse, index) => toParse(args[index])); + console.log('格式化完毕'); + return descriptor.value.apply(this, newArgs); + }, + }; + }; +}; + +export const RoleGuardDecorator = (roles: string[]) => { + return function roleGuard( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + console.log('开始验证角色'); + ... + descriptor.value = function(...args: any[]) { + ... + console.log('验证角色完毕'); + return method.apply(this, args); + }; + return descriptor; + }; +}; + +export class UserService { + ... + @RoleGuardDecorator(['admin']) + // 把parseDecorator改成parseDecorator() + @parseDecorator() + getRoles() { + // 提供验证角色为admin + return ['admin']; + } +} + +const user = new UserService(); +user.delete(1); +console.log(user.getUsers()); + +// 控制台输出 +// 开始格式化数据 +// 开始验证角色 +// 验证角色完毕 +// 格式化完毕 +// [ { id: 2, username: 'lichnow' } ] +``` + +### 调用顺序 + +每种类型的装饰器的调用顺序是不同的,具体顺序如下: + +1. *参数装饰器*,然后依次是*方法装饰器*,*访问符装饰器*,或*属性装饰器*应用到每个实例成员(即类原型的成员)。 +2. *参数装饰器*,然后依次是*方法装饰器*,*访问符装饰器*,或*属性装饰器*应用到每个静态成员。 +3. *参数装饰器*应用到构造函数(即类原型)。 +4. *类装饰器*应用到类。 + +例如:我们使用元信息结合方法和参数装饰器来验证参数的*required*,其调用顺序为*参数装饰器*->*方法装饰器* + +```typescript +const requiredMetadataKey = Symbol('required'); + +export const RequiredDecorator = ( + target: Object, + propertyKey: string | symbol, + parameterIndex: number, +) => { + const existingRequiredParameters: number[] = + Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; + existingRequiredParameters.push(parameterIndex); + Reflect.defineMetadata( + requiredMetadataKey, + existingRequiredParameters, + target, + propertyKey, + ); +}; + +export const ValidateDecorator = ( + target: any, + propertyName: string, + descriptor: TypedPropertyDescriptor, +) => { + const method = descriptor.value; + descriptor.value = function() { + const requiredParameters: number[] = Reflect.getOwnMetadata( + requiredMetadataKey, + target, + propertyName, + ); + if (requiredParameters) { + for (const parameterIndex of requiredParameters) { + if ( + parameterIndex >= arguments.length || + arguments[parameterIndex] === undefined + ) { + throw new Error('Missing required argument.'); + } + } + } + + return method.apply(this, arguments); + }; +}; + + @ValidateDecorator + createUser(@RequiredDecorator username?: string, id?: number) { + const ids: number[] = this.users.map(userEntity => userEntity.id); + const newUser: UserEntity = { + // 如果不提供ID参数,则新用户的ID为所有用户的最大ID + 1 + id: id || Math.max(...ids) + 1, + // 如果不提供username参数,则生成随机字符串作为用户名 + username: + username || + Math.random() + .toString(36) + .substring(2, 15), + }; + this.users.push(newUser); + return newUser; + } + + +const user = new UserService(); +user.createUser(); +console.log(user.getUsers()); + +// 控制台抛出异常: Error: Missing required argument. + +// 尝试去除required装饰器 +@ValidateDecorator + createUser(@RequiredDecorator username?: string, id?: number) {...} +// 控制台输出 +// [ +// { id: 1, username: 'admin' }, +// { id: 2, username: 'lichnow' }, +// { id: 3, username: 'q5cb3pgfdhq' } +// ] +``` \ No newline at end of file diff --git a/src/assets/posts/yargs.md b/src/assets/posts/yargs.md new file mode 100644 index 0000000..0b2e450 --- /dev/null +++ b/src/assets/posts/yargs.md @@ -0,0 +1,1066 @@ +# Yargs中文文档 + +**Yargs是一个用于创建node.js命令行的库** + +## 基础 + +### 说明 : + +Yargs通过解析参数和生成优雅的用户界面来帮助您构建交互式命令行工具。 + +它为你提供: + +* 命令和(分组)选项 (`my-program.js serve --port=5000`)。 +* 根据您的参数动态生成的帮助菜单。 + +> + +* 命令和选项的bash-completion快捷方式. +* [更多api](https://github.com/yargs/yargs/blob/master/docs/api.md). + +### 安装 + +稳定版本: +```bash +npm i yargs +``` + +最新版本: +```bash +npm i yargs@next +``` + +### 用法 : + +#### 简单示例 + +````javascript +#!/usr/bin/env node +const argv = require('yargs').argv + +if (argv.ships > 3 && argv.distance < 53.5) { + console.log('Plunder more riffiwobbles!') +} else { + console.log('Retreat from the xupptumblers!') +} +```` + +```bash +$ ./plunder.js --ships=4 --distance=22 +Plunder more riffiwobbles! + +$ ./plunder.js --ships 12 --distance 98.7 +Retreat from the xupptumblers! +``` + +#### 比较复杂的案例 + +```javascript +#!/usr/bin/env node +require('yargs') // eslint-disable-line + .command('serve [port]', 'start the server', (yargs) => { + yargs + .positional('port', { + describe: 'port to bind on', + default: 5000 + }) + }, (argv) => { + if (argv.verbose) console.info(`start server on :${argv.port}`) + serve(argv.port) + }) + .option('verbose', { + alias: 'v', + default: false + }) + .argv +``` + +运行上面的示例并加上 `--help` 选项以查看应用程序的帮助。 + +## TypeScript + +yargs 的类型定义在 [@types/yargs][type-definitions]包中,请先安装此包 + +``` +npm i @types/yargs --save-dev +``` + +### TypeScript用法示例 + +TypeScript的定义 需要考虑到 yargs的 `type` 键以及`demandOption`/`default`(如果存在的话)。 + +`.options()`定义如下: + +```typescript +#!/usr/bin/env node +import * as yargs from 'yargs'; + +const argv = yargs.options({ + a: { type: 'boolean', default: false }, + b: { type: 'string', demandOption: true }, + c: { type: 'number', alias: 'chill' }, + d: { type: 'array' }, + e: { type: 'count' }, + f: { choices: ['1', '2', '3'] } +}).argv; +``` + +则`argv`返回的值的类型会像下面这样子: + +```typescript +{ + [x: string]: unknown; + a: boolean; + b: string; + c: number | undefined; + d: (string | number)[] | undefined; + e: number; + f: string | undefined; + _: string[]; + $0: string; +} +``` + +您可能希望为应用程序定义一个接口,描述解析`argv`后将采用的形式: + +```typescript +interface Arguments { + [x: string]: unknown; + a: boolean; + b: string; + c: number | undefined; + d: (string | number)[] | undefined; + e: number; + f: string | undefined; +} +``` + +要改进`choices`选项类型,您还可以指定它的类型: + +```typescript +type Difficulty = 'normal' | 'nightmare' | 'hell'; +const difficulties: ReadonlyArray = ['normal', 'nightmare', 'hell']; + +const argv = yargs.option('difficulty', { + choices: difficulties, + demandOption: true +}).argv; +``` + +`argv`会得到类型`'normal' | 'nightmare' | 'hell'`。 + +## 解析技巧 + + + +### 停止解析 + +使用 `—`标志来停止解析,此标志后的参数将会放入 `argv._`. + + $ node examples/reflect.js -a 1 -b 2 -- -c 3 -d 4 + { _: [ '-c', '3', '-d', '4' ], + a: 1, + b: 2, + '$0': 'examples/reflect.js' } + + + +### 设置字段为false + +如果要将字段显式设置为false而不是将其保留为未定义或覆盖默认值,则可以执行此操作`--no-key`。 + + $ node examples/reflect.js -a --no-b + { _: [], a: true, b: false, '$0': 'examples/reflect.js' } + + + +### 整型 + +每个看起来像数字(`!isNaN(Number(arg))`)的参数都会转换为一个。通过这种方式,你可以直接`net.createConnection(argv.port)`并且你可以在`argv`之外使用`+`添加数字,不过这样是产生没有意义的并列值,这非常令人沮丧。 + + +### 数组 + +如果多次指定一个标志,它将变为一个包含所有值的数组。 + + $ node examples/reflect.js -x 5 -x 8 -x 0 + { _: [], x: [ 5, 8, 0 ], '$0': 'examples/reflect.js' } + +您还可以将选项配置为[type `array`](https://github.com/yargs/yargs/blob/master/docs/api.md#array),以支持表单的数组`-x 5 6 7 8`。 + + +### 对象 + +当你在参数名中使用点符号(`.`s)时,将假定一个隐式对象路径。这使您可以将参数组织到嵌套对象中。 + + $ node examples/reflect.js --foo.bar.baz=33 --foo.quux=5 + { _: [], + foo: { bar: { baz: 33 }, quux: 5 }, + '$0': 'examples/reflect.js' } + + + +### 问题 + +当您使用包含破折号(`-`)的字符串参数时,shell会将这些参数视为单独的选项,而不是字符串的一部分。问题是像bash这样的shell往往会删除引号。解决方案是将字符串包装在两组引号中。 + +在单引号内使用双引号。 + +``` +$ node examples/reflect.js --foo '"--hello -x=yes -v"' +{ _: [], foo: '--hello -x=yes -v', + '$0': 'examples/reflect.js' } +``` + +在双引号内转义双引号。 + +``` +$ node examples/reflect.js --foo "\"--hello -x=yes -v\"" +{ _: [], foo: '--hello -x=yes -v', + '$0': 'examples/reflect.js' } +``` + +## 高级主题 + + + +### 命令 + +Yargs提供了一组强大的工具来组成模块化命令驱动的应用程序。在本节中,我们将介绍此API中提供的一些高级功能: + +#### 默认命令 + +要指定默认命令,请使用字符串`*`或`$0`。如果提供的位置参数与未知命令匹配,则将运行默认命令。默认命令允许您使用与子命令类似的API来定义应用程序的端点。 + +```js +const argv = require('yargs') + .command('$0', 'the default command', () => {}, (argv) => { + console.log('this command will be run by default') + }) +``` + +如果运行`./my-cli.js --x=22`,将执行上面定义的命令将被执行。 + +默认命令也可以用作命令别名,如下所示: + +```js +const argv = require('yargs') + .command(['serve', '$0'], 'the serve command', () => {}, (argv) => { + console.log('this command will be run by default') + }) +``` + +如果程序运行`./my-cli.js --x=22`或`./my-cli.js serve --x=22`,则将执行上面定义的命令将被之心。 + +#### 位置参数 + +命令可以接受*可选*和*必需的*位置参数。必需的位置参数采用表单``,可选参数采用表单`[bar]`。解析的位置参数将填充在 `argv`属性中: + +```js +yargs.command('get [proxy]', 'make a get HTTP request') + .help() + .argv +``` + +#### 位置参数别名 + +可以使用该`|`字符为位置参数提供别名。例如,假设我们的应用程序允许username*或* email作为第一个参数: + +```js +yargs.command('get [password]', 'fetch a user by username or email.') + .help() + .argv +``` + +以这种方式,在命令执行时`argv.username`和`argv.email`都将被填充。 + +##### 可选的位置参数 + +最后一个位置参数可以选择接受一个值数组,方法是使用`..`运算符: + +```js +yargs.command('download [files..]', 'download several files') + .help() + .argv +``` + +##### 位置参数的帮助信息 + +您可以在命令构造器函数中使用[`.positional()`](https://github.com/yargs/yargs/blob/master/docs/api.md#positionalkey-opt)方法来描述和配置位置参数: + +```js +yargs.command('get [proxy]', 'make a get HTTP request', (yargs) => { + yargs.positional('source', { + describe: 'URL to fetch content from', + type: 'string', + default: 'http://www.google.com' + }).positional('proxy', { + describe: 'optional proxy URL' + }) +}) +.help() +.argv +``` + +#### 命令执行 + +当在命令行上给出命令时,yargs将执行以下动作: + +1. 将命令推入当前上下文 +2. 重置非全局配置 +3. 如果给定了`builder`,则通过它来配置命令 +4. 从命令行解析并验证参数,包括位置参数 +5. 如果验证成功,则运行`handler`函数(如果给定) +6. 从当前上下文中弹出命令 + +#### 命令别名 + +您可以通过将命令及其所有别名放入数组来为命令定义别名。 + +或者,一个命令模块可以指定`aliases`属性,该属性可以是字符串或字符串数组。通过`command`属性和`aliases`属性定义的所有别名将连接在一起。 + +数组中的第一个元素被认为是规范命令,它可以定义位置参数,数组中的其余元素被视为别名。别名从规范命令继承位置参数,因此将忽略别名中定义的任何位置参数。 + +如果在命令行上给出了规范命令或其任何别名,则将执行该命令。 + +```js +#!/usr/bin/env node +require('yargs') + .command(['start [app]', 'run', 'up'], 'Start up an app', {}, (argv) => { + console.log('starting up the', argv.app || 'default', 'app') + }) + .command({ + command: 'configure [value]', + aliases: ['config', 'cfg'], + desc: 'Set a config variable', + builder: (yargs) => yargs.default('value', 'true'), + handler: (argv) => { + console.log(`setting ${argv.key} to ${argv.value}`) + } + }) + .demandCommand() + .help() + .wrap(72) + .argv +``` + +``` +$ ./svc.js help +Commands: + start [app] Start up an app [aliases: run, up] + configure [value] Set a config variable [aliases: config, cfg] + +Options: + --help Show help [boolean] + +$ ./svc.js cfg concurrency 4 +setting concurrency to 4 + +$ ./svc.js run web +starting up the web app +``` + +#### 提供命令模块 + +对于复杂的命令,您可以将逻辑拉入模块。模块只需要导出: + +* `exports.command`: 在命令行上给出字符串(或字符串数组)时执行此命令,第一个字符串可能包含位置参数 +* `exports.aliases`: 表示别名的字符串数组(或单个字符串),别名中exports.command定义的位置参数将被忽略 +* `exports.describe`: 用作帮助文本中命令的描述的字符串,`false`用于隐藏命令 +* `exports.builder`: 一个声明命令接受的选项的对象,或者接受和返回yargs实例的函数 +* `exports.handler`: a function which will be passed the parsed argv. + +```js +// my-module.js +exports.command = 'get [proxy]' + +exports.describe = 'make a get HTTP request' + +exports.builder = { + banana: { + default: 'cool' + }, + batman: { + default: 'sad' + } +} + +exports.handler = function (argv) { + // do something with argv. +} +``` + +然后注册模块如下: + +```js +yargs.command(require('my-module')) + .help() + .argv +``` + +或者,如果该模块不导出`command`和`describe`(或如果你只是想重写它们): + +```js +yargs.command('get [proxy]', 'make a get HTTP request', require('my-module')) + .help() + .argv +``` + +##### 测试命令模块 + +如果你想完整地测试一个命令,你可以像这样测试它: + +```js +it("returns help output", async () => { + // Initialize parser using the command module + const parser = yargs.command(require('./my-command-module')).help(); + + // Run the command module with --help as argument + const output = await new Promise((resolve) => { + parser.parse("--help", (err, argv, output) => { + resolve(output); + }) + }); + + // Verify the output is correct + expect(output).toBe(expect.stringContaining("helpful message")); +}); +``` + +此示例使用[jest](https://github.com/facebook/jest)作为测试运行器,但该概念独立于框架。 + +### .commandDir(directory, [opts]) + +从一个模块的相对目录来应用命令模块的时候调用此方法。 + +这允许您将多个命令组织到单个目录下的自己的模块中,并同时应用所有这些命令而不是多次调用 `.command(require('./dir/module'))`。 + +默认情况下,它会忽略子目录。这样您就可以使用目录结构来表示命令层次结构,其中每个命令在其构建器函数中使用此方法来应用其子命令。请参阅下面的示例。 + +请注意,yargs假设给定目录中的所有模块都是命令模块,如果遇到非命令模块,则会出错。在这种情况下,您可以将模块移动到其他目录,也可以使用`exclude`或 `visit`选项手动将其过滤掉。更多相关内容如下。 + +`directory` 是一个字符串类型的相对目录路径(必需)。 + +`opts`是一个选项对象(可选)。仅对以下选项有效: + +- `recurse`: 布尔值,默认值 `false` + + 在所有子目录中查找命令模块,并将它们应用为扁平化(非分层)列表。 + +- `extensions`: 字符串或数组,默认 `['js']` + + 需要命令模块时要查找的文件类型。 + +- `visit`: 函数 + + A synchronous function called for each command module encountered. Accepts + `commandObject`, `pathToFile`, and `filename` as arguments. Returns + `commandObject` to include the command; any ji to exclude/skip it. + + 每个命令模块都会调用的同步函数。接受 `commandObject`,`pathToFile`和`filename`作为参数。返回包含命令的`commandObject`对象; 使用任何虚拟值来排除或跳过它。 + +- `include`: RegExp或函数 + + 将某些模块列入白名单。有关详细信息,请参阅[`require-directory`白名单](https://www.npmjs.com/package/require-directory#whitelisting)。 + +- `exclude`: RegExp or function + + 将某些模块列入黑名单。有关详细信息,请参阅[`require-directory`黑名单](https://www.npmjs.com/package/require-directory#blacklisting)。 + +#### 使用示例命令层次结构 `.commandDir()` + +预期实现的 CLI: + +```sh +$ myapp --help +$ myapp init +$ myapp remote --help +$ myapp remote add base http://yargs.js.org +$ myapp remote prune base +$ myapp remote prune base fork whatever +``` + +目录结构: + +``` +myapp/ +├─ cli.js +└─ cmds/ + ├─ init.js + ├─ remote.js + └─ remote_cmds/ + ├─ add.js + └─ prune.js +``` + +cli.js: + +```js +#!/usr/bin/env node +require('yargs') + .commandDir('cmds') + .demandCommand() + .help() + .argv +``` + +cmds/init.js: + +```js +exports.command = 'init [dir]' +exports.desc = 'Create an empty repo' +exports.builder = { + dir: { + default: '.' + } +} +exports.handler = function (argv) { + console.log('init called for dir', argv.dir) +} +``` + +cmds/remote.js: + +```js +exports.command = 'remote ' +exports.desc = 'Manage set of tracked repos' +exports.builder = function (yargs) { + return yargs.commandDir('remote_cmds') +} +exports.handler = function (argv) {} +``` + +cmds/remote_cmds/add.js: + +```js +exports.command = 'add ' +exports.desc = 'Add remote named for repo at url ' +exports.builder = {} +exports.handler = function (argv) { + console.log('adding remote %s at url %s', argv.name, argv.url) +} +``` + +cmds/remote_cmds/prune.js: + +```js +exports.command = 'prune [names..]' +exports.desc = 'Delete tracked branches gone stale for remotes' +exports.builder = {} +exports.handler = function (argv) { + console.log('pruning remotes %s', [].concat(argv.name).concat(argv.names).join(', ')) +} +``` + + + +### 构建可配置的CLI应用程序 + +yargs的目标之一是检索JavaScript CLI社区中常见的实践,并使这些易于使用的约定应用于您自己的应用程序。 + +已经出现的一组有用的约定是关于应用程序如何允许用户扩展和定制其功能。 + +#### .rc 文件 + +一些命令库(例如,[Babel](https://babeljs.io/docs/usage/babelrc/),[ESLint](https://github.com/eslint/eslint#configuration))允许您通过填充`.rc`文件来提供配置是很常见的。 + +Yargs的 [`config()`](https://github.com/yargs/yargs/blob/master/docs/api.md#config)与模块[查找](https://www.npmjs.com/package/find-up)相结合,可以轻松实现`.rc`功能: + +```js +const findUp = require('find-up') +const fs = require('fs') +const configPath = findUp.sync(['.myapprc', '.myapprc.json']) +const config = configPath ? JSON.parse(fs.readFileSync(configPath)) : {} +const argv = require('yargs') + .config(config) + .argv +``` + +#### 在package.json中提供配置 + +另一种常见做法是允许用户通过package.json中的保留字段提供配置。例如,分别使用`nyc`和`babel`键来配置 [nyc](https://github.com/istanbuljs/nyc#configuring-nyc) 或 [babel](https://babeljs.io/docs/usage/babelrc/#lookup-behavior): + +```json +{ + "nyc": { + "watermarks": { + "lines": [80, 95], + "functions": [80, 95], + "branches": [80, 95], + "statements": [80, 95] + } + } +} +``` + +Yargs使用下面的[`pkgConf()`](https://github.com/yargs/yargs/blob/master/docs/api.md#config) 方法为您提供此功能: + +```js +const argv = require('yargs') + .pkgConf('nyc') + .argv +``` + +#### 创建插件架构 + +[`pkgConf()`](https://github.com/yargs/yargs/blob/master/docs/api.md#config)和[`config()`](https://github.com/yargs/yargs/blob/master/docs/api.md#config)都支持`extends`关键字。`extends`允许您从[其他npm模块](https://www.npmjs.com/package/@istanbuljs/nyc-config-babel)继承配置,从而可以构建类似于[Babel presets](https://babeljs.io/docs/plugins/#presets)这样的插件架构: + +```json +{ + "nyc": { + "extends": "@istanbuljs/nyc-config-babel" + } +} +``` + + + +#### 定制Yargs的解析器 + +不是每个人都同意`process.argv`的解析方式; 使用[`parserConfiguration()`](https://github.com/yargs/yargs/blob/master/docs/api.md#parserConfiguration)方法可以打开和关闭某些yargs的解析功能: + +```js +yargs.parserConfiguration({ + "yargs": { + "short-option-groups": true, + "camel-case-expansion": true, + "dot-notation": true, + "parse-numbers": true, + "boolean-negation": true + } +}) +``` + +有关此功能的详细文档,请参阅[yargs-parser](https://github.com/yargs/yargs-parser#configuration)模块。 + +### 中间件 + +有时您可能希望在参数到达命令处理程序之前对其进行转换。例如,您可能希望验证是否已提供凭据,否则从文件加载凭据。 + +中间件只是一堆函数,每个函数都传递当前解析的参数,然后可以通过添加值,删除值或覆盖值来更新。 + +Diagram: + +``` + -------------- -------------- --------- +stdin ----> argv ----> | Middleware 1 | ----> | Middleware 2 | ---> | Command | + -------------- -------------- --------- +``` + +#### 凭据中间件示例 + +在这个例子中,我们的中间件将检查`username`和`password`的参数值。如果没有,它将加载`~/.credentials`,并填充`argv.username`和`argv.password`值。 + +##### 中间件函数 + +``` +const normalizeCredentials = (argv) => { + if (!argv.username || !argv.password) { + const credentials = JSON.parse(fs.readSync('~/.credentials')) + return credentials + } + return {} +} + +// Add normalizeCredentials to yargs +yargs.middleware(normalizeCredentials) +``` + +#### 异步凭据中间件示例 + +这个例子完全相同,但它异步加载`username`和`password`。 + +##### 中间件函数 + +``` +const { promisify } = require('util') // since node 8.0.0 +const readFile = promisify(require('fs').readFile) + +const normalizeCredentials = (argv) => { + if (!argv.username || !argv.password) { + return readFile('~/.credentials').then(data => JSON.parse(data)) + } + return {} +} + +// Add normalizeCredentials to yargs +yargs.middleware(normalizeCredentials) +``` + +##### yargs解析配置 + +``` +var argv = require('yargs') + .usage('Usage: $0 [options]') + .command('login', 'Authenticate user', (yargs) =>{ + return yargs.option('username') + .option('password') + } ,(argv) => { + authenticateUser(argv.username, argv.password) + }, + [normalizeCredentials] + ) + .argv; +``` + +#### 使用非单例接口 + +要使yargs不作为单例运行,请执行以下操作: +``` +const argv = require('yargs/yargs')(process.argv.slice(2)) +``` + +当在库中使用yargs时,这尤其有用,因为第三方库不应该污染全局状态。 + +## Yargs示例 + +有关Yargs的更多演示,请参阅[示例文件夹](https://github.com/yargs/yargs/blob/master/example)。 + +### 选项只是一个哈希值! + +plunder.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs').argv; + +if (argv.ships > 3 && argv.distance < 53.5) { + console.log('Plunder more riffiwobbles!'); +} else { + console.log('Retreat from the xupptumblers!'); +} +``` + +------ + +``` +$ ./plunder.js --ships=4 --distance=22 +Plunder more riffiwobbles! + +$ ./plunder.js --ships 12 --distance 98.7 +Retreat from the xupptumblers! +``` + +### 你可以设置一些简短的选项: + +short.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs').argv; +console.log('(%d,%d)', argv.x, argv.y); +``` + +------ + +``` +$ ./short.js -x 10 -y 21 +(10,21) +``` + +### 布尔值,长,短,甚至分组: + +bool.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs').argv; + +if (argv.s) { + process.stdout.write(argv.fr ? 'Le perroquet dit: ' : 'The parrot says: '); +} +console.log( + (argv.fr ? 'couac' : 'squawk') + (argv.p ? '!' : '') +); +``` + +------ + +``` +$ ./bool.js -s +The parrot says: squawk + +$ ./bool.js -sp +The parrot says: squawk! + +$ ./bool.js -sp --fr +Le perroquet dit: couac! +``` + +### 还有非连字符选项!只要使用`argv._`! + +nonopt.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs').argv; +console.log('(%d,%d)', argv.x, argv.y); +console.log(argv._); +``` + +------ + +``` +$ ./nonopt.js -x 6.82 -y 3.35 rum +(6.82,3.35) +[ 'rum' ] + +$ ./nonopt.js "me hearties" -x 0.54 yo -y 1.12 ho +(0.54,1.12) +[ 'me hearties', 'yo', 'ho' ] +``` + +### Yargs使用布尔值 + +count.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .count('verbose') + .alias('v', 'verbose') + .argv; + +VERBOSE_LEVEL = argv.verbose; + +function WARN() { VERBOSE_LEVEL >= 0 && console.log.apply(console, arguments); } +function INFO() { VERBOSE_LEVEL >= 1 && console.log.apply(console, arguments); } +function DEBUG() { VERBOSE_LEVEL >= 2 && console.log.apply(console, arguments); } + +WARN("Showing only important stuff"); +INFO("Showing semi-important stuff too"); +DEBUG("Extra chatty mode"); +``` + +------ + +``` +$ node count.js +Showing only important stuff + +$ node count.js -v +Showing only important stuff +Showing semi-important stuff too + +$ node count.js -vv +Showing only important stuff +Showing semi-important stuff too +Extra chatty mode + +$ node count.js -v --verbose +Showing only important stuff +Showing semi-important stuff too +Extra chatty mode +``` + +### 告诉用户如何使用您的选项并设置要求。 + +area.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .usage('Usage: $0 -w [num] -h [num]') + .demandOption(['w','h']) + .argv; + +console.log("The area is:", argv.w * argv.h); +``` + +------ + +``` +$ ./area.js -w 55 -h 11 +The area is: 605 + +$ node ./area.js -w 4.91 -w 2.51 +Usage: area.js -w [num] -h [num] + +Options: + -w [required] + -h [required] + +Missing required arguments: h +``` + +### 符合要求后,需求更多!请求非连字符的参数! + +demand_count.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .demandCommand(2) + .argv; +console.dir(argv); +``` + +------ + +``` +$ ./demand_count.js a + +Not enough non-option arguments: got 1, need at least 2 + +$ ./demand_count.js a b +{ _: [ 'a', 'b' ], '$0': 'demand_count.js' } + +$ ./demand_count.js a b c +{ _: [ 'a', 'b', 'c' ], '$0': 'demand_count.js' } +``` + +### 甚至更多的TIMBERS! + +default_singles.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .default('x', 10) + .default('y', 10) + .argv +; +console.log(argv.x + argv.y); +``` + +------ + +``` +$ ./default_singles.js -x 5 +15 +``` + +default_hash.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .default({ x : 10, y : 10 }) + .argv +; +console.log(argv.x + argv.y); +``` + +------ + +``` +$ ./default_hash.js -y 7 +17 +``` + +### 如果你真的想得到所有的描述... + +boolean_single.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .boolean('v') + .argv +; +console.dir(argv.v); +console.dir(argv._); +``` + +------ + +``` +$ ./boolean_single.js -v "me hearties" yo ho +true +[ 'me hearties', 'yo', 'ho' ] +``` + +boolean_double.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .boolean(['x','y','z']) + .argv +; +console.dir([ argv.x, argv.y, argv.z ]); +console.dir(argv._); +``` + +------ + +``` +$ ./boolean_double.js -x -z one two three +[ true, false, true ] +[ 'one', 'two', 'three' ] +``` + +### Yargs在这里帮助你...... + +您可以描述帮助消息的参数并设置别名。Yargs知道如何自动格式化一个方便的帮助字符串。 + +line_count.js: + +```javascript +#!/usr/bin/env node +var argv = require('yargs') + .usage('Usage: $0 [options]') + .command('count', 'Count the lines in a file') + .example('$0 count -f foo.js', 'count the lines in the given file') + .alias('f', 'file') + .nargs('f', 1) + .describe('f', 'Load a file') + .demandOption(['f']) + .help('h') + .alias('h', 'help') + .epilog('copyright 2019') + .argv; + +var fs = require('fs'); +var s = fs.createReadStream(argv.file); + +var lines = 0; +s.on('data', function (buf) { + lines += buf.toString().match(/\n/g).length; +}); + +s.on('end', function () { + console.log(lines); +}); +``` + +------ + +``` +$ node line_count.js +Usage: line_count.js [options] + +Commands: + line_count.js count Count the lines in a file + +Options: + --version Show version number [boolean] + -f, --file Load a file [required] + -h, --help Show help [boolean] + +Examples: + line_count.js count -f foo.js count the lines in the given file + +copyright 2019 + +Missing required argument: f + +$ node line_count.js count +line_count.js count + +Count the lines in a file + +Options: + --version Show version number [boolean] + -f, --file Load a file [required] + -h, --help Show help [boolean] + +Missing required argument: f + +$ node line_count.js count --file line_count.js +25 + +$ node line_count.js count -f line_count.js +25 +``` + diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 60bcf78..2dae293 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -1,6 +1,8 @@ import { toNumber } from 'lodash'; import { createDBConfig } from '@/modules/database/config'; +import { ContentFactory } from '@/modules/database/factories/content.factory'; +import ContentSeeder from '@/modules/database/seeders/content.seeder'; export const database = createDBConfig((configure) => ({ common: { synchronize: true }, @@ -12,6 +14,8 @@ export const database = createDBConfig((configure) => ({ username: configure.env.get('DB_USERNAME', '3r'), password: configure.env.get('DB_PASSWORD', '12345678'), database: configure.env.get('DB_NAME', '3r'), + seeders: [ContentSeeder], + factories: [ContentFactory], }, ], })); diff --git a/src/modules/core/helpers/utils.ts b/src/modules/core/helpers/utils.ts index 4ac6060..f72c5bd 100644 --- a/src/modules/core/helpers/utils.ts +++ b/src/modules/core/helpers/utils.ts @@ -88,3 +88,40 @@ export async function panic(option: PanicOption | string) { process.exit(1); } } + +/** + * 获取小于N的随机整数 + * @param count + */ +export function getRandomIndex(count: number) { + return Math.floor(Math.random() * count); +} + +/** + * 从列表中获取一个随机项 + * @param list + */ +export function getRandomItemData(list: T[]) { + if (isNil(list) || list.length === 0) { + throw new Error('list is empty'); + } + return list[getRandomIndex(list.length)]; +} + +/** + * 从列表中获取多个随机项组成一个新列表 + * @param list + */ +export function getRandomListData(list: T[]) { + if (isNil(list) || list.length === 0) { + throw new Error('list is empty'); + } + const result: T[] = []; + for (let i = 0; i < getRandomIndex(list.length); i++) { + const random = getRandomItemData(list); + if (!result.find((p) => p.id === random.id)) { + result.push(random); + } + } + return result.length === 0 ? [list[0]] : result; +} diff --git a/src/modules/database/factories/content.data.ts b/src/modules/database/factories/content.data.ts new file mode 100644 index 0000000..f2e0110 --- /dev/null +++ b/src/modules/database/factories/content.data.ts @@ -0,0 +1,134 @@ +export interface PostData { + title: string; + contentFile: string; + summary?: string; + category?: string; + tags?: string[]; +} + +export interface CategoryData { + name: string; + children?: CategoryData[]; +} + +export interface TagData { + name: string; +} + +export interface ContentConfig { + fixture?: { + categories: CategoryData[]; + posts: PostData[]; + }; +} + +export const posts: PostData[] = [ + { + title: '基于角色和属性的Node.js访问控制', + contentFile: 'rbac.md', + category: '后端', + tags: ['node'], + }, + { + title: 'docker简介', + contentFile: 'docker-introduce.md', + category: '运维', + tags: ['devops'], + }, + { + title: 'go协程入门', + contentFile: 'goroutings.md', + category: '后端', + tags: ['go'], + }, + { + title: '基于lerna.js构建monorepo', + contentFile: 'lerna.md', + category: '后端', + tags: ['ts'], + }, + { + title: '通过PHP理解IOC编程', + contentFile: 'php-di.md', + category: '后端', + tags: ['php'], + }, + { + title: '玩转React Hooks', + contentFile: 'react-hooks.md', + category: '前端', + tags: ['react'], + }, + { + title: 'TypeORM fixtures cli中文说明', + contentFile: 'typeorm-fixtures-cli.md', + category: '后端', + tags: ['ts', 'node'], + }, + { + title: '使用yargs构建node命令行(翻译)', + contentFile: 'yargs.md', + category: '后端', + tags: ['ts', 'node'], + }, + { + title: 'Typescript装饰器详解', + summary: + '装饰器用于给类,方法,属性以及方法参数等增加一些附属功能而不影响其原有特性。其在Typescript应用中的主要作用类似于Java中的注解,在AOP(面向切面编程)使用场景下非常有用', + contentFile: 'typescript-decorator.md', + category: '基础', + tags: ['ts'], + }, +]; + +export const categories: CategoryData[] = [ + { + name: '技术文档', + children: [ + { + name: '基础', + }, + { + name: '前端', + }, + { + name: '后端', + }, + { + name: '运维', + }, + ], + }, + { + name: '随笔记忆', + children: [ + { + name: '工作历程', + }, + { + name: '网站收藏', + }, + ], + }, +]; + +export const tags: TagData[] = [ + { + name: 'ts', + }, + { + name: 'react', + }, + { + name: 'node', + }, + { + name: 'go', + }, + { + name: 'php', + }, + { + name: 'devops', + }, +]; diff --git a/src/modules/database/factories/content.factory.ts b/src/modules/database/factories/content.factory.ts new file mode 100644 index 0000000..f723f36 --- /dev/null +++ b/src/modules/database/factories/content.factory.ts @@ -0,0 +1,44 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import * as fakerjs from '@faker-js/faker'; + +import { Configure } from '@/modules/config/configure'; +import { CategoryEntity, CommentEntity, PostEntity, TagEntity } from '@/modules/content/entities'; +import { getTime } from '@/modules/core/helpers/time'; +import { defineFactory, getFakerLocales } from '@/modules/database/utils'; + +export type IPostFactoryOptions = Partial<{ + title: string; + summary: string; + body: string; + isPublished: boolean; + category: CategoryEntity; + tags: TagEntity[]; + comments: CommentEntity[]; +}>; + +export const ContentFactory = defineFactory( + PostEntity, + async (configure: Configure, options: IPostFactoryOptions) => { + const faker = new fakerjs.Faker({ locale: await getFakerLocales(configure) }); + const post = new PostEntity(); + const { title, summary, body, category, tags } = options; + post.title = title ?? faker.lorem.sentence(Math.floor(Math.random() * 10) + 6); + if (summary) { + post.summary = summary; + } + post.body = body ?? faker.lorem.paragraph(Math.floor(Math.random() * 500) + 1); + if (Math.random() > 0.5) { + post.publishedAt = (await getTime(configure)).toDate(); + } + if (Math.random() > 0.5) { + post.deleteAt = (await getTime(configure)).toDate(); + } + if (category) { + post.category = category; + } + if (tags) { + post.tags = tags; + } + return post; + }, +); diff --git a/src/modules/database/resolver/data.factory.ts b/src/modules/database/resolver/data.factory.ts index 668c481..71e661f 100644 --- a/src/modules/database/resolver/data.factory.ts +++ b/src/modules/database/resolver/data.factory.ts @@ -3,6 +3,7 @@ import { isPromise } from 'node:util/types'; import { isNil } from 'lodash'; import { EntityManager, EntityTarget } from 'typeorm'; +import { Configure } from '@/modules/config/configure'; import { panic } from '@/modules/core/helpers'; import { DBFactoryHandler, FactoryOverride } from '@/modules/database/types'; @@ -11,7 +12,7 @@ export class DataFactory { constructor( public name: string, - public config: Configure, + public configure: Configure, public entity: EntityTarget

, protected em: EntityManager, protected factory: DBFactoryHandler, @@ -95,7 +96,7 @@ export class DataFactory { entity[attr] = await (item as any).make(); } } catch (error) { - const message = `Could not make ${(subEntityFactory as any).name}`; + const message = `Could not make ${(item as any).name}`; await panic({ message, error }); throw new Error(message); } diff --git a/src/modules/database/seeders/content.seeder.ts b/src/modules/database/seeders/content.seeder.ts new file mode 100644 index 0000000..4297f8d --- /dev/null +++ b/src/modules/database/seeders/content.seeder.ts @@ -0,0 +1,128 @@ +import * as fs from 'node:fs'; +import path from 'node:path'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import * as fakerjs from '@faker-js/faker'; +import { existsSync } from 'fs-extra'; +import { DataSource, EntityManager, In } from 'typeorm'; + +import { CategoryEntity, CommentEntity, PostEntity, TagEntity } from '@/modules/content/entities'; +import { CategoryRepository, TagRepository } from '@/modules/content/repositories'; +import { getRandomItemData, getRandomListData, panic } from '@/modules/core/helpers'; +import { BaseSeeder } from '@/modules/database/base/BaseSeeder'; + +import { + categories, + CategoryData, + PostData, + posts, + TagData, + tags, +} from '@/modules/database/factories/content.data'; + +import { IPostFactoryOptions } from '@/modules/database/factories/content.factory'; + +import { getCustomRepository, getFakerLocales } from '@/modules/database/utils'; + +import { DBFactory } from '../commands/types'; + +export default class ContentSeeder extends BaseSeeder { + protected truncates = [PostEntity, CategoryEntity, TagEntity, CommentEntity]; + protected factory: DBFactory; + + async run(factory?: DBFactory, dataSource?: DataSource, em?: EntityManager): Promise { + this.factory = factory; + await this.loadCategory(categories); + await this.loadTag(tags); + await this.loadPosts(posts); + } + + private async getRandomComment(post: PostEntity, count: number, parent?: CommentEntity) { + const comments: CommentEntity[] = []; + const faker = new fakerjs.Faker({ locale: await getFakerLocales(this.configure) }); + for (let i = 0; i < count; i++) { + const comment = new CommentEntity(); + comment.body = faker.lorem.paragraph(Math.floor(Math.random() * 18) + 6); + comment.post = post; + if (parent) { + comment.parent = parent; + } + comments.push(await this.em.save(comment)); + if (Math.random() >= 0.7) { + comment.children = await this.getRandomComment( + post, + Math.floor(Math.random() * 5), + comment, + ); + await this.em.save(comment); + } + } + return comments; + } + + private async loadCategory(data: CategoryData[], parent?: CategoryEntity) { + let order = 0; + for (const item of data) { + const category = new CategoryEntity(); + category.name = item.name; + category.customOrder = order; + if (parent) { + category.parent = parent; + } + await this.em.save(category); + order += 1; + if (item.children) { + await this.loadCategory(item.children, category); + } + } + } + + private async loadTag(data: TagData[]) { + for (const item of data) { + const tag = new TagEntity(); + tag.name = item.name; + await this.em.save(tag); + } + } + + private async loadPosts(data: PostData[]) { + const allCategories: CategoryEntity[] = await this.em.find(CategoryEntity); + const allTags = await this.em.find(TagEntity); + for (const item of data) { + const filePath = path.join(__dirname, '../../../assets/posts', item.contentFile); + if (!existsSync(filePath)) { + await panic({ + spinner: this.spinner, + message: `post content file ${filePath} not exits!`, + }); + } + + const options: IPostFactoryOptions = { + title: item.title, + body: fs.readFileSync(filePath, 'utf8'), + isPublished: true, + }; + if (item.summary) { + options.summary = item.summary; + } + if (item.category) { + options.category = await getCustomRepository( + this.dataSource, + CategoryRepository, + ).findOneBy({ id: item.category }); + } + if (item.tags) { + options.tags = await getCustomRepository(this.dataSource, TagRepository).find({ + where: { name: In(item.tags) }, + }); + } + const post = await this.factory(PostEntity)(options).create(); + await this.getRandomComment(post, Math.floor(Math.random() * 8)); + } + + await this.factory(PostEntity)({ + tags: getRandomListData(allTags), + category: getRandomItemData(allCategories), + }).createMany(10); + } +} diff --git a/src/modules/database/types.ts b/src/modules/database/types.ts index a9eac37..3a84933 100644 --- a/src/modules/database/types.ts +++ b/src/modules/database/types.ts @@ -123,5 +123,5 @@ export type DefineFactory = ( ) => () => DBFactoryOption; export type FactoryOverride = { - [Property in keyof Entity]: Entity[Property]; + [Property in keyof Entity]?: Entity[Property]; }; diff --git a/src/modules/database/utils.ts b/src/modules/database/utils.ts index e99ade7..a99d6c9 100644 --- a/src/modules/database/utils.ts +++ b/src/modules/database/utils.ts @@ -1,3 +1,4 @@ +import * as fakerjs from '@faker-js/faker'; import { Type } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; @@ -15,6 +16,7 @@ import { } from 'typeorm'; import { Configure } from '@/modules/config/configure'; +import { AppConfig } from '@/modules/core/types'; import { DBFactoryBuilder, FactoryOptions, @@ -302,3 +304,21 @@ export const factoryBuilder: DBFactoryBuilder = settings, ); }; + +/** + * 本地化假数据 + * @param configure + */ +export async function getFakerLocales(configure: Configure) { + const app = await configure.get('app'); + const locales: fakerjs.LocaleDefinition[] = []; + const locale = app.locale as keyof typeof fakerjs; + const fallbackLocale = app.fallbackLocale as keyof typeof fakerjs; + if (!isNil(fakerjs[locale])) { + locales.push(fakerjs[locale] as fakerjs.LocaleDefinition); + } + if (!isNil(fakerjs[fallbackLocale])) { + locales.push(fakerjs[fallbackLocale] as fakerjs.LocaleDefinition); + } + return locales; +}