Vite 2.0 is finally out. Here is how to build a front-end single-page application using Vite

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

Source address, welcome star, will be updated later maintenance: fe-project-base

Here are a few things you can learn from the article:

  • Vscode editor configuration
  • How to configure git pre-commit
  • ESLint + Pritter configuration
  • Standard front-end single-page application directory planning
  • Learn vite build optimization from 0 to 1
  • Mobx /6.x + React + TypeScript best practices

For a quick look at Vite configuration builds, you can jump right here

Initialize the project

Here our project name is fe-project-base and here we use Vite 2.0 to initialize our project

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

At this time, the command line prompt will appear, we follow the template we want, select the corresponding initialization type OK

Installation project dependencies

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

  1. react & react-dom: Basic core
  2. react-router: Route configuration
  3. @loadable/component: Dynamic route loading
  4. classnames: a better way to write className
  5. react-router-config: Better React-Router routing configuration package
  6. mobx-react & mobx-persist: Mobx status management
  7. eslint & lint-staged & husky & prettier: code verification configuration
  8. eslint-config-alloy: ESLint configures plugins

dependencies:

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

DevDependencies:

NPM install --save-dev eslint Lint-staged [email protected] prettierCopy the code

The pre – commit configuration

After installing the above dependencies, check to see if husky is successfully installed using cat.git /hooks/pre-commit. If this file is not found, husky failed to be installed and needs to be re-installed

Husky here uses the 4.x version, which is no longer a free agreement

Git /hooks/pre-commit configuration failed. Update node/14.16.0 to fix this

After the above installation configuration is complete, we also need to add related configuration to 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"]}}Copy the code

At this point, our entire project has the ability to do ESLint validation and fix formatting for submitted files

Editor configuration

We first solve the problem of editor collaboration within the team. At this time, we need to install the EditorConfig plug-in in the developer’s editor (here we take 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
Copy the code

Configure automatic formatting and code validation

In the VSCode editor, the Mac shortcut command + is used 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"}}Copy the code

At this point, our editor already has the ability to save and automatically format

ESLint + Prettier

Prettier, ESLint, Prettier the next step in ESLint and Prettier can be: Know ESLint and Prettier completely

  1. .eslintignore: configure ESLint to ignore files

  2. Eslintrc: ESLint code rule configuration, here we recommend using the industry standard, here I recommend AlloyTeam eslint-config-alloy, according to the documentation to install the corresponding ESLint configuration:

  3. npm install --save-dev eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy
    Copy the code
  4. . Prettierignore: Configure Prettier to ignore a file

  5. . Prettierrc: Formatting a user-defined configuration

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

There are several reasons to choose eslint-config-alloy:

  1. Clearer ESLint hints: for example, special characters need to be escaped, etc
    error `'` can be escaped with `' `, `‘ `, ` & # 39; `, `’ ` react/no-unescaped-entitiesCopy the code
  2. Stricter ESLint configuration prompts: for example, ESLint will be alerted if a version specifying React is not configured
    Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-react#configuration
    Copy the code

    Let’s make up pairs herereactVersion Configuration

    // .eslintrc
    {
      "settings": {
        "react": {
          "version": "detect" // Probes the react version of the current node_modules installation}}}Copy the code

Overall Catalog Planning

For a basic front-end single-page application, the general directory architecture is as follows:

The directory partition under SRC is used as an example

.├ ─ app.tsx ├─ Assets, │ ├─ favicon. SVG │ ├─ common // │ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ Business components or UI components │ ├ ─ ─ Toast ├ ─ ─ the config / / configuration file directory │ ├ ─ ─ but ts ├ ─ ─ hooks / / custom hook │ └ ─ ─ index. The ts ├ ─ ─ layouts / / template, Different routing, can configure different templates │ └ ─ ─ index. The TSX ├ ─ ─ lib / / here usually prevent third-party libraries, ├─ ├─ exercises - ├─ exercises - ├─ exercises - exercises - exercises - ├─ exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - exercises - Exercises - Exercises - Exercises - Exercises - Exercises - Exercises - Exercises - Exercises Ofessional page level component │ ├ ─ ─ home ├ ─ ─ routes / / routing configuration │ └ ─ ─ index. The ts ├ ─ ─ store / / global state management │ ├ ─ ─ common. Ts │ ├ ─ ─ index. Ts │ └ ─ ─ ├─ ├─ lessons.├ ─ lessons.├ ─ lessons.├ ─ lessons.└Copy the code

OK, now that we have a rough directory structure for the front-end project, we need to configure aliases to optimize the code, such as: import XXX from ‘@/utils’ path experience

There is usually a public directory that is the same as the SRC directory, and files in that directory are copied directly to the build directory

The alias configuration

To configure aliases, we need to focus on two places: vite.config.ts & tsconfig.json

Vite. Config. ts is used for compilation and identification; Tsconfig. json is used for Typescript recognition;

@/ is recommended. Why not use @? This is to avoid conflicts with some NPM package names in the industry (e.g. @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')}}}Copy the code
  • 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"]}Copy the code

Build configurations from 0 to 1 Vite

By the time the author wrote the article,viteVersion forVite / 2.1.2, all the following configurations are only applicable to this version

The configuration file

The default vite initialization project does not create.env,.env.production, and.env.devlopment configuration files. The official template provides package.json files that will be used by each script. So we need to manually create first, here provides the official documentation:.env configuration

# package.json
{
  "scripts": {
    "dev": "vite".Vite -m development ='serve',mode='development'
    "build": "tsc && vite build".Vite -m production command='build', mode='production'
    "serve": "vite preview"."start:qa": "vite -m qa" // Custom commands will look for.env.qa configuration files; Command ='serve', mode='qa'}}Copy the code

At the same time here the command corresponds to the configuration file: mode distinction

import { ConfigEnv } from 'vite'
export default ({ command, mode }: ConfigEnv) => {
  // Command defaults to === 'serve'
  // Command === 'build' when executing vite build
  // The configuration of the corresponding environment can be exported according to the condition judgment of command and mode
}
Copy the code

For details about the configuration file, see fe-project-vite/vite.config.ts

Routing planning

First, the most important part of a project is routing configuration. So we need a configuration file as the entry point to configure all the page routes. Here we use the react-router as an example:

Route configuration file configuration

SRC /routes/index.ts, we introduced the @loadable/ Component library to load routes dynamically. Vite supports dynamic loading by default to improve program packaging efficiency

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 routing
  {
    path: '/hybird'.exact: true.component: Layout,
    routes: [{path: '/'.exact: false.component: loadable(() = > import('@/pages/hybird'))}},// H5 related routes
  {
    path: '/h5'.exact: false.component: H5Layout,
    routes: [{path: '/'.exact: false.component: loadable(() = > import('@/pages/h5')}]}]export default routesConfig
Copy the code

The entry main. TSX file configures the routed intersections

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'))Copy the code

React-router-config: renderRoutes: react-router-config: renderRoutes: react-router-config: renderRoutes: react-router-config: renderRoutes: react-router-config:

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;
Copy the code

Through the above two configurations, we can basically run the project, but also have the lazy loading capability of the route;

Execute NPM run build and view the file output to see that our dynamic route loading has been configured successfully

$ tsc && vite buildVite v2.1.2 Building for Production... ✓ 53 modules transformed. Dist/index. HTML 0.41 KB dist/assets/index c034ae3d. Js 0.11 KB/brotli: 0.09 KB dist/assets/index. C034ae3d. Js. Map 0.30 KB dist/assets/index f0d0ea4f. Js 0.10 KB/brotli: 0.09 KB dist/assets/index. F0d0ea4f. Js. Map 0.29 KB dist/assets/index. 8105412 a. js 2.25 KB/brotli: 0.89 KB dist/assets/index. 8105412 a. js. Map 8.52 KB dist/assets/index. 7 be450e7. CSS 1.25 KB/brotli: 0.57 KB dist/assets/vendor. 7573543 b. js 151.44 KB/brotli: 43.17 KB dist/assets/vendor. 7573543 b. js. Map 422.16 KB ✨ Done in 9.34 s.Copy the code

Careful students may find that in the routing configuration above, we deliberately split two Layout & H5Layout, the purpose of doing so here is to distinguish the difference between wechat H5 and Hybird set up template entrance, you can decide whether to need the Layout layer according to their own business

Style to deal with

Speaking of styling, our example here uses.less files, so we need to install the corresponding parsing library in the project

npm install --save-dev less postcss
Copy the code

To support CSS modules, you need to enable the corresponding configuration items in the viet.config. ts file:

// vite.config.ts
{
  css: {
    preprocessorOptions: {
      less: {
        // Support inline JavaScript
        javascriptEnabled: true}},modules: {
      // Style small hump transform,
      //css: goods-list => tsx: goodsList
      localsConvention: 'camelCase'}}},Copy the code

Compile build

In fact, this is basically the entire vite build, 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('Config file does not exist, ignore')}}const isBuild = command === 'build'
  // const base = isBuild ? process.env.VITE_STATIC_CDN : '//localhost:3000/'

  config.base = process.env.VITE_STATIC_CDN

  if (isBuild) {
    // Compress the Html plug-in
    config.plugins = [...plugins, minifyHtml()]
  }

  if (process.env.VISUALIZER) {
    const { plugins = [] } = rollupOptions
    rollupOptions.plugins = [
      ...plugins,
      visualizer({
        open: true.gzipSize: true.brotliSize: true}})]// The import.meta. Env variable cannot be used here
  if (command === 'serve') {
    config.server = {
      // Reverse proxy
      proxy: {
        api: {
          target: process.env.VITE_API_HOST,
          changeOrigin: true.rewrite: (path: any) = > path.replace(/^\/api/.' ')}}}}return config
}
Copy the code

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

For details, see Demo

Build optimization

  1. In order to better and more intuitively know the dependency problem after the project packaging, we can use the rollup-plugin-Visualizer package to achieve visual packaging dependency

  2. To build the configuration file using a custom environment, in.env.custom, configure

    # .env.custom
    NODE_ENV=production
    Copy the code

    NODE_ENV=production does not work in custom configuration files. It is compatible with the following methods

    // vite.config.ts
    const config = {
      ...
      define: {
        'process.env.NODE_ENV': '"production"'}... }Copy the code
  3. Antd-mobile is loaded on demand and configured 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'}]}]}Copy the code

    The above configuration ensures the normal operation of ANTD in local development mode, however, before executionbuildCommand, an error is reported on the server access, similar toissueYou can refer to

    NPM install Indexof. NPM install Indexof

Mobx6. x + React + typescript practice

When I use mobx, the version is already [email protected], and I find that there are some differences in the use of API compared to the old version, so I would like to share my experience here

Store division

It is important to note that when the store is initialized, if you want the data to be responsive, you need to set the default value to be undefined or null during initialization. In this case, the data cannot be responsive

// store.ts
import { makeAutoObservable, observable } from 'mobx'

class CommonStore {
  // An initializer must be given, otherwise reactive data will not take effect
  title = ' '
  theme = 'default'

  constructor() {
    // This is the key to implementing responsiveness
    makeAutoObservable(this)}setTheme(theme: string) {
    this.theme = theme
  }

  setTitle(title: string) {
    this.title = title
  }
}

export default new CommonStore()

Copy the code

Store injection

mobx@6x data injection, using the React Context feature; It is divided into the following three steps

Root node change

Use the Provider component to inject global stores

// Import file 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'))Copy the code

The Provider is provided by mox-React. Provier implements the react Context internally:

// Mobx-React Provider
import React from "react"
import { shallowEqual } from "./utils/utils"
import { IValueMap } from "./types/IValueMap"

// Create a Context
export const MobXProviderContext = React.createContext<IValueMap>({})

export interface ProviderProps extends IValueMap {
    children: React.ReactNode
}

export function Provider(props: ProviderProps) {
    // All except the children attribute are store values
    const{ children, ... stores } = propsconst parentValue = React.useContext(MobXProviderContext)
    // store references the latest value
    constmutableProviderRef = React.useRef({ ... parentValue, ... stores })const value = mutableProviderRef.current

    if (__DEV__) {
        constnewValue = { ... 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>
}

// Displays the Provider name for the debugging tool
Provider.displayName = "MobXProvider"
Copy the code

Store use

Since function components can’t use annotations, we need to use custom hooks:

/ / useStore implementation
import { MobXProviderContext } from 'mobx-react'
import counterStore from './counter'
import commonStore from './common'

const _store = {
  counterStore,
  commonStore
}

export type StoreType = typeof _store

// Declare the store type
interface ContextType {
  stores: StoreType
}

// These are function declarations, overloaded
function useStores() :StoreType
function useStores<T extends keyof StoreType> (storeName: T) :StoreType[T] /** * get the rootstoreOr specifystoreName data * @param storeNameSpecify the childstoreName * @returns typeof StoreType[storeName] * /function useStores<T extends keyof StoreType> (storeName? : T) {// hereMobXProviderContextIs the abovemobx-reactTo provide theconst rootStore = React.useContext(MobXProviderContext)
  const { stores } = rootStore as ContextType
  return storeName ? stores[storeName] : stores
}

export { useStores }
Copy the code

Component references refer to stores by customizing components

import React from 'react'
import { useStores } from '@/hooks'
import { observer } from 'mobx-react'

// Implemented through the Observer high-level component
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
Copy the code

You can see the custom hooks we designed earlier that provide friendly code hints through Typescript features

That’s how mobx + typescript works in a functional component; If you have any questions, please feel free to comment 🙂

The resources

  • React Hook useContext
  • Mobx official documentation
  • Vite build case Vite-Concent-Pro