人傻 發表於 2020-10-29 15:35:00

Vue3、Vuex、Typescript 项目实践、工具类封装

<p>原文: 本人github文章</p>
<p>关注公众号: 微信搜索 <code>前端工具人</code> ; 收货更多的干货</p>
<h2 id="一开篇">一、开篇</h2>
<ul>
<li><code>vue3.0beta</code>版正式上线,作为新技术热爱者,新项目将正式使用<code>vue3.0</code>开发; 接下来总结(对自己技术掌握的稳固)介绍(分享有需要的猿友)</li>
<li>上篇博客介绍了<code>vue3.0</code>常用语法及开发技巧;有需要的请点击 Vue3.0 进阶、环境搭建、相关API的使用</li>
<li>觉得对您有用的 <code>github</code> 点个 <code>star</code> 呗</li>
<li>项目<code>github</code>地址:<code>https://github.com/laijinxian/vue3-typescript-template</code></li>
</ul>
<h2 id="二项目介绍移动端">二、项目介绍(移动端)</h2>
<ul>
<li>1)技术栈: <code>vue3 + vuex + typescript + webpack + vant-ui + axios + less + postcss-pxtorem(rem适配)</code></li>
<li>2)没用官方构建工具<code>vite</code>原因:<code>vite</code> 坑还真的不少,有时候正常写法<code>webpack</code>没问题, 在<code>vite</code>上就报错;一脸懵逼的那种, <code>vite</code> 的<code>github</code> 提 Issues 都没用, 维护人员随便回答了下就把我的 <code>Issues</code> 给关了,我也是醉了;</li>
<li>3)不过自己还是很期待 <code>vite</code> 的, 等待他成熟吧, 在正式使用;</li>
<li>4)涉及点:目前只贴出项目初期的几个功能
<ul>
<li><code>webpack require</code> 自动化注册路由、自动化注册异步组价</li>
<li><code>axios</code> 请求封装(请求拦截、响应拦截、取消请求、统一处理)</li>
<li><code>vuex</code> 业务模块化、 接管请求统一处理</li>
</ul>
</li>
</ul>
<h2 id="三项目搭建">三、项目搭建</h2>
<p>可参考上篇文章 Vue3.0 进阶、环境搭建、相关API的使用</p>
<ol>
<li><code>vue-cli、vue</code> 下载最新版本</li>
<li>执行命令 <code>vue create my_app_name</code></li>
<li>执行完上面命令接下来选择手动配置(第三个),不要选择默认配置,有很多我们用不上,我的选择如下图:<br>
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de9c45a9a40543df9c7240c62b747c27~tplv-k3u1fbpfcp-watermark.image" alt="" loading="lazy"></li>
</ol>
<h2 id="三项目主要功能">三、项目主要功能</h2>
<p><strong>1. <code>webpack require</code> 自动化注册路由、自动化注册异步组价</strong></p>
<pre><code>// 该文件在 utils 下的 global.ts
// 区分文件是否自动注册为组件,vue文件定义 isComponents 字段; 区分是否自动注册为路由定义 isRouter 字段
// 使用方式分别在 main.ts 里方法asyncComponent() 以及路由文件router下的index.ts 方法 vueRouters()

import { defineAsyncComponent } from 'vue'
import { app } from '../main'
import { IRouter } from './interface'

// 获取所有vue文件
function getComponent() {
return require.context('../views', true, /\.vue$/);
}

// 首字母转换大写
function letterToUpperCase(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

// 首字母转换小写
function letterToLowerCase(str: string): string {
return str.charAt(0).toLowerCase() + str.slice(1);
}

export const asyncComponent = (): void =&gt; {

// 获取文件全局对象
const requireComponents = getComponent();

requireComponents.keys().forEach((fileSrc: string) =&gt; {

    const viewSrc = requireComponents(fileSrc);

    const fileNameSrc = fileSrc.replace(/^\.\//, '')

    const file = viewSrc.default;

    if (viewSrc.default.isComponents) {
      
      // 异步注册组件
      let componentRoot = defineAsyncComponent(
      () =&gt; import(`@/views/${fileNameSrc}`)
      )
      
      app.component(letterToUpperCase(file.name), componentRoot)
    }
});
};

// 获取路由文件
export const vueRouters = (): IRouter[] =&gt; {

const routerList: IRouter[] = [];

const requireRouters = getComponent();

requireRouters.keys().forEach((fileSrc: string) =&gt; {

    // 获取 components 文件下的文件名
    const viewSrc = requireRouters(fileSrc);

    const file = viewSrc.default;

    // 首字母转大写
    const routerName = letterToUpperCase(file.name);

    // 首字母转小写
    const routerPath = letterToLowerCase(file.name);

    const fileNameSrc = fileSrc.replace(/^\.\//, '');

    if (file.isRouter) {
      routerList.push({
      path: `/${routerPath}`,
      name: `${routerName}`,
      component: () =&gt; import(`@/views/${fileNameSrc}`)
      });
    }
});
return routerList;
};
</code></pre>
<p><strong>2. <code>axios</code> 请求封装(请求拦截、响应拦截、取消请求、统一处理)</strong></p>
<pre><code>import axios, { AxiosRequestConfig, AxiosResponse, Canceler } from 'axios'
import router from '@/router'
import { Toast } from 'vant'

if (process.env.NODE_ENV === 'development') {
// 开发环境
axios.defaults.baseURL = `https://test-mobileapi.qinlinkeji.com/api/`
} else {
// 正式环境
axios.defaults.baseURL = `正式环境地址`
}

let sourceAjaxList: Canceler[] = []

export const axionInit = () =&gt; {
axios.interceptors.request.use((config: AxiosRequestConfig) =&gt; {
    // 设置 cancel token用于取消请求 (当一个接口出现401后,取消后续多有发起的请求,避免出现好几个错误提示)
    config.cancelToken = new axios.CancelToken(function executor(cancel: Canceler): void {
      sourceAjaxList.push(cancel)
    })

    // 存在 sessionId 为所有请求加上 sessionId
    if (localStorage.getItem(`h5_sessionId`) &amp;&amp; config.url!.indexOf('/user/login') &lt; 0) config.url += ('sessionId=' + localStorage.getItem(`h5_sessionId`))
    if (!config.data) config.data = {}
    return config
}, function (error) {
    // 抛出错误
    return Promise.reject(error)
})

axios.interceptors.response.use((response: AxiosResponse) =&gt; {
    const { status, data } = response
    if (status === 200) {
      // 如果不出现错误,直接向回调函数内输出 data
      if (data.code === 0) {
      return data
      } else if (data.code === 401) {
      // 出现未登录或登录失效取消后面的请求
      sourceAjaxList.length &amp;&amp; sourceAjaxList.length &gt; 0 &amp;&amp; sourceAjaxList.forEach((ajaxCancel, index) =&gt; {
          ajaxCancel() // 取消请求
          delete sourceAjaxList
      })
      Toast({
          message: data.message,
          duration: 2000
      })
      return router.push('/login')
      } else {
      return data
      }
    } else {
      return data
    }
}, error =&gt; {
    const { response } = error
    // 这里处理错误的 http code or 服务器或后台报错
    if (!response || response.status === 404 || response.status === 500) {
      if (!response) {
      console.error(`404 error %o ${error}`)
      } else {
      if (response.data &amp;&amp; response.data.message) {
          Toast.fail({
            message: '请求异常,请稍后再试!',
            duration: 2000
          })
      }
      }
    }
    return Promise.reject(error.message)
})
}
</code></pre>
<p><strong>3. <code>vuex</code> 业务模块化、 接管请求统一处理</strong></p>
<pre><code>// 具体请看项目store目录
import { Module } from 'vuex'
import { IGlobalState, IAxiosResponseData } from '../../index'
import * as Types from './types'
import { IHomeState, ICity, IAccessControl, ICommonlyUsedDoor, AGetCtiy } from './interface'
import qs from 'qs';
import * as API from './api'

const state: IHomeState = {
cityList: [],
communityId: 13,
commonlyUsedDoor: {
    doorControlId: '',
    doorControlName: ''
},
accessControlList: []
}

const home: Module&lt;IHomeState, IGlobalState&gt; = {
namespaced: true,
state,
actions: {
    // 获取小区列表
    async ({ commit }) {
      const result = await API.getCityList&lt;IAxiosResponseData&gt;()
      if (result.code !== 0) return
      commit(Types.GET_CITY_LIST, result.data)
    },
    // 获取小区门禁列表
    async ({ commit }) {
      const result = await API.getCityAccessControlList&lt;IAxiosResponseData&gt;({
      communityId: state.communityId
      })
      if (result.code !== 0) return
      commit(Types.GET_ACCESS_CONTROL_LIST, result.data.userDoorDTOS)
      commit(Types.SET_COMMONLY_USERDOOR, result.data.commonlyUsedDoor)
    },
},
mutations: {
    // 设置小区列表
    (state, cityList: ICity[]) {
      if (cityList.length !== 0) state.cityList = cityList
    },
    // 设置小区门禁列表
    (state, accessControlList: IAccessControl[]) {
      if (accessControlList.length !== 0) return state.accessControlList = accessControlList
    },
    // 设置当前小区
    (state, commonlyUsedDoor: ICommonlyUsedDoor) {
      state.commonlyUsedDoor = commonlyUsedDoor
    }
}
}
export default home
</code></pre>
<p><strong>4. <code>home</code> 文件代码</strong></p>
<pre><code>&lt;template&gt;
&lt;div class="home-container"&gt;
    &lt;header&gt;
      &lt;Suspense&gt;
      &lt;template #default&gt;
          &lt;HomeSwiper&gt;&lt;/HomeSwiper&gt;
      &lt;/template&gt;
      &lt;template #fallback&gt;
          &lt;div&gt;...loading&lt;/div&gt;
      &lt;/template&gt;
      &lt;/Suspense&gt;
    &lt;/header&gt;
    &lt;section&gt;
      &lt;Suspense&gt;
      &lt;template #default&gt;
          &lt;HomeContent
            :cityList="cityList"
            :accessControlList="accessControlList"
          &gt;&lt;/HomeContent&gt;
      &lt;/template&gt;
      &lt;template #fallback&gt;
          &lt;div&gt;...loading&lt;/div&gt;
      &lt;/template&gt;
      &lt;/Suspense&gt;
    &lt;/section&gt;
&lt;/div&gt;
&lt;/template&gt;

&lt;script lang="ts"&gt;
import { defineComponent, reactive, toRefs, computed, onMounted } from 'vue'
import { Store, useStore } from 'vuex'
import { IGlobalState } from "@/store";
import * as Types from "@/store/modules/Home/types";
import qs from 'qs';

/**
* 该hook目的:个人理解:
*1、类似于全局的公共方法;可以考虑提到工具类函数中
*2、cityList, accessControlList 均是只做为展示的数据,没有后续的修改; 所以可考虑提取出来由父组件管理
*3、假如该方法内部逻辑比较多,其他页面又需要用到, 所以提取比较合适
*4、当然自由取舍, 放到 steup 方法内部实现也没问题, 但不利于其他页面引用获取
*5、vuex actions、mutations 函数逻辑应尽可能的少,便于维护; 逻辑处理应在页面内部
*/
function useContentData(store: Store&lt;IGlobalState&gt;) {
let cityList = computed(() =&gt; store.state.home.cityList)
let accessControlList = computed(() =&gt; store.state.home.accessControlList)
onMounted(() =&gt; {
    if (cityList.value.length === 0) store.dispatch(`home/${Types.GET_CITY_LIST}`)
    if (accessControlList.value.length === 0) store.dispatch(`home/${Types.GET_ACCESS_CONTROL_LIST}`, {
      communityId: 13
    })
})
return {
    cityList,
    accessControlList
}
}

export default defineComponent({
name: 'home',
isComponents: true,
setup() {
    let store = useStore&lt;IGlobalState&gt;()
    let { cityList, accessControlList } = useContentData(store)
    const state = reactive({
      active: 0,
    })
    return {
      ...toRefs(state),
      cityList,
      accessControlList
    }
}
})
&lt;/script&gt;

&lt;style scoped lang="less"&gt;
.home-container {
height: 100%;
background: #f6f6f6;
header {
    overflow: hidden;
    height: 500px;
    background-size: cover;
    background-position: center 0;
    background-image: url("~@/assets/images/home_page_bg.png");
}
section {
    position: relative;
    top: -120px;
    padding: 0 20px;
}
}
&lt;/style&gt;
</code></pre>
<p><strong>5. <code>login</code> 文件代码</strong></p>
<pre><code>&lt;template&gt;
&lt;div class="login-container"&gt;
    &lt;p&gt;手机号登录&lt;/p&gt;
    &lt;van-cell-group&gt;
      &lt;van-field
      v-model="phone"
      required
      clearable
      maxlength="11"
      label="手机号"
      placeholder="请输入手机号" /&gt;
      &lt;van-field
      v-model="sms"
      center
      required
      clearable
      maxlength="6"
      label="短信验证码"
      placeholder="请输入短信验证码"&gt;
      &lt;template #button&gt;
          &lt;van-button
            size="small"
            plain
            @click="getSmsCode"&gt;{{isSend ? `${second} s` : '发送验证码'}}&lt;/van-button&gt;
      &lt;/template&gt;
      &lt;/van-field&gt;
    &lt;/van-cell-group&gt;
    &lt;div class="login-button"&gt;
      &lt;van-button
      :loading="isLoading"
      size="large"
      @click="onLogin"
      loading-text="正在登录..."
      type="primary"&gt;登录&lt;/van-button&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;/template&gt;

&lt;script lang="ts"&gt;
import { defineComponent, reactive, toRefs } from 'vue'
import { useStore } from "vuex";
import { IGlobalState } from "@/store";
import * as Types from "@/store/modules/Login/types";
import { Toast } from 'vant'
import router from '@/router'
export default defineComponent({
    name: 'login',
    isRouter: true,
    setup(props, ctx) {
      let store = useStore &lt;IGlobalState&gt; ()
      const state = reactive({
      sms: '',
      phone: '',
      second: 60,
      isSend: false,
      isLoading: false
      })
      const phoneRegEx = /^{9}$/
      // 获取验证码
      const getSmsCode = async () =&gt; {
      localStorage.removeItem('h5_sessionId')
      store.commit(`login/${Types.SAVE_PHONE}`, state.phone)
      if (!phoneRegEx.test(state.phone)) return Toast({
          message: '手机号输入有误!',
          duration: 2000
      })
      store.dispatch(`login/${Types.GET_SMS_CODE}`, state.phone).then(res =&gt; {
          if (res.code !== 0) return
          Toast({
            message: '验证码已发送至您手机, 请查收',
            duration: 2000
          })
          state.isSend = true
          const timer = setInterval(() =&gt; {
            state.second--;
            if (state.second &lt;= 0) {
            state.isSend = false
            clearInterval(timer);
            }
          }, 1000);
      })
      }
      // 登录
      const onLogin = () =&gt; {
      state.isLoading = true
      store.commit(`login/${Types.SAVE_SMS_CODE}`, state.sms)
      store.dispatch(`login/${Types.ON_LOGIN}`).then(res =&gt; {
          state.isLoading = false
          if (res.code !== 0) return
          localStorage.setItem('h5_sessionId', res.data.sessionId)
          store.commit(`login/${Types.SAVE_USER_INFO}`, res.data)
          router.push('/index')
      })
      }
      return {
      ...toRefs(state),
      onLogin,
      getSmsCode
      }
    }
})
&lt;/script&gt;

&lt;style lang="less" scoped&gt;
.login-container {
    padding: 0 20px;
    &gt;p {
      padding: 50px 20px 40px;
      font-size: 40px;
    }
    .login-button {
      margin-top: 50px;
    }
}
&lt;/style&gt;
</code></pre>
<h2 id="四项目ui">四、项目ui</h2>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b24cd073cbe4d89a25fd14ef24cb678~tplv-k3u1fbpfcp-watermark.image" alt="" loading="lazy"><br>
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6adf633bd1b94901b09f277c9aeec2f0~tplv-k3u1fbpfcp-watermark.image" alt="" loading="lazy"></p>
<h2 id="五结语">五、结语</h2>
<p>以上为个人实际项目开发总结, 有不对之处欢迎留言指正</p><br><br>
来源:https://www.cnblogs.com/ljx20180807/p/13897008.html
頁: [1]
查看完整版本: Vue3、Vuex、Typescript 项目实践、工具类封装