vite2.0+vue3+ts前端最新热门技术项目搭建

闲来无事,趁假期来加强一波技能学习!不废话直接上流程上图;

超级简洁和极致的本地开发体验!!!

 来个项目简介吧;

<!--
 * @Description: Vue 3 + Typescript + Vite2.0 +vant3 +  vue-i18n@next国际化 搭建移动端项目简介
 * @Version: 2.0
 * @Autor: lhl
 * @Date: 2021-04-03 23:18:54
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-11 22:39:02
-->

# Vue 3 + Typescript + Vite

#### 项目初始化 yarn or cnpm or npm 安装 【本项目为了速度一律 cnpm】

cnpm init @vitejs/app or yarn create @vitejs/app 或者快捷命令 cnpm init @vitejs/app my-vue-app --template vue-ts Node.js: - 版本最好大于 12.0.0 yarn > npm > cnpm: - 包管理工具

### 安装依赖

cnpm i

### 安装路由

cnpm i vue-router@4 -S 【--save】

### 安装 vuex

cnpm i vuex@next -S 【--save】

### 安装国际化

cnpm i vue-i18n@next -S cnpm i js-cookie -S cnpm i @types/js-cookie -D
console.log(import.meta.env.VITE_APP_BASE_URL, 'VITE_APP_BASE_URL')
console.log(process.env.NODE_ENV)

### 启动项目

cnpm run dev

### 代码规范 vscode 需要安装的相关插件 Eslint Prettier Prettier Eslint 三个即可

cnpm i eslint -D 根目录下创建 .eslintrc.js 文件 eslint 官方配置文档:https://eslint.org/docs/user-guide/configuring/configuration-files#using-configuration-files

node 环境的类型检查 cnpm i @types/node -D

cnpm i prettier -D 根目录下创建 .prettierrc.js 文件 prettier 官方配置文档:https://prettier.io/docs/en/

安装相关依赖 
cnpm i @typescript-eslint/eslint-plugin -D 
cnpm i @typescript-eslint/parser -D 
cnpm i eslint-config-prettier -D 
cnpm i eslint-plugin-prettier -D 
cnpm i eslint-plugin-vue -D 
cnpm i eslint-define-config -D

### git 代码提交

cnpm i husky lint-staged -D 【git 代码提交规范】 package.json 文件中配置下

cnpm i eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest babel-eslint -D cnpm i eslint-config-standard eslint-friendly-formatter eslint-plugin-import eslint-plugin-standard eslint-plugin-promise -D

### 移动端适配问题

先说特别的 iPhonex 的适配 iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量: safe-area-inset-left:安全区域距离左边边界距离 safe-area-inset-right:安全区域距离右边边界距离 safe-area-inset-top:安全区域距离顶部边界距离 safe-area-inset-bottom:安全区域距离底部边界距离只有设置了 viewport-fit=cover,才能使用 constant 函数

<meta name=“viewport” content=“width=device-width, viewport-fit=cover”>
body {
  padding-bottom: constant(safe-area-inset-bottom);
}
fixed 元素的适配
{
  padding-bottom: constant(safe-area-inset-bottom);
}

或者直接设置 body{ padding: env(safe-area-inset-left,20px) env(safe-area-inset-right,20px) env(safe-area-inset-top,20px) env(safe-area-inset-bottom,20px) }

再或者媒体查询 /_兼容 iphoneX_/ /_判断是否是 iphoneX,使用@media 媒体查询尺寸_/ @media only screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) { body { top: constant(safe-area-inset-top); bottom: constant(safe-area-inset-bottom); left: constant(safe-area-inset-left); right: constant(safe-area-inset-right); } } ios11 webview 状态栏问题 【设置了固定定位页面滚动过程中两边留白】如果是纯色背景的话,可以通过给 body 设置 background 来实现填充两边的留白

scss 函数 $designWidth: 750;
  @function px2vm($size){ @return #{100\*$size / $designWidth}vw } 调用 px2vm(50)

### vm vw 适配方案

cnpm i postcss-px-to-viewport -D

### 第三方 UI 库 vant

cnpm i vant@next -S 官方文档:https://vant-contrib.gitee.io/vant/v3/#/zh-CN 

"browserslist": [
  "defaults", // 默认 "last 2 versions", 
  "last 2 versions",  // 兼容主流浏览器的最近两个版本 "> 1%", 
  "> 1%", // 兼容主流浏览器的最近两个版本 "> 1%", 
  "iOS 7",  // 使用的浏览器需要在市场上的份额大于 1 "iOS 7",
  "last 3 iOS versions" // 兼容 ios 的最新 3 个版本 
]

组件样式按需加载配置 
cnpm i vite-plugin-style-import -D 
import styleImport from 'vite-plugin-style-import'
css:{ preprocessorOptions:{ less:{ modifyVars:{}, javascriptEnabled: true } } }, plugins:[ styleImport({ libs:[ { libraryName: 'ant-design-vue', esModule: true, resolveStyle: name => `ant-design-vue/es/${name}/style/index` } ] }) ]

### 自动添加 css 前缀插件

cnpm i autoprefixer -D

### SASS 预处理器

cnpm i node-sass sass-loader sass -D

### 生产环境生成 .gz 文件

cnpm i vite-plugin-compression -D 参考文档:https://github.com/anncwb/vite-plugin-compression

### package.json 文件配置打包命令

环境变量 VITE_ 开头

"build:dev": "vue-tsc --noEmit && vite build --mode development", 
"build:test": "vue-tsc --noEmit && vite build --mode test", 
"build:prod": "vue-tsc --noEmit && vite build --mode production"

### PWA 配置

cnpm i vite-plugin-pwa -D 
import { VitePWA } from 'vite-plugin-pwa' 
参考文档:https://github.com/antfu/vite-plugin-pwa

plugins:[
  VitePWA({ 
    manifest: {}, 
    workbox: { skipWaiting: true, clientsClaim: true } 
  }) 
]

再来个vite.config.ts常用配置

/*
 * @Description: vite.config.ts vite2.0配置
 * @Version: 2.0
 * @Autor: lhl
 * @Date: 2021-04-03 23:18:54
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-11 20:43:27
 */
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteCompression from 'vite-plugin-compression'
import path from 'path'
const resolve = (dir: string) => path.join(__dirname, dir)
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // 生产环境生成 .gz 文件
    viteCompression({
      verbose: true,
      disable: false,
      threshold: 10240,
      algorithm: 'gzip',
      ext: '.gz'
    })
  ],
  base: './', // 打包路径
  resolve: {
    // 设置别名
    alias: {
      '@': resolve('src'),
      // 解决vue-i18n警告You are running the esm-bundler build of vue-i18n. It is recommended to configure your bundler to explicitly replace feature flag globals with boolean literals to get proper tree-shaking in the final bundle.
      'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
    }
  },
  server: {
    host: '0.0.0.0',
    https: false,
    port: 4000, // 启动端口
    open: true,
    // proxy: {
    //   // 选项写法
    //   '/api': 'http://xxxx'// 代理网址
    // },
    cors: true
  },
  build: {
    // 生产环境移除 console
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  }
})

接着main.ts

/*
 * @Description: 入口文件
 * @Version: 2.0
 * @Autor: lhl
 * @Date: 2021-04-03 23:18:54
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-11 22:41:30
 */
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import i18n from './locales/index'
import Vant from 'vant'
import 'vant/lib/index.css' // 全局引入样式
// vite版本不需要配置组件的按需加载,因为Vant 3.0 内部所有模块都是基于 ESM 编写的,天然具备按需引入的能力

console.log(import.meta.env.VITE_APP_BASE_URL, 'VITE_APP_BASE_URL')
console.log(process.env.NODE_ENV)
createApp(App).use(i18n).use(Vant).use(router).use(store).mount('#app')

接着是国际化的配置

/*
 * @Description: vscode自带注释
 * @Version: 2.0
 * @Autor: lhl
 * @Date: 2021-04-11 11:29:47
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-11 12:11:08
 */

import { createI18n } from 'vue-i18n'
import { getLanguage } from '../utils/cookies'

// Vant built-in lang
import { Locale } from 'vant'
import enUS from 'vant/es/locale/lang/en-US'
import zhCN from 'vant/es/locale/lang/zh-CN'
import zhTW from 'vant/es/locale/lang/zh-TW'
import jaJP from 'vant/es/locale/lang/ja-JP'

// User defined lang
import enUsLocale from './en_US/index'
import zhCnLocale from './zh_CN/index'
import zhTwLocale from './zh_TW/index'
import jaJpLocale from './ja_JP/index'

const messages: any = {
  'zh-CN': {
    ...zhCN,
    ...zhCnLocale
  },
  'zh-TW': {
    ...zhTW,
    ...zhTwLocale
  },
  'en-US': {
    ...enUS,
    ...enUsLocale
  },
  'ja-JP': {
    ...jaJP,
    ...jaJpLocale
  }
}

export const getLocale = () => {
  const cookieLanguage = getLanguage()
  if (cookieLanguage) {
    document.documentElement.lang = cookieLanguage
    return cookieLanguage
  }

  const language = navigator.language.toLowerCase()
  const locales = Object.keys(messages)
  for (const locale of locales) {
    if (language.indexOf(locale) > -1) {
      document.documentElement.lang = locale
      return locale
    }
  }

  // Default language is english
  return 'en-US'
}

const CURRENT_LANG = getLocale()
// first entry
Locale.use(CURRENT_LANG, messages[CURRENT_LANG])

const i18n = createI18n({
  locale: CURRENT_LANG,
  messages
})

export default i18n
/*
 * @Description: vscode自带注释
 * @Version: 2.0
 * @Autor: lhl
 * @Date: 2021-04-11 11:40:24
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-11 11:43:09
 */
import Cookies from 'js-cookie'

// App
const languageKey = 'language'
export const getLanguage = () => Cookies.get(languageKey)
export const setLanguage = (language: string) => Cookies.set(languageKey, language)

新建四个ts文件存放四种语言;这里只放一种作为演示

/*
 * @Description: 中文
 * @Version: 2.0
 * @Autor: lhl
 * @Date: 2021-04-11 11:52:33
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-11 14:31:08
 */
const zh_CN = {
  appHeader: {
    title: 'Vue App',
    selectLanguage: '语言选择'
  },
  goBack: {
    text: '返回'
  },
  tabBarItem: {
    home: '首页',
    product: '产品页',
    vue3Study: 'vue3Study',
    myCenter: '个人中心'
  },
  langSelect: {
    pickerTitle: '当前语言'
  }
}

export default zh_CN

兴趣来了还封装了一个语言选择组件vant3 popup+piker组件合成

<!--
 * @Description: vscode自带注释
 * @Version: 2.0
 * @Autor: lhl
 * @Date: 2021-04-10 21:56:40
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-11 14:08:24
-->
<template>
  <van-popup v-model:show="showPicker" v-bind="popupConfig">
    <van-picker
      show-toolbar
      swipe-duration="300"
      :title="$t('langSelect.pickerTitle')"
      :columns="langs"
      :default-index="defaultIndex"
      @confirm="onConfirm"
      @cancel="onClose"
    />
  </van-popup>
</template>

<script>
  import { defineComponent, toRefs, reactive, onMounted, getCurrentInstance, computed } from 'vue'
  import { useStore } from 'vuex'
  import { Locale } from 'vant'
  import { setLanguage } from '@/utils/cookies'

  export default defineComponent({
    name: 'LangSelect',
    props: {
      popupConfig: {
        type: Object,
        default: () => ({
          overlay: true,
          position: 'bottom',
          duration: 0.3,
          closeOnPopstate: true,
          transitionAppear: true,
          safeAreaInsetBottom: true
        })
      }
    },
    setup() {
      const state = reactive({
        langs: [
          {
            text: '中文(简体)',
            value: 'zh-CN'
          },
          {
            text: '中文(繁体)',
            value: 'zh-TW'
          },
          {
            text: 'English',
            value: 'en-US'
          },
          {
            text: '日本語',
            value: 'ja-JP'
          }
        ]
      })
      const store = useStore()
      const { proxy } = getCurrentInstance()
      console.log(store, 'store', getCurrentInstance(), 'getCurrentInstance')
      const computedData = {
        showPicker: computed(() => store.getters.langPicker),
        defaultIndex: computed(
          () => state.langs.findIndex((item) => item.value === proxy.$i18n.locale) || 0
        )
      }
      const methods = {
        onConfirm({ value }) {
          // Vant basic
          Locale.use(value, proxy.$i18n.messages[value])
          // Business component
          proxy.$i18n.locale = value
          // Cookie
          setLanguage(value)
          store.dispatch('changeShowPicker', false)
        },
        onClose() {
          store.dispatch('changeShowPicker', false)
        }
      }
      return {
        ...toRefs(state),
        ...methods,
        ...computedData
      }
    }
  })
</script>

<style lang="scss" scoped></style>

最后给个文件目录和效果图吧

 

 

 再底部tabbar组件

<!--
 * @Descripttion: 底部tabbar
 * @version: 
 * @Author: lhl
 * @Date: 2021-04-06 16:29:07
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-14 10:47:34
-->
<template>
  <div>
    <van-tabbar v-model="active">
      <template v-for="(item, index) in tabbars" :key="index">
        <van-tabbar-item :icon="item.icon" :to="item.path">{{ $t(item.title) }}</van-tabbar-item>
      </template>
    </van-tabbar>
  </div>
</template>

<script lang="ts">
  // 注明v-for中的国际化需要自动注意 直接数组上 t(''message.menuItme.home') 不会更新
  import { defineComponent, onMounted, toRefs, reactive, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import { Toast } from 'vant'
  import { useRoute } from 'vue-router'
  export default defineComponent({
    name: 'Tabbar',
    setup() {
      const { t } = useI18n()
      const active = ref(0)
      const state = reactive({
        // active: 0,
        //函数接收一个普通对象,返回一个响应式的数据对象
        tabbars: [
          {
            path: '/home',
            title: 'message.menuItme.home',
            icon: 'home-o'
          },
          {
            path: '/product',
            title: 'message.menuItme.product',
            icon: 'coupon-o'
          },
          {
            path: '/vue3Grammer',
            title: 'message.menuItme.study',
            icon: 'hot-o'
          },
          {
            path: '/my',
            title: 'message.menuItme.my',
            icon: 'friends-o'
          }
        ]
      })
      const route = useRoute()
      const methods = {
        initActive() {
          state.tabbars.map((item, index) => {
            if (item.path === route.path) {
              active.value = index
            }
          })
        }
      }
      // watch的使用
      watch(
        () => route.path,
        (value) => {
          console.log('value改变', value)
          if (value) {
            let vIndex = state.tabbars.findIndex((item) => {
              return item.path == value
            })
            console.log(vIndex, 'vIndex')
            if (vIndex > -1) {
              active.value = vIndex
              // Toast.success(t('message.menuItme.study'))
            }
          }
        }
      )
      onMounted(() => {
        methods.initActive()
      })

      return {
        active,
        ...methods,
        ...toRefs(state)
      }
    }
  })
</script>

<style lang="less"></style>

 再贴一个vuex配置

/*
 * @Descripttion: vuex配置入口
 */
import { createStore } from 'vuex'
const store = createStore({
  state: {
    langPicker: false,
    loading: false
  },
  mutations: {
    // 语言选择框
    handleShowPicker(state) {
      state.langPicker = !state.langPicker
    },
    // 显示loading
    showLoading(state) {
      state.loading = true
    },
    // 隐藏loading
    hideLoading(state) {
      state.loading = false
    }
  },
  getters: {
    langPicker: (state) => state.langPicker
  },
  actions: {
    changeShowPicker(context, value) {
      context.commit('handleShowPicker', value)
    }
  },
  modules: {}
})

export default store

再贴一个路由配置

/*
 * @Descripttion: 路由配置文件  参考文档:https://next.router.vuejs.org/zh/introduction.html
 */
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/home',
    name: '/',
    component: () => import('@/components/layout/index.vue'),
    children: [
      {
        path: '/home',
        // name: 'home',
        meta: {
          title: '首页',
          i18Title: 'message.menuItme.home'
        },
        component: () => import('@/pages/home/index.vue')
      },
      {
        path: '/carDetail',
        name: 'carDetail',
        meta: {
          title: '购物车详情',
          i18Title: 'message.menuItme.carDetail'
        },
        component: () => import('@/pages/home/carDetail.vue')
      },
      {
        path: '/product',
        name: 'product',
        meta: {
          title: '产品列表',
          i18Title: 'message.menuItme.product'
        },
        component: () => import('@/pages/product/index.vue')
      },
      {
        path: '/vue3Grammer',
        name: 'vue3Grammer',
        meta: {
          title: 'vue3语法',
          i18Title: 'message.menuItme.study'
        },
        component: () => import('@/pages/vue3Grammer/index.vue')
      },
      {
        path: '/my',
        name: 'my',
        meta: {
          title: '个人中心',
          i18Title: 'message.menuItme.my'
        },
        component: () => import('@/pages/my/index.vue')
      }
    ]
  },
  {
    path: '/404',
    name: '404',
    component: () => import('@/pages/notFound/index.vue')
  },
  {
    path: '/:pathMatch(.*)', // 和以前配置有所不一样 or  /:catchAll(.*)
    redirect: '/404'
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

console.log(router, 'router')
// 路由前置钩子
router.beforeEach((to, from, next) => {
  const title = to.meta && (to.meta.title as string)
  if (title) {
    document.title = title
  }
  next()
})

router.afterEach((to, from) => {
  console.log(to, 'to', from, 'from')
})

// 路由配置上定义 路由独享的守卫
// beforeEnter: (to, from) => {
//   // reject the navigation
//   return false
// },

// 导航守卫
// onBeforeRouteLeave, onBeforeRouteUpdate

export default router

再贴一个vue3基础用法

<!--
 * @Descripttion: vue3语法  --tsx写法(就是react 的jsx风格) ts写法 .vue写法  options API Composition API写法都可以 
-->
<template>
  <div class="vue3-grammer">
    <div class="num-box">{{ num }}----{{ newNum }}----{{ refData }}</div>
    <div class="change-num-btn">
      <van-button size="large" type="primary" @click="handleChange">改变数据</van-button>
    </div>
    <div class="from-box">
      <van-form @submit="onSubmit">
        <van-field v-model="username" name="username" label="用户名" placeholder="用户名" :rules="[{ required: true, message: '请填写用户名' }]" />
        <van-field v-model="password" type="password" name="password" label="密码" placeholder="密码" :rules="[{ required: true, message: '请填写密码' }]" />
        <child @reciveChildData="reciveFromChildData" />
        <div style="margin: 16px">
          <van-button round block type="primary" native-type="submit"> 提交表单 </van-button>
        </div>
      </van-form>
    </div>
    <!-- vant 组件使用 -->
    <van-button size="large" type="success" @click="goNextPage">路由跳转</van-button>
  </div>
</template>

<script lang="ts">
  // defineComponent最重要的是:在TypeScript下,给予了组件 正确的参数类型推断
  import { defineComponent, toRefs, reactive, getCurrentInstance, watch, computed, ref } from 'vue'
  //   beforeCreate created  -->用 setup 代替
  import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
  import { useRouter, useRoute } from 'vue-router'
  import child from './child.vue'

  export default defineComponent({
    name: 'vue3Grammer',
    components: { child },
    setup(props, context) {
      console.log(props, 'props', context, 'context')
      //这里的ctx  类似于vue2的this
      // 会报错 且开发环境可以生产环境会报错
      //   const { ctx: any } = getCurrentInstance()
      //   console.log(ctx, 'ctx')
      // 获取组件实例 用于高阶用法或库的开发
      // internalInstance.appContext.config.globalProperties // 访问 globalProperties
      const internalInstance = getCurrentInstance()
      console.log(internalInstance, 'internalInstance') // 访问 globalProperties

      const refData = ref(12) //ref包裹 变为响应式对象
      // 个人觉着还是这样写舒服一点 类似于vue2中的data
      const state = reactive({
        //函数接收一个普通对象,返回一个响应式的数据对象
        num: 0,
        username: '',
        password: ''
      })
      //计算属性 个人喜欢写在对象中 因为看得更清晰一下 防止计算属性方法等混在一起不好区分
      const computedData = {
        // 计算属性写法 别忘记引入 computed
        newNum: computed(() => state.num * 2)
      }
      const router = useRouter()
      console.log(router, 'this.$router')
      const route = useRoute()
      console.log(route, 'this.$route', route.meta)

      const methods = {
        // 改变数据
        handleChange: () => {
          state.num++
          // ref包裹的数据 必须用.value获取
          refData.value++
        },
        // 提交表单
        onSubmit: (values: object) => {
          console.log('submit', values)
        },
        // 跳转
        goNextPage() {
          //路由跳转
          router.push({
            name: '/'
          })
        },
        // 父组件接收子组件的值
        reciveFromChildData(val: any) {
          console.log(val, 'val-来自子组件的值')
          state.username = val.username
          state.password = val.password
        }
        //网络请求
        // main.js传入的封装axios
        // async getUser() {
        //   try {
        //     let { data } = await userApi()
        //     console.log(data)
        //   } catch (error) {
        //     console.log(error)
        //   }
        // }
      }
      onBeforeMount(() => {
        console.log('生命周期---等同于vue2的 beforeMount')
      })

      onMounted(() => {
        // methods.getUser()
        console.log('生命周期---等同于vue2的 mounted')
      })

      onBeforeUpdate(() => {
        console.log('生命周期---等同于vue2的 beforeUpdate')
      })

      onUpdated(() => {
        console.log('生命周期---等同于vue2的 updated')
      })

      onBeforeUnmount(() => {
        console.log('生命周期---等同于vue2的 beforeUnmount')
      })

      onUnmounted(() => {
        console.log('生命周期---等同于vue2的 unmounted ')
      })

      // watch的使用
      watch(
        () => state.num,
        (value) => {
          console.log('num改变', value)
        }
      )
      return {
        ...toRefs(state), // 将响应式的对象变为普通对象 使用时不需要state.num 直接num即可使用
        ...methods, // 解构赋值
        ...computedData,
        refData
      }
    }
  })
</script>

<style lang="less" scoped>
  .num-box {
    background: #000;
    color: #fff;
    font-size: 16px;
    text-align: center;
    padding: 10px 0;
  }
  .change-num-btn {
    margin: 8px 0;
  }
</style>子组件
<!--
 * @Descripttion: 子组件---vant3表单常用组件组合
-->
<template>
  <div class="child">
    <van-divider :style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }"> 子组件表单 </van-divider>
    <!-- 开关 -->
    <van-field name="switch" label="开关">
      <template #input>
        <van-switch v-model="checked" size="20" />
      </template>
    </van-field>
    <!-- 复选框组 -->
    <van-field name="checkboxGroup" label="学习框架">
      <template #input>
        <van-checkbox-group v-model="groupChecked" direction="horizontal">
          <van-checkbox name="1" shape="square">react</van-checkbox>
          <van-checkbox name="2" shape="square">vue</van-checkbox>
        </van-checkbox-group>
      </template>
    </van-field>
    <!-- 单选框 -->
    <van-field name="radio" label="多端">
      <template #input>
        <van-radio-group v-model="checkedradio1" direction="horizontal">
          <van-radio name="1">taro</van-radio>
          <van-radio name="2">uni-app</van-radio>
        </van-radio-group>
      </template>
    </van-field>
    <van-field name="radio" label="跨平台">
      <template #input>
        <van-radio-group v-model="checkedradio2" direction="horizontal">
          <van-radio name="3">Flutter</van-radio>
          <van-radio name="4">React native</van-radio>
        </van-radio-group>
      </template>
    </van-field>
    <!-- 步进器 -->
    <van-field name="stepper" label="步进器">
      <template #input>
        <van-stepper v-model="count" />
      </template>
    </van-field>
    <!-- 评分 -->
    <van-field name="rate" label="评分">
      <template #input>
        <van-rate v-model="rateVal" />
      </template>
    </van-field>
    <!-- 滑块 -->
    <van-field name="slider" label="滑块">
      <template #input>
        <van-slider v-model="sliderVal" />
      </template>
    </van-field>
    <!-- 文件上传 -->
    <van-field name="uploader" label="文件上传">
      <template #input>
        <van-uploader v-model="uploadVal" />
      </template>
    </van-field>
    <!-- Picker 组件 -->
    <van-field v-model="pickerVal" readonly clickable name="picker" label="选择器" placeholder="点击选择城市" @click="showPicker = true" />
    <van-popup v-model:show="showPicker" position="bottom">
      <van-picker :columns="columns" @confirm="onConfirm" @cancel="showPicker = false" />
    </van-popup>
    <!-- DatetimePicker 组件 -->
    <van-field v-model="datetimePickerVal" readonly clickable name="datetimePicker" label="时间选择" placeholder="点击选择时间" @click="showDatetimePicker = true" />
    <van-popup v-model:show="showDatetimePicker" position="bottom">
      <van-datetime-picker type="time" @confirm="onDatetimePickerConfirm" @cancel="showDatetimePicker = false" />
    </van-popup>
    <!-- 省市区选择器 Area 组件 -->
    <van-field v-model="areaVal" readonly clickable name="area" label="地区选择" placeholder="点击选择省市区" @click="showArea = true" />
    <van-popup v-model:show="showArea" position="bottom">
      <van-area :area-list="areaList" @confirm="onAreaConfirm" @cancel="showArea = false" />
    </van-popup>
    <!-- 日历 Calendar 组件 -->
    <van-field v-model="calendarVal" readonly clickable name="calendar" label="日历" placeholder="点击选择日期" @click="showCalendar = true" />
    <van-calendar v-model:show="showCalendar" @confirm="onCalendarConfirm" />
  </div>
</template>

<script lang="ts">
  //用到地区组件需要加载数据或者接口数据去拿
  // yarn add @vant/area-data  cnpm i @vant/area-data -S
  import { defineComponent, reactive, toRefs } from 'vue'
  import { areaList } from '@vant/area-data'

  export default defineComponent({
    name: 'child',
    setup(props, context) {
      console.log(props, context)
      const state = reactive({
        username: '111',
        password: '222',
        checked: false,
        groupChecked: [],
        checkedradio1: '1',
        checkedradio2: '3',
        count: 1, // 最小就是1了
        rateVal: 1,
        sliderVal: 10,
        uploadVal: [],
        pickerVal: '',
        showPicker: false,
        columns: ['北京', '上海', '广州', '深圳'],
        datetimePickerVal: '',
        showDatetimePicker: false,
        areaVal: '',
        showArea: false,
        areaList, // 数据格式见 Area 组件文档
        calendarVal: '',
        showCalendar: false
      })
      // 自定义事件
      context.emit('reciveChildData', state)
      const methods = {
        onConfirm: (value: any) => {
          console.log(value, 'Picker组件')
          state.pickerVal = value
          state.showPicker = false
        },
        onDatetimePickerConfirm: (value: any) => {
          console.log(value, '时间')
          state.datetimePickerVal = value
          state.showDatetimePicker = false
        },
        onAreaConfirm: (value: any) => {
          console.log(value, '地区')
          state.showArea = false
          state.areaVal = value
            .filter((item: any) => !!item)
            .map((item: any) => item.name)
            .join('/')
        },
        onCalendarConfirm: (date: any) => {
          console.log(date, '日期')
          state.calendarVal = `${date.getMonth() + 1}/${date.getDate()}`
          state.showCalendar = false
        }
      }

      return {
        ...toRefs(state),
        ...methods
      }
    }
  })
</script>

<style lang="less" scoped></style>

再贴一个axios封装

/*
 * @Descripttion: api统一管理入口
 */
import * as testApi from './testApi'

export default {
  testApi
}

 

/*
 * @Descripttion: 单独api单独配置 RESTful 风格 api
 * RESTful解释 参考文档:http://www.ruanyifeng.com/blog/2011/09/restful.html
 */
import request from './request'

/**
 * 封装get请求
 * @param {string} url 请求连接
 * @param {Object} params 请求参数
 * @param {Object} header 请求需要设置的header头
 */
export const getTest = (params: object, header: {}) => {
  return request({
    url: `/posts`,
    method: 'get',
    params: params,
    headers: header
  })
}

/**
 * 封装post请求
 * @param {string} url 请求连接
 * @param {Object} data 请求参数
 * @param {Object} header 请求的header头
 */
export const postTest = (data: object, header: {}) => {
  return request({
    url: `/posts`,
    method: 'post',
    data: data,
    headers: header
  })
}

/**
 * 封装put请求
 * @param {string} url 请求连接
 * @param {Object} data 请求参数
 * @param {Object} header 请求设置的header头
 */
export const putTest = (data: object, header: {}) => {
  return request({
    url: `/posts/1`,
    method: 'put',
    data: data,
    headers: header
  })
}

/**
 * 封装delete请求
 * 如果服务端将参数作为java对象来封装接受 (data入参)
 * 如果服务端将参数作为url参数来接受,则请求的url为:www.xxx/url?a=1&b=2形式 (params入参)
 * @param {string} url 请求连接
 * @param {Object} params 请求参数
 * @param {Object} header 请求设置的header头
 */
export const deleteTest = (params: object, header: {}) => {
  return request({
    url: `/posts/1`,
    method: 'delete',
    data: params,
    headers: header
  })
}
/*
 * @Descripttion: aioxs二次封装
 */
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios'
import store from '../store'
import { Toast } from 'vant'

// 定义接口
interface PendingType {
  url?: string
  method?: Method
  params: any
  data: any
  cancel: Function
}

// 取消重复请求

const repeatRequstList: Array<PendingType> = []

// 使用 cancel token 取消请求
const CancelToken = axios.CancelToken
console.log(import.meta.env.VITE_APP_BASE_URL, 'axios')
const instance = axios.create({
  // baseURL: <string>import.meta.env.VITE_APP_BASE_URL, // 给类型 不然报错
  baseURL: import.meta.env.VITE_APP_BASE_URL as string, // 断言 不然报错
  // 指定请求超时的毫秒数(0 表示无超时时间)
  timeout: 10000,
  // 表示跨域请求时是否需要使用凭证
  withCredentials: true
})
// let loadingInstance: ElLoadingComponent

// 移除重复请求
const removePending = (config: AxiosRequestConfig) => {
  for (const key in repeatRequstList) {
    console.log(key, 'key')
    const item: number = +key
    const list: PendingType = repeatRequstList[key]
    // 当前请求在数组中存在时执行函数体
    if (list.url === config.url && list.method === config.method && JSON.stringify(list.params) === JSON.stringify(config.params) && JSON.stringify(list.data) === JSON.stringify(config.data)) {
      // 执行取消操作
      list.cancel('操作太频繁,请稍后再试')
      // 从数组中移除记录
      repeatRequstList.splice(item, 1)
    }
  }
}

// 添加请求拦截器
instance.interceptors.request.use(
  (config) => {
    store.commit('showLoading')

    removePending(config)
    config.cancelToken = new CancelToken((c) => {
      repeatRequstList.push({ url: config.url, method: config.method, params: config.params, data: config.data, cancel: c })
    })
    return config
  },
  (error: any) => {
    store.commit('hideLoading')
    return Promise.reject(error)
  }
)

// 添加响应拦截器
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    store.commit('hideLoading')
    removePending(response.config)
    return response.data
  },
  (error: any) => {
    store.commit('hideLoading')
    const { response } = error
    // 根据返回的 http 状态码做不同的处理 ?.语法需要最新的谷歌浏览器才支持目前
    // switch (response?.status) {
    //   case 401:
    //     // token失效
    //     break
    //   case 403:
    //     // 没有权限
    //     break
    //   case 404:
    //     // 请求的资源不存在
    //     break
    //   case 500:
    //     // 服务端错误
    //     Toast('提示内容')
    //     break
    //   default:
    //     break
    // }

    // 超时重新请求
    const config = error.config
    // 全局的请求次数,请求的间隙
    const [RETRY_COUNT, RETRY_DELAY] = [3, 1000]

    if (config && RETRY_COUNT) {
      // 设置用于跟踪重试计数的变量
      config.__retryCount = config.__retryCount || 0
      // 检查是否已经把重试的总数用完
      if (config.__retryCount >= RETRY_COUNT) {
        return Promise.reject(response || { message: error.message })
      }
      // 增加重试计数
      config.__retryCount++

      // 创造新的Promise来处理指数后退
      // 在typescript中定义promise返回类型 首先要在tsconfig.json中配置ES2015.promise的lib 不然ts无法支持promise
      const backClose = new Promise((resolve) => {
        setTimeout(() => {
          // 写上 null or undefined 不然ts下面会报错
          resolve(null)
        }, RETRY_DELAY || 1)
      })

      // instance重试请求的Promise
      return backClose.then(() => {
        return instance(config)
      })
    }

    // eslint-disable-next-line
    return Promise.reject(response || { message: error.message })
  }
)

export default instance
/*
 * @Descripttion:
 * @version:
 * @Author: lhl
 * @Date: 2021-04-02 15:03:55
 * @LastEditors: lhl
 * @LastEditTime: 2021-04-14 18:13:04
 */
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Vant from 'vant'
import Vconsole from 'vconsole'
import 'vant/lib/index.css'
import i18n from './language/index'
import API from './api/index'
import 'normalize.css/normalize.css'
import './assets/style/index.less'

console.log(import.meta.env.VITE_APP_BASE_URL, '环境变量')

const isProd = process.env.NODE_ENV === 'production'
if (!isProd) {
  new Vconsole()
}

const app = createApp(App)
app.config.globalProperties.API = API
console.log(app.config.globalProperties.API, 'app.config.globalProperties.API')
app.use(i18n).use(router).use(store).use(Vant).mount('#app')

再贴个vscode的个人配置

{
    "workbench.colorTheme": "One Dark Pro",
    "fileheader.customMade": {
        "Description": "vscode自带注释",
        "Version": "2.0",
        "Autor": "lhl",
        "Date": "Do not edit",
        "LastEditors": "lhl",
        "LastEditTime": "Do not edit"
      },
      "fileheader.cursorMode": {
        "description":"",
        "param": "",
        "return": "",
        "author":"lhl"
      },
      "[javascript]": {
        "editor.defaultFormatter": "HookyQR.beautify"
      },
      // "[jsonc]": {
      //   "editor.defaultFormatter": "HookyQR.beautify"
      // },
      "javascript.updateImportsOnFileMove.enabled": "always",
      // "emmet.excludeLanguages": [
      //   "markdown"
      // ],
      "emmet.includeLanguages": {
        "javascript": "javascriptreact"
      },
      "emmet.triggerExpansionOnTab": true,
      "eslint.codeAction.showDocumentation": {
        "enable": true
      },

      //===========================================
      //============= Editor ======================
      //===========================================
      "explorer.openEditors.visible": 0,
      "editor.tabSize": 2,
      "editor.renderControlCharacters": true,
      "editor.minimap.renderCharacters": false,
      "editor.minimap.maxColumn": 300,
      "editor.minimap.showSlider": "always",
      "editor.cursorBlinking": "phase",
      "editor.cursorSmoothCaretAnimation": true,
      "editor.detectIndentation": false,
      "editor.defaultFormatter": "esbenp.prettier-vscode",
      "diffEditor.ignoreTrimWhitespace": false,
      "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
      "editor.suggestSelection": "first",
      "editor.trimAutoWhitespace": true,
      "editor.quickSuggestions": {
        "other": true,
        "comments": true,
        "strings": true
      },
      //===========================================
      //============= Other =======================
      //===========================================
      "breadcrumbs.enabled": true,
      "open-in-browser.default": "chrome",
      //===========================================
      //============= emmet =======================
      //===========================================
      "emmet.showAbbreviationSuggestions": true,
      "emmet.showExpandedAbbreviation": "always",
      "emmet.syntaxProfiles": {
        "vue-html": "html",
        "vue": "html",
        "xml": {
          "attr_quotes": "single"
        }
      },
      //===========================================
      //============= files =======================
      //===========================================
      "files.trimTrailingWhitespace": true,
      "files.insertFinalNewline": true,
      "files.trimFinalNewlines": true,
      "files.eol": "\n",
      "search.exclude": {
        "**/node_modules": true,
        "**/*.log": true,
        "**/*.log*": true,
        "**/bower_components": true,
        "**/dist": true,
        "**/elehukouben": true,
        "**/.git": true,
        "**/.gitignore": true,
        "**/.svn": true,
        "**/.DS_Store": true,
        "**/.idea": true,
        "**/.vscode": false,
        "**/yarn.lock": true,
        "**/tmp": true,
        "out": true,
        "dist": true,
        "node_modules": true,
        "CHANGELOG.md": true,
        "examples": true,
        "res": true,
        "screenshots": true
      },
      "files.exclude": {
        "**/bower_components": true,
        "**/.idea": true,
        "**/tmp": true,
        "**/.git": true,
        "**/.svn": true,
        "**/.hg": true,
        "**/CVS": true,
        "**/.DS_Store": true
      },
      "files.watcherExclude": {
        "**/.git/objects/**": true,
        "**/.git/subtree-cache/**": true,
        "**/.vscode/**": true,
        "**/node_modules/**": true,
        "**/tmp/**": true,
        "**/bower_components/**": true,
        "**/dist/**": true,
        "**/yarn.lock": true
      },
      // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
      // ===========================================
      // ================ Eslint ===================
      // ===========================================
      "eslint.alwaysShowStatus": true,
      "eslint.options": {
        "plugins": ["html", "vue", "javascript", "jsx", "typescript"],
        "extensions": [".js", ".jsx", ".ts", ".tsx", ".vue"]
      },
      "eslint.validate": [
        "javascript",
        "typescript",
        "reacttypescript",
        "reactjavascript",
        "html",
        "vue"
      ],
      // ===========================================
      // ================ Vetur ====================
      // ===========================================
      "vetur.experimental.templateInterpolationService": true,
      "vetur.format.options.tabSize": 2,
      "vetur.format.defaultFormatter.html": "js-beautify-html",
      "vetur.format.defaultFormatter.scss": "prettier",
      "vetur.format.defaultFormatter.css": "prettier",
      "vetur.format.defaultFormatter.ts": "prettier-tslint",
      "vetur.format.defaultFormatter.js": "prettier",
      "vetur.languageFeatures.codeActions": false,
      "vetur.format.defaultFormatterOptions": {
        "js-beautify-html": {
          "wrap_attributes": "force-expand-multiline"
        },
        "prettier": {
          "eslintIntegration": true,
          "arrowParens": "always",
          "semi": false,
          "singleQuote": true
        }
      },
      "terminal.integrated.rendererType": "dom",
      "telemetry.enableCrashReporter": false,
      "telemetry.enableTelemetry": false,
      "workbench.settings.enableNaturalLanguageSearch": false,
      "path-intellisense.mappings": {
        "/@/": "${workspaceRoot}/src"
      },
      "prettier.requireConfig": true,
      "typescript.updateImportsOnFileMove.enabled": "always",
      "workbench.sideBar.location": "left",
      "[javascriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[typescriptreact]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[html]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[css]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[less]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      "[scss]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      // "[markdown]": {
      //   "editor.defaultFormatter": "esbenp.prettier-vscode"
      // },
      "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
      },
      "[vue]": {
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": false
        }
      },
      "terminal.integrated.automationShell.windows": "F:\\Git\\bin\\bash.exe",
      "terminal.integrated.shell.windows": "F:\\Git\\bin\\bash.exe",
      "editor.formatOnSave": true,
      "editor.formatOnPaste": true,
      "editor.formatOnType": true,
      "files.autoSave": "afterDelay",
  }

 好嘞尝鲜到此结束~明天继续搬砖ing!!!!内容为自己总结原创,未经同意,请勿随意转载~谢谢合作~~

posted @ 2021-04-03 23:22  蓝色帅-橙子哥  阅读(2539)  评论(0编辑  收藏  举报