前端脚手架之根据用户选择,自动生成所需的页面模块

前言

前不久,产品经理A跑来对我说,你看,这项目(cloud-web)能不能只有jingzhe(目前项目里的一个独立的功能模块)?

产品经理B又跑来对我说,我这个项目,既需要jingzhe,也需要yuyi(亦为一个独立的功能模块)

我看了下项目结构,如下图所示,略微思索了一下,回答他们,可以
在这里插入图片描述
views下面的这些模块,皆为独立的路由模块,比如本地jingzhe的访问路径为http:localhost:3000/jingzhe

changeLog为日志模块,始终存在于各个不同的项目里

如何编写脚手架,根据用户选择,只生成用户所需的页面模块呢?

正文

开始

新建一个项目,名为sf-cli,在该项目下,执行npm init命令,自动生成package.json文件

mkdir sf-cli && cd sf-cli

npm init

创建各个目录

在项目内新建binlibtemplates文件夹,如下图所示
在这里插入图片描述

bin文件夹里新建sf.js,代码如下所示:

#!/usr/bin/env node

const program = require('commander');

program
    .command('init')
	.alias('i')
	.description('欢迎使用 @sf/cli')
	.action(() => {
		const Init = require('../lib/init')
        new Init();
	});

program.parse(process.argv)
// 若参数不足2个,则打印该脚本所有命令
if (!process.argv.slice(2).length) {
  program.outputHelp();
}

新建init.js

lib文件夹内新建init.js,代码如下所示:

const clear = require('clear');
const figlet = require('figlet');
const chalk = require('chalk');
const ora = require('ora');
const inquirer = require('inquirer');
const GitDownloader = require('./initUtils/GitDownloader');
const Selector = require('./initUtils/Selector');

class InitInquirer {
    interactionsHandler() {
        // 提问模式
        const questions = [
          {
            type: 'input',
            name: 'name',
            message: '请输入项目名称:',
            validate: (input) => {
              if (!input) {
                return '请输入项目名称!';
              }
              return true;
            },
            default: 'cloud-web'
          },
          {
            type: 'input',
            name: 'description',
            message: '请输入项目描述:',
            default: 'Base on sf-cli'
          }
        ];
    
        return inquirer.prompt(questions);
    }
}
module.exports = class Init {
    constructor() {
        clear();
        console.log('*****************************');
        console.log(chalk.green(figlet.textSync('SF Starter', { horizontalLayout: 'full' })));
        console.log('*****************************');
        console.log();

        const spinner = ora('👉 检查构建环境...').start();

        spinner.succeed(chalk.green('构建环境正常!'));
        console.log();
        this.init();
    }

    async init() {
        const answer = await new InitInquirer().interactionsHandler();

        // 依据配置下载所需的模板
        const finalAnswer = await new Selector().interactionsSelect(answer);

        // 构建配置保存
        await new GitDownloader().syncDownload(answer, finalAnswer);
    }
}

clear()会清空控制台所有信息,如下图所示

在这里插入图片描述

init()函数获取到用户的输入信息answer,将其传给选择器Selector,选择器Selector内会让用户选择是否有jingzheyuyi等功能模块,再将finalAnswer传递给GitDownloader,执行仓库下载即可

新建init.js相关的工具库

Selector.js

lib目录下新建initUtils目录,再在该目录下,新建Selector.js文件,代码如下所示:

const inquirer = require('inquirer');
const choiceList = require('./choiceList');

module.exports = class Selector {
    async interactionsSelect() {
        const questionsChoice = this.generateChoiceList(choiceList);
        const resultChoice = await inquirer.prompt(questionsChoice);
        return resultChoice;
    }
    generateChoiceList(choiceList){
        return [
          {
            type: 'checkbox',
            name: 'selectTypes',
            message: '选择需要的页面类型',
            choices: choiceList
          }
        ];
      }
}

同级目录下,新建choiceList.js文件,代码如下所示:

module.exports =  [
    { name: 'jingzhe', value: 'jingzhe' },
    { name: 'longhua', value: 'longhua' },
    { name: 'medical', value: 'medical' },
    { name: 'solution', value: 'solution' },
    { name: 'yuyi', value: 'yuyi' }
]

resultChoice为多选数组,比如你选择了jingzheyuyi,则resultChoice[{ name: 'jingzhe', value: 'jingzhe' },{ name: 'yuyi', value: 'yuyi' }]

GitDownloader.js

initUtils目录下,新建GitDownloader.js文件,代码如下所示:

const chalk = require('chalk');
const ora = require('ora');
const child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const OptionsFilter = require('./OptionsFilter');

const templates = {
    vue2: {
        url: 'http://git.sf-express.com/scm/gis-dsb/gis-dsb-core-cloud.git',
        description: 'SF Starter'
    }
}
module.exports = class GitDownloader {
    async syncDownload({type='vue2', name='', description}, finalOptions) {
        console.log();
        console.log(chalk.green('👉  开始构建,请稍侯...'));
        console.log();

        const spinner = ora('正在构建模板...').start();

        const { url } = templates[type];

        // 执行下载
        await this.executeDownload(url, name);

        const optionsFilter = new OptionsFilter();
        // 选择包括模块:排除不用内容
        await optionsFilter.excludeModules(name, finalOptions);

        // 执行成功相关操作
        this.executeBuildSuccess(spinner, {name, description});
    }

    executeDownload(url, name) {
        return new Promise((resolve) => {
            child_process.execSync(`git clone -b feature/dev/baseline ${url} ${name}`);
            resolve(true);
        });
    }
    executeBuildSuccess(spinner, options) {
        console.log();
        spinner.succeed(chalk.green('构建成功!'));
        const packagePath = path.join(options.name, './code/cloud-web/package.json');
        try {
            const packageContent = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
            packageContent.name = options.name;
            packageContent.description = options.description;
        
            // 写入配置
            fs.writeFileSync(packagePath, JSON.stringify(packageContent, null, 2), {
                encoding: 'utf8'
            });
        } catch (error) {
            console.log('write file error==', error);
        }
    
        console.log();
        console.log(chalk.green('👏 初始化项目完成!👏'));
        console.log();
        console.log(chalk.blue('命令提示:'));
        console.log(chalk.blue(`  # 进入项目`));
        console.log(chalk.blue(`  $ cd ./${options.name}`));
        console.log(chalk.blue(`  # 安装依赖`));
        console.log(chalk.blue(`  $ npm install`));
        console.log(chalk.blue(`  # 运行`));
        console.log(chalk.blue(`  $ npm run dev`));
        console.log();
      }
}

executeDownload调用Node.js内置的child_process.execSync方法,拉取远程仓库http://git.sf-express.com/scm/gis-dsb/gis-dsb-core-cloud.git,其分支名为feature/dev/baseline里的代码

executeDownload里的第二个参数name,为init.js里用户在命令行终端输入的项目名称name,即会默认在当前命令执行目录内,生成name文件夹,其里面的代码来源于上面的仓库

executeBuildSuccess根据init.js里用户的选择,重新生成package.json,并替代原来的文件

OptionsFilter.js文件

initUtils目录下,新建OptionsFilter.js文件,代码如下所示:

const del = require('del');
const choiceList = require('./choiceList');
const path = require('path');
const fs = require('fs-extra');
const Handlebars = require('handlebars');

function camelize (str) {
  return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '')
}
module.exports = class OptionsFilter {
  async excludeModules(name, finalOptions) {
    const excludeTypeList = [];
    const includeTypeList = [];
    const cwd = process.cwd();
    const parentPath = path.join(`${cwd}/${name}`, "./code/cloud-web/src/pages/index");
    choiceList.forEach(list => {
      // 为空时代表要排除,返回值代表需要保留
      if (!this.checkFileNeedtoKeep(list.value, finalOptions)) {
        excludeTypeList.push(list);
      } else {
        includeTypeList.push(list)
      }
    })
    this.generateRouteFile(includeTypeList, parentPath);
    this.generateStoreFile(includeTypeList, parentPath);
    // 执行删除
    for (const needToExcludeItem of excludeTypeList) {
      try {
        if (needToExcludeItem.value) {
          const delPath = path.join(parentPath, `./views/${needToExcludeItem.value}`);
          await del(delPath);
        }
      } catch (error) {
        console.log('excludeModules error..', error);
      }
    }
  }

  /**
   * 检测是否需要保留内容
   *
   * @private
   * @param {*} finalOptions
   * @param {*} checkStr
   * @returns
   *
   * @memberOf OptionsFilter
   */
  checkFileNeedtoKeep(checkStr, finalOptions) {
    let findStr = '';
    for (let index = 0; index < finalOptions.selectTypes.length; index++) {
      const elementSelectTypeItem = finalOptions.selectTypes[index];
      if (checkStr === elementSelectTypeItem) {
        findStr = elementSelectTypeItem;
        break;
      }
    }

    return findStr;
  }
  /**
   * 生成路由文件
   * @param {Array} includeTypeList 选中的views列表
   * @param {String} parentPath 父级目录地址
   */
  generateRouteFile(includeTypeList, parentPath) {
    const solutionIndex = includeTypeList.findIndex(a => a.name === 'solution')
    let defaultPage = ''
    if (solutionIndex > -1) {
      defaultPage = 'solution'
    } else {
      defaultPage = includeTypeList[0].name
    }
    const sourceFile = fs.readFileSync(path.join(__dirname, '../../templates/init/router.handlebars'), 'utf-8');
    const source = Handlebars.compile(sourceFile)({
      defaultPage,
      list: includeTypeList
    });
    fs.outputFileSync(path.join(parentPath, `./config/router.config.js`), source, 'utf8');
  }
  generateStoreFile(includeTypeList, parentPath) {
    const files = fs.readdirSync(path.join(parentPath, `./store/modules`), { withFileTypes: true })
    const hasLonghua = includeTypeList.findIndex(a => a.name === 'longhua') > -1
    const hasMedical = includeTypeList.findIndex(a => a.name === 'medical') > -1
    const hasJingZhe = includeTypeList.findIndex(a => a.name === 'jingzhe') > -1
    const sourceFile = fs.readFileSync(path.join(__dirname, '../../templates/init/store.handlebars'), 'utf-8');
    const modules = files.map(a => {
      const formatName = camelize(a.name.split('.')[0])
      return {
        name: formatName
      }
    })
    const source = Handlebars.compile(sourceFile)({
      modules,
      hasLonghua,
      hasMedical,
      hasJingZhe
    });
    fs.outputFileSync(path.join(parentPath, `./store/index.js`), source, 'utf8');
  }
}

generateRouteFile根据templates里的router.handlebars模板以及用户选择的includeTypeList,自动生成路由文件router.config.js

generateStoreFile根据templates里的store.handlebars模板以及用户选择的includeTypeList,自动生成store目录下的index.js

执行del命令,删除没有被选中的目录文件

templates

模板文件,我选择Handlebars,当然,你们也可以选择其他模板引擎

templates目录下新建init目录,在该目录下,新建router.handlebarsstore.handlebars俩文件,其代码分别如下所示:

router.handlebars

// auto-generated by sf-cli
/**
 * 当前以后台动态配置为准,此配置为备用项
 * 走菜单,走权限控制
 * @type {[null,null]}
 */
 export const asyncRouterMap = [
    {
        path: '*',
        redirect: '/404',
        hidden: true,
    },
]
/**
 * 基础路由
 * @type { *[] }
 */
export const constantRouterMap = [
    {
        path: '/',
        redirect: '/{{defaultPage}}',
        hidden: true,
    },
    {{#each list}}
    {
        path: '/{{name}}',
        name: '{{name}}',
        component: () => import(/* webpackChunkName: {{name}}*/ '@index/views/{{name}}/index'),
        hidden: true,
    },
    {{/each}}
    {
        path: '/404',
        component: () => import(/* webpackChunkName: "fail" */ '@/core/views/exception/404'),
    }
]

store.handlebars

// auto-generated by sf-cli
import Vue from 'vue'
import Vuex from 'vuex'

import getters from './getters'
import createLogger from 'vuex/dist/logger'

{{#each modules}}
import {{name}} from './modules/{{name}}'
{{/each}}

{{#if hasLonghua}}
import LongHuaModules from '@index/views/longhua/store'
{{/if}}
{{#if hasMedical}}
import MedicalModules from '@index/views/medical/store'
{{/if}}
{{#if hasJingZhe}}
import JingZheModules from '@index/views/jingzhe/store'
{{/if}}
import { SFDModules } from '@sf/map-service/es/index'
import { SFViewGetters, SFViewModules } from '@/sf-view-vue/src/index.js'
import { blockdata } from '@sf/block-data'


Vue.use(Vuex)

const plugins = process.env.NODE_ENV !== 'production' ? [createLogger({})] : []


export default new Vuex.Store({
    modules: {
        {{#each modules}}
        {{name}},
        {{/each}}
        {{#if hasLonghua}}
        ...LongHuaModules,
        {{/if}}
        {{#if hasMedical}}
        ...MedicalModules,
        {{/if}}
        {{#if hasJingZhe}}
        ...JingZheModules,
        {{/if}}
        ...SFViewModules,
        ...SFDModules,
        ...blockdata
    },
    getters: {
        ...getters,
        ...SFViewGetters,
    },
    plugins,
})

package.json

package.json里的代码如下所示:

{
  "name": "@sf/cli",
  "version": "3.0.4",
  "description": "Command line interface for rapid Sf development",
  "bin": {
    "sf": "bin/sf.js"
  },
  "keywords": [
    "sf",
    "cli"
  ],
  "author": "Lucy Zhu",
  "license": "MIT",
  "dependencies": {
    "@sf/cli-ui": "^3.0.3",
    "@vue/cli-shared-utils": "^4.5.7",
    "babel-runtime": "^6.9.2",
    "chalk": "^1.1.3",
    "clear": "^0.1.0",
    "commander": "^2.20.0",
    "debug": "^4.1.0",
    "deepmerge": "^4.2.2",
    "del": "^6.0.0",
    "figlet": "^1.5.0",
    "fs-extra": "^7.0.1",
    "handlebars": "^4.0.11",
    "inquirer": "^7.1.0",
    "lodash.clonedeep": "^4.5.0",
    "minimist": "^1.2.5",
    "ora": "^1.2.0",
    "recast": "^0.18.8",
    "resolve": "^1.17.0",
    "shortid": "^2.2.15",
    "slash": "^3.0.0"
  },
  "engines": {
    "node": ">=8.9"
  },
  "publishConfig": {
    "registry": "http://artifactory.sf-express.com/artifactory/api/npm/npm-sf-local/"
  }
}

bin表示我们发布该npm包后,执行sf init,相当于执行node bin/sf.js init命令

publishConfignpm包地址

发布npm包

执行npm publish发包成功后,在命令行终端执行npm install @sf/cli -g全局安装该包

在命令行终端输入sf,有以下提示,即表明安装成功

在这里插入图片描述

E盘内(或电脑的任何磁盘内)打开git bash,执行npm init即可在E盘下自动生成cloud-web项目(如果你没有更改项目名称的话),项目内的router.config.jsstore -> index.js均为根据handlebars模板自动生成

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值