• 使用中台 Admin.Core 实现了一个Razor模板的通用代码生成器


    前言

    前面使用 Admin.Core 的代码生成器生成了通用代码生成器的基础模块 分组,模板,项目,项目模型,项目字段的基础功能,本篇继续完善,实现最核心的模板生成功能,并提供生成预览及代码文件压缩下载

    准备

    首先清楚几个模块的关系,如何使用,简单画一个流程图

    image

    前面完成了基础的模板组,模板管理,项目,模型,字段管理,都是由 Admin.Core 框架的代码生成器完成,感兴趣的可参考前篇使用,文末也会给出仓库地址,有问题欢迎交流

    image

    本文主要分享项目代码的生成,先放出效果图

    • 项目生成管理,支持多模板组

    image

    • 模板生成预览,预览页可以直接编辑模板

    image

    • 模板生成,将生成压缩包并下载

    image

    实现

    需要实现上面效果的代码生成器,基础的增删改查都可以借由代码生成器生成,也写过几篇了这里就不再赘述,我们只需要关注核心部分:如何生成?如何预览?如何下载?

    根据模板生成对应项目的代码文件

    当我们有了一个模板(模板内容),也有了对应的配置项(项目&项目模型&项目字段),中间肯定是需要一个模板引擎来将模板根据配置项解析出来的。

    市面上有很多模板引擎,比如 ejs,art 等,但既然是搞C#的,那不妨试试看 Razor (之前的Admin.Core代码生成器也是基于razor模板引擎,也可以换其他的或者支持多种模板引擎),C#的语法写起来还是挺舒服的,并且其实还可以新建一个.net core 的项目添加页面后可以复制模板和模型到页面为自己的模板增加智能提示

    模板引擎的使用

    • 项目中引用包:RazorEngine.NetCore

      • 原版只支持 framework,大佬打包的 netcore 版本
    <ItemGroup>
      <PackageReference Include="RazorEngine.NetCore" Version="3.1.0" />
    ItemGroup>
    
    • 使用方式:指定模型,内容,模板名称即可
    var code="模板内容";
    var key="模板名";
    //模型名称
    var model=new DevProjectRazorRenderModel();
    RazorEngine.Engine.Razor.RunCompile(new LoadedTemplateSource(code), key, model.GetType(), model);
    
    • 模板内容的写法

      • 我这里定义了固定模型,所以需要先什么模型的变量,这里指定了 gen
      • 模板语法文档
    @{
    var gen = Model as ZhonTai.Module.Dev.DevProjectRazorRenderModel;
    }
    
    • 这里定义了一个公共的项目模型,后续增加项目模型字段的信息都无需更搞代码,生成即可,另外也可以再属性模型中添加字典等属性,即可灵魂的再模板中使用动态配置了
    //模型渲染
    var gen = new DevProjectRazorRenderModel()
    {
        Project = Mapper.Map<DevProjectGetOutput>(project),
        Model = Mapper.Map<DevProjectModelGetOutput>(model),
        Fields = Mapper.Map<List<DevProjectModelFieldGetOutput>>(modelFields),
    };
    

    模板内容的生成

    • 这里分为了两部分,一个是文件路径,一个是文件内容
    • 因为要生成的路径可能也会包含一些模块或者模型的信息,所以可以将模板路径也使用模板生成,这里直接拼接一个模板即可
    var pathCodeText = @"
    @{
    var gen = Model as ZhonTai.Module.Dev.DevProjectRazorRenderModel;
    }
    " + outPath;
    //转换路径
    var outPath = RazorCompile(gen, $"{project.Code}_{model.Code}_{tpl.Name}_Path.tpl", pathCodeText).Trim();
    

    文末附完整代码

    内容文件的下载

    • 因为是多模板组多模板,所以每次生成项目代码都基本是多个文件,一个个下载很明显不合理,所以可以将所有代码内容文件打包成压缩包进行下载,下面是核心压缩代码,无需引用包,暂时只在Windows中测试使用
    • 完整代码如下,做了一些文件和文件夹的判断处理,可自行封装
    • 通过GenerateAsync获取到文件信息后写入文件,再将目录进行打包返回即可
    /// 
    /// 下载
    /// 
    /// 
    /// 
    [HttpPost]
    public async Task DownAsync(DevProjectGenGenerateInput input)
    {
        var path = Path.Combine(AppContext.BaseDirectory, "DownCodes", DateTime.Now.ToString("yyyyMMddHHmmss"));
        var zipFileName = $"源码{DateTime.Now.ToString("yyyyMMddHHmmss")}.zip";
        var zipPath = Path.Combine(AppContext.BaseDirectory, "DownCodes", zipFileName);
        try
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            //获取内容信息
            var codes = await GenerateAsync(input);
            foreach (var code in codes)
            {
                var codePath = Path.Combine(path, code.Path);
                var directory = Path.GetDirectoryName(codePath);
                if (!Directory.Exists(directory))
                {
                    Directory.CreateDirectory(directory);
                }
                if (!File.Exists(codePath))
                {
                    using (var fs = File.Open(codePath, FileMode.Create, FileAccess.ReadWrite))
                    {
                        await fs.WriteAsync(Encoding.UTF8.GetBytes(code.Content));
                    }
                }
            }
            ZipFile.CreateFromDirectory(path, zipPath);
            var bytes = await File.ReadAllBytesAsync(zipPath);
            return new FileContentResult(bytes, "application/zip")
            {
                FileDownloadName = zipFileName
            };
        }
        finally
        {
            if (Directory.Exists(path))
            {
                Directory.Delete(path, true);
            }
            if (File.Exists(zipPath))
            {
                File.Delete(zipPath);
            }
        }
    }
    

    前端对应需要支持文件下载,可以修改为生成对应下载URL,用GET请求直接打开新窗口进行下载

    项目中前端页面的重点

    整个框架主要使用者肯定是.net开发,如果没有写过 vue 项目,写起来的时候可能会有一些吃力,但因为现在有了代码生成器,大部分代码都可以生成,也可以做参考,所以这里只做一些关键点的说明

    框架页面菜单的添加说明:具体可参考前文进行创建

    • 系统管理-添加视图->指定 vue 页面文件的路径,可再权限菜单中复用这个视图生成不同的路由地址
    • 接口管理-同步接口->将后端服务映射为权限点,对应后端功能权限
    • 权限管理-添加权限->添加菜单,功能点
    • 用户-角色->通过角色分配角色权限,用户管理角色

    Vue 文档

    有时间的话至少过一遍再开始,磨刀不误砍柴工

    生命周期的一定花时间看看:官方文档

    页面中获取路由信息与页面跳转

    以项目生成页面调整到预览页面为例

    image

    • 项目生成页和预览页引入定义
    //引入路由
    import { useRoute, useRouter } from 'vue-router'
    
    //路由信息
    const route = useRoute()
    //路由跳转
    const router = useRouter()
    
    • 跳转到预览页
      router.push({
        path: '/dev/dev-project-gen/preview', query: {
          projectId: row.projectId,
          groupIds: row.groupIds_Values
        }
      })
    
    • 预览页获取生成列表传递的参数
    //从路由中获取query参数
    onMounted(() => {
      state.filter.projectId = route.query.projectId
      state.filter.groupIds = route.query.groupIds
    })
    

    左侧树/列表右侧预览实现

    将左侧的列表列封装为一个组件,右侧如果简单可封装,也可以直接写到预览页面

    这里参考框架中用户列表做的

    <my-layout class="my-layout">
        <pane size="30" min-size="20" max-size="35">
          <div class="my-flex-column w100 h100">
            <group-template-menu :groupIds="state.filter.groupIds" :projectId="state.filter.projectId"
              @node-click="onNodeClick" select-first-node>group-template-menu>
          div>
        pane>
        <pane size="70" v-loading="state.loading">
          <div class="my-flex-column w100 h100">
              内容预览部分
          div>
        pane>
      my-layout>
    

    当然,封装了组件记得引入用到的组件

    const GroupTemplateMenu = defineAsyncComponent(() => import('./components/dev-group-template-menu.vue'))
    const MyLayout = defineAsyncComponent(() => import('/@/components/my-layout/index.vue'))
    

    组件中获取传递的参数示例

    interface Props {
      modelValue: number[] | null | undefined
      selectFirstNode: boolean,
      projectId: number,
      groupIds: number[]
    }
    
    const props = withDefaults(defineProps<Props>(), {
      modelValue: () => [],
      selectFirstNode: false,
      projectId: 0,
      groupIds: () => []
    })
    

    在预览左侧菜单中,我们可以看到一个标记的小图标,用来直接编辑模板

    image

    这个弹窗其实是直接引用了编辑模板的组件

    <template>
    ...
    <dev-template-form ref="devTemplateFormRef" :title="'编辑模板'">dev-template-form>
    ...
    template>
    <script>
    ...
    // 引入组件
    const DevTemplateForm = defineAsyncComponent(() => import('../../dev-template/components/dev-template-form.vue'))
    //使用
    const devTemplateFormRef = ref()
    const editTemplate = (node, data) => {
      devTemplateFormRef.value.open({
        id: data.id
      })
    }
    ...
    script>
    
    defineExpose({
      open,
    })
    

    如上可以看到我们使用和组件同名的 const devTemplateFormRef = ref() 即可获取到组件引用

    另外调用的方法可以查看 dev-template-form.vue 是开放了方法的

    defineExpose({
      open,
    })
    

    下载压缩包文件

    首先需要后端返回文件流,然后调用对应接口的时候指定format格式为blob,并创建下载连接点击即可

    const genCode = async (row: DevProjectGenGetOutput) => {
      new DevProjectGenApi().down({ projectId: row.projectId, groupIds: row.groupIds_Values?.map(s => Number(s)) }, {
        loading: false,
        showErrorMessage: false,
        format: 'blob'
      })
        .then((res) => {
          const a = document.createElement('a');
          a.href = URL.createObjectURL(res as Blob);
          a.download = '源码.zip';
          a.click();
        });
    }
    

    后语

    本文所有代码皆在 yimogit/Emo.Dev 仓库中可以找到,觉得有用的来个 Star 吧

    基于前面代码生成器生成的功能模块上,周末花了两天完善项目生成,终于算是搞定

    后面还需要逐步完善生成器,欢迎点个赞,留个言,交流指点一二

    相关文档

  • 相关阅读:
    聊聊Vim的工作原理
    Vue项目的记录(五)
    可编程 USB 转串口适配器开发板芯片驱动文件说明
    trie树(零)零基础入门
    二叉排序树(BTS树)
    大量短信群发?不妨来看看这几个平台
    Flink Dynamic Tables
    suse10sp1编译glib-2
    clamav杀毒
    面试经典 150 题 14 —(数组 / 字符串)— 134. 加油站
  • 原文地址:https://www.cnblogs.com/morang/p/18294868/zhontai_admin_core_module_dev_common