微前端自动化实践

912 阅读6分钟

一、前言

​ 脚手架可以快速生成项目模板,节约创建和配置项目时间,提升开发效率;比如,vue、angular和react都各自生成脚手架的CLI工具。最近在使用微前端的架构开发项目,以微前端应用为例,开发一个CLI工具来帮我们快速生成项目模板。

项目地址:github.com/limzgiser/m…

二、微前端CLI

1、准备模板

​ 预先从网上找几个为前端框架模板,分别使用不同的分支来管理,并上传到github。后面会使用控制台询问的方式让用户输入命令,来从指定分支下载模板。

我找了几个测试模板并上传到我的github,地址:github.com/limzgiser/m…

1.png

2、相关插件

  • art-template

    • 模板引擎
    • 使用用户输入的变量替换模板文件中的占位符。
  • chalk

    • node终端样式库
    • 改变控制台输出样式
  • commander

    • node.js命令行界面解决方案
  • download-git-repo

    • node下载并提取一个git库(GitHub, GitLab, Bitbucket)
  • execa

    ​ 调用shell和本地外部程序的javascript库,会启动子进程执行

  • fs-extra

    • 扩展node fs
  • inquirer

    • 用户与命令行交换工具
  • lodash

    • js实用工具库,提供丰富工具函数
  • ora

    • 优雅的终端转轮

主要会用到这些插件,可以到npm或github中搜索了解插件的使用。

3、获取用户输入参数

​ 使用commander插件创建create命令,获取用户输入参数。需要用户在创建项目的时候,输入appType(创建主应用还是子应用)、frameType(应用使用什么框架实现)、install(是否自动安装项目依赖),默认不安装、pkg-took(如果安装依赖,是使用npm还是yarn安装)。

// index.ts
import { Command } from 'commander';
import create from "./actions/create";
const program = new Command('mfc');
program.version('0.0.1')
program
  .usage('create <projectName> [options]')
  .command('create <projectName>')
  .description('创建项目')
  .option('-a --appType [value]', '选择应用类型:主应用(main)|子(child)')
  .option('-t --frameType [value]', '选择框架类型:vue|react|angular')
  .option('-i --install', '是否自动安装依赖', false)
  .option('-pt --pkg-tool [value]', 'npm or yarn?')
  .action(create);
program.parse(process.argv)

action输入一个回调函数,我们可以在回调中拿到用户输入的参数。例如,在控制台中输入一个使用vue编写的主应用,且创建完成后自动使用yarn安装项目的依赖。

export default async function (projectName: string, options: CreateOptions) {
   console.log('项目名称:', projectName);
   console.log('输入参数:', options);
}

2.png

4、下载远程模板

​ 根据用户输入的应用类型和框架类型参数,到远程仓库下载指定分支的项目模板。分支名是使用"应用类型+'_‘+框架类型"命名。用户在创建项目时,如果没有指定应用类型和框架类型参数就在控制台询问,让用户选择。

// 获取用户输入应用类型和框架类型参数
async function getTplParams(options: CreateOptions) {
  let appType = '', frameType = '';
  if (!options.appType) {
      // 如果没有输入appType就询问用户,让用户选择
    const answers = await inquirer.prompt([appTypeQues]);
    appType = answers.appType;
  } else {
    appType = options.appType;
  }
  if (!options.frameType) {
      // 如果没有输入项目名称就询问用户,让用户选择
    const answers = await inquirer.prompt([frameTypeQues]);
    frameType = answers.frameType;
  } else {
    frameType = options.frameType;
  }
  return {
    appType,
    frameType
  }
}
// inquirers.ts
const appTypeQues = {
  type: 'list',
  name: 'appType',
  choices: ['main', 'child'],
  default: 'npm',
  message: '选择应用类型,主应用(main)或子应用(child)!'
}
const frameTypeQues = {
  type: 'list',
  name: 'frameType',
  choices: ['vue', 'react', 'angular'],
  default: 'npm',
  message: '选择应用框架!'
}

3.png

​ 在获取应用类型和框架类型参数用,就可以根据参数拼接成分支名,从远程仓库下载项目模板。

  let { appType, frameType } = await getTplParams(options);
  let branch = appType + "_" + frameType;
  try {
    const spinner = ora(chalk.blue("初始化模版...")).start();
    await downloadTemplate(
      'direct:https://github.com/limzgiser/mfc-cli.git#' + branch,
      projectName,
      { clone: true }
    );
    spinner.info('模版初始化成功');
  } catch (error) {

  }

4.png

5、处理模板

​ 如何动态修改模板中的变量,例如:使用创建项目时候输入的项目名称替换package.json的name值。首先递归获取项目目录中的文件路径,并标识当前路径是否是文件夹。提去文件路径列表,然后遍历读取文件,使用模板引擎替换文件中的变量。最后将替换后的内容替换原内容。

// 递归读取项目目录,标识是否是文件夹
function recursiveDir(sourceDir: string) {
  const res: FileItem[] = [];
  function traverse(dir: string) {
    readdirSync(dir).forEach((file: string) => {
      const pathname = `${dir}/${file}`;
      const isDir = statSync(pathname).isDirectory();
      res.push({ file: pathname, isDir });
      if (isDir) {
        traverse(pathname);
      }
    })
  }
  traverse(sourceDir);
  return res;
}
// 使用模板引擎绑定变量
function tplFile(projectName: string, files: Array<FileItem>) {
  files.forEach(item => {
    if (!item.file.includes('assets') && !item.file.includes('public')  ) {
      const content = template(process.cwd() + '/' + item.file, { projectName });
      let dest = item.file;
      if (dest.includes('.art')) {
        unlinkSync(dest);
        dest = dest.replace(/\.art/, '');
      }
      writeFileSync(dest, content);
    }
  });
}

5.png

6、安装依赖

​ 如果用户创建项目时指定了自动安装依赖,就直接进入使用什么工具安装依赖,npm或yarn?如果用户没有指定自动安装依赖,就询问用户,是否安装依赖?选否就跳过安装,是,则询问用户使用npm还是yarn安装?如果选择yarn判断用户是否安装了yarn,如果没有,就提示用户先安装yarn。

 spinner.info('模版初始化成功');
    const cwd = './' + projectName;
    if (options.install) {
      installPkg(options.pkgTool, cwd);
    } else {
      const answers = await inquirer.prompt([
        installQues,
        {
          ...pkgToolQues,
          when(currentAnswers) {
            return currentAnswers.install && !options.pkgTool;
          }
        }
      ]);
      if (answers.install) {
        installPkg(answers.pkgTool || options.pkgTool, cwd);
      } else {
        console.log(chalk.green('项目创建成功'));
      }
    }
// 安装依赖包
async function installPkg(pkgTool: "npm" | "yarn", cwd: string) {
  let tool = pkgTool;
  if (!tool) {
    const answers = await inquirer.prompt([pkgToolQues]);
    tool = answers.pkgTool;
  }
  if (tool === 'yarn' && !hasYarn()) {
    console.log(chalk.red('请先安装yarn'));
  } else {
    const spinner = ora(chalk.blue('正在安装依赖...')).start();
    await exec(tool + ' install', { cwd });
    spinner.succeed(chalk.green('项目创建成功'));
  }
}
// 执行安装依赖
function exec(command: string, options: execa.Options) {
  return new Promise((resolve, reject) => {
    const subProcess = execa.command(command, options);
    subProcess.stdout!.pipe(process.stdout);
    subProcess.stdout!.on('close', resolve);
    subProcess.stdout!.on('error', reject);
  });
}
// 判断是否安装yarn
function hasYarn(): boolean {
  try {
    execa.commandSync('yarn -v', { stdio: 'ignore' });
    return true;
  } catch (error) {
    return false;
  }
}

7、创建子命令

​ 可以通过子命令(add)来为已创建的项目,添加组件、指令等模板。下面是一个创建组件的命令。组件的模板就不需要到远程仓库里面下载了,可以定义在本地目录中。支持用户创建.vue和.tsx组件。

program.addCommand(childCommand(Command));
export default function (Command: CommandConstructor) {
  const generate = new Command('add');
  generate
    .command('c <name>')
    .description('添加一个组件')
    .option('--tsx', 'Is tsx', false)
    .action(addComponent);
  return generate;
}
import template from "art-template";
import { join } from "path";
import { outputFileSync } from "fs-extra";
import { kebabCase } from "lodash";
import chalk from "chalk";
export default function (name: string, options: { tsx: boolean; }) {
  let basePath = 'components';
  let trueName = name;
  const data = name.split('/');
  if (data.length > 1) {
    trueName = data.pop()!;
    basePath = data.join('/');
  }
  let suffix = '.vue';
  if (options.tsx) {
    suffix = '.tsx';
  }
  try {
    const content = template(
      join(__dirname, '../../templates', 'component' + suffix),
      { name: trueName, rootCls: kebabCase(trueName) }
    );
    const dest = `src/${basePath}/${trueName}${suffix}`;
    outputFileSync(dest, content);
    console.log(chalk.green('创建成功>>', dest));
  } catch (e) {
    console.log(chalk.red('创建失败'));
    throw e;
  }
}

8、编译TS

​ 使用gulp来编译项目。在根目录中创建gulp.ts文件:

import { src, dest, series } from 'gulp';
import del from 'del';
import gts from 'gulp-typescript';
const outputDir = 'dist';
on clean() {
  return del(outputDir);
}
function script() {
  return src('src/**/*.ts', { base: 'src' })
    .pipe(gts.createProject('tsconfig.json')())
    .pipe(dest(outputDir));
}
export default series(clean, script);

​ 在package.json中添加script命令,执行编译ts项目。

  "scripts": {
    "build": "gulp"
  },
npm run build

9、测试发布脚手架

​ package.json中添加bin。

 "bin": {
    "mfc": "index.js"
  },

​ 在根目录中创建index.js

#! /usr/bin/env node

require('./dist/index');

​ 控制台执行脚本

cnpm link

​ 测试脚手架

mfc create child-vue

10、发布脚手架

​ 在package.json中指定发布包含的文件,files数组中定义的项,会上传到npm。

 "files": [
    "templates",
    "dist",
    "index.js",
    "package.json",
    "readme.md"
  ],

​ cli作为库被安装时,devDependencies中依赖不会被安装,所以,cli发布后使用的插件要明确为开发依赖。

npm login
npm publish

6.png

三、总结

​ 详细可以参考【前端自动化高手训练营】,年前看的,一直没有实践。正好最近在使用微前端架构开发项目。借此场景写了个CLI工具,后续应该还会继续完善。

四、参考资源

【前端自动化高手训练营】www.bilibili.com/video/BV11K…