elpis之学习总结

elpis之学习总结

为什么需要elpis

在日常开发中,随着客户的需求越来越多,我们不得不陷入重复的crud 工作,拿着已经封装好的组件cv之后修修补补又是一个新的组件。这就是绝大多数前端开发者所遇到的困境。为了解决这个问题,elpis 应运而生。elpis能够解决什么问题呢?

  1. 沉淀80%的重复工作(支持配置化),提供20%的定制开发
  2. 支持多个入口,一个入口即一个平台
  3. 系统间功能重复使用
  4. 支持SSR
  5. 全栈开发,引入BFF、devops等流程

elpis-core

elpis-core 是elpis框架服务端的启动引擎 ,它利用koa 作为底层,通过loader 的方式结合约定式目录 将每一个目录中的文件都转换为一个对象挂载到koa实例。如图所示:

elpis-core的运行时

elpis-core的运行时是一个洋葱圈模型,如图所示:

工程化

随着前端项目越发庞大,VueReact等工具应运而生,但是浏览器只认识HTML、CSS、JavaScript。那怎么办呢?我们就需要一种工程化工具将浏览器不认识的文件如.vue、.jsx文件等转换成浏览器认识的JavaScript(这类工具可以是webpack、vite、rollup等等等等)。

如图,工程化工具在以上流程当中充当的就是解析引擎的角色。

热更新原理

在开发环境中,不仅需要走完解析引擎编译、打包、压缩这一系列流程之外,为了能够实时感知到代码变化后页面的变化,我们需要引入一个服务器,这个服务器需要具备以下两个能力:

  1. 监听能力 -> 能够实时监听到文件的变化
  2. 通知能力 -> 在监听到文件变化后通知浏览器刷新页面

除此以外,与生产环境不同的是,开发环境最终只会产出模版文件(比如.tpl文件),剩余的资源文件(如js、css)将会以内存的方式存在于服务器当中,这种方式的好处是节省将文件写入时间,提高开发效率。

性能优化

  • 构建方面
  1. 利用happypackthread-loader多线程打包,提升构建速度
  2. 使用loader解析对应文件时排除掉非必要依赖(node_modules)
  3. 利用缓存提升构建速度
  • 代码方面
  1. 利用mini-css-extract-plugin插件将css单独抽离
  2. 利用terser-webpack-plugin插件对代码进行压缩
  3. 利用optimization.splitChunks对代码进行分包,将长期不变的node_modules单独分包
  4. 利用tree-shaking树摇摇掉未使用的代码依赖

领域模型

什么是领域模型

领域模型 又称dsl ,是一份通用的基础配置,通过领域模型,我们可以以其为基础,派生出很多份配置,以面向对象 的角度来谈,领域模型就是一个基类 ,通过这个基类,我们可以利用继承 的思想派生出无数个子类。比如在电商领域,我们可以定义一个基类,然后通过继承便可以派生出诸如淘宝拼多多抖音电商等站点。

领域模型的定义

利用json-schema的定义规范对字段进行描述,如下:

js 复制代码
export default {
    // 模板类型,不同模板类型对应不一样的模板数据结构
    mode: 'dashboard',
    name: '', // 名称
    desc: '', // 描述
    icon: '', // 图标
    homePage: '', //首页(项目配置)
    // 头部菜单
    menu: [
        {
            key: '', // 菜单唯一描述
            name: '', // 菜单名称
            menuType: '', // 枚举值, group / module
            // 当menuType = group 时, 可填
            subMenu: [
                {
                    // 可以递归menuItem
                }
            ],
            // 当menuType = module 时, 可填
            moduleType: '', // 枚举值 schema/custom/iframe/sider
            // 当moduleType = schema 时, 可填
            schemaConfig: {
                api: '', // 数据源api 遵循RESTful规范
                // 板块数据结构
                schema: {
                    type: 'object',
                    properties: {
                        key: {
                            ...schema, // 标准schema 配置
                            type: '', // 字段类型
                            label: '', // 字段中文名
                            // 字段在table中的相关配置
                            tableOption: {
                                ...elTableColumnConfig, //标准el-table-column 配置
                                visible: true, // 默认为true(false 表示不在表单显示)
                                toFixed: 2 // 保留小数点后几位
                            },
                            // 字段在search-bar中的相关配置
                            searchOption: {
                                ...elComponentConfig, //标准el-component(form 表单组件) 配置
                                comType: '', // 配置控件类型(input/select)
                                default: '', // 默认值
                                // 当comType = select 时, 可填
                                enumList: [],
                                // 当comType = dynamicSelect 时, 可填
                                api: '' // 枚举列表数据源api 遵循RESTful规范
                            }
                        }
                    }
                },
                // table相关配置
                tableConfig: {
                    headerButtons: [
                        {
                            label: '', // 按钮中文名
                            eventKey: '', // 按钮事件名
                            eventOption: {}, // 按钮具体配置
                            ...elButtonConfig // 标准el-button配置
                        }
                    ],
                    rowButtons: [
                        {
                            label: '', // 按钮中文名
                            eventKey: '', // 按钮事件名
                            eventOption: {
                                // 当eventKey = remove 时, 可填
                                params: {
                                    // paramKey = 参数的键值
                                    // rowValueKey = 参数值 ,格式为 schema::xxx时,到table中找响应的字段
                                    paramKey: rowValueKey
                                }
                            }, // 按钮事件具体配置
                            ...elButtonConfig // 标准el-button配置
                        }
                    ]
                },
                searchConfig: {}, // search-bar相关配置
                components: {} // 模块组件相关配置
            },
            // 当moduleType = custom 时, 可填
            customConfig: {
                path: '' // 自定义路径
            },
            // 当moduleType = iframe 时, 可填
            iframeConfig: {
                path: '' // iframe路径
            },
            // 当moduleType = sider 时, 可填
            siderConfig: {
                menu: [
                    {
                        // 可递归 menuItem(除moduleType = sider)
                    }
                ]
            }
        }
    ]
}

动态组件

动态组件的实现思路

配置设计

json 复制代码
{
    "schemaConfig": {
        "schema": {
            "type": "",
            "properties": {
                "key": {
                    "createFormOption": {
                        "comType": ""
                    },
                    "editFormOption": {
                        "comType": ""
                    },
                    "detailPanelOption": {
                        "comType": ""
                    }
                }
            }
        }
    },
    "componentConfig": {
        "createForm": {
            "title": "",
            "saveText": ""
        },
        "editForm": {
            "title": "",
            "saveText": ""
        },
        "detailPanel": {
            "mainKey": "",
            "title": ""
        }
    }
}

配置解析

js 复制代码
import { ref, watch, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { useMenuStore } from '$elpisStore/menu'

export const useSchema = () => {
    const route = useRoute()
    const menuStore = useMenuStore()
    const api = ref('')
    const tableSchema = ref({})
    const tableConfig = ref()

    const searchConfig = ref()
    const searchSchema = ref({})

    const components = ref({})

    // 通用构建方法(清除噪音)
    const buildDtoSchema = (_schema, comName) => {
        if (!_schema?.properties) return
        const dtoSchema = {
            type: 'object',
            properties: {}
        }

        // 提取有效 schema 字段信息

        for (const key in _schema.properties) {
            const props = _schema.properties[key]
            const option = props[`${comName}Option`]
            if (option) {
                let dtoProps = {}
                // 提取props中非 option 的部分, 存放到dtoProps
                for (const pKey in props) {
                    if (pKey.indexOf('Option') < 0) {
                        dtoProps[pKey] = props[pKey]
                    }
                }
                // 处理 comName Option
                dtoProps = Object.assign({}, dtoProps, { option })
                // 处理 required
                const { required } = _schema
                const isRequired = required && required.find(pKey => pKey === key)
                if (isRequired) {
                    dtoProps.option.required = true
                }
                dtoSchema.properties[key] = dtoProps
            }
        }

        return dtoSchema
    }

    // 构造schemaConfig 相关配置,传输给schemaView
    const buildData = () => {
        const { key, sider_key: siderKey } = route.query
        const mItem = menuStore.findMenuItem({
            key: 'key',
            value: siderKey ?? key
        })
        if (mItem && mItem.schemaConfig) {
            const { schemaConfig: sConfig } = mItem
            const configSchema = JSON.parse(JSON.stringify(sConfig.schema))
            api.value = sConfig.api ?? ''

            tableSchema.value = {}
            tableConfig.value = undefined
            searchConfig.value = undefined
            searchSchema.value = {}
            components.value = {}

            nextTick(() => {
                // 构建tableSchema 和 tableConfig
                tableSchema.value = buildDtoSchema(configSchema, 'table')
                tableConfig.value = sConfig.tableConfig
                // 构建searchConfig 和 searchSchema
                searchConfig.value = sConfig.searchConfig
                const dtoSearchSchema = buildDtoSchema(configSchema, 'search')
                for (const key in dtoSearchSchema.properties) {
                    if (route.query[key] !== undefined) {
                        dtoSearchSchema.properties[key].option.default = route.query[key]
                    }
                }
                searchSchema.value = dtoSearchSchema
                // 构造components = { comKey: { schema: {}, config: {} } }
                const { componentConfig } = sConfig
                if (componentConfig && Object.keys(componentConfig).length > 0) {
                    const dtoComponents = {}
                    for (const comName in componentConfig) {
                        dtoComponents[comName] = {
                            schema: buildDtoSchema(configSchema, comName),
                            config: componentConfig[comName]
                        }
                    }
                    components.value = dtoComponents
                }
            })
        }
    }

    onMounted(() => {
        buildData()
    })

    watch(
        [() => route.query.key, () => route.query.sider_key, () => menuStore.menuList],
        () => {
            buildData()
        },
        { deep: true }
    )

    return {
        api,
        tableSchema,
        tableConfig,
        searchSchema,
        searchConfig,
        components
    }
}

实现示例

  1. 生成组件配置对象
  2. 利用<component /> 动态渲染组件
js 复制代码
import CreateForm from './create-form/create-form.vue'
// 编写动态组件配置
const ComponentConfig = {
    createForm: {
        component: CreateForm
    }
}
export default ComponentConfig
vue 复制代码
<script setup>
import ComponentConfig from './components/component-config'
// 获取动态组件对应的配置对象
const { components } = useSchema()
</script>
<template>
<!-- 完成动态组件的渲染 -->
<component
    :is="ComponentConfig[key].component"
    v-for="(item, key) in components"
    :key="key"
/>
</template>

npm包的发布

实现步骤

  1. 区分框架使用者与开发者的路径
  2. 开放入口给框架使用者
  3. 允许框架使用者自定义webpack配置
  4. webpack别名配置
  5. 允许框架使用者自定义组件
  6. 发布到npm
区分路径

使用__dirnameprocess.cwd()区分框架使用与开发的路径

__dirname表示当前文件所在的路径,process.cwd()表示当前程序启动的路径。对于框架使用者来说,应该使用process.cwd()。对于开发者来说,应该使用__dirname

实现示例(elpis-core的改造)
js 复制代码
module.exports = {
     /**
     * 读取文件
     * @param {string} dirPath
     * @param {string} filePath
     * @param {object} app
     * @returns string[]
     */
    readFile(dirPath, filePath, app) {
        const actualPath = app ? app.businessDir : __dirname
        const assignPath = path.resolve(actualPath, dirPath)
        return glob.sync(path.resolve(assignPath, filePath))
    }
}
js 复制代码
module.exports = (app) => {
    // 读取业务/app/controller下所有文件(框架使用者的目录)
    const businessFileList = readFile(`.${sep}controller`, `.${sep}**${sep}**.js`, app)
    // 读取elpis/app/controller下所有文件(开发者的目录)
    const elpisFileList = readFile(`..${sep}..${sep}app${sep}controller`, `.${sep}**${sep}**.js`)
    const controllers = {}
    const handleFile = file => {
        // 提取文件名称
        let name = path.resolve(file)
        // 截取路径
        name = subPath(name, 'controller')
        // 将custom-module/custom-controller -> customModule/customController
        name = toCamelCase(name)
        //挂载controller到app实例
        let tempController = controllers
        const names = name.split(sep) // [customModule, customController]
        for (let i = 0, len = names.length; i < len; i++) {
            if (i === len - 1) {
                const ControllerModule = require(path.resolve(file))(app)
                // 是文件
                tempController[names[i]] = new ControllerModule()
            } else {
                // 文件夹
                if (!tempController[names[i]]) {
                    tempController[names[i]] = {}
                }
                tempController = tempController[names[i]]
            }
        }
    }
    businessFileList.forEach(file => handleFile(file))
    elpisFileList.forEach(file => handleFile(file))
}
开放入口
js 复制代码
const ElpisCore = require('./elpis-core')
// 引入前端工程化构建方法
const FEBuildDev = require('./app/webpack/dev')
const FEBuildProd = require('./app/webpack/prod')

module.exports = {
    /**
     * 启动 elpis
     * @param {Object} options - 项目配置,透传到ElpisCore
     */
    serverStart(options = {}) {
        const app = ElpisCore.start(options)
        return app
    },
    /**
     * 编译构建前端工程
     * @param {String} env 环境变量 dev/prod
     */
    frontedBuild(env) {
        if (env === 'dev') {
            FEBuildDev()
        } else if (env === 'prod') {
            FEBuildProd()
        }
    },
    /**
     * 服务端基础
     */
    Controller: {
        Base: require('./app/controller/base')
    },
    Service: {
        Base: require('./app/service/base')
    }
}
自定义webpack配置

框架使用者在app/webpack.config.js编写自己的webpack配置,elpis会将他与原来的webpack进行覆盖合并,代码如下:

js 复制代码
// 加载业务webpack 配置
let businessWebpackConfig = {}
try {
    businessWebpackConfig = require(path.resolve(process.cwd(), './app/webpack.config.js'))
} catch (error) {
    console.error('加载业务webpack配置失败:', error)
}
开放别名给框架使用者
js 复制代码
module.exports = {
    resolve: {
        alias: (() => {
            const aliasMap = {}
                const blankModulePath = path.resolve(__dirname, '../libs/blank.js')
                // dashboard 路由拓展配置
                const businessDashboardRouterConfig = path.resolve(process.cwd(), './app/pages/dashboard/router.js')
                aliasMap['$businessDashboardRouterConfig'] = fs.existsSync(businessDashboardRouterConfig)
                    ? businessDashboardRouterConfig
                    : blankModulePath
                // schema-view 组件拓展配置
                const businessComponentConfig = path.resolve(
                    process.cwd(),
                    './app/pages/dashboard/complex-view/schema-view/components/component-config.js'
                )
                aliasMap['$businessComponentConfig'] = fs.existsSync(businessComponentConfig)
                    ? businessComponentConfig
                    : blankModulePath
                // schema-search-bar 组件拓展配置
                const businessFormItemConfig = path.resolve(
                    process.cwd(),
                    './app/pages/widgets/schema-form/form-item-config.js'
                )
                aliasMap['$businessFormItemConfig'] = fs.existsSync(businessFormItemConfig)
                    ? businessFormItemConfig
                    : blankModulePath

                const businessSearchBarConfig = path.resolve(
                    process.cwd(),
                    './app/pages/widgets/schema-search-bar/schema-item-config.js'
                )
                aliasMap['$businessSearchBarConfig'] = fs.existsSync(businessSearchBarConfig)
                    ? businessSearchBarConfig
                    : blankModulePath
                return {
                    // 解决elpis框架在使用时无法找到vue的问题,表示在elpis中找vue
                    vue: require.resolve('vue'),
                    $elpisPages: path.resolve(__dirname, '../../pages'),
                    $elpisCommon: path.resolve(__dirname, '../../pages/common'),
                    $elpisWidgets: path.resolve(__dirname, '../../pages/widgets'),
                    $elpisHeaderContainer: path.resolve(
                        __dirname,
                        '../../pages/widgets/header-container/header-container.vue'
                    ),
                    $elpisSiderContainer: path.resolve(
                        __dirname,
                        '../../pages/widgets/sider-container/sider-container.vue'
                    ),
                    $elpisSchemaTable: path.resolve(__dirname, '../../pages/widgets/schema-table/schema-table.vue'),
                    $elpisSchemaForm: path.resolve(__dirname, '../../pages/widgets/schema-form/schema-form.vue'),
                    $elpisSchemaSearchBar: path.resolve(
                        __dirname,
                        '../../pages/widgets/schema-search-bar/schema-search-bar.vue'
                    ),
                    $elpisStore: path.resolve(__dirname, '../../pages/store'),
                    $elpisCurl: path.resolve(__dirname, '../../pages/common/curl.js'),
                    $elpisUtils: path.resolve(__dirname, '../../pages/common/utils.js'),
                    $elpisBoot: path.resolve(__dirname, '../../pages/boot.js'),
                    ...aliasMap
                }
        })()
    }
}
自定义组件

框架使用者在约定好的目录如app/widgets/schema-form/complex-view创建新的组件,elpis会将其合并到原本的组件配置对象中。框架使用者按照以下格式书写组件配置对象

js 复制代码
const formItemConfig = {
    组件名: {
        component: Vue组件
    }
}
export default formItemConfig
js 复制代码
import InputComponent from './complex-view/input/input.vue'
import InputNumberComponent from './complex-view/input-number/input-number.vue'
import SelectComponent from './complex-view/select/select.vue'
import businessFormItemConfig from '$businessFormItemConfig'
const FormItemConfig = {
    input: {
        component: InputComponent
    },
    inputNumber: {
        component: InputNumberComponent
    },
    select: {
        component: SelectComponent
    }
}
export default {
    ...businessFormItemConfig,
    ...FormItemConfig
}

发布npm包

经过以上一系列的改造之后,我们还需要使用npm link命令在本地测试一下,如果没问题才可以发布到npm,npm link的流程如下:

  1. 在elpis的开发目录使用npm link
  2. 创建一个测试目录,使用npm link 包名链接包到本地开始测试

测试完成之后,没问题,开始发布到npm,步骤如下:

  1. 创建npm帐号并登录,可以在npm官网创建帐号并完成登录,也可以创建好帐号之后使用npm login进行登录
  2. npm config get registry确保是官方镜像
  3. npm publish,首次发布,需要加上--access public参数 表示这不是一个私有包
相关推荐
笔尖的记忆6 小时前
【前端架构和框架】react中Scheduler调度原理
前端·面试
_advance6 小时前
我是怎么把 JavaScript 的 this 和箭头函数彻底搞明白的——个人学习心得
前端
右子6 小时前
React 编程的优雅艺术:从设计到实现
前端·react.js·mobx
清灵xmf7 小时前
npm install --legacy-peer-deps:它到底做了什么,什么时候该用?
前端·npm·node.js
超级大只老咪7 小时前
字段行居中(HTML基础语法)
前端·css·html
IT_陈寒7 小时前
Python开发者必看!10个高效数据处理技巧让你的Pandas代码提速300%
前端·人工智能·后端
只_只8 小时前
npm install sqlite3时报错解决
前端·npm·node.js
FuckPatience8 小时前
Vue ASP.Net Core WebApi 前后端传参
前端·javascript·vue.js
数字冰雹8 小时前
图观 流渲染打包服务器
服务器·前端·github·数据可视化