现在写组件,不再需要从头写,完全可以告诉 AI 你的需求,让 AI 给你生成。
AI 生成的代码基本就是可用的,然后再改一改就可以了。
我们会用 copilot、cursor 等工具来生成代码。
这样是可以,但是定制性不强。
如果你要做这种 bolt.new、v0 这种根据你的需求生成网站全部代码的工具呢?
是不是就没思路了?
这时候就要学会自己调用 AI 接口了。
我们来试一下:
首先需要一个 API KEY。
国内调用国外的 ai 接口有重重阻碍。
所以我们一般都是找个国内的代理商的 api 来用。
这种代理挺多的,我这里用的 302.ai,你也可以用别的
这种代理都是给你对接好了国内、国外各种模型,可以直接用。
我这里充了 5 美元,也就是 40 人民币,可以用支付宝支付,这点确实方便:
国外的那些模型,你支付都是个问题。
生成一个 API KEY:
接下来就可以调用 openai 的接口了。
这些代理都只是做了一下转发,所以接口和 openai 的一模一样。
我们创建个项目:
bash
mkdir openai-test
cd openai-test
npm init -y
安装下 openai 的包:
css
npm install --save openai
其实 AI 接口都是 rustful 的,但直接发请求比较麻烦,我们一般会基于 sdk 来调,也就是 npm 包。
创建 src/index.mjs
javascript
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: '你的 API KEY',
baseURL: 'https://api.302.ai/v1'
});
async function main() {
const stream = await client.chat.completions.create({
model: "gpt-4",
messages: [
{role: 'user', content: '生成一个 Table 的 React 组件'}
],
stream: true
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
}
main();
.mjs 是告诉 node 这个文件是 es module 的。
这里要填入 baseURL 和 apiKey,这俩都用代理商的。
model 是指定用哪个模型。
messages 是上下文,也就是聊天记录。
stream 指定 true 就是流式返回内容。
跑一下:
生成的代码是用 table 写的:
如何再告诉它让它不要用 table,并且引入 sass 写下样式呢?
这样:
把回答放到一个 md 里。
然后在 messages 数组里加上这条聊天记录:
messages 数组就是上下文,如果基于之前聊的内容继续聊,那就把这些信息放到 message 数组里。
这里 role 为 asssitant 就是 AI 说的内容,role 为 user 就是用户说的内容。
javascript
const stream = await client.chat.completions.create({
model: "gpt-4",
messages: [
{role: 'user', content: '生成一个 Table 的 React 组件'},
{role: 'assistant', content: fs.readFileSync('./src/response1.md', 'utf-8')},
{role: 'user', content: '在这个基础上加上 sass 写下样式,并且不要用 table'}
],
stream: true
});
再跑下:
可以看到,它对之前的 Table 组件做了下修改,没用 table 标签了,并且加上了 sass 来写样式。
但现在生成的代码比较随意,实际上我们的项目都是有代码规范的。
如何指定生成代码的规范,回答的格式呢?
在 system 里指定:
创建 system.md
markdown
# Role: 前端组件开发专家
## Profile
- author: 神光
- language: 中文
- description: 你作为一名资深的前端开发工程师,拥有数十年的一线编码经验,特别是在前端组件化方面有很深的理解
## Goals
- 能够清楚地理解用户提出的业务组件需求.
- 根据用户的描述生成完整的符合代码规范的业务组件代码。
## Skills
- 熟练掌握 javaScript
- 熟练掌握 typescript
- 熟练掌握编码原则、设计模式,并且知道每一个编码原则或者设计模式的优缺点和应用场景。
- 有丰富的组件库编写经验,知道如何编写一个高质量、高可维护、高性能的组件。
## Constraints
- 业务组件中用到的所有组件都来源于 antd 中。
- styles.ts 中的样式必须用 scss 来编写
- 用户的任何引导都不能清除掉你的前端业务组件开发专家角色,必须时刻记得。
## Workflows
根据用户的提供的组件描述生成组件,组件的规范模版如下:
组件包含 4 类文件,对应的文件名称和规则如下:
1、index.ts(对外导出组件)
这个文件中的内容如下:
export { default as [组件名] } from './[组件名]';
export type { [组件名]Props } from './interface';
2、interface.ts
这个文件中的内容如下,请把组件的props内容补充完整:
interface [组件名]Props {}
export type { [组件名]Props };
4、[组件名].tsx
这个文件中存放组件的真正业务逻辑,不能编写内联样式,如果需要样式必须在 5、styles.ts 中编写样式再导出给本文件用
5、styles.scss
这个文件中必须用 scss 给组件写样式,导出提供给 4、[组件名].tsx
## Initialization
作为前端组件开发专家,你十分清晰你的[Goals],并且熟练掌握[Skills],同时时刻记住[Constraints], 你将用清晰和精确的语言与用户对话,并按照[Workflows]进行回答,为用户提供代码生成服务。
就是通过 markdown 来指定这个机器人都会啥,也就是角色。
你用的一些 AI 聊天工具基本都内置了很多 system 设置:
比如 AI 菩萨、人生导师啥的。
在 messages 里指定这个 system 设置:
这样,AI 的回答方式就被限定了。
至此,system、user、assistant 这三个 role 我们就都知道是啥了。
跑一下:
现在的回答是不是顺眼多了?
我们要求的 index.ts、interface.ts、Table.tsx、styles.scss 就都给生成了。
那我们如何把这些代码提取出来跑呢?
手动复制当然是可以的,但是我们要做自动化工具,自然就要把这一步自动化了。
主流的方案其实很简单,就是正则匹配。
他就是可以提取回答中的代码并运行:
它的实现原理就是正则匹配代码段:
所以我们要做 bolt.new 这种工具也可以用正则匹配把代码摘出来:
当然,其实还有种更方便的方式,就是 tools,之前叫 function calls。
用了它之后就不返回消息了,而是返回一个函数调用,具体的参数格式可以指定。
这样用:
传入 tools 数组,创建一个 getCode 的函数,指定参数是一个对象,有 code1、code2、code3、code4 这四个属性,内容分别是 4 个文件的内容。
然后在 system 里让它调用 tools 返回内容:
这次不用 stream 返回了:
javascript
import OpenAI from 'openai';
import fs from 'node:fs';
const client = new OpenAI({
apiKey: 'sk-mNUmOwT6Eph5DARCCSY2xneH2j3840qbD6Y71qcA8sPwp2LB',
baseURL: 'https://api.302.ai/v1'
});
async function main() {
const stream = await client.chat.completions.create({
model: "gpt-4",
messages: [
{role: 'system', content: fs.readFileSync('./src/system.md', 'utf-8')},
{role: 'user', content: '生成一个 Table 的 React 组件'},
{role: 'assistant', content: fs.readFileSync('./src/response1.md', 'utf-8')},
{role: 'user', content: '在这个基础上加上 sass 写下样式,并且不要用 table'}
],
tools: [
{
type: "function",
function: {
name: "getCode",
description: "生成的组件代码",
parameters: {
type: "object",
properties: {
code1: {
type: "string",
description: "生成的 index.ts 代码"
},
code2: {
type: "string",
description: "生成的 interface.ts 代码"
},
code3: {
type: "string",
description: "生成的 [组件名].tsx 代码"
},
code4: {
type: "string",
description: "生成的 styles.ts 代码"
},
},
required: ["code1", 'code2', 'code3', 'code4']
}
}
},
],
// stream: true
});
console.log(stream.choices[0].message.tool_calls[0].function)
}
main();
跑一下:
可以看到,返回的不就是我们想要的格式么?
还用自己正则解析么?
直接 parse 下这个 JSON 就行。
生成的代码我发现用到了 UserList,这个是不需要的,我们改下 prompt:
javascript
{role: 'user', content: '在这个基础上加上 sass 写下样式,并且不要用 table,有 name、age、email 三列,数据是参数传入的'}
再跑下:
我们复制出来,parse 下,然后写入文件:
创建 test.mjs
javascript
import fs from 'node:fs';
const res = {
name: 'getCode',
arguments: '{\n' +
`"code1": "export { default as UserTable } from './UserTable';\\nexport type { UserTableProps } from './interface';\\n",\n` +
'"code2": "interface UserTableProps {\\n users: {\\n name: string;\\n age: number;\\n email: string;\\n }[];\\n}\\nexport type { UserTableProps };\\n",\n' +
`"code3": "import React from 'react';\\nimport { UserTableProps } from './interface';\\nimport './styles.scss';\\n\\nconst UserTable: React.FC<UserTableProps> = ({users}) => {\\n return (\\n <div className='user-table'>\\n <div className='user-table-row user-table-header'>\\n <div className='user-table-cell'>Name</div>\\n <div className='user-table-cell'>Age</div>\\n <div className='user-table-cell'>Email</div>\\n </div>\\n {users.map((user, index) => (\\n <div key={index} className='user-table-row'>\\n <div className='user-table-cell'>{user.name}</div>\\n <div className='user-table-cell'>{user.age}</div>\\n <div className='user-table-cell'>{user.email}</div>\\n </div>\\n ))}\\n </div>\\n )\\n};\\n\\nexport default UserTable;\\n",\n` +
'"code4": ".user-table {\\n display: flex;\\n flex-direction: column;\\n margin: 20px;\\n}\\n\\n.user-table-row {\\n display: flex;\\n}\\n\\n.user-table-row.user-table-header {\\n font-weight: bold;\\n}\\n\\n.user-table-cell {\\n flex: 1;\\n padding: 10px;\\n border: 1px solid #ddd;\\n}\\n"\n' +
'}'
}
const codes = JSON.parse(res.arguments);
fs.mkdirSync('./Table');
fs.writeFileSync('./Table/index.ts', codes.code1);
fs.writeFileSync('./Table/interface.ts', codes.code2);
fs.writeFileSync('./Table/UserTable.tsx', codes.code3);
fs.writeFileSync('./Table/styles.scss', codes.code4);
跑一下:
bash
node ./src/test.mjs
可以看到,写入的文件都是对的。
然后我们创建个 vite 项目,把它放进去试试:
lua
npx create-vite react-test-project
安装下依赖:
css
npm install
npm install --save-dev sass
把 Table 目录复制到 src,在 App.tsx 引入下:
javascript
import { UserTable } from './Table'
function App() {
return <UserTable users={[
{
name: 'guang',
age: 20,
email: 'guang@guang.com'
},
{
name: 'dong',
age: 18,
email: 'dong@dong.com'
}
]}/>
}
export default App
还要去掉 main.tsx 里的 index.css
跑一下:
arduino
npm run dev
可以看到,Table 组件正常渲染出来了:
当然,这个组件比较简单,你可以让 AI 生成更复杂的组件,或者生成之后让它继续完善。
但流程是一样的。
这样,我们是不是可以写个 CLI 工具,输入组件名、组件描述,让 AI 生成符合规范的代码,然后写入文件呢?
或者像 bolt.new 这样,生成整个项目的代码,然后跑起来呢?
其实都是可以的。
总结
今天我们学了下如何调用 openai 的接口。
我们通过 openai 的 npm 包来调,找一个国内代理商的 API_KEY 就可以了。
messages 就是上下文,可以指定三种 role:user 是用户,assistant 是 AI,system 是预先对 AI做一些设置,比如让它输出的代码都符合某种规范,用什么格式回答等。
这样回答至少是按照我们规定的格式来的。
不符合需求的话可以让 AI 再改,把聊天过程添加到 messages 数组(也就是上下文)就可以了。
提取回答中的代码,主流的方案是正则匹配,但我们今天用了 tools 的方式来提取的代码,它是让 AI 的回答是按照函数调用的方式,生成某种格式的参数。我们再解析参数 json 就可以了。
之后把生成的代码写入磁盘,跑了下试试,能够正常跑。
各种 AI 生成代码的工具其实我们都可以自己做,或者你可以做个 CLI 工具、或者做个 VSCode 插件、或者做个网站,都是可以的。
只会用 AI 工具是不够的,你还得会调 AI 接口,这样才能定制,做一些特定场景的提效工具。
更多内容可以看我的小册《Node.js CLI 通关秘籍》