本周代码提交分支至: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方法的代码逻辑为:
- 判断当前目录是否为空
- 是否强制清空
- 选择创建项目或组件
- 获取项目/组件的基本信息
本节主要实现的代码是判断当前目录是否为空
prepare(){if(!this.isCwdEmpty()){// 询问是否继续创建}}isCwdEmpty(){const localPath = process.cwd();let fileList = fs.readdirSync(localPath)// 文件过滤逻辑fileList = fileList.filter((file)=>{!file.startsWith('.') && ['node_modules'].indexOf <=0})}
本节知识点:
- 拿到当前目录的方法一:process.cwd()
- 拿到当前目录的方法二: path.resolve(‘.’)
- path.resolve(__dirname):拿到的是当前执行代码的目录
- 读取当前目录下的文件列表:fs.readdirSync()
3-2 inquirer基本用法和常用属性入门
**
继续写代码前,首先在测试项目里体验inquirer
const inquirer = require('inquirer')inquirer.prompt([{type:'input',name:'name',message:'your name:',default:'liugezhou',validate:function(v){ //对输入的参数进行校验,检验通过可进行下一步return typeof v === 'string'},filter:function(v){ //对用户输入的内容进行优化返回return v+'!'},transformer: function(v){ //相当于一个placeholder显示作用return 'name :'+ v}},{type:'number', // inquirer可以传入数组name:'age',message:'your age:',default:'18'}]).then(answers => {console.log(answers.name)console.log(answers.age)}).catch(error => {if(error.isTtyError) {console.log('error')} else {// Something else when wrong}});
3-3 inquirer其他交互形式演示
**
本节主要对list、rawlist、expand、confirm、checkbox等进行了功能与代码测试 测试代码提交至 inquirer
3-4 强制清空当前目录功能开发
**
本节主要是清空当前目录,进行清空下,使用命令行交互inquirer问询,以及用 force这个参数添加业务逻辑,进行目录的清空判断
清空目录功能主要是使用了第三方库fs-extra的emptyDirSync(localPath)方法。
3-5 获取项目基本信息功能开发
本节使用inquirer进行了项目或者组件的选择询问、以及版本号控制台输入功能,但未对输入内容进行校验 这里调整好代码逻辑即可。
3-6 项目名称和版本号合法性校验
本节的主要内容为合法项目名称的正则校验
function isValidName(v) {// 规则一:输入的首字符为英文字符// 规则二:尾字符必须为英文或数字// 规则三:字符仅允许-和_两种// \w=a-zA_Z0-9_return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)}
正则表达式规则: 首字符:^ 尾字符:$ \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中
'use strict';/*** @param {Egg.Application} app - egg application*/module.exports = app => {const { router, controller } = app;router.get('/project/template', controller.project.getTemplate);};
controller of project
'use strict';const Controller = require('egg').Controller;class ProjectController extends Controller {async getTemplate() {const { ctx } = this;ctx.body = 'get template';}}module.exports = ProjectController;
4-4 云mongodb开通+本地mongodb调试技巧讲解
本地安装mongodb:https://www.runoob.com/mongodb/mongodb-osx-install.html
启动:
- 终端输入:mongod
- 报错,提示找不到/data/db目录
- 添加dbpath:
- 在本地新建 /Users/liugezhou/data/db目录
- 同步启动方式:mongod —dbpath=/Users/liugezhou/data/db
- 异步启动:在后面添加 —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;
> - 在utils目录下新建mongodb.js(@pick-star/cli-mongodb源码)> ```javascript'use strict';const MongoClient = require('mongodb').MongoClient;const logger = require('./log');class Mongo {constructor(url) {this.url = url;}connect() {return new Promise((resolve, reject) => {MongoClient.connect(this.url,{useNewUrlParser: true,useUnifiedTopology: true,},(err, client) => {if (err) {reject(err);} else {const db = client.db();resolve({ db, client });}});});}connectAction(docName, action) {return new Promise(async (resolve, reject) => {const { db, client } = await this.connect();try {const collection = db.collection(docName);action(collection, result => {this.close(client);logger.verbose('result', result);resolve(result);}, err => {this.close(client);logger.error(err.toString());reject(err);});} catch (err) {this.close(client);logger.error(err.toString());reject(err);}});}query(docName) {return this.connectAction(docName, (collection, onSuccess, onError) => {collection.find({}, { projection: { _id: 0 } }).toArray((err, docs) => {if (err) {onError(err);} else {onSuccess(docs);}});});}insert(docName, data) {return this.connectAction(docName, (collection, onSuccess, onError) => {collection.insertMany(data, (err, result) => {if (err) {onError(err);} else {onSuccess(result);}});});}remove(docName, data) {return this.connectAction(docName, (collection, onSuccess, onError) => {collection.deleteOne(data, (err, result) => {if (err) {onError(err);} else {onSuccess(result);}});});}update() {}close(client) {client && client.close();}}module.exports = Mongo;
utils/mongo.js 代码修改:
'use strict';const Mongodb = require('./mongodb');const { mongoDbName } = require('../../config/db');function mongo() {return new Mongodb(mongodbUrl);}module.exports = mongo;
接着,在mongo.js暴露出去
'use stirct'const Mongodb = require('./mongodb')const { mongoDbUrl,mongodbName} = require('../../config/db') // 配置这两个参数function mongo(){return new Mongodb(mongoDbUrl,mongodbName)}// config/db.js'use strict';// Mondodbconst mongodbUrl = `mongodb://${user}:${pass}@liugezhou.com:27017/${database}`;module.exports = {mongodbUrl,};
最后,在Controller的project中访问:
const mongo = require('./mongo.js')async getTemplate(){const { ctx } = this;const data = await mongo().query('project');ctx.body = data;}
第五章 项目模板开发 + 获取项目模板 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
// utils/request/lib.indexconst axios = require('axios')const baseURL = process.env.CLI_URL?process.env.CLI_URL:'https://liugezhou.com:7001'const request = axios.create({baseURL,timeout:5000})request.interceptors.response.use({response =>{return response.data},error =>{return Promise.reject(error)}})module.exports = request
commands/init引入@cloudsope-cli/request包 新建 lib/getProjectTemplate
const request = require('@cloudscope-cli/request')modules.exports = function(){return request({url:'/project/template'})}
// commands/init/lib/index.jsconst getProjectTemplate = require('./getProjectTemplate')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 脚手架下载项目模板功能开发
**
本节的主要内容为项目模版的安装
// commands/init/lib/index.jsasync downloadTemplate(){const {projectTemplate} = this.projectInfoconst templateInfo = this.template.find(item=> item.npmName === projectTemplate)const targetPath = path.resolve(userHome,'.cloudscope-cli','template')const storeDir = path.resolve(userHome,'.cloudscope-cli','template','node_modules')const {npmName,version} = templateInfoconst templatePkg = new Package({targetPath,storeDir,packageName:npmName,packageVersion:version})if(await templatePkg.exists()){// 更新packagelog.verbose('更新template')await templatePkg.update();}else{// 安装packagelog.verbose('安装template')await templatePkg.install();}}
6-2 通过spinner实现命令行loading效果
**
首先在utils/utils中添加spinnerStart和sleep方法
// utils/utils/lib/index.jsfunction spinnerStart(msg,spinnerString ='|\-\\'){const Spinner = require('cli-spinner').Spinnerconst spinner = new Spinner(`${msg} %s`)spinner.setSpinnerString(spinnerString)spinner.start()return spinner}function sleep(timeout = 1000){return new Promise(resolve => setTimeout(resolve,timeout))}
然后在commands/init/lib/index.js中将spinner引入使用、测试。
6-3 项目模板更新功能调试
**
本节主要是安装功能的测试,以及第一次安装模版不存在时,关于spinner.stop的finnal处理。
PS:在本节完成之后,发布至npm,本地全局安装的时候,出现错误,还未找到原因。
第七章:本周加餐:inquirer源码解析:彻底搞懂命令行交互原理
7-1 本章学习路径和目标
- 掌握 readline/events/stream/ansi-escapes/rxjs
- 掌握命令行交互的实现原理,并实现一个可交互的列表
- 分析inquirer源码掌握其中的关键实现
7-2 readline的使用方法和实现原理
**
readline是Node.js中的一个内置库,主要是用来管理输入流的
const readline = require('readline')const rl = readline.createInterface({input:process.stdin,output:process.stdout})rl.question('your name:',(answer =>{console.log('your name is:'+answer)rl.close()}))
源码分析:
- 强制将函数转为构建函数
function Interface(input, output, completer, terminal) {if (!(this instanceof Interface)) {return new Interface(input, output, completer, terminal);}…………}- 获得事件驱动能力:EventEmitter.call(this);
- 监听键盘事件: ```typescript emitKeypressEvents(input, this);
// input usually refers to stdin
input.on(‘keypress’, onkeypress);
input.on(‘end’, ontermend);
<a name="DV7nQ"></a>#### 7-3 高能:深入讲解readline键盘输入监听实现原理> 略**7-4 秀操作:手写readline核心实现**```typescriptfunction setpread(callback){function onkeypress(s){output.write(s);line += sswitch (s) {case '\r':input.pause();callback(line)break;default:break;}}const input = process.stdin;const output = process.stdout;let line =''emitKeypressEvents(input)input.on('keypress',onkeypress)input.setRawMode(true) //进入原生模式input.resume()}function emitKeypressEvents(stream){function onData(chunk){g.next(chunk.toString())}const g = emitKeys(stream)g.next()stream.on('data',onData)}function* emitKeys(stream){while (true) {let ch = yield;stream.emit('keypress',ch)}}setpread(function(s){console.log('answer:',s)})
7-5 命令行样式修改的核心原理:ansi转义序列讲解
ansi-escape-code:ansi转义序列 定义的一个规范,终端通过转义字符实现特殊操作。 通过这里查询:https://handwiki.org/wiki/ANSI_escape_code
// 固定格式为:( \x1B[ ) + ('通过上面网站查询出来的参数')console.log('\x1B[31m\x1B[4m%s','your name:')console.log('\x1B[20G%s','test')
7-6 讨论readline
7-7 响应式库rxjs快速入门
rxjs是一个异步的库,和我们的Promise是非常相似的。readline源码大量使用了这个库。
// npm install rxjsconst range = require('rxjs').range;const { map, filter } = require('rxjs/operators');const pipe = range(1, 200).pipe(filter(x => x % 2 === 1),map(x => x + x),filter(x => x%3 === 0),filter(x => x%5 === 0))pipe.subscribe(x => console.log(x));
7-8& 7-9 放大招:手写命令行交互式列表组件

const EventEmitter = require('events');const readline = require('readline');const MuteStream = require('mute-stream');const { fromEvent } = require('rxjs');const ansiEscapes = require('ansi-escapes');const option = {type: 'list',name: 'name',message: 'select your name:',choices: [{name: 'sam', value: 'sam',}, {name: 'shuangyue', value: 'sy',}, {name: 'zhangxuan', value: 'zx',}],};function Prompt(option) {return new Promise((resolve, reject) => {try {const list = new List(option);list.render();list.on('exit', function(answers) {resolve(answers);})} catch (e) {reject(e);}});}class List extends EventEmitter {constructor(option) {super();this.name = option.name;this.message = option.message;this.choices = option.choices;this.input = process.stdin;const ms = new MuteStream();ms.pipe(process.stdout);this.output = ms;this.rl = readline.createInterface({input: this.input,output: this.output,});this.selected = 0;this.height = 0;this.keypress = fromEvent(this.rl.input, 'keypress').forEach(this.onkeypress);this.haveSelected = false; // 是否已经选择完毕}onkeypress = (keymap) => {const key = keymap[1];if (key.name === 'down') {this.selected++;if (this.selected > this.choices.length - 1) {this.selected = 0;}this.render();} else if (key.name === 'up') {this.selected--;if (this.selected < 0) {this.selected = this.choices.length - 1;}this.render();} else if (key.name === 'return') {this.haveSelected = true;this.render();this.close();this.emit('exit', this.choices[this.selected]);}};render() {this.output.unmute();this.clean();this.output.write(this.getContent());this.output.mute();}getContent = () => {if (!this.haveSelected) {let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[0m\x1B[2m(Use arrow keys)\x1B[22m\n';this.choices.forEach((choice, index) => {if (index === this.selected) {// 判断是否为最后一个元素,如果是,则不加\nif (index === this.choices.length - 1) {title += '\x1B[36m❯ ' + choice.name + '\x1B[39m ';} else {title += '\x1B[36m❯ ' + choice.name + '\x1B[39m \n';}} else {if (index === this.choices.length - 1) {title += ' ' + choice.name;} else {title += ' ' + choice.name + '\n';}}});this.height = this.choices.length + 1;return title;} else {// 输入结束后的逻辑const name = this.choices[this.selected].name;let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[36m' + name + '\x1B[39m\x1B[0m \n';return title;}};clean() {const emptyLines = ansiEscapes.eraseLines(this.height);this.output.write(emptyLines);}close() {this.output.unmute();this.rl.output.end();this.rl.pause();this.rl.close();}}Prompt(option).then(answers => {console.log('answers:', answers);});
7-10 inquirer源码执行流程分析
略
