Skip to content

本文例举利用 AI 生成模板代码,探索 AI 生成重复性高、逻辑性低的代码来减少重复劳动的可行性。文中使用的编程工具为 Cursor。

代码质量测评 ​

首先肯定要去验证一下,AI 生成的代码是否可靠、实用。刚好我有个祖传博客代码需要重构,可以通过 AI 来生成一下其中的文章模块。

在 Cursor 编辑器中创建 app.js,按住 command + K 输入 prompt。

这里我写 prompt 习惯是,我要用框架/库实现什么功能,其中的细节是什么。这样描述基本能让 AI 理解你的需求

创建 express 服务 ​

利用express启动一个服务

01.gif

javascript
const express = require('express'); // 引入express模块
const app = express(); // 创建express实例

app.listen(3000, () => { // 监听3000端口
  console.log('Server started on port 3000');
});

第二步连接数据库,因为这里可能涉及到改动原先的代码,所以可以先全选代码,再输入 prompt,编辑器会在原代码上做修改。

连接 mongo,database 为 blog

02.gif

javascript
const express = require('express'); // 引入express模块
const mongoose = require('mongoose'); // 引入mongoose模块
const app = express(); // 创建express实例

mongoose.connect('mongodb://localhost/blog', { useNewUrlParser: true }); // 链接mongo,database为blog

app.listen(3000, () => { // 监听3000端口
  console.log('Server started on port 3000');
});

最终成功启动服务

image.png

创建文章 model ​

用 mongoose 创建文章 schema
包含标题、内容、摘要,类型自行判断
timestamps 为 true
最后导出

04.gif

javascript
const mongoose = require('mongoose');

const articleSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true
  },
  content: {
    type: String,
    required: true
  },
  summary: {
    type: String,
    required: true
  },
}, { timestamps: true });

module.exports = mongoose.model('Article', articleSchema);

其中我提到了“类型自行判断”,实际上并不需要,因为在你没提到的内容,它会自行给到它推断合理的代码。不会因为你不说,就不会生成。但是对于一些特定的参数,比如 timestamps,就需要额外指定。

创建文章 controller ​

创建 controller 需要导入之前写的 model,但是编辑器不会跨文件解析。这时你可以在 prompt 说明在哪个文件中导入叫哪个名字的 model,甚至可以直接把代码贴在 prompt 中

// model.js 代码....

根据以上文章model代码,导入并生成文章的增删改查、获取文章列表的controller
javascript
const Article = require('./model');

exports.createArticle = async (req, res) => {
  try {
    const article = await Article.create(req.body);
    res.status(201).json(article);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.getArticleById = async (req, res) => {
  try {
    const article = await Article.findById(req.params.id);
    if (!article) {
      return res.status(404).json({ error: 'Article not found' });
    }
    res.json(article);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.updateArticle = async (req, res) => {
  try {
    const article = await Article.findByIdAndUpdate(req.params.id, req.body, { new: true });
    if (!article) {
      return res.status(404).json({ error: 'Article not found' });
    }
    res.json(article);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.deleteArticle = async (req, res) => {
  try {
    const article = await Article.findByIdAndDelete(req.params.id);
    if (!article) {
      return res.status(404).json({ error: 'Article not found' });
    }
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.getArticles = async (req, res) => { // Adding a new controller to get all articles
  try {
    const articles = await Article.find();
    res.json(articles);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

以上代码大致还行,看起来能跑,但是太简单了。获取文章列表没有分页等功能,当然也是我的 prompt 写得不够详细。下面再优化一下

选中 getArticles 方法

修改getArticles,支持分页查询和文章标题模糊搜索

05.gif

javascript
exports.getArticles = async (req, res) => { // Adding a new controller to get all articles
  try {
    const { page = 1, limit = 10, title } = req.query;
    const query = {};
    if (title) {
      query.title = { $regex: title, $options: 'i' };
    }
    const articles = await Article.find(query)
      .limit(limit * 1)
      .skip((page - 1) * limit)
      .exec();
    const count = await Article.countDocuments(query);
    res.json({
      articles,
      totalPages: Math.ceil(count / limit),
      currentPage: page
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

还行吧。。至少说明有足够详细的 prompt 就可以生成对应的功能代码。

注册路由 ​

最后创建 routes.js 注册文章路由。这次我描述一下 controller 里面的内容

从controller.js导入 createArticle、getArticleById、updateArticle、deleteArticle、getArticles方法
注册相应的express路由
javascript
const express = require('express');
const router = express.Router();
const { createArticle, getArticleById, updateArticle, deleteArticle, getArticles } = require('./controller');

router.post('/', createArticle);
router.get('/:id', getArticleById);
router.put('/:id', updateArticle);
router.delete('/:id', deleteArticle);
router.get('/', getArticles);

module.exports = router;

最后把路由挂载在 express 上,顺便注册插件解决一下 json 参数问题

注册article文件夹下的routes
注册json插件

06.gif

javascript
const express = require('express'); // 引入express模块
const mongoose = require('mongoose'); // 引入mongoose模块
const app = express(); // 创建express实例
const articleRoutes = require('./article/routes'); // 引入article文件夹下的routes
const bodyParser = require('body-parser'); // 引入body-parser模块

mongoose.connect('mongodb://localhost/blog', { useNewUrlParser: true }); // 链接mongo,database为blog

app.use(bodyParser.json()); // 注册json插件
app.use('/article', articleRoutes); // 注册article文件夹下的routes

app.listen(3000, () => { // 监听3000端口
  console.log('Server started on port 3000');
});

总结 ​

验证一下接口是否可用

创建文章

07.png

获取列表

image.png

获取详情

image.png

更新文章

image.png

当然以上的功能还是不完善的,比如还缺少权限认证,但只要你有相应的 prompt,就可以生成对应的代码,因为篇幅关系就不展示了。以上的测评至少可用说明 AI 生成的代码是可用的。

但是以上的代码还是太太太简单了,远远达不到生产代码,只能当做花瓶。要达到能在生产使用的模板代码,还需要做些额外的操作。

模板代码的生成 ​

先思考一下,作为前端写过最多的 UI 界面是什么?如果是 B 端开发,围绕着各种管理后台,肯定绕不过表格表单。

我在 3 年前写过一个早期的低代码平台,通过拖拖拽拽生成 UI 代码。

image.png

这种在当时看起来还是比较好的生成代码方案,但也存在几个致命的问题

  1. 依赖于框架

比如上述平台生成的是 React 代码,换了工作用了 Vue,那基本就废弃了。再写一套模板的工作量是很大的

  1. 依赖于业务

假如换了另一套业务,上面的 UI 也不太适用了,得重新编写物料组件,同样工作量不小

  1. 难以涉及简单逻辑

比如表格中的操作按钮,可能会有链接、删除、打开某个弹窗,即便是简单逻辑也很难去编写、配置

所以换了工作就没动力维护了

等到 AI 出现能处理代码,我觉得这一切都可行了,毕竟 AI 就是一个天然庞大的物料组件库。

简单 UI 模板 ​

以 Vue3 + ant-design-vue 为例,先写一个 Table 组件的基础模板,通过描述表格项 prompt 来让 AI 帮我们生成。

javascript
<template>
    <a-table
        :data-source="dataSource"
        :columns="columns">
    </a-table>
</template>
<script setup>
import { ref } from 'vue';

const dataSource = ref([]);

const columns = [];
</script>
生成学号、姓名、性别、出生年月、操作这5项。
出生年月格式化年月日形式
操作包含查看详情、删除

12.gif

javascript
<template>
  <a-table
    :data-source="dataSource"
    :columns="columns"
  >
    <template #birth="{ text }">
      {{ new Date(text).toLocaleDateString() }}
    </template>
    <template #action>
      <a @click="handleView">查看详情</a>
      <a-divider type="vertical" />
      <a @click="handleDelete">删除</a>
    </template>
  </a-table>
</template>

<script setup>
import { ref } from 'vue';

const dataSource = ref([
  {
    id: 1,
    name: 'John Doe',
    gender: 'Male',
    birth: '1990-01-01',
  },
  {
    id: 2,
    name: 'Jane Doe',
    gender: 'Female',
    birth: '1995-05-05',
  },
]);

const columns = [
  {
    title: '学号',
    dataIndex: 'id',
  },
  {
    title: '姓名',
    dataIndex: 'name',
  },
  {
    title: '性别',
    dataIndex: 'gender',
  },
  {
    title: '出生年月',
    dataIndex: 'birth',
    slots: { customRender: 'birth' },
  },
  {
    title: '操作',
    slots: { customRender: 'action' },
  },
];

function handleView() {
  // handle view logic here
}

function handleDelete() {
  // handle delete logic here
}
</script>

生成的代码是可运行的,还帮你 mock 了数据,但还有小问题。其中 column.slots 已经被新版的 antdv 废弃了,虽然能用但我还是想用最新的写法。其次我希望用 dayjs 库来格式化时间。于是来修改一下模板

javascript
<template>
    <a-table
        :data-source="dataSource"
        :columns="columns">
+        <template #bodyCell="{ column, record }">
+        </template>
    </a-table>
</template>
<script setup>
import { ref } from 'vue';

const dataSource = ref([]);

const columns = [];
</script>
生成学号、姓名、性别、出生年月、操作这5项。
其中出生年月是自定义项,在表格中用dayjs格式化年月日形式
操作是自定义项,包含查看详情、删除
javascript
<template>
  <a-table
    :data-source="dataSource"
    :columns="columns"
  >
    <template #bodyCell="{ column, record }">
      <template v-if="column.key === 'birth'">{{ dayjs(record[column.key]).format('YYYY-MM-DD') }}</template>
      <template v-else-if="column.key === 'action'">
        <a-button type="link" @click="handleView(record)">查看详情</a-button>
        <a-button type="link" @click="handleDelete(record)">删除</a-button>
      </template>
      <template v-else>{{ record[column.key] }}</template>
    </template>
  </a-table>
</template>

<script setup>
import { ref } from 'vue';
import dayjs from 'dayjs';

const dataSource = ref([
  {
    id: 1,
    name: '张三',
    gender: '男',
    birth: '1990-01-01',
  },
  {
    id: 2,
    name: '李四',
    gender: '女',
    birth: '1995-05-05',
  },
]);

const columns = [
  {
    title: '学号',
    dataIndex: 'id',
    key: 'id',
  },
  {
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: '性别',
    dataIndex: 'gender',
    key: 'gender',
  },
  {
    title: '出生年月',
    dataIndex: 'birth',
    key: 'birth',
  },
  {
    title: '操作',
    key: 'action',
  },
];

const handleView = (record) => {
  // handle view logic here
};

const handleDelete = (record) => {
  // handle delete logic here
};
</script>

这样就符合我的要求了。其实即便没有模板,AI 也能给你生成像样的代码。这里模板代码的作用就是限制 AI 在你的模板上生成符合你的要求的代码。不过你也可以不用修改模板,或者补充 prompt 添加例如 不需要column.slots等更明确的说法,也能达到相同的效果。

逻辑生成 ​

可能有人会说,上面的代码生成,我复制粘贴也可以,能省多少事?当然不止如此,同时我们也可以让 AI 去生成简单逻辑。比如查看详情按钮可以跳转学生的详情页,删除按钮弹出确认提示。修改下 prompt

查看详情点击后使用 vue-router 跳转学生详情页面
点击删除弹窗提示
其他不用修改

13.gif

javascript
<template>
  <a-table
    :data-source="dataSource"
    :columns="columns"
  >
    <template #bodyCell="{ column, record }">
      <template v-if="column.key === 'birth'">{{ dayjs(record[column.key]).format('YYYY-MM-DD') }}</template>
      <template v-else-if="column.key === 'action'">
        <a-button type="link" @click="handleView(record)">查看详情</a-button>
        <a-button type="link" @click="handleDelete(record)">删除</a-button>
      </template>
      <template v-else>{{ record[column.key] }}</template>
    </template>
  </a-table>
</template>

<script setup>
import { ref } from 'vue';
import dayjs from 'dayjs';
import { useRouter } from 'vue-router';
import { Modal } from 'ant-design-vue';

const dataSource = ref([
  {
    id: 1,
    name: '张三',
    gender: '男',
    birth: '1990-01-01',
  },
  {
    id: 2,
    name: '李四',
    gender: '女',
    birth: '1995-05-05',
  },
]);

const columns = [
  {
    title: '学号',
    dataIndex: 'id',
    key: 'id',
  },
  {
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: '性别',
    dataIndex: 'gender',
    key: 'gender',
  },
  {
    title: '出生年月',
    dataIndex: 'birth',
    key: 'birth',
  },
  {
    title: '操作',
    key: 'action',
  },
];

const router = useRouter();

const handleView = (record) => {
  router.push(`/student/${record.id}`);
};

const handleDelete = (record) => {
  Modal.confirm({
    title: '确认删除',
    content: `确定要删除学号为 ${record.id} 的学生吗?`,
    onOk() {
      // handle delete logic here
    },
  });
};
</script>

基本实现了简单逻辑,如果需要更多逻辑编写,只要描述对应的 prompt 即可。

代码识别 ​

在真实的开发环境中,其实没有这么多“demo”型的基础代码,更多是封装过的。那 AI 是否能理解这些代码,并能在这基础上生成我们需要的代码?

下面写了一个自定义组件,并且用了一个自定义 hooks,同样的需求看是否能生成。

javascript
<template>
  <my-table
    :data="data"
    :loading="loading"
    :columns="columns"
    :pagination="pagination"
    @change="handleTableChange">
    <template #bodyCell="{ column, record, index }">
          <template v-if="column.dataIndex === 'action'">
              <span>
                  <a>操作</a>
              </span>
          </template>
      </template>
  </my-table>
</template>

<script setup>
import { ref } from 'vue';
import useTable from '@/hooks/useTable';

const data = ref([]);
const columns = [];

const {
    pagination,
    loading,
    handleTableChange
} = useTable();

</script>
生成学号、姓名、性别、出生年月、操作这5项。
mock2组数据
操作是自定义项,包含查看详情、删除
表格组件的change事件会传递页码信息,现在需要在当前方法上再封装一层打印页码
javascript
<template>
  <my-table
    :data="data"
    :loading="loading"
    :columns="columns"
    :pagination="pagination"
    @change="handleTableChangeWithLogging">
    <template #bodyCell="{ column, record, index }">
      <template v-if="column.dataIndex === 'action'">
        <span>
          <a @click="handleViewDetail(record)">查看详情</a>
          <a @click="handleDelete(record)">删除</a>
        </span>
      </template>
    </template>
  </my-table>
</template>

<script setup>
import { ref } from 'vue';
import useTable from '@/hooks/useTable';

const data = ref([
  {
    id: '001',
    name: 'John Doe',
    gender: 'Male',
    birth: '1990-01-01'
  },
  {
    id: '002',
    name: 'Jane Doe',
    gender: 'Female',
    birth: '1995-05-05'
  }
]);
const columns = [
  {
    title: '学号',
    dataIndex: 'id'
  },
  {
    title: '姓名',
    dataIndex: 'name'
  },
  {
    title: '性别',
    dataIndex: 'gender'
  },
  {
    title: '出生年月',
    dataIndex: 'birth'
  },
  {
    title: '操作',
    dataIndex: 'action'
  }
];

const {
  pagination,
  loading,
  handleTableChange
} = useTable();

const handleViewDetail = (record) => {
  console.log(`View detail of record with id ${record.id}`);
};

const handleDelete = (record) => {
  console.log(`Delete record with id ${record.id}`);
};

const handleTableChangeWithLogging = (pagination, filters, sorter) => {
  console.log(`Current page: ${pagination.current}`);
  handleTableChange(pagination, filters, sorter);
};
</script>

这次提出了 4 个需求,1-3 是都可以直接实现的,至少 AI 能通过变量命名和模板代码示例能理解它是做什么的。但是对于第 4 个需求,想打印页码,试了几次终于写出了合理 prompt 让它帮我生成了对应的方法。从这里开始就有负担了(太难写了)我得描述得很清楚,如果想实现其他功能,我可能要把这个 my-table 组件的文档贴给它。

所以我们需要充足的 prompt 库。

总结 ​

通过 AI 生成模板代码的效果总体令人差强人意,但是在我这么多次实验下来,还是展露了一些问题。比如同样的 prompt 生成的代码略微都有不相同之处。相比于之前提到的低代码平台生成的模板代码,低代码平台能生成更稳定、一致的代码。而 AI 生成的,还是可能得需要我们去微调,无法保证生成即可使用。

其实主要的原因还是在于我们的 prompt 描述得不够细致,无法让 AI 理解我们的具体需求,以至于无法让 AI 限定生成我们想要的代码。所以如果真正要实现完全靠 AI 生成模板代码,则需要一个庞大的 prompt 库的支持。

拓展 ​

除了基础代码,其实还可以直接通过 AI 生成应用层面的东西。比如这个邮件

1681634650983.png

当然现在这种图片肯定不是靠手动画出来的,一般会有一个创作平台,有一套基础模板,然后妥妥拽拽生成想要的排版,再导出图片。

既然有了基础模板,那同理 AI 也可以直接生成,甚至你可以指挥它修改。

首先我也写一个简单的模板,大概这样

image.png

差不多就行。然后把模板和文章内容交给 AI,让它自行解析文章,匹配模板返回 html 代码。

下面是一段模板html代码
主标题
小标题
正文
链接

客服POPO:XXXXX

``` 给你一段文章内容,自行解析文章结构匹配模板html代码 检测到是主标题,输出 div.title 检测到是小标题,输出 div.sub-title 检测到是正文文字,输出 div.content 检测到图片地址,输出 img.img 宽度 100% 检查到链接并附带链接地址,输出a.link 检测到是联系方式,输出div.contact 文章内容如下 ``` AIGC大赛开始啦 https://static.zzh.cool/20180726213434.jpg?imageView2/2/w/400/q/75 URS致力于为业务团队提供安全稳定的帐号服务。网易邮箱一直以来都是URS的重点合作对象,2021年URS紧跟邮箱需求,进行了多次深度合作,协助邮箱在帐号登录风控和修复率提升等方面取得了诸多成果。在2022年URS将继续保持开放共享的态度,与业务共同发展,达成更高的使命和愿景 匠心卓越奖 DDoS防护平台 安全BP始终和业务在一起,打通业务和安全的最后一公里; 不断深入业务,帮助业务评估安全风险和解决安全难题; 持续赋能业务,致力于业务安全体系建设与合规发展。 https://static.zzh.cool/20180726213405.jpg?imageView2/2/w/400/q/75 业务安全合作伙伴建设 提前布局,实现IPv6防御,满足游戏小小云需求知己知彼,紧跟攻击趋势,给业务稳稳的安全感精益求精,解决业务痛点,落地大数据智能防御 https://static.zzh.cool/20180726213336.jpg?imageView2/2/w/400/q/75 https://static.zzh.cool/20180726213336.jpg?imageView2/2/w/400/q/75 点击申报[www.test.com] 若您有产品使用问题,请联系 客服POPO:123456 产品反馈群:123456 ``` ```

返回结果:

image.png

大致差不多,还有一点点瑕疵,再微调一下

修改以上代码
“匠心卓越奖”改成小标题,设置红色
最后2张图片宽度改为49%,同一排展示
“若您有产品使用问题,请联系”这句话居中显示,与上面间距20像素
其他不变

最终结果

image.png

对于没有 html 基础的人来说,还算是方便的。

甚至不仅仅限制于代码,能让程序去完成的重复性劳动同样也能靠简单的语言指挥 AI 去做。比如结合命令行,输入 “删除某某文件” 这样的任务,让 AI 去理解你的意图,如把 delete filename 等关键字写入你提供的脚本,然后自动执行就可以指挥它做你想要做的事。

总结 ​

与传统拼模板字符串的低代码工具相比,AI 生成的代码不局限于框架和语言,无论是 Vue 还是 React,无论是 js 还是 java 都能给到你想要的结果。但是低代码工具能生成非常标准且一致的代码,AI 则要更准确、详细的 prompt,不然生成的代码也会略有差别,还需要手动去调整。

与同类型产品 Copilot 相比,通过 GPT 模型生成通用且长的代码块更具有优势。但是在复杂逻辑上,开发者需要花费精力去表达逻辑,思考想要的东西,编写详尽的 prompt。即便如此,也并非一定能生成开发者想要的代码。但是 Copilot 没有这样的心智负担,它自己能通过代码上下文去理解开发者意图,再去给开发者提出建议。日常开发中能把两者结合使用,应该能提高很大的开发效率。

最后从上述的测评、测试来看,AI 生成的代码是可用但不是完美的。不过我们不用专门花心思去写某个功能的物料,只要平日多用多记录,把成功案例的 prompt 记录下来。等到下次再遇到类似的功能,就可以把之前的 prompt 拿过来做点修改就可以使用、生成代码了。