从零开始搭建一个自己的前端脚手架(一):基础篇

文章目录

    • 前言
      • 1.1 脚手架本质
      • 1.2 脚手架工作流程
      • 1.3 依赖工具库
      • 1.4 脚手架目录结构
    • 一、功能拆分
      • 1.1 创建bin目录全局调试
      • 1.2 引入commander简化命令解析
      • 1.3 引入download-git-repo实现模板下载的过程
      • 1.4 引入inquirer实现交互问询
      • 1.5 引入handlebars替换package.json中项目元信息
    • 二、简单优化&发布
      • 2.1 代码优化
        • 1、 新增generator.js
        • 2、 优化create.js
        • 3、终端测试
      • 2.2 npm发布
    • 三、写在最后

前言

本篇文章旨在说明一个简单脚手架工具的开发流程和简要的依赖工具,对于功能更人性化,代码健壮性更强的脚手架工具,详细见后续

1.1 脚手架本质

现在的开发过程中,第一步都是搭建项目目录,像vue-cli, create-react-app, express-generator等脚手架都提供了这样一个功能来帮助开发者创建基础项目目录。而一个稳定的开发团队,技术选型及技术栈相当稳定,每次开发都从新搭建项目目录太过麻烦,则可以通过自己开发一套脚手架的方式来引入自己团队提炼出的项目模板。

  • 本质 :以Vue-cli2举例,它提供了很多命令选项并预设了一些功能,但其本质还是通过解析用户在命令终端中输入的命令后,下载远程模板库代码,并通过替换关键信息达到个性化修改
    从零开始搭建一个自己的前端脚手架(一):基础篇_第1张图片

1.2 脚手架工作流程

通过上述对cli的工作流程分析,接下来我们实现脚手架的步骤大致如下:

  • 1、解析用户命令
    • commander:命令行工具,解析用户输入命令,commander文档
    • 匹配不成功:显示help命令信息
    • 匹配成功,进入下一步
  • 2、 拉取远端模板
    • download-git-repo:通过git下载相应模板到本地
  • 3、 根据用户输入替换内容
    • handlebars 模板
  • 4、 文件操作渲染
    • 增删改 fs-extra
  • 5、 结束初始化

1.3 依赖工具库

  1. commander
    • 用于帮助快速开发Node.js命令行工具的库, 使用文档
  2. chalk
    • chalk是一个颜色的插件。可以通过chalk.green(‘success’)来改变颜色,使用文档
  3. download-git-repo
    • 用来通过git下载项目模板的插件,使用文档
  4. ora
    • 用于实现node命令环境的loading效果,并显示各种状态的图标, 使用文档
  5. inquier
    • 用于命令行交互问询等, 使用文档
  6. metalsmith
    • 一个静态网站生成器,可以用在批量处理模板的场景。使用文档
  7. handlebars
    • 模板引擎工具,可用于修改package.json中相关字段,使用文档
  8. log-symbols
    • 用于在打印输出的内容中加入icon更友好,使用文档
  9. rimraf
    • 在node环境下使用unix 命令rm -rf来删除文件

1.4 脚手架目录结构

=================
  |__ bin
    |__ my-cli  # 命令脚本
  |__ src
    |__ create.js  # 初始化命令
    |__ list.js  # 列出模板
  |__ node_modules
  |__ package.json  
  |__ templates.json  # 模板列表

一、功能拆分

通过如上目录结构先创建项目目录 npm init --yes

1.1 创建bin目录全局调试

1. 在bin文件夹下创建my-cli.js 如下,

  • /bin/my-cli.js
#! usr/bin/env node
console.log(process.argv)
  • #!/usr/bin/env node : 这一行是必须加的,这是告诉系统当前脚本由node.js来解析执行。在这里,我们使用node来作为脚本的解释程序。而我们#! /usr/bin/env node这样写,目的是使用env来找到node,并使用node来作为程序的解释程序。
  • env:在env中规定很多系统的环境变量,包括我们安装的一些环境的路径等。在不同的操作系统中,我们安装node的路径可能会有所不同,但是其环境变量会存在于env里面,所以,这里我们使用env来找到node,并用node作为解释程序。所以,env的主要目的就是让我们的脚本在不同的操作系统上都能够正常的被解释,启动。

2. 在package.json新增bin字段

"bin":{
	"my-cli": "bin/my-cli.js"
}

3. 执行npm link用于调试

  • 命令行执行 npm link,创建软链接至全局,这样我们就可以全局使用my-cli命令了,在开发 npm 包的前期都会使用link方式在其他项目中测试来开发,后期再发布到npm上
  • 注意这里在 npm link后可能不成功,建议加上 npm link --force再试一次
  • 如下通过执行命令实现本地文件与命令行关联,如下参数通过第[2]位可以拿到用户输入项,后续依次
# 命令行执行
my-cli 123
# 输出如下:
[
 ‘xxxx\node.exe',
 'xxxx\bin\my-cli.js'
 ‘123’
]

上面步骤可能遇到的问题:

  • 1、npm link 执行不成功,找不到文件:
    • my-cli对应的文件后缀需要严格一致(带上.js)
    • 系统中已经建立了“my-cli"软链,通过npm link --force强制覆盖
  • 2、执行’"my-cli"命令,提示系统找不到文件
    • 项目文件名与“my-cli”指令名冲突,建议package.json中项目名称与定义的指令名称不一样

1.2 引入commander简化命令解析

通过上述1.1中步骤已经可以基本实现对简单命令的解析了,为了简化很杂cli参数操作,这里引入commander工具(提前npm install安装),关于commander的具体使用,请参考commander文档

  • bin/my-cli.js
#! /usr/bin/env node

const program = require('commander')
const version = require('../package.json').version;

// 定义当前版本
program.version(version, "-v, --version")

// 定义使用方法 
program
    .command("create ")
    .description("使用my-cli创建一个新的模板项目")
    .action(()=>{
        require('../src/init')
    })

program
    .command("list")
    .description("可用模板列表")
    .action(()=>{
        require('../src/list')
    })

// 必须,用于解析用户命令输入内容
program.parse(process.argv)
if(!program.args.length){
    program.help()
}
  • 再次通过在命令终端输入my-cli,显示如下内容
    从零开始搭建一个自己的前端脚手架(一):基础篇_第2张图片

1.3 引入download-git-repo实现模板下载的过程

git clone 128错误,可能是本地已经存在同名文件,删除后可重试。在template.json中预设了一个项目模板地址

  • template.json
{"admin":"https://gitee.com/cy-edu/cli-template-admin.git"}
  • src/create.js(对应my-cli create命令执行文件)
const templateUrl = require('../template.json').admin
const download = require('download-git-repo')
// tmp-临时存放下载文件的地址
download(`direct:${templateUrl}`, "tmp", {clone: true},(err) => {
  console.log("下载完毕回调");
});

通过上述操作后,执行my-cli create my-app可以执行git cloneadmin项目模板到本地tmp文件夹中

1.4 引入inquirer实现交互问询

上述步骤大致实现了从解析命令到下载模板的过程,为了实现根据用户输入项来实现模板内容自定义,这里需要引入 inquirer.js来实现交互问询功能。使用文档

  • src/create.js
const templateUrl = require('../template.json').admin
const download = require('download-git-repo')
const inquirer = require("inquirer");


download(`direct:${templateUrl}`, "tmp", {clone: true},(err) => {
  console.log("下载完毕回调");
    inquirer
      .prompt([
        { name: "decription", message: "请输入项目描述" },
        { name: "author", message: "请输入作者" },
      ])
      .then((params) => {
        console.log(params);
      });
});

如上代码,再次执行my-cli create my-app会显示出相应问询内容,并通过回调中的params拿到用户输入内容
在这里插入图片描述

1.5 引入handlebars替换package.json中项目元信息

通过上述步骤我们已经可以拿到用户输入的项目描述信息及作者信息了,下一步即可通过handlebars模板替换工具来将package.json中对应内容替换为用户输入的内容。关于handlebars的使用文档 请参考这里

// handlebars 基础使用
var source = "

Hello, my name is {{name}}. I am from {{hometown}}. I have " + "{{kids.length}} kids:

"
+ "
    {{#kids}}
  • {{name}} is {{age}}
  • {{/kids}}
"
; var template = Handlebars.compile(source); var data = { "name": "Alan", "hometown": "Somewhere, TX", "kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]}; var result = template(data);

上述是handlebars的基本使用方法,其通过Mustache模板语法识别到{{ }}中的内容后,动态的替换。因此,我们首先要改造一下模板中的package.json文件,使其符合mustache规范

  • template.json模板项目admin中package.json。【PS: 这是模板项目中的package.json并不是当前脚手架项目中的哦~】
{
  "name": "{{ name }}",
  "version": "1.0.0",
  "description": "{{ description }}",
  "author": "{{ author }}",
}
  • 改造后的create.js
const templateUrl = require('../template.json').admin
const download = require('download-git-repo')
const inquirer = require("inquirer");
// 新增项
const fs = require('fs')
const path = require('path')
const Handlebars = require('handlebars')

const packagePath = path.join(__dirname,'../','/tmp/package.json')
// 注意这里JSON.stringify(data, '','\t') 是为了让最终生成的JSON保留制表符等格式
const content = JSON.stringify(require(packagePath),'','\t')
const template = Handlebars.compile(content)


download(`direct:${templateUrl}`, "tmp", {clone: true},(err) => {
  console.log("下载完毕回调");
    inquirer
      .prompt([
        { name: "name", message: "请输入项目名称" },
        { name: "description", message: "请输入项目描述" },
        { name: "author", message: "请输入作者" },
      ])
      .then((params) => {
        const result = template(params);
        fs.writeFileSync(packagePath, result);
        console.log(params);
      });
});
  • 终端自测
    从零开始搭建一个自己的前端脚手架(一):基础篇_第3张图片
  • 最终tmp/package.json对应字段替换如下:
    从零开始搭建一个自己的前端脚手架(一):基础篇_第4张图片

二、简单优化&发布

经过上述操作我们已经可以基本的实现一个模板从命令解析--》模板拉取--》交互问询--》模板信息替换--》生成最终项目的过程,但也可以发现,上述只是简单的把模板下载到一个固定名为"tmp"的文件夹下,以上有代码也有许多不严谨的地方,因此,接下来做一些简单的优化:

  1. 美化:加颜色【chalk】,加loading【ora】,加icon【log-symbols】
  2. 代码健壮性:提示用户输入模板名称,项目名称,用于替换
  3. 自动命名:下载的模板项目自动以输入的项目名称命名
  4. 发布到npm

2.1 代码优化

1、 新增generator.js

/* 生成最终项目  generator.js*/
// npm i handlebars metalsmith -D

const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')

module.exports = function(metadata = {}, src, dest = '.'){
    if(!src){
        return Promise.reject(new Error(`我效的source: ${src}`))
    }
    return new Promise((resolve, reject)=>{
        Metalsmith(process.cwd()) // 在当前目录下执行
        .metadata(metadata) // 设置全局元信息-可被应用于所有文件
        .clean(false)   // 设置是否在写入文件前移除目标路径   
        .source(src)    // 设置资源目录(相对路径)
        .destination(dest) // 目标路径
        .use((files, metalsmith,done) =>{ // 使用函数插件
            const meta = metalsmith.metadata()
            // 修改package.json中的内容
            Object.keys(files).filter(x=>x.includes('package.json')).forEach(fileName=>{
                const t= files[fileName].contents.toString()
                files[fileName].contents = Buffer.from(Handlebars.compile(t)(meta), 'utf-8')
            })
            done()
        }).build(err=>{
            err ? reject(err):resolve()
        })
    })
}

2、 优化create.js

#!/usr/bin/env node

const download = require('download-git-repo')
const template = require('../template.json')
const ora = require('ora') // loading效果
const chalk = require('chalk')  // 添加颜色
const inquirer = require('inquirer')    // 交互命令
const handlebars = require('handlebars') // 元信息替换
const getUser = require('./libs/git-user')
const fs = require('fs')
const generator = require('./libs/generator')

const question = [
    {
        name: 'templateName',
        type: 'input',
         message: '请输入模板名称',
         validate(val){
             if(val===''){
                 console.log(chalk.yellow('模板名称不能为空'))
                 console.log(chalk.grey('请通过my-cli list命令查看可用模板'))
                 return
             }else if(!template[val]){
                 console.log(chalk.red('无此模板,请确认模板名称后重试'))
                 return
             }else{
                 return true
             }
         }
    },
    {
        name: 'projectName',
        type: 'input',
        message: '请输入项目名称',
        validate(val){
            if(val===''){
                console.log(chalk.yellow('项目名称不能为空'))
                return
            }
            return true
        }
    }
]
inquirer
    .prompt(question)
    .then(answers=>{
        let {templateName, projectName} = answers
        const spinner = ora('项目模板下载中...')
        const tempUrl = template[templateName]
        spinner.start()
        download(`direct:${tempUrl}`,projectName,{clone:true}, err=>{
            if(err) {
                spinner.fail()
                console.log(chalk.red(`项目生成失败:${err}`))
                return
            }
            spinner.succeed()
            let currentPath = process.cwd()
            let packageJSON = require(`${currentPath}/${projectName}/package.json`)
            let template = handlebars.compile(JSON.stringify(packageJSON))
            let gitUser = getUser()
            let metaData = {
                name: projectName,
                author: gitUser.name,
                description: '这是默认项目描述呀'
            }
            generator(metaData, `${currentPath}/${projectName}`,`${currentPath}/${projectName}`)
        })
    })

3、终端测试

  • 终端命令测试
    终端测试
  • 最终项目结果
    从零开始搭建一个自己的前端脚手架(一):基础篇_第5张图片

2.2 npm发布

# 1. 登录npm
npm login
# 2. 输入用户名&密码后,发布
npm publish

三、写在最后

本篇blog简单的介绍了一个脚手架的基础创建流程,这其中还有很多需要补充优化的地方,参考vue-cli2的源码 可以学习理解更多。后续也将根据vue-cli2的开发方式来出一波更详细的优化篇教程,包括接入github开放式API来自动获取模板仓库中的内容。

  • 本节项目源码在这儿: https://gitee.com/EChu/my-cli-demo.git
  • 未完待续。。。

你可能感兴趣的:(生产力工具,vue.js,脚手架,从零开始)