基于vite+vue3+typescript搭建一个前端开发项目

1,131 阅读7分钟

vite

什么是vite?

  • 一个开发服务器,基于原生ESModule 提供了丰富的内部构建功能:

    1、原生ES导入不支持裸模块导入,Vite会检测到裸模块导入的源文件并将CommonJS或UMD转换为ESM格式,重写导入为合法的URL使得浏览器可以正确导入。

    2、预构建这一步由esbuild执行,Vite的冷启动时间比基于js的任何打包器都要快。同时Vite提供了一套原生ESM的HMR API,具有HMR功能的框架可以利用该API进行无需重加载或清除应用程序状态的即时更新。

  • 一套构建指令,使用Rollup打包代码,并且是预配置的,可输出用于生产环境的高度优化过的静态资源:

    1、静态资源使用需要import导入,会返回解析后的URL,小于4M的图片资源会被编译成base64格式

    2、支持使用特殊的import.meta.glob函数从文件系统导入多个模块,通常用于动态路由的文件读取

    3、动态引入时,Rollup对共用chunk的情况进行优化处理:

    Vite 将使用一个预加载步骤自动重写代码,来分割动态导入调用,以实现当 A 被请求时,C 也将 同时 被请求:C 也可能有更深的导入,在未优化的场景中,这会导致更多的网络往返。Vite 的优化会跟踪所有的直接导入,无论导入的深度如何,都能够完全消除不必要的往返。

    Entry ---> (A + C)
    

自动支持

默认支持TS和css/JSON引入

自动支持PostCSS 添加postcss.config.js配置文件

自动对sass等css预处理器进行了处理

只支持现代浏览器(不包括ie)

支持jsx和react项目的搭建

和vue-cli区别

  1. webpack会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。
  2. 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显
  3. 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。(代码需开启本地服务才能运行,否则会产生跨域错误)
  4. vite是按需加载,webpack是全部加载:在HMR(热更新)方面,当改动了一个模块后,vite仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。
  5. vite的优势在开发环境:当需要打包到生产环境时,vite使用传统的rollup(也可以自己手动安装webpack来)进行打包,因此,vite的主要优势在开发阶段。另外,由于vite利用的是ES Module,因此在代码中(除了vite.config.js里面,这里是node的执行环境)不可以使用CommonJS 总体来说1. 工具本身定位不同 webpack是底层的东西,vite则是更上层的工具。webpack是配置化,灵活度极高的工具,vite是开箱即用,使用更简单的工具。 2.原理不同 webpack是bundle,自己实现了一套模块导入导出机制。vite是利用浏览器的esm能力,是bundless。 3.vite开箱即用,更加简单,基于浏览器esm,使得hmr更加优秀 达到极速效果。 webpack更加灵活,api以及插件生态更加丰富,高可定制,兼容更多浏览器,例如ie11。

搭建项目

兼容性注意

需要Node版本大于12.0.0,有些模块需要依赖更高版本的Node才能运行

包管理工具推荐使用yarn

创建项目

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

yarn 安装依赖 设置package.json的脚本 “dev”:“vite --open”

yarn run dev可以自动打开浏览器

安装项目所需插件

状态管理

vuex

vuex由于vuex4版本对ts兼容性不是很好,推荐中小型项目的话使用pinia进行状态管理

pinia

yarn add pinia

pinia和vuex区别:

1、id必须,将所使用store连接到devtools

2、创建方式不同

3、没有mutations

4、特殊的actions更加灵活:

  • 可以通过组件或其他 action 调用
  • 可以从其他 store 的 action 中调用
  • 直接在 store 实例上调用
  • 支持同步或异步
  • 有任意数量的参数
  • 可以包含有关如何更改状态的逻辑(也就是 vuex 的 mutations 的作用)
  • 可以 $patch 方法直接更改状态属性

新建store文件夹,新建index.ts文件

import {defineStore} from 'pinia'export const useStore = defineStore('storeId', {
    state: () => {
        return {
            counter: 0,
            name: 'Eduardo',
            isAdmin: true,
        }
    },
    getters:{
        nameLength:(state)=>state.name.length
    },
    actions:{ //可以做异步
        async insertPost(data:string){
            const res = await Ajax(data)
        }
    }
})
​
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
组件内使用:
import {useStore} from "@/store/store";
import {index} from "@/types";
​
const store:index = useStore()
console.log(useStore().$state)
//修改数据使用store.$patch

VueRouter

yarn add vue-router@4

src新建router文件夹:router.js文件

可以设置meta属性方便做一些功能的实现

import {createRouter, createWebHistory, RouteRecordRaw} from 'vue-router'const routes: RouteRecordRaw[] = [
    {
        path: '/',
        name: 'Login',
        meta:{
            title:string//页面标题
            icon?:string//图标,搭配菜单使用
            auth?:boolean//是否需要登录权限
            ignoreAuth?:boolean//是否忽略权限
            roles?:RoleEnum[],//可以访问的角色
            keepAlive?:boolean//是否开启页面缓存
            hideMenu?:boolean//隐藏起来不在菜单中展示的路由
            order?:number//菜单排序
        },
        component: () => import('@/pages/login/Login.vue'), // 注意这里要带上 文件后缀.vue
    },
]
​
const router = createRouter({
    history: createWebHistory(),
    routes,
})
​
export default router

Element-plus

yarn add element-plus

yarn add -D unplugin-vue-components

可以搭配antfu大佬开发的unplugin-vue-components实现组件和UI库组件自动引入

配置vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite' // 新增
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' // 引入ElementPlusResolver// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    Components({resolvers: [ElementPlusResolver()]}) // 添加配置
  ]
})

Axios

或者使用VueRequest

yarn add axios

yarn add nprogress安装进度条

新建utils文件夹,新建request.ts文件

import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from "axios";
import NProgress from 'nprogress'
import {Message} from '@element-plus/icons-vue'interface ResType<T> { //返回值得类型 根据公司实际需要自己配置减少any使用
    code: number
    data?: T
    msg: string
    err?: string
}
​
interface Http { //
    get<T>(url: string, params?: unknown): Promise<ResType<T>>
    post<T>(url: string, params?: unknown): Promise<ResType<T>>
    upload<T>(url: string, params: unknown): Promise<ResType<T>>
    download(url: string): void
}
​
// 设置请求头和请求路径
axios.defaults.baseURL = '/api'
axios.defaults.timeout = 10000
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'//请求拦截器
axios.interceptors.request.use(
    (config: AxiosRequestConfig) => {
        const token = window.sessionStorage.getItem('token')
        if (token) {
            // @ts-ignore
            config.headers.token = token
        }
        return config
    },
    (error: Error) => {
        return error
    }
)
// 响应拦截
axios.interceptors.response.use(
    (res: AxiosResponse) => {
        switch (res.data.code) {
            case 111:
                sessionStorage.setItem('token', '');
                return res
            case 200:
                return JSON.stringify(res.data)
            default :
                return
        }
    },
    (error: AxiosError) => {
        // 接收到异常响应的处理开始
        if (error && error.response) {
            // 1.公共错误处理
            // 2.根据响应码具体处理
            switch (error.response.status) {
                case 400:
                    error.message = '错误请求'
                    break;
                case 401:
                    error.message = '未授权,请重新登录'
                    break;
                case 403:
                    error.message = '拒绝访问'
                    break;
                case 404:
                    error.message = '请求错误,未找到该资源'
                    window.location.href = "/NotFound"
                    break;
                case 405:
                    error.message = '请求方法未允许'
                    break;
                case 408:
                    error.message = '请求超时'
                    break;
                case 500:
                    error.message = '服务器端出错'
                    break;
                case 501:
                    error.message = '网络未实现'
                    break;
                case 502:
                    error.message = '网络错误'
                    break;
                case 503:
                    error.message = '服务不可用'
                    break;
                case 504:
                    error.message = '网络超时'
                    break;
                case 505:
                    error.message = 'http版本不支持该请求'
                    break;
                default:
                    error.message = `连接错误${error.response.status}`
            }
        } else {
            // 超时处理
            if (JSON.stringify(error).includes('timeout')) {
                Message.error('服务器响应超时,请刷新当前页')
            }
            Message.error('连接服务器失败')
        }
        Message.error(error.message)
        //处理结束
        //如果不需要错误处理,以上的处理过程都可省略
        return Promise.resolve(error.response)
    }
)
​
const Http: Http = {
    get(url, params) {
        return new Promise((resolve, reject) => {
            NProgress.start()
            axios
                .get(url, {params})
                .then((res) => {
                    NProgress.done()
                    resolve(res.data)
                })
                .catch((err) => {
                    NProgress.done()
                    reject(err.data)
                })
        })
    },
    post(url, params) {
        return new Promise((resolve, reject) => {
            NProgress.start()
            axios
                .post(url, JSON.stringify(params)) //注意此处根据实际情况进行处理
                .then((res) => {
                    NProgress.done()
                    resolve(res.data)
                })
                .catch((err) => {
                    NProgress.done()
                    reject(err.data)
                })
        })
    },
    upload(url, file) {
        return new Promise((resolve, reject) => {
            NProgress.start()
            axios
                .post(url, file, {
                    headers: {'Content-Type': 'multipart/form-data'}, //文件手动上传时调用
                })
                .then((res) => {
                    NProgress.done()
                    resolve(res.data)
                })
                .catch((err) => {
                    NProgress.done()
                    reject(err.data)
                })
        })
    },
    download(url) {
        const iframe = document.createElement('iframe')
        iframe.style.display = 'none'
        iframe.src = url
        iframe.onload = function () {
            document.body.removeChild(iframe)
        }
        document.body.appendChild(iframe)
    },
}
​
export default Http;
​

环境变量配置

1、.env.development文件:NODE_ENV=development VITE_APP_API='API_URL'

2、.env.production文件:NODE_ENV=production VITE_APP_API= 'YOUR WEB URL'

组件使用:import.meta.env.VITE_APP_API

3、打包区分开发环境和生产环境

"build:dev": "vite build --mode development",
"build:pro": "vite build --mode production",

vite常用基础配置

代理和打包配置

生产环境生成.gz文件:开启gzip压缩静态资源,提高页面加载速度

yarn add --dev vite-plugin-compression

下面是vite.config.ts的全部配置

vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import viteCompression from 'vite-plugin-compression'
import { resolve } from "path";
​
// https://vitejs.dev/config/
export default defineConfig({
  base: "./", // 类似publicPath,'./'避免打包访问后空白页面,要加上,不然线上也访问不了
  plugins: [
    vue(),
    //配置插件
    plugins: [
        vue(),
        vueJsx(),
        viteCompression({
            verbose: true,
            disable: false,
            threshold: 10240,
            algorithm: 'gzip',
            ext: '.gz',
        }),
    ],
    AutoImport({
      imports: [
        "vue",
        "vue-router",
        "pinia",
        {
          axios: [["default", "axios"]]
        }
      ],
      dts: "src/auto-import.d.ts",
      eslintrc: {
        enabled: true
      }
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      dts: true
    })
  ],
  build: {
    outDir: "",
    assetsDir: "assets", //指定静态资源存放路径
    sourcemap: false, //是否构建source map 文件
    terserOptions: {
      // 生产环境移除console
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  server: {
    https: false, // 是否开启 https
    open: false, // 是否自动在浏览器打开
    port: 8888, // 端口号
    host: "0.0.0.0",
    // hmr: {
    //   overlay: false
    // },
    proxy: {
      "/api": {
        target: "", // 后台接口
        changeOrigin: true,
        secure: false, // 如果是https接口,需要配置这个参数
        // ws: true, //websocket支持
        rewrite: (path) => path.replace(/^/api/, "")
      }
    }
  },
  resolve: {
    alias: {
      //拼接文件夹名和后面自定的名字
      "@": resolve(__dirname, "src"),
      "@assets": resolve(__dirname, "src/assets"),
      "@components": resolve(__dirname, "src/components"),
      "@images": resolve(__dirname, "src/assets/images"),
      "@views": resolve(__dirname, "src/views"),
      "@store": resolve(__dirname, "src/store")
    }
  }
});
​
main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import * as ElIconModules from "@element-plus/icons-vue";
import ElementPlus from "element-plus";
import store, { key } from "@/store";
import "element-plus/dist/index.css";
import { Request } from "@/utils/request/index";
import VueAxios from "vue-axios";
​
// 统一注册Icon图标
for (const iconName in ElIconModules) {
  if (Reflect.has(ElIconModules, iconName)) {
    const item = ElIconModules[iconName];
    createApp(App).component(iconName, item);
  }
}
​
createApp(App)
  .use(router)
  .use(ElementPlus)
  .use(store, key)
  .use(VueAxios, Request.init())
  .mount("#app");
tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "baseUrl": ".",
    "suppressImplicitAnyIndexErrors": true,
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "**/*.ts",
    "**/*.tsx",
    "src/**/*.vue"
  ]
}
package.json
{
  "name": "",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite --open",
    "build": "vue-tsc --noEmit && vite build",
    "build:dev": "vue-tsc --noEmit && vite build --mode development",
    "build:prod": "vue-tsc --noEmit && vite build --mode production",
    "preview": "vite preview",
    "prepare": "cd .. && husky install ./radar-config-project/.husky",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^0.2.7",
    "@types/uuid": "^8.3.4",
    "axios": "^0.24.0",
    "core-js": "^3.6.5",
    "element-plus": "^1.3.0-beta.5",
    "mitt": "^3.0.0",
    "pinia": "^2.0.9",
    "vue": "^3.2.26",
    "vue-axios": "^3.4.0",
    "vue-class-component": "^8.0.0-0",
    "vue-router": "^4.0.12",
    "vuex": "^4.0.0-0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.9.0",
    "@typescript-eslint/parser": "^5.9.0",
    "@vitejs/plugin-vue": "^2.0.1",
    "eslint": "^8.6.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-vue": "^8.2.0",
    "husky": "^7.0.4",
    "lint-staged": "^12.1.7",
    "prettier": "^2.5.1",
    "sass": "^1.47.0",
    "typescript": "^4.5.4",
    "unplugin-auto-import": "^0.5.10",
    "unplugin-vue-components": "^0.17.11",
    "vite": "^2.7.10",
    "vue-tsc": "^0.29.8"
  },
  "lint-staged": {
    "*.{vue,ts}": []
  }
}
.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
    "vue/setup-compiler-macros": true
  },
  parser: "vue-eslint-parser",
  parserOptions: {
    ecmaVersion: 13,
    parser: "@typescript-eslint/parser",
    sourceType: "module"
  },
  extends: [
    "plugin:vue/vue3-recommended",
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "./.eslintrc-auto-import.json"
  ],
  rules: {
    "vue/multi-word-component-names": 0,
    endOfline: "auto"
  }
};

其他常用插件

可以查看官方文档:vitejs.cn/plugins/\

@vitejs/plugin-vue 提供 Vue 3 单文件组件支持 @vitejs/plugin-vue-jsx 提供 Vue 3 JSX 支持(通过 专用的 Babel 转换插件) @vitejs/plugin-legacy 为打包后的文件提供传统浏览器兼容性支持