【前端】工程结构最佳实践(v4)

阅读之前

你应该:

  • 了解node与npm相关内容
  • 了解lerna包管理工具的相关内容
  • 了解git仓库的相关内容
  • 了解rollup的相关内容

工程结构包示例

Vue项目结构包下载

工程结构详解

工程结构
├── packages
│   ├── kunlun-boot
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── favicon.ico
│   │   │   └── index.html
│   │   ├── src
│   │   │   ├── main.ts
│   │   │   └── shim-vue.d.ts
│   │   ├── tsconfig.json
│   │   └── vue.config.js
│   ├── kunlun-module-demo
│   │   ├── scripts
│   │   │   ├── postpublish.js
│   │   │   └── prepublish-only.js
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   └── shim-vue.d.ts
│   │   ├── index.ts
│   │   ├── package.json
│   │   ├── rollup.config.js
│   │   └── tsconfig.json
│   └── kunlun-modules-demo
│       ├── scripts
│       │   ├── build.config.js
│       │   ├── postpublish.js
│       │   └── prepublish-only.js
│       ├── packages
│       │   ├── module-demo1
│       │   │   ├── index.ts
│       │   │   ├── package.json
│       │   │   ├── rollup.config.js
│       │   │   └── src
│       │   │       ├── index.ts
│       │   │       └── shim-vue.d.ts
│       │   ├── module-demo2
│       │   │   ├── index.ts
│       │   │   ├── package.json
│       │   │   ├── rollup.config.js
│       │   │   └── src
│       │   │       ├── index.ts
│       │   │       └── shim-vue.d.ts
│       │   └── module-dependencies
│       │       ├── index.ts
│       │       ├── package.json
│       │       └── src
│       │           └── dependencies.ts
│       ├── lerna.json
│       ├── tsconfig.json
│       └── package.json
├── lerna.json
└── package.json

壳工程

├── packages
│   └── ......
├── lerna.json
└── package.json

壳工程仅用于将多个包进行统一安装,并通过lerna包管理工具进行相关的软链接操作。

壳工程包含了除了node和npm外其他相关的安装依赖,如lernarimraf等。这样可以避免由于开发环境差异带来的一系列问题。

我们建议所有开发人员均使用相同的壳工程进行开发,因此,我们有必要将其提交并推送到git远程仓库进行共享。

壳工程目录下执行npm install命令即可完成一系列安装操作。

需要注意的是,lerna.json中定义了所有需要统一安装的子工程路径,安装前需要确认是否满足当前需要,但这些修改都应该只在本地完成,而不应该推送到git远程仓库

安装完成后,所有公共依赖均会安装在壳工程node_modules目录下,每个子工程的node_modules中仅包含软链接目录。并且在壳工程node_modules目录下不会出现本地已经存在的工程依赖。

单工程包管理

├── packages
│   └── kunlun-module-demo
│       ├── scripts
│       │   ├── postpublish.js
│       │   └── prepublish-only.js
│       ├── src
│       │   ├── index.ts
│       │   └── shim-vue.d.ts
│       ├── index.ts
│       ├── package.json
│       ├── rollup.config.js
│       └── tsconfig.json
├── lerna.json
└── package.json

作为一个独立的工程,它可以是一个项目的总工程,也可以是多个项目共同使用的子工程。这也是最常见的工程包结构。

通常情况下,kunlun-module-demo工程会使用一个独立的git仓库进行管理。

通过kunlun-module-demo打包生成的@kunlun/module-demo包,将可以被发布到npm仓库或其他地方,供其他工程或项目使用。

启动工程中的package.json中添加"@kunlun/module-demo": "~1.0.0"相关依赖进行引入。并在入口文件main.ts中导入相关的js和css相关内容。

如示例中所示:

// 按需引入
import '@kunlun/module-demo/dist/kunlun-module-demo.css';

import '@kunlun/module-demo';

需要注意的是,在本地开发中,css文件相关内容应该是被临时注释的,因为通过import '@kunlun/module-demo';已经将相关内容引入,无需使用打包好的css文件。

多工程包管理

├── packages
│   └── kunlun-modules-demo
│       ├── scripts
│       │   ├── build.config.js
│       │   ├── postpublish.js
│       │   └── prepublish-only.js
│       ├── packages
│       │   ├── module-demo1
│       │   │   ├── index.ts
│       │   │   ├── package.json
│       │   │   ├── rollup.config.js
│       │   │   └── src
│       │   │       ├── index.ts
│       │   │       └── shim-vue.d.ts
│       │   ├── module-demo2
│       │   │   ├── index.ts
│       │   │   ├── package.json
│       │   │   ├── rollup.config.js
│       │   │   └── src
│       │   │       ├── index.ts
│       │   │       └── shim-vue.d.ts
│       │   └── module-dependencies
│       │       ├── index.ts
│       │       ├── package.json
│       │       └── src
│       │           └── dependencies.ts
│       ├── lerna.json
│       ├── tsconfig.json
│       └── package.json
├── lerna.json
└── package.json

单工程包管理类似,多工程包管理方案为了解决整体结构复杂,并且功能需要做最小化拆分的场景。在多工程包中提供的module-dependencies作为整个工程包的总入口向外提供功能。

如示例中所示:

// 视情况按需引入
// import '@kunlun/module-demo/dist/kunlun-module-demo1.css';
// import '@kunlun/module-demo/dist/kunlun-module-demo2.css';

import '@kunlun/modules-dependencies';

与单工程包唯一的区别在于启动工程的导入方式有所不同,仅需通过import '@kunlun/modules-dependencies';导入所有功能即可。在总入口处将对所有子工程包进行统一的导出并且包含了对应的css相关内容。

特别的是,如果多个工程间的css样式出现冲突,但又需要依赖js文件时,css相关内容将不能在总入口处进行导入,需要放在对应的启动工程中进行导入处理。原则上,我们应该避免这种情况的发生。

启动工程

├── packages
│   └── kunlun-boot
│       ├── package.json
│       ├── public
│       │   ├── favicon.ico
│       │   └── index.html
│       ├── src
│       │   ├── main.ts
│       │   └── shim-vue.d.ts
│       ├── tsconfig.json
│       └── vue.config.js
├── lerna.json
└── package.json

作为vue项目的启动工程,它应该仅用于工程组合,即选择性的使用某几个工程进行启动。

rollup脚本简述

通过package.json构建所需工程名称

const packagePrefix = 'kunlun-';

const buildName = (name) => {
  const libraryName = name.replace('@', '').replace('/', '-');
  const pathName = libraryName.substring(packagePrefix.length);
  const camelCaseName = libraryName.replace(/-(\w)/g, (all, letter) => letter.toUpperCase());
  return { libraryName, pathName, camelCaseName };
};

使用方法:

import pkg from './package.json';

const res = buildName(pkg.name);
const libraryName = res.libraryName;

转换结果:

  • libraryName:@kunlun/module-demo -> kunlun-module-demo
  • pathName:@kunlun/module-demo -> module-demo
  • camelCaseName:@kunlun/module-demo -> kunlunModuleDemo

这三个名称在单工程和多工程中都有使用,以实现统一脚本命令的自动化构建。

需要注意的是:

  • package.json中使用的名称需要手动和packagePrefix定义的变量进行匹配。
  • 在多工程中,每个子工程目录对应的名称应该与pathName属性完全一致。

多工程包构建脚本的使用

方法签名
function rollupConfig(name = '', external = [], hasSCSS = true, extendPlugins)
示例脚本
import pkg from './package.json';
import rollupConfig from '../../scripts/build.config.js';

export default rollupConfig(pkg.name, [
  'vue',
  'lodash-es',
  'ant-design-vue',
  '@ant-design/icons-vue',
  'element-plus',
  '@element-plus/icons-vue',
  '@kunlun/dependencies',
  '@kunlun/vue-ui-antd',
  '@kunlun/vue-ui-el'
], false);
  • pkg.name:通过当前路径的package.json获取name属性。
  • external:打包时排除当前工程中导入的第三方包,原则上需要全部排除,否则会出现内存变量不一致的问题。
  • hasSCSS:是否进行scss编译。当使用scss时,必须开启。
  • extendPlugins:扩展插件。

Oinone社区 作者:nation原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/47.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
nation的头像nation数式员工
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

  • oio-dropdown 下拉菜单

    向下弹出的列表。 何时使用 当页面上的操作命令过多时,用此组件可以收纳操作元素。点击或移入触点,会出现一个下拉菜单。可在列表中进行选择,并执行相应的命令。 用于收罗一组命令操作。 Select 用于选择,而 Dropdown 是命令集合。 API 属性如下 参数 说明 类型 默认值 destroyOnHide 关闭后是否销毁 Dropdown boolean false disabled 菜单是否禁用 boolean – getTriggerContainer 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 Function(triggerNode) () => document.body overlay(v-slot) 菜单 Menu – overlayClassName 下拉根元素的类名称 string – overlayStyle 下拉根元素的样式 object – placement 菜单弹出位置 bottomLeft | bottom | bottomRight | topLeft | top | topRight bottomLeft trigger 触发下拉的行为, 移动端不支持 hover Array<click|hover|contextmenu> ['hover'] visible(v-model:visible) 菜单是否显示 boolean – 事件 事件名称 说明 回调参数 onUpdateValue 菜单显示状态改变时调用,参数为 visible。点击菜单按钮导致的消失不会触发 function(visible)

    2023年12月18日
    1.0K00
  • 字段组件submit方法详解

    场景介绍 在日常开发调试表单页的过程中,细心的小伙伴应该注意到,视图内的数据(通过vue调试工具看到的formData就是视图的数据)和最终通过服务端动作提交的数据不总是一致的,本文将带领大家解开疑惑。 为什么会出现这种现象? 出现这种情况都是当前模型上有关联关系字段的场景,以多对一(M2O)场景为例,由于当前模型的关联关系字段是通过字段配置中的referenceFields属性和当前模型的relationFields属性进行关联的,所以提交数据的时候只需要拿到relationFields配置的字段就可以了,没有必要再去多拿关联关系字段本身的数据。 结合业务场景说明 这里以商品模型和类目模型举例,商品模型内有个类目的m2o字段category和对应的relationFields字段categoryId,数据提交到后端的时候前端默认会根据字段配置只获取categoryId,而category的整个对象都不会被提交。 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.demo.api.model.DemoItemCategory; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.base.common.CodeModel; @Model.model(DemoItem.MODEL_MODEL) @Model(displayName = "测试商品") public class DemoItem extends CodeModel { private static final long serialVersionUID = -5104390780952631397L; public static final String MODEL_MODEL = "demo.DemoItem"; @Field.String @Field(displayName = "商品名称") private String name; @Field.Integer @Field(displayName = "类目ID") private Long categoryId; @Field.many2one @Field.Relation(relationFields = {"categoryId"}, referenceFields = {"id"}) @Field(displayName = "商品类目") private DemoItemCategory category; } 前端是如何处理数据的 前端的字段组件提供了submit()方法来让我们可以有就会在提交数据的时候改变数据。 // 字段组件基类 export class BaseFormItemWidget< Value = unknown, Props extends BaseFormItemWidgetProps = BaseFormItemWidgetProps > extends BaseDataWidget<Props> { /** * 数据提交的方法,例如:m2o字段user(假设其关系字段为userId)的值{id: 1, name: 'xxx'},但是实际后端数据只需要其中的id,所以用m2o对应的关系字段userId提交数据就可以了 * @param submitValue */ public submit(submitValue: SubmitValue): ReturnPromise<Record<string, unknown> | SubmitRelationValue | undefined> { return undefined; } } 这里先以FormStringFieldSingleWidget组件处理密码类型的字段讲解。密码一般在输入的时候是明文,为了提高提交到后端的安全性,可以将这个密码加密后再传到后端,后端再做进一步处理,这个场景中,视图中的密码和提交给后端的密码就出现了不一致的情况, @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: [ViewType.Form, ViewType.Search], ttype: ModelFieldType.String }) ) export class FormStringFieldSingleWidget extends FormStringFieldWidget { public submit(submitValue: SubmitValue) { let finalValue = this.value; /** * 数据提交的时候,如果判断当前字段是否需要加密,需要加密的情况用encrypt函数做加密处理 */ if (this.crypto && finalValue) { finalValue = encrypt(finalValue); } return SubmitHandler.DEFAULT(this.field, this.itemName, submitValue, finalValue); } 注意:关系字段配置的透出字段只影响该字段的查询数据方法的返回值,不会因为此配置就在提交数据里加上这部分配置的字段 字段需要提交关联关系字段内的所有数据如何处理? 我们可以在自定义组件里覆写submit()方法,直接将this.value内的数据返回这里以覆写多对多m2m字段为例 import { BaseFieldWidget, FormM2MFieldSelectWidget, ModelFieldType, SPI, SubmitValue, ViewType } from '@kunlun/dependencies'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: ViewType.Form, ttype: ModelFieldType.ManyToMany, widget: 'Select', model: 'xxx.yyyyy', name: 'fileName01',…

    2024年9月10日
    1.3K00
  • 前端日期组件国际化支持方案

    在 oinone 平台中,系统默认支持基础的国际化翻译功能。但由于日期时间组件的国际化依赖对应语言包,而全量引入语言包会显著增加打包体积,因此前端默认仅集成了中、英文的日期时间支持。若需为日期时间组件扩展其他语言(如日语)的国际化支持,需手动导入对应语言包并完成配置,具体步骤如下: 假设我们现在国际化翻译切换成了日语,那么我们在日期时间也要支持日语,那么需要如下操作: 1: 重写 RootWidget 继承平台默认的 RootWidget,SPI 注册条件保持跟平台一致即可覆盖平台默认的RootWidget // CustomRootWidget.ts import { RootComponentSPI, RootWidget, SPIFactory } from '@oinone/kunlun-dependencies'; import Root from './Root.vue'; // 通过SPI注册覆盖平台默认的root组件 @SPIFactory.Register(RootComponentSPI.Token({ widget: 'root' })) export class CustomRootWidget extends RootWidget { public initialize() { super.initialize(); this.setComponent(Root); return this; } } 2: 覆盖 Root 组件的 Vue 文件 自定义的 Vue 文件需负责导入目标语言(如日语)的语言包,并根据当前语言环境动态切换配置。这里需要同时处理 ant-design-vue、element-plus 组件库及 dayjs 工具的语言包,确保日期组件的展示和交互统一适配目标语言。 <!– Root.vue –> <template> <a-config-provider :locale="antLocale"> <el-config-provider :locale="eleLocale"> <match :rootToken="root"> <template v-for="page in pages" :key="page.widget"> <route v-if="page.widget" :path="page.path" :slotName="page.slotName" :widget="page.widget"> <slot :name="page.slotName" /> </route> </template> <route :path="pagePath" slotName="page" :widgets="{ page: widgets.page }"> <slot name="page" /> </route> <route path="/" slotName="homePage"> <slot name="homePage" /> </route> </match> </el-config-provider> </a-config-provider> </template> <script lang="ts"> import { CurrentLanguage, EN_US_CODE, UrlHelper, ZH_CN_CODE } from '@oinone/kunlun-dependencies'; import { ConfigProvider as AConfigProvider } from 'ant-design-vue'; import { ElConfigProvider } from 'element-plus'; import dayjs from 'dayjs'; // 导入ant-design-vue语言包 import enUS from 'ant-design-vue/es/locale/en_US'; import zhCN from 'ant-design-vue/lib/locale/zh_CN'; import jaJP from 'ant-design-vue/lib/locale/ja_JP'; // 新增:日语语言包 // 导入 dayjs的语言包 import 'dayjs/locale/zh-cn'; import 'dayjs/locale/ja'; // 新增:日语语言包 // 导入element-plus语言包 import elEn from 'element-plus/dist/locale/en.mjs'; import elZhCn from 'element-plus/dist/locale/zh-cn.mjs'; import elJaJP from 'element-plus/dist/locale/ja.mjs'; // 新增:日语语言包 import { computed, defineComponent, onMounted,…

    2025年8月13日
    62500
  • GraphQL请求详解(v4)

    阅读之前 什么是GraphQL? Oinone官方解读GraphQL入门 可视化请求工具 insomnia下载 概述 (以下内容简称GQL) 众所周知,平台的所有功能都是通过一系列元数据定义来驱动的,而GQL作为服务端和客户端交互协议,其重要性不言而喻。下面会从以下几个方面介绍GQL在平台中是如何运作的: 服务端定义元数据生成GQL对应的schema 通过HttpClient发起一个GQL请求 通过window.open使用GET方式发起一个GQL请求 客户端泛化调用任意API服务 客户端通过运行时上下文RuntimeContext发起GQL请求 准备工作 在开始介绍GQL之前,我们需要定义一个可以被GQL请求的服务端函数,以此来介绍我们的相关内容。(对服务端不感兴趣的同学可以跳过代码部分) DemoModel.java @Model.model(DemoModel.MODEL_MODEL) @Model(displayName = "演示模型", labelFields = {"name"}) public class DemoModel extends IdModel { private static final long serialVersionUID = -7211802945795866154L; public static final String MODEL_MODEL = "demo.DemoModel"; @Field(displayName = "名称") private String name; @Field(displayName = "是否启用") private Boolean isEnabled; } DemoModelAction.java @Model.model(DemoModel.MODEL_MODEL) public class DemoModelAction { @Action(displayName = "启用") public DemoModel enable(DemoModel data) { data.setIsEnabled(true); data.updateById(); return data; } @Action(displayName = "禁用") public DemoModel disable(DemoModel data) { data.setIsEnabled(false); data.updateById(); return data; } } 上面的java代码定义了演示模型的字段和动作: 字段 field id:ID 整数 Integer name:名称 字符串 String isEnabled:是否启用 布尔 Boolean 动作 action enable:启用 提交动作 ServerAction disable:禁用 提交动作 ServerAction 服务端定义元数据生成GQL对应的schema 模型和字段 type DemoModel { id: Long name: String isEnabled: Boolean } 动作 type DemoModelInput { id: Long name: String isEnabled: Boolean } type DemoModelQuery { queryOne(query: DemoModelInput): DemoModel …… } type DemoModelMutation { enable(data: DemoModelInput): DemoModel disable(data: DemoModelInput): DemoModel …… } PS:平台内置了多种Query和Mutation定义,通过模型继承关系将自动生成,无需手动定义。比如Query定义包括queryOne、queryPage等;Mutation定义包括create、update等。特殊情况下,默认逻辑无法满足时,服务端通常采用函数重载的方式进行替换,客户端则无需关心。 生成规则 type DemoModel:通过模型编码demo.DemoModel取.分隔后的最后一位,并转换为大驼峰格式。字段与声明类型一致。 type DemoModelInput:动作入参定义,未做特殊声明的情况下与模型定义一致。 type DemoModelQuery和type DemoModelMutation:Query和Mutation为固定后缀,分别生成动作相关类型。当函数类型为QUERY时,使用Query后缀,其他情况使用Mutation后缀。 (此处仅做简单解释,详细生成规则过于复杂,客户端无需关心) Query类型的GQL示例 query { demoModelQuery { queryOne(query: { id: ${id} }) { id name isEnabled } } } Mutation类型的GQL示例 mutation…

    2023年11月1日
    2.3K00
  • OioProvider详解

    OioProvider OioProvider是平台的初始化入口。 示例入口 main.ts import { VueOioProvider } from '@kunlun/dependencies'; VueOioProvider(); 网络请求/响应配置 http 平台统一使用apollo作为统一的http请求发起服务,并使用GraphQL协议作为前后端协议。 参考文档: apollo-client graphql 配置方式 VueOioProvider({ http?: OioHttpConfig }); OioHttpConfig /** * OioHttp配置 */ export interface OioHttpConfig { /** * base url */ url: string; /** * 拦截器配置 */ interceptor?: Partial<InterceptorOptions>; /** * 中间件配置(优先于拦截器) */ middleware?: NetworkMiddlewareHandler | NetworkMiddlewareHandler[]; } 内置拦截器可选项 InterceptorOptions /** * 拦截器可选项 */ export interface InterceptorOptions { /** * 网络错误拦截器 */ networkError: NetworkInterceptor; /** * 请求成功拦截器 (success) */ requestSuccess: NetworkInterceptor; /** * 重定向拦截器 (success) */ actionRedirect: NetworkInterceptor; /** * 登录重定向拦截器 (error) */ loginRedirect: NetworkInterceptor; /** * 请求错误拦截器 (error) */ requestError: NetworkInterceptor; /** * MessageHub拦截器 (success/error) */ messageHub: NetworkInterceptor; /** * 前置拦截器 */ beforeInterceptors: NetworkInterceptor | NetworkInterceptor[]; /** * 后置拦截器 */ afterInterceptors: NetworkInterceptor | NetworkInterceptor[]; } 内置拦截器执行顺序: beforeInterceptors:前置拦截器 networkError:网络错误 actionRedirect:重定向 requestSuccess 请求成功 loginRedirect:登录重定向 requestError:请求错误 messageHub:MessageHub afterInterceptors:后置拦截器 NetworkInterceptor /** * <h3>网络请求拦截器</h3> * <ul> * <li>拦截器将按照注册顺序依次执行</li> * <li>当任何一个拦截器返回false时,将中断拦截器执行</li> * <li>内置拦截器总是优先于自定义拦截器执行</li> * </ul> * */ export interface NetworkInterceptor { /** * 成功拦截 * @param response 响应结果 */ success?(response: IResponseResult): ReturnPromise<boolean>; /** * 错误拦截 * @param response 响应结果 */ error?(response: IResponseErrorResult): ReturnPromise<boolean>; } 自定义路由配置 router 配置方式 VueOioProvider({ router?: RouterPath[]…

    2023年11月6日
    1.3K00

Leave a Reply

登录后才能评论