前端腳手架通過自動化的方式可以提高開發效率并減少重復工作,而最強大的腳手架并不是現成的那些工具而是屬于你自己團隊量身定制的腳手架!本篇文章將帶你了解腳手架開發的基本技巧,幫助你掌握如何構建適合自己需求的工具,并帶著你一步步走向前端開發的全新高度。
目錄
初始化項目
模板命令操作
倉庫文檔操作
模板下載操作
模板渲染操作?
初始化項目
關于腳手架的基本概念和一些常用工具的講解,上篇文章 地址?已經講解的比較清楚了,本篇文章不再贅述,接下來我們還是初始化一個新的項目進行操作,終端執行 pnpm init 初始化項目:
然后接下來我們執行如下命令安裝腳手架需要的相關插件,不清楚的可以參考我上篇文章講解:
pnpm install commander @inquirer/prompts chalk ini ora -s
pnpm install @types/node typescript nodemon -D
安裝完成一些基礎插件之后,接下來我們需要設置ts模塊然后將ts編譯成js然后在運行項目,終端執行如下命令生成ts配置文件,執行報錯全局cmd安裝一下?npm i -g typescript 即可:
tsc --init
然后我們根據自身情況配置如下內容即可,
{"compilerOptions": {"target": "es6", // 編譯成es6代碼"module": "NodeNext", // 模塊選擇es6"outDir": "bin","moduleResolution": "nodenext", // 模塊解析策略"esModuleInterop": true, // 允許導入非ES模塊"resolveJsonModule": true, // 允許導入json模塊"rootDir": "src", // 根目錄"baseUrl": "./src" // 基礎目錄},"include": ["src"],"exclude": ["node_modules"],
}
然后我們設置編譯打包內容如下所示,執行打包命令組織和執行我們定義的關鍵字就能執行了
但是每次寫完代碼都要重新打包然后再執行一遍,很費時間所以這里我們通過nodemon來設置自動編譯打包執行,nodemon提供了許多實用的命令行選項幫助定制其行為,以下是一些常用的選項:
參數 | 說明 |
---|---|
-w 或 --watch | 指定監視的文件或文件夾 |
-e 或 --ext | 指定要監視的文件擴展名 |
-i 或 --ignore | 指定要忽略的文件或文件夾 |
-d 或 --delay | 設置文件變化后的延遲重啟時間(單位為秒) |
--exec | 執行指定的命令而不是直接啟動 node 命令 |
-r 或 --require | 加載一個模塊,通常用于加載環境配置或預處理腳本 |
根據上面的規則配置了如下script命令,時刻監聽src目錄下所以ts文件,一旦有變化就執行tsc:
"scripts": {"test": "echo \"Error: no test specified\" && exit 1","dev": "nodemon --watch ./src --ext ts --exec tsc","build": "tsc"
},
效果如下,當我們修改ts文件之后,就會立即熱更新編譯成js文件,效果不錯:
模板命令操作
????????隨著腳手架框架的不斷累積和完善,模板命令也會越來越復雜以適應不同場景下的模板創建,所以我們需要對我們的模板創建命令進行一個抽離和封裝以方便后期簡化操作,這里我們直接在src目錄下新建一個commands文件夾,里面存放封裝commands命令的內容以及命令對應要執行的函數內容,當然這里根據個人喜好配置,博主設置的內容如下所示:
基礎options設置:在基礎的封裝options函數中,這里我將讀取的json文件里面的內容傳遞了進去,對于json文件的讀取,前端tsconfig中已經設置了 "resolveJsonModule": true, // 允許導入JSON模塊 這個配置,我之前還用的好好的,能直接引入pack文件然后讀取使用,后面可能由于ts版本或者其他因素版本的影響導致讀取不了數據了,這里我就抽離了一個工具函數,兩個方法都能讀取json文件的內容:
import fs from 'fs';
import { createRequire } from "module";
import { fileURLToPath } from "url";
import { dirname, join } from "path";const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const require = createRequire(_filename);
const pkg = require(join(_dirname, '../../package.json'));
const loadJSON = (path) => JSON.parse(fs.readFileSync(new URL(path, import.meta.url)).toString());
const Pack = loadJSON('../../package.json');export {Pack,pkg
}
拿到json文件里面的內容數據之后,我們就可以傳遞到基礎配置當中,直接設置腳手架的名稱、描述、作者及版本等相關項目的配置:
import { program } from "commander";const baseOptions = (Pack) => {program.name(Pack.name).version(Pack.version, '-v, --version', '輸出當前版本號').usage('<command>(必填項) [options](可選項)').description(`${Pack.description} (作者:${Pack.author})`).addHelpText("after", `\nRun yyue-cli <command> --help for detailed usage of given command.\n`)
};export default baseOptions;
command命令配置:接下來開始寫command命令配置,這個是大頭,用戶在第一次接觸腳手架使用的使用,都是通過command設置的命令配置,才能了解到腳手架如何使用。因為command命令配置后期可能隨著腳手架的復雜度的升高會導致產生各自各樣的命令,所以這里我們需要對其命令配置抽離出一個對象,然后通過循環遍歷的方式來生成對應的command配置,也很方便,具體實現的代碼如下所示:
import { program } from "commander";// 配置指令命令
const mapActions = {create: {alias: 'c',description: 'create a new project',options: [{ flags: "-f, --force", description: "overwrite target directory if it exists" }],examples: ['yyue-cli create <project-name>'],action: async (name, option) => (await import("../hook/create.js")).default(name, option),},config: {alias: 'conf',description: 'config project variable',options: [{ flags: "-g, --get <k>", description: "get value from path" },{ flags: "-s, --set <k> <v>", description: "set value to path" },{ flags: "-d, --delete <k>", description: "delete value from path" }],examples: ['yyue-cli conf set <k><v>', 'yyue-cli conf get <k>'],action: async (value, option) => (await import("../hook/config.js")).default(value, option),},'*': {alias: '',description: 'command not found',options: [],examples: ['yyue-cli <cmd>'],action: () => console.log('command not found'),}
};const CustomCommand = () => {// 循環創建命令Reflect.ownKeys(mapActions).forEach((key: string) => {const { alias, description, options, action } = mapActions[key];const cmd = program.command(key) // 配置命令名稱.alias(alias) // 配置命令別名.description(description); // 配置命令描述if (key === 'create') {cmd.argument('<project-name>', 'name of the project');} else if(key === 'config') {cmd.argument('<k>', 'key of the variable').argument('<v>', 'value of the variable');} else if (key === '*') {cmd.argument('<cmd>', 'command of the project');}// 配置選項options?.forEach((option) => {cmd.option(option.flags, option.description);});cmd.action(action); // 配置命令執行函數});// 監聽用戶的help事件program.on('--help', () => {console.log("\nExamples:")Reflect.ownKeys(mapActions).forEach((key: string) => {const { examples } = mapActions[key];examples?.forEach((example) => {console.log(` ${example}`);})});})
};export default CustomCommand;
每個命令的執行都有其對應的action函數來進行執行,這里的action也是大頭,所以說這里我們仍然將其抽離出封裝成一個hook函數,方便后期的維護,增大耦合度才能讓項目的維護更加方便,運行的效果如下所示,感覺還是不錯的哈:
倉庫文檔操作
當我們通過模板的一些創建命令交互式的選擇好我們想要創建的模板之后,我們就需要從倉庫中拉取事先配置好的模板到本地目錄中,因為github有時候由于網絡原因訪問過慢,所以我們可以將模板都配置到國內的 碼云?遠程倉庫當中,該倉庫也配置了 api文檔?方便對倉庫當中的一些項目進行操作,這也方便了我們遠程拉取模板的操作,如下所示我們點擊右上角的申請授權之后,每個接口都會自動添加對于的access_token,后面我們根據自身情況來選擇傳遞對于的參數:
然后我們在untils工具文件夾下封裝一個axios工具函數,這里注意一下當我們對對git服務進行請求的時候,可能會出現如下的問題,導致這個問題的原因就是因為git服務的ssl協議沒有通過驗證,我們可以重新生成正規的SSL證書,當然有可能我們的gitlab就是用的ip地址,這時候也可以通過關閉驗證直接解決,具體直接通過如下代碼所示:
git SSL certificate problem: unable to get local issuer certificate
// axios基礎封裝
import axios from 'axios'
import * as https from 'https'const http = axios.create({baseURL: 'https://gitee.com/api/v5',timeout: 5000,httpsAgent: new https.Agent({rejectUnauthorized: false // 拒絕校驗未被授權的證書(SSL證書)})
})// 請求攔截器
http.interceptors.request.use(config => {// 請求攔截器return config
}, error => {return Promise.reject(error)
})// 響應攔截器
http.interceptors.response.use(res => {return res.data}, error => {return Promise.reject(error)}
)export default http
接下來我們就可以創建一個api文件夾,然后可以調用git服務接口來獲取倉庫的一些相關信息:
// 統一管理倉庫的相關接口
import http from '@utils/http.js'const access_token = '你的授權token值'
// 統一管理接口
enum API {REPOS_URL = '/user/repos', // 列出授權用戶的所有倉庫
}
// 列出授權用戶的所有倉庫接口
export const reqGetRepositoriesProjects = (data) =>http.get<any, any>(API.REPOS_URL, {headers: {'Authorization': `Bearer ${access_token}`,},data: { access_token, ...data }})
接口寫完之后我們就可以在command命令行中的action事件函數當中調用改接口,可以看到我們倉庫當中的所以倉庫信息都被打印出來了:
當然gitee申請授權的token是有過期時間的,如果想設置不過期的token需要打開個人中心,然后找到私人令牌,然后給選擇令牌的過期是時間是永不過期即可
當然后面如果配置好模板之后,想把模板設置私有倉庫下載的話,可以設置一下ssh密鑰,執行如下命令在git bash命令行上,生成ssh key:
ssh-keygen -t ed25519 -C "Gitee SSH Key"
輸入命令一直回車即可:?
查看生成的 SSH 公鑰和私鑰,輸出:私鑰文件 id_ed25519;公鑰文件 id_ed25519.pub
ls ~/.ssh/
讀取公鑰文件 ~/.ssh/id_ed25519.pub,輸出密鑰之后,復制到gitee的ssh公鑰配置上:
cat ~/.ssh/id_ed25519.pub
模板下載操作
當我們配置好模板命令、交互選擇以及倉庫文檔等操作之后,我們就可以下載我們的模板了,獲取到項目模板名稱和對應的版本之后,我們就可以直接下載了,上篇文章:?地址?我們下載模板的庫是 git-clone,這里不再過多贅述,這里我們通過gitee的命令獲取gitee上的私有倉庫,并且倉庫的特征關鍵字是template,如下所示:
接下來我們通過上面的一個簡單的示例,把私有倉庫當中的的template-test內容down到本地當中,如下所示:
模板渲染操作?
????????如果用戶想定制下載模板中的內容,這里我們就需要對模板渲染進行操作,拿package.json舉例,用戶可以根據終端交互命令選擇的項目名稱和一些其他操作,根據相對于的詢問生成最終下載的模板的package.json內容,核心原理就是將下載的模板文件依次遍歷根據用戶填寫的信息渲染模板,然后將渲染的模板拷貝到執行目錄下,這里我們需要將模板渲染用到的插件進行安裝,終端執行如下命令操作:
// metalsmith: 遍歷所有文件目錄配置json渲染
pnpm i metalsmith -D
安裝完成之后,這里我把渲染模板文件的功能函數抽離出來,具體的代碼如下所示:
import Metalsmith from 'metalsmith';
import { promisify } from 'util';
import { ejs } from 'consolidate';
import path from 'path';
import fs from 'fs-extra';let { render } = ejs;
render = promisify(render);export const handleTemplateRenders = async (name, metadataData = {}) => {const projectRoot = path.join(process.cwd(), name); // 獲取項目根路徑if (!fs.pathExistsSync(projectRoot)) { // 確保項目目錄存在console.error(`項目目錄不存在: ${projectRoot}`);return;}// 配置元數據const metadata = {name: name,author: 'Your Name',date: new Date().toLocaleDateString(),...metadataData};// 創建一個臨時變量來存儲需要處理的文件let filesToProcess = [];// 創建 Metalsmith 實例await new Promise<void>((resolve, reject) => {Metalsmith(projectRoot).source('.') // 從項目根目錄讀取文件.destination('.') // 輸出到項目根目錄(覆蓋原始文件).clean(false) // 不清除目標目錄,避免刪除其他文件.metadata(metadata) // 設置元數據// 第一個插件:收集需要處理的文件.use((files, metalsmith, done) => {// 過濾需要處理的文件類型filesToProcess = Reflect.ownKeys(files).filter((file: any) => {const fileInfo = files[file];const content = fileInfo.contents.toString();const hasEjsTags = content.includes('<%') && content.includes('%>'); // 檢查文件是否包含 EJS 標簽return hasEjsTags;});done();})// 第二個插件:處理 EJS 模板.use(async (files, metalsmith, done) => {const meta = metalsmith.metadata();try {for (const file of filesToProcess) {const fileInfo = files[file];const originalContent = fileInfo.contents.toString();const renderedContent = await render(originalContent, meta); // 渲染 EJS 模板// 更新文件內容files[file].contents = Buffer.from(renderedContent);}done();} catch (err) {console.error('渲染模板時出錯:', err);done(err);}}).build(err => {if (err) {console.error('Metalsmith 構建失敗:', err);reject(err);} else {resolve();}});});// 驗證渲染結果validateRenderResults(name);
};// 驗證渲染結果的輔助函數
function validateRenderResults(name) {const projectRoot = path.join(process.cwd(), name);const packageJsonPath = path.join(projectRoot, 'package.json');if (fs.pathExistsSync(packageJsonPath)) {try {const content = fs.readFileSync(packageJsonPath, 'utf8');const pkg = JSON.parse(content);console.log('\n=== 渲染結果驗證 ===');console.log('package.json 中的 name:', pkg.name);console.log('package.json 中的 author:', pkg.author);if (pkg.name === '<%= name %>' || pkg.author === '<%= author %>') {console.error('? 渲染失敗: EJS 模板語法未被正確替換');} else {console.log('? 渲染成功: EJS 模板語法已被正確替換');}} catch (err) {console.error('驗證渲染結果時出錯:', err);}}
}
上面代碼封裝的功能函數中,形參name就是項目文件夾,metadataData就是你要渲染的模板的實際數據,這里我在倉庫當中設置一個模板語法的package.json文件,如下所示可以看到我們的項目名稱以及對應的作者名稱都是需要通過用戶輸入來動態渲染的:
這里我們在下面完模板之后,調用一下替換模板語法的函數,這里就會當模板下載之后就會立即遍歷整個文件夾,找到對應的有模板語法的文件,然后進行替換:
實現的效果如下所示,可以看到效果非常好,后期也可以根據自身的項目需求,讓這個模板渲染變得更加復雜以適應不同的項目情況,這些都是可以的:
當然我們還可以通過EJS來實現模板渲染, EJS(Embedded JavaScript)是一個模板引擎,允許在HTML中插入動態內容,可以通過EJS渲染數據并生成最終的HTML頁面,終端執行如下命令安裝插件:?
// ejs: 動態渲染數據并生成最終的HTML頁面
pnpm i ejs -D
EJS允許在HTML模板中嵌入JavaScript代碼,用來動態生成內容,基本案例如下所示:
const ejs = require('ejs');const data = { title: 'Hello World', body: 'This is a test.' };ejs.renderFile('template.ejs', data, (err, str) => {if (err) {console.error(err);} else {console.log(str); // 渲染后的 HTML 內容}
});
Consolidate.是一個模板引擎的統一接口,它提供了一種統一的方式來使用多種模板引擎(如 EJS、Pug等),通過Consolidate可以輕松地切換不同的模板引擎,終端執行如下命令安裝插件:
// consolidate: 返回渲染函數,統一所有模板引擎
pnpm i consolidate -D
Consolidate.會根據傳入的模板引擎來調用相應的渲染方法,基本用法如下所示:
const consolidate = require('consolidate');
const ejs = consolidate.ejs;ejs.renderFile('template.ejs', { title: 'Hello' }, (err, html) => {if (err) throw err;console.log(html); // 渲染后的 HTML
});