交互式創(chuàng)建自定義vue模板

vue腳手架能生成vue的模板,把所有的都封裝好了。但是比如我們是一個專題項目或衡,對于公司來說肯定有些自己的配置,每次用腳手架生成后车遂,還要修改封断,那么我們能不能自己生成一個類似于腳手架,每次就生成封裝好的模板呢艰额。

在之前澄港,先介紹會用的的儲備知識

1. ora進(jìn)度轉(zhuǎn)輪

用于node的控制臺進(jìn)度美化
直接看栗子吧

const ora = require('ora');
const spinner = ora('進(jìn)入loading狀態(tài)...')
spinner.start()
setTimeout(() => {
  // spinner.stop()
  // spinner.succeed()
  // spinner.fail()
  spinner.fail('失敗了')
}, 2000)
image.png

當(dāng)我們創(chuàng)建模板過程中,更好的用戶體驗柄沮,顯示loading狀態(tài), stop直接結(jié)束。 成功和失敗 還可以傳入不同的文字祖搓。

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 片段連接到一起(使用平臺特定的分隔符作為定界符)藏姐,然后規(guī)范化生成的路徑。

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() 方法規(guī)范化給定的 path该贾,解析 '..' 和 '.' 片段羔杨。

當(dāng)找到多個連續(xù)的路徑段分隔字符時(例如 POSIX 上的 /、Windows 上的 \ 或 /)杨蛋,則它們將被替換為單個平臺特定的路徑段分隔符(POSIX 上的 /兜材、Windows 上的 \)。 尾部的分隔符會保留逞力。

如果 path 是零長度的字符串曙寡,則返回 '.',表示當(dāng)前工作目錄寇荧。

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 -- 文件操作相關(guān)工具庫
fs-extra模塊是系統(tǒng)fs模塊的擴(kuò)展举庶,提供了更多便利的 API,并繼承了fs模塊的 API

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

fs.copy(src, dest, [option],callback)
復(fù)制文件或目錄

  • src <String>請注意揩抡,如果src是目錄户侥,它將復(fù)制此目錄中的所有內(nèi)容,而不是整個目錄本身(請參閱問題#537)捅膘。
  • dest <String>請注意添祸,如果src是文件,dest則不能是目錄(請參閱問題#323)寻仗。
  • options <Object>
    • overwrite <boolean>:覆蓋現(xiàn)有文件或目錄刃泌,默認(rèn)為true請注意署尤,如果將其設(shè)置為false并且目標(biāo)存在耙替,則復(fù)制操作將以靜默方式失敗。使用該errorOnExist選項可以更改此行為曹体。
    • errorOnExist <boolean>:當(dāng)overwriteisfalse并且目的地存在時俗扇,引發(fā)錯誤。默認(rèn)值為false箕别。
    • dereference <boolean>:取消引用符號鏈接铜幽,默認(rèn)為false滞谢。
    • preserveTimestamps <boolean>:為true時,將設(shè)置對原始源文件的修改和訪問時間除抛。如果為false狮杨,則時間戳記行為取決于OS。默認(rèn)值為false到忽。
    • filter <Function>:用于過濾復(fù)制的文件/目錄的功能橄教。返回true以復(fù)制該項目,false忽略它喘漏。

emptyDir(dir[, callback])

確保目錄為空护蝶。如果目錄不為空,則刪除目錄內(nèi)容翩迈。如果該目錄不存在持灰,則會創(chuàng)建該目錄。目錄本身不會被刪除帽馋。

ensureFile(file[, callback])

確保文件存在搅方。如果請求創(chuàng)建的文件位于不存在的目錄中,則會創(chuàng)建這些目錄

ensureDir(dir [绽族,options] [姨涡,callback])

確保目錄存在。如果目錄結(jié)構(gòu)不存在吧慢,則會創(chuàng)建它涛漂。

pathExistsSync
別名fs.existsSync(),為與保持一致而創(chuàng)建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

它是非常容易去處理以下幾種事情的:

  • 提供錯誤回調(diào)
  • 詢問操作者問題
  • 獲取并解析用戶輸入
  • 檢測用戶回答是否合法
  • 管理多層級的提示
inquirer.prompt([ { 
  type: 'confirm', 
  name: 'test', 
  message: 'Are you handsome?', 
  default: true 
}]).then((answers) => { console.log('結(jié)果為:'); console.log(answers)})
image.png

image.png

問題的標(biāo)題和默認(rèn)結(jié)果值都是可以預(yù)設(shè)的攻泼。而在回答完成后會返回一個Promise對象火架,在其then方法中可以獲取到用戶輸入的所有回答。其中傳遞給prompt方法的參數(shù)為一個question問題數(shù)組忙菠,數(shù)組中的每個元素都是一個問題對象何鸡。其包含的屬性共有以下幾種:

  type: String, // 表示提問的類型,下文會單獨(dú)解釋 name: String, // 在最后獲取到的answers回答對象中牛欢,作為當(dāng)前這個問題的鍵
  message: String|Function, // 打印出來的問題標(biāo)題骡男,如果為函數(shù)的話 
  default: String|Number|Array|Function, // 用戶不輸入回答時,問題的默認(rèn)值傍睹「羰ⅲ或者使用函數(shù)來return一個默認(rèn)值犹菱。假如為函數(shù)時,函數(shù)第一個參數(shù)為當(dāng)前問題的輸入答案骚亿。 
  choices: Array|Function, // 給出一個選擇的列表已亥,假如是一個函數(shù)的話熊赖,第一個參數(shù)為當(dāng)前問題的輸入答案来屠。為數(shù)組時,數(shù)組的每個元素可以為基本類型中的值震鹉。 
  validate: Function, // 接受用戶輸入俱笛,并且當(dāng)值合法時,函數(shù)返回true传趾。當(dāng)函數(shù)返回false時迎膜,一個默認(rèn)的錯誤信息會被提供給用戶。 
  filter: Function, // 接受用戶輸入并且將值轉(zhuǎn)化后返回填充入最后的answers對象內(nèi)浆兰。 
  when: Function|Boolean, // 接受當(dāng)前用戶輸入的answers對象磕仅,并且通過返回true或者false來決定是否當(dāng)前的問題應(yīng)該去問。也可以是簡單類型的值簸呈。 
  pageSize: Number, // 改變渲染list,rawlist,expand或者checkbox時的行數(shù)的長度榕订。}

Prompt types —— 問題類型

  • List
    {type: 'list'}
    問題對象中必須有type,name,message,choices等屬性,同時蜕便,default選項必須為默認(rèn)值在choices數(shù)組中的位置索引(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類似,主要區(qū)別在于仿荆,是以一個checkbox的形式進(jìn)行選擇贰您。同時在choices數(shù)組中,帶有checked: true屬性的選項為默認(rèn)值赖歌。
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屬性庐冯,則屬性值應(yīng)為Boolean類型
inquirer.prompt([ { 
  type: 'confirm', 
  name: 'test', 
  message: '需要默認(rèn)選擇嗎孽亲?', 
  default: false,
}]).then((answers) => { console.log(answers)})
image.png

默認(rèn)是大寫字母

  • Input
    {type: 'input'}
    獲取用戶輸入字符串
inquirer.prompt([ { 
  type: 'input', 
  name: 'test', 
  message: '輸入手機(jī)號', 
  default: '110120',
}]).then((answers) => { console.log(answers)})
image.png
  • Password
    {type: 'password'}與input類型類似,只是用戶輸入在命令行中呈現(xiàn)為XXXX
inquirer.prompt([ { 
  type: 'password', 
  name: 'test', 
  message: '輸入密碼', 
  default: '110120',
}]).then((answers) => { console.log(answers)})
image.png
  • Editor
    {type: 'editor'}
    終端打開用戶默認(rèn)編輯器展父,如vim返劲,notepad玲昧。并將用戶輸入的文本傳回
inquirer.prompt([ { 
  type: 'editor', 
  name: 'test', 
  message: '輸入', 
}]).then((answers) => { console.log(answers)})
image.png

enter鍵進(jìn)入編輯頁面


image.png

7.handlebars

Handlebars 是一種簡單的 模板語言
類似于 vue 。 這里就是用來解析 默認(rèn) 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",
....

創(chuàng)建模板是替換為上面輸入的 值篮绿,生成新的 package.json文件

8.process.platform 屬性會返回標(biāo)識操作系統(tǒng)平臺(Node.js 進(jìn)程運(yùn)行其上的)的字符串孵延。

當(dāng)前可能的值有:

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

9.child_process 子進(jìn)程

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

  • command <string> 要運(yùn)行的命令。
  • args <string[]> 字符串參數(shù)的列表亲配。
  • options <Object>
    • cwd <string> 子進(jìn)程的當(dāng)前工作目錄尘应。
    • env <Object> 環(huán)境變量的鍵值對。 默認(rèn)值: process.env吼虎。
    • argv0 <string> 顯式地設(shè)置發(fā)送給子進(jìn)程的 argv[0] 的值犬钢。 如果沒有指定,則會被設(shè)置為 command 的值思灰。
    • stdio <Array> | <string> 子進(jìn)程的 stdio 配置玷犹,參見 options.stdio
    • detached <boolean> 使子進(jìn)程獨(dú)立于其父進(jìn)程運(yùn)行洒疚。 具體行為取決于平臺歹颓,參見 options.detached
    • uid <number> 設(shè)置進(jìn)程的用戶標(biāo)識油湖,參見 setuid(2)巍扛。
    • gid <number> 設(shè)置進(jìn)程的群組標(biāo)識,參見 setgid(2)肺魁。
    • serialization <string> 指定用于在進(jìn)程之間發(fā)送消息的序列化類型电湘。 可能的值為 'json''advanced'。 詳見高級序列化鹅经。 默認(rèn)值: 'json'寂呛。
    • shell <boolean> | <string> 如果為 true,則在 shell 中運(yùn)行 command瘾晃。 在 Unix 上使用 '/bin/sh'贷痪,在 Windows 上使用 process.env.ComSpec。 可以將不同的 shell 指定為字符串蹦误。 參見 shell 的要求默認(rèn)的 Windows shell劫拢。 默認(rèn)值: false(沒有 shell)。
    • windowsVerbatimArguments <boolean> 在 Windows 上不為參數(shù)加上引號或轉(zhuǎn)義强胰。 在 Unix 上會被忽略舱沧。 如果指定了 shell 并且是 CMD,則自動設(shè)為 true偶洋。 默認(rèn)值: false熟吏。
    • windowsHide <boolean> 隱藏子進(jìn)程的控制臺窗口(在 Windows 系統(tǒng)上通常會創(chuàng)建)。 默認(rèn)值: false
  • 返回: <ChildProcess>

child_process.spawn() 方法使用給定的 command 衍生新的進(jìn)程牵寺,并傳入 args 中的命令行參數(shù)悍引。 如果省略 args,則其默認(rèn)為空數(shù)組帽氓。

如果啟用了 shell 選項趣斤,則不要將未經(jīng)過處理的用戶輸入傳給此函數(shù)。 包含 shell 元字符的任何輸入都可用于觸發(fā)任意命令的執(zhí)行黎休。

栗子

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(`子進(jìn)程退出浓领,退出碼 ${code}`); // code 為 0 成功
});
image.png

ok 下面看代碼

create.js (#!/usr/bin/env node頂上要加上,這樣發(fā)布到npm后 才能按node執(zhí)行)

#!/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;
  // 創(chuàng)建 專題  還是  插件
  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');
  // 把 基礎(chǔ)模板 拷貝 到用戶 輸入后的新位置
  fs.copySync(templatePath, projectBasePath);

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

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

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

  // 如果開始交互式 有選擇 默認(rèn)安裝依賴玄渗,則安裝依賴
  answers.autoInitialize && yarnInstall(projectBasePath, answers.packageManager);
}

// 處理pc 和 wap的差異
function handlePlatform (projectBasePath) {
  // html模板引用文件
  const indexHtmlFileName = `${projectBasePath}/src/index.ejs`;
  // 讀取 頁面內(nèi)容  把 引入的 pc轉(zhuǎn)換為 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)}`));

  // 獲取創(chuàng)建后 新的package.json
  const fileName = `${projectBasePath}/package.json`;

  // 獲取里面 的內(nèi)容
  const content = fs.readFileSync(fileName).toString();

  // 用模板引擎 修改  {{}} 的內(nèi)容
  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() 當(dāng)前命令執(zhí)行的目錄
    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(`下載依賴成功~請執(zhí)行cd ${dir}`));
    return true;
  });
}

結(jié)構(gòu)

image.png

當(dāng)然藤树, template/base中就是 你們公司 需要定制化的一套模板了

** 補(bǔ)充 **
如果要要使用類似于 vue-cli的腳手架,用命令行就能安裝的話拓萌,
新建一個bin目錄岁钓,把create.js放進(jìn)去。當(dāng)然 create.js中有些路徑的話要自己修改一下了微王。
需要去package.json中 增加一個bin命令

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

然后本地測試的時候 屡限,要npm link 把my-create命令創(chuàng)建為全局命令,類似于cnpm 炕倘,yarn等等一樣钧大。 此時 命令行輸入 my-create 其實就是 進(jìn)入bin目錄運(yùn)行 node create.js命令了。
如果沒問題罩旋,就可以把自己的 這個模板項目發(fā)布到npm上了啊央。
以后自己下載到全局,運(yùn)行 my-create就可以 執(zhí)行按照模板了涨醋。
比如:我安裝好之后就是 npm i zxx-cli-test -g 安裝好之后直接運(yùn)行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',
        '養(yǎng)老家醫(yī)': 'bbbbb',
        '健康有約': 'cccc',
        '私人牙醫(yī)': 'ddddd',
    },
    production: {
        '通用散客im': 'eeee',
        '養(yǎng)老家醫(yī)': 'ffff',
        '健康有約': 'ggg',
        '私人牙醫(yī)': 'dds',
    }
}
const typeList = ['通用散客im', '養(yǎng)老家醫(yī)', '健康有約', '私人牙醫(yī)']

const routerObj = {
    '通用散客im': 'commonImRouter.js',
    '養(yǎng)老家醫(yī)': 'elderlyRouter.js',
    '健康有約': 'healthAppointRouter.js',
    '私人牙醫(yī)': '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: '小應(yīng)用類型',
        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] }

    // 獲取模板里面 的內(nèi)容
    const appConfigContent = fs.readFileSync(appConfig_base_path).toString();
    const envBetaContent = fs.readFileSync(envBeat_base_path).toString();
    const envProductionContent = fs.readFileSync(envProduction_base_path).toString();

    // 用模板引擎 修改  {{}} 的內(nèi)容
    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('配置修改完成, 請去手動打包'));
        // 自己要打包,所有不會恢復(fù)public/appConfig.json
    }
    // 如果開始交互式 有選擇 默認(rèn)安裝依賴貌亭,則安裝依賴
    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',
    }
    // 如果是生產(chǎn)環(huán)境
    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() 當(dāng)前命令執(zhí)行的目錄
    });

    // 這里僅僅是輸出 查看,其實可以去掉
    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(`安裝依賴打包成功,去平臺發(fā)布吧`));
        return true;
    });
}
image.png

start.js 啟動不同的服務(wù)

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',
        '養(yǎng)老家醫(yī)': 'bbbb',
        '健康有約': 'cccc',
        '私人牙醫(yī)': 'ddddd',
    }
}

const routerObj = {
    '通用散客im': 'commonImRouter.js',
    '養(yǎng)老家醫(yī)': 'elderlyRouter.js',
    '健康有約': 'healthAppointRouter.js',
    '私人牙醫(yī)': 'privateDentist.js'
}
const typeList = ['通用散客im', '養(yǎng)老家醫(yī)', '健康有約', '私人牙醫(yī)']

// 模板文件地址
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: '啟動小應(yīng)用類型',
        choices: typeList,
        default: 0,
    }
]).then((answers) => {
    updateTemplate(answers);
    updateRouter(answers);
})

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

    // 用模板引擎 修改  {{}} 的內(nèi)容
    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);
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拦键,一起剝皮案震驚了整個濱河市谣光,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芬为,老刑警劉巖萄金,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異媚朦,居然都是意外死亡氧敢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門询张,熙熙樓的掌柜王于貴愁眉苦臉地迎上來孙乖,“玉大人,你說我怎么就攤上這事份氧∥ò溃” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵蜗帜,是天一觀的道長恋拷。 經(jīng)常有香客問我,道長厅缺,這世上最難降的妖魔是什么蔬顾? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮湘捎,結(jié)果婚禮上诀豁,老公的妹妹穿的比我還像新娘。我一直安慰自己消痛,他們只是感情好且叁,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秩伞,像睡著了一般逞带。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纱新,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天展氓,我揣著相機(jī)與錄音,去河邊找鬼脸爱。 笑死遇汞,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播空入,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼络它,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了歪赢?” 一聲冷哼從身側(cè)響起化戳,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎埋凯,沒想到半個月后点楼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡白对,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年掠廓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甩恼。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡蟀瞧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出媳拴,到底是詐尸還是另有隱情黄橘,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布屈溉,位于F島的核電站,受9級特大地震影響抬探,放射性物質(zhì)發(fā)生泄漏子巾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一小压、第九天 我趴在偏房一處隱蔽的房頂上張望线梗。 院中可真熱鬧,春花似錦怠益、人聲如沸仪搔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烤咧。三九已至,卻和暖如春抢呆,著一層夾襖步出監(jiān)牢的瞬間煮嫌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工抱虐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昌阿,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像懦冰,于是被迫代替她去往敵國和親灶轰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容