搭建 vite + vue3 + tsx 项目
锁死 npm 版本号
npm config set save-prefix=''
1. 创建项目
以下命令二选一
pnpm create vite@2.9.0 mangosteen-fe-1 -- --template vue-ts
npm create vite@2.9.0 mangosteen-fe-1 -- --template vue-ts
然后进入项目,分别运行
pnpm run dev
pnpm run build
运行 build 的时候报错
解决方法:在 tsconfig.json 里添加
{
"compilerOptions": {
+ "skipLibCheck": true,
}
}
build path
把 HTML、CSS、JS 部署到 GitHub 或服务器时必须配置 build path
配置规则见文档
在哪里配
vite.config.js 里添加 base: '/' 或 '/reponame/' 等
run preview
- 运行目的
看看 dist 目录是否能正常运行 - 大约等价于
pnpm i http-server
http-server -p 4173 dist
2.部署到 Github
1). 将我们的 dist 目录上传,然后把 dist 目录的路径添加到 vite.config.ts 的 base 字段里
export default defineConfig({
+ base: '/bill-fe/dist/',
})
2). 重新运行
pnpm run build
3). push
4). 删除远程的 dist 目录
将我们的 dist 加入到 ignore 里,然后运行
git rm -r --cached dist
然后再重新 add commit push
3. template vs tsx
template 写法
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0)
const onClick = () => {
count.value += 1
}
</script>
tsx 写法
1). 新建一个 .tsx 文件
import { defineComponent, ref } from 'vue';
export const App = defineComponent({
setup() {
const refCount = ref(0);
const onClick = () => {
refCount.value += 1;
}
// 这里需要返回一个函数
return () => (
<>
<div>
{refCount.value}
</div>
<div>
<button onClick={onClick}>+1</button>
</div>
</>
)
}
})
2). 安装 @vitejs/plugin-vue-jsx 插件
pnpm i -D @vitejs/plugin-vue-jsx
3). 在 vite.config.ts 里配置 vueJsx
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
+ vueJsx({
transformOn: true,
mergeProps: true,
})
]
})
4. 引入 vue router 4
1). 安装
pnpm i vue-router@4
2). 使用
- main.ts
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router';
import {App} from './App';
import { Bar } from './views/Bar';
import { Foo } from './views/Foo';
const routes = [
{
path: '/', component: Foo
},
{
path: '/about', component: Bar
}
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
const app = createApp(App)
app.use(router)
app.mount('#app')
- App.tsx
import { defineComponent } from 'vue';
import { RouterView } from 'vue-router';
export const App = defineComponent({
setup() {
return () => (
<RouterView />
)
}
})
5. 使用 css module 和全局 css
使用 css module
1). 在当前目录下创建一个.module.scss 文件
2). 引入这个 css 文件通过变量名的形式
3). 通过 s.样式名来使用
- Welcome.module.scss
.wrapper {
color: red;
}
- Welcome.tsx
import { defineComponent } from 'vue';
import s from './Welcome.module.scss';
export const Welcome = defineComponent({
setup: (props, context) => {
return () => (
<div class={s.wrapper}>
aaa
</div>
)
}
});
因为我们用的是 sass 所以需要使用 pnpm i sass
使用全局 css
1). 新建一个.css 文件
2). 直接通过 import './***.css' 引入
6. 使用 slot 插槽
import { defineComponent } from 'vue';
import s from './First.module.scss';
export const First = defineComponent({
setup: (props, {slots}) => {
return () => (
<div class={s.wrapper}>
<div class={s.card}>
{slots.icon?.()}
{slots.title?.()}
</div>
<div class={s.actions}>
{slots.buttons?.()}
</div>
</div>
)
}
})
- demo
import { WelcomeLayout } from './WelcomeLayout';
export const First = defineComponent({
setup: (props, context) => {
const slots = {
icon: () => <span>icon</span>,
title: () => 'hi',
buttons: () => <><button>+1</button></>
}
return () => (
<WelcomeLayout v-slots={slots} />
)
}
})
或者
export const First = defineComponent({
setup: (props, context) => {
return () => (
<WelcomeLayout>
{{
icon: () => <span>icon</span>,
title: () => 'hi',
buttons: () => <><button>+1</button></>
}}
</WelcomeLayout>
)
}
})
7. 使用多个 RouterView
router.tsx
{
path: '/welcome',
component: Welcome,
children: [
{ path: '', redirect: '/welcome/1', },
{ path: '1', components: { main: First, footer: FirstActions }, },
{ path: '2', components: { main: Second, footer: SecondActions }, },
{ path: '3', components: { main: Third, footer: ThirdActions }, },
{ path: '4', components: { main: Forth, footer: ForthActions }, },
]
}
- demo
import { RouterView } from 'vue-router';
export const Welcome = defineComponent({
setup: (props, context) => {
return () => <div class={s.wrapper}>
<header>
<img src={logo} />
<h1>山竹记账</h1>
</header>
<main class={s.main}><RouterView name="main" /></main>
<footer>
<RouterView name="footer" />
</footer>
</div>
}
})
路由动画
<main class={s.main}>
<RouterView name="main">
{({Component: Content, route: R}: { Component: VNode, route: RouteLocationNormalizedLoaded}) => (
<Transition
enterFromClass={s.slide_fade_enter_from}
enterActiveClass={s.slide_fade_enter_active}
leaveToClass={s.slide_fade_leave_to}
leaveActiveClass={s.slide_fade_leave_active}
>
{Content}
</Transition>
)}
</RouterView>
</main>
.slide_fade_enter_active,
.slide_fade_leave_active {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
transition: all 0.5s ease-out;
}
.slide_fade_enter_from {
transform: translateX(100vw);
}
.slide_fade_leave_to {
transform: translateX(-100vw);
}
8. 写一个svg vite 插件用来预加载所有的svg
问题:我们页面的svg在路由切换的时候有可能还没加载完成,会出现图片加载慢的问题
解决:
1). 安装 svgo 和 svgstore
pnpm i svgo svgstore
2). 创建 vite_plugins/svgstore.js
/* eslint-disable */
import path from 'path'
import fs from 'fs'
import store from 'svgstore' // 用于制作 SVG Sprites
import { optimize } from 'svgo' // 用于优化 SVG 文件
export const svgstore = (options = {}) => {
const inputFolder = options.inputFolder || 'src/assets/icons';
return {
name: 'svgstore',
// 解析 如果文件是 @svgstore 直接加载 svg_bundle.js
// 引入的时候直接使用 import '@svgstore'
resolveId(id) {
if (id === '@svgstore') {
return 'svg_bundle.js'
}
},
load(id) {
if (id === 'svg_bundle.js') {
// 创建一个大的 svg
const sprites = store(options);
const iconsDir = path.resolve(inputFolder);
// 遍历所有的svg,然后把每一个都添加到这个大的里
for (const file of fs.readdirSync(iconsDir)) {
const filepath = path.join(iconsDir, file);
const svgid = path.parse(file).name
let code = fs.readFileSync(filepath, { encoding: 'utf-8' });
sprites.add(svgid, code)
}
// 优化大的 svg
const { data: code } = optimize(sprites.toString({ inline: options.inline }), {
plugins: [
'cleanupAttrs', 'removeDoctype', 'removeComments', 'removeTitle', 'removeDesc',
'removeEmptyAttrs',
{ name: "removeAttrs", params: { attrs: "(data-name|data-xxx)" } }
]
})
// 把这个大的 svg 变成js文件
return `const div = document.createElement('div')
div.innerHTML = \`${code}\`
const svg = div.getElementsByTagName('svg')[0]
if (svg) {
svg.style.position = 'absolute'
svg.style.width = 0
svg.style.height = 0
svg.style.overflow = 'hidden'
svg.setAttribute("aria-hidden", "true")
}
// listen dom ready event
document.addEventListener('DOMContentLoaded', () => {
if (document.body.firstChild) {
document.body.insertBefore(div, document.body.firstChild)
} else {
document.body.appendChild(div)
}
})`
}
}
}
}
3). 在 vite.config.ts 里注册这个配置
import { svgstore } from './src/vite_plugins/svgstore';
export default defineConfig({
plugins: [
+ svgstore(),
]
})
4). 在入口文件中引入我们的svgstore
- main.ts
import '@svgstore';
5). 将我们的 <img> 标签换成 svg
<svg>
<use xlinkHref='#chart'></use>
</svg>
9. hooks
- useSwipe
import { computed, onMounted, onUnmounted, ref, Ref } from "vue"
type Point = {
x: number;
y: number;
}
export const useSwipe = (element: Ref<HTMLElement | null>) => {
const start = ref<Point | null>(null)
const end = ref<Point | null>(null)
const swiping = ref(false)
const distance = computed(() => {
if (!start.value || !end.value) { return null }
return {
x: end.value.x - start.value.x,
y: end.value.y - start.value.y,
}
})
const direction = computed(() => {
if (!distance.value) { return '' }
const { x, y } = distance.value
if (Math.abs(x) > Math.abs(y)) {
return x > 0 ? 'right' : 'left'
} else {
return y > 0 ? 'down' : 'up'
}
})
const onStart = (event: TouchEvent) => {
swiping.value = true
end.value = start.value = { x: event.touches[0].screenX, y: event.touches[0].screenY }
}
const onMove = (event: TouchEvent) => {
if (!start.value) { return }
end.value = { x: event.touches[0].screenX, y: event.touches[0].screenY, }
}
const onEnd = (event: TouchEvent) => {
swiping.value = false
}
onMounted(() => {
if (element.value) {
element.value.addEventListener('touchstart', onStart)
element.value.addEventListener('touchmove', onMove)
element.value.addEventListener('touchend', onEnd)
}
})
onUnmounted(() => {
if (element.value) {
element.value.removeEventListener('touchstart', onStart)
element.value.removeEventListener('touchmove', onMove)
element.value.removeEventListener('touchend', onEnd)
}
})
return {
swiping,
direction,
distance
}
}
使用
export const Welcome = defineComponent({
setup: (props, context) => {
const main = ref<HTMLElement | null>(null)
const { direction, swiping } = useSwipe(main)
return () => (
<main ref={main/>
)
}
10. 自定义组件类型声明
- 子组件
// 方法1
interface Props {
onClick: (event: MouseEvent) => void;
name: 'add' | 'chart';
}
export const Button = defineComponent<Props>({
setUp: (props, context) => {
// 使用<Props> 这种方式只有内置的属性才能访问到 onClick 是内置的所以能访问到
console.log(props.onClick)
// name 内部没有定义所以访问不到
console.log(props.name)
}
})
// 方法2(获取我们自己定义的 props)
export const Button = defineComponent({
props: {
name: {
// String 是js PropType里面是 ts
type: String as PropType<'add' | 'chart'>
}
}
setUp: (props, context) => {
console.log(props.name)
}
})
- 父组件
const onClick = () => {}
<Button onClick={onClick} name={'lifa'}>按钮</Button>
11. 打包静态资源
如果我们需要引入图片资源有两种方式
1). 把图片资源放到 public 目录里,直接通过 public 目录下的路径引入
- public/images/logo.png
<img src="/images/logo.png" />
这样我们打包后 dist 目录下就会多一个 images 文件里面有我们的 logo.png
2). 我们自己创建的目录,比如我在 src/assets/icons/logo.png
那么我们可以通过 import 语法
import logo from "@/assets/icons/logo.png";
<img src={logo}
这样打包后就会生成一个 asset/logo.chunk值.png
12. proxy
使用 proxy 就是 将你本地的 localhost:3000/api 代理到对应的后端域名,
所以一定要保证我们是通过 localhost 来调这个接口的,如果使用axios的话,baseUrl 要写成 /
server: {
// Listening on all local IPs
cors: true,
proxy: {
"/api": {
target: "http://f2e-sit.ccc.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
这样我们调 localhost:3000/api 就会代理到 http://f2e-sit.ccc.com