大厂做项目的流程

项目流程

大厂的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模块技术方案

命令执行流程

  • 准备阶段 core模块prepare

    • 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));
            }
      
  • 命令注册

  • 命令执行

涉及技术点

核心库

  • 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
Copyright © imooc-lego (2020 - present) all right reserved,powered by GitbookFile Modify: 2021-06-27 08:04:57

results matching ""

    No results matching ""