LiangRongfu / Blog

一篇博文的的重点是让自己对知识点进行梳理和沉淀
1 stars 0 forks source link

自定义脚手架与项目统一规范 #2

Open LiangRongfu opened 4 years ago

LiangRongfu commented 4 years ago

前言

最近在公司兼顾了三个项目的开发,跟不同的人合作发现很多不统一的问题:

等等这些问题,在从开发者的角度来说,先要花时间去熟悉其他人搭的项目,清楚代码之间的依赖才能才能真正的去完成开发任务,或许这里面花的时间不会很长,但是每个人都花了这样的时间,从公司角度来说那应该是一个很大的成本了,我们应该做的是去统一规范、统一架构。另外公司目前前端框架的选型统一是使用vue,当我们每次用vue-cli去初始化项目的时候,都要重新写一些公用的代码或者去旧项目copy,这时有一个专属于公司的一个模版会不会更好?从这里出发,我就查看了一些解决方案和脚手架的实现方式。

脚手架

说到脚手架应该就会想到vue-cli 、 create-react-app 等等,实际上脚手架的作用是什么呢?简单来说就是在前端工作流中负责项目起始阶段创建初始文件。那这样是否可以把前言所述的问题,归类总结做成一个综合解决这些问题的项目模版,通过脚手架来初始化每个项目的初始文件呢?事实是可以的。那下面就明确需求,写一个自定义脚手架初始化项目文件。以下谨记录简单实现过程,重点是解决工作中存在的问题。

第三方库(npm包)

初始化项目后运行安装

npm install commander axios ora download-git-repo inquirer chalk metalsmith consolidate ncp

初始化项目

新建文件夹“xxx-cli”,进入到该目录

npm init -y   # 初始化package.json

文件结构

├── bin
│   └── www  // 全局命令执行的根文件
├── src
│   ├── main.js // 入口文件
│   └── create.js   // create
│   └── constants.js   // 存放常量
│── package.json

www文件中使用main作为入口文件,并且以node环境执行此文件

#! /usr/bin/env node
require('../src/main.js');

设置在命令下执行xxx-cli时调用bin目录下的www文件,package.json中加入; ('xxx-cli'这个名字可以任意起,但如果需要发包需要到官网上验证是否已被用)

"bin": {
    "xxx-cli": "./bin/www"
}

链接包到全局下使用

npm link

我们已经可以成功的在命令行中使用xxx-cli命令,并且可以执行main.js文件!

了解commander 命令行工具

constants.js

const { version } = require('../package.json');
/ 存储模板的位置
const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.template`;
module.exports = {
  version,
  downloadDirectory,
};

main.js就是我们的入口文件

const program = require('commander');
const { version } = require('./constants');

program
    .command('create')
    .alias('c')
    .description('to create a new project')
    .action(() => {
      console.log('create');
    });

program.version(version)
  .parse(process.argv); // process.argv就是用户在命令行中传入的参数

执行 xxx-cli --versionxxx-cli create 是不是已经有一提示了!以上就是commander作用的表现。

create命令

下面就重点来实现create的action的逻辑。为了后续的拓展,改造一下main.js,把create的逻辑写在create.js里

// main.js
const program = require('commander');
const path = require('path');
const { version } = require('./constants');

const mapAction = { // 需要生成的指令数据
  create: {
    alias: 'c',
    description: 'create a project',
    examples: [
      'xxx-cli create <project-name>',
    ],
  },
  '*': {
    alias: '',
    description: 'command not found',
    examples: [],
  },
};
Reflect.ownKeys(mapAction).forEach((action) => {
  program
    .command(action) // 命令名
    .alias(mapAction[action].alias) // 命令别名
    .description(mapAction[action].description) // 命令描述
    .action(() => { // 命令执行的操作
      if (action === '*') { // 命令不存在
        console.log(mapAction[action].description);
      } else {
        require(path.resolve(__dirname, action))(...process.argv.slice(3)); // 引入命令对应操作模块
      }
    });
});

program.on('--help', () => { // help命令打印帮助信息
  console.log('\nExample');
  Reflect.ownKeys(mapAction).forEach((action) => {
    mapAction[action].examples.forEach((item) => {
      console.log(item);
    });
  });
});

program
  .version(version)
  .parse(process.argv);

create功能需求:在命令行输入“xxx-cli create project-name”建立一个“project-name”的项目,项目里的文件是我们提前建好的template,可以选择不同的template。下面来实先create的操作:

现在需要在git上拉取项目模版,这里用axios去获取

npm insatll axios

create.js

// create.js
const fs = require('fs'); 
const path = require('path');

const axios = require('axios');
const ora = require('ora');
const Inquirer = require('inquirer');
const { promisify } = require('util');
const chalk = require('chalk');
const MetalSmith = require('metalsmith');
let { render } = require('consolidate').els;
let downloadGitRepo = require('download-git-repo');
let ncp = require('ncp');

render = promisify(render);
downloadGitRepo = promisify(downloadGitRepo);
const { downloadDirectory } = require('./constants');

ncp = promisify(ncp);

// 获取仓库列表
const fetchRepoList = async () => {
  // 获取当前组织中的所有仓库信息,这个仓库中存放的都是项目模板
  const { data } = await axios.get('https://api.github.com/orgs/xxx/repos');// xxx代表某个仓库
  return data;
};
// 获取选中模版的tags列表
const fechTagList = async (repo) => {
  const { data } = await axios.get(`https://api.github.com/repos/xxx/${repo}/tags`);
  return data;
};

// 封装loading
const waitFnloading = (fn, message) => async (...args) => { // 高阶函数
  const spinner = ora(message);
  spinner.start();
  const result = await fn(...args);
  spinner.succeed();
  return result;
};

// 下载模版
const download = async (repo, tag) => {
  let api = `xxx/${repo}`;
  if (tag) {
    api += `#${tag}`;
  }
  // /user/xxxx/.template/repo
  const dest = `${downloadDirectory}/${repo}`;
  await downloadGitReop(api, dest); // 下载模版存放到指定路径
  return dest; // 下载的最终目录路径
};

// 逻辑主体部分
module.exports = async (projectName = 'my-project') => {
  if (fs.existsSync(projectName)) { // 判断 projectName 文件夹是否存在?
    console.log(chalk.red('Folder already exists.'));
  } else {
  // 1.获取组织下的所有模版;
    let repos = await waitLoading(fetchRepoList, 'fetching template...')();
    repos = repos.map((item) => item.name);
    const { repo } = await Inquirer.prompt({ // 选择模版
      name: 'repo',
      type: 'list',
      message: 'please choise a template to create project',
      choices: repos,
    });

    // 2.获取当前选择项目的对应版本号
    let tags = await waitLoading(fetchTagList, 'fetching tags...')(repo);
    let result;
    if (tags.length > 0) {
      tags = tags.map((item) => item.name);
      const { tag } = await Inquirer.prompt({ // 选择模版的版本号
        name: 'tag',
        type: 'list',
        message: 'please choise tags to create project',
        choices: tags,
      });
      result = await waitLoading(download, 'download template...')(repo, tag); // 下载模版,拿到缓存模版的路径
    } else {
      result = await waitLoading(download, 'download template...')(repo); // 下载模版,拿到缓存模版的路径
    }

    if (!fs.existsSync(path.join(result, 'ask.js'))) { // 是否需要输入信息
      try {
        await ncp(result, path.resolve(projectName)); // 把模版复制到projectName
        console.log('\r\n', chalk.green(`cd ${projectName}\r\n`), chalk.yellow('npm install\r\n')); // 信息提示
      } catch (error) {
        console.log(error);
      }
    } else {
      await new Promise((resolve, reject) => { // 需要用户输入信息
        MetalSmith(__dirname)
          .source(result)
          .destination(path.resolve(projectName)) 
          .use(async (files, metal, done) => {
            const args = require(path.join(result, 'ask.js')); // 获取填写选项
            const select = await Inquirer.prompt(args);
            const meta = metal.metadata(); // 用户填写的结果
            Object.assign(meta, select);
            delete files['ask.js'];
            done();
          })
          .use((files, metal, done) => { // 根据用户输入编写模版
            const obj = metal.metadata();
            Reflect.ownKeys(files).forEach(async (file) => {
              if (file.includes('js') || file.includes('json')) {
                let content = files[file].contents.toString();
                if (content.includes('<%')) {
                  content = await render(content, obj);
                  files[file].contents = Buffer.from(content);
                }
              }
            });
            done();
          })
          .build((err) => {
            if (err) {
              reject();
            } else {
              console.log('\r\n', chalk.green(`cd ${projectName}\r\n`), chalk.yellow('npm install\r\n'));
              resolve();
            }
          });
      });
    }
  }
};

以上创建项目的代码基本完成。

发布

nrm use npm
npm publish

发布后,安装

npm install xxx-cli -g // 安装脚手架

xxx-cli create projext-xxx // 创建项目

一个简单的脚手架就这样实现了,虽然简单,但是在公司推行起来作用和效果还是蛮大的。

以上仅从个人遇到的问题的角度出发去实现,如有错误或不妥的地方请指出,感谢阅读。

LiangRongfu commented 4 years ago

在此之前vue-cli3已经有做了这个预设的功能,可以快速解决到我遇到的问题,脚手架是考虑到可以拉去vue以外的模版,同时建立公司的模版库