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

简介: 前段时间写了个Vue3的工程项目用起来还不错,借此把它移植过来React这边,给大家介绍下Vite2+React+TypeScript如何合理搭建和使用周边插件,以及让他们组合到整个工程中去。

网络异常,图片无法展示
|

本文为原创文章,引用请注明出处,欢迎大家收藏和分享💐💐

引言

Hello大家好,前段时间写了个Vue3的工程项目用起来还不错,其实老早前就想把它移植过来React这边,奈何工作比较忙一直拖到现在,才陆陆续续把杂七杂八的模块补充好。

既然迁移过来了,也借着空闲时间给大家介绍下一个 Vite2 + React + TypeScript 的项目中, 如何合理搭建和使用周边插件,以及让他们组合到整个工程中去,也欢迎大家阅览和补充更优想法。

接下来,为了让大家更好理解本项目工程化的思路,本文会按照以下关键词去逐步研读:

React

其实自react hook诞生以来,网上两把声音对其褒贬不一,和传统class component写法比较的优缺点大概就下面这些:

hooks优点

1. 更容易复用代码:每份useHook都能生成独立状态,更易于组件抽离,工程解耦等;

2. 代码量更少:不需要定义繁琐的react component模板代码,状态的读写不需要在每个生命钩子中穿插使用,使代码结构变得浅层、简单;



hooks缺点

1. 副作用的性能开销:在监控某个状态变化时用的useEffect假如使用不当,很容易造成其他状态相互依赖而产生调用链,带来额外的性能开销;另外监听的global属性「如:location等...」,还有可能会造成全局污染;

2. 异步的代码的处理:在多个状态有前后依赖时,很难处理他们的读写顺序;

本项目所有单文件组件都是React v16.8+ 的hooks写法,其考虑点主要在于本项目主要以工程框架介绍为主,hook写法能更好帮助组件的定义和抽离,呈现模块化结构,也更利于理解整个结构。

Typescript

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

Vite

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

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

  • Vite更加轻量,并且构建速度足够快
    webpack是使用nodejs去实现,而viite使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快不是一个数量级。
  • Vue官方出品,之前在vue项目实践过效果不错,另外vite也支持了react模板
  • 发展势头迅猛,未来可期

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

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

Redux Toolkit

React的状态管理库历来就是轮子重灾区,各种设计模式层出不穷,这里就不多介绍了。

项目不复杂,要求性能不高的直接用useContext、useReducer就行,简单也容易实现;假如你追求优秀的设计模式并且适配项目结构,直接基于Redux手写个轮子出来也行。

本项目选用Redux Toolkit作为项目管理,一来,它在众多产品中算是比较优秀的一个框架,使用起来也简单、结构清晰;二来,它封装了immer,写起异步逻辑挺方便的,用起来也可以应对大多数情景。

工程化搭建

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

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

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

1. 技术栈

编程: React16.8+ + Typescript

构建工具:Vite

路由 | 状态管理:react-router-dom v6 + @reduxjs/toolkit

UI Element:Ant Design Mobile

2. 工程结构

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

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

3. 工程配置

搭建Vite + React项目

# npm 6.x
npm init vite@latest my-vue-app --template react-ts
# npm 7+, 需要额外的双横线:
npm init vite@latest my-vue-app -- --template react-ts
# yarn
yarn create vite my-vue-app --template react-ts
# pnpm
pnpm create vite my-vue-app -- --template react-ts

然后按照提示操作即可!

Vite配置

/* eslint-disable no-extra-boolean-cast */
import { defineConfig, ConfigEnv } from 'vite';
import styleImport from 'vite-plugin-style-import';
import react from '@vitejs/plugin-react';
import { viteMockServe } from 'vite-plugin-mock';
import { visualizer } from 'rollup-plugin-visualizer';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig(({ command }: ConfigEnv) => {
  return {
    base: './',
    plugins: [
      react(),
      // mock
      viteMockServe({
        mockPath: 'mock', //mock文件地址
        localEnabled: !!process.env.USE_MOCK, // 开发打包开关
        prodEnabled: !!process.env.USE_CHUNK_MOCK, // 生产打包开关
        logger: false, //是否在控制台显示请求日志
        supportTs: true
      }),
      styleImport({
        libs: []
      }),
      !!process.env.REPORT
        ? visualizer({
            open: true,
            gzipSize: true,
            filename: path.resolve(__dirname, 'dist/stats.html')
          })
        : null
    ],
    resolve: {
      alias: [
        {
          find: '@',
          replacement: '/src'
        }
      ]
    },
    css: {
      // css预处理器
      preprocessorOptions: {
        less: {
          javascriptEnabled: true,
          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.tsx入口调用initialize(app)方法,initialize代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import { Toast } from 'antd-mobile';
import App from './App';
import { initialize } from '@/utils/workflow';
// 初始化总线
initialize().then(flat => {
  if (!flat) {
    Toast.show({
      icon: 'fail',
      content: '初始化失败'
    });
    return;
  }
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
});

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

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

4. React Router

因为使用的是react-router-dom v6,所以与之前的写法和hook有所区别,一个个来说。另外,v6版本还是有不少优势的,可参考官方团队解读

tsx组件

// src/App.tsx
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { DotLoading } from 'antd-mobile';
import { Provider } from 'react-redux';
import RouterComponent from '@/routes';
import Header from '@/components/header';
import store from '@/store';
import style from './app.module.less';
const App = () => {
  return (
    <Provider store={store}>
      <div className={style.appBody}>
        <React.Suspense fallback={<DotLoading />}>
          <BrowserRouter>
            <Header />
            <RouterComponent />
          </BrowserRouter>
        </React.Suspense>
      </div>
    </Provider>
  );
};
export default App;

RouterComponent组件和Header包裹在BrowserRouter中,因为涉及到整个单页都会用到路由能力。下面我们再来看看RouterComponent的实现:

// src/routes/index.tsx
import React, { FC, useEffect } from 'react';
import routes from './routesConfig';
import { Route, Routes, useNavigate, Navigate } from 'react-router-dom';
import { ErrorBlock } from 'antd-mobile';
import { IRoute } from '@/types/router';
import { isLogin } from '@/utils/userLogin';
// 路由装饰器
const RouteDecorator = (props: { route: IRoute }) => {
  const { route } = props;
  const navigate = useNavigate();
  useEffect(() => {
    // 鉴权路由守卫
    if (route.meta?.requireAuth) {
      if (!isLogin()) {
        navigate('/login', { state: { redirect: route.pathname } });
      }
    }
    // 自定义路由守卫
    route.beforeCreate && route.beforeCreate(route);
    return () => route.beforeDestroy && route.beforeDestroy(route);
  }, [route]);
  return <route.component />;
};
const RouterComponent: FC = () => (
  <Routes>
    <Route path="/" element={<Navigate to="/index" />} />
    <Route path="*" element={<ErrorBlock fullPage />} />
    {routes.map(route => (
      <Route
        key={route.pathname}
        path={route.pathname}
        element={<RouteDecorator route={route} />}
      />
    ))}
  </Routes>
);
export default RouterComponent;
  • 定义2个特殊路由:重定向和404;
  • 定义1个routesConfig配置文件,记录每个路由页面的信息,类型定义如下:
export interface IRoute extends RouteProps {
    // 路径
    pathname: string;
    // 名称
    name: string;
    // 中文描述,可用于侧栏列表
    title: string;
    // react组件函数
    component: FC;
    // 页面组件创建时执行的hook
    beforeCreate: (route: IRoute) => void;
    // 页面组件销毁时执行的hook
    beforeDestroy: (route: IRoute) => void;
    // 属性
    meta: {
      navigation: string;
      requireAuth: boolean;
    };
  }
  • 定义路由装饰器RouteDecorator:主要作用是路由守卫,另外执行每个路由页面创建时和销毁时的自定义hooks;
  • 在config中,每个组件通过react-lazily-component插件懒加载,优化加载策略;

5. 请求中心

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 { Toast } from 'antd-mobile';
import {
  IRequestParams,
  IRequestResponse,
  TBackData
} from '@/types/global/request';
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.show({
          icon: 'fail',
          content: 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.show({
            icon: 'fail',
            content: '网络连接异常,请稍后再试!'
          });
        }
        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.show({
          icon: 'fail',
          content: '请求的资源不存在'
        });
        break;
      default:
        // 错误信息判断
        message &&
          Toast.show({
            icon: 'fail',
            content: message
          });
    }
  }
}

这里面做了几件事情:

  1. 配置axios实例,在拦截器设置请求和相应拦截操作,规整服务端返回的retcodemessage
  2. 改写AxiosInstance的ts类型(由AxiosPromisePromise),矫正调用方能正确判断返回数据的类型;
  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后注入主应用中,整个项目启动时间只花了1463ms,性能相当快,这里不由感叹尤大对工程研究确实有一套😆。

构建后的资源包

网络异常,图片无法展示
|

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

Lighthouse测试

网络异常,图片无法展示
|

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

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

Performance

网络异常,图片无法展示
|

参考文章

《Vite 的好与坏》

《Vite和Webpack的核心差异》

写在最后

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

GitHub项目传送门

相关文章
|
4天前
|
前端开发 JavaScript 开发者
React和TypeScript各自以其独特的优势赢得了广大开发者的青睐
【6月更文挑战第12天】React和TypeScript是前端开发的强强联合。TypeScript提供静态类型检查和面向对象特性,增强代码健壮性和团队协作效率;React凭借组件化、高性能和丰富生态系统引领UI构建。两者结合,能精确定义React组件类型,提升代码组织和维护性,通过安装TypeScript、配置、编写及构建步骤,可在React项目中实现这一优势。这种结合为前端开发带来进步,未来应用将更加广泛。
18 1
|
6天前
|
前端开发 JavaScript 测试技术
|
10天前
|
前端开发
react框架对Excel文件进行上传和导出
react框架对Excel文件进行上传和导出
|
10天前
|
前端开发 JavaScript vr&ar
|
1月前
|
前端开发 网络架构 UED
React Next.js框架
React Next.js 是一个基于 React 的服务器渲染框架,用于构建高性能的现代Web应用。它提供内置的服务器渲染、静态导出、动态路由功能,并自动进行代码拆分。Next.js 还支持热模块替换、CSS-in-JS 和客户端路由,拥有丰富的插件生态系统,简化了开发过程,提升了应用的性能和用户体验。
|
1月前
|
JavaScript 前端开发 IDE
【JavaScript与TypeScript技术专栏】TypeScript在JavaScript库与框架开发中的作用
【4月更文挑战第30天】TypeScript,微软开发的JavaScript超集,以其强类型和面向对象特性,正成为提升Web项目质量和效率的关键工具,尤其在库和框架开发中。它通过类型系统减少运行时错误,提供内置文档,便于重构,增强IDE支持,以及支持模块化。流行框架如React、Angular已支持TypeScript,未来有望成为开发高质量库和框架的标准语言。随着社区增长,TypeScript将在Web开发领域扮演更重要角色。
|
1月前
|
JavaScript 安全 编译器
【TypeScript 技术专栏】TypeScript 与 Jest 测试框架
【4月更文挑战第30天】本文探讨了TypeScript与Jest测试框架的结合在确保代码质量和稳定性上的重要性。Jest以其易用性、内置断言库、快照测试和代码覆盖率分析等特点,为TypeScript提供全面的测试支持。两者结合能实现类型安全的测试,提高开发效率,并涵盖各种测试场景,包括异步操作。通过实际案例分析,展示了如何有效利用这两个工具提升测试质量和开发效率,为项目成功奠定基础。
|
1月前
|
JavaScript 前端开发 开发者
【TypeScript技术专栏】TypeScript与React的完美结合
【4月更文挑战第30天】React和TypeScript在前端开发中备受推崇。React以其组件化、高性能和灵活的生态系统引领UI构建,而TypeScript通过静态类型检查和面向对象特性增强了代码的健壮性和可维护性。两者结合,能提升开发效率,降低错误,使React组件结构更清晰。通过安装TypeScript,配置tsconfig.json,然后用TypeScript编写和打包代码,可实现两者的无缝集成。这种结合为前端开发带来更强的代码质量和团队协作效果,随着技术发展,其应用将更加广泛。
|
1月前
|
前端开发 JavaScript 安全
【亮剑】探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式
【4月更文挑战第30天】本文探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式。文章分为三部分:首先解释了样式传递的必要性,包括模块化、主题化和动态样式以及TypeScript集成。接着介绍了内联样式的基本用法和最佳实践,展示了一个使用内联样式自定义按钮颜色的例子。最后,讨论了使用CSS模块和TypeScript接口处理复杂样式的方案,强调了它们在组织和重用样式方面的优势。结合TypeScript,确保了样式的正确性和可维护性,为开发者提供了灵活的样式管理策略。
|
1月前
|
前端开发 JavaScript API
blog-engine-07-gatsby 建极速网站和应用程序 基于React的最佳框架,具备性能、可扩展性和安全性。
探索多种博客引擎:Jekyll, Hugo, Hexo, Pelican, Gatsby, VuePress, Nuxt.js 和 Middleman。了解Gatsby,基于React的静态站点生成器,以其特性如React基础、静态生成、广泛插件支持、简便部署和现代开发流程脱颖而出。安装Gatsby需先安装Node.js,然后通过`gatsby-cli`创建新站点。开发涉及编写页面、本地开发、生成静态文件及部署。尽管Gatsby具有现代化和高性能优势,但也存在学习曲线和配置复杂性的挑战。