Detailed steps for building vite+vue3+ts project (configuring multi-language version)


Preface

  • Times have changed, and we have witnessed examples such aswebpack, Rollup and Parcel and other tools, which have greatly improved the development experience of front-end developers.
  • However, as we start building larger and larger applications, the amount of JavaScript code that needs to be processed grows exponentially. Large projects containing thousands of modules are quite common. Tools developed based on JavaScript will begin to encounter performance bottlenecks: it often takes a long time (even minutes!) to start the development server, and even using hot module replacement (HMR), the effects of file modifications will take several seconds to reflected in the browser. In this cycle, slow feedback will greatly affect the development efficiency and happiness of developers.
  • Vite aims to address these issues by taking advantage of new developments in the ecosystem: browsers are starting to support ES modules natively, and JavaScript tools are increasingly written in compiled languages.

The above is quoted from the description on Vite’s official website.

Why record this article in detail? That's because when I entered the current company, there wasn't even a basic front-end architecture. Every time I did a project, I reused those bloated code templates on the Internet (make one set and throw another away, without reservations) Universal front-end template framework). Then this project team used to do development with vue2. Recently, the leader said that future projects will consider using vue3 for development, so I am the only pure front-end person in this project team (the others are all C# big guys, and many projects are not front-end or back-end). separation), you have to consider building a custom vue3 architecture from scratch. Combining the experience of previous projects, I built this front-end architecture from scratch (since there is no boss to lead it, I can only check the information bit by bit and step by step I have been exploring for a while and encountered many problems during this period). If there are any areas for optimization, please leave comments and exchanges. Hold your fist!

Then let’s build it step by stepvite+vue3+ts~


Tips: This article is configured step by step according to the configuration, so it is a little longer. If you are a novice, it is recommended to configure it step by step, and it will not take a long time.

1. Build a basic project template

  • You need to install it firstNode
    First look at the version after installing node
node -v

Insert image description here

Tips Since Vite is not supported in lower node versions, it is recommended to install the latest stable version, or 14.18+

  • Install the latest version of Vue basic template

The current basic project template is built according to the latest version of vue (for other installation methods, you can view more on the asynchronous vite official website)
The installation command is as follows:

npm init vue@latest

When executing the above, some configurations will need to be selected. The specific selections for this project are as follows:
Insert image description here
After execution, the directory will be generated as shown below:
Insert image description here

  • Install dependencies
npm i
  • Project implementation
npm run dev
  • Visit the page
    After following the above operations, visit http://localhost:5173/ and the following content will be displayed
    Insert image description here

Because the current demo is based onelement-plus, install it firstnpm i element-plus -S
and then apply it inmain.js

import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/index.css'
...
const app = createApp(App)
...
app.use(ElementPlus)

2. Build and optimize the project structure based on the basic template (continuously updated)

Since the initialized basic project structure is relatively simple, it is generally necessary to optimize the structure and configure related information according to personal habits.

A. Environment configuration optimization

The above basic template can be seen, but different environment variables are not seen. In fact, there are test environment and production environment. You can use import.meta to print out the details.
inmain.tsoutputimport.meta.env
Insert image description here

Then executenpm run dev. Looking at the page console, you can see that the value of MODE is"development"
Insert image description here
Execute npm run build-only, place the files in the built dist on the server.

  • Recommended for local developmentnginx (recommended), please refer toNginx usage and configuration details in Window and Mac environments
  • You can also useexpress-generatorto turn on the service
  • Of course, you can also access it directly in development and testinghttp://localhost:5173/dist/index.html, but this requires modifying the corresponding file introduction path. If you want to know more, you can do your own research

Then visit and you will see that the console prints out the value of MODE as"production"
Insert image description here

However, since the project may have different environmental requirements from development to launch, we need to configure environment variables to correspond to the APIs of different environments. After configuration, if there are no special modifications, there is no need to modify the environment configuration related. In this way This can avoid environment errors after deployment and build.

The normal process is generally divided into

  • Development environment (the API of the corresponding environment can be linked according to the actual development situation)
  • sit environment (test)
  • uat environment (business acceptance)
  • prod environment (online environment)

Configure the environment variables first, and create new files for different environments in the root directory (the specific configuration key defaults to starting with VITE_, but can also be configuredenvPrefix' Value customized)

  • .env (this is a public environment configuration, and the configuration information inside will be obtained)
  • .env.development
  • .env.production
  • .env.sit
  • .env.uat
    Insert image description here
    The .env file is a shared configuration, so even if the configuration configuration corresponds to the execution script environment, it will be obtained
# 公有全局环境配置

VITE_STORE_NAME = invade-

Insert image description here

If you want to execute different commands to read the corresponding environment configuration file, you need to configure the corresponding script inpackage.json. After executing the script, vite will read the corresponding environment file according to the compiled command. The new additions are as follows

{
    
    
  ...
  "scripts": {
    
    
    "dev": "vite serve --mode development",
    "dev-sit": "vite serve --mode sit",
    "dev-uat": "vite serve --mode uat",
    "dev-prod": "vite serve --mode production",
    "sit": "vite build --mode sit",
    "uat": "vite build --mode uat",
    "prod": "vite build --mode production",
    ...
  },
  ...
}
  • vite serve corresponds to local development and joint debugging testing. Different environments may require local joint debugging and testing problems, so there aredev-sit these. If not specified--modedevelopment
  • vite build is built and packaged for execution. If the value after --mode is not specified, the default is production
  • --mode is followed by the configured environment variable value, which corresponds to the value of MODE seen on the console in the screenshot above

However, we feel that the project structure does not look that elegant (mainly because we don’t want to update too many directory files, which will affect reading).
According to the configuration provided by the official website, you can customize a folder to place these environment configuration files. Then we will create a new folder env in the root directory (the name of this file can be customized and corresponds to the envDir configuration below value), move all environment configuration files intoInsert image description here
and need to modifyvite.config.ts the configuration, specifyenvDir: 'env',

import {
    
     fileURLToPath, URL } from 'node:url'

import {
    
     defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
    
    
  // env配置文件变量前缀, 默认 VITE_,可自行定义
  envPrefix: 'VITE_',
  // env配置文件夹位置
  envDir: 'env',
  plugins: [vue(), vueJsx()],
  resolve: {
    
    
    alias: {
    
    
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

Example .env.developmentPlacement

# 本地开发环境的代理配置配置, 任何代理请写好注释

# 接口地址
 VITE_PROXY_API=http://localhost:8000
 
# 构建后文件引用 相对路径
 VITE_PUBLIC_PATH=/

# 输出路径
 VITE_OUT_DIR=/dist/

Executenpm run dev
console printimport.meta.env, you can see .env and .env.development Configuration information, as follows
Insert image description here
For example.env.sitconfiguration

# 本地开发环境的代理配置配置, 任何代理请写好注释

# 接口地址
 VITE_PROXY_API=http://localhost:8001
 
# 构建后文件引用 相对路径
 VITE_PUBLIC_PATH=/

# 输出路径
 VITE_OUT_DIR=/dist/

Executionnpm run dev-sit (Of course, what is printed after executing npm run sit after building is the same, and the same is true in other environments)
Console prints import.meta.env, and you can see the configuration information of .env and .env.sit, and VITE_PROXY_API The value is different from that of the dev environment, as follows
Insert image description here

B. After optimizing the production build, the console and debugger are closed.

Since production generally avoids printing relevant information on the console, Vite also has this kind of configuration optimization. Without further ado, let’s go directly to the configuration.

import {
    
     defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
    
    
  ...
  build: {
    
    
    minify: "terser", // 必须开启:使用 terserOptions 才有效果
    terserOptions: {
    
    
      compress: {
    
    
        drop_console: true,
        drop_debugger: true,
      },
    },
  }
})

If you build directly after this configuration, an error will be reported. terser not found.Prompt that you need to install thisterser. You can execute npm i terser -D to install it.
Insert image description here
After installing terser, the build was successful again. Then access the built content and find that console.log(import.meta.env) is not printed out, indicating that the configuration has taken effect.
Insert image description here
However, with this configuration, it is found that if the test environment wants to see the printing information, judgment errors, etc., there is still a problem. You have to modify the configuration and build it again, which is obviously unfriendly, so we can slightly optimize it to only take effect in the prod environment, that is, use the callback function's return parameters to configure the configuration in defineConfig (Looking at the source code, you can see that there are two parameters for defineConfig), as follows

import {
    
     defineConfig, loadEnv } from 'vite'
// https://vitejs.dev/config/

export default defineConfig(({
    
     command, mode, ssrBuild }) => {
    
    
  const env = loadEnv(mode, `${
      
      process.cwd()}/env`, '')
  // 为什么要这样做,是为了 process.env和mode一致性
  Object.assign(process.env, env, {
    
     NODE_ENV: mode })
  return {
    
    
    mode,
  	...
  	build: {
    
    
      minify: "terser", // 必须开启:使用 terserOptions 才有效果
	  terserOptions: {
    
    
	    compress: {
    
    
	      drop_console: process.env.NODE_ENV === 'production' ? true : false, // 也可直接使用mode进行判断
          drop_debugger: process.env.NODE_ENV === 'production' ? true : false,
	    },
     },
    }
  }
})

You can see that there are three parameters,ssrBuild is optional

command: 'build' | 'serve'; // 二选一,默认为serve ,可在执行脚本配置(配置在vite 后面) 例如 "dev": "vite serve --mode development",
mode: string; // 自定义 一般会设置为 development | sit | uat | production
/**
 * @experimental
 */
ssrBuild?: boolean; // 看起来应该是配置SSR模式使用的,后面有机会再ssr模式下详细记录下

Combined with the above configuration, the printing is still there when buildingsit | uat, but it disappears when buildingproduction, which means that our configuration is successful.

Then why not just useprocess.env.NODE_ENV to judge directly? Because the default value of process.env.NODE_ENV in the development environment is development, even if dev-sit is run, it will still be development. Let’s look at a screenshot, so we need to use the callback function to judge based on the configured--mode
Insert image description here

C. Optimize file reference paths

In many cases, different pages need to use the same component, and the introduced paths may be inconsistent, so how can they be unified? In fact, after initializing the build project, the framework itself has already generated the most basic file points. Take a look at the configuration.

import {
    
     fileURLToPath, URL } from 'node:url'
...
import {
    
     defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
    
    
  ...
  resolve: {
    
    
    alias: {
    
    
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  ...
})

refers to the relevant configuration pointer indefineConfig configurationresolve. If the pointer./src does not meet the needs, you can customize it yourself. (We have to get used to using an @. The prerequisite is to rationalize the project directory structure, that is, not to nest too deeply and divide the directories according to modules)

How to use it, import it into the required file, for example, introduce the /src/views/HomeView.vue file in /src/components/index/demo.ts
The following code can be seen This can be avoided by using the configuration to point to the file imported from the multi-layer ../ File introduction errors, etc.alias@./src

// import HomeView from '../../views/HomeView.vue'
import HomeView from '@/views/HomeView.vue'

D. Optimize project global file type declaration configuration (generic type of ts)

  • .d.tsFile, which is used for type declaration (declare). It is only used for type detection and tells TypeScript what types we have;
  • dYes (declare, statement)
  • declare module** Statement file
  • declare namespace** Declare namespace

The root directory created by initialization has a env.d.ts file, which will be imported in the tsconfig.app.json file (if not configured, some file imports will report an error because there is no Declare it anywhere. Some of them will have their own declarations. For example, document is declared in the lib.dom.d.ts file. For details, you can press ctrlKeyboard and left mouse button click to view)

If there are multiple such configuration files, you have to manually introduce them one by one in thetsconfig.app.json file, for example, add a newwechat.d.ts file configuration a>weixin-js-sdkGlobal.

{
    
    
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "wechat.d.ts"],
  ...
}

In fact, we can also configure this configuration directly in theenv.d.ts file, but what if there are more configurations? There are many configurations together. Maybe when modifying or adding a certain configuration, other configurations are accidentally deleted. How to avoid this situation?

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

declare module 'weixin-js-sdk'

You can see thatenv.d.ts the configuration file ends with .d.ts, then let’s optimize the above configuration and create a new one in the root directory first< a i=3> folder, and then put the file in, and modify the file (generally if the is modified, it will automatically Modify at the introduced position), that is, is changed to typesenv.d.tstsconfig.app.jsonenv.d.ts"env.d.ts""types/env.d.ts"

{
    
    
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["types/env.d.ts", "src/**/*", "src/**/*.vue"],
  ...
}

Then optimizetsconfig.app.json configuration"types/env.d.ts" and change it to"types/*.d.ts", that is, dynamically introduce all items in the types folder< a file ending with i=4>, as follows.d.ts

{
    
    
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["types/*.d.ts", "src/**/*", "src/**/*.vue"],
  ...
}

New ·global.d.ts

/**
 * 自定义全局类型
 */
declare module 'js-cookie'

type IfEquals<X, Y, A=X, B=never> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? A : B;

/** 获取可编辑key */
export type WritableKeys<T> = {
    
    
  [P in keyof T]-?: IfEquals<{
    
    
    [Q in P]: T[P];
  }, {
    
    
    -readonly [Q in P]: T[P];
  }, P>
}[keyof T];

New·shims-vue.d.ts, configure some declaration files, such as .vue format files, lodash, various file formats, etc.


declare module '*.vue' {
    
    
  import type {
    
     DefineComponent } from 'vue';

  const component: DefineComponent<{
    
    }, {
    
    }, any>;
  export default component;
}

// declare module '*.vue' {
    
    
//   import { DefineComponent } from 'vue'
//   const component: DefineComponent<{}, {}, any>
//   export default component
// }


/**
 * window上属性自定义
 */
interface Window {
    
    
  [key: string]: string | number | Types.Noop | Types.PlainObject | unknown;
  WeixinJSBridge: any;
}

declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module '*.mp3';

declare module 'lodash';

declare module '@/utils/pictureVerfy/pawebjs.min.js';

You can configure the above according to your needs

E. Configure css global variables

We use less processing here and need to install the following dependency support

npm i less -D
npm i less-loader -D

General projects will have many reused colors, layout position information, etc. At this time, we can use less to dynamically configure the corresponding variables to use the function
First, we must create a new .less The file is created in with the following content./src/assets/variables.less

/**
 * 定义全局使用的 less 变量
 */
 @paddingDefault: 32px;
/**
 * 颜色值
 */
 @a94442: #a94442;

Then configurevariables.less the file to the global project, otherwise you will not be able to use these defined variables

import {
    
     fileURLToPath, URL } from 'node:url'
import {
    
     resolve } from 'path'
import {
    
     defineConfig} from 'vite'

// https://vitejs.dev/config/
export default defineConfig(({
    
     command, mode, ssrBuild }) => {
    
    
  ...
  return {
    
    
    ...
    css: {
    
    
      preprocessorOptions: {
    
    
        // less: {
    
    
        //     javascriptEnabled: true,
        //     additionalData:  `@import "${resolve(__dirname, 'src/assets/styles/base.less')}";`
        // }
        less: {
    
    
          modifyVars: {
    
    
            hack: `true; @import (reference) "${
      
      resolve('src/assets/styles/variables.less')}";`,
          },
          javascriptEnabled: true
        }
      }
    },
    build: {
    
    
      ...
    }
  }
})

Usage: After the above configuration, you can use it directly where you need to use it.

<div class="content"></div>
<style lang="less">
.content {
      
      
  background: @a94442;
  width: @paddingDefault;
  height: @paddingDefault;
}
</style>

As shown in the figure below, the corresponding value has becomevariables.lessthe value configured in the file
Insert image description here

F. Create a new folder (different functions and contents are separated)

Good habits and semantic directory names help project maintenance and developers’ reading mood

Whether the directory structure is reasonable is also a very important part of today's team project development

First create the following directories under the/src directory (the detailed structure is placed at the bottom, if you are interested, you can scroll directly to the bottom to view)

  • App
  • composables
  • const
  • entity
  • enum
  • infrastructure
  • interface
  • just
  • server
  • utils

G. Introduce axios

  • Install axios
npm i axios -S
  • Introducing axios

1. Create new and files in the /src/server folder Create a new file in the folder, and then create a new file in the interface folder It has been used, so it needs to be installed as follows (other configurations can be customized)index.tsinterface.ts
/srcinterfaceindex.ts
lodash-esnpm i lodash-es -D
/src/server/index.ts

import axios, {
    
     type AxiosRequestConfig } from 'axios'
import {
    
     assign } from 'lodash-es'
import {
    
     ElMessage as message } from 'element-plus'
const UN_SUPPORT_DIY_HEADER_REGEX = /^http(s)?:\/\//i
// 请求错误统一处理
import ERRORCODES from '@/enum/error-code'

import {
    
     resetInterfacePath } from '@/utils'
// 默认请求失效时间60s
export const AXIOS_TIMEOUT_LIMIT = 60000
axios.defaults.timeout = AXIOS_TIMEOUT_LIMIT;
import type {
    
     NUNBER_STRING as ERROR_CODES_TYPES } from '@/interface'
// 也可以直接使用 typeof 获取 ERROR_CODES 的接口类型,这个时候需要ERROR_CODES 在同一文件内才有效果
// type ERROR_CODES_TYPES = typeof ERROR_CODES
const ERROR_CODES = ERRORCODES as ERROR_CODES_TYPES 
/**
 * 后台接口公共的返回格式
 * 具体根据实际跟后台约定的定义
 */
export interface ResCommonType<T = unknown> {
    
    
  code: number
  data: T
  msg?: string
}

// 请求拦截
axios.interceptors.request.use(
  (config) => {
    
    
    /**
     * Request header not allowed by Access-Control-Allow-Headers in preflight response
     * 第三方接口不支持头
    */
    config.url = resetInterfacePath(config.url || '')
    if (!UN_SUPPORT_DIY_HEADER_REGEX.test(config.url ?? '')) {
    
    
      assign(config.headers, {
    
    
        // 'X-RequestFrom': 'person',
      })
    }
    // if (config?.url?.includes('/DownloadFile')) {
    
    
    //   assign(config.headers, {
    
    
    //     'Accept': 'ext/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    //     'Content-Type': 'application/x-www-form-urlencoded',
    //     'responseType': 'blob'
    //   })
    // }
    return config
  },
  (err) => Promise.reject(err),
)

// 响应拦截
axios.interceptors.response.use(
  (response) => {
    
    
    if (typeof response.data === 'string') {
    
    
      // location.href = '/signIn'
      // return Promise.reject('登录失效')
    }
    const data = response.data
    const resCode: keyof ERROR_CODES_TYPES = data.status || data.code
    console.log('ERROR_CODES[resCode]', ERROR_CODES[resCode])
    if (ERROR_CODES[resCode]) {
    
    
      return Promise.reject(data)
    }
    return Promise.resolve(data)
  },
  (err) => {
    
    
    let errCode: keyof ERROR_CODES_TYPES = 500
    let errMsg = err?.message || '连接到服务器失败'
    if (err?.response) {
    
    
      const {
    
     code, status } = err.response
      errCode = code || status || 500
      errMsg = ERROR_CODES[errCode]
    }
    console.log('ERROR_CODES[]', errCode, ERROR_CODES[errCode])
    message.error(errMsg)
    return Promise.reject({
    
    
      code: errCode,
      msg: errMsg,
      data: err || null,
    })
  },
)

/**
 * 发起GET请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
 * @param {string} url 请求链接
 * @param {object} params 请求参数
 * @param {object} config 配置
 */
export const get = <U = unknown, T = unknown>(
  url: string,
  params?: U,
  config?: AxiosRequestConfig,
) => axios.get<T, T>(
    url, {
    
     params: {
    
     ...params, t: Date.now() }, ...config },
  )

/**
 * 发起POST请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
 * @param {string} url 请求链接
 * @param {object} params 请求参数
 * @param {object} config 配置
 */
export const post = <U = unknown, T = unknown>(
  url: string,
  params?: U,
  config: AxiosRequestConfig = {
    
    },
) => {
    
    
  if (Array.isArray(params)) {
    
    
    return axios.post<T, T>(url, [...params], config)
  }
  return axios.post<T, T>(url, {
    
     ...params }, config)
}

/**
 * 发起FormData请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
 * @param {string} url 请求链接
 * @param {object} params 请求参数
 * @param {object} config 配置
 */
// export const postForm = <U = unknown, T = unknown>(
//   url: string,
//   params?: U,
//   config: AxiosRequestConfig = {},
// ) => axios.post<T, ResCommonType<T>>(url, qs.stringify({ ...params }), config);
export const postForm = <U = unknown, T = unknown>(
  url: string,
  params?: U,
  config: AxiosRequestConfig = {
    
    },
) => axios.post<T, T>(url, params, config)

/**
 * 文件下载请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
 * @param {string} url 请求链接
 * @param {object} params 请求参数
 * @param {object} config 配置
 */
// export const postFile = <U = unknown, T = unknown>(
//   url: string,
//   params?: U,
//   config: AxiosRequestConfig = { responseType: 'blob' },
// ) => axios.post<T, ResCommonType<T>>(url, { ...params }, config);

export default {
    
    
  get,
  post,
  // postForm,
  // postFile,
}

/src/server/interface.tsAs follows (for the common interface definition part of the interface)


export interface BaseResType {
    
    
  ResultCode: number
  ResultDescription: string
}
export interface ApiResponseType<T> extends BaseResType{
    
    
  result: T
}

2. Create a new file in the /src/enum folder (configure it into multiple languages ​​later)error-code.ts

// 请求错误统一处理
const ERROR_CODES = {
    
    
    400: '错误请求,状态码:400',
    401: '未授权,请重新登录,状态码:401',
    403: '拒绝访问,状态码:403',
    404: '请求错误,未找到该资源,状态码:404',
    405: '请求方法未允许,状态码:405',
    408: '请求超时,状态码:408',
    500: '服务器端出错,状态码:500',
    501: '网络未实现,状态码:501',
    502: '网关错误,状态码:502',
    503: '服务不可用,状态码:503',
    504: '网络超时,状态码:504',
    505: 'HTTP版本不支持该请求,状态码:505',
}

export default ERROR_CODES

3. Create a new file in the /src/utils folder (for methods common to the entire project, etc.)index.ts


import type {
    
     unKnow } from "@/interface";
/**
 * 拼接接口路径,例如代理接口的时候后台接口前缀不统一等,可以自定义个前缀,有助于代理配置。
 * @param url 接口路径(一般只会配置相对路径,也可直接配置绝对路径)
 * @returns 
 */
export const resetInterfacePath = (url: string) => {
    
    
    // return `/api/${url}`
    return url
}
/**
 * 对象转formData
 * @param data
 */
export const objectToFormData = (data: object): FormData => {
    
    
  let formData = new FormData();
  for (const [key, value] of Object.entries(data)) {
    
    
    if (value !== null && value !== undefined) {
    
    
      formData.append(key, value);
    } else {
    
    
      formData.append(key, '');
    }
  }
  return formData;
};
/**
 * 对象转数组
* @param data
*/
export const objectToArray = (data: object): Array<Object> => {
    
    
 let arr: Array<Object> = []
 for (const [key, value] of Object.entries(data)) {
    
    
   if (value !== null && value !== undefined) {
    
    
     arr.push(value)
   }
 }
 return arr;
};
/**
 * 地址数据转换
 * @param data 
 * @returns 
 */
export const translateAddress = (data: any) => {
    
    
  if (data instanceof Object && !Array.isArray(data)) {
    
    
    data = objectToArray(data)
    data = data.map((item: any) => {
    
    
      return {
    
    
        value: item.code,
        label: item.name,
        children: translateAddress(item.node)
      }
    })
  }
  return data
}

interface Obj {
    
    
  [key: string]: string | number;
}
/**
 * 根据对象value获取key
 * @param obj 
 * @param value 
 * @returns 
 */
export const objectGetKeyForValue = (obj: Obj | undefined, value: string | number): string => {
    
    
  if (!obj) {
    
    
    return ''
  }
  for (const key in obj) {
    
    
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
    
    
      const element = obj[key];
      if (value === element) {
    
    
        return key
      }
    }
  }
  return ''
}
/**
 * 根据数组中子对象value获取对应label
 * @param arr 
 * @param value 
 * @returns 
 */
export const arrayGetLabelForValue = (arr: Array<unKnow> | undefined, value: string | number): string => {
    
    
  if (!arr?.length) {
    
    
    return ''
  }
  let label = ''
  arr.forEach(element => {
    
    
    if (element.value === value) {
    
    
      label = element.label
    }
  });
  return label
}

4. Add in /src/interface/index.ts (the interface interface definition of the entire project, that is, the public part)

export interface unKnow {
    
    
    [key: string]: any;
  }
  export interface Undefined {
    
    
    [key: string]: undefined;
  }
  export interface NUNBER_STRING{
    
    
    [key: number]: string;
  }
  export interface OBJ_STRING {
    
    
    [key: string]: string;
  }
  

After configuringaxios, you need to verify whether the configuration is available.

Take the interface api as/user/login as an example (the other interfaces can be configured in sequence according to this logic)
Then let’s do it/src/serverCreate a new folder in the directory asuser, which is the prefix part of the API, and then create two new files hereindex.ts and interface.ts

index.tsdocument


import {
    
     post, get, postForm } from '@/server/index';
import {
    
     objectToFormData } from '@/utils'
import type {
    
     
  LoginParamsType, LoginResType
} from './interface';

/**
 * 登录
 * @param params
 */
export const login = (params: LoginParamsType) => {
    
    
  return post<LoginParamsType, LoginResType>('/user/login', params)
}
// export const login = (params: LoginParamsType) => {
    
    
//   return get<LoginParamsType, LoginResType>('/user/login', params)
// }
// export const login = (params: LoginParamsType) => {
    
    
//   return postForm<FormData, LoginResType>('/user/login', objectToFormData( params ))
// }

interface.tsdocument

/**
 * 登录参数
 */
export interface LoginParamsType {
    
    
  
}
/**
 * 登录返回结果
 */
export interface LoginResType {
    
    

}

Then you can use it directly wherever you need to call this interface, as follows:

import {
    
     login } from '@/server/user'
login({
    
    })

It can be seen that the interface is called. Since the API has not been opened yet, it is 404. However, even if the interface service is enabled, cross-domain situations will occur. The next step will be recorded in detail.
Insert image description here

H. Cross-domain problems inevitably encountered in project development

Cross-domain is inevitable for browsers to call APIs, so we have also talked about same-origin policy and cross-domain solutions before

Here we mainly describe the Vite agent solution span solution in local development
In many cases, the backend Api will have different Api prefixes. Of course, some backend bosses will all start with /api (Helping for proxy configuration), the front end itself can also automatically configure Api splicing/api as prefix

Because we don’t want to seevite.config.ts the file is too long, so we create a new build folder and add < under this folder a i=3>Fileproxy.ts

const fs = require('fs');
const path = require('path');

/**
 * 自动添加代理
 * 遍历mock代理配置 START
 */
const getProxyConfig = () => {
    
    
  // 读取 ../src/server 下的目录作为代理的根目录
  const mockFiles = fs.readdirSync(path.resolve(__dirname, '../src/server'));
  const proxyMap = {
    
    };
  mockFiles.forEach((fileName) => {
    
    
    if (!fileName.includes('.ts')) {
    
    
      proxyMap[`^/${
      
      fileName}`] = {
    
    
        target: process.env.VITE_PROXY_API,
        ws: true,
        secure: true,
        changeOrigin: true,
      };
    }
  });
  /**
   * 统一以 /api 为前缀(也可为其他自定义,这里以 /api 为例)
   * 如果后端Api没有此前缀,前端可自行在调用接口路径前拼接 /api 例如 后台为 /login ,在实际调用接口改为 /api/login(即当前构建配置的resetInterfacePath方法处理)
   * 不过不想手动拼接/api前缀,也可直接在server目录下新建 api 文件夹,把对应配置的接口放入即可,按照mockFiles的配置会直接配置上的,不过的注意 rewrite 问题哦
   * 所以这里直接写上 /api 的代理配置
   */
  proxyMap[`^/api`] = {
    
    
    target: process.env.VITE_PROXY_API, // 这个配置用到了上面env的环境配置字段
    ws: true,
    secure: true,
    changeOrigin: true,
    // 如果后端没有这个前缀,需要重写"/api"为 ""
    rewrite: (path: string) => path.replace(/^\/api/, ""),
    bypass(req: any, res: any, options: any) {
    
     // 查看代理前路径
      const proxyUrl = new URL( options.rewrite(req.url) || '', (options.target) as string)?.href || ''
      res.setHeader("x-req.proxyUrl", proxyUrl)
    }
  };
  console.log('proxyMap_proxyMap', proxyMap)
  return proxyMap;
};

module.exports = getProxyConfig;

Recentlyvite.config.tsSentence arrangementproxyImmediately available

import {
    
     fileURLToPath, URL } from 'node:url'
import {
    
     defineConfig, loadEnv } from 'vite'
const getProxyConfig = require('./build/proxy');

// https://vitejs.dev/config/
export default defineConfig(({
    
     command, mode }) => {
    
    
  const env = loadEnv(mode, `${
      
      process.cwd()}/env`, '')
  Object.assign(process.env, env, {
    
     NODE_ENV: mode })
  return {
    
    
    mode,
    ...
    server: {
    
    
      // host设置为true才可以使用network的形式,以ip访问项目
      host: true,
      // 本地编译后访问的端口号
      port: 8082,
      // 编译后是否自动打开浏览器访问
      open: true,
      hmr: {
    
    
        overlay: false
      },
      // 跨域设置允许
      cors: false,
      // 如果端口已占用直接退出
      strictPort: false,
      // 设置代理
      proxy: getProxyConfig()
    },
    ...
  }
})

Speaking of this, let us also mention that when developing and compiling locally, the access port defaults5173 that ishttp://localhost:5173/
and can be found above in =3>, please see the notes aboveserverport

Insert image description here

I. Multi-language configuration

Multi-language system websites are common, so we will also configure a multi-language system.

Plug-ins commonly used in multiple languages ​​includei18n, and those suitable for the vue framework includevue-i18n. Install them firstnpm i vue-i18n -S
. Usually in After switching the language, page refresh and other operations must retain the switched language system. We can use cache records. Here we use js-cookie for cache settings, that is, use cookies to record the current display language. Since TS You must install two dependencies js-cookie and @types/js-cookie to use in the environment, otherwise an error will occur (of course you can also use handwritten settings to set cookies directly)

npm i @types/js-cookie -D
npm i js-cookie -D

If you only install @types/js-cookie the following error will be prompted
Insert image description here

Then create the following files (folders) under the /src/lang folder

  • Shinken index.tsText item
  • For the language folder corresponding to , take Chinese and English as an example, create zh-CN and en-US folders, and create index.ts
  • Create a third-party resource library for the application (for example, create a new element-plus.ts file)
    Insert image description here

/src/lang/index.ts file (the specific configuration will not be detailed here, just look at the code, including the multi-language configuration of elememt-plus)

import {
    
     createI18n } from 'vue-i18n'
import Cookies from 'js-cookie'
import elementPlus from './element-plus'
import enLocale from './en-US/index'
import zhLocale from './zh-CN/index'
const messages = {
    
    
  'zh-CN': zhLocale,
  'en-US': enLocale
}

/**
 * 设置语言环境
 * @param lang 语言环境
 */
export function setLanguage(lang: string) {
    
    
  Cookies.set('language', lang || 'zh-CN')
}
/**
 * 获取配置环境
 * @returns 
 */
export function getLanguage() {
    
    
  // const bool = true
  // if (bool) {
    
    
  //   return 'en'
  // }
  const chooseLanguage = Cookies.get('language')
  if (chooseLanguage) return chooseLanguage
  // 如果有需要也可以根据当前用户设备浏览器默认语言
  // const language = navigator.language.toLowerCase() // IE10及IE10以下的版本 使用 navigator.browserLanguage
  // const locales = Object.keys(messages)
  // for (const locale of locales) {
    
    
  //   if (language.indexOf(locale.toLowerCase()) > -1) return locale
  // }
  return 'en-US'
}
const i18n = createI18n({
    
    
    globalInjection: true, // 全局生效$t
    locale: getLanguage(), // getLanguage()
    messages,
    legacy: false
})
export const elementPlusLocale = elementPlus
export const lang = () => {
    
    
  const lang = getLanguage()
  switch (lang) {
    
    
    case 'zh-CN':
      return messages['zh-CN']
    case 'en-US':
      return messages['en-US']
  }
  return messages['zh-CN']
}
export default i18n

/src/lang/zh-CN.index.tsdocument

export default {
    
    
  ErrorPageTips: `抱歉!您访问的页面失联啦`
}

/src/lang/en-US/index.tsdocument

export default {
    
    
  ErrorPageTips: `I'm sorry! The page you visitedis lost`
}

/src/lang/element-plus.tsFile (other configuration methods can be directly viewed on the official website)


import elementZhLocale from 'element-plus/lib/locale/lang/zh-CN'
import elementEnLocale from 'element-plus/lib/locale/lang/en'
import {
    
     getLanguage }  from './index'
export default () => {
    
    
  const lang = getLanguage()
  switch (lang) {
    
    
    case 'zh-CN':
      return elementZhLocale
    case 'en-US':
      return elementEnLocale
  }
}

After creating and configuring the above content, you can apply it directly in the main.ts file

import {
    
     createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/index.css'
import App from '@/App/index.vue'
import i18n, {
    
     elementPlusLocale } from './lang'
...
// import VConsole from 'vconsole';
// 设置语言变量
// import { setLanguage } from '@/lang/index'
// setLanguage('zh-CN')
// const vConsole = new VConsole() as any
const app = createApp(App)
...
// .use(vConsole)
.use(ElementPlus, {
    
     locale: elementPlusLocale() })
.use(i18n)
.mount('#app')

After is configured and referenced to the language configuration, it can be used.
For example, use directly on
html

<div>{
   
   {$t('ErrorPageTips')}}</div>

Insert image description here
Or use it when writing logic (note that it needs to be used within setup)

import {
    
     defineComponent } from 'vue'
import {
    
     useI18n } from 'vue-i18n'
export default defineComponent({
    
    
  ...
  setup() {
    
    
    const {
    
     t } = useI18n()
    console.log(t('ErrorPageTips'))
    return {
    
     ... }
  }
}

Insert image description here

Follow the above operations, multi-language configuration will be implemented. As for switching languages, you can directly trigger the setLanguage method on the button that needs to be switched

Now that multiple languages ​​are configured here, the above files in the /src/enum foldererror-code.ts need to be modified. Create a new folder here first< /span>index.ts

import {
    
     getLanguage } from "@/lang"
/**
 * 获取枚举
 * @param ZHCN 中文枚举
 * @param EN 英文枚举
 * @returns 
 */
export const getEnum = (ZHCN: unknown, EN: unknown) => {
    
    
  const lang = getLanguage()
  switch (lang) {
    
    
    case 'zh-CN':
      return ZHCN
    case 'en-US':
      return EN
    default :
      return {
    
    }
  }
}

Then modify the error-code.ts file to be, so that the corresponding enumeration value data can be obtained according to the locale (the same is true for other enumerations)

import {
    
     getEnum } from "./index"
export const errorCodeEN =  {
    
    
  400: 'Error request, status code:400',
  401: 'Unauthorized, please login again, status code:401',
  403: 'Access denied, status code:403',
  404: 'Request error, the resource was not found, status code:404',
  405: 'Request method not allowed, status code:405',
  408: 'Request timeout, status code:408',
  500: 'Server side error, status code:500',
  501: 'Network not implemented, status code:501',
  502: 'Gateway error, status code:502',
  503: 'Service unavailable, status code:503',
  504: 'Network timeout, status code:504',
  505: 'The HTTP version does not support this request, status code:505',
}
export const errorCodeCN = {
    
    
  400: '错误请求,状态码:400',
  401: '未授权,请重新登录,状态码:401',
  403: '拒绝访问,状态码:403',
  404: '请求错误,未找到该资源,状态码:404',
  405: '请求方法未允许,状态码:405',
  408: '请求超时,状态码:408',
  500: '服务器端出错,状态码:500',
  501: '网络未实现,状态码:501',
  502: '网关错误,状态码:502',
  503: '服务不可用,状态码:503',
  504: '网络超时,状态码:504',
  505: 'HTTP版本不支持该请求,状态码:505',
}
export default getEnum(errorCodeCN, errorCodeEN)

K. Introduction of state management (pinia, Vuex)

Because the official website recommends pinia, let’s use it directly
You can first check whether the initialized template has been usedpinia
and the cache is not used by default The operation can be introduced on a certain page/src/stores/counter.ts and the writing method is combined development
and then modify the corresponding field value, jump to another route, and repeat Introduce/src/stores/counter.ts to get the corresponding field. You can see that the data is the value that was just modified
. For example, routing/a /b attaches the following code respectively.

import {
    
     storeToRefs } from "pinia"
import {
    
     useCounterStore} from '@/stores/counter'
// 如果此代码是抽离到了单独ts文件内,那下面部分需要再 放置 setup 内
// 变量需要使用 storeToRefs 转为响应式数据
const {
    
     count } = storeToRefs(useCounterStore())
// 方法直接使用即可
const {
    
     increment } = useCounterStore()
increment()
console.log('count', count)

As shown in the figure, when routing /a /b switches to each other, you will find that the values ​​​​of count have been superimposed, indicating that the data is shared

Insert image description here

If you have caching requirements, you can also install another pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate -S
and introduce the plug-in in the main.ts file

import {
    
     createApp } from 'vue'
import App from '@/App/index.vue'
import {
    
     createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import './assets/styles/app.less'
...
const app = createApp(App)
app.use(
  createPinia()
  .use(piniaPluginPersistedstate)
)
...
.mount('#app')

When using cache, the definition method is different, and combined development cannot be used. For example, create a new file under /src/stores. Check the code directly for the specific writing method. , as followsmapState.ts

import {
    
     defineStore } from 'pinia'

export interface MapState {
    
    
  address: string;
}
const {
    
     SIS_STORE_NAME } = import.meta.env

export const useMapStore = defineStore(SIS_STORE_NAME + 'map', {
    
    
  state: (): MapState => ({
    
    
    address: '',
  }),
  getters: {
    
    },
  actions: {
    
    
    setAdress(address: string) {
    
    
      this.address = address;
    },
    clearMessage() {
    
    
      this.address = '';
    },
  },
  persist: {
    
    
    /**
     * 使用的存储
     * @default $store.id
     */
    key: SIS_STORE_NAME + 'map',
    /**
     * 存储位置
     * @default localStorage
     */
    storage: sessionStorage,
    /**
     * 需要设置缓存的state 如果未设置即存储所有state
     * @default undefined
     */
    // paths: [],
    /**
     * 存储之前
     * @default null
     */
    beforeRestore: () => {
    
    },
    /**
     * 存储之后
     * @default undefined
     */
    afterRestore: () => {
    
    },
     /**
     * 启用时在控制台中记录错误。
     * @default false
     */
     debug: true
  },
});

How to use it

  • /a Route setting value, and then look at the cache (currently this is sessionStorage)
import {
    
     useMapStore} from '@/stores/mapState'
// 如果此代码是抽离到了单独ts文件内,那下面部分需要再 放置 setup 内
// 变量需要使用 storeToRefs 转为响应式数据
const {
    
     setAdress } = useMapStore()
setAdress('验证地区')

You can see that it has been set up successfully

Insert image description here

  • /broute get value
import {
    
     useMapStore} from '@/stores/mapState'
// 如果此代码是抽离到了单独ts文件内,那下面部分需要再 放置 setup 内
// 变量需要使用 storeToRefs 转为响应式数据
const {
    
     address } = useMapStore()
console.log('address', address)

You can see that the printed data is the value set above (without refreshing the page, you can also get the value by turning off the cache in the mapState.ts file)
Insert image description here

J. Introduce resources on demand

For example, the project usesEcharts. If it is introduced in full, it will increase the file size after construction, and most projects cannot use most of the graphics of Echarts. At this time, we need Import the parts used in the project as needed.
Since I have written an article before The use of Echarts (optimized introduction on demand) , if you need this, you can read it here Ha

K. Optimized construction

Optimizing construction actually includes introducing resources on demand, image compression, js compression, etc.

This optimization was also written separately beforeVite construction of packaging optimization (view analysis, CDN introduction, dependency subcontracting, gzip compression) , interested friends can read it asynchronously

3. Optimize the project structure again

At this point, the parts that require configuration optimization have basically been completed.

Of course, in addition to what has been mentioned above, there are also some folder definition specifications, such as

  • AppExtract project routing entry
  • composablesPublic Hooks function
  • entityEntity (class)
  • interfaceInterface type definition

…etc

According to my project habits, the project structure is as follows (I will spend some time later to build a simple complete configuration project with this structure and upload it. Interested friends can also follow the above steps to configure their own custom template step by step)

└── build // 构建抽离
    ├── proxy.js // 接口代理配置
└── docs  // 项目文档说明
    ├── user-url.json // 项目测试连接账号
└── env  // 环境变量相关配置
    ├── .env // 公用变量配置
    ├── .env.development // 开发环境变量配置
    ├── .env.production // 生产环境变量配置
    ├── .env.sit // 测试环境变量配置
    ├── .env.uat // 业务验收环境变量配置
└── public // 静态资源(无需经过构建内容)
└── src
    ├── App // 项目路由入口
    └── assets // 静态文件
        ├── dic // 字段库
        ├── images // 图片
        ├── js // js库
        ├── styles // 样式库
    └── components // 公用组件
    └── composables // 公用Hooks函数
    └── const // 常量定义
    └── entity // 实体(class类)-- 页面级别
    └── enum // 枚举值
    └── infrastructure // 基础设施实体(class类) -- 项目级别(如配置数据、人脸、定位等功能相关实体)
    └── interface // 类型定义
    └── lang // 多语言
    └── router // 路由配置
    └── server // 接口
    └── sotres // 状态管理(缓存)
    └── types // 自定义全局类型
    └── utils // 常用方法
    └── views // 页面
    └── main.js // Vue配置入口
├── .eslintrc.cjs // ESLint 规则配置
├── index.html // 项目单页入口
├── package.json // 项目依赖

Summarize

Since I almost always used Webpack to build projects, I occasionally used gulp . I have only started using Vue3 in the past two years for development projects, so I specially recorded the use of Vite during the construction period. You can try to configure it according to the above steps. Welcome everyone. Comment exchange. I will also spend time writing articles about webpack construction projects in the future, and I look forward to future updates~

Guess you like

Origin blog.csdn.net/weiCong_Ling/article/details/130694386