jjzjj

从0搭建前端脚手架详解(小白也可以搭建)

喵十一点半 2024-06-12 原文

本篇文章用来为大家提供一个搭建简易前端脚手架的思路。

先来看一眼实现的效果。

从图上来看这个脚手架的功能非常的简单只有一个创建的命令,其他都是帮助和显示版本号的。

也就是上图这句,创建一个新项目,只需要输入create 项目名便可使用,在创建时执行了一系列的操作,这一块的思路很简单,就是将git仓库中的项目模板拷贝下来再依据使用者的不同操作对复制下来的模板的部分文件进行修改就可以了,大致思路便介绍到这里,接下来我们便来详细的讲讲如何实现,以及会用到的依赖。

脚手架目录结构

了解搭建的脚手架

脚手架就是在启动的时候询问一些简单的问题,并且通过用户回答的结果去渲染对应的模板文件,我们接下来的流程亦是如此

脚手架的初始化

由于它是一个npm的包,因此我们需要使用npm的初始化命令,随意新建一个文件夹打开命令行,输入npm init,会出现以下情况。

名称意思默认值
package name包的名称创建文件夹时的名称
version版本号1.0.0
description包的描述创建文件时的名称
entry point入口文件index.js
test command测试命令
git repositorygit仓库地址
keywords关键词,上传到npm官网时在页面中展示的关键词
author作者信息,对象的形式,里面存储一些邮箱、作者名、url
license执照MIT

这就是输入初始化命令时会询问的东西,回答完这些后就会生成一个 package.json 的文件,这个文件就是记录包的信息。

如果想要了解更多,可查看如下地址:
package.json详解

脚手架依赖安装

用到如下依赖请安装。

npm i path
npm i chalk@4.1.0
npm i fs-extra
npm i inquirer@8.2.4
npm i commander
npm i axios
npm i download-git-repo

询问用户问题

创建入口文件

在询问问题前我们需要先创建一个入口文件,创建完成后在package.json中添加bin项,并且将入口文件路径写进去

填写完入口文件路径后在入口文件内随便输出一句, 但必须在入口文件顶层声明文件执行方式为node。
声明代码:#! /usr/bin/env node

写完后我们需要测试一下我们是否可以正常的访问的我们的脚手架,在本文件夹打开命令行,输入 npm link ,该命令会创建一个全局访问的包的快捷方式,这个是临时的就是本地测试的时候用的,这个在命令行输入你的脚手架的名称可以看到入口文件输出的内容。

最基本的交互命令

在完成上一步后我们就要开始与用户进行交互了,这个时候我们就需要用到一个用于自定义命令行指令的依赖 commander

引入依赖:

const program = require('commander')

简单介绍一下commander依赖常用的方法

command

命令。.command()的第一个参数为命令名称。命令参数可以跟在名称后面,也可以用.argument()单独指定。

参数可为必选的(尖括号表示)、可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。

例如:

// 创建一个create命令
.command('create <app-name>')

parse

解析。.parse()的第一个参数是要解析的字符串数组,也可以省略参数而使用process.argv,这里我们也是用process.argv用来解析node的参数。

例如:

// 解析用户执行命令传入参数
program.parse(process.argv);

option

选项。option()可以附加选项的简介。第一个参数可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(–后面接一个或多个单词),使用逗号、空格或|分隔。第二个参数为该选项的简介。

例如:

.option('-f, --force', '如果存在的话强行覆盖')

action

处理函数。用command创建的自定义命令的处理函数,action携带的实参顺序就是命令上的参数的顺序。

例如:

program
.command('create <app-name>')
// 这个name 就代表第一个必填参数 options就代表其余, 如果有第二个就在写一个,最后一个永远是剩余参数
.action((name, options) => {
    console.log(name)
    // 打印执行结果
    // require("../lib/create")(name, options)
})

编写交互命令 create

入口文件

#! /usr/bin/env node
const program = require('commander');
const chalk = require('chalk');

// 定义命令和参数
// create命令
program
.command('create <app-name>')
.description('create a new project')
// -f or --force 为强制创建,如果创建的目录存在则直接覆盖
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
    // 打印执行结果
    console.log('项目名称', name)
})

// 解析用户执行命令传入参数
program.parse(process.argv);

这里我们创建了一个叫 create 的自定义指令,这个命令有着必填的项目名、可以选择的强制覆盖的选项 -f,有着处理函数action

我们在action中接收并打印了用户输入的项目名称。

接下来我们再次运行一下自己的脚手架并带上create命令,我的叫test

test-cli create app

出现如下就说明第一个命令创建成功了

这里请注意 解析用户命令参数的操作一定要在最后一行否则什么都不会出现。

program.parse(process.argv)

到这里为止我们成功为我们脚手架创建了第一个交互命令,想查看更多关于 commander 的请点击这里commander

创建第一个模板项目

在创建了一个基本命令 create 后我们就要开始创建一个模板并在用户使用该命令时复制并修改我们所创建的模板。

创建一个模板

我们在复制模板前需要一个模板,现在的我们随便创建一个文件夹并取名为template里面创建一个html。

像这样创建好后,我们就有了一个模板,但我们依然需要让模板有一个可被下载、查询的地方,这里我选择的是使用 git 组织仓库,因为这样可以直接通过git提供的接口进行文件下载,包括选择不同的模板等。

上传模板

我们先去 git 的官网中新建一个存放模板的组织仓库。


点击图中的位置进入组织,并点击下图的创建

会进入到付费的位置,没有大需求就选免费

填写信息完基本就算创建成功了

接下来在组织中创建一个储存库

这里我们暂且选择可见的仓库,千万不要选择私人仓库,否则git接口会找不该仓库

创建好后的仓库,就直接将模板代码提交至也本次创建的仓库中就可以了,我们在vscode中进行演示。
先点击推送

如果没有推送的仓库则会提示是否添加推送仓库,我们点击推送远程仓库,并从中找到自己的仓库

择完成后输入仓库名称,然后会报错,报错原因就是因为暂无推送的内容,这个使用,正常的在 vscode 中提交代码就行了,然后查看自己的仓库,会出现上传的内容

增加一个新的版本标签

跟着下列图操作




点击发布发行版后就可以了。

下载模板

我们上传模板后可以通过 git 提供的接口来完成下载模板的功能,首先我们先去询问用户要下载的模板名称然后在用依赖包来进行下载:
https://api.github.com/orgs/geeksTest/repos 获取该组织下的所有模板

create命令后续操作

上传模板后,我们就可以继续完成create命令的后续操作了。

create命令下使用创建函数

program
.command('create <app-name>')
.description(chalk.cyan('create a new project'))
// -f or --force 为强制创建,如果创建的目录存在则直接覆盖
.option('-f, --force', 'overwrite target directory if it exist')
.action((name, options) => {
    // 打印执行结果
    require("../lib/create")(name, options)
})

创建create文件

创建 create 文件用来回应用户的 create 命令。

这里用到的依赖

// lib/create.js

const path = require('path')
// fs-extra 是对 fs 模块的扩展,支持 promise 语法
const fs = require('fs-extra')
// 用于交互式询问用户问题
const inquirer = require('inquirer')
// 导出Generator类
const Generator = require('./Generator')

//1. 抛出一个方法用来接收用户要创建的文件夹(项目)名 和 其他参数
module.exports = async function (name, options) {
  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)
  
  //2 判断是否存在相同的文件夹(项目)名
  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {
    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      // 询问用户是否确定要覆盖
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target directory already exists Pick an action:',
          choices: [
            {
              name: 'Overwrite',
              value: 'overwrite'
            },{
              name: 'Cancel',
              value: false
            }
          ]
        }
      ])
      // 如果用户拒绝覆盖则停止剩余操作
      if (!action) {
        return;
      } else if (action === 'overwrite') {
        // 移除已存在的目录
        console.log(`\r\nRemoving...`)
        await fs.remove(targetAir)
      }
    }
  }

  //3 新建generator类
  const generator = new Generator(name, targetAir);
  generator.create();
}

创建generator类

// lib/Generator.js

const { getRepoList, getTagList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')
const util = require('util')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise
const chalk = require('chalk')
const path = require('path');
const fs = require("fs-extra");

// 添加加载动画
async function wrapLoading(fn, message, ...args) {
  // 使用 ora 初始化,传入提示信息 message
  const spinner = ora(message);
  // 开始加载动画
  spinner.start();

  try {
    // 执行传入方法 fn
    const result = await fn(...args);
    // 状态为修改为成功
    spinner.succeed();
    return result; 
  } catch (error) {
    // 状态为修改为失败
    spinner.fail('Request failed, refetch ...');
  } 
}

class Generator {
  constructor (name, targetDir){
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetDir = targetDir;
    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }

  // 获取用户选择的模板
  // 1)从远程拉取模板数据
  // 2)用户选择自己新下载的模板名称
  // 3)return 用户选择的名称

  async getRepo() {
    // 1)从远程拉取模板数据
    const repoList = await wrapLoading(getRepoList, 'waiting fetch template');
    if (!repoList) return;
    // 过滤我们需要的模板名称
    const repos = repoList.map(item => item.name);

    // 2)用户选择自己新下载的模板名称
    const { repo } = await inquirer.prompt({
      name: 'repo',
      type: 'list',
      choices: repos,
      message: 'Please choose a template to create project'
    })

    // 3)return 用户选择的名称
    return repo;
  }

  // 获取用户选择的版本
  // 1)基于 repo 结果,远程拉取对应的 tag 列表
  // 2)自动选择最新版的 tag

  async getTag(repo) {
    // 1)基于 repo 结果,远程拉取对应的 tag 列表
    const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
    if (!tags) return;
    
    // 过滤我们需要的 tag 名称
    const tagsList = tags.map(item => item.name);

    // 2)return 用户选择的 tag
    return tagsList[0]
  }

  // 下载远程模板
  // 1)拼接下载地址
  // 2)调用下载方法
  async download(repo, tag){
    // 1)拼接下载地址
    const requestUrl = `geeksTest/${repo}${tag ? '#'+tag : ''}`;

    // 2)调用下载方法
    await wrapLoading(
      this.downloadGitRepo, // 远程下载方法
      'waiting download template', // 加载提示信息
      requestUrl, // 参数1: 下载地址
      path.resolve(process.cwd(), this.targetDir) // 参数2: 创建位置
    ) 
  }

  // 核心创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  // 4) 对uniapp模板中部分文件进行读写
  // 5) 模板使用提示
  async create(){

    // 1)获取模板名称
    const repo = await this.getRepo()

    // 2) 获取 tag 名称
    const tag = await this.getTag(repo)

    // 3)下载模板到模板目录
    await this.download(repo, tag)
    
    // 5)模板使用提示
    console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
    console.log(`\r\n  cd ${chalk.cyan(this.name)}`)
    console.log(`\r\n  启动前请务必阅读 ${chalk.cyan("README.md")} 文件`)
  
  }
}

module.exports = Generator;

创建http文件

新建一个http.js的文件用来存放要请求的接口,我们用axios去请求.

依赖安装 npm i commander

// lib/http.js

// 通过 axios 处理请求
const axios = require('axios')

axios.interceptors.response.use(res => {
  return res.data;
})

/**
 * 获取模板列表
 * @returns Promise
 */
async function getRepoList() {
  return axios.get('https://api.github.com/orgs/geeksTest/repos')
}

/**
 * 获取版本信息
 * @param {string} repo 模板名称
 * @returns Promise
 */
async function  getTagList(repo) {
  return axios.get(`https://api.github.com/repos/geeksTest/${repo}/tags`)
}

module.exports = {
  getRepoList,
  getTagList
}

最后导出了两个方法, 模板列表、模板tag列表。
这个时候的api接口是可以直接在浏览器中访问到的,如果不想被人随意访问读取数据则可以在git中增加双因素验证,然后每次访问api时都会要求带上git的访问token否则会访问不到,查看双因素详情

搭建完成

完成这一步后我们再去进行test-cli create app命令,会看到下图。

会询问要创建的模板项目,我这里的远程组织模板叫做test,大家选择自己的模板回车,稍等一下就会创建成功,并看到在你使用命令的路径上多出一个项目名的文件夹,就成功了。

如果有对模板在下载后进行操作的需求可以使用fs依赖进行操作,到这里为止我们已经完成了一个简易的脚手架搭建,感谢大家耐心观看。

结语: 天才无非是长久的忍耐,努力吧!

有关从0搭建前端脚手架详解(小白也可以搭建)的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  3. ruby - 我可以使用 Ruby 从 CSV 中删除列吗? - 2

    查看Ruby的CSV库的文档,我非常确定这是可能且简单的。我只需要使用Ruby删除CSV文件的前三列,但我没有成功运行它。 最佳答案 csv_table=CSV.read(file_path_in,:headers=>true)csv_table.delete("header_name")csv_table.to_csv#=>ThenewCSVinstringformat检查CSV::Table文档:http://ruby-doc.org/stdlib-1.9.2/libdoc/csv/rdoc/CSV/Table.html

  4. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  5. ruby - 有人可以帮助解释类创建的 post_initialize 回调吗 (Sandi Metz) - 2

    我正在阅读SandiMetz的POODR,并且遇到了一个我不太了解的编码原则。这是代码:classBicycleattr_reader:size,:chain,:tire_sizedefinitialize(args={})@size=args[:size]||1@chain=args[:chain]||2@tire_size=args[:tire_size]||3post_initialize(args)endendclassMountainBike此代码将为其各自的属性输出1,2,3,4,5。我不明白的是查找方法。当一辆山地自行车被实例化时,因为它没有自己的initialize方法

  6. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

  7. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

    我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

  8. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  9. ruby - 可以通过多少种方法将方法添加到 ruby​​ 对象? - 2

    当谈到运行时自省(introspection)和动态代码生成时,我认为ruby​​没有任何竞争对手,可能除了一些lisp方言。前几天,我正在做一些代码练习来探索ruby​​的动态功能,我开始想知道如何向现有对象添加方法。以下是我能想到的3种方法:obj=Object.new#addamethoddirectlydefobj.new_method...end#addamethodindirectlywiththesingletonclassclass这只是冰山一角,因为我还没有探索instance_eval、module_eval和define_method的各种组合。是否有在线/离线资

  10. ruby-on-rails - 如何在 Rails 3 中创建自定义脚手架生成器? - 2

    有这些railscast。http://railscasts.com/episodes/218-making-generators-in-rails-3有了这个,你就会知道如何创建样式表和脚手架生成器。http://railscasts.com/episodes/216-generators-in-rails-3通过这个,您可以了解如何添加一些文件来修改脚手架View。我想把两者结合起来。我想创建一个生成器,它也可以创建脚手架View。有点像RyanBates漂亮的生成器或web_app_themegem(https://github.com/pilu/web-app-theme)。我

随机推荐