鎖死 npm 版本號
npm config set save-prefix=''
1. 創(chuàng)建項目
以下命令二選一
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 或服務(wù)器時必須配置 build path
配置規(guī)則見文檔
在哪里配
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;
}
// 這里需要返回一個函數(shù)
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). 在當前目錄下創(chuàng)建一個.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 插件用來預(yù)加載所有的svg
問題:我們頁面的svg在路由切換的時候有可能還沒加載完成,會出現(xiàn)圖片加載慢的問題
解決:
1). 安裝 svgo 和 svgstore
pnpm i svgo svgstore
2). 創(chuàng)建 vite_plugins/svgstore.js
/* eslint-disable */
import path from 'path'
import fs from 'fs'
import store from 'svgstore' // 用于制作 SVG Sprites
import { optimize } from 'svgo' // 用于優(yōu)化 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') {
// 創(chuàng)建一個大的 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)
}
// 優(yōu)化大的 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> 這種方式只有內(nèi)置的屬性才能訪問到 onClick 是內(nèi)置的所以能訪問到
console.log(props.onClick)
// name 內(nèi)部沒有定義所以訪問不到
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. 打包靜態(tài)資源
如果我們需要引入圖片資源有兩種方式
1). 把圖片資源放到 public 目錄里,直接通過 public 目錄下的路徑引入
- public/images/logo.png
<img src="/images/logo.png" />
這樣我們打包后 dist 目錄下就會多一個 images 文件里面有我們的 logo.png
2). 我們自己創(chuàng)建的目錄,比如我在 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 代理到對應(yīng)的后端域名,
所以一定要保證我們是通過 localhost 來調(diào)這個接口的,如果使用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/, ""),
},
},
},

這樣我們調(diào) localhost:3000/api 就會代理到 http://f2e-sit.ccc.com