IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    偷“师”学“艺” · 看不见我的美 · 是你瞎了眼

    馬腊咯稽发表于 2021-07-19 00:00:00
    love 0
    偷“师”学“艺”

    monorepo

    公司的主要业务是混合云管,所以除了官网这种 ToC 的项目之外,其余均是 ToB 的管理后台项目;根据具体业务分为了:容器服务、镜像管理、服务商监控、应用管理等等。这些子项目有统一的 UI 风格、相近的交互逻辑、耦合的业务逻辑;所以非常适合 monorepo 的项目管理方式。

    Lerna 是前端 monorepo 比较好的解决方案,搭配 yarn 可以非常好的管理项目外部与项目之间的依赖。每个项目都是一个独立的 package,每个 package 都有自己的 workspace,可以将自己的 APIs 通过 lib 暴露并 publish 到内部 npm 仓库,这样每个 package 既是独立的项目也是其他 package 的依赖。

     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
    
    // lerna.json
    {
     "packages": [
     "packages/@cli/*", // 脚手架依赖,放置 linters 规则和配置
     "packages/@common/*", // 公共依赖,放置工具函数和组件
     "packages/@project/*" // 业务代码,放置具体的业务实现
     ],
     "version": "independent", // 独立模式,每个 package 有独立的版本号
     "useWorkspaces": true, // 启用 workspace 模式
     "npmClient": "yarn", // 使用 yarn 取代 npm
     "command": {
     "publish": {
     "ignoreChanges": ["ignored-file", "*.temp", "*.md"], // 不会包含在更改/发布中的文件,防止为不必要地更改发布新版本,例如修复 ReadMe 拼写错误
     "message": "chore(release): publish", // 发布新版本时,自定义的提交消息
     "registry": "https://npm.pkg.github.com", // 包发在哪里,一般是公司内部的 npm 服务器
     "conventionalCommits": true // 自动生成 ChangeLog
     },
     "bootstrap": {
     "ignore": "component-*",
     "npmClientArgs": ["--no-package-lock", "--production"]
     }
     }
    }
    // package.json
    {
     "name": "lerna",
     "private": true,
     "workspaces": [
     "packages/@cli/*",
     "packages/@common/*",
     "packages/@project/*"
     ],
     // ...
    }
    

    Lerna 两种模式:

    • 固定模式,项目统一单个版本(所有的包使用一个版本号,Babel 使用的这种模式),版本信息保存在根目录下的 lerna.json 中;如果主版本号为 0,任何更新都将被视为不兼容更新;
    • 独立模式,每个包的版本号是独立的(公司使用的这种模式,因为每个包是一个独立的项目、不是一个完整功能的一部分,搭配 yarn 干活不累);发包时,需要指定此次更新的粒度(修复、小版本、大版本?)。

    Lerna 常用命令:

    • lerna init 创建/升级为 Lerna 仓库;
      • –independent/-i 使用独立的 版本控制模式
    • lerna bootstrap 进入引导流程,安装所有依赖并链接交叉依赖;
    • lerna create [name] [loc] 创建新的包,名字为 name 位置在 loc;
    • lerna import [pathToRepo] 导入本地路径中的 repo 并提交 commit;
    • lerna exec – [command] [..args] 对每个包中执行任意命令;
      • – rm -rf ./node_modules 删除每个包下的 node_modules
    • lerna publish 发布自上次发布之后有更新的包(private 为 true 的除外),提示“输入新版本号”并更新 git 和 npm 上的所有包;
      • –force-publish [packages] 强制发布“一个、多个(以逗号分隔)或全部(*)”软件包
      • –npm-tag [tagname] — 使用给定的 npm dist-tag(默认 latest)发布到 npm
      • –canary/-c 创建一个 canary 版本
      • –skip-git 不运行 git 命令
    • lerna changed 检查自上次发布以来哪些软件包被修改过;
    • lerna clean 移除所有包的 node_modules 目录(不会移除根目录的 node_modules,即使加上 –hoist 参数);
    • lerna diff [package?] 列出所有或某个软件包自上次发布以来的修改情况;
    • lerna info 打印本地环境信息;
    • lerna run [script] 在每一个包含 [script] 脚本的包中运行此 NpmScript;
      • lerna run build–concurrency 5 对所有包进行打包,最大并行数为 5
    • lerna list/ls 列出当前 Lerna 仓库中的所有公共软件包;
      • –json 以 Array JSON 的形式输出
      • –ndjson 以 Newline Delimited JSON 的形式输出
      • –all 列出 private 为 true 的包(默认不输出)
      • –long 展示更多信息
      • –parseable 展示可解析输出,而不是列化视图
      • –toposort 按拓扑顺序进行排序输出
      • –graph 以图的形式输出
    • lerna link 为所有依赖彼此的包软连接在一起。
      • lerna link convert 将公用的 devDependencies 放到根目录的 package.json

    commit

    对于需要多人维护的复杂项目,相同的代码样式和具体的提交信息是必要的;借助 husky 和 lint-staged(相似的工具还有 pretty-quick,不过只能调用 Prettier,不能调用 Lints),可以对待提交的文件进行校验、格式化,也可以对 commit message 进行约束。

    提交工作流钩子

    • pre-commit:在键入提交信息前运行;它用于检查即将提交的快照,例如,检查是否有所遗漏,确保测试运行,以及核查代码;不过你可以用 git commit –no-verify 来绕过这个环节。你可以利用该钩子来检查代码风格是否一致(运行类似 lint 的程序)等;
    • prepare-commit-msg:在启动提交信息编辑器之前,默认信息被创建之后运行;它允许你编辑提交者所看到的默认信息。它对一般的提交来说并没有什么用;然而对那些会自动产生默认信息的提交,如提交信息模板、合并提交、压缩提交和修订提交等非常实用;
    • commit-msg:接收一个参数(存有当前提交信息的临时文件的路径)。可以用来在提交通过前验证项目状态或提交信息;
    • post-commit:在整个提交过程完成后运行;该钩子一般用于通知之类的事情。

    workspace 信息

     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
    
    const { execSync } = require('child_process');
    // 将 yarn workspaces info 输出为对象
    const getWorkspacesInfo = () => {
     let temp;
     try {
     const yarnStdout = execSync('yarn workspaces info', {
     encoding: 'utf8'
     })
     .replace(/^yarn workspaces.*$/m, '')
     .replace(/^Done.*$/m, '')
     .replace(/\n/g, '');
     temp = JSON.parse(yarnStdout);
     } catch (error) {
     throw new Error(
     `执行 yarn workspaces info 命令时出错,具体如下:\n${error}`
     );
     }
     if (Object.prototype.toString.call(temp) !== '[object Object]') {
     return {};
     }
     return temp;
    };
    const getWorkspaces = () => {
     let temp;
     try {
     temp = getWorkspacesInfo();
     } catch (error) {
     throw new Error(`获取不到 workspaces 信息。\n${error}`);
     }
     if (Object.prototype.toString.call(temp) !== '[object Object]') {
     return [];
     }
     return Object.keys(temp).map(name => ({ name, ...temp[name] }));
    };
    const getWorkspacesName = () => {
     let temp;
     try {
     temp = getWorkspacesInfo();
     } catch (error) {
     throw new Error(`获取不到 workspaces name 信息。\n${error}`);
     }
     if (Object.prototype.toString.call(temp) !== '[object Object]') {
     return [];
     }
     return Object.keys(temp);
    };
    exports.getWorkspaces = getWorkspaces;
    exports.getWorkspacesInfo = getWorkspacesInfo;
    exports.getWorkspacesName = getWorkspacesName;
    

    git 信息

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    const { execSync } = require('child_process');
    const { getWorkspacesName } = require('./workspace');
    // 获取当前所在 git 分支
    const getCurrentBranch = () => {
     try {
     const gitStatus = execSync('git status', { encoding: 'utf8' });
     return gitStatus.split('\n')[0].replace('On branch ', '');
     } catch (error) {
     throw new Error(`执行 git 命令时出错,具体如下:\n${error.toString()}`);
     }
    };
    const getCommitScopes = () => {
     const temp = getWorkspacesName();
     if (Array.isArray(temp)) {
     return temp.map(name =>
     name.charAt(0) === '@' ? name.split('/')[1] : name
     );
     }
     return [];
    };
    exports.getCurrentBranch = getCurrentBranch;
    exports.getCommitScopes = getCommitScopes;
    

    stylelint 配置

     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
    
    module.exports = {
     defaultSeverity: 'error',
     extends: [
     'stylelint-config-standard',
     'stylelint-config-rational-order',
     'stylelint-config-prettier'
     ],
     ignoreDisables: true,
     ignoreFiles: ['**/*.json'],
     plugins: [
     'stylelint-declaration-block-no-ignored-properties',
     'stylelint-declaration-strict-value',
     'stylelint-scss'
     ],
     rules: {
     'at-rule-no-unknown': null,
     'scss/at-rule-no-unknown': true,
     'plugin/declaration-block-no-ignored-properties': true,
     'scale-unlimited/declaration-strict-value': [
     ['/color$/', 'z-index'],
     {
     ignoreKeywords: {
     '/color$/': ['currentColor', 'inherit', 'transparent'],
     'z-index': ['auto']
     },
     disableFix: true
     }
     ]
     }
    };
    

    eslint 配置

     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
    
    module.exports = {
     globals: {
     __PRERENDER__INJECTED__: 'readonly',
     __PROJECT__: 'readonly'
     },
     env: {
     browser: true,
     node: true
     },
     extends: [
     'airbnb-base', // 剔除 react 规则 的 eslint-config-airbnb
     'plugin:vue/essential', // vue 2.x 规则
     'plugin:promise/recommended', // promise 规则
     'plugin:markdown/recommended', // markdown 规则
     'plugin:prettier/recommended' // prettier 冲突规则
     ],
     plugins: ['html'],
     root: true, // 根配置
     rules: {
     'import/prefer-default-export': 'off', // 不强制使用默认导出
     'no-param-reassign': [
     'error',
     {
     props: false // 禁止对函数参数进行重新赋值,形参属性除外
     }
     ],
     'no-plusplus': [
     'error',
     {
     allowForLoopAfterthoughts: true // 允许在 for 循环中使用 ++
     }
     ],
     'no-restricted-syntax': ['error', 'WithStatement'], // 可以使用函数表达式和 in 操作符号,不能用 with
     'no-shadow': [
     'error',
     {
     allow: ['state', 'series'] // 禁止变量声明与外层作用域的变量同名
     }
     ],
     'no-underscore-dangle': 'off', // 标识符中可以有下划线
     'no-unused-expressions': [
     'error',
     {
     allowShortCircuit: true // 允许在表达式中使用逻辑短路求值
     }
     ],
     'no-use-before-define': [
     'error',
     {
     functions: false // 禁止在变量定义之前使用它们,函数声明除外
     }
     ],
     'prefer-destructuring': [
     'error', // 不强制使用数组解构
     {
     array: false,
     object: true
     }
     ],
     'promise/always-return': 'off', // then 方法可以没有返回值
     'promise/catch-or-return': [
     'error',
     {
     allowFinally: true // 允许使用 finally
     }
     ],
     'vue/require-default-prop': 'off', // 不要求 prop 有默认值
     'vue/require-prop-types': 'off' // 不要求必须规定 prop 类型
     }
    };
    

    prettier 配置

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    {
     "arrowParens": "avoid",
     "bracketSpacing": true,
     "embeddedLanguageFormatting": "auto",
     "endOfLine": "lf",
     "htmlWhitespaceSensitivity": "strict",
     "jsxBracketSameLine": true,
     "jsxSingleQuote": true,
     "printWidth": 120,
     "proseWrap": "preserve",
     "quoteProps": "consistent",
     "semi": false,
     "singleQuote": true,
     "tabWidth": 2,
     "trailingComma": "es5",
     "useTabs": false,
     "vueIndentScriptAndStyle": true
    }
    

    husky 配置

    .husky/commit-msg 钩子:

    1
    2
    3
    4
    
    #!/bin/sh
    . "$(dirname "$0")/_/husky.sh"
    
    npx --no-install commitlint --edit "$1"
    

    .husky/pre-commit 钩子:

    1
    2
    3
    4
    
    #!/bin/sh
    . "$(dirname "$0")/_/husky.sh"
    
    npx lint-staged
    

    commitlint 配置

    commitlint.rules:

     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
    
    function customSubjectActionEnum({ subject }, when = 'never', value = []) {
     const always = when === 'always';
     if (!always) {
     return [true];
     }
     if (
     typeof subject !== 'string' ||
     (typeof subject === 'string' && subject.length === 0)
     ) {
     return [false, `提交的简要描述不能为空`];
     }
     const action = subject.slice(0, 2);
     if (!value.find(item => action === item)) {
     return [
     false,
     `提交的简要描述应该以中文动词加一个空格起头,动词可选值为 [${value}],当前值为 '${action}'`
     ];
     }
     if (!subject.startsWith(`${action} `) || subject.slice(3, 4) === ' ') {
     return [false, `提交的简要描述中动词后需且仅需添加一个空格`];
     }
     return [true];
    }
    function customScopeEnum({ scope }, when = 'never', value = []) {
     const always = when === 'always';
     if (!always) {
     return [true];
     }
     return [
     value.find(item => scope === item),
     `scope 必须为 [${value}] 其中之一,当前为 ${scope}`
     ];
    }
    module.exports = {
     'custom-subject-action-enum': customSubjectActionEnum,
     'custom-scope-enum': customScopeEnum
    };
    

    commitlint.config:

     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
    
    const { getCommitScopes } = require('./git');
    const rules = require('./commitlint.rules');
    const commitScopes = getCommitScopes();
    /*
     * type(scope?): subject
     * body?
     * footer?
     */
    module.exports = {
     extends: ['@commitlint/config-conventional'],
     plugins: [
     {
     rules
     }
     ],
     /*
     * Level-[0.1.2]: 0-disables、1-warning、2-error
     * Applicable-always|never: never-inverts the rule
     * Value: value to use for this rule
     */
     rules: {
     'body-full-stop': [0],
     'body-leading-blank': [2, 'always'],
     'body-empty': [0],
     'body-max-length': [2, 'always', 144],
     'body-max-line-length': [0],
     'body-min-length': [0],
     'body-case': [0],
     'footer-leading-blank': [2, 'always'],
     'footer-empty': [0],
     'footer-max-length': [0],
     'footer-max-line-length': [2, 'always', 1],
     'footer-min-length': [0],
     'header-case': [0],
     'header-full-stop': [0],
     'header-max-length': [0],
     'header-min-length': [0],
     'references-empty': [0],
     'scope-enum': [0],
     'scope-case': [2, 'always', 'lower-case'],
     'scope-empty': [2, 'never'],
     'scope-max-length': [0],
     'scope-min-length': [0],
     'subject-case': [0],
     'subject-empty': [2, 'never'],
     'subject-full-stop': [0],
     'subject-max-length': [0],
     'subject-min-length': [2, 'always', 5],
     'type-enum': [
     2,
     'always',
     ['build', 'ci', 'feat', 'fix', 'st', 'sit', 'style', 'version']
     ],
     'type-case': [2, 'always', 'lower-case'],
     'type-empty': [2, 'never'],
     'type-max-length': [0],
     'type-min-length': [0],
     'signed-off-by': [0],
     'custom-subject-action-enum': [
     2,
     'always',
     [
     '添加',
     '完善',
     '修复',
     '解决',
     '删除',
     '禁用',
     '修改',
     '调整',
     '优化',
     '重构',
     '发布',
     '合并'
     ]
     ],
     'custom-scope-enum': [2, 'always', ['root', ...commitScopes]]
     }
    };
    

    commitizen 配置

     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
    
    const { getCommitScopes } = require('./git');
    const scopes = getCommitScopes();
    module.exports = {
     subjectLimit: 72,
     subjectSeparator: ': ',
     typePrefix: '',
     typeSuffix: '',
     scopes: [{ name: 'root' }, ...scopes.map(name => ({ name }))],
     scopeOverrides: {},
     allowCustomScopes: false,
     allowBreakingChanges: ['feat', 'fix', 'st', 'sit'],
     skipQuestions: [],
     appendBranchNameToCommitMessage: false,
     // ticketNumberPrefix: '',
     // breakingPrefix: '',
     // footerPrefix: '',
     breaklineChar: '|',
     upperCaseSubject: false,
     askForBreakingChangeFirst: false,
     types: [
     {
     name: 'build: 涉及构建系统及与其相关的外部依赖变动',
     value: 'build'
     },
     {
     name: 'ci: 涉及 CI 配置文件或脚本修改的变动',
     value: 'ci'
     },
     {
     name: 'feat: 增加了新的功能特征,对应 action 只能为“添加”',
     value: 'feat'
     },
     {
     name: 'fix: 修复 bug,对应 action 只能为“修复”',
     value: 'fix'
     },
     {
     name: 'st: 解决 ST 单子,仅限 release 分支,对应 action 只能为“修复”',
     value: 'st'
     },
     {
     name: 'sit: 解决 SIT 单子,仅限 release 分支,对应 action 只能为“修复”',
     value: 'sit'
     },
     {
     name: 'style: 不影响代码含义的风格修改,比如空格、格式化、缺失的分号等,对应 action 只能为“调整”',
     value: 'style'
     },
     {
     name: 'version: 发布版本,一般由脚本自动化发布,对应 action 只能为“发布”',
     value: 'version'
     }
     ],
     messages: {
     type: '选择本次更改的提交类型:\n',
     scope: '选择本次更改涉及模块:\n',
     subject: '输入针对本次更改的简要说明:\n',
     body: '输入针对本次更改的详细描述(非必须),使用" | "进行换行,每行长度不要超过 72: \n', // 这里有问题
     breaking:
     '如果本次更改存在不兼容的修改,则须对更改的内容进行详细描述,否则无需填写:\n',
     footer: '列举本次更改对应的 Tapd 缺陷 ID(比如 #31、#34): \n',
     confirmCommit: '是否使用以上提交信息进行提交?'
     }
    };
    

    package.json 对应配置

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    {
     "lint-staged": {
     "**/*.{js,ts,vue,htm,html,md}": "eslint --format=codeframe",
     "**/*.{css,scss,sass,vue}": "stylelint --formatter verbose",
     "**/*": "prettier --check"
     },
     "config": {
     "commitizen": {
     "path": "cz-customizable"
     }
     },
     "scripts": {
     "commit": "git add . && cz",
     "format": "prettier --write .",
     "eslint:fix": "eslint --fix . --ext .js,.ts,.vue,.htm,.html,.md",
     "stylelint:fix": "stylelint --fix **/*.{css,scss,sass,vue}",
     }
     // ...
    }
    

    整个提交流程

    注意:git add . 相比 git add –all,不处理删除文件;仅提交当前目录或者后代目录下的文件。

    verdaccio

    私有仓库使用 Verdaccio 搭建并使用 pm2 守护(找运维小哥):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    # 安装 wget
    yum install -y wget
    # 下载 node
    wget https://nodejs.org/dist/v10.6.0/node-v10.6.0-linux-x64.tar.xz
    # 解压 node
    tar -xvf node-v10.6.0-linux-x64.tar.xz
    # 重命名安装目录
    mv node-v10.6.0-linux-x64.tar.xz nodejs
    # 建立软连接
    ln -s /usr/local/node/nodejs/bin/npm /usr/local/bin
    ln -s /usr/local/node/nodejs/bin/node /usr/local/bin
    # 安装 verdaccio
    npm install -g verdaccio
    # 建立软连接
    ln -s /usr/local/node/nodejs/bin/verdaccio /usr/local/bin
    # 安装 pm2
    npm install -g pm2
    # 建立软连接
    ln -s /usr/local/node/nodejs/bin/pm2 /usr/local/bin
    # 使用 pm2 运行 verdaccio
    pm2 start verdaccio
    

    配置 .npmrc、.yarnrc:

    1
    
    registry "http://npm.inner.com/"
    

    配置 package.json:

    1
    2
    3
    4
    5
    6
    
    {
     "publishConfig": {
     "registry": "http://npm.inner.com/"
     }
     // ...
    }
    

    安装依赖时,会向私服请求 package,如果找不到,服务器就会向 npmjs.org 请求并缓存。

    request

    后端数据会通过无渲染组件请求,组件通过 props 接受请求参数,再将返回数据通过作用域插槽暴露。

    RlApiRequest 的 render 函数返回了 $scopedSlots.default(this.cSlotScopeProps);继承了 RlApiRequest 的组件通过传入不同的 props 和 aSlotScopeProps(会改写 cSlotScopeProps),达到不同的目的。

    pipes

    对于数据的处理都会交给管道层,组件通过 props 接受原始数据,处理过的数据通过默认的作用域插槽返回。

    多个 pipe 共同组成了 pipes 总管道,通过传入 enablePipeFormat、enablePipeFilter 等 props 控制对应管道的开闭;本质上是作用域插槽的应用。



沪ICP备19023445号-2号
友情链接