【vite】构建标准化react应用

背景

之前公司项目采用的是umi脚手架一体化构建工具,得益于对webpack与各框架的集成和封装,使得快速上手的能力大大加强,但是随着项目的不断迭代与功能增加,依赖的库也是越来越多,目前最明显的感受就是每次启动与打包构建的时长,往往是好几分钟~,热更新有时也要耗费数秒,对于开发效率与体验影响很大。。。

之前尤大发布vite1.0时也了解了一点,最明显感受就是一个字“快”,不过一直没仔细研究过,只知道是基于`esbuild`和`rollup`,目前`vite2.0`已经发布,完全作为一个独立的构建工具,对`react`等其他非`vue`框架有着很好的支持。最近也算忙里偷闲,算是稍微研究了一下基本知识。本篇文章记录我以`vite`构建`react`的过程及细节,后续会继续深入研究输出`vite`相关系列文章,敬请期待

目标

我对构建项目的要求如下:

  • 支持Typescript
  • 支持ReactJSX语法
  • 支持ES6语法
  • 支持Less module
  • 支持EslintPrettierPre-commit hook
  • 支持HMR快速热更新
  • 支持Antd按需引入与主题样式覆盖
  • 支持Proxy代理、alias别名
  • 兼容传统浏览器
  • 开发启动速度要够快,以秒计算
  • 支持懒加载和chunk分割

介绍

前置条件之一

浏览器原生支持 ES 模块。

特点

  • 基于原生 ES 模块,即 <script type="module" >,做到快速加载
  • 使用 Esbuild 预构建依赖 (本地开发环境)
  • 使用 Rollup 打包代码(线上生产环境)
  • HMR 是在原生 ESM 上执行的
  • 利用对 HTTP 头信息的控制,优化缓存与重加载,高效率利用浏览器能力。
  • 开箱即用,内置多种支持,如:Typescript支持、JSX支持、CommonJSUMD兼容、css预处理器与css modules

vite对模块的分类

  • 依赖
    • 在开发时不会变动的纯 JavaScript。(如第三方依赖antdlodash等)
    • 在该场景采用具有优势的 Esbuild 处理。(大量模块的组件库、CommonJS格式的文件等)
  • 源码
    • 通常包含一些并非直接是 JavaScript 的文件,需要转换,时常会被编辑。(JSXCSSVue/React组件等)
    • 会根据路由拆分代码按需加载模块
    • Vite 以 原生 ESM 方式提供源码

概念

预构建:将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。简单来说就是尽量合并与减少请求。

例如:当我们引入的一个第三方模块依赖了大量其他模块时,在不合并请求的情况下,会请求上百次不等,造成网络拥塞影响性能,而通过预构建合并后只需要一个请求即可。(lodash-es 有超过个内置模块!当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求!通过预构建 lodash-es 成为一个模块,我们就只需要一个 HTTP 请求了!)

相较于传统的 webpack 构建工具,先打包构建所有的依赖和项目代码,然后再启动开发服务器。Vite 则利用浏览器对 ESM 的支持,先启动开发服务器,然后再根据代码执行按需加载剩下所需的对应模块。

因为缓存,在我们第二次启动时几乎可以做到秒开!非常的可怕~

官网图

官网图很清晰的描绘了区别:

Bundle based dev server
Native ESM based dev server

步骤

项目初始化

官方支持React模板预设有:reactreact-ts,因为我需要Typescript,所以直接用这个模板,省事了~

# npm 6.x
npm init @vitejs/app my-react-app --template react-ts

# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-react-app -- --template react-ts

# yarn
yarn create @vitejs/app my-react-app --template react-ts

引入react三件套

这里有兴趣的可以尝试下 pnpm 包管理工具,安装速度很快,不了解的可以查看pnpm官方文档,相较于传统的npmyarn工具都有很好的性能提升与使用体验,这里不做过多介绍,放张图大家体会下~

注:就目前我的使用情况来看大部分场景几乎都没问题的,不过还是存在一小部分问题。如:安装precommit后Git hooks不生效等。

安装速度比较

安装依赖

# pnpm
pnpm add react react-dom react-router-dom
# or npm
npm i react react-dom react-router-dom

创建页面

src目录下创建pages目录放置页面组件模块,然后我们简单写两个页面测试下:

// pages/Home/index.tsx
import React from 'react';

const Home: React.FC = () => <div> Home </div>;

export default Home;

// pages/About/index.tsx
import React from 'react';

const About: React.FC = () => <div> About </div>;

export default About;

修改文件App.tsx

// App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Home from './pages/Home'
import About from './pages/About'

const App = () => {
  return (
    <Suspense fallback={<span>loading</span>}>
      <Router>
        <Switch>
          <Route key="/home" path="/home" component={Home}></Route>
          <Route key="/about" path="/about" component={About}></Route>
        </Switch>
      </Router>
    </Suspense>
  );
};

export default App;
vite-react-app-1
vite-react-app-2

配置路由/界面

新建layouts组件,主要用于区别渲染登录注册页面布局界面:

layouts/BasicLayout.tsxlayouts/UserLayout.tsx

这里就不一一做展示了,详细代码见仓库,地址贴在下面了。

新建路由配置文件router/index.ts:

import React from 'react';

const Page404 = React.lazy(() => import('../pages/404'));
const Home = React.lazy(() => import('../pages/Home'));
const Login = React.lazy(() => import('../pages/User/Login'));
const Register = React.lazy(() => import('../pages/User/Register'));

const routes: IRoute[] = [
  {
    path: '/user',
    component: React.lazy(() => import('../layouts/UserLayout')),
    meta: {
      title: '用户路由',
    },
    redirect: '/user/login',
    children: [],
  },
  {
    path: '/',
    component: React.lazy(() => import('../layouts/BasicLayout')),
    meta: {
      title: '系统路由',
    },
    redirect: '/home',
    children: [
      {
        path: '/home',
        meta: {
          title: '首页',
          icon: 'home',
        },
        component: <Home />,
      },
      {
        path: '/about',
        meta: {
          title: '关于',
          icon: 'about',
        },
        component: <About />,
      },
    ],
  },
]

export default routes;

创建store状态管理文件

react hooks诞生后,大部分场景使用hooksprops进行状态管理基本可以满足多数需求,少部分全局应用信息与用户信息等需要全局状态管理的,这里我觉得也不需要完整引入一个reduxmobx等这种库。当然还要结合具体场景和公司技术栈确定,较大型和复杂的项目等视情况而定~

我这里使用了zustand做了简单配置,使用起来也是比较的简单,详情参见官方文档

// 创建store
import create from 'zustand'

const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 })
}))
// 组件绑定
function BearCounter() {
  const bears = useStore(state => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

引入Antd组件库并配置按需加载

这里就不废话了,直接展示如何在vite中配置antd的按需加载,首先我们安装一个插件:

pnpm add vite-plugin-imp -D

然后在vite.config.ts文件的plugins中添加配置vitePluginImp

这里我们顺势再引入一个less-vars-to-js包,less-vars-to-js可以将less文件转化为json键值对的形式,当然你也可以直接在modifyVars属性后写json键值对。这样做的好处是可以把全局配置统一放到config文件进行管理,方便维护。

自定义覆盖主题色

config/variables.less // @primary-color: '#ff7875';

import reactRefresh from '@vitejs/plugin-react-refresh';
import lessToJS from 'less-vars-to-js';
import path from 'path';
import { defineConfig } from 'vite';
// vite-plugin-imp 该插件按需加载存在部分样式丢失的情况
// import vitePluginImp from 'vite-plugin-imp';
// 由于 vite 本身已按需导入了组件库,因此仅样式不是按需导入的,因此只需按需导入样式即可。
import styleImport from 'vite-plugin-style-import';

const themeVariables = lessToJS(
  fs.readFileSync(path.resolve(__dirname, './config/variables.less'), 'utf8'),
);

export default defineConfig({
  base,
  plugins: [
    reactRefresh(),
    // 配置按需引入antd
    // vitePluginImp({
    //   libList: [
    //     {
    //       libName: 'antd',
    //       style: (name) => `antd/es/${name}/style/index.less`,
    //     },
    //   ],
    // }),
    styleImport({
      libs: [
        {
          libraryName: 'antd',
          esModule: true,
          resolveStyle: (name) => {
            return `antd/es/${name}/style/index`;
          },
        },
      ],
    }),
  ],
  css: {
    preprocessorOptions: {
      less: {
        // 支持内联 JavaScript,支持 less 内联 JS
        javascriptEnabled: true,
        // 重写 less 变量,定制样式
        modifyVars: themeVariables,
      },
    },
  }
})
vite-img-4

环境变量

方案一

通过 --mode 注入配置参数以匹配测试/开发环境等。

我们修改下package.json文件:

scripts: {
  "build:beta": "vite build --mode beta",
  "build:release": "vite build --mode release",
  "build:legacy ": "vite build --mode legacy ",
}

node环境下直接process.argv即可获取到,我们可以在vite.config.ts中打印信息查看

// vite.config.ts
import {defineConfig} from 'vite'

const env = process.argv[process.argv.length - 1];
console.log('env:', env);

export default defineConfig({})

方案二

使用函数式写法配置动态获取环境变量等参数

首先在根目录下创建.env文件

# port
VITE_PORT = 3100

# HTTP API
VITE_HTTP_API = http://127.0.0.1:8000

# title
VITE_APP_TITLE = Vite React App

然后我们调整下 vite.config.ts 文件

// 函数式配置
import { loadEnv } from 'vite';
import type { ConfigEnv, UserConfig } from 'vite';

export default ({ command, mode }: ConfigEnv): UserConfig => {
  const root = process.cwd();
  const env = loadEnv(mode, root);

  console.log('env', env);
  console.log('command', command);
  console.log('mode', mode);
}

组件内可通过import.meta.env获取,我们可以在Home/index.tsx中打印信息查看

// Home/index.tsx
import React from 'react'
import { Button } from 'antd'

const Home: React.FC = () => {
  console.log('import.meta.env', import.meta.env)
  return <div>
    <Button type='primary'>Home</Button>
  </div>
}
export default Home

alias 别名设置

export default defineConfig({
    ...
  resolve: {
    alias: [
      { find: /^~/, replacement: path.resolve(__dirname, './') },
      { find: '@', replacement: path.resolve(__dirname, 'src') },
    ],
    // alias: {
    //   '~': path.resolve(__dirname, './'), // 根路径
    //   '@': path.resolve(__dirname, 'src') // src 路径
    // }
  }
  ...
})

proxy代理配置

export default defineConfig({
    ...
    server: {
    port: 8080, // 开发环境启动的端口
    proxy: {
      '/api': {
        // 当遇到 /api 路径时,将其转换成 target 的值
        target: 'http://127.0.0.1:8080/api/',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''), // 将 /api 重写为空
      },
    }
  }
})

兼容传统浏览器

vite默认只支持现代浏览器,即ES module,对于IE等老版本浏览器,可使用@vitejs/plugin-legacy插件在build时进行polyfill

yarn add @vitejs/plugin-legacy -D

// vite.config.js
import legacy from '@vitejs/plugin-legacy'

export default {
  plugins: [
    legacy({
      targets: ['ie >= 11'],
      additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
  ]
}

Eslint、Prettier、Stylelint 代码检查与格式化

使用eslint这种代码检查工具是为了更好的规范代码和写法,列如:禁用var,建议使用constlet,以及配合TS使用时对各种类型规范等,对于代码优化与后期维护都很方便。

而格式化对于多人协同开发时,统一代码风格很有用。

VSCode 集成

插件规则遵循就近原则,会优先启用本项目下的配置文件,当前配置会覆盖全局配置,如果没有就采用全局配置。

source.fixAll.eslint 开启后可使编辑器按照eslint规则 auto fix

vscode 中安装 EslintPrettier 插件,并开启功能,推荐在全局配置(或当前工作目录下)中添加:

  // file: vscode setting.json
  // onSave
  "editor.formatOnSave": true, //每次保存的时候自动格式化
  // eslint
  "eslint.alwaysShowStatus": true,  // 总是在 VSCode 显示 ESLint 的状态
  "eslint.quiet": true,             // 忽略 warning 的错误
  "editor.codeActionsOnSave": {     // 保存时使用 ESLint 修复可修复错误
      "source.fixAll": true,
      "source.fixAll.eslint": true
  },
  // prettier
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[less]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[yaml]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },

为了方便配置,可以使用 VsCode插件 setting sync(该插件通过GitHub Gist ID 绑定,实现云同步上传下载),通过GitHub Gist ID云同步配置和插件。

git commit 集成

git commit原理:

git在执行的过程会提供相关钩子函数,如commitpush时,在项目目录下的.git目录下的hooks文件夹下存在相关git生命周期的钩子函数配置,我们可以手动自己配置,只需删除后缀.sample即可,不过因为这是本地文件,对于团队协同开发同步配置不太友好,所以还是建议采用第三方库统一管理。

hooks文件夹./.git/hooks/

vite-react-app-2
  1. 我们先在项目根目录下创建.husky文件夹

  2. 然后添加文件pre-commit

    #!/bin/sh
     . "$(dirname "$0")/_/husky.sh"
     npx --no-install lint-staged
    
  3. package.json 中添加如下配置:

    script 配置 postinstall是为了确保在安装依赖完成后,npm可以执行postinstall钩子,使husky安装配置文件到.husky文件下,npm在安装执行的过程提供了一些生命周期钩子,postinstall、prepare等。

    {
      "script": {
        "postinstall": "husky install",
        "lint:fix": "eslint --cache --ext .js,.jsx,.ts,.tsx --no-error-on-unmatched-pattern --quiet --fix ./src",
        "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
      },
      "lint-staged": {
        "**/*.{js,jsx,tsx,ts,json}": [
          "npm run lint:fix",
          "git add --force"
        ],
        "**/*.{less}": [
          "npm run lint:style",
          "git add --force"
        ]
      },
    }
    
  4. 再安装 huskylint-staged 依赖库,

在提交代码时,lint git 暂存区的代码,若 lint 不通过则中断提交,保证问题代码不进入代码仓库。

目录结构

├── dist                                // 默认的 build 输出目录
├── config                              // 全局配置文件
└── src                                 // 源码目录
    ├── assets                          // 公共的文件(如image、css、font等)
    ├── components                      // 项目组件
    ├── constants                       // 常量/接口地址等
    ├── layout                          // 全局布局
    ├── routes                          // 路由
    ├── store                           // 状态管理器
    ├── utils                           // 工具库
    ├── pages                           // 页面模块
        ├── Home                        // Home模块,建议组件统一大写开头
        ├── ...
    ├── App.tsx                         // react顶层文件
    ├── main.ts                         // 项目入口文件
    ├── typing.d.ts                     // ts类型文件
├── .editorconfig                       // IDE格式规范
├── .env                                // 环境变量
├── .eslintignore                       // eslint忽略
├── .eslintrc                           // eslint配置文件
├── .gitignore                          // git忽略
├── .npmrc                              // npm配置文件
├── .prettierignore                     // prettierc忽略
├── .prettierrc                         // prettierc配置文件
├── .stylelintignore                    // stylelint忽略
├── .stylelintrc                        // stylelint配置文件
├── index.html                          // 项目入口文件
├── LICENSE.md                          // LICENSE
├── package.json                        // package
├── pnpm-lock.yaml                      // pnpm-lock
├── postcss.config.js                   // postcss
├── README.md                           // README
├── tsconfig.json                       // typescript配置文件
└── vite.config.ts                      // vite

项目地址

项目Github地址

【vite】构建标准化react应用 | 小帅の技术博客 (ssscode.com)

参考

vitejs

Vite 2.0 + React + Ant Design 4.0 搭建开发环境

zustand

如何为你的 Vue 项目添加配置 Stylelint

从零配置 Eslint + Prettier + husky + lint-staged 构建前端代码工作流

ESLint 使用指南

深入浅出eslint——关于我学习eslint的心得

在 pre-commit 的钩子中运行 npm script

@umijs/fabric

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容