Vite 使用指南

15,076 阅读5分钟

契子

本文主要简单介绍一下 Vite 相关的知识点及使用,本文会不定期更新优化收集在使用 Vite 过程中遇到的一些问题和相关配置。

一、前言

Vite 下一代前端开发与构建工具 >>

Vite 是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  • 一个开发服务器,基于原生 ES模块 提供了丰富的内建功能,如模块热更新(HMR);
  • 一套构建指令,使用 Rollup 打包你的代码,并且它是预配置的,可以输出用于生产环境的优化过的静态资源。

其特点就是:

  • 极速的服务启动:使用原生 ESM 文件,无需打包!
  • 轻量快速的热重载:无论应用程序大小如何,都始终极快的模块热重载(HMR
  • 丰富的功能:对 TypeScriptJSXCSS 等支持开箱即用。
  • 优化的构建:可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建
  • 通用的插件:在开发和构建之间共享 Rollup-superset 插件接口。
  • 完全类型化的API:灵活的 API 和完整 TypeScript 类型。

浏览器支持:

  1. 开发环境中:Vite 需要在支持 原生 ES 模块动态导入 的浏览器中使用。
  2. 生产环境中:默认支持的浏览器需要支持 通过脚本标签来引入原生 ES 模块 。可以通过官方插件 @vitejs/plugin-legacy 支持旧浏览器。

二、搭建项目

参考这里 >>

$ yarn create @vitejs/app

快速创建项目指令:

# vue + ts
$ yarn create @vitejs/app vue-ts-proj --template vue-ts
# react + ts
$ yarn create @vitejs/app react-ts-proj --template react-ts

三、Vite 配置项

配置文件:vite.config.js

1. 取别名

import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
   resolve: {
    alias: {
      "@": resolve(__dirname, "src"),
    },
  }
});

提示:如果你使用 ts,那么你还需要在 ts.config.json 文件中添加如下配置:

{
  "compilerOptions": {
   "baseUrl": ".",
   "paths": {
     "@/*": ["src/*"]
   }
 },

2. 配置代理

// 配置代理
server: {
  proxy: {
    "/api": {
      target: "http://jsonplaceholder.typicode.com",
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, ""),
    },
  },
},

3. 环境变量

①. 在根目录新建 .env.dev.env.production.env.test

# 开发环境:.env.dev
NODE_ENV=development
VITE_APP_ENV=development

# 服务器地址
VITE_APP_HOST=开发环境服务器地址

# 基础路径
VITE_APP_BASE=""

# appID相关
VITE_APP_APPID_WEIXIN=微信appID
VITE_APP_APPID_ALIPAY=支付宝appID
# 生产环境:.env.production 
NODE_ENV=production
VITE_APP_ENV=production

# 服务器地址
VITE_APP_HOST=生产环境服务器地址

# 基础路径
VITE_APP_BASE=""

# appID相关
VITE_APP_APPID_WEIXIN=微信appID
VITE_APP_APPID_ALIPAY=支付宝appID
# 测试环境:.env.test
NODE_ENV=production
VITE_APP_ENV=production

# 服务器地址
VITE_APP_HOST=测试环境服务器地址

# 基础路径
VITE_APP_BASE=""

# appID相关
VITE_APP_APPID_WEIXIN=微信appID
VITE_APP_APPID_ALIPAY=支付宝appID

Tips:

1)环境变量名必须以 VITE_APP 开头;

2)配置文件的内容请根据项目需要设置;

②. 修改 package.json 指令

**@Vue **

"dev": "vite --mode dev --host 0.0.0.0",
"test": "vue-tsc --noEmit && vite build --mode test --base=/二级目录/",
"build": "vue-tsc --noEmit && vite build --mode production",

@React

"dev": "vite --mode=dev --host=0.0.0.0",
"test": "tsc && vite build --mode=test",
"build": "tsc && vite build --mode=production",

③. 配置环境变量智能提示

@Vue

根目录新建 env.d.ts 文件,键入以下内容:

interface ImportMetaEnv {
  VITE_APP_BASE: string;
  VITE_APP_HOST: string;
  VITE_APP_APPID_WEIXIN: string;
  VITE_APP_APPID_ALIPAY: string;
}

@React

vite-env.d.ts 文件中,键入以下内容:

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

interface ImportMetaEnv {
  VITE_APP_BASE: string;
  VITE_APP_HOST: string;
  VITE_APP_APPID_WEIXIN: string;
  VITE_APP_APPID_ALIPAY: string;
}

④. 使用环境变量

import.meta.env.VITE_APP_HOST

四、项目配置

1. 预处理样式

直接安装 less/sass 即可使用,无需任何配置。

$ yarn add less -D
$ yarn add sass -D

2. 移动端适配方案

安装依赖:

$ yarn add postcss-pxtorem autoprefixer -D

在根目录下创建 postcss.config.js 文件:

module.exports = {
  plugins: {
    autoprefixer: {
      overrideBrowserslist: [
        "Android 4.1",
        "iOS 7.1",
        "Chrome > 31",
        "ff > 31",
        "ie >= 8",
      ],
    },
    "postcss-pxtorem": {
      rootValue: 37.5, // Vant 官方根字体大小是 37.5
      propList: ["*"],
      selectorBlackList: [".norem"], // 过滤掉.norem-开头的class,不进行rem转换
    },
  },
};

新建 src/utils/rem.ts 文件:

// rem等比适配配置文件
// 基准大小,注意此值要与 postcss.config.js 文件中的 rootValue保持一致
const baseSize = 37.5;
// 设置 rem 函数
function setRem() {
  // 当前页面宽度相对于375宽的缩放比例,可根据自己需要修改,一般设计稿都是宽750(图方便可以拿到设计图后改过来)。
  const scale = document.documentElement.clientWidth / 375; // 设置页面根节点字体大小(“Math.min(scale, 2)” 指最高放大比例为2,可根据实际业务需求调整)
  document.documentElement.style.fontSize = baseSize * Math.min(scale, 2) + "px";
}
// 初始化
setRem();
// 改变窗口大小时重新设置 rem
window.onresize = function () {
  setRem();
};

mian.ts(x) 引入

import "./utils/rem"

提示:如果提示 “无法在 "--isolatedModules" 下编译“rem.ts”,因为它被视为全局脚本文件。请添加导入、导出或空的 "export {}" 语句来使它成为模块 ”此时你应该在 tsconfig.json 文件中将 isolatedModules 字段设置为 false

3. 全局TS类型声明

在根目录创建 typings/index.d.ts 文件

export {};

// => 全局类型声明
declare global {
  interface Window {
    _hmt: any;
    wx: any;
    AlipayJSBridge: any;
  }
  namespace GD {
    interface BaseResponse<T = any> {
      code: number;
      data: T;
      msg: string;
      page: {
        pageNo: number;
        pageSize: number;
        pages: number;
        total: number;
      };
    }
  }
}

提示:你需要在 tsconfig.json 文件里面的 include 字段中加入 [**/*.d.ts]

然后就可以在全局访问了,如:

const response = GD.BaseResponse<{name: string}>

4. mock.js

①. 安装依赖:

$ yarn add mockjs -S
$ yarn add vite-plugin-mock -D

②. 引入插件 vite.config.js:

import { viteMockServe } from 'vite-plugin-mock';
export default defineConfig({
  plugins: [
    viteMockServe({})
  ],
});

③. 新建 mock/test.ts 文件

export default [
  {
    url: "/api-dev/users",
    method: "get",
    response: (req) => {
      return {
        code: 0,
        data: {
          name: "Li-HONGYAO",
          phone: "173 **** 8669",
          address: "成都市高新区雅和南四路216号",
        },
        msg: "success",
      };
    },
  },
];

④. 模拟请求

fetch("/api-dev/users")
  .then((res) => res.json())
  .then((users) => {
    console.log(users);
  });

5. axios

①. 安装相关依赖:

$ yarn add axios lg-tools lg-cookie

lg-cookie >> :我封装的一个关于操作Cookie的库。

lg-tools >>:我封装的一个关于工具函数的库。

②. 创建 /src/utils/request.ts 文件

import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import Cookie from "lg-cookie";
import Tools from "lg-tools";

const service = axios.create({
  baseURL: import.meta.env.VITE_APP_HOST,
  timeout: 20000,
});

// 请求拦截
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // => 如果是GET请求追加时间戳
    if (config.method && /get/i.test(config.method)) {
      config.params = {
        ...config.params,
        timeState: Tools.randomCharacters(1) + Date.now(),
      };
    }
    // => 配置请求头
    const token = Cookie.get<string>("AUTHORIZATION_TOKEN");
    config.headers = {
      "Content-Type": "application/json",
      Authorization: Cookie.get<string>("AUTHORIZATION_TOKEN"),
    };
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, msg } = response.data;
    switch (code) {
      case 0:
        return response.data;
      default:
        console.log(msg);
        return response.data;
    }
  },
  (error: any) => {
    console.log(error);
    /timeout/.test(error.message) && console.log("请求超时,请检查网络");
    return Promise.reject(error);
  }
);

export default service;

③. 创建 /src/api/index.ts && /src/api/test.ts 文件

// 1. src/api/test.ts
import request from "../utils/request";

/**
 * 获取用户信息
 * @returns
 */
export function users<T>() {
  return request.get<T>("/api-dev/users");
}
// 2. src/api/index.ts
import * as test from "./test";

export default {
  test,
};

④. 在全局TS类型声明文件中添加 axios 相关类型配置

// => axios 模块定义
import { AxiosRequestConfig } from "axios";

declare module 'axios' {
  export interface AxiosInstance {
    <T = any>(config: AxiosRequestConfig): Promise<T>;
    request<T = any> (config: AxiosRequestConfig): Promise<T>;
    get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>;
    post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
    patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
  }
}

⑤. 调用接口请求数据

// => 引入api
import api from "./api";

// => 响应数据类型声明
interface UserProps {
  name: string;
  phone: string;
  address: string;
}

// => 接口调用
api.test.users<GD.BaseResponse<UserProps>>().then(res => {
  if(res && res.code === 0) {
    console.log(res.data.name);
  }
})

6. 动态加载静态资源

在使用 webpack 动态引入静态资源可以使用 require 形式,但是在vite中不可取,会抛出 require is not defined 的错误。

不过我们可以通过 import.meta.url 的形式引入,它是一个 ESM 的原生功能,会暴露当前模块的 URL。将它与原生的 URL 构造器 组合使用,在一个 JavaScript 模块中,通过相对路径我们就能得到一个被完整解析的静态资源 URL,其语法形式如下:

new URL(url, import.meta.url)

使用示例:

const imgs = [
  { imgUrl: new URL('./assets/logo_1.png', import.meta.url), text: "标题1" },
  { imgUrl: new URL('./assets/logo_2.png', import.meta.url), text: "标题2" },
];
// => react
{imgs.map((item) => (
  <img src={item.imgUrl.toString()} key={item.title} />
))}

值得注意的是,在生产环境中会抛出 “URL is not defined xxx” 的错误,这个时我们需要使用一个插件:rollup-plugin-import-meta-url-to-module

使用方式比较简单,首先安装依赖:

$ yarn add rollup-plugin-import-meta-url-to-module

然后再 “vit.config.js” 中配置plugins:

import urlToModule from 'rollup-plugin-import-meta-url-to-module';

export default {
  plugins: [
    urlToModule()
  ]
};

# 五、差异化配置

1. vue

- 路由配置

安装依赖:

$ yarn add vue-router@next

在src下新建router目录,新建index.ts文件

import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    redirect: "/login",
  },
  {
    path: "/login",
    name: "login",
    component: () => import("pages/Login/Login.vue"),
    meta: {
      title: "登录",
    },
  },
  {
    path: "/index-page",
    name: "indexPage",
    component: () => import("pages/IndexPage/IndexPage.vue"),
    meta: {
      title: "首页",
    },
  },
];

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

// 导航守卫
router.beforeEach((to, from, next) => {
  if (to.path !== "/favicon.icon") {
    document.title = to.meta.title ? (to.meta.title as string) : "";
    next();
  }
});

export default router;

在main.ts挂载路由

import App from "./App.vue";
import router from "./router";

// App配置/挂载相关
// 1. 创建App
const app = createApp(App);
// 2. 注入
app.use(router);
// 3. 挂载
app.mount("#app");

- 状态管理

安装依赖:

$ yarn add vuex@next

配置:在src下创建store目录,并在store下创建index.ts

import { InjectionKey } from "vue";
import { createStore, ActionTree, GetterTree, MutationTree, Store } from "vuex";

// 1. 声明Store类型
declare interface StoreProps {
  authUrlForIos?: string;
}
// 2. 定义注入类型
export const globalStoreKey: InjectionKey<Store<StoreProps>> = Symbol();

// 3. ---- state
const state: StoreProps = {
  authUrlForIos: "",
};
// 3. ---- getters
const getters: GetterTree<StoreProps, any> = {
  authUrlForIos: (state) => state.authUrlForIos,
};
// 3. ---- mutations
const mutations: MutationTree<StoreProps> = {
  updateAuthUrlForIos(state, payload) {
    state.authUrlForIos = payload;
  },
};
// 3. ---- actions
const actions: ActionTree<StoreProps, any> = {
  updateAuthUrlForIos({ commit }, payload) {
    commit("updateAuthUrlForIos", payload);
  },
};

// 4. 创建导出
export default createStore<StoreProps>({
  state,
  getters,
  mutations,
  actions,
});

挂载:在main.ts挂载数据中心

import App from "./App.vue";
import router from "./router";
import store, { globalStoreKey } from "./store";

// App配置/挂载相关
// 1. 创建App
const app = createApp(App);
// 2. 注入
app.use(router).use(store, globalStoreKey).use(vant);
// 3. 挂载
app.mount("#app");

- Vant3

安装依赖:

$ yarn add vant@next

提示:vite版本不需要配置组件的按需加载,因为Vant 3.0 内部所有模块都是基于 ESM 编写的,天然具备按需引入的能力,但是样式必须全部引入137.2k

在main.ts全局引入样式:

import App from "./App.vue";
import router from "./router";
import store from './store';
import vant from 'vant';
import "vant/lib/index.css";   

// App配置/挂载相关
// 1. 创建App
const app = createApp(App);
// 2. 注入
app.use(router).use(store).use(vant);
// 3. 挂载
app.mount("#app");

- 二级目录部署

方案1:

打包时设置 -base 选项:

"build": "vue-tsc --noEmit && vite build --mode production --base=/二级目录名/",

然后再 router/index.ts 中配置如下:

const router = createRouter({
  // 部署二级目录:createWebHistory(base?: string)
  history: createWebHistory("/二级目录名/"),
  routes,
});

方案2:

vite.config.js 配置项中添加 base 配置,如:

base:"/YOUR_PROJECT_NAME/"

然后再 router/index.ts 中配置如下:

const router = createRouter({
  // 部署二级目录:createWebHistory(base?: string)
  history: createWebHistory("/二级目录名/"),
  routes,
});

2. React

- 路由配置

①. 目录结构

.
├── components
│   ├── Fallback # 懒加载loading提示 
│   ├── NotEnv   # 非微信/支付宝环境提示
│	  └── TabBar   # 标签栏
│   │   ├── index.tsx
│   │   └── index.less
├── layouts
│	  └── index.tsx
├── pages
│   ├── Auth
│   ├── IndexPage
│   ├── Integral
│   ├── Mine
│   ├── NotFound
│	  └── PrivilegeBrand
├── router
│   ├── index.tsx
│	  └── routes.ts
└── main.tsx

提示:这里只例举部分目录,仅供参考。

②. 安装依赖

$ yarn add react-router-dom

提示:目前我使用的版本是 ^6.0.2,关于 v6 版本的路由使用请 参考这里 >>

③. 文件代码

src/layout/index.tsx

import TabBar from '@/components/@lgs-react/TabBar';
import React from 'react';
import { Outlet } from 'react-router-dom';

const Layout: React.FC = () => {
  return (
    <>
      {/* 视图容器,类似于vue中的 router-view */}
      <Outlet />
      {/* 标签栏 */}
      <TabBar />
    </>
  );
};

export default Layout;

src/App.tsx

import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';

import Fallback from '@/components/@lgs-react/Fallback';
import Layout from '@/layout';
import IndexPage from '@/pages/IndexPage';
import Integral from '@/pages/Integral';
import Mine from '@/pages/Mine';
import PrivilegeBrand from '@/pages/PrivilegeBrand';
import Auth from '@/pages/Auth';
import NotFound from './pages/NotFound';
import NotEnv from './components/@lgs-react/NotEnv';

import Tools from 'lg-tools';

const Details = React.lazy(() => import('@/pages/Details'));
const Download = React.lazy(() => import('@/pages/Download'));
const Test = React.lazy(() => import('@/pages/Test'));

/**
 * 环境判断
 * 如果 VITE_APP_SOURCE === 'mp',即表示公众号/生活号
 * 那么在浏览器环境将提示 “请在微信或支付宝客户端打开链接”
 * @param param0
 * @returns
 */
export const GuardEnv: React.FC = ({ children }) => {
  return import.meta.env.VITE_APP_SOURCE === 'mp' &&
    ['weixin', 'alipay'].indexOf(Tools.getEnv()) === -1 ? (
    <NotEnv />
  ) : (
    <>{children}</>
  );
};

/**
 * appRouter
 * @returns
 */
export const AppRouter: React.FC = ({ children }) => {
  return (
    <Suspense fallback={Fallback}>
      <Router basename={import.meta.env.VITE_APP_BASE}>{children}</Router>
    </Suspense>
  );
};

/**
 * appRoutes - 渲染路由
 * @returns
 */
export const AppRoutes: React.FC = () => {
  return (
    <Routes>
      <Route path='/' element={<Layout />}>
        <Route index element={<IndexPage />} />
        <Route path='privilege-brand' element={<PrivilegeBrand />} />
        <Route path='integral' element={<Integral />} />
        <Route path='mine' element={<Mine />} />
      </Route>
      <Route path='/details' element={<Details />} />
      <Route path='/download' element={<Download />} />
      <Route path='/test' element={<Test />} />
      <Route path='/auth/:type' element={<Auth />} />
      <Route path='*' element={<NotFound />} />
    </Routes>
  );
};

src/main.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import ErrorBoundary from '@/components/@lgs-react/ErrorBoundary';

import '@/utils/rem';
import '@/index.css';
import { AppRouter, AppRoutes, GuardEnv } from './App';
import Schemes from 'lg-schemes';

// 1. 开发环境 & 测试环境 启用vconsole --- Tips:目前启用vconsole打包会出现异常
import vconsole from 'vconsole';
if (import.meta.env.VITE_APP_ENV !== 'pro') {
  new vconsole();
}

// 2. 渲染视图
ReactDOM.render(
  <React.StrictMode>
    <ErrorBoundary>
      <GuardEnv>
        <AppRouter>
          <AppRoutes />
        </AppRouter>
      </GuardEnv>
    </ErrorBoundary>
  </React.StrictMode>,
  document.getElementById('root')
);

- Antd-mobile

目前使用的 Antd-mobile v5(白杨) 版本。

① 安装依赖

$ yarn add antd-mobile@next

在 beta 阶段我们有可能会在后续的新版本中发布 break change,所以强烈建议使用 lockfile 锁定版本或手动锁定版本

② 引入

直接引入组件即可,antd-mobile 会自动为你加载 css 样式文件:

import { Button } from 'antd-mobile'

- 二级目录部署

①. 在 package.json 文件中配置指令时时设置 --base=/二级目录名/

②. 在各环境配置文件中根据需要设置 VITE_APP_BASE 字段值

②. 在配置路由时设置 basename 属性,如下所示:

/**
 * src/router/index.tsx
 * appRouter
 * @returns
 */
export const AppRouter: React.FC = ({ children }) => {
  return (
    <Suspense fallback={Fallback}>
      <Router basename={import.meta.env.VITE_APP_BASE}>{children}</Router>
    </Suspense>
  );
};

六、模板地址

闲暇之余,结合 Vite 封装了两个模板,大家可以参考:

提示:本文中的代码片段均摘自上述模板文件。