1. 前言
腳手架大家一定都不陌生,比如我們經常使用的 vue-cli、create-react-app,它可以幫助我們快速的初始化一個專案,無需從零配置,極大的方便我們的開發,到這里你可能會疑惑,既然市面上有成熟的腳手架,為什么需要寫一個屬于自己的腳手架呢,因為公共腳手架雖然強大,但并不能滿足我們的實際開發需求,
例如專案中已有的沉淀,專案架構、介面請求的統一處理、換膚、業務組件、eslint配置等,這些想要用到新專案中,只能通過復制粘貼,會存在以下弊端:
-
重復性勞動,繁瑣且浪費時間 -
已有專案沉淀分散在各處,很容易有所遺漏 -
專案間的配置差異很可能會被忽略 -
人工操作永遠都有可能犯錯,建新專案時,總要花時間去排錯
如果我們自己開發一套腳手架,定制自己的模板,復制粘貼的人工流程就會轉換為 cli 的自動化流程, 還可以通過維護不同的模板以適應不同業務需求,既然要開發一套腳手架,站在巨人肩膀上顯然省事多了,我們先來看看業界知名腳手架Vue CLI是如何實作的,
2.Vue CLI 原理分析
Vue CLI 是一個基于 Vue.js 進行快速開發的完整系統,提供:
-
通過 @vue/cli 實作的互動式的專案腳手架, -
通過 @vue/cli + @vue/cli-service-global 實作的零配置原型開發, -
一個運行時依賴 (@vue/cli-service),該依賴: -
可升級; -
基于 webpack 構建,并帶有合理的默認配置; -
可以通過專案內的組態檔進行配置; -
可以通過插件進行擴展, -
一個豐富的官方插件集合,集成了前端生態中最好的工具,
-
一套完全圖形化的創建和管理 Vue.js 專案的用戶界面,
2.1
全域 vue 執行命令存放在哪里
2.2
vue命令是從哪里注冊的
找到原始碼目錄中的package.json,我們會看到如下代碼:
2.3
依賴包分析
包名 | 用途 |
---|---|
commander | 完整的 node.js 命令列解決方案, Commander 負責將引數決議為選項和命令引數 |
shelljs | 用來執行shell命令 |
inquirer | 通用互動式命令列用戶界面的集合 |
semver | 語意化版本控制 |
chalk | 設定終端字串樣式 |
2.4
腳手架都做了哪些事情
2.4.1 HTML 和靜態資源
html檔案是一個會被 html-webpack-plugin 處理的模板,在構建程序中,資源鏈接會被自動注入,另外,Vue CLI 也會自動注入 resource hint (preload/prefetch、manifest 和圖示鏈接 (當用到 PWA 插件時) 以及構建程序中處理的 JavaScript 和 CSS 檔案的資源鏈接,
2.4.2 CSS 相關
Vue CLI生成專案支持 PostCSS、CSS Modules 和包含 Sass、Less、Stylus 在內的前處理器,你可以在創建專案的時候選擇前處理器,
2.4.3 webpack 相關
Vue CLI基于 webpack 構建,并帶有合理的默認配置,可以通過專案內的組態檔進行配置,還可以通過插件進行擴展,
2.4.4 模式與環境變數
模式是 Vue CLI 專案中一個重要的概念,默認情況下,一個 Vue CLI 專案有三個模式:
development 模式用于 vue-cli-service servetest 模式用于 vue-cli-service test:unitproduction 模式用于 vue-cli-service build 和 vue-cli-service test:e2e你可以通過傳遞 --mode 選項引數為命令列覆寫默認的模式,
2.4.5 構建目標
當你運行 vue-cli-service build 時,你可以通過 --target 選項指定不同的構建目標,它允許你將相同的源代碼根據不同的用例生成不同的構建,
通過以上對Vue CLI 的分析,我們就對腳手架工具提供的構建集成能力有了一個大概的了解,這有助于我們在使用具體工具時快速定位問題的邊界,當我們自己設計腳手架的時候,我們也可以參照和借鑒,可以適用于我們業務的有:
-
通過命令列與用戶互動 -
根據用戶的選擇生成對應的檔案,實作的零配置原型開發
-
基于 vite 構建,并帶有合理的默認配置; -
預定義業務模板,根據用戶選擇生成 -
業務模板基礎支持: -
HTML 和靜態資源處理 -
內置css前處理器 -
內置vite配置,可以直接修改vite組態檔 -
內置test、pre、pro三種模式,并生成對應的組態檔
按照上述總結,讓我們一步一步撰寫自己的腳手架吧,首先是通過命令列與用戶互動,那么我們需要有一個可執行命令的名字,也是腳手架的名字,這里我們就叫做dt-fe-cli
3.腳手架實作
3.1
命令列工具撰寫
3.1.1 初始化專案
我們的腳手架叫做dt-fe-cli,創建dt-fe-cli檔案夾,執行npm init -y初始化倉庫,生成package.json檔案,
在dt-fe-cli檔案夾下創建bin檔案夾,并在里面創建cli.mjs檔案,此檔案作為我們腳手架的入口,需要將其配置到package.json的bin欄位,
{
"name": "@auto/dt-fe-cli",
"version": "0.0.1",
"bin": {
"dt-fe-cli": "bin/cli.mjs"
}
}
這樣我們腳手架的入口就有了,繼續撰寫腳手架的功能吧
3.1.2 指令
dt-fe-cli 作為全域命令,同時提供了很多指令,
-
dt-fe-cli --version可以查看 dt-fe-cli 版本 -
dt-fe-cli --help可以查看幫助檔案 -
dt-fe-cli create xxx可以創建一個專案 ...
3.1.3 create命令
create接受一個專案名作為引數,這里還提供了額外選項-f, --force,此選項代表如果本地已經存在同名檔案夾,是否覆寫,命令列解決方案需要依賴第三方庫commander
import create from '../lib/create.mjs'
program
.command('create <app-name>')
.description('create a new project powered by dt-fe-cli')
.option("-f, --force", "overwrite target directory if it exists")
.action((projectName, options) => {
create(projectName, options)
})
3.1.4 create方法設計與實作
執行create命令后,如何創建專案呢,我們公司的專案都是托管在內部gitlab上面的,所以直接使用git clone去拉取模板專案,這里需要依賴第三方庫shelljs,那么這里就需要首先判斷git是否存在,不存在提示并退出,之前我們還寫了一個額外選項,用來表示如果本地已經存在同名檔案夾,是否覆寫,若沒有此選項,還需要互動式的詢問,這里需要依賴第三方庫inquirer,
create.mjs:
import chalk from 'chalk'
import fse from 'fs-extra'
import shelljs from 'shelljs'
import path from 'path'
import inquirer from 'inquirer'
async function create(projectName, options) {
const targetDirectory = path.join(process.cwd(), projectName)
try {
// 判斷是否存在git,不存在則提示并退出
if (!shelljs.which('git')) {
console(chalk.red('Sorry, dt-fe-cli requires git'));
return
}
const isExist = await fse.pathExists(targetDirectory)
// 判斷目錄下是否存在同名檔案夾
if (isExist) {
if (options.force) {
await fse.remove(targetDirectory);
} else {
const { isOverwrite } = await new inquirer.prompt([
{
name: "isOverwrite", // 與回傳值對應
type: "list",
message: "Target directory already exists. Pick an action:",
choices: [
{ name: "Overwrite", value: true },
{ name: "Cancel", value: false },
],
},
]);
// 移除同名檔案夾
if (isOverwrite) {
console.log('remove existing directory...')
await fse.remove(targetDirectory);
} else {
return;
}
}
}
// 專案型別
const { projectType } = await new inquirer.prompt([
{
name: "projectType", // 與回傳值對應
type: "list",
message: "Please select project type:",
choices: [
{ name: "pc", value: 'pc' },
{ name: "h5", value: 'h5' },
],
},
]);
const PROJECT_MAP = {
pc: 'pc.git',
h5: 'h5.git'
}
// 安裝依賴專案
shelljs.exec(`git clone ${PROJECT_MAP[projectType]} ${projectName}`, async (code, stdout, stderr) => {
if (code === 0) {
progress.start()
try {
// 洗掉原有.git
await fse.remove(path.join(process.cwd(), projectName, '.git'))
} catch (error) {
console.log(error)
}
progress.succeed()
console.log(`\r\nSuccessfully created project ${chalk.cyan(projectName)}`);
console.log(`\r\n cd ${chalk.cyan(projectName)}`);
console.log(" git init");
console.log(" pnpm install");
console.log(" pnpm dev");
}
})
} catch (error) {
console.log(error);
}
}
3.1.5 node版本檢查
package.json:
{
"engines": {
"node": ">= 14.18.0"
},
}
cli.mjs:
import { readFile } from 'fs/promises'
import semverSatisfies from 'semver/functions/satisfies.js'
const { engines: { node: requiredVersion }, version } = JSON.parse(
await readFile(
new URL('../package.json', import.meta.url)
)
)
function checkNodeVersion (wanted, id) {
if (!semverSatisfies(process.version, wanted, { includePrerelease: true })) {
console.log(chalk.red(
'You are using Node ' + process.version + ', but this version of ' + id +
' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
))
process.exit(1)
}
}
checkNodeVersion(requiredVersion, 'dt-fe-cli')
到這里,腳手架的基本功能就已經開發完畢了,剩下的就是我們的專案模板了
3.2
模版設計支持功能
3.2.1 TypeScript
使用 Vite3 構建,Vite3 天然支持引入 .ts 檔案
3.2.2 打包自動上傳CDN
const { execSync } = require('child_process');
const { loadEnv } = require('vite')
const env = process.argv[2]
const { VITE_BASE_URL } = loadEnv(env, process.cwd(), '')
const prefix = `${env}${VITE_BASE_URL}`
execSync(`vite build --mode ${env} --base=https://cdn.com/${prefix}`);
uploadCDN({
Dir: `dist/assets`,
Prefix: prefix
})
3.2.3 commit 校驗
npm install husky --save-dev
npm pkg set scripts.prepare="husky install"
npm run prepare
npx husky add .husky/pre-commit "npm run lint"
git add .husky/pre-commit
3.2.4 eslint校驗
ESLint通用配置的部分這里就不再贅述了,這里介紹一下我們業務里面自定義的ESLint插件,eslint校驗大家都很熟悉,市面上也有很多eslint插件,但隨著專案不斷迭代發展,我們團隊的編碼規范使用現有的eslint插件已經無法滿足了,需要自己創建插件,并融入到cli的模板當中,
? 創建插件
開始創建插件的最簡單方法是使用 Yeoman 生成器,生成器將指導您設定插件的骨架
npm i -g yo generator-eslint
yo eslint:plugin
以上命令會生成如下目錄
.
├── README.md
├── lib
│ ├── index.js
│ └── rules
├── package.json
└── tests
└── lib
└── rules
插件可以在 ESLint 中使用的額外規則,為此,插件必須匯出一個包含規則 ID 到規則的鍵值映射的規則物件,舉個簡單的例子,我們想創建一條不允許使用console.log的規則
? 創建規則
yo eslint:rule
此命令會在lib/rules檔案夾下創建一個新的js檔案,一個規則對應一個可匯出的 node 模塊
"use strict";
//-------------------------------------------------------
// Rule Definition
//-------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow unnecessary semicolons",
recommended: true,
url: "https://eslint.org/docs/rules/no-extra-semi"
},
fixable: "code",
schema: [] // no options
},
create(context) {
return {
// callback functions
};
}
};
上面這段代碼是一個規則的原始碼檔案的基本格式,一個規則的源檔案輸出一個物件,它由 meta 和 create 兩部分組成,
?meta(物件)包含規則的元資料,如規則型別、檔案、可接受引數的schema等等
?create (function) 回傳一個物件,其中包含了 ESLint 在遍歷 JavaScript 代碼的抽象語法樹 AST (ESTree 定義的 AST) 時,用來訪問節點的方法,
核心其實在于create方法,我們若想知道如何撰寫create方法,首先要明白其原理,那就是 ESLint 是如何分析我們所撰寫的代碼呢?相信大家對此也都有所了解,沒錯,就是AST (Abstract Syntax Tree(抽象語法樹))
? 插件原理
ESLint 決議器將代碼轉換為 ESLint 可以評估的抽象語法樹,默認情況下,ESLint 使用內置的 Espree 決議器,它與標準的 JavaScript 運行時和版本兼容,然后去攔截檢測是否符合我們規定的書寫方式,最后讓其展示報錯、警告或正常通過,ESLint 的核心就是規則(rules),而定義規則的核心就是利用 AST 來做校驗,那就讓我們看一下代碼 AST 中會表現為什么樣子,
? 撰寫規則
"use strict";
//-------------------------------------------------------
// Rule Definition
//-------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
fixable: "code",
schema: [], // no options
},
create(context) {
return {
// key 是 selector
'CallExpression MemberExpression': (node) => {
const { property, object } = node;
// 如果在 AST 中匹配到了console.log,就用 context.report() 來發布警告或錯誤
if (object.name === 'console' && property.name === 'log') {
context.report({
node,
message: 'console.log is forbidden.'
});
}
}
};
}
};
至此,包含一條規則(禁止使用console.log)的 ESLint 插件就撰寫完成了,接下來將此專案發布到npm平臺就可以在專案模板中下載使用了
最后
本文介紹了如何從零撰寫一個我們自己的腳手架,并且可以根據不同業務場景區分模版,把業務已有的積累沉淀進去,以上便是本次分享的全部內容,希望對你有所幫助 ^_^
作者| 馬春鍵
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/Frontend-Engineering-Practice---Developing-Enterprise-Level-CLI.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/556589.html
標籤:其他
上一篇:前端Vue自定義精美懸浮選單按鈕fab button 可設定按鈕背景顏色 選單按鈕展開條目
下一篇:返回列表