搭建 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容