超详细的前端脚手架入门篇
脚手架初认识
其实,我们通过平时使用的脚手架就不难推测得出脚手架的基本作用了。
比如: Vue-cli 就是一个脚手架,能够帮助我们快速搭建和配置 vuejs 项目,它提供了基本的项目结构、构建流程和开发工具链,让开发者更专注于业务的开发,提高开发和部署效率。
又比如:npm 一款依赖管理工具,简化了依赖安装和打包构建的过程。
又比如:commitizen 能够帮助我们去规范 git commit 的信息和结构,提高项目的规范性。
所以,前端脚手架的有以下优点:
快速初始化:脚手架可以自动生成项目所需的基本文件结构、配置文件和示例代码,使项目的初始化过程变得简单和快速。规范项目结构:脚手架定义了一套标准的项目结构和文件组织方式,使团队成员能够在不同项目中保持一致的开发风格和规范,提高团队协作效率。依赖管理和构建流程:脚手架通常集成了依赖管理工具(如npm、yarn)和构建工具(如Webpack、Rollup)。可扩展性:脚手架提供了一些自定义配置的选项,允许开发人员根据项目需求进行灵活的配置和扩展,满足特定的需求。你将学到什么
本文是一篇脚手架入门文,适合想要搭建自己的脚手架却不知道从何入手的开发者。
如何搭建一个脚手架工程。如何开发和调试一个脚手架脚手架中如何接收和处理命令参数。如何实现文件的拷贝。如何创建询问式的交互。如何处理路径问题。本文完整代码已上传至 github
实现
环境
node 16.14+包管理工具:pnpm 8.5+第三方依赖
yargs node 命令解决方案。inquirer 在 shell 命令行中建立询问式交互copy-dir 实现文件拷贝mustache 动态更改文件ora 在 shell 命令行中实现加载动画fs-extra 操作文件的库,比 node 自带的 fs 更强大一些如何搭建一个脚手架工程
先创建一个脚手架的文件夹,如:simple-cli,并执行初始化。# 创建文件夹 mkdir simple-cil # 进入文件夹 cd simple-cli # 初始化 npm init -y 123456 创建bin文件夹,添加index.js。
#! /usr/bin/env node console.log('hello cli'); 123 在package.json中指定执行命令和执行的文件。
"scripts": { "cli": "node ./bin/index.js" }, "bin": { "cli": "./bin/index.js" }, 123456 在当前根目录下,执行 pnpm cli,得到以下结果:
如何开发和调试一个脚手架
/bin 放置的是入口文件,我们新建一个 /src 目录来放置业务逻辑的代码。
nodejs中,默认是使用 CommonJs 规范去编写代码的, 但是如果想用 es6 语法,就必须添加一些插件去做转译工作。
我们可以试试不使用babel转译工具,直接使用es语法会怎么样。
试错环节开始新建 /src/test/index.js, 添加
const test = async () => { console.log('hello test'); } console.log('hello cli'); export default test 123456
/bin/index.js
#! /usr/bin/env node var cli = require('../src/index.js'); module.exports = cli; 1234
根目录下,执行 pnpm cli 发现以下报错:无法识别 export 语法
试错环节结束
所以使用es6语法,需要有以下步骤:
安装babel相关插件:pnpm add @babel/cli @babel/core @babel/plugin-proposal-object-rest-spread @babel/preset-env -D 1 根目录下,新建 babel.config.js,并添加以下内容:
module.exports = { presets: [ [ "@babel/preset-env", { targets: { node: "10", }, }, ], ], plugins: ["@babel/plugin-proposal-object-rest-spread"], }; 12345678910111213 根目录下,新建 jsconfig.json,并添加以下内容:
{ "compilerOptions": { "target": "ES6", "module": "commonjs", "experimentalDecorators" : true } } 1234567
这样就支持用 ESM 的方式去编写了。
但是!我们还需要将 ESM 编译成 CommonJs 的形式,让nodejs去执行。
"scripts": { .... "build": "babel src --out-dir dist", "build:watch": "babel --watch src --out-dir dist" }, 123456
执行打包命: npm run build:watch。命令会一直开启着,监听本地文件变化,实现自动打包输出。发现根目录下多了 /dist 这个目录,就是打包的产物。
修改 /bin/index.js 里文件的引用路径:
#! /usr/bin/env node var cli = require('../dist/index.js'); module.exports = cli; 12345
这时候再来试试效果吧。
✌✌成功了!接着探索吧!
脚手架中如何接收和处理命令参数。
nodejs中的 process 模块提供了当前进程相关的全局环境信息,如:命令参数、环境变量、命令运行路径等。我们把它打印出来看一下:修改 /src/index.js
const process = require('process'); console.log(process.argv); 123
cli 自定义命令后面可以设置自定义变量,标准命令参数有两种格式:
pnpm run cli --name=project --open pnpm run cli --name project --open 12
结果:
可以看到:可以获取到相关的全局环境信息。
通过 process.argv 来获取,要额外处理两种不同的命令参数格式,不方便。yargs 提供了一套node命令解决方案。
使用 yargs 进行命令参数的解析。pnpm add yargs -S 1
/src/index.js
import yargs from 'yargs' console.log(yargs.argv) 12
已经把命令参数解析好了,可以通过 yargs.argv.name 获取到name的值。
设置子命令脚手架要对外提供多个功能,不能将所有的功能都集中在 cli 命令中实现,不同的功能分发给不同的子命令。
可以通过 yargs 提供的 command 方法来设置一些子命令,让每个子命令对应各自功能,各司其职。
yargs.command 的用法是 yargs.command(cmd, desc, builder, handler), 具体用法在此
cmd:字符串,子命令名称,也可以传递数组,如 ['create', 'c'],表示子命令叫 create,其别名是 c;desc:字符串,子命令描述信息;builder:一个返回数组的函数,子命令参数信息配置;handler: 函数,可以在这个函数中专门处理该子命令参数;接下来 通过一个子命令 实现文件的拷贝。
实现文件的拷贝
前提
新建一个模版文件 /src/copy/template/index.js。根目录下安装 copy-dir (实现文件拷贝)、 fs-extra (操作文件的库)。/src/index.js 中处理命中子命令['copy']、获取命令参数 argv,并将参数交给 /src/copy/index.js 去处理。
import yargs from 'yargs' import { hideBin } from 'yargs/helpers' yargs(hideBin(process.argv)) .command( ['copy'], 'Copy a new template from local file', function (yargs) { return yargs.option('name', { alias: 'n', demand: true, describe: '模板名称', type: 'string' }) }, (argv) => { console.log(argv) // import('./copy/index.js').then(({ default: parseOptions }) => { // parseOptions(argv); // }); } ) .parse()
1234567891011121314151617181920212223执行 pnpm cli copy --name=project,可以看到,命令参数已经解析好了
如果我们输入不存在的命令参数呢,试试:
可以看到会有报错,并且有提示信息。
/src/copy/index.js 中,实现拷贝逻辑的处理。
import path from 'path' import copydir from 'copy-dir'; import fs from 'fs-extra'; const parseOptions = async (argv) => { const { name } = argv; const isMkdirExists = checkMkdirExists( path.resolve(process.cwd(), `./${name}`) ); if (isMkdirExists) { console.log(`${name}文件夹已经存在`) } else { // 拷贝文件夹 copyDir( path.resolve(__dirname, `./template`), path.resolve(process.cwd(), `./${name}`) ) } } // 拷贝文件夹 function copyDir (from, to, options) { copydir.sync(from, to, options); } // 路径判断 function checkMkdirExists (path) { return fs.existsSync(path) }; export default parseOptions
1234567891011121314151617181920212223242526272829303132根据获取到的name参数,判断当前操作目录下是否存在同名的文件夹,如果存在提示并退出,如果不存在则写入。
执行 pnpm cli copy --name=project: 可以看到已经生成了project文件夹,里面的内容是从/src/copy/template 里拷贝过来的。
process.cwd() 代表当前 Node.js 进程 执行时 的文件所属目录的绝对路径。比如,在simple-cli根目录下去执行命令,那 process.cwd() 得到的就是 /Users/wisdom/Documents/simple-cli。
__dirname 动态获取当前命令正在操作的文件所属目录的绝对路径。比如:执行 /copy/index.js 中的方法时,那 __dirname 得到的就是 /Users/wisdom/Documents/simple-cli/dist/copy。
path 模块提供的 path.resolve( [from…], to ) 方法将路径转成绝对路径:
从后向前拼接路径;若 to 以 / 开头,不会拼接到前面的路径;若 to 以 ../ 开头,拼接前面的路径,且不含最后一节路径;若 to 以 ./ 开头或者没有符号,则拼接前面路径。打印出来验证一下:
/copy/index.js
... const parseOptions = (argv) => { console.log('process.cwd()', process.cwd()) console.log('__dirname', __dirname) ... } ... 12345678
目前实现的都是直接对命令参数的解析,忽略了与用户的交互,所以接下来学习怎么建立询问时的交互。
如何创建询问式的交互
询问式是比较友好的交互形式,当功能越来越多,需要的命令参数也越来越多,我们不可以一股脑的都显式的写在命令参数上,第一是太冗长且不灵活,第二是对用户来说是个心智负担。
这种询问式的交互就超级友好!那我们就开始吧!
这里推荐 inquirer 开源库来实现:通过向用户提问问题,获取用户的输入,检验用户输入是否合法。
pnpm add inquirer@8.2.5 -S 1
通过inquirer.prompt() 来实现,接收一个数组,数组的每一项都是一个问题,有以下配置项:
type:提问的类型,有 input、confirm、list、checkbox。name:存储当前问题答案的变量。message: 问题的描述。default: 默认值。choices: 选项列表。validate:对用户答案进行校验。filter:对用户答案进行过滤,并返回处理后的值。比如我们创建一个模板文件,大概会询问用户:模板文件名称、模板类型、使用什么框架开发、使用框架对应的哪个组件库开发等等。下面我们来实现这个功能。
新建 /src/copy/inquirer.js
import inquirer from 'inquirer'; import path from 'path'; // 交互式询问列表 function inquirerPrompt (argv) { const { name } = argv; return new Promise((resolve, reject) => { inquirer.prompt([ { type: 'input', name: 'name', message: 'Project name', default: name, validate: function (val) { if (!/^[a-zA-Z]+$/.test(val)) { return "The template name can only contain English"; } if (!/^[A-Z]/.test(val)) { return "The first letter of the template name must be capitalized" } return true; }, }, { type: 'list', name: 'type', message: 'Choose Template type', choices: ['form', 'dynamicForm', 'nestedForm'], filter: function (value) { return { 'form': "form", 'dynamicForm': "dynamicForm", 'nestedForm': "nestedForm", }[value]; }, }, { type: 'list', message: 'Choose Frame type', choices: ['vue', 'react'], name: 'frame', } ]).then(answers => { const { frame } = answers; if (frame === 'react') { inquirer.prompt([ { type: 'list', message: 'Choose UI Library', choices: [ 'Ant Design', ], name: 'library', } ]).then(answers1 => { resolve({ ...answers, ...answers1, }) }).catch(error => { reject(error) }) } if (frame === 'vue') { inquirer.prompt([ { type: 'list', message: 'Choose UI Library', choices: ['Element'], name: 'library', } ]).then(answers2 => { resolve({ ...answers, ...answers2, }) }).catch(error => { reject(error) }) } }).catch(error => { reject(error) }) }) } export { inquirerPrompt }
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788inquirerPrompt 返回的是一个 Promise, 我们用then去获取用户输入的所有的答案,再根据具体的答案去做具体的处理。
接着改造一下 /src/copy/index.js, 加入询问式交互:
import { inquirerPrompt } from './inquirer' ... const parseOptions = (argv) => { inquirerPrompt(argv).then(answers => { const { name, type } = answers; ... }) } export default parseOptions 12345678910111213
既然我们已经向用户询问了模版名称,那就没必要在命令参数中要求输入模版名称了,冗余了,所以把yargs.command中的 builder函数 删除。
执行 pnpm cli copy
建立询问式交互,目的达成!
如何处理路径问题
如果项目名称已存在,应该再询问一次当前文件夹已存在,是否需要覆盖,而不是直接结束进程,没有给用户选择的余地。
/src/copy/inquirer.js 添加 isOverride 方法
const isOverride = async (name, targetDir) => { return new Promise((resolve, reject) => { inquirer.prompt([ { name: 'action', type: 'list', message: `${name} is existed, do you want to overwrite this directory`, choices: [ { name: 'overwrite', value: true }, { name: 'cancel', value: false }, ], }, ]).then(options => { const { action } = options resolve(action) }) }) } export { inquirerPrompt, isOverride }
1234567891011121314151617181920改造 /src/copy/index.js parseOptions方法
import { inquirerPrompt, isOverride } from './inquirer' import fs from 'fs-extra'; const parseOptions = (argv) => { inquirerPrompt(argv).then(answers => { const { name, type } = answers; const targetDir = path.resolve(process.cwd(), `./${name}`) const isMkdirExists = checkMkdirExists(targetDir); if (isMkdirExists) { isOverride(name, targetDir).then(async action => { console.log('action', action) if (!action) { return; } else { console.log('rnoverwriting...'); await fs.remove(targetDir); console.log('overwrite done'); copyDir( path.resolve(__dirname, `./template/${type}`), path.resolve(process.cwd(), `./${name}`) ) } }) } else { // 拷贝文件夹 copyDir( path.resolve(__dirname, `./template/${type}`), path.resolve(process.cwd(), `./${name}`) ) } }) }
123456789101112131415161718192021222324252627282930313233执行 pnpm cli copy
到此,就结束了!
结语
本文完整代码已上传至 github。这只是一个入门级别的脚手架,需要好好研究再精进一下。平时看到实操性强的文章可以试试跟着练一练,实战才能检验真理。
参考文章:https://juejin.cn/post/7260144602471776311#heading-15
相关知识
兰花的养殖方法(超详细入门篇)
兰花的养殖方法,超详细兰花种植入门篇
兰花的养殖方法(超详细入门篇),手把手教你养殖
兰花养护(入门篇)
快速上手web前端开发(超详细教程)
兰花的养殖方法(超详细入门篇)
插花基础知识入门篇
【花篮式悬挑脚手架】
关于植物施肥的一些问题,植物小白入门篇
Vue.js 增删查改库存管理系统教程:新手友好的完整代码与详细步骤」 「从零开始的库存管理系统:Vue.js 实现搜索、编辑、删除与数据更新」 「超详细的 Vue.js CRUD 教程:带你一步步构
网址: 超详细的前端脚手架入门篇 https://www.huajiangbk.com/newsview949327.html
上一篇: [保姆级] Vue3 开发文档 |
下一篇: Microi 吾码:前端开发的创 |
推荐分享

- 1君子兰什么品种最名贵 十大名 4012
- 2世界上最名贵的10种兰花图片 3364
- 3花圈挽联怎么写? 3286
- 4迷信说家里不能放假花 家里摆 1878
- 5香山红叶什么时候红 1493
- 6花的意思,花的解释,花的拼音 1210
- 7教师节送什么花最合适 1167
- 8勿忘我花图片 1103
- 9橄榄枝的象征意义 1093
- 10洛阳的市花 1039