成效
引言
随着前端工程化的理念不断深入,越来越多的人选择使用脚手架从零到一搭建自己的项目。其中大家最熟悉的就是create-react-app
和vue-cli
,它们可以帮助我们初始化配置生成项目结构、自动安装依赖,最后我们一行指令可运行项目开始开发,或者进行项目构建(build)。
这些脚手架提供的都是普遍意义上的最佳实践,但是实际开发中发现,随着业务的不断发展,必然会出现需要针对业务开发的实际情况来进行调整。例如:
-
通过调整插件与配置实现
Webpack
打包性能优化后 -
项目架构调整
-
编码风格
-
用户权限控制
-
融合公司的基建
-
...
总而言之,随着业务发展,我们往往会沉淀出一套更“个性化”的业务方案。这时候我们最直接的做法就是开发出一个该方案的脚手架来,以便今后能复用这些最佳实践与方案。
1. 脚手架怎么工作?
功能丰富程度不同的脚手架,复杂程度自然也不太一样。但是总体来说,脚手架的工作大体都会包含几个步骤:
- 初始化,一般在这个时候会进行环境的初始化,做一些前置的检查,如版本更新。
- 用户输入,进行交互,例如用
vue-cli
的时候,它会“问”你很多的配置选项 - 生成模板
- 生成配置文件
- 安装依赖
- 清理,校验等收尾工作
在企业中一般我们只是想轻量级,快速的创建一个特定场景的脚手架(可能并不需要像vue-cli
那么完备)。所有下面将演示制作的基本流程,从制作简单的demo调试,到复杂的功能开发。
2.脚手架整体架构设计与基本流程
基本流程
3. 开发脚手架我们需要用到的三方库
库名 | 描述 | commander | 处理控制台命令 | chalk | 五彩斑斓的控制台 | latest-version | 活动最新的npm包 | inquirer | 控制台询问 | download-git-repo | git远程仓库拉取 | figlet | 粉笔字 | glob | 匹配指定路径文件 | ora | 命令行环境的loading效果 | clear | 清除控制台输出的信息 | log-symbols | 各种日记级别的彩色符号 | metalsmith | 处理模板 |
---|
4.项目实战
- 目录结构
|-- .gitignore
|-- .npmignore
|-- package-lock.json
|-- package.json
|-- README.md
|-- bin
| |-- cunw
| |-- cunw-init
| |-- cunw-list
|-- conf
| |-- const.js
| |-- index.js
| |-- template.json
|-- lib
| |-- download.js
| |-- generator.js
| |-- projectConstruction.js
|-- utils
|-- log.js
1. 项目初始化
新建一个 cunw
文件夹,初始化项目npm init --y
,并安装所需依赖
{
"name": "cunw-cli",
"version": "0.0.1",
"description": "快速生成项目的脚手架",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "forestxiecode",
"license": "ISC",
"dependencies": {
"chalk": "^4.1.1",
"clear": "^0.1.0",
"commander": "^6.1.0",
"download-git-repo": "^3.0.2",
"ejs": "^2.7.4",
"figlet": "^1.5.0",
"git-clone": "^0.1.0",
"glob": "^7.1.6",
"handlebars": "^4.7.6",
"inquirer": "^6.5.2",
"latest-version": "^5.1.0",
"log-symbols": "^4.0.0",
"metalsmith": "^2.3.0",
"minimatch": "^3.0.4",
"ora": "^5.1.0",
"rimraf": "^3.0.2",
"wrap-ansi": "^7.0.0"
},
"devDependencies": {},
}
2. 测试环境
在根目录下lib文件夹下,新建cunw-init.js
文件,编写测试代码。
+ #! /usr/bin/env node
+ console.log('测试')
并且修改pack.json文件,全局link指令,测试环境,调试代码,如。
+ "bin": {
+ "cunw-init": "bin/cunw-init"
+ }
- 在根目录 全局link下
npm link
- 指令测试
3.编写程序代码
根据输入,获取项目名称
// 根据输入,获取项目名称
let projectName = program.args[0];
if (!projectName) {
// 相当于执行命令的--help选项,显示help信息,这是commander内置的一个命令选项
program.help();
return;
}
// 返回 Node.js 进程的当前工作目录
let rootName = path.basename(process.cwd());
执行主函数,在这使用figlet
工具打印大写的粉笔字体
const data = await figlet('WELCOM CUNW CLI')
console.log(chalk.green(data))
进入版本检查,获取本地package.json
文件下的版本号,用latest-version
模块获取最后一次版本号进行对比
function checkVersion() {
return new Promise(async (resolve, reject) => {
const spinner = ora(`检测版本....`);
spinner.start();
let webVersion = await latestVersion(`${CONST.CLI_NAME}`);
let localVersion = require("../package.json").version;
spinner.succeed();
console.log(`本地版本${localVersion}, 最新版本${webVersion}\n`);
let webVersionArr = webVersion.split(".");
let localVersionArr = localVersion.split(".");
let isNew = webVersionArr.some((item, index) => {
return Number(item) > Number(localVersionArr[index]);
});
if (isNew) {
log.warn(`检查已存在更新版本,请执行指令 npm install @cunw/cunw-cli -g 更新版本\n`)
setTimeout(() => {
resolve(isNew);
}, 2000)
} else {
setTimeout(() => {
resolve(isNew);
}, 1000)
}
});
}
路径检查,判断当前是否已经存在该文件夹,否则创建该文件
// 路径检查
function checkDir() {
return new Promise(async (resolve, reject) => {
const list = glob.sync("*"); // 遍历当前目录
if (list.length) {
if (
list.filter(name => {
const fileName = path.resolve(process.cwd(), path.join(".", name));
const isDir = fs.statSync(fileName).isDirectory();
return name.indexOf(projectName) !== -1 && isDir;
}).length !== 0
) {
log.error(`项目${projectName}已经存在`)
reject(`项目${projectName}已经存在`);
}
resolve(projectName);
} else if (rootName === projectName) {
let answer = await inquirer.prompt([
{
name: "buildInCurrent",
message:
"当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目",
type: "confirm",
default: true
}
]);
resolve(answer.buildInCurrent ? "." : projectName);
} else {
resolve(projectName);
}
});
}
// 创建该文件
function makeDir(projectRoot) {
if (projectRoot !== ".") {
fs.mkdirSync(projectName);
}
}
使用inquirer.js处理命令行交互,让用户现在自己需要的模板进行渲染。
function selectTemplate() {
return new Promise((resolve, reject) => {
let choices = []
Object.values(templateConfig).forEach(item => {
if (item.enable) {
choices.push({
name: item.name,
value: item.value
});
}
});
let config = {
// type: 'checkbox',
type: "list",
message: "请选择创建项目类型",
name: "select",
choices: [new inquirer.Separator("模板类型"), ...choices]
};
inquirer.prompt(config).then(data => {
let { select } = data;
let { name, git, value } = templateConfig[select];
resolve({
git,
name,
value
});
});
});
}
得到用户的模板地址,使用download-git-repo
模块下载模板。
module.exports = function (target, url) {
const spinner = ora(`正在下载项目模板,源地址:${url}`)
target = path.join(CONST.TEMPLATE_NAME)
spinner.start()
return new Promise((resolve, reject) => {
download(`direct:${url}`,
target, { clone: true }, (err) => {
if (err) {
spinner.fail()
console.log(logSymbols.fail, chalk.red("模板下载失败"));
reject(err)
} else {
spinner.succeed()
console.log(logSymbols.success, chalk.green("模板下载完毕"));
resolve(target)
}
})
})
}
使用metalsmith
处理模板
引用官网的介绍:
它就是一个静态网站生成器,可以用在批量处理模板的场景,类似的工具包还有Wintersmith
、Assemble
、Hexo
。它最大的一个特点就是EVERYTHING IS PLUGIN
,所以,metalsmith
本质上就是一个胶水框架,通过黏合各种插件来完成生产工作。
给项目模板添加变量占位符。
module.exports = function (config) {
let { metadata, src, dest } = config;
if (!src) {
return Promise.reject(new Error(`无效的source:${src}`));
}
// 官方模板
return new Promise((resolve, reject) => {
const metalsmith = Metalsmith(process.cwd())
.metadata(metadata)
.clean(false)
.source(src)
.destination(dest);
const ignoreFile = path.resolve(process.cwd(), src, CONST.FILE_IGNORE);
if (fs.existsSync(ignoreFile)) {
// 定义一个用于移除模板中被忽略文件的metalsmith插件
metalsmith.use((files, metalsmith, done) => {
const meta = metalsmith.metadata();
// 先对ignore文件进行渲染,然后按行切割ignore文件的内容,拿到被忽略清单
const ignores = ejs
.render(fs.readFileSync(ignoreFile).toString(), meta)
.split("\n")
.filter(item => !!item.length);
Object.keys(files).forEach(fileName => {
// 移除被忽略的文件
ignores.forEach(ignorePattern => {
if (minimatch(fileName, ignorePattern)) {
delete files[fileName];
}
});
});
done();
});
}
metalsmith
.use((files, metalsmith, done) => {
const meta = metalsmith.metadata();
// 编译模板
Object.keys(files).forEach(fileName => {
try {
const t = files[fileName].contents.toString();
if (/(<%.*%>)/g.test(t)) {
files[fileName].contents = new Buffer.from(ejs.render(t, meta));
}
} catch (err) {
// console.log("fileName------------", fileName);
// console.log("er -------------", err);
}
});
done();
})
.build(err => {
rm(src);
err ? reject(err) : resolve();
});
});
};
package.json
的name
、version
、description
字段的内容被替换成了handlebar
语法的占位符,模板中其他地方也做类似的替换,完成后重新提交模板的更新。
调用该函数删除一些无用的文件,做一些清理工作。
function deleteCusomizePrompt(target) {
// 自定义选项模板路径
const cusomizePrompt = path.join(process.cwd(), target, CONST.CUSTOMIZE_PROMPT)
if (fs.existsSync(cusomizePrompt)) {
rm(cusomizePrompt)
}
// 忽略文档路径
const fileIgnore = path.join(process.cwd(), target, CONST.FILE_IGNORE)
if (fs.existsSync(fileIgnore)) {
rm(fileIgnore)
}
}
最后执行结束的回调,初始化项目。
clear()
log.succes("创建成功")
// 初始化项目
await initProject(projectRoot)
// 运行项目
console.log(chalk.green(`====================================\n
运行项目 ...\n
cd ./demmo\n
npm run serve\n
===================================
`))
5. 如何发布一个npm包
1. 注册一个npm账号
2. 项目根目录下npm login
登入npm
账号,最后执行npm publish
发布
总结
去模仿,去参照,其实实现一个脚手架也不是特别复杂。
- 通过node可以很好的解决一些工程化上的问题
- 其实npm存在很好的node模块库如:
- 通过download-git-repo处理下载
- 通过inquirer.js处理终端交互
- 通过metalsmith和模板引擎将交互输入项插入到项目模板中
通过这次开发自己的脚手架中。还想自己存在很多不足,也在模仿,和学习。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!