美文网首页
企业级脚手架分析与调研

企业级脚手架分析与调研

作者: 这个前端不太冷 | 来源:发表于2022-05-21 22:09 被阅读0次
e98439c2700d0181383771997.png

大纲
😁 什么是脚手架
😁 企业级脚手架的原理
😁 命令行工具的实现思路
🏆 举个例子 create-react-app命令行工具
😁 企业级脚手架模版的设计
🏆 企业级脚手架模版的基本组成有哪些
🏆 举个例子 UmiJS


什么是脚手架

编程领域中的“脚手架(Scaffolding)”指的是能够快速搭建项目“骨架”的一类工具。例如大多数的React项目都有src目录,public目录,webpack配置文件,babel配置等等,而src目录中又通常包含components目录,reducers目录等等。每次在新建项目时,你不得不手动创建这些固定的文件目录,繁琐而累赘。脚手架的作用就是帮助你完成这些重复性的工作,包括一键生成主要的目录结构、安装依赖等等。create-react-app、UmiJS就是著名的脚手架工具。

企业级脚手架的原理

我们一定使用过,create-react-app或vue-cli这些官方的脚手架。回忆下使用方式:

create-react-app my-app
vue create my-app

在进入到my-app目录,从package.json文件中找到对应的命令,执行即可开发自己的项目了。
这里我们把整个流程抽象一下:

  1. 先通过npm安装了一个包(脚手架)
  2. 这个包里有一个可执行文件,可以在全局执行对应的命令。
  3. 执行命令,会将用户输入的信息捕获,并与用户交互。
  4. 命令最终会将一个配置好的模板,拷贝到用户本地目录下,我们就可以直接用它来开发项目。
    逻辑如下:


    脚手架原理.png

于是,可以把脚手架的实现分为2个主要部分:

  1. 命令行工具的开发;
  2. 不同场景脚手架模版的开发

命令行工具的实现思路

企业级脚手架命令行工具的开发,首先,我们要发布一个npm包。这样就用户就可以通过安装命令行工具的包选择相应的配置去github上拉取对应的模版代码。所以命令行工具的实现思路如下:

  1. 注册npm账户,用于后期发包
  2. 开发终端命令工具,进行命令解析,解析成功后执行相应测操作,比如create-react-app my-app,匹配到create-react-app命令后开始执行my-app工程的创建工作
  3. 命令识别成功之后,需要实现一个终端交互功能,来获取获取用户的输入信息,最终根据信息匹配出合适的模版
  4. 匹配到模版之后需要拷贝模板文件,从远程下载文件到本地->读取下载的内容->将文件中特殊标识提换成用户输入->生成对应的文件及目录->将原目录删除。

以上思路有一些非常好用的工具可以帮助我们开发。学会将多种工具组合使用来达成自己的目标,也是一种能力。这里列出开发脚手架基础的包

  • commander:获取终端输入的参数
  • inquirer:交互式命令行工具
  • download-git-repo:下载模板
  • chalk:美化控制台显示
  • fs-extra:文件的操作,复制,粘贴,增加,删除,文件内容的新增,替换

实现代码如下:
1.终端命令工具命令实现:

// 四种模板。对应我git仓库四个仓库地址
const tempIndex = {
    react: 'reactTemplate', // react 模板路径
    vue: 'vueTemplate', // vue 模板路径
    h5: 'h5Template', // h5模板路径
    dva: 'dvaTemplate', // dva模板路径
};

let templateName; // 模板名称
let inputIndex; // 除了拷贝模板,还支持自定义模板路径下载,
const program = new commander.Command(packageJson.name)
    .version('v' + packageJson.version, '-v, --version')
    .arguments('<templateName>')
    .option('-f, --force', 'force delete the exist director')
    .option('-d, --directly', 'copy the not specified template')
    .alias('cp')
    .description('create-blue-cli react myProject')
    .action(function (index,name) {
        inputIndex = index;
        // 允许目标项目名和要复制的模板类型名顺序颠倒
        if (tempIndex[index] || tempIndex[name]) {
            if (tempIndex[index]) {
                templateName = tempIndex[index];
            } else {
                templateName = tempIndex[name];
            }
        }
        if (program.directly) {
            templateName = index;
        }
    });
program.parse(process.argv)
// 没有输入任何参数,报语法错误,并打印help
if (program.args.length === 0) {
    console.log(chalk.red('syntax error'));
    program.help()
}

if (templateName) {
    inquire(templateName, program.force);
} else {
    console.log(`the template ${inputIndex} you want download do not exist`);
}
  1. 终端交互功能的实现
const inquire = (templateName, force) => {
    inquirer.prompt([{
        type: 'list', // 开发环境
        name: 'latype',
        prefix: '****',
        suffix: '****',
        message: '请选择脚手架模版:',
        choices: [
            'react',
            'vue',
            'h5',
            'dva'
        ],
        default: 0
    }]).then(answers => {
        create(templateName, tempIndex[answers.latype], force);
    });
}
  1. 拷贝模板文件:
   async function create(temp, renameFile, force = false) {
    tempName = temp;
    forceDel = force;
    const file = process.cwd().replace(/\\/g, '/') + '/';
    try {
        // 检测项目文件夹是否已存在, 若存在,抛出错误
        const res = await fs.pathExists(file);
        if (res) {
            if (forceDel) {
                console.log(green('force remove the exist directory'));
                await fs.remove(file);
                downloadByGit(renameFile, tempName);
            } else {
                // 抛出错误,并提示可以使用-f参数来强制删除已存在的项目
                console.log(chalk.red('Error, In this directory, the project name already exsits !'));
                console.log(chalk.green('you can use option -f to force delete the directory !'));
            }
            return;
        }
        // 若不存在,直接从git下载
        downloadByGit(renameFile, tempName);
    } catch (err) {
        console.error(red(err));
    }
}

关于chalk,这是一款颜色标记插件,将要打印的文字用不同的颜色标记出来,像下面这样:

image.png
  1. git文件下载:
  function downloadByGit(callback, template) {
    console.log(green('start download'));
    console.log(`git@github.com:closertb/${template}.git`);
    const result = spawn(
        'git',
        ['clone', `git@github.com:closertb/${template}.git`],
        { stdio: 'inherit' }
    );
    const error = result.error;
    if (error) {
        console.log(red(error));
        return;
    }
    // 定义回调;
    callback && callback();
}

主要就这4段代码,就实现了命令的解析,和从git源端拷贝模板到本地。

举个例子 create-react-app命令行工具

企业级脚手架架模版的设计

企业级脚手架的基本组成有哪些
前端脚手架.png
举个例子 UmiJS

umi,中文可发音为乌米,是一个可插拔的企业级 react 应用框架,默认开启约定式路由、按需加载配置.
umi跟create-react-app有什么区别呢?大多数前端同学都试过用cra来搭react项目吧,每次总是要花很多时间来配置typescript/less/css modules,还有配套的react router/redux等,搭建一个基础项目就能花上好几天,而umi这个企业级的前端应用框架,则已经帮我们内置了很多前端开发常用的功能,让我们节省了很多时间去配置,做到真正的开箱即用。

执行npx @umijs/create-umi-app初始化出来的react项目,天然就支持typecsript、less、css modules,并整合了antd、dva(阿里自研的数据流方案),提供国际化、权限、数据流、配置式路由等开发者常用的功能,能够节省大量的初始化项目时间。

抄个架构图

Umijs.png
约定化的思想

在使用umi框架的时候,很容易就发现它很多东西都是约定式的。所谓约定式就是指,按照约定好的方式开发,就能达到某种效果,中间的过程由框架帮我们完成,特别适合我们这种懒懒的开发。

  • 建一个 locales 目录,就拥有了国际化
  • 建一个 models 目录,就拥有了数据流
  • 建一个 mock 目录,就拥有了数据 mock
  • 建一个 access.ts 文件,就拥有了权限策略
  • ...

这看起来是非常黑盒非常酷的,用这种方式其实对于团队代码风格的统一是非常有好处的,直接在框架层面就约束了大家的目录组织模式,便于团队维护。

但是缺点也是挺明显的,灵活性不如配置式的高,因为只能按特定的模式来开发,如果原本约定的方式不满足业务需求,就需要额外开发umi插件来魔改原本的功能。而且约定式的开发是相对其他框架来说很特别的一点,对新上手的同学来说需要时间去通读官网文档了解约定式的规则。

约定式路由

约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过src/pages目录和文件及其命名分析出路由配置, 也就是让umi根据约定好的目录结构帮我们生成路由配置文件。

比如以下文件结构:

  └── pages
    ├── index.tsx
    └── users.tsx
    └── setting
       └── index.tsx

会得到以下路由配置,

[
  { exact: true, path: '/', component: '@/pages/index' },
  { exact: true, path: '/users', component: '@/pages/users' },
  { exact: true, path: '/setting', component: '@/pages/setting/index' }
]

需要注意的是,满足以下任意规则的文件不会被注册为路由,

  • 以.或_开头的文件或目录
  • 以d.ts结尾的类型定义文件
  • 以test.ts、spec.ts、e2e.ts结尾的测试文件(适用于.js、.jsx和.tsx文件)
  • components和component目录
  • utils和util目录
  • 不是.js、.jsx、.ts或.tsx文件
  • 文件内容不包含 JSX 元素

动态路由

src/pages/users/[id].tsx 生成的对应path /users/:id

嵌套路由

└── pages
    └── users
        ├── _layout.tsx
        ├── index.tsx
        └── list.tsx

生成的对应path

[
  { exact: false, path: '/users', component: '@/pages/users/_layout',
    routes: [
      { exact: true, path: '/users', component: '@/pages/users/index' },
      { exact: true, path: '/users/list', component: '@/pages/users/list' },
    ]
  }
]

全局Layout

src/layouts/index.tsx

[
  { exact: false, path: '/', component: '@/layouts/index',
    routes: [
      { exact: true, path: '/', component: '@/pages/index' },
      { exact: true, path: '/users', component: '@/pages/users' },
    ],
  },
]

404路由

src/pages/404.tsx

生成的对应path

[
  { exact: true, path: '/', component: '@/pages/index' },
  { exact: true, path: '/users', component: '@/pages/users' },
  { component: '@/pages/404' },
]
 

项目结构

├── /dist/           # 项目输出目录
├── /mock/           # 数据mock
├── /public/         # 公共文件,编译时copy至dist目录
├── /src/            # 项目源码目录
│ ├── /components/   # UI组件及UI相关方法
│ ├── /layouts/      # 全局组件
│ │ └── index.js     # 入口文件
│ ├── /models/       # 数据模型
│ ├── /pages/        # 页面组件
│ │ └── index.js     # html模版
│ ├── /services/     # 数据接口
│ ├── /themes/       # 项目样式
│ │ ├── default.less # 全局样式
│ │ └── vars.less    # 全局样式变量
│ ├── /utils/        # 工具函数
│ │ ├── config.js    # 项目常规配置
│ │ ├── menu.js      # 菜单及面包屑配置
│ │ ├── config.js    # 项目常规配置
│ │ ├── request.js   # 异步请求函数(axios)
│ │ └── theme.js     # 项目需要在js中使用到样式变量
├── package.json     # 项目信息
├── .eslintrc        # Eslint配置
└── .umirc.js        # umi配置
└── .umirc.mock.js   # mock配置
└── .theme.config.js # 主题less编译配置

.umirc.js

alias: { // webpack 额外配置一些文件夹的快速访问
    themes: resolve(__dirname, './src/themes'),
    components: resolve(__dirname,"./src/components"),
    utils: resolve(__dirname,"./src/utils"),
    config: resolve(__dirname,"./src/utils/config"),
    enums: resolve(__dirname,"./src/utils/enums"),
    services: resolve(__dirname,"./src/services"),
    models: resolve(__dirname,"./src/models"),
    routes: resolve(__dirname,"./src/routes"),
}

约定式路由,建立文件夹就生产路由
类 next.js 的约定式路由,无需再维护一份冗余的路由配置,支持权限、动态路由、嵌套路由等等。

云端打包会把package.json ^ 开头的包都拉当下大版本最新的包安装来打包

相关文章

网友评论

      本文标题:企业级脚手架分析与调研

      本文链接:https://www.haomeiwen.com/subject/plecprtx.html