养乐多一瓶
IP:
0关注数
0粉丝数
2获得的赞
工作年
编辑资料
链接我:

创作·89

全部
问答
动态
项目
学习
专栏
养乐多一瓶

带你从0到1部署nestjs项目

前言最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn)最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲如何部署。一直有兄弟问prisma如何部署,这篇文章就帮你扫清障碍,文章可能比较长,希望耐心看完后端技术栈nestjsmysqlredisminioprisma部署需要掌握的知识dockergithub actions服务器实战nestjs打包镜像我们部署的时候用的docker,docker需要拉镜像,然后生成容器,docker的知识可以去学习下,这里就默认大家会了,我们在打包的时候要写Dockerfile文件,后端项目是需要保留node_modules的,所以打包的时候一起打进去,我的项目用的pnpm包管理工具,我的文件挂载时有点点问题,我就没有用pm2去执行多阶段打包,多阶段打包速度会比较快,还有就是比如开发环境的依赖可以不打,当然这都是优化的地方,暂时没有去做,大家可以自行尝试# 因为我们项目使用的是pnpm安装依赖,所以找了个支持pnpm的基础镜像,如果你们使用npm,这里可以替换成node镜像 # FROM nginx:alpine FROM gplane/pnpm:8 as builder # 设置时区 ENV TZ=Asia/Shanghai \ DEBIAN_FRONTEND=noninteractive RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/* # 创建工作目录 RUN mkdir -p /app # 指定工作目录 WORKDIR /app # 复制当前代码到/app工作目录 COPY . ./ RUN npm config set registry https://registry.npm.taobao.org/ # pnpm 安装依赖 COPY package.json /app/package.json RUN rm -rf /app/pnpm-lock.yml RUN cd /app && rm -rf /app/node_modules && pnpm install RUN cd /app && rm -rf /app/dist && pnpm build EXPOSE 3000 # 启动服务 CMD pnpm run start:prod 这样后端镜像就构建好了,接下来去编写github action的文件,github actions是做ci/cd的,让我们每次的部署走自动化流程,不要每次手动去做这些工作github actions在我们的根目录下面创建这样一个文件,这个文件名字可以随便取然后在里面编写逻辑name: Docker on: push: branches: ['main'] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write steps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup Docker buildx uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf - name: Cache Docker layers uses: actions/cache@v2 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} restore-keys: | ${{ runner.os }}-buildx- - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image id: build-and-push uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new - name: Move cache run: | rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache - name: SSH Command uses: D3rHase/ssh-command-action@v0.2.1 with: HOST: ${{ secrets.SERVER_IP }} PORT: 22 USER: root PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }} COMMAND: cd /root && ./run.sh这里的['main']就是我们要执行哪个分支,你不是main分支,那就改成你的分支就可以,其他都是固定的模板,直接用SSH Command 这个是我们取做ci/cd的时候,每次我们提交代码,然后配置了ssh密钥,就可以让服务器执行run.sh命令,这个shell脚本我们后面可以用到,这里就记住是让服务器去执行拉取镜像以及执行启动容器的。当我们做到这一步之后,我们提交代码的时候,应该会出现这样的情况因为还没有去配置ssh密钥,这个肯定跑不起来,看到我们上面ssh command里面有两个变量,就是我们要配置的,接下来我们去搞服务器。服务器最近双十一活动,买个服务器还是挺香的,我买的阿里云2核2g的99/年,买的时候选操作系统,随便一个都可以,我因为对ubuntu熟悉一下,就买了ubuntu操作系统的,买好之后,记得重置密码后面我们用shell工具连接的时候需要用到密码的之后我们去下载一个shell工具,连接服务器用的,常见的有xshell finalshell,我用的第二个。就傻瓜式安装,下一步就可以,然后我们去连接一下服务器,去下载宝塔。第二步那里选择ssh连接就可以了,然后主机就是你的服务器公网ip,密码就是刚刚的,用户名就是root连接上了之后,去下载宝塔,这个是ubuntu的命令,其他的操作系统有差别,可以去搜一下就有wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh下载好之后输入bt default命令就可以打开了因为宝塔是个可视化操作面板,比较方便,所以先弄好。接下来我们去搞服务器密钥我们在这里创建好密钥对,记得它只有一次机会,所以下载好了记得保存在你记得住的地方,然后创建好,记得要绑定,不然没效果,然后我们就要得用ssh密钥来连接服务器了至此,我们的服务器也弄好了github绑定密钥这个是settings界面的,然后大家按照步骤创建就可以,到这里我们的配置就结束了。创建shell脚本我们上面不是说了,我们要写一个bash文件吗,现在就要来写,这个bash文件我们要执行拉镜像和跑容器我们可以选择在宝塔中操作docker-compose pull && docker-compose up --remove-orphans然后我们在同目录下也就是root目录下面新建一个docker-compose.yml文件,来启动容器的,这个文件就不要展示了,也就是创建了哪些服务,挂载哪些卷,如果有需要评论区说一下就行,很简单,因为我们用了很多服务,mysql redis minio nginx 这些多镜像,就得多个容器来跑,docker-compose无疑就好到这里后端项目就部署完了,我们还得迁移数据库对吧数据库部署pirsma迁移因为我用的mysql和prisma,typeorm思路差不多,可以一样用。我们的prisma以及typeorm迁移的时候只可以同步表结构,数据不会同步过去,所以我们跑迁移命令的时候,跑完会发现没有数据,我们需要手动导入数据另外注意点,我们docker-compose.yml里面的mysql容器名字对应我们连接的主机名,这里记得更改prisma连接,不然你的prisma还连接在localhost肯定找不到我们来上手操作这是我现在在跑的容器,我要找到我的后端项目对应的容器id,进去执行命令docker exec -it <容器id> /bin/sh 跑这个我们就可以在容器内部执行命令然后就可以把表结构同步过去了,我们也可以在生成Dockerfile的时候写迁移命令也是可以的,这样就不用手动同步了数据库导出我们需要将本地的数据迁移上去,需要先导出sql文件,这个就不用在这里展开说了,很简单,不会可以去找个博客教程,不到30s就完了,导出后我们需要将那个sql文件然后我们在宝塔操作,找到你正在运行的mysql容器目录将你的sql文件上传上去,放哪里都无所谓,记得路径就行然后我们进入mysql容器里面,跑上面的那个命令登录账号 mysql -u root -p输入密码 ******* 输入你数据库连接的那个密码进入之后 USE <database_name> 就选中了那张表然后执行 source 刚刚的那个sql文件路径这样操作数据就同步上去了,注意,数据同步前是一定要有表结构的,所以有先后顺序,这个地方注意。也可以用这个命令, 将sql文件拷贝到你的容器内,然后跑上面的步骤,看个人喜好了。 docker cp /本地路径/your_file.sql 容器名称:/容器路径/your_file.sql到这里我们的部署就结束了,等项目正式上线的时候,还有其他注意点还会再写一篇博客的
0
0
0
浏览量838
养乐多一瓶

Vue3中的diff算法——乱序处理

举例因为乱序的代码是比较多的,所以我们主要来说一下它的核心逻辑。对于我们当前的例子而言,首先,我们经过第一步(自前向后比对)、第二步(子后向前比对)之后,结果如下:对于第三步(挂载多的新节点)、第四步(卸载多的旧节点)而言,我们的新、旧节点数量是相同的,所以不需要做任何处理。这样,我们就来到了第五步 —— 乱序。乱序处理对于乱序而言,它主要处理三种情况:删除旧节点 f挂载新节点 g对于节点 b-d 进行移动删除旧节点删除的逻辑是:如果旧节点的 key 在新节点列表中找不到了,那么我们就认为这个旧节点应该被删除,比如我们这里的节点 old-f,它的 key 是 6,在新节点列表中找不到 key 为 6 的元素,所以我们就将它删除。但是,如果对于每个旧节点,我们都通过这个旧节点的 key 去新节点列表中查找,有时候是不必要的。所以,vue 在这里做了一个优化,它在记录 2 个变量:已经修复的新节点数量 patched 和 新节点待修复的数量 toBePatched。在每次修复完一个新节点后,都会 patched++,然后在执行我们上面的查找行为之前,会判断一下:patched >= toBePatched?,如果是 true,就代表着当前所有的新节点都已经处理完了,那么对于剩下的旧节点,我们不用再去查找,直接删除即可。挂载新节点挂载的逻辑就比较简单了:如果拿新节点的 key,没有找到对应的旧节点,比如我们这里的节点 new-g,它的 key 是 7,在旧节点列表中找不到 key 为 7 的元素,所以我们就将它挂载。移动节点现在,我们有 3 个节点需要移动:b、c、d。那么,如何移动才能最高效呢?我们通过肉眼可以知道:我们只需要将 b 移动到末尾就可以了!但是,vue 该如何知道呢?这里,我们就需要介绍一个 最长递增子序列 的概念。最长递增子序列假如我们有:旧数组:[1, 2, 3, 4, 5, 6]新数组:[1, 3, 2, 4, 6, 5]对于新数组而言,我们如果想找到 递增子序列 的话,可以找到很多:1, 3, 61, 2, 4, 5...确认这个递增子序列的目的,就是为了在由旧数组变成新数组的时候,递增子序列里的这些元素可以不用移动。比如,对于 1, 3, 6 而言,我们可以不需要移动它们,只需要移动剩下的 2, 4, 5 到对应的位置即可,总共需要 3 次移动。对于 1, 2, 4, 5 而言,我们可以不需要移动它们,只需要移动剩下的 3, 6 到对应的位置即可,总共需要 2 次移动。显而易见的是,当我们的递增子序列越长,我们所需要的移动次数就越少。所以,我们求解最长递增子序列的目的就是:减少移动次数,进行更高效地移动。对于我们目前的例子对于我们现在而言,我们的数组里的元素是:新节点在旧节点列表里的索引。比如,我们的 new-c,它在旧节点中的索引是 2,同理,new-d 是 3,new-b 是 1。所以,我们可以得到一个数组:[2, 3, 1]。对于这个数组而言,最长递增子序列为:[2, 3],也就是 c 和 d 不需要移动,只需要移动 b 即可。这样一来,我们就通过 最长递增子序列 完成了最高效的移动操作。总结至此,我们完成了乱序的处理。其中,涉及到了一个新的概念,最长递增子序列,它其实只是用来处理移动的场景,让我们的移动能够高效的进行。
Vue
2
0
0
浏览量960
养乐多一瓶

NodeServe:构建高效静态文件服务器的完美指南

什么是静态文件服务器作为前端搬砖工,一定接触过静态文件服务器。静态文件服务器它的工作是将静态文件通过http/https传输给客户端。静态文件又是什么?静态文件是指内容不需要动态生成的文件,如:图片、CSS文件、JS文件等等文件。我们常用的静态文件服务器有webpack-dev-server这也是为什么我们能在本地开发环境可以通过链接访问页面的原因,还有就是Nginx,一般线上环境使用它,因为它性能更加高效、稳定。运动主页简单,完全没必要在Docker打包时再下载Nginx镜像打包进去,直接用Node.js实现静态文件服务器的功能即可。功能介绍我需要的静态服务器只需要一个功能:当用户请求的内容是文件时,返回文件内容项目静态文件结构是这样的:client ├─src │ └─js文件 └─index.html 详细文件结构请访问仓库地址。启动项目拉取项目:git clone https://github.com/CatsAndMice/keep 下载依赖:npm i 创建.env文件,写入Keep帐号、密码:MOBILE="Keep帐号" PASSWORD="Keep密码" 最后,执行npm run serve,项目即可启动。当我们直接访问http://localhost:3000就能访问index.html文件,index.html文件内容如下:代码实现根据上文的需求描述,我们先用流程图来设计一下我们的逻辑如何实现:静态文件服务器的实现思路还是很简单的:先判断资源存不存在,不存在就直接报错,资源存在的话根据资源的类型返回对应的结果给客户端就可以了。Express实现运动主页非常简单,很多静态JS、图片文件已放置在云端,index.html文件只引用了一份本地的JS文件。不论是什么请求方式,当有请求路径存在/src时都会走app.all('/src/*',()⇒{...})。不同的静态文件对应不同的请求头Content-Type值,如:JS文件对应application/javascript 、CSS文件对应text/css等等,点Content-Type类型可以查看还有哪些值。Content-Type 标头目的是为了告诉客户端实际返回的内容的内容类型,以便客户端依据文件类型进行解析。const express = require('express'); const { getTotal, getFirstPageRecentUpdates } = require("./src") const { to } = require('await-to-js'); const path = require('path'); const fs = require('node:fs') const app = express(); const port = 3000; //获取client文件夹的绝对路径 const htmlPath = path.join(__dirname, './client'); //请求头Content-Type值 const contentType = { '.js': 'application/javascript;charset=utf8' } //省略其他逻辑 //路径存在"/src",执行下列代码块逻辑 app.all('/src/*', (req, res) => { const { url } = req; const filePath = htmlPath + url; //判断文件是否存在 if (fs.existsSync(filePath)) { const extname = path.extname(filePath); res.setHeader('Content-Type', contentType[extname]); const content = fs.readFileSync(filePath); res.send(content); return } //不存在,浏览器状态码返回404 res.sendStatus('404') }) //访问http://localhost:3000时,执行这部分逻辑返回index.html内容 app.get('/', async (req, res) => { res.setHeader('Content-Type', 'text/html;charset=utf8'); const readHtmlPath = htmlPath + '/index.html' const html = fs.readFileSync(readHtmlPath) res.send(html) }) app.listen(port, () => { console.log('服务已开启'); }) 写一个能用的静态文件服务器还是简单的,这里依赖Express框架方便得多。原生模块实现有兴趣的同学可以去写写。最后文章中介绍实现了一个最简单能用的静态文件服务器,如果开发一个完善的静态文件服务器还有非常多的功能要考虑,如:静态文件缓存、压缩等等。另外还有一个真实案例给大家实践,有兴趣的同学clone下来自己玩玩。
0
0
0
浏览量755
养乐多一瓶

如何优雅的使用nestjs实现邮件发送

前言邮箱发送是我们常见的一个服务,本篇文章带大家用nestjs来实现一下前置准备首先我们需要开通邮箱服务,这里我以qq邮箱为例子演示一下我这里已经开通好了,我们第一次开通时会给你一个密钥,这个需要记下来,后面会用到。这里开通很简单,就问你用途,无脑填即可。当我们开通之后,我们在nestjs中创建好一个邮箱服务这里我们需要用到一个包 nodemailer 这个包的生态很成熟,帮我们做好了那些协议,我们直接用即可我的习惯是,对于单独的服务例如邮箱服务,prisma查询构造生成器等,我会放到services目录下,其他模块想使用直接依赖注入就可下面我们进入实操这个是nodemailer这个包需要配置的配置项,我们来解释一下host:服务器邮箱地址,这里qq的是 smtp.qq.com,其他邮箱例如网易的,可以百度搜索的到port: 服务器端口号, qq的是465, 一般都是465secure:表示安全连接auth: 账户信息,第一个user就填写你开通服务的邮箱账号, 第二个pass就是刚刚生成的密钥import { Injectable } from '@nestjs/common'; import * as nodemailer from 'nodemailer'; interface MailInfo { // 接收方邮箱 to: string; // 标题 subject: string; // 文本 text?: string; // 富文本,如果文本和富文本同时设置,富文本生效。 html?: string; } @Injectable() export class EmailService { private transporter: nodemailer.Transporter; private mailConfig = { host: 'smtp.qq.com', port: 465, secure: true, auth: { user: '你开通的邮箱账号', pass: '生成的密钥' } } constructor() { this.transporter = nodemailer.createTransport(this.mailConfig); } async sendEmail(mailInfo: MailInfo) { const info = await this.transporter.sendMail({ from: this.mailConfig.auth.user, //发送方邮箱 ...mailInfo }) return info } }对于mailConfig最好抽离成一个单独的模块,可以用命名空间存放,然后用configService读取出来,这里为了演示就简单操作了。上面的代码就是配置项,主要就是为了利用它的sendMail服务实现邮箱发送,这里需要的参数,我这里也写得很清楚了。接下来让我们实战演练,以邮箱验证码为例实战演练我们在开通账号时,我需要邮箱的验证码,只有验证码正确了才能开通,我的逻辑是,前端在点击验证码发送时,将邮箱账号传递到后端,此时后端生成一个验证码,并且存到redis中,设置有效期,然后通过邮箱服务将验证码发送给这个邮箱账号,在邮箱中获取到验证码后再在表单中输入,然后提交时,再和redis中的验证码进行比对。前端代码我就不展示了,就一个点击发送后设置一个倒计时,主要还是展示后端 @Post('/send/emailCaptcha') async sendEmailCaptcha(@Body() emailInfo: {email: string}){ if(!emailInfo) { throw new HttpException('邮箱不能为空', HttpStatus.BAD_REQUEST) } //生成随机四位数 const emailCaptcha = Math.floor(Math.random() * 9000) + 1000 //生成的数据存在redis中,后面添加用户做验证 await this.redisClient .multi() .set( `emailCaptcha:${emailInfo.email}`, emailCaptcha, ) .expire( `emailCaptcha:${emailInfo.email}`, 60 * 30) //30min .exec() this.emailService.sendEmail({ to: emailInfo.email, html: `<div> 您本次的验证码是<span style="color:#FFB6C1; font-weight:700; font-size:24px">${emailCaptcha}</span>, 验证码有效期是30分钟 </div>`, subject: 'xxx平台邮箱检验提醒' }) }这样当我们前端点击验证码发送后,调用这个接口,然后我们就可以在邮箱中收到验证码了差不多就是这样一个效果,具体样式可以自己调整,核心逻辑就是这里了,后面的逻辑就很简单了,不做演示了。
0
0
0
浏览量377
养乐多一瓶

【Node.js】写一个数据自动整理成表格的脚本

企业项目进行数据埋点后,埋点事件名需要整理成Excel表格便于统计,目标是将下图左侧数据转化成下图右侧的Excel表格:考虑到左侧埋点数据是随项目迭代增加的,埋点数据每增加一次我就要把数据一条一条的Ctrl+C/V复制粘贴至Excel表格内。懒,不想这样玩,于是我写了一个自动帮我整理成表格的脚本。脚本实现实现流程Node.js生成Excel表格工具库技术选型单独复制一份埋点数据出来,保证它的变动不会影响业务相关埋点逻辑整理埋点数据成我们需要的数据结构分成三步走技术选型Node.js操作Excel表格工具库有:exceljsexcellentexportnode-xlsxxlsx-template仅罗列以上四个。选择的角度有以下几点:学习成本低,文档API简单易用,仅生成表格即可,其他功能并不需要,所以API越简单越好生成Excel表格需要提供的数据结构简单,便于实现能导出xlsx表格,满足最基本要求node-xlsx最贴近以上要求,首选使用它。node-xlsx官方生成Excel表格给出的代码块:import xlsx from 'node-xlsx'; // Or var xlsx = require('node-xlsx').default; const data = [ [1, 2, 3], [true, false, null, 'sheetjs'], ['foo', 'bar', new Date('2014-02-19T14:30Z'), '0.3'], ['baz', null, 'qux'], ]; var buffer = xlsx.build([{name: 'mySheetName', data: data}]); // Returns a buffer 生成表格数据data是二维数组,对应表格的行列。data.length 为表格行数,data[0].length 为表格的列数;data[0][0]对应至表格的第一行第一列的值,data[0][1]对应至表格的第一行第二列的值。所以将埋点数据整理为一个二维数组即可,二维数组数据结构整理容易实现。复制埋点数据埋点数据统一放置在buryData.js 文件,但不能随意改动它,所以将该文件单独再复制一份出来。buryData.jsexport default { version1: 'v1.5.3', bury1: 'ding提醒', bury2: '审批-筛选', bury3: '任务-点击任务标题打开任务详情', bury4: '任务详情弹框-点击详情tab', bury5: '任务详情弹框-点击日志记录tab', bury6: '任务详情弹框-点击工作总结tab', bury7: '任务详情弹框-点击动态tab', //... } buryData.js复制出来文件命名为bury.js,还有一个问题:bury.js需要执行它,拿到它导出的数据对象,导出数据是使用ES6模块化语法,这边需要将ES6模块化转化成CommonJs模块化,将export default {} 替换成module.exports ={}即可做到。Node.js fs模块+正则替换是可以达成以上目的,但为了更快捷,我选择使用工具库magic-stringmagic-string它是操作字符串库,它可以帮我去掉写正则替换字符串的步骤。const path = require('path'); const magicString = require('magic-string') const fs = require('fs'); //buryData.js 文件路径 const buryFile = path.join(__dirname, '../src/lib/buryData.js') const getBuryContent = (filePath) => { const content = fs.readFileSync(filePath, 'utf8') //将export default 替换成module.exports = const s = new magicString(content) s.replace('export default', 'module.exports = ') return s.toString() } (async () => { const str = getBuryContent(buryFile) //将替换后的内容写入至bury.js文件 const copyFilePath = path.join(__dirname, '/bury.js') fs.writeFileSync(copyFilePath, str) //动态导入bury.js 获取埋点数据 const { default: data } = await import(copyFilePath) })() 生成二维数组上文已提及,node-xlsx生成表格需要先将数据整理成二维数组。export default { version1: 'v1.5.3', bury1: 'ding提醒', /... version2: 'v1.5.4', bury21: '通讯录人员列表', //.. } 以上数据整理成:[ ['v1.5.3','v1.5.4'], ['ding提醒','通讯录人员列表'], //... ] 首先,将数据全部存放至一个Map对象中。因为埋点数据是一个对象,其中version1、version2 表示版本号,随项目迭代版本号会增多version3、version4……以version进行划分Map 值。const _ = require('lodash'); //... const getFormatDataMap = (data) => { let version const map = new Map(); _.forIn(data, (value, key) => { if (key.includes('version')) { version = value !map.has(version) && map.set(version, [value]) return } const mapValue = map.get(version) mapValue.push(value) }) return map } (async () => { const str = getBuryContent(buryFile) const copyFilePath = path.join(__dirname, '/bury.js') fs.writeFileSync(copyFilePath, str) const { default: data } = await import(copyFilePath) //新增 const map = getFormatDataMap(data) })() getFormatDataMap 函数执行后,返回的数据是:{ 'v1.5.3'=>['v1.5.3','ding提醒' //...] 'v1.5.4'=>['v1.5.4','通讯录人员列表' //...] } 然后,需要知道表格最大行数,表格列数即为map.size(),最大行数通过获取Map.values()获取所有的值values,遍历values获取values内存放的每一个数组的长度,长度统一用另一个数组lens临时记录,遍历结束后比较lens中的数值得到最大的值。MAX_LEN 即为表格最大的行数,也是values存放的所有数组中长度最大的值。const _ = require('lodash'); //... const getMergeArr = (map) => { const values = _.toArray(map.values()) const lens = [] //获取长度,长度值统一存放至lens数组中 values.forEach((value) => { lens.push(value.length) }) //比较 const MAX_LEN = _.max(lens) return getTargetItems({ mapValue: values, forNum: MAX_LEN }) } (async () => { const str = getBuryContent(buryFile) const copyFilePath = path.join(__dirname, '/bury.js') fs.writeFileSync(copyFilePath, str) const { default: data } = await import(copyFilePath) const map = getFormatDataMap(data) //新增 const table = getMergeArr(map) })() 最后,以values、MAX_LEN进行双循环。表格列数map.size()可获取,但为了方便直接mapValue.length,两者是相等的。有了表格列数即可创建二维数组的第二层数组,new Array(len).fill(' ')第二层数组长度即为mapValue.length,创建时数组内的值先统一填充为' '。const getTargetItems = ({ mapValue, forNum }) => { const len = mapValue.length const targetItems = [] mapValue.forEach((v, i) => { for (let index = 0; index < forNum; index++) { const element = v[index]; let targetItem = targetItems[index] if (!targetItem) { //创建数组,值先统一填充为' ' targetItem = new Array(len).fill(' ') } /** 如果当前index大于数组v的长度,这时获取值v[index]为undefined。 为undefined的话直接跳过,保持targetItem[i]为' ' */ targetItem[i] = element ? element : ' ' targetItems[index] = targetItem } }) return targetItems } 完成二维数组的转化,数据结构为下图:生成表格数据已完成,留下的就是写入数据生成表格,直接复制node-xlsx演示的代码下来。//... (async () => { const str = getBuryContent(buryFile) const copyFilePath = path.join(__dirname, '/bury.js') fs.writeFileSync(copyFilePath, str) const { default: data } = await import(copyFilePath) const map = getFormatDataMap(data) const table = getMergeArr(map) //写入数据,生成表格,返回buffer数据 const buffer = xlsx.build([{ name: '埋点', data: table }]) const outPath = path.join(__dirname, '/bury.xlsx') //bury.js文件可以删除,bury.xlsx如果已存在就先删了 fs.existsSync(outPath) && fs.unlinkSync(outPath) fs.existsSync(copyFilePath) && fs.unlinkSync(copyFilePath) //创建一个bury.xlsx文件,将得到的buffer写入 fs.writeFileSync(outPath, buffer) })() 脚本完。完整源码:const path = require('path'); const fs = require('fs'); const xlsx = require('node-xlsx'); const magicString = require('magic-string') const _ = require('lodash'); const buryFile = path.join(__dirname, '../src/lib/buryData.js') const getBuryContent = (filePath) => { const content = fs.readFileSync(filePath, 'utf8') const s = new magicString(content) s.replace('export default', 'module.exports = ') return s.toString() } const getFormatDataMap = (data) => { let version const map = new Map(); _.forIn(data, (value, key) => { if (key.includes('version')) { version = value !map.has(version) && map.set(version, [value]) return } const mapValue = map.get(version) mapValue.push(value) }) return map } const getTargetItems = ({ mapValue, forNum }) => { const len = mapValue.length const targetItems = [] mapValue.forEach((v, i) => { for (let index = 0; index < forNum; index++) { const element = v[index]; let targetItem = targetItems[index] if (!targetItem) { targetItem = new Array(len).fill(' ') } targetItem[i] = element ? element : ' ' targetItems[index] = targetItem } }) return targetItems } const getMergeArr = (map) => { const values = _.toArray(map.values()) const lens = [] values.forEach((value) => { lens.push(value.length) }) const MAX_LEN = _.max(lens) return getTargetItems({ mapValue: values, forNum: MAX_LEN }) } (async () => { const str = getBuryContent(buryFile) const copyFilePath = path.join(__dirname, '/bury.js') fs.writeFileSync(copyFilePath, str) const { default: data } = await import(copyFilePath) const map = getFormatDataMap(data) const table = getMergeArr(map) debugger const buffer = xlsx.build([{ name: '埋点', data: table }]) const outPath = path.join(__dirname, '/bury.xlsx') fs.existsSync(outPath) && fs.unlinkSync(outPath) fs.existsSync(copyFilePath) && fs.unlinkSync(copyFilePath) fs.writeFileSync(outPath, buffer) })() 去掉空行,一百行以内。总结Node.js可使用的场景非赏多,不单单是用于服务器接口的开发,我们还能通过写脚本的形式解决生活中重复性的工作,凭藉js的语法简单及强大的生态,前端不必学习shell、python等,仅使用js就可以搞定爬虫、自动化脚本等场景。
0
0
0
浏览量499
养乐多一瓶

nest自定义验证类及自定义验证装饰器

书接上回,我们在class-validator发现了许多优秀的验证规则,但是在我们实际开发需求中,有很多地方需要验证,但是包缺少了,这个时候只能我们自己去写了,我们也是根据这个包的文档去实现自定义验证。自定义验证类这次我们以常见的注册模块中的两次密码验证为例带领大家来实现一下。首先创建一个post请求用于表单提交的验证,并使用管道,管道在上一篇有提到过,相信看到这里的小伙伴都了解管道了,不多说,管道代码给大家贴出import { ArgumentMetadata, BadRequestException, HttpException, HttpStatus, Injectable, PipeTransform } from '@nestjs/common'; import { plainToInstance } from 'class-transformer' import { validate } from 'class-validator'; @Injectable() export class MengPipe implements PipeTransform { async transform(value: any, metadata: ArgumentMetadata) { const object = plainToInstance(metadata.metatype, value) const errors = await validate(object) if (errors.length) { const messages = errors.map(error => ({ name: error.property, message: Object.values(error.constraints).map(v => v) })) throw new HttpException(messages, HttpStatus.BAD_REQUEST) } return value } } 接着在dto文件夹下面创建一个验证密码的文件,这两个都是class-validator给我们提供的可以直接用,上篇文章都有讲 接着在github找到这个地方 将代码复制下来后,接着创建一个rules文件夹存放自定义验证规则 我们在使用的时候其实就是向外暴露了validate这个函数,下面的defaultMessage是返回我们的报错信息的,所以结构还是比较简单的,具体可以看官方文档,很清晰。我们对代码来简单调整一下装饰器里面的用不到,先给他拿走, 来看看text以及args里面是啥东西 先把这个服务类在dto里面注册一下,让它明确我们要验证的是密码 这里返回的是错误不用管,因为我们validate返回的是fasletext就是我们要验证的字段的数值args可以拿到很多属性,这样我们就可以做验证了  当我们输入正确时可以成功返回,不正确时也能够捕捉错误但是我们的写法有点low,来优化一下 这样,如果有很多需要验证的我们都可以用这一个验证类,达到复用的效果。自定义验证装饰器我们来用唯一字段验证这个例子带领大家掌握这个的用法 我们在rules下创建一个is_not_exist.ts文件这个类型报错可以这样解决 把这个装饰器给引入一下 我们发现需要传入1~2个参数 property validationOptions ,也就是下面这里要传的参数我们先在mysql中创建一条表数据 我们这里就以的添加表字段为例,实现一下验证装饰器的用法我们以prisma实例来写一个查表的方法 这里要注意异步处理。 我们用的是是user这张表,所以传的property是'user' 当我们发起一个携带同样的username的post请求时 发现已经成功实现  ,正常请求也是没有问题写在最后nest有很多值得我们去探索的东西,我总结的都是在开发中大概率会用到的,希望能帮助到大家!
0
0
0
浏览量562
养乐多一瓶

Vue3中的diff算法——什么时候需要使用diff算法

不是所有的更新都需要用到 diff算法在我们日常开发中,对于一个节点的操作主要有 3 种:挂载、更新和卸载。在更新的时候,我们就会涉及到 diff 算法。但不是所有的更新都需要用到 diff算法,比如:旧节点:<div>hello</div>新节点:<div>hello update</div>这个时候,只是 div 的文本发生了变化,所以我们只要将 div 的文本内容进行直接更新就可以了。div.textContent = "hello update";我们并不需要使用什么 diff 算法。需要使用 diff算法 的地方那么,什么场景下,我们需要 diff 算法呢?—— 当我们需要对一组节点进行更新的时候!!比如:旧节点,ul 元素下是一组 li,总共 3 个 li:<ul> <li>a</li> <li>b</li> <li>c</li> </ul>新节点,ul 元素下也是一组 li,总共也是 3 个 li:<ul> <li>a</li> <li>b</li> <li>d</li> // 第三个 li 元素发生了变化!👾 </ul>这个时候,我们应该如何更新我们的节点呢?最简单的做法是:依次删除旧的 a、b、c 三个 li依次挂载新的 a、b、d 三个 li总共 6 次操作。但是,我们可以有更聪明的做法,那就是对于我们的可以把新、旧两组 li 进行遍历,然后比较出差异,再进行更新,比如:jsconst oldChildren = ul1.children; const newChildren = ul2.children; for (let i = 0; i < oldChildren.length; i++) { // 调用 patch 函数依次更新子节点 patch(oldChildren[i], newChildren[i]); }这个时候,我们只要比较 3 次,就可以完成更新,比之前的 6 次,效率提升了 1 倍!总结当我们比较 2 组子节点时,用于比较的算法就是 diff算法。使用 diff算法 的目的,就是为了减少性能开销,提高效率!
Vue
0
0
0
浏览量570
养乐多一瓶

仿jsDoc写一个最简单的文档生成

上图,从isNaN.ts 转成isNaN.md,在/*...*/写好注释直接生成该方法的文档介绍,这是我想要的。类似jsDoc工具提供的功能。其实,学习编程的过程就是学习造轮子的过程,咱也不重新造轮子,仿写一个最简单的功能即可。step1:前置构思JavaScript注释一般有两种方式:单行注释、多行注释单行注释//单行注释 多行注释/** *多行注释 * */ 使用的是TypeScript语言,它是JavaScript的超集,注释方式与JavaScript完全是一样的。我们只考虑多行注释,忽略掉单行注释。多行注释应该包含以下部分:方法的描述,具体做什么的使用者需要知晓该方法是哪个版本新增的调用方法需要传入的参数详情调用方法后,它返回的值使用该方法的具体示例代码多行注释时,需要规定关键字段用于表示上述部分的内容。规定:@desc、@version、@param、@return、@example 分别表示方法描述、该方法新增的版本、方法传入的参数详情、方法调用返回的值、示例代码。step2: 实现思路提取代码文件中的多行注释代码文件不止一个,这边需要遍历获取/src/xx/ 路径下所有的.ts 文件:typescript复制代码import { srcPath } from "../build/const" import getSrcLists from "../build/getSrcLists" (async () => { const lists = await getSrcLists(srcPath) })() srcPath 即为目录src 路径, const srcPath = path.join(__dirname, '../src'); 一行代码得到src绝对路径。getSrcLists 方法用于获取src目录下所有的文件或文件夹:import fsPromises from "fs/promises"; export default async (examplePath: string) => { const dirs = await fsPromises.readdir(examplePath) const isIncludes = (dir: any[] | string) => dir.includes('.DS_Store') if (isIncludes(dirs)) { const index = dirs.findIndex((dir) => isIncludes(dir)) dirs.splice(index, 1) } return dirs } 调用NodeJS原生文件模块fs/promises下的fsPromises.readdir(examplePath)方法,即可获取到examplePath路径下所有的文件或文件夹。有坑小心,MacBook系统有文件夹下会自动生成.DS_Store 文件的问题,至于什么是.DS_Store 各位读者自己去寻找答案了。.DS_Store我们不需要,而且会导致拼接路径读取文件时报错,这里需要将其移除掉。src目录结构如下:/src /Array /customKey.ts /getMax.ts /... /Date /isDate.ts /... /... 想获取.ts文件,即路径上还需要再拼接一个文件夹名,再次调用getSrcLists方法:import { srcPath } from "../build/const" import getSrcLists from "../build/getSrcLists" import path from "path" (async () => { const lists = await getSrcLists(srcPath) lists.forEach(async list => { const filePath = path.resolve(srcPath, list) const files = await getSrcLists(filePath) }) })() 接下来,重复调用path.resolve(filePath, file)路径再拼接上xxx.ts文件名,即为完整的xxx.ts 路径:import { srcPath } from "../build/const" import getSrcLists from "../build/getSrcLists" import path from "path" (async () => { const lists = await getSrcLists(srcPath) lists.forEach(async list => { const filePath = path.resolve(srcPath, list) const files = await getSrcLists(filePath) files.forEach(async file => { const srcFilePath = path.resolve(filePath, file) const content = await fsPromises.readFile(srcFilePath, 'utf-8') }) }) })() 最后终于可以fsPromises.readFile(srcFilePath, 'utf-8') 把拼接完整路径的xxx.ts文件内容读取出来。每一个xxx.ts 文件可能导出不止一个方法,例如:对于这种情况,我们依然往对应的.md文件内叠加内容。针对上述问题,需要把每一个xxx.ts文件中的每一个多行注释块提取单独处理。TypeScript使用的是ES6模块化,import ... from "..." 导入,export 关键字导出。每一个导出的方法必须使用export,这个是不变的。如此针对读取xxx.ts内容后,以export分割内容,即可把每个xxx.ts文件内的每一个多行注释块分离出来:import { srcPath } from "../build/const" import getSrcLists from "../build/getSrcLists" import path from "path" (async () => { const lists = await getSrcLists(srcPath) lists.forEach(async list => { const filePath = path.resolve(srcPath, list) const files = await getSrcLists(filePath) files.forEach(async file => { const srcFilePath = path.resolve(filePath, file) const content = await fsPromises.readFile(srcFilePath, 'utf-8') //新增 const exportsArray = content.split('export') }) }) })() 为方便下一步提取多行注释中的关键字段,要做的是把多行注释干净的提取出来,不需要有任何其他代码片段,正则exec这个时候就能派上用场:import { srcPath } from "../build/const" import getSrcLists from "../build/getSrcLists" import path from "path" import { ANNOTATION } from "./const" //新增 function getAnnotation(content: string) { const execContent = ANNOTATION.exec(content) if (isEmpty(execContent)) return const comment = (execContent as any[])[0] return comment } (async () => { const lists = await getSrcLists(srcPath) lists.forEach(async list => { const filePath = path.resolve(srcPath, list) const files = await getSrcLists(filePath) files.forEach(async file => { const srcFilePath = path.resolve(filePath, file) const content = await fsPromises.readFile(srcFilePath, 'utf-8') const exportsArray = content.split('export') //新增 const promises: any[] = [] exportsArray.forEach(exportsContent => { if (isEmpty(exportsContent)) return promises.push(Promise.resolve().then(() => getAnnotation(exportsContent))) }) }) }) })() 其中ANNOTATION 值即为正则/\/\*(\s|.)*?\*\//提取多行注释中的关键字段通过上面步骤,我们就可以完整的获取到干净的多行注释,现在要获取多行注释中的关键字段,如@desc 。多行注释是以/**开头,*/结束,它们对于我们来说是多余字符,所以在获取关键字段前,我们先将其处理掉。//省略 import { ANNOTATION, CHAR } from "./const" //新增 function getQuery(comment: string) { comment = comment.replace(/\*\/$/, '')// */替换成"" let splitComment = comment.split(CHAR) splitComment = splitComment.slice(1, splitComment.length).map(val => (CHAR + val.replace(/((\* $)|(\* ))/gm, '')).trim()) return splitComment } function getAnnotation(content: string) { const execContent = ANNOTATION.exec(content) if (isEmpty(execContent)) return const comment = (execContent as any[])[0] return getQuery(comment) } //省略 CHAR 常量值为@,关键字段均是以@ 开头定义,所以用@再次分割内容,将内容分割多个部分,分割后数据清理/**。这是多行注释的开头,分割后索引为0,将其剔除splitComment.slice(1, splitComment.length),保留的数据由于只对@、/**、*/做了处理,所以现有数据头部存在* 特殊字符。(CHAR + val.replace(/((\* $)|(\* ))/gm, '')).trim() 这行它做了三件事:替换*为""、去除字符串两端空字符、重新将@拼接。整理关键字段内容,以合适的数据结构存储这个时候,splitComment 变量值即存储了所有的关键字段内容,数据结构为:["@desc 判断参数是否为`NaN`","@version v3.3.5",...] 这样的数据结构并不方便取值,所以要把现在的数据结构转变下。//省略 //新增 function cacheMap(comments: string[]) { const map = new Map<string, Set<string>>() const reg = /^@([a-z]*) / comments.forEach((comment) => { const regComment = reg.exec(comment) if (isEmpty(regComment)) return const readyComment = comment.replace(reg, '') const key = (((regComment as any[])[0]) as string).trim().replace(CHAR, '') const mapValue = map.get(key) if (mapValue) { mapValue.add(readyComment) return } const set = new Set<string>() set.add(readyComment) map.set(key, set) }) return map } function getQuery(comment: string) { comment = comment.replace(/\*\/$/, '') let splitComment = comment.split(CHAR) splitComment = splitComment.slice(1, splitComment.length).map(val => (CHAR + val.replace(/((\* $)|(\* ))/gm, '')).trim()) return cacheMap(splitComment) } //省略 依然是使用正则const reg = /^@([a-z]*) /,在遍历comments数组时,将@desc 等关键字段捕获。如"@desc 判断参数是否为`NaN`" 捕获出"@desc ",并在原字符串上将其替换成"",原字符串变成"判断参数是否为`NaN`"。捕获出的"@desc "再去除掉@、去除掉两端空字符,把desc 作为Map对象的Key值;原字符串"判断参数是否为`NaN`"作为Set对象的Value值;将该Set对象作为Map对象中Key值为desc的Value。使用Set对象的原因:支持多个相同关键字段/** * ... * @param value(any):参数1 * @param value(any):参数2 * ... */ 如上注释,多个@param 描述参数,实际中多个参数是正常的,因此我们要做的是获取多个相同关键字段内容时去重,去重使用Set对象是最方便的。经过cacheMap 方法处理后,最终的数据结构为:Map:{ desc:Set["判断参数是否为`NaN`"], version: Set["v3.3.5"], ... } 提前定好文档模板,对应位置填写入关键字段内容template.ts://省略 export const generateDocs = async (doc: docs, callBack: () => string) => { const filePath = callBack() const splitFilePath = filePath.split(path.sep) const file = splitFilePath[splitFilePath.length - 1] if (or(isEmptyObj(doc), isEmpty(doc.version))) { err(`请完善${file}文档`) process.exit(1); } return `${doc.desc ? getSetValue(doc.desc) : ''} **添加版本** ${getSetValue(doc.version)} **参数** ${doc.param ? getSetValue(doc.param) : ''} **返回** ${doc.return ? getSetValue(doc.return) : ''} **例子** ${doc.example ? getExample(getSetValue(doc.example)) : await generateExample(filePath)}` } 仅粘贴核心部分,其中该部分很容易理解。doc 为对象,由Map对象转化成Object对象,然后判断param、return 等字段是否存在,存在则将字段值传递给getSetValue 方法。function getSetValue(set: Set<string>) { let setValue = '' set.forEach((s) => { setValue += `${s} \n` }) return setValue } 其他逻辑是处理一些边界。完整代码:medash/template.ts at dev · CatsAndMice/medash (github.com)最后创建.md文件,写入内容(async () => { const lists = await getSrcLists(srcPath) lists.forEach(async list => { const filePath = path.resolve(srcPath, list) const files = await getSrcLists(filePath) files.forEach(async file => { //省略 const promises: any[] = [] exportsArray.forEach(exportsContent => { if (isEmpty(exportsContent)) return promises.push(Promise.resolve().then(() => getAnnotation(exportsContent))) }) let docsContent = '' Promise.all(promises).then(async (result) => { const docsPromises: any[] = [] result.forEach((res) => { //拼接字符串 const promiseFn = async () => { const isMapNoSize = isMap(res) && isEmpty(res.size) if (or(isEmpty(res), isMapNoSize)) return const docs = await generateDocs((mapToObj(res as Map<string, Set<string>>)) as docs, () => getLastPath(srcFilePath)) if (isEmpty(docs)) return docsContent += `${docs} \n` return docsContent } //Promise类型统一添加至数组中 docsPromises.push(promiseFn()) }) }) }) }) })() Promise.all(promises) 等待所有的Promise结束后,我们再遍历所有Promise返回的结果,将生成的内容逻辑代码,使用async函数promiseFn包裹,执行promiseFn()并push至docsPromises数组。同样的逻辑,还是使用 Promise.all,等待docsPromises数组中所有的Promise对象出结果后,说明所有生成的内容已拼接赋值于docsContent 变量:(async () => { const lists = await getSrcLists(srcPath) lists.forEach(async list => { const filePath = path.resolve(srcPath, list) const files = await getSrcLists(filePath) files.forEach(async file => { //省略 let docsContent = '' Promise.all(promises).then(async (result) => { const docsPromises: any[] = [] result.forEach((res) => { //拼接字符串 const promiseFn = async () => { const isMapNoSize = isMap(res) && isEmpty(res.size) if (or(isEmpty(res), isMapNoSize)) return const docs = await generateDocs((mapToObj(res as Map<string, Set<string>>)) as docs, () => getLastPath(srcFilePath)) if (isEmpty(docs)) return docsContent += `${docs} \n` return docsContent } //Promise类型统一添加至数组中 docsPromises.push(promiseFn()) }) //新增 //所有的Promise完成后,docsContent已拼接完成 Promise.all(docsPromises).then(() => { const splitFilePath = filePath.split(path.sep) const mdPath = path.join(docsPath, splitFilePath[splitFilePath.length - 1]) isEmpty(docsContent) ? null : createDocs(mdPath, file.replace(/\.[a-z]*$/, ''), docsContent) }) }) }) }) })() createDocs 就是一个创建、写入内容的方法逻辑,相对容易不进行粘贴了。至此,一个最简单的文档生成功能就完成了。完整代码:medash/index.ts at dev · CatsAndMice/medash (github.com)
0
0
0
浏览量458
养乐多一瓶

Node.js搭建Https服务

Node.js用于做小程序后台服务,域名要求必须是Https协议。在Node.js开启Http服务是非常简单的,如下:const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/html;charset=utf8' }); res.end('访问成功') }); server.listen(8080, () => { console.log('服务已开启'); }) 如果想使用Https服务需要两步:1. 需要有一份SSL证书;2. 使用Node.js自身的Https模块。SSL证书获取SSL证书方式有两种:自己借助openSSL工具生成SSL证书下载某些平台提供的免费/付费的SSL证书(推荐)我是使用某云平台提供免费的证书点击下载后选择服务器类型下载后的文件分别是以.key、.pem为后缀,其中.key文件是base64加密私钥,.pem文件是base64加密的证书使用Node.js自身的Https模块开启一个服务相较Http,它多了一个options参数。const https = require('https'); const fs = require('fs'); const path = require('path'); const options = { key: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.key')), cert: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.pem')), }; const server = https.createServer(options, (req, res) => { res.writeHead(200, { 'Content-Type': 'text/html;charset=utf8' }); res.end('访问成功') }); server.listen(8080, () => { console.log('服务已开启'); }) 由于SSL证书我绑定的域名是www.linglan01.cn ,当我使用https://127.0.0.1:8080 访问服务时与绑定的域名不相符,它会被拦截访问,仅允许 www.linglan01.cn 访问。使用域名为www.linglan01.cn 才能正常的访问。使用Express框架开启Https工作中肯定是使用社区的Express等框架进行开发,想在Express等框架中开启Https也非常容易,以Express举例:const https = require('https'); const fs = require('fs'); const path = require('path'); const express = require('express') const app = express(); app.get('/chat', (req, res) => { res.send('我是https') }); const options = { key: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.key')), cert: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.pem')), }; const server = https.createServer(options, app); server.listen(8080, () => { console.log('服务已开启'); }) Node.js中搭建Https服务不难,Node.js已经为我们提供了Https模块可以快捷的完成搭建。Https服务实际中仅会使用到线上环境,如果本地环境也需要,我们也可以使用openSSL工具生成一个证书。
0
0
0
浏览量510
养乐多一瓶

Vue3中的diff算法——diff算法的前4步处理

diff 算法的 5 大步首先,我们来看下源码中,diff 算法的步骤分为哪些:// vue-next/packages/runtime-core/src/renderer.ts/patchKeyedChildren 中 const patchKeyedChildren = ( oldChildren, // 旧的一组子节点 newChildren, // 新的一组子节点 ) => { let i = 0 // 新的一组子节点的长度 const newChildrenLength = newChildren.length // 旧的一组子节点中最大的 index let oldChildrenEnd = oldChildren.length - 1 // 新的一组子节点中最大的 index let newChildrenEnd = newChildrenLength - 1 // 1. 自前向后比对 while (i <= e1 && i <= e2) { ... } // 2. 自后向前比对 while (i <= e1 && i <= e2) { ... } // 3. 新节点多于旧节点,挂载多的新节点 if (i > e1) { if (i <= e2) { ... } } // 4. 新节点少于旧节点,卸载多的旧节点 else if (i > e2) { while (i <= e1) { ... } } // 5. 乱序 else { ... } }可以看到,vue 总共分为 5 步来进行 diff 算法,那么我们依次来看一下每一步都是怎么做的。第一步:自前向后比对这里我们简化了一下代码,并将变量名重新命名了一下,下同。let i = 0; while (i <= oldChildrenEnd && i <= newChildrenEnd) { const oldVNode = oldChildren[i]; const newVNode = newChildren[i]; if (isSameVNodeType(oldVNode, newVNode)) { patch(oldVNode, newVNode); } else { break; } i++; }举例,以下的所有元素都是 li 元素:第一步的话,vue 是先定义了一个指针 i,初始值为 0。然后分别获取第 1 个旧节点,第 1 个新节点,并将它们扔到一个 isSameVNodeType 的函数中。我们来看下这个 isSameVNode 函数:export function isSameVNodeType(n1: VNode, n2: VNode): boolean { return n1.type === n2.type && n1.key === n2.key; }这个函数就是在比较,两个节点的 type 和 key 是否相同,如果相同,就认为是同一个节点。那么,对于我们目前的例子而言,第 1 个旧节点,第 1 个新节点,他们的 type 都是 li,key 都为 1,所以它们是相同的节点,直接 patch 更新就好。更新完第一个节点后,i++,依旧满足 while 循环的条件,所以我们继续进入循环。同理,会更新第二、三个节点,这样子,我们就处理完了所有的子节点了。最后,我们来观察下,我们这个 while 循环什么时候会提前 break 掉呢?if (isSameVNodeType(oldVNode, newVNode)) { patch(oldVNode, newVNode); } else { break; }可以发现,就是当 isSameVNodeType 返回 false 的时候,所以假如当我们碰到一对新、旧节点,它们不是相同的节点,那么这个循环就会提前 break 掉。第二步:自后向前比对在第 1 步自前向后的循环结束之后,就会来到第 2 步 —— 子后向前比对。// 旧的一组子节点中最大的 index let oldChildrenEnd = oldChildren.length - 1; // 新的一组子节点中最大的 index let newChildrenEnd = newChildrenLength - 1; while (i <= oldChildrenEnd && i <= newChildrenEnd) { const oldVNode = oldChildren[oldChildrenEnd]; const newVNode = newChildren[newChildrenEnd]; if (isSameVNodeType(oldVNode, newVNode)) { patch(oldVNode, newVNode, container, null); } else { break; } oldChildrenEnd--; newChildrenEnd--; }举例,以下的所有元素都是 li 元素,注意,new-a 的 key 和 old-a 的 key 不同:根据我们上面所说的,首先还是会来到第一步,自前向后比对。但是当比较第一个新、旧节点的时候,发现:key 不同,所以不是同一个节点,直接会 break 掉,然后就会进入到第二步 —— 自后向前比对。自后向前比对 的步骤其实也是和 自前向后比对 的步骤很类似,也是如果是相同的节点,那么就 patch 更新,否则就 break 掉,只不过是倒叙的一个循环而已。这样一来,所有的子节点也可以更新完毕。第三步:新节点多于旧节点,挂载多的新节点上面的例子中,我们的节点数量是相同的。但如果节点数量不同,那么就要涉及到 挂载 和 卸载 的操作了。我们先来看下 新节点多于旧节点 的情况:经过第一步、第二步之后,i、oldChildrenEnd、newChildrenEnd 的值如图:可以看到,我们只需要挂载 i 到 newChildrenEnd 之间的新元素即可。第四步:新节点少于旧节点,卸载多的旧节点我们再看下 新节点少于旧节点 的情况:经过第一步、第二步、第三步之后,i、oldChildrenEnd、newChildrenEnd 的值如图:可以看A到,我们只需要卸载 i 到 oldChildrenEnd 之间的旧元素即可。第五步:乱序乱序的代码是比较多的,有 100 多行:而且,其中还涉及到了 最长递增子序列 这样的新概念,所以我们下次再单独来讲它。总结这次,我们一共分析了 vue3 中 diff算法 的前 4 步:自前向后比对自后向前比对新节点多于旧节点新节点少于旧节点通过这几步,我们已经能够处理一些基本的场景了。
Vue
0
0
0
浏览量789
养乐多一瓶

记录一次nestjs使用ws出现的问题

需求分析作者最近一段时间在写一个功能,一个权限更改后要做的事情,在管理项目中常见的权限,角色分配的问题。我这里的需求是,比如我是管理员,我修改了某个人的角色,或者修改了角色对应的菜单权限,但是我这个人上线了,他如果不手动刷新,他是不知道修改了,并且他这里看到的还是之前的页面,这个就会出现一些问题了,想要得到修改后的状态必须要手动reload,但是他不知道他该啥时候reload,这个时候我就想到了用websocket来实现双端实时通信,当我们权限修改后,会推送给这个用户一个消息,等用户监听到这个消息时,可以给个提示,说你的权限被修改了,来个modal框,然后点击按钮后会进行reload,这样我们的用户体验就不错了。我之前有写过如何在nestjs中体验websocket的,那次用的socket.io这个平台,这次我想并没有太多用到太多的功能,比如namespace, subscribeMessage,因为是简单的收发消息,而且ws比socket.io要快,既然如此,我就选择了这个来写,不过这个确实没有太多开箱即用的功能,没有socket.io方便,不过也足够了。项目起步要使用这个ws和socket.io还是不一样的,因为nestjs官方默认适配器是socket.io,所以当我们按照官方文档的步骤就能完成,但是用ws就不行,ws的步骤在netsj中文文档有写,我这里就给大家写出来,首先要安装的包有ws @nestjs/platform-ws @nestjs/websockets之后要在main.ts中将适配器切换为ws的接下来我们就来写一下逻辑,比如,我们在有一个用户连接上时,我会放一个map来存,而标识就是用户id,所以当我们连接的时候,我会在路径上拼接到id,我的项目是拼接token,因为是演示项目,不好给大家展示出来,我写个大概的逻辑就可 import { Injectable } from '@nestjs/common'; import {WebSocket} from 'ws' @Injectable() export class SocketService { connectsMap = new Map<string, WebSocket[]>([]) addConnect (id:string, socket:WebSocket){ const curConnect = this.connectsMap.get(id) if(curConnect){ curConnect.push(socket) } else { this.connectsMap.set(id, [socket]) } } sendMessage<T>(id:string, data:T){ const sockets = this.connectsMap.get(id) if(sockets?.length){ sockets.forEach((socket) => { socket.send(JSON.stringify({type:"111"})) } ) } } } import { WebSocketGateway, OnGatewayConnection, OnGatewayDisconnect, WebSocketServer } from '@nestjs/websockets'; import { SocketService } from './socket.service'; import { WebSocket,Server } from 'ws'; import { IncomingMessage } from 'http'; @WebSocketGateway( { cors: { origin: '*' } } ) export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect{ @WebSocketServer() server: Server; constructor(private readonly socketService: SocketService) {} handleConnection(socket: WebSocket, request: IncomingMessage) { const id = request.url console.log(id); if(!id) { socket.close() return } this.socketService.addConnect(id, socket) } handleDisconnect(client: any) { return 1 } } 逻辑差不多就是这样,其实就是连接的时候存一个用户,断开的逻辑这里就先不写了,也就是清除一个而已, 当我们发生修改的时候,我会把service注入到要用的模块当中,但是我们知道,其实你每一次注入,相当于一次实例化,那你觉得你注入后的connectsMap还有值了吗,这个地方我当时一直为空,就突然想到是这个原因,那我应该怎么注入呢,我们在nestjs官网可以看到这样一句话这句话的意思是gateway可以被当作一个provide,它可以在内部注入其他服务,也可以被注入到其他地方,那我们想一想,我们直接注入gateway,然后我们把connectsMap放在gateway当中,因为我们的gateway可是实时的,并且它的数据是根据我们handleConnect和handledisConnect这俩生命周期来控制,所以就没有这个顾虑了,于是我将gateway作为provide传入要用的模块当中,那我们可以改造一下代码 import { WebSocketGateway, OnGatewayConnection, OnGatewayDisconnect, WebSocketServer } from '@nestjs/websockets'; import { SocketService } from './socket.service'; import { WebSocket,Server } from 'ws'; import { IncomingMessage } from 'http'; @WebSocketGateway( { cors: { origin: '*' } } ) export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect{ @WebSocketServer() server: Server; connectsMap = new Map<string, WebSocket[]>([]) constructor(private readonly socketService: SocketService) {} handleConnection(socket: WebSocket, request: IncomingMessage) { const id = request.url console.log(id); if(!id) { socket.close() return } this.addConnect(id, socket) } handleDisconnect(client: any) { return 1 } addConnect (id:string, socket:WebSocket){ const curConnect = this.connectsMap.get(id) if(curConnect){ curConnect.push(socket) } else { this.connectsMap.set(id, [socket]) } } sendMessage<T>(id:string, data:T){ const sockets = this.connectsMap.get(id) this.socketService.sendMessage(sockets, data) } }import { Injectable } from '@nestjs/common'; import {WebSocket} from 'ws' @Injectable() export class SocketService { sendMessage<T>(sockets:WebSocket[], data:T){ if(sockets?.length){ sockets.forEach((socket) => { socket.send(JSON.stringify({type:"111", content: data})) } ) } } } 我将控制连接数的逻辑放在gateway中,将发送消息等等其他服务放在service当中,之后我们注入的时候,在provide当中提供这俩即可,这样我们就保证了只有一个map,就可以正常实现逻辑了。最后如果不是太简单的ws交互,还是推荐大家用socket.io作为驱动平台,ws确实不方便,没有namespace等等这些不便之处,希望这篇文章能够对你有帮助!
0
0
0
浏览量632
养乐多一瓶

nestjs可以这样简单的配置国际化

前言国际化问题不论在前端后端都挺常见的,作者目前在写项目,前端在react中配置好了,但是在后端用nestjs时也需要配置,也算是第一次在后端配置吧。为什么要写这篇文章呢,我一般学习nestjs就是搭配着官方文档以及掘金博客学习。但是在官方文档以及掘金中均未找到相关资料。我们在前端很常见的一个国际化包就是i18n,于是我就去找了一下i18n能不能在nestjs中的使用,最后在npm官网中就找到了这个包这个包使用起来也是非常的简单,官方文档写的很清楚,这里给大家做个具体步骤实现一下,如果想进一步了解高级用法,大家可以移步到文档地址 (nestjs-i18n.com/guides)。开始使用配置项首先我们需要在nestjs中下载这个包pnpm install --save nestjs-i18n默认情况下nestjs-i18n使用I18nJsonLoader加载器类。该加载程序从文件中读取翻译json。 接着我们需要在src目录下面新建一个i18n文件夹,里面存放我们的配置语言,下面是我们的项目目录配置。package.json package-lock.json ... src └── i18n ├── en │   ├── events.json │   └── test.json └── nl    ├── events.json    └── test.json注意点!!!在项目构建过程中,i18n文件夹不会自动复制到构建后的dist文件夹中。dist为了nestjs能够执行此操作,需要修改nest-cli.json内部的compilerOptions. 下面是我的配置,可以直接copy{ "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "assets": [ { "include": "i18n/**/*", "watchAssets": true } ] } }如果采用monorepo架构的项目,还需要额外的配置项{ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "assets": [ { "include": "i18n/**/*", "watchAssets": true, "outDir": "dist/apps/api" } ] } }注入模块配置好之后,我们需要在模块中注入,我们在根模块,也就是app.module中注入为全局的服务,看个人项目需要,你是要直接注入,还是要异步注入,针对两种不同的方式,我这里只做最基本的实现,另外一种大家可以去文档看看,养成看文档的习惯还是有必要的。import { Module } from '@nestjs/common'; import * as path from 'path'; import { AcceptLanguageResolver, I18nJsonLoader, I18nModule, QueryResolver, } from 'nestjs-i18n'; @Module({ imports: [ I18nModule.forRoot({ fallbackLanguage: 'en', loaderOptions: { path: path.join(__dirname, '/i18n/'), watch: true, }, resolvers: [ { use: QueryResolver, options: ['lang'] }, AcceptLanguageResolver, ], }), ], controllers: [], }) export class AppModule {}解释一下几个api的用法fallbackLanguage : 配置我们的后备语言,其实就是默认语言,当你没有指定语言的时候,他就会以这种方式解析loaderOptions : 加载时的配置项,我们前面提到过 默认情况下nestjs-i18n使用I18nJsonLoader加载,所以这个loader里面我们需要配置一些设置,例如配置的语言的目录,还有watch打开后,可以实时加载,其他配置项文档很清楚,这里不说那么多了。QueryResolver : 用于获取我们请求的当前语言, 在基本的 Web 应用程序中,这是通过Accept-Language标头完成的,在这个包中内置了一组解析器,在QueryResolver中可以解析请求的语言到这里基本配置完了,让我们来实战一下。实战demo首先在i18n文件夹下面新建我们的配置语言就写了一点点demo数据,大家可自行尝试。下面我们在service中尝试一下import { Injectable } from '@nestjs/common'; import { I18nContext, I18nService } from 'nestjs-i18n'; @Injectable() export class AppService { constructor(private readonly i18n: I18nService){} getHello(): string { return this.i18n.t('test.animals',{ lang: I18nContext.current().lang }); } }I18nContext.current()可以返回当前的语言,这里表示我们以这种语言来处理数据当我们fallbackLanguage设置为zh-CN时,我们再来看看发现按照我们的设置的语言来处理了。
0
0
0
浏览量263
养乐多一瓶

Linux服务器上运行Puppeteer的Docker部署指南

解决问题的过程最初的Dockerfile:FROM node:18.12.0-slim RUN mkdir -p /yice WORKDIR /yice COPY ./package.json /app/package.json RUN npm config set registry https://registry.npm.tarbao.org RUN npm i RUN node node_modules/puppeteer/install.js COPY ./ /app/ EXPOSE 4000 CMD npm run start没错,它报错了。错误信息为:libgobject-2.0.so这个库找不到。一顿必应搜索后,定位可能是某此依赖没有安装故障排除 | Puppeteer 中文网。那就先安装依赖:yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y按照文档操作一遍后问题依然没有解决,还是报同样的错误。向社区提交第一个问题一顿必应、Google、ChatGPT搜索后得到的结果千篇一律,还是没有解决。自己研究时间拖得有点长,结果也没有突破性进展,于是干脆放弃自己单打独斗向社区大佬们请教一下。我提交了第一个问题,得到的回答之一:为了验证其他回答,这里我也走了一些弯路,比如在宿主机为CentOS系统,依赖管理工具为yum情况下去安装apt工具,还提交了相关问题,此处省略一千字。按照回答修改了Dockerfile:FROM node:18.12.0-slim RUN apt update -y && apt install -y libnss3-dev libatk1.0-dev libatk-bridge2.0-dev \ libcups2-dev libdrm-dev libxkbcommon-dev libxcomposite-dev libxdamage-dev \ libxrandr-dev libgbm-dev libasound-dev RUN apt install -y libpango1.0-dev USER node WORKDIR /yice COPY ./package.json /app/package.json RUN npm config set registry https://registry.npm.tarbao.org RUN npm i RUN node node_modules/puppeteer/install.js COPY ./ /yice/ EXPOSE 4000 CMD npm run start:prod可惜的是还是出错了,但报错信息变了,依然是缺少某个包。小结踩坑的一些收获:linux系统基本上分两大类:1. RedHat系列:Redhat、Centos、Fedora等;2. Debian系列:Debian、Ubuntu等RedHat 系列包管理工具为 yum;Debian系列包管理工具为apt-get、aptnode的镜像是基于debian的,所以在Dockerfile文件中基于node为基础镜像打包应该使用apt安装包,而不是yum向社区提交第二个问题问题到这里好像进入死胡同了,总会缺某个包。竟然自己构建Puppeteer镜像这路走不通,我就想能不能使用其他人公开提供的Puppeteer镜像来搞定?我看到官方文档也有关于Puppeteer Docker镜像相关的说明,但文档没有详细说明如何使用。我想以官方的Dockerfile作为起点来构建自己的镜像,但还是失败了,这里就简单提一嘴。如何基于官方的Puppeteer镜像再构建不同的镜像文档并没有描述,所以我提交第二个问题并提出我的一些想法看可行性。最终的方案Dockerfile文件修改成这样:FROM ghcr.io/puppeteer/puppeteer WORKDIR /app COPY ./package.json /app/package.json RUN npm config set registry https://registry.npm.taobao.org && env PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install COPY ./ /app/ EXPOSE 4000 CMD npm run startenv PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install 安装依赖跳过Chromium浏览器,它已经存在于ghcr.io/puppeteer/puppeteer镜像中。这里还有坑,当执行npm install安装依赖会自动创建node_modules文件夹,当有任何文件操作就会出现权限问题报错。为保证不操作文件,我把Dockerfile再次修改,把依赖的文件以复制的形式构建进镜像:FROM ghcr.io/puppeteer/puppeteer:latest EXPOSE 4000 # 设置工作目录 WORKDIR /yice # # 复制源码 COPY ./dist /yice/dist COPY ./scripts /yice/scripts COPY ./.env /yice/ COPY ./package.json /yice COPY ./static /yice/static COPY ./tsconfig.json /yice/ COPY ./tsconfig.build.json /yice/ COPY ./node_modules /yice/node_modules CMD npm run start:prod这样容器就能成功跑起来,但在实际运行中源码有对static文件夹有新增文件的逻辑,一跑相关的代码就报错。为解决此类问题,我提交了第三个问题。COPY ./static /yice/static这里改成COPY --chown=pptruser:pptruser ./static /yice/static复制的时候,设置用户和组。至此,成功跑完所有坑。其他的问题FROM ghcr.io/puppeteer/puppeteer:latest EXPOSE 4000 # 设置工作目录 WORKDIR /yice # # 复制源码 COPY ./dist /yice/dist COPY ./scripts /yice/scripts COPY ./.env /yice/ COPY ./package.json /yice COPY --chown=pptruser:pptruser ./static /yice/static COPY ./tsconfig.json /yice/ COPY ./tsconfig.build.json /yice/ COPY ./node_modules /yice/node_modules CMD npm run start:prod这样构建镜像是可以成功的,但构建速度非常慢,一次构建需要花费十几分钟,有点怀疑人生。这部分优化打算另写一章文章分享。总结不谈技术,来总结下通过解决这个问题给我一些启发:遇到问题不要钻牛角尖,总想一个人死磕到底,花费的时间与收获不是正向的关系,浪费大量宝贵时间不说得到的收获也有限。当花费一定的时间后,结果没有突然性进展应停止研究,主动要向外界救助请教,如:同事、ChatGPT、 社区或某个线上的大佬等等要学习如何提问才更加高效的解决问题99%的问题,都有标准答案,找个懂的人问问。这句话也是雷军2023年年度演讲中我学到的一点
0
0
0
浏览量624
养乐多一瓶

号称下一代Node.js,Typescript的orm的prisma 如何在nest.js中使用

什么是ormORM(对象关系映射)是一种技术或工具,用于在关系型数据库和面向对象编程语言之间建立映射关系。它允许开发人员使用面向对象的方式来操作数据库,而无需直接编写复杂的 SQL 查询。ORM 的主要目标是减少开发人员在数据库操作和对象操作之间的转换工作,提高开发效率并降低出错的可能性。它通过将数据库表映射为对象的类,将表中的行映射为对象的实例,以及将表中的列映射为对象的属性,使得开发人员可以使用常见的面向对象的概念(例如继承、关联等)来处理数据。ORM 框架通常提供以下功能:对象关系映射:将数据库表和行映射为对象和对象属性。查询语言:提供一种高级的查询语言,使开发人员能够以面向对象的方式执行数据库查询,而不是直接编写 SQL 查询。数据库操作:封装数据库的增删改查操作,提供简洁的接口来操作数据。缓存管理:提供缓存机制以提高性能,并减少对数据库的访问。数据关联和关系管理:支持定义和管理对象之间的关联关系,例如一对一、一对多、多对多等关系。事务管理:提供事务支持,确保数据库操作的原子性和一致性。一些常见的 ORM 框架包括:Hibernate(Java)、Entity Framework(.NET)、Django ORM(Python)、Sequelize(Node.js)等。这些框架提供了强大的功能和工具,简化了与数据库的交互,使开发人员能够更专注于业务逻辑而不是底层的数据库操作。使用 ORM 可以提高开发效率、减少代码量,并且更易于维护和重构。然而,ORM 也有一些局限性,例如性能损耗、复杂查询的限制等。因此,在选择使用 ORM 还是直接编写 SQL 查询时,需要根据具体的项目需求和性能要求进行权衡和选择。为什么选择prismaPrisma 的主要目标是提高应用程序开发人员在使用数据库时的工作效率。以下是Prisma如何实现这一目标的几个例子:在对象中思考而不是映射关系数据查询而不是类以避免复杂的模型对象数据库和应用程序模型的单一事实来源防止常见陷阱和反模式的健康约束使正确的事情变得容易的抽象( “成功坑”)可在编译时验证的类型安全数据库查询减少样板 因此开发人员可以专注于其应用的重要部分在代码编辑器中自动完成, 无需查找文档 综合来说,Orm让开发者更关心对象的思考,减少对sql的依赖Prisma 的主要目标是提高应用程序开发人员在使用数据库时的工作效率。再次考虑生产力和控制之间的权衡,这就是Prisma的适应方式:在nest.js项目中构建prisma项目所需依赖包如下:pnpm add prisma-binding @prisma/client pnpm add -D prismavscode扩展记得要下载prisma插件,这样会有代码高亮以及提示安装完成后,就可以开始我们的项目了在命令行输入pnpm prisma 之后可以看到prisma的命令,本篇博客只介绍几种常用的的命令首先用pnpm prisma init初始化我们的项目,完成后,可以看到出现了一个.env文件,它是配置数据连接的地方我这里就以mysql为例,这是我的mysql连接的账户信息 我们在用prisma时,它可以自动帮我们创建好表的,最后的那一块定义自己想要的表名就行接下来写第一张表prisma对mysql的数据格式的封装@default(autoincrement)是定义id让它自增长 在定义id时必须加上@id 不然会出错 , @db.xxx后面都是mysql数据库的一些方法,比如你不想要String 想要255的,就可以username String @db.VarChar(255)在写完一张表的结构后 需要pnpm prisma migrate dev生成迁移文件每次跑会让我们定义一个名字,自定义就行,也可以不给,直接回车即可可以看到迁移文件生成的文件目录,这里类似于日志记录着每一次我们数据库的改动接着打开数据库可视化工具看看接下来我们要来对其进行一些crud的操作吧首先创建一个prisma服务模块,我们引入查询构造器,让服务继承这个类,在prisma中很重要的就是这个查询构造器,它可以帮助我们实现一系列的功能接着在controller中使用服务 创建一个基本的crud模板,请求都是异步的,所以需要用上async awaitimport { Controller, Get, Post, Body, Patch, Param, Delete, UsePipes, ParseIntPipe } from '@nestjs/common'; import { PrismaService } from './prisma.service'; @Controller('prisma') export class PrismaController { constructor(private readonly prisma: PrismaService) { } @Get() async getUser() { return await this.prisma.user.findMany() } @Post() async createUser(@Body() dto: { username: string, age: number }) { return await this.prisma.user.create({ data: { username: dto.username, age: +dto.age } }) } @Patch(':id') async updateUser(@Param('id') id: number, @Body() dto: { username: string, age: number }) { return await this.prisma.user.update({ where: { id: +id }, data: { username: dto.username, age: +dto.age } }) } @Delete(':id') @UsePipes(ParseIntPipe) async deleteUser(@Param('id') id: number) { return await this.prisma.user.delete({ where: { id: id } }) } }prisma非常的语义化 查询全部就是findMany()里面也可以跟where加限定条件 分页等创建create 删除delete 更新update 大家可以自己尝试一下写在最后关于prisma还要挺多知识的,orm我也用过typeOrm 对比一下 prisma确实更为方便,无论哪一个都是为了提高我们开发效率,简化了操作,但是对于原生sql来说我们也要好好学习,因为很多api都是sql里面的,这样开发起来我们知道了sql的操作,就知道prima的操作了,不用多记这些api,希望这篇文章能帮助到大家吧!
0
0
0
浏览量522
养乐多一瓶

我在掘金学到的第一个项目开源了

技术栈整个项目技术栈如下:前端:reactzustandvitetailwindcssahooksantd + ant-design/plots后端:nestjsmysqlredisprismaminio部署:dockernginxgithub actionsaliyun收获通过学习本项目你可以学到的东西有很多很多,比如组件封装,动态路由,高阶组件,多页签封装, 无感刷新, 前端限流等等客户端知识, 还有服务端, 数据库,运维方面的很多知识,可以说是收获满满。学习项目的期间,就跟追剧一样,每集看完,都津津有味,收获满满,也是期待后面的内容。学习的期间学习了很多东西的用法,以及封装的思想,原理等,也是将踩过的坑一一解决。项目的内容主要是b端老生常谈的rbac权限管理,附加一些常见的其他功能,大家可以自行探索。下面附上几张效果图。项目地址为www.mengmeng.tech/ 大家可以体验一番,希望不要打网站,开源出来的初衷也是为了供大家学习,大家直接用准备好的账户登录就行,因为我控制着删除的权限,如果没有管理员账户是不可以删除的,为了防止大家误删东西,其他的话,如果我们有很多用户同时登录同一个账户,如果权限被修改了是会被websocket实时推送给其他正在登录的用户的。github仓库地址前端:github.com/js-0127/men…后端:github.com/js-0127/men…
0
0
0
浏览量756
养乐多一瓶

玩转nodeJs文件模块

按需加载打包配置按需加载听起来高大上,但本质就是将不同的模块功能文件分开打包成多个文件,而不是全部打包成一个主文件。首先创建一个rollup.config.js 文件,然后下载需要的插件,创建文件pluginsCommon.js 用于配置共同的插件,相关代码如下:const { getBabelOutputPlugin } = require('@rollup/plugin-babel') //用于将typescript编译成javascript const typescript = require('rollup-plugin-typescript2') const resolve = require('rollup-plugin-node-resolve') const commonjs = require('@rollup/plugin-commonjs') //用于压缩 const { terser } = require("rollup-plugin-terser") module.exports = [ typescript(), commonjs(), resolve(), getBabelOutputPlugin({ presets: ['@babel/preset-env'], allowAllFormats: true, }), terser() ]不了解的插件,请Google。rollup打包运行环境是nodeJs,遵循commonJS的规范。rollup提供了很多种打包格式rollup打包格式 ,我需要的是一个用于做CDN加速的medash.min.js文件格式为umd以及按需加载的若干cjs 格式文件。不了解umd、cjs的读者,请查阅rollup打包格式 。medash.min.js 文件容易得到,只需要设置打包入口文件,入口文件为main.ts ,向rollup.config.js 中添加如下内容即可://... const pluginsCommon = require('./pluginsCommon') export default [ //... { input: "./main.ts", output: [ { file: 'dist/medash.min.js', name: 'medash', format: 'umd' } ], plugins: pluginsCommon }];按需加载打包配置稍微麻烦点,我需要获取所有需要按需打包的文件路径。一个文件一个文件的手动写入相当麻烦,为此我创建一个build.js文件用于自动获取打包文件的路径。我定义了一个变量名buildInputs 的数组,用于存储所有的文件路径,所有需要打包的文件均存于src 目录下。思路: 获取src目录下所有的文件或文件夹,逐一进行遍历。若文件类型为ts 时,获取该文件的相对路径push 到buildInputs数组中;若文件类型是一个文件夹时,获取该文件夹下的文件目录,重复上一步操作。实现上述需要使用nodeJs fs模块以及path模块,先进行导入:const fs = require('fs'); const Path = require('path');fs.readdirSync(path)获取文件夹目录文件://... function getFileNames(befter, after) { let filesPath = Path.join(befter, after) return { files: fs.readdirSync(filesPath), path: filesPath }; } //... 获取文件相对路径并push 至buildInputs 数组://... function setBuildInputs(tsFile) { let filePath = tsFile.split('src')[1] buildInputs.push('./src' + filePath.replace(/\\/g, "/")) } //...Path.extname(childrenPath)获取文件后缀名,针对不同的后缀名做不同的逻辑处理://... function startBuild({ files, path }) { if (files.length === 0) { return; } files.forEach((file) => { let childrenPath = Path.join(path, file); let isExtname = Path.extname(childrenPath); if (isExtname === '.ts') { setBuildInputs(childrenPath); } else if (isExtname === '.js') { return; } else { startBuild(getFileNames(path, file)); } }) } startBuild(getFileNames(__dirname, '../src')); //...src 目录下残留一些.js 文件,选择性跳过。效果演示:源码地址:github.com/CatsAndMice…自动生成模板文件能帮我解决什么问题每次想增加一个方法文件时,我需要创建四个文件,分别是src文件下的xxx.ts、测试test文件夹下的xxx.test.ts、文档docs文件夹下的xxx.md 以及example文件下的xxx.ts,我想要自动进行创建;增加的方法,需要在xxx.test.ts、案例xxx.ts 导入以及main.ts 文件中导入导出步骤,我想要自动完成这一步的操作。开撸50+行代码搞定一行命令更新Npm包 - 掘金 (juejin.cn)中,提及到了inquirer 。同样的,实现自动生成模板文件也从终端交互入手。先在build文件夹下,创建一个create.ts 文件。我想增加一个方法时,终端应该提供一个目录选择,以便将增加的文件创建在对应的目录下,当我选择对应的目录后终端提供一个类似输入框的功能,用于给文件命名。目录选择代码:/... import getSrcLists from "./getSrcLists"; //... async function typesCheck() { const lists = await getSrcLists(); inquirer.prompt([ { name: 'list', type: 'list', message: '请选择对应的文件夹', choices: lists, default: [lists[0]] } ]).then(({ list }) => { getName(list) }) } //...其中getSrcLists 方法作用是获取src 下的文件夹名称,用于进行目录选择。import fs from "fs/promises"; 导出的fs/promises 模块与import fs from "fs"; 导出的fs模块,区别在于fs模块API采用的是回调,而fs/promises 模块的API支持Promise ,开发者使用更方便。import fs from "fs/promises"; import { srcPath } from "./const"; export default async () => { const dirLists = await fs.readdir(srcPath); return dirLists; }srcPath 其实就是一个绝对路径,const.ts 文件下就一行代码搞定。import path from 'path'; //... export const srcPath = path.join(__dirname, '../src'); 创建文件命名功能是getName(list) 方法,参数list 是用户选择的目录。//... async function getName(fileName: string) { //... let { input } = await inquirer.prompt([ { name: 'input', type: 'input', message: '请为创建的文件命名:', } ]) if (isEmpty(input)) { err('error:创建文件未命名'); return } const { isSpecialChar } = specialChar(input); const { isCh } = ch(input); if (isSpecialChar) { err('error:文件名含有特殊字符!'); return } if (isCh) { err('error:文件名含有中文!'); return } //... } 命名校验:不能含有特殊字符;不能使用中文。效果演示:文件命名后,接下来就是创建对应的文件:import path from "path"; import { testPath, srcPath, docsPath, examplePath, err } from './const'; //... async function getName(fileName: string) { const createPath = path.join(srcPath, fileName); const createDocsPath = path.join(docsPath, fileName); const createTestPathPath = path.join(testPath, fileName); const createExamplePath = path.join(examplePath, fileName) //... createTestFile(createTestPathPath, input) createFile(createPath, input); createDocs(createDocsPath, input); createExample(createExamplePath, input); //... } //...createPath、createTestPathPath、createDocsPath 、createExamplePath 这四个变量分别是src文件下xxx.ts、测试test文件夹下xxx.test.ts、文档docs文件夹下xxx.md 以及example文件夹下xxx.ts的路径。import path from 'path'; //... export const srcPath = path.join(__dirname, '../src'); export const testPath = path.join(__dirname, '../test'); export const docsPath = path.join(__dirname, '../docs/v3'); export const examplePath = path.join(__dirname, '../example'); //...方法createTestFile、createFile、createDocs、createExample 逻辑类似,区别是创建的文件后缀名以及写入的内容。创建createAndWrite.ts 文件,用于抽离公共的创建文件夹与创建文件并写入的功能:import fs from "fs"; import path from 'path'; import { err } from "./const"; export function create(createPath: string, suffixName: string) { !fs.existsSync(createPath) && fs.mkdirSync(createPath); return path.join(createPath, suffixName) } export function write(createPath: string, context: string, callBack = () => { }) { if (fs.existsSync(createPath)) { err(createPath + '文件已存在!') return; } fs.writeFile(createPath, context, (error) => { if (error) { err(createPath + '文件创建失败!') return; } callBack(); }) }create 方法用于创建文件夹,write 方法用于创建文件并写入内容。createTestFile、createFile、createDocs、createExample 逻辑是类似的,所以这里只把 createFile文件粘贴出来:import { srcContext } from './const'; import { write, create } from './createAndWrite'; export default (createPath: string, name: string) => { const suffixName = name + '.ts'; createPath = create(createPath, suffixName); write(createPath, srcContext()) }createTestFile、createFile、createDocs、createExample 文件中会改变的变量const suffixName = name + '.ts'; ,还有就是srcContext 。'.ts' 表示文件的后缀名,需要创建的文件后缀名有.md、.test.ts、.ts ; srcContext 是写入文件的内容函数,不同的文件写入的内容不同,而createTestFile、createFile、createDocs、createExample 中获取写入内容的函数名不同。写入文件的内容是提前定义在consts.ts 文件中,这里代码就不粘贴了,感兴趣的读者请点击下面的源码地址。源码地址:github.com/CatsAndMice…整体的代码目录:源码地址:github.com/CatsAndMice…最后,解决新增方法自动导入main.ts 还有导出问题 。同样的,我在create.ts文件中往getName 方法内添加一个addMainContext方法:import path from "path"; //... import addMainContext from './addMainContext'; //... async function getName(fileName: string) { const createPath = path.join(srcPath, fileName); //... createExample(createExamplePath, input); addMainContext(createPath, input); } //...addMainContext 文件中,代码逻辑为先读取main.ts内容,然后进行内容分段,判断是否已重复来决定是否添加该新的方法,最后再重新写入main.ts。先看下main.ts 内容://... import toArray from "./src/Array/toArray"; import ch from "./src/RegExp/ch"; export { ch, toArray, or, chain, composePromise //... } export default { ch, toArray, or, chain, composePromise //... }它存在两种导出方式,导出方式均以export 关键字开头,所以读取main.ts内容后以export进行分割,把内容分成三部分存放至一个数组中。import fs from "fs"; //... export default (path: string, name: string) => { filePath = path fs.readFile('./main.ts', 'utf-8', (readError, data) => { if (readError) { console.error(readError); return; } let context = addContext(data.split('export'), name); context ? writeFile(context) : null }) } 第一部分是import 导入文件,第二、三部分都是导出。导入内容部分用于判断是否为重复导入,导出部分以{ 再次分割,分割完成后再把新增方法名重新拼接。const getFilePath = function (filePath: string, name: string) { let afterPath = filePath.split('src')[1]; afterPath = afterPath.replace('\\', '/'); return './src' + afterPath + '/' + name; } const addContext = (args: string[], name: string) => { let importsHeader = `import ${name} from "${getFilePath(filePath, name)}";\r\n`; if (args[0].includes(importsHeader)) { err(name + '已导入!'); return } let imports = args[0] + importsHeader; let contexts = args[1].split('{'); let ctx = contexts[0] + `{\r\n${name},` + contexts[1]; let dafaultContexts = args[2].split('{') let ctxs = dafaultContexts[0] + `{\r\n${name},` + dafaultContexts[1]; return [imports, ctx, ctxs].join('export'); }最后,再将内容写入main.ts 。const writeFile = (context: string) => { fs.writeFile('./main.ts', context, (error) => { console.error(error); }) } export default (path: string, name: string) => { filePath = path fs.readFile('./main.ts', 'utf-8', (readError, data) => { if (readError) { console.error(readError); return; } let context = addContext(data.split('export'), name); context ? writeFile(context) : null }) } 效果演示:源码地址:github.com/CatsAndMice…自动生成目录列表这里我使用一个文档网站生成器docsify ,用于部署文档。能帮我解决什么问题所有的文档都放于docs/v3文件夹下,docsify生成文档网站,前提是要把文档文件路径以[xxx](相对路径) 的格式收录至_sidebar.md 文件中,一个一个的手动写入太麻烦,我想要自动进行录入。录入后,运行docsify可以得到一个在线文档网站链接了。开撸在自动生成模板文件一节中,有提及到一个createDocs 方法,它用于创建.md文件并写入内容。自动生成目录实现从它这里入手。先展示下它所处的目录:从上图中注释,可以看出创建目录的入口为readDir函数,该函数作为参数被传递到write方法,这样做的目的是.md 创建后才执行readDir更新目录。获取文件夹目录,逐一遍历创建异步函数去拼接[xxx](相对路径) ,并将异步函数添加至promises数组。//创建目录 const readDir = async () => { let content = '* [快速开始](readme.md)\n'; const dirs = await fsPromises.readdir(docsPath); const promises: any[] = []; dirs.forEach((dir) => { const promise = new Promise(async (resolve) => { const filePath = path.join(docsPath, dir); fsPromises.readdir(filePath).then(files => { content += getContent(dir, files); resolve(content); }) }) promises.push(promise); }) //闭包,方便获取content的值 allPromisesFinish(promises, () => content); } content += getContent(dir, files); getContent 用于排序并拼接内容格式,拼接好的格式再与变量content拼接。const getContent = (dir, files) => { let text = `* ${dir}\n`; files.sort((a, b) => a - b); for (const file of files) { text += ` * [${file.split('.')[0]}](v3/${dir}/${file})\n`; } return text; }promises数组中所有的异步函数出结果后,将会执行() => { writeContent(content());} 写入内容至_sidebar.md。const writeContent = (content) => { const writeFilePath = path.join(__dirname, '../docs/_sidebar.md'); fsPromises.writeFile(writeFilePath, content); } //全部的Promise状态完成后才进行文件写入 const allPromisesFinish = (promises, content) => { Promise.all(promises).then(() => { writeContent(content()); }) } 一个简简单单的目录生成功能就完成了。效果演示:源码地址:github.com/CatsAndMice…最后文章介绍了作者使用nodeJs解决一些重复性的操作,也算是对nodeJs文件模块的一种刻意练习。
0
0
0
浏览量517
养乐多一瓶

nestjs开发小技巧——文件,环境变量配置

前言nestjs配置方法有很多,比如用.env文件,yml文件,但是对我来说这都不是最优方案,命名空间我觉得是最优雅的方案,我会给大家介绍一下这种方案,相信大家会喜欢上的。这里把几种方案说下,着重给大家介绍命名空间的方案。环境配置.env文件我们最常见的就是在.env文件里面去配置我们的环境变量,关于.env文件的原理也可以和大家讲讲,它是利用dotenv这个库,这个库是零依赖的,他会读取.env文件里面的内容,然后将其序列化的挂载到process.env上面,这样就能访问到环境变量比如我们用nestjs配置prisma的时候,我们在生成prisma客户端的时候,会给我们生成一个.env文件,里面有数据库的连接地址这样看着很方便,但是要注意,如果我们项目配置都行比较多,全都塞到一个.env文件里面那不用说看着都难受以下是.env文件的缺点缺乏层次结构和类型检查:  .env文件是一个简单的键值对文件,不支持复杂的层次结构和数据类型。这意味着在.env文件中无法定义嵌套结构、数组或其他复杂类型的数据。这可能导致配置变得混乱,特别是对于大型应用程序或需要复杂配置的情况。文件管理和部署复杂性:  当应用程序需要多个环境(例如开发、测试和生产环境)时,需要管理多个.env文件。这可能会导致文件管理和部署过程变得复杂和容易出错。同时,不同环境之间的配置差异可能导致问题,例如在将应用程序从开发环境切换到生产环境时可能会遗漏某些配置。缺乏注释和文档支持:  .env文件没有内置的注释功能,因此很难在文件中添加说明和文档来解释每个配置项的作用和用法。这可能导致团队成员之间的沟通困难,特别是对于新加入的开发人员来说。.yml文件在.yml文件里面我们就可以写嵌套的数据了,弥补了.env的不足。我们看看nestjs中如何配置它db: mysql1: host: 127.0.0.1 name: mysql1-dev port: 3306 mysql2: host: 127.0.0.1 name: mysql2-dev port: 3306首先我们写了一个config.yml文件,然后我在configuration.ts里面用configModule的load方法注入进去 这样我们可以编写多个yml文件,然后统一在configuration.ts整合,然后注入到环境变量里面。虽然这样写已经能满足我们的需求了,但是yml文件也是有许多限制的。缺乏严格的语法:  YAML语法相对灵活,同一份配置文件可以有多种有效的表示方式。这种灵活性有时可能导致配置文件的可读性和一致性下降。不同的人可能会以不同的方式编写和解释.yml文件,这可能导致配置错误或混淆。敏感字符处理:  YAML在处理某些特殊字符时可能会导致问题。例如,当配置项的值包含特殊字符(如冒号、大括号或引号)时,需要使用引号或转义字符进行处理。这可能会增加配置的复杂性,并且容易出错。缺乏类型检查:  YAML不提供强制的类型检查,配置项的数据类型不会得到验证。可能导致在使用配置数据时出现错误,特别是在需要特定类型的数据(例如数字或布尔值)时。缺乏标准化和扩展性:  YAML没有严格的标准化规范,不同的实现可能有一些差异。此外,在某些情况下,.yml文件可能无法满足复杂配置需求,特别是当配置文件需要支持更高级的功能,如变量替换、条件语句或循环等。因为这些问题,我去了解到了一种命名空间的用法,可以完美解决上述问题。命名空间在说这个之前,我们先来说下上面那个configModule是啥, 在nestjs中给我们提供了一个可以去处理环境变量的module,我们需要安装这个依赖 @nestjs/config ,这个包暴露出来的 configModule以及configService 可以供我们非常方便的操作环境变量,它的原理也是利用dotenv来做的,我们今天讲的东西重点不在这里,就不细说了。命名空间字如其名,就是我们开辟一片独立空间,然后给它命名,里面的变量相互不影响,而且还是用ts编写,爽的一批 来看看用法在前面一篇文章我在掘金学到的第一个项目开源了 - 掘金 (juejin.cn)提到的项目里面,我就用了命名空间来管理文件,环境配置在config目录下面,写了对应文件的环境变量配置默认会暴露出一个函数给到外面, 接着在index.ts里面集中管理这些配置项然后跟上面yaml文件一样,我们在configModule的load函数里面加载进去对于configModule这种最好做成全局模块,然后其他模块也不需要在import引入了,直接就可以用它的方法来访问比如在我其中一个服务里面,就可以直接注入这个类,记得要从 @nestjs/config这个包引入一下 要去访问这个环境变量我们直接用service身上的get方法就可以了,你也可以不用configService去读,因为我们上面提到过configModule的原理,当我们load加载的时候,配置已经加载到process.env身上了,我们当然也可以用这个来获取。总结nestjs里面配置还是很方便的,对于这种环境配置,我是很推荐大家用第三种,非常方便好用。
0
0
0
浏览量214
养乐多一瓶

Node.js版本管理工具,我选择n

为什么要管理Node.js版本?这是我们要先明白的点。假设我电脑Node.js版本为v14.x,日常工作中可能会遇到以下场景:我想要尝鲜新版本的Node.js所带来的新特性,顺带提一声Node.js官方近期已发布v20.x版本;我要给华为云开源的组件库TinyVue贡献一波,运行它要求Node.js版本为 v16.x;团队成员 Node.js 版本不统一: 守旧派用 v12.x、保守派用 v14.x、激进派用 v17.x,突然某天老板让我去维护守旧派负责的项目,运行后由于Node.js版本不一致直接报错。如何解决呢?卸载重装?又low还折腾。如果我们使用Node.js版本管理工具就能任意切换Node.js版本,不需要卸载重装。它能帮我们做到v12.x、v14.x、v20.x等等版本之间反复横跳。简单选型我们看看社区有哪些Node.js版本管理工具,做一个简单的选型吧。Node.js版本管理工具npm下载量/周GitHub Star特点nvm不支持npm安装66.9k支持Linux、MacOS,不支持Windows,Windows设备使用nvm-windows。n70k+17.6k支持Linux、MacOS;Windows平台必须通过 WSL(Linux 的 Windows 子系统)工作; 无配置,使用简单。nvs不支持npm安装2.3k基于Node.js开发,跨平台。fnm不支持npm安装11.8kRust语言编写,一个字快,支持跨平台。再说明一下我的情况:我电脑是MacOS,环境已安装Node.js具备npm包管理器,对Node.js版本管理器功能要求不多,方便我切换Node.js版本就够了,综上我选择n。选择理由:它支持npm方式安装,不需要我再学习其他不熟悉的安装工具;简单,无需配置;支持MacOS。童鞋们视自身情况选择合适的Node.js版本管理工具,并不一定要选择n。如果您与我的情况一样,推荐使用n。n安装npm/yarn安装:npm i n -g # 或 yarn global add n 使用 Brew 安装,未安装可以参考 Brew 官网安装。brew install nn命令详情仅说明常用命令,其他的命令童鞋们自己去研究一波。命令命令作用n lsr查看 Node.js 远程版本n i 版本安装指定版本n list查看本地已安装的Node.js版本n交互式切换Node.js版本n rm 版本删除指定版本n lsr查看远程版本,默认20条数据,想查看所有的版本使用n lsr —-all。安装指定版本n i 版本,直接安装最新版本n i 20.1.0 。n list查看到v20.1.0已安装至本地。n交互式允许我们选择想要的Node.js版本。over,上述命令足够了,简单吧!!!总结工欲善其事,必先利其器。想优雅且快速的切换Node版本,当选n Node.js版本管理工具。另外也存在其他Node.js版本管理工具,它们各有优劣,童鞋们视自身情况选择。
0
0
0
浏览量1011
养乐多一瓶

50+行代码搞定一行命令更新Npm包

实现终端交互选择版本号版本号修改只考虑四种情况:更新主版本号, 如1.0.0修改成2.0.0更新子版本号,如1.0.0修改成1.1.0阶段版本号,如1.0.0修改成1.0.1Beta版本,如1.0.0修改成1.0.0-beta.1版本号的命名规则是由. 进行拼接,所以可以使用正则将版本信息获取出来。首先,版本号信息存储在package.jsonversion 字段值中,代码是使用了TypeScript中文网 · TypeScript——JavaScript的超集 (tslang.cn)。Ts执行时,es6模块化会被编译成node环境中支持的CommonJS模块化。即import pkg from "../package.json"; 会被转化成const pkg = require("../package.json") 。 CommonJS标准规定,导入Json文件时,将会有把Json反序列化的操作,所以读取的json 已经被反序列化,不需要再JSON.parse 操作。import pkg from "../package.json"; const version = pkg.version; const reg = /([1-9])\.([0-9])\.([0-9])(?:(\-\w*)\.([1-9]+))?/g; const execs = reg.exec(version) as Array<any>;然后,npm install inquirer 安装inquirer ,它是一个提供终端交互的工具包。按照上述版本修改的四种情况,程序提供的版本号选择列表选择可以分成两种情况:版本号含有beta;版本号没有含有beta。是否含有beta,通过对execs[4] 是否存在来判断。//... const onSelectVersion = async () => { const beta = execs[4]; //处理版本含有beta的情况 const lists = beta ? getBetaVersionLists(beta) : getVersionlists(); inquirer.prompt([{ name: 'list', type: 'list', message: '请选择发布的版本:', choices: lists, default: [lists[0]] }]) //... }处理含有beta的情况const getBetaVersionLists = (beta) => ([ getVersion([execs[1], execs[2], execs[3]]), getVersion([execs[1], execs[2], execs[3]]) + `${beta}.${addOne(execs[5])}` ])处理没有beta的情况const getVersionlists = () => ([ getVersion([addOne(execs[1]), execs[2], execs[3]]), getVersion([execs[1], addOne(execs[2]), execs[3]]), getVersion([execs[1], execs[2], addOne(execs[3])]) ])其中getVersion 和addOne 方法分别用于拼接完整的版本号和数字+1const addOne = (num) => Number(num) + 1; const getVersion = ([major, minor, patch]) => `v${major}.${minor}.${patch}`不存在beta的效果:存在beta的效果:注意点:代码语言是使用TypeScript,所以执行时需要ts-node - npm (npmjs.com)工具帮我们编译下。npm run release 是自定义命令,在文件package.json的scripts下配置即可。//... "scripts": { //.. "release": "ts-node scripts/release.ts" }, //...最后,导入node文件模块,将选择确定后的版本号重新赋值给version,再写入package.json//... import path from "path"; import fs from "fs"; //... const onSelectVersion = async () => { const beta = execs[4]; //处理版本含有beta的情况 const lists = beta ? getBetaVersionLists(beta) : getVersionlists(); inquirer.prompt([{ name: 'list', type: 'list', message: '请选择发布的版本:', choices: lists, default: [lists[0]] }]).then(async ({ list }) => { pkg.version = list //... fs.writeFile(path.join(__dirname, '../package.json'), String(JSON.stringify(pkg)), 'utf8', async (error) => { if (error) { return; } //... }); }) } 好了,一个版本选择的功能就完成了。跟Shell语言说再见版本选择完成,接下来需要处理Git一系列命令提交代码、Git自动打Tag以及向Npm更新等操作。需要处理命令有:git add . git commit -m xx git tag ** git push origin ** //向远程仓库提交tag git push origin branch npm run build //rullop打包 npm publish //npm 发版 其中npm run build 将执行rullop打包在终端中自动化执行一些命令,这个时候就需要Shell,创建Shell文件去完成对应的逻辑。作为前端工程师就需要去学习下Shell,有学习成本。秉承能用Js实现的最终用Js实现的想法,node child_process 子进程模块中提供了可执行Shell命令的API。当然还有更便捷的工具zx,由google开源。npm i zx安装zx,导入zx,#!/usr/bin/env zx 是zx要求的。#!/usr/bin/env zx /... import { $ } from 'zx'; 正则匹配获取当前Git 分支:const onSelectVersion = async () => { const beta = execs[4]; //处理版本含有beta的情况 const lists = beta ? getBetaVersionLists(beta) : getVersionlists(); inquirer.prompt([{ //... }]).then(async ({ list }) => { pkg.version = list let branch = await $`git branch`; const { stdout } = branch; const reg = /\*\D(.+)\D/g; branch = (reg.exec(stdout) as any[])[1]; fs.writeFile(path.join(__dirname, '../package.json'), String(JSON.stringify(pkg)), 'utf8', async (error) => { if (error) { return; } //... }); }) }按顺序执行命令const onSelectVersion = async () => { //.. inquirer.prompt([{ //... }]).then(async ({ list }) => { //... fs.writeFile(path.join(__dirname, '../package.json'), String(JSON.stringify(pkg)), 'utf8', async (error) => { if (error) { return; } await $`git add .`; await $`git commit -m ${list}`; await $`git tag ${list}`; await $`git push origin ${list}`; await $`git push origin ${branch}`; await $`npm run build&&npm publish`; }); }) }zx方法$ 原理是使用了 child_process 子进程模块中spawn 方法到此,一个简简单单的脚本就完成了,算上空行一共就50+行。最后实现逻辑非常简单,没有什么难点。工作中肯定也会有一些需要反复做的事件,我们需要多去思考能否将其简化,学会做一个"会偷懒"的工具人。感谢阅读,文章涉及的代码已开源,欢迎下载尝试release.ts。
0
0
0
浏览量1021
养乐多一瓶

带你入门——如何在nestjs中体验websocket

websocket作为一种h5新增的通信方式,在日常开发中很常见,聊天室,数据大屏等项目中都有它的影子,本文将会根据文档以及自己的理解引导大家学习如何在nestjs中实现websocket,并且会搭配vue3做个小demo帮助大家更好的理解,本文内容有点多,大概需要5-8分钟的学习,希望想学习的小伙伴能够耐心看完,知识点不难,希望能够帮助大家快速入门前言准备在nestjs官方文档中提到,在nestjs中有两个现成支持的 WS 平台:socket.io 和 ws,这俩都是基于websocket封装的框架,其中socket.io在websocket的基础上集成了强大的功能实时双向通信:Socket.IO 提供了实时的双向通信,允许服务器主动向客户端发送消息,并且客户端也可以向服务器发送消息。这种双向通信方式非常适合实时应用程序的需求,例如聊天或协作工具。自动重新连接:当客户端与服务器之间的连接中断时,Socket.IO 具有自动重新连接的能力,它会尝试重新建立连接,并维护连接的稳定性。分房间/分组:Socket.IO 允许将客户端连接划分到不同的房间或分组中,可以根据不同的需求将客户端进行分组管理,并实现群发或与特定组进行通信。自定义事件:Socket.IO 允许开发者定义和触发自定义事件,这些事件可以是应用程序内部的逻辑事件或用户自定义的事件。通过自定义事件,可以实现灵活的消息传递和通信模式。跨平台支持:Socket.IO 不仅支持浏览器端,还支持服务器端的多种编程语言,如 Node.js、Python、Java 和 .NET 等。这使得开发者能够在不同的环境中使用相同的通信协议和模式。并且当 WebSocket 连接不可用时,Socket.IO 会使用 HTTP 协议作为备选方案来保持实时通信。这俩框架选谁都可以,我这里用socket.io来为大家做演示了。构建准备先安装对应的包pnpm i --save @nestjs/websockets @nestjs/platform-socket.io接着通过创建一个gateway模块,与此同时创建一个event.gateway.ts,用于创建我们的websocket服务接着在我们的event.gateway.ts中创建我们的类import {WebSocketGateway} from '@nestjs/websockets' @WebSocketGateway() export class EventGateway { }@WebSocketGateway是一个装饰器,用于创建WebSocket网关类。WebSocket网关类是用于处理 WebSocket连接和消息的核心组件之一。它充当WebSocket服务端的中间人,负责处理客户端发起的连接请求,并定义处理不同类型消息的逻辑对于它可以接受一些参数,这里我们就快速过一遍挑重要的说 @WebSocketGateway(80, options)第一个参数可以传递一个端口号,如果不传默认和http监听的端口一样,也就是main.ts中的端口,nestjs默认是3000,第二个是一些配置项,有很多例如跨域配置cors,以及namespace命名空间,这里只说配置cors跨域,其他的配置项,可以自行去[服务器选项 |Socket.IO]看看在现在的新版本中客户端连接nestjs端的websocket服务时会出现跨域问题的,所以需要配置一下@WebSocketGateway({ cors: { origin: '*', }, })配置完这些,我们还需要在module中提供一下,因为网关可以被视为provider,可以注入依赖项,也可以被其他类注入import { Module } from '@nestjs/common'; import { EventGateway } from './enent.gateway'; @Module({ providers: [EventGateway] }) export class GatewayModule {}实现订阅消息到这里我们就配置好了,但是我们想想一个最简易的聊天是不是得有发送消息和接收消息吧,就是发布订阅模式,nestjs中为我们内置好了对应的装饰器 @SubscribeMessage,看到这个英文单词就知道啥意思了,就是订阅消息的意思,在我们订阅到消息时可以写一下逻辑处理import { MessageBody, SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' @WebSocketGateway({ cors: { origin: '*' } }) export class EventGateway { @SubscribeMessage('newMessage') handleMessage(@MessageBody() body: any) { console.log(body); } }@MessageBody这个装饰器可以获取消息,到这里我们的订阅服务已经起来了,打开控制台启动一下项目看看,看到里面有Websocketscontroller就代表启动成功了如果不想用装饰器可以这样做,效果一样@SubscribeMessage('events') handleEvent(client: Socket, data: any): any { return data; }第一个参数是socket实例,第二个data是从客户端接受的消息,但是官方不推荐这种写法哈,建议还是用装饰器实现发布消息那么处理好了订阅消息,如何实现发布消息呢,官方文档中介绍了一个装饰器 @ConnectedSocket,可以实例化一个socket,我们可以通过上面的emit方法来监听一个自定义事件来发布消息import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' import { Socket } from 'socket.io' @WebSocketGateway({ cors: { origin: '*' } }) export class EventGateway { @SubscribeMessage('newMessage') handleMessage(@MessageBody() body: any, @ConnectedSocket() client: Socket,) { client.emit('onMessage') console.log(body); } }接着打开postman来测试,我推荐这款工具的原因是,我在apifox中发现对websocket的支持不好,而且在postman中有对socket.io专门的测试。下面带着大家上手试试经过这些操作我们就拿到了发出的消息 注意要订阅的是newMessage也就是subscribe装饰器的参数下面介绍另外一种,借助WebsocketServerimport { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets' import { Server, Socket } from 'socket.io' @WebSocketGateway({ cors: { origin: '*' } }) export class EventGateway { @WebSocketServer() server: Server @SubscribeMessage('newMessage') handleMessage(@MessageBody() body: any, @ConnectedSocket() client: Socket,) { this.server.emit('onMessage', { msg: 'new Message', content: body }) } }实现的效果一样的。利用SocketClient创建客户端接下来利用postman模拟客户端新开一个nestjs服务,注意mian.ts中的端口号改一下,别冲突了,创建一个socket模块,跟上面步骤一样import { Module } from '@nestjs/common'; import { SocketClient } from './socket-client'; @Module({ providers: [SocketClient] }) export class SocketModule { } 下面在socket-client.ts中创建如下代码import { Injectable, OnModuleInit } from "@nestjs/common"; import { io, Socket } from 'socket.io-client' @Injectable() export class SocketClient implements OnModuleInit { public socketClient: Socket constructor() { this.socketClient = io('http://localhost:3000') } onModuleInit() { this.registerConsumerEvent() } private registerConsumerEvent() { this.socketClient.on('connect', () => { console.log('connect to gateway'); }) this.socketClient.on('onMessage', (payload: any) => { console.log('socketClientClass'); console.log(payload); }) } }我们这里构造了一个socketClient客户端, OnMouleInit是nestjs的生命周期函数,就是模块初始化时,我们调用了registerConsumerEvent()函数,在这个函数内,我们做了两件事,第一件事,在连接时,做 了逻辑处理,第二件事,监听了onMessage事件,并且能够别人发布的值,接着利用postman来测试一下也是输入url连接之后,需要选监听的events,打开listen 做到这样就可以了,我们从第一个服务中发送一条消息试试可以看到监听到了数据,到此我们已经做完了利用socketClient构造客户端vue3搭配nestjs实现websocket小demo首先在前端中导入包pnpm i socket.io-client之后我们直接在socket.io的官网拿到vue3模板import { reactive } from "vue"; import { io } from "socket.io-client"; export const state = reactive <{ connected: boolean fooEvents: Array<any> barEvents: Array<any> }> ({ connected: false, fooEvents: [], barEvents: [] }); const URL = "http://localhost:3000" export const socket = io(URL); socket.on("connect", () => { state.connected = true; }); socket.on("disconnect", () => { state.connected = false; }); socket.on("foo", (...args) => { state.fooEvents.push(args); }); socket.on("bar", (...args) => { state.barEvents.push(args); });接着在app.vue中引入<script setup lang="ts"> import { onBeforeMount, onMounted, onUnmounted, reactive } from 'vue'; import { socket } from './socket' const chatList = reactive<{ value: string list: Array<any> }>({ value: '', list: [] }) // 组件挂载前让socket连接起来 onBeforeMount(() => { socket.connect(); }); // 组件挂载完毕完成后,监听onMessage事件 onMounted(() => { socket.on("onMessage", (e) => { console.log(e); chatList.list.push(e.content); }); }); // 组件销毁时断开连接 onUnmounted(() => { socket.disconnect(); }); // 点击btn发送socket消息 const handleClick = () => { socket.emit("newMessage", chatList.value, (e: any) => { console.log(e); }); }; </script> <template> <div> <input v-model="chatList.value" /> <button @click="handleClick()" style="margin-left: 12px;">发送</button> <div > <ul> <li v-for="(item, index) in chatList.list" :key="index">{{ item }}</li> </ul> </div> </div> </template> <style scoped> header { line-height: 1.5; } .logo { display: block; margin: 0 auto 2rem; } @media (min-width: 1024px) { header { display: flex; place-items: center; padding-right: calc(var(--section-gap) / 2); } .logo { margin: 0 2rem 0 0; } header .wrapper { display: flex; place-items: flex-start; flex-wrap: wrap; } } </style>到此我们就配置完成了,接下来看看效果吧到此我们就实现了websocket在nestjs的使用,并且能够和客户端进行连接
0
0
0
浏览量1018
养乐多一瓶

用Node.js吭哧吭哧撸一个运动主页

简单唠唠某乎问题:人这一生,应该养成哪些好习惯?问题链接:www.zhihu.com/question/46…如果我来回答肯定会有定期运动的字眼。平日里也有煅练的习惯,时间久了后一直想把运动数据公开,可惜某运动软件未开放公共的接口出来。幸运的是,在Github平台冲浪我发现了有同行和我有类似的想法,并且已经用Python实现了他自己的运动主页。项目链接:github.com/yihong0618/…Python嘛简单,看明白后用Node.js折腾一波,自己撸两个接口玩玩。Github地址:github.com/CatsAndMice…梳理思路平时跑步、骑行这两项活动多,所以我只需要调用这两个接口,再调用这两个接口前需要先登录获取到token。1. 登陆接口: https://api.gotokeep.com/v1.1/users/login 请求方法:post Content-Type: "application/x-www-form-urlencoded;charset=utf-8" 2. 骑行数据接口:https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=cycling&lastDate={last_date} 请求方法: get Content-Type: "application/x-www-form-urlencoded;charset=utf-8" Authorization:`Bearer ${token}` 3. 跑步数据接口:https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=running&lastDate={last_date} 请求方法: get Content-Type: "application/x-www-form-urlencoded;charset=utf-8" Authorization:`Bearer ${token}` Node.js服务属于代理层,解决跨域问题并再对数据包裹一层逻辑处理,最后发给客户端。运动数据总和请求跑步接口方法:getRunning.js文件链接github.com/CatsAndMice…const { headers } = require('./config'); const { isEmpty } = require("medash"); const axios = require('axios'); module.exports = async (token, last_date = 0) => { if (isEmpty(token)) return {} headers["Authorization"] = `Bearer ${token}`; const result = await axios.get(`https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=running&lastDate=${last_date}`, { headers }) if (result.status === 200) { const { data: loginResult } = result; return loginResult.data; } return {}; } 请求骑行接口方法:getRunning.js文件链接 github.com/CatsAndMice…const { headers } = require('./config'); const { isEmpty } = require("medash"); const axios = require('axios'); module.exports = async (token, last_date = 0) => { if (isEmpty(token)) return {} headers["Authorization"] = `Bearer ${token}`; const result = await axios.get(`https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=cycling&lastDate=${last_date}`, { headers }) if (result.status === 200) { const { data: loginResult } = result; return loginResult.data; } return {}; } 现在要计算跑步、骑行的总数据,因此需要分别请求跑步、骑行的接口获取到所有的数据。getAllLogs.js文件链接github.com/CatsAndMice…const { isEmpty } = require('medash'); module.exports = async (token, firstResult, callback) => { if (isEmpty(firstResult)||isEmpty(token)) { console.warn('请求中断'); return; } let { lastTimestamp, records = [] } = firstResult; while (1) { if (isEmpty(lastTimestamp)) break; const result = await callback(token, lastTimestamp) if (isEmpty(result)) break; const { lastTimestamp: lastTime, records: nextRecords } = result records.push(...nextRecords); if (isEmpty(lastTime)) break; lastTimestamp = lastTime } return records } 一个while循环干到底,所有的数据都会被push到records数组中。返回的records数据再按年份分类计算某年的总骑行数或总跑步数,使用Map做这类事别提多爽了。getYearTotal.js文件链接 github.com/CatsAndMice…const { getYmdHms, mapToObj, each, isEmpty } = require('medash'); module.exports = (totals = []) => { const yearMap = new Map() totals.forEach((t) => { const { logs = [] } = t logs.forEach(log => { if(isEmpty(log))return const { stats: { endTime, kmDistance } } = log const { year } = getYmdHms(endTime); const mapValue = yearMap.get(year); if (mapValue) { yearMap.set(year, mapValue + kmDistance); return } yearMap.set(year, kmDistance); }) }) let keepRunningTotals = []; each(mapToObj(yearMap), (key, value) => { keepRunningTotals.push({ year: key, kmDistance: Math.ceil(value) }); }) return keepRunningTotals.sort((a, b) => { return b.year - a.year; }); } 处理过后的数据是这样子的:[ {year:2023,kmDistance:99}, {year:2022,kmDistance:66}, //... ] 计算跑步、骑行的逻辑,唯一的变量为请求接口方法的不同,getAllLogs.js、getYearTotal.js我们可以复用。骑行计算总和:cycling.js文件链接github.com/CatsAndMice…const getCycling = require('./getCycling'); const getAllLogs = require('./getAllLogs'); const getYearTotal = require('./getYearTotal'); module.exports = async (token) => { const result = await getCycling(token) const allCycling = await getAllLogs(token, result, getCycling); const yearCycling = getYearTotal(allCycling) return yearCycling } 跑步计算总和:run.js文件链接 github.com/CatsAndMice…const getRunning = require('./getRunning'); const getAllRunning = require('./getAllLogs'); const getYearTotal = require('./getYearTotal'); module.exports = async (token) => { const result = await getRunning(token) // 获取全部的跑步数据 const allRunning = await getAllRunning(token, result, getRunning); // 按年份计算跑步运动量 const yearRunning = getYearTotal(allRunning) return yearRunning } 最后一步,骑行、跑步同年份数据汇总。src/index.js文件链接github.com/CatsAndMice…const login = require('./login'); const getRunTotal = require('./run'); const getCycleTotal = require('./cycling'); const { isEmpty, toArray } = require("medash"); require('dotenv').config(); const query = { token: '', date: 0 } const two = 2 * 24 * 60 * 60 * 1000 const data = { mobile: process.env.MOBILE, password: process.env.PASSWORD }; const getTotal = async () => { const diff = Math.abs(Date.now() - query.date); if (diff > two) { const token = await login(data); query.token = token; query.date = Date.now(); } //Promise.all并行请求 const result = await Promise.all([getRunTotal(query.token), getCycleTotal(query.token)]) const yearMap = new Map(); if (isEmpty(result)) return; if (isEmpty(result[0])) return; result[0].forEach(r => { const { year, kmDistance } = r; const mapValue = yearMap.get(year); if (mapValue) { mapValue.year = year mapValue.data.runKmDistance = kmDistance } else { yearMap.set(year, { year, data: { runKmDistance: kmDistance, cycleKmDistance: 0 } }) } }) if (isEmpty(result[1])) return; result[1].forEach(r => { const { year, kmDistance } = r; const mapValue = yearMap.get(year); if (mapValue) { mapValue.year = year mapValue.data.cycleKmDistance = kmDistance } else { yearMap.set(year, { year, data: { runKmDistance: 0, cycleKmDistance: kmDistance } }) } }) return toArray(yearMap.values()) } module.exports = { getTotal } getTotal方法会将跑步、骑行数据汇总成这样:[ { year:2023, runKmDistance: 999,//2023年,跑步总数据 cycleKmDistance: 666//2023年,骑行总数据 }, { year:2022, runKmDistance: 99, cycleKmDistance: 66 }, //... ] 每次调用getTotal方法都会调用login方法获取一次token。这里做了一个优化,获取的token会被缓存2天省得每次都调,调多了登陆接口会出问题。//省略 const query = { token: '', date: 0 } const two = 2 * 24 * 60 * 60 * 1000 const data = { mobile: process.env.MOBILE, password: process.env.PASSWORD }; const getTotal = async () => { const diff = Math.abs(Date.now() - query.date); if (diff > two) { const token = await login(data); query.token = token; query.date = Date.now(); } //省略 } //省略 最新动态骑行、跑步接口都只请求一次,同年同月同日的骑行、跑步数据放在一起,最后按endTime字段的时间倒序返回结果。getRecentUpdates.js文件链接 github.com/CatsAndMice…const getRunning = require('./getRunning'); const getCycling = require('./getCycling'); const { isEmpty, getYmdHms, toArray } = require('medash'); module.exports = async (token) => { if (isEmpty(token)) return const recentUpdateMap = new Map(); const result = await Promise.all([getRunning(token), getCycling(token)]); result.forEach((r) => { if (isEmpty(r)) return; const records = r.records || []; if (isEmpty(r.records)) return; records.forEach(rs => { rs.logs.forEach(l => { const { stats } = l; if (isEmpty(stats)) return; // 运动距离小于1km 则忽略该动态 if (stats.kmDistance < 1) return; const { year, month, date, } = getYmdHms(stats.endTime); const key = `${year}年${month + 1}月${date}日`; const mapValue = recentUpdateMap.get(key); const value = `${stats.name} ${stats.kmDistance}km`; if (mapValue) { mapValue.data.push(value) } else { recentUpdateMap.set(key, { date: key, endTime: stats.endTime, data: [ value ] }); } }) }) }) return toArray(recentUpdateMap.values()).sort((a, b) => { return b.endTime - a.endTime }) } 得到的数据是这样的:[ { date: '2023年8月12', endTime: 1691834351501, data: [ '户外跑步 99km', '户外骑行 99km' ] }, //... ] 同样的要先获取token,在src/index.js文件:const login = require('./login'); const getRecentUpdates = require('./getRecentUpdates'); //省略 const getFirstPageRecentUpdates = async () => { const diff = Math.abs(Date.now() - query.date); if (diff > two) { const token = await login(data); query.token = token; query.date = Date.now(); } return await getRecentUpdates(query.token); } //省略 最新动态这个接口还是简单的。express创建接口运动主页由于我要将其挂到我的博客,因为端口不同会出现跨域问题,所以要开启跨源资源共享(CORS)。app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.setHeader('Content-Type', 'application/json;charset=utf8'); next(); }) index.js文件链接github.com/CatsAndMice…const express = require('express'); const { getTotal, getFirstPageRecentUpdates } = require("./src") const { to } = require('await-to-js'); const fs = require('fs'); const https = require('https'); const path = require('path'); const app = express(); const port = 3000; app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.setHeader('Content-Type', 'application/json;charset=utf8'); next(); }) app.get('/total', async (req, res) => { const [err, result] = await to(getTotal()) if (result) { res.send(JSON.stringify({ code: 200, data: result, msg: '请求成功' })); return } res.send(JSON.stringify({ code: 400, data: null, msg: '请求失败' })); }) app.get('/recent-updates', async (req, res) => { const [err, result] = await to(getFirstPageRecentUpdates()) if (result) { res.send(JSON.stringify({ code: 200, data: result, msg: '请求成功' })); return } res.send(JSON.stringify({ code: 400, data: null, msg: '请求失败' })); }) const options = { key: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.key')), cert: fs.readFileSync(path.join(__dirname, './ssl/9499016_www.linglan01.cn.pem')), }; const server = https.createServer(options, app); server.listen(port, () => { console.log('服务已开启'); })
0
0
0
浏览量1013
养乐多一瓶

Vue3中的diff算法——diff算法需要处理的几种场景

两组元素个数相同的时候首先,我们先来看下元素个数相同的时候,比如:旧节点,ul 元素下是一组 li,总共 3 个 li:<ul> <li>a</li> <li>b</li> <li>c</li> </ul>新节点,ul 元素下也是一组 li,总共也是 3 个 li:<ul> <li>a</li> <li>b</li> <li>d</li> // 第三个 li 元素发生了变化!👾 </ul>这个时候,我们只需要遍历两组子节点,然后依次更新每一个节点就可以了,代码如下:const oldChildren = ul1.children; const newChildren = ul2.children; for (let i = 0; i < oldChildren.length; i++) { // 调用 patch 函数依次更新子节点 patch(oldChildren[i], newChildren[i]); }两组元素个数不同的时候但在实际情况中,我们新的一组元素常常和旧的一组元素个数不一样。这个时候,就会有 2 种情况:新的一组元素数量 > 旧的一组元素数量新的一组元素数量 < 旧的一组元素数量这个时候,我们可以首先获取两组子节点公共的元素数量的长度,然后如果新的一组元素多,则挂载剩余的新元素;新的一组元素少,则卸载旧元素即可。代码如下:const oldChildren = ul1.children; const newChildren = ul2.children; const oldChildrenLength = oldChildren.length; const newChildrenLength = newChildren.length; // 获取公共的长度 const commonLength = Math.min(oldChildrenLength, newChildrenLength); for (let i = 0; i < commonLength; i++) { patch(oldChildren[i], newChildren[i]); } // 新的一组元素多,则挂载剩余的新元素 if (newChildrenLength > oldChildrenLength) { for (let i = commonLength; i < newChildrenLength; i++) { patch(null, newChildren[i]); } } // 新的一组元素少,则卸载旧元素即可 else if (newChildrenLength < oldChildrenLength) { for (let i = commonLength; i < oldChildrenLength; i++) { unmount(oldChildren[i]); } }两组元素的顺序不同还有一种更加复杂,但是也是很常见的情况,就是新的一组元素的顺序和旧的一组元素的顺序是不同的,比如:旧节点:[ { type: "p", children: "我是p1" }, { type: "p", children: "我是p2" }, { type: "p", children: "我是p3" }, ];新节点:[ { type: "p", children: "我是p2" }, { type: "p", children: "我是p3" }, { type: "p", children: "我是p1" }, ];这个时候,最高效的更新节点的方式是:将 我是p1 移动到新的一组节点的末尾即可。但是,这对于我们肉眼来说,是很简单的。但程序怎么知道谁是 p1 呢?因为我们现在只用了 type 这个属性来区分不同的节点,这个属性的值可能是 p、div、li...所以,我们需要引入一个新的属性来区分相同的 type 为 p 的节点,这个新属性就是 key。旧节点:[ { type: "p", children: "我是p1", key: 1 }, { type: "p", children: "我是p2", key: 2 }, { type: "p", children: "我是p3", key: 3 }, ];新节点:[ { type: "p", children: "我是p2", key: 2 }, { type: "p", children: "我是p3", key: 3 }, { type: "p", children: "我是p1", key: 1 }, ];这个时候,我们就很清晰的知道:对于 p1 来说,它从旧节点的第一位,移动到了新节点的第三位。总结那么今天,我们一共探讨了diff算法 需要处理的 3 种不同的情况。但现在,我们只是从理论的角度进行了分析,但 vue3 中,它具体是如何实现这个 diff算法 的呢?比如,我们这里最后说的 移动节点,我们可以把 p1 移动到最后,也可以把 p2、p3 往前移动,那么 vue3 是如何求得最优的移动方案的呢?这个,我们就留到下一回再说。
Vue
0
0
0
浏览量1086
养乐多一瓶

不想再做唯一值判断? 来看看dto的自定义校验规则

前言在我们的开发中经常需要验证字段唯一性,比如当我们注册账号的时候,经常要判断用户有没有注册过,对于字段较少的还好,如果有很多字段都需要判断,那我们就需要查很多字段,在代码中写一堆这个东西,看着挺不舒服的,就比如下面这样我们对于这四个唯一字段每次创建前都需要判断,而且其他服务创建前都有某个或某些字段需要判断唯一性,那么我们能不能封装一个方法,来简化这种操作呢。当然有,下面进入我们的正题。本章所需知识nestjsprismamysqlclass-validator封装方法我们在做dto验证的时候大概都用过class-validator这个包,我这里就不介绍了,一个验证规则包,本文主要就是讲它的自定义校验规则。github上找到这个,然后看他下面的文档找到这俩,一个是自定义验证类,一个是自定义装饰器,他们都可以,本文都会介绍到。根据文档指引,我们先把模板搭建好。自定义验证类现在就在dto里面写自定义验证类import { IsNotEmpty, Validate } from 'class-validator' import { IsExistRule } from './custom/is_exist' export class registerDto { @Validate(IsExistRule, { message: '用户名已存在' }) @IsNotEmpty({message: '用户名不能为空'}) username: string @IsNotEmpty({message: '密码不能为空'}) password: string } 我们可以看到text字段以及agrs代表什么,如果只是这样,那我们每次在内部进行唯一性判断的时候,表名就定死了,有没有什么办法呢,当然有,我们可以看到args里面的constraints为undefined,这个关联值,我们可以设置为表名那么我们可以每次都拿这个关联值就可以动态表名了,但这里我们就可以开始了。这里的逻辑就写完了,测试就不给大家展示了,感兴趣的可以自己用测试工具测一下,另外dto的验证错误捕获,对于不是很复杂的项目,大可用系统内置的validatePipe来捕获自定义验证装饰器其实也就是写法不一样,看到这些属性名就很熟悉,和刚刚的差不多import { PrismaClient } from '@prisma/client'; import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; export function IsNotExist(property: string, validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ name: 'IsNotExist', target: object.constructor, propertyName: propertyName, constraints: [property], options: validationOptions, validator: { async validate(value: any, args: ValidationArguments) { const prisma = new PrismaClient const tableName = args.constraints[0] const property = args.property const res = await prisma[tableName].findUnique({ where: { [property]: value } }) return res }, }, }); }; }import { IsNotEmpty, Validate } from 'class-validator' import { IsNotExist } from './custom/is_not_exist' export class LoginDto { @IsNotExist('user', { message: '用户不存在' }) @IsNotEmpty({message: '用户名不能为空'}) username: string @IsNotEmpty({message: '密码不能为空'}) password: string }当我们登陆的时候往往需要判断是否有这个值,可以这样来判断,但是不建议哈,因为本身字段就少,我们写这么多代码得不偿失,但是上面那个就可以,应用场景我们也能用的到。结语通过本章你可以学习到自定义验证的用法,可以对你的代码结构进行简化,也可以满足你的验证需求,希望大家能够有所收获!😘
0
0
0
浏览量469
养乐多一瓶

nestjs开发小技巧——任务调度

任务调度在NestJS 中,调度(scheduling)是一种用于安排和执行任务的机制。它基于 cron 表达式,允许你按照给定的时间间隔或者特定日期时间来执行代码逻辑。调度在许多应用场景中都非常有用,例如:执行定时任务:使用调度功能,你可以按照设定的时间间隔自动执行一些任务,比如每隔一段时间清理数据库、导出报表、发送定期邮件等。这样可以减少手动干预,提高系统的自动化程度。执行周期性任务:有时候我们需要定期执行一些任务,比如每天、每周或每月执行一次。调度功能可以帮助你基于日期和时间规则设定这些任务,并按照规定时间自动触发执行。处理后台任务:通常,一些任务需要在后台运行,而不是在请求-响应循环中直接处理。调度功能可以帮助你创建后台任务,将它们添加到调度器中,然后在特定时间条件下自动触发执行。开始使用在 NestJS 中,可以使用 ScheduleMoudle模块来实现调度功能。该模块提供了一组装饰器和类,用于定义调度任务(schedule tasks)和调度器(schedulers)项目所需依赖pnpm i @nestjs/schedule pnpm i @types/cron --save-dev先在根模块中注册好schedule的静态方法 接着创建一个服务,里面来调我们的服务可以看到它是在每分钟的第50秒开始调用,而且是每两秒调用一次 cron我们来看看cron模式字符串的中的每个位置代表的含义在 cron 模式字符串中,每个位置表示不同的时间单位和范围。字符串由空格分隔为 6 个字段,每个字段都表示特定时间单位的取值范围。以下是每个位置的含义:秒(Seconds):表示每分钟的秒数,取值范围是 0 到 59分钟(Minutes):表示每小时的分钟数,取值范围是 0 到 59小时(Hours):表示每天的小时数,取值范围是 0 到 23日期(Day of month):表示每月的日期数,取值范围是 1 到 31(但某些月份日期数可能会有所不同)月份(Month):表示一年中的月份,取值范围是 1 到 12,或者使用对应的缩写字符串来表示月份(如:JAN 表示一月,FEB 表示二月)星期几(Day of week):表示一周中的天数,取值范围是 0 到 7,其中 0 和 7 都表示周日,1 表示周一,以此类推。你也可以使用对应的缩写字符串来表示天数(如:SUN 表示周日,MON 表示周一)此外,还可以在每个位置中使用特殊字符来表示不同的含义:星号(*):匹配该位置的任意值。例如,在分钟位置使用 * 表示每分钟都触发。逗号(,):用于指定多个取值。例如, 表示取值为 1、3 和 5 的情况。1,3,5连字符(-):用于指定一个范围。例如, 表示取值范围为 10 到 15。10-15步长(/):用于指定一个间隔值。例如, 表示每隔 5 个单位触发一次。*/5第一个位置可以不写,那么他就会按照后面设置的时间执行举几个简单的例子cron模式执行时间* * * * * *每秒钟都执行45 * * * * *每1分钟的第 45 秒执行0 10 * * * *每小时的在第 10 分钟执行0 */30 9-17 * * *每隔 30 分钟,在上午 9 点到下午 5 点之间的每个小时都触发0 30 11 * * 1-5周一至周五上午11:30大家也可以去Crontab.guru - The cron schedule expression editor 该网站上去练习体会一下注意一下,这里只有5个数,第一个秒被省略了,做的时候注意一下除此之外,nestjs还有它特有的调度的写法 CronExpression,其实也就是nestjs内置好了的写法,如果不想用cron标准方式的话可以用这种这个意思就是每十秒调一次Intervalnestjs特有的任务调度的方法, 声明方法以(定期)指定的间隔运行,在方法定义前面加上修饰器。将间隔值(以毫秒为单位的数字)传递给装饰器比如我们这样就是每秒钟执行 总结时间间隔以及CronExpression是nestjs有的, 调度在docker,k8s,操作系统中都有,遵循的是cron标准写法,所以推荐用cron字符串模式,不能满足条件的时候再用其他方式项目实践我前一篇文章写的那个项目就用到了定时任务,背景是这样,我在上传图片的时候因为我的图片是优先于我的用户先创建的,所以我给file的这个表里面的外键userId是为null, 在后面创建用户的时候,会再判断,然后更新userId, 但是有的图片,比如我上传了,但是我没有去创建用户,但是我这个图片已经传到文件服务器了,这个时候就出现了脏数据了,我们就需要定时清理这些脏数据。 @Cron('0 0 0 * * *') async clearEmptyUserIdFiles(){ const fileRecordToDelete = await this.prisma.file.findMany({ where: { userId: null } }) await this.prisma.$transaction(async(prisma) => { await Promise.all([ fileRecordToDelete.map(record => { this.minioClient.removeObject(this.configService.get('bucket').name, record.fileName) }), prisma.file.deleteMany({ where: { userId: null } }) ]) }) }在我的文件服务里面,我会每天00:00来清理那些userId为null的文件,因为要清理的不仅是是数据库里面也有minio里面的,所以我用了事务来保证提交的一致性。
0
0
0
浏览量1015
养乐多一瓶

Node.js操作Dom ,轻松hold住简单爬虫

前言前段时间,我发现一个开源题库,题目非常有意思。我想把它整成一个JSON文件做为数据储备,方便整活。一共有一百五十多道题目,手动CV我肯定是不想干的。于是写了个脚本,在写脚本的过程中,我发现一个能让Node.js操作Dom的开源项目。有了它,再加上jQuery就可以应对简单爬虫抓取数据了,所以写下这篇文章跟大家分享下。源码仓库地址:CatsAndMice/auto-script (github.com)解析markdown文件获取数据读取markdown文件,去除无用的开头内容const readMd = (path) => { let content = fs.readFileSync(path, { encoding: 'utf-8' }); content = content.split('---'); // 去除开头无用开头内容 content.shift(0, 1); return content; } markdown文件中每一段内容都是—-分隔,那就直接以它来分割文件内容为若干块,第一块为开头内容content.shift(0, 1)去除掉使用markdown-it将markdown内容渲染成html内容,这个时候的html仅仅是字符串,jsDom将html字符串转化成Domconst mdIt = require('markdown-it')(); const jsdom = require("jsdom"); const { JSDOM } = jsdom; //... const parseMd = (md = '') => { const mdHtml = mdIt.render(md) const dom = new JSDOM(mdHtml) const { window } = dom return window } //... 完成markdown渲染成html字符串,html字符串转化成Dom后,直接把window这个变量返回出去即可。引入jQuery操作Domconst getMdMapValue = (mds = []) => { const mdMap = new Map(); mds.forEach((md, index) => { const id = index + 1 const window = parseMd(md) const $ = require('jquery')(window); //... }) return Array.from(mdMap.values()) } 接下来,即使用$ 针对性的获取Dom内容const getMdMapValue = (mds = []) => { const mdMap = new Map(); mds.forEach((md, index) => { const id = index + 1 const window = parseMd(md) const $ = require('jquery')(window); //新增 const answer = parseAnswer($('p')) const obj = { id, title: $('h6').text(), result: $('h4').text(), code: $('.language-javascript').text(), answer } // 如果选项解析失败,则抛弃该题目 try { parseSelect($('ul>li'), (key, value) => { const option = { key, value } if (obj.options) { obj.options.push(option) return } obj.options = [option] }) mdMap.set(id, obj) } catch (error) { console.warn('解析出错:', error) } }) return Array.from(mdMap.values()) } 其他的逻辑就是将<code>、<em>等标签转化成`、** 等markdown符号相关边界性问题处理,不详细粘贴代码,读者可以查看源码写个小爬虫我选择爬取fabiaoqing.com/bqb/lists/t… 网站的表情图,逻辑非常简单,几十行搞定。还是使用jsDom将请求响应的html文件内容转化为Dom,再使用jQuery操作。crawl.jsconst axios = require('axios'); const { JSDOM } = require('jsdom'); let $ = require('jquery'); const fs = require('fs'); const path = require('path'); (async () => { const { data } = await axios.get('https://fabiaoqing.com/bqb/lists/type/hot.html') const page = new JSDOM(data) const window = page.window $ = $(window) $('.bqppdiv').each(async (index, e) => { const src = $(e).find('.image')[0].getAttribute('data-original') const type = path.extname(src) const fileName = Date.now() + type const { data } = await axios.get(src, {responseType: 'stream'}) const download = path.join(__dirname, fileName) data.pipe(fs.createWriteStream(download)) }) })() 这里,我仅演示下爬虫不再深入,类似简单爬虫仅使用jquery、jsDom即可搞定,不需要学习其他复杂的爬虫工具。总结通过解析markdown文件将纯markdown文本解析成html字符串,又将html字符串转化成真实的Dom对象,再使用jQuery来获取Dom,达成markdown文件内信息转成JSON文件作为数据存储的目的,最后演示NodeJs操作Dom,并简单写一个爬虫做为练习。
0
0
0
浏览量1016
养乐多一瓶

【Node.js】ssh2.js+Shell一套组合拳下来,一年要花2080分钟做的工作竟然节省到5

进入了新的一年,团队被分配了新的工作内容——每周巡检。巡检工作简单,但需要人工重复性地登陆远程服务器、输入重复的命令,然后将命令的结果记录下来。每做一次估计花40分钟,但要每周做,一年52周,一年下来就要花40*52=2080分钟,这仅仅是团队一个人一年要花的时间。不能这么玩呀,纯纯工具人,所以我一直在思考如何用程序帮我自动巡检掉。这篇文章的出现,说明我的想法方向是正确的,收益可观一年要花2080分钟,被我减到52 分钟。如果再扩展程序帮助到团队,这个公式将从40*52*团队人数变成1*52*团队人数,时间等于金钱。未自动巡检:手动连接登陆远程服务器,再输入相应的命令获取结果,然后人工依据结果判断是否异常,相当麻烦,而且我要执行的命令不止一条。自动巡检:运行macOS笔记本创建好的快捷指令,它会自动巡检服务器,并且巡检完成后直接打开巡检结果表格。当然没有macOS依然可以,但就是没有快捷指令这步,需要自己执行程序。完整源码:blog/ssh实现实现难点自动化巡检思路简单,思路如下:本地程序连接登陆远程服务器→本地shell命令远程执行→本地程序获取命令结果→结果数据整理成表格实现过程中主要有以下两个难点:Node.js本地运行程序如何连接登陆远程服务器登陆远程服务器帐号权限不足,在使用sudo命令时,如何自动输入密码实现细节解决Node.js本地运行程序如何连接登陆远程服务器:社区已有的方案ssh2,它是用纯JavaScript为Node.js编写的SSH2客户端和服务器模块。可以使用它连接到远程服务器,并且ssh2提供了方法可以执行shell命令。ssh2官方案例://... const { Client } = require('ssh2'); const conn = new Client(); conn.on('ready', () => { console.log('Client :: ready'); //执行uptime conn.exec('uptime', (err, stream) => { if (err) throw err; stream.on('close', (code, signal) => { console.log('Stream :: close :: code: ' + code + ', signal: ' + signal); conn.end(); }).on('data', (data) => { //监听数据 console.log('STDOUT: ' + data); }).stderr.on('data', (data) => { console.log('STDERR: ' + data); }); }); }) //... 官方案例仅执行一条shell命令,当按照顺序依次执行一条以上的命令,官方的这个写法会非常麻烦。例如:首先执行docker ps -a -q获取所有docker容器id,然后再docker logs --tail 200 id //... // 获取docker所有容器ID conn.exec('docker ps -a -q', (err, stream) => { if (err) throw err; stream.on('close', (code, signal) => { /** docker ps -a -q命令执行完成 再执行docker logs -f --tail 200 id */ conn.exec(`docker logs --tail 200 ${id}`,(err,stream)=>{ if (err) throw err; stream.on('close', () => { //如果命令再复杂点,还需要继续这样写下去 }).on('data', (data) => { console.log( data); }).stderr.on('data', (data) => { console.log(data); }); }) }).on('data', (data) => { console.log('STDOUT: ' + data); }).stderr.on('data', (data) => { console.log('STDERR: ' + data); }); }); //... 要想写法整洁点,我们需要再给 exec方法用Promise包一层。execFn.js:module.exports = (c = conn) => { return (command) => { return new Promise((resolve, reject) => { c.exec(command, (err, stream) => { if (err) { reject(err) return } let result = '' stream.on('close', () => { resolve(String(result)) }).on('data', (data) => { //data数据是Buffer类型,需要转化成字符串 result += data }) }) }) } } 包一层后,再执行命令:const execFn = require('./execFn.js') module.exports = (config, conn) => { conn.on('ready',async ()=>{ const exec = execFn(conn) const result = await exec('docker ps -a -q') //... exec(`docker logs --tail 200 ${id}`) }) //... } 这样代码会显得更整洁点,使用也更方便。解决登陆远程服务器帐号权限不足,在使用sudo命令时,如何自动输入密码,可行方案有两种:简单粗暴,直接使用root帐号密码进行登陆,这样即可不用考虑如何跳过密码输入的交互使用shell管道命令echo '密码' | sudo -S 命令root帐号密码团队不能给到我,所以我采用了后者来解决。shell实现自动输入密码方法不只有使用管道命令echo '密码' | sudo -S 命令,还有其他的方法,但它在自动巡检的场景中是最合适的,它不需要额外要求服务器下载其他工具包,像expect指令它就需要安装expect包。巡检不只巡检一台服务器,如果每台都安装expect包,这工作量也烦人。未自动输入密码:自动输入密码:至此,自动化巡检难点之处已解决,下面的工作就是以执行shell命令返回的结果判断服务器状态是否正常,如:团队巡检文档规定当执行 docker info |grep -A 5 "WARNING"时,如果有返回结果则为异常。//... const before = `echo "${config.password}" | sudo -S ` exec(before + 'docker info |grep -A 5 "WARNING"').then((content) => { if (content) { rol[2] = '异常' } }) //... 该部分逻辑以团队巡检文档内容为准,不过多赘述,该部分代码在sshServer.js文件。为了做到巡检多台服务器的目的,巡检相关的逻辑代码使用函数进行包裹并从sshServer.js文件中导出。sshServer.js:const execFn = require('./execFn.js') //... module.exports = (config, conn) => { return new Promise((resolve, reject) => { const exec = execFn(conn) conn.on('ready', async (err) => { if (err) reject(err) console.log('连接成功'); //省略 }).connect({ ...config, readyTimeout: 5000 }); }) } 所有的服务器帐号密码均放置在config.json文件中:[ { "host": "xx", "port": "xx", "username": "xx", "password": "xx" } //... ] 在config.json文件涉及到服务器信息需要保密,config.json文件不会被提交至仓库。目录结构如下:最后,将巡检的结果数据整理成表格,如何将数据导出表格已有对应的文章实现说明【Node.js】写一个数据自动整理成表格的脚本思路是一样的。index.jsconst { Client } = require('ssh2'); const configs = require('./config.json') const sshServer = require('./sshServer.js'); const fs = require('fs'); const path = require('path'); const nodeXlsx = require('node-xlsx') const promises = [] //表格数据 二维数组 const tables = [ ['服务器ip', 'docker是否正常运行', 'docker远程访问', 'Docker日志是否有报错信息'] ] configs.forEach((config) => { const conn = new Client(); promises.push(sshServer(config, conn)) }) Promise.all(promises).then((data) => { data.forEach((d) => { if (Array.isArray(d)) { tables.push(d) } }) //生成xlsx表格 const buffer = nodeXlsx.build([{ name: '巡检', data: tables }]) const file = path.join(__dirname, '/server.xlsx') fs.writeFileSync(file, buffer, 'utf-8') }) 巡检结果统一暂存于tables数组中,以便导出。实现快捷指令巡检使用命令行巡检还是太累了。 最好是鼠标点下自动触发自动巡检。我们可以借助Mac快捷指令自定义再简化下。快捷指令可以运行Shell。这样只需要编写一个名字叫做【巡检服务器】的快捷指令。运行Shell后,以WPS打开server.xlsx文件。快捷指令添加至访达。这样就可以轻松实现自动巡检服务器功能了。总结文章灵感来源于工作,通过使用Node.js+Shell+ssh2做到自动连接登陆远程服务器,运行相关Shell命令,检查服务器程序运行是否正常等情况。
0
0
0
浏览量1011
养乐多一瓶

Puppeteer无头浏览器:开启自动化之门,掌握浏览器世界的无限可能

大概还是入门期,我曾用Puppeteer做爬虫工具以此来绕过某网站的防爬机制。近期有需求要做任意链接网页截图,像这种场景非常适合用Puppeteer完成。无头浏览器我已知的还有Selenium。完成截图需求踩的最大的坑不是具体的逻辑代码,而是Docker部署Puppeteer到服务器总是缺少某个包。踩坑过程我想另外写一篇文章分享,所以这篇就单纯给读者介绍Puppeteer无头浏览器。什么是Puppeteer?Puppeteer是由Google开发和维护的一款强大的Node.js库,它为开发人员提供了高级API,以编程方式操控Chromium浏览器。与传统的浏览器自动化工具相比,Puppeteer的独特之处在于它可以运行无头浏览器,即在没有UI界面的情况下运行浏览器实例。这意味着你可以在后台运行浏览器,执行各种任务,而无需手动操作浏览器界面。Puppeteer的背后是Chromium浏览器,这是一款开源的浏览器项目,也是Google Chrome浏览器的基础。因此,Puppeteer具备了与Chrome相同的功能和兼容性。Puppeteer的设计目标是为开发者提供一种简单而直观的方式来自动化浏览器操作。它提供了丰富的API,可以轻松地进行页面导航、元素查找、表单填写、数据提取等操作。你可以编写脚本来模拟用户在浏览器中的操作,从而实现自动化测试、网页截图、数据爬取等任务。此外,Puppeteer还支持无头模式,这意味着浏览器在后台运行,不会显示界面。这使得Puppeteer非常适合在服务器环境中运行,例如自动化测试的CI/CD流水线、数据挖掘和网络爬虫等场景。Puppeteer的应用场景模拟用户操作: Puppeteer可以模拟用户在浏览器中的各种操作,如点击、输入表单、页面导航等。你可以通过编写代码来自动化执行这些操作,轻松模拟用户行为;屏幕截图和PDF生成: Puppeteer可以帮助你捕获网页的屏幕截图或生成PDF文件;网页内容抓取: Puppeteer可以加载网页并提取其中的文本、图像、链接等内容;表单自动填充和提交: Puppeteer可以模拟用户的交互操作,自动填写表单字段并触发提交操作;性能监测和分析: Puppeteer可以帮助你评估网页的性能指标,如加载时间、资源使用情况等。你可以使用这些工具来发现性能瓶颈、进行优化并监测改进效果;网页自动化测试: Puppeteer可以模拟用户的操作并与网页进行交互,你可以编写测试脚本,让Puppeteer自动加载网页、执行操作,并验证网页的行为和结果是否符合预期。以上是Puppeteer的一些常见的应用场景,由于其强大的功能和灵活性,你可以使用Puppeteer完成各种与网页操作和自动化相关的任务。Puppeteer的安装首先,node -v检查本地Node.js版本,官方要求版本在14以上。不满足要求的同学参考Node.js版本管理工具,我选择n 选择合适的工具切换Node.js版本。然后要有心理准备,当我们npm i puppeteer安装Puppeteer时,它会顺带将Chromium浏览器安装。Chromium浏览器几百MB,下载相对慢,而且经常性出错下载失败。如果失败,尝试执行些命令再次下载:node node_modules/puppeteer/install.js 下载前,建议将切换npm仓库源至国内:npm config set registry https://registry.npm.taobao.org Puppeteer的使用不考虑复杂的网页,简单实现的截图:const puppeteer = require('puppeteer'); const path = require('path'); const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: "domcontentloaded" }); await page.setViewport({ width: 1280, height: 720 }); await page.screenshot({ path: path.resolve(__dirname, `./${Date.now()}.png`), type: 'png', fullPage: true }) await browser.close() 几行代码,基本就能实现截图,当然没有考虑网页图片等异步加载的资源情况。执行它就能得到一张截图。如果执行报错:Could not find Chrome (ver. 116.0.5845.96). This can occur if either说明Chromium没有安装。执行上述安装命令node node_modules/puppeteer/install.js总结Puppeteer是一款无头浏览器工具,它为开发者们提供了一种强大而灵活的方式来控制浏览器。通过模拟用户行为、截取屏幕截图、拦截网络请求和进行自动化测试,Puppeteer能够轻松应对各种开发任务。
0
0
0
浏览量1017
养乐多一瓶

Nest.js实战指南

Nest.js实战指南是一个面向开发者的专栏系列,旨在帮助您深入了解和掌握Nest.js框架的各种实用技巧和最佳实践。从简单的国际化配置到使用WebSocket体验实时通信,再到Prisma作为下一代Node.js和TypeScript的ORM工具的使用,以及任务调度等小技巧,本专栏将带您逐步学习和实践在Nest.js中构建高质量应用所需的关键概念和技术。通过实际示例和详细的解释,您将学会如何从零开始部署Nest.js项目,并获得开发高效、可维护和可扩展的应用程序所需的技能。无论您是新手还是有经验的开发者,本专栏都将为您提供实用的指导,助您在Nest.js开发中取得成功。
0
0
0
浏览量1185
养乐多一瓶

Vue3中的Diff算法解析与优化

这个专栏将深入探讨Vue3中的Diff算法,重点关注乱序处理、Diff算法的前4步处理、需要处理的几种场景以及何时需要使用Diff算法。我们将详细解释乱序处理的原理和优化策略,剖析Diff算法的前4步处理过程,包括新节点的创建、删除、更新和移动。我们还将深入探讨Diff算法在不同场景下的应用,如列表渲染、条件渲染和动态组件等。通过这个专栏,你将全面了解Vue3中的Diff算法,并学会优化应用程序的性能和渲染效率。
Vue
0
0
0
浏览量113
养乐多一瓶

Node.js 实战指南

Node.js 实战指南是一本面向开发者的专栏,旨在帮助读者掌握 Node.js 的各种实用技巧和应用场景。本专栏涵盖了从最简单的文档生成到玩转文件模块、Node.js 操作 DOM、轻松实现简单爬虫,以及利用 Node.js 编写数据自动整理成表格的脚本、使用 ssh2.js 和 Shell 实现高效操作,甚至在 Linux 服务器上部署 Puppeteer 的 Docker 指南等内容。通过详细的解释、示例代码和实际案例,读者将能够深入了解 Node.js 的核心功能和相关工具,提升开发效率并构建出色的应用。无论您是初学者还是有一定经验的开发者,本专栏都将为您提供实战经验和技巧,让您轻松掌握 Node.js 并在实际项目中取得成功。
0
0
0
浏览量1188
养乐多一瓶

每天3分钟,重学ES6-ES12(三)标签模版字符串

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是模版字符串和标签模版字符串模版字符串在ES6之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly)。ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接: 首先,我们会使用 `` 符号来编写字符串,称之为模板字符串; 其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;代码演示// ES6之前拼接字符串和其他标识符 const name = "yz" const age = 24 const height = 175 console.log("my name is " + name + ", age is " + age + ", height is " + height) // my name is yz, age is 24, height is 175 // ES6提供模板字符串 `` const message = `my name is ${name}, age is ${age}, height is ${height}` console.log(message) // my name is yz, age is 24, height is 175 const info = `age double is ${age * 2}` console.log(info) // age double is 48 function doubleAge() { return age * 2 } const info2 = `double age is ${doubleAge()}` console.log(info2) // double age is 48 标签模版字符串函数调用的一种特殊形式模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。 我们一起来看一个普通的JavaScript的函数:function foo(m, n, x) { console.log(m, n, x, '---------') } foo("Hello", "World") 如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:// 另外调用函数的方式: 标签模块字符串 // foo`` // foo`Hello World` const name = "why" const age = 18 // ['Hello', 'Wo', 'rld'] foo`Hello${name}Wo${age}rld` 语法标签模板由标签函数和模板字符串两部分组成。其中标签函数的名称大家可以根据项目规范随意命名,模板字符串往往是是需要处理的数据内容。// 语法标准 tag(arrStrings, exp1, exp2, exp3, ...) // 实际使用 foo`Hello${name}Wo${age}rld` 对照着来看tag就是函数名 如foo = tagarrStrings 指的是被 ${...} 这种表达式分隔的字符串 如arrStrings = ['Hello','Wo','rld'] = foo函数中的第一个参数mexp1, exp2, ... 分别表示第1个 ${...} 占位符中表达式的值,第2个 ${...} 表达式的值 exp1 = ${name} = 'yz' = foo函数中的第二个参数n exp2 = ${age} = '18' = foo函数中的第一个参数x应用场景标签模板功能很强大,可能一开始并不会觉得厉害之处,平时工作中也不会用到,但是这些知识是有用的,在很多库中会用到它。react的styled-components可以直接生成组件,动态生成样式const Button = styled.button` background: ${props => props.primary ? 'palevioletred' : 'white'}; color: ${props => props.primary ? 'white' : 'palevioletred'}; font-size: 1em; margin: 1em; padding: 0.25em 1em; border: 2px solid palevioletred; border-radius: 3px; `; 多语言转化(国际化处理)i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!` // "欢迎访问xxx,您是第xxxx位访问者!" 最后,这是我第一次参加更文活动,茫茫人海中,如果有幸遇到你,读到我这篇文章,那真是太好了。我深知还有很多不足,希望大家能多提建议,还是想舔着脸皮,向屏幕前的大帅比们,大漂亮们,恳请一个小小的点赞,这会是对我莫大鼓励。也祝愿点赞的大帅比们,大漂亮们升职加薪走向人生巅峰!
0
0
0
浏览量2017
养乐多一瓶

每天3分钟,重学ES6-ES12(八)ES11 ES12新增内容

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是ES11 ES12中新增的内容ES11BigInt 大整数类型在早期的JavaScript中,我们不能正确的表示过大的数字: 大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的。那么ES11中,引入了新的数据类型BigInt,用于表示大的整数: BitInt的表示方法是在数值的后面加上n代码演示// ES11之前 max_safe_integer const maxInt = Number.MAX_SAFE_INTEGER console.log(maxInt) // 9007199254740991 console.log(maxInt + 1) // 9007199254740992 console.log(maxInt + 2) // 9007199254740992表示错误 // ES11之后: BigInt const bigInt = 900719925474099100n console.log(bigInt + 10n) // 900719925474099110n const num = 100 console.log(bigInt + BigInt(num)) // 900719925474099200n const smallNum = Number(bigInt) console.log(smallNum) // 900719925474099100 Nullish Coalescing Operator 空值合并操作符 ??空值合并操作符( ?? )是一个逻辑操作符,当左侧的操作数为 [null] 或者 [undefined] 时,返回其右侧操作数,否则返回左侧操作数。与[逻辑或操作符(||)]不同,逻辑或操作符会在左侧操作数为[假值]时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为。比如为假值(例如,'' 或 0)时。见下面的例子。代码演示// ES11: 空值合并运算 ?? const foo = undefined const bar1 = foo || "default value" const bar2 = foo ?? "defualt value" console.log(bar1,bar2) //"default value","default value" const foo1 = '' const bar3 = foo1 || "default value" const bar4 = foo1 ?? "defualt value" console.log(bar1,bar2) //"default value","" Optional Chaining 链式操作符可选链也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁:代码演示const info = { friend: { girlFriend: { name: "hmm" } } } //ES11 之前的判断方案 console.log(info.friend.girlFriend.name) if (info && info.friend && info.friend.girlFriend) { console.log(info.friend.girlFriend.name) } // ES11提供了可选链(Optional Chainling) console.log(info.friend?.girlFriend?.name) Global This全局对象在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的 比如在浏览器中可以通过this、window来获取; 比如在Node中我们需要通过global来获取;那么在ES11中对获取全局对象进行了统一的规范:globalThis代码演示// 获取某一个环境下的全局对象(Global Object) // 在浏览器下 // console.log(window) // console.log(this) // 在node下 // console.log(global) // ES11 console.log(globalThis) for..in标准化在ES11之前,虽然很多浏览器支持for...in来遍历对象类型,但是并没有被ECMA标准化。在ES11中,对其进行了标准化,for...in是用于遍历对象的key的:ES12FinalizationRegistryFinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调。 FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调 用一个清理回调。(清理回调有时被称为 finalizer ); 你可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;代码演示// ES12: FinalizationRegistry类 const finalRegistry = new FinalizationRegistry((value) => { console.log("注册在finalRegistry的对象, 某一个被销毁", value) }) let obj = { name: "why" } let info = { age: 18 } finalRegistry.register(obj, "obj") finalRegistry.register(info, "value") obj = null info = null WeakRef如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用:如果我们希望是一个弱引用的话,可以使用WeakRef代码演示// ES12: WeakRef类 // WeakRef.prototype.deref: // > 如果原对象没有销毁, 那么可以获取到原对象 // > 如果原对象已经销毁, 那么获取到的是undefined const finalRegistry = new FinalizationRegistry((value) => { console.log("注册在finalRegistry的对象, 某一个被销毁", value) }) let obj = { name: "why" } let info = new WeakRef(obj) finalRegistry.register(obj, "obj") obj = null setTimeout(() => { console.log(info.deref()?.name) console.log(info.deref() && info.deref().name) }, 10000) // 注册在finalRegistry的对象, 某一个被销毁 obj // undefined // undefined logical assignment operators 逻辑赋值操作符// 1.||= 逻辑或赋值运算 let message = "hello world" // es5 写法 message = message || "default value" // es12 简写 message ||= "default value" console.log(message) // 2.&&= 逻辑与赋值运算 let info = { name: "why" } // 1.判断info // 2.有值的情况下, 取出info.name // es5写法 info = info && info.name // es12写法 info &&= info.name console.log(info) // 3.??= 逻辑空赋值运算 let message = 0 message ??= "default value" console.log(message) 最后,这是我第一次参加更文活动,茫茫人海中,如果有幸遇到你,读到我这篇文章,那真是太好了。我深知还有很多不足,希望大家能多提建议,还是想舔着脸皮,向屏幕前的大帅比们,大漂亮们,恳请一个小小的点赞,这会是对我莫大鼓励。也祝愿点赞的大帅比们,大漂亮们升职加薪走向人生巅峰!
0
0
0
浏览量2013
养乐多一瓶

每天3分钟,重学ES6-ES12(十四)async/await

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们介绍了迭代器和生成器,今天继续介绍async 和 await异步函数 async functionasync关键字用于声明一个异步函数: async是asynchronous单词的缩写,异步、非同步; sync是synchronous单词的缩写,同步、同时;异步函数的执行流程异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行。异步函数有返回值时,和普通函数会有区别: 情况一:异步函数也可以有返回值,但是异步函数的返回值会被包裹到Promise.resolve中; 情况二:如果我们的异步函数的返回值是Promise,Promise.resolve的状态会由Promise决定; 情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定;如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;异步函数和普通函数的区别-返回值异步函数的返回值一定是一个Promiseasync function foo() { console.log("中间代码~") // 1.返回一个值 // promise then function exec: 1 return 1 // 2.返回thenable // promise then function exec: hahahah return { then: function(resolve, reject) { resolve("hahahah") } } // 3.返回Promise // promise then function exec: hehehehe return new Promise((resolve, reject) => { setTimeout(() => { resolve("hehehehe") }, 2000) }) } // 异步函数的返回值一定是一个Promise const promise = foo() promise.then(res => { console.log("promise then function exec:", res) }) 异步函数和普通函数的区别-异常异步函数中的异常会被作为promise的reject的值async function foo() { console.log("foo function start~") console.log("中间代码~") // 异步函数中的异常, 会被作为异步函数返回的Promise的reject值的 throw new Error("error message") console.log("foo function end~") } // 异步函数的返回值一定是一个Promise foo().catch(err => { console.log("err:", err) }) await关键字async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的。await关键字有什么特点呢? 通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise; 那么await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数;如果await后面是一个普通的值,那么会直接返回这个值;如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的 reject值;代码演示// 1.await 跟上表达式 function requestData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(222) // reject(1111) }, 2000); }) } async function foo() { const res1 = await requestData() console.log("后面的代码1", res1) console.log("后面的代码2") const res2 = await requestData() console.log("res2后面的代码", res2) } // 依次打印 // 后面的代码1 222 // 后面的代码2 // res2后面的代码 222 // 2.跟上其他的值 async function foo() { const res1 = await 123 // 返回值 res1: 123 const res1 = await { then: function(resolve, reject) { resolve("abc") } } // 返回值 res1: abc const res1 = await new Promise((resolve) => { resolve("why") }) // 返回值 res1: why console.log("res1:", res1) } // 3.reject值 async function foo() { const res1 = await requestData() console.log("res1:", res1) } foo().catch(err => { console.log("err:", err) })
0
0
0
浏览量2017
养乐多一瓶

每天3分钟,重学ES6-ES12(十五)异步代码处理方案

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们介绍了promise,生成器和迭代器,async await,现在我们总结一下针对异步代码处理方案业务场景请求一个接口,拿到返回值,对返回值进行处理,当作第二个接口的请求参数,拿到返回值处理,当作第三个接口的请求参数。最终数据需要调用依次调用3个接口才能拿到。1> url: why -> res: why2> url: res + "aaa" -> res: whyaaa3> url: res + "bbb" => res: whyaaabbbfunction requestData(url) { // 异步请求的代码会被放入到executor中 return new Promise((resolve, reject) => { // 模拟网络请求 setTimeout(() => { // 拿到请求的结果 resolve(url) }, 2000); }) } 方案一 回调函数优点:便于理解缺点:回调地狱,不能捕获错误ajax('url', () => { // callback 函数体 ajax('url', () => { // callback 函数体 ajax('url', () => { // callback 函数体 }) }) }) 方案二 事件监听f1.on('done', f2); function f1(){      setTimeout(function () {        // f1的任务代码       f1.trigger('done');      }, 1000);    } 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数;缺点:整个流程都要变成事件驱动型,运行流程会变得不清晰。方案三  发布订阅模式jQuery.subscribe("done", f2); function f1(){     setTimeout(function () {       // f1的任务代码       jQuery.publish("done");     }, 1000); } jQuery.unsubscribe("done", f2); 与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。方案四 Promise中then的返回值来解决回调问题requestData("why").then(res => { return requestData(res + "aaa") }).then(res => { return requestData(res + "bbb") }).then(res => { console.log(res) }) 优点:解决了层层回调问题,相对直观缺点:无法精确捕获到哪个promise错误,链式调用依然不如同步函数代码直观方案五 Promise + generator实现function* getData() { const res1 = yield requestData("why") const res2 = yield requestData(res1 + "aaa") const res3 = yield requestData(res2 + "bbb") const res4 = yield requestData(res3 + "ccc") console.log(res4) } // 1> 手动执行生成器函数 const generator = getData() generator.next().value.then(res => { generator.next(res).value.then(res => { generator.next(res).value.then(res => { generator.next(res) }) }) }) // 2> 自己封装了一个自动执行的函数 function execGenerator(genFn) { const generator = genFn() function exec(res) { const result = generator.next(res) if (result.done) { return result.value } result.value.then(res => { exec(res) }) } exec() execGenerator(getData) 优点:代码直观,* yield 可以重新组织代码,同步的方式执行异步代码缺点:无法执行并发请求,只能调用next()一步一步请求,* yield 对开发者不太友好,难以理解方案六 async/awaitasync function getData() { const res1 = await requestData("why") const res2 = await requestData(res1 + "aaa") const res3 = await requestData(res2 + "bbb") const res4 = await requestData(res3 + "ccc") console.log(res4) } getData() async 是generator的语法糖 内置执行器,无需手动执行next()方法*/yield => async/await优点:在generator 的基础上更加语义化,使用简单,无需执行next 方法缺点:无法执行并发请求,必须有try catch才能捕获到异常业务使用Promise + async/awaitasync/await是基于generator的语法糖,返回的也是一个promise,所以返回值可以调用promise的方法。当处理并发一般Promise.all + async/awit 结合使用async function getData(){ await Promise.all([requestData(a),requestData(b)]) } 总结async await意义在于‘期望异步代码和同步代码可以流畅混淆而不至于被 then 分割’。同步代码不多的情况,async await和promise的使用可以取决于个人喜好。async/await设计初衷并不是为了取代Promise,而是为了让使用Promise更加方便。JS异步的发展历程是callback->promise/generator->async/await这三种历程我认为并没有 相互优越的区别,而是有使用场景的区别注册事件必须是用回调,async await 可以梳理平常的业务代码 更容易理解拆分业务 ,而generator 适用于需要暂停的业务逻辑,promise 适用于 构建通用异步函数
0
0
0
浏览量2012
养乐多一瓶

每天3分钟,重学ES6-ES12(七)ES10 新增内容

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是ES10中新增的内容ES10flatflat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。相当于数组扁平化,接受一个参数,设置扁平化的层级代码演示// 1.flat的使用 const nums = [10, 20, [2, 9], [[30, 40], [10, 45]], 78, [55, 88]] const newNums = nums.flat() console.log(newNums) //[10, 20, 2, 9, [30,40], [10,45], 78, 55, 88] const newNums2 = nums.flat(2) console.log(newNums2) //[10, 20, 2, 9, 30, 40, 10, 45, 78, 55, 88] flatMapflatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。 注意一:flatMap是先进行map操作,再做flat的操作; 注意二:flatMap中的flat相当于深度为1;代码演示// 2.flatMap的使用 // 可以拆分字符串,把数组的中的短语拆成单词 const messages = ["Hello World", "hello lyh", "my name is coderwhy"] const words = messages.flatMap(item => { return item.split(" ") }) console.log(words) // ['Hello', 'World', 'hello', 'lyh', 'my', 'name', 'is', 'coderwhy'] Object fromEntries在前面,我们可以通过 Object.entries 将一个对象转换成 entries,那么如果我们有一个entries了,如何将其转换 成对象呢? ES10提供了 Object.formEntries来完成转换: 那么这个方法有什么应用场景呢?代码演示const obj = { name: "yz", age: 24, height: 1.88 } const entries = Object.entries(obj) console.log(entries) //[['name', 'yz'], ['age', 24],['height', 1.88]] const newObj = {} for (const entry of entries) { newObj[entry[0]] = entry[1] } console.log(newObj) //{name: 'yz', age: 24, height: 1.88} // 1.ES10中新增了Object.fromEntries方法 // 可以直接解析类对象数组 const newObj = Object.fromEntries(entries) console.log(newObj) //{name: 'yz', age: 24, height: 1.88} // 2.Object.fromEntries的应用场景 // 解析地址上的参数 const queryString = 'name=yz&age=24&height=1.88' const queryParams = new URLSearchParams(queryString) for (const param of queryParams) { console.log(param) } const paramObj = Object.fromEntries(queryParams) console.log(paramObj) ////{name: 'yz', age: 24, height: 1.88} trimStart trimEnd去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?代码演示const message = " Hello World " console.log(message.trim()) console.log(message.trimStart()) console.log(message.trimEnd()) 最后,这是我第一次参加更文活动,茫茫人海中,如果有幸遇到你,读到我这篇文章,那真是太好了。我深知还有很多不足,希望大家能多提建议,还是想舔着脸皮,向屏幕前的大帅比们,大漂亮们,恳请一个小小的点赞,这会是对我莫大鼓励。也祝愿点赞的大帅比们,大漂亮们升职加薪走向人生巅峰!
0
0
0
浏览量2012
养乐多一瓶

每天3分钟,重学ES6-ES12(十三)不常用但却常问的生成器函数

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们介绍了迭代器今天继续介绍生成器和生成器函数什么是生成器?生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。生成器函数也是一个函数,但是和普通的函数有一些区别: 首先,生成器函数需要在function的后面加一个符号: * 其次,生成器函数可以通过yield关键字来控制函数的执行流程: 最后,生成器函数的返回值是一个Generator(生成器):生成器事实上是一种特殊的迭代器; MDN:Instead, they return a special type of iterator, called a Generator.代码演示我们新建一个生成器函数foo,通过yield 分割。当执行一次next是,遇到yield 会自动中断,这样函数就可以通过我们在外部控制一步一步执行。//生成器函数 function* foo(){ console.log(1) const value1 =10 console.log(value1) const n = yield value1 //可以在中断的时候返回值 const value2 =20 * n console.log(value2) yield const value3 =30 console.log(value3) yield console.log(2) } const generator = foo() generator.next() generator.next(100) //可传入值 generator.next() 生成器函数执行过程生成器函数foo的执行体压根没有执行,它只是返回了一个生成器对象那么它执行函数中的东西呢?调用next即可;通过yield来返回结果;next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;生成器提前结束 – return函数 return传值后这个生成器函数就会结束,之后调用next不会继续生成值了;生成器抛出异常 – throw函数 抛出异常后我们可以在生成器函数中捕获异常; 但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;const value1 = 100 try { yield value1 } catch (error) { console.log("捕获到异常情况:", error) yield "abc" // 不会执行 } console.log("第二段代码继续执行") const value2 = 200 yield value2 生成器实现迭代器function* createArrayIterator(arr) { // 3.第三种写法 yield* yield* arr // 2.第二种写法 // for (const item of arr) { // yield item // } // 1.第一种写法 // yield "abc" // { done: false, value: "abc" } // yield "cba" // { done: false, value: "abc" } // yield "nba" // { done: false, value: "abc" } } const names = ["abc", "cba", "nba"] const namesIterator = createArrayIterator(names) console.log(namesIterator.next()) console.log(namesIterator.next()) console.log(namesIterator.next()) console.log(namesIterator.next()) 异步方案处理介绍了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。需求:function requestData(url) { // 异步请求的代码会被放入到executor中 return new Promise((resolve, reject) => { // 模拟网络请求 setTimeout(() => { // 拿到请求的结果 resolve(url) }, 2000); }) } function* getData() { const res1 = yield requestData("url") const res2 = yield requestData(res1 + "aaa") const res3 = yield requestData(res2 + "bbb") const res4 = yield requestData(res3 + "ccc") console.log(res4) } 但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?function* getData(){ const res1 = yield requestData("why") const res2 = yield requestData(res1+'bbb') const res3 = yield requestData(res2+'ccc') console.log(res3) } // 1> 手动执行生成器函数 const generator = getData() generator.next().value.then(res => { generator.next(res).value.then(res => { generator.next(res).value.then(res => { generator.next(res) }) }) }) // 2> 自己封装了一个自动执行的函数 function execGenerator(genFn) { const generator = genFn() function exec(res) { const result = generator.next(res) if (result.done) { return result.value } result.value.then(res => { exec(res) }) } exec() } execGenerator(getData) 这其实也就是我们ES6中提出的generator 生成器函数,也是为了解决promise 异步回调地狱的问题。同是也增加了代码量,不太语义化,因此后面又提出了async/await
0
0
0
浏览量2015
养乐多一瓶

一文搞清楚ES6新增数据结构 Symbol Map WeakMap Set WeakSet

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是新增的数据结构Symbol Map WeakMap Set WeakSetSymbolSymbol的基本使用Symbol是什么呢?Symbol是ES6中新增的一个基本数据类型,翻译为符号。那么为什么需要Symbol呢? 在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突; 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下, 很容易造成冲突,从而覆盖掉它内部的某个属性; 比如我们在写apply、call、bind实现时,我们有给其中添加一个fn属性,那么如果它内部原来已经有了fn属性了呢? 比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉;Symbol就是为了解决上面的问题,用来生成一个独一无二的值。 Symbol值是通过Symbol函数来生成的,生成后可以作为属性名; 也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值;Symbol即使多次创建值,它们也是不同的:Symbol函数执行后每次创建出来的值都是独一无二的;我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性;代码演示// 1.ES6之前, 对象的属性名(key) var obj = { name: "why", friend: { name: "kobe" }, age: 18 } // obj['newName'] = "james" // console.log(obj) // 2.ES6中Symbol的基本使用 const s1 = Symbol() const s2 = Symbol() console.log(s1 === s2) // false // ES2019(ES10)中, Symbol还有一个描述(description) const s3 = Symbol("aaa") console.log(s3.description) // aaa Symbol作为属性名我们通常会使用Symbol在对象中表示唯一的属性名注意: 不能通过.语法获取使用Symbol作为key的属性名,在遍历/Object.keys等中是获取不到这些Symbol值需要Object.getOwnPropertySymbols来获取所有Symbol的key代码演示// 3.Symbol值作为key // 3.1.在定义对象字面量时使用 const obj = { [s1]: "abc", [s2]: "cba" } // 3.2.新增属性 obj[s3] = "nba" // 3.3.Object.defineProperty方式 const s4 = Symbol() Object.defineProperty(obj, s4, { enumerable: true, configurable: true, writable: true, value: "mba" }) console.log(obj[s1], obj[s2], obj[s3], obj[s4]) // 注意: 不能通过.语法获取 // console.log(obj.s1) // 4.使用Symbol作为key的属性名,在遍历/Object.keys等中是获取不到这些Symbol值 // 需要Object.getOwnPropertySymbols来获取所有Symbol的key console.log(Object.keys(obj)) console.log(Object.getOwnPropertyNames(obj)) console.log(Object.getOwnPropertySymbols(obj)) const sKeys = Object.getOwnPropertySymbols(obj) for (const sKey of sKeys) { console.log(obj[sKey]) } 相同值的Symbol前面我们讲Symbol的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的Symbol应该怎么 来做呢? 我们可以使用Symbol.for方法来做到这一点; 并且我们可以通过Symbol.keyFor方法来获取对应的key;代码演示const sa = Symbol.for("aaa") const sb = Symbol.for("aaa") console.log(sa === sb) // true const key = Symbol.keyFor(sa) console.log(key) // aaa const sc = Symbol.for(key) console.log(sa === sc) // true SetSet的基本使用在ES6之前,我们存储数据的结构主要有两种:数组、对象。 在ES6中新增了另外两种数据结构:Set、Map,以及它们的另外形式WeakSet、WeakMap。Set是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是元素不能重复。 创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式):我们可以发现Set中存放的元素是不会重复的,那么Set有一个非常常用的功能就是给数组去重。代码演示// 1.创建Set结构 const set = new Set() set.add(10) set.add(20) set.add(40) set.add(333) set.add(10) console.log(set) //// 10, 20, 40, 333 // 3.对数组去重(去除重复的元素) const arr = [33, 10, 26, 30, 33, 26] const newArr = [] for (const item of arr) { if (newArr.indexOf(item) !== -1) { newArr.push(item) } } const arrSet = new Set(arr) const newArr = Array.from(arrSet) const newArr = [...arrSet] console.log(newArr) Set的常见方法Set常见的属性: size:返回Set中元素的个数;Set常用的方法: add(value):添加某个元素,返回Set对象本身; delete(value):从set中删除和这个值相等的元素,返回boolean类型; has(value):判断set中是否存在某个元素,返回boolean类型; clear():清空set中所有的元素,没有返回值; forEach(callback, [, thisArg]):通过forEach遍历set;另外Set是支持for of的遍历的。代码演示const arrSet = new Set() // size属性 console.log(arrSet.size) // Set的方法 // add arrSet.add(100) console.log(arrSet) // delete arrSet.delete(33) console.log(arrSet) // has console.log(arrSet.has(100)) // clear arrSet.clear() console.log(arrSet) // 6.对Set进行遍历 arrSet.forEach(item => { console.log(item) }) for (const item of arrSet) { console.log(item) } WeakSetWeakSet的基本使用和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。那么和Set有什么区别呢? 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型; 区别二:WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么垃圾回收机制可以对该对象进行回收;代码演示const weakSet = new WeakSet() // 1.区别一: 只能存放对象类型 // TypeError: Invalid value used in weak set // weakSet.add(10) // 强引用和弱引用的概念(看图) // 2.区别二: 对对象是一个弱引用 let obj = { name: "why" } weakSet.add(obj) const set = new Set() // 建立的是强引用 set.add(obj) // 建立的是弱引用 weakSet.add(obj) WeakSet常见方法add(value):添加某个元素,返回WeakSet对象本身;delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型;has(value):判断WeakSet中是否存在某个元素,返回boolean类型;WeakSet的应用注意:WeakSet不能遍历 因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。 所以存储到WeakSet中的对象是没办法获取的;那么这个东西有什么用呢? 事实上这个问题并不好回答,我们来使用一个Stack Overflow上的答案;代码演示不能通过非构造方法创建出来的对象 调用构造函数的方法const personSet = new WeakSet() class Person { constructor() { personSet.add(this) } running() { if (!personSet.has(this)) { throw new Error("不能通过非构造方法创建出来的对象调用running方法") } console.log("running~", this) } } let p = new Person() p.running() p = null p.running.call({name: "why"}) MapMap的基本使用另外一个新增的数据结构是Map,用于存储映射关系。 但是我们可能会想,在之前我们可以使用对象来存储映射关系,他们有什么区别呢? 事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;那么我们就可以使用Map:代码使用// 1.JavaScript中对象中是不能使用对象来作为key的 const obj1 = { name: "why" } const obj2 = { name: "kobe" } const info = { [obj1]: "aaa", [obj2]: "bbb" } console.log(info) // 2.Map就是允许我们对象类型来作为key的 // 构造方法的使用 const map = new Map() map.set(obj1, "aaa") map.set(obj2, "bbb") map.set(1, "ccc") console.log(map) const map2 = new Map([[obj1, "aaa"], [obj2, "bbb"], [2, "ddd"]]) console.log(map2) // Map(3) {{…} => 'aaa', {…} => 'bbb', 2 => 'ddd'} Map的常见方法Map常见的属性: size:返回Map中元素的个数;Map常见的方法: set(key, value):在Map中添加key、value,并且返回整个Map对象; get(key):根据key获取Map中的value; has(key):判断是否包括某一个key,返回Boolean类型; delete(key):根据key删除一个键值对,返回Boolean类型; clear():清空所有的元素; forEach(callback, [, thisArg]):通过forEach遍历Map;Map也可以通过for of进行遍历。代码演示// 常见的属性和方法 console.log(map2.size) // set map2.set("why", "eee") console.log(map2) // get(key) console.log(map2.get("why")) // has(key) console.log(map2.has("why")) // delete(key) map2.delete("why") console.log(map2) // clear // map2.clear() // console.log(map2) // 遍历map map2.forEach((item, key) => { console.log(item, key) }) for (const item of map2) { console.log(item[0], item[1]) } for (const [key, value] of map2) { console.log(key, value) } WeakMapWeakMap的使用和Map类型的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。那么和Map有什么区别呢? 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key; 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;WeakMap常见的方法有四个: set(key, value):在Map中添加key、value,并且返回整个Map对象; get(key):根据key获取Map中的value; has(key):判断是否包括某一个key,返回Boolean类型; delete(key):根据key删除一个键值对,返回Boolean类型;代码演示const obj = {name: "obj1"} // 1.WeakMap和Map的区别二: const map = new Map() map.set(obj, "aaa") const weakMap = new WeakMap() weakMap.set(obj, "aaa") // 2.区别一: 不能使用基本数据类型 // weakMap.set(1, "ccc") // 3.常见方法 // get方法 console.log(weakMap.get(obj)) // has方法 console.log(weakMap.has(obj)) // delete方法 console.log(weakMap.delete(obj)) // WeakMap { <items unknown> } console.log(weakMap) WeakMap的应用注意:WeakMap也是不能遍历的 因为没有forEach方法,也不支持通过for of的方式进行遍历;那么我们的WeakMap有什么作用呢?代码演示可以做对象的依赖收集先把对象的属性和属性对应的依赖,存储为Map结构,一个key 对应一组收集的函数依赖然后把对象 和Map 结构存储为weakMap当代理的对象有变化时,我们去weakMap 取key,再执行依赖函数集// 应用场景(vue3响应式原理) const obj1 = { name: "why", age: 18 } function obj1NameFn1() { console.log("obj1NameFn1被执行") } function obj1NameFn2() { console.log("obj1NameFn2被执行") } function obj1AgeFn1() { console.log("obj1AgeFn1") } function obj1AgeFn2() { console.log("obj1AgeFn2") } // 1.创建WeakMap const weakMap = new WeakMap() // 2.收集依赖结构 // 2.1.对obj1收集的数据结构 const obj1Map = new Map() obj1Map.set("name", [obj1NameFn1, obj1NameFn2]) obj1Map.set("age", [obj1AgeFn1, obj1AgeFn2]) weakMap.set(obj1, obj1Map) // 2.2如果obj1.name发生了改变 // Proxy/Object.defineProperty obj1.name = "james" const targetMap = weakMap.get(obj1) const fns = targetMap.get("name") fns.forEach(item => item()) 最后,这是我第一次参加更文活动,茫茫人海中,如果有幸遇到你,读到我这篇文章,那真是太好了。我深知还有很多不足,希望大家能多提建议,还是想舔着脸皮,向屏幕前的大帅比们,大漂亮们,恳请一个小小的点赞,这会是对我莫大鼓励。也祝愿点赞的大帅比们,大漂亮们升职加薪走向人生巅峰!
0
0
0
浏览量2017
养乐多一瓶

每天3分钟,重学ES6-ES12(十八)ES Module

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们介绍了模块化的历史,今天介绍模块化处理方案ES ModuleJavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等, 所以在ES推出自己的模块化系统时,大家也是兴奋异常。ES Module和CommonJS的模块化有一些不同之处: 一方面它使用了import和export关键字; 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;ES Module模块采用export和import关键字来实现模块化: export负责将模块内的内容导出; import负责从其他模块导入内容;采用ES Module将自动采用严格模式:use strictexports关键字export关键字将一个模块中的变量、函数、类等导出;我们希望将其他中内容全部导出,它可以有如下的方式: 方式一:在语句声明的前面直接加上export关键字 方式二:将所有需要导出的标识符,放到export后面的 {}中 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的; 所以: export {name: name},是错误的写法;方式三:导出时给标识符起一个别名代码演示// 1.第一种方式: export 声明语句 export const name = "why" export const age = 18 export function foo() { console.log("foo function") } export class Person { } // 2.第二种方式: export 导出 和 声明分开 const name = "why" const age = 18 function foo() { console.log("foo function") } export { name, age, foo } // 3.第三种方式: 第二种导出时起别名 export { name as fName, age as fAge, foo as fFoo } import关键字import关键字负责从另外一个模块中导入内容导入内容的方式也有多种: 方式一:import {标识符列表} from '模块'; 注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容; 方式二:导入时给标识符起别名 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上代码演示// 1.导入方式一: 普通的导入 import { name, age, foo } from "./foo.js" import { fName, fAge, fFoo } from './foo.js' // 2.导入方式二: 起别名 import { name as fName, age as fAge, foo as fFoo } from './foo.js' // 3.导入方式三: 将导出的所有内容放到一个标识符中 import * as foo from './foo.js' console.log(foo.name) console.log(foo.age) foo.foo() const name = "main" console.log(name) console.log(age) export和import结合使用补充:export和import可以结合使用为什么要这样做呢? 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中; 这样方便指定统一的接口规范,也方便阅读; 这个时候,我们就可以使用export和import结合使用;代码演示import { add, sub } from './math.js' import { timeFormat, priceFormat } from './format.js' export { add, sub, timeFormat, priceFormat } default用法前面我们学习的导出功能都是有名字的导出(named exports): 在导出export时指定了名字; 在导入import时需要知道具体的名字;还有一种导出叫做默认导出(default export) 默认导出export时可以不需要指定名字; 在导入时不需要使用 {},并且可以自己来指定名字; 它也方便我们和现有的CommonJS等规范相互操作;注意:在一个模块中,只能有一个默认导出(default export);代码演示const name = "why" const age = 18 const foo = "foo value" // 1.默认导出的方式一: export { // named export name, // age as default, // foo as default } // 2.默认导出的方式二: 常见 export default foo // 注意: 默认导出只能有一个 import函数通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:为什么会出现这个情况呢? 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系; 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况; 甚至下面的这种写法也是错误的:因为我们必须到运行时能确定path的值;但是某些情况下,我们确确实实希望动态的来加载某一个模块: 如果根据不懂的条件,动态来选择加载模块的路径; 这个时候我们需要使用 import() 函数来动态加载;import metaimport.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。代码演示// import { name, age, foo } from './foo.js' // console.log(name) // import函数返回的结果是一个Promise import("./foo.js").then(res => { console.log("res:", res.name) }) console.log("后续的代码都是不会运行的~") // ES11新增的特性 // meta属性本身也是一个对象: { url: "当前模块所在的路径" } console.log(import.meta) ES Module的解析流程ES Module的解析过程可以划分为三个阶段: 阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record); 阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向 对应的内存地址。 阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中;代码演示// foo.js let name = "why" let age = 18 setTimeout(() => { name = "kobe" age = 40 }, 100) export { name, age } // main.js import { name, age } from './foo.js' console.log(name, age) setTimeout(() => { console.log(name, age) }, 2000) // index.html <html lang="en"><head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script src="./main.js" type="module"></script> </body></html> 执行结果为:why 18
0
0
0
浏览量2013
养乐多一瓶

每天3分钟,重学ES6-ES12(十一)Promise的类方法

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们学习的then、catch、finally方法都属于Promise的实例方法,都是存放在Promise的prototype上的。 下面我们再来学习一下Promise的类方法。resolve方法有时候我们已经有一个现成的内容了,希望将其转成Promise来使用,这个时候我们可以使用 Promise.resolve 方法来完成。Promise.resolve的用法相当于new Promise,并且执行resolve操作: Promise.resolve('a')等价于new Promise((resolve)=>resolve('a'))resolve参数的形态:reject方法reject方法类似于resolve方法,只是会将Promise对象的状态设置为reject状态。Promise.reject的用法相当于new Promise,只是会调用reject:Promise.reject('a')等价于new Promise((resolve,rejext)=>reject('a'))Promise.reject传入的参数无论是什么形态,都会直接作为reject状态的参数传递到catch的。all方法它的作用是将多个Promise包裹在一起形成一个新的Promise;新的Promise状态由包裹的所有Promise共同决定: 当所有的Promise状态变成fulfilled状态时,新的Promise状态为fulfilled,并且会将所有Promise的返回值 组成一个数组; 当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数;allSettled方法all方法有一个缺陷:当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。 那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的;在ES11(ES2020)中,添加了新的API Promise.allSettled: 该方法会在所有的Promise都有结果(settled),无论是fulfilled,还是reject时,才会有最终的状态; 并且这个Promise的结果一定是fulfilled的;allSettled的结果是一个数组,数组中存放着每一个Promise的结果,并且是对应一个对象的; 这个对象中包含status状态,以及对应的value值;代码演示// 创建多个Promise const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(11111) }, 1000); }) const p2 = new Promise((resolve, reject) => { setTimeout(() => { reject(22222) }, 2000); }) const p3 = new Promise((resolve, reject) => { setTimeout(() => { resolve(33333) }, 3000); }) // allSettled Promise.allSettled([p1, p2, p3]).then(res => { console.log(res) }).catch(err => { console.log(err) }) //[{status: 'fulfilled', value: 11111},  //{status: 'rejected', reason: 22222}, //{status: 'fulfilled', value: 33333}] race方法如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race方法: race是竞技、竞赛的意思,表示多个Promise相互竞争,谁先有结果,那么就使用谁的结果;any方法any方法是ES12中新增的方法,和race方法是类似的: any方法会等到一个fulfilled状态,才会决定新Promise的状态; 如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态; 如果所有的Promise都是reject的,那么会报一个AggregateError的错误。总结Promise在面试中,属于经常会问的知识,但是很少让手写promise的原理,因为时间不允许,但是手写promise源码是最能考察基本功的,所以大多是面试题都变成了,考察promsie的类方法,手写一个类方法的实现。之后有精力也会和大家分享类方法的实现。
0
0
0
浏览量2014
养乐多一瓶

每天3分钟,重学ES6-ES12(九)Promise简单介绍

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是ES6中新增的内容Promise异步任务的处理在ES6出来之后,有很多关于Promise的讲解、文章,也有很多经典的书籍讲解Promise 虽然等你学会Promise之后,会觉得Promise不过如此,但是在初次接触的时候都会觉得这个东西不好理解;那么这里我从一个实际的例子来作为切入点: 我们调用一个函数,这个函数中发送网络请求(我们可以用定时器来模拟); 如果发送网络请求成功了,那么告知调用者发送成功,并且将相关数据返回过去; 如果发送网络请求失败了,那么告知调用者发送失败,并且告知错误信息;代码演示/**  * 这种回调的方式有很多的弊端:  *  1> 如果是我们自己封装的requestData,那么我们在封装的时候必须要自己设计好callback名称, 并且使用好  *  2> 如果我们使用的是别人封装的requestData或者一些第三方库, 那么我们必须去看别人的源码或者文档, 才知道它这个函数需要怎么去获取到结果  */ // request.js function requestData(url, successCallback, failtureCallback) { // 模拟网络请求 setTimeout(() => { // 拿到请求的结果 // url传入的是yz, 请求成功 if (url === "yz") { // 成功 let names = ["abc", "cba", "nba"] successCallback(names) } else { // 否则请求失败 // 失败 let errMessage = "请求失败, url错误" failtureCallback(errMessage) } }, 3000); } // main.js requestData("kobe", (res) => { console.log(res) }, (err) => { console.log(err) }) 什么是Promise呢?在上面的解决方案中,我们确确实实可以解决请求函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题: 第一,我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等; 第二,对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以便可以理解它这个函数到底怎么用;我们来看一下Promise的API是怎么样的: Promise是一个类,可以翻译成 承诺、许诺 、期约; 当我们需要给予调用者一个承诺:待会儿我会给你回调数据时,就可以创建一个Promise的对象; 在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor; 这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject; 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数; 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数;Promsise 代码结构我们来看一下Promise代码结构:下面Promise使用过程,我们可以将它划分成三个状态:待定(pending): 初始状态,既没有被兑现,也没有被拒绝; 当执行executor中的代码时,处于该状态;已兑现(fulfilled): 意味着操作成功完成; 执行了resolve时,处于该状态;已拒绝(rejected): 意味着操作失败; 执行了reject时,处于该状态;function foo() {   // Promise   return new Promise((resolve, reject) => {     resolve("success message")     // reject("failture message")   }) } const fooPromise = foo() // then方法传入的回调函数两个回调函数: // > 第一个回调函数, 会在Promise执行resolve函数时, 被回调 // > 第二个回调函数, 会在Promise执行reject函数时, 被回调 fooPromise.then((res) => {   console.log(res) }, (err) => {   console.log(err) }) //catch方法传入的回调函数, 会在Promise执行reject函数时, 被回调 fooPromise.catch(() => { }) // 传入的这个函数, 被称之为 executor // > resolve: 回调函数, 在成功时, 回调resolve函数 // > reject: 回调函数, 在失败时, 回调reject函数 const promise = new Promise((resolve, reject) => { console.log("promise传入的函数被执行了")   // resolve() reject() }) promise.then(() => { }) promise.catch(() => { }) Promise重构请求那么有了Promise,我们就可以将之前的代码进行重构了:// request.js function requestData(url,) { // 异步请求的代码会被放入到executor中 return new Promise((resolve, reject) => { // 模拟网络请求 setTimeout(() => { // 拿到请求的结果 // url传入的是yz, 请求成功 if (url === "yz") { // 成功 let names = ["abc", "cba", "nba"] resolve(names) } else { // 否则请求失败 // 失败 let errMessage = "请求失败, url错误" reject(errMessage) } }, 3000); }) } // main.js const promise = requestData("coderwhy") promise.then((res) => { console.log("请求成功:", res) }, (err) => { console.log("请求失败:", err) }) 后续后面会继续介绍 promise的三种状态,promise的方法,手写promise,敬请期待。最近略忙,日更活动还有10篇任务,等忙完这段时间一定好好写博客。就不求赞了!
0
0
0
浏览量2010
养乐多一瓶

每天3分钟,重学ES6-ES12(十七)模块化历史

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们介绍了异步代码处理方案,今天介绍模块化的历史什么是模块化到底什么是模块化、模块化开发呢? 事实上模块化开发最终的目的是将程序划分成一个个小的结构; 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构; 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用; 也可以通过某种方式,导入另外结构中的变量、函数、对象等;上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程;无论你多么喜欢JavaScript,以及它现在发展的有多好,它都有很多的缺陷: 比如var定义的变量作用域问题; 比如JavaScript的面向对象并不能像常规面向对象语言一样使用class; 比如JavaScript没有模块化的问题;Brendan Eich本人也多次承认过JavaScript设计之初的缺陷,但是随着JavaScript的发展以及标准化,存在的缺陷问题基 本都得到了完善。无论是web、移动端、小程序端、服务器端、桌面应用都被广泛的使用;模块化的历史在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的: 这个时候我们只需要讲JavaScript代码写到<script>标签中即可; 并没有必要放到多个文件中来编写;甚至流行:通常来说JavaScript 程序的长度只有一行。但是随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了: ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过JavaScript进行前端页面的渲染; SPA的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过JavaScript来实现; *包括Node的实现,JavaScript编写复杂的后端程序,没有模块化是致命的硬伤;所以,模块化已经是JavaScript一个非常迫切的需求: 但是JavaScript本身,直到ES6(2015)才推出了自己的模块化方案; 在此之前,为了让JavaScript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、CommonJS等;之后的内容,我将详细介绍JavaScript的模块化,尤其是CommonJS和ES6的模块化。没有模块化带来的问题早期没有模块化带来了很多的问题:比如命名冲突的问题当然,我们有办法可以解决上面的问题:立即函数调用表达式(IIFE) IIFE (Immediately Invoked Function Expression)但是,我们其实带来了新的问题: 第一,我必须记每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用; 第二,代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写; 第三,在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的。 我们需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码; 这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性; JavaScript社区为了解决上面的问题,涌现出一系列好用的规范,接下来我们就学习具有代表性的一些规范。代码演示// a.js var moduleA = (function() { var name = "why" var age = 18 var isFlag = true return { name: name, isFlag: isFlag } })() // b.js (function() { if (moduleA.isFlag) { console.log("我的名字是" + moduleA.name) } })()
0
0
0
浏览量2015
养乐多一瓶

每天3分钟,重学ES6-ES12(十六)错误异常处理方案

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们介绍了异步代码处理方案,今天介绍js中错误异常处理方案错误处理方案开发中我们会封装一些工具函数,封装之后给别人使用: 在其他人使用的过程中,可能会传递一些参数; 对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果;很多时候我们可能验证到不是希望得到的参数时,就会直接return: 但是return存在很大的弊端:调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined; 事实上,正确的做法应该是如果没有通过某些验证,那么应该让外界知道函数内部报错了;如何可以让一个函数告知外界自己内部出现了错误呢? 通过throw关键字,抛出一个异常;throw语句: throw语句用于抛出一个用户自定义的异常; 当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行);如果我们执行代码,就会报错,拿到错误信息的时候我们可以及时的去修正代码。代码演示/** * 如果我们有一个函数, 在调用这个函数时, 如果出现了错误, 那么我们应该是去修复这个错误. */ function sum(num1, num2) { // 当传入的参数的类型不正确时, 应该告知调用者一个错误 if (typeof num1 !== "number" || typeof num2 !== "number") { // return undefined throw "parameters is error type~" } return num1 + num2 } // 调用者(如果没有对错误进行处理, 那么程序会直接终止) // console.log(sum({ name: "why" }, true)) console.log(sum(20, 30)) console.log("后续的代码会继续运行~") throw关键字throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息:throw关键字可以跟上哪些类型呢? 基本数据类型:比如number、string、Boolean 对象类型:对象类型可以包含更多的信息但是每次写这么长的对象又有点麻烦,所以我们可以创建一个类:class HYError { constructor(errorCode, errorMessage) { this.errorCode = errorCode this.errorMessage = errorMessage } } Error类型事实上,JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象:Error包含三个属性: messsage:创建Error对象时传入的message; name:Error的名称,通常和类的名称一致; stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;Error有一些自己的子类: RangeError:下标值越界时使用的错误类型; SyntaxError:解析语法错误时使用的错误类型; TypeError:出现类型错误时,使用的错误类型;function foo(type) { console.log("foo函数开始执行") if (type === 0) { // 1.抛出一个字符串类型(基本的数据类型) // throw "error" // 2.比较常见的是抛出一个对象类型 // throw { errorCode: -1001, errorMessage: "type不能为0~" } // 3.创建类, 并且创建这个类对应的对象 // throw new HYError(-1001, "type不能为0~") // 4.提供了一个Error // const err = new Error("type不能为0") // err.name = "why" // err.stack = "aaaa" // 5.Error的子类 const err = new TypeError("当前type类型是错误的~") throw err // 强调: 如果函数中已经抛出了异常, 那么后续的代码都不会继续执行了 console.log("foo函数后续的代码") } console.log("foo函数结束执行") } foo(0) console.log("后续的代码继续执行~") 异常的处理一个函数抛出了异常,调用它的时候程序会被强制终止: 这是因为如果我们在调用一个函数时,这个函数抛出了异常,但是我们并没有对这个异常进行处理,那么这个异常会继续传 递到上一个函数调用中; 而如果到了最顶层(全局)的代码中依然没有对这个异常的处理代码,这个时候就会报错并且终止程序的运行;我们先来看一下这段代码的异常传递过程: foo函数在被执行时会抛出异常,也就是我们的bar函数会拿到这个异常; 但是bar函数并没有对这个异常进行处理,那么这个异常就会被继续传递到调用bar函数的函数,也就是test函数; 但是test函数依然没有处理,就会继续传递到我们的全局代码逻辑中; 依然没有被处理,这个时候程序会终止执行,后续代码都不会再执行了;function foo(){ throw "123" } function bar(){ foo() } function test(){ bar() } test() console.log('后续代码') 异常的捕获但是很多情况下当出现异常时,我们并不希望程序直接退出,而是希望可以正确的处理异常:这个时候我们就可以使用try catch在ES10(ES2019)中,catch后面绑定的error可以省略。当然,如果有一些必须要执行的代码,我们可以使用finally来执行: finally表示最终一定会被执行的代码结构; 注意:如果try和finally中都有返回值,那么会使用finally当中的返回值;代码演示function foo(){ throw "123" } function bar(){ foo() } function test() { try { bar() } catch (error) { console.log("error:", error) } } test() console.log('后续代码')
0
0
0
浏览量2013
养乐多一瓶

每天3分钟,重学ES6-ES12系列文章汇总

规划未来的一周(当然也可能是两周,也可能是一个月),我准备了以下题目。每篇严格控制字数,写长了也没人看,写短了我还能省点力气。重新学习ES6ES6是ECMAScript 6的缩写简称,这个好理解。顾名思义,它是ECMAScript的第6个版本,也就是说它有更早的版本,以后还会有更多版本。ES6也可以说是一个泛指,指5.1版本以后的JavaScript的下一代标准,涵盖了ES2015,ES2016,ES2017等;亦指下一代JavaScript语言。为什么学习ES6嗯~ES6的语法相信大家都烂熟于心,已经在开发中日常使用我知道屏幕前的大漂亮,大帅比肯定都会了。但是我还是得写,原因你懂的,请看文章第一句。如果你已经会了,可以点开不看。如果你还不会,点开请先点赞。每天一首打油诗三十而立已到中年,技术平平且不善言辞 每逢面试十面九散,未来何期也诚惶诚恐 数十余年人在漂泊,碌碌无为还不知不觉 二零一九新冠肺炎,方然顿悟欲难欲要抗 便是一生无功与名,愿为妻儿换一世太平嗯嗯,就到这了,戛然而止,因为字数够了。
0
0
0
浏览量2017
养乐多一瓶

每天3分钟,重学ES6-ES12(十九)Proxy-Reflect

每天3分钟,重学ES6-ES12文章汇总监听对象的操作我们先来看一个需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程 通过我们前面所学的知识,能不能做到这一点呢? 其实是可以的,我们可以通过之前的属性描述符中的存储属性描述符来做到;这段代码就利用了前面讲过的 Object.defineProperty 的存储属性描述符来对 属性的操作进行监听。const obj = { name: "why", age: 18 } Object.defineProperty(obj, "name", { get: function() { console.log("监听到obj对象的name属性被访问了") }, set: function() { console.log("监听到obj对象的name属性被设置值") } }) 但是这样做有什么缺点呢?Proxy基本使用在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的: 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy对象); 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作;我们可以将上面的案例用Proxy来实现一次: 首先,我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler; const p = new Proxy(target, handler) 其次,我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;Proxy的set和get捕获器如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap):set和get分别对应的是函数类型; set函数有四个参数: target:目标对象(侦听的对象); property:将被设置的属性key; value:新属性值; receiver:调用的代理对象; get函数有三个参数: target:目标对象(侦听的对象); property:被获取的属性key; receiver:调用的代理对象;代码展示const obj = { name: "why", age: 18 } const objProxy = new Proxy(obj, { // 获取值时的捕获器 get: function(target, key) { console.log(`监听到对象的${key}属性被访问了`, target) return target[key] }, // 设置值时的捕获器 set: function(target, key, newValue) { console.log(`监听到对象的${key}属性被设置值`, target) target[key] = newValue } }) Proxy所有捕获器13个捕捉器分别是做什么的呢?handler.getPrototypeOf() Object.getPrototypeOf 方法的捕捉器。 handler.setPrototypeOf() Object.setPrototypeOf 方法的捕捉器。 handler.isExtensible() Object.isExtensible 方法的捕捉器。 handler.preventExtensions() Object.preventExtensions 方法的捕捉器。 handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor 方法的捕捉器。 handler.defineProperty() Object.defineProperty 方法的捕捉器。 handler.ownKeys() Object.getOwnPropertyNames 方法和Object.getOwnPropertySymbols 方法的捕捉器。 handler.has() in 操作符的捕捉器。 handler.get() 属性读取操作的捕捉器。 handler.set() 属性设置操作的捕捉器。 handler.deleteProperty() delete 操作符的捕捉器。 handler.apply() 函数调用操作的捕捉器。 handler.construct() new 操作符的捕捉器。 Proxy的construct和apply当然,我们还会看到捕捉器中还有construct和apply,它们是应用于函数对象的:function foo() { } const fooProxy = new Proxy(foo, { apply: function(target, thisArg, argArray) { console.log("对foo函数进行了apply调用") return target.apply(thisArg, argArray) }, construct: function(target, argArray, newTarget) { console.log("对foo函数进行了new调用") return new target(...argArray) } }) fooProxy.apply({}, ["abc", "cba"]) new fooProxy("abc", "cba") Reflect的作用Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射。那么这个Reflect有什么用呢? 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法; 比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf(); 比如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty() ;如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢? 这是因为在早期的ECMA规范中没有考虑到这种对 对象本身 的操作如何设计会更加规范,所以将这些API放到了Object上面; 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适; 另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的; 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上;那么Object和Reflect对象之间的API关系,可以参考MDN文档: developer.mozilla.org/zh-CN/docs/…Reflect的常见方法Reflect中有哪些常见的方法呢?它和Proxy是一一对应的,也是13个:Reflect.getPrototypeOf(target) 类似于 Object.getPrototypeOf() Reflect.setPrototypeOf(target, prototype) 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返 回true。 Reflect.isExtensible(target) 类似于 Object.isExtensible() Reflect.preventExtensions(target) 类似于 Object.preventExtensions()。返回一个Boolean。 Reflect.getOwnPropertyDescriptor(target, propertyKey) 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined. Reflect.defineProperty(target, propertyKey, attributes) 和 Object.defineProperty() 类似。如果设置成功就会返回 true Reflect.ownKeys(target) 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(), 但不会受enumerable影响). Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。 Reflect.get(target, propertyKey[, receiver]) 获取对象身上某个属性的值,类似于 target[name]。 Reflect.set(target, propertyKey, value[, receiver]) 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。 Reflect.deleteProperty(target, propertyKey) 作为函数的delete操作符,相当于执行 delete target[name]。 Reflect.apply(target, thisArgument, argumentsList) 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。 Reflect.construct(target, argumentsList[, newTarget]) 对构造函数进行 new 操作,相当于执行 new target(...args)。 Reflect的使用那么我们可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:const obj = { name: "why", age: 18 } const objProxy = new Proxy(obj, { get: function(target, key, receiver) { console.log("get---------") return Reflect.get(target, key) }, set: function(target, key, newValue, receiver) { console.log("set---------") target[key] = newValue const result = Reflect.set(target, key, newValue) if (result) { } else { } } }) objProxy.name = "kobe" console.log(objProxy.name) Receiver的作用我们发现在使用getter、setter的时候有一个receiver的参数,它的作用是什么呢?
0
0
0
浏览量2036
养乐多一瓶

每天3分钟,重学ES6-ES12(六)ES7 ES8 新增内容

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是ES7 ES8中新增的内容ES7 新增Array Includes在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,根据情况,如果包含则返回 true, 否则返回false。代码演示const names = ["abc", "cba", "nba", "mba", NaN] if (names.indexOf("cba") !== -1) { console.log("包含abc元素") } // ES7 ES2016 if (names.includes("cba", 2)) { console.log("包含abc元素") } if (names.indexOf(NaN) !== -1) { console.log("包含NaN") } if (names.includes(NaN)) { console.log("包含NaN") } 指数(乘方) exponentiation运算符在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。在ES7中,增加了 ** 运算符,可以对数字来计算乘方。代码演示const result1 = Math.pow(3, 3) // ES7: ** const result2 = 3 ** 3 console.log(result1, result2) ES8 新增Object values之前我们可以通过 Object.keys 获取一个对象所有的key,在ES8中提供了Object.values 来获取所有的value值:代码演示const obj = { name: "yz", age: 24 } console.log(Object.keys(obj)) // ['name', 'age'] console.log(Object.values(obj)) // ['yz', 24] // 也可以用于字符串 数组 // 用的非常少 console.log(Object.values(["abc", "cba", "nba"])) // ['abc', 'cba', 'nba'] console.log(Object.values("abc")) // ['a', 'b', 'c'] Object entries通过Object.entries可以获取到一个数组,数组中会存放可枚举属性的键值对数组。代码演示const obj = { name: "yz", age: 18 } console.log(Object.entries(obj)) const objEntries = Object.entries(obj) // [['name':'yz'],['age',18]] objEntries.forEach(item => { console.log(item[0], item[1]) }) // name yz // yz 18 console.log(Object.entries(["abc", "cba", "nba"])) // [['0':'abc'],['1','cba'],['2','nba']] console.log(Object.entries("abc")) // [['0':'a'],['1':'b'],['2':'c']] String Padding字符串填充某些字符串我们需要对其进行前后的填充,来实现某种格式化效果ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。padStart()和padStart()一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。如果省略第二个参数,默认使用空格补全长度。我们简单具一个应用场景:比如需要对身份证、银行卡的前面位数进行隐藏代码演示const message = "Hello World" const newMessage = message.padStart(15, "*").padEnd(20, "-") console.log(newMessage) // ****Hello World----- // 案例 const cardNumber = "321324234242342342341312" const lastFourCard = cardNumber.slice(-4) const finalCard = lastFourCard.padStart(cardNumber.length, "*") console.log(finalCard) // ********************1312 Trailing Commas 尾逗号在ES8中,我们允许在函数定义和调用时多加一个逗号:个人感觉↔作用不大,用到的不多function foo(m, n,) { } foo(20, 30,) Object DescriptorsES5 有一个Object.getOwnPropertyDescriptor方法,返回某个对象属性的描述对象( descriptor )ES8 增加了另一个对对象的操作是 Object.getOwnPropertyDescriptorsObject.getOwnPropertyDescriptors(data) 获取objcet对象所有数据的描述符Object.getOwnPropertyDescriptor(data,“key”) 获取单个数据的描述符代码演示// ES5 var obj = { name: 'yz',age:18 }; Object.getOwnPropertyDescriptor(obj, 'name') // {value: 'yz', writable: true, enumerable: true, configurable: true} Object.getOwnPropertyDescriptors(obj) //{ // age: {value: 18, writable: true, enumerable: true, configurable: true} // name: {value: 'yz', writable: true, enumerable: true, configurable: true} //} 该方法允许对一个属性的描述进行检索。在 Javascript 中, 属性 由一个字符串类型的“名字”(name)和一个“属性描述符”(property descriptor)对象构成。value该属性的值(仅针对数据属性描述符有效)writable当且仅当属性的值可以被改变时为true。(仅针对数据属性描述有效) 设置读写configurable当且仅当指定对象的属性描述可以被改变或者属性可被删除时,为true。enumerable当且仅当指定对象的属性可以被枚举出时,为 true。最后,这是我第一次参加更文活动,茫茫人海中,如果有幸遇到你,读到我这篇文章,那真是太好了。我深知还有很多不足,希望大家能多提建议,还是想舔着脸皮,向屏幕前的大帅比们,大漂亮们,恳请一个小小的点赞,这会是对我莫大鼓励。也祝愿点赞的大帅比们,大漂亮们升职加薪走向人生巅峰!
0
0
0
浏览量2017
养乐多一瓶

每天3分钟,重学ES6-ES12(四)函数的补充 展开语法

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是函数的补充 展开语法函数的默认参数在ES6之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求: 传入了参数,那么使用传入的参数; 没有传入参数,那么使用一个默认值;而在ES6中,我们允许给函数一个默认值:代码演示// ES5以及之前给参数默认值 /** * 缺点: * 1.写起来很麻烦, 并且代码的阅读性是比较差 * 2.这种写法是有bug */ function foo(m, n) { m = m || "aaa" n = n || "bbb" console.log(m, n) } // 1.ES6可以给函数参数提供默认值 function foo(m = "aaa", n = "bbb") { console.log(m, n) } // foo() foo(0, "") 函数默认值的补充默认值也可以和解构一起来使用:另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的): *但是JavaScript允许不将其放到最后,但是意味着还是会按照顺序来匹配;另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了。代码演示// 1.对象参数和默认值以及解构 function printInfo({name, age} = {name: "why", age: 18}) { console.log(name, age) } printInfo({name: "kobe", age: 40}) // 另外一种写法 function printInfo1({name = "why", age = 18} = {}) { console.log(name, age) } printInfo1() // 2.有默认值的形参最好放到最后 function bar(x, y, z = 30) { console.log(x, y, z) } // bar(10, 20) bar(undefined, 10, 20) // 3.有默认值的函数的length属性 function baz(x, y, z, m, n = 30) { console.log(x, y, z, m, n) } console.log(baz.length) // 打印 4 函数的剩余参数ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;那么剩余参数和arguments有什么区别呢? 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参; arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作; arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构, 而rest参数是ES6中提供 并且希望以此来替代arguments的; 剩余参数必须放到最后一个位置,否则会报错。代码演示function foo(...args, m, n) { console.log(m, n) console.log(args) console.log(arguments) } foo(20, 30, 40, 50, 60) // 报错 // Uncaught SyntaxError: Rest parameter must be last formal parameter // rest paramaters必须放到最后 // Rest parameter must be last formal parameter function foo(m, n = m + 1) { console.log(m, n) } foo(10); 展开语法展开语法(Spread syntax): 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开; 还可以在构造字面量对象时, 将对象表达式按key-value的方式展开;展开语法的场景: 在函数调用时使用; 在数组构造时使用; 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性;注意:展开运算符其实是一种浅拷贝;代码演示const names = ["abc", "cba", "nba"] const name = "why" const info = {name: "why", age: 18} // 1.函数调用时 function foo(x, y, z) { console.log(x, y, z) } // foo.apply(null, names) foo(...names) //打印 abc cba nba foo(...name) // 打印 w h y // 2.构造数组时 const newNames = [...names, ...name] console.log(newNames) //打印 ['abc', 'cba', 'nba', 'w', 'h', 'y'] // 3.构建对象字面量时ES2018(ES9) const obj = { ...info, address: "广州市", ...names } console.log(obj) // 打印 {0: 'abc', 1: 'cba', 2: 'nba', name: 'why', age: 18, address: '广州市'} // 数组被展开 最后,这是我第一次参加更文活动,茫茫人海中,如果有幸遇到你,读到我这篇文章,那真是太好了。我深知还有很多不足,希望大家能多提建议,还是想舔着脸皮,向屏幕前的大帅比们,大漂亮们,恳请一个小小的点赞,这会是对我莫大鼓励。也祝愿点赞的大帅比们,大漂亮们升职加薪走向人生巅峰!
0
0
0
浏览量2012
养乐多一瓶

每天3分钟,重学ES6-ES12(十八) CJS

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,前面我们介绍了模块化的历史,今天介绍模块化处理方案CommonJSCommonJS规范和Node关系common Js 是一个规范,指出是在浏览器以外的地方,可以简称为CJSNode 是commonJs 在服务器端一个具体有代表性的一个实现webpack打包工具具备对commonJS的支持和转换node 中每一个js文件都有是一个单独的模块包括CommonJS规范的核心变量:exports,module.exports, requrie导出方式exports.amodule.exports ={a:a}// 源码 module.exports = {} exports = module.exports //最终能导出的一定是module.exports() 内部原理// main.js const why = require("./why.js") console.log(why) setTimeout(() => { console.log(why.name) why.name = "james" }, 1000) // why.js const info = { name: "why", age: 18, foo: function() { console.log("foo函数~") } } setTimeout(() => { // info.name = "kobe" console.log(info.name) }, 2000) module.exports = info 两个文件 执行node main.js输出结果为{ name: 'why', age: 18, foo: [Function: foo] }whyjames先打印why对象,然后打印why 修改why name 打印 james由此可见,require 引入的文件是加载时执行require执行细节require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象。require(X)查找规则1.如果X是node的一个核心模块,如http path 则直接返回核心模块,并停止查找2.如果X是一个路径2.1.将X当作是文件名在对应的目录下查找 如果有后缀名,按照后缀名的格式查找对应的文件 如果没有后缀名,会按照顺序 直接查找文件X > 查找X.js文件 > 查找X.json文件>查找X.node文件 2.2.如果没有找到对应的文件,将X作为一个目录 查找目录下的index文件 查找X/index.js文件>查找X/index.json文件>查找X/index.node文件3.非路径也非核心模块(第三方包)每层级依次去寻找node_modules 如果都没找到,那么报错:not found模块的加载过程结论一:模块在被第一次引入时,模块中的js代码会被运行一次结论二:模块被多次引入时,会缓存,最终只加载(运行)一次结论三:如果有循环引入,Node采用的是深度优先算法,寻找模块CommonJS规范缺点CommonJS加载模块是同步的: 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行; 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;如果将它应用于浏览器呢? 浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行; 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;所以在浏览器中,我们通常不使用CommonJS规范: 当然在webpack中使用CommonJS是另外一回事; 因为它会将我们的代码转成浏览器可以直接执行的代码;在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD: 但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ESModule代码的转换; AMD和CMD已经使用非常少了,所以这里我们也不展开介绍了。
0
0
0
浏览量2025
养乐多一瓶

每天3分钟,重学ES6-ES12(十)Promise参数实例方法介绍

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是ES6中新增的内容Promise的then、catch、finally方法,都属于Promise的实例方法,都是存放在Promise的prototype上的。Promise回调函数ExecutorExecutor是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数:new Promise((resovle,reject)=>{ console.log('Executor') }) 通常我们会在Executor中确定我们的Promise状态: 通过resolve,可以兑现(fulfilled)Promise的状态,我们也可以称之为已决议(resolved); 通过reject,可以拒绝(reject)Promise的状态;这里需要注意:一旦状态被确定下来,Promise的状态会被 锁死,该Promise的状态是不可更改的 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成兑现(fulfilled);在之后我们去调用reject时,已经不会有任何的响应了(并不是这行代码不会执行,而是无法改变Promise状态);代码演示promise 三种状态const promise = new Promise((resolve, reject) => { }) promise.then(res => { }, err => { }) // 完全等价于下面的代码 // 注意: Promise状态一旦确定下来, 那么就是不可更改的(锁定) new Promise((resolve, reject) => { // pending状态: 待定/悬而未决的 console.log("--------") reject() // 处于rejected状态(已拒绝状态) resolve() // 处于fulfilled状态(已敲定/兑现状态) console.log("++++++++++++") }).then(res => { console.log("res:", res) }, err => { console.log("err:", err) }) resovle不同值的区别情况一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数;情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态:代码演示// 1.传入Promise的特殊情况 const newPromise = new Promise((resolve, reject) => { // resolve("aaaaaa") reject("err message") }) new Promise((resolve, reject) => { // pending -> fulfilled resolve(newPromise) }).then(res => { console.log("res:", res) }, err => { console.log("err:", err) }) // err:reject message 情况三:如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据 then方法的结果来决定Promise的状态:代码演示new Promise((resolve, reject) => { // pending -> fulfilled const obj = { then: function(resolve, reject) { // resolve("resolve message") reject("reject message") } } resolve(obj) }).then(res => { console.log("res:", res) }, err => { console.log("err:", err) }) // err:reject message Promise有哪些实例方法console.log(Object.getOwnPropertyDescriptors(Promise.prototype)) then方法接受两个参数then方法是Promise对象上的一个方法:它其实是放在Promise的原型上的 Promise.prototype.thenthen方法接受两个参数: fulfilled的回调函数:当状态变成fulfilled时会回调的函数; reject的回调函数:当状态变成reject时会回调的函数;多次调用一个Promise的then方法是可以被多次调用的:返回值then方法本身是有返回值的,它的返回值是一个Promise,所以我们可以进行如下的链式调用: 但是then方法返回的Promise到底处于什么样的状态呢?Promise有三种状态,那么这个Promise处于什么状态呢? 当then方法中的回调函数本身在执行的时候,那么它处于pending状态; 当then方法中的回调函数返回一个结果时,那么它处于fulfilled状态,并且会将结果作为resolve的参数; 情况一:返回一个普通的值; 情况二:返回一个Promise; 情况三:返回一个thenable值; 当then方法抛出一个异常时,那么它处于reject状态;catch方法多次调用catch方法也是Promise对象上的一个方法:它也是放在Promise的原型上的Promise.prototype.catch一个Promise的catch方法是可以被多次调用的: 每次调用我们都可以传入对应的reject回调; 当Promise的状态变成reject的时候,这些回调函数都会被执行;返回值事实上catch方法也是会返回一个Promise对象的,所以catch方法后面我们可以继续调用then方法或者catch方法:const promise = new Promise((resolve, reject) => { reject("111111") }) promise.catch(err => { console.log("err1:", err) }).catch(err2 => { console.log("err2:", err2) }).then(res => { console.log("res result:", res) }) // err1: 111111 // res result: undefined 那么如果我们希望后续继续执行catch,那么需要抛出一个异常:const promise = new Promise((resolve, reject) => { reject("111111") }) promise.then(res => { console.log("res:", res) }).catch(err => { console.log("err:", err) throw new Error("catch return value") }).then(res => { console.log("res result:", res) }).catch(err => { console.log("err result:", err) }) // err: 111111 // err result: Error: catch return value finally方法finally是在ES9(ES2018)中新增的一个特性:表示无论Promise对象无论变成fulfilled还是reject状态,最终都会 被执行的代码。finally方法是不接收参数的,因为无论前面是fulfilled状态,还是reject状态,它都会执行。代码演示const promise = new Promise((resolve, reject) => { // resolve("resolve message") reject("reject message") }) promise.then(res => { console.log("res:", res) }).catch(err => { console.log("err:", err) }).finally(() => { console.log("finally code execute") })
0
0
0
浏览量2015
养乐多一瓶

每天3分钟,重学ES6-ES12(一)字面量的增强 解构

为什么学习ES6嗯~ES6的语法相信大家都烂熟于心,已经在开发中日常使用我知道屏幕前的大漂亮,大帅比肯定都会了。但是我还是得写,原因你懂的,请看文章第一句。如果你已经会了,可以点开不看。如果你还不会,点开请先点赞。前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是字面量的增强和解构。字面量的增强ES6中对 对象字面量 进行了增强,称之为 Enhanced object literals(增强对象字面量)。字面量的增强主要包括下面几部分: 属性的简写:Property Shorthand 方法的简写:Method Shorthand 计算属性名:Computed Property Namesvar name = "yz" var age = 24 var obj = { // 1.property shorthand(属性的简写) // es5 name:name, // es6 age, // 2.method shorthand(方法的简写) // es5 foo: function() { console.log(this) }, // es6 bar() { console.log(this) }, baz: () => { console.log(this) }, // 3.computed property name(计算属性名) [name + 123]: 'hehehehe' } obj.baz() obj.bar() obj.foo() // obj[name + 123] = "hahaha" console.log(obj) 计算属性名 定义对象key的时候加上[],可以动态定义对象名 []解构Destructuring概述ES6中新增了一个从数组或对象中方便获取数据的方法,称之为解构Destructuring。我们可以划分为:数组的解构和对象的解构。数组结构数组的解构:var names = ["abc", "cba", "nba"] // var item1 = names[0] // var item2 = names[1] // var item3 = names[2] // 对数组的解构: [] var [item1, item2, item3] = names console.log(item1, item2, item3) // 打印 abc cba nba // 解构后面的元素 var [, , itemz] = names console.log(itemz) // 打印 nba // 解构出一个元素,后面的元素放到一个新数组中 var [itemx, ...newNames] = names console.log(itemx, newNames) // 打印 abc ['cba', 'nba'] // 解构的默认值 var [itema, itemb, itemc, itemd = "aaa"] = names console.log(itemd) // 打印 aaa 对象的解构对象的解构:var obj = { name: "yz", age: 25, height: 180 } // 对象的解构: {} var { name, age, height } = obj console.log(name, age, height) // 打印 yz 25 180 var { age } = obj console.log(age) // 打印 25 var { name: newName } = obj console.log(newName) // 打印 yz var { address: newAddress = "北京市" } = obj console.log(newAddress) // 打印 北京市 function foo(info) { console.log(info.name, info.age) } foo(obj) // 打印 yz 25 function bar({name, age}) { console.log(name, age) } bar(obj) // 打印 yz 25 应用场景解构目前在开发中使用是非常多的:let obj1 = { name:'yz', age:24 } function test(obj){ // es5 console.log(obj.name,obj.age) // es6 const {name,age} = obj console.log(name,age) } test(obj1) 总结字面量的增强 方便我们写对象属性和方法时,少写代码解构 方便我们更容易的处理对象数组的属性,少写代码
0
0
0
浏览量2017
养乐多一瓶

每天3分钟,重学ES6-ES12(二)var let const的选择

前言今天开始和大家一起系统的学习ES6+,每天3分钟,用一把斗地主的时间,重学ES6+,今天介绍的是var let 和const。let/const基本使用在ES5中我们声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const let、const在其他编程语言中都是有的,所以也并不是新鲜的关键字; 但是let、const确确实实给JavaScript带来一些不一样的东西;let关键字: 从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量const关键字: const关键字是constant的单词的缩写,表示常量、衡量的意思; 它表示保存的数据一旦被赋值,就不能被修改; 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;注意:另外let、const不允许重复声明变量;代码演示var foo = "foo" let bar = "bar" // const constant(常量/衡量) const name = "abc" name = "cba" // 注意事项一: const本质上是传递的值不可以修改 // 但是如果传递的是一个引用类型(内存地址), 可以通过引用找到对应的对象, 去修改对象内部的属性, 这个是可以的 const obj = { foo: "foo" } obj = {} obj.foo = "aaa" console.log(obj.foo) // aaa // 注意事项二: 通过let/const定义的变量名是不可以重复定义 var foo = "abc" var foo = "cba" let foo = "abc" // SyntaxError: Identifier 'foo' has already been declared let foo = "cba" console.log(foo) let/const作用域提升作用域提升: 在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;let、const和var的另一个重要区别是作用域提升: 我们知道var声明的变量是会进行作用域提升的; 但是如果我们使用let声明的变量,在声明之前访问会报错;ECMA262对let和const的描述; 这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值; let、const没有进行作用域提升,但是会在解析阶段被创建出来。代码演示 console.log(foo) var foo = "foo" // Reference(引用)Error: Cannot access 'foo' before initialization(初始化) // let/const他们是没有作用域提升 // foo被创建出来了, 但是不能被访问 // 作用域提升: 能提前被访问 console.log(foo) let foo = "foo" let const 与window的关系全局通过var来声明一个变量,事实上会在window上添加一个属性但是let、const是不会给window上添加任何属性的。块级作用域之前,我们都知道,在ES5中,JavaScript只会形成两个作用域:全局作用域和函数作用域。var 没有块级作用域,ES5中放到一个代码中定义的变量,外面是可以访问的:// ES5中没有块级作用域 // 块代码(block code) { // 声明一个变量 var foo = "foo" } console.log(foo) 在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的但是我们会发现函数拥有块级作用域,但是外面依然是可以访问的: 这是因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升;// ES6的代码块级作用域 // 对let/const/function/class声明的类型是有效 { let foo = "why" function demo() { console.log("demo function") } class Person {} } // console.log(foo) // foo is not defined // 不同的浏览器有不同实现的(大部分浏览器为了兼容以前的代码, 让function是没有块级作用域) // demo() var p = new Person() // Person is not defined 其他块级作用域如 if语句的代码就是块级作用域,switch语句的代码也是块级作用域,for语句的代码也是块级作用域//if语句的代码就是块级作用域 if (true) { var foo = "foo" let bar = "bar" } console.log(foo) console.log(bar) //foo //Uncaught ReferenceError: bar is not defined // switch语句的代码也是块级作用域 var color = "red" switch (color) { case "red": var foo = "foo" let bar = "bar" } console.log(foo) console.log(bar) // foo // Uncaught ReferenceError: bar is not defined // for语句的代码也是块级作用域 for (var i = 0; i < 3; i++) { console.log("Hello World" + i) } console.log(i) // 3 for (let i = 0; i < 3; i++) { console.log("Hello World" + i) } console.log(i) // Uncaught ReferenceError: i is not defined 暂时性死区在ES6中,我们还有一个概念称之为暂时性死区: 它表达的意思是在一个代码中,使用let、const声明的变量,在声明之前,变量都是不可以访问的; 我们将这种现象称之为 temporal dead zone(暂时性死区,TDZ);代码演示var foo = 'foo' if(true){ console.log(foo) // Uncaught ReferenceError: Cannot access 'foo' before initialization let foo = 'bar' } var let const 的选择那么在开发中,我们到底应该选择使用哪一种方式来定义我们的变量呢?对于var的使用:我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些 历史遗留问题; 其实是JavaScript在设计之初的一种语言缺陷; 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解; 但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;对于let、const: 对于let和const来说,是目前开发中推荐使用的; 我们会有限推荐使用const,这样可以保证数据的安全性不会被随意的篡改; 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let; 这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;最后,这是我第一次参加更文活动,茫茫人海中,如果有幸遇到你,读到我这篇文章,那真是太好了。我深知还有很多不足,希望大家能多提建议,还是想舔着脸皮,向屏幕前的大帅比们,大漂亮们,恳请一个小小的点赞,这会是对我莫大鼓励。也祝愿点赞的大帅比们,大漂亮们升职加薪走向人生巅峰!
0
0
0
浏览量2016

履历