脚手架创建项目流程设计和开发
脚手架项目创建功能架构设计
“凡事预则立,不预则废”。在开始本周的编码工作之前呢,sam 老师一如既往的会给我们讲一下本周我们要做的内容是什么以及架构设计和流程是怎样的,具体来说主要包括了项目创建前准备阶段架构设计和下载项目模板阶段架构设计两个部分。
项目创建前准备阶段架构设计
下载项目模板阶段架构设计
项目基本信息获取功能开发
根据上面我们画的架构设计图来看,在准备阶段我们要做的事情还真不少呢,概括的说就是获取基本信息,而这些信息从哪里来呢?自然是要让使用者告诉我们啦,那这里就有一个非常好用而强大的库帮助我们和使用者交互,它就是 inquirer。在这一小章节中,我们要完成的功能主要有以下几项:
- 判断当前目录(要运行我们的脚手架命令进行模板安装的那个目录)是否为空
- inquirer 的基本用法和常用属性入门、多种交互形式的演示
- 强制清空当前目录功能开发
- 获取项目基本信息功能开发
- 项目名称和版本号的合法性校验
判断当前目录(要运行我们的脚手架命令进行模板安装的那个目录)是否为空
我们这里使用的是 Node.js 提供给我们的文件系统操作模块(fs)。
isDirEmpty(localPath) {
let fileList = fs.readdirSync(localPath)
// 文件过滤的逻辑
fileList = fileList.filter(file => (!file.startsWith('.') && ['node_modules'].indexOf(file) < 0))
return !fileList || fileList.length <= 0
}
强制清空当前目录功能开发
我们要先判断当前目录是否为空,在不为空的情况下我们会使用 inquirer 来询问用户是否要继续创建项目,这里要注意的是 force 参数的获取;但是因为清空文件夹的操作是一个不可逆的操作,我们还是要至少询问一次用户是否确认清空。如果用户确认清空,那我们就会清空当前目录,用到的库是 fs-extra。
const localPath = process.cwd()
if (!this.isDirEmpty(localPath)) {
// 询问是否继续创建 使用到inquirer这个库
// 如果 用户不是强制更新,那么就要询问用户是否继续创建
let ifContinue = false
if (!this.force) {
ifContinue = (
await inquirer.prompt({
type: 'confirm',
name: 'ifContinue',
message: '当前目录不为空,是否继续创建?',
default: false
})
).ifContinue
if (!ifContinue) {
return
}
}
// 不管用户是否是强制更新,最后都会展示这次询问,因为清空当前目录文件是一个非常严谨的操作
if (ifContinue || this.force) {
// 做二次确认
const { confirmDelete } = await inquirer.prompt({
type: 'confirm',
name: 'confirmDelete',
message: '是否确认清空当前目录下的文件?',
default: false
})
if (confirmDelete) {
// 清空当前目录 使用 fse-extra
fse.emptyDirSync(localPath)
}
}
}
获取项目基本信息功能的开发
在清空了文件夹以后,我们要询问用户一些基本信息,比如这个项目的名字、版本号以及可能存在的描述信息(组件模板的情况下),将来这些信息都会通过 ejs 模板引擎渲染到 package.json 文件中。
// 声明一个对象用来接收项目信息 最后返回的也是这个对象
let projectInfo = {}
// 校验项目名称的正则,封装在一个函数内
function isValidName(v) {
return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)
}
// 默认项目名称是不通过的
let isProjectNameValid = false
// 如果用户在输入命令时的名称符合我们的规则 就直接用这个
if (isValidName(this.projectName)) {
isProjectNameValid = true
projectInfo.projectName = this.projectName
}
// inquirer获取用户想要下载的是组件模板还是项目模板
const { type } = await inquirer.prompt({
type: 'list',
name: 'type',
message: '请选择初始化项目类型?',
default: TYPE_PROJECT,
choices: [
{
name: '项目',
value: TYPE_PROJECT
},
{
name: '组件',
value: TYPE_COMPONENT
}
]
})
// 通过条件过滤对应的模板
this.template = this.template.filter((template) => {
return template.tag.includes(type)
})
const title = type === TYPE_PROJECT ? '项目' : '组件'
// 兼容项目和模板两种情况的交互询问
const projectNamePrompt = {
type: 'input',
name: 'projectName',
message: `请输入${title}名称`,
default: '',
validate: function (v) {
const done = this.async()
// 1.首字符必须为英文字符
// 2.尾字符必须为英文字符或数字,不能为字符
// 3.字符仅允许“-_”
// 4.兼容只有一个字母的情况
setTimeout(function () {
if (!isValidName(v)) {
done(`请输入合法的${title}名称,例:a1 | a_b_c | a1_b1_c1`)
return
}
// Pass the return value in the done callback
done(null, true)
}, 0)
},
filter: function (v) {
return v
}
}
// 这个数组是最后要传给inquirer的参数
const projectPrompt = []
// 如果用户在命令行输入的名称不符合我们的要求,那么我们就将后来用户输入的名称添加到我们的数组中
if (!isProjectNameValid) {
projectPrompt.push(projectNamePrompt)
}
// 除了项目名称以外 我们还要知道用户输入的版本号、选择的模板
projectPrompt.push(
{
type: 'input',
name: 'projectVersion',
message: `请输入${title}版本号`,
default: '1.0.0',
validate: function (v) {
const done = this.async()
setTimeout(function () {
if (!!!semver.valid(v)) {
done('请输入合法的项目版本号,例:1.0.0')
return
}
// Pass the return value in the done callback
done(null, true)
}, 0)
return
},
filter: function (v) {
if (!!semver.valid(v)) {
return semver.valid(v)
}
return v
}
},
{
type: 'list',
name: 'projectTemplate',
message: `请选择${title}模板`,
choices: this.createProjectTemplate()
}
)
// 如果用户选择的是项目模板 那我们直接将上面的projectPrompt传递给inquirer即可 然后用将所有我们要用到的信息进行拼装,就是我们要的projectInfo
if (type === TYPE_PROJECT) {
const project = await inquirer.prompt(projectPrompt)
projectInfo = {
...projectInfo,
type,
...project
}
} else if (type === TYPE_COMPONENT) {
// 如果用户选择的是组件模板,那么我们要在前面的基础上追问一条描述信息
const descriptionPrompt = {
type: 'input',
name: 'componentDescription',
message: '请输入组件描述信息',
default: '',
validate: function (v) {
const done = this.async()
setTimeout(() => {
if (!v) {
done('请输入组件描述信息')
return
}
done(null, true)
}, 0)
}
}
projectPrompt.push(descriptionPrompt)
const component = await inquirer.prompt(projectPrompt)
projectInfo = {
...projectInfo,
type,
...component
}
}
// 最后我们对拿到的项目信息进行一些转换 这里在转换项目名称的时候用到了kebab-case这个库,可以帮助我们将驼峰格式的名称转为连字符格式的
// 生成classname
if (projectInfo.projectName) {
projectInfo.name = projectInfo.projectName
projectInfo.className = require('kebab-case')(projectInfo.projectName)
}
// 生成version
if (projectInfo.projectVersion) {
projectInfo.version = projectInfo.projectVersion
}
// 生成description
if (projectInfo.componentDescription) {
projectInfo.description = projectInfo.componentDescription
}
// 至此,我们想要的项目基本信息就获取完成了
return projectInfo
egg.js + 云 mongodb 快速入门
在看这部分的时候没有感到什么阻力,可能之前学了双越老师、7 七月老师的相关课程吧,做 web server 的时候用了 mongodb 和 mysql 两种数据库,也就没有开通云 mongodb,所使用的是我们自己服务器上安装的 mongodb 数据库,新建了一个数据库;将 egg.js 模板的代码进行一番修改以后就达到了我们课程的效果(改成自己的接口地址,返回我们自己数据库的内容);最后将服务运行在自己的服务器上面,使用 nginx 进行 7001 端口的转发。
项目模板开发 + 获取项目模板 API 开发
这里的话我上传了一个 vue3 的模板(使用 vue-cli 创建的项目进行了删减)和花裤衩大佬的 vue-element-admin 模板总共两个模板进行测试,对接的是自己服务器的接口,内容比较简单,就不进行记录了。
脚手架项目模板下载功能开发
到了这一步,我们在前面创建的 Package 这个类就派上用场了,会发现我们已经将安装和更新功能都封装好了,直接用就好了,很方便~,如果模板不存在的话,就调用 Package 实例的 install 方法进行安装,如果存在的话,就要调用 Package 实例的 update 方法进行更新。
async downloadTemplate() {
const { projectTemplate } = this.projectInfo
const templateInfo = this.template.find((item) => item.npmName === projectTemplate)
const targetPath = path.resolve(userHome, '.tangmen-cli-dev', 'template')
const storeDir = path.resolve(userHome, '.tangmen-cli-dev', 'template', 'node_modules')
const { npmName, version } = templateInfo
this.templateInfo = templateInfo
const templateNpm = new Package({
targetPath,
storeDir,
packageName: npmName,
packageVersion: version
})
if (!await templateNpm.exists()) {
const spinner = spinnerStart('正在下载模板...')
await sleep()
try {
await templateNpm.install()
} catch (e) {
throw e
} finally {
spinner.stop(true)
if (await templateNpm.exists()) {
log.success('下载模板成功')
}
this.templateNpm = templateNpm
}
} else {
const spinner = spinnerStart('正在更新模板...')
await sleep()
try {
await templateNpm.update()
} catch (e) {
throw e
} finally {
spinner.stop(true)
if (await templateNpm.exists()) {
log.success('更新模板成功')
}
this.templateNpm = templateNpm
}
}
}
inquirer源码解析
暂时没啃这块,待更新~