本周代码提交分支至:lesson05

第一章:本周导学


1-1本周整体内容介绍和学习方法

收获

  • 完成脚手架创建项目流程开发
  • 命令行交互方法
  • 服务端框架egg.js应用和API开发方法
  • egg.js集成mongodb

第二章 脚手架项目创建功能架构设计


2-1 脚手架创建项目功能架构背后的思考

**
架构背后的思考

  • 可扩展:能够快速复用到不同团队,适用不同团队的差异。
  • 低成本:在不改动脚手架源码的基础上,新增模版,且新增模板的成本很低。
  • 高性能:控制存储空间,安装时充分利用Node多进程提升安装性能。

2-2 项目创建前准备阶段架构设计

init
**

2-3 下载项目模板阶段架构设计

downloadTemplate

第三章 项目基本信息获取功能开发(详解命令行交互)


3-1 项目创建准备阶段——判断当前目录是否为空功能开发

本周代码从commands/init/lib/index.js文件中的exec方法开始启动。 根据上面的两小节分析,exec方法的代码逻辑为:

  • 准备阶段 【this.prepare()】
  • 下载模版
  • 安装模版(下周实现)

prepare方法的代码逻辑为:

  1. 判断当前目录是否为空
  2. 是否强制清空
  3. 选择创建项目或组件
  4. 获取项目/组件的基本信息

本节主要实现的代码是判断当前目录是否为空

  1. prepare(){
  2. if(!this.isCwdEmpty()){
  3. // 询问是否继续创建
  4. }
  5. }
  6. isCwdEmpty(){
  7. const localPath = process.cwd();
  8. let fileList = fs.readdirSync(localPath)
  9. // 文件过滤逻辑
  10. fileList = fileList.filter((file)=>{
  11. !file.startsWith('.') && ['node_modules'].indexOf <=0
  12. })
  13. }

本节知识点:

  • 拿到当前目录的方法一:process.cwd()
  • 拿到当前目录的方法二: path.resolve(‘.’)
  • path.resolve(__dirname):拿到的是当前执行代码的目录
  • 读取当前目录下的文件列表:fs.readdirSync()

3-2 inquirer基本用法和常用属性入门

**

继续写代码前,首先在测试项目里体验inquirer

  1. const inquirer = require('inquirer')
  2. inquirer
  3. .prompt([{
  4. type:'input',
  5. name:'name',
  6. message:'your name:',
  7. default:'liugezhou',
  8. validate:function(v){ //对输入的参数进行校验,检验通过可进行下一步
  9. return typeof v === 'string'
  10. },
  11. filter:function(v){ //对用户输入的内容进行优化返回
  12. return v+'!'
  13. },
  14. transformer: function(v){ //相当于一个placeholder显示作用
  15. return 'name :'+ v
  16. }
  17. },{
  18. type:'number', // inquirer可以传入数组
  19. name:'age',
  20. message:'your age:',
  21. default:'18'
  22. }])
  23. .then(answers => {
  24. console.log(answers.name)
  25. console.log(answers.age)
  26. })
  27. .catch(error => {
  28. if(error.isTtyError) {
  29. console.log('error')
  30. } else {
  31. // Something else when wrong
  32. }
  33. });

3-3 inquirer其他交互形式演示

**

本节主要对list、rawlist、expand、confirm、checkbox等进行了功能与代码测试 测试代码提交至 inquirer

3-4 强制清空当前目录功能开发

**

本节主要是清空当前目录,进行清空下,使用命令行交互inquirer问询,以及用 force这个参数添加业务逻辑,进行目录的清空判断

清空目录功能主要是使用了第三方库fs-extraemptyDirSync(localPath)方法。

3-5 获取项目基本信息功能开发

本节使用inquirer进行了项目或者组件的选择询问、以及版本号控制台输入功能,但未对输入内容进行校验 这里调整好代码逻辑即可。

3-6 项目名称和版本号合法性校验

本节的主要内容为合法项目名称的正则校验

  1. function isValidName(v) {
  2. // 规则一:输入的首字符为英文字符
  3. // 规则二:尾字符必须为英文或数字
  4. // 规则三:字符仅允许-和_两种
  5. // \w=a-zA_Z0-9_
  6. return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)
  7. }

正则表达式规则: 首字符:^ 尾字符:$ \w=a-zA-Z0-9 首字符必须为英文字符:/^[a-zA-Z]$/ 中间可以为英文数字或者-:/^[a-zA-Z]+[\w-]/ 尾字符必须为英文或者数字:/^[a-zA-Z][\w-]*[a-zA-Z]$/.test(v)

以上表达式规则,没有处理当项目名称为一个的时候的问题

给出不合法的命名有:1,a-,a,a1,a-1 /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])$/.test(v)_

PS:3-6这里关于检验性输错后依旧报错的问题,没有得到解决,先暂缓了!

第4章 预备知识:egg.js + 云 mongodb 快速入门


4-1 下载项目模板功能实现流程分析+egg.js简介

上一节我们获得了项目信息,这一节通过获得的项目信息进行模板的下载

通过项目模版API获取项目模版信息

  • 通过egg.js搭建一套后台系统 (4-2 至 4-5)
  • 通过npm存储项目模版
  • 将项目模版信息存储到mongodb数据库中
  • 通过egg.js获取mongodb中的数据并且通过API将其返回

在进行egg.js快速搭建后台系统前,对egg.js + 云mongodb进行一个快速的入门学习。

  • egg基于koa2生成的一个企业级框架。

4-2 cloudscope-cli-server后端项目创建


快速搭建项目**

  • mkdir cloudscope-cli-server
  • cd cloudscope-cli-server
  • npm init egg —type=simple
  • npm i
  • npm run dev

这里需要注意的一点是:npm init egg 实际执行的是 create-egg这个包。

4-3 通过egg.js框架添加新的API

**

本节主要多egg脚手架进行了简单演示,将原路由home以及文件删除,新建了project/template路由以及controller。 路由在app/router.js中

  1. 'use strict';
  2. /**
  3. * @param {Egg.Application} app - egg application
  4. */
  5. module.exports = app => {
  6. const { router, controller } = app;
  7. router.get('/project/template', controller.project.getTemplate);
  8. };

controller of project

  1. 'use strict';
  2. const Controller = require('egg').Controller;
  3. class ProjectController extends Controller {
  4. async getTemplate() {
  5. const { ctx } = this;
  6. ctx.body = 'get template';
  7. }
  8. }
  9. module.exports = ProjectController;

4-4 云mongodb开通+本地mongodb调试技巧讲解

本地安装mongodb:https://www.runoob.com/mongodb/mongodb-osx-install.html

启动:

  1. 终端输入:mongod
    1. 报错,提示找不到/data/db目录
  2. 添加dbpath:
    1. 在本地新建 /Users/liugezhou/data/db目录
    2. 同步启动方式:mongod —dbpath=/Users/liugezhou/data/db
    3. 异步启动:在后面添加 —fork

注:由于我本地之前已经配好了,所以我本地的启动方式为:mongod —config /usr/local/etc/mongod.conf 查看mongodb.conf文件,我本地的dbpath路径为:/usr/local/var/mongodb

安装第三方工具:Robot 3T 连接到本地后:

  • create database (liugezhou-cli)
  • create collection (project)
  • insert Doucument (添加数据)
  • add user (cloudscope/cloudscope)

4-5 egg.js接入mongodb方法

**

本地mongodb数据库创建完成后,开始连接我们的本地数据库。

回到上节新创建的项目,sam老师安装的第三方依赖为

  • app下新建utils/mongo.js
  • npm i -S @pick-star/cli-mongodb

由于@pick-star/cli-mongodb代码较少,我这里选择不安装,本地敲一遍代码:

  • cnpm i -S npmlog mpngodb
  • 在utils目录下新建log.js ```javascript ‘use strict’

const npmlog = require(‘npmlog’) log.level = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : ‘info’; log.headingStyle = { fg: ‘red’, bg: ‘white’ }; log.heading = ‘liugezhou’; log.addLevel(‘success’, 2500, { fg: ‘green’ });

module.exports = log;

  1. > - utils目录下新建mongodb.js(@pick-star/cli-mongodb源码)
  2. > ```javascript
  3. 'use strict';
  4. const MongoClient = require('mongodb').MongoClient;
  5. const logger = require('./log');
  6. class Mongo {
  7. constructor(url) {
  8. this.url = url;
  9. }
  10. connect() {
  11. return new Promise((resolve, reject) => {
  12. MongoClient.connect(
  13. this.url,
  14. {
  15. useNewUrlParser: true,
  16. useUnifiedTopology: true,
  17. },
  18. (err, client) => {
  19. if (err) {
  20. reject(err);
  21. } else {
  22. const db = client.db();
  23. resolve({ db, client });
  24. }
  25. });
  26. });
  27. }
  28. connectAction(docName, action) {
  29. return new Promise(async (resolve, reject) => {
  30. const { db, client } = await this.connect();
  31. try {
  32. const collection = db.collection(docName);
  33. action(collection, result => {
  34. this.close(client);
  35. logger.verbose('result', result);
  36. resolve(result);
  37. }, err => {
  38. this.close(client);
  39. logger.error(err.toString());
  40. reject(err);
  41. });
  42. } catch (err) {
  43. this.close(client);
  44. logger.error(err.toString());
  45. reject(err);
  46. }
  47. });
  48. }
  49. query(docName) {
  50. return this.connectAction(docName, (collection, onSuccess, onError) => {
  51. collection.find({}, { projection: { _id: 0 } }).toArray((err, docs) => {
  52. if (err) {
  53. onError(err);
  54. } else {
  55. onSuccess(docs);
  56. }
  57. });
  58. });
  59. }
  60. insert(docName, data) {
  61. return this.connectAction(docName, (collection, onSuccess, onError) => {
  62. collection.insertMany(data, (err, result) => {
  63. if (err) {
  64. onError(err);
  65. } else {
  66. onSuccess(result);
  67. }
  68. });
  69. });
  70. }
  71. remove(docName, data) {
  72. return this.connectAction(docName, (collection, onSuccess, onError) => {
  73. collection.deleteOne(data, (err, result) => {
  74. if (err) {
  75. onError(err);
  76. } else {
  77. onSuccess(result);
  78. }
  79. });
  80. });
  81. }
  82. update() {
  83. }
  84. close(client) {
  85. client && client.close();
  86. }
  87. }
  88. module.exports = Mongo;

utils/mongo.js 代码修改:

  1. 'use strict';
  2. const Mongodb = require('./mongodb');
  3. const { mongoDbName } = require('../../config/db');
  4. function mongo() {
  5. return new Mongodb(mongodbUrl);
  6. }
  7. module.exports = mongo;

接着,在mongo.js暴露出去

  1. 'use stirct'
  2. const Mongodb = require('./mongodb')
  3. const { mongoDbUrl,mongodbName} = require('../../config/db') // 配置这两个参数
  4. function mongo(){
  5. return new Mongodb(mongoDbUrl,mongodbName)
  6. }
  7. // config/db.js
  8. 'use strict';
  9. // Mondodb
  10. const mongodbUrl = `mongodb://${user}:${pass}@liugezhou.com:27017/${database}`;
  11. module.exports = {
  12. mongodbUrl,
  13. };

最后,在Controller的project中访问:

  1. const mongo = require('./mongo.js')
  2. async getTemplate(){
  3. const { ctx } = this;
  4. const data = await mongo().query('project');
  5. ctx.body = data;
  6. }

第五章 项目模板开发 + 获取项目模板 API 开发


5-1 脚手架初始化项目模版开发

模版项目代码提交至:liugezhou-cli-dev-template

项目模板建好后,npm publish发布至npm。

5-2 脚手架请求项目模板API开发


回到脚手架项目**

  • 在utils下创建包: lerna create @cloudscope-cli/request
  • cd utils
  • npm i -S axios
  1. // utils/request/lib.index
  2. const axios = require('axios')
  3. const baseURL = process.env.CLI_URL?process.env.CLI_URL:'https://liugezhou.com:7001'
  4. const request = axios.create({
  5. baseURL,
  6. timeout:5000
  7. })
  8. request.interceptors.response.use({
  9. response =>{
  10. return response.data
  11. },
  12. error =>{
  13. return Promise.reject(error)
  14. }
  15. })
  16. module.exports = request

commands/init引入@cloudsope-cli/request包 新建 lib/getProjectTemplate

  1. const request = require('@cloudscope-cli/request')
  2. modules.exports = function(){
  3. return request({
  4. url:'/project/template'
  5. })
  6. }
  1. // commands/init/lib/index.js
  2. const getProjectTemplate = require('./getProjectTemplate')
  3. const template = getProjectTemplate()

最后在测试项目下测试,打印template,成功。

5-3 通过环境变量配置默认URL+选择项目模板功能开发

本章内容回顾了process.env的配置,以及inquirer新添加询问需要选择的项目模版是什么。

5-4 基于vue-element-admin开发通用的中后台项目模板

5-1 中已将项目模版更新至git仓库,且已发布到npm中。 只需要在mongodb数据库将后台模版name、npmName、version添加后即可。

第六章 脚手架项目模板下载功能开发


6-1 脚手架下载项目模板功能开发

**
本节的主要内容为项目模版的安装

  1. // commands/init/lib/index.js
  2. async downloadTemplate(){
  3. const {projectTemplate} = this.projectInfo
  4. const templateInfo = this.template.find(item=> item.npmName === projectTemplate)
  5. const targetPath = path.resolve(userHome,'.cloudscope-cli','template')
  6. const storeDir = path.resolve(userHome,'.cloudscope-cli','template','node_modules')
  7. const {npmName,version} = templateInfo
  8. const templatePkg = new Package({
  9. targetPath,
  10. storeDir,
  11. packageName:npmName,
  12. packageVersion:version
  13. })
  14. if(await templatePkg.exists()){
  15. // 更新package
  16. log.verbose('更新template')
  17. await templatePkg.update();
  18. }else{
  19. // 安装package
  20. log.verbose('安装template')
  21. await templatePkg.install();
  22. }
  23. }

6-2 通过spinner实现命令行loading效果

**

首先在utils/utils中添加spinnerStart和sleep方法

  1. // utils/utils/lib/index.js
  2. function spinnerStart(msg,spinnerString ='|\-\\'){
  3. const Spinner = require('cli-spinner').Spinner
  4. const spinner = new Spinner(`${msg} %s`)
  5. spinner.setSpinnerString(spinnerString)
  6. spinner.start()
  7. return spinner
  8. }
  9. function sleep(timeout = 1000){
  10. return new Promise(resolve => setTimeout(resolve,timeout))
  11. }

然后在commands/init/lib/index.js中将spinner引入使用、测试。

6-3 项目模板更新功能调试

**

本节主要是安装功能的测试,以及第一次安装模版不存在时,关于spinner.stop的finnal处理。

PS:在本节完成之后,发布至npm,本地全局安装的时候,出现错误,还未找到原因。
image.png

第七章:本周加餐:inquirer源码解析:彻底搞懂命令行交互原理


7-1 本章学习路径和目标

  • 掌握 readline/events/stream/ansi-escapes/rxjs
  • 掌握命令行交互的实现原理,并实现一个可交互的列表
  • 分析inquirer源码掌握其中的关键实现

7-2 readline的使用方法和实现原理

**

readline是Node.js中的一个内置库,主要是用来管理输入流的

  1. const readline = require('readline')
  2. const rl = readline.createInterface({
  3. input:process.stdin,
  4. output:process.stdout
  5. })
  6. rl.question('your name:',(answer =>{
  7. console.log('your name is:'+answer)
  8. rl.close()
  9. }))

源码分析:

  • 强制将函数转为构建函数
    1. function Interface(input, output, completer, terminal) {
    2. if (!(this instanceof Interface)) {
    3. return new Interface(input, output, completer, terminal);
    4. }
    5. …………
    6. }
  • 获得事件驱动能力:EventEmitter.call(this);
  • 监听键盘事件: ```typescript emitKeypressEvents(input, this);

// input usually refers to stdin input.on(‘keypress’, onkeypress); input.on(‘end’, ontermend);

  1. <a name="DV7nQ"></a>
  2. #### 7-3 高能:深入讲解readline键盘输入监听实现原理
  3. > 略
  4. **7-4 秀操作:手写readline核心实现**
  5. ```typescript
  6. function setpread(callback){
  7. function onkeypress(s){
  8. output.write(s);
  9. line += s
  10. switch (s) {
  11. case '\r':
  12. input.pause();
  13. callback(line)
  14. break;
  15. default:
  16. break;
  17. }
  18. }
  19. const input = process.stdin;
  20. const output = process.stdout;
  21. let line =''
  22. emitKeypressEvents(input)
  23. input.on('keypress',onkeypress)
  24. input.setRawMode(true) //进入原生模式
  25. input.resume()
  26. }
  27. function emitKeypressEvents(stream){
  28. function onData(chunk){
  29. g.next(chunk.toString())
  30. }
  31. const g = emitKeys(stream)
  32. g.next()
  33. stream.on('data',onData)
  34. }
  35. function* emitKeys(stream){
  36. while (true) {
  37. let ch = yield;
  38. stream.emit('keypress',ch)
  39. }
  40. }
  41. setpread(function(s){
  42. console.log('answer:',s)
  43. })

7-5 命令行样式修改的核心原理:ansi转义序列讲解

ansi-escape-code:ansi转义序列 定义的一个规范,终端通过转义字符实现特殊操作。 通过这里查询:https://handwiki.org/wiki/ANSI_escape_code

  1. // 固定格式为:( \x1B[ ) + ('通过上面网站查询出来的参数')
  2. console.log('\x1B[31m\x1B[4m%s','your name:')
  3. console.log('\x1B[20G%s','test')

7-6 讨论readline

7-7 响应式库rxjs快速入门

rxjs是一个异步的库,和我们的Promise是非常相似的。readline源码大量使用了这个库。

  1. // npm install rxjs
  2. const range = require('rxjs').range;
  3. const { map, filter } = require('rxjs/operators');
  4. const pipe = range(1, 200)
  5. .pipe(
  6. filter(x => x % 2 === 1),
  7. map(x => x + x),
  8. filter(x => x%3 === 0),
  9. filter(x => x%5 === 0)
  10. )
  11. pipe.subscribe(x => console.log(x));

7-8& 7-9 放大招:手写命令行交互式列表组件

image.png

  1. const EventEmitter = require('events');
  2. const readline = require('readline');
  3. const MuteStream = require('mute-stream');
  4. const { fromEvent } = require('rxjs');
  5. const ansiEscapes = require('ansi-escapes');
  6. const option = {
  7. type: 'list',
  8. name: 'name',
  9. message: 'select your name:',
  10. choices: [{
  11. name: 'sam', value: 'sam',
  12. }, {
  13. name: 'shuangyue', value: 'sy',
  14. }, {
  15. name: 'zhangxuan', value: 'zx',
  16. }],
  17. };
  18. function Prompt(option) {
  19. return new Promise((resolve, reject) => {
  20. try {
  21. const list = new List(option);
  22. list.render();
  23. list.on('exit', function(answers) {
  24. resolve(answers);
  25. })
  26. } catch (e) {
  27. reject(e);
  28. }
  29. });
  30. }
  31. class List extends EventEmitter {
  32. constructor(option) {
  33. super();
  34. this.name = option.name;
  35. this.message = option.message;
  36. this.choices = option.choices;
  37. this.input = process.stdin;
  38. const ms = new MuteStream();
  39. ms.pipe(process.stdout);
  40. this.output = ms;
  41. this.rl = readline.createInterface({
  42. input: this.input,
  43. output: this.output,
  44. });
  45. this.selected = 0;
  46. this.height = 0;
  47. this.keypress = fromEvent(this.rl.input, 'keypress')
  48. .forEach(this.onkeypress);
  49. this.haveSelected = false; // 是否已经选择完毕
  50. }
  51. onkeypress = (keymap) => {
  52. const key = keymap[1];
  53. if (key.name === 'down') {
  54. this.selected++;
  55. if (this.selected > this.choices.length - 1) {
  56. this.selected = 0;
  57. }
  58. this.render();
  59. } else if (key.name === 'up') {
  60. this.selected--;
  61. if (this.selected < 0) {
  62. this.selected = this.choices.length - 1;
  63. }
  64. this.render();
  65. } else if (key.name === 'return') {
  66. this.haveSelected = true;
  67. this.render();
  68. this.close();
  69. this.emit('exit', this.choices[this.selected]);
  70. }
  71. };
  72. render() {
  73. this.output.unmute();
  74. this.clean();
  75. this.output.write(this.getContent());
  76. this.output.mute();
  77. }
  78. getContent = () => {
  79. if (!this.haveSelected) {
  80. let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[0m\x1B[2m(Use arrow keys)\x1B[22m\n';
  81. this.choices.forEach((choice, index) => {
  82. if (index === this.selected) {
  83. // 判断是否为最后一个元素,如果是,则不加\n
  84. if (index === this.choices.length - 1) {
  85. title += '\x1B[36m❯ ' + choice.name + '\x1B[39m ';
  86. } else {
  87. title += '\x1B[36m❯ ' + choice.name + '\x1B[39m \n';
  88. }
  89. } else {
  90. if (index === this.choices.length - 1) {
  91. title += ' ' + choice.name;
  92. } else {
  93. title += ' ' + choice.name + '\n';
  94. }
  95. }
  96. });
  97. this.height = this.choices.length + 1;
  98. return title;
  99. } else {
  100. // 输入结束后的逻辑
  101. const name = this.choices[this.selected].name;
  102. let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[36m' + name + '\x1B[39m\x1B[0m \n';
  103. return title;
  104. }
  105. };
  106. clean() {
  107. const emptyLines = ansiEscapes.eraseLines(this.height);
  108. this.output.write(emptyLines);
  109. }
  110. close() {
  111. this.output.unmute();
  112. this.rl.output.end();
  113. this.rl.pause();
  114. this.rl.close();
  115. }
  116. }
  117. Prompt(option).then(answers => {
  118. console.log('answers:', answers);
  119. });

7-10 inquirer源码执行流程分析