tnfe / TNT-Weekly

🙈 🙉 🙊 每周为您推荐国内外前端领域最新的优秀文章以及行业进展
5.13k stars 314 forks source link

【开源自荐】🎉Vue TSX Admin, 中后台管理系统开发的新方向 #131

Open manyuemeiquqi opened 7 months ago

manyuemeiquqi commented 7 months ago

大家好,我是蔓越莓曲奇,今天我想给大家分享的是我最近开源的中后台管理系统模板, Vue TSX Admin。 正如项目名称所表述的,该项目是完全通过 Vue3 + TSX 开发的。

为什么使用 JSX 写中后台管理

在讲为什么使用 JSX 前,我想先说些在中后台业务开发中,使用 template 开发的痛点。

template 写中后台管理系统的痛点

如果直接使用 element 的组件库,我们需要这样构建模板

<template>
  <el-table :data="tableData">
    <el-table-column prop="name" label="Name" width="120" />
    <el-table-column prop="state" label="Salary" width="120" />
    <el-table-column prop="city" label="Address" width="320" />
    <el-table-column prop="address" label="Email" width="600" />
  </el-table>
</template>
<script>

export default {
  setup() {
  const tableData = [];
    return {
      columns,
      data
    }
  },
}
</script>

这样做的缺点是需要开发者需要重复地表达结构相似的 table-column 元素

于是我们进行优化,假定每列渲染的结构相同,那么开发者只需传入每列的所渲染的数据的 key 值,就可以省略掉重复的 column。


<template>
  <a-table
    :columns="columns"
    :data="data"
  />
</template>

<script>
import { reactive } from 'vue';

export default {
  setup() {

    const columns = [
      {
        title: 'Name',
        dataIndex: 'name',
      },
      {
        title: 'Salary',
        dataIndex: 'salary',
      },
      {
        title: 'Address',
        dataIndex: 'address',
      },
      {
        title: 'Email',
        dataIndex: 'email',
      },
    ];
  const data = [];

    return {
      columns,
      data
    }
  },
}
</script>

但是这样仍不解决问题,实际业务中,表格列的渲染形态并不是固定死的,并不能简单的根据传入 data 所对应的 key 跟 value 进行渲染,自定义列的并不能简单默认渲染为 value 值,可能是按钮,可能是 Tag,还可能是各种权限杂糅下的渲染资源,因而需要自定义化,交给开发者决定某些列该如何渲染,我们再次优化,进行插槽拓展,具体思路为传入的 columns 中,需要自定义化的配置 slotName,不需要的走默认字段渲染逻辑。

<template>
      <a-table
        :columns="(cloneColumns as TableColumnData[])"
        :data="data"
      >
        <template #name="{ record }">
          <span v-if="record.status === 'offline'" class="circle"></span>
          <span v-else class="circle pass"></span>
          {{ $t(`searchTable.form.status.${record.status}`) }}
          {{record.name}}
        </template>
      </a-table>
</template>

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

export default {
  setup() {
    const show = ref(true)

    const columns = [{
      title: 'Name',
      dataIndex: 'name',
       slotName: 'operate'
    }, {
      title: 'Salary',
      dataIndex: 'salary',
    }, {
      title: 'Address',
      dataIndex: 'address',
    }, {
      title: 'Email',
      dataIndex: 'email',
    }];
    const data = [];

    return {
      columns,
      data,
    }
  },
}
</script>

这样似乎已经优化到极致了,但开发体验仍旧不好。 在动辄 200 行的 SFC 中,template 的内容一旦增多,我就需要这样开发 image.png 一份文件分割成两个屏幕(一个分成模板,一个分成 script),然后进行开发,一般改 bug 时,我需要两边一起定位开发,这不得不说是很痛苦的开发体验。

但是在 JSX 中,只需要这样表达


export default defineComponent({
  name: ViewNames.searchTable,
  setup() {

    // table columns render logic

    const colList = ref([
      {
        getTitle: () => t('searchTable.columns.number'),
        dataIndex: 'number',
        checked: true
      },
      {
        getTitle: () => t('searchTable.columns.name'),
        dataIndex: 'name',
        checked: true
      },
      {
        getTitle: () => t('searchTable.columns.contentType'),
        dataIndex: 'contentType',
        render: ({ record }: { record: PolicyRecord }) => {
          const map: Record<PolicyRecord['contentType'], string> = {
            img: '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image',
            horizontalVideo:
              '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77721e365eb2ab786c889682cbc721c1.svg~tplv-49unhts6dw-image.image',
            verticalVideo:
              '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/ea8b09190046da0ea7e070d83c5d1731.svg~tplv-49unhts6dw-image.image'
          }
          return (
            <>
              <Space>
                <Avatar size={16} shape="square">
                  <img alt="avatar" src={map[record.contentType]} />
                </Avatar>
                {t(`searchTable.form.contentType.${record.contentType}`)}
              </Space>
            </>
          )
        },
        checked: true
      },
      {
        getTitle: () => t('searchTable.columns.filterType'),
        dataIndex: 'filterType',
        render: ({ record }: { record: PolicyRecord }) => (
          <>{t(`searchTable.form.filterType.${record.filterType}`)}</>
        ),
        checked: true
      },
      {
        getTitle: () => t('searchTable.columns.count'),
        dataIndex: 'count',
        checked: true
      },
      {
        getTitle: () => t('searchTable.columns.createdTime'),
        dataIndex: 'createdTime',
        checked: true
      },
      {
        getTitle: () => t('searchTable.columns.status'),
        dataIndex: 'status',
        render: ({ record }: { record: PolicyRecord }) => {
          return (
            <Space>
              <Badge status={record.status === 'offline' ? 'danger' : 'success'}></Badge>
              {t(`searchTable.form.status.${record.status}`)}
            </Space>
          )
        },
        checked: true
      },
      {
        getTitle: () => t('searchTable.columns.operations'),
        dataIndex: 'operations',
        render: () =>
          checkButtonPermission(['admin']) && (
            <Link>{t('searchTable.columns.operations.view')}</Link>
          ),
        checked: true
      }
    ])

    return () => (
          <Table
            data={renderData.value}
            columns={colList.value}
          ></Table>
    )
  }
})

这样做的好处是可以获取到上下文的信息,对自定义列进行开发时,可以灵活的向下拓展,不必再同时关注模板跟 script 。

使用声明式弹窗的好处不再赘述,但是当使用模板进行开发时,我们很难获得使用声明式弹窗的完美体验。

声明式弹窗对于 SFC 的难点是怎么在函数调用时,把虚拟 DOM 传递进去,Vue 中无非就三种可能,字符串、h 函数 跟 JSX,字符串需要引入框架编译时代码,因此不考虑。 大部分组件库都是用的 h 函数这种方案

<template>
  <el-button text @click="open">Click to open Message Box</el-button>
</template>

<script lang="ts" setup>
import { h } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const open = () => {
  ElMessageBox({
    title: 'Message',
    message: h('p', null, [
      h('span', null, 'Message can be '),
      h('i', { style: 'color: teal' }, 'VNode'),
    ]),
    showCancelButton: true,
    confirmButtonText: 'OK',
    cancelButtonText: 'Cancel',
    beforeClose: (action, instance, done) => {
      if (action === 'confirm') {
        instance.confirmButtonLoading = true
        instance.confirmButtonText = 'Loading...'
        setTimeout(() => {
          done()
          setTimeout(() => {
            instance.confirmButtonLoading = false
          }, 300)
        }, 3000)
      } else {
        done()
      }
    },
  }).then((action) => {
    ElMessage({
      type: 'info',
      message: `action: ${action}`,
    })
  })
}
</script>

但是这种方案很难用,阅读体验跟维护成本都很高。

还有一些组件库的封装思路是不传递虚拟节点了,只传参,通过参数控制弹窗的结构跟行为,但这种方式并不是一种很好的解决方案,因为如果想保持 modal 的灵活性,弹窗内部的大量状态跟行为都需要向外暴露为参数,这就导致了开发者使用需要查看文档,维护者拓展需要继续加参数的局面。 image.png image.png

以上各种解决方案都是命令式弹窗在 SFC 开发限制下的妥协产物。 但在 JSX 中,只需要这样,就可以调用一个弹窗。

    const handleError = () => {
      Modal.error({
        title: () => <div>error</div>,
        content: () => (
          <p>
            <span>Message can be error</span>
            <IconErro />
          </p>
        )
      })
    }

SFC 的特点是什么,关注点分离,关注点分离有什么好处呢?

但在有些场景下,我们并不希望这样的分离。 业务开发中,经常会出现一些小组件,会让我陷入矛盾:需不需要为这些组件单独创建一个新的 Vue 文件进行维护?分割必然会导致组件状态维护成本与通信成本的提高,不封装的后果则是组件经过业务多轮迭代以后,分离这些代码就会成为一件极为痛苦的事情,因为我既需要分离 template ,又需要从混乱的业务中提取维护这些 template 所需要的状态。

但在 JSX 中,可以在 setup 中随时随地的通过函数创建组件,等到分割的时候,只关注这部分维护函数正常运行所需要的状态就可以。

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

以上林林总总的痛点都可以归咎于一个问题: 在 SFC 的开发方式中,没有找到一种对开发者友好的方式在 script 中表达虚拟 DOM。 但在 JSX 中,可以通过 JavaScript 创建 JSX 进而表达虚拟 DOM,解决了这个问题。

为什么选择了 Vue 还去选择 JSX

大部分开发者反驳 Vue + JSX 开发者的第一个问题就是,你都选择了 JSX 为什么还用 Vue 呢?


大部分开发者纠结于 JSX 开发的无非以下几点

v-showv-model目前可以在 JSX 中使用 事件修饰符可以通过 withModifiers 进行替换 但是 v-prev-cloakv-memo目前还没有特别完美的替代方案,有条件的同学可以去提 PR。

插槽写法变的更加容易理解 -> 本质上就是函数传参

const A = (props, { slots }) => (
  <>
    <h1>{slots.default ? slots.default() : 'foo'}</h1>
    <h2>{slots.bar?.()}</h2>
  </>
);

const App = {
  setup() {
    const slots = {
      bar: () => <span>B</span>,
    };
    return () => (
      <A v-slots={slots}>
        <div>A</div>
      </A>
    );
  },
};

// or

const App = {
  setup() {
    const slots = {
      default: () => <div>A</div>,
      bar: () => <span>B</span>,
    };
    return () => <A v-slots={slots} />;
  },
};

// or you can use object slots when `enableObjectSlots` is not false.
const App = {
  setup() {
    return () => (
      <>
        <A>
          {{
        default: () => <div>A</div>,
        bar: () => <span>B</span>,
      }}
        </A>
        <B>{() => 'foo'}</B>
      </>
    );
  },
};

事件绑定需要注意的一点就是如果要传递自定义的参数,就需要使用箭头函数或者通过 bind 绑 this,否则就会造成回调函数自动触发。

JSX 性能是比不过模板的,这点无可否认,但是模板的性能优化究竟占据了多大一个部分?Vue 模板比 JSX 更高效的原因在于,Vue 的编译过程可以在编译阶段对模板进行静态分析,并生成更精确的渲染函数。我们可以将其理解为在编译过程中, Vue 在以一种 treeshaking 的思路进行优化,通过删除无用的逻辑分支,以此生成最优代码。听起来很高大上是不是,但是按照计算机科学的角度来讲,这一部分进行的优化的效果是极为有限的,这一点我也向官方求证了,维护者对模板跟 JSX 的性能差异是这么形容的: image.png

但是前端好歹是一门工科,a bit less 如何用数字衡量呢? 为此,我找到了 js-framework-benchmark ,一个基准测试框架性能的工具,也就是我们俗称的跑分,这个工具的原理是让各种渲染框架都去实现一个业务场景,然后使用 puppeteer 模拟各种浏览器行为进行测试获取性能指标。 js-framework-benchmark 目前是没有 Vue JSX 的跑分结果的,为此我 clone 了项目进行了本地测试。 动画.gif 由于目前渲染框架众多,我只选取了几个主流框架进行对比,测试结果如下,有兴趣的同学可以跟开源的测试结果进行比对,由于计算机硬件的不一致,性能指标的毫秒数字并没有太大的对比意义,只需查看每个表格最后一行的 weighted geometric mean 查看一个大概趋势就可以,同时我也提交了 PR(内部我认为还有优化空间,有兴趣的同学可以进行 PR 调优),或许过几天后就可以看到官方测试结果了。 image.png image.png image.png

通过表格可以看出,Vue + JSX 的性能是差,但也是只略差,并不能成为抵触 Vue + JSX 开发的理由,换一方面来说,中后台开发中能触碰到到 Vue 性能瓶颈的场景真的多么? 这个问题打个比方,就好像我在玩 LOL,你在跟我说玩 LOL,用 4090 跟 4070 存在性能差距、4090 开启超频后体验会更好,这不是跟我扯犊子么,我玩个 LOL 还需要特别在意用 4090 还是 4070, 4090 显卡是否超频么? 中后台业务中虚拟化数据渲染跟增量更新基本已经满足大部分性能场景,如果说一个业务方案的性能瓶颈都需要考虑到 DSL 方面的性能,那么这个业务本身的设计方案也需要重新审视跟考量了。

大家都在讨论 Vue3 + JSX 的可行性,但是却鲜有开源开箱即用的业务项目,担心踩坑没有方案参考或者投入成本的淹没,同时公司内部确实没有一个良好的环境提供开发者进行实践与探索。但开源无疑是最好的方式,这一点也是我做这个项目的原因,于是 Vue-TSX-Admin 就诞生了 🎉 。

Vue TSX Admin 是什么

logo-8b7cc132.svg

简介

Vue TSX Admin 是一个免费开源的中后台管理系统模块版本,UI 参考 acro design pro + ant design pro,它使用了最新的前端技术栈,完全采用 Vue3 + TSX 的模式进行开发,提供了开箱即用的中后台前端解决方案,内置了 i18n 国际化解决方案,可配置化布局,主题色修改,权限验证,提炼了典型的业务模型,可以帮助你快速搭建起一个中后台前端项目。

主要的开发方案为:

代码地址

安装使用

进入项目目录

cd vue-tsx-admin

安装依赖

pnpm install

启动服务

pnpm run dev

浏览器访问: [http://localhost:5173/vue-tsx-admin/](http://localhost:5173/vue-tsx-admin/) 即可

- 发布
```javascript
pnpm run build

格式化

pnpm run format

代码 lint + fix

pnpm run lint pnpm run lint-style



### 浏览器支持

- Chrome >=87
- Firefox >=78
- Safari >=14
- Edge >=88
- Vue3 不支持 IE

### 演示
![动画.gif](https://cdn.nlark.com/yuque/0/2024/gif/22817409/1704072677179-76719f50-5e8a-4f7f-aaab-b1e3952ef6d5.gif#averageHue=%23d5c9b1&clientId=uf128b628-9083-4&from=drop&id=u79f05bb4&originHeight=1007&originWidth=1919&originalType=binary&ratio=1&rotation=0&showTitle=false&size=2616308&status=done&style=none&taskId=u01bc0557-2a52-4e92-8d81-02530d08ada&title=)
### 作者
[manyuemeiquqi](https://github.com/manyuemeiquqi/vue-tsx-admin/commits?author=manyuemeiquqi)
### License
[MIT License](https://github.com/manyuemeiquqi/vue-tsx-admin?tab=MIT-1-ov-file)

最后,如果本项目帮助到你,希望你可以帮作者点个 [star](https://github.com/manyuemeiquqi/vue-tsx-admin?tab=readme-ov-file)  ⭐ 表示鼓励
如果你发现项目 [bug](https://github.com/manyuemeiquqi/vue-tsx-admin/issues) ,欢迎提 [PR](https://github.com/manyuemeiquqi/vue-tsx-admin/pulls) , 感谢 🤞