大厂做项目的流程
大厂的git操作规范
脚手架需求分析
痛点分析
痛点分析:
- 创建项目/组件时,存在大量重复代码拷贝:快速复用已有沉淀
- 协同开发时,由于git操作不规范,导致分支混乱,操作耗时:制定标准的git操作规范并集成到脚手架
- 发布上线时,容易出现各种错误:制定标准的上线流程和规范并集成到脚手架
需求分析
- 通用的研发脚手架
通用的项目/组件创建能力
- 模板支持定制
- 模板支持快速接入,极地的接入成本
通用个的项目/组件发布能力
- 发布过程自动完成标准的git操作
- 发布成功后自动删除开发分支并创建tag
- 发布后自动完成云构建、OSS上传、CDN上传、域名绑定
- 发布过程支持测试/正式两种模式
winbridge-cli 脚手架架构设计图
核心模块
脚手架
- 脚手架核心框架
- 初始化体系
- 标准git操作体系
- 发布体系
服务
- OPEN API
- WebSocket
支撑体系
- 本地缓存
- 模板库
- 数据体系
- 代码仓库
- 资源体系
- 远程缓存体系
脚手架拆包策略
拆包结果
- 核心流程: core
命令: commands
- 初始化
- 发布
- 清除缓存
模型层: models
- Command 命令
- Project 项目
- Component 组件
- Npm 模块
- Git 仓库
支撑模块: utils
- Git 操作
- 云构建
- 工具方法
- API 请求
- Git API
拆分原则
根据模块的功能拆分:
- 核心模块: core
- 命令模块: commands
- 模型模块: models
- 工具模块: utils
core模块技术方案
命令执行流程
准备阶段
- checkPkgVersion 检查当前脚手架的版本号
function checkPkgVersion() { log.info('cli-version', require('package.json').version) }
- checkNodeVersion 检查Node版本号
function checkNodeVersion() { const semver = require('semver'); // 第一步, 获取当前Node版本号 const currentVersion = process.version; // 第二步, 比对最低版本号 const lowertVersion = '12.0.0'; if(!semver.gte(currentVersion, lowertVersion)) { throw new Error(colors.red(`winbridge-cli 需要安装 v${lowertVersion} 以上版本的 Node.js`)); } }
- checkRoot 检查root账户
function checkRoot() { const rootCheck = require('root-check'); rootCheck(); }
- checkUserHome 检查用户主目录
function checkUserHome() { const userHome = require('userHome); if(!userHome || !pathExists(userHome)) { throw new Error(colors.red('当前登录用户主目录不存在!')); } }
checkInputArgs 检查命令行参数
// 检查命令行参数 let args; function checkInputArgs() { const minimist = require('minimist'); args = minimist(process.argv.slice(2)); checkArgs(); } // 检查参数 function checkArgs() { if(args.debug) { process.env.LOG_LEVEL = 'verbose'; } else { process.env.LOG_LEVEL = 'info'; } log.level = process.env.LOG_LEVEL; }
checkEnv 检查环境变量
// 检查环境变量 const path = require('path'); const userHome = require('user-home'); const pathExits = require('path-exists'); function checkEnv() { const dotenv = require('dotenv'); const dotenvPath = path.resolve(userHome, '.env'); // C:/Users/86130/.env if(pathExists(dotenvPath)) { // 当这个文件存在的时候会生成一个config,不存在就生成一个默认的config dotenv.config({ path: dotenvPath }); // 将.env文件中的内容取出加载到process.env中 } createDefaultConfig(); log.verbose('环境变量', process.env.CLI_HOME); } // 创建默认环境变量配置文件 function createDefaultConfig() { const cliConfig = { home: userHome }; if (process.env.CLI_HOME) { cliConfig['cliHome'] = path.join(userHome, process.env.CLI_HOME); } else { cliConfig['cliHome'] = path.join(userHome, constant.DEFAULT_CLI_HOME); } process.env.CLI_HOME = cliConfig.cliHome; }
checkGlobalUpdate 检查是否需要全局更新
async function checkGlobalUpdate() { // 1.获取当前版本号和模块名 const currentVersion = pkg.version; const npmName = pkg.name; // const npmName = '@imooc-cli/core'; // 2.调用npm API, 获取所有版本号 http://registry.npmjs.org/@winbridge-cli/core // 3.提取所有版本号,比对哪些版本号是大于当前版本号 // 4.获取最新的版本号,提示更新 const { getNpmSemverVersion } = require('@winbridge-cli/get-npm-info'); const lastVersions = await getNpmSemverVersion(currentVersion, npmName); if(lastVersions && semver.gt(lastVersions, currentVersion)) { log.warn('更新提示', colors.yellow(`请手动更新 ${npmName} 当前版本: ${currentVersion} 最新版本: ${lastVersions} 更新命令: npm install -g ${npmName}`)) } } *** @winbridge-cli/get-npm-info *** 'use strict'; const axios = require('axios'); const urlJoin = require('url-join'); const semver = require('semver'); // 1 async function getNpmSemverVersion(baseVersion, npmName, registry) { const versions = await getNpmVersions(npmName, registry); const newVersions = getNpmSemverVersions(baseVersion, versions); if(newVersions && newVersions.length > 0) return newVersions[0]; return null; } // 2 async function getNpmVersions(npmName, registry) { const data = await getNpmInfo(npmName, registry); if(data) { return Object.keys(data.versions); } else { return []; } } // 3 function getNpmInfo(npmName, registry) { if(!npmName) { return null; } const registryUrl = registry || getDefaultRegistry(); const npmInfoUrl = urlJoin(registryUrl, npmName); return axios.get(npmInfoUrl) .then(res => { if(res.status === 200) { return res.data; } return null; }).catch(err => { return Promise.reject(err); }) } // 4 function getDefaultRegistry(isOriginal = false) { return isOriginal ? 'http://registry.npmjs.org' : 'http://registry.npm.taobao.org'; } // 5 function getNpmSemverVersions(baseVersion, versions) { return versions .filter(versions => semver.satisfies(versions, `^${baseVersion}`)) .sort((a, b) => semver.gt(b, a)); }
- checkPkgVersion 检查当前脚手架的版本号
命令注册
- 命令执行
涉及技术点
核心库
- import-local
commander
#!/usr/bin/env node const { Command } = require('commander'); const pkg = require('../package.json'); // 第一种使用方法: 获取commande的单例 // const { program } = commander; // 第二种使用方法: 手动实例化一个commander实例 const program = new Command(); // 注册参数 program .name(Object.keys(pkg.bin)[0]) // 包名 .usage('<command> [options]') // 使用建议 .version(pkg.version) // 获取版本号 .option('-d, --debug', '是否开启调试模式', false) // 配置参数 .option('-e, --envName <envName>', '获取环境变量名称', '.imooc-test') // 获取环境变量 // console.log(program.envName); // 获取输入的环境变量参数 // program.outputHelp(); // 输出帮助信息 // console.log(program.opts()); // 输出所有注册的参数 { version: '1.0.3', debug: false, envName: '123' } // command 注册的是当前脚手架下的命令且返回值是command对象而不是program对象 const clone = program.command('clone <source> [destination]'); // 注册命令名称 clone .description('clone a repository into a newly created directory') .option('-f, --force', '是否强制克隆') .action((source, destination, cmdObj) => { console.log(source, destination, cmdObj.force); }); // 注册clone命令的回调 // addCommand 注册的是当前脚手架下的子命令 const service = new Command('service'); service .command('start [port]') .description('start service at some port') .action((prot) => { console.log('do service start', prot) }); service .command('stop [port]') .description('stop service') .action((prot) => { console.log('stop service') }); program.addCommand(service); program .command('install [name]', 'install one or more package', { // 这条命令执行的是个脚本文件相当于在当前目录下执行node node_module@winbridge-cli/core/bin executableFile: 'node_modules/@winbridge-cli/core/bin', // 设置可执行文件路径 // isDefault: true, // 执行imooc-test的时候默认执行这条命令 // hidden: true // 隐藏imooc-test -h 中command的隐藏 }) .alias('i'); // 命令注册的自动匹配 // program // .arguments('<cmd> [options]') // .description('test command', { // cmd: 'command to run', // options: 'options for command' // }) // .action((cmd, options) => { // console.log(cmd, options) // }) // 高级定制1: 自定义help信息 program.helpInformation() 获取帮助信息 // console.log(program.helpInformation()); // 方法一 // program.helpInformation = function() { return '' } // 定制 imooc-test --help 返回的帮助信息 // 方法二 // program.on('--help', function() { // 监听 命令行输入的 --help 参数, 并返回信息 // console.log('your help information'); // }) // 高级定制2: 实现 dubug 模式 program.on('option:debug', function() { if(program.debug) { process.env.LOG_LEVEL = 'verbose'; } console.log(process.env.LOG_LEVEL); }) // 高级定制3: 对未知命令监听 program.on('command:*', function(obj) { console.error('未知的命令:' + obj[0]); const availableCommands = program.commands.map(cmd => cmd.name()); console.log('可用命令: '+ availableCommands.join(', ')); }); program .parse(process.argv); // 参数解析
工具库
- npmlog (命令行打印)
- fs-extra ()
- semver (版本比对)
- colors (命令行输出自定义颜色字体)
- user-home (获取文件主目录)
- dotenv (将.env文件内的配置加载到process.env中)
- root-check (检查是否为root用户)
脚手架执行准备过程实现知识点
require支持加载资源的类型
- .js => module.exports/exports
- .json => JSON.parse
- .node => process.dloper
- any => .js (如果任意文件类型内包含的是js代码,那么同样可以解析)
新模块的创建及引用
- lerna create
- 如果需要修改新模块lib目录下的.js文件名, 则需要在新模块package.json文件中修改main的入口
- 在需要引用新模块的模块中, 修改package.json的dependencies(如:"@winbridge-cli/log": "file:../../utils/log")
- 重新执行npm link
- 这样就可以通过require引入新模块
npmlog可调用的方法 默认 log.level 为'info',所以小于2000等级的方法都不会被调用
- log.addLevel('silly', -Infinity, { inverse: true }, 'sill')
- log.addLevel('verbose', 1000, { fg: 'blue', bg: 'black' }, 'verb')
- log.addLevel('info', 2000, { fg: 'green' })
- log.addLevel('timing', 2500, { fg: 'green', bg: 'black' })
- log.addLevel('http', 3000, { fg: 'green', bg: 'black' })
- log.addLevel('notice', 3500, { fg: 'blue', bg: 'black' })
- log.addLevel('warn', 4000, { fg: 'black', bg: 'yellow' }, 'WARN')
- log.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!')
- log.addLevel('silent', Infinity)
Node项目如何支持ES Module
方案一: webpack + bable-loader
安装模块
- npm i -D babel-loader @babel/core @babel/preset-env
- npm i -D @babel/plugin-transform-runtime
- npm i -D @babel/runtime-corejs3
创建webpack.config.js
const path = require('path');
module.exports = {
entry: './bin/core.js',
output: {
path: path.join(__dirname, '/dist'),
filename: 'core.js'
},
mode: 'development',
target: 'node', // 默认是 web 环境
// 以上四步完成后就可以支持es module
// 还想要支持低版本的node, 就需要配置babel-loader转义
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
regenerator: true,
useESModules: true,
helpers: true
}
]
]
}
}
}
]
}
}
方案二: Node原生支持ES Module
这种方案的实现,所有文件必须以.mjs结尾。文件内必须以ES Module的方式导出或引用
- 创建index.mjs文件
- node版本小于14 node --experimental-modules index.mjs
- node版本大于14 node index.mjs