Hello everyone, I am a bowl week, a front end that does not want to be drunk (inrolled). If I am lucky enough to write an article that you like, I am very lucky

Writing in the front

This article will build an enterprise usable project skeleton from 0, here I use the package management tool Yarn, don’t ask why, ask is like to use this; If you are NPM, simply replace yarn add with NPM I.

Install the Vue3 project through Vite

The installation is relatively simple, first enter the command

npm create vite
Copy the code

You will then be asked to enter the project name

Step 3 lets you select a framework, in this case Vue

For the last step we choose VUe-TS, which is vue +TypeScript,

Then it is created, as shown below:

Code specification

With the expansion of the team, everyone has his or her own coding style. However, if there are multiple coding styles in a project, the maintainability and readability of the code are greatly reduced. Therefore, a project specification is of great importance to the front-end team.

ESlint+Prettier

The two tools, one for code style checking and the other for formatting, are now configured.

First, install the dependencies:

yarn add eslint eslint-plugin-vue eslint-define-config --dev # eslink
yarn add prettier eslint-plugin-prettier @vue/eslint-config-prettier --dev# prettire
yarn add @vue/eslint-config-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser --dev # Support for TS
Copy the code

The second step is to compile the corresponding configuration file

.eslintrc.js

const { defineConfig } = require('eslint-define-config')

module.exports = defineConfig({
  root: true./* Specifies how to parse the syntax. * /
  parser: 'vue-eslint-parser'./* The priority is lower than parse's parsing configuration */
  parserOptions: {
    parser: '@typescript-eslint/parser'.// Modular solution
    sourceType: 'module',},env: {
    browser: true.es2021: true.node: true.// resolve defineProps and defineEmits generate no-undef warnings
    'vue/setup-compiler-macros': true,},// https://eslint.bootcss.com/docs/user-guide/configuring#specifying-globals
  globals: {},
  extends: [
    'plugin:vue/vue3-recommended'.'eslint:recommended'.'plugin:@typescript-eslint/recommended'.// typescript-eslint recommends rules,
    'prettier'.'plugin:prettier/recommended',].// https://cn.eslint.org/docs/rules/
  rules: {
    // Do not use var
    'no-var': 'error'.semi: 'off'.// Use interface instead of type
    '@typescript-eslint/consistent-type-definitions': ['error'.'interface'].'@typescript-eslint/no-explicit-any': 'off'.// You can use any type
    '@typescript-eslint/explicit-module-boundary-types': 'off'.// require() require statement not part of import statement. The problem of
    '@typescript-eslint/no-var-requires': 0.// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/ban-types.md
    '@typescript-eslint/ban-types': [
      'error',
      {
        types: {
          // add a custom message to help explain why not to use it
          Foo: "Don't use Foo because it is unsafe".// add a custom message, AND tell the plugin how to fix it
          String: {
            message: 'Use string instead'.fixWith: 'string',},'{}': {
            message: 'Use object instead'.fixWith: 'object',},},},],// Disallow unused variables
    '@typescript-eslint/no-unused-vars': [
      'error',
      { vars: 'all'.args: 'after-used'.ignoreRestSiblings: false},].'vue/html-indent': 'off'.// Disable the formatting rule for prettier,
    'vue/max-attributes-per-line': ['off'].// Hump is preferred, except for Element components
    'vue/component-name-in-template-casing': [
      'error'.'PascalCase',
      {
        ignores: ['/^el-/'.'/^router-/'].registeredComponentsOnly: false],},// Force the use of humps
    camelcase: ['error', { properties: 'always'}].// Const is preferred
    'prefer-const': [
      'error',
      {
        destructuring: 'any'.ignoreReadBeforeAssign: false,},],},})Copy the code

.eslintignore

/node_modules/
/public/
.vscode
.idea
Copy the code

.prettierrc

{
  "semi": false."singleQuote": true."printWidth": 80."trailingComma": "all"."arrowParens": "avoid"."endOfLine": "lf"
}

Copy the code

husky

Husky is a Git Hook that allows you to verify your commit, push, and commit information. Husky is a Git Hook that allows you to verify your commit information before a commit.

npx husky-init && npm install  # npm
npx husky-init && yarn         # Yarn 1
npx husky-init --yarn2 && yarn # Yarn 2+
Copy the code

After execution, a. Husky directory with a pre-commit file will appear in the root directory of the project. We will change NPM test to the command we need to execute as follows:

#! /bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint

Copy the code

Finally, let’s configure package.json with the following example code:

"scripts": {
  "lint": ""lint":"eslint src --fix --ext .js,.ts,.jsx,.tsx,.vue && prettier --write --ignore-unknown""
},
Copy the code
  • src: Target folder to verify;
  • --fix: automatic repair command;
  • --ext: Specifies the suffix of the detection file.

Now we will test and format the code before committing.

lint-staged

Once Husky has been configured, it is necessary to have code checked and formatted for the entire project regardless of whether one or two lines have been changed. Lint-staged can be used to have only git staging checked and formatted as follows:

First, install Lint-staged

yarn add lint-staged --dev
Copy the code

Second, configure package.json

{
  "scripts": {},
  / / new
  "lint-staged": {
    "*.{vue,js,ts,tsx,jsx}": [
      "eslint --fix"."prettier --write --ignore-unknown"]}}Copy the code

Husky /pre-commit:

#! /bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

Copy the code

At this point the configuration is complete.

A commit message specification

Commit Messages in good projects have a uniform style, which has the advantage of being able to quickly locate each commit for later versioning. Now let’s configure the Commit Message specification.

Submit specifications

  1. Install commitizen

    yarn add commitizen --dev
    Copy the code
  2. Here we use CZ-Xconventional – Changelog or SELECT CZ-Customizable, and we will install it first

    yarn add cz-conventional-changelog --dev
    Copy the code
  3. Modify package.json as follows:

    "config": {
      "commitizen": {
        "path": "./node_modules/cz-conventional-changelog"}}Copy the code
  4. Commit using the CZ CLI tool

    yarn cz # 或者 npx cz
    Copy the code

    The first step is to select the update type. The functions of each type are shown in the following table:

    Type role
    feat The new features
    fix Fix the Bug
    docs Modify the document
    style Code format modification
    refactor Code refactoring
    perf To improve the performance
    test test
    build Changing project builds or external dependencies (e.g. scopes: webpack, gulp, NPM, etc.)
    ci Change the continuous integration software configuration file andpackage.jsonIn thescriptsThe command
    chore Changing the build process or ancillary tools (such as changing the test environment)
    revert Code back

    The second step is to fill in the scope of the change, which can be component name or file name. The third step is to fill in the information of the submission. The fourth step is to fill in the detailed description of the submission

  5. We can also configure a script with the following example: package.json

    "scripts": {
      "commit": "cz"
    },
    Copy the code

    After the configuration, we can commit the code through YARN Commit.

Utools plug-in

If you are using uTools, here is a plugin for uTools, which is written by a friend of mine. The plugin is as follows:

This version also added a shortcut key copy operation, for the following reasons:

Big brother can place, there is demand really write ah.

VScode plug-in

Using the command line to submit may not be very friendly for some students. Here is a Visual Studio Code Commitizen Support plug-in that can be used graphically.

The operation process is very simple. After installation, click on the blue icon in the Source Control panel, as shown below

Then follow through and commit automatically.

Another good plug-in, the Commit Message Editor, is a graphical interface with both forms and text boxes.

The message validation

Now that we have a commit specification defined, we do not prevent non-commitlint from committing to the specification. Here we implement a validation rule for the commit information through commitLint and HusKY.

First, install the dependencies

yarn add @commitlint/config-conventional @commitlint/cli --dev
Copy the code

Step 2, create commitlint.config.js and configure commitLint

module.exports = {
  extends: ['@commitlint/config-conventional'],}Copy the code

For more configuration items, see the official documents

Third, use husky to generate the commit-msg file and verify the commit information

yarn husky add .husky/commit-msg "npx --no-install commitlint --edit The $1"
Copy the code

At this point, commitlint configuration is complete, and if message is not written properly, commit is blocked.

Configuration Vite

Vite is an open source packaging tool of Utah University. So far, I have used Vite in two projects. The overall feeling is not bad, and the development environment is really fast.

Here I introduce a basic configuration:

The alias configuration

Configuring aliases helps us find components, images, etc. quickly without using.. /.. /.. /, first configure vite.config.ts and resolve. Alias.

import { resolve } from 'path'

export default defineConfig(({ mode }: ConfigEnv) = > {
  return {
    resolve: {
      alias: {
        The '@': resolve(__dirname, 'src'),
        cpns: resolve(__dirname, 'src/components'),},extensions: ['.js'.'.json'.'.ts'.'.vue'].// You can add or subtract the suffix you want to omit when using the path alias
    },
    /* more config */}})Copy the code

Json: tsconfig.json: tsconfig.json: tsconfig.json: tsconfig.json: tsconfig.json: tsconfig.json: tsconfig.json

"compilerOptions": {
  // Set the base directory for resolving non-relative module names. Relative modules are not affected by baseUrl
  "baseUrl": "."."paths": {
    // Used to set the module name to baseUrl based path mapping
    "@ / *": [ "src/*"]."cpns/*": [ "src/components/*"]}},Copy the code

The environment variable

The env file

In Vite, you can read the configuration as an environment variable using a.env file. By default, Vite allows you to use the following files:

.env                # load in all cases
.env.local          In all cases it is loaded, but is ignored by Git
.env.[mode]         Load only in specified mode
.env.[mode].local   # load only in the specified mode, but git ignores it
Copy the code

Env <.env.local<.env.[mode]<.env.[mode].local; Vite also presets some environment variables, which have the highest priority and will not be overridden, as follows:

  • MODE: {string}: Application running mode (in the development environmentdevelopment, the generation environment isproduction).
  • BASE_URL: {string}: Basic URL for deploying applications. He madebaseConfiguration item decision.
  • PROD: {boolean}: Indicates whether the current production environment is used.
  • DEV: {boolean}: Whether the current development environment (forever withPRODOn the contrary).

These environment variables, Vite, allow us to obtain them through import.meto.env.

Defining environment variables

If we want to customize an environment variable, we must start with VITE_. If we want to modify it, we need to use the envPrefix configuration item, which allows us to pass in a non-empty string as the prefix to the variable.

.env

VITE_APP_API_BASE_URL = http://127.0.0.1:8080/Copy the code

Once the definition is complete, we can fetch it in the project as import.meta.env.vite_app_API_base_URL.

To get TypeScript type hints, create a SRC /env.d.ts.

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_API_BASE_URL: string
  // Define more environment variables
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

Copy the code

You get a smart reminder when you use it.

Get the environment variables in viet.config. ts

If we want to get environment variables in vite.config.ts, we need to use the loadEnv() method provided by Vite, which is defined as follows:

function loadEnv(
  mode: string, 
  envDir: string, prefixes? :string | string[]
) :Record<string.string>
Copy the code

The above three parameters are interpreted as follows:

  • mode: mode;
  • envDir: directory where environment variable configuration files reside.
  • prefixes: [Optional] Accepted environment variable prefix. The default value isVITE_.

With the API in mind, sample code to get the environment variable in viet.config. ts is as follows:

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import type { ConfigEnv } from 'vite'

// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv) = > {
  const env = loadEnv(mode, process.cwd())
  return {
    /* more config */
    server: {
      proxy: {
        '/api': {
          target: env.VITE_APP_API_BASE_URL,
          changeOrigin: true.rewrite: path= > path.replace(/^\/api/.' '),},},},}})Copy the code

Project depend on

The installation Element – plus

  1. The installation Element – plus

    yarn add element-plus
    Copy the code
  2. Volar support If you use Volar, specify the global component type through compileroptions. type in tsconfig.json.

    // tsconfig.json
    {
      "compilerOptions": {
        // ...
        "types": ["element-plus/global"]}}Copy the code
  3. Import on demand Install related plug-ins to implement automatic import on demand

    yarn add unplugin-vue-components unplugin-auto-import --dev
    Copy the code

    vite.config.js

    import AutoImport from 'unplugin-auto-import/vite'
    import Components from 'unplugin-vue-components/vite'
    import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
    
    export default {
      plugins: [
        // ...
        AutoImport({
          resolvers: [ElementPlusResolver()],
        }),
        Components({
          resolvers: [ElementPlusResolver()],
        }),
      ],
    }
    Copy the code

Tailwind CSS

  1. The installation

    yarn add tailwindcss postcss autoprefixer --dev
    Copy the code
  2. Initialize the

    yarn tailwindcss init -p
    Copy the code
  3. Configuration tailwind. Config. Js

    module.exports = {
      content: ['./index.html'.'./src/**/*.{vue,js,ts,jsx,tsx}'].theme: {
        extend: {},},plugins: [].// tree shaking
      purge: [
        './src/**/*.html'.'./src/**/*.vue'.'./src/**/*.jsx'.'./src/**/*.tsx',]}Copy the code
  4. Configuration postcss. Config. Js

    module.exports = {
      plugins: {
        tailwindcss: {},
        autoprefixer: {},}},Copy the code
  5. Introduce Tailwind CSS./ SRC /index.css

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    Copy the code

    ./src/main.ts

    import { createApp } from 'vue'
    import App from './App.vue'
    import './index.css'
    createApp(App).mount('#app')
    
    Copy the code

VueRouter

First, install the VueRouter

yarn add vue-router@next
Copy the code

Second, create the VueRouter entry file

src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

// Configure routing information
const routes: RouteRecordRaw[] = []

const router = createRouter({
  routes,
  history: createWebHistory(),
})

export default router

Copy the code

Third, introduce the VueRouter in main.ts

import { createApp } from 'vue'
import App from './App.vue'
Vue / / in the router
import router from './router'
import './assets/css/index.css'

createApp(App).use(router).mount('#app')

Copy the code

pinia

Step one, install pinia

yarn add vuex@next
Copy the code

Step 2, create the VUEX entry file

src/store/index.ts

import { createPinia } from 'pinia'

const store = createPinia()

export default store

Copy the code

Third, introduce Pinia in main.ts

import { createApp } from 'vue'
import App from './App.vue'
Vue / / in the router
import router from './router'
/ / introduce vuex
import store from './store'
import './assets/css/index.css'

createApp(App).use(router).use(store).mount('#app')

Copy the code

Step 4, create test data

src/store/modules/count.ts

import { defineStore } from 'pinia'
import type { CountInterface } from './types'
export const useCountStore = defineStore({
  id: 'count'.// ID Must be unique
  // state
  state: (a) :CountInterface= > {
    return {
      count: 0,}},// getters
  getters: {
    doubleCount: state= > {
      return state.count * 2}},// actions
  actions: {
    // Actions also support asynchronous writing
    countAdd() {
      // The contents of state can be accessed through this
      this.count++
    },
    countReduce() {
      this.count--
    },
  },
})

Copy the code

src/store/modules/type.ts

export interface CountInterface {
  count: number
}

Copy the code

The last step is to import and export all contents in SRC /store/index.ts for convenience. The code is as follows:

import { createPinia } from 'pinia'
import { useCountStore } from './modules/count'
const store = createPinia()

export default store
export { useCountStore }

Copy the code

The test code is as follows:

<template>
  <div class="index">
    <span>Current value {{countComputed}}</span>
    <br />
    <span>Double value {{doubleCount}}</span>
    <br />
    <el-button type="primary" size="default" @click="countStore.countAdd">+ 1</el-button>
    <el-button type="primary" size="default" @click="countStore.countAdd">- 1</el-button>
  </div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useCountStore } from '@/store'
import { storeToRefs } from 'pinia'
const countStore = useCountStore()
// By calculating attributes
const countComputed = computed(() = > countStore.count)
// Through the storeToRefs API structure
const { doubleCount } = storeToRefs(countStore)
</script>
<style scoped></style>

Copy the code

Encapsulation Axios

As the front-end, Axios uses the highest HTTP request library, and the weekly downloads have reached more than 20 million. We also use Axios in this project. Here, we carry out secondary encapsulation of Axios through TS to facilitate our use in the project.

This piece of encapsulation process is more complex, the process refer to my last article, I jump; The complete code is as follows:

src\service\index.ts

import Request from './request'

import type { RequestConfig } from './request/types'
interface YWZRequestConfig<T> extendsRequestConfig { data? : T }interface YWZResponse<T> {
  statusCode: number
  desc: string
  result: T
}

const request = new Request({
  baseURL: import.meta.env.BASE_URL,
  timeout: 1000 * 60 * 5.interceptors: {
    // Request interceptor
    requestInterceptors: config= > config,
    // Response interceptor
    responseInterceptors: result= > result,
  },
})

/ * * *@description: Function description *@interface D Interface * of the request parameter@interface T Response structure intercept *@param {YWZRequestConfig} Config uses data * for both GET and POST requests@returns {Promise}* /
constywzRequest = <D, T = any>(config: YWZRequestConfig<D>) => { const { method = 'GET' } = config if (method === 'get' || method === 'GET') { config.params = Config. Data} return request. Request <YWZResponse<T>>(config)} export const cancelRequest = (url: String | string []) = > {return request. CancelRequest (url)} / / cancel all requests export const cancelAllRequest = () = > {return request.cancelAllRequest() } export default ywzRequestCopy the code

src\service\request\index.ts

import axios, { AxiosResponse } from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import type {
  RequestConfig,
  RequestInterceptors,
  CancelRequestSource,
} from './types'

class Request {
  / / axios instance
  instance: AxiosInstance
  // Interceptor objectinterceptorsObj? : RequestInterceptors/ * storing way to cancel a collection of * will cancel the request after creating the request to push the collection in one * encapsulates a method, you can cancel the request, the incoming url: string | string [] * determine whether the same url exists before the request, if there's a cancellation request * /cancelRequestSourceList? : CancelRequestSource[]/* Holds a collection of all requested urls * the URL needs to be pushed into the collection before the request * removes the URL from the collection after the request is complete * adds are done before the request is sent, deletes deletes after the response */requestUrlList? :string[]

  constructor(config: RequestConfig) {
    this.requestUrlList = []
    this.cancelRequestSourceList = []
    this.instance = axios.create(config)
    this.interceptorsObj = config.interceptors
    // Interceptor execution sequence interface request > instance request > global request > instance response > global response > interface response
    this.instance.interceptors.request.use(
      (res: AxiosRequestConfig) = > res,
      (err: any) = > err,
    )

    // Use instance interceptor
    this.instance.interceptors.request.use(
      this.interceptorsObj?.requestInterceptors,
      this.interceptorsObj?.requestInterceptorsCatch,
    )
    this.instance.interceptors.response.use(
      this.interceptorsObj?.responseInterceptors,
      this.interceptorsObj?.responseInterceptorsCatch,
    )
    // The global response interceptor guarantees last execution
    this.instance.interceptors.response.use(
      // Since the data of our interface is under res.data, we return res.data directly
      (res: AxiosResponse) = > {
        return res.data
      },
      (err: any) = > err,
    )
  }
  / * * *@descriptionCancelRequestSourceList: retrieves the index * of the specified URL in cancelRequestSourceList@param {string} url
   * @returns {number} Index position */
  private getSourceIndex(url: string) :number {
    return this.cancelRequestSourceList? .findIndex((item: CancelRequestSource) = > {
        return Object.keys(item)[0] === url
      },
    ) as number
  }
  / * * *@description: Delete requestUrlList and cancelRequestSourceList *@param {string} url
   * @returns {*}* /
  private delUrl(url: string) {
    const urlIndex = this.requestUrlList? .findIndex(u= > u === url)
    const sourceIndex = this.getSourceIndex(url)
    // Delete the URL and cancel methodurlIndex ! = = -1 && this.requestUrlList? .splice(urlIndexas number.1) sourceIndex ! = = -1 &&
      this.cancelRequestSourceList? .splice(sourceIndexas number.1)
  }
  request<T>(config: RequestConfig): Promise<T> {
    return new Promise((resolve, reject) = > {
      // If we set up interceptors for a single request, we use interceptors for a single request
      if (config.interceptors?.requestInterceptors) {
        config = config.interceptors.requestInterceptors(config)
      }
      const url = config.url
      // url Contains the save cancel request method and the current request URL
      if (url) {
        this.requestUrlList? .push(url) config.cancelToken =new axios.CancelToken(c= > {
          this.cancelRequestSourceList? .push({ [url]: c, }) }) }this.instance
        .request<any, T>(config)
        .then(res= > {
          // If we set interceptors for a single response, we use interceptors for a single response
          if (config.interceptors?.responseInterceptors) {
            res = config.interceptors.responseInterceptors<T>(res)
          }

          resolve(res)
        })
        .catch((err: any) = > {
          reject(err)
        })
        .finally(() = > {
          url && this.delUrl(url)
        })
    })
  }
  // Cancel the request
  cancelRequest(url: string | string[]) {
    if (typeof url === 'string') {
      // Cancel a single request
      const sourceIndex = this.getSourceIndex(url)
      sourceIndex >= 0 && this.cancelRequestSourceList? .[sourceIndex][url]() }else {
      // There are multiple addresses to cancel the request
      url.forEach(u= > {
        const sourceIndex = this.getSourceIndex(u)
        sourceIndex >= 0 && this.cancelRequestSourceList? .[sourceIndex][u]() }) } }// Cancel all requests
  cancelAllRequest() {
    this.cancelRequestSourceList? .forEach(source= > {
      const key = Object.keys(source)[0]
      source[key]()
    })
  }
}

export default Request
export { RequestConfig, RequestInterceptors }

Copy the code

src\service\request\types.ts

import type { AxiosRequestConfig, AxiosResponse } from 'axios'
export interface RequestInterceptors {
  // Request interceptionrequestInterceptors? :(config: AxiosRequestConfig) = >AxiosRequestConfig requestInterceptorsCatch? :(err: any) = > any
  // Response interceptionresponseInterceptors? :<T = AxiosResponse>(config: T) => T responseInterceptorsCatch? : (err: any) => any} export Interface RequestConfig extends AxiosRequestConfig {interceptors? : RequestInterceptors } export interface CancelRequestSource { [index: string]: () => void }Copy the code

src\api\index.ts

import request from '@/service'
interface Req {
  apiKey: stringarea? :stringareaID? :string
}
interface Res {
  area: string
  areaCode: string
  areaid: string
  dayList: any[]}export const get15DaysWeatherByArea = (data: Req) = > {
  return request<Req, Res>({
    url: '/api/common/weather/get15DaysWeatherByArea'.method: 'GET',
    data,
    interceptors: {
      requestInterceptors(res) {
        return res
      },
      responseInterceptors(result) {
        return result
      },
    },
  })
}

Copy the code

Write in the last

At this point, the basic skeleton of the project has been built, and dependencies can be added dynamically according to the needs of the project.

The project template has been open source on Github, click Star to increase my motivation ~