本文相关代码:youth_camp_ykjun: 青训营&开课吧 实战课程代码仓库 - Gitee.com (opens new window)
# 一、前言
今天来复习&总结青训营实训营第二天的内容,在上次课程中我们从0到1创建了一个vue的脚手架 (opens new window),今天我们跟着崔老师@阿崔cxr (opens new window)使用自动化思维来搭建一个koa脚手架
PS:本博文是崔老师 前端工程化 课程的第一部分:初始化,后面我会尽力更新完~ 包括:webpack原理、rollup原理、实现mini-vite、自动测试等
# 二、分析vue-cli
- 使用指令创建一个项目
vue create hello-world
- 提供选择 单选/多选
- 安装依赖
总结一下步骤就是:① 输入指令 ② 选择配置 ③ 创建文件 ④ 安装依赖
那么我们就按照这个步骤来写我们的脚手架
# 三、起步
接下来我们就要自己实现一个脚手架,有了上节课的基础知识,今天的内容可以让你更上一层楼。
首先创建我们脚手架的项目目录 learn-koa-setup
然后初始化项目npm init -y
将项目模块化规范改成ESM,即在package.json中添加"type": "module",
创建好index.js
,在这里编写脚手架要做的事情
import fs from "fs";
// 核心:自动化思维【先想手动创建一个项目的基本步骤】
// 1. 创建了文件夹(项目名)
fs.mkdirSync("ykyk");
// 2. 创建了index.js
fs.writeFileSync("./ykyk/index.js", "index");
// 3. 创建了package.json
fs.writeFileSync("./ykyk/package.json", "package");
// 4. 安装依赖
// TODO package -> npm
我们测试一下
node index.js
文件夹和文件已经创建,也填充了基本内容
我们每次执行这个命令,都要手动删除ykyk这个文件夹,然后再新建,比较麻烦,我们用指令让他自动
执行,在package.json中添加指令(我这里使用的是windows删除文件夹的指令)
"scripts": {
"test": "rd /s ykyk && node index.js"
},
此时就可以直接使用 npm test
命令来完成删除文件夹,然后执行index.js文件了!
# 四、完善
上面的步骤虽然很easy,但是已经是大体的方向已经明确了,接下来要做的就是不断完善这里面的具体细节。
注意:我们这里使用
小步骤
的方式写代码。一点一点的完善,一次只完成一个很小的功能,然后进行测试。功能没问题再往下写,这样保证我们在编写代码的时候及时知道哪里出了错,及时修正。
我们这里用到了很多根路径,我们将他封装起来
function getRootPath() {
return "./ykyk";
}
就可以这样使用了
// 1. 创建了文件夹(项目名)
fs.mkdirSync(getRootPath());
// 2. 创建了index.js
fs.writeFileSync(getRootPath()+"/index.js", "index");
// 3. 创建了package.json
fs.writeFileSync(getRootPath()+"/package.json", "package");
# 1. 生成index.js内容
# ① 生成模板
我们创建一个模板 indexTemplate.js
先将模板内容写死,然后导出创建模板的函数
export function createIndexTemplate() {
return `
const Koa = require("koa");
const app = new Koa();
app.listen(8000, () => {
console.log("open server localhost:8000");
});
`;
}
在index.js中引入,并使用
import { createIndexTemplate } from "./indexTemplate.js";
fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate());
最后执行指令,查看效果
# ② 动态生成模板
接下来我们就要让模板的内容可以根据不同的设置,来呈现不同的代码
这里我们用到一个第三方库 ejs 高效的嵌入式 JavaScript 模板引擎 (opens new window)
安装 npm i ejs
创建文件 template/index.ejs
const Koa = require("koa");
const app = new Koa();
app.listen(8000, () => {
console.log("open server localhost:8000");
});
此时在 indexTemplate.js 就应该这样引入和使用
import ejs from "ejs";
import fs from "fs";
export function createIndexTemplate() {
// 读取ejs模板
const template = fs.readFileSync("./template/index.ejs", "utf-8");
// 交给ejs来渲染
const code = ejs.render(template);
// 返回渲染后的模板
return code;
}
此时再测试一下,发现没有问题,然后就要进行条件渲染了
修改index.ejs文件【根据文档中给出的语法】
const Koa = require("koa");
<% if (router) { %>
const Router = require("koa-router");
<% } %>
const app = new Koa();
<% if (router) { %>
const router = new Router();
router.get("/", (ctx) => {
ctx.body = "hello YK菌";
});
app.use(router.routes());
<% } %>
app.listen(8000, () => {
console.log("open server localhost:8000");
});
通过给渲染函数传递参数来决定是否渲染响应的内容
const code = ejs.render(template, {
router: true,
});
# ③ 模拟用户输入的数据
其实这个参数应该是由用户来选择的,但是我们是小步骤
的方式编写代码,所以我们先将用户的输入写死。
// 先把配置中间件数据写死
const inputConfig = {
middleware: {
router: true,
},
};
// 将配置文件传入创建模板函数
fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));
在indexTemplate.js
中这样使用即可
export function createIndexTemplate(inputConfig) {
// 读取ejs模板
const template = fs.readFileSync("./template/index.ejs", "utf-8");
// 交给ejs来渲染
const code = ejs.render(template, {
router: inputConfig.middleware.router,
});
// 返回渲染后的模板
return code;
}
# ④ 拓展完整选项
我们把四个选项补齐
// 程序的input
const inputConfig = {
middleware: {
router: true,
static: true,
views: true,
body: true
},
};
indexTemplate.js
import ejs from "ejs";
import fs from "fs";
export function createIndexTemplate(inputConfig) {
// 读取ejs模板
const template = fs.readFileSync("./template/index.ejs", "utf-8");
// 交给ejs来渲染
const code = ejs.render(template, {
router: inputConfig.middleware.router,
static: inputConfig.middleware.static,
views: inputConfig.middleware.views,
body: inputConfig.middleware.body
});
// 返回渲染后的模板
return code;
}
template (opens new window)/index.ejs
const Koa = require("koa");
<% if (router) { %>
const Router = require("koa-router");
<% } %>
<% if (static) { %>
const serve = require("koa-static");
<% } %>
<% if (views) { %>
const views = require("koa-views");
<% } %>
<% if (body) { %>
const body = require("koa-body");
<% } %>
const app = new Koa();
<% if (router) { %>
const router = new Router();
router.get("/", (ctx) => {
ctx.body = "hello YK菌";
});
app.use(router.routes());
<% } %>
<% if (static) { %>
app.use(serve(__dirname + "/static"));
<% } %>
<% if (views) { %>
app.use(
views(__dirname + "/views", {
extension: "pug",
})
);
<% } %>
<% if (body) { %>
app.use(
body({
multipart: true,
})
);
<% } %>
app.listen(8000, () => {
console.log("open server localhost:8000");
});
测试我们的代码功能 npm test
此时创建出的ykyk项目中的index.js就是根据选项生成的完整的代码了
显示 ykyk/index.js 中的内容符合我们的预期
const Koa = require("koa");
const Router = require("koa-router");
const serve = require("koa-static");
const views = require("koa-views");
const body = require("koa-body");
const app = new Koa();
const router = new Router();
router.get("/", (ctx) => {
ctx.body = "hello YK菌";
});
app.use(router.routes());
app.use(serve(__dirname + "/static"));
app.use(
views(__dirname + "/views", {
extension: "pug",
})
);
app.use(
body({
multipart: true,
})
);
app.listen(8000, () => {
console.log("open server localhost:8000");
});
# 2. 生成package.json内容
简单使用ejs生成了index.js模板,我们再基于数据生成package.json,和上面的步骤基本一样
# ① 生成模板
首先创建 packageTemplate.js
import ejs from "ejs";
import fs from "fs";
export function createPackageTemplate(inputConfig) {
// 读取ejs模板
const template = fs.readFileSync("./template/package.ejs", "utf-8");
// 交给ejs来渲染
const code = ejs.render(template, {
router: inputConfig.middleware.router,
static: inputConfig.middleware.static,
views: inputConfig.middleware.views,
body: inputConfig.middleware.body,
});
// 返回渲染后的模板
return code;
}
然后创建一个模板 template/package.ejs
{
"name": "koa-setup-ykyk",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rm -rf hei && node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.13.1"
<% if (router) { %>
,"koa-router": "^10.1.1"
<% } %>
<% if (static) { %>
,"koa-static": "^5.0.0"
<% } %>
<% if (views) { %>
,"koa-views": "^7.0.1"
,"pug": "^3.0.2"
<% } %>
<% if (body) { %>
,"koa-body": "^4.1.3"
<% } %>
}
}
这里有一个小技巧,就是把可选包的前面加上逗号,这样就可以保证这里的格式没有问题了~
# ② 获取项目名称
模拟用户输入packageName
// 程序的input
const inputConfig = {
packageName: 'yk_demo',
middleware: {
router: true,
...
},
};
在这里引入即可
...
// 交给ejs来渲染
const code = ejs.render(template, {
packageName: inputConfig.packageName,
...
});
...
最后根据ejs的语法在template/package.ejd中这样写就可以
"name": "<%= packageName %>",
最后我们测试一下npm test
,可以看到生成的package.json符合我们的预期
{
"name": "yk_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rm -rf hei && node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.13.1",
"koa-router": "^10.1.1",
"koa-static": "^5.0.0",
"koa-views": "^7.0.1",
"pug": "^3.0.2",
"koa-body": "^4.1.3"
}
}
相同的写法,我们可以把index.js中的端口号也设置成可供用户自己选择的模式,具体的可以看我代码仓库中的代码 youth_camp_ykjun: 青训营&开课吧 实战课程代码仓库 - Gitee.com (opens new window)
# 3. 获取用户输入
接下来我们要做的就是获取真实的用户输入,这里我们使用 inquirer
这个库(A collection of common interactive command line user interfaces.) 网址:inquirer - npm (npmjs.com) (opens new window)
首先安装这个库 npm i inquirer
创建question/index.js获取用户输入
import inquirer from "inquirer";
export function question() {
return inquirer.prompt([
// 获取用户输入的项目名称
{ type: "input", name: "packageName", message: "set package name" },
// 获取用户输入的端口号,默认值为8080
{
type: "number",
name: "port",
message: "set port number",
default: () => 8080,
},
// 多选,获取用户选择的中间件
{
type: "checkbox",
name: "middleware",
choices: [
{ name: "koaRouter" },
{ name: "koaStatic" },
{ name: "koaViews" },
{ name: "koaBody" },
],
},
]);
}
然后将模拟的数据替换成真实用户输入的数据
import { question } from "./question/index.js";
const answer = await question();
// 程序的input
const inputConfig = {
packageName: answer.packageName,
port: answer.port,
middleware: {
router: answer.middleware.indexOf("koaRouter") !== -1,
static: answer.middleware.indexOf("koaStatic") !== -1,
views: answer.middleware.indexOf("koaViews") !== -1,
body: answer.middleware.indexOf("koaBody") !== -1,
},
};
这里我们使用了一个await
,但是没有使用async
函数包裹,这是因为新版(14.8+)的node.js中支持在顶层直接使用await
当然我们可以把index.js中获取用户输入的逻辑抽离出去,并将代码进行简化
创建一个config.js
export function createConfig(answer) {
return {
packageName: answer.packageName,
port: answer.port,
middleware: {
router: haveMiddleware("koaRouter"),
static: haveMiddleware("koaStatic"),
views: haveMiddleware("koaViews"),
body: haveMiddleware("koaBody"),
},
};
function haveMiddleware(name) {
return answer.middleware.indexOf(name) !== -1;
}
}
index.js直接引入
import { question } from "./question/index.js";
import { createConfig } from "./config.js";
const answer = await question();
const inputConfig = createConfig(answer);
大功告成,看看效果
文件内容ykyk/index.js
const Koa = require("koa");
const Router = require("koa-router");
const views = require("koa-views");
const app = new Koa();
const router = new Router();
router.get("/", (ctx) => {
ctx.body = "hello YK菌";
});
app.use(router.routes());
app.use(
views(__dirname + "/views", {
extension: "pug",
})
);
app.listen(8080, () => {
console.log("open server localhost:8080");
});
文件内容ykyk/package.json
{
"name": "ykyk",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rm -rf hei && node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.13.1",
"koa-router": "^10.1.1",
"koa-views": "^7.0.1",
"pug": "^3.0.2"
}
}
# 4. 安装依赖
上次我们使用的是nodejs原生调用子进程安装依赖【青训营Pro】🛠️从0到1实现一个自己的前端约定路由项目脚手架🏗️ 工具~ - 掘金 (juejin.cn) (opens new window),今天我们使用一种社区里比较常用的 execa - npm (npmjs.com) (opens new window) 来自动安装依赖
首先就是安装npm i execa
直接引入使用
import { execa } from "execa";
// 4. 安装依赖
// TODO package -> npm
// 配置安装路径 和 显示安装状态
execa("npm install", {cwd: getRootPath(), stido: [2, 2, 2] });
测试一下
启动成功~
# 5. 格式化代码
使用prettier
的API调用的形式来格式化我们的代码 API · Prettier (opens new window)
安装 npm i prettier
分别在indexTemplate.js和packageTemplate.json中使用
...
import prettier from "prettier";
export function createIndexTemplate(inputConfig) {
...
// 返回渲染后的模板
return prettier.format(code, {
parser: "babel",
});
}
...
import prettier from "prettier";
export function createPackageTemplate(inputConfig) {
...
// 返回渲染后的模板
return prettier.format(code, {
parser: "json",
});
}
可以自己测试一下效果
# 6. 处理路径问题
最后我们处理一下我们的路径问题将我们之前写的 相对路径
改成 绝对路径
首先是项目根路径
...
import path from "path";
...
function getRootPath() {
// 拼接 根路径 + 项目名 得到项目根路径
return path.resolve(process.cwd(), inputConfig.packageName);
}
然后就是indexTemplate.js导入index.ejs的路径问题
在commonjs中可以使用__dirname来获取路径,但是我们这里使用的是esm,所以我们用url中的fileURLToPath来处理
...
import { fileURLToPath } from "url";
import path from "path";
...
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 读取ejs模板
const template = fs.readFileSync(
path.resolve(__dirname, "./template/index.ejs"),
"utf-8"
);
...
那在packageTempalte.js中就是一样的操作了
# 五、发布
后面的步骤就和之前的一样啦~ 【青训营Pro】🛠️从0到1实现一个自己的前端约定路由项目脚手架🏗️ 工具~ - 掘金 (juejin.cn) (opens new window)首先创建一个全局的指令来创建项目,然后上传至npm仓库
将之前写的代码都放在bin目录下,然后修改package.json中的内容和之前的步骤一样 (opens new window)
把项目通过 npm link
链接到全局, 然后可以通过npm root -g
查看全局的npm信息
可以看到此时项目已经链接到全局了,通过指令 learn-koa-setup-yk
就可以启动我们的脚手架项目了
npm config set registry=https://registry.npmjs.org
npm publish
npm config set registry=https://registry.npm.taobao.org
可以分享给别人你写好的脚手架啦 npm i learn-koa-setup-yk
最后,欢迎关注我的专栏,和YK菌做好朋友