开发一个简单的前端脚手架
手写前端脚手架 why-cli
前言
- cli脚手架工具就是拉取模板,编译模板
- 常用的一些工具函数,项目的基本配置,可以放到模板里配置好
1.必备模块 npm 包
- commander 命令行参数解析
- inquirie 命令行交互工具,可以实现命令行的选择功能,输入功能
- download-git-repo 从 github 下载模板
- chalk 颜色
- metalSmith 读取所有文件,实现模板渲染
- consolidate 统一模板引擎
- ora 命令行 loading 效果
- axios
- ncp 拷贝文件
2.工程创建
2.1创建目录
- |--bin
- |--www //全局命令执行文件
- |--src
- |-- create.js // create 命令逻辑
- |-- config.js // config 命令逻辑
- |-- main.js // 入口文件
- |-- utils // 存放工具方法
2.2初始化项目
-
生成 pakeage.json
npm init -y
-
pakeage.json 中设置命令
"bin": { "why-cli": "./bin/www" }
-
www 文件中,使用node环境执行此文件,引入main.js入口文件
#! /usr/bin/env node
require('../src/main.js)
-
链接包到全局下使用
npm link
cmd 命令行中就可以执行 why-cli
2.2 使用 eslint
- 安装 eslint
cnpm i eslint --save
- 初始化 eslint 配置文件
npx eslint --init
2.3 使用 commander 获取版本号
- 安装 commander
cnpm i commander --save
- main.js入口文件
const { program } = require("commander"); // process.argv 就是命令行中传入的参数 program.version('0.0.1').parse(process.argv)
- 动态获取版本号
const { program } = require("commander"); // 动态获取版本号 const { version } = require("../package.json"); // process.argv 就是命令行中传入的参数 program.version('0.0.1').parse(process.argv)
2.4 配置指令命令
-
将指令写成一个对象形式数据,通过遍历,去注册命令
const mapCommands = { create: { alias: "c", description: "create a project", examples: ["active-cli create <project-name>"], }, "*": { alias: "", description: "not found command", examples: [], }, }; // 循环创建命令 Reflect.ownKeys(mapCommands).forEach((command) => { program .command(command) .alias(mapCommands[command].alias) .description(mapCommands[command].description) .action(() => { console.log(command) }); });
-
create 命令主要就是下载gitee仓库中的模板,如果有需要填写的信息,拿到用户填写的信息,渲染模板
main.js
const { program } = require("commander"); program .command("create") // 命令名称 .alias("c") // 命令别名 .description("create a project") // 该命令的描述信息 .action(() => { // ...process.argv.slice(3) 将命令行传入的参数展开 require(path.resolve(__dirname, "create"))(...process.argv.slice(3)); } }); .parse();
create.js
module.exports = async (projectName) => { projectName })
2.5 拉取项目
- 模板放在gitee.com 上,使用axios获取项目信息
- 安装 axios
cnpm i axios --save
- 获取组织下的仓库模板
// 获取组织下的所有仓库 const fetchRepoList = async () => { const url = `https://gitee.com/api/v5/orgs/${organization}/repos?access_token=${access_token}&type=all&page=1&per_page=20`; const { data } = await axios(url); return data; }; // 获取组织下的全部模板 let repos = await waitFnLoading(fetchRepoList, "fetch template...")(); repos = repos.map((repo) => repo.name);
2.6 ora inquirer
-
ora 命令行 loading 效果,inquirer 命令行交互,选择模板,填写信息等
-
安装
cnpm i ora inquirer --save
-
选择模板信息
// 选择一个模板 const { repo } = await inquirer.prompt({ name: "repo", type: "list", message: "please choise a template to create project", choices: repos, }); // loading 效果 const ora = (await import("ora")).default; const spinner = ora(message); spinner.start(); const data = await fetchRepoList(); spinner.succeed();
-
获取版本信息和选择版本
// 根据选择的模板,获取该模板所有的tags let tags = await waitFnLoading(fetchTagsList, "fetch tags...")(repo); tags = tags.map((tag) => tag.name); console.log(tags); let tag = ""; if (tags.length > 0) { // 选择tag const { tagname } = await inquirer.prompt({ name: "tagname", type: "list", message: "please choise a tag to create project", choices: tags, }); tag = tagname; }
2.7 下载模板
- 安装 download-git-repo
cnpm i download-git-repo --save
- 下载前先找个临时目录,存放下载的目录
- 临时目录就是当前用户的.template文件夹中
// 模板临时存储位置 const tempDestination = `${ process.env[process.platform === "darwin" ? "HOME" : "USERPROFILE"] }/.template`;
- 开始下载
/** * @description 下载模板 * @param {string} repo 模板 * @param {string} tag 标签 * @return {string} 临时下载文件的目录 */ const downloadTemplate = async (repo, tag) => { let url = `direct:https://gitee.com/${organization}/${repo}`; if (tag) { url += `#${tag}`; } const tempDest = `${tempDestination}/${repo}`; await new Promise((resolve, reject) => { downloadGitRepo(url, tempDest, { clone: true }, function (err) { console.log(err ? "Error" : "Success"); if (err) { reject(err); } else { resolve(); } }); }); // 返回临时存储目录 return tempDest; };
2.9 编译拷贝模板
-
简单模板,直接使用ncp拷贝文件
-
安装 ncp
cnpm i ncp --save
-
拷贝
await ncpPromise(tempDest, path.join(path.resolve(), projectName));
-
复杂模板,metalsmith 编译,用 consolidate.ejs 渲染
-
安装
cnpm i metalsmith ejs consolidate --save
-
遍历所有的文件目录,编译模板,只要是模板渲染都需要
await new Promise((resolve, reject) => { Metalsmith(__dirname) // 如果传入路径,默认遍历当前文件夹 src 目录 .source(tempDest) // 设置相对目录 .destination(path.resolve(projectName)) // 目标文件夹路径 .use(async (files, metal, done) => { // 取出 ask.js // 用 inquirer, 让用户填写信息 // 将信息挂载到metal.metadata()上,传递给下一个中间件使用 // 删除 ask.js 文件 const ask = require(path.join(tempDest, `ask.js`)); const answer = await inquirer.prompt(ask); const tempMeta = metal.metadata(); Object.assign(tempMeta, answer); delete files[`ask.js`]; done(); }) .use(async (files, metal, done) => { // 从 metal.metadata() 拿到用户填写的信息 // 取到js。json文件,是否存在<%=模板符号 // 通过渲染函数 consolidate.ejs.render渲染内容,将渲染结果赋值给对应文件 const answer = metal.metadata(); Reflect.ownKeys(files).forEach(async (fileKey) => { if (fileKey.includes(".js") || fileKey.includes(".json")) { const contentsString = files[fileKey].contents.toString(); if (contentsString.includes("<%=")) { // 渲染模板 const renderContent = await renderPromise( contentsString, answer ); // 用渲染结果,替换对应文件内容 files[fileKey].contents = Buffer.from(renderContent); } } }); done(); }) .build(function (err) { if (err) { reject(err); } else { resolve(); } }); })
-
去掉 link
npm unlink active-cli
3.项目发布
- npm login
- npm publish
4.常见问题
- npm publish 报错403,可能原因以下:
1. 用了淘宝镜像源 - 换成npm的源 nrm use npm
2. 包名重复 - 删掉之前的包,改个名字。
3. npm账户没有验证邮箱 - 验证邮箱。
4. vpn冲突 - 关掉所有vpn再次尝试。 - npm i active-cli -g之后,使用active-cli时报错:not found modules...
可能原因就是 开发时用 cnpm xxx --save, 不使用cnpm xxx --save-dev