大傢好,我是短暫又燦爛的,一個不想被喝(內卷)的前端。如果寫的文章有幸可以得到你的青睞,萬分有幸~
寫在前面
本篇文章將從0開始搭建一個企業可用的項目骨架,這裡我使用的包管理工具時Yarn,別問為什麼,問就是喜歡用這個;如果你是npm的話,直接將yarn add
全部替換為npm i
即可(廢話文學)。
通過Vite安裝Vue3項目
安裝比較簡單,首先輸入命令
npm create vite
然後會讓你輸入項目名稱
Project name: vite-project
第三步讓你選擇一個框架,這裡選擇Vue
最後一步我們選擇vue-ts
,也就是Vue+TypeScript,
然後就創建完畢瞭,如下圖:
代碼規范
隨著團隊的不斷擴大,每個人都有自己的coding風格,但是如果一個項目中的代碼存在多種風格,那對於代碼的可維護性和可讀性都大大減少,所以說一個項目規范對於前端團隊來說的重要性。
ESlint+Prettier
這兩個工具一個是進行代碼風格檢查,另一個是格式化工具,現在我們開始配置。
第一步,安裝相關依賴:
yarn add eslint eslint-plugin-vue eslint-define-config --dev # eslinkyarn add prettier eslint-plugin-prettier @vue/eslint-config-prettier --dev# prettireyarn add @vue/eslint-config-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser --dev # 對ts的支持
第二步,編寫對應配置文件
.eslintrc.js
const { defineConfig } = require('eslint-define-config')module.exports = defineConfig({ root: true, /* 指定如何解析語法。*/ parser: 'vue-eslint-parser', /* 優先級低於parse的語法解析配置 */ parserOptions: { parser: '@typescript-eslint/parser', //模塊化方案 sourceType: 'module', }, env: { browser: true, es2021: true, node: true, // 解決 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推薦規則, 'prettier', 'plugin:prettier/recommended', ], // https://cn.eslint.org/docs/rules/ rules: { // 禁止使用 var 'no-var': 'error', semi: 'off', // 優先使用 interface 而不是 type '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], '@typescript-eslint/no-explicit-any': 'off', // 可以使用 any 類型 '@typescript-eslint/explicit-module-boundary-types': 'off', // 解決使用 require() Require statement not part of import statement. 的問題 '@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', }, }, }, ], // 禁止出現未使用的變量 '@typescript-eslint/no-unused-vars': [ 'error', { vars: 'all', args: 'after-used', ignoreRestSiblings: false }, ], 'vue/html-indent': 'off', // 關閉此規則 使用 prettier 的格式化規則, 'vue/max-attributes-per-line': ['off'], // 優先使用駝峰,element 組件除外 'vue/component-name-in-template-casing': [ 'error', 'PascalCase', { ignores: ['/^el-/', '/^router-/'], registeredComponentsOnly: false, }, ], // 強制使用駝峰 camelcase: ['error', { properties: 'always' }], // 優先使用 const 'prefer-const': [ 'error', { destructuring: 'any', ignoreReadBeforeAssign: false, }, ], },})
.eslintignore
/node_modules//public/.vscode.idea
.prettierrc
{ "semi": false, "singleQuote": true, "printWidth": 80, "trailingComma": "all", "arrowParens": "avoid", "endOfLine": "lf"}
husky
husky
是一個Git Hook,可以幫助我們對commit
前,push
前以及commit
提交的信息進行驗證,現在我們就來安裝並配置一下這個工具,首先通過自動配置命令安裝,命令如下:
npx husky-init && npm install # npmnpx husky-init && yarn # Yarn 1npx husky-init --yarn2 && yarn # Yarn 2+
執行完畢之後會在項目的根目錄出現一個.husky
的目錄,目錄下有一個pre-commit
文件,我們將npm test
修改為我們需要執行的命令,示例代碼如下:
#!/bin/sh. "$(dirname "$0")/_/husky.sh"yarn lint
最後我們配置一下package.json
,示例代碼如下:
"scripts": { "lint": ""lint": "eslint src --fix --ext .js,.ts,.jsx,.tsx,.vue && prettier --write --ignore-unknown""},
src
:要驗證的目標文件夾;--fix
:自動修復命令;--ext
:指定檢測文件的後綴。
現在我們進行commit
之前會對代碼進行檢測並進行格式化。
lint-staged
我們配置好瞭husky
後,會出現一個問題,就是我們不管是改動一行還是兩行都會對整個項目進行代碼檢查和格式化,我們可以通過lint-staged這個工具來實現隻對git暫存區中的內容進行檢查和格式化,配置步驟如下:
第一步,安裝lint-staged
yarn add lint-staged --dev
第二步,配置package.json
{ "scripts": {}, // 新增 "lint-staged": { "*.{vue,js,ts,tsx,jsx}": [ "eslint --fix", "prettier --write --ignore-unknown" ] },}
第三步,修改.husky/pre-commit
,修改內容如下:
#!/bin/sh. "$(dirname "$0")/_/husky.sh"npx lint-staged
到這就配置完成瞭。
commit message 規范
優秀項目中commit message都是統一風格的,這樣做的好處就是可以快速定位每次提交的內容,方便之後對版本進行控制。現在我們就來配置一下commit message 規范。
提交規范
-
安裝commitizen
yarn add commitizen --dev
-
配置項目提交說明,這裡我們使用cz-conventional-changelog,或者選擇cz-customizable,我們先進行安裝
yarn add cz-conventional-changelog --dev
-
修改
package.json
,代碼如下:"config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" }}
-
進行
commit
,通過cz
這個cli工具yarn cz # 或者 npx cz
第一步選擇本次更新的類型,每個類型的作用如下表所示:
Type 作用 feat 新增特性 fix 修復 Bug docs 修改文檔 style 代碼格式修改 refactor 代碼重構 perf 改善性能 test 測試 build 變更項目構建或外部依賴(例如 scopes: webpack、gulp、npm等) ci 更改持續集成軟件的配置文件和 package.json
中的scripts
命令chore 變更構建流程或輔助工具(比如更改測試環境) revert 代碼回退 第二步填寫改變的作用域,可以寫組件名或者文件名 第三步填寫提交的信息 第四步填寫提交的詳細描述 第五步選擇是否是一次重大的更改 第六步是否影響某個open issue 整個過程如下圖
我們也可以配置一個script
,示例代碼如下: package.json
"scripts": { "commit": "cz"},
配置完成後我們可以通過yarn commit
的方式提交代碼瞭。
utools插件
如果有同學在使用utools的話,這裡推薦一個utools插件,是關系不錯的一個大哥寫的,他的CSDN(看不看沒啥區別),utools插件如下:
這一版還加瞭一個快捷鍵復制的操作,原因如下:
使用命令行提交可能對於某些同學來說不是很友好,這裡推薦一個Vscode插件(Visual Studio Code Commitizen Support),可以通過圖形化的方式進行操作。
操作流程非常簡單,安裝之後在【源代碼管理】面板點擊藍色的icon,如下圖
然後就跟著操作即可,完成之後會自動commit。
另一個插件Commit Message Editor也比較不錯,是圖形化界面的,存在表單和文本框兩種模式。
message驗證
現在我們定義瞭提交規范,但是並不能阻止不按照這個規范進行提交,這裡我們通過commitlint
配合husky
來實現對提交信息的驗證規則。
第一步,安裝相關依賴
yarn add @commitlint/config-conventional @commitlint/cli --dev
第二步,創建commitlint.config.js
配置commitlint
module.exports = { extends: ['@commitlint/config-conventional'],}
更多配置項可以參考官方文檔
第三步,使用husky
生成commit-msg
文件,驗證提交信息
yarn husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
到這,commitlint的配置就完成瞭,如果message編寫不規范,則會阻止提交。
配置Vite
Vite是尤大開源的一款打包工具,目前已經在兩個項目中使用瞭Vite,總體感覺還不錯,開發環境下真的超快,唯一的不足我覺得是HMR吧,體驗並不是很好。
這裡我介紹一下一個基礎配置:
別名配置
配置別名可以幫助我們快速的找到我們想要的組件、圖片等內容,不用使用../../../
的方式,首先配置vite.config.ts
,通過resolve.alias
的方式配置,示例代碼如下:
import { resolve } from 'path'export default defineConfig(({ mode }: ConfigEnv) => { return { resolve: { alias: { '@': resolve(__dirname, 'src'), cpns: resolve(__dirname, 'src/components'), }, extensions: ['.js', '.json', '.ts', '.vue'], // 使用路徑別名時想要省略的後綴名,可以自己 增減 }, /* more config */ }})
這裡配置兩個兩個別名,分別是@
和cpns
,然後配置tsconfig.json
,允許別名在使用,代碼如下:
"compilerOptions": { // 用於設置解析非相對模塊名稱的基本目錄,相對模塊不會受到baseUrl的影響 "baseUrl": ".", "paths": { // 用於設置模塊名到基於baseUrl的路徑映射 "@/*": [ "src/*" ], "cpns/*": [ "src/components/*" ] }},
環境變量
.env文件
在Vite中通過.env開頭的文件去讀取配置,來作為環境變量,Vite默認允許我們使用以下文件:
.env # 所有情況下都會加載.env.local # 所有情況下都會加載,但會被 git 忽略.env.[mode] # 隻在指定模式下加載.env.[mode].local # 隻在指定模式下加載,但會被 git 忽略
這些文件是有優先級的,他們的優先級是.env
<.env.local
<.env.[mode]
<.env.[mode].local
;Vite中還預設瞭一些環境變量,這些的優先級是最高的,不會被覆蓋,分別如下:
MODE: {string}
:應用運行的模式(開發環境下為development
,生成環境為production
)。BASE_URL: {string}
:部署應用時的基本 URL。他由base
配置項決定。PROD: {boolean}
:當前是否是生產環境。DEV: {boolean}
:當前是否是開發環境 (永遠與PROD
相反)。
這些環境變量Vite允許我們通過import.meto.env
方式獲取。
定義環境變量
如果我麼你想要自定義環境變量,就必須以VITE_
開頭,如果修改則需要通過envPrefix配置項,該配置項允許我們傳入一個非空的字符串作為變量的前置。
.env
VITE_APP_API_BASE_URL=http://127.0.0.1:8080/
定義完成之後我們就可以在項目中通過import.meta.env.VITE_APP_API_BASE_URL
的方式獲取。
如果想要獲得TypeScript的類型提示,需要在創建一個src/env.d.ts
,示例代碼如下:
/// interface ImportMetaEnv { readonly VITE_APP_API_BASE_URL: string // 定義更多環境變量}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 export default component}
在使用時就會獲得智能提示。
在vite.config.ts中獲取環境變量
如果我們想要在vite.config.ts
中獲取環境變量,需要使用Vite提供的loadEnv()
方法,該方法的定義如下:
function loadEnv( mode: string, envDir: string, prefixes?: string | string[]): Record
上面的三個參數的解釋如下:
mode
:模式;envDir
:環境變量配置文件所在目錄;prefixes
:【可選】接受的環境變量前綴,默認為VITE_
。
瞭解瞭使用的API,在vite.config.ts
中獲取環境變量示例代碼如下:
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/, ''), }, }, }, }})
項目依賴 安裝Element-plus
-
安裝Element-plus
yarn add element-plus
-
Volar 支持 如果您使用 Volar,請在
tsconfig.json
中通過compilerOptions.type
指定全局組件類型。// tsconfig.json{ "compilerOptions": { // ... "types": ["element-plus/global"] }}
-
按需導入 安裝相關插件實現自動按需導入
yarn add unplugin-vue-components unplugin-auto-import --dev
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()], }), ],}
Tailwind CSS
-
安裝
yarn add tailwindcss postcss autoprefixer --dev
-
初始化
yarn tailwindcss init -p
-
配置
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', ],}
-
配置
postcss.config.js
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, },}
-
引入Tailwind CSS
./src/index.css
@tailwind base;@tailwind components;@tailwind utilities;
./src/main.ts
import { createApp } from 'vue'import App from './App.vue'import './index.css'createApp(App).mount('#app')
VueRouter
第一步,安裝VueRouter
yarn add vue-router@next
第二步,創建VueRouter入口文件
src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'import type { RouteRecordRaw } from 'vue-router'// 配置路由信息const routes: RouteRecordRaw[] = []const router = createRouter({ routes, history: createWebHistory(),})export default router
第三步,在main.ts
中引入VueRouter
import { createApp } from 'vue'import App from './App.vue'// 引入 vue-routerimport router from './router'import './assets/css/index.css'createApp(App).use(router).mount('#app')
pinia
第一步,安裝pinia
yarn add vuex@next
第二步,創建vuex入口文件
src/store/index.ts
import { createPinia } from 'pinia'const store = createPinia()export default store
第三步,在main.ts
中引入pinia
import { createApp } from 'vue'import App from './App.vue'// 引入 vue-routerimport router from './router'// 引入 vueximport store from './store'import './assets/css/index.css'createApp(App).use(router).use(store).mount('#app')
第四步,創建測試數據
src/store/modules/count.ts
import { defineStore } from 'pinia'import type { CountInterface } from './types'export const useCountStore = defineStore({ id: 'count', // id必填,且需要唯一 // state state: (): CountInterface => { return { count: 0, } }, // getters getters: { doubleCount: state => { return state.count * 2 }, }, // actions actions: { // actions 同樣支持異步寫法 countAdd() { // 可以通過 this 訪問 state 中的內容 this.count++ }, countReduce() { this.count-- }, },})
src/store/modules/type.ts
export interface CountInterface { count: number}
最後一步,為瞭方便導入,我們將所有內容統一在src/store/index.ts
進行導入導出,代碼如下:
import { createPinia } from 'pinia'import { useCountStore } from './modules/count'const store = createPinia()export default storeexport { useCountStore }
測試代碼如下:
當前數值{{ countComputed }}
雙倍數值{{ doubleCount }}
+1 -1 import { computed } from 'vue'import { useCountStore } from '@/store'import { storeToRefs } from 'pinia'const countStore = useCountStore()// 通過計算屬性const countComputed = computed(() => countStore.count)// 通過 storeToRefs api 結構const { doubleCount } = storeToRefs(countStore)
封裝Axios
Axios作為前端使用最高的HTTP請求庫,周下載量已經達到瞭2000多萬,在這個項目中我們也使用Axios,這裡我們通過TS對Axios進行二次封裝,方便我們在項目中使用。
這一塊封裝過程比較復雜,過程參考我的上一篇文章,點我跳轉;完整代碼如下:
src\service\index.ts
import Request from './request'import type { RequestConfig } from './request/types'interface YWZRequestConfig extends RequestConfig { data?: T}interface YWZResponse { statusCode: number desc: string result: T}const request = new Request({ baseURL: import.meta.env.BASE_URL, timeout: 1000 * 60 * 5, interceptors: { // 請求攔截器 requestInterceptors: config => config, // 響應攔截器 responseInterceptors: result => result, },})/** * @description: 函數的描述 * @interface D 請求參數的interface * @interface T 響應結構的intercept * @param {YWZRequestConfig} config 不管是GET還是POST請求都使用data * @returns {Promise} */const ywzRequest = (config: YWZRequestConfig) => { const { method = 'GET' } = config if (method === 'get' || method === 'GET') { config.params = config.data } return request.request<YWZResponse>(config)}// 取消請求export const cancelRequest = (url: string | string[]) => { return request.cancelRequest(url)}// 取消全部請求export const cancelAllRequest = () => { return request.cancelAllRequest()}export default ywzRequest
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: AxiosInstance // 攔截器對象 interceptorsObj?: RequestInterceptors /* 存放取消方法的集合 * 在創建請求後將取消請求方法 push 到該集合中 * 封裝一個方法,可以取消請求,傳入 url: string|string[] * 在請求之前判斷同一URL是否存在,如果存在就取消請求 */ cancelRequestSourceList?: CancelRequestSource[] /* 存放所有請求URL的集合 * 請求之前需要將url push到該集合中 * 請求完畢後將url從集合中刪除 * 添加在發送請求之前完成,刪除在響應之後刪除 */ requestUrlList?: string[] constructor(config: RequestConfig) { this.requestUrlList = [] this.cancelRequestSourceList = [] this.instance = axios.create(config) this.interceptorsObj = config.interceptors // 攔截器執行順序 接口請求 -> 實例請求 -> 全局請求 -> 實例響應 -> 全局響應 -> 接口響應 this.instance.interceptors.request.use( (res: AxiosRequestConfig) => res, (err: any) => err, ) // 使用實例攔截器 this.instance.interceptors.request.use( this.interceptorsObj?.requestInterceptors, this.interceptorsObj?.requestInterceptorsCatch, ) this.instance.interceptors.response.use( this.interceptorsObj?.responseInterceptors, this.interceptorsObj?.responseInterceptorsCatch, ) // 全局響應攔截器保證最後執行 this.instance.interceptors.response.use( // 因為我們接口的數據都在res.data下,所以我們直接返回res.data (res: AxiosResponse) => { return res.data }, (err: any) => err, ) } /** * @description: 獲取指定 url 在 cancelRequestSourceList 中的索引 * @param {string} url * @returns {number} 索引位置 */ private getSourceIndex(url: string): number { return this.cancelRequestSourceList?.findIndex( (item: CancelRequestSource) => { return Object.keys(item)[0] === url }, ) as number } /** * @description: 刪除 requestUrlList 和 cancelRequestSourceList * @param {string} url * @returns {*} */ private delUrl(url: string) { const urlIndex = this.requestUrlList?.findIndex(u => u === url) const sourceIndex = this.getSourceIndex(url) // 刪除url和cancel方法 urlIndex !== -1 && this.requestUrlList?.splice(urlIndex as number, 1) sourceIndex !== -1 && this.cancelRequestSourceList?.splice(sourceIndex as number, 1) } request(config: RequestConfig): Promise { return new Promise((resolve, reject) => { // 如果我們為單個請求設置攔截器,這裡使用單個請求的攔截器 if (config.interceptors?.requestInterceptors) { config = config.interceptors.requestInterceptors(config) } const url = config.url // url存在保存取消請求方法和當前請求url if (url) { this.requestUrlList?.push(url) config.cancelToken = new axios.CancelToken(c => { this.cancelRequestSourceList?.push({ [url]: c, }) }) } this.instance .request(config) .then(res => { // 如果我們為單個響應設置攔截器,這裡使用單個響應的攔截器 if (config.interceptors?.responseInterceptors) { res = config.interceptors.responseInterceptors(res) } resolve(res) }) .catch((err: any) => { reject(err) }) .finally(() => { url && this.delUrl(url) }) }) } // 取消請求 cancelRequest(url: string | string[]) { if (typeof url === 'string') { // 取消單個請求 const sourceIndex = this.getSourceIndex(url) sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][url]() } else { // 存在多個需要取消請求的地址 url.forEach(u => { const sourceIndex = this.getSourceIndex(u) sourceIndex >= 0 && this.cancelRequestSourceList?.[sourceIndex][u]() }) } } // 取消全部請求 cancelAllRequest() { this.cancelRequestSourceList?.forEach(source => { const key = Object.keys(source)[0] source[key]() }) }}export default Requestexport { RequestConfig, RequestInterceptors }
src\service\request\types.ts
import type { AxiosRequestConfig, AxiosResponse } from 'axios'export interface RequestInterceptors { // 請求攔截 requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig requestInterceptorsCatch?: (err: any) => any // 響應攔截 responseInterceptors?: (config: T) => T responseInterceptorsCatch?: (err: any) => any}// 自定義傳入的參數export interface RequestConfig extends AxiosRequestConfig { interceptors?: RequestInterceptors}export interface CancelRequestSource { [index: string]: () => void}
src\api\index.ts
import request from '@/service'interface Req { apiKey: string area?: string areaID?: string}interface Res { area: string areaCode: string areaid: string dayList: any[]}export const get15DaysWeatherByArea = (data: Req) => { return request({ url: '/api/common/weather/get15DaysWeatherByArea', method: 'GET', data, interceptors: { requestInterceptors(res) { return res }, responseInterceptors(result) { return result }, }, })}
寫在最後
到這為止,項目的基礎骨架就已經搭建完畢瞭,後期根據項目需求可動態添加依賴。