如何為前端項目一鍵自動添加eslint和prettier的支持

語言: CN / TW / HK

theme: vuepress highlight: tomorrow-night


本文參加了由公眾號@若川視野 發起的每週源碼共讀活動,點擊瞭解詳情一起參與。

本文屬於源碼共讀第35期 | 為 vite 項目自動添加 eslint 和 prettier點擊瞭解本期詳情一起參與

前言

我之前好多次都是一步一步的安裝eslint和prettier及相關依賴,一個配置文件一個配置文件的粘貼複製,並修改其中的相關配置。而且可能會在每個項目中都要去處理,如果項目工程規劃化以後,eslint和prettier確實是項目少不了的配置。不知道你有沒有像我一樣操作過呢?

那麼有沒有一種更簡單的方式去處理呢?答案是我終於遇到了。通過若川大佬的源碼共讀活動發現了,真的是太棒了。

本文以vite腳手架創建的項目為基礎進行研究的,如果是其他腳手架創建的項目,那麼就要自己去修改處理,但是原理是一樣的。

那麼接下來,我就要來一探究竟,先看看如何使用,然後查閲一下它的源碼,看看它到底是如何實現的呢?

1、vite創建項目

  • 創建項目 javascript yarn create vite 一頓操作以後項目就創建完畢了

image.png - 2、安裝依賴 javascript yarn - 3、運行項目 javascript yarn dev

  • 4、運行初始化eslint和prettier命令 javascript yarn create vite-pretty-lint 先來看沒有執行命令前的文件目錄

image.png

再來看執行完命令後的文件目錄

image.png

可以發現文件目錄中增加了eslint和prettier的相關配置,package.json中增加了相關的依賴、以及vite.config.xx文件也增加了相關配置,具體的文件變更可以查看https://github.com/lxchuan12/vite-project/commit/6cb274fded66634191532b2460dbde7e29836d2e。

一個命令幹了這麼多事情,真的太優秀了。接下來我們就去看看這如此優秀的源代碼吧

2、整個過程的示意圖

通過大致的查看源代碼,簡單總結出來的代碼執行過程示意圖,僅供參考

vite-pretty-lint (1).png

3、源碼調試過程

3.1、找到調試代碼的位置

通過package.json中的bin節點可以發現,yarn create vite-pretty-lint最終執行的便是lib/main.js中的代碼 javascript "bin": { "create-vite-pretty-lint": "lib/main.js" },

3.2、 開始調試的命令

因為我們現在只是要執行lib/main.js這個入口文件,通過package.jsonscripts 也沒有發現執行命令,所以現在我們可以直接通過node來運行代碼 javascript node lib/main.js 調試成功的結果如下圖所示

企業微信截圖_16564645675849.png

3.3、 查看頭部引入的模塊

  • chalk終端多色彩輸出 ```javascript npm i chalk

import chalk from 'chalk'

const log = console.log // 字體背景顏色設置 log(chalk.bgGreen('chalk打印設置') )

// 字體顏色設置 log(chalk.blue('Hello') + ' World' + chalk.red('!'))

// 自定義顏色 const custom = chalk.hex('#F03A17') const bgCustom = chalk.bgHex('#FFFFFF') log(custom('customer')) log(bgCustom('bgCustom')) ``` 執行效果如下圖所示

image.png

  • gradient 文字顏色漸變 ```javascript // 安裝 npm i gradient-string // 引入 import gradient from 'gradient-string'

// 使用 console.log(gradient('cyan', 'pink')('你好啊賽利亞歡迎來到編碼世界')); console.log(gradient('cyan', 'pink')('你好啊賽利亞歡迎來到編碼世界')); console.log(gradient('cyan', 'pink')('你好啊賽利亞歡迎來到編碼世界')); console.log(gradient('cyan', 'pink')('你好啊賽利亞歡迎來到編碼世界')); console.log(gradient('cyan', 'pink')('你好啊賽利亞歡迎來到編碼世界')); ``` 執行效果如下圖所示

image.png

  • child_process node.js中的子進程。

在node.js中,只有一個線程執行所有的操作,如果某個操作需要大量消耗CPU資源的話,後續的操作就需要等待。後來node.js就提供了一個child_process模塊,通過它可以開啟多個子進程,在多個子進程之間可以共享內存空間,可以通過子進程的互相通信來實現信息的交換。 ```javascript import { exec } from 'child_process';

exec('ls',(error, stdout,stderr)=> { if(error) { console.log(error) return; } console.log('stdout: ' + stdout) console.log('執行其他操作') }) ``` 執行效果如下圖所示

image.png - fs fs用來操作文件的模塊

```javascript import fs from 'fs'

// 同步的讀取方法,用來讀取指定文件中的內容 fs.readFileSync()  // 同步的寫入方法,用來向指定文件中寫內容 fs.writeFileSync()  ```

  • path路徑分類 ```javascript import path from 'path';

// 拼接路徑 console.log(path.join('src', 'task.js')); // src/task.js

```

  • nanospinner命令行中的加載動畫 ```javascript // 安裝 npm i nanospinner

// 引入模塊 import { createSpinner } from 'nanospinner';

const spinner = createSpinner('Run test').start()

setTimeout(() => { spinner.success() }, 1000) ``` 執行效果如下圖所示(Run test在加載的一個效果)

3.gif

  • enquirer (utils.js文件)

交互式詢問CLI 簡單説就是交互式詢問用户輸入 ```javascript npm i enquirer

import enquirer from 'enquirer'

let tempArray = ['major(1.0.0)','minor(0.1.0)', 'patch(0.0.4)', "customer" ] const { release } = await enquirer.prompt({ type: 'select', name: 'release', message: 'Select release type', choices: tempArray })

if(release === 'customer') { console.log(release, 'customer') } else { const targetVersion = release.match(/((.*))/)[1] console.log(targetVersion, 'targetVersion') }

``` 執行效果如下圖所示:先出來一個下拉選擇,選擇完後根據if判斷進行輸出

4.gif

3.4、 調試具體代碼

3.4.1、 main.js中的入口

```javascript async function run() { // 所有的邏輯代碼 }

run().catch((e) => { console.error(e); }); `` 通過run函數封裝異步方法,這樣最外面調用run函數時可以通過異步方法的catch`捕獲錯誤異常。

看一個小例子 ```javascript const runTest = async () => { console.log('Running test') throw new Error('run test報錯了') }

runTest().catch(err => { console.log('Error: ' + err) }) 執行後打印順序如下javascript Running test Error: Error: run test報錯了 ``` 可以發現catch中截獲了異常

接下來開始進入run函數了

3.4.2、 打印色彩字體

javascript // 這個看上面的引入模塊解析即可 console.log( chalk.bold( gradient.morning('\n🚀 Welcome to Eslint & Prettier Setup for Vite!\n') ) );

3.4.3、 交互式命令行

```javascript export function getOptions() { const OPTIONS = []; fs.readdirSync(path.join(__dirname, 'templates')).forEach((template) => { const { name } = path.parse(path.join(__dirname, 'templates', template));

OPTIONS.push(name);

}); return OPTIONS; }

export function askForProjectType() { return enquirer.prompt([ { type: 'select', name: 'projectType', message: 'What type of project do you have?', choices: getOptions(), }, { type: 'select', name: 'packageManager', message: 'What package manager do you use?', choices: ['npm', 'yarn'], }, ]); }

try { const answers = await askForProjectType(); projectType = answers.projectType; packageManager = answers.packageManager; } catch (error) { console.log(chalk.blue('\n👋 Goodbye!')); return; } ```

getOptions 函數根據fs.readdirSync讀取項目工程template文件夾下的所有文件,並通過path.parse轉換對象,來獲取文件名稱name

askForProjectType函數通過enquirer.prompt返回兩個交互式命令行,供用户進行選擇 projectType選擇項目類型: 【react-ts】 【react】【vue-ts】 【vue】 packageManager選擇項目包管理方式:【npm】 【yarn】

3.4.4、根據交互命令行返回結果進行匹配模板

假如我們上面選擇的是[vue-ts]

js const { packages, eslintOverrides } = await import( `./templates/${projectType}.js` ); /template/vue-ts.js模板中的代碼(其中代碼較多但一看就明白我就不貼了),就是export導出了兩個固定的模板變量數組,packages則相當於要引入的npm模塊列表,eslintOverrides這算是.eslintrc.json初始化模板。

3.4.5、拼接變量數組

javascript const packageList = [...commonPackages, ...packages]; const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides]; const eslint = { ...eslintConfig, overrides: eslintConfigOverrides }; commonPackagesshared.js中預定義的公共的npm 模塊 eslint則是通過公共npm模塊中的eslintConfig和上面選擇的template/xxxx.js中的進行拼接組成。

3.4.6、 生成安裝依賴包的命令

javascript const commandMap = { npm: `npm install --save-dev ${packageList.join(' ')}`, yarn: `yarn add --dev ${packageList.join(' ')}`, };packageList數組通過join轉換為字符串,通過命令將所有拼接npm模塊一起安裝

image.png

3.4.7、 讀取項目的vite配置文件

```javascript const projectDirectory = process.cwd();

const viteJs = path.join(projectDirectory, 'vite.config.js'); const viteTs = path.join(projectDirectory, 'vite.config.ts'); const viteMap = { vue: viteJs, react: viteJs, 'vue-ts': viteTs, 'react-ts': viteTs, };

const viteFile = viteMap[projectType]; const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8')); const installCommand = commandMap[packageManager];

if (!installCommand) { console.log(chalk.red('\n✖ Sorry, we only support npm and yarn!')); return; } ``` 根據選擇的項目類型,來拼接vite.config的路徑,並讀取項目中的vite.config配置文件

上面用到了一個函數viteEslint,這個具體的實現可以去看shared.js中,主要就是讀取文件內容後,傳入的參數code,就是vite.config.ts中的所有字符

1656558114378.png 通過babel的parseSync轉換為ast。ast對象如下圖所示

1656558646620.png

對ast數據進行了一系列的處理後,再通過babeltransformFromAstSync將ast轉換為代碼字符串。

對於babel處理這一塊我也不太瞭解,有時間我得去加一下餐,具體的可以參考 https://juejin.cn/post/6844904008679686152

3.4.8 執行命令、執行完將eslint和prettier配置重寫

``javascript const spinner = createSpinner('Installing packages...').start(); exec(${commandMap[packageManager]}`, { cwd: projectDirectory }, (error) => { if (error) { spinner.error({ text: chalk.bold.red('Failed to install packages!'), mark: '✖', }); console.error(error); return; }

const eslintFile = path.join(projectDirectory, '.eslintrc.json');
const prettierFile = path.join(projectDirectory, '.prettierrc.json');
const eslintIgnoreFile = path.join(projectDirectory, '.eslintignore');

fs.writeFileSync(eslintFile, JSON.stringify(eslint, null, 2));
fs.writeFileSync(prettierFile, JSON.stringify(prettierConfig, null, 2));
fs.writeFileSync(eslintIgnoreFile, eslintIgnore.join('\n'));
fs.writeFileSync(viteFile, viteConfig);

spinner.success({ text: chalk.bold.green('All done! 🎉'), mark: '✔' });
console.log(
  chalk.bold.cyan('\n🔥 Reload your editor to activate the settings!')
);

}); `` 首先通過createSpinner來創建一個命令行中的加載,然後通過child_process中的exec來執行[3.4.6]`中生成的命令,去安裝依賴並進行等待。

如果命令執行成功,則通過fs.writeFileSync將生成的數據寫入到三個文件當中.eslintrc.json.prettierrc.json.eslintignorevite.config.xx

4、npm init、npx

印象裏面大家可能對它的記憶可能都停留在,npm init之後是快速的初始化package.json,並通過交互式的命令行讓我們輸入需要的字段值,當然如果想直接使用默認值,也可以使用npm init -y

create-app-react創建項目命令,官網鏈接可以直接查看 https://create-react-app.dev/docs/getting-started ```js //官網的三種命令 npx create-react-app my-app npm init react-app my-app yarn create react-app my-app

//我又發現npm create也是可以的 npm create react-app my-app ``` 上述這些命令最終效果都是可以執行創建項目的

同樣的vite創建項目的命令 ```javascript //官網的命令 npm create vite@latest yarn create vite pnpm create vite

// 指定具體模板的 // npm 6.x npm create vite@latest my-vue-app --template vue //npm 7+, extra double-dash is needed: npm create vite@latest my-vue-app -- --template vue

yarn create vite my-vue-app --template vue

pnpm create vite my-vue-app --template vue

```

可以發現vite官網沒有使用npx命令,不過我在我自己電腦上嘗試了另外幾個命令確實也是可以的 javascript npx create-vite my-app npm init vite my-app

image.png

通過上面的對比可以一個小問題,yarn create去官網查了是存在這個指令的,官網地址可看 https://classic.yarnpkg.com/en/docs/cli/create#search

而對於npm create這個命令在npm官網是看不到的,但是在一篇博客中發現了更新日誌

image.png

意思就是説npm create xxxnpm init xxx以及yarn create xxx效果是一致的。那麼我們來本文的命令行

```javascript // 我們是通過npm安裝的,並且包名裏是包含create的 npm i create-vite-pretty-lint

// 那麼以下幾種方式都可以使用的 npm init vite-pretty-lint npm create vite-pretty-lint yarn create vite-pretty-lint npx create-vite-pretty-lint

```

再來看一下npx

假如我們只在項目中安裝了vite,那麼node_modules.bin文件夾下是會存在vite指令的

image.png

如果我們想在該項目下執行該命令第一種方式便是

image.png

第二種方式就是直接在package.json的scripts屬性下

image.png

關於npx的詳細説明可以看一下阮一峯大佬的精彩分享 http://www.ruanyifeng.com/blog/2019/02/npx.html

5、總結

  • npm init xxx的妙用,以及對npx的瞭解,感覺對package.json的每一個屬性,可以專門去學習一下

  • 對於自動添加eslint和prettier配置的原理分析

  • .eslintrc.json、.eslintignore、.prettierrc.json算是直接新增文件,處理相對簡單一些

  • 最重要的學習點:對vite.config文件在原有基礎上的修改,這裏就涉及到了AST抽象語法樹

6、加餐 V8下的AST抽象語法樹

有興趣的話可以看看我前幾天剛剛總結的關於V8引擎是如何運行JavaScript代碼的,其中就涉及到關於AST的部分https://juejin.cn/post/7109410330295402509

接下來有時間我會簡單的把AST詳細的學習一下,查了很多資料發現AST還是非常重要的,無論是babel、webpack、vite、vue、react、typescript等都使用到了AST。

我正在參與掘金技術社區創作者簽約計劃招募活動,點擊鏈接報名投稿

「其他文章」