神舟记账摘要

386 阅读11分钟
  • 本博客使用图片和代码来自jirengu.com

技术选型

  • Vue 3 + TSX + Rails

Vue 3 + TSX 是比较新的前端框架,像使用React一样使用Vue3,体验比React更好;

Rails 7 是比较好用的后端框架;缺点是无法利用模板的性能优化;

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 项目架构

image.png

  1. 用户使用浏览器发送请求,通过Nginx代理分发请求,依照路由分发:

     1)后端请求:路由为`/api/v1/resources`会全部重定向到Rails
    
     2)前端请求:路由为`index.html``style-xxx.scss`等静态文件,会在目录里返回数据
    
     3)除上述两种路由外的路径,重定向到`index.html`
    
  2. 通过VueRouter,根据不同的路径加载对应的view视图

  3. 每个视图用到的多个组件,就去components中拿到对应组件

  4. 用最新的状态管理库 Pinia 来做 hooks,然后 hooks 通过 AJAX 发请求到后端

  5. 登陆时用JWT、访问APIController吐出的JSON

  6. 数据库的加载: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 手机页面调试(不要等到功能做完再用手机测)

  • 基本原则:
  1. 常见浏览器优先(微信内嵌页面、系统浏览器、UC、腾讯浏览器)

  2. 真机优先(调试器无法体现所有bug)

  3. 远程调试工具

安卓搜索 Chrome远程调试手机页面(只兼容Webkit内核浏览器,Firefox除外)

苹果搜索 Safari远程调试手机页面

非Webkit浏览器用模拟控制台Tencent/vConsole(适合所有浏览器)

  • 具体步骤:
  1. 修改默认路由:
{ 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},
]}
  1. 连接手机到电脑

参考淘宝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

  • 页面
image.png image.png image.png

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

  • 页面

image.png image.png

{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 关于属性和类型声明

  1. vue自身声明属性类型(允许的组件属性)有 classstyle,比如 <div class={s.button}>
  2. onClick写在Button组件事件绑定时,就会报错需要类型声明
  3. defineComponent接收两种参数,一种是JS参数(),即对象(setup函数),另一种是TS参数<>,即泛型,尖括号内可设置props接口,如下写法:
interface Props{
    onClick: Function
    // 可以把函数的参数和返回值分开写
    onClick: (e:MouseEvent) => void
}
export const StartPage = defineComponent<Props>({

4.2 创建 Button 组件

image.png

// 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 创建IconFloatButton 组件

image.png

// 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 组件

  1. 写样式时预留刘海屏的上部区域
  2. 点击StartPage页面的菜单按钮,出现浮动层页面(绑定value为true)
  3. 点击事件响应先写在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

  • 页面

image.png

// 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 页面

  • 页面

image.png

  • 重构 time
  • 实现 item统计

写 统计图表 页面

image.png

前后端合作流程

详细见我的另一篇博客:项目开发流程、前后端合作流程

写登陆页面 SignInPage

image.png

用户输入邮箱地址,点发送验证码按钮,如果在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 优化