初识Umi.JS

在这里插入图片描述

什么是Umi.js?

Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

为什么使用Umi.js?

我们做react开发的时候会不会遇到以下问题?:
1.项目做大的时候,开发调试的启动和热更新时间会变得很长。
2.大应用下,网站打开很慢,有没有办法基于路由做到按需加载。
3.dva的model每次都要手写载入,能否一开始就同项目初始化好?

使用乌米,即可解决以上问题,并且还能提供如下优势:

  • 🎉开箱即用,内置 react、react-router 等
  • 📦类 next.js 且功能完备的路由约定,同时支持配置的路由方式
  • 🐠完善的插件体系,覆盖从源码到构建产物的每个生命周期
  • 🚀 一键兼容到 IE9
  • 🍉完善的 TypeScript 支持
  • 🍗与 dva 数据流的深入融合

umi 有 2 和 3 两个版本。两个版本的使用都差不多。umi2 对 javascript 支持比较好,umi3 默认支持 typeScript

起步Umi

node环境安装

建议安装最新的稳定版本,笔者这里为 14.15.3。同时建议使用 yarn

Umi快速上手

创建空目录 umi-learn

# 新建应用
$ mkdir umi-learn && cd umi-learn

# 使用命令
$ yarn create umi

# 安装依赖
$ yarn install

安装过程选择 app

在这里插入图片描述

在这里插入图片描述

项目工程结构

mock                    // mock文件
src
  |-- assets        	// 静态资源文件
  |-- layouts           // 全局布局文件
  |-- pages           	// 项目页面文件
  |-- globals           // 全局样式
  |--.eslintignore     // eslint过滤文件清单
  |--.eslintrc.js      // eslint配置
  |--.eslintignore     // eslint过滤文件清单
  |--.eslintignore     // eslint过滤文件清单
  |--.umirc.js 		   // umi 配置文件

约定式路由

启动 umi start 后,大家会发现 pages 下多了个 .umi 的目录。不要直接在这里修改代码,umi 重启或者 pages 下的文件修改都会重新生成这个文件夹下的文件,约定 pages 下所有的 (j|t)sx? 文件即路由

动态生成路由

npx umi g page demo

page 目录下生成 demo.js 和 demo.css。.umirc.js 会自动生成相对应的路由,访问 /demo 路由。即可看到页面

npx umi g page class/index

page 目录下生成 class 文件夹 / index.js 和 index.css。.umirc.js 会自动生成相对应的路由,访问 /class/index 路由。即可看到页面

手动生成的文件,.umirc.js 文件中不会生成相对应的路由

获取路由中的参数

该文件必须以 $ 开头命名,这时 .umi 文件夹下的 router.js 文件会生成对应的路由

umi2 --> umi3

$ yarn create @umijs/umi-app
$ yarn install

使用dva

在 umi 项目中,你可以使用 dva 来处理数据流,以响应一些复杂的交互操作。
在 umi2 中要使用 dva 的功能很简单,只要使用 umi-plugin-react 插件并配置 dva:true 即可。
修改配置的文件:./umirc.js

// ref: https://umijs.org/config/
export default {
  plugins: [
    // ref: https://umijs.org/plugin/umi-plugin-react.html
    ['umi-plugin-react', {
      antd: true,
      dva: true, // 在此处启用 dva
      dynamicImport: false,
      title: 'hero',
      dll: false,
      routes: {
        exclude: [],
      },
      hardSource: false,
    }],
  ],
}

在dva中,处理数据流的文件统一放在 models 文件夹下,每一个文件默认导出一个对象,里面包含数据和处理数据的方法,通常我们称之为 model 。如以下count.js,model结构一般是如此:

./src/models/count.js
export default {
  namespace: 'count', // 默认与文件名相同
  state: 'count',
  subscriptions: {
    setup({ dispatch, history }) {
    },
  },
  // 同步
  reducers: {
    update(state) {
      return `${state}_count`;
    },
  },
  // 异步
  effects: {
    *fetch({ payload }, { call, put }) {
        yield put({
            type: 'update',
            payload
        })
    },
  },
}

在项目页面中使用model

我们需要导入connect将页面和model绑定在一起。

import { connect } from 'dva';  
function CountPage(props) {  
 //从props属性中打印namespace为count的model的state数据       
  console.log(props.count);      
  return (
    <div className={styles.normal}>
      <h1>数量大小</h1>
      <h2>This is {props.count}</h2>
    </div>
  );
}
export default connect(({ count }) => ({ count }))(CountPage);  

如果使用es7的装饰器,我们可以改成这样的写法:

import { connect } from 'dva'; 
// 装饰器 
@connect(({ count }) => ({ count }))
function CountPage(props) {  
 // 从 props 属性中打印 namespace 为 count 的 model 的 state 数据       
  console.log(props.count);      
  return (
    <div className={styles.normal}>
      <h1>数量大小</h1>
      <h2>This is {props.count}</h2>
    </div>
  );
}
export default CountPage;

mock 文件夹

一般的文件格式如下,umi 的 mock 是对 express 的封装

export default {
    'GET /api/getLists': {
        lists: ['a', 'b', 'c']
    },
    'GET /api/getListsAsync': (req, res) => {
        console.log(req)
        res.json({
            lists: Array(10).fill(req, query.value)
        })
    }
} 

src / services 文件夹

请求有关的处理文件

export function getLists(value) {
    return fetch('/api/getLists?value=' + value)
        .then(res => res.json())
    	.catch(err => {
			console.log(err)
    	})
}

上述内容其实在真实的项目开发当中所用不多,使用 umi 框架开发项目的方式,与 react 几乎无异。既然如何那为何要学?识万卷书,行万里路。见得东西越多,越能明白自己的不足之处。

下面是笔者开发项目架构,各位可以做个参考

在这里插入图片描述
源代码存放在 gitee 中

================================================================

来更新啦啦啦啦啦

================================================================

可以自定义 CLI,以后使用起来更加方便快捷。

啊,生活已经很累了,为啥你还要折磨我
在这里插入图片描述
我重复造轮子不就行了吗?(骂骂咧咧中~~~~~)啊,那随你吧
在这里插入图片描述

PS:
1. npm link 或者 npm 其他情况下如果报错,请使用管理员权限,加个 sudo
2. 请在 github 上创建一个组织,加入进去,在组织中放入自己的代码(不要问为啥,问就是我不想继续探索了,我饿了,找了一个最简单的办法写完,我想去吃饭)

如何在 github 上创建组织

PS:github 容易抽风

需要实现哪些基本功能:

  • 通过 sumi create <name> 命令启动项目
  • 询问用户需要下载的模板
  • 远程拉取模板

1. 创建项目

目录结构

s-umi-cli           
├─ bin                
│  └─ cli.js  # 启动文件      
├─ README.md          
└─ package.json       

配置脚手架启动文件

{
  "name": "s-umi-cli",
  "version": "1.0.0",
  "description": "umi cli",
  "main": "index.js",
  "bin": {
    "sumi": "./bin/cli.js" // 配置启动文件路径,sumi 为别名
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": [],
  "license": "MIT"
}

cli.js

#! /usr/bin/env node

console.log('~~~~~~~')

为了方便开发调试,使用 npm link 链接到全局

npm link

如果报错,请加上 sudo

终端输入

sumi

就可以看见 console.log 中的内容

2. 创建脚手架启动命令

借助 commander 依赖去实现这个需求

2.1 安装依赖
npm install commander --save
2.2 创建命令

cli.js

#! /usr/bin/env node

const program = require('commander')

program
  // 定义命令和参数
  .command('create <app-name>')
  .description('create a new project')
  // -f or --force 为强制创建,如果创建的目录存在则直接覆盖
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    // 打印执行结果
    console.log('name:',name,'options:',options)
  })
  
program
   // 配置版本号信息
  .version(`v${require('../package.json').version}`)
  .usage('<command> [option]')
  
// 解析用户执行命令传入参数
program.parse(process.argv);

中端输入 sumi

sumi
Usage: sumi <command> [option]

Options:
  -V, --version                output the version number
  -h, --help                   display help for command

Commands:
  create [options] <app-name>  create a new project
  help [command]               display help for command

我们可以看到 Commands 下面已经有了 create [options] <app-name>,接着执行一下这个命令

sumi create
error: missing required argument 'app-name'

sumi create my-project
执行结果 >>> name: my-project options: {}

sumi create my-project -f
执行结果 >>> name: my-project options: { force: true }

sumi create my-project --force
执行结果 >>> name: my-project options: { force: true }

成功拿到命令行输入信息

2.3 执行命令

创建 lib 文件夹并在文件夹下创建 create.js

// lib/create.js

module.exports = async function (name, options) {
  // 验证是否正常取到值
  console.log('>>> create.js', name, options)
}

在 cli.js 中使用 create.js

// bin/cli.js

......
program
  .command('create <app-name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if it exist') // 是否强制创建,当文件夹已经存在
  .action((name, options) => {
    // 在 create.js 中执行创建任务
    require('../lib/create.js')(name, options)
  })
......

执行一下 sumi create my-project,此时在 create.js 正常打印了我们出入的信息

sumi create my-project
>>> create.js
my-project {}

在创建目录的时候,需要判断是否已经存在

如果存在

{ force: true } 时,直接移除原来的目录,直接创建
{ force: false } 时 询问用户是否需要覆盖

如果不存在,直接创建

这里用到了 fs 的扩展工具 fs-extra,先来安装一下
fs-extra 是对 fs 模块的扩展,支持 promise

npm install fs-extra --save

接着完善一下 create.js 内部的实现逻辑

// lib/create.js

const path = require('path')
const fs = require('fs-extra')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {

    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      // 询问用户是否确定要覆盖
    }
  }
}

询问部分的逻辑,我们将在下文继续完善

2.4 创建更多命令

如果想添加其他命令也是同样的处理方式

// bin/cli.js

// 配置 config 命令
program
  .command('config [value]')
  .description('inspect and modify the config')
  .option('-g, --get <path>', 'get value from option')
  .option('-s, --set <path> <value>')
  .option('-d, --delete <path>', 'delete option from config')
  .action((value, options) => {
    console.log(value, options)
  })

// 配置 ui 命令
program
  .command('ui')
  .description('start add open roc-cli ui')
  .option('-p, --port <port>', 'Port used for the UI Server')
  .action((option) => {
    console.log(option)
  })
2.5 完善帮助信息

可以看一下 vue-cli 执行 --help 打印的信息

对比 sumi --help 打印的结果,结尾处少了一条说明信息,这里我们做补充,重点需要注意说明信息是带有颜色的,这里就需要用到我们工具库里面的 chalk 来处理

// bin/cli.js

program
  // 监听 --help 执行
  .on('--help', () => {
    // 新增说明信息
    console.log(`\r\nRun ${chalk.cyan(`sumi <command> --help`)} for detailed usage of given command\r\n`)
  })
2.6 打印个 Logo

给脚手架来一个 Logo,使用工具库里的 figlet

// bin/cli.js

program
  .on('--help', () => {
    // 使用 figlet 绘制 Logo
    console.log('\r\n' + figlet.textSync('sumi', {
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 80,
      whitespaceBreak: true
    }));
    // 新增说明信息
    console.log(`\r\nRun ${chalk.cyan(`sumi <command> --help`)} show details\r\n`)
  })

3. 询问用户问题获取创建所需信息

使用 inquirer 解决命令行交互的问题
上一步遗留:询问用户是否覆盖已存在的目录

  • 用户选择模板
  • 用户选择版本
  • 获取下载模板的链接
3.1 询问是否覆盖已存在的目录

这里解决上一步遗留的问题:

如果目录已存在

{ force: false } 时 询问用户是否需要覆盖

逻辑实际上已经完成,这里补充一下询问的内容

安装 inquirer

npm install inquirer --save

然后询问用户是否进行 Overwrite

// lib/create.js

const path = require('path')

// fs-extra 是对 fs 模块的扩展,支持 promise 语法
const fs = require('fs-extra')
const inquirer = require('inquirer')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {

    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir)
    } else {

      // 询问用户是否确定要覆盖
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target directory already exists Pick an action:',
          choices: [
            {
              name: 'Overwrite',
              value: 'overwrite'
            },{
              name: 'Cancel',
              value: false
            }
          ]
        }
      ])

      if (!action) {
        return;
      } else if (action === 'overwrite') {
        // 移除已存在的目录
        console.log(`\r\nRemoving...`)
        await fs.remove(targetAir)
      }
    }
  }
}

github 提供了 api 接口来获取信息

api.github.com/orgs/ 接口获取模板信息
api.github.com/repos/ 接口获取版本信息

我们在 lib 目录下创建一个 http.js 专门处理模板和版本信息的获取

// lib/http.js

// 通过 axios 处理请求
const axios = require('axios')

axios.interceptors.response.use(res => {
  return res.data;
})


/**
 * 获取模板列表
 * @returns Promise
 */
async function getRepoList() {
  return axios.get('https://api.github.com/orgs/zhurong-cli/repos')
}

/**
 * 获取版本信息
 * @param {string} repo 模板名称
 * @returns Promise
 */
async function  getTagList(repo) {
  return axios.get(`https://api.github.com/repos/zhurong-cli/${repo}/tags`)
}

module.exports = {
  getRepoList,
  getTagList
}
3.3 用户选择模板

我们专门新建一个 Generator.js 来处理项目创建逻辑

// lib/Generator.js

class Generator {
  constructor (name, targetDir){
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetDir = targetDir;
  }

  // 核心创建逻辑
  create(){

  }
}

module.exports = Generator;

在 create.js 中引入 Generator 类

// lib/create.js

...
const Generator = require('./Generator')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {
    ...
  }

  // 创建项目
  const generator = new Generator(name, targetAir);

  // 开始创建项目
  generator.create()
}

询问用户选择模版的逻辑

// lib/Generator.js

const { getRepoList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')

// 添加加载动画
async function wrapLoading(fn, message, ...args) {
  // 使用 ora 初始化,传入提示信息 message
  const spinner = ora(message);
  // 开始加载动画
  spinner.start();

  try {
    // 执行传入方法 fn
    const result = await fn(...args);
    // 状态为修改为成功
    spinner.succeed();
    return result; 
  } catch (error) {
    // 状态为修改为失败
    spinner.fail('Request failed, refetch ...')
  } 
}

class Generator {
  constructor (name, targetDir){
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetDir = targetDir;
  }

  // 获取用户选择的模板
  // 1)从远程拉取模板数据
  // 2)用户选择自己新下载的模板名称
  // 3)return 用户选择的名称

  async getRepo() {
    // 1)从远程拉取模板数据
    const repoList = await wrapLoading(getRepoList, 'waiting fetch template');
    if (!repoList) return;

    // 过滤我们需要的模板名称
    const repos = repoList.map(item => item.name);

    // 2)用户选择自己新下载的模板名称
    const { repo } = await inquirer.prompt({
      name: 'repo',
      type: 'list',
      choices: repos,
      message: 'Please choose a template to create project'
    })

    // 3)return 用户选择的名称
    return repo;
  }

  // 核心创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  async create(){

    // 1)获取模板名称
    const repo = await this.getRepo()
    
    console.log('用户选择了,repo=' + repo)
  }
}

module.exports = Generator;

此时,成功拿到模板名称 repo 的结果 ✌️

3.4 用户选择版本

过程和 3.3 一样

// lib/generator.js

const { getRepoList, getTagList } = require('./http')
...

// 添加加载动画
async function wrapLoading(fn, message, ...args) {
  ...
}

class Generator {
  constructor (name, targetDir){
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetDir = targetDir;
  }

  // 获取用户选择的模板
  // 1)从远程拉取模板数据
  // 2)用户选择自己新下载的模板名称
  // 3)return 用户选择的名称

  async getRepo() {
    ...
  }

  // 获取用户选择的版本
  // 1)基于 repo 结果,远程拉取对应的 tag 列表
  // 2)用户选择自己需要下载的 tag
  // 3)return 用户选择的 tag

  async getTag(repo) {
    // 1)基于 repo 结果,远程拉取对应的 tag 列表
    const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
    if (!tags) return;
    
    // 过滤我们需要的 tag 名称
    const tagsList = tags.map(item => item.name);

    // 2)用户选择自己需要下载的 tag
    const { tag } = await inquirer.prompt({
      name: 'tag',
      type: 'list',
      choices: tagsList,
      message: 'Place choose a tag to create project'
    })

    // 3)return 用户选择的 tag
    return tag
  }

  // 核心创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  async create(){

    // 1)获取模板名称
    const repo = await this.getRepo()

    // 2) 获取 tag 名称
    const tag = await this.getTag(repo)
     
    console.log('用户选择了,repo=' + repo + ',tag='+ tag)
  }
}

module.exports = Generator;

到此询问的工作就结束了,可以进行模板下载了

4. 下载远程模板

下载远程模版需要使用 download-git-repo 工具包,但它是不支持 promise的,所以我们这里需要使用 util 模块中的 promisify 方法对其进行 promise 化。

4.1 安装依赖与 promise 化
npm install download-git-repo --save

进行 promise 化处理

// lib/Generator.js

...
const util = require('util')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise

class Generator {
  constructor (name, targetDir){
    ...

    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }
  
  ...
}
4.2 核心下载功能

接着,就是模板下载部分的逻辑了

// lib/Generator.js

...
const util = require('util')
const path = require('path')
const downloadGitRepo = require('download-git-repo') // 不支持 Promise

// 添加加载动画
async function wrapLoading(fn, message, ...args) {
  ...
}

class Generator {
  constructor (name, targetDir){
    ...

    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }
  ...
  
  // 下载远程模板
  // 1)拼接下载地址
  // 2)调用下载方法
  async download(repo, tag){

    // 1)拼接下载地址
    const requestUrl = `zhurong-cli/${repo}${tag?'#'+tag:''}`;

    // 2)调用下载方法
    await wrapLoading(
      this.downloadGitRepo, // 远程下载方法
      'waiting download template', // 加载提示信息
      requestUrl, // 参数1: 下载地址
      path.resolve(process.cwd(), this.targetDir)) // 参数2: 创建位置
  }

  // 核心创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  // 4)模板使用提示
  async create(){

    // 1)获取模板名称
    const repo = await this.getRepo()

    // 2) 获取 tag 名称
    const tag = await this.getTag(repo)

    // 3)下载模板到模板目录
    await this.download(repo, tag)
    
    // 4)模板使用提示
    console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
    console.log(`\r\n  cd ${chalk.cyan(this.name)}`)
    console.log('  npm run dev\r\n')
  }
}

module.exports = Generator;

完成这块,一个简单的脚手架就完成了
来试一下效果如何,执行 sumi create my-project

这个时候,我们就可以看到模板就已经创建好了
s-umi-cli

├─ bin                      
│  └─ cli.js                
├─ lib                      
│  ├─ Generator.js          
│  ├─ create.js             
│  └─ http.js               
├─ my-project .............. 我们创建的项目             
│  ├─ public                
│  │  ├─ favicon.ico        
│  │  └─ index.html         
│  ├─ src                   
│  │  ├─ assets             
│  │  │  └─ logo.png        
│  │  ├─ components         
│  │  │  └─ HelloWorld.vue  
│  │  ├─ App.vue            
│  │  └─ main.js            
│  ├─ README.md             
│  ├─ babel.config.js       
│  └─ package.json          
├─ README.md                
├─ package-lock.json        
└─ package.json             

5. 发布项目

上面都是在本地测试,实际在使用的时候,可能就需要发布到 npm 仓库,通过 npm 全局安装之后,直接到目标目录下面去创建项目,如何发布呢?

第一步,在 git 上建好仓库
第二步,完善 package.json 中的配置

{
  "name": "zhurong-cli",
  "version": "1.0.4",
  "description": "",
  "main": "index.js",
  "bin": {
    "zr": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "files": [
    "bin",
    "lib"
  ],
  "author": {
    "name": "T-Roc",
    "email": "lxp_work@163.com"
  },
  "keywords": [
    "zhurong-cli",
    "zr",
    "脚手架"
  ],
  "license": "MIT",
  "dependencies": {
    "axios": "^0.21.1",
    "chalk": "^4.1.1",
    "commander": "^7.2.0",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.5.0",
    "fs-extra": "^10.0.0",
    "inquirer": "^8.0.0",
    "ora": "^5.4.0"
  }
}

第三步,使用 npm publish 进行发布,更新到时候,注意修改版本号

这样就发布成功了,我们打开 npm 网站搜索一下 🔍

已经可以找到它了,这样我们就可以通过 npm 或者 yarn 全局安装使用了。

关注公众号:大明贵妇,获取 Umi.js 学习资料(回复 Umi ),期待各位客官来临
在这里插入图片描述

参考文章:https://www.jianshu.com/p/dc493809a2fd

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值