从0开始写一个前端脚手架

3,713 阅读4分钟

从0开始写一个前端脚手架

入职两年了,一直都在疑惑,前端脚手架是怎么生成的,原理是什么。

刚开始参加工作的时候因为项目很忙,工作经验不多,所以一直没有什么空余时间去做一些自己想做的事情。 现在在空余时间学习了一下如何从0开始自己搭建一个自己的脚手架。

写脚手架的方式有很多种,我所知道的方式有shell还有node shell没有搞懂,所以先搞了搞node的方式。

初始化npm

npm init 一路回车下去  或者执行 npm init -y。就可以免去一路回车的痛苦了

image.png

创建执行文件 cli.js

image.png

在package.json中添加"bin":"cli.js"

image.png

将当前文件的执行方法使用npm link连接到全局

mac有可能会提示没有权限请使用 sudo执行。 window用户就请自行百度啦。

   执行 npm link or sudo npm link
   

出现下方情况则代表连接成功 image.png

创建模版

1、创建一个文件夹就叫templates

2、创建下方一个文件格式

image.png

具体文件内内容请查看juejin.cn/post/700215…

模版我们要使用到ejs插件来写模版 需要添加ejs依赖

npm install ejs -S

参数在模版中是怎样使用的呢🧐

如下

image.png

那参数是如何传进模版中的呢? 在编写cli.js文件的时候我会详细说明。

编写cli文件内容

首先要进行交互 使用inquirer插件 使用方法blog.csdn.net/qq_26733915…

cli.js文件 执行 npm install inquirer -S 安装inquirer

// 这句话很重要代表使用node环境
#! /usr/bin/env node 

//  inquirer插件可以做人机交互,询问创建者问题。
const inquirer = require('inquirer');
...... 
// question 是人机交互的问题和字段
const question = [
  {
    type: 'input',
    message: 'projectName',
    default: 'my-app',
    name: 'projectName',
  },
  {
    name: 'language',
    type: 'list',
    choices: ['react', 'vue'],
    default: 'react',
  },
  {
    name: 'style',
    type: 'list',
    choices: ['less', 'sass'],
    default: 'less',
  },
  {
    name: 'jsType',
    type: 'list',
    choices: ['js', 'ts'],
    default: 'js',
  },
  {
    type: 'input',
    message: 'version',
    name: 'version',
    default: '1.0.0',
  },
  {
    type: 'input',
    message: 'prot',
    default: '3000',
    name: 'prot',
  },
];

inquirer.prompt(question).then((answer)=>{console.log(answer)})

因为上面已经将文件名字链接到全局了所以直接执行 myl-cli

image.png

然后就可以进行生成文件了

就叫他 createProject

   // node中文件函数
   const fs = require('fs');
   
   // node中路径函数
   const path = require('path');
   
   const createProject=(answer)=>{
       // 首先我们要使用fs方法获取到当前文件的路径 fs方法是node提供的方法直接引用就可以了
       // 然后创建一个文件夹,文件夹名字就从answer中取
       // 创建文件之前需要获取到当前文件的路径。使用path方法与fs一样
       
       /**
        先获取到当前文件中的templates文件夹
        __dirname 代表当前文件的路径
       */

      const templatespath = path.join(__dirname, 'templates');
      fs.mkdir(answer.projectName,{ recursive: true },()=>{
          createFIles(templatespath, answer, answer.projectName);
      })
      /**
        ej.renderFile 无法读取文件夹,所以需要手动创建,脚手架的结构是固定的所以不需要写太多逻辑在里面
        只需在需要创建文件夹的地方通过fs.mkdir创建一下
       */
      fs.mkdir(`${answer.projectName}/src/app`, { recursive: true }, () => {
        const app = `${process.cwd()}/templates/src/app`;
        createFIles(app, answer, `${answer.projectName}/src/app`);
      });
   }

再提取一个createFIles方法出来,创建文件

 /**
  create files
 */
const createFIles = (templatespath, answer, paths) => {
  // 使用文件函数读取当前文件夹内的文件
  fs.readdir(templatespath, (err, files) => {
    // files是一个数组 遍历这个数据
    files.forEach((file) => {
      if (file === 'src' || file === 'app') return;
      // 通过ejs渲染模版   ejs通过renderFile会获取到文件里面的内容
      ejs
        .renderFile(path.join(templatespath, file), answer)
        .then((data) => {
          /**
          下面两个if是为了兼容不同的后缀文件
            path.extname(file)  获取文件的后缀名称
            path.basename(file, '.ejs') 获取后缀名称为.ejs的文件名称。
         */
          if (path.extname(file) === '.ejs') {
            file = `${path.basename(file, '.ejs')}.js`;
          }
          if (path.basename(file, '.js') === 'package') {
            file = 'package.json';
          }
          // 通过fs.writeFileSync方法将 renderFile获取到的文件内容写进新的文件中
          fs.writeFileSync(path.join(__dirname, `${paths}/${file}`), data);
        })
        .catch((err) => {
          console.log(err);
        });
    });
  });
};


然后我们优化一下 最后完整的cli.js文件如下

#! /usr/bin/env node

//  inquirer插件可以做人机交互,询问创建者问题。
const inquirer = require('inquirer');
// node中文件函数
const fs = require('fs');
// node中路径函数
const path = require('path');
// ejs插件  创建ejs模版可以将用户输入的问题渲染到模版中
const ejs = require('ejs');
// 执行任务的loading
const Listr = require('listr');
// node中child_process的exec  可以在node中执行shell脚本
const { exec } = require('child_process');
// 自动执行install的插件
const { projectInstall } = require('pkg-install');
// 打印logo
const figlet = require('figlet');
// 输出文字的颜色样式。
const chalk = require('chalk');
const axios = require('axios');

const question = [
  {
    type: 'input',
    message: 'projectName',
    default: 'my-app',
    name: 'projectName',
  },
  {
    name: 'language',
    type: 'list',
    choices: ['react', 'vue'],
    default: 'react',
  },
  {
    name: 'style',
    type: 'list',
    choices: ['less', 'sass'],
    default: 'less',
  },
  {
    name: 'jsType',
    type: 'list',
    choices: ['js', 'ts'],
    default: 'js',
  },
  {
    type: 'input',
    message: 'version',
    name: 'version',
    default: '1.0.0',
  },
  {
    type: 'input',
    message: 'prot',
    default: '3000',
    name: 'prot',
  },
];

/**
  create files
 */
const createFIles = (templatespath, answer, paths) => {
  // 使用文件函数读取当前文件夹内的文件
  fs.readdir(templatespath, (err, files) => {
    // files是一个数组 遍历这个数据
    files.forEach((file) => {
      if (file === 'src' || file === 'app') return;
      // 通过ejs渲染模版   ejs通过renderFile会获取到文件里面的内容
      ejs
        .renderFile(path.join(templatespath, file), answer)
        .then((data) => {
          /**
          下面两个if是为了兼容不同的后缀文件
            path.extname(file)  获取文件的后缀名称
            path.basename(file, '.ejs') 获取后缀名称为.ejs的文件名称。
         */
          if (path.extname(file) === '.ejs') {
            file = `${path.basename(file, '.ejs')}.js`;
          }
          if (path.basename(file, '.js') === 'package') {
            file = 'package.json';
          }
          // 通过fs.writeFileSync方法将 renderFile获取到的文件内容写进新的文件中
          fs.writeFileSync(path.join(__dirname, `${paths}/${file}`), data);
        })
        .catch((err) => {
          console.log(err);
        });
    });
  });
};
console.log(
  chalk.green(
    '\r\n' +
      figlet.textSync('myl-cli', {
        font: 'Ghost',
        horizontalLayout: 'default',
        verticalLayout: 'default',
        width: 80,
        whitespaceBreak: true,
      }),
  ),
);

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

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

// create project
const createProject = (answer) => {
  // const res = await getRepoList();
  /**
    先获取到当前文件中的templates文件夹
    __dirname 代表当前文件的路径
   */

  const templatespath = path.join(__dirname, 'templates');

  /**
    1、先创建 ${answer.projectName} 文件夹
    2、然后创建文件夹中的文件
   */
  fs.mkdir(answer.projectName, { recursive: true }, () => {
    createFIles(templatespath, answer, answer.projectName);
  });
  /**
    ej.renderFile 无法读取文件夹,所以需要手动创建,脚手架的结构是固定的所以不需要写太多逻辑在里面
        只需在需要创建文件夹的地方通过fs.mkdir创建一下
   */
  fs.mkdir(`${answer.projectName}/src/app`, { recursive: true }, () => {
    const app = `${process.cwd()}/templates/src/app`;
    createFIles(app, answer, `${answer.projectName}/src/app`);
  });
};

inquirer.prompt(question).then(async (answer) => {
  /**
    Listr 是一个一步的loading插件。
    task指的是要执行那一个任务
    Listr执行顺序是从上到下依次执行
  */
  const tasks = new Listr([
    {
      title: chalk.yellow('create directory and file'),
      task: () => createProject(answer),
    },
    {
      title: chalk.blue('create git'),
      task: () =>
        exec('git init', {
          cwd: path.join(__dirname, answer.projectName),
        }),
    },
    {
      title: chalk.magenta('npm install'),
      task: () =>
        projectInstall({
          cwd: path.join(__dirname, answer.projectName),
          prefer: 'npm',
        }),
    },
  ]);
  await tasks.run();
});

然后执行 myl-cli 就可以啦

此文是我学习了掘金一个大佬的文章之后写的。写的不是很详细。

最后附上大佬文章地址 juejin.cn/post/696611… 大佬的文章更详细