脚手架开发

... 2022-3-11 About 6 min

# 脚手架开发

开发过一些项目之后,有了自己的开发习惯,以及代码习惯,但是非常讨厌每次新做项目的时候,还需要copy之前的配置,甚至整个项目copy然后再删除,这个过程太恶心了,所以开发了一个脚手架

# 技术思维

脚手架架构图.png

做事情我喜欢的方式是从结果来看,至于怎么实现只是手段,我需要的东西很明确:

  1. 可以生成一个项目,里面包括我所有的配置和开发习惯,并可以快速进入开发
  2. 配置好cdn
  3. 能快速配置好一个文件的结构

对于脚手架本身:

  1. 所有的模板单独维护,不依赖脚手架的发版
  2. 邮件通知,组内人新创建模板项目需要通过邮件让相关人员知道基本信息

# 代码建设

# 入口文件

#!/usr/bin/env node
// NOTE: 说明node的执行环境

let fs = require('fs')
let path = require('path')
let program = require('commander') // NOTE: 获取命令信息
const packageConfig = require('../package.json')

program.command('init <type>').action(function (type) {
  handleType(type)
})

function handleType (type) {
  let isExsit = fs.existsSync(path.resolve(__dirname, `./${type}/index.js`))
  if (!isExsit) {
    require('./help.js')()
  } else {
    require(`./${type}/index.js`)()
  }
}

program.command('usage').action(function () {
  require('./help.js')()
})

program.version(packageConfig.version).option('--no-sauce', 'Remove sauce').parse(process.argv)
if (!program.args.length) {
  program.help()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# help.js

module.exports = async function (params) {
  console.log(
    '\n nic init [type]\n' +
    '\n h5项目: nic init h5 \n' +
    '\n web项目: nic init web \n' +
    '\n 微前端项目: nic init micro \n' +
    '\n node项目: nic init node \n' +
    '\n 小程序: nic init mp \n'
  )
}
1
2
3
4
5
6
7
8
9
10

# utils.js

const chalk = require('chalk')
const nodemailer = require('nodemailer')
const ejs = require('ejs')
const path = require('path')
const fse = require('fs-extra')
const fs = require('fs')
const download = require('download-git-repo')
const template = require('art-template')
const { exec } = require('child_process')
const username = require('username')
template.defaults.rules.shift() // 移除ejs支持

/**
 * 关于颜色的方法
 */
const loading = text => chalk.bgGreen(chalk.white(text))

const success = text => chalk.green(text)

const warning = text => chalk.yellow(text)

const info = text => chalk.blue(text)

const c_loading = text => console.log(chalk.bgGreen(chalk.white(text)))

const c_success = text => console.log(chalk.green(text))

const c_warning = text => console.log(chalk.yellow(text))

const c_info = text => console.log(chalk.blue(text))

/**
 * 邮件发送
 */
const Mail = '1066788870@qq.com'
const mailTemplate = ejs.compile(
    fs.readFileSync(path.resolve(__dirname, 'mail.ejs'), 'utf8')
)
let transporter = nodemailer.createTransport({
    service: 'qq',
    port: 465,
    secureConnection: true,
    auth: {
        user: '',
        pass: '' // NOTE: 邮箱的相关配置
    }
})
const mail = config => {
const html = mailTemplate(
    Object.assign(
        {
            title: `welcome ${config.mails} into nic-cli`
        },
        config
    )
)
return new Promise((resolve,reject) => {
        nodemailer.createTestAccount(err => {
            if (err) {
                reject(err)
            }
            let mailOptions = {
                from: '1066788870@qq.com',
                to: `${Mail},${config.mails}`,
                subject: 'git配置信息',
                html: html
            }
            transporter.sendMail(mailOptions, error => {
                if (error) {
                    reject(error)
                }
                resolve('Message sent successly')
            })
        })
    })
}

/**
 *创建项目目录
 *
 * @param {*} projectName
 */
async function createProject (projectName, projectPath) {
    projectPath = projectPath || process.env.PWD
    const dir = await fse.ensureDir(projectPath + '/' + projectName)
    return dir
}
  
/**
 *检查是否能创建DIR
 *
 * @param {*} projectName 项目名称
 * @param {*} projectPath 项目地址
 * @returns
 */
const ensureDir = (projectName, projectPath) => {
    projectPath = projectPath || process.env.PWD
    const dir = fs.existsSync(projectPath + '/' + projectName)
    return dir
} 

/**
 *
 * 复制远程模板
 * @param {*} type  h5-vue  h5-react web-vue web-react node applet
 * @param {*} target
 * @returns
 */
async function copyTemplate (type, target) {
    const error = await copyGitTemplace(type, target)
    return error
}


/**
 * 复制远程模板
 * type: 在之前已经拼成了和远程的name相同了
 */
function copyGitTemplace (type, target) {
    info('\n 开始拉取远程模板\n')
    let url = 'https://github.com:0227vera/' + type + '#master' // 模板的git地址
    if (type === 'standard') {
        url = 'https://gitee.com:panjiachen/vue-element-admin#master'
    }
    return new Promise((resolve, reject) => {
        download(url, target, { clone: true }, function (err) {
            if (!err) {
                info('\n 拉取远程模板完成\n')
            } else {
                info('\n 拉取远程模板失败,查看是否拥有模板权限,或联系脚手架管理人员\n')
            }
            err ? reject(err) : resolve(true)
        })
    })
}

/**
 * 复制一个文件或者文件夹到另一个目录
 * @param {string} src 文件或者文件夹路径
 * @param {string} dest 文件夹路径
 */
async function copy (src, dir) {
    const err = await fse.copy(src, dir)
    return err
}


/**
 * 将模板中的变量替换成指定值
 */
async function rewriteTemplate (data, files) {
    await files.forEach((file) => {
        let newFile = template(file, data)
        fs.writeFile(file, newFile, function () {})
    })
}

/**
 * 执行命令
 */
async function execCmd (cmd) {
    let res = await exec(cmd, async (error, stdout, stderr) => {
        if (error) return error
        return stderr
    })
    return res
}

/**
 * 获取当前项目用户
 */
async function getUsername () {
    const name = await username()
    return name
}

module.exports = {
    color: {
        loading,
        success,
        warning,
        info,
        c_loading,
        c_success,
        c_warning,
        c_info,
    },
    mail,
    rewriteTemplate,
    createProject,
    copyTemplate,
    getUsername,
    ensureDir,
    execCmd,
    copy
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196

# 拿一个H5的模板来说

h5/index.js

/**
 * 创建H5模板
 * inquirer: 交互使用的工具
 * ora: 交互loading
 */
let inquirer = require('inquirer')
const ora = require('ora')
const path = require('path')

const {
  rewriteTemplate,
  createProject,
  copyTemplate,
  getUsername,
  ensureDir,
  execCmd,
  color,
  mail
} = require('../utils')

let base = [
  {
    type: 'input',
    name: 'projectName',
    message: color.info('请输入项目名称'),
    validate: function(value) {
      if (ensureDir(value)) {
        return '此目录已经存在,请重新输入'
      }
      return true
    },
    default: function() {
      return 'h5-template'
    }
  },
  {
    type: 'input',
    name: 'description',
    message: color.info('请输入项目描述'),
    default: function() {
      return 'h5开发模版'
    }
  },
  {
    type: 'input',
    name: 'mails',
    message: color.info('请输入开发人员邮箱(请输入真实有效的邮箱),如果是多人使用英文 "," 分割'),
    default: function() {
      return 'xxx@xxxx.com'
    }
  },
  {
    type: 'list',
    name: 'langType',
    message: color.info('请选择使用vue/react编写'),
    choices: [{ name: 'vue', value: 1 }, { name: 'react', value: 2 }],
    default: 0 // 默认是下标为0的选项
  },
  {
    type: 'input',
    name: 'projectContext',
    message: color.info('请输入项目上下文(用于项目中的代理)'),
    default: function() {
      return '/context'
    }
  },
  {
    type: 'input',
    name: 'projectProxyUrl',
    message: color.info('请输入项目需要代理到的服务器(api文档地址))'),
    default: function() {
      return 'http://api.xxx.com/mock/xxx/'
    }
  },
  {
    type: 'confirm',
    name: 'needInitGit',
    message: color.warning('项目初始化之后是否直接通过命令上传第一次git'),
    default: function() {
      return true
    }
  },
  {
    type: 'input',
    name: 'gitAddress',
    message: color.warning('请输入项目git地址,用于项目init,确保真实有效'),
    default: function() {
      return 'https://github.com/'
    },
    when(answer) {
      return answer.needInitGit
    }
  },
  {
    type: 'confirm',
    name: 'isAddCI',
    message: color.warning('是否现在填写CI信息?'),
    default: true,
    when(answer) {
      return answer.langType === 0 // NOTE: 暂时隐藏ci的功能,使用misc的功能
    }
  },
  {
    type: 'input',
    name: 'productionAddress',
    message: color.warning(
      '请输入项目打包之后的地址(前缀会自动加上https://misc.xxx.com/app/)'
    ),
    default: function() {
      return 'bi/xxx'
    }
  }
]

let vueAddCi = [
  {
    type: 'input',
    name: 'parkName',
    message: color.warning('请输入构建时输出补丁的压缩包名称(tar.gz)'),
    default: function() {
      return 'app.front.tar.gz'
    }
  },
  {
    type: 'input',
    name: 'host',
    message: color.warning('请输入SFTP服务器地址'),
    default: function() {
      return '127.0.0.1'
    }
  },
  {
    type: 'input',
    name: 'port',
    message: color.warning('请输入SFTP服务器端口'),
    default: function() {
      return '80'
    }
  },
  {
    type: 'input',
    name: 'username',
    message: color.warning('请输入SFTP用户名'),
    default: function() {
      return 'xxx'
    }
  },
  {
    type: 'input',
    name: 'password',
    message: color.warning('请输入SFTP密码'),
    default: function() {
      return 'xxx'
    }
  },
  {
    type: 'input',
    name: 'sftpProjectPath',
    message: color.warning('请输入SFTP上传目录'),
    default: function() {
      return '/xxx/xxx/xxx_demo_V1.0.0_000_20200331_name_前端全部补丁'
    }
  }
]

module.exports = async function() {
  let answer = await inquirer.prompt(base)
  const type = answer.langType === 1 ? 'vue' : 'react'
  // NOTE: 根据基本信息的答案,判断接下来需要问的问题
  // NOTE: 如果是需要vue的模版,需要考虑到原来的数据是否需要添加ci,不添加给默认值
  if (answer.isAddCI) {
    Object.assign(answer, await inquirer.prompt(vueAddCi))
  } else {
    answer.parkName = 'app.front.tar.gz'
    answer.host = '127.0.0.1'
    answer.port = '80'
    answer.username = 'xxxx'
    answer.password = 'xxx'
    answer.sftpProjectPath =
      '/xxx/xxx/xxx_demo_V1.0.0_000_20200331_name_前端全部补丁'
  }

  const spinner = ora(color.loading('building for production...\n'))
  spinner.start()
  answer.username = await getUsername()
  let dir = await createProject(answer.projectName)
  await copyTemplate('h5-' + type, dir)
  let awiatArr =
    type === 'vue'
      ? [
          path.resolve(dir, './package.json'),
          path.resolve(dir, './vue.config.js'),
          path.resolve(dir, './.env.temp'),
          path.resolve(dir, './src/services/services.js')
        ]
      : [
          path.resolve(dir, './package.json'),
          path.resolve(dir, './src/services/commenPromise.js'),
          path.resolve(dir, './devProxy.js'),
          path.resolve(dir, './webpack.config.js')
        ]
  if (answer.needInitGit) {
    awiatArr.push(path.resolve(dir, './init.sh'))
  } else {
    execCmd('rm -rf ' + path.resolve(dir, './init.sh'))
  }
  await rewriteTemplate(answer, awiatArr)
  await execCmd(
    'mv ' + path.resolve(dir, './.env.temp') + ' ' + path.resolve(dir, './.env')
  )
  spinner.stop()
  color.c_success(`\n 项目初始化完成.\n 位置----> ${dir}\n`)

  // NOTE: 配置了git并且需要初始化信息的时候通过邮箱告知相关人员添加配置
  const sending = ora(color.loading('正在为将您的信息发送邮件给相关人,请稍等……'))
  sending.start()
  const msg = await mail(answer)
  color.c_info(`\n ${msg} \n `)
  sending.stop()
  color.c_success(`\n 已将您的项目信息发送给相关人员 \n `)
  color.c_info('-----------------------------------------------------------')

  // NOTE: 如果有git相关信息,直接通过命令初始化git,如果没有需要自己去初始化
  if (answer.needInitGit) {
    color.c_success(`\n cd ${answer.projectName} \n npm run init`)
  } else {
    color.c_success(`\n cd ${answer.projectName} \n npm i \n npm start/npm run dev \n`)
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
Last update: December 23, 2022 13:14
Contributors: salvatoreliaoxuan