Vite + React + Typescript 构建实战

程序员成长指北

共 18709字,需浏览 38分钟

 ·

2021-06-18 07:09











导语


最近前端大火的 Vite 2.0 版本终于出来了,在这里分享一下使用 vite 构建一个前端单页应用以及踩过的坑,希望能带给大家一些收获。


文章首发于个人博客:heavenru.com

该文章主要面向对 Vite 感兴趣,或者做前端项目架构的同学

源码地址:fe-project-base

https://github.com/lichenbuliren/fe-project-base

通过这篇文章,你能了解到以下几点:

  • vscode 编辑器配置

  • git pre-commit 如何配置

  • ESLint + Pritter 配置

  • 标准前端单页应用目录规划

  • 从 0 到 1 学习 vite 构建优化

  • mobx/6.x + react + TypeScript 最佳实践

想快速了解 Vite 配置构建的,可以直接跳到 这里




初始化项目

这里我们项目名是 fe-project-base 这里我们采用的 vite 2.0 来初始化我们的项目

  1. npm init @vitejs/app fe-project-base --template react-ts

这个时候,会出现命令行提示,咱们按照自己想要的模板,选择对应初始化类型就 OK 了




安装项目依赖

首先,我们需要安装依赖,要打造一个基本的前端单页应用模板,咱们需要安装以下依赖:

  1. react & react-dom:基础核心

  2. react-router:路由配置

  3. @loadable/component:动态路由加载

  4. classnames:更好的 className 写法

  5. react-router-config:更好的 react-router 路由配置包

  6. mobx-react & mobx-persist:mobx 状态管理

  7. eslint & lint-staged & husky & prettier:代码校验配置

  8. eslint-config-alloy:ESLint 配置插件

dependencies:

  1. npm install --save react react-dom react-router @loadable/component classnames react-router-config mobx-react mobx-persist

devDependencies:

  1. npm install --save-dev eslint lint-staged husky@4.3.8 prettier





pre-commit 配置

在安装完上面的依赖之后,通过 cat.git/hooks/pre-commit 来判断 husky 是否正常安装,如果不存在该文件,则说明安装失败,需要重新安装试试

这里的 husky 使用 4.x 版本,5.x 版本已经不是免费协议了 测试发现 node/14.15.1 版本会导致 husky 自动创建 .git/hooks/pre-commit 配置失败,升级 node/14.16.0 修复该问题

在完成了以上安装配置之后,我们还需要对 package.json 添加相关配置

  1. {

  2.  "husky": {

  3.    "hooks": {

  4.      "pre-commit": "lint-staged"

  5.    }

  6.  },

  7.  "lint-staged": {

  8.    "src/**/*.{ts,tsx}": [

  9.      "eslint --cache --fix",

  10.      "git add"

  11.    ],

  12.    "src/**/*.{js,jsx}": [

  13.      "eslint --cache --fix",

  14.      "git add"

  15.    ]

  16.  },

  17. }

到这里,我们的整个项目就具备了针对提交的文件做 ESLint 校验并修复格式化的能力了

ESLintError↑




编辑器配置

工欲善其事必先利其器,我们首要解决的是在团队内部编辑器协作问题,这个时候,就需要开发者的编辑器统一安装 EditorConfig 插件(这里以 vscode 插件为例)

首先,我们在项目根目录新建一个配置文件:.editorconfig 参考配置:

  1. root = true

  2. [*]

  3. indent_style = space

  4. indent_size = 2

  5. end_of_line = lf

  6. charset = utf-8

  7. trim_trailing_whitespace = true

  8. insert_final_newline = true

配置自动格式化与代码校验 在 vscode 编辑器中,Mac 快捷键 command+, 来快速打开配置项,切换到 workspace 模块,并点击右上角的 open settings json 按钮,配置如下信息:

  1. {

  2.  "editor.formatOnSave": true,

  3.  "editor.codeActionsOnSave": {

  4.    "source.fixAll.tslint": true

  5.  },

  6.  "editor.defaultFormatter": "esbenp.prettier-vscode",

  7.  "[javascript]": {

  8.    "editor.formatOnSave": true,

  9.    "editor.defaultFormatter": "esbenp.prettier-vscode"

  10.  },

  11.  "[typescript]": {

  12.    "editor.defaultFormatter": "esbenp.prettier-vscode"

  13.  },

  14.  "typescript.tsdk": "node_modules/typescript/lib",

  15.  "[typescriptreact]": {

  16.    "editor.defaultFormatter": "esbenp.prettier-vscode"

  17.  }

  18. }

这个时候,咱们的编辑器已经具备了保存并自动格式化的功能了




ESLint + Prettier

关于 ESLint 与 Prettier 的关系,可以移步这里:彻底搞懂 ESLint 和 Prettier

1、.eslintignore配置 ESLint 忽略文件

2、.eslintrcESLint 编码规则配置,这里推荐使用业界统一标准,这里我推荐 AlloyTeam 的 eslint-config-alloy,按照文档安装对应的 ESLint 配置:

npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy

3、.prettierignore配置 Prettier 忽略文件

4、.prettierrc格式化自定义配置

{  "singleQuote": true,  "tabWidth": 2,  "bracketSpacing": true,  "trailingComma": "none",  "printWidth": 100,  "semi": false,  "overrides": [    {      "files": ".prettierrc",      "options": { "parser": "typescript" }    }  ]}


选择 eslint-config-alloy 的几大理由如下:

  1. 更清晰的 ESLint 提示:比如特殊字符需要转义的提示等等

    error `'` can be escaped with `'``‘``'``’`  react/no-unescaped-entities
  2. 更加严格的 ESLint 配置提示:比如会提示 ESLint 没有配置指明 React 的 version 就会告警

    Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration

    这里我们补上对 react 版本的配置

    // .eslintrc{  "settings": {    "react": {      "version": "detect" // 表示探测当前 node_modules 安装的 react 版本    }  }}



整体目录规划

一个基本的前端单页应用,需要的大致的目录架构如下:

这里以 src 下面的目录划分为例

  1. .

  2. ├── app.tsx

  3. ├── assets // 静态资源,会被打包优化

  4.   ├── favicon.svg

  5.   └── logo.svg

  6. ├── common // 公共配置,比如统一请求封装,session 封装

  7.   ├── http-client

  8.   └── session

  9. ├── components // 全局组件,分业务组件或 UI 组件

  10.   ├── Toast

  11. ├── config // 配置文件目录

  12.   ├── index.ts

  13. ├── hooks // 自定义 hook

  14.   └── index.ts

  15. ├── layouts // 模板,不同的路由,可以配置不同的模板

  16.   └── index.tsx

  17. ├── lib // 通常这里防止第三方库,比如 jweixin.js、jsBridge.js

  18.   ├── README.md

  19.   ├── jsBridge.js

  20.   └── jweixin.js

  21. ├── pages // 页面存放位置

  22.   ├── components // 就近原则页面级别的组件

  23.   ├── home

  24. ├── routes // 路由配置

  25.   └── index.ts

  26. ├── store // 全局状态管理

  27.   ├── common.ts

  28.   ├── index.ts

  29.   └── session.ts

  30. ├── styles // 全局样式

  31.   ├── global.less

  32.   └── reset.less

  33. └── utils // 工具方法

  34.  └── index.ts

OK,到这里,我们规划好了一个大致的前端项目目录结构,接下来我们要配置一下别名,来优化代码中的,比如:importxxxfrom'@/utils' 路径体验

通常这里还会有一个 public 目录与 src 目录同级,该目录下的文件会直接拷贝到构建目录

别名配置

别名的配置,我们需要关注的是两个地方:vite.config.ts & tsconfig.json

其中 vite.config.ts 用来编译识别用的;tsconfig.json 是用来给 Typescript 识别用的;

这里建议采用的是 @/ 开头,为什么不用 @ 开头,这是为了避免跟业界某些 npm 包名冲突(例如 @vitejs)

  • vite.config.ts

  1. // vite.config.ts

  2. {

  3.  resolve: {

  4.    alias: {

  5.      '@/': path.resolve(__dirname, './src'),

  6.      '@/config': path.resolve(__dirname, './src/config'),

  7.      '@/components': path.resolve(__dirname, './src/components'),

  8.      '@/styles': path.resolve(__dirname, './src/styles'),

  9.      '@/utils': path.resolve(__dirname, './src/utils'),

  10.      '@/common': path.resolve(__dirname, './src/common'),

  11.      '@/assets': path.resolve(__dirname, './src/assets'),

  12.      '@/pages': path.resolve(__dirname, './src/pages'),

  13.      '@/routes': path.resolve(__dirname, './src/routes'),

  14.      '@/layouts': path.resolve(__dirname, './src/layouts'),

  15.      '@/hooks': path.resolve(__dirname, './src/hooks'),

  16.      '@/store': path.resolve(__dirname, './src/store')

  17.    }

  18.  },

  19. }

  • tsconfig.json

  1. {

  2.  "compilerOptions": {

  3.    "paths": {

  4.      "@/*": ["./src/*"],

  5.      "@/components/*": ["./src/components/*"],

  6.      "@/styles/*": ["./src/styles/*"],

  7.      "@/config/*": ["./src/config/*"],

  8.      "@/utils/*": ["./src/utils/*"],

  9.      "@/common/*": ["./src/common/*"],

  10.      "@/assets/*": ["./src/assets/*"],

  11.      "@/pages/*": ["./src/pages/*"],

  12.      "@/routes/*": ["./src/routes/*"],

  13.      "@/hooks/*": ["./src/hooks/*"],

  14.      "@/store/*": ["./src/store/*"]

  15.    },

  16.    "typeRoots": ["./typings/"]

  17.  },

  18.  "include": ["./src", "./typings", "./vite.config.ts"],

  19.  "exclude": ["node_modules"]

  20. }





从 0 到 1 Vite 构建配置

截止作者写该篇文章时, vite 版本为 vite/2.1.2,以下所有配置仅针对该版本负责

配置文件

默认的 vite 初始化项目,是不会给我们创建 .env.env.production.env.devlopment 三个配置文件的,然后官方模板默认提供的 package.json 文件中,三个 script 分别会要用到这几个文件,所以需要我们手动先创建,这里提供官方文档:.env 配置

  1. # package.json

  2. {

  3.  "scripts": {

  4.    "dev": "vite", // 等于 vite -m development,此时 command='serve',mode='development'

  5.    "build": "tsc && vite build", // 等于 vite -m production,此时 command='build', mode='production'

  6.    "serve": "vite preview",

  7.    "start:qa": "vite -m qa" // 自定义命令,会寻找 .env.qa 的配置文件;此时 command='serve',mode='qa'

  8.  }

  9. }

同时这里的命令,对应的配置文件:mode 区分

  1. import { ConfigEnv } from 'vite'

  2. export default ({ command, mode }: ConfigEnv) => {

  3.  // 这里的 command 默认 === 'serve'

  4.  // 当执行 vite build 时,command === 'build'

  5.  // 所以这里可以根据 command 与 mode 做条件判断来导出对应环境的配置

  6. }

具体配置文件参考:fe-project-vite/vite.config.ts

路由规划

首先,一个项目最重要的部分,就是路由配置;那么我们需要一个配置文件作为入口来配置所有的页面路由,这里以 react-router 为例:

路由配置文件配置

src/routes/index.ts,这里我们引入的了 @loadable/component 库来做路由动态加载,vite 默认支持动态加载特性,以此提高程序打包效率

  1. import loadable from '@loadable/component'

  2. import Layout, { H5Layout } from '@/layouts'

  3. import { RouteConfig } from 'react-router-config'

  4. import Home from '@/pages/home'

  5. const routesConfig: RouteConfig[] = [

  6.  {

  7.    path: '/',

  8.    exact: true,

  9.    component: Home

  10.  },

  11.  // hybird 路由

  12.  {

  13.    path: '/hybird',

  14.    exact: true,

  15.    component: Layout,

  16.    routes: [

  17.      {

  18.        path: '/',

  19.        exact: false,

  20.        component: loadable(() => import('@/pages/hybird'))

  21.      }

  22.    ]

  23.  },

  24.  // H5 相关路由

  25.  {

  26.    path: '/h5',

  27.    exact: false,

  28.    component: H5Layout,

  29.    routes: [

  30.      {

  31.        path: '/',

  32.        exact: false,

  33.        component: loadable(() => import('@/pages/h5'))

  34.      }

  35.    ]

  36.  }

  37. ]

  38. export default routesConfig

入口 main.tsx 文件配置路由路口

  1. import React from 'react'

  2. import ReactDOM from 'react-dom'

  3. import { BrowserRouter } from 'react-router-dom'

  4. import '@/styles/global.less'

  5. import { renderRoutes } from 'react-router-config'

  6. import routes from './routes'

  7. ReactDOM.render(

  8.  {renderRoutes(routes)},

  9.  document.getElementById('root')

  10. )

这里面的 renderRoutes 采用的 react-router-config 提供的方法,其实就是咱们 react-router 的配置写法,通过查看 源码 如下:

  1. import React from "react";

  2. import { Switch, Route } from "react-router";

  3. function renderRoutes(routes, extraProps = {}, switchProps = {}) {

  4.  return routes ? (

  5. {routes.map((route, i) => ( route.render ? ( route.render({ ...props, ...extraProps, route: route }) ) : ( ) } /> ))}

  6. ) : null; } export default renderRoutes;

通过以上两个配置,咱们就基本能把项目跑起来了,同时也具备了路由的懒加载能力;

执行 npm run build,查看文件输出,就能发现我们的动态路由加载已经配置成功了

  1. $ tsc && vite build

  2. vite v2.1.2 building for production...

  3. 53 modules transformed.

  4. dist/index.html                  0.41kb

  5. dist/assets/index.c034ae3d.js    0.11kb / brotli: 0.09kb

  6. dist/assets/index.c034ae3d.js.map 0.30kb

  7. dist/assets/index.f0d0ea4f.js    0.10kb / brotli: 0.09kb

  8. dist/assets/index.f0d0ea4f.js.map 0.29kb

  9. dist/assets/index.8105412a.js    2.25kb / brotli: 0.89kb

  10. dist/assets/index.8105412a.js.map 8.52kb

  11. dist/assets/index.7be450e7.css   1.25kb / brotli: 0.57kb

  12. dist/assets/vendor.7573543b.js   151.44kb / brotli: 43.17kb

  13. dist/assets/vendor.7573543b.js.map 422.16kb

  14.  Done in 9.34s.

细心的同学可能会发现,上面咱们的路由配置里面,特意拆分了两个 Layout & H5Layout,这里这么做的目的是为了区分在微信 h5 与 hybird 之间的差异化而设置的模板入口,大家可以根据自己的业务来决定是否需要 Layout

样式处理

说到样式处理,这里咱们的示例采用的是 .less 文件,所以在项目里面需要安装对应的解析库

  1. npm install --save-dev less postcss

如果要支持 css modules 特性,需要在 vite.config.ts 文件中开启对应的配置项:

  1. //  vite.config.ts

  2. {

  3.  css: {

  4.    preprocessorOptions: {

  5.      less: {

  6.        // 支持内联 JavaScript

  7.        javascriptEnabled: true

  8.      }

  9.    },

  10.    modules: {

  11.      // 样式小驼峰转化,

  12.      //css: goods-list => tsx: goodsList

  13.      localsConvention: 'camelCase'

  14.    }

  15.  },

  16. }

编译构建

其实到这里,基本就讲完了 vite 的整个构建,参考前面提到的配置文件:

  1. export default ({ command, mode }: ConfigEnv) => {

  2.  const envFiles = [

  3.    /** mode local file */ `.env.${mode}.local`,

  4.    /** mode file */ `.env.${mode}`,

  5.    /** local file */ `.env.local`,

  6.    /** default file */ `.env`

  7.  ]

  8.  const { plugins = [], build = {} } = config

  9.  const { rollupOptions = {} } = build

  10.  for (const file of envFiles) {

  11.    try {

  12.      fs.accessSync(file, fs.constants.F_OK)

  13.      const envConfig = dotenv.parse(fs.readFileSync(file))

  14.      for (const k in envConfig) {

  15.        if (Object.prototype.hasOwnProperty.call(envConfig, k)) {

  16.          process.env[k] = envConfig[k]

  17.        }

  18.      }

  19.    } catch (error) {

  20.      console.log('配置文件不存在,忽略')

  21.    }

  22.  }

  23.  const isBuild = command === 'build'

  24.  // const base = isBuild ? process.env.VITE_STATIC_CDN : '//localhost:3000/'

  25.  config.base = process.env.VITE_STATIC_CDN

  26.  if (isBuild) {

  27.    // 压缩 Html 插件

  28.    config.plugins = [...plugins, minifyHtml()]

  29.  }

  30.  if (process.env.VISUALIZER) {

  31.    const { plugins = [] } = rollupOptions

  32.    rollupOptions.plugins = [

  33.      ...plugins,

  34.      visualizer({

  35.        open: true,

  36.        gzipSize: true,

  37.        brotliSize: true

  38.      })

  39.    ]

  40.  }

  41.  // 在这里无法使用 import.meta.env 变量

  42.  if (command === 'serve') {

  43.    config.server = {

  44.      // 反向代理

  45.      proxy: {

  46.        api: {

  47.          target: process.env.VITE_API_HOST,

  48.          changeOrigin: true,

  49.          rewrite: (path: any) => path.replace(/^\/api/, '')

  50.        }

  51.      }

  52.    }

  53.  }

  54.  return config

  55. }

在这里,我们利用了一个 dotenv 的库,来帮我们将配置的内容绑定到 process.env 上面供我们配置文件使用

详细配置请参考 demo:https://github.com/lichenbuliren/fe-project-base

构建优化

  1. 为了更好地、更直观的知道项目打包之后的依赖问题,我们,我们可以通过 rollup-plugin-visualizer 包来实现可视化打包依赖

  2. 在使用自定义的环境构建配置文件,在 .env.custom 中,配置

    # .env.customNODE_ENV=production

    截止版本 vite@2.1.5,官方存在一个 BUG,上面的 NODE_ENV=production 在自定义配置文件中不生效,可以通过以下方式兼容

    // vite.config.tsconst config = {  ...  define: {    'process.env.NODE_ENV': '"production"'  }  ...}
  3. antd-mobile 按需加载,配置如下:

    import vitePluginImp from 'vite-plugin-imp'// vite.config.tsconst config = {  plugins: [    vitePluginImp({      libList: [        {          libName: 'antd-mobile',          style: (name) => `antd-mobile/es/${name}/style`,          libDirectory: 'es'        }      ]    })  ]}

    以上配置,在本地开发模式下能保证 antd 正常运行,但是,在执行 build 命令之后,在服务器访问会报一个错误

    ,类似 issue 可以参考

    解决方案 手动安装单独安装 indexof npm 包:npm install indexof




mobx6.x + react + typescript 实践

作者在使用 mobx 的时候,版本已经是 mobx@6.x,发现这里相比于旧版本,API 的使用上有了一些差异,特地在这里分享下踩坑经历

Store 划分

store 的划分,主要参考本文的示例 需要注意的是,在 store 初始化的时候,如果需要数据能够响应式绑定,需要在初始化的时候,给默认值,不能设置为 undefined 或者 null,这样子的话,数据是无法实现响应式的

  1. // store.ts

  2. import { makeAutoObservable, observable } from 'mobx'

  3. class CommonStore {

  4.  // 这里必须给定一个初始化的只,否则响应式数据不生效

  5.  title = ''

  6.  theme = 'default'

  7.  constructor() {

  8.    // 这里是实现响应式的关键

  9.    makeAutoObservable(this)

  10.  }

  11.  setTheme(theme: string) {

  12.    this.theme = theme

  13.  }

  14.  setTitle(title: string) {

  15.    this.title = title

  16.  }

  17. }

  18. export default new CommonStore()

Store 注入

mobx@6x的数据注入,采用的 reactcontext 特性;主要分成以下三个步骤

根节点变更

通过 Provider 组件,注入全局 store

  1. // 入口文件 app.tsx

  2. import { Provider } from 'mobx-react'

  3. import counterStore from './counter'

  4. import commonStore from './common'

  5. const stores = {

  6.  counterStore,

  7.  commonStore

  8. }

  9. ReactDOM.render(

  10.  {renderRoutes(routes)},

  11.  document.getElementById('root')

  12. )

这里的 Provider 是由 mobx-react 提供的 通过查看源码我们会发现, Provier内部实现也是 React Context:

  1. // mobx-react Provider 源码实现

  2. import React from "react"

  3. import { shallowEqual } from "./utils/utils"

  4. import { IValueMap } from "./types/IValueMap"

  5. // 创建一个 Context

  6. export const MobXProviderContext = React.createContext({})

  7. export interface ProviderProps extends IValueMap {

  8.    children: React.ReactNode

  9. }

  10. export function Provider(props: ProviderProps) {

  11.    // 除开 children 属性,其他的都作为 store 值

  12.    const { children, ...stores } = props

  13.    const parentValue = React.useContext(MobXProviderContext)

  14.    // store 引用最新值

  15.    const mutableProviderRef = React.useRef({ ...parentValue, ...stores })

  16.    const value = mutableProviderRef.current

  17.    if (__DEV__) {

  18.        const newValue = { ...value, ...stores } // spread in previous state for the context based stores

  19.        if (!shallowEqual(value, newValue)) {

  20.            throw new Error(

  21.                "MobX Provider: The set of provided stores has changed. See: https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error."

  22.            )

  23.        }

  24.    }

  25.    return {children}

  26. }

  27. // 供调试工具显示 Provider 名称

  28. Provider.displayName = "MobXProvider"

Store 使用

因为函数组件没法使用注解的方式,所以咱们需要使用自定义 Hook 的方式来实现:

  1. // useStore 实现

  2. import { MobXProviderContext } from 'mobx-react'

  3. import counterStore from './counter'

  4. import commonStore from './common'

  5. const _store = {

  6.  counterStore,

  7.  commonStore

  8. }

  9. export type StoreType = typeof _store

  10. // 声明 store 类型

  11. interface ContextType {

  12.  stores: StoreType

  13. }

  14. // 这两个是函数声明,重载

  15. function useStores(): StoreType

  16. function useStores<T extends keyof StoreType>(storeName: T): StoreType[T]

  17. /**

  18. * 获取根 store 或者指定 store 名称数据

  19. * @param storeName 指定子 store 名称

  20. * @returns typeof StoreType[storeName]

  21. */

  22. function useStores<T extends keyof StoreType>(storeName?: T) {

  23.  // 这里的 MobXProviderContext 就是上面 mobx-react 提供的

  24.  const rootStore = React.useContext(MobXProviderContext)

  25.  const { stores } = rootStore as ContextType

  26.  return storeName ? stores[storeName] : stores

  27. }

  28. export { useStores }

组件引用通过自定义组件引用 store

  1. import React from 'react'

  2. import { useStores } from '@/hooks'

  3. import { observer } from 'mobx-react'

  4. // 通过 Observer 高阶组件来实现

  5. const HybirdHome: React.FC = observer((props) => {

  6.  const commonStore = useStores('commonStore')

  7.  return (

  8.    <>

  9.      <div>Welcome Hybird Homediv>

  10.      <div>current theme: {commonStore.theme}div>

  11.      <button type="button" onClick={() => commonStore.setTheme('black')}>

  12.        set theme to black

  13.      button>

  14.      <button type="button" onClick={() => commonStore.setTheme('red')}>

  15.        set theme to red

  16.      button>

  17.  )

  18. })

  19. export default HybirdHome

可以看到前面咱们设计的自定义 Hook,通过 Typescript 的特性,能够提供友好的代码提示

code demo↑

以上就是整个 mobx+typescript 在函数式组件中的实际应用场景了;如果有什么问题,欢迎评论交流 :)




参考资料
  • React Hook useContext

    (https://reactjs.org/docs/hooks-reference.html#usecontext)

  • Mobx 官方文档

    (https://mobx.js.org/react-integration.html)

  • vite 构建案例 vite-concent-pro

    (https://github.com/tnfe/vite-concent-pro)

如果觉得这篇文章还不错
点击下面卡片关注我
来个【分享、点赞、在看】三连支持一下吧

   “分享、点赞在看” 支持一波 

浏览 91
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报