從0到1系列第二輯,從命令行到腳手架

運行一行終端命令來建立腳手架,究竟經歷了什麼呢?

腳手架,必然運用到不少的命令行知識。本文會向你們介紹一個簡單的vue腳手架從無到有的全過程,但願你們有所收穫。javascript

項目目錄

|──bin
|    └──try.js
|──config
|    └──config.js
|──lib
|    ├──actions.js
|    ├──create.js
|    └──otherCommand.js
|──utils
|    └──terminal.js
|──templates
|    └──component.ejs
複製代碼

用到的庫

命令行操控工具  --  commander
下載、克隆利器  --  download-git-repo 
命令行交互工具  --  inquirer
文件操做        --  fs-extra
地址操做        --  path
複製代碼

直接上手

1. 建立項目

npm init -y  // 初始化package.json
複製代碼

2. 建立執行文件,配置目錄

2.1 建立/bin文件夾,配置命令的執行入口文件:/bin/try.js

文件的第一行必須是:vue

#! /usr/bin/env node
複製代碼

#! 稱爲shebang,增長這一行是爲了指定:以 node 執行腳本文件。java

2.2 在package.json內配置「bin」字段

爲咱們的第一個腳手架工具命名爲:just-cli,鍵名對應的值指向的地址則是命令行的執行入口文件地址。node

{
	"bin": {
        "just-cli": "./bin/try.js"
    },
}
複製代碼

3 連接命令至全局

終端執行 **npm link **,便可將配置的命令註冊至全局。webpack

npm link  // 取消連接: npm unlink
複製代碼

這時候,windows用戶到npm安裝目錄查看,會發現咱們多了這些文件:git

  • just-cli
  • just-cli.cmd
  • just-cli.ps1

PS:mac用戶能夠到 /usr/local/lib/node_modules 查看。web

4 配置版本號

4.1 首先安裝包:commander
npm install commander --save
複製代碼
4.2 配置版本號命令:just-cli -V & just-cli -v
#! /usr/bin/env node

/* * 文件:/bin/try.js */

//引入 commander
const program = require('commander')

//版本號配置
program.version(`Version is ${require('../package.json').version}`, '-v, --version')//支持小寫:‘-v’
    .description('手寫一個腳手架')
    .usage('<command> [options]')

//解析參數,常置於定義命令以後
program.parse(process.argv);
複製代碼

到這裏,你已經爲 just-cli 配置了版本信息,這個版本信息是從腳手架項目的package.json中動態獲取的,並支持如下兩種,不區分大小寫的查看版本信息的方式。npm

just-cli -v
just-cli -V
複製代碼

試試看打印你本身的腳手架版本信息吧!json

5 第一個命令

定義一個形如:create name [options] 的命令,用於執行腳手架的初始化。windows

5.1 定義命令

首先咱們定義這個命令,配置命令攜帶參數:name,添加命令描述,爲其賦予 -f 的能力,指定執行函數:createAction

/* * 文件:/lib/create.js * 命令:搭建腳手架 * create <name> */

const program = require('commander')
const {createAction} = require('./actions')
const create = function(){
    program.command('create <name>')
    .description('create a new project')
    .option('-f, --force', 'title to use before name')
    .action(createAction)
}
module.exports =create
複製代碼

而後在命令入口文件中引入上文的配置。

/* * 文件:/bin/try.js */

//......

//引入拆分的配置文件
const createConfig = require('../lib/create')

//添加拆分的配置:create <name>
createConfig()

//......
複製代碼
5.2 定義命令的執行內容
/* * 文件:/lib/actions.js * 用途:定義命令的執行內容 */
const open = require('open')
const { promisify } = require('util')

const download = promisify(require('download-git-repo'))

const { labAdress } = require('../config/lab-config')
const { spawnCommand } = require('../utils/terminal')



const createAction = async (project,options) => {
    console.log(`creating a project:${project},\noptions:\n${JSON.stringify(options)}`)
	
    //執行模板的克隆
    console.log(`downloading the templates documents`)
    await download(labAdress, project, { clone: true })

    // 執行命令 npm install
    // 判別命令在不一樣操做系統的類型,且windows系統會統一返回'win32',無論其自己操做系統是32/64位
    const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
    console.log(`spawn npm install:`)
    await spawnCommand(npm, ['install'], { cwd: `./${project}` })

    // 同步執行命令 npm run dev
    // 同步調用的緣由:線程在項目運行後,會阻塞後續代碼執行
    console.log(`spawn npm run dev`)
    spawnCommand(npm, ['run', 'dev'], { cwd: `./${project}` })

    // // 同步打開瀏覽器
    open('http://localhost:8080');

}
module.exports = {
    createAction
}
複製代碼

看看有哪些須要須要特別理解的地方:

  1. /config/config.js 文件儲存了模板的下載地址,全局變量統一存儲在這個文件。
  2. promisify 方法提供一個把同步方法轉爲異步方法的功能,避免回調地獄,很好用!
  3. 打開瀏覽器 一般不會在腳手架中配置,此處是爲了直觀地體現腳手架建立全流程。一般在模板的webpack中指定在打包完成後打開對應端口的網頁。
  4. spawnCommand 是封裝的child_process.spwan方法,封裝內容以下。實際上,child_process的exec和spawn方法都能執行終端命令,child_process.spawn 適合用在處理大量數據返回的場景中,返回的數據大小沒有限制,在此處下載場景下更適合。
/* 執行終端命令 */
const { spawn } = require('child_process')

//args參數包括:command、args、options
const spawnCommand = (...args) => {
    return new Promise((resolve, reject) => {
        const spawnProcess = spawn(...args)
        //複製控制檯打印到主進程
        spawnProcess.stdout.pipe(process.stdout)
        spawnProcess.stderr.pipe(process.stderr)
        //監聽進程的關閉,在執行完畢或error時觸發
        spawnProcess.on('close', () => {
            resolve()
        })
    })
}
module.exports = {
    spawnCommand
}
複製代碼

6 設計交互邏輯

在上一節,實現了運用命令行建立腳手架模板,接下來,還須要對腳手架的建立過程進行優化,在命令的執行方法中添加必要的交互動做和邏輯判斷。 引入 inquirer 工具,能很好地解決命令行交互的問題,交互和邏輯判斷代碼以下。

/* * 文件:/lib/actions.js * 用途:定義命令的執行內容 */
const open = require('open')
const path = require('path')
const fsextra = require('fs-extra')
const Inquirer = require('inquirer')
const { promisify } = require('util')
const download = promisify(require('download-git-repo'))

const { labAdress } = require('../config/lab-config')
const { spawnCommand } = require('../utils/terminal')
const { delDir } = require('../utils/common')

async function createAction(projectName, options) {
    console.log(`${projectName},\noptions:\n${JSON.stringify(options)}`)

    const cwd = process.cwd();// 獲取當前命令執行時的工做目錄 
    const targetDir = path.join(cwd, projectName);// 目標目錄 
    console.log(targetDir)

    if (fsextra.existsSync(targetDir)) {
        if (options.force) {
            delDir(targetDir);
            console.log('刪除原目錄成功')
            create(projectName)
        } else {
            let { action } = await Inquirer.prompt([
                {
                    name: 'action',
                    type: 'list',
                    message: '目標文件夾已存在,請選擇是否覆蓋:',
                    choices: [
                        { name: '覆蓋', value: true },
                        { name: '取消', value: false }
                    ]
                }
            ])
            if (!action) {
                console.log('取消操做')
                return
            } else {
                console.log(`\r\n正在刪除原目錄....`);
                delDir(targetDir)
                console.log('刪除成功')
                create(projectName)
            }
        }
    } else {
        create(projectName)
    }
}

const create = async (projectName) => {
    // 見5.2小節內容
}
module.exports = {
    createAction
}
複製代碼

上文中的 delDir 方法是咱們封裝的一個向內遞歸遍歷文件、刪除文件夾的方法,定義以下:

// 引入fs模塊
const fs = require('fs-extra');

function delDir(path) {
  // 讀取文件夾中全部文件及文件夾
  const list = fs.readdirSync(path)
  list.forEach((item) => {
    // 拼接路徑
    const url = path + '/' + item
    // 讀取文件信息
    const stats = fs.statSync(url)
    // 判斷是文件仍是文件夾
    if (stats.isFile()) {
      // 當前爲文件,則刪除文件
      fs.unlinkSync(url)
    } else {
      // 當前爲文件夾,則遞歸調用自身
      arguments.callee(url)
    }
  })
  // 刪除空文件夾
  fs.rmdirSync(path)
}

module.exports={
    delDir
}
複製代碼

7 腳手架的其餘功能

除了建立模板,腳手架還應該具有其餘方便工做的指令,下面介紹如何運用腳手架命令添加一個新組件。 咱們定義一個形如:

just-cli component <componentName>  -d <destination>
複製代碼

的命令,其中 -d 和 destination 參數爲選填。咱們須要如下四個步驟來完成這個命令的執行。

  1. 模板文件
  2. 交互判斷:是否存在/是否覆蓋/地址參數等
  3. 傳入參數,編譯模板
  4. 生成指定位置/默認位置的.vue文件

即刻着手! 首先咱們定義這個命令,配置 -f 和 -d 能力,攜帶指定文件位置的參數:dest

/* * 文件:/lib/newComponent.js * 定義命令:新增頁面 * page <pageName> */

const program = require('commander')
const { addPageAction } = require('./actions')
const page = function () {
    program.command('page <name>')
        .description('add a new page')
        .option('-f, --force', 'spwan with force')
        .option('-d, --dest <dest>', '指定存儲地址,例如:just-cli component <newPage> -d /studio/task')
        .action(addPageAction)
}
module.exports = page
複製代碼
7.1 模板文件

使用ejs模板實現模板的參數佔位,一個預設的.vue文件模板以下:

<template>
    <div class="<%= data.lowerName %>">{{msg}} <h1>{{message}}</h1> </div>
</template>
<script> export default { name: '<%= data.name %>', props: { msg: String }, components: {}, mixins: [], data() { return { message: "<%= data.name %>" } }, create() { }, mounted() { }, computed: {} } </script>
<style> .<%=data.lowerName %> {} </style>
複製代碼

注意:模板字符串也能實現這個功能。

7.2 交互判斷

略,參考第6節

7.3 編譯模板

模板的編譯函數封裝以下:

const ejs = require('ejs')
const path = require('path');

const compileEjs = (templateName, data) => {
    const templatePosition = `../templates/${templateName}`
    const templatePath = path.join(__dirname, templatePosition)
    return new Promise((resolve, reject) => {
        ejs.renderFile(templatePath, { data }, {}, (err, res) => {
            if (err) {
                console.log(err);
                reject(err)
                return
            }
            resolve(res)
        })
    })
}
複製代碼

其中,參數data中包含了要傳入模板的參數,如: name。

7.4 生成指定位置/默認位置的.vue文件

文件的寫入函數封裝以下:

const fs = require('fs-extra');
const writeToFile = (path, templateContent) => {
    return fs.promises.writeFile(path, templateContent)
}
複製代碼

順序完成這四個步驟,一個新增組件的命令就已經完成了!

------------------------------------------------------------

學到這裏,小夥伴們已經能夠將經常使用的項目配置經過上傳代碼倉庫、運行腳手架來生成項目腳手架模板了,但這還只是腳手架的一點點,腳手架定製遠不止這些內容,請大開想象,動手定製你專屬的腳手架吧!

相關文章
相關標籤/搜索