0


超详细的前端脚手架入门篇

脚手架初认识

其实,我们通过平时使用的脚手架就不难推测得出脚手架的基本作用了。

比如: 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
  • 创建bin文件夹,添加index.js。
#! /usr/bin/env node

console.log('hello cli');
  • 在package.json中指定执行命令和执行的文件。
"scripts":{"cli":"node ./bin/index.js"},"bin":{"cli":"./bin/index.js"},
  • 在当前根目录下,执行 pnpm cli,得到以下结果:

image.png

如何开发和调试一个脚手架

/bin

放置的是入口文件,我们新建一个

/src

目录来放置业务逻辑的代码。

nodejs中,默认是使用

CommonJs

规范去编写代码的, 但是如果想用

es6

语法,就必须添加一些插件去做转译工作。

我们可以试试不使用babel转译工具,直接使用es语法会怎么样。

💥试错环节开始

新建 /src/test/index.js, 添加

consttest=async()=>{
  console.log('hello test');}
console.log('hello cli');exportdefault test

/bin/index.js

#! /usr/bin/env nodevar cli =require('../src/index.js');
module.exports = cli;

根目录下,执行

pnpm cli

发现以下报错:无法识别 export 语法

image.png

💥试错环节结束

所以使用es6语法,需要有以下步骤:

  • 安装babel相关插件:
pnpmadd @babel/cli @babel/core @babel/plugin-proposal-object-rest-spread @babel/preset-env -D
  • 根目录下,新建 babel.config.js,并添加以下内容:
module.exports ={presets:[["@babel/preset-env",{targets:{node:"10",},},],],plugins:["@babel/plugin-proposal-object-rest-spread"],};
  • 根目录下,新建 jsconfig.json,并添加以下内容:
{"compilerOptions":{"target":"ES6","module":"commonjs","experimentalDecorators":true}}

这样就支持用 ESM 的方式去编写了。
但是!我们还需要将

ESM 编译成 CommonJs

的形式,让nodejs去执行。

  • 编译:在 pacakge.json 中,添加打包命令,打包后的产物放在输出目录 /dist 下:
"scripts":{...."build":"babel src --out-dir dist","build:watch":"babel --watch src --out-dir dist"},
  • 执行打包命: npm run build:watch。命令会一直开启着,监听本地文件变化,实现自动打包输出。发现根目录下多了 /dist 这个目录,就是打包的产物。
  • 修改 /bin/index.js 里文件的引用路径:
#! /usr/bin/env nodevar cli =require('../dist/index.js');
module.exports = cli;

这时候再来试试效果吧。

image.png

✌✌成功了!接着探索吧!

脚手架中如何接收和处理命令参数。

  • nodejs中的 process 模块提供了当前进程相关的全局环境信息,如:命令参数、环境变量、命令运行路径等。我们把它打印出来看一下:

修改 /src/index.js

const process =require('process');

console.log(process.argv);

cli 自定义命令后面可以设置自定义变量,标准命令参数有两种格式:

pnpm run cli --name=project --openpnpm run cli --name project --open

结果:

image.png

可以看到:可以获取到相关的全局环境信息。

通过

process.argv

来获取,要额外处理两种不同的命令参数格式,不方便。yargs 提供了一套node命令解决方案。

  • 使用 yargs 进行命令参数的解析。
pnpmadd yargs -S

/src/index.js

import yargs from'yargs'
console.log(yargs.argv)

image.png

已经把命令参数解析好了,可以通过

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()

执行

pnpm cli copy --name=project

,可以看到,命令参数已经解析好了

image.png

如果我们输入不存在的命令参数呢,试试:

image.png

可以看到会有报错,并且有提示信息。

/src/copy/index.js 中,实现拷贝逻辑的处理。

import path from'path'import copydir from'copy-dir';import fs from'fs-extra';constparseOptions=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}`))}}// 拷贝文件夹functioncopyDir(from, to, options){
 copydir.sync(from, to, options);}// 路径判断functioncheckMkdirExists(path){return fs.existsSync(path)};exportdefault parseOptions

根据获取到的name参数,判断当前操作目录下是否存在同名的文件夹,如果存在提示并退出,如果不存在则写入。

执行

pnpm cli copy --name=project

: 可以看到已经生成了project文件夹,里面的内容是从/src/copy/template 里拷贝过来的。

image.png

💥 process.cwd() 和 __dirname 的区别:

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

...constparseOptions=(argv)=>{
  console.log('process.cwd()', process.cwd())
  console.log('__dirname', __dirname)...}...

image.png

目前实现的都是直接对命令参数的解析,忽略了与用户的交互,所以接下来学习怎么建立询问时的交互。

如何创建询问式的交互

询问式是比较友好的交互形式,当功能越来越多,需要的命令参数也越来越多,我们不可以一股脑的都显式的写在命令参数上,第一是太冗长且不灵活,第二是对用户来说是个心智负担。

image.png

这种询问式的交互就超级友好!那我们就开始吧!

这里推荐 inquirer 开源库来实现:通过向用户提问问题,获取用户的输入,检验用户输入是否合法。

pnpmadd [email protected] -S

通过

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';// 交互式询问列表functioninquirerPrompt(argv){const{ name }= argv;returnnewPromise((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"}returntrue;},},{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 }
inquirerPrompt

返回的是一个

Promise

, 我们用then去获取用户输入的所有的答案,再根据具体的答案去做具体的处理。

接着改造一下 /src/copy/index.js, 加入询问式交互:

import{ inquirerPrompt }from'./inquirer'...constparseOptions=(argv)=>{inquirerPrompt(argv).then(answers=>{const{ name, type }= answers;...})}exportdefault parseOptions

既然我们已经向用户询问了模版名称,那就没必要在命令参数中要求输入模版名称了,冗余了,所以把yargs.command中的 builder函数 删除。

执行

pnpm cli copy

image.png

建立询问式交互,目的达成!

如何处理路径问题

如果项目名称已存在,应该再询问一次当前文件夹已存在,是否需要覆盖,而不是直接结束进程,没有给用户选择的余地。

/src/copy/inquirer.js 添加 isOverride 方法

constisOverride=async(name, targetDir)=>{returnnewPromise((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 }

改造 /src/copy/index.js parseOptions方法

import{ inquirerPrompt, isOverride }from'./inquirer'import fs from'fs-extra';constparseOptions=(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(asyncaction=>{
       console.log('action', action)if(!action){return;}else{
         console.log('\r\noverwriting...');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}`))}})}

执行

pnpm cli copy

image.png

到此,就结束了!

结语

本文完整代码已上传至 github。这只是一个入门级别的脚手架,需要好好研究再精进一下。平时看到实操性强的文章可以试试跟着练一练,实战才能检验真理。

参考文章:https://juejin.cn/post/7260144602471776311#heading-15

标签: 前端 架构

本文转载自: https://blog.csdn.net/yadang2/article/details/132548377
版权归原作者 CCC_go 所有, 如有侵权,请联系我们删除。

“超详细的前端脚手架入门篇”的评论:

还没有评论