微前端构建工具Cli的诞生

604 阅读7分钟

一、微前端背景及脚手架的开始诞生

基于内部CRM系统复杂的业务场景及组内技术栈统一(小组内使用的技术栈是React,大团队后续统一向Vue方向发展)的强大背景下,将前端系统改造成微前端体系已经迫在眉睫。

前端发展历史长河,iframe是最早的微前端体系解决方案,iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。但是使用过iframe嵌入网页前端工程师们都知道打开页面真的是太慢啦同时还有其他问题,使用iframe开发前端界面的缺点:

  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  • UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  • 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

让我们不得不放弃他选择新的解决方式。

阿里qiankun微前端库qiankun,基于single-spa库做了相应的封装提供了开箱即用的API能力。通过Html Entry配置,像iframe嵌入页面一样简单,同时内部提供了CSS和JS沙箱隔离的能力。资源预加载,在浏览器空闲时间内加载子应用的资源。

问题:但是基座和子应用的样式还是会存在冲突问题,如果是vue,在style里面使用style标签自带的scoped属性,即可解决样式冲突问题,React可以配置css moudle的不同的规则,如果规则相同,样式还是会存在冲突。

解决方案:主应用的样式规则: 子应用样式规则:
同时各大厂都了自己微前端的实践方案:字节提供了的微前端开源工具Garfish,同时开源了前端生态体系mordern,方便我们通过命令的方式创建主子应用,而且还提供了远程调试本地子应用的方式。因为内部已经基于qiankun做了业务实现,同时也想完善组内微前端体系及在开发过程中的痛点我们也想搭建自己的微前端脚手架,并在此启发下,基座能够提供远程调试的能力。 远程调试子应用的具体实现方案:url配置子应用注册的必要参数,如下图所示,通过获取url地址的配置参数然后向子应用列表中注册子应用

二、脚手架的实现

1、项目初始化

npm init 初始化一个项目,初始化成功以后会帮助我们创建一个package.json文件

2、添加脚手架命令

配置脚本文件

在package.json文件中添加bin配置来指定自己的脚手架命令及可执行文件,可配置多个,接下来我们开始micro-next.js文件的编写 #!/usr/bin/env node 是必须添加的,指定node解析这个脚本;在项目根目录下执行npm link就可以将脚手架命令添加到全局脚本了,如果没有这一步我们的脚本命令将无法执行。

具体原理: 首先我们了解一下npm的几个命令:npm install把发布在npmjs平台上的模块包下载到本地,npm install -g在下载包的同时,还帮助我们配好全局变量,让我们可以直接使用命令而无需通过node来执行,或着配置package.json里的script脚本来run。但是没有发布过的npm包如何使用呢,这就需要用到了npm link 。 npm link 帮助我们模拟包安装后的状态,它会在系统中做一个快捷方式映射,让本地的包就好像install过一样,可以直接使用。在MAC中,我们在终端可以直接敲命令,其实是在执行/usr/local/bin目录下的脚本,这个目录可以认为是我们的全局命令所在的地方。npm link 以后,/usr/local/lib 下的 node_modules  里不是存的真实的文件,而是存了一个快捷方式,指向你当前执行  npm link  的目录。如果开发的的是 node 包,则执行的命令名和真实执行的文件入口,会通过项目的 package.json 里 bin 的配置来获取。

commander注册命令

配置.version查看版本号,注意的是末尾的program.parse(process.argv)的代码不能丢,如果没有则无法解析我们输入的参数。

command注册脚手架命令
action执行命令的操作
description 对命令的描述
option定义选项

通过运行脚手架,我们可以看到自己注册的脚手架命令和相应的描述

inquirer处理用户交互

我们经常看到vue或者create脚手架,有命令行交互,那么他是如何做到的呢,那么我们就来介绍一下inquirer(inquirer的使用指南),专门用于处理命令行用户交互。

inquirer.prompt([
          {
            type: 'list',
            message: isSubApp
              ? '请选择您的微前端子应用技术栈'
              : '请选择您的微前端基座技术栈',
            name: 'techStack',
            choices: [
              { name: 'React', value: '1' },
              { name: 'vue 2.0', value: '2' },
              // { name: 'vue 3.0', value: '2' },
            ],
          },
        ])
        .then((answers) => {
          // 根据用户输入进行相应的处理
        })
        .catch((error) => {
           console.error(`抛出异常:${error}`)
        })

根据用户输入的选项进行不同技术栈下模板或微前端项目的改造

download-git-repo下载模板项目

类似于vue-cli或其他脚手架工具也是从git上下载一份代码工程到本地。

const download = require('download-git-repo');
// projectRoot是项目的目录
module.exports = function (projectRoot) {
  // 模板文件的git地址,#branch 如果默认是master分支也需要带上
  const url = https://github.com/Jenny1029/micro-template.git#basic-react;
  return new Promise((resolve, reject) => {
    download(`direct:${url}`, projectRoot || 'test', { clone: true }, (err) => {
      if (err) {
        // 异常抛出
        reject(err);
      } else {
        // 下载完成
        resolve(projectRoot);
      }
    });
  });
};

文件目录名的校验,如果文件名已存在,则提示用户是否进行覆盖,使用inquire的type类型为confirm,default默认答案是false。因为涉及到文件读写的操作,就需要使用nodejs 的fs 模块

const glob = require('glob');
// process.cwd() 是当前执行node命令时候的文件夹地址
//__dirname 是被执行的js 文件的地址 ——文件所在目录

const list = glob.sync('*'); // 遍历当前目录,数组类型
  let next = null;
  let rootName = path.basename(process.cwd());
  if (list.length) {
    // 如果当前目录不为空
    if (
      list.some((n) => {
        const fileName = path.resolve(process.cwd(), n);
        const isDir = fs.statSync(fileName).isDirectory();
        return projectName === n && isDir; // 找到创建文件名和当前目录文件存在一致的文件
      })
    ) {
      // 如果文件已经存在
      next = () =>
        new Promise((resolve, reject) => {
          inquirer
            .prompt([
              {
                name: 'isRemovePro',
                message: `项目${projectName}已经存在,是否覆盖文件`,
                type: 'confirm',
                default: false,
              },
            ])
            .then((answer) => {
              if (answer.isRemovePro) {
                removeDir(path.resolve(process.cwd(), projectName));
                rootName = projectName;
                resolve(projectName);
              } else {
                next = undefined;
                reject('停止创建');
              }
            });
        });
} else {
    next = () => Promise.resolve(projectName); // 返回resole函数,并传递projectName
}

cfonts在控制台添加字体炫酷效果 chalk修改控制台字体的样式

模板下载成功以后,给用户提示实现较好的用户体验。

具体使用:

// 成功提示
const successLog = `cd ${projectRoot}\nnpm install\n${
context.techStack === '1' ? 'npm run start' : 'npm run serve'
}`;
console.log(chalk.green(successLog))

//失败提示
console.log(chalk.red(`${err}`))

最后实现的效果: 我们的一个比较完整的脚手架就已经搭建完成了。

三、脚手架npm包的发布

可以参考这篇文章发布npm包:xie.infoq.cn/article/54b…  

常见问题:

  1. 你如果使用的公司的npm源,需要使用nrm 进行源切换
  2. 发布的npm包需要保证唯一性,如果发布失败,可能是因为包名重复,提示没有权限发布该包,需要更改包名重新发布
  3. 发布成功以后下次发布需要更新package.json version的版本号
  4. 因为是脚手架命令,我们需要使用npm install -g xxx安装到本地才可使用

最后:下次增加一个微前端项目就不需要再次打开qiankun文档配置微前端相关的文件啦,缩短了项目前期的搭建工作。欢迎大家使用micro-next-cli脚手架对搭建一个微前端工程,也欢迎留言指出您的需求。micro-next-cli脚手架使用文档