如何為前端項目一鍵自動添加eslint和prettier的支持
本文來自讀者@那個曾經的少年回來了 寫的源碼共讀35期筆記文章,授權投稿,寫的真好。
前言
我之前好多次都是一步一步的安裝eslint和prettier及相關依賴,一個配置文件一個配置文件的粘貼復制,并修改其中的相關配置。而且可能會在每個項目中都要去處理,如果項目工程規(guī)劃化以后,eslint和prettier確實是項目少不了的配置。不知道你有沒有像我一樣操作過呢?
那么有沒有一種更簡單的方式去處理呢?答案是我終于遇到了。通過若川大佬的源碼共讀活動發(fā)現了,真的是太棒了。
本文以vite腳手架創(chuàng)建的項目為基礎進行研究的,如果是其他腳手架創(chuàng)建的項目,那么就要自己去修改處理,但是原理是一樣的。
那么接下來,我就要來一探究竟,先看看如何使用,然后查閱一下它的源碼,看看它到底是如何實現的呢?
1、vite創(chuàng)建項目
創(chuàng)建項目
yarn create vite
一頓操作以后項目就創(chuàng)建完畢了

2、安裝依賴
yarn
3、運行項目
yarn dev
4、運行初始化eslint和prettier命令
yarn create vite-pretty-lint
先來看沒有執(zhí)行命令前的文件目錄

再來看執(zhí)行完命令后的文件目錄

可以發(fā)現文件目錄中增加了eslint和prettier的相關配置,package.json中增加了相關的依賴、以及vite.config.xx文件也增加了相關配置,具體的文件變更可以查看https://github.com/lxchuan12/vite-project/commit/6cb274fded66634191532b2460dbde7e29836d2e。
一個命令干了這么多事情,真的太優(yōu)秀了。接下來我們就去看看這如此優(yōu)秀的源代碼吧
2、整個過程的示意圖
通過大致的查看源代碼,簡單總結出來的代碼執(zhí)行過程示意圖,僅供參考

3、源碼調試過程
3.1、找到調試代碼的位置
通過package.json中的bin節(jié)點可以發(fā)現,yarn create vite-pretty-lint最終執(zhí)行的便是lib/main.js中的代碼
"bin": {
"create-vite-pretty-lint": "lib/main.js"
},
3.2、 開始調試的命令
因為我們現在只是要執(zhí)行lib/main.js這個入口文件,通過package.json的scripts 也沒有發(fā)現執(zhí)行命令,所以現在我們可以直接通過node來運行代碼
node lib/main.js
調試成功的結果如下圖所示

3.3、 查看頭部引入的模塊
chalk終端多色彩輸出
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'))
執(zhí)行效果如下圖所示

gradient 文字顏色漸變
// 安裝
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')('你好啊賽利亞歡迎來到編碼世界'));
執(zhí)行效果如下圖所示

child_process node.js中的子進程。
在node.js中,只有一個線程執(zhí)行所有的操作,如果某個操作需要大量消耗CPU資源的話,后續(xù)的操作就需要等待。后來node.js就提供了一個
child_process模塊,通過它可以開啟多個子進程,在多個子進程之間可以共享內存空間,可以通過子進程的互相通信來實現信息的交換。
import { exec } from 'child_process';
exec('ls',(error, stdout,stderr)=> {
if(error) {
console.log(error)
return;
}
console.log('stdout: ' + stdout)
console.log('執(zhí)行其他操作')
})
執(zhí)行效果如下圖所示

fs fs用來操作文件的模塊
import fs from 'fs'
// 同步的讀取方法,用來讀取指定文件中的內容
fs.readFileSync()
// 同步的寫入方法,用來向指定文件中寫內容
fs.writeFileSync()
path路徑分類
import path from 'path';
// 拼接路徑
console.log(path.join('src', 'task.js')); // src/task.js
nanospinner命令行中的加載動畫
// 安裝
npm i nanospinner
// 引入模塊
import { createSpinner } from 'nanospinner';
const spinner = createSpinner('Run test').start()
setTimeout(() => {
spinner.success()
}, 1000)
執(zhí)行效果如下圖所示(Run test在加載的一個效果)

enquirer (utils.js文件)
交互式詢問CLI 簡單說就是交互式詢問用戶輸入
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')
}
執(zhí)行效果如下圖所示:先出來一個下拉選擇,選擇完后根據if判斷進行輸出

3.4、 調試具體代碼
3.4.1、 main.js中的入口
async function run() {
// 所有的邏輯代碼
}
run().catch((e) => {
console.error(e);
});
通過run函數封裝異步方法,這樣最外面調用run函數時可以通過異步方法的catch捕獲錯誤異常。
看一個小例子
const runTest = async () => {
console.log('Running test')
throw new Error('run test報錯了')
}
runTest().catch(err => {
console.log('Error: ' + err)
})
執(zhí)行后打印順序如下
Running test
Error: Error: run test報錯了
可以發(fā)現catch中截獲了異常
接下來開始進入run函數了
3.4.2、 打印色彩字體
// 這個看上面的引入模塊解析即可
console.log(
chalk.bold(
gradient.morning('\n?? Welcome to Eslint & Prettier Setup for Vite!\n')
)
);
3.4.3、 交互式命令行
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]
const { packages, eslintOverrides } = await import(
`./templates/${projectType}.js`
);
/template/vue-ts.js模板中的代碼(其中代碼較多但一看就明白我就不貼了),就是export導出了兩個固定的模板變量數組,packages則相當于要引入的npm模塊列表,eslintOverrides這算是.eslintrc.json初始化模板。
3.4.5、拼接變量數組
const packageList = [...commonPackages, ...packages];
const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides];
const eslint = { ...eslintConfig, overrides: eslintConfigOverrides };
commonPackages是shared.js中預定義的公共的npm 模塊eslint則是通過公共npm模塊中的eslintConfig和上面選擇的template/xxxx.js中的進行拼接組成。
3.4.6、 生成安裝依賴包的命令
const commandMap = {
npm: `npm install --save-dev ${packageList.join(' ')}`,
yarn: `yarn add --dev ${packageList.join(' ')}`,
};
將packageList數組通過join轉換為字符串,通過命令將所有拼接npm模塊一起安裝

3.4.7、 讀取項目的vite配置文件
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中的所有字符
通過babel的parseSync轉換為ast。ast對象如下圖所示

對ast數據進行了一系列的處理后,再通過babel的transformFromAstSync將ast轉換為代碼字符串。
對于babel處理這一塊我也不太了解,有時間我得去加一下餐,具體的可以參考 https://juejin.cn/post/6844904008679686152
3.4.8 執(zhí)行命令、執(zhí)行完將eslint和prettier配置重寫
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來創(chuàng)建一個命令行中的加載,然后通過child_process中的exec來執(zhí)行[3.4.6]中生成的命令,去安裝依賴并進行等待。
如果命令執(zhí)行成功,則通過fs.writeFileSync將生成的數據寫入到三個文件當中.eslintrc.json、.prettierrc.json、.eslintignore、vite.config.xx。
4、npm init、npx
印象里面大家可能對它的記憶可能都停留在,npm init之后是快速的初始化package.json,并通過交互式的命令行讓我們輸入需要的字段值,當然如果想直接使用默認值,也可以使用npm init -y。
create-app-react創(chuàng)建項目命令,官網鏈接可以直接查看 https://create-react-app.dev/docs/getting-started
//官網的三種命令
npx create-react-app my-app
npm init react-app my-app
yarn create react-app my-app
//我又發(fā)現npm create也是可以的
npm create react-app my-app
上述這些命令最終效果都是可以執(zhí)行創(chuàng)建項目的
同樣的vite創(chuàng)建項目的命令
//官網的命令
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
可以發(fā)現vite官網沒有使用npx命令,不過我在我自己電腦上嘗試了另外幾個命令確實也是可以的
npx create-vite my-app
npm init vite my-app

通過上面的對比可以一個小問題,yarn create去官網查了是存在這個指令的,官網地址可看 https://classic.yarnpkg.com/en/docs/cli/create#search
而對于npm create這個命令在npm官網是看不到的,但是在一篇博客中發(fā)現了更新日志

意思就是說npm create xxx和npm init xxx 以及yarn create xxx效果是一致的。那么我們來本文的命令行
// 我們是通過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指令的

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

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

關于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詳細的學習一下,查了很多資料發(fā)現AST還是非常重要的,無論是babel、webpack、vite、vue、react、typescript等都使用到了AST。
