Vue項目中引入ESLint校驗代碼避免代碼錯誤
1 ESLint 是什么
ESLint 是一個插件式的 JavaScript / JSX 代碼檢查工具,用于檢測和修復(fù) JavaScript 代碼中的問題,目標(biāo)是讓代碼更一致并避免錯誤。
2 在 Vue 項目中引入 ESLint
使用 Vue CLI 搭建的 Vue2 項目已經(jīng)自帶 ESLint,就不贅述,我們看下 Vite 搭建的 Vue3 項目中怎么引入 ESLint。
使用以下命令搭建一個 Vue3 項目:
npm create vite@latest vue3-project
創(chuàng)建之后,啟動起來:
npm i npm run dev
效果如下:
2.1 引入 ESLint
執(zhí)行以下命令:
npm init @eslint/config
進(jìn)入交互式界面,可通過上下方向鍵選擇,通過按回車鍵確定。
第一個問題是:
- 你希望用 ESLint 來干嘛?
- 我們選擇最全面的那個:檢查語法,發(fā)現(xiàn)問題,并強(qiáng)制統(tǒng)一代碼樣式
$ npm init @eslint/config ? How would you like to use ESLint? … To check syntax only To check syntax and find problems ? To check syntax, find problems, and enforce code style
第二個問題是:
- 你的項目用的是什么模塊系統(tǒng)?
- 因為是運(yùn)行在瀏覽器端,選擇
ESModule
? What type of modules does your project use? … ? JavaScript modules (import/export) CommonJS (require/exports) None of these
第三個問題是:
- 你用的什么框架?(居然沒有 Angular)
- 選擇
Vue
? Which framework does your project use? … React ? Vue.js None of these
第四個問題是:
- 你是否使用 TypeScript?
- 選擇
Yes
? Does your project use TypeScript? ? No / Yes
第五個問題是:
- 你的代碼運(yùn)行在什么環(huán)境?(這個可以多選)
- 選擇
Browser
瀏覽器環(huán)境
? Where does your code run? … (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Browser ? Node
第六個問題是:
- 你想定義怎樣的代碼風(fēng)格?
- 選擇使用一個流行的代碼風(fēng)格
? How would you like to define a style for your project? … ? Use a popular style guide Answer questions about your style
第七個問題是:
- 你想使用哪個樣式風(fēng)格?
Airbnb
用的人比較多,就選這個吧
? Which style guide do you want to follow? … ? Airbnb: https://github.com/airbnb/javascript Standard: https://github.com/standard/standard Google: https://github.com/google/eslint-config-google XO: https://github.com/xojs/eslint-config-xo
第八個問題是:
- 配置文件用什么格式?
- 就選 JavaScript 吧(生成
eslintrc.js
文件)
? What format do you want your config file to be in? … ? JavaScript YAML JSON
完成!是不是超級簡單!
看下我們都選了哪些配置:
? How would you like to use ESLint? · style ? What type of modules does your project use? · esm ? Which framework does your project use? · vue ? Does your project use TypeScript? · Yes ? Where does your code run? · browser ? How would you like to define a style for your project? · guide ? Which style guide do you want to follow? · airbnb ? What format do you want your config file to be in? · JavaScript
主要給我們安裝了以下依賴:
eslint-config-airbnb-base@15.0.0
eslint-plugin-import@2.26.0
eslint-plugin-vue@9.2.0
eslint@8.20.0
@typescript-eslint/parser@5.30.6
@typescript-eslint/eslint-plugin@5.30.6
并生成了一個 eslintrc.cjs
配置文件:
module.exports = { env: { browser: true, es2021: true, }, extends: [ 'plugin:vue/vue3-essential', 'airbnb-base', ], parserOptions: { ecmaVersion: 'latest', parser: '@typescript-eslint/parser', sourceType: 'module', }, plugins: [ 'vue', '@typescript-eslint', ], // 自定義 rules 規(guī)則 rules: { }, };
2.2 ESLint 配置
- parser 解析器
- extends 配置擴(kuò)展
- plugins 插件
- rules 自定義規(guī)則 eslint.org/docs/latest…
- eslint-disable-next-line 禁用ESLint
2.3 執(zhí)行 ESLint 代碼檢查
在 package.json 文件的 scripts 中配置 lint 腳本命令:
"scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", // 配置 lint 腳本命令 "lint": "eslint --ext .vue,.ts src/" },
執(zhí)行 lint 腳本命令:
npm run lint
出現(xiàn)了一堆報錯:
/vue3-project/src/App.vue 4:53 error Missing semicolon semi /vue3-project/src/components/HelloWorld.vue 2:26 error Missing semicolon semi 4:31 error Missing semicolon semi 6:21 error Missing semicolon semi /vue3-project/src/main.ts 1:32 error Missing semicolon semi 2:21 error Missing semicolon semi 3:28 error Missing semicolon semi 5:29 error Missing semicolon semi /vue3-project/src/vite-env.d.ts 4:3 error Expected 1 empty line after import statement not followed by another import import/newline-after-import 4:45 error Missing semicolon semi 5:48 error Missing semicolon semi 6:27 error Missing semicolon semi ? 12 problems (12 errors, 0 warnings) 12 errors and 0 warnings potentially fixable with the `--fix` option.
大部分都是說句尾沒有分號,因為我們選擇的是 Airbnb 代碼規(guī)范,所以會有這個報錯提示,不同的代碼規(guī)范,內(nèi)置的檢查規(guī)則不一定完全相同。
2.4 自動修復(fù) ESLint 問題
在 scripts 中增加自動修復(fù) ESLint 問題的腳本命令:
"scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint --ext .vue,.ts src/", // 自動修復(fù) ESLint 問題腳本命令 "lint:fix": "eslint --ext .vue,.ts src/ --fix" },
執(zhí)行:
npm run lint:fix
執(zhí)行自動修復(fù)的命令之后,所有分號都加上了,未使用的變量也自動移除了。
再次執(zhí)行:
npm run lint
沒有再報錯。
3 配置 husky 和 PR 門禁
3.1 配置 husky 門禁
為了確保每次提交(git commit)之前代碼都通過 ESLint 檢查,我們增加一個 pre-commit 門禁。
- 第一步:安裝 husky 和 lint-staged
npm i lint-staged husky -D
- 第二步:在 package.json 的 scripts 中增加 prepare 腳本命令
"scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint --ext .vue,.ts src/", "lint:fix": "eslint --ext .vue,.ts src/ --fix", // 在 npm install 之后自動執(zhí)行,生成`.husky`目錄。 "prepare": "husky install" },
- 第三步:執(zhí)行 prepare 腳本
npm run prepare
該命令執(zhí)行完會在項目根目錄自動生成.husky
目錄。
- 第四步:增加 pre-commit 鉤子
執(zhí)行以下命令,會在.husky
目錄自動生成pre-commit
文件鉤子。
npx husky add .husky/pre-commit "npx lint-staged"
- 第五步:增加 lint-staged 配置
"lint-staged": { "src/**/*.{vue,ts}": "eslint --fix" },
通過以上五個步驟,以后每次使用git commit
命令提交提交代碼,都會:
- 被 pre-commit 鉤子攔截
- 執(zhí)行 npx lint-staged 命令
- 進(jìn)而執(zhí)行 eslint --fix 命令,對本次提交修改的代碼涉及的文件進(jìn)行代碼檢查,并自動修復(fù)能修復(fù)的錯誤,不能修復(fù)的錯誤會提示出來,只有所有 ESLint 錯誤都修復(fù)了才能提交成功
3.2 配置 PR 門禁
如果你在做自己的開源項目,并且非常幸運(yùn),有一群志同道合的小伙伴愿意一起參與貢獻(xiàn),這時為了統(tǒng)一大家的代碼風(fēng)格,讓貢獻(xiàn)者們專注于特性開發(fā),不用擔(dān)心代碼格式規(guī)范問題,并通過 ESLint 工具提示貢獻(xiàn)者,哪些代碼可能帶來潛在的風(fēng)險,你就有必要給提交的 PR 加上 ESLint 門禁。
我們已經(jīng)增加了本地的 ESLint 命令:
"scripts": { "lint": "eslint --ext .vue,.ts src/", },
我們需要在本目錄創(chuàng)建一個.github/workflows/pull-request.yml
文件,在該文件中寫入以下內(nèi)容:
name: Pull Request on: push: branches: [ dev, main ] pull_request: branches: [ dev, main ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x] name: "ESLint" steps: - name: Checkout uses: actions/checkout@v2 - name: Install pnpm uses: npm/action-setup@v2 with: version: 6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Install deps run: npm i - name: ESLint run: npm run lint
這樣只要 PR 是往 dev 或 main 分支合入的,都會跑一遍這個 Github Actions 工作流任務(wù),ESLint 檢查不通過的話,PR 的 checks 里面會提示,攔截該 PR 的合入。
PR 的提交者看到提示,也可以點到任務(wù)里面去看是哪里報錯,修改掉這些 ESLint 問題,PR 就會變成綠色,項目的管理員就可以順利合入 PR 到目標(biāo)分支啦??
4 常見的 ESLint 問題及修復(fù)案例
接下來跟大家分享 Vue DevUI 開源 Vue3 組件庫 ESLint 問題修復(fù)過程中遇到的典型問題。
4.1 案例1:
warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
該問題出現(xiàn)頻率比較高,原因是有些類型寫了any
,需要明確的類型。
比如Pagination組件的單元測試文件pagination.spec.ts
中:
const wrapper = mount({ components: { DPagination }, template: `<d-pagination ... />` }, globalOption); const btns = wrapper.findAll('a.devui-pagination-link'); expect(btns.map((ele: any) => ele.text()).join()).toEqual('<,1,...,4,5,6,...,16,>');
其中的ele: any
就屬于這類問題。
解決辦法是給ele
加上明確的類型,看邏輯是<button>
元素,由于是@vue/test-utils
庫的包裹元素,因此需要包一層DOMWrapper
:
import { DOMWrapper } from '@vue/test-utils'; expect(btns.map((ele: DOMWrapper<Element>) => ele.text()).join()).toEqual('<,1,...,4,5,6,...,16,>');
4.2 案例2:
'xxx' was used before it was defined no-use-before-define
這也是一個比較常見的問題,在聲明之前使用變量或方法,解決辦法也很簡單,只需要調(diào)整下代碼的順序即可,將變量或方法的聲明放在調(diào)用的語句之前。
比如Pagination組件的pagination.tsx
中:
// 極簡模式下,可選的下拉選擇頁碼 const litePageOptions = computed(() => liteSelectOptions(totalPages.value)); // 當(dāng)前頁碼 const cursor = computed({ get() { // 是否需要修正錯誤的pageIndex if (!props.showTruePageIndex && props.pageIndex > totalPages.value) { emit('update:pageIndex', totalPages.value || 1); return totalPages.value || 1; } return props.pageIndex || 1; }, set(val: number) { emit('update:pageIndex', val); } }); // 總頁數(shù) const totalPages = computed(() => Math.ceil(props.total / props.pageSize));
其中的totalPages
的聲明在比較靠后的位置,但是卻在聲明之前在litePageOptions
和cursor
變量中都使用了totalPages
,所以提示 ESLint 問題。
解決的方法就是將totalPages
的聲明放在litePageOptions
和cursor
之前。
// 總頁數(shù) const totalPages = computed(() => Math.ceil(props.total / props.pageSize)); // 極簡模式下,可選的下拉選擇頁碼 const litePageOptions = computed(() => liteSelectOptions(totalPages.value)); // 當(dāng)前頁碼 const cursor = computed({ ... });
4.3 案例3:
warning Missing return type on function @typescript-eslint/explicit-module-boundary-types
該問題是因為函數(shù)缺少返回類型,比如Fullscreen組件utils.ts
文件的launchImmersiveFullScreen
方法中:
export const launchImmersiveFullScreen = async (docElement: any) => { let fullscreenLaunch = null; if (docElement.requestFullscreen) { fullscreenLaunch = docElement.requestFullscreen(); } else if (docElement.mozRequestFullScreen) { fullscreenLaunch = docElement.mozRequestFullScreen(); } else if (docElement.webkitRequestFullScreen) { fullscreenLaunch = Promise.resolve(docElement.webkitRequestFullScreen()); } else if (docElement.msRequestFullscreen) { fullscreenLaunch = Promise.resolve(docElement.msRequestFullscreen()); } return await fullscreenLaunch.then(() => !!document.fullscreenElement); };
先看下launchImmersiveFullScreen
方法的參數(shù)問題,docElement
用了any
,也缺失了返回類型,docElement
其實就是document
對象,可以使用HTMLElement
類型,但是launchImmersiveFullScreen
這個方法是用來啟動沉浸式全屏的,為了實現(xiàn)瀏覽器兼容,比如使用了docElement.mozRequestFullScreen
兼容火狐,而這些方法在HTMLElement中是沒有的,會報TS類型錯誤,所以需要做一些改造。
interface CompatibleHTMLElement extends HTMLElement { mozRequestFullScreen?: () => void; webkitRequestFullScreen?: () => void; msRequestFullscreen?: () => void; }
這里定義了一個CompatibleHTMLElement
的類型,繼承了HTMLElement
,并增加了一些自定義的方法。
export const launchImmersiveFullScreen = async (docElement: CompatibleHTMLElement) => { ... }
再來看下launchImmersiveFullScreen
方法的返回類型問題。
return await fullscreenLaunch.then(() => !!document.fullscreenElement);
該方法返回了一個Promise
對象,它的類型是一個泛型,我們需要傳入具體的類型:
export const launchImmersiveFullScreen = async (docElement: CompatibleHTMLElement): Promise<boolean> => { ... return await fullscreenLaunch.then(() => !!document.fullscreenElement); };
4.4 案例4:
'xxx' is already declared in the upper scope @typescript-eslint/no-shadow
這個問題是由于嵌套的作用域中定義了相同的變量名,比如Tree組件的use-checked.ts
文件中:
export default function useChecked(...) { const onNodeClick = (item: TreeItem) => { // 這里定義了 id 變量 const { id } = item; ... filter 里面又定義了一個 id 參數(shù) const currentSelectedItem = flatData.filter(({ id }) => currentSelected[id] && currentSelected[id] !== 'none'); ... } }
修改方式就是將其中一個 id 的名字改了,比如把里面的 id 改成 itemId:
const currentSelectedItem = flatData.filter(({ id: itemId }) => currentSelected[itemId] && currentSelected[itemId] !== 'none');
歡迎在評論區(qū)分享你在項目中遇到的 ESLint
問題????
5 感謝
在 Vue DevUI 組件庫 ESLint 問題清零之路上,不得不提的一位小伙伴就是 @linxiang07 同學(xué),他從2022年3月到7月,持續(xù)4個多月,累計修復(fù)40多個組件的100多個 ESLint 問題,直到7月5日 ESLint 清零,并給 PR 添加 ESLint 門禁,后續(xù) PR 如果未通過 ESLint 檢查將無法合入。
感謝 linxiang
同學(xué)的付出!
以下是 linxiang
同學(xué)提交的部分PR:
linxiang
同學(xué)也因此成為 Vue DevUI
組件庫 TOP3 的貢獻(xiàn)者,并成為我們的 Committer 和管理員????
值得一提的是 linxiang
同學(xué)還是我們的 VirtualList
虛擬列表組件和 Tree
組件等多個組件的田主和貢獻(xiàn)者,并且完善了多個組件的單元測試,能力很強(qiáng),是非?;钴S和積極的貢獻(xiàn)者。
以上就是Vue項目中引入ESLint校驗代碼避免代碼錯誤的詳細(xì)內(nèi)容,更多關(guān)于Vue引入ESLint代碼校驗的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue 組件中使用 transition 和 transition-group實現(xiàn)過渡動畫
本文給大家分享一下vue 組件中使用 transition 和 transition-group 設(shè)置過渡動畫,總結(jié)來說可分為分為 name 版, js 鉤子操作類名版, js 鉤子操作行內(nèi)樣式版,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友參考下吧2019-07-07Vue.js+express利用切片實現(xiàn)大文件斷點續(xù)傳
斷點續(xù)傳就是要從文件已經(jīng)下載的地方開始繼續(xù)下載,本文主要介紹了Vue.js+express利用切片實現(xiàn)大文件斷點續(xù)傳,具有一定的參考價值,感興趣的可以了解下2023-05-05vue如何實現(xiàn)路由跳轉(zhuǎn)到外部鏈接界面
這篇文章主要介紹了vue如何實現(xiàn)路由跳轉(zhuǎn)到外部鏈接界面,具有很好的參考價值,希望對大家有所幫助。2022-10-10vue3.0使用vue-pdf-embed在線預(yù)覽pdf 控制頁碼顯示范圍不生效問題解決
這篇文章主要介紹了vue3.0使用vue-pdf-embed在線預(yù)覽pdf 控制頁碼顯示范圍不生效問題的問題及解決方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-01-01使用vuedraggable實現(xiàn)從左向右拖拽功能
這篇文章主要為大家詳細(xì)介紹了使用vuedraggable實現(xiàn)從左向右拖拽功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04vuejs開發(fā)組件分享之H5圖片上傳、壓縮及拍照旋轉(zhuǎn)的問題處理
這篇文章主要介紹了vuejs開發(fā)組件分享之H5圖片上傳、壓縮及拍照旋轉(zhuǎn)的問題處理,需要的朋友可以參考下2017-03-03