前端团队基建心路历程

2,174 阅读6分钟

痛点

  • 项目多了之后,每个项目重复安装相同的依赖包,每个项目都很“重”;
  • 组件、功能复用性,复制粘贴,一个改动,各个项目跟着都得修改调试,太难以维护;
  • 成员多了之后,代码风格参差不齐,难以规范管理,项目产能也就因人而异了;
  • 不同复杂度项目的 webpack 构建环境配置甚至不一样,需要一一去匹配,然后等你想升级的时候发现工作量巨大;
  • webpack 更迭,导致项目中构建版本参差不齐,难以维护,甚至发现相同的代码,换个项目不能用了...;
  • 各个项目生产环境打包结构差异,难以控制结构,造成的运维负担;
  • 欢迎大家补充更多痛点,一起解决;
    痛点.png

解决思路

以上种种都将不同程度的影响团队效能,团队效能对于产品发展的重要性想必大家都很清楚,下面分享一下我的解决思路:

  • 统一前端开发规范,目录结构、语法、命名等;
    • 硬方式,构建工具加入对应语法监听(tslint / stylelint / htmllint / jslint等),然后配置对应语法的描述文件;
    • 软方式,习惯养成计划,通过 codereview 方式周期性维护团队开发习惯;
    • 思路和好处灌输的必要性,代领团队共同成长;
  • 统一前端物料(公共组件、公共UI、工具类、sdk等);
    • 公共组件库建立的必要性,统筹产品的复用业务组件,写一次,所有项目通用;
    • 公共 UI,统一前端样式规范,这个需要 UED 配合来统一建设;
    • 私有仓库建设的必要性,公开库隐私托管,比如阿里 Node.js 性能平台 > 模块仓库,这个是免费的,目前用下来还不错;
  • 统一构建流程;
    • webpack 构建流程统一,比如支持对 vue / react / angular 的支持,这个视团队技术栈定论;
    • 开发团队 cli 工具,统一配置公共 webpack.config.js,统一的优势便是方便统一管理和升级,不过劣势也很明显,一荣俱荣,一损俱碎,不过我觉得优势更甚,要对自己有信心;
  • 统一前端 CI(持续集成) / CD(持续交付);
    • 上面构建流程统一后,便可以统一前端 构建产物 的规则,再配合迭代版本号,构建产物 变有条不紊了;
    • 构建产物固定文件规则,方便运维认知和维护,避免不必要的运维成本;
    • 结合 oss构建产物 的版本管理,这里就可以扩展 cli 指令,去对接 oss api,完成对各个项目迭代版本的 构建产物 的上传和管理,这里就可以轻松做到版本的切换自如了,好处可想而知;
      解决思路.png

上图便是 cli 解决方案核心模块功能图,可以解决上述众多 痛点,可以统一管理前端众多项目,并且解耦项目中构建相关依赖包,项目变“轻”,项目的各方面维护都只会更简单和便捷,下面分享一下实现的一些细节与功能核心;

欢迎大家提出更多思路,多多益善,帮助完善 cli;

cli 开发核心点

自定义脚本指令

  1. 首先需要了解 package.json 文件中的 bin 属性,该属性专门用来定义用户的自定义脚本,安装完包之后,会自动在 ./node_modules/.bin/ 中建立一个软连接(window下叫快捷方式);
    {
        "name": "@iosecret/cli",
        "version": "0.0.2",
        "description": "iosecret cli",
        "bin": {
            "iosecret": "bin/iosecret",
            "iosecret-api": "bin/iosecret-api",
            "iosecret-dev": "bin/iosecret-dev",
            "iosecret-prod": "bin/iosecret-prod"
        }
    }
    
    • name 这里定义包名,即将来发布到 npm 仓库 或者其他私有库提供外部安装下载的唯一标识;
    • version 当前发布的版本号,每次 npm publish 时版本号必须不同;
    • bin 声明快捷命令指向,以上配置执行 npm link 或者通过仓库安装后将会在本地生成一个软连接,映射到当前 bin/iosecret 文件,即执行 iosecret 指令,将直接执行 bin/iosecret 脚本;
  2. 其实大家可能也注意到了我们常用的 webpack/gulp/rimraf等等在 ./node_modules/.bin 下出现,然后在 package.json 文件中的 scripts 使用的脚本会默认到 ./node_modules/.bin 下进行查找;
    {
        "scripts": {
            "rm": "rimraf dist"
        }
    }
    
    • 这里若执行 npm run rm 则会默认之前 scriptsrm 指令,即 rimraf dist,而npm则会默认先去 ./node_modules/.bin 下寻找指令 rimraf,再去当前系统命令中寻找指令;

指令引导

  1. 这里需要用到 commander 用来定义指令描述和指令扩展;
// 解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。
#!/usr/bin/env node

const program = require("commander");

program
  .version(require("../package").version)
  // 定义指令描述
  .usage(
    `
  iosecret api -n [name]
  iosecret dev -t [type]
  iosecret prod -t [type] -i`
  )
  // 定义指令描述详情
  .description(
    `Params:
  api:
    -n [name] 模块名称

  dev|prod:
    -t [type] 语言类型,vue/react/angular
    -i 是否打印详细信息`
  )
  // 定义指令备注
  .command("api", "生成api文件")
  .command("dev", "开发环境构建")
  .command("prod", "生产环境构建");
  
// 解析参数
program.parse(process.argv);
  1. 执行指令 iosecret,可以看到下图现象,这里指令的雏形已完成

单指令开发

  1. 获取指令参数
const program = require("commander");

program
  .usage("[options] -i")
  // 定义接收参数类型
  .option("-t, --type [type]", "语言类型, vue/react/angular")
  .option("-i, --info", "是否打印记录")
  // 解析指令参数
  .parse(process.argv);
  
// program.type ==> 获取语言类型参数
// program.info ==> 获取是否打印记录
  1. 开启子进程监听,执行其他系统命令的时候需要用到,比如 scp/ps等Linux命令,这里需要调用 webpack-dev-server 去开启构建的开发模式,这里需要参考 child_process api

const { join } = require("path");
// node 子进程调用方法
const { spawn } = require("child_process");

// 开启子进程监听, 用法参考 
// 这里的思路是通过调用 webpack-dev-server 再去调用对应的 webpack.dev.js 配置文件,去开启项目开发模式监听
// 具体的配置文件即可对应团队项目技术栈去适配,理论上可以去支持各种技术栈选型的
const build = spawn(
  "node",
  [
    `${join(
      __dirname,
      "../node_modules/webpack-dev-server/bin/webpack-dev-server.js"
    )}`,
    "--watch",
    "--config",
    `${join(__dirname, `../lib/iosecret-build/webpack.dev.js`)}`
  ],
  {
    stdio: "inherit",
    shell: true,
    cwd: process.cwd()
  }
);

build.on("error", error => console.error(error));

build.on("exit", code => {
  console.log("exit", code);
  process.exitCode = code;
  process.exit();
});

指令拓展

  1. 生产模式监听
#!/usr/bin/env node

const { red } = require("chalk");
const program = require("commander");

program
  .usage("[options] -i")
  .option("-t, --type [type]", "语言类型, vue/react/angular")
  .option("-i, --info", "是否打印记录")
  .parse(process.argv);

const { getApp } = require("../lib/utils/getConfig");
// 获取项目配置文件
const appConfig = getApp();

if (!appConfig) {
  console.error(red("> 缺少 app 配置"));
  return;
}

const webpack = require("webpack");
const isInfo = program.info;

webpackConfig = require(`../lib/iosecret-build/webpack.prod.js`)({
  ...appConfig
});

const buildStamp = Date.now();

// webpack 启动监听
webpack(webpackConfig, async (err, stats) => {
  if (err || stats.hasErrors()) {
    if (err) {
      console.error(red(err.stack || err));
      err.details && console.error(red(err.details));
      return;
    }

    stats.hasErrors() &&
      console.error(stats.toString({ colors: true, chunks: false }));

    console.log("\n> 构建异常 \n");
  } else {
    isInfo && console.warn(stats.toString({ colors: true, chunks: false }));

    console.log(`\n> 构建完成,耗时 ${Date.now() - buildStamp} ms`);
  }
});
  1. 其他工具拓展,比如 api 代码生成,原理是基于 swagger api json 去生成对应 ts 代码,这个在多项目组迭代进程中能省不少事
#!/usr/bin/env node

const program = require("commander");

program
  .usage("[options] -n")
  .option("-n, --name [name]", "api模块别名")
  .parse(process.argv);

if (typeof program.name !== "string" || !program.name) {
  console.log(yellow("> 缺少api模块别名"));
  console.log(yellow("> iosecret api --name api模块别名"));
  return;
}

const { join } = require("path");
const { getApi } = require("../lib/utils/getConfig");
const ApiGenerator = require("../lib/iosecret-api/generate");

const config = getApi();
// 初始化实例
const generator = new ApiGenerator({
  swaggerApi: config[program.name],
  destPath: join(process.cwd(), config.destPath),
  filename: program.name
});
// 生成
generator.start();

总结

以上简单列出了基本的实现思路,其实开发过程中遇到了挺多问题的,这里就不再赘述了,欢迎小伙伴们提出新的见解,一起探讨。