Skip to content

Latest commit

 

History

History
730 lines (552 loc) · 14.5 KB

slides.md

File metadata and controls

730 lines (552 loc) · 14.5 KB
theme layout background highlighter lineNumbers info drawings colorSchema
default
cover
/cover.jpg
shiki
false
## Vitify Admin Presentation for Vitify Admin and new features in Online Monitor.
persist
dark

Welcome to Vitify Admin

Presentation for Vitify Admin and new features in Online Monitor

Yue JIN

啥是 Vitify Admin?

Vitify 是用于快速创建web后台应用的起始模板。Vite + Vuetify = Vitify

  • ⚡️ Vite 3, pnpm, ESBuild - 快得一逼
  • 🗂️ Pages - 基于文件的路由
  • 📑 布局系统 - 零配置布局
  • 🦾 TypeScript - 支持 Vuetify2 模板 intelisense, Powerer by Volar
  • 🔥 <script setup> - 使用 Vue3 语法
  • 🍍 状态管理 - Pinia
  • 🧪 测试 - Vitest 单元/组件测试 + Cypress E2E 测试
  • 🌍 I18n - 国际化开箱即用
  • 📦 自动import - 组件自动化加载,API自动导入
  • 😃 Icons - SVG图标自动注册
  • 📉 数据可视化 - Vue-Echarts
<style> h1 { background-color: #2B90B6; background-image: linear-gradient(45deg, #4EC5D4 10%, #146b8c 20%); background-size: 100%; -webkit-background-clip: text; -moz-background-clip: text; -webkit-text-fill-color: transparent; -moz-text-fill-color: transparent; } </style>

pnpm

  • 安装速度大幅提升
  • 大量减少磁盘占用空间
  • patch package: 允许直接修改node_modules中的文件
  • Vue团队的所有项目及多数社区知名项目已经迁移到了pnpm
  • pnpm prune

Vite

  • 更快的冷启动
  • 超快的热重载
  • 通过环境变量更改后端api地址,不再需要请求config.json


vite-plugin-pages

  • 基于文件系统生成路由
  • 静态路由,支持debug
  • 配合vite-plugin-vue-layouts自动在路由设置中添加layout组件
src/pages/
  ├── users/
  │  ├── [id].vue
  │  └── index.vue
  └── users.vue
```ts [ { path: "/users", component: "/src/pages/users.vue", children: [ { path: "", component: "/src/pages/users/index.vue", name: "users" }, { path: ":id", component: "/src/pages/users/[id].vue", name: "users-id" } ] } ] ```

Auto Import

不用再写一大堆import

  • unplugin-vue-components 取代 vuetify-loader 自动导入 Vuetify 组件
  • unplugin-auto-import 自动import API

without

with

import { computed, ref } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
const count = ref(0)
const doubled = computed(() => count.value * 2)

自动导入注册 svg icons

把svg格式的图标放在相应文件夹下,即可在<v-icon>中使用

  • import.meta.glob一次性导入所有svg
  • vite-plugin-vue2-svg将导入的svg编译成vue component
  • 根据文件名在vuetify中注册同名图标
const svgIcons = Object.fromEntries(
  Object.entries(import.meta.globEager('@/assets/icons/*.svg')).map(
    ([k, v]) => [
      k
        .split(/(\\|\/)/g)
        .pop()!
        .replace(/\.[^/.]+$/, ''),
      { component: v.default },
    ]
  )
)
new Vuetify({icon:{values:{...svgIcons}}})

Volar

  • 模板TypeScript检查
  • .vue单组件文件类型
  • 单组件窗格分离
  • 通过@volar-plugins/vetur获得vetur的功能,迁移旧项目及js项目
  • 项目已内置 Vuetify 2 type支持


TypeScript Project Reference

  • 为不同环境提供正确的类型(app vs test)
  • VSCode中获得正确的intellisense
  • 防止意外引入不必要的源文件


layout: section

使用 Vue3 的新特性

更简洁的代码,更好的 ECMAScript 和 TypeScript 支持


Before

<script lang="ts">
import { reactive, defineComponent } from 'vue'
import BtnCount from './BtnCount.vue'
export default defineComponent({
  components: { BtnCount },
  setup() {
    const state = reactive({ count: 0 })
    function increment() {
      state.count++
    }
    return {
      state,
      increment
    }
  }
})
</script>
<template>
  <BtnCount @click="increment">
    {{ state.count }}
  </BtnCount>
</template>

With <setup script>

<script setup lang="ts">
import BtnCount from './BtnCount.vue'
const state = reactive({ count: 0 })
function increment() {
  state.count++
}
</script>
<template>
  <BtnCount @click="increment">
    {{ state.count }}
  </BtnCount>
</template>

Before

import type { PropType } from 'vue'
export default defineComponent({
  props: {
    dataset: {
      type: Array as PropType<number[][]>,
      required: false,
      default: () => [],
    },
    sym: {
      type: Boolean,
      default: false,
    },
    valueFormatter: {
      type: Function as PropType<(val: number) => string>,
      required: false,
      default: (val: number) => val.toFixed(3),
    },
  }
})

With <setup script>

const props = withDefaults(
  defineProps<{
    dataset?: number[][]
    sym?: boolean
    valueFormatter?: (val: number) => string
  }>(),
  {
    dataset: () => [],
    sym: false,
    valueFormatter: (val: number) => val.toFixed(3),
  }
)

在模板里写ES6

Before

<template>
  <span> {{ power && power.toFixed(1) }} </span>
</template>

vue-template-babel-compiler

<template>
  <span> {{ power?.toFixed(1) }} </span>
</template>

🍍Pinia

  • 支持 options api 和 composition api
  • 不再需要写mutation
  • 配合vue devtools debug 全局状态

layout: two-cols

Portal-Vue

在 Vue2 中使用 Vue3 内置的<Teleport>

  • 避免同页面组件间的数据交换
    • header
    • footer
  • 避免z-index地狱
    • dialog
    • notification

::right::


I18n

vue-i18n v8 + vue-i18n-bridge + unplugin-vue-i18n + i18n-Ally

  • Messages 预编译
  • SFC Custom block
// zh.json
{
  "viewLevel": {
    "private": "自己",
    "group": "组内",
    "public": "公开",
    "disabledInfo": "不能改变该对象可见级别,请确认你具有权限。"
  }
}
<!-- ViewLevel.vue -->
<i18n lang="json" locale="zh">
{
  "private": "自己",
  "group": "组内",
  "public": "公开",
  "disabledInfo": "不能改变该对象可见级别,请确认你具有权限。"
}
</i18n>

Vue-Echarts

Apache ECharts component for Vue.js

  • Auto resize
<v-chart autoresize />
  • Provide/Inject
const vuetify = useVuetify()
const { locale } = useI18n()
provide(
  THEME_KEY,
  computed(() => (vuetify?.theme.dark ? 'dark' : undefined))
)
provide(
  INIT_OPTIONS_KEY,
  computed(() => ({ locale: locale.value.toUpperCase() }))
)
  • echarts.on ➡️ Component Event listener
<v-chart
  @mousemove="onMousemove"
  @mouseout="onMouseout"
  @selectchanged="onSelectChanged"
/>

layout: section

Testing

—— 应测尽测,每天一次大筛


layout: two-cols

Vitest

  • 与 Vite 共享设置
  • 快如闪电的 watch mode
  • Jest expect compatible APIs

::right::


layout: two-cols

How to test tips

  • 黑盒测试

  • 避免组件的实现细节
    • 组件的内部状态
    • 组件的内部方法
    • 组件的生命周期方法
    • 子组件

::right::


Testing Library

  • 处理 DOM 节点而不是组件实例
  • 基于 @vue/test-utils 并隐藏了不适用于组件测试规范的方法
it('login correctly', async () => {
  const { getBytext, getByLabelText } = renderWithVuetify(loginPage)
  getByText('用户登录')
  const userInput = getByLabelText('用户名')
  await fireEvent.update(userInput, 'admin')

  const passwordInput = getByLabelText('密码')
  await fireEvent.update(passwordInput, 'admin')

  const button = getByText('登录')
  await fireEvent.click(button)

  const store = useUserStore()
  expect(store.login).toBeCalledWith({ username: 'admin', password: 'admin' })
})
  • 90%以上的测试不需要用到@vue/test-utils

Component Testing Example

it('message should show and disappear after seconds', async () => {
  vi.useFakeTimers()
  const { getByText, getByTitle, queryByText } = renderWithVuetify(AppMessage)
  const store = useMessageStore()
  store.messages = [
    { text: '测试消息', type: 'info', show: true, time: new Date(), id: 1 },
  ]
  getByText('没有新的通知')
  await flushPromises()
  getByText('测试消息')
  vi.runAllTimers()
  await nextTick()
  expect(queryByText('测试消息')).toBeNull()
  vi.useRealTimers()
  const buttonEmpty = getByTitle('清除所有通知')
  await fireEvent.click(buttonEmpty)
  getByText('没有新的通知')
})

Store Testing Example

vi.mock('@/api/monitor', () => {
  return {
    getLastCorrectData: vi.fn(() => ({
      data: [{ time_produced: '2022-04-30T13:00:00', id: 4 }],
    })),
  }
})
describe('Monitor Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  it('Should remove CorrectData more than 24h ago', async () => {
    const store = useMonitorStore()
    ;(store.correctList as any) = [
      { time_produced: '2022-04-20T13:00:00', id: 1 },
      { time_produced: '2022-04-21T13:00:00', id: 2 },
      { time_produced: '2022-04-30T12:00:00', id: 3 },
    ]
    await store.getLatestData()
    expect(store.correctList.length).toBe(2)
  })
})

Cypress(WIP) Upcoming

E2E测试,模拟真实的生产环境

  • Build完之后配合后端一起测试
    • Router
    • Request handling
    • Top-level components
    • Browser
  • Cypress也可以组件测试
    • style
    • native DOM events
    • cookies, localStorage

Why not Vue 2.7?

  • 还不够稳定
    • 和 Pinia 一起使用会报错
  • 社区还没跟上
    • vue-i18n-bridge 不支持 Vue 2.7
    • @vue/test-util 不能正常更新dom
  • 还不够稳定
  • 社区还没跟上

Coming Soon!

优点

  • 很多依赖已不再需要
  • 未来一年多官方会持续维护 2.7
  • 更严格的 type 检查
  • css-bind 等新功能,sfc语法、API与 Vue3 几乎完全相同

建议

  • Do one thing, and do it well
  • 思考更优的交互体验
  • 注意数组的性能
const weightedDist = computed(() => {
+ const mesh = burnupMesh.value!
  return dist.value
    ? dist.value.map((layer, j) =>
-       layer.map((val, i) => (val * burnupMesh.value[j][i]) / activeHeight.value!)
+       layer.map((val, i) => (val * mesh[j][i]) / activeHeight.value!)
      )
    : undefined
})

命名

  • physical experiment ➡ physics test
  • tripleAO ➡ AO3
  • ladder ➡ trapezoid
  • ...


Recap

  • 新的 Vitiy Admin 模板
  • Vite 生态
  • Vue3 新语法和全家桶
  • 单元、组件、E2E测试
  • 在线监测的一些改动
  • 前端技术栈及项目架构已达到企业级

layout: center class: 'text-center pb-5 :'

Thank You!

Slides can be found on docs.nustarnuclear.com