美文网首页
Vite2+Vue3+TypeScript:搭建企业级轻量框架实

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

作者: ZQ蓝仔 | 来源:发表于2022-01-14 10:35 被阅读0次
    image.png

    引言

    随着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-componentvue-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 代码拆分

    工程化搭建

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

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

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

    备注:关于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

    待补充...

    性能测试

    开发环境启动

    image.png

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

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

    构建后的资源包

    image.png

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

    Lighthouse测试

    image.png

    以上为本地测试,首屏大约1000ms~1500ms,压力主要来源vendor.js的加载以及首屏图片资源拉取(首屏图片资源来源于网络)。其实通过模块分割加载后,首页的js包通过gzip压缩到4.3kb。

    当然真实场景是,项目部署上云服务器后肯定达不到本地资源加载速度,但可以通过CDN来加速优化,其效果也比较显著。

    Performance

    image.png



    参考文章

    《组合式API》

    《Vite 的好与坏》

    《Vite和Webpack的核心差异》

    写在最后

    感谢大家阅览并欢迎纠错。

    GitHub项目传送门

    相关文章

      网友评论

          本文标题:Vite2+Vue3+TypeScript:搭建企业级轻量框架实

          本文链接:https://www.haomeiwen.com/subject/ltracrtx.html