应用 Vite + React + Typescript 打造一个前端单页利用模板

最近前端大火的 Vite 2.0 版本终于进去了,在这里分享一下如何应用 vite 构建一个前端单页利用

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

源码地址,欢送 star 跟踪最新变更:fe-project-base

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

  • vscode 编辑器配置
  • git pre-commit 如何配置
  • ESLint + Pritter 配置
  • 规范前端单页利用目录布局
  • 从 0 到 1 学习 vite 构建优化
  • mobx/6.x + react + TypeScript 最佳实际

想疾速理解 Vite 配置构建的,能够间接跳到 这里

初始化我的项目

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

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:

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

devDependencies:

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

pre-commit 配置

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

<span style="color:red;font-weight:bold;">
这里的 husky 应用 4.x 版本,5.x 版本曾经不是收费协定了<br/>测试发现 node/14.15.1 版本会导致 husky 主动创立 .git/hooks/pre-commit 配置失败,降级 node/14.16.0 修复该问题
</span>

在实现了以上装置配置之后,咱们还须要对 package.json 增加相干配置

{  "husky": {    "hooks": {      "pre-commit": "lint-staged"    }  },  "lint-staged": {    "src/**/*.{ts,tsx}": [      "eslint --cache --fix",      "git add"    ],    "src/**/*.{js,jsx}": [      "eslint --cache --fix",      "git add"    ]  },}

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

<span id="editor">编辑器配置</span>

工欲善其事必先利其器,咱们首要解决的是在团队外部编辑器合作问题,这个时候,就须要开发者的编辑器对立装置 EditorConfig 插件(这里以 vscode 插件为例)

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

参考配置:

root = true[*]indent_style = spaceindent_size = 2end_of_line = lfcharset = utf-8trim_trailing_whitespace = trueinsert_final_newline = true

配置主动格式化与代码校验

在 vscode 编辑器中,Mac 快捷键 command + , 来疾速关上配置项,切换到 workspace 模块,并点击右上角的 open settings json 按钮,配置如下信息:

{  "editor.formatOnSave": true,  "editor.codeActionsOnSave": {    "source.fixAll.tslint": true  },  "editor.defaultFormatter": "esbenp.prettier-vscode",  "[javascript]": {    "editor.formatOnSave": true,    "editor.defaultFormatter": "esbenp.prettier-vscode"  },  "[typescript]": {    "editor.defaultFormatter": "esbenp.prettier-vscode"  },  "typescript.tsdk": "node_modules/typescript/lib",  "[typescriptreact]": {    "editor.defaultFormatter": "esbenp.prettier-vscode"  }}

这个时候,咱们的编辑器曾经具备了保留并主动格式化的性能了

<span id="eslint">ESLint + Prettier</span>

对于 ESLint 与 Prettier 的关系,能够移步这里:彻底搞懂 ESLint 和 Prettier

  1. .eslintignore:配置 ESLint 疏忽文件
  2. .eslintrc:ESLint 编码规定配置,这里举荐应用业界统一标准,这里我举荐 AlloyTeam 的 eslint-config-alloy,依照文档装置对应的 ESLint 配置:
  3. npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy
  4. .prettierignore:配置 Prettier 疏忽文件
  5. .prettierrc:格式化自定义配置

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

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

  6. 更清晰的 ESLint 提醒:比方特殊字符须要本义的提醒等等

    error `'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`  react/no-unescaped-entities
  7. 更加严格的 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 版本    }  }}

<span id="dir">整体目录布局</span>

一个根本的前端单页利用,须要的大抵的目录架构如下:

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

.├── app.tsx├── assets // 动态资源,会被打包优化│   ├── favicon.svg│   └── logo.svg├── common // 公共配置,比方对立申请封装,session 封装│   ├── http-client│   └── session├── components // 全局组件,分业务组件或 UI 组件│   ├── Toast├── config // 配置文件目录│   ├── index.ts├── hooks // 自定义 hook│   └── index.ts├── layouts // 模板,不同的路由,能够配置不同的模板│   └── index.tsx├── lib // 通常这里避免第三方库,比方 jweixin.js、jsBridge.js│   ├── README.md│   ├── jsBridge.js│   └── jweixin.js├── pages // 页面寄存地位│   ├── components // 就近准则页面级别的组件│   ├── home├── routes // 路由配置│   └── index.ts├── store // 全局状态治理│   ├── common.ts│   ├── index.ts│   └── session.ts├── styles // 全局款式│   ├── global.less│   └── reset.less└── utils // 工具办法  └── index.ts

OK,到这里,咱们布局好了一个大抵的前端我的项目目录构造,接下来咱们要配置一下别名,来优化代码中的,比方: import xxx from '@/utils' 门路体验

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

别名配置

别名的配置,咱们须要关注的是两个中央:vite.config.ts & tsconfig.json

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

这里倡议采纳的是 @/ 结尾,为什么不必 @ 结尾,这是为了防止跟业界某些 npm 包名抵触(例如 @vitejs)

  • vite.config.ts
// vite.config.ts{  resolve: {    alias: {      '@/': path.resolve(__dirname, './src'),      '@/config': path.resolve(__dirname, './src/config'),      '@/components': path.resolve(__dirname, './src/components'),      '@/styles': path.resolve(__dirname, './src/styles'),      '@/utils': path.resolve(__dirname, './src/utils'),      '@/common': path.resolve(__dirname, './src/common'),      '@/assets': path.resolve(__dirname, './src/assets'),      '@/pages': path.resolve(__dirname, './src/pages'),      '@/routes': path.resolve(__dirname, './src/routes'),      '@/layouts': path.resolve(__dirname, './src/layouts'),      '@/hooks': path.resolve(__dirname, './src/hooks'),      '@/store': path.resolve(__dirname, './src/store')    }  },}
  • tsconfig.json
{  "compilerOptions": {    "paths": {      "@/*": ["./src/*"],      "@/components/*": ["./src/components/*"],      "@/styles/*": ["./src/styles/*"],      "@/config/*": ["./src/config/*"],      "@/utils/*": ["./src/utils/*"],      "@/common/*": ["./src/common/*"],      "@/assets/*": ["./src/assets/*"],      "@/pages/*": ["./src/pages/*"],      "@/routes/*": ["./src/routes/*"],      "@/hooks/*": ["./src/hooks/*"],      "@/store/*": ["./src/store/*"]    },    "typeRoots": ["./typings/"]  },  "include": ["./src", "./typings", "./vite.config.ts"],  "exclude": ["node_modules"]}

<span id="vite">从 0 到 1 Vite 构建配置</span>

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

配置文件

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

# package.json{  "scripts": {    "dev": "vite", // 等于 vite -m development,此时 command='serve',mode='development'    "build": "tsc && vite build", // 等于 vite -m production,此时 command='build', mode='production'    "serve": "vite preview",    "start:qa": "vite -m qa" // 自定义命令,会寻找 .env.qa 的配置文件;此时 command='serve',mode='qa'  }}

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

import { ConfigEnv } from 'vite'export default ({ command, mode }: ConfigEnv) => {  // 这里的 command 默认 === 'serve'  // 当执行 vite build 时,command === 'build'  // 所以这里能够依据 command 与 mode 做条件判断来导出对应环境的配置}

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

路由布局

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

路由配置文件配置

src/routes/index.ts,这里咱们引入的了 @loadable/component 库来做路由动静加载,vite 默认反对动静加载个性,以此进步程序打包效率

import loadable from '@loadable/component'import Layout, { H5Layout } from '@/layouts'import { RouteConfig } from 'react-router-config'import Home from '@/pages/home'const routesConfig: RouteConfig[] = [  {    path: '/',    exact: true,    component: Home  },  // hybird 路由  {    path: '/hybird',    exact: true,    component: Layout,    routes: [      {        path: '/',        exact: false,        component: loadable(() => import('@/pages/hybird'))      }    ]  },  // H5 相干路由  {    path: '/h5',    exact: false,    component: H5Layout,    routes: [      {        path: '/',        exact: false,        component: loadable(() => import('@/pages/h5'))      }    ]  }]export default routesConfig

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

import React from 'react'import ReactDOM from 'react-dom'import { BrowserRouter } from 'react-router-dom'import '@/styles/global.less'import { renderRoutes } from 'react-router-config'import routes from './routes'ReactDOM.render(  <React.StrictMode>    <BrowserRouter>{renderRoutes(routes)}</BrowserRouter>  </React.StrictMode>,  document.getElementById('root'))

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

import React from "react";import { Switch, Route } from "react-router";function renderRoutes(routes, extraProps = {}, switchProps = {}) {  return routes ? (    <Switch {...switchProps}>      {routes.map((route, i) => (        <Route          key={route.key || i}          path={route.path}          exact={route.exact}          strict={route.strict}          render={props =>            route.render ? (              route.render({ ...props, ...extraProps, route: route })            ) : (              <route.component {...props} {...extraProps} route={route} />            )          }        />      ))}    </Switch>  ) : null;}export default renderRoutes;

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

执行 npm run build,查看文件输入,就能发现咱们的动静路由加载曾经配置胜利了

$ tsc && vite buildvite v2.1.2 building for production...✓ 53 modules transformed.dist/index.html                  0.41kbdist/assets/index.c034ae3d.js    0.11kb / brotli: 0.09kbdist/assets/index.c034ae3d.js.map 0.30kbdist/assets/index.f0d0ea4f.js    0.10kb / brotli: 0.09kbdist/assets/index.f0d0ea4f.js.map 0.29kbdist/assets/index.8105412a.js    2.25kb / brotli: 0.89kbdist/assets/index.8105412a.js.map 8.52kbdist/assets/index.7be450e7.css   1.25kb / brotli: 0.57kbdist/assets/vendor.7573543b.js   151.44kb / brotli: 43.17kbdist/assets/vendor.7573543b.js.map 422.16kb✨  Done in 9.34s.

仔细的同学可能会发现,下面咱们的路由配置外面,特意拆分了两个 Layout & H5Layout,这里这么做的目标是为了辨别在微信 h5 与 hybird 之间的差异化而设置的模板入口,大家能够依据本人的业务来决定是否须要 Layout

款式解决

说到款式解决,这里咱们的示例采纳的是 .less 文件,所以在我的项目外面须要装置对应的解析库

npm install --save-dev less postcss

如果要反对 css modules 个性,须要在 vite.config.ts 文件中开启对应的配置项:

//  vite.config.ts{  css: {    preprocessorOptions: {      less: {        // 反对内联 JavaScript        javascriptEnabled: true      }    },    modules: {      // 款式小驼峰转化,       //css: goods-list => tsx: goodsList      localsConvention: 'camelCase'    }  },}

编译构建

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

export default ({ command, mode }: ConfigEnv) => {  const envFiles = [    /** mode local file */ `.env.${mode}.local`,    /** mode file */ `.env.${mode}`,    /** local file */ `.env.local`,    /** default file */ `.env`  ]  const { plugins = [], build = {} } = config  const { rollupOptions = {} } = build  for (const file of envFiles) {    try {      fs.accessSync(file, fs.constants.F_OK)      const envConfig = dotenv.parse(fs.readFileSync(file))      for (const k in envConfig) {        if (Object.prototype.hasOwnProperty.call(envConfig, k)) {          process.env[k] = envConfig[k]        }      }    } catch (error) {      console.log('配置文件不存在,疏忽')    }  }  const isBuild = command === 'build'  // const base = isBuild ? process.env.VITE_STATIC_CDN : '//localhost:3000/'  config.base = process.env.VITE_STATIC_CDN  if (isBuild) {    // 压缩 Html 插件    config.plugins = [...plugins, minifyHtml()]  }  if (process.env.VISUALIZER) {    const { plugins = [] } = rollupOptions    rollupOptions.plugins = [      ...plugins,      visualizer({        open: true,        gzipSize: true,        brotliSize: true      })    ]  }  // 在这里无奈应用 import.meta.env 变量  if (command === 'serve') {    config.server = {      // 反向代理      proxy: {        api: {          target: process.env.VITE_API_HOST,          changeOrigin: true,          rewrite: (path: any) => path.replace(/^\/api/, '')        }      }    }  }  return config}

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

具体配置请参考 demo

构建优化

  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

<span id="mobx">mobx6.x + react + typescript 实际</span>

作者在应用 mobx 的时候,版本曾经是 mobx@6.x,发现这里相比于旧版本,API 的应用上有了一些差别,顺便在这里分享下踩坑经验

Store 划分

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

// store.tsimport { makeAutoObservable, observable } from 'mobx'class CommonStore {  // 这里必须给定一个初始化的只,否则响应式数据不失效  title = ''  theme = 'default'  constructor() {    // 这里是实现响应式的要害    makeAutoObservable(this)  }  setTheme(theme: string) {    this.theme = theme  }  setTitle(title: string) {    this.title = title  }}export default new CommonStore()

Store 注入

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

根节点变更

通过 Provider 组件,注入全局 store

// 入口文件 app.tsximport { Provider } from 'mobx-react'import counterStore from './counter'import commonStore from './common'const stores = {  counterStore,  commonStore}ReactDOM.render(  <React.StrictMode>    <Provider stores={stores}>      <BrowserRouter>{renderRoutes(routes)}</BrowserRouter>    </Provider>  </React.StrictMode>,  document.getElementById('root'))

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

// mobx-react Provider 源码实现import React from "react"import { shallowEqual } from "./utils/utils"import { IValueMap } from "./types/IValueMap"// 创立一个 Contextexport const MobXProviderContext = React.createContext<IValueMap>({})export interface ProviderProps extends IValueMap {    children: React.ReactNode}export function Provider(props: ProviderProps) {    // 除开 children 属性,其余的都作为 store 值    const { children, ...stores } = props    const parentValue = React.useContext(MobXProviderContext)    // store 援用最新值    const mutableProviderRef = React.useRef({ ...parentValue, ...stores })    const value = mutableProviderRef.current    if (__DEV__) {        const newValue = { ...value, ...stores } // spread in previous state for the context based stores        if (!shallowEqual(value, newValue)) {            throw new Error(                "MobX Provider: The set of provided stores has changed. See: https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error."            )        }    }    return <MobXProviderContext.Provider value={value}>{children}</MobXProviderContext.Provider>}// 供调试工具显示 Provider 名称Provider.displayName = "MobXProvider"

Store 应用

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

// useStore 实现import { MobXProviderContext } from 'mobx-react'import counterStore from './counter'import commonStore from './common'const _store = {  counterStore,  commonStore}export type StoreType = typeof _store// 申明 store 类型interface ContextType {  stores: StoreType}// 这两个是函数申明,重载function useStores(): StoreTypefunction useStores<T extends keyof StoreType>(storeName: T): StoreType[T]/** * 获取根 store 或者指定 store 名称数据 * @param storeName 指定子 store 名称 * @returns typeof StoreType[storeName] */function useStores<T extends keyof StoreType>(storeName?: T) {  // 这里的 MobXProviderContext 就是下面 mobx-react 提供的  const rootStore = React.useContext(MobXProviderContext)  const { stores } = rootStore as ContextType  return storeName ? stores[storeName] : stores}export { useStores }

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

import React from 'react'import { useStores } from '@/hooks'import { observer } from 'mobx-react'// 通过 Observer 高阶组件来实现const HybirdHome: React.FC = observer((props) => {  const commonStore = useStores('commonStore')  return (    <>      <div>Welcome Hybird Home</div>      <div>current theme: {commonStore.theme}</div>      <button type="button" onClick={() => commonStore.setTheme('black')}>        set theme to black      </button>      <button type="button" onClick={() => commonStore.setTheme('red')}>        set theme to red      </button>    </>  )})export default HybirdHome

能够看到后面咱们设计的自定义 Hook,通过 Typescript 的个性,可能提供敌对的代码提醒

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

参考资料

  • React Hook useContext
  • Mobx 官网文档
  • vite 构建案例 vite-concent-pro