什么是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, // 在此处启用 dvadynamicImport: 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 几乎无异。既然如何那为何要学?识万卷书,行万里路。见得东西越多,越能明白自己的不足之处。
下面是笔者开发项目架构,各位可以做个参考
================================================================
来更新啦啦啦啦啦
================================================================
可以自定义 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 nodeconsole.log('~~~~~~~')
为了方便开发调试,使用 npm link 链接到全局
npm link
如果报错,请加上 sudo
终端输入
sumi
就可以看见 console.log
中的内容
2. 创建脚手架启动命令
借助 commander 依赖去实现这个需求
2.1 安装依赖
npm install commander --save
2.2 创建命令
cli.js
#! /usr/bin/env nodeconst 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 commandCommands:create [options] <app-name> create a new projecthelp [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.jsmodule.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.jsconst 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.jsprogram// 监听 --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.jsprogram.on('--help', () => {// 使用 figlet 绘制 Logoconsole.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.jsconst 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.jsclass 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.jsconst { getRepoList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer')// 添加加载动画
async function wrapLoading(fn, message, ...args) {// 使用 ora 初始化,传入提示信息 messageconst spinner = ora(message);// 开始加载动画spinner.start();try {// 执行传入方法 fnconst 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.jsconst { 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 用户选择的 tagasync 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)用户选择自己需要下载的 tagconst { tag } = await inquirer.prompt({name: 'tag',type: 'list',choices: tagsList,message: 'Place choose a tag to create project'})// 3)return 用户选择的 tagreturn 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') // 不支持 Promiseclass 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