什么是脚手架?
简而言之它就是一个工具,方便我们新建项目用的,通过这个工具创建的项目之后我们可以直接开发了。
市面常见的脚手架?
- vue-cli 提供vue开发的webpack,pwa等模板
- create-react-app React团队官方出的一个构建React单页面应用的脚手架工具
- Yeoman 通用型脚手架,过于通用,不够专注,使用麻烦
为什么要自己搭建?
- 专心快速的完成业务
- 代码更加规范化
- 少造轮子,少拷贝代码,简化流程
如何搭建一款简易脚手架?
一、目录搭建
- 创建一个文件夹,取名为lang-cli
- 在该目录下执行 npm init -y,就会生成一个package.json,在packjson中写入以下依赖并执行npm install安装。
"dependencies": {
"axios": "^0.24.0",
"chalk": "^4.1.2",
"commander": "^8.3.0",
"download-git-repo": "^3.0.2",
"fs-extra": "^10.0.0",
"inquirer": "^8.2.0",
"ora": "^5.4.1"
}
- 新建一个bin文件夹,并在bin目录下新建一个无后缀的文件,取名为lang(这个文件将作为我们整个脚手架的入口文件, 用node ./bin/lang也可以运行),并写入以下内容,这个语句的意思就是为了让系统看到这一行的时候,沿着该路径去查找node并执行。
#! /usr/bin/env node
console.log('hello lang')
- 由于一直在本地用node ./bin/lang运行起来很麻烦,所以可以挂载到全局。在package.json中加入如下一行,在根目录下执行npm link,之后每次输入lang,就可以直接运行了。
{
"name": "lang",
"bin": "./bin/lang", // 默认取的name的名字
}
{
"name": "lang",
"bin": {
'lang-cli': './bin/lang'
}
}
二、编写具体指令(配置可执行命令)
commander用来编写指令和处理命令行的一个工具。chalk 是用来修改控制台输出内容样式的,比如颜色啊,具体用法如下:
const program = require("commander")
const chalk = require("chalk")
// 定义当前版本
// 定义使用方法
program
.version(`lang-cli@${require("../package.json").version}`)
.usage(' [option]')
// 定义指令 create
program
.command('create ')
.description('create a new project')
.option('-f,--force', 'overwrite target directory if it exsit')
.action((name, cmd) => {
console.log(name, cmd)
//每个功能放单独模块中写,调用create模块去创建
require('../lib/create.js')(name, cmd)
})
//vue config set/get 等指令 这里就不详述了
// 监听 "--help命令输入"
program
.on('--help', function () {
console.log()
console.log(`Run ${chalk.cyan('lang --help')} show details`)
console.log()
})
// 解析命令行参数
program.parse(process.argv)
三、编写创建逻辑
- 创建一个lib文件夹,在此文件中创建一个create.js,具体代码如下:
const path = require('path')
const fs = require('fs-extra')
const inquirer = require('inquirer')
const Creator = require('./Creator')
module.exports = async function (projectName, option) {
// 创建项目
const cwd = process.cwd(); // 获取当前命令执行时的工作目录
const targetDir = path.join(cwd, projectName) // 目标目录
if (fs.existsSync(targetDir)) {
if (option.force) {//如果强制创建,删除已有的
await fs.remove(targetDir)
} 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;
if (action === 'overwrite') {
console.log('removing.....')
await fs.remove(targetDir)
}
}
}
// 创建项目(单独提出来一个类去做)
const creator = new Creator(projectName, targetDir)
creator.create()
}
Creator.js类中的代码如下:
const { fetchRepoList, fetchTagList } = require('./request')
const { wrapLoading } = require('./util')
const inquirer = require('inquirer')
const downloadGitRepo = require('download-git-repo') //不支持promise
const util = require('util') //node自带的
const path = require('path') //node自带的
class Creator {
constructor(projectName, targetDir) {
this.name = projectName;
this.target = targetDir;
this.downloadGitRepo = util.promisify(downloadGitRepo) //转换成promise方法
}
async fetchRepo() {
// 失败要重新拉取(网慢的话)
let repos = await wrapLoading(fetchRepoList, 'waiting fetch template')
if (!repos) return;
repos = repos.map(i => i.name)
let { repo } = await inquirer.prompt({
name: 'repo',
type: 'list',
message: 'please choose a template to create project',
choices: repos,
})
return repo;
}
async fetchTag(repo) {
let tags = await wrapLoading(fetchTagList, 'waiting fetch tag', repo)
if (!tags) return;
tags = tags.map(i => i.name)
let { tag } = await inquirer.prompt({
name: 'tag',
type: 'list',
message: 'please choose a tag to create project',
choices: tags,
})
return tag;
}
async download(repo, tag) {
// 1.拼接出下载路径 https://github.com/wave1994
let requestUrl = `wave1994/${repo}${tag ? '#' + tag : ''}`
// 2.把资源下载到某个路径上
// await this.downloadGitRepo(requestUrl, this.target)
await this.downloadGitRepo(
requestUrl,
path.resolve(process.cwd(), `${repo}@${tag}`)
)
}
async create() {
//开始创建
// 1. 采用远程拉取的方式
// 1). 先去拉取当前组织下的模板
let repo = await this.fetchRepo()
// 2). 再通过模板找到版本号
let tag = await this.fetchTag(repo)
// 3). 下载
let downloadUrl = await this.download(repo, tag)
}
}
module.exports = Creator
request.js中代码:
// 通过axios获取结果
const axios = require('axios')
axios.interceptors.response.use(res => res.data)
async function fetchRepoList() {
return axios.get('https://api.github.com/users/wave1994/repos')
}
async function fetchTagList(repo) {
return axios.get(`https://api.github.com/repos/wave1994/${repo}/tags`)
}
module.exports = {
fetchRepoList,
fetchTagList
}
util.js文件中代码:
const ora = require('ora')
async function sleep(n) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), n);
})
}
// 等待loading函数
async function wrapLoading(fn, msg, ...args) {
const spinner = ora(msg)
spinner.start(); // 开始加载
try {
let repos = await fn(...args);
spinner.succeed() //成功
return repos;
} catch (error) {
spinner.fail('fetch failed, refetch....')
await sleep(1000)
return wrapLoading(fn, msg, ...args)
}
}
module.exports = {
wrapLoading
}
四、执行命令
lang create app-name
总结
脚手架是前端工程化领域的基本项,个人认为掌握前端脚手架的开发是十分重要的,本文旨在提供一个大概思路及样板,目前只包含了命令行、模板拉取,相对于成熟的脚手架如vue-cli、create-react-app等来说,还有很多很多工作要做,包括本地服务、打包构建、集成部署、周边其他等都还需要完善,想要在工程化领域有所建树的同学,不妨在这几个方面多下下功夫。
参考文档:https://vleedesigntheory.github.io/tech/front/cli20200701.html#%E5%89%8D%E8%A8%80