分享最近在做的两个小项目给大家~

前言

写此文章的想法是最近基于崔大教学的小项目,做出一些分享给大家

跟随崔大去走完整个过程是非常有趣的,自己感悟有很多

首先我去跟随崔大写小项目是因为崔大最近分享的一期:

实现在终端运行的 AI 聊天机器人 | ChatGPT & Nodejs ,当自己按照崔大教学指导去完成时候确实觉得很快乐,一个很有趣的终端集成openai而且个人运用到了自己手中去体验,在其中自己也的确感受到了关于崔大开篇讲的那样:通过构建一个小玩具和小应用的方式学习编程是最有趣也是最正确的方式。虽然是小项目但是成就感那是必须有的,因为中间也遇到了奇怪的小问题卡住,自己思索到深夜不知道如何去解决,但是学习嘛,哪里是不动脑子就能学会呢?正是自己参与了思考,遇到问题,并且自己也付出行动去解决了才有最后做完时候的喜悦和带来的收获。其中项目的知识点,崔大带着大家完完整整的运用了一遍,测试技术readlineSyncrollup 打包库等等,对于我个人做完整个项目后来说是感觉受益匪浅。

第二个小项目是之前崔大就分享在B站的一期mini编译器的项目:

实现超级 mini 的编译器 | 词法分析 tokenizer | 前端学习编译原理的最佳案例 ,mini编译器项目是一个不同于上一个分享终端聊天机器人AI的,这个项目是通过完成编写一个mini compiler 可以让我们更好的理解抽象语法树以及对一些编译工具的实现原理有更深的认知,在前端领域中实现各种工具的插件是离不开 ast 的,这也是崔大开篇对于这个项目的介绍。在跟随崔大实现这个项目的时候,个人是完全跟着崔大思路走的,原因是自己压根没有听过并且了解过关于编译器的东西。但是真正去从划分好每一个模块跟着去走去做,中间也尝试开始跟着崔大去画图去理解这个模块需要去写的内容和要实现的预期结果,将大模块拆分成小部分,依次去实现。这个过程是让我感觉真正理解了每一个模块,并且能够清楚的认识到我做完整个项目后对这个项目的认知程度提高了。从词法分析语法分析 ,再结合ast树transformer转换一直到最后的编译部分,整个思路是很清晰的,也能对前端构建工具那些有一定的理解。

分享自身跟随崔大去做的项目也是希望能和小伙伴们一起去做一些有意思的项目来巩固个人的基础和提升自己的能力,也希望和大家一起去进步学习。

在此感谢崔大的分享优质的小项目,以及自己询问问题时崔大给予的帮助,特此感谢崔大

1.chatgpt & nodejs

项目中所涉及到的知识点

  • chatGPT api
  • readlineSync
  • dotenv 环境变量的设置
  • colors 给字体来点颜色
  • ora 终端显示进度条动画
  • rollup 打包库
  • typescript
  • 重构技巧
  • 模块化思维
  • 全局 cli

首先,向大家演示一下个人终端去使用这个聊天

可以看到全局去调用写好的chatbox后,就可以去提出问题:vue的作者是谁?这时候会调用openai的api 来回答我们问的问题。并且再下一步去调用时候是结合了上下文的 ,通过给出的图片大家可以发现结合了上文去回答的问题:实现了哪些库,最后一个是展示在询问问题时候会有一个loading加载动画 ,并且是支持exit去退出我们当前的对话。

这个就是最终成品的展示我们小机器人最终展示的功能,是不是很酷(。ì _ í。)

实现调用openai的api部分

一.环境搭建

1.首先创建 src/index.ts 目录

2.pnpm init 去初始化我们的项目,将package.json创建好

3.tsc --init 创建tsconfig,构建ts环境,tsc命令无用:pnpm install -g typescript

4.index.ts中写入console.log("index") 执行ts-node src/index.ts测试环境是否搭建好

2.引入openai,配置

1.pnpm i openai 这个库:openai官方提供的node.js库

2.引入配置对象和openai api

typescript 复制代码
import { Configuration, OpenAIApi } from "openai";
import dotenv from "dotenv";
dotenv.config()
const openAi = new OpenAIApi(
  new Configuration({
    basePath: "https://jiushi21.win/v1",
    apiKey: process.env.OPEN_API_KEY,
  })
);

basePath:为代理地址

apiKey:openai key ,这里使用了环境变量方式会更安全

导入dotenv这个库 pnpm i dotenv,并且引入配置 ,创建.env在其中设置自己的key

这里想跟大家说明一下:basePath不能使用官方给定的地址(原因自己查)。可以使用国内代理的地址(崔大在视频中就是使用了国内代理的地址,是需要付费key的),或者自己做接口转发去配置一个地址。这里个人推荐cloudflare去做,感兴趣的可以去了解一下3美刀部署,jiushi21.win/v1 是个人去创建好的接口转发地址,免费供大家使用嘿嘿,这样就可以用自己官方key,只需要配置在basepath中就行

openai官网api,可以进入看到官方提供的参数

typescript 复制代码
(async () => {
const chatCompletion = await openAi.createChatCompletion({
        model: "gpt-3.5-turbo",
        messages,
        messages: [
          {
            role: "user",
            content: "vuejs的作者是谁?",  
          },
        ],
      };
      console.log(chatCompletion)
     )()

这样配置好,进行ts-node src/index.ts执行,可以看到是成功的。

chatCompletion.data.choices[0].message?.content; 再去打印消息中的内容,就可以得到content提问返回的答案,到这儿其实已经完成了这个项目的核心功能了。

readlineSync库

当我们需要使用关于提问的部分时,需要用到readlineSync这个库

1.pnpm i readline-sync

typescript 复制代码
import readlineSync from "readline-sync";
const userInput = readlineSync.question("you:");
console.log(userInput);

引入成功后,注释掉之前的代码,可以tsnode去测试一下readlineSync的功能是否成功

2.将创建的 userInput 放入到(async () =>调用中去,并且将内部的 content: userInput,进行替换,这样再去执行ts-node就可以发现可以成功去提问并且返回答案

typescript 复制代码
(async () => {
//放入到这里
const userInput = readlineSync.question("you:");
const chatCompletion = await openAi.createChatCompletion({
...
messages:[
...
//content替换
content:userInput

3.当这样做完后,发现回答完后直接退出,而我们需要的是持续去提问 ,需要在上面代码中加入一层while循环做处理。虽然这样去解决了关于循环提问的问题,但是会发现不会按照上下文去回答。那么我们就需要把之前提问的记录传回给message

所以我们首先要做的:将之前回答和提问的信息给收集起来,下次提问时候再发过去

定义一下message的类型,角色分为user和ai

const messages: { role: "user" | "assistant"; content: string }[] = [];

先将用户的输入给push进来:

messages.push({ role: "user", content: userInput });

再去处理ai部分,定义一个answer去接收,并且push进去:

const answer = chatCompletion.data.choices[0].message?.content; messages.push({ role: "assistant", content: answer! }); //这里anser涉及到类型问题,有可能会是undefined,所以增加!

typescript 复制代码
const messages: { role: "user" | "assistant"; content: string }[] = [];
(async () => {
messages.push({ role: "user", content: userInput });

     const chatCompletion = await openAi.createChatCompletion({
       model: "gpt-3.5-turbo",
       messages,
       messages: [
         {
           role: "user",
            content: "vuejs的作者是谁?",
           content: userInput,
         },
       ],
     });
       const answer = chatCompletion.data.choices[0].message?.content;
       messages.push({ role: "assistant", content: answer! });

       console.log(answer);
  }
})();

最后console去打印时候,answer回答去替换,这样就可以实现了聊天机器人支持上下文。

优化 colors 字体 / exit退出

colors 给字体来点颜色

pnpm i colors 然后将colors进行导入,在question提问中进行使用即可,colors.rainbow

readlineSync.question(colors.rainbow("you:"));

并且在机器人回答部分同样进行设置即可(bold加粗)

console.log(colors.bold.red("Jarvis: "), answer);

搭配上ui的效果,这样使得我们项目更加完善

exit/quit退出功能增加

typescript 复制代码
if (
      userInput.toLowerCase() === "exit" ||
      userInput.toLowerCase() === "quit"
    ) {
      process.exit();
    }

重构

当我们写到这一步核心功能实现完成后,但是代码可读性非常差,完全是过程式代码,可读性不高。虽然是小项目,但是为了让可读性更高,进行重构。

封装一个user.ts

在user.ts中创建askQuestion将之前提问的逻辑封装进去

在message封装好后,在user中还需要引入

在原先的index.ts中我们只需要调用askQuestion使用,就可以很清晰的明白意图

封装一个check.ts

创建一个check.ts,这里封装好后,在index.ts调用时,就可以传入userInput

封装message.ts

addUserMessage去使用时候,引入到user中去

封装bot.ts

基于机器人提问回答的部分,以及openai配置部分,我们都可以封装到这里

但是注意,在使用时候 处理执行逻辑优先导致未注入的问题

所以我们在其中封装了函数initBot()来延迟初始化的时间

重构完成的代码结构

可以看到逻辑十分的精简整洁

ora loading / 打包工具rollup

ora loading加载模块

在这里我们需要借助ora来实现这个部分

首先 pnpm i ora,并且在index.ts中去引入ora,并且去使用它

rollup打包

但是当我们去执行ts-node index.ts时 会报错

原因是es module不支持requeir 的,因为ts-node无法进行转换 ,借助打包工具

对于库最佳选择我们使用rollup:pnpm i rollup -D

创建rollup.config.js,在其中我们需要先去配置,并且package.json加入type:module

并且script:增加 -> "build": "rollup -c rollup.config.js",

但是还有一步需要去做,rollup不懂ts所以我们需要额外去安装依赖,并且配置

pnpm i @rollup/plugin-typescript -D

我们执行pnpm build时候,去看dist中文件并没有将代码打包进来

我们需要去tsconfig中更改 "module": "NodeNext", 将commonjs替换

NodeNext是支持esm,再次执行会发现让我们每个文件写全引入的路径,.js增加

这样配置好后,再去执行node dist/bundler.js 就会发现运行是没问题的

重构loading模块

这块需要增加一部分重构的代码并且修改一些小细节

原因是在调用后我们会发现loading并没有换行存在,创建loading.ts

这里我们 /r去完成换行,并且增加stop()逻辑

重构的好处,即使loading中的库发生改变了,也不会影响其他模块,在其他模块中只认接口

重构完成后我们的代码逻辑应该是这样的,十分简洁清晰:

我们在执行文件时候,应该重新编译打包后的代码,pnpm build -w 进入watch模式

这样我们修改完代码,就会自动进行编译处理了,不用每次手动去重新编译

而且在执行文件时候,package.json中,增加"dev": "node dist/bundler.js"方便我们每次去用命令处理

pnpm dev再次去执行发现我们已经是ok啦!!

全局调用

在package.json中,我们需要去做的事:

"bin": "./dist/bundler.js",写入我们打包好的路径

并且name:是我们全局调用时候需要去命名的name

写完之后,执行 pnpm link --global 注册全局 //如果pnpm配置后失效换npm link全局

有些问题需要去处理的就是:在bundler中我们需要指定要执行的程序

但是我们无法直接去写入,这个时候可以借助rollup的插件用shebang头去完成

pnpm i rollup-plugin-add-shebang 安装后在rollup.config中去引用并且配置

这样重新打包你就会发现解决了上面所提到的问题

最后还有一个坑需要去填:当我们全局去终端使用的时候,需要去指定好配置路径

需要在index.ts中去解决得到绝对路径,不然我们是无法找到的,配置完可以console去看查看一下配置的路径是否是正确的

这样去重新编译,再去重新link,去终端尝试:

这样就已经成功完成了!!!

扩展部分

其实写到这里,我们已经能够完成了一个属于自己的聊天机器人,并且能够在终端全局去调用,对于我个人感觉是一件很酷的事情。

我们在其中还有很多能去优化的点,就像崔大在视频末尾提到的:message全部放在一个里面消息增多token消耗是很大的。崔大也私下里指点了我关于这个项目一些额外扩展:用户可以手动的设置basepath/apikey,这时候我们就要思考怎么存这些配置,有点像git的config一样。当然,小伙伴们都可以去动手实现一下,并且可以延伸做很多额外的改变。

这样一个有趣的小项目给我个人带来的感悟,将涵盖的零碎的知识点在一起去拼凑,能够去学习到很多知识的揉合,个人去跟随教程完成也是提升自己的过程。

以上项目代码上传到github仓库:github.com/zclsx/termi...

如果觉得不错的话,帮忙点个star小星星 吧!谢谢大家!

特此十分感谢崔大给予的帮助和支持!!也希望和小伙伴们一起交流进步呀!

2.实现超级 mini 的编译器

这个小项目,是我在做完了崔大关于openai聊天小机器人后,又刷到并且最近动手去做的另一个有意思的项目,而且和上一个项目是完全不同的,给我带来的感悟也不一样。

这个编译器是github上有个200行代码去实现一个编译器,其中是写了大量的注释去告诉我们如何去做,如何去实现github.com/jamiebuilds...

这个项目最终是实现:类似LISP代码去编译成C的代码

类似于很多的构建工具都是需要依靠构建工具去完成,所以对于前端而言,我们可以通过这个项目去了解认识编译原理,希望感兴趣的小伙伴看下崔大教学视频,食用更佳!

当然在此项目中也是结合了数据结构,算法,测试知识,测试也是本项目十分重要的一个点,可以让我们小步走的去完成实现每一个模块功能再到最终去整合完成整个模块。

我们来一起实现一下:

词法分析 tokenizer

这个模块,是根据我们提供的代码(add 2 (subtract 4 2))将其处理成tokens

首先我们需要去安装测试依赖:vitest : pnpm i vitest

在package.json调试中加入 "test": "vitest"

测试部分(重要性)

然后我们去创建一个 tokenizer.spec.ts 文件,将我们要将预期完成的token通过tokenizer转换去完成这一部分的内容,所以通过测试的功能写好先确立我们的预期目标

这里如果看视频教学的小伙伴,发现崔大是通过画图的方式给大家去讲解

在此推荐一个vscode的插件:excalidraw 超级好用

在写这部分模块时候,可以通过创建一个 xxx.excalidraw.svg / png 的文件即可

既是记笔记的过程,也可以画图来和崔大一样让我们清晰项目的结构,增强理解

使用上了后,我们就可以这样去写代码,个人是感觉对于学习的时候很有帮助!!

tokenizer

我们先去分析这一个模块,需要做的是当我们去处理这一整个代码时候,里面首先依次去寻找它的值将其转换为token,比如add当他判断不是a-z的情况下就做出判断,当遇到数字又是一种情况。这样我们依次去比较就可以把整个小模块去拆分成一步一步走。

而对应的测试部分就可以先把预期我们拆分的条件去写出来:

我们依次去看这部分会发现,我们是将原先代码,拆分成了一个一个小的模块去判断

对于左括号的情况,对于add情况,对于22数字这种情况都是拆分处理

在tokenizer.ts中,我们去写实现的逻辑部分:

首先去定义一下tokenizer这部分函数,其中还需要定义ts类型

原先测试case中,我们需要把type去进行一个引入替换

这样写完了后,我们去执行pnpm test ,会发现测试case是通过了

实现add部分

首先去写测试case,还是和上面一样

其次我们在写逻辑判断时候,要注意处理指针的问题,并且写入正则判断

处理好后,我们再去看我们的测试case,是一个通过的状态

如果到这一步小伙伴应该会发现,我们可以按照上面的逻辑去走,接着拆分number的逻辑

此处先略过这一部分包括右括号的逻辑部分(大家按照原视频走,此处更多是思路分享)

按照上面图,去实现一个较为复杂的逻辑部分

先看我们定义的测试case

这时候我们要思考如何让其去处理多个?-> 增加循环

指针每次都会去走,处理完就走下一个,并且处理完成后,要进行continue,还有空格的逻辑

如果这样去将逻辑补充完整,我们再去执行我们的测试case会发现是没问题的

最终我们再去看原先一开始的测试case时候,我们要想将之前所做的所有功能去进行实现

要把原先case中的type进行替换

那么最终再执行测试,发现所有的case都是一个通过状态。

这样我们就完成了这个tokenizer词法分析 模块

TDD 在这里要跟大家说的是,我们通过一个大的模块去进行拆分测试,换成小的模块去走,并且依赖测试去先将预期要完成的目标去写入,也就是测试驱动开发TDD。这样做的好处,对于我个人而言,是感觉可以细化的去分析这个模块的内容,并且通过自动化测试去一开始定下目标并且依次去实现它,就像打怪做任务一样,结合画图分析,会让我对于这部分的内容理解更加透彻,总结一句:越做越爽,体会编程的乐趣!

语法分析parser

目标分析

一上来先放图!

这个模块是基于我们上个词法分析模块后去做的内容,要将原先tokens转换为ast树

通过测试case我们来看一下要完成的目标:

这是我们的tokens部分:

最后要转换成ast树的部分:

如何实现(拆分测试)

对于我们来说,上面的测试case跟词法分析一样,一上来都过于庞大,我们需要拆分测试去写

先去实现拆分成一个小模块:

而内部怎么去做,就相当于我们把这个token去通过调用paser,实现成ast树

目标明确,内部paser部分实现就很简单了(因为是重构过的代码,主要将思路给大家): 推荐各位原视频去看,这样才能一步一步走!

如果去写完了这部分逻辑,并且将ts类型也去完善了后,测试case就会通过

表达式部分

可以清晰的看到,我们是将目标从数字拆分后,第二步去拆分了表达式部分去走

而实现的逻辑也就是我图里画出来的部分,那么从上面部分走下来的小伙伴,应该思路是很清晰了。我们可以直接通过测试case的定义,先把表达式部分的内容跟确认好,从而再去实现其内部的逻辑,最终完成我们测试case的要求。

这里涉及到的点就是:如果说判断是括号,我们首先要更新指针++,而且当我们取完表达式的值后,依旧是需要指针的更新这一步。

增加最后一步的测试

我们走到这一步去增加最后一个测试case是为了补充最终我们要去实现的模块部分

当遇到两个表达式时候,我们判断完逻辑end,要去进行while循环,封装成了一个walk

也就是下图所标注的内容部分,涉及到有最终处理undefined的情况还要进行一个抛出异常

那最终这些逻辑实现完,走完了测试case通过,就可以去实现我们最终的目标测试case

traverser 遍历 AST

这一小节我们需要完成两个功能:

  1. 遍历整棵树

  2. 引入visitor模式:让用户对当前的节点进行增删改查

遍历整棵树的逻辑

首先我们去看root根节点,其对应下面的字节点有CallExpression节点,再往下走是number节点和subtract CallExpression节点,而number节点没有子节点,但是subtract CallExpression节点还有对应子节点需要继续遍历,这就是其中的逻辑。

这部分的代码逻辑,大家观看视频看会很详细,不再赘述,完成后打印可以看到下面这张图:

从图中,我们可以看到跟上面所画的图的逻辑是一一对应的

visitor逻辑

这部分我们要实现的就是在遍历节点的时候,去增加enter和exit的功能

在测试这部分内容时候,会发现其实是用了当前节点和父节点类型判断

代码逻辑部分跟着视频去完成一下。

这部分完成后,会发现通过了测试case,也就实现了目标功能。

transformer 转换 AST

当写完了traverser 遍历 AST,要将实现好的AST 通过transformer转换成目标想要的AST

AST explorer add(2, subtract(4,2)) 将代码放入生成目标AST结构

source AST -> target AST 我们可以先用测试case进行对比

会发现区别就是结构会发生一些改变,name需要有callee进行包裹,params需要换成arguments,而关键的是发现目标AST是有一个statement进行包裹的。当遇到表达式时候而且父级不是表达式时候,就需要去增添statement,需要去做的逻辑部分如下图:

建立一颗新的树,并且ast.context = newAst.body;orgin树和new的树建立链接关系

这样后续去增添的部分,就可以通过orgin树增添到context里面,也就是我们的newAST

最后通过测试case,就可以利用写好的transformer去实现我们生成的目标AST

codegen&compiler

最后部分需要完成两个功能:

  1. 从上一部分完成的目标AST树让其生成对应的代码

  2. 实现一个compiler函数,其中就将我们之前完成的所有功能进行整合

    只需要给出源代码经过compiler之后去生成我们的目标代码

codegen

这部分就是将我们生成好的AST树把它去生成其代码

回顾目标AST树:

当我们去处理NumberLiteral时候,我们只需要去拿value的值。

当我们处理CallExpression时候,生成对应的函数subtract(4,2),而对应的name我们就去取name:subtract,这里需要去增加括号"(",接下来去走当又遇到number类型的时候再将value去取,并且arguments里面的节点需要增加一个逗号","作分隔,最后再去处理一个")"即可。

实现这部分代码逻辑:

这里要注意的点是,我们在测试case中增添了一个case去做验证

验证如果是two ExpressionStatement情况下是否能通过case

toMatchInlineSnapshot是一个测试断言的API,用于生成和匹配内联快照。内联快照是在测试代码中直接嵌入的预期输出。使用toMatchInlineSnapshot可以方便地生成和更新快照,并将其与实际输出进行比较。感兴趣的小伙伴可以去了解一下vitest的api,这里也推荐一下崔大的vitest前端测试课程

compiler部分

最后compiler部分的结构十分的清晰简单,我们可以看下测试的目标case:

结合这边画的图应该会帮助大家分析:

最后实现的逻辑代码:

视频中崔大也画出了对应的完整的逻辑关系图,这里截取一下供大家去理解:

最后写完了执行我们最终的pnpm test,会发现passed!:

最后部分

当我们走到这一步去完成了所有模块部分,并通过最终的compiler,将我们最终期望的功能实现。相当于之前所有的每一步都是一个小任务,最后完成了整个大的任务模块。

经过了层层闯关!终于将我们的source code 编译成了 target code。

就像崔大讲的那样,真的通过一个小项目感觉编程好像打怪通关一样让人重新捡起乐趣。

了解前端工具原理,我们就可以通过这个案例去完成一下体验其中的原理。很多打包工具或者eslint都需要去做transformer这一层,都需要将代码去转换成AST。

里面包含的测试部分是给我带来的感受很深切,也推荐大家感兴趣的小伙伴去学习一下。

这里最后放一下崔大的github仓库项目地址:view code

推荐小伙伴对前端单元测试感兴趣的,想体验TDD乐趣的可以看下崔大的课程介绍

FrontEndTesting 前端的单元测试课

总结

个人去写这两个小项目的原因,也是想逃避摆脱一下枯燥无味的项目,去换个别的小项目去练手体验一下不同的感觉,写完之后确实收获到了快乐!写文分享给大家,是希望和小伙伴们一起成长进步,其中有不足或者是有错误的地方也请大家帮忙指出,文笔见拙,望请大家谅解!

最后,很感谢崔大的项目分享,而且有问题的时候也会骚扰他哈哈,崔大也是给予我很多帮助帮我去解决问题,在此感谢

欢迎各位小伙伴一起去动手做一下,并且有问题大家一起去交流,谢谢各位!

相关推荐
还是大剑师兰特35 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解36 分钟前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~42 分钟前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django
张张打怪兽1 小时前
css-50 Projects in 50 Days(3)
前端·css