前端脚手架开发指南:提高开发效率的核心操作
前端脚手架通过自动化的方式可以提高开发效率并减少重复工作,而最强大的脚手架并不是现成的那些工具而是属于你自己团队量身定制的脚手架!本篇文章将带你了解脚手架开发的基本技巧,帮助你掌握如何构建适合自己需求的工具,并带着你一步步走向前端开发的全新高度。
目录
初始化项目
模板命令操作
仓库文档操作
模板下载操作
模板渲染操作
初始化项目
关于脚手架的基本概念和一些常用工具的讲解,上篇文章 地址 已经讲解的比较清楚了,本篇文章不再赘述,接下来我们还是初始化一个新的项目进行操作,终端执行 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
});