[Dry goods] 15 front-end infrastructure sharing for the company~

Table of contents

1. Project directory specification

2. Code writing specification

3. State manager optimization and unification

3.1 Optimizing state management

3.2 store directory structure

3.3 Define the state manager

3.4 Using the state manager

4. Unified management of local storage

5. Package request unification and project decoupling

6. Unified api interface management

7. Function library - general method extraction and reuse

8. Component library - general component extraction and reuse

Nine. Unification of css superset and css modular scheme

10. Introduce immer to optimize performance and simplify writing

Eleven. Build npm private server

12. General template packaging for various types of projects

Thirteen. Build the cli scaffolding to download the template.

Fourteen. git operation specification

15. Specification and use of documentation output documentation site


1. Project directory specification

        There are two commonly used methods for file directory organization. The second method adopted by the company later is more convenient. There is no best of the two methods, only the one that is more suitable for your company. As long as the company reaches an agreement, it will be very convenient to use either one.

1.1 Divided by Function Type

        According to the function type of the file, such as api , component , page , route , hooks , store , whether it is used globally or locally on a single page, it is placed in the corresponding directory under src according to the function type for unified management .

├─src               #  项目目录
│  ├─api                #  数据请求
│  │  └─Home            #  首页页面api
│  │  └─Kind            #  分类页面api
│  ├─assets             #  资源
│  │  ├─css             #  css资源
│  │  └─images          #  图片资源
│  ├─config             #  配置
│  ├─components         #  组件
│  │  ├─common            #  公共组件
│  │  └─Home              #  首页页面组件
│  │  └─Kind              #  分类页面组件
│  ├─layout             #  布局
│  ├─hooks              #  自定义hooks组件
│  ├─routes             #  路由
│  ├─store              #  状态管理
│  │  └─Home              #  首页页面公共的状态
│  │  └─Kind              #  分类页面公共的状态
│  ├─pages              #  页面
│  │  └─Home              #  首页页面
│  │  └─Kind              #  分类页面
│  ├─utils              #  工具
│  └─main.ts            #  入口文件

1.2 Divided by domain model

        According to the function of the page, the components and APIs that will be used globally are still placed under the src for global management. The APIs and components that are used separately in the page are placed in the folder of the corresponding page. When using it, you do not need to search for files up and down. It can be found under the folder, which is more convenient and the functions are more cohesive.

├─src               #  项目目录
│  ├─assets             #  资源
│  │  ├─css             #  css资源
│  │  └─images          #  图片资源
│  ├─config             #  配置
│  ├─components         #  公共组件
│  ├─layout             #  布局
│  ├─hooks              #  自定义hooks组件
│  ├─routes             #  路由
│  ├─store              #  全局状态管理
│  ├─pages              #  页面
│  │  └─Home              #  首页页面
│  │    └─components      #  Home页面组件文件夹
│  │    ├─api             #  Home页面api文件夹
│  │    ├─store           #  Home页面状态
│  │    ├─index.tsx       #  Home页面
│  │  └─Kind              #  分类页面
│  ├─utils              #  工具
│  └─main.ts            #  入口文件

2. Code writing specification

There are many specifications, here is just a brief list of the basic specification constraints and the use of tools to automate the specification code.

2.1 Component structure

react component

import React, { memo, useMemo } from 'react'

interface ITitleProps {
  title: string
}

const Title: React.FC<ITitleProps> = props => {
  const { title } = props

  return (
    <h2>{title}</h2>
  )
}

export default memo(Title)

        ITitleProps  starts with I to represent the type , the middle is the semantic Title , and the following Props is the type, representing the component parameters.

2.2 Define the interface

        Example 1: Login interface, define the parameter type and response data type, the parameter type directly defines the type of params , and the response data is placed in the paradigm , which needs to be handled well during encapsulation.

import { request } from '@/utils/request'

/** 公共的接口响应范型 */
export interface HttpSuccessResponse<T> {
  code: number
  message: string
  data: T
}

/** 登录接口参数 */
export interface ILoginParams {
  username: string
  password: string
}

/** 登录接口响应 */
export interface ILoginData {
  token: string
}

/* 用户登录接口 */
export const loginApi = (params: ILoginApi) => {
  return request.post<ILoginData>('/xxx', params)
}

2.3 Events

Starting with on represents an event, this is just a specification, on is shorter than handle , haha.

const onChange = () => {

}

2.4 Tool constraint code specification

In addition to the customary specifications, we also need some tools and plug-ins to help us better complete the specification.

code specification

  1. vscode [1]: Unified front-end editor.

  2. editorconfig [2]: Unified team vscode editor default configuration.

  3. prettier [3]: Save files to automatically format code.

  4. eslint [4]: ​​Detect code syntax specifications and errors.

  5. stylelint [5]: detect and format style file syntax

You can read my article: [Front-end engineering] Configure React+ts enterprise code specification and style format and git submission specification [6]

git commit specification

  1. husky [7]: It can monitor the execution of githooks [8], and do some processing operations during the execution phase of the corresponding hook .

  2. lint-staged [9]: Only detect the file code in the temporary storage area, and optimize the detection speed of eslint .

  3. pre-commit [10]: One of githooks , use tsc and eslint to check the grammar before submitting the commit .

  4. commit-msg [11]: One of the githooks , which detects the commit remark information before the commit is submitted .

  5. commitlint [12]: Detects commit remark information in the pre-commit phase of githooks .

  6. commitizen [13]: A standardized submission tool for git , which assists in filling in commit information.

You can read my article: [Front-end engineering] Configure React+ts enterprise code specification and style format and git submission specification [14]

3. State manager optimization and unification

3.1 Optimizing state management

        A simple state manager encapsulated with react context , with complete type promotion, supports use inside and outside the component, and has also been published to npm [ 15 ]

import React, { createContext,  useContext, ComponentType, ComponentProps } from 'react'

/** 创建context 组合useState 状态Store */
function createStore<T>(store: () => T) {
  // eslint-disable-next-line
  const ModelContext: any = {};

  /** 使用model */
  function useModel<K extends keyof T>(key: K) {
    return useContext(ModelContext[key]) as T[K];
  }

  /** 当前的状态 */
  let currentStore: T;
  /** 上一次的状态 */
  let prevStore: T;

  /** 创建状态注入组件 */
  function StoreProvider(props: { children: React.ReactNode }) {
    currentStore = store();
    /** 如果有上次的context状态,做一下浅对比,
     * 如果状态没变,就复用上一次context的value指针,避免context重新渲染
     */
    if (prevStore) {
      for (const key in prevStore) {
        // @ts-ignore
        if (shallow(prevStore[key], currentStore[key])) {
          // @ts-ignore
          currentStore[key] = prevStore[key];
        }
      }
    }
    prevStore = currentStore;
    // @ts-ignore
    let keys: any[] = Object.keys(currentStore);
    let i = 0;
    const length = keys.length;
    /** 遍历状态,递归形成多层级嵌套Context */
    function getContext<T, K extends keyof T>(
      key: K,
      val: T,
      children: React.ReactNode,
    ): JSX.Element {
      const Context =
        ModelContext[key] || (ModelContext[key] = createContext(val[key]));
      const currentIndex = ++i;
      /** 返回嵌套的Context */
      return React.createElement(
        Context.Provider,
        {
          value: val[key],
        },
        currentIndex < length
          ? getContext(keys[currentIndex], val, children)
          : children,
      );
    }
    return getContext(keys[i], currentStore, props.children);
  }

  /** 获取当前状态, 方便在组件外部使用,也不会引起页面更新 */
  function getModel<K extends keyof T>(key: K): T[K] {
    return currentStore[key];
  }

  /** 连接Model注入到组件中 */
  function connectModel<Selected, K extends keyof T>(
    key: K,
    selector: (state: T[K]) => Selected,
  ) {
    // eslint-disable-next-line func-names
    // @ts-ignore
    return function <P, C extends ComponentType<any>>(
      WarpComponent: C,
    ): ComponentType<Omit<ComponentProps<C>, keyof Selected>> {
      const Connect = (props: P) => {
        const val = useModel(key);
        const state = selector(val);
        // @ts-ignore
        return React.createElement(WarpComponent, {
          ...props,
          ...state,
        });
      };
      return Connect as unknown as ComponentType<
        Omit<ComponentProps<C>, keyof Selected>
      >;
    };
  }

  return {
    useModel,
    connectModel,
    StoreProvider,
    getModel,
  };
}

export default createStore

/** 浅对比对象 */
function Shallow<T>(obj1: T, obj2: T) {
  if(obj1 === obj2) return true
  if(Object.keys(obj1).length !== Object.keys(obj2).length) return false
  for(let key in obj1) {
    if(obj1[key] !== obj2[key]) return false
  }
  return true
}

3.2 store directory structure

├─src               #  项目目录
│  ├─store              #  全局状态管理
│  │  └─modules           #  状态modules
│  │    └─user.ts           #  用户信息状态
│  │    ├─other.ts          #  其他全局状态
│  │  ├─createStore.ts          #  封装的状态管理器
│  │  └─index.ts          #  store入口页面

3.3 Define the state manager

1. Introduce in store/index.ts

import { useState } from 'react'

/** 1. 引入createStore.ts */
import createStore from './createStore'

/** 2. 定义各个状态 */
// user
const userModel = () => {
  const [ userInfo, setUserInfo ] = useState<{ name: string }>({ name: 'name' })
  return { userInfo, setUserInfo }
}

// other
const otherModel = () => {
  const [ other, setOther ] = useState<number>(20)
  return { other, setOther }
}

/** 3. 组合所有状态 */
const store = createStore(() => ({
  user: userModel(),
  other: otherModel(),
}))

/** 向外暴露useModel, StoreProvider, getModel, connectModel */
export const { useModel, StoreProvider, getModel, connectModel } = store

2. Inject state through StoreProvider at the top level

// src/main.ts
import React from 'react'
import ReactDOM from 'react-dom'
import App from '@/App'
// 1. 引入StoreProvider
import { StoreProvider } from '@/store'

// 2. 使用StoreProvider包裹App组件
ReactDOM.render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  document.getElementById('root')
)

3.4 Using the state manager

1. Used in function components, with the help of useModel

import React from 'react'
import { useModel } from '@/store'

function FunctionDemo() {

  /** 通过useModel取出user状态 */
  const { userInfo, setUserInfo } = useModel('user')

  /** 在点击事件中调用setUserInfo改变状态 */
  const onChangeUser = () => {
    setUserInfo({
      name: userInfo.name + '1',
    })
  }

  // 展示userInfo.name
  return (
    <button onClick={onChangeUser}>{userInfo.name}--改变user中的状态</button>
  )
}

export default FunctionDemo

2. Used in class components, with the help of connectModel

import React, { Component } from 'react'
import { connectModel } from '@/store'

// 定义class组件props
interface IClassDemoProps {
  setOther: React.Dispatch<React.SetStateAction<string>>
  other: number
}

class ClassDemo extends Component<IClassDemoProps> {
  // 通过this.props获取到方法修改状态
  onChange = () => {
    this.props.setOther(this.props.other + 1)
  }
  render() {
    // 通过this.props获取到状态进行展示
    return <button onClick={this.onChange}>{this.props.other}</button>
  }
}

// 通过高阶组件connectModel把other状态中的属性和方法注入到类组件中
export default connectModel('other',state => ({
  other: state.other,
  setOther: state.setOther
}))(ClassDemo)

3. Use outside the component, with the help of getModel

You can also read and modify the state method in the component without causing an update

import { getModel } from '@/store'

export const onChangeUser = () => {
  // 通过getModel读取usel状态,进行操作
  const user = getModel('user')
  user.setUserInfo({
    name: user.userInfo.name + '1'
  })
}

// 1秒后执行onChangeUser方法
setTimeout(onChangeUser, 1000)

4. Unified management of local storage

You can simply encapsulate localStorage , sessionStorage , and cookies . The benefits of using after encapsulation:

  1. Automatic serialization, converting to a string when storing, and converting back when obtaining.

  2. The type is automatically inferred, the type is passed in when instantiating, and the type is automatically inferred when setting and getting the value.

  3. It can be managed in a unified way, and the local storage can be put in one file, so as to avoid the confusion and poor maintenance of local storage in the later stage.

  4. To smooth out platform differences, this idea is suitable for web , applets, mobile terminals, and desktop terminals.

// src/utils/storage.ts
const prefix = 'xxx.'

interface IStorage<T> {
  key: string
  defaultValue: T
}
export class LocalStorage<T> implements IStorage<T> {
  key: string
  defaultValue: T
  constructor(key, defaultValue) {
    this.key = prefix + key
    this.defaultValue = defaultValue
  }
  /** 设置值 */
  setItem(value: T) {
    localStorage.setItem(this.key, JSON.stringify(value))
  }
  /** 获取值 */
  getItem(): T {
    const value = localStorage[this.key] && localStorage.getItem(this.key)
    if (value === undefined) return this.defaultValue
    try {
      return value && value !== 'null' && value !== 'undefined'
        ? (JSON.parse(value) as T)
        : this.defaultValue
    } catch (error) {
      return value && value !== 'null' && value !== 'undefined'
        ? (value as unknown as T)
        : this.defaultValue
    }
  }
  /** 删除值 */
  removeItem() {
    localStorage.removeItem(this.key)
  }
}

Instantiate encapsulated local storage

// src/common/storage.ts
import { LocalStorage } from '@/utils/storage'

/** 管理token */
export const tokenStorage = new LocalStorage<string>('token', '')

/** 用户信息类型 */
export interface IUser {
    name?: string
    age?: num
}

/** 管理用户信息 */
export const userStorage = new Storage<IUser>('user', {})

Use within the page

import React, { memo, useMemo } from 'react'
import { userStorage } from '@/common/storage'

interface ITitleProps {
  title: string
}

const Title: React.FC<ITitleProps> = props => {
  const { title } = props
    
  useEffect(() => {
    userStorage.setItem({ name: '姓名', age: 18 })
    const user = userStorage.getItem()
    console.log(user) // { name: '姓名', age: 18 }
  }, [])

  return (
    <h2>{title}</h2>
  )
}

export default memo(Title)

5. Package request unification and project decoupling

5.1 Existing packages

        The current request encapsulation of the project is coupled with the project business logic, which is not convenient for direct reuse, and it is troublesome to use. Each time you need to pass the GET and POST types, the GET parameters must be processed separately each time, and the parameter type restrictions are weak.

5.2 Recommended use

It is recommended to use the fetch package or axios         directly . The secondary package is based on this in the project, and only focuses on the logic related to the project, not the implementation logic of the request. When the request is abnormal, Promise.reject() is not returned  , but an object is returned, but the code is changed to the code of the abnormal state , so that when used in the page, there is no try/catch package, and only if is used to judge whether the code is correct. .

import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import { tokenStorage } from '@/common/storage'
/** 封装axios的实例,方便多个url时的封装 */
export const createAxiosIntance = (baseURL: string): AxiosInstance => {
  const request = axios.create({ baseURL })
  // 请求拦截器器
  request.interceptors.request.use((config: AxiosRequestConfig) => {
    config.headers['Authorization'] = tokenStorage.getItem()
    return config
  })
  // 响应拦截器
  request.interceptors.response.use(
    response => {
      const code = response.data.code
      switch (code) {
        case 0:
          return response.data
        case 401:
          // 登录失效逻辑
          return response.data || {}
        default:
          return response.data || {}
      }
    },
    error => {
      // 接口请求报错时,也返回对象,这样使用async/await就不需要加try/catch
      // code为0为请求正常,不为0为请求异常,使用message提示
      return { message: onErrorReason(error.message) }
    }
  )
  return request
}

/** 解析http层面请求异常原因 */
function onErrorReason(message: string): string {
  if (message.includes('Network Error')) {
    return '网络异常,请检查网络情况!'
  }
  if (message.includes('timeout')) {
    return '请求超时,请重试!'
  }
  return '服务异常,请重试!'
}

export const request = createAxiosIntance('https://xxx')

5.3 use

Use the above code to name and define the loginApi example of the interface type

/** 登录 */
const onLogin = async () => {
  const res = await loginApi(params)
  if(res.code === 0) {
    // 处理登录正常逻辑
  } else {
    message.error(res.message) // 错误提示也可以在封装时统一添加
  }
}

6. Unified api interface management

folder path

├─pages                 #  页面
│  ├─Login              #  登录页面
│  │  └─api             #  api文件夹
│  │    └─index.ts      #  api函数封装
│  │    ├─types.ts      #  api的参数和响应类型

define type

// api/types.ts

/** 登录接口参数 */
export interface ILoginParams {
  username: string
  password: string
}

/** 登录接口响应 */
export interface ILoginData {
  token: string
}

Define the request interface

import { request } from '@/utils/request'
import { ILoginParams, ILoginData } from './types'

/* 用户登录接口 */
export const loginApi = (params: ILoginParams) => {
  return request.post<ILoginData>('/distribute/school/login', params)
}

Use the request interface

Use the above code to name and define the loginApi example of the interface type

/** 登录 */
const onLogin = async () => {
  const res = await loginApi(params)
  if(res.code === 0) {
    // 处理登录正常逻辑
  } else {
    message.error(res.message) // 错误提示也可以在封装时统一添加
  }
}

7. Function library - general method extraction and reuse

The methods and hooks         commonly used in the company's projects are extracted to form a function library , which is convenient for use in various projects. By writing function methods and jest unit tests, the overall level of team members can also be improved. At that time, both the interns and regular members of the front-end team were participating in the construction of the function library, and many of them had  30+  functions and hooks, which were still increasing.

        For the function library developed by dumi2 , you can read my article:

        [Front-end engineering] Use dumi2 to build a detailed tutorial of React component library and function library [16]

8. Component library - general component extraction and reuse

        If there are too many projects in the company, there will be many public components, which can be extracted to facilitate reuse by other projects. Generally, they can be divided into the following components:

  1. UI components

  2. business component

  3. Functional components: pull up to refresh, scroll to the bottom to load more, virtual scrolling, drag and drop sorting, lazy loading of pictures..

        Since the company's technology stack is mainly react , the component library also adopts the dumi2 solution, you can read my article

        [Front-end engineering] Use dumi2 to build a detailed tutorial of React component library and function library [17]

Nine. Unification of css superset and css modular scheme

css superset

        Use less or scss , depending on the specific situation of the project, it can be unified if the whole project can be unified.

css modularization

        Vue uses its own style scopedand react uses the css-module solution.

        It is also easy to enable. Take vite as an example, it is supported by default. You can modify the vite.config.ts configuration:

// vite.config.ts
export default defineConfig({
  css: {
    // 配置 css-module
    modules: {
      // 开启 camelCase 格式变量名转换
      localsConvention: 'camelCase',
      // 类名格式,[local]是自己原本的类名,[hash:base64:5]是5位的hash值
      generateScopedName: '[local]-[hash:base64:5]',
    }
  },
})

When using it, the style file name suffix needs to add  .module , for example index.module.less:

// index.module.less
.title {
 font-size: 18px;
  color: yellow;
}

Inside the component use:

import React, { memo, useMemo } from 'react'
import styles from './index.module.less'

interface ITitleProps {
  title: string
}

const Title: React.FC<ITitleProps> = props => {
  const { title } = props

  return (
    <h2 className={styles.title}>{title}</h2>
  )
}

export default memo(Title)

After compilation, the class name will become title-[hash:5]  , which can effectively avoid style conflicts and reduce the pain of class names.

10. Introduce immer to optimize performance and simplify writing

        Immer [18] is  an immutable  library   written by the author of  mobx . The core implementation is to use ES6  Proxy  (environments that do not support Proxy will automatically use Object.defineProperty to implement it) , and almost realize the   immutable data structure of js at the minimum cost. , is easy to use, small in size, and ingenious in design, which meets our needs for js immutable data structures.

1. Optimize performance

modify user information

const [ userInfo, setUserInfo ] = useState({ name: 'immer', info: { age: 6 } })
const onChange = (age: number) => {
  setUserInfo({...userInfo, info: {
    ...userinfo.info,
    age: age
  }})
}

        The above modification age has not changed, but a new object is generated every time when setUserInfo is set. If the reference changes before and after the update, the component will be refreshed.

After using immer , no new references will be generated when the age does not change, and the syntax is more concise, which can optimize performance.

import produce from 'immer'

const [ userInfo, setUserInfo ] = useState({ name: 'immer', age: 5 })
const onChange = (age: number) => {
  setUserInfo(darft => {
    darft.age = age
  })
}

2. Simplified writing

        React follows the concept of immutable data flow. Every time the state is modified, a new reference must be generated, and the original reference cannot be modified. Therefore, when operating on a reference type object or array, it is always necessary to make a shallow copy and then process it. , when the modified state level is deeper, the writing method will be more complicated.

Take an array as an example to modify the quantity of an item in the shopping cart:

import produce from 'immer'

const [ list, setList ] = useState([{ price: 100, num: 1 }, { price: 200, num: 1 }])

// 不使用用immer
const onAdd = (index: number) => {
  /** 不使用immer */
  // const item = { ...list[index] }
  // item.num++
  // list[index] = item
  // setList([...list])

  /** 使用immer */
  setList(
    produce(darft => {
      darft[index].num++
    }),
  )
}

3. You can use use-immer [19] to simplify the writing :

import useImmer from 'use-immer'

const [ list, setList ] = useImmer([{ price: 100, num: 1 }, { price: 200, num: 1 }])

const onAdd = (index: number) => {
  setList(darft => {
      darft[index].num++
  })
}

Eleven. Build npm private server

        It is not recommended to use too many third-party packages for the company's front-end projects. You can build the company's private npm server to host the company's own packaged state management library, request library, component library, and npm packages such as scaffolding cli , sdk, etc. , to facilitate reuse and management.

You can read my two articles, both of which can build npm private servers:

[Front-end engineering] Skillfully use Alibaba Cloud oss ​​service to create a front-end npm private warehouse [20]

[Front-end engineering] Use verdaccio to build the company's npm private library complete process and stepping records [21]

12. General template packaging for various types of projects

        According to the company's business needs in advance, we can encapsulate the corresponding general development templates for each end, package the project directory structure, interface requests, status management, code specifications, git specification hooks, page adaptation, permissions, local storage management, etc., to reduce The preparatory work time when developing new projects can also better unify the company's overall code specifications.

  1. General background management system basic template package

  2. Basic template package for general applets

  3. General h5 base template package

  4. General node base template package

  5. Other types of projects are packaged with default templates to reduce duplication of work.

Thirteen. Build the cli scaffolding to download the template.

        Build cli command line scaffolding like vue-clivitecreate-react-app to quickly select and download packaged templates, which is more convenient than git pulling code.

        For the implementation of specific cli scaffolding, please refer to my article:

        [Front-end engineering] From entry to proficiency, 100 lines of code to build your front-end CLI scaffolding road [22]

Fourteen. git operation specification

        Git operation specifications are also very important. If the process is not standardized, more complicated problems will easily occur. It is necessary to formulate a set of git flow development specifications suitable for your company based on the company's current situation and the industry's better practice plans, and use various restrictions to avoid If there is a problem, this specific stream specification will be summarized in an article later.

15. Specification and use of documentation output documentation site

        The code specification and git submission specification, as well as the usage instructions of each packaged library, should be output as documents and deployed online, so that new colleagues can quickly get familiar with and use them.

        This is very important. No matter how much infrastructure and specifications have been done, if there is no public document to consult, there is no way to quickly get familiar with it. Therefore, an online specification document is required to write all the specifications in it. bird.

Guess you like

Origin blog.csdn.net/lambert00001/article/details/131965892