前端新脚手架搭建,Vite+Vue3+Vue-router4

基础依赖

构建工具

Vite: https://cn.vitejs.dev/config/

前端框架

Vue@next(+sfc setup):https://v3.cn.vuejs.org/api/

Vue-router@next: https://next.router.vuejs.org/zh/guide/

Vuex@next: https://next.vuex.vuejs.org/zh/

UI 框架

ant-design-vue: https://2x.antdv.com/components/overview-cn/

windicss: https://tailwindcss.com/docs/configuration

iconify: https://icon-sets.iconify.design/

辅助工具

编辑器插件:Volar

浏览器扩展:Vue.js devtools@6-beta

关于 UI 框架

以 ant-design-vue 为核心组件库,windicss 作为样式工具库,iconify 作为主要的 icon 来源。

关于 windicss

windicsstailwindcss的优化版本,两者均是下一代的 UI 框架。

与 bootstrap 不同,windicss 并不是通过修改变量的形式实现定制化,而是在构建时根据 css 类名来动态生成所需要的的 css,这样在灵活性和代码体积上都有很大的优势。;

很多样式不用拘泥与约束,例如间距,字号甚至字体颜色都不需要预定义,类似 p-1 代表 1rem 的 padding,p-1px 代表 1px 的 padding,而这些样式都是在构建时生成的,用多少就生成多少,能够最小程度地使用 css,降低了代码包的体积,同时也省去了配置超多的预设。

同时他也支持通过预设提前配置好各种变量,支持通过样式组合生成新的类名,使用更加灵活,模板看起来更加简洁。

1
2
3
4
5
6
7
8
9
10
.portlet {
@apply border-1 border-gray-200 border-solid py-15px px-20px;
}
.caption {
@apply text-green-500 font-bold text-16px pt-20px;

svg {
display: inline-block;
}
}

关于自定义指令

现在 vnode 无法获取到 context,也就是组件 this 实例对象了。同时由于 ts 类型校验,也无法往 vnode 上添加属性(本身官方也推荐该属性只读),但可以往 el 上添加自定义属性。

另外,如果指令中有副作用代码,记得在 unmounted 钩子中进行销毁。

关于自定义组件

由于安装了 unplugin-vue-components 插件,全局组件能够自动按需引入,无需导入和注册的过程,常用第三方组件也做了预设,但页面的私有组件是无法自动注册的,通过添加 unplugin-vue-components 自定义组件配置实现页面私有组件的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function kebabCase(key: string) {
const result = key.replace(/([A-Z])/g, " $1").trim();
return result.split(" ").join("-").toLowerCase();
}
ViteComponents({
customComponentResolvers: [
AntDesignVueResolver({
resolveIcons: true,
}),
ElementPlusResolver(),
VantResolver(),
ViteIconsResolver(),
(name) => {
// where `name` is always CapitalCase
if (name.startsWith("App")) {
return {
importName: "default",
path: `./components/${kebabCase(name)}/index.vue`,
};
}
},
],
globalComponentsDeclaration: true,
});

注意事项:

  1. 页面组件名必须以app开头
  2. 页面组件需要放置在./components/{{app-xxx}}/index.vue
  3. 全局组件(./src/components/**)没有路径和文件名限制,使用文件夹形式和单文件组件形式均可
  4. 全剧组件优先级比页面组件优先级高,建议页面组件在页面上加以区分

扩展插件

unplugin-auto-import

https://github.com/antfu/unplugin-auto-import

省略引用 Vue 全局 api,例如:

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
// import可省略
// import { ref, computed, watch } from 'vue'

const counter = ref(0);

const doubled = computed(() => counter.value * 2);

watch(doubled, (v) => {
console.log("New value: " + v);
});
</script>

vite-plugin-pages

https://github.com/hannoeru/vite-plugin-pages

基于文件系统路径自动生成 vue 路由,可配置 nuxt 风格。name 和 meta 可由 route 标签自定义,最终生成的还是基于 vue-router 的路由结构

1
<route lang="yaml"> name: name-override meta: requiresAuth: true </route>

注意如果有页面级别的组件需要将组件目录排除:

1
2
3
4
5
6
7
Pages({
exclude: [
"**/components/**/*.{vue,ts,js}",
"**/composables/**/*.{vue,ts,js}",
],
nuxtStyle: true,
});

vite-plugin-vue-layouts

https://github.com/JohnCampionJr/vite-plugin-vue-layouts

配合 vite-plugin-pages 使用,指定路由使用的 layout(新增 layout 需要重启服务

1
<route lang="yaml"> meta: layout: users </route>

unplugin-vue-components

https://github.com/antfu/unplugin-vue-components

自动按需引入组件,components 目录下的无需手动引入,插件还预设了包括 ant-design-vue,vant,element+等常见 UI 框架,这些组件均不需要手动写 import 和注册 components。

vite-plugin-style-import

https://github.com/anncwb/vite-plugin-style-import/blob/main/README.zh_CN.md

使用 ant-design-vue 等 UI 框架时,按需引入无需手动引入样式文件。

unplugin-icons

https://github.com/antfu/unplugin-icons

支持上千种 Iconify 中的 icon,以组件的形式引用,同样无需引入和注册,程序自动按需加载(但不支持动态组件的形式)。

vite-plugin-qiankun

https://github.com/tengmaoqing/vite-plugin-qiankun

qiankun 插件,可快速接入微前端(不完整接入)。

关于环境变量

关于环境变量的配置可参考官方文档:https://cn.vitejs.dev/guide/env-and-mode.html

但环境变量无法在配置文件 vite.config.ts 中使用,也有人提出相关的 issue:https://github.com/vitejs/vite/issues/1930 http://events.jianshu.io/p/4973bd983e96

好在 vite 提供了一个 api 可以加载当前的 env 文件,但此 api 在文档中没有记录,具体使用方法可以在上方 issue 中找到。

一些注意事项

关于 lodash

请使用 lodash-es 替代 lodash,在 treeshaking 之后能够减少大量的包体积。

关于使用 lodash 处理响应式数据

在使用 vue 响应式 api 时,如果需要使用 lodash 对响应式数据进行修改,需要注意对象指针变化导致的响应式失效的问题,举个例子:

A 模块代码:

1
2
3
4
5
6
7
8
export default function useSearch() {
const data = ref<any>([]);
// 调用api获取数据
apiProxy1.post("/api/xxx", toRaw(formState)).then((res) => {
data.value = res;
});
return data;
}

B 模块代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function useTable(data: Vue.Ref<any[]>) {
const page = ref(0);
const hit = ref(10);
const dataSource = computed(() =>
_.map(_.chunk(data.value, hit.value)[page.value], (o) => {
return {
test: o.test,
};
})
);
return {
dataSource,
};
}

组件代码:

1
2
3
4
5
6
7
8
9
10
<template>
<div v-for="o in dataSource" @click="change(o)">{{ o.test }}</div>
</template>
<script setup>
const { data } = useSearch();
const { dataSource } = useTable(data);
const change = (val) => {
o.test = o.test + "aaa";
};
</script>

这样的写法在展示上并没有什么问题,但当我们想修改数组某一项的属性时,发现视图并不会更新,说明数据的变化并没有响应。

问题出在了_.map 函数上!

当使用 map 函数之后,dataSource 中的每一个对象已经和原始 data 中的对象不是同一个引用了。所以即使 dataSource 中的属性发生了变化,也不会反馈到 data 中去,也就无法在视图上进行更新。

修改一下原来的写法:

A 模块:

1
2
3
4
5
6
7
8
9
10
11
12
export default function useSearch() {
const data = ref<any>([]);
// 调用api获取数据
apiProxy1.post("/api/xxx", toRaw(formState)).then((res) => {
data.value = _.map(data.value, (o) => {
return {
test: o.test,
};
});
});
return data;
}

B 模块代码:

1
2
3
4
5
6
7
8
9
export default function useTable(data: Vue.Ref<any[]>) {
const page = ref(0);
const hit = ref(10);
const dataSource = computed(() => _.chunk(data.value, hit.value));
return {
page,
dataSource,
};
}

组件代码:

1
2
3
4
5
6
7
8
9
10
<template>
<div v-for="o in dataSource[page]" @click="change(o)">{{ o.test }}</div>
</template>
<script setup>
const { data } = useSearch();
const { dataSource } = useTable(data);
const change = (val) => {
o.test = o.test + "aaa";
};
</script>

这种写法的能够保证 dataSource 中的每一项和 data 中的对象是同一个引用,修改后即可触发视图更新。

一些小疑问

useStore 和 store

在组合式 API 中使用 vuex 时,官方推荐了一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义 injection key
export const key: Vue.InjectionKey<Store<State>> = Symbol("injection key");

export const store = createStore<State>({
state: {
count: 0,
},
});

// 定义自己的 `useStore` 组合式函数
export function useStore() {
return baseUseStore(key);
}

组件使用时需要引用自定义的 useStore 函数:

1
2
3
import { useStore } from "../../../../store";
const store = useStore();
const count = computed(() => store.state.count++);

那么既然都要使用自定义的函数,为什么不直接使用导出的 store 呢?

1
2
import { store } from "../../../../store";
const count = computed(() => store.state.count++);

引用的文件路径也是一样的,而且还省去了获取 store 的过程,岂不是更方便?

经过对比发现两个 store 连引用也是一样的:

1
2
3
import { store, useStore } from '../../../../store'
const store2 = useStore()
console.log(store === store2) // true

发现的一些小问题

ant-design-vue 组件部分热重载失效

例如在组件上修改 css 属性,有时候能够热重载但有时候必须刷新页面才会生效(新增往往能生效但修改和删除经常不生效,怀疑是缓存问题)

eslint import/named 错误

可以修改写法:

1
2
import * as Vue from "vue"; // import { InjectionKey } from 'vue'
export const key: Vue.InjectionKey<Store<State>> = Symbol("injection key"); // export const key: InjectionKey<Store<State>> = Symbol('injection key')

如果是引入 Vue 全局 API,可以将 vue 改成 vue-demi 库,也可以解决:

1
import { InjectionKey } from "vue-demi";

前端新脚手架搭建,Vite+Vue3+Vue-router4
https://www.wobushi.top/2021/前端新脚手架搭建,Vite-Vue3-Vue-router4/
作者
Pride Su
发布于
2021年8月31日
许可协议