交互式创建自定义vue模板

vue脚手架能生成vue的模板,把所有的都封装好了。但是比如我们是一个专题项目,对于公司来说肯定有些自己的配置,每次用脚手架生成后,还要修改,那么我们能不能自己生成一个类似于脚手架,每次就生成封装好的模板呢。

在之前,先介绍会用的的储备知识

1. ora进度转轮

用于node的控制台进度美化
直接看栗子吧

const ora = require('ora');
const spinner = ora('进入loading状态...')
spinner.start()
setTimeout(() => {
  // spinner.stop()
  // spinner.succeed()
  // spinner.fail()
  spinner.fail('失败了')
}, 2000)
image.png

当我们创建模板过程中,更好的用户体验,显示loading状态, stop直接结束。 成功和失败 还可以传入不同的文字。

2. chalk 控制台 以 彩色显示 提示信息

const chalk = require('chalk');
console.log(chalk.blue('Hello world!'))
console.log(chalk.blue('Hello') + 'World' + chalk.red('!')); 
console.log(chalk.blue.bgRed.bold('Hello world!'));
console.log(chalk.green('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
console.log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
image.png

3.log-symbols - 为各种日志级别提供着色的符号(控制台 左边的 ❌,✔️,⚠️,!等)

const symbols = require('log-symbols');
const chalk = require('chalk');
console.log(symbols.error, chalk.red('错误'));
console.log(symbols.warning, chalk.yellow('警告'));
console.log(symbols.info, chalk.blue('提示'));
console.log(symbols.success, chalk.green('成功'));
image.png

4.path 路径

项目中用到的

path.sep

提供平台特定的路径片段分隔符:

Windows 上是 \。
POSIX 上是 /。

console.log('foo/bar/baz'.split(path.sep)); //  [ 'foo', 'bar', 'baz' ]

path.resolve([...paths])

path.resolve() 方法会将路径或路径片段的序列解析为绝对路径

path.resolve('/目录1/目录2', './目录3');
// 返回: '/目录1/目录2/目录3'

path.resolve('/目录1/目录2', '/目录3/目录4/');
// 返回: '/目录3/目录4'

path.join([...paths])

path.join() 方法会将所有给定的 path 片段连接到一起(使用平台特定的分隔符作为定界符),然后规范化生成的路径。

path.join('/目录1', '目录2', '目录3/目录4', '目录5', '..');
// 返回: '/目录1/目录2/目录3/目录4'

path.resolve('/目录1/目录2', '/目录3/目录4/');
// 返回: '/目录1/目录2/目录3/目录4'

path.normalize(path)

path.normalize() 方法规范化给定的 path,解析 '..' 和 '.' 片段。

当找到多个连续的路径段分隔字符时(例如 POSIX 上的 /、Windows 上的 \ 或 /),则它们将被替换为单个平台特定的路径段分隔符(POSIX 上的 /、Windows 上的 \)。 尾部的分隔符会保留。

如果 path 是零长度的字符串,则返回 '.',表示当前工作目录。

console.log(path.normalize('')); // .
console.log(path.normalize('/a//b////c///////d////')); //  /a/b/c/d/
console.log(path.normalize('/a/b/c/../../d')); //  /a/d
image.png

5.fs-extra

fs-extra -- 文件操作相关工具库
fs-extra模块是系统fs模块的扩展,提供了更多便利的 API,并继承了fs模块的 API

const fs = require('fs-extra');

fs.copy(src, dest, [option],callback)
复制文件或目录

  • src <String>请注意,如果src是目录,它将复制此目录中的所有内容,而不是整个目录本身(请参阅问题#537)。
  • dest <String>请注意,如果src是文件,dest则不能是目录(请参阅问题#323)。
  • options <Object>
    • overwrite <boolean>:覆盖现有文件或目录,默认为true请注意,如果将其设置为false并且目标存在,则复制操作将以静默方式失败。使用该errorOnExist选项可以更改此行为。
    • errorOnExist <boolean>:当overwriteisfalse并且目的地存在时,引发错误。默认值为false
    • dereference <boolean>:取消引用符号链接,默认为false
    • preserveTimestamps <boolean>:为true时,将设置对原始源文件的修改和访问时间。如果为false,则时间戳记行为取决于OS。默认值为false
    • filter <Function>:用于过滤复制的文件/目录的功能。返回true以复制该项目,false忽略它。

emptyDir(dir[, callback])

确保目录为空。如果目录不为空,则删除目录内容。如果该目录不存在,则会创建该目录。目录本身不会被删除。

ensureFile(file[, callback])

确保文件存在。如果请求创建的文件位于不存在的目录中,则会创建这些目录

ensureDir(dir [,options] [,callback])

确保目录存在。如果目录结构不存在,则会创建它。

pathExistsSync
别名fs.existsSync(),为与保持一致而创建pathExists()

如果路径存在,则返回 true,否则返回 false。

readJsonSync(file[, options])

读取JSON文件,然后将其解析为一个对象。

const fs = require('fs-extra')

const packageObj = fs.readJsonSync('./package.json')
console.log(packageObj.version) // => 2.0.0

6.inquirer NodeJs交互式命令行工具Inquirer.js

它是非常容易去处理以下几种事情的:

  • 提供错误回调
  • 询问操作者问题
  • 获取并解析用户输入
  • 检测用户回答是否合法
  • 管理多层级的提示
inquirer.prompt([ { 
  type: 'confirm', 
  name: 'test', 
  message: 'Are you handsome?', 
  default: true 
}]).then((answers) => { console.log('结果为:'); console.log(answers)})
image.png

image.png

问题的标题和默认结果值都是可以预设的。而在回答完成后会返回一个Promise对象,在其then方法中可以获取到用户输入的所有回答。其中传递给prompt方法的参数为一个question问题数组,数组中的每个元素都是一个问题对象。其包含的属性共有以下几种:

  type: String, // 表示提问的类型,下文会单独解释 name: String, // 在最后获取到的answers回答对象中,作为当前这个问题的键
  message: String|Function, // 打印出来的问题标题,如果为函数的话 
  default: String|Number|Array|Function, // 用户不输入回答时,问题的默认值。或者使用函数来return一个默认值。假如为函数时,函数第一个参数为当前问题的输入答案。 
  choices: Array|Function, // 给出一个选择的列表,假如是一个函数的话,第一个参数为当前问题的输入答案。为数组时,数组的每个元素可以为基本类型中的值。 
  validate: Function, // 接受用户输入,并且当值合法时,函数返回true。当函数返回false时,一个默认的错误信息会被提供给用户。 
  filter: Function, // 接受用户输入并且将值转化后返回填充入最后的answers对象内。 
  when: Function|Boolean, // 接受当前用户输入的answers对象,并且通过返回true或者false来决定是否当前的问题应该去问。也可以是简单类型的值。 
  pageSize: Number, // 改变渲染list,rawlist,expand或者checkbox时的行数的长度。}

Prompt types —— 问题类型

  • List
    {type: 'list'}
    问题对象中必须有type,name,message,choices等属性,同时,default选项必须为默认值在choices数组中的位置索引(Boolean)
inquirer.prompt([ { 
  type: 'list', 
  name: 'test', 
  choices: ['a','b','c'],
  message: '请选择?', 
  default: 1 
}]).then((answers) => { console.log(answers)})
image.png

加上pageSize

inquirer.prompt([ { 
  type: 'list', 
  name: 'test', 
  choices: ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','b','c'],
  message: '请选择?', 
  default: 1 ,
  pageSize: 2
}]).then((answers) => { console.log(answers)})
image.png

此时就滞后两个选择了

  • Raw list
    {type: 'rawlist'}
    与List类型类似,不同在于,list打印出来为无序列表,而rawlist打印为有序列表
inquirer.prompt([ { 
  type: 'rawlist', 
  name: 'test', 
  choices: ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','b','c'],
  message: '请选择?', 
  default: 1 ,
}]).then((answers) => { console.log(answers)})
image.png
  • Expand
    {type: 'expand'}
    同样是生成列表,但是在choices属性中需要增加一个属性:key,这个属性用于快速选择问题的答案。类似于alias或者shorthand的东西。同时这个属性值必须为一个小写字母
inquirer.prompt([ { 
  type: 'expand', 
  name: 'test', 
  choices: [
    {
      key: 'y',
      name: 'Overwrite',
      value: 'overwrite',
    },
    {
      key: 'a',
      name: 'Overwrite this one and all next',
      value: 'overwrite_all',
    },
    {
      key: 'd',
      name: 'Show diff',
      value: 'diff',
    },
    {
      key: 'x',
      name: 'Abort',
      value: 'abort',
    },
  ],
  message: '请选择?', 
}]).then((answers) => { console.log(answers)})
image.png

直接输入key 值就可以选择了

  • Checkbox
    {type: 'checkbox'}
    其余诸项与list类似,主要区别在于,是以一个checkbox的形式进行选择。同时在choices数组中,带有checked: true属性的选项为默认值。
inquirer.prompt([ { 
  type: 'checkbox', 
  name: 'test', 
  choices:['a','b','c','d'],
  message: '请选择?', 
  default: ['b','d'] ,
}]).then((answers) => { console.log(answers)})
image.png
  • Confirm
    {type: 'confirm'}
    提问,回答为Y/N。若有default属性,则属性值应为Boolean类型
inquirer.prompt([ { 
  type: 'confirm', 
  name: 'test', 
  message: '需要默认选择吗?', 
  default: false,
}]).then((answers) => { console.log(answers)})
image.png

默认是大写字母

  • Input
    {type: 'input'}
    获取用户输入字符串
inquirer.prompt([ { 
  type: 'input', 
  name: 'test', 
  message: '输入手机号', 
  default: '110120',
}]).then((answers) => { console.log(answers)})
image.png
  • Password
    {type: 'password'}与input类型类似,只是用户输入在命令行中呈现为XXXX
inquirer.prompt([ { 
  type: 'password', 
  name: 'test', 
  message: '输入密码', 
  default: '110120',
}]).then((answers) => { console.log(answers)})
image.png
  • Editor
    {type: 'editor'}
    终端打开用户默认编辑器,如vim,notepad。并将用户输入的文本传回
inquirer.prompt([ { 
  type: 'editor', 
  name: 'test', 
  message: '输入', 
}]).then((answers) => { console.log(answers)})
image.png

enter键进入编辑页面


image.png

7.handlebars

Handlebars 是一种简单的 模板语言
类似于 vue 。 这里就是用来解析 默认 package.json中的值比如

{
  "name": "{{name}}",
  "year": "{{year}}",
  "outputFolder": "{{outputFolder}}",
  "outputFolderNeibu": "neibu-dist",
  "version": "1.0.0",
  "ztType": "{{ztType}}",
  "author": "{{author}}",
  "platform": "{{platform}}",
  "description": "",
  "main": "index.js",
....

创建模板是替换为上面输入的 值,生成新的 package.json文件

8.process.platform 属性会返回标识操作系统平台(Node.js 进程运行其上的)的字符串。

当前可能的值有:

  • 'aix'
  • 'darwin'
  • 'freebsd'
  • 'linux'
  • 'openbsd'
  • 'sunos'
  • 'win32'
    console.log(此平台是 ${process.platform});

9.child_process 子进程

child_process.spawn(command[, args][, options])

  • command <string> 要运行的命令。
  • args <string[]> 字符串参数的列表。
  • options <Object>
    • cwd <string> 子进程的当前工作目录。
    • env <Object> 环境变量的键值对。 默认值: process.env
    • argv0 <string> 显式地设置发送给子进程的 argv[0] 的值。 如果没有指定,则会被设置为 command 的值。
    • stdio <Array> | <string> 子进程的 stdio 配置,参见 options.stdio
    • detached <boolean> 使子进程独立于其父进程运行。 具体行为取决于平台,参见 options.detached
    • uid <number> 设置进程的用户标识,参见 setuid(2)
    • gid <number> 设置进程的群组标识,参见 setgid(2)
    • serialization <string> 指定用于在进程之间发送消息的序列化类型。 可能的值为 'json''advanced'。 详见高级序列化默认值: 'json'
    • shell <boolean> | <string> 如果为 true,则在 shell 中运行 command。 在 Unix 上使用 '/bin/sh',在 Windows 上使用 process.env.ComSpec。 可以将不同的 shell 指定为字符串。 参见 shell 的要求默认的 Windows shell默认值: false(没有 shell)。
    • windowsVerbatimArguments <boolean> 在 Windows 上不为参数加上引号或转义。 在 Unix 上会被忽略。 如果指定了 shell 并且是 CMD,则自动设为 true默认值: false
    • windowsHide <boolean> 隐藏子进程的控制台窗口(在 Windows 系统上通常会创建)。 默认值: false
  • 返回: <ChildProcess>

child_process.spawn() 方法使用给定的 command 衍生新的进程,并传入 args 中的命令行参数。 如果省略 args,则其默认为空数组。

如果启用了 shell 选项,则不要将未经过处理的用户输入传给此函数。 包含 shell 元字符的任何输入都可用于触发任意命令的执行。

栗子

const { spawn } = require('child_process');
const ls = spawn('node', ['-v']);

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

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

ls.on('close', (code) => {
  console.log(`子进程退出,退出码 ${code}`); // code 为 0 成功
});
image.png

ok 下面看代码

create.js (#!/usr/bin/env node顶上要加上,这样发布到npm后 才能按node执行)

#!/usr/bin/env node

const fs = require('fs-extra');
const inquirer = require('inquirer');
const handlebars = require('handlebars');
const ora = require('ora');
const chalk = require('chalk');
const symbols = require('log-symbols');
const { spawn } = require('child_process');
const path = require('path');

const now = new Date();

// 交互式获取用户 的需求
inquirer.prompt([
  {
    type: 'input',
    name: 'year',
    message: '请输入项目年份,比如2019(插件请输入"plugins")',
    default: now.getFullYear(),
  }, {
    type: 'input',
    name: 'name',
    message: '请输入项目名称,比如0702projectname(注意不要使用过长的文件名,CMS会报错)',
    default: `${String(now.getMonth() + 1).padStart(2, '00')}${now.getDate()}-projectName`,
  }, {
    type: 'list',
    name: 'platform',
    message: 'PC端项目还是wap端项目?',
    choices: ['pc', 'wap'],
    default: 0,
  }, {
    type: 'input',
    name: 'author',
    message: '请输入作者名称',
    default: 'author_name',
  }, {
    type: 'input',
    name: 'ztType',
    message: '请输入专题类型(kaoyan):',
    default: 'kaoyan',
  }, {
    type: 'confirm',
    name: 'autoInitialize',
    message: '是否自动安装依赖?',
  }, {
    type: 'list',
    name: 'packageManager',
    message: '使用哪种包管理工具?',
    choices: ['npm', 'yarn'],
    default: 0,
  },
]).then((answers) => {
  const { year, name } = answers;
  // 创建 专题  还是  插件
  const projectBasePath = year == 'plugins' ? 'project/plugins' : `project/zt/${year}/${name}`;
  
  answers.outputFolder = 'pro-dist';

  // 项目已存在则退出
  if (fs.existsSync(projectBasePath)) {
    return console.log(symbols.error, chalk.red('项目已存在'));
  }

  // 拷贝模板
  downloadTemplate(projectBasePath, answers);
  return true;
});

// 拷贝模板
function downloadTemplate (projectBasePath, answers) {
  let templatePath = path.join(__dirname, '../', 'template', 'base');
  // 把 基础模板 拷贝 到用户 输入后的新位置
  fs.copySync(templatePath, projectBasePath);

  // 处理wap的差异
  if (answers.platform === 'wap') {
    handlePlatform(projectBasePath);
  }

  // 处理package.json文件
  updatePackageJson(answers, projectBasePath);

  console.log(symbols.success, chalk.green('项目初始化成功'));

  // 如果开始交互式 有选择 默认安装依赖,则安装依赖
  answers.autoInitialize && yarnInstall(projectBasePath, answers.packageManager);
}

// 处理pc 和 wap的差异
function handlePlatform (projectBasePath) {
  // html模板引用文件
  const indexHtmlFileName = `${projectBasePath}/src/index.ejs`;
  // 读取 页面内容  把 引入的 pc转换为 wap 的(wap会做rem处理)
  const htmlFile = fs.readFileSync(indexHtmlFileName)
    .toString()
    .replace('./ejstpls/public-pc.ejs', './ejstpls/public-wap.ejs');

  // 处理之后把 模板 页面替换
  fs.writeFileSync(indexHtmlFileName, htmlFile);
}

// 合并选项到package.json
function updatePackageJson (answers, projectBasePath) {
  const meta = { ...answers };
  console.log(symbols.success, chalk.green(`你的配置是:\n${JSON.stringify(meta)}`));

  // 获取创建后 新的package.json
  const fileName = `${projectBasePath}/package.json`;

  // 获取里面 的内容
  const content = fs.readFileSync(fileName).toString();

  // 用模板引擎 修改  {{}} 的内容
  const result = handlebars.compile(content)(meta);

  // 重新 package.json
  fs.writeFileSync(fileName, result);
}

// 安装依赖
function yarnInstall (dir, pm) {
  // 提示用户安装中
  const spinner = ora('正在安装依赖……').start();
  // pm 是用选择的 npm 或者yarn  开始安装package.json 中的依赖
  const sp = spawn(pm, ['install'], {
    cwd: path.resolve(process.cwd(), `./${dir}`), // process.cwd() 当前命令执行的目录
    shell: /^win/.test(process.platform),
  });

  // 这里仅仅是输出 查看,其实可以去掉
  sp.on('message', (msg) => {
    console.log(msg.toString());
  });
  sp.stdout.on('data', (data) => {
    console.log(data.toString());
  });

  sp.on('close', (code) => {
    // 安装依赖失败
    if (code !== 0) {
      spinner.fail();
      return console.log(chalk.red(`安装失败,退出码 ${code}`));
    }
    // 安装依赖成功
    spinner.succeed();
    console.log(chalk.green(`下载依赖成功~请执行cd ${dir}`));
    return true;
  });
}

结构

image.png

当然, template/base中就是 你们公司 需要定制化的一套模板了

** 补充 **
如果要要使用类似于 vue-cli的脚手架,用命令行就能安装的话,
新建一个bin目录,把create.js放进去。当然 create.js中有些路径的话要自己修改一下了。
需要去package.json中 增加一个bin命令

    "bin": {
        "my-create": "./bin/create.js"
    },

然后本地测试的时候 ,要npm link 把my-create命令创建为全局命令,类似于cnpm ,yarn等等一样。 此时 命令行输入 my-create 其实就是 进入bin目录运行 node create.js命令了。
如果没问题,就可以把自己的 这个模板项目发布到npm上了。
以后自己下载到全局,运行 my-create就可以 执行按照模板了。
比如:我安装好之后就是 npm i zxx-cli-test -g 安装好之后直接运行my-create 就可以安装我的模板了;

其他

同一个套代码,不同的启动方式,配置不同的项目

const inquirer = require('inquirer'); // 8.0.0
const fs = require('fs-extra');
const path = require('path');
const { spawn } = require('child_process');
const handlebars = require('handlebars');
const ora = require('ora');  // 5.4.0
const chalk = require('chalk'); // 4.1.2
const symbols = require('log-symbols'); // 4.1.0

const peizhiObj = {
    test: {
        '通用散客im': 'aaaaa',
        '养老家医': 'bbbbb',
        '健康有约': 'cccc',
        '私人牙医': 'ddddd',
    },
    production: {
        '通用散客im': 'eeee',
        '养老家医': 'ffff',
        '健康有约': 'ggg',
        '私人牙医': 'dds',
    }
}
const typeList = ['通用散客im', '养老家医', '健康有约', '私人牙医']

const routerObj = {
    '通用散客im': 'commonImRouter.js',
    '养老家医': 'elderlyRouter.js',
    '健康有约': 'healthAppointRouter.js',
    '私人牙医': 'privateDentist.js'
}

// 模板文件地址
const appConfig_base_path = path.resolve(process.cwd(), './build/appConfig.json')
const envBeat_base_path = path.resolve(process.cwd(), './build/.env.beta')
const envProduction_base_path = path.resolve(process.cwd(), './build/.env.production')

// 需要替换文件地址
const appConfig_dest_path = path.resolve(process.cwd(), './public/appConfig.json')
const envBeat_dest_path = path.resolve(process.cwd(), './.env.beta')
const envProduction_dest_path = path.resolve(process.cwd(), './.env.production')

// 临时保存的文件地址
const appConfig_tem_path = path.resolve(process.cwd(), './build/tem/appConfig.json')
const envBeat_tem_path = path.resolve(process.cwd(), './build/tem/.env.beta')
const envProduction_tem_path = path.resolve(process.cwd(), './build/tem/.env.production')


// 交互式获取用户 的需求
inquirer.prompt([
    {
        type: 'list',
        name: 'env',
        message: '打测试包还是线上包',
        choices: Object.keys(peizhiObj), // ['test', 'production']
        default: 0,
    },
    {
        type: 'list',
        name: 'type',
        message: '小应用类型',
        choices: typeList,
        default: 0,
    }, {
        type: 'confirm',
        name: 'autoInitialize',
        message: '是否自动安装依赖?',
    }
]).then((answers) => {
    // 临时保存以前的 appid
    fs.copySync(appConfig_dest_path, appConfig_tem_path);
    fs.copySync(envBeat_dest_path, envBeat_tem_path);
    fs.copySync(envProduction_dest_path, envProduction_tem_path);
    updateTemplate(answers)
    updateRouter(answers);
    return true;
})

function updateTemplate (answers) {
    const { env, type, autoInitialize } = answers
    const data = { appid: peizhiObj[env][type] }

    // 获取模板里面 的内容
    const appConfigContent = fs.readFileSync(appConfig_base_path).toString();
    const envBetaContent = fs.readFileSync(envBeat_base_path).toString();
    const envProductionContent = fs.readFileSync(envProduction_base_path).toString();

    // 用模板引擎 修改  {{}} 的内容
    const result_appConfigContent = handlebars.compile(appConfigContent)(data);
    const result_envBetaContent = handlebars.compile(envBetaContent)(data);
    const result_envProductionContent = handlebars.compile(envProductionContent)(data);

    fs.writeFileSync(appConfig_dest_path, result_appConfigContent);
    fs.writeFileSync(envBeat_dest_path, result_envBetaContent);
    fs.writeFileSync(envProduction_dest_path, result_envProductionContent);
    if (!autoInitialize) {
        fs.unlink(appConfig_tem_path)
        console.log(symbols.success, chalk.green('配置修改完成, 请去手动打包'));
        // 自己要打包,所有不会恢复public/appConfig.json
    }
    // 如果开始交互式 有选择 默认安装依赖,则安装依赖
    autoInitialize && yarnInstall(env);
}
// 更新路由
function updateRouter (answers) {
    const { type } = answers;
    // 配置的router
    const nowRouterPath = path.resolve(process.cwd(), './src/router/routerConfig/' + routerObj[type]);
    // 用的路由地址
    const useRouterPath = path.resolve(process.cwd(), './src/router/router.js');
    const nowRouter = fs.readFileSync(nowRouterPath).toString();

    // 先删除
    fs.unlinkSync(useRouterPath);

    fs.writeFileSync(useRouterPath, nowRouter);
}

function yarnInstall (env) {
    // 提示用户安装中
    const spinner = ora('正在安装依赖……').start();
    // pm 是用选择的 npm  开始安装package.json 中的依赖

    const binObj = {
        test: 'beta',
        production: 'pre-build',
    }
    // 如果是生产环境
    if (env === 'production') {
        const packagePath = path.resolve(process.cwd(), './package.json')
        const res = fs.readFileSync(packagePath, 'utf8');
        const data = JSON.parse(res);
        //   获取健康依赖包
        let jkChronic = data.dependencies['jk-chronic']
        let jkFollowupplan = data.dependencies['jk-followupplan']
        let jkHealthRecords = data.dependencies['jk-health-records']
        let jkQuestionaire = data.dependencies['jk-questionaire']
        let jkVideocomponent = data.dependencies['jk-videocomponent']
        // 去除 ^ ~ 等符号 只留下具体版本号
        jkChronic = jkChronic.replace(/[^0-9|\.]/g, '')
        jkFollowupplan = jkFollowupplan.replace(/[^0-9|\.]/g, '')
        jkHealthRecords = jkHealthRecords.replace(/[^0-9|\.]/g, '')
        jkQuestionaire = jkQuestionaire.replace(/[^0-9|\.]/g, '')
        jkVideocomponent = jkVideocomponent.replace(/[^0-9|\.]/g, '')
        // 修改package.json中的 更新指定 健康包版本
        data.scripts.updateWithVersion = `npm install jk-chronic@${jkChronic} jk-followupplan@${jkFollowupplan} jk-questionaire@${jkQuestionaire} jk-health-records@${jkHealthRecords} jk-videocomponent@${jkVideocomponent}  --legacy-peer-deps`

        // 重新package.json (自己手动去格式化一下该文件,样式不好看)
        fs.writeFileSync(packagePath, JSON.stringify(data, "", "\t"));
    }

    const sp = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', binObj[env]], {
        cwd: path.resolve(process.cwd(), './'), // process.cwd() 当前命令执行的目录
    });

    // 这里仅仅是输出 查看,其实可以去掉
    sp.on('message', (msg) => {
        console.log(msg.toString());
    });
    sp.stdout.on('data', (data) => {
        console.log(data.toString());
    });

    sp.on('close', (code) => {
        // 安装依赖失败
        if (code !== 0) {
            spinner.fail();
            return console.log(symbols.error, chalk.green(`安装失败,退出码 ${code}`));
        }
        // 安装依赖成功
        spinner.succeed();
        // 还原appid
        fs.copySync(appConfig_tem_path, appConfig_dest_path)
        fs.copySync(envBeat_tem_path, envBeat_dest_path)
        fs.copySync(envProduction_tem_path, envProduction_dest_path)
        // 删除临时保存的appid
        fs.unlink(appConfig_tem_path)
        fs.unlink(envBeat_tem_path)
        fs.unlink(envProduction_tem_path)
        console.log(symbols.success, chalk.green(`安裝依赖打包成功,去平台发布吧`));
        return true;
    });
}
image.png

start.js 启动不同的服务

const inquirer = require('inquirer'); // 8.0.0
const fs = require('fs-extra');
const path = require('path');
const handlebars = require('handlebars');

const peizhiObj = {
    test: {
        '通用散客im': 'aaa',
        '养老家医': 'bbbb',
        '健康有约': 'cccc',
        '私人牙医': 'ddddd',
    }
}

const routerObj = {
    '通用散客im': 'commonImRouter.js',
    '养老家医': 'elderlyRouter.js',
    '健康有约': 'healthAppointRouter.js',
    '私人牙医': 'privateDentist.js'
}
const typeList = ['通用散客im', '养老家医', '健康有约', '私人牙医']

// 模板文件地址
const envDevelopment_base_path = path.resolve(process.cwd(), './build/.env.development')

// 需要替换文件地址
const envDevelopment_dest_path = path.resolve(process.cwd(), './.env.development')

// 交互式获取用户 的需求
inquirer.prompt([
    {
        type: 'list',
        name: 'type',
        message: '启动小应用类型',
        choices: typeList,
        default: 0,
    }
]).then((answers) => {
    updateTemplate(answers);
    updateRouter(answers);
})

function updateTemplate(answers) {
    const { type } = answers
    const data = { appid: peizhiObj['test'][type] }
    // 获取模板里面 的内容
    const envDevelopmentContent = fs.readFileSync(envDevelopment_base_path).toString();

    // 用模板引擎 修改  {{}} 的内容
    const result_envDevelopmentContent = handlebars.compile(envDevelopmentContent)(data);

    fs.writeFileSync(envDevelopment_dest_path, result_envDevelopmentContent);
}

function updateRouter(answers) {
    const { type } = answers;
    // 配置的router
    const nowRouterPath = path.resolve(process.cwd(), './src/router/routerConfig/' + routerObj[type]);
    // 用的路由地址
    const useRouterPath = path.resolve(process.cwd(), './src/router/router.js');
    const nowRouter = fs.readFileSync(nowRouterPath).toString();

    // 先删除
    fs.unlinkSync(useRouterPath);

    fs.writeFileSync(useRouterPath, nowRouter);
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容