青训营pro️从0到1实现一个自己的前端约定路由项目脚手架️工具~(代码片段)

YK菌 YK菌     2023-01-15     303

关键词:


前言

今天我们来复习&总结青训营实战班第一天的课程,也就是全栈然叔带来的Node基础Api与CLI实战课程。

今天主要学习如何创建一个自己的脚手架工具 🛠️,这样可以避免每次写小demo的时候要进行各种重复配置 🕹️。最后我们发布到npm仓库,让周围的小伙伴也可以使用你写的工具~ 其实我们今天主要要学习的是一种自动化的思想,这也是前端工程化重要的一环。

本文从 ① Node基础 到 ② 脚手架搭建 到 ③ npm仓库上传 ,完全从0到1造轮子,没有基础的小伙伴也能看懂~
PS:这是第一篇,后面还有一篇 我们用happy-path小步走的编码方式再写一个koa的脚手架工具,你会收获更多~~ 点赞 👍 越多更新越快 ✈️

相关的代码在这里https://gitee.com/ykang2020/youth_camp_ykjun

一、Node基础API

在字节跳动青训营基础班,我们学习了一些Node.js的基础概念,今天我们先来学习下Node.js的一些基础API,为我们后面实践部分做好铺垫 🏃‍♂️ 🏃‍ ~

PS: 基础班的课程笔记博文在这里:

node.jsAPI 官方文档 中文:http://nodejs.cn/api/ 英文:https://nodejs.org/dist/latest-v14.x/docs/api/

核心API - 无需 require

  • buffer
  • module
  • process

内置API - 需要 require 无需 install

  • os
  • fs
  • path
  • http
  • event

1. fs与异步IO

参考:http://nodejs.cn/api/fs.html 以及 https://nodejs.org/dist/latest-v14.x/docs/api/fs.html

首先引入 fs 文件模块

const fs = require("fs");

① 同步读取文件

// 同步读取
const data = fs.readFileSync("./04_buffer.js");
console.log("data", data.toString());

这里如果不调用 datatoString() 方法,结果则会返回一个 buffer ,因为对于计算机来说,它所存储的所有文件都是二进制文件

使用 datatoString() 方法是默认将二进制文件按 utf-8 转换成文本,也就是默认调用 toString('utf-8')

② 异步读取文件

错误优先的回调函数,在回调函数中进行文件处理操作

fs.readFile("./04_buffer.js", (err, data) => 
  if (err) throw err;
  console.log(data.toString());
)

我们知道使用回调形式进行异步操作不好,我们现在都是用 promise 了,所以自然的想到可以将异步文件读取有没有 promise 风格的API可以使用,然后就可以使用 async/await 来使用异步文件操作

Node.js提供了Promise风格的fs文件操作API,可以看文档

③ promisify

在Node.js的 util 中提供了一个方法 promisify ,可以将回调的方法修饰成 promise 风格的API,就可以使用 async/await 语法了

(async () => 
  const fs = require("fs");
  const  promisify  = require("util");
  const readFile = promisify(fs.readFile);
  
  const data = await readFile("./04_buffer.js");
  console.log(data.toString());
)();

2. buffer与字符集

buffer 翻译过来就是缓冲区

分配一个10个字节的 buffer

const buf1 = Buffer.alloc(10)
console.log(buf1) // <Buffer 00 00 00 00 00 00 00 00 00 00>

打印输出看看,方便显示阅读,转换成了十六进制,底层当然是二进制存储的

下面使用 buffer 存储一个英文文字,一个字母一个字节

const buf2 = Buffer.from('yk')
console.log(buf2) //<Buffer 79 6b>

使用 buffer 存储一个中文文字,一个文字三个字节(utf-8)

const buf3 = Buffer.from('菌')
console.log(buf3) // <Buffer e8 8f 8c>   utf-8/16/32

可以连接两个 buffer

const buf4 = Buffer.concat([buf2, buf3])
console.log(buf4, buf4.toString()) // <Buffer 79 6b e8 8f 8c> yk菌

之前也说过,使用 toString 方法就可以显示文字

3. http

使用 http 模块快速写一个 http 服务器

const http = require("http");

http
  .createServer((request, response) => 
    console.log("a request");
    response.end("Hi YK菌");  // 这个end不太好,不好理解
  )
  .listen(3000, () => 
    console.log("Server at 3000");
  );

我们来看看原型链,先来定义一个函数,来获取对象的原型链

function getPrototypeChain(obj) 
  const protoChain = [];
  while ((obj = Object.getPrototypeOf(obj))) 
    protoChain.push(obj);
  
  return protoChain;

来看看 requestresponse 的原型链

const http = require("http");

http
  .createServer((request, response) => 
    console.log(
      "a request",
      getPrototypeChain(request),
      getPrototypeChain(response)
    );
    response.end("Hi YK node");
  )
  .listen(3000, () => 
    console.log("Server at 3000");
  );

request的原型链

response的原型链

可以发现 requestresponse 都是继承自 Stream

这个问题我们后面再讨论,我们继续使用我们的 http 模块来搭建服务器

我们返回一个index.html文件,这就用到我们上面说到的 fs 文件读取API了

const fs = require("fs");
const http = require("http");

http
  .createServer((request, response) => 
    const  url, method  = request;
    console.log("url:", url);
    if (url === "/" && method === "GET") 
      fs.readFile("index.html", (err, data) => 
        if (err) 
          response.writeHead(500, 
            "Content-Type": "text/plain;charset=utf-8",
          );
          response.end("500 服务器挂了,兄弟!!!");
        
        response.statusCode = 200;
        response.setHeader("Content-Typr", "text/html");
        response.end(data);
      );
     else 
      response.statusCode = 400;
      response.setHeader("Content-Type", "text/plain;charset=utf-8");
      response.end("404 找不到了呀!");
    
  )
  .listen(3000, () => 
    console.log("Server at 3000");
  );

练习1 node基础 fs\\buffer\\http 搭建一个http服务

写一个http服务器,返回index.html

如果我们的index.html文件中添加了一个img标签,引入了一张图片,我们应该怎么处理?直接用 readFile 读取可以吗?图片一般都比较大,这样会大量消耗内存资源,不要直接使用 readFile 读取, 因为它要把全部图片内容加载到服务器。

那应该用什么? 我们引入下面 Stream 流的概念

4. stream

在本地复制一个图片

const fs = require("fs");
// 图片复制  使用 fs read+write也可以,但是需要读到内存再从内存中写出去

// 我们换一种方式
const rs = fs.createReadStream("./YK菌.jpg");
const ws = fs.createWriteStream("./YK菌分身.jpg");

// 管道,文件流,从一个文件流到另一个文件
rs.pipe(ws);

文件流,从一个文件流到另一个文件

所以我们就可以这样编写代码

const fs = require("fs");
const http = require("http");

http
  .createServer((request, response) => 
    const  url, method  = request;
    console.log("url:", url);
    if (url === "/" && method === "GET") 
      fs.readFile("index.html", (err, data) => 
        if (err) 
          response.writeHead(500, 
            "Content-Type": "text/plain;charset=utf-8",
          );
          response.end("500 服务器挂了,兄弟!!!");
        
        response.statusCode = 200;
        response.setHeader("Content-Typr", "text/html");
        response.end(data);
      );
     else if (url === "/users" && method === "GET") 
      response.writeHead(200,  "Content-Type": "application/json" );
      response.end(JSON.stringify( name: "yk菌" ));
     else if (method === "GET" && headers.accept.indexOf("image/*" !== -1)) 
      // 拿到所有图片
      // 不要直接使用readFile读取, 因为它要把全部图片内容加载到服务器
      // 使用 流 stream !!!!
      fs.createReadStream("." + url).pipe(response);
     else 
      response.statusCode = 400;
      response.setHeader("Content-Type", "text/plain;charset=utf-8");
      response.end("404 找不到了呀!");
    
  )
  .listen(3000, () => 
    console.log("Server at 3000");
  );

在index.html添加图片,然后在服务器返回图片的时候使用流的形式返回,这样可以节省内存

5. 子进程

child_process模块给予Node可以随意创建子进程(child_process)的能力。

它提供了4个方法用于创建子进程。

  • spawn():启动一个子进程来执行命令。
  • exec():启动一个子进程来执行命令,与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。
  • execFile():启动一个子进程来执行可执行文件。
  • fork():与spawn()类似,不同点在于它创建Node的子进程只需指定要执行的JavaScript文件模块即可。

二、实战:实现 cli 工具

目标:利用我们学过的Node.js的知识,实现一个自己的脚手架工具,脚手架用来快速创建一个项目,一个会自动生成前端路由的模板项目。我们叫它约定路由

1. 初始化

新建一个项目目录,并初始化

mkdir vue-auto-router-cli
cd vue-auto-router-cli
npm init -y

创建一个文件夹bin,然后创建一个yk.js文件,作为入口文件

要让全局可以使用 yk 指令,在 package.json 中加入这个配置项

"bin": 
  "yk": "./bin/yk.js"
,

在 yk.js 头部加上,用来指定解释器类型为node

#!/usr/bin/env node
console.log('欢迎使用YK菌的 cli 工具');

最后别忘了 link 一下

npm link

此时在任意目录下的控制台中执行 yk 指令 后台就都会用 node 执行yk.js文件

2. 定制命令行页面

接下来我们来定制一个我们自己的命令行页面

#!/usr/bin/env node
console.log("欢迎使用YK菌的 cli 工具");
console.log(process.argv);

可以看到,用户输入的信息都保存在process.argv

我们先安装一下接下来要用到的所有的第三方库

npm i commander download-git-repo ora@5 handlebars figlet clear chalk open -s

这里我们使用一个第三方库 commander 来定制我们的命令行

我们的yk.js这样编写

#!/usr/bin/env node
// console.log("欢迎使用YK菌的 cli 工具");
// console.log(process.argv);
const program = require("commander");

program.version(require("../package.json").version);

program
  .command("init <name>")
  .description("init project")
  .action((name) => 
    console.log("init " + name);
  );
program.parse(process.argv);

现在再使用我们的 yk 指令,就像点样子了~

3. 定制初始化项目的欢迎界面

我们将program.action中不可能只打印一句话,我们把初始化项目的逻辑抽离出来

program.action(require("../lib/init"))

我们就创建文件 lib/init.js

// 打印欢迎界面
const  promisify  = require("util");
const figlet = promisify(require("figlet"));
const clear = require("clear");
const chalk = require("chalk");
// 封装一个输出绿色文字的API
const log = (content) => console.log(chalk.green(content));

module.exports = async (name) => 
  // 打印欢迎界面
  clear();

  const data = await figlet.textSync("YK!Welcome");
  log(data);

此时使用 yk 初始化一个项目的时候,就会出现欢迎页面了~

当然这里的文字的样式可以自己定制 可以看看文档 https://www.npmjs.com/package/figlet

这里我就随便配置一个字体样式了~

const data = await figlet.textSync("YK!Welcome",
  font: "Ghost",
  horizontalLayout: "default",
  verticalLayout: "default",
  width: 200,
  whitespaceBreak: true,
);

4. 下载代码模板

我们新建一个文件用来处理下载代码模板的逻辑

新建 lib/download.js

const  promisify  = require("util");

module.exports.clone = async function (repo, desc) 
  const download = promisify(require("download-git-repo"));

  const ora = require("ora"); // 进度条
  const process = ora(`✈下载.......$repo`);

  await process.start();
  await download(repo, desc);
  process.succeed();
;

这里用到两个库 download-git-repo 库用来下载代码模板 和 ora 库用来显示下载进度条(这里我们安装的是5版本,最新的是6版本,为什么不用最新版呢,因为最新版只支持ESM模块化引入)

在GitHub上面准备一个代码模板

在init.js中引入并使用下载模块即可

const  clone  = require("./download");

module.exports = async (name) => 

  log("创建项目" + name);
  // 这里的git仓库可以自己指定
  await clone("github:yk2012/vue-template", name);
;

可以看到文件就已经被下载到当前目录下了

5. 自动安装项目依赖

在Node.js中使用子进程 child_process 安装依赖

创建一个子进程安装的 promise 风格的接口,然后还要合并子进程的流到主进程中

const spawn = async (...args) => 
  // 同步Promise api
  const  spawn  = require("child_process");
  return new Promise((resolve) => 
    
    // windows系统兼容性处理
    const options = args[args.length - 1];
    if (process.platform === "win32") 
      options.shell = true;
    

    const proc = spawn(...args);
    // 输出流 子进程 合并到 主进程
    proc.stdout.pipe(process.stdout);
    proc.stderr.pipe(process.stderr);
    proc.on("close", () => 
      resolve();
    );
  );
;

注意中间有一段处理Windows兼容性问题的代码。

调用子进程安装依赖 (就是在项目目录下执行npm install

// 下载依赖 npm i
// 子进程
// spawn("npm", ["install"]);
log(`安装依赖....`);
await spawn("npm", ["install"],  cwd: `./<

青训营pro自定义脚手架从1到∞-自动化思维搭建koa脚手架(代码片段)

...6.处理路径问题五、发布本文相关代码:youth_camp_ykjun:青训营&开课吧实战课程代码仓库-Gitee.com一、前言今天来复习&总结青训营实训营第二天的内容,在上次课程中我们从0到1创建了一个vue的脚手架,今天我们跟... 查看详情

青训营pro前端框架设计理念-vue3动机-手写实现mini-vue(代码片段)

...2.响应式①reactive②依赖收集3.虚拟DOM4.diffpatch更新diff对比青训营实战班的课程也结束了,今天先来撸一遍周五杨村长带来的mini-vue课程。错过课程的小伙伴一 查看详情

青训营pro前端框架设计理念-vue3动机-手写实现mini-vue(代码片段)

...2.响应式①reactive②依赖收集3.虚拟DOM4.diffpatch更新diff对比青训营实战班的课程也结束了,今天先来撸一遍周五杨村长带来的mini-vue课程。错过课程的小伙伴一 查看详情

青训营pro自定义脚手架从1到∞-自动化思维搭建koa脚手架(代码片段)

嗨!~大家好,我是YK菌🐷,一个微系前端✨,爱思考,爱总结,爱记录,爱分享🏹,欢迎关注我呀😘~[微信公众号:ykyk2012]文章目录一、前言二、分析vue-cli三、起步四、完善1.生成i... 查看详情

结营啦!有缘相聚于青训,未来高处见呀~~(代码片段)

📸叮!记·字节跳动第一届青训营顺利结营啦!从8月份的青训营,到9月份的实训营,搁置了许久的结营心得终于拾起来辽!🎬开营进行时从答疑会开始,负责人仔细的阐述了本次训练营的性质和... 查看详情

字节跳动青训营每日一练编程题

1:实现一个36进制的加法0-9a-z。#include<bits/stdc++.h>typedeflonglongll;constllN=2e5+10;usingnamespacestd;ints[N];intmain()int_;stringa,b;while(cin>>a>>b)stringans;intpos 查看详情

从0到1实现一个android路由——初探路由(代码片段)

从0到1实现一个Android路由系列文章从0到1实现一个Android路由(1)——初探路由从0到1实现一个Android路由(2)——URL解析器从0到1实现一个Android路由(3)——APT收集路由从0到1实现一个Android路由(4)——多模块的APT收集路由从0到1实现一个A... 查看详情

青训营html基础-语义化标签-浏览器渲染过程-笔记及拓展(代码片段)

...)博主还是很久之前学习的,这次趁着字节跳动青训营的活动,就再学习一遍HTML。一小时的课程,巩固了之前的一些知识,也学到了很多新知识。我把这次课程的内容与我这一年来学习前端的经验相结合,... 查看详情

从0到1实现一个android路由——拦截请求再跳转(代码片段)

从0到1实现一个Android路由系列文章从0到1实现一个Android路由(1)——初探路由从0到1实现一个Android路由(2)——URL解析器从0到1实现一个Android路由(3)——APT收集路由从0到1实现一个Android路由(4)——多模块的APT收集路由从0到1实现一个A... 查看详情

对于go语言的进阶与依赖管理|青训营笔记(代码片段)

一.Go语言进阶与依赖管理1.1并发和并行Go可以充分发挥多核优势,高效运行。多线程程序在单核心的cpu上运行,称为并发;多线程程序在多核心的cpu上运行,称为并行。并发与并行并不相同,并发主要由切换时间片来实现“同时... 查看详情

字节跳动青训营--前端day8(代码片段)

文章目录前言一、CSR,SSR,SSG1.CSR2.SSR3.SSG4.SSR,SSG的优势利于SEO更短的首屏时间二、什么是Next.js三、Next.js客户端开发1.Api2.CSSModules3.Layout4.文件式路由四、Next.js服务端开发前言仅以此文章记录学习历程。一、CSR,SS... 查看详情

字节跳动青训营每日一练编程题

1:实现一个36进制的加法0-9a-z。#include<bits/stdc++.h>typedeflonglongll;constllN=2e5+10;usingnamespacestd;ints[N];intmain()int_;stringa,b;while(cin>>a>>b)stringans;intpos;reverse(a.begin(),a.end());reverse(b.begin(),b.end());for(inti=0;i<max(a... 查看详情

青训营月影老师告诉我写好javascript的三大原则之——各司其责(代码片段)

YK菌从大学开始到现在一共学习了挺多语言的(C/java/matlab/pathon/),最后终于确定了JavaScript是我的本命语言,刚开始学的时候觉得JavaScript上手真的很快,很简单,但是随着自己学习的深入,发现JavaScript真的特别... 查看详情

青训营月影老师告诉我写好javascript的四大技巧——妙用特性(代码片段)

文章目录起步案例1:判断4的幂菜鸟版菜鸟学废版进阶版JS特性技巧版案例2:深拷贝平平无奇普通版JS特性技巧版案例3:归并排序《算法(第四版)》Java直译版JS特性技巧版一些小技巧转进制交换元素总结更多... 查看详情

青训营月影老师告诉我写好javascript的三大原则之——过程抽象(代码片段)

...5.总结过程抽象/HOF/装饰器命令式/声明式参加了这次字节青训营的活动,见到了传说中的月影老师࿰ 查看详情

[前端学习]从0到1做一个vue风格的todolist(vue牛刀小试)(代码片段)

文章目录前言整体的目录结构:App.vueMyHeader.vueMyList.vueMyFooter.vueMyItem.vue运行效果:❤️往期精彩回顾❤️:前言大家好,我是程序员manor,我希望自己能成为国家复兴道路的铺路人,大数据领域的耕耘者,平凡但不甘于平庸的人。学... 查看详情

[前端学习]从0到1做一个vue风格的todolist(vue牛刀小试)(代码片段)

文章目录前言整体的目录结构:App.vueMyHeader.vueMyList.vueMyFooter.vueMyItem.vue运行效果:❤️往期精彩回顾❤️:前言大家好,我是程序员manor,我希望自己能成为国家复兴道路的铺路人,大数据领域的耕耘者,平凡但不甘于平庸的人。学... 查看详情

青训营node.js基础-web应用开发-开发调试-线上部署(代码片段)

文章目录Web应用开发HTTP模块Koa介绍中间件常用中间件基于Koa的前端框架调试断点调试日志调试线上部署利用多核CPU进程守护复杂计算前端开发与后端开发对比前几天学了一些Node.js的基础,今天来学习Web应用开发,在开发... 查看详情