前端脚手架了解

4 阅读4分钟

一、要实现什么

希望像@vue/cli一样的方式

  1. 安装我的脚手架包,基于@vue/cli
  2. 能全局使用我的命令来创建项目,能提示并根据用户输入的参数来创建
  3. 创建的项目是写好的脚手架模板,集成了所需的组件和工具和配置
  4. cd进新创建的项目
  5. 执行npm install,执行完npm install后自动执行一个脚本,来安装本地组件库,减少了手动操作,像patch-package
  6. 执行npm run dev,自动启动项目,并自动打开浏览器

二、需要怎么做

  1. 要是一个npm包的形式
  2. 安装后要能全局使用命令,来下载模板项目
  3. 开发模板项目,上传到git

1.新建文件夹

新建目录,cd进mycli目录,开发mycli的npm包

2.执行npm init

生成package.json

3.打开package.json

以下为常用字段说明:

  • 3.1 name: 包名称
  • 3.2 version: 版本号
  • 3.3 description: 描述
  • 3.4 main: 入口文件
  • 3.5 scripts: 脚本
  • 3.6 author: 作者
  • 3.7 license: 许可证
  • 3.8 keywords: 关键字
  • 3.9 dependencies: 依赖
  • 3.10 bin: 命令

4.main字段说明

这里在根目录下新建入口文件index.js。如果想以esmodule的方式导入导出模块,需要文件后缀为.mjs,并且设置type: "module"

5.scripts字段说明

它的每一个属性都对应一个脚本。我们在项目当中运行npm run xxx的时候,主要分为以下几步: 1、从package.json当中读取scrips选项。 2、以传给npm run命令的第一个参数作为键,在scripts中找到要执行的命令,没有找到会报错。 3、找到命令后,自动创建一个shell,其中只要是shell可以运行的命令,就可以写在npm script当中。 4、将当前目录下的node_modules/.bin这个子目录加入PATH变量(这就意味着,当前目录的node_modules/.bin子目录里的所有脚本,都可以直接用脚本名调用,而不需要加路径) 5、在这个shell上执行上述命令

6.bin字段说明

  1. 首先我们要清楚,能在命令行中识别的命令,一定是在环境变量中能找到的,所以当我们安装nodejs后,会在电脑环境变量中增加nodejs命令目录,然后就能全局使用node和node全局目录下的所有命令
  2. 为什么全局安装 @vue/cli 后添加的命令为 vue。 包安装时根据package.json中bin对象,key作为环境变量中可执行命令,value指向实际运行的js文件
  3. 在根目录下新建bin目录(bin 目录用来存放可执行命令的文件夹),在bin目录下新建mycli.js(可以不带后缀名),内容如下:
#!/usr/bin/env node // 告诉系统此脚本用node执行
require('./index.js') // 入口文件

在package.json中添加bin字段,内容如下:

"bin": {
  "mycli": "bin/mycli.js"
}

这样,在包全局安装的时候,下载包到全局 node_modules 中 js读取package.json的bin字段创建可执行文件放在全局node_modules中(能在环境变量中找到),指向bin/mycli.js文件 然后就能在全局使用mycli命令了 执行mycli命令,会在环境变量中查找,然后使用node环境执行bin/mycli.js文件

7.准备项目模板

以上步骤准备好了npm包,接下来就准备好项目模板,上传至git仓库,在脚本中使用

三、实战代码

1.包结构

F:\PRIVATEREPO\MYCLI
│  index.js
│  package.json
│
└─bin
        mycli.js

2.mycli.js

#!/usr/bin/env node // 告诉系统此脚本用node执行
require('./index.js') // 入口文件

3.package.json

{
  "name": "mycli",
  "version": "1.0.0",
  "description": "我的脚手架",
  "main": "index.js",
  "scripts": {
    "dev": "node bin/mycli",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "mycli": "bin/mycli"
  },
  "keywords": [
    "cli",
    "typescript",
    "node"
  ],
  "author": "me",
  "license": "ISC",
  "dependencies": {
    "co": "^4.6.0",
    "co-prompt": "^1.0.0",
    "commander": "^2.15.1",
    "ora": "^5.4.1"
  }
}

4.index.js

'use strict'
// 操作命令行
const exec = require('child_process').exec;
const co = require('co');
const prompt = require('co-prompt');
const ora = require('ora');
const program = require('commander');
const packageInfo = require('./package.json');
const spinner = ora('正在生成...');

// commander是一个命令行解析器
program.version(packageInfo.version)
// 以下是执行mycli init的时候做的事情。可以扩展其他命令,比如增加模板,下载指定模板等
program
    .command('init') // init
    .description('生成一个项目')
    .alias('i') // 简写
    .action(() => {
      // 可以按命令写在单独模块里,在入口文件里引入。这里可以将action代码折叠,看项目思路

      // 下载项目
      const resolve = (result) => {
        const { projectName } = result;
        // git命令,远程拉取项目并自定义项目名
        const url = 'https://github.com/***/***.git'
        const cmdStr = `git clone ${url} ${projectName}`;
    
        spinner.start();
    
        exec(cmdStr, (err) => {
          execRm(err, projectName);
        });
      };

      // 进入项目,删除模板项目的.git文件夹
      const execRm = (err, projectName) => {
        if (err) {
          console.log(JSON.stringify(err));
          console.log('fail', '请重新运行!');
          process.exit();
        }
        // 删除 git 文件,以下删除命令在windows下是可行的,mac没测试,可能命令不同
        exec('cd ' + projectName + ' && rd /s /q ".git"', (err, out) => {
            execFinish(err, projectName);
        });
      }

      // 完成,提示cd项目,npm install
      const execFinish = (err, projectName) => {
        spinner.stop();
    
        if (err) {
          console.log(JSON.stringify(err));
          console.log('err', '请手动删除或重新初始化模板项目的.git文件夹!');
          process.exit();
        }
    
        console.log('suc', '初始化完成!');
        console.log(`cd ${projectName} && npm install`);
        process.exit();
      };
    
      // 处理用户输入
      co(function *() {
        const projectName = yield prompt('项目名字: ');
        return new Promise((resolve, reject) => {
          resolve({
            projectName,
          });
        });
      }).then(resolve);
    });

program.parse(process.argv);
if(!program.args.length){
  program.help()
}

5.准备项目模板

  1. 模板中的package.json中的dependencies要设置好,确保安装完即可正常运行项目,开发时注意最好局部安装而非全局安装
  2. package.json中的scripts字段,有个postinstall脚本,这个脚本会在所有包安装完成后执行,在下面这个例子中,some-script.js 会在所有依赖安装完成后运行。所以可以利用他,实现像patch-package的功能,比如安装完依赖后自动npm link 本地组件库,就不用每次都手动npm link了。
{
  "scripts": {
    "postinstall": "node some-script.js"
  }
}

四、延伸

child_process模块

通过以下方法,你可以在Node.js应用程序中调用npm install命令,安装所需的依赖包,也可以执行其他命令。

// 这种方式会阻塞事件循环,直到命令执行完成。
const { exec } = require('child_process');
exec('npm install <package-name>', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});
// 这种方式不会阻塞事件循环,适合需要同时处理其他任务的场景。
const { spawn } = require('child_process');
const npm = spawn('npm', ['install', '<package-name>']);

npm.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

npm.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});