- 本博客使用图片和代码来自
jirengu.com
技术选型
Vue 3+TSX+Rails
Vue 3 + TSX 是比较新的前端框架,像使用React一样使用Vue3,体验比React更好;
Rails 7 是比较好用的后端框架;缺点是无法利用模板的性能优化;
-
template 对比 TSX ,请查看我的另一篇博客:Vue中使用 JSX或TSX语法
-
setup函数,请查看我的另一篇博客详解Vue 3中setup函数 -
插件
plugin-vue-jsx,使用TSX之前需要安装
pnpm i -D @vitejs/plugin-vue-jsx
// vite-config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(),
vueJsx({
transformOn: true,
mergeProps: true
}),
})
1.1 项目需求
记账、云同步、可视图表
1.2 项目架构
-
用户使用浏览器发送请求,通过Nginx代理分发请求,依照路由分发:
1)后端请求:路由为`/api/v1/resources`会全部重定向到Rails 2)前端请求:路由为`index.html``style-xxx.scss`等静态文件,会在目录里返回数据 3)除上述两种路由外的路径,重定向到`index.html` -
通过VueRouter,根据不同的路径加载对应的
view视图 -
每个视图用到的多个组件,就去
components中拿到对应组件 -
用最新的状态管理库
Pinia来做hooks,然后hooks通过AJAX发请求到后端 -
登陆时用
JWT、访问API用Controller吐出的JSON -
数据库的加载:
Controller会调用Model,而Model会去调用Postgresql
环境搭建(开发环境)
// 锁死 npm 版本号
npm config set save-prefix=''
// 初始化pnpm仓库 14.1.1
pnpm i -g http-server
// 如果报错 ERROR Unable to find the global bin directory
// pnpm setup | Created /Users/jialonliu/.zshrc
// source /Users/jialonliu/.zshrc | 再运行 http-server即可
// 创建项目 framework:vue variant:vue-ts
pnpm create vite@2.9.0 mangosteen-fe-4 -- --template vue-ts
// 用vscode打开项目
code mangosteen-fe-4
// 安装项目依赖,项目中保持使用pnpm 而不能混用yarn或npm
pnpm i
// 开启开发用的服务器端口
pnpm run dev
// 注意 pnpm 和yarn或者npm不能混用 不小心安装错了依赖
rm -rf node_modules
pnpm i
// 让vue支持jsx插件
pnpm i -D @vitejs/plugin-vue-jsx
2.1 打包相关
pnpm run build
// build path 部署到GitHub时必须配置 vite.config.js里添加 base:'/'或‘/reponame/’
// 打包后打开8080端口 自动转发 | 也可以用 pnpm run preview 打开4173端口
http-server dist
// 如果打包报错 尝试先 pnpm install vite@latest vue-tsc@latest 再重新打包
// 如果仍报错 rm -rf node_modules rm -rf pnpm-lock.yaml
// 再把package.json中的 vue-tsc校验删掉 再重新打包
// 锁死版本号 把package.json中的带`^`符号的替换为空,重新安装依赖 pnpm i
// tsconfig.json
"compilerOptions": {
"skipLibCheck": true, // 新增配置 解决打包报错 然而并没解决 先继续再说
2.2 部署相关
// 本地 git init | git add . | git commit -m init (--amend)修复
创建完远程仓库以后,本地与远程建立链接
git remote add origin https://github.com/jianlong96/githubpages-demo-1.git
git remote set-url origin [url] // 修改推送源
git branch -M main // 当前分支重命名为 main 可以省略 本地默认就是 main
git push -u origin main // 把当前本地分支 push到远程仓库的 main分支
// 如果 push时贼慢,就设置淘宝镜像 弃用官方镜像 npm config set registry https://registry.npmmirror.com/
// build部署 删掉.gitignore里的dist文件 重新git add . git commit -m 'dist' git push -u origin main
// index.html 所在的目录复制到本地 `vite.config.ts`中,再重新build 再部署一遍
export default defineConfig({
base:'/githubpages-demo-1/dist/',
plugins: [vue()]
})
// 部署完毕后 生成页面链接
// 部署测试结束后,恢复到开发模式,删掉base配置(部署github时才配置),恢复dist的.gitignore 同时删掉远程git仓库的dist:
git rm -r --cached dist
2.3 页面划分
pnpm i vue-router@4.1.5
// routes.tsx
import { RouteRecordRaw } from 'vue-router'
export const routes: RouteRecordRaw[] = [
{ path: '/welcome', redirect: '/welcome/1' },
]
| 页面标识 | 路径 |
|---|---|
| 欢迎 | /welcome/1-4 |
| 开始 | /start |
| 记一笔 | /items/new |
| 新建标签 | /tags/new |
| 更新标签 | /tags/:id |
| 列表清单 | /items |
| 登陆 | /session/new |
| 图表 | /statistics |
2.4 组件规划
| 组件标识 | 名称 |
|---|---|
| 欢迎页 | layout/welcome |
| 主要内容 | layout/main |
| 选项卡 | tabs |
| 按钮 | button |
| 浮动层 | overlay |
| 线状图表 | lineChart |
| 饼状图表 | pieChart |
2.5 使用 CSS Modules
因为本项目摒弃了 template 配合 scoped 的css方案,使用tsx配合css module的样式方案,这样做的优点是:vite 零配置(吊打webpack),只需要安装 sass 依赖就行,比webpack安装各种loader要方便很多
pnpm i sass // 安装`sass`
import s from './Welcome.module.scss' // s 是 style 的缩写
2.6 手机页面调试(不要等到功能做完再用手机测)
- 基本原则:
-
常见浏览器优先(微信内嵌页面、系统浏览器、UC、腾讯浏览器)
-
真机优先(调试器无法体现所有bug)
-
远程调试工具
安卓搜索 Chrome远程调试手机页面(只兼容Webkit内核浏览器,Firefox除外)
苹果搜索 Safari远程调试手机页面
非Webkit浏览器用模拟控制台Tencent/vConsole(适合所有浏览器)
- 具体步骤:
- 修改默认路由:
{ path:'/', redirect:'/welcome'},
{ path:'/welcome', component:Welcome, children:[
{path:'', redirect:'/welcome/1'},
{path:'1', component:First},
{path:'2', component:Second},
{path:'3', component:Thrid},
{path:'4', component:Forth},
]}
- 连接手机到电脑
参考淘宝viewport标签:
<meta name="viewport" content="width=device-width,initial-scale=1,
minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
taobao底部导航栏用的fix定位,而项目用的是flex定位
页面的层叠结构为:app > page > welcome_wrapper > header/main
先定义样式变量:
:root {
--welcome-card-bg: white;
}
> .actions{
position: fixed;
z-index: 1;
left: 0;
bottom: 0;
width: 100%;
padding: 24px 16px;
}
2.7 微信页面调试
Webview in com
制作 欢迎页面 Welcome
- 页面
3.1 封装 WelcomeLayout 组件
封装的原则是出现代码的重复,四个欢迎页面有很多重复,只有几处是不重复的,就用插槽来代替:icon图片、h2文字、button按钮
插槽使用规则的文档在 babel-plugin-jsx ,在里面找到 babel-plugin-jsx 文档:GitHub - vuejs/babel-plugin-jsx: JSX for Vue 3,搜索 slot
//内部组件 WelcomeLayout.tsx
export const WelcomeLayout = defineComponent({
setup: (props, context) => {
const { slots } = context
return ()=>(
<div class={s.wrapper}>
<div class={s.card}>
{slots.icon?.()}
{slots.title?.()}
</div>
<div class={s.actions}>
{slots.buttons?.()}
</div>
</div>
)
}
})
// 内部组件 WelcomeLayout.tsx 简写
export const WelcomeLayout: FunctionComponent = (props, context) =>{
const {slots:{icon, title, buttons}} =context
return (
<div class={s.wrapper}>
<div class={s.card}>
{icon?.()}
{title?.()}
</div>
<div class={s.actions}>
{buttons?.()}
</div>
</div>
//使用封装的组件-写法1
import { WelcomeLayout } from './WelcomeLayout';
export const First = defineComponent({
setup: (props, context) => {
const slots = {
icon:() => <img class={s.pig} src={pig} />,
title: () => <h2>会挣钱<br />还要会省钱</h2>,
buttons: () => <>
<RouterLink class={s.fake} to="/start">跳过</RouterLink>
<RouterLink to="/welcome/2">下一页</RouterLink>
<RouterLink to="/start">跳过</RouterLink>
</>
}
return ()=>(
<WelcomeLayout v-slots={slots}></WelcomeLayout>
)
}
})
//使用封装的组件-写法2
import { WelcomeLayout } from './WelcomeLayout';
export const Second = defineComponent({
setup: (props, context) => {
return ()=>(
< WelcomeLayout >
{{
icon:()=> <img class={s.pig} src={pig} />,
title: () => <h2>会挣钱<br />还要会省钱</h2>,
buttons: () => <>
<RouterLink class={s.fake} to="/start">跳过</RouterLink>
<RouterLink to="/welcome/2">下一页</RouterLink>
<RouterLink to="/start">跳过</RouterLink>
</>
}}
</WelcomeLayout>
)
}
})
//使用封装的组件-写法3
import { WelcomeLayout } from './WelcomeLayout';
export const Third = () => {
return(
<WelcomeLayout>
{{
icon:()=> <img class={s.pig} src={pig} />,
title: () => <h2>会挣钱<br />还要会省钱</h2>,
buttons: () => <>
<RouterLink class={s.fake} to="/start">跳过</RouterLink>
<RouterLink to="/welcome/2">下一页</RouterLink>
<RouterLink to="/start">跳过</RouterLink>
</>
}}
</WelcomeLayout>
)
}
Third.displayName = 'Third'
3.2 为不同广告页添加动画过渡效果
重构路由(明确添加动画的对象:不同页的logo和下面一行文字,其他不需要动画):
{path:'1', components:{main: First, footer: FirstAction}}
<main class={s.main}><RouterView name="main"></main>
<footer><RouterView name="footer"></footer>
搜索vue js router animation得到如何加动画效果的方法
<router-view v-slot="{ Component }">
<transition name="fade">
<component :is="Component" /> // 前面component是占位符
</transition>
</router-view>
可以写为:
<router-view v-slot="{ Component }">
{ ()=>{} }
</router-view>
router-view v-slot="{ Component }">
{ (obj:any)=>{ return <div class='main'><obj.Component/></div> }
</router-view>
router-view v-slot="{ Component }">
{ (obj:any)=>{ return <transition name="fade"><obj.Component/></transition>} }
</router-view>
搜索vue transition用法
<Transition name="fade">
...
</Transition>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
以上初步实现的动画效果,需要自定义动画效果:
<Transition name="slide-fade">
<p v-if="show">hello</p>
</Transition>
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.8s ease-out;
}
.slide-fade-enter-from{
transform: translateX(100vw); // 从屏幕宽度最右方进入
}
.slide-fade-leave-to {
transform: translateX(-100vw);
opacity: 0;
}
目前动画过程是两页同时出现(flex上下布局),需要改为先后出现,加绝对定位
.slide-fade-enter-from{
transform: translateX(100vw); // 从屏幕宽度最右方进入
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%
}
.slide-fade-leave-to {
transform: translateX(-100vw);
opacity: 0;
}
以上样式是加在全局样式中的,可以改写为用transition自身属性来实现相同效果:
<RouterView name="main">
// 解构了上述 obj:any 再把之前的 any类型定义
{
({ Component: X, 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}>
{X}
</Transition>
}
</RouterView>
Vue里叫插槽(默认是个函数,传参数可以动态渲染)React里叫Childern
3.3 自制 Vite SVG Sprites 插件
CSS sprite雪碧图是把所有
png都放在一起,不适合svg这种矢量图
Vite SVG Sprite 本质为webpack-loader,能够把所有的icons都load进项目中,具体使用时直接写icon name
pnpm i svgo@2.0.0
pnpm i svgstore@3.0.1
- 实现过程:
export const svgstore = (options = {})=>{
const inputFolder = options.inputFolder || 'src/assets/icons';
return {
name:'svgstore',
resolveId(id){ // 兼容 vscode或其他 编辑器
if(id === '@svgstore'){
return 'svg_bundle.js'
}
},
load(id){
if(id === 'svg_bundle.js'){
const sprites = store(options);
const iconsDir = path.resolve(inputFolder);
for(const file if fs.readdirSync(iconsDir)){
const filepath = path.join()
}
}
}
}
}
3.4 支持触控滑动事件「封装useSwipe Hook」
- hooks 相当于 vue中的 Composition API
src下新建hooks/useSwipe.tsx,通过监听触控移动的坐标变化情况(按下手指的一瞬间start,松开手指结束时end),来判断是否滑动到下一页
// hooks/useSwipe.tsx
onMounted(()=>{if (!element.value) {return} })
onUnmounted(() => {
if (!element.value) { return }
element.value.removeEventListener('touchstart', onStart)
element.value.removeEventListener('touchmove', onMove)
element.value.removeEventListener('touchend', onEnd)
})
watchEffect类似于 React 的useEffect,作用是只要当前作用域的任何一个变量发生了变化,就执行函数
// Welcome.tsx
export const Welcome = defineComponent({
setup: (props, context) => {
const main = ref<HTMLElement | null>(null)
const { direction, swiping } = useSwipe(main)
watchEffect(() => {
})
制作 开始页面 StartPage
- 页面
{path:'/start', component: StartPage}
// StartPage.tsx
import {defineComponent} from 'vue';
import s from './StartPage.module.scss';
import {Button} from '../shared/Button';
export const StartPage = defineComponent({
setup:(props, context) => {
const onClick = ()=>{console.log('hi')}
return ()=> (
<div>
<div class={s.button_wrapper}>
// 把onClick绑定在组件上时,vue默认实现了:组件最外层的根元素被点击时就调用onClick函数
// 可通过 inheritAttrs:false 取消自动继承属性
<Button class={s.button} onClick={onClick}>测试</Button>
</div>
</div>
)
}
})
4.1 关于属性和类型声明
- vue自身声明属性类型(允许的组件属性)有
class和style,比如<div class={s.button}> onClick写在Button组件事件绑定时,就会报错需要类型声明defineComponent接收两种参数,一种是JS参数(),即对象(setup函数),另一种是TS参数<>,即泛型,尖括号内可设置props接口,如下写法:
interface Props{
onClick: Function
// 可以把函数的参数和返回值分开写
onClick: (e:MouseEvent) => void
}
export const StartPage = defineComponent<Props>({
4.2 创建 Button 组件
// StartPage.tsx
import {defineComponent} from 'vue';
import s from './Button.module.scss';
export const Button = defineComponent({
setup:(props, context) => {
return ()=> (
<button>
{context.slots.default?.()}
</button>
)
}
})
4.3 创建Icon 和 FloatButton 组件
// Icon.tsx (完整的 Vue 3 + TSX 组件写法)
import {defineConmponent} from 'vue';
import s from './Icon.module.scss';
export const Icon = defineComponent({
props:{
name:{
type: String as PropType< 'add'| 'chart'>
}
}
setup:(props, context)=> {
props.name
return ()=>{
<svg class={s.icon}>
<use xlinkHref={'#'+ props.name}></use>
</svg>
}
}
})
// FloatButton.tsx
import {defineConmponent} from 'vue';
import s from './FloatButton.module.scss'
import {Icon} from './Icon';
export const FloatButton = defineComponent({
setup:(props, context)=> {
return ()=>{
<div>
<Icon name="add" class={s.icon}/>
</div>
}
}
})
4.4 制作 Center 组件(实现图标或其他自动居中)
import { defineComponent, PropType } from 'vue';
import s from './Center.module.scss';
const directionMap = {
'-': 'horizontal',
'|': 'vertical',
'horizontal': 'horizontal',
'vertical': 'vertical'
}
export const Center = defineComponent({
props: {
direction: {
type: String as PropType<'-' | '|' | 'horizontal' | 'vertical'>,
default: 'horizontal'
}
},
setup: (props, context) => {
const extraClass = directionMap[props.direction]
return () => (
<div class={[s.center, extraClass]}>{
context.slots.default?.()
}</div>
)
}
})
// StartPage.tsx
<Center class={s.pig_wrapper}>
<Icon name="pig" class={s.pig} />
</Center>
4.5 制作 Navbar 组件
import { defineComponent, PropType } from 'vue';
import s from './Navbar.module.scss';
export const Navbar = defineComponent({
props: {
name: {
type: String as PropType<string>
}
},
setup: (props, context) => {
const { slots } = context
return () => (
<div class={s.navbar}>
<span class={s.icon_wrapper}>
{slots.icon?.()}
</span>
<span class={s.title_wrapper}>
{slots.default?.()}
</span>
</div>
)
}
})
- 合并以上组件,完成
StartPage页面
import xx from xx;
export const StartPage = defineComponent({
setup: (props, context) => {
const refOverlayVisible = ref(false)
const onClickMenu = () => {
refOverlayVisible.value = !refOverlayVisible.value
}
return () => (<div>
<Navbar>{
{default: () => '山竹记账',
icon: () =>
<Icon name="menu"
class={s.navIcon}
onClick={onClickMenu} />
}
}</Navbar>
<Center class={s.pig_wrapper}>
<Icon name="pig" class={s.pig} />
</Center>
<div class={s.button_wrapper}>
<RouterLink to="/items/create">
<Button class={s.button}>开始记账</Button>
</RouterLink>
</div>
<RouterLink to="/items/create">
<FloatButton iconName='add' />
</RouterLink>
{refOverlayVisible.value
&& <Overlay onClose={() => refOverlayVisible.value = false} />}
</div>)
}
})
4.6 制作 Overlay 组件
- 写样式时预留刘海屏的上部区域
- 点击
StartPage页面的菜单按钮,出现浮动层页面(绑定value为true) - 点击事件响应先写在icon上,不写在外层的Navbar上(也支持后退到新路由页面):
// StartPage.tsx
const onClickMenu = ()=>{}
return () => (
<Navbar>{
{
default:'山竹记账',
icon:<Icon name="menu" class={s.navIcon} onClick={onClickMenu} />
}
}</Navbar>
)
// Overlay.tsx
import {defineComponent, PropType} from 'vue';
export const Overlay = defineComponent({
setup:(props, context) => {
return () => (
<div>Overlay</div>
)
}
})
// StartPage.tsx
const overlayVisible = ref(false) // 用变量控制是否存在
const onClickMenu = () => {
overlayVisible.value = !overlayVisible.value
}
return () => {
<div>
<Navbar></Navbar>
<Center></Center>
<div>
<Routerlink>
<Button>
</Routerlink>
</div>
{refOverlayVisible.value &&
<Overlay onClose={() => refOverlayVisible.value = false} />
}
</div>
}
制作记一笔页面 Itemcreate
在
#/start首页下,点击开始记账 或 Floatbutton 跳转到 记一笔 ItemCreate 页面#/items/create
- 页面
// Itemcreate.tsx
<MainLayout>{{
title: () => '记一笔',
icon: () => <Icon name='left' class={s.navIcon} />,
default: () => <>
<Tabs v-model:selected={refKind.value}>
<Tab name="支出">
Icon列表1
</Tab>
<Tab name="收入">
Icon列表2
</Tab>
</Tabs>
<div class={s.inputPad_wrapper}>
<InputPad />
</div>
</>
}}</MainLayout>
5.1 新增 item 相关路由
{path:'/items', component: ItemsPage,
children:[
{path:'', component:Itemlist},
{path:'create', component:ItemCreate},
]
}
// StartPage.tsx
<div class={s.button_wrapper}>
<RouterLink to="/items/create">
<Button class={s.button}>开始记账</Button>
</RouterLink>
</div>
<RouterLink to="/items/create">
<FloatButton iconName='add' />
</RouterLink>
5.2 制作 MainLayout 组件
import { defineComponent, PropType, ssrContextKey } from 'vue';
import { Navbar } from "../shared/Navbar";
export const MainLayout = defineComponent({
setup: (props, context) => {
return () => (
<div>
<Navbar>{
{
default: () => context.slots.title?.(),
icon: () => context.slots.icon?.()
}
}</Navbar>
{context.slots.default?.()}
</div>
)
}
})
- 重构
StartPage页面,使用MainLayout组件
import xx from 'xx';
export const StartPage = defineComponent({
setup: (props, context) => {
const refOverlayVisible = ref(false)
const onClickMenu = () => {
refOverlayVisible.value = !refOverlayVisible.value
}
return () => (
<MainLayout>{
{
title: () => '山竹记账',
icon: () => <Icon name="menu" class={s.navIcon} onClick={onClickMenu} />,
default: () => <>
<Center class={s.pig_wrapper}>
<Icon name="pig" class={s.pig} />
</Center>
<div class={s.button_wrapper}>
<RouterLink to="/items/create">
<Button class={s.button}>开始记账</Button>
</RouterLink>
</div>
<RouterLink to="/items/create">
<FloatButton iconName='add' />
</RouterLink>
{refOverlayVisible.value &&
<Overlay onClose={() => refOverlayVisible.value = false} />
}</>
}
}</MainLayout>
)
}
})
5.3 设计 Tabs 接口
Tabs通过selected属性记住用户选择的tab(切换时改变的变量),由于切换时导致selected值变化,Tabs通过onUpdateSelected事件监听值的变化,把值传出,onUpdateSelected函数接收传入的name参数,返回出refKind.value再传给name
// ItemCreate.tsx
setup:(props, context)=>{
const refKind = ref('支出')
default:()=><>
<Tabs selected={refKind.value} onUpdateSelected={ name => refKind.value = name}>
<Tab name="支出">Icon列表1</Tab> // 只能是Tab标签
<Tab name="收入">Icon列表2</Tab>
</Tabs>
</>
}
5.4 实现 Tabs 功能
- 在
ItemCreate中使用Tab时,只能用Tab标签,而不能用div或其他
const array = context.slots.default?.()
if (!array) return () => null
for (let i = 0; i < array.length; i++) {
if (array[i].type !== Tab) {
throw new Error('<Tabs> only accepts <Tab> as children')
}
}
// Tabs.tsx
{array.map(item =>
<li class={item.props?.name === props.selected ? s.selected : ''}
onClick={() => props.onUpdateSelected?.(item.props?.name)}>
{item.props?.name}
</li>)}
// ItemCreate.tsx
<Tabs selected={refKind.value} onUpdateSelected={name => refKind.value = name}>
- 使用
v-model简化代码:
// Tabs.tsx
{array.map(item =>
<li class={item.props?.name === props.selected ? s.selected : ''}
// 改写为 context.emit
onClick={() => context.emit('update:selected', item.props?.name)}>
{item.props?.name}
</li>)}
5.5 制作 数字按键 页面
- 封装 time.tsx
- 使用 Vant UI 制作数字按键
- 数字输入检查
5.6. 制作 记一笔「新建标签」 页面
记一笔的 tagList 列表,由
创建tag和编辑tag组成,分别对应的跳转页面是 新建标签 和 编辑标签(标签详情)
{
path: '/tags', component: TagPage,
children: [
{ path: 'create', component: TagCreate },
{ path: ':id', component: TagEdit }
]
}
5.7 制作 记一笔「编辑标签」 页面
- 封装
Emoji组件
tagList 页面
- 页面
- 重构 time
- 实现 item统计
写 统计图表 页面
前后端合作流程
详细见我的另一篇博客:项目开发流程、前后端合作流程
写登陆页面 SignInPage
用户输入邮箱地址,点发送验证码按钮,如果在SignInPage组件请求成功,就开始在FormItem组件倒计时
父组件调用子组件
// SignInPage.tsx
const onClickSendValidationCode = async()=>{
const response = await axios.post("/validation_codes", {email: formData.email})
// 失败
.catch(()=>{ })
// 成功
console.log(response)
}
- Vue支持子组件暴露API供父组件用
第一步,先在子组件中使用context.expose的API,接受哈希key:value,其中的value为函数且key与value同名时,可简写为{函数名};
第二部,父组件用ref引用到子组件,然后使用ref.value拿到子组件的值,再调用需要的api
// Form.tsx
const startCount = () =>
(timer.value = setInterval(() => {
count.value -= 1;
if (count.value === 0) {
clearInterval(timer.value);
timer.value = undefined;
count.value = props.countFrom;
}
}, 1000);
context.expose({ startCount }); // 哈希表的简写,startCount:startCount
// SignInPage.tsx
const refValidationCode = ref<any>();
<FormItem
ref={refValidationCode}
/>
const onClickSendValidationCode = async()=>{
const response = await axios.post("/validation_codes", {email: formData.email})
// 失败
.catch(()=>{ })
// 成功
refValidationCode.value.startCount(); // 调用子组件的startCount函数
}
axios封装请求、axios拦截器
详细见我的另一篇博客:axios封装请求、axios拦截器
登录鉴权、路由守卫
详细见我的另一篇博客:登录鉴权、路由守卫
打包优化
- dynamic import 动态导入
- Rollup chunk 优化