加载中...
  • 脚手架搭建 loading

    开发脚手架的核心目标: 提升研发效率

    项目共通操作

    1. 创建项目 + 通用代码
      • 埋点
      • HTTP 请求
      • 工具方法
      • 组件库
    2. git 操作
      • 创建仓库
      • 代码冲突
      • 远程代码同步
      • 创建版本
      • 发布打 tag
    3. 构建 + 发布上线
      • 依赖安装和构建
      • 资源上传 CDN
      • 域名绑定
      • 测试/正式服务器

    脚手架核心价值

    1. 自动化: 项目重复代码拷贝/git 操作/发布上线操作
    2. 标准化: 项目创建/git flow/发布流程/回滚流程
    3. 数据化: 研发过程系统化、数据化、使得研发过程可量化

    和自动化构建工具的区别

    jenkins、travis 等自动化构建工具已经比较成熟了, 为什么还需要自研脚手架?

    不满足需求: jenkins、travis 通常在 git hooks 中触发, 需要服务端执行, 无法覆盖研发人员本地的功能, 如: 创建项目自动化, 本地 git 操作自动化

    定制复杂: jenkins、travis 定制过程需要开发插件, 其过程较为复杂, 需要使用 java 语言, 对前端同学不够友好

    从使用角度理解什么是脚手架

    脚手架简介

    脚手架本质是一个==操作系统的客户端==, 它通过命令行执行, 比如:

    vue create vue-test-app

    上述这条命令由 3 个部分组成:

    1. 主命令: vue
    2. commamd: create
    3. command 的 param: vue-test-app

    它表示创建一个 vue 项目, 项目的名称是 vue-test-app, 以上是一个较为简单的脚手架命令, 但实际场景往往更为复杂, 比如:

    当前目录已经有文件了, 我们需要覆盖当前目录下的文件, 强制进行安装 vue 项目, 此时我们就可以输入:

    vue create vue-test-app –force (–force 可以理解为–force true 的简写)

    这里的--force叫做option, 用来辅助脚手架确认在特定场景下用户的选择(可以理解为配置), 还有一种场景:

    通过vue create创建项目时, 会自动执行npm install帮助用户安装依赖, 如果我们希望使用淘宝源来安装, 可以输入命令:

    vue create vue-test-app –force -r https://reqistry.npm.taobao.org

    这里的-r也叫做 option, 它与--force不同的是它使用-, 并且使用简写, 这里的-也可以替换成--reqistry

    vue create –help 查看支持的所有 options

    脚手架执行原理

    1. 在终端中输入vue create vue-test-app
    2. 终端解析出vue指令 (which vue /usr/local/bin/vue)
    3. 终端在环境变量中找到vue命令
    4. 终端根据vue命令链接到实际文件vue.js
    5. 终端利用node执行vue.js
    6. vue.js解析commamd/options
    7. 执行完毕, 退出执行

    从应用的角度看如何开发一个脚手架

    以 vue-cli 为例

    1. 开发npm项目, 该项目中应包含一个bin/vue.js文件, 并将这个项目发布到npm
    2. npm项目安装到nodelib/node_modules
    3. nodebin目录下配置vue软链接指向lib/node_modules/@vue/cli/bin/vue.js

    疑问

    • 为什么全局安装@vue/cli后会添加的命令为vue?

    在 package.json 文件中, bin { vue: /bin/vue.js } bin 文件 vue 指向 vue.js

    npm install -g @vue-cli

    • 全局安装@vue-cli时发生了什么?

      1. npm 把包下载到 node_modules
      2. 解析 package.json, 在 node bin 目录下创建一个软链接, 指向 bin 配置的文件
    • 为什么vue指向了一个js文件, 我们却可以直接通过vue命令直接去执行它?

    1
    2
    #!/usr/bin/env node 使用node执行 命令
    创建一个软链接 指向vue.js 文件
    • 为什么说脚手架的本质是操作系统的客户端? 它和我们在 PC 上安装的应用/软件有什么区别?

    node.exe 是一个可执行文件 node ./test.js 的本质是 node -e ‘console.log(‘123’)’

    本质上没有区别, node 没有提供 gui

    • 如何为 node 脚手架命令创建别名?
    1
    2
    3
    添加软链接
    cd /usr/local/bin
    ln -s 地址 启动名
    • 描述脚手架命令执行的全过程

      1. 在环境变量$PATH 中查询命令 相当于执行 which vue
      2. 查询实际链接文件
      3. 通过#!/usr/bin/env node 执行文件

    脚手架开发流程

    1. 创建 npm 项目
    2. 创建脚手架入口文件, 最上方添加#!/usr/bin/env node
    3. 配置 package.json, 添加 bin 属性
    4. 编写脚手架代码
    5. 将脚手架发布到 npm

    脚手架开发难点

    1. 分包: 将复杂的系统拆分成若干个模块
    2. 命令注册 如: vue create
    3. 参数解析 如–version -h vue command [options]
    4. 帮助文档
    5. 命令行交互
    6. 日志打印
    7. 命令行文字变色
    8. 网络通信: HTTP/WebSocket
    9. 文件处理

    脚手架的本地调试

    1. 创建软链接指向本地目录文件
    2. 在当前文件执行 npm link

    分包

    1. npm link 其他包文件
    2. 本地安装包

    链接本地脚手架

    1
    2
    cd your-cli-dir
    npm link

    链接本地库文件

    1
    2
    3
    4
    cd your-lib-dir
    npm link
    cd your-cli-dir
    npm link your-lib

    取消链接本地库文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cd your-lib-dir
    npm unlink
    cd your-cli-dir

    #link存在
    npm unlink your-lib

    #link不存在
    rm -rf node_modules
    npm install your-lib

    脚手架命令注册和参数解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #!/usr/bin/env node
    const lib = require('tut-lib/lib')

    // 注册一个命令 tut init
    const argv = require('process').argv
    const command = argv[2]
    const options = argv.slice(3)
    let [option, param] = options
    if (option) {
    option = option.replace(/^--/, '')
    }

    lib[command] && lib[command]({ option, param })

    // 实现参数解析 --version 和 init --name
    const versionReg = /^-{1,2}/
    if (versionReg.test(command)) {
    const globOption = command.replace(versionReg, '')
    if (['version', 'V'].includes(globOption)) {
    fs.readFile('./package.json', 'utf-8', function (err, data) {
    if (err) return
    const { version } = JSON.parse(data)
    console.log('version: ', version)
    })
    }
    }

    脚手架框架

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Plop
    yeoman-generator -> vscode插件

    Schematics
    配合schematics-utilities可以做到语法级别的样板代码生成
    引入了虚拟文件系统, 可以保证写入原子性
    支持多个schametics之间的组合和管道

    ink
    react开发cli应用

    Lerna 学习

    原生脚手架开发痛点分析

    • 痛点一: 重复操作

      1. 多 Package 本地 link
      2. 多 Package 依赖安装
      3. 多 Package 单元测试
      4. 多 Package 代码提交
      5. 多 Package 代码发布
    • 痛点二: 版本一致性

      1. 发布时版本一致性
      2. 发布后互相依赖版本升级 (lerna link)

    Lerna 简介

    Lerna 是一个优化基于 git + npm 的多 Package 项目的管理工具

    优势

    1. 大幅减少重复操作
    2. 提升操作的标准化

    Lerna 是架构优化的产物, 它揭示了一个架构真理: 项目复杂度提升后, 就需要对项目进行架构优化, 架构优化的主要目标往往都是以效能为核心. lerna 的主仓库必须是私有的

    Lerna 开发脚手架流程

    脚手架项目初始化

    1. 初始化 npm 项目
    2. 安装 lerna
    3. lerna init 初始化项目

    创建 package

    1. lerna create 创建 package
    2. lerna add 为指定 package 安装依赖 lerna add package utils/
    3. lerna link 链接依赖
    4. lerna init

    脚手架开发和测试

    1. lerna exec – (–scope + 包名)执行 shell 脚本 在每一个 package 中执行操作
    2. lerna run (–scoped + 包名) 执行 npm 命令 (可以用来执行单测)
    3. lerna clean 清空依赖
    4. lerna bootstrap 重装依赖
    5. lerna bootstrap –hoist 依赖安装到根目录

    脚手架发布上线

    package.json publishConfig{.”access”: “public” }

    1. lerna version bump version (提升版本号)
    2. lerna changed 查看上版本以来的所有变更
    3. lerna diff 查看 diff
    4. lerna publish 项目发布 发布后会给 git 仓库推一个 tag

    脚手架核心流程开发

    core 模块技术方案

    命令执行流程

    准备阶段
    1
    2
    prepare
    检查版本号->检查node版本->检查root启动->检查用户主目录->检查入参->检查环境变量->检查是否为最新版本->提示更新
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    核心库
    import-local
    commander
    inquirer
    工具库
    npmlog
    fs-extra
    semver // 版本匹配/比对
    colors // 终端打印不同颜色的文本
    path-exists
    user-home // 快速拿到用户主目录
    minimist // 解析argument参数
    dotenv // 拿到环境变量
    url-join
    root-check // 降级权限
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    'use strict'

    module.exports = core

    // require 支持加载的类型 .js .json .node(C++)
    // any => 通过.js进行解析
    const semver = require('semver')
    const colors = require('colors/safe')
    const userHome = require('user-home')
    const pathExists = require('path-exists').sync

    const pkg = require('../package.json')
    const log = require('@tut-cli-dev/log')
    const { getNpmSemverVersion } = require('@tut-cli-dev/get-npm-info')
    const path = require('path')
    const { LOWEST_NODE_VERSION, DEFAULT_CLI_HOME } = require('./const')

    let args

    async function core(argvs) {
    try {
    checkPackageVersion()
    checkNodeVersion()
    checkRoot()
    checkUserHome()
    checkInputArgs(argvs)
    checkEnv()
    await checkGlobalUpdate()
    } catch (e) {
    // NOTICE
    log.error(e.message)
    }
    }
    function checkPackageVersion() {
    log.notice('cli', pkg.version)
    }
    function checkNodeVersion() {
    const currentVersion = process.version
    if (!semver.gte(currentVersion, LOWEST_NODE_VERSION)) {
    throw new Error(
    colors.red(`tut-cli 需要安装${LOWEST_NODE_VERSION}以上的node版本`)
    )
    }
    }
    function checkRoot() {
    const rootCheck = require('root-check')
    // core: invode process.setuid()
    rootCheck()
    // 管理员为0 使用rootCheck => 管理员不为0
    // console.log(process.getuid())
    }
    function checkUserHome() {
    if (!userHome || !pathExists(userHome)) {
    throw new Error(colors.red('用户主目录不存在!'))
    }
    }
    function checkInputArgs(argvs) {
    const minimist = require('minimist')
    args = minimist(argvs)
    checkArgs()
    // console.log('args: ', args) // args: { _: [ 'init', 'u' ], g: true }
    }
    function checkArgs() {
    if (args.debug) {
    process.env.LOG_LEVEL = 'verbose'
    } else {
    process.env.LOG_LEVEL = 'info'
    }
    // NOTICE
    log.level = process.env.LOG_LEVEL
    }
    function checkEnv() {
    // process.cwd() 返回 Node.js 进程当前工作的目录
    const dotenv = require('dotenv')
    const dotenvPath = path.resolve(userHome, '.env')
    if (pathExists(dotenvPath)) {
    dotenv.config({ path: dotenvPath })
    }
    createDefaultConfig()
    log.verbose('缓存路径', process.env.CLI_HOME_PATH)
    }
    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, DEFAULT_CLI_HOME)
    }
    // BETTER 对环境变量的值处理后生成新的环境变量
    process.env.CLI_HOME_PATH = cliConfig.cliHome
    // return cliConfig
    }
    async function checkGlobalUpdate() {
    // 1. 获取当前版本号和模块名
    // 2. 调用npmApi, 获取所有版本号 https://registry.npmjs.org/@tut-cli-dev/core
    // 3. 提取所有版本号, 比对当前版本号, 如果当前版本号小于最新版本号, 提示更新
    // 4. 提示用户更新到最新版本
    const currentVersion = pkg.version
    const npmName = pkg.name
    const lastVersion = await getNpmSemverVersion(currentVersion, npmName)
    if (lastVersion && semver.gt(lastVersion, currentVersion)) {
    log.warn(
    colors.yellow(
    `当前版本号${currentVersion}不是最新版本号${lastVersion}, 请更新到最新版本
    更新命令: npm install -g ${npmName}@${lastVersion}`
    )
    )
    }
    }

    @get-npm-info

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    'use strict'

    const axios = require('axios')
    const urlJoin = require('url-join')
    const semver = require('semver')

    function getNpmInfo(npmName, registry) {
    if (!npmName) return null
    const registryUrl = registry || getDefaultRegistry(true)
    const npmInfoUrl = urlJoin(registryUrl, npmName)
    return axios
    .get(npmInfoUrl)
    .then((res) => {
    if (res.status === 200) {
    return res.data
    } else {
    return null
    }
    })
    .catch((err) => {
    throw Promise.reject(err)
    })
    }

    function getDefaultRegistry(isOriginal = false) {
    return isOriginal
    ? 'https://registry.npmjs.org'
    : 'https://registry.npm.taobao.org'
    }

    async function getNpmVersions(npmName, registry) {
    const data = await getNpmInfo(npmName, registry)
    if (!data) return []
    return Object.keys(data.versions)
    }

    function getSemverVersions(baseVersion, versions) {
    versions = versions
    .filter((version) => {
    return semver.satisfies(version, `^${baseVersion}`)
    })
    .sort((a, b) => semver.gt(b, a))
    return versions
    }

    async function getNpmSemverVersion(baseVersion, npmName, registry) {
    const versions = await getNpmVersions(npmName, registry)
    const newVersions = getSemverVersions(baseVersion, versions)
    if (newVersions && newVersions.length > 0) {
    return newVersions[0]
    }
    }

    module.exports = {
    getNpmInfo,
    getNpmVersions,
    getNpmSemverVersion
    }

    命令注册
    1
    2
    3
    4
    registerCommand
    注册init命令 -> 注册publish命令 -> 注册clean命令 -> 支持debug

    commander
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    #!/usr/bin/env node

    const utils = require('@tut-cli-dev/utils')
    // 全局安装的包如果本地有安装,优先用本地的
    const importLocal = require('import-local')
    const colors = require('colors')
    const commander = require('commander')
    const pkg = require('../package.json')

    // 获取commanderd单例
    // const { program } = commander
    // 手动实例化一个commander
    const program = new commander.Command()
    program
    .name(Object.keys(pkg.bin)[0])
    .usage('<command> [options]')
    .version(pkg.version)
    .option('-d, --debug', '是否启动调试模式', false)
    .option('-e, --env <envName>', '获取环境变量名称')
    // commmand api注册命令
    // []: 可填项 <>必填项
    const clone = program.command('clone <source> [destination]')
    // 这里不能连写 初始化clone(program.command)会返回一个新的command对象
    clone
    .description('clone a repository')
    .option('-f, --force', '是否强制克隆')
    .action((source, destination, cmdObj) => {
    // cmdObj 当前command
    })

    // addCommnad 注册子命令
    const service = new commander.Command('service')
    service.description('some server options')

    service
    .command('start [port]')
    .description('start service at some port')
    .action((port) => {
    console.log(port)
    })

    service
    .command('stop')
    .description('stop service')
    .action(() => {
    console.log('stop service')
    })

    program.addCommand(service)

    // 高级定制 实现debug 模式
    program.on('option:debug', function () {
    if (this.opts().debug) {
    process.env.LOG_LEVEL = 'verbose'
    }
    })

    // 高级定制 对未知命令监听
    program.on('command:*', function (unknowCmdList) {
    console.log(colors.red(`未知的命令: ${unknowCmdList.join('、')}`))
    const availableCommands = program.commands.map((cmd) => cmd.name())
    console.log(colors.green(`可用的命令: ${availableCommands.join('、')}`))
    })

    // 监听所有的命令输入, 处理未定义的命令
    // program
    // .arguments('[command] [options]')
    // .description('test command', {
    // command: 'command to run',
    // options: 'options for command'
    // })
    // .action((cmd, env) => {
    // // console.log(cmd, env)
    // })

    // 通过独立的的可执行文件实现命令 (注意这里指令描述是作为`.command`的第二个参数)
    // 多脚手架串用 tut install init => tut-i init
    program
    .command('install [name]', 'install package', {
    executableFile: 'tut-i', // 修改可执行文件名称
    isDefault: false,
    hidden: true
    })
    .alias('i') // tut-install

    // 高级定制: 自定义help内容
    // program.helpInformation = function () {
    // return 'info'
    // }
    // program.on('--help', function () {
    // console.log('info')
    // })

    program.parse(process.argv)
    // program.outputHelp()
    // console.log(program.opts())
    // __filename 获取当前目录
    // __dirname 获取当前目录的父目录

    // if (importLocal(__filename)) {
    // } else {
    // // 去除node路径和bin路径
    // require('../lib/index.js')(process.argv.slice(2))
    // }

    痛点分析

    1
    2
    3
    4
    5
    6
    7
    8
    core cli
    commamds init
    models
    utils get-npm-info log utils

    问题
    1. cli 安装速度慢: 所有package都集成在cli里, 因此当命令较多时, 会减慢cli的安装速度
    2. 灵活性差: init 命令只能使用@tut-cli-dev/init, 对于集团公司而言, 每个部门的init命令可能都不相同, 可能需要实现init命令动态化

    流程改进

    1
    脚手架启动阶段 => commander脚手架初始化 => 动态加载initCommand => new initCommand => Command constructor => 命令的准备阶段 => 命令的执行阶段 => init业务逻辑 => end

    图片

    图片

    脚手架创建项目流程设计和开发

    脚手架背后的思考

    1. 可拓展: 能够快速复用到不同的团队, 适用不同团队之间的差异
    2. 低成本: 在不改动脚手架源码的情况下, 能够新增模版, 且新增模版的成本很低
    3. 高性能: 控制存储空间, 安装时充分利用 Node 多进程提升安装性能

    架构图

    图片

    图片

    图片

    inquirer 命令行交互

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    async getProjectInfo() {
    let projectInfo = {}
    // 3. 选择创建项目或组件
    const { type } = await inquirer.prompt({
    type: 'list',
    name: 'type',
    message: '请选择初始化类型',
    default: TYPE_PROJECT,
    choices: [
    { name: '项目', value: TYPE_PROJECT },
    { name: '组件', value: TYPE_COMPONENT }
    ]
    })
    log.verbose('type', type)
    if (type === TYPE_PROJECT) {
    // 4. 获取项目的基本信息
    const project = await inquirer.prompt([
    {
    type: 'input',
    name: 'projectName',
    message: '请输入项目名称',
    validate: function (v) {
    // 1. 输入的首字符必须为英文字符
    // 2. 尾字符必须为英文或数字, 不能为字符
    // 3. 字符仅仅允许'-_'

    // tip
    const done = this.async()

    setTimeout(() => {
    if (
    !/^[a-zA-Z]+(-[a-zA-Z][a-zA-Z0-9]*|_[a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)
    ) {
    done('请输入合法的项目名称')
    return
    }
    done(null, true)
    }, 0)
    }
    },
    {
    type: 'input',
    name: 'projectVersion',
    message: '请输入项目版本号',
    validate: function (v) {
    const done = this.async()

    setTimeout(() => {
    if (!semver.valid(v)) {
    done('请输入合法的版本号')
    return
    }
    done(null, true)
    }, 0)
    },
    filter: function (v) {
    if (!!semver.valid(v)) {
    return semver.valid(v)
    } else {
    return v
    }
    }
    }
    ])
    projectInfo = {
    type,
    ...project
    }
    } else if (type === TYPE_COMPONENT) {
    }

    return projectInfo
    }

    脚手架创建项目和组件功能开发

    1
    2
    3
    4
    5
    6
    7
    下载模版
    安装依赖
    拷贝模版代码至当前目录
    依赖安装(white hosue )
    const WHITE_COMMAND = ['npm', 'cnpm', 'yarn', 'pnpm']
    启动执行命令

    上一篇:
    前端性能揭秘阅读笔记
    下一篇:
    常见数据结构与算法
    本文目录
    本文目录