• 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- 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": "
    ",
          "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!!!!内容为自己总结原创,未经同意,请勿随意转载~谢谢合作~~

  • 相关阅读:
    适合 C++ 新手学习的开源项目——在 GitHub 学编程
    原生JS封装常用函数
    C# 将excel文件导入到SqlServer数据库
    热配置的部署以及容易失败原因
    连接真机开发安卓(Android)移动app MUI框架 添加购物车等——混合式开发(四)
    标志寄存器06 零基础入门学习汇编语言59
    指针07 零基础入门学习C语言47
    标志寄存器05 零基础入门学习汇编语言58
    标志寄存器03 零基础入门学习汇编语言56
    标志寄存器04 零基础入门学习汇编语言57
  • 原文地址:https://www.cnblogs.com/lhl66/p/14615338.html
Copyright © 2020-2023  润新知