前端全栈框架实现 - node + koa

71 阅读7分钟

前言

最近学习实并现了一个全栈框架:elpis,这是一个什么框架,有什么用呢? --首先,这是一个中后台系统应用框架,主要技术栈是用vue3 + webpack5 + nodejs + koa + mysql, elpis 的核心设计理念是【领域模型】,现在市面上大部分的前端工作包括后端,基本都是CRUD的重复性工作,消耗了开发者大量的时间精力,从长远角度来看,单纯的CURD开发者一定会被淘汰。但elpis凭借它的设计思想可以沉淀80%的重复性工作,可以让开发者把更多的时间去用作系统框架的迭代开发。

项目结构


│
└── app                                        # 应用源代码
│   │
│   ├── controller                             # controller处理器
│   ├── extend                                 # 服务拓展  
│   ├── middleware                             # 中间件    
│   ├── pages                                  # 页面目录
│   ├── public                                 # 静态目录
│   ├── router                                 # router接口路由分发
│   ├── router-schema                          # router路由规则
│   ├── service                                # service处理器
│   ├── view                                   # 
│   ├── webpack                                # webpack工程化配置
└── .editorconfig.js                           # 代码格式化文件
├── config                                     # 配置文件
└── elips-core                                 # 
│   │
│   ├── loader                                 # 静态文件 =》loader加载器 =》解析到运行时
├── model                                      # 系统配置文件
├── test                                       # 单元测试
├── .eslintignore                              # eslintignore文件
├── .eslintrc.js                               # eslint配置文件
├── .gitignore                                 # gitignore文件
├── index.js                                
├── package.json                               # 项目运行环境及依赖包
├── README.md                                  # 项目说明文件

系统设计

展示层

前端页面展示(CSR/SSR)、webpack工程化配置

BFF层

  • BFF 分为接入层、业务层、服务层 接入层:定义路由规则 业务层:业务逻辑处理、环境配置、服务拓展等 服务层:servier处理器 =》连接数据库

数据层

操作数据库、调用外部服务处理数据,日志生成等

1758612535553.png

架构设计

=》一、基于nodejs实现服务端内核引擎 =》二、基于webpack5完成工程化建设 =》三、基于vue3完成领域模型架构建设 =》四、基于vue3完成动态组件库建设 =》五、完成框架 & npm包封装发布

一、基于nodejs实现服务端内核引擎

elpis-core:基于nodejs + koa实现loader解析器,读取解析位于app目录下对应的业务逻辑文件,实现页面渲染、API校验请求、数据库连接、服务启动等

const Koa = require("koa");
const path = require("path");
const { sep } = path; // 兼容不同操作系统上的斜杆
const env = require("./env");
const middlewareLoader = require("./loader/middleware");
const routerSchemaLoader = require("./loader/router-schema");
const routerLoader = require("./loader/router");
const controllerLoader = require("./loader/controller");
const serviceLoader = require("./loader/service");
const configLoader = require("./loader/config");
const extendLoader = require("./loader/extend");

/**
 * 启动项目
 * @params options 项目配置
 * options = {
 *  name // 项目名称
 *  homePath // 项目首页
 * }
 *
 */
module.exports = {
  start(options = {}) {
    const app = new Koa();

    // 应用配置
    app.options = options;

    // 基础路径
    app.baseDir = process.cwd();

    // 业务文件路径
    app.businessPath = path.resolve(app.baseDir, `.${sep}app`);

    // 初始化环境配置
    app.env = env();
    console.log(`-- [start] env: ${app.env.get()} --`);

    // 加载 middleware
    middlewareLoader(app);
    console.log(`-- [start] load middleware done --`);

    // 加载 routerSchema
    routerSchemaLoader(app);
    console.log(`-- [start] load routerSchema done --`);

    // 加载 controller
    controllerLoader(app);
    // console.log(app.controller);
    console.log(`-- [start] load controller done --`);

    // 加载 service
    serviceLoader(app);
    // console.log(app.service);
    console.log(`-- [start] load service done --`);

    // 加载 config
    configLoader(app);
    // console.log(app.config);
    console.log(`-- [start] load config done --`);

    // 加载 extend
    extendLoader(app);
    // console.log(app);
    console.log(`-- [start] load extend done --`);

    // 注册 elips 全局中间件
    const elipsMiddlewarePath = path.resolve(__dirname, `..${sep}app${sep}middleware.js`);
    const elipsMiddleware = require(elipsMiddlewarePath);
    elipsMiddleware(app);
    console.log(`-- [start] load global elips middleware done --`);

    // 注册业务全局中间件
    try {
      require(`${app.businessPath}${sep}middleware.js`)(app);
      console.log(`-- [start] load global business middleware done --`);
    } catch (e) {
      console.log("[exception] there is no global business middleware file.");
    }

    // 注册路由 --- 需要放在最后
    routerLoader(app);
    console.log(`-- [start] load router done --`);

    // 启动服务
    try {
      const port = process.env.PORT || 8080;
      const host = process.env.IP || "0.0.0.0";
      app.listen(port, host);
      console.log(`Server running at:` + `http://${host}:${port}/`);
    } catch (e) {
      console.log("启动失败:", e);
    }
    return app;
  },
};

二、基于webpack5完成工程化建设

webpack:处理业务文件,经过解析引擎(解析编译、模块分包、压缩优化),最终生成浏览器能识别的资源文件。 在这个项目里,首先会定义一个webpack-base配置,再根据不同环境处理合并,并生成最终对应的webpack配置。

webpack基础配置

{
entry: "",  // 入口配置
module: {}, // 模块解析配置
outpup: {}, // 产物输出路径
resolve: {}, // 配置模块解析的具体行为(定义 webpack在打包时,如何找到并解析具体模块的路径)
plugins:[], // webpack插件
optimization: {}, // 配置打包输出优化(配置代码分割、模块合并、缓存、TreeShake,压缩等优化策略)
}

开发环境

开发服务器基于express实现,并引入webpack-dev-middleware(监控文件改动) 和 webpack-hot-middleware 中间件来实现开发环境热更新

生产环境

1.引入happypack实现多线程打包; 2.引入css-minimizer-webpack-plugin来优化并压缩 css 资源; 3.引入terser-webpack-plugin压缩代码,提升打包构建速度; 4.引入clean-webpack-plugin,实现构建前清空打包目录。

三、基于vue3完成领域模型架构建设

领域模型架构:通过一个模板配置(DSL),最终生成不同类型的页面 模板配置,结构如下:

const config = {
  mode: 'dashboard', // 模板类型, 不同类型模板对应不同的模板数据结构
  name: '', // 模板名称
  title: '', // 模板标题
  desc: '', // 模板描述
  icon: '', // 模板图标
  homePage: '', // 模板首页
  // 头部菜单
  menu: [
    {
      key: '', // 菜单唯一描述
      name: '', // 菜单名称
      menuType: '', // 菜单类型(菜单目录、菜单项) group | module

      // 当 menuType === group
      subMenu: [
        {
          // 可递归的菜单项 menuItem
        },
        // ...
      ],

      // 当 menuType === module
      moduleType: '', // 模块类型: sider | iframe | custom | schema

      // 当 moduleType === schema
      schemaConfig: {
        api: '/api/user', // 数据源 restful api
        schema: { // 板块数据配置
          type: 'object',
          properties: {
            key: {
              ...schema, // 标准 schema 配置
              type: 'string', // 字段类型
              label: '', // 字段中文名
              // 字段在 table 中的相关配置
              tableOption: {
                ...elTableColumnConfig, // 标准 el-table-column 配置
                toFixed: 0, // 数字字段,保留几位小数
                visible: true, // 是否在表格中可见,默认true(false:不展示)
              },
              // 字段在 search-bar 对应的配置
              searchOption: {
                ...elComponentConfig, // 标准 elementui 组件配置
                comType: '', // 配置的组件类型
                default: '', // 默认值
              },
              // 字段在动态组件中的配置,前缀对应componentConfig的键值
              // 如:createFormOption = createForm + Option
              createFormOption: {
                ...elComponentConfig,
                comType: '', // 控件类型,如:input、select单独
                visible: true, // 默认true,(true/false)false不展示
                disabled: false, // 是否禁用(true/false)--是否可编辑

                // comType === 'select' 时,启用
                enumList: [], // 枚举值

                // comType === 'dynamicSelect' 时,启用
                api: '', // dynamicSelect 控件数据源
              },
              // 字段在 editForm 中的配置
              editFormOption: {
                ...elComponentConfig,
                comType: '', // 控件类型,如:input、select单独
                visible: true, // 默认true,(true/false)false不展示
                disabled: false, // 是否禁用(true/false)--是否可编辑

                // comType === 'select' 时,启用
                enumList: [], // 枚举值

                // comType === 'dynamicSelect' 时,启用
                api: '', // dynamicSelect 控件数据源
              },
              // 字段在 detailPanel 中的配置
              detailPanelOption: {
                ...elComponentConfig,
              }
            },
            // ...
          },
          // 必填 字段
          required: [],
        },
        // 表格配置
        tableConfig: {
          // 表格头部按钮
          headerButtons: [
            {
              label: '', // 按钮名称
              eventKey: '', // 事件 key
              // 按钮配置项
              eventOption: {
                // 当 eventKey === 'showComponent' 时,启用 comName,决定调用哪个组件
                comName: '', // 组件名
              },
              ...elButtonConfig, // 标准的el-button 配置项
            },
            // ...
          ],
          // 表格行内按钮
          rowButtons: [
            {
              label: '', // 按钮名称
              eventKey: '', // 事件 key
              eventOption: {
                // 当 eventKey === 'showComponent' 时,启用 comName,决定调用哪个组件
                comName: '', // 组件名

                // 当 eventKey === 'remove'(add、remove、update、read)
                params: {
                  // paramKey = 参数的键值
                  // rowValueKey = 参数值(格式:schema::tableKey时,从table中查找传值的值)
                  paramKey: rowValueKey,
                }
              }, // 按钮配置项
              ...elButtonConfig, // 标准的el-button 配置项
            },
            // ...
          ]
        },
        // search-bar 搜索配置
        searchConfig: {},
        // 动态组件配置
        componentConfig: {
          // 新增 form表单 组件配置
          createForm: {
            title: '', // 表单标题
            saveBtnText: '', // 保存按钮文案
          },
          // 编辑 form 表单
          editForm: {
            mainKey: '', // 单条数据唯一标识-主键
            title: '', // 组件标题
            saveBtnText: '', // 保存按钮文案
          },
          // detail-panel 查看单条详情
          detailPanel: {
            mainKey: '', // 单条数据唯一标识-主键
            title: '', // 组件标题
          }
        },
      },

      // 当 moduleType === custom
      customConfig: {
        path: '', // 自定义模块路径
      },

      // 当 moduleType === iframe
      iframeConfig: {
        path: '', // iframe 模块路径
      },

      // 当 moduleType === sider
      siderConfig: {
        menu: [
          {
            // 可递归的菜单项 menuItem ( moduleType !== sider)
          },
          // ...
        ],
      }
    }
  ]
}

module.exports = config;

在这份模板配置中,通过配置可以实现如iframe嵌入页面自定义页面开发、但一个中后台系统最多的都是表格和查询组件,也就是配置中的schema类型。 基于这份模板配置,开发了顶部动态菜单侧边栏菜单element表格组件条件查询组件,可以通过配置自动生成对应功能。

四、基于vue3完成动态组件库建设

在第三章节中的模板配置中,有涉及到动态组件的配置,比如条件查询组件中,有很多的子组件(input/select/dateRange/textarea等),那具体怎么实现呢? 1.首先基于element-plus,比如把el-input封装成我们自定义的组件,并通过v-bind实现el-input属性的透传,再通过配置文件引入

import input from "./input.vue";


const SearchItemConfig = {
  input: {
    component: input,
  },

};
export default SearchItemConfig ;

2.通过动态引入input组件

<component :is="SearchItemConfig[配置模板中自定义的模板名]?.component" />

完成框架 & npm包封装发布

这样,一个轻量简易的全栈框架就搭建完成了,如果想要发布到npm上,可以参考下这篇文章: juejin.cn/spost/75528…

附上npm地址:点击