34
头图

Use Vite + React + Typescript to create a front-end single-page application template

Recently, the front-end Vite 2.0 version has finally come out, here is how to use vite to build a front-end single-page application

This article is mainly for students who are interested in Vite or who are doing front-end project architecture

Source address, welcome to star to track the latest changes: fe-project-base

Through this article, you can learn the following:

If you want to quickly understand the Vite configuration and construction, you can jump directly to here

Initialize the project

Here our project name is fe-project-base
Here we use vite 2.0 to initialize our project

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

At this time, a command line prompt will appear, let's select the corresponding initialization type according to the template we want, and it will be OK

Install project dependencies

First, we need to install dependencies. To create a basic front-end single-page application template, we need to install the following dependencies:

  1. react & react-dom : basic core
  2. react-router : routing configuration
  3. @loadable/component : dynamic route loading
  4. classnames : Better writing of className
  5. react-router-config : Better react-router routing configuration package
  6. mobx-react & mobx-persist : mobx state management
  7. eslint & lint-staged & husky & prettier : code verification configuration
  8. eslint-config-alloy : ESLint configuration plugin

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 configuration

After installing the above dependencies, use cat .git/hooks/pre-commit to determine whether husky is installed normally. If the file does not exist, the installation has failed and you need to reinstall it.

<span style="color:red;font-weight:bold;">
Husky here uses 4.x version, 5.x version is no longer a free agreement<br/>The test found that node/14.15.1 version will cause Husky to automatically create .git/hooks/pre-commit configuration failure, upgrade node/14.16 .0 fix the problem
</span>

After completing the above installation and configuration, we also need to add related configuration 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"
    ]
  },
}

At this point, our entire project has the ability to do ESLint verification and repair the formatting of the submitted files.

ESLintError

<span id="editor">Editor configuration</span>

your . The first thing we solve is the problem of editor collaboration within the team. At this time, you need to install the 160819d8abbc65 EditorConfig 160819d8abbc67 plug-in in the editor of the developer (here, take the vscode plug-in as an example)

First, we create a new configuration file in the project root directory: .editorconfig

Reference configuration:

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

configure automatic formatting and code verification

In the vscode editor, Mac shortcut key command + , to quickly open the configuration items, switch to the workspace module, and click the open settings json button in the upper right corner to configure the following information:

{
  "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"
  }
}

At this time, our editor already has the function of saving and formatting automatically

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

Regarding the relationship between ESLint and Prettier, you can move here: thoroughly understand ESLint and Prettier

  1. .eslintignore : Configure ESLint to ignore files
  2. .eslintrc : ESLint coding rule configuration, here it is recommended to use the industry unified standard, here I recommend AlloyTeam's eslint-config-alloy , install the corresponding ESLint configuration according to the document:
  3. npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy
  4. .prettierignore : Configure Prettier to ignore files
  5. .prettierrc : Format custom configuration

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

The eslint-config-alloy are as follows:

  1. Clearer ESLint prompts: such as prompts that special characters need to be escaped, etc.

    error `'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`  react/no-unescaped-entities
  2. Strict ESLint configuration prompts: for example, it will prompt that ESLint is not configured and specify the version of React and an alarm will be issued

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

    Here we fill in the configuration react

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

<span id="dir">Overall directory planning</span>

A basic front-end single-page application requires a rough directory structure as follows:

Here is an example of the directory division 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, at this point, we have planned a rough front-end project directory structure. Next, we need to configure aliases to optimize the code, such as: import xxx from '@/utils' path experience

Usually there will be a public directory at the same level as the src directory, and the files in this directory will be copied directly to the build directory

Alias configuration

For alias configuration, we need to pay attention to two places: vite.config.ts & tsconfig.json

Among them, vite.config.ts used to compile and recognize; tsconfig.json is used to recognize Typescript;

It is recommended to @/ , why not @ , this is to avoid conflicts with some npm package names in the industry (for example, @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">Vite build configuration from 0 to 1</span>

As of the time the author writes this article, the vite version is vite/2.1.2 , and all the following configurations are only responsible for this version

Configuration file

The default vite initialization project will not create three configuration files for us, .env.production , .env.devlopment .env package.json files provided by the official template by default, the three script will use these files respectively, so we need Manually create first, here is the official document: .env configuration

# 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'
  }
}

At the same time, the command here, the corresponding configuration file: mode distinguish

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

Specific configuration file reference: fe-project-vite/vite.config.ts

Routing planning

First of all, the most important part of a project is the routing configuration; then we need a configuration file as the entry point to configure all page routing, here is react-router as an example:

Routing profile configuration

src/routes/index.ts , here we have introduced the @loadable/component library for dynamic routing loading, vite supports dynamic loading feature default to improve the efficiency of program packaging

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

Entrance main.tsx file configuration routing junction

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')
)

The method provided by renderRoutes used by the react-router-config here is react-router . By viewing the source code as follows:

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;

Through the above two configurations, we can basically run the project, and at the same time have the lazy loading ability of routing;

Execute npm run build and check the file output, you can find that our dynamic route loading has been configured successfully

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

Attentive students may find that in our routing configuration above, two Layout & H5Layout deliberately split. The purpose of doing this here is to distinguish the template entry set for the difference between WeChat h5 and hybrid. You can Decide whether you need the Layout layer according to your own business

Style processing

Speaking of style processing, our example here uses the .less file, so the corresponding parsing library needs to be installed in the project

npm install --save-dev less postcss

If you want to support the css modules feature, you need to enable the corresponding configuration item vite.config.ts

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

Compile and build

In fact, at this point, I have basically finished the entire construction of vite. Refer to the configuration file mentioned earlier:

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
}

Here, we use a dotenv library to help us bind the configuration content to process.env for our configuration file to use

For detailed configuration, please refer to demo

Build optimization

  1. In order to better and more intuitively know the dependency problem after the project is packaged, we can realize the visual packaging dependency rollup-plugin-visualizer
  2. In the use of a customized environment to build the configuration file, in .env.custom , configure

    # .env.custom
    NODE_ENV=production

    As of version vite@2.1.5 , there is an official BUG. The above NODE_ENV=production does not take effect in the custom configuration file. It can be compatible in the following ways

    // vite.config.ts
    const config = {
      ...
      define: {
        'process.env.NODE_ENV': '"production"'
      }
      ...
    }
  3. antd-mobile loaded on demand, and the configuration is as follows:

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

    The above configuration can ensure the normal operation of antd in the local development mode, but after executing the build command, an error will be reported in the server access
    antd-error , similar to issue can refer to

    solution
    Manual installation separately install indexof npm package: npm install indexof

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

When the author used mobx , the version was already mobx@6.x , and found that compared with the old version, there are some differences in the use of the API. I hereby share the experience of stepping on the pit

Store division

The division of store, mainly refer to the example
It should be noted that when the store is initialized, if the data needs to be responsively bound, the default value must be given at the time of initialization, and it cannot be set to undefined or null. In this case, the data cannot be responsive.

// store.ts
import { 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 injection

mobx@6x data injection, the use of react of context characteristics; mainly divided into the following three steps

Root node change

Through the Provider component, inject the global store

// 入口文件 app.tsx
import { 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')
)

The Provider here is provided by mobx-react
We find that by looking at the source code, Provier internal implementation is also React Context :

// mobx-react Provider 源码实现
import React from "react"
import { shallowEqual } from "./utils/utils"
import { IValueMap } from "./types/IValueMap"

// 创建一个 Context
export 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 uses

Because function components cannot use annotations, we need to use custom Hook to achieve:

// 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(): StoreType
function 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 }

Component reference through custom component reference 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

You can see that the custom Hook we designed earlier can provide friendly code hints Typescript

code demo

The above is the actual application scenario of the mobx + typescript
If you have any questions, please comment and exchange :)

Reference


离尘不理人
1.9k 声望732 粉丝