说明

本文记录一下 Vue3 的一个基本的开发框架的搭建。主要在公司开发业务,主体框架不用每次都搭建。时间长了就容易忘记了,当初的框架如何搭建的。文章记录的框架搭建的主要实现功能如题,然后研究一些快捷的操作。例如,ts 定义的空间自动导入等等。

工具介绍

  1. Vite 是一种新型前端构建工具,能够显著提升前端开发体验。细致的介绍看官网
  2. Vue3 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。
  3. TypeScript 是一门静态类型、弱类型的语言。 TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。 TypeScript 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中。
  4. Axios 是一个基于 promise 网络请求库,作用于 node.js 和浏览器中。
  5. Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。详细的介绍看官网
  6. Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。

Vue Router 和 Vue 的版本有一个范围支持的。前面使用了 Vue3.x 所以 Vue Router 使用 4.x

使用 Vite

Vite 需要 Node.js 版本 14.18+,16+。所以安装之前检查你的 node 版本

# npm 6.x
npm create vite@latest my-vue-app --template vue

# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

# yarn
yarn create vite my-vue-app --template vue

# pnpm
pnpm create vite my-vue-app --template vue

--template 后面的参数有 vanillavanilla-tsvuevue-tsreactreact-tspreactpreact-tslitlit-tssveltesvelte-ts。查看create-vite以获取每个模板的更多细节。
这里就使用命令

npm create vite@latest my-vue-app --template vue-ts
# 执行之后安装它给出的提示安装模块和启动

到此 Vue3 + Vite + Ts 就很简单的完成了,但是这还只是一部分内容

安装 Axios

npm install axios

安装完成之后,创建一个如下的文件src/assets/js/my-axios.ts,这里要做一个自己的项目接口配置,可以参考我的,根据个人需求来改动。本文件完成了以下几个需求。

  1. 在具体页面中通过api.get或者api.post这种方式来调用函数,创建对应的方法。
  2. 通过请求前拦截和请求后拦截来做一个身份的验证和一个数据加载动画。
  3. 有时候需要手动上传图片数据,可以对接口请求参数类型是否是[object FormData]头部修改字段。
    还需要安装@types/node包,因为文件中使用到了 Timeout 函数,来处理加载动画到操作。
    以及安装 element-plus,使用其中的信息组件

@types 是什么?

> 有些包并不是 TypeScript 编写的,自然也不会导出 TypeScript 声明文件。所以在TS项目中使用会报错,TS官方给了2个解决方法。

	1. 安装 @types
	2. 自己 declare module
# 安装@types/node
npm install @types/node
# 安装element-plus
npm install element-plus --save

自定义的 axios 文件

var root = "/api";

import axios, { AxiosRequestConfig, AxiosRequestHeaders } from "axios";
import { ElMessage as Message } from "element-plus";

// 自定义判断元素类型JS
function toType(obj: any): string {
  return {}.toString
    .call(obj)
    .match(/\s([a-zA-Z]+)/)![1]
    .toLowerCase();
}
// 参数过滤函数
function filterNull(o: any) {
  for (var key in o) {
    if (o[key] === null) {
      delete o[key];
    }
    if (toType(o[key]) === "string") {
      o[key] = o[key].trim();
    } else if (toType(o[key]) === "object") {
      o[key] = filterNull(o[key]);
    } else if (toType(o[key]) === "array") {
      o[key] = filterNull(o[key]);
    }
  }
  return o;
}
/*
  接口处理函数
  这个函数每个项目都是不一样的,我现在调整的是适用于
  https://cnodejs.org/api/v1 的接口,如果是其他接口
  需要根据接口的参数进行调整。参考说明文档地址:
  https://cnodejs.org/topic/5378720ed6e2d16149fa16bd
  主要是,不同的接口的成功标识和失败提示是不一致的。
  另外,不同的项目的处理方法也是不一致的,这里出错就是简单的alert
*/

function apiAxios(
  method: string,
  url: string,
  params: null | string | object,
  success: any,
  failure: any
) {
  let contentTypeIsJson = false;
  if (params && typeof params != "string") {
    params = filterNull(params);
  } else contentTypeIsJson = true;

  // axios 对特殊字符处理
  if (params && (method === "GET" || method === "DELETE")) {
    const arr: Array<string> = [];
    Object.entries(params).forEach((item) => {
      arr.push(`${item[0]}=${encodeURIComponent(item[1])}`);
    });
    url = `${url}?${arr.join("&")}`;
  }
  axios({
    method: method,
    url: url,
    data: method === "POST" || method === "PUT" ? params : null,
    // params: method === 'GET' || method === 'DELETE' ? params : null,
    params: method === "GET" || method === "DELETE" ? "" : null,
    baseURL: root,
    withCredentials: true,
    crossDomain: true,
    transformRequest: [
      function (data) {
        // Do whatever you want to transform the data
        if (contentTypeIsJson) return data;
        let ret = "";
        for (let it in data) {
          ret +=
            encodeURIComponent(it) + "=" + encodeURIComponent(data[it]) + "&";
        }
        return ret;
      },
    ],
    headers: {
      // 'Content-Type': 'application/x-www-form-urlencoded',
      "Content-Type": contentTypeIsJson
        ? "application/json"
        : "application/x-www-form-urlencoded",
    },
  } as AxiosRequestConfig<any>)
    .then(function (res) {
      let response = res.data;
      if (response.code == 302) {
        window.location.href = response.urlToRedirectTo;
        return;
      } else if (response.code == 0) {
        if (success) {
          success(response);
        }
      } else {
        if (failure) {
          failure(response);
        } else {
          if (response.data == 2) {
            Message.error(response.msg); //错误处理
            setTimeout(() => {
              location.reload();
            }, 1000);
          } else {
            Message.info(response.msg); //错误处理
          }
        }
      }
    })
    .catch(function (err) {
      let res = err.response;
      console.error(res || err);
      if (res) {
        Message.closeAll();
        clearTimeout(timeObj);
        if (res.data.msg) {
          Message.error(res.data.msg); //错误处理
        } else {
          Message.error("网络请求出错"); //错误处理
        }
        return;
      }
    });
}

//字符串的16进制表达
function strToHexCharCode(_str: string) {
  if (_str === "") return "";
  var hexCharCode: Array<string> = [];
  for (var i = 0; i < _str.length; i++) {
    var str = _str.charCodeAt(i).toString(16);
    if (_str.length == 1) str += "0" + _str;
    hexCharCode.push(str);
  }
  return hexCharCode.join("");
}

let requestCount = 0;
let timeObj: NodeJS.Timeout;
// http request 拦截器
axios.interceptors.request.use((config) => {
  requestCount++;
  if (requestCount == 1) {
    timeObj = setTimeout(() => {
      Message.info({ message: "加载中...", duration: 0 });
    }, 800);
  }

  if (
    config.data &&
    Object.prototype.toString.call(config.data) == "[object FormData]"
  ) {
    config.headers!!["Content-Type"] = "multipart/form-data;charset=utf-8";
    config.transformRequest = [
      function (data) {
        return data;
      },
    ];
  }

  if (localStorage.getItem("currentRole")) {
    (config.headers as AxiosRequestHeaders)["currentRole"] =
      localStorage.getItem("currentRole");
  }
  return config;
});

// http response 拦截器
axios.interceptors.response.use((response) => {
  requestCount--;
  if (requestCount === 0) {
    setTimeout(() => {
      Message.closeAll();
    }, 1500);
    clearTimeout(timeObj);
  }
  return response;
});
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// axios.defaults.headers.post['Content-Type'] = 'application/json; charset=utf-8';
// axios.defaults.withCredentials = true

// 返回在vue模板中的调用接口
export default {
  get: function (
    url: string,
    params: string | object | null,
    success: any,
    failure: any
  ) {
    return apiAxios("GET", url, params, success, failure);
  },
  post: function (
    url: string,
    params: string | object,
    success: any,
    failure: any
  ) {
    return apiAxios("POST", url, params, success, failure);
  },
  put: function (
    url: string,
    params: string | object,
    success: any,
    failure: any
  ) {
    return apiAxios("PUT", url, params, success, failure);
  },
  delete: function (
    url: string,
    params: string | object,
    success: any,
    failure: any
  ) {
    return apiAxios("DELETE", url, params, success, failure);
  },
};

同时新建一个src/api/index.ts,主要用来抛出定义的接口名称,算是按需加载吧
另外,有很多时候前端项目需要部署到很多服务器上,接口的域名会改变,所以做一个动态改变配置

// 导出接口配置文件
import conf from "@/assets/config/host.config";
let BaseUrl = conf.serviceHost + "/api";
export const GetUserInfo = `${BaseUrl}/user/GetUserInfo`;

动态配置文件加载

新建src/config/host.config.ts,然后在main.ts中引入它。最后需要在 index.html 中引入一个<script src="./config/index.js"></script>,在第一次部署的时候加一个这个文件。

// src/config/host.config.ts
let config = {
  serviceHost: "http://xx.com",
};
if (typeof (window as any).conf !== "undefined") config = (window as any).conf;
export default config;
// ./config/index.js
window.conf = {
  serviceHost: "http://xx.com",
};

这样以后如果域名什么的修改了,就不用手动打包再上传了。也可以定义自己的配置,动态修改。

如何使用

写好的my-axios.ts配置文件在需要的文件中引入就可以了

<script setup lang="ts">
import api from '@/assets/js/myaxios'
import {
  GetXXXX,
} from '@/api/index.js'
</script>

补充另一个大佬提供的方案

文件中可能需要安装一些插件或者引入一些文件,看个人需求修改或者自定义一下

/* eslint-disable class-methods-use-this */
import NProgress from "nprogress";
import qs from "qs";
import axios, {
  AxiosError,
  AxiosInstance,
  AxiosPromise,
  AxiosRequestConfig,
  AxiosResponse,
} from "axios";
import axiosRetry from "axios-retry";
import { useUserInfo } from "@/store/userInfo";
import { BASE_TIMEOUT, ENV_URL } from "../static/service";

class HttpService {
  private http!: AxiosInstance;

  constructor() {
    this.http = axios.create({
      baseURL: ENV_URL, // 配置接口地址
      timeout: BASE_TIMEOUT, // 配置超时时间
    });
    // 重试
    // axiosRetry(this.http, {
    //     retries: 2, //设置自动发送次数
    //     shouldResetTimeout: true, //重置超时时间
    //     // 重复请求延迟
    //     retryDelay: (retryCount: number) => {
    //         return retryCount * 1000
    //     },
    //     // 返回true为打开自动发送,false为关闭自动发送请求
    //     retryCondition: (error: AxiosError) => {
    //         if (error.message.includes('timeout')) {
    //             return true;
    //         }
    //         return !error.response || error.response.status !== 401;
    //     }
    // })
    this.addInterceptors(this.http);
  }

  get<T>(url: string, param?: unknown, config?: AxiosRequestConfig) {
    return this.handleErrorWrapper<T>(
      this.http.get(`${url}${param ? "?" : ""}${qs.stringify(param)}`, config)
    );
  }

  getDownload<T>(url: string, param?: unknown) {
    return this.handleErrorWrapper<T>(
      this.http.get(`${url}?${qs.stringify(param)}`, {
        responseType: "arraybuffer",
      })
    );
  }

  post<T>(url: string, param: unknown, config?: AxiosRequestConfig) {
    return this.handleErrorWrapper<T>(this.http.post(url, param, config));
  }

  postDownload<T>(url: string, param?: unknown) {
    return this.handleErrorWrapper<T>(
      this.http.post(url, param, { responseType: "arraybuffer" })
    );
  }

  put<T>(url: string, param?: unknown, config?: AxiosRequestConfig) {
    return this.handleErrorWrapper<T>(this.http.put(url, param, config));
  }

  delete<T>(url: string, param?: unknown, config?: AxiosRequestConfig) {
    return this.handleErrorWrapper<T>(
      this.http.delete(url, { data: param, ...config })
    );
  }

  private addInterceptors(http: AxiosInstance) {
    // 请求拦截器
    http.interceptors.request.use((config: AxiosRequestConfig) => {
      NProgress.start();
      // 添加token
      const { token } = useUserInfo();
      if (token) config.headers!.Authorization = token;
      // 验证请求状态码
      config.validateStatus = (status: number): boolean => {
        switch (status) {
          case 200:
            break;
          case 401:
            // 这里捕获用户信息过期到状态码,看个人需求修改
            window.localStorage.clear();
            window.sessionStorage.clear();
            ElMessageBox.alert("用户信息过期,请重新登录!", "提示", {
              showClose: false,
              showConfirmButton: false,
            });
            setTimeout(() => {
              ElMessageBox.close();
              window.localStorage.clear();
              window.location.reload();
            }, 1000);
            break;
          case 404:
            ElNotification.error("接口错误!");
            break;
          case 503:
            ElNotification.error("服务不可用!");
            break;
          default:
            ElNotification.error("服务器错误");
            console.warn(`status = ${status}`);
            break;
        }
        return status === 200;
      };
      return config;
    });

    // 响应拦截
    http.interceptors.response.use(
      (response: AxiosResponse) => {
        NProgress.done();
        return response;
      },
      (error) => {
        NProgress.done();
        return Promise.reject(error);
      }
    );
  }

  private async handleErrorWrapper<T>(
    p: AxiosPromise
  ): Promise<DataRequest<T>> {
    return p
      .then((response) => {
        if (response.config.responseType === "arraybuffer") {
          return response.data;
        }
        const {
          data: { code, msg },
        } = response;
        if (code === 200) {
          // 捕获到正常请求,每个人定义到状态码不一样
          return response.data;
        }
        ElMessage.error(msg);
        return Promise.reject(msg);
      })
      .catch((error: AxiosError) => {
        console.warn(error);
        if (error.code === "ERR_NETWORK") ElMessage.error("网络错误");
        if (error.code === "ECONNABORTED") ElMessage.error("请求超时");
        return error;
      });
  }
}
export const httpService = new HttpService();

在上文中设计到一个部署到地址问题,对应目录下新建service.ts文件

const NODE_ENV = import.meta.env.MODE as string;
const VITE_APP_URL: string = import.meta.env.VITE_APP_URL as string;
// 不同环境下url地址
export const ENV_URL: string =
  NODE_ENV === "config" ? (window as any).g.baseUrl : VITE_APP_URL;

在项目根目录新建.env.development.env.production.env.test这种类似的文件,可以写上import.meta.env.MODEimport.meta.env.VITE_APP_URL的值
这些具体可以在vite 文档中查看
在 public 目录下新建一个 config.js 文件,其内容如下

window.g = {
  baseUrl: "ENV_BASE_URL",
};

在 index.html 中引入这个文件

.......
<script>
  document.write(
    '<script src="/config.js?time="' +
      new Date().getTime() +
      ' type="module" charset="utf-8"><\/script>'
  );
</script>

.......

对应的可以部署在 Docker 上,案例给的 Dockerfile

FROM nginx:1.17.6
COPY dist/ /usr/share/nginx/html/
CMD ["/bin/bash", "-c", "sed -i \"s@ENV_BASE_URL@\"${ENV_BASE_URL}\"@\" /usr/share/nginx/html/confing.js; nginx -g \"daemon off;\" "]

打包使用方法

docker build -t app:1.0 --build-arg ENV_BASE_URL=http://xxx.com .

安装 Pinia

npm install pinia

同时为了能够数据持久化安装**pinia-plugin-persistedstate**

npm i pinia-plugin-persistedstate

接下来我们要做一些自己的个性化设置,让代码更加有规则吧
src/store/index.ts写入如下代码

// pinia数据持久化存储
import { createPinia } from "pinia";
import { createPersistedState } from "pinia-plugin-persistedstate";
// 第一个参数是应用程序中 store 的唯一 id
const store = createPinia();
store.use(
  createPersistedState({
    serializer: {
      // 指定参数序列化器
      serialize: JSON.stringify,
      deserialize: JSON.parse,
    },
  })
);
export default store;

在 main.ts 中注册 store 以及上面编写的动态配置文件

import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);

import config from "@/assets/config/host.config.js";
app.config.globalProperties.$config = config;

// Pinia 持久化
import store from "@/store/index";
// 注册已经加上了持久化的pinia
app.use(store);
// app.use(ElementPlus);
app.mount("#app");

提示找不到文件的错误

这里会看到一些报错提示,找不到模块“./App.vue”或其相应的类型声明。
找不到模块“@/store/index”或其相应的类型声明。
第一个解决的方法是src/env.d.ts中加入

/// <reference types="vite/client" />

declare module "*.vue" {
  import { DefineComponent } from "vue";
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

第二个找不到文件的问题主要是@没有被解析,ts 文件不能被正确识别。
解析@在根目录下的 vite.config.js

import path from 'path'
import { defineConfig } from 'vite'
.......
const pathSrc = path.resolve(__dirname, 'src')
......
export default defineConfig({
......
 resolve: {
    alias: {
      "@/": `${pathSrc}/`
    }
  },
})

对于 ts 文件的识别就需要在项目根目录下新建一个tsconfig.json文件

{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": false,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "paths": {
      "@/*": ["src/*"]
    },
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

有了上面的这个作为基础,我们就能定义自己想要的数据了,例如我定义一个存储用户信息的 store
src/store/user.ts

import { defineStore } from "pinia";
// 第一个参数是应用程序中 store 的唯一 id
export const useUserStore = defineStore("user", {
  // 其它配置项
  // persist: {
  //   // 自定义数据持久化方式
  //   // key: 'store-key', 指定key进行存储,此时非key的值不会持久化,刷新就会丢失
  //   storage: window.localStorage, // 指定换成地址
  //   // paths: ['nested.data'],// 指定需要持久化的state的路径名称
  //   beforeRestore: (context) => {
  //     console.log('Before' + context)
  //   },
  //   afterRestore: (context) => {
  //     console.log('After' + context)
  //   },
  // },
  persist: { storage: sessionStorage }, //开启数据持久化,所有store数据将被持久化到指定仓库
  state: () => ({
    name: "",
    age: -1,
  }),
  getters: {
    getUser() {
      return { name: this.name, age: this.age };
    },
  },
  actions: {
    setUser(user) {
      for (let i in user) {
        this[i] = user[i];
      }
    },
  },
});

这样就可以非常简单的在需要使用的文件种导入,例如。

<template>
  <div>{{ userStore.name }}--{{ userStore.age }}</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/user'
let userStore = useUserStore()

userStore.setUser({ name: '张三', age: 22 })
</script>

<style scoped></style>

安装 VueRouter

vue3.x 的对应的 VueRouter 版本在 v4.x,如果你是 vue2.x 使用的路由版本在 4.x 会提示一些很奇怪的错误。

  1. 安装
    npm install vue-router@4
    
2. 路由注册文件
   
   在`/src/router/index.ts`文件中编写自定的路由。这里主要是引入注册页面,并且希望它能够动态导入路由。目前这项功能我没使用上,不过记录一下。
   ```typescript
   import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
       function getRoutes() {
       // const { routes } = loadRouters()
       const routes = [
           {
           path: '/home',
           name: '我的APP',
           meta: {
               title: '主页',
               keepAlive: true,
           },
           component: () => import('../views/teacher/home.vue'),
           },
       ]
       /**
        * 如果要对 routes 做一些处理,请在这里修改
        */
       return routes
       }
       const router = createRouter({
       history: createWebHashHistory(),
       routes: getRoutes(),
       })
       // router.beforeEach((to, from, next) => {
       //  next()
       // })
       export default router
   
       // 下面这个函数就是能自动导入指定页面下的页面, 并自动组合成上面写的router
       /** 以下代码不要修改 */
       function loadRouters() {
       const context = import.meta.globEager('../views/**/*.vue')
       const routes: RouteRecordRaw[] = []
       let modules = import.meta.glob('../views/**/*.vue')
       Object.keys(context).forEach((key: any) => {
           if (key === './index.ts') return
           let name = key.replace(/(\.\.\/views\/|\.vue)/g, '')
           let path = '/' + name.toLowerCase()
           if (name === 'Index') path = '/'
           routes.push({
           path: path,
           name: name,
           component: modules[`../views/${name}.vue`],
           })
       })
       return { context, routes }
       }```
3. 将写好的文件导入注册到`main.ts`中
   ```typescript
   .......
   //导入路由
   import router from '@/router/index'
   app.use(router)
   ......```
4. 那么我们就只要在APP.vue中加入`<router-view></router-view>`就能够跳转路由渲染页面了。
   ```typescript
   // 引入useRouter
   import { useRouter } from 'vue-router'
   let router = useRouter()
   
   const toMain = () => {
    router.push({ path: '/home' })
    }```

这里就完成了路由的跳转配置

### 路由的灵活缓存方案

但是平常可能会遇到这样一个这样的场景A->B,返回到A页面的时候希望保存之前的状态,A->C返回的时候就重新调用函数。那么就需要一个灵活的缓存操作。而如果你使用`activated`这种方法,你就要在URL或者什么地方记录本次是否重新调用接口,这样就显得太麻烦了。我们都了解使用`keep-alive`能缓存组件,同时`include`和`exclude`能让那些组件缓存和不缓存,而且`exclude`优先级比`include`高。那么我是否可以通过操作这2个数组来控制是否缓存页面。在`/src/router/keepAlive.ts`文件中写入以下代码

```typescript
import { ComponentInternalInstance, ref } from 'vue'
export const excludes = ref<string[]>([])
export function removeKeepAliveCache(instance: ComponentInternalInstance) {
  excludes.value.push(instance.type.name!)
  console.log(excludes.value, 'remove')
}
export function resetKeepAliveCache(instance: ComponentInternalInstance) {
  excludes.value = excludes.value.filter((item) => item !== instance.type.name)
  console.log(excludes.value, 'reset')
}
export const SimpleEvents = {
  map: new Map(),
  registerEvent(key: string, cb: (data?: any) => void) {
      this.map.set(key, cb)
},
  emit(key: string, params?: any) {
      if (this.map.has(key)) {
          this.map.get(key)(params)
      }
  },
}
export const GlobalData = {
 animationMode: ref('slide'),
}

一个简单高效的通过控制exclude数组来改变是否缓存页面的方法就出现了。在 APP.vue 页面引入

<!-- App.vue -->
<template>
  <router-view v-slot="{ Component, route }">
    <keep-alive :include="include" :exclude="excludes">
      <component :is="Component"></component>
    </keep-alive>
  </router-view>
</template>

<script lang="ts">
  export default {
    name: "Home",
  };
</script>
<script lang="ts" setup>
  import { excludes } from "@/router/keepAlive";
  const include = ["A", "B", "C"];
</script>

当我们从 A 跳入 C 的时候我们希望 C 返回到 A 不缓存

<!-- A -->
<template> A页面 </template>

<script lang="ts">
  export default {
    name: "A",
  };
</script>
<script lang="ts" setup>
  import { useRouter, onBeforeRouteLeave } from "vue-router";
  import {
    removeKeepAliveCache,
    resetKeepAliveCache,
  } from "@/router/keepAlive";
  const instance = getCurrentInstance()!;
  onBeforeRouteLeave((to, from) => {
    if (to.path === "/C") {
      removeKeepAliveCache(instance);
    } else {
      resetKeepAliveCache(instance);
    }
  });
</script>
<!-- C -->
<template> C页面 </template>

<script lang="ts">
  export default {
    name: "C",
  };
</script>
<script lang="ts" setup></script>

通过removeKeepAliveCache, resetKeepAliveCache暴露的方法来控制 exclude 数组,从而达到是否缓存这个页面,同时还能做一些动画。这里如果有时间就去琢磨实现一下。

优化

上面的步骤基本完成了一个项目的搭建,和一些开发时候的优化。这里做的优化是一些代码编写时候的便捷优化。Vue3 比较不同的是它可以采用组合式编程,一段一段的。

自动导入

由于经常要引入 vue 的方法,而我又不想自己手动写,想让它自动导入,这怎么弄呢

  1. 安装unplugin-auto-import

    npm i -D unplugin-auto-import
    
  2. vite.config.ts 配置

    ......
    import AutoComplete from 'unplugin-auto-import/vite'
    ......
    const pathSrc = path.resolve(__dirname, 'src')
    export default defineConfig({
       .....
       plugins:[
        AutoComplete({
            imports: ['vue', 'vue-router'],
            dts: path.resolve(pathSrc, 'auto-imports.d.ts'),
            }),
       ]
    })
    

打包优化

这个只要在 vite.config.ts 中配置一下

export default defineConfig({
  base: "./" /* 这个就是webpack里面的publicPath */,
  build: {
    rollupOptions: {
      output: {
        // 最小化拆分包
        manualChunks: (id) => {
          if (id.includes("node_modules")) {
            return id
              .toString()
              .split("node_modules/")[1]
              .split("/")[0]
              .toString();
          }
        }, // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
        entryFileNames: "js/[name].[hash].js", // 用于命名代码拆分时创建的共享块的输出命名
        chunkFileNames: "js/[name].[hash].js", // 用于输出静态资源的命名,[ext]表示文件扩展名
        // assetFileNames: '[ext]/[name].[hash].[ext]', // 拆分js到模块文件夹 // chunkFileNames: (chunkInfo) => { //     const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/') : []; //     const fileName = facadeModuleId[facadeModuleId.length - 2] || '[name]'; //     return `js/${fileName}/[name].[hash].js`; // },
      },
    },
  },
});

最后可能有其他的优化我暂时记不起来了,把整个配置文件分享出来,以后想起来了,再接着编写

import path from "path";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// import ElementPlus from 'unplugin-element-plus/vite'
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
import AutoImport from "unplugin-auto-import/vite";
import { splitVendorChunkPlugin } from "vite";
import Unocss from "unocss/vite";
import {
  presetAttributify,
  presetIcons,
  presetUno,
  transformerDirectives,
  transformerVariantGroup,
} from "unocss";
const pathSrc = path.resolve(__dirname, "src");
// https://vitejs.dev/config/
export default defineConfig({
  base: "./" /* 这个就是webpack里面的publicPath */,
  build: {
    rollupOptions: {
      output: {
        // 最小化拆分包
        manualChunks: (id) => {
          if (id.includes("node_modules")) {
            return id
              .toString()
              .split("node_modules/")[1]
              .split("/")[0]
              .toString();
          }
        }, // 用于从入口点创建的块的打包输出格式[name]表示文件名,[hash]表示该文件内容hash值
        entryFileNames: "js/[name].[hash].js", // 用于命名代码拆分时创建的共享块的输出命名
        chunkFileNames: "js/[name].[hash].js", // 用于输出静态资源的命名,[ext]表示文件扩展名
        // assetFileNames: '[ext]/[name].[hash].[ext]', // 拆分js到模块文件夹 // chunkFileNames: (chunkInfo) => { //     const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/') : []; //     const fileName = facadeModuleId[facadeModuleId.length - 2] || '[name]'; //     return `js/${fileName}/[name].[hash].js`; // },
      },
    },
  },
  resolve: {
    alias: {
      "@/": `${pathSrc}/`,
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`,
      },
    },
  },
  plugins: [
    vue(),
    AutoImport({
      // Auto import functions from Vue, e.g. ref, reactive, toRef...
      // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
      imports: ["vue"],
      // Auto import functions from Element Plus, e.g. ElMessage, ElMessageBox... (with style)
      // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
      resolvers: [
        ElementPlusResolver(),
        // Auto import icon components
        // 自动导入图标组件
        IconsResolver({
          prefix: "Icon",
        }),
      ],
      dts: path.resolve(pathSrc, "auto-imports.d.ts"),
    }),
    Components({
      // allow auto load markdown components under `./src/components/`
      extensions: ["vue", "md"],
      // allow auto import and register components used in markdown
      include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
      resolvers: [
        ElementPlusResolver({
          importStyle: "sass",
        }),
        // Auto register icon components
        // 自动注册图标组件
        IconsResolver({
          enabledCollections: ["ep"],
        }),
      ],
      dts: path.resolve(pathSrc, "components.d.ts"),
    }),
    // https://github.com/antfu/unocss
    // see unocss.config.ts for config
    Unocss({
      presets: [
        presetUno(),
        presetAttributify(),
        presetIcons({
          scale: 1.2,
          warn: true,
        }),
      ],
      transformers: [transformerDirectives(), transformerVariantGroup()],
    }),
    splitVendorChunkPlugin(),
    Icons({
      autoInstall: true,
    }),
    // ElementPlus({
    // // options
    // }),
  ],
  server: {
    proxy: {
      // 字符串简写写法
      // '/foo': 'http://localhost:4567',
      // 选项写法
      "/api": {
        target: "http://xx.com",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
      // 正则表达式写法
      // '^/fallback/.*': {
      //   target: 'http://jsonplaceholder.typicode.com',
      //   changeOrigin: true,
      //   rewrite: (path) => path.replace(/^\/fallback/, '')
      // },
      // 使用 proxy 实例
      // '/api': {
      //   target: 'http://jsonplaceholder.typicode.com',
      //   changeOrigin: true,
      //   configure: (proxy, options) => {
      //     // proxy 是 'http-proxy' 的实例
      //   }
      // },
      // Proxying websockets or socket.io
      // '/socket.io': {
      //   target: 'ws://localhost:3000',
      //   ws: true
      // }
    },
  },
});

模版编写优化 tsx

这个问题是在写element-plusVirtualized Table 虚拟化表格中遇到的,由于它的示例代码里面有用到 tsx,所以需要用到@vitejs/plugin-vue-jsx这个插件。

注意:

  1. 这个插件和 vite 之间有版本对应的关系。我的 vite 是 2.9.9 所以我安装了版本1.3.10

    npm install @vitejs/plugin-vue-jsx@^1.3.10 -D
    
  2. 安装之后要在 vite.config.ts 中引入这个插件

    ......
    import VueJsx from '@vitejs/plugin-vue-jsx'
    ....
    plugins:[VueJsx(),]
    

然后就可以将script上lang="ts",改成lang="tsx"