Vite2+Vue3+TypeScript:搭建企业级轻量框架实践

28,876 阅读10分钟

引言

随着Vue3为广大开发者所接受和自身生态逐渐完善,更多同学往vue3的工程化方向完善,本文恰好给大家介绍下如何更好使用vue3及其周边插件,以及让他们组合到整个工程中去。

另外,Vue3支持Typescript语法编程也是其中一大亮点,为了探索新技术的工程化搭建,本文会把Typescript、vite、pinia等官方周边整合到工程里面。

接下来,为了让大家更好理解本项目工程化的思路,本文会按照以下关键词去逐步研读(看项目代码可跳过前4步)

script setup

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。

搞个简单demo对比script-setupscript区别:

// 单文件组件script-setup编写模式
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>
// 普通script编写模式
<script>
import { ref } from 'vue'

export default {
    setup(props) {
      const count = ref(0)

      // 暴露给 template
      return {
        count
      }
    }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

上述例子可以看出,script-setup弱化了vue模板式编程体验,也使得代码更简洁,开发者只需要引入正确的hooks后,把逻辑写在script内就足以。

本项目所有组件都采用这种开发模式,相比于普通的 <script> 语法,vue官方肯定了它的优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和抛出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)

最后笔者认为,从某方面讲Vue3是一次vue-hooks的革命,通过compositionApi的引用使组件写法更轻便简洁;而script-setup正好使得这种体验更加彻底,使单文件组件写法更接近函数式编程,在react和vue之间无缝切换。


Typescript

近几年前端对 TypeScript的呼声越来越高,Typescript也成为了前端必备的技能。TypeScript 是 JS类型的超集,并支持了泛型、类型、命名空间、枚举等特性,弥补了 JS 在大型应用开发中的不足。

在vue2版本时候,假如你要使用typescript,需要借用vue-class-component 、vue-property-decorator 等装饰器加以判断,而且要改成特定的代码结构让vue去识别,并不方便。

到了Vue3的时代,框架已经完美兼容了typescript,而且配置也简单,对代码入侵也小,给开发者带来了很大便利。


Vite

Vite是一种新型前端构建工具,能够显著提升前端开发体验。比起webpack,vite还是有它很独特的优势,这里推荐一篇文章《Vite 的好与坏》给大家参考下。

项目为什么选vite代替webpack,结合社区和个人考虑,有几点:(具体就不展开,推文已经分析的很细致了)

  • Vite更加轻量,并且构建速度足够快
    webpack是使用nodejs去实现,而viite使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快不是一个数量级。
  • Vue官方出品,对vue项目兼容性不错
  • 发展势头迅猛,未来可期

当然事物都有两面性的,至目前为止,vite也有不少缺陷,例如:生态没有webpack成熟、生产环境下隐藏的不稳定因素等都是它如今要面临的问题。

但是,心怀梦想敢于向前,没有新势力的诞生,哪里来的技术发展?相比之下,vite更像一个青年,并逐步前行。


Pinia

Pinia 是 Vue.js 的轻量级状态管理库,最近很受欢迎。它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。

比起Vuex,Pinia具备以下优点:

  • 完整的 TypeScript 支持:与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
  • 极其轻巧(体积约 1KB)
  • store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见
  • 支持多个Store
  • 支持 Vue devtools、SSR 和 webpack 代码拆分 关于Pinia的进阶使用,笔者额外开了一篇专文介绍,有兴趣可以阅览:《Pinia进阶:优雅的setup(函数式)写法+封装到你的企业项目》

工程化搭建

言归正传,我们通过以上技术,整合到一个项目中去。一般用于企业级生产的项目,要具备以下能力:

  • 容错性、可拓展性强
  • 组件高内聚,减少模块之间耦合度
  • 清晰的项目执行总线,方便增加插槽逻辑
  • 高度抽象的全局方法
  • 资源压缩+性能优化等

对照这些指标,我们来逐步搭建一个初步的工程框架。

备注:关于vue3语法、pinia使用等编程知识不会在这里细述了,大家可以到网上检索或者直接在项目里面寻找。

1. 技术栈

编程: Vue3.x + Typescript
构建工具:Vite
路由 | 状态管理:vue-router + Pinia
UI Element:nutui

2. 工程结构

.
├── README.md
├── index.html           项目入口
├── mock                 mock目录
├── package.json
├── public
├── src
│   ├── App.vue          主应用
│   ├── api              请求中心
│   ├── assets           资源目录(图片、less、css等)
│   ├── components       项目组件
│   ├── constants        常量
│   ├── env.d.ts         全局声明
│   ├── main.ts          主入口
│   ├── pages            页面目录
│   ├── router           路由配置
│   ├── types            ts类型定义
│   ├── store            pinia状态管理
│   └── utils            基础工具包
├── test                 测试用例
├── tsconfig.json        ts配置
├── .eslintrc.js         eslint配置
├── .prettierrc.json     prettier配置
├── .gitignore           git忽略配置
└── vite.config.ts       vite配置

其中,src/utils里面放置全局方法,供整个工程范围的文件调用,当然工程初始化的事件总线也放在这里「下面会细述」。src/typessrc/constants分别存放项目的类型定义和常量,以页面结构来划分目录。

3. 工程配置

搭建Vite + Vue项目

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

# npm 7+, 需要额外的双横线:
npm init 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

然后按照提示操作即可!

Vite配置

import { defineConfig, ConfigEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import styleImport from 'vite-plugin-style-import';

import { viteMockServe } from 'vite-plugin-mock';

const path = require('path')

// https://vitejs.dev/config/
export default defineConfig(({ command }: ConfigEnv) => {
  return {
    base: './',
    plugins: [
      vue(),
      // mock
      viteMockServe({
        mockPath: 'mock', //mock文件地址
        localEnabled: !!process.env.USE_MOCK, // 开发打包开关
        prodEnabled: !!process.env.USE_CHUNK_MOCK, // 生产打包开关
        logger: false, //是否在控制台显示请求日志
        supportTs: true
      }),
      styleImport({
        libs: [
          // nutui按需加载配置,详见  https://nutui.jd.com/#/start
          {
            libraryName: '@nutui/nutui',
            libraryNameChangeCase: 'pascalCase',
            resolveStyle: name => {
              return `@nutui/nutui/dist/packages/${name}/index.scss`;
            }
          }
        ]
      })
    ],
    resolve: {
      alias: [
        {
          find: '@',
          replacement: '/src'
        }
      ]
    },
    css: {
      // css预处理器
      preprocessorOptions: {
        scss: {
          // 配置 nutui 全局 scss 变量
          additionalData: `@import "@nutui/nutui/dist/styles/variables.scss";`
        },
        less: {
          charset: false,
          additionalData: '@import "./src/assets/less/common.less";'
        }
      }
    },
    build: {
      terserOptions: {
        compress: {
          drop_console: true
        }
      },
      outDir: 'dist', //指定输出路径
      assetsDir: 'assets' //指定生成静态资源的存放路径
    }
  };
});

工程添加了mock模式供开发者在没有服务端情况下模拟数据请求,通过vite-plugin-mock插件全局配置到vite中,mock接口返回在mock目录下增加,mock模式启动命令:npm run dev:mock

FYI:vite-plugin-mock插件在vite脚手架下提供devtools network拦截能力,假如你要实现更多mock场景,请使用mockjs「项目已安装,直接可用」

编码规范

tsconfig
eslint
prettier

事件总线

为了规范项目的初始化流程,方便在流程中插入自定义逻辑,在main.ts入口调用initialize(app)方法,initialize代码如下:

/**
 * 项目初始化总线
 */

// 初始化nutui样式
import '@nutui/nutui/dist/style.css';

import { initRem } from '@/utils/calcRem';
import nutUiList from '@/utils/nutuiImport';
import router from '@/router';
import { createPinia } from 'pinia';
import { registerStore } from '@/store';

export const initialize = async (app: any) => {
  // 初始化rem
  initRem(window, document.documentElement);
  window.calcRem(1080);
  console.trace('rem初始化完成...');

  // 按需加载nutui组件
  Object.values(nutUiList).forEach(co => {
    app.use(co);
  });
  console.trace('nutui组件加载完成...');

  // 挂载路由
  app.use(router);
  console.trace('router已挂载...');

  // 注册pinia状态管理库
  app.use(createPinia());
  registerStore();
  console.trace('pinia状态库已注册...');
};

在方法里面,分别完成页面的rem自适应布局初始化、UI组件按需加载、路由、状态库初始化等操作,另外initialize支持异步逻辑注入,需要的自行添加并使用Promise包裹返回即可。

ps:initialize方法执行时机在主App挂载之前,请勿将dom操作逻辑放置此处

4. 请求中心

src/api包含每个页面的异步请求,也是通过页面结构来划分目录。src/api/index.ts是其入口文件,用来聚合每个请求模块,代码如下:

import { Request } from './request';
import box from './box';
import user from './user';

// 初始化axios
Request.init();

export default {
  box,
  user
  // ...其他请求模块
};

这里的Request是请求中心的类对象,返回1个axios实例,src/api/request.ts代码如下:

import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import {
  IRequestParams,
  IRequestResponse,
  TBackData
} from '@/types/global/request';
import { Toast } from '@nutui/nutui';

interface MyAxiosInstance extends AxiosInstance {
  (config: AxiosRequestConfig): Promise<any>;
  (url: string, config?: AxiosRequestConfig): Promise<any>;
}

export class Request {
  public static axiosInstance: MyAxiosInstance;

  public static init() {
    // 创建axios实例
    this.axiosInstance = axios.create({
      baseURL: '/api',
      timeout: 10000
    });
    // 初始化拦截器
    this.initInterceptors();
  }

  // 初始化拦截器
  public static initInterceptors() {
    // 设置post请求头
    this.axiosInstance.defaults.headers.post['Content-Type'] =
      'application/x-www-form-urlencoded';
    /**
     * 请求拦截器
     * 每次请求前,如果存在token则在请求头中携带token
     */
    this.axiosInstance.interceptors.request.use(
      (config: IRequestParams) => {
        const token = localStorage.getItem('ACCESS_TOKEN');
        if (token) {
          config.headers.Authorization = 'Bearer ' + token;
        }
        return config;
      },
      (error: any) => {
        Toast.fail(error);
      }
    );

    // 响应拦截器
    this.axiosInstance.interceptors.response.use(
      // 请求成功
      (response: IRequestResponse): TBackData => {
        const {
          data: { code, message, data }
        } = response;
        if (response.status !== 200 || code !== 0) {
          Request.errorHandle(response, message);
        }
        return data;
      },
      // 请求失败
      (error: AxiosError): Promise<any> => {
        const { response } = error;
        if (response) {
          // 请求已发出,但是不在2xx的范围
          Request.errorHandle(response);
        } else {
          Toast.fail('网络连接异常,请稍后再试!');
        }
        return Promise.reject(response?.data);
      }
    );
  }

  /**
   * http握手错误
   * @param res 响应回调,根据不同响应进行不同操作
   * @param message
   */
  private static errorHandle(res: IRequestResponse, message?: string) {
    // 状态码判断
    switch (res.status) {
      case 401:
        break;
      case 403:
        break;
      case 404:
        Toast.fail('请求的资源不存在');
        break;
      default:
        // 错误信息判断
        message && Toast.fail(message);
    }
  }
}

这里面做了几件事情:

  1. 配置axios实例,在拦截器设置请求和相应拦截操作,规整服务端返回的retcodemessage
  2. 改写AxiosInstance的ts类型(由AxiosPromisePromise<any>),矫正调用方能正确判断返回数据的类型;
  3. 设置1个初始化函数init(),生成一个axios的实例供项目调用;
  4. 配置errorHandle句柄,处理错误;

当然在第2步,你可以添加额外的请求拦截,例如RSA加密,本地缓存策略等,当逻辑过多时,建议通过函数引入。

至此,我们就能愉快使用axios去请求数据了。

// api模块→请求中心
import { Request } from './request';

userInfo: (options?: IRequestParams): Promise<TUser> =>
  Request.axiosInstance({
    url: '/userInfo',
    method: 'post',
    desc: '获取用户信息',
    isJSON: true,
    ...options
  })
  
  
  
// 业务模块→api模块
import request from '@/api/index';

request.user
  .userInfo({
    data: {
      token
    }
  })
  .then(res => {
    // do something...
  });

5. SSR

待补充...


性能测试

开发环境启动

图中可以看出,Vite在冷启动时对6项依赖进行Pre-Bundling后注入主应用中,整个项目启动时间只花了738ms,性能相当快,这里不由感叹尤大对工程研究确实有一套😆。

另外,本项目也使用vite-plugin-style-import插件对nutui视图框架的样式按需引入,在资源节省也起到正向作用。

构建后的资源包

分包策略是依据路由页面来切割,对js和css单独分离。

Lighthouse测试

以上为本地测试,首屏大约1000ms~1500ms,压力主要来源vendor.js的加载以及首屏图片资源拉取(首屏图片资源来源于网络)。其实通过模块分割加载后,首页的js包通过gzip压缩到4.3kb。
当然真实场景是,项目部署上云服务器后肯定达不到本地资源加载速度,但可以通过CDN来加速优化,其效果也比较显著。

Performance



参考文章

《组合式API》
《Vite 的好与坏》
《Vite和Webpack的核心差异》


写在最后

感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹

GitHub项目传送门