第一章 本周导学
1-1 本周整体介绍和学习方法
- GitFlow实战
- 通过simple-git操作git命令
- Github和Gitee openAPI接入
- 本周加餐:Node最佳实践分享
主要内容
- Git仓库初始化(利用Github和Gitee OpenAPI)
- 本地Git初始化
- GitFlow流程实现(代码自动提交)
附赠内容
Node项目最佳实践:
- 项目结构最佳实践
- 异常处理最佳实践
- 测试最佳实践
- 发布上线最佳实践
- 安全最佳实践
第二章 Git Flow 模块架构设计
2-1 GitFlow模块架构设计
2-2 GitFlow流程回顾
第三章 Github&Gitee API 接入
3-1 创建Git类
- 首先创建一个新的package—git,用来管理与git相关的所有内容
- lerna create git models/git
- 在上周代码写完this.prepare()之后(commands/publish/lib/index.js中),我们就需要去调用这个新建的git包,实例化出来一个对象,并将projectInfo的信息传递进去
- const Git = require(‘@cloudscope-cli/git’)
- const git = new Git()
- 我们在新建的 models/git/lib/index.js中新建一个Git类,本节代码内容为:
'use strict';
const SimpleGit = require('simple-git')
class Git {
constructor({name, version, dir}){
this.name = name
this.version = version
this.dir = dir
this.git = SimpleGit(dir)
this.gitServer = null
}
prepare(){
}
init(){
console.log('Git init')
}
}
module.exports = Git;
3-2 用户主目录检查逻辑开发
本节的主要代码为在Class Git中编写prepare方法,检查用户主目录是否存在
'use strict';
const path = require('path')
const fs = require('fs')
const SimpleGit = require('simple-git')
const userHome = require('user-home')
const log = require('@cloudscope-cli/log')
const fse = require('fs-extra')
const DEFAULT_CLI_HOME = '.cloudscope-cli'
class Git {
constructor({name, version, dir}){
this.name = name
this.version = version
this.dir = dir
this.git = SimpleGit(dir)
this.gitServer = null
this.homePath = null
}
async prepare(){
this.checkHomePath();// 检查缓存主目录
}
checkHomePath(){
if(!this.homePath){
if(process.env.CLI_HOME_PATH){
this.homePath = process.env.CLI_HOME_PATH
}else{
this.homePath = path.resolve(userHome,DEFAULT_CLI_HOME)
}
}
log.verbose('home:',this.homePath )
fse.ensureDirSync(this.homePath);
if(!fs.existsSync(this.homePath)){
throw new Error('用户主目录获取失败!')
}
}
init(){
console.log('Git init')
}
}
module.exports = Git;
本节对path/fs/user-home/fs-extra进行简单回顾。
- path:node内置,path模块提供了处理文件和目录的路径的实用工具。
- path.dirname(path) ,返回something(文件或文件夹)所在的文件目录。
- path.extname(path):返回最后一次出现.字符到字符串的结尾
- path.isAbsolute(path):确定这个path路径是否为绝对路径。
- path.join([…paths]):路径拼接
- path.parse(path):将路径返回成一个对象{root,dir,name,ext,base}
- path.resolve(path):将路径或路径片段的序列解析为绝对路径,给定的路径序列从右向左处理
- path.seq:window上是\,Mac上是/
- fs:node内置,文件系统
- fs.existsSync(path):如果路径存在返回true,不存在返回false
- user-home\fs-extra:第三方
3-3 选择远程Git仓库逻辑开发
- 上一节的内容总结一句大白话就是:获取 /User/username/.cloudscope-cli目录,如果没有就创建。
- 本节的内容一句话总结就是实现了一个checkGitServer方法,这个方法的主要功能是检查在上文提到的.cloudscope-cli下是否有.git/.git-server文件,没有的话通过 inquirer询问创建
- 且在 @cloudscope-cli/utils下新建了 readFile和writeFile方法
- 并且添加了一个参数:refreshServer,如果有这个参数就判断是否重写.git-server文件
本节新开发添加代码如下:
const { readFile,writeFile } = require('@cloudscope-cli/utils')
const inquirer = require('inquirer')
const DEFAULT_CLI_HOME = '.cloudscope-cli'
const GIT_ROOT_DIR = '.git'
const GIT_SERVER_FILE = '.git_server'
const GITHUB = 'github'
const GITEE ='gitee'
const GIT_SERVER_TYPE = [{
name:'Github',
value: GITHUB
},{
name: 'Gitee',
value: GITEE
}]
async prepare(){
this.checkHomePath();// 检查缓存主目录
await this.checkGitServer();//检查用户远程仓库类型
}
async checkGitServer(){
const gitServerPath = this.createPath(GIT_SERVER_FILE)
let gitServer = readFile(gitServerPath)
if(!gitServer){ // 如果没有读取到.git-server文件中的内容
gitServer = await this.choiceServer(gitServerPath)
log.success('git server 写入成功',`${gitServer} -> ${gitServerPath}`)
}else{ // 如果读取到了 内容
if(this.refreshServer){ // 是否重写标识
const refresh = (await inquirer.prompt([{
type:'confirm',
name:'ifContinue',
default:false,
message:'当前.git-server目录已存在,是否要重写选择托管平台?'
}])).ifContinue
if(refresh){
gitServer = await this.choiceServer(gitServerPath)
log.success('git server 重写成功',`${gitServer} -> ${gitServerPath}`)
}else{
log.success('git server 获取成功 ', gitServer)
}
}else{ //不重写,直接读取
log.success('git server 获取成功 ', gitServer)
}
}
this.gitServer = this.createServer(gitServer)
}
async choiceServer(gitServerPath){
const gitServer = (await inquirer.prompt({
type:'list',
name:'server',
message:'请选择你想要托管的Git平台',
default: GITHUB,
choices:GIT_SERVER_TYPE
})).server;
writeFile(gitServerPath,gitServer)
return gitServer
}
createPath(file){
const rootDir = path.resolve(this.homePath,GIT_ROOT_DIR)
const serverDir = path.resolve(rootDir,file)
fse.ensureDirSync(rootDir)
return serverDir
}
@cloudscope-cli/utils 下的readFile和writeFile方法实现为使用node自带的fs.readFileSync(path)和fs.writeFileSync(path)方法
3-4 创建GitServer类
本节主要创建了一个GitServer类,并新建Github类和Gitee类分别继承GitServer。
function error(methodName) {
throw new Error(`${methodName} must be implemented!` )
}
class GitServer {
constructor(type,token){
this.type= type
this.token = token
}
setToken(){
error('setToken')
}
createRepo(){
error('createRepo')
}
createOrgRepo(){
error('createOrgRepo')
}
getRemote(){
error('getRemote')
}
getuser(){
error('getuser')
}
getOrg(){
error('getOrg')
}
}
module.exports = GitServer
3-5 生成远程仓库Token逻辑开发
本节课的主要内容为生成远程仓库Token。
- 首先在类Github.js和Gitee.js中分别实现获取帮助文档和相应token的文档方法
//Github.js
getSSHKeysUrl(){
return 'https://gitee.com/profile/sshkeys';
}
//Gitee.js
getSSHKeysUrl(){
return 'https://gitee.com/profile/sshkeys';
}
getTokenHelpUrl(){
return 'https://gitee.com/help/articles/4191'
}
然后在models/git/index.js中实现获取Giteetoken的方法
- 安装了 terminal-link库,且版本号为2.1.1,功能是直接在ternimal中点击跳转链接
const terminalLink = require('terminal-link')
const GIT_TOKEN_FILE = '.git_token'
async checkGitToken(){
const tokenPath = this.createPath(GIT_TOKEN_FILE)
let token = readFile(tokenPath)
if(!token || this.refreshServer){
log.warn(this.gitServer.type + ' token未生成,请先生成!' + this.gitServer.type + ' token,'+terminalLink('链接',this.gitServer.getTokenHelpUrl())) ;
token = (await inquirer.prompt({
type:'password',
name:'token',
message:'请将token复制到这里',
default:'',
})).token
writeFile(tokenPath,token)
log.success('token 写入成功',` ${tokenPath}`)
}else{
log.success('token获取成功',tokenPath)
}
this.token = token
this.gitServer.setToken(token)
}
3-6 Gitee API接入+获取用户组织信息功能开发
本章节主要是Gitee API的接入:获取用户信息和组织信息
- 使用第三方库有 axios
- 新建的文件有 GitServerRequest.js
const axios = require('axios')
const BASE_URL = 'https://gitee.com/api/v5'
class GiteeRequest {
constructor(token){
this.token = token
this.service = axios.create({
baseURL:BASE_URL,
timeout:5000
})
this.service.interceptors.response.use(
response =>{
return response.data
},
error =>{
if(error.response && error.response.data){
return error.response
} else {
return Promise.reject(error)
}
}
)
}
get(url,params,headers){
return this.service({
url,
params:{
...params,
access_token:this.token
},
method:'get',
headers,
})
}
}
module.exports = GiteeRequest
index.js主代码主要实现的方法是getUserAndOrgs:
async getUserAndOrgs(){
this.user = await this.gitServer.getUser()
if(!this.user){
throw new Error('用户信息获取失败')
}
this.orgs = await this.gitServer.getOrg(this.user.login)
if(!this.orgs){
throw new Error('组织信息获取失败')
}
log.success(this.gitServer.type + ' 用户和组织信息获取成功')
}
3-7 Github API接入开发
本节代码类似于Gitee API,不同点在于,Github API需要在headers中传入token:config.headers[‘Authorization’] =
token ${**this**.token}
然后,基础BASE_URL更换,获取组织URL更换,改动部分代码即可。
3-8 远程仓库类型选择逻辑开发
之前的章节我们选择了Git托管类型,生成了相关托管平台的token,获取到了个人和组织的信息,然后这节我们将要继续选择:确认远程仓库的类型(是个人还是组织),如果我们拿到的信息只有个人,那么就不显示组织选项。 同样,我们要将拿到的个人或者组织登录名(login)以及类型(owner)写入到缓存文件中.
const GIT_OWN_FILE = '.git_own'
const GIT_LOGIN_FILE = '.git_login'
const REPO_OWNER_USER = 'user'
const REPO_OWNER_ORG = 'org'
const GIT_OWNER_TYPE = [{
name:'个人',
value: REPO_OWNER_USER
},{
name: '组织',
value: REPO_OWNER_ORG
}]
const GIT_OWNER_TYPE_ONLY = [{
name:'个人',
value: REPO_OWNER_USER
}]
………………
await this.checkGitOwner();//确认远程仓库类型
………………
async checkGitOwner(){
const ownerPath =this.createPath(GIT_OWN_FILE) ;
const loginPath =this.createPath(GIT_LOGIN_FILE) ;
let owner = readFile(ownerPath)
let login = readFile(loginPath)
if(!owner || !login || this.refreshOwner){
owner = (await inquirer.prompt({
type:'list',
name:'owner',
message:'请选择远程仓库类型',
default: REPO_OWNER_USER,
choices:this.orgs.length > 0 ? GIT_OWNER_TYPE : GIT_OWNER_TYPE_ONLY
})).owner
if(owner === REPO_OWNER_USER){
login = this.user.login
}else{
login = (await inquirer.prompt({
type:'list',
name:'login',
message:'请选择',
choices:this.orgs.map(item =>({
name:item.login,
value: item.login,
}))
})).login
}
writeFile(ownerPath,owner)
writeFile(loginPath,login)
log.success('owner 写入成功',`${owner} -> ${ownerPath}`)
log.success('login 写入成功',`${login} -> ${loginPath}`)
}else{
log.success('owner 读取成功',`${owner} -> ${ownerPath}`)
log.success('login 读取成功',`${login} -> ${loginPath}`)
}
this.owner = owner
this.login = login
}
第四章 GitFlow 初始化流程开发
4-1 Gitee获取和创建仓库API接入
- 这节的代码,出了一个小bug,调试到了天亮,bug方法的实现为下面示例50行:this.handleResponse(response),课程代码讲到在获取一个仓库的API时没有status参数,经测试是有的。
- 本节主要完成的功能有:
- 检查并创建远程仓库 checkRepo 方法实现
- GiteeRequest添加post请求
- Gitee类实现 createRepo()和 getRepo() 方法
await this.checkRepo(); // 检查并创建远程仓库
//models/git/lib/index.js
async checkRepo(){
let repo = await this.gitServer.getRepo(this.login,this.name)
log.verbose('repo',repo)
if(!repo){ //如果远程仓库不存在,就去创建
let spinner = spinnerStart('开始创建远程仓库')
try {
if(this.owner === REPO_OWNER_USER){
repo = await this.gitServer.createRepo(this.name)
log.success('用户个人远程仓库创建成功!')
}else{
this.gitServer.createOrgRepo(this.name,this.login)
log.success('用户组织远程仓库创建成功1')
}
} catch (error) {
log.error(error)
}finally {
spinner.stop(true)
}
if(!repo){
throw new Error('远程仓库创建失败')
}
}else{
log.success('远程仓库已存在且获取成功!')
}
this.repo = repo
}
//models/git/lib/GiteeRequest.js
post(url,data,headers){
return this.service({
url,
params:{
access_token:this.token,
},
data,
method:'POST',
headers
})
}
//models/git/lib/Gitee.js
getRepo(login,name){
//GET https://gitee.com/api/v5/repos/{owner}/{repo}
return this.request
.get(`/repos/${login}/${name}`)
.then(response =>{
return this.handleResponse(response)
})
}
createRepo(name){
// POST https://gitee.com/api/v5/user/repos
return this.request.post('/user/repos',{
name,
})
}
4-2 Github获取和创建仓库API接入
与Gitee获取和创建仓库API类似。GithubRequest同样实现了post方法。 类Github同样实现了getRepo和createRepo方法。
4-3 Github&Gitee组织仓库创建API接入
本节内容较为简单,实现了远程创建组织仓库API
createOrgRepo(name,login){
return this.request.post(`/orgs/${login}/repos`,{
name
},{
accept:'application/vnd.github.v3+json'
})
4-4 gitignore文件检查
提交准备工作:有些项目没有默认创建.gitignore,因此会引发提交大量无用或无关代码。 因此,我们需要检查并创建.gitignore文件的方法 这里需要注意的是安装的.gitignore安装目录为当前执行文件,而不是缓存文件
const GIT_IGNORE_FILE='.gitignore'
this.checkGitIgnore();//检查并创建.gitignore文件
checkGitIgnore(){
const gitIgnorePath = path.resolve(this.dir,GIT_IGNORE_FILE)
console.log(gitIgnorePath)
if(!fs.existsSync(gitIgnorePath)){
writeFile(gitIgnorePath,`.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
`)
log.success(`自动写入${GIT_IGNORE_FILE}文件成功!`)
}
}
4-5 git本地仓库初始化和远程仓库绑定
本节主要完成的功能为本地的仓库的初始化:即执行git init方法和git addRemote方法。
await this.init(); //完成本地仓库初始化
async init(){
if(await this.getRemote()){
return
}
await this.initAndAddRemote();
}
async initAndAddRemote(){
log.info('执行git初始化')
await this.git.init(this.dir)
log.info('添加git remote')
const remotes = await this.git.getRemotes();
console.log('git remotes',remotes)
if(!remotes.find(item => item.name === 'origin')){
await this.git.addRemote('origin',this.remote)
}
}
async getRemote(){
const gitPath = path.resolve(this.dir,GIT_ROOT_DIR)
this.remote = this.gitServer.getRemote(this.login,this.name)
if(fs.existsSync(gitPath)){
log.success('git已完成初始化')
return true
}
}
4-6 git自动化提交功能开发
上一节的流程在本地实现了两个操作
- git init
- git remote add origin ‘git@github.com:${login}/${name}.git’
紧接着这一节按照本地的操作,我们应该实现 git add. / git commit -m’的操作。 本节实现initCommit()方法:
- 首先检查是否有代码冲突
- 然后检查代码是否有未提交
- 然后判断远程分支是否已存在
- 不存在的话直接push代码
- 存在的话就需要使用git pull去拉取代码,且使用 ‘—allow-unrelated-histories:all’:null 参数。
async initCommit(){
await this.checkConflicted(); //检查代码冲突
await this.checkNotCommitted();//检查代码未提交
if(await this.checkRemoteMaster()){ //判断远程仓库master分支是否已存在
await this.pullRemoteRepo('master',{
'--allow-unrelated-histories':null
})
} else {
await this.pushRemoteRepo('master') //如果不存在直接push代码
}
}
async checkConflicted(){
log.info('代码冲突检查')
const status = await this.git.status()
if(status.conflicted.length > 0 ){
throw new Error('当然代码存在冲突,请手动处理合并后再试')
}
log.success('代码冲突检查通过')
}
async checkNotCommitted(){
const status = await this.git.status()
if(status.not_added.length >0 ||
status.created.length >0 ||
status.deleted.length>0 ||
status.modified.length>0 ||
status.renamed.length>0
){
log.verbose('status',status)
await this.git.add(status.not_added)
await this.git.add(status.created)
await this.git.add(status.deleted)
await this.git.add(status.modified)
await this.git.add(status.renamed)
let message;
while (!message) {
message = (await inquirer.prompt({
type:'text',
name:'message',
message:'请输入commit信息'
})).message
}
await this.git.commit(message)
log.success('本次commit提交成功!')
}
}
async checkRemoteMaster(){
// git ls-remote
return (await this.git.listRemote(['--refs'])).indexOf('refs/heads/master') >=0
}
async pushRemoteRepo(branchName){
log.info(`推送代码至${branchName} 分支`)
await this.git.push('origin',branchName)
log.success('推送代码成功!')
}
async pullRemoteRepo(branchName,options){
log.info(`同步远程${branchName}分支代码`)
await this.git.pull('origin',branchName,options)
.catch(err=>{
log.error(err.message)
})
}
第五章 GitFlow本地仓库代码自动提交
5-1 自动生成开发分支原理讲解1
第一遍:这节课听的有点懵逼。
5-2 自动生成开发分支功能开发
本节主要实现为 获取远程发布分支列表(git ls-remote —refs)和获取远程最新发布分支号(通过正则匹配release分支,并排序获取最新分支),详细代码如下:
const semver = require('semver')
const VERSION_RELEASE = 'release'
const VERSION_DEVELOP = 'dev'
async commit(){
// 1.生成开发分支
await this.getCorrectVersion()
// 2.在开发分支上提交代码
// 3.合并远程开发分支
//4.推送开发分支
}
async getCorrectVersion(type){
// 1.获取远程发布分支
// 规范:release/x.y.z ,dev/x.y.z
// 版本号递增规范:major/minor/patch
log.info('获取远程仓库代码分支')
const remoteBranchList = await this.getRemoteBranchList(VERSION_RELEASE)
let releaseVersion = null;
if(remoteBranchList && remoteBranchList.length>0){
releaseVersion = remoteBranchList[0]
}
log.verbose('releaseVersion',releaseVersion)
}
async getRemoteBranchList(type){
const remoteList = await this.git.listRemote(['--refs'])
let reg;
if(type === VERSION_RELEASE ){
reg = /.+?refs\/tags\/release\/(\d+\.\d+\.\d+)/g
}else{
}
return remoteList.split('\n').map(remote =>{
const match = reg.exec(remote)
reg.lastIndex = 0
if(match &&semver.valid(match[1]) ){
return match[1]
}
}).filter(_ => _ ).sort((a,b) => {
if(semver.lte(b,a)){
if(a===b) return 0;
return -1
}
return 1
})
}
5-3 高端操作:自动升级版本号功能开发
根据5-1图示,上两节我们完成的部分为:获取远程发布分支号列表、获取远程最新发布分支号,并在上节代码中经过处理,拿到了最新的远程发布的版本号,接下来我们实现
- 判断最新发布版本号是否存在
- 不存在:生成本地开发分支
- 存在:与本地开发分支版本号通过semver对比
- 本地分支小于远程最新发布分支版本号
- 通过inquirer询问选择本地版本的升级方式
- 获取选择升级的版本号
- 重新写入到本地package.json中的version中去
- 本地分支大于远程最新发布分支版本号
this.branch = null //本地开发分支
//接着上一节的代码,在getCorrectVersion方法中继续:
//2.生成本地开发分支
const devVersion = this.version
if(!releaseVersion){ // 不存在远程发布分支
this.branch = `${VERSION_DEVELOP}/${devVersion}`
}else if(semver.gt(this.version,releaseVersion)){ //本地分支大于远程发布分支
log.info('当前版本大于线上最新版本',`${devVersion} >= ${releaseVersion}`)
this.branch = `${VERSION_DEVELOP}/${devVersion}`
} else {
log.info('当前线上版本大于本地版本',`${releaseVersion} > ${devVersion}`)
const incType = (await inquirer.prompt({
type:'list',
name:'incType',
message:'自动升级版本,请选择升级版本',
default:'patch',
choices:[{
name:`小版本(${releaseVersion} -> ${semver.inc(releaseVersion,'patch')})`,
value:'patch'
},{
name:`中版本(${releaseVersion} -> ${semver.inc(releaseVersion,'minor')})`,
value:'minor'
},{
name:`大版本(${releaseVersion} -> ${semver.inc(releaseVersion,'major')})`,
value:'major'
}]
})).incType
const incVersion = semver.inc(releaseVersion,incType)
this.branch = `${VERSION_DEVELOP}/${incVersion}`
this.version = incVersion
}
log.verbose('本地开发分支',this.branch)
//3.将version同步到package.json
this.syncVersionToPackageJson()
syncVersionToPackageJson(){
const pkg = fse.readJsonSync(`${this.dir}/package.json`)
if(pkg && pkg.version!== this.version){
pkg.version = this.version
fse.writeJsonSync(`${this.dir}/package.json`,pkg,{spaces:2})
}
}
5-4 GitFlow代码自动提交流程梳理+stash区检查功能开发
本地执行git status 有未提交的代码时,执行 git stash将未提交的代码缓存在stash区当中。 然后通过git status命令发现,没有代码可提交 这里温习了git stash的个命令:git stash / git stash list / git stash pop
本节代码实现
async checkStash(){
//1. 检查stash list
const stashList = await this.git.stashList()
if(stashList.all.length >0){
await this.git.stash['pop']
log.success('stash pop成功')
}
}
5-5 代码冲突处理+Git代码删除后还原方法讲解
本节以及上一节听的有些懵逼,需要第二遍重新学习
5-6 自动切换开发分支+合并远程分支代码+推送代码功能开发
先暂时略过笔记。
第六章 本周加餐:Node编码最佳实践
6-1 Node最佳实践学习说明
6-2 Node项目架构最佳实践
6-3 Node异常处理最佳实践
代码示例: 捕获 unresolved 和 rejected 的 promise
process.on('unhandledRejection', (reason, p) => {
//我刚刚捕获了一个未处理的promise rejection, 因为我们已经有了对于未处理错误的后备的处理机制(见下面), 直接抛出,让它来处理
throw reason;
});
process.on('uncaughtException', (error) => {
//我刚收到一个从未被处理的错误,现在处理它,并决定是否需要重启应用
errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error))
process.exit(1);
});