✨
搭建一个开箱即用的基于 UmiJS + AntDesign + TailwindCSS + TypeScript 的工程
UI框架以Ant-Design
为例
编写此笔记时所使用的UmiJS
版本为4.0.87
相关文档
初始化项目
mkdir umi-starter && cd umi-starter
pnpm dlx create-umi@latest
初始化完毕后再安装一个cross-env
用来兼容在不同的操作系统中使用环境变量
pnpm add -D cross-env
🎉
这样就创建好一个以UmiJS
为脚手架的基础工程了,接下来我们对它做亿点点额外的配置
配置EditorConfig
新建.editorconfig
,设置编辑器和 IDE 规范,内容根据自己的喜好或者团队规范
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
配置ESLint
Umi的方案
详细文档看这里
pnpm add -D @umijs/lint eslint stylelint
touch .eslintrc.js
touch .eslintignore
touch .stylelintrc.js
module.exports = {
extends: require.resolve('umi/eslint'),
rules: {
complexity: ['error', 10],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
}
.DS_Store
node_modules
dist
.idea
.vscode
.umi
.umi-production
module.exports = {
extends: require.resolve('umi/stylelint'),
rules: {
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['tailwind', 'apply'],
},
],
},
}
{
"scripts": {
// ...
"lint": "umi lint"
}
}
社区方案
如果你想用其他的社区方案的话,这里推荐使用Nuxt团队的Anthony Fu大佬的eslint-config
pnpm dlx @antfu/eslint-config@latest
编辑eslint.config.mjs
import antfu from '@antfu/eslint-config'
export default antfu({
ignores: ['node_modules', '**/node_modules/**', 'dist', '**/dist/**', '.umi', '**/.umi/**'],
formatters: true,
typescript: true,
react: true,
})
编辑package.json
,添加如下内容
{
// ...
"scripts": {
// ...
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}
配置Prettier
官方脚手架有快速生成Prettier
配置的指令,详细的文档看这里
⚡提示
prettier-plugin-organize-imports
这个插件的作用是自动移除没有被使用的import
,如果不想要这个功能就在plugins
字段中移除
pnpm umi g prettier
{
"printWidth": 120,
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "es5",
"proseWrap": "never",
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
}
node_modules
.umi
.umi-production
.DS_Store
dist
.idea
.vscode
⚡注意
如果你的ESLint
配置使用的是上述社区方案,并且又想同时使用prettier
的话,需要编辑.vscode/settings.json
,把prettier
启用。因为 Anthony Fu 大佬的这套eslint-config
默认禁用prettier
{
"prettier.enable": true
// ...
}
安装TailwindCSS
官方脚手架有快速生成TailwindCSS
配置的指令,详细的文档看这里
pnpm umi g tailwindcss
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/pages/**/*.{jsx,tsx}',
'./src/components/**/*.{jsx,tsx}',
'./src/layouts/**/*.{jsx,tsx}',
],
corePlugins: {
preflight: true,
},
plugins: [],
}
@tailwind base;
@tailwind components;
@tailwind utilities;
助手函数
新建src/utils/utils.ts
,封装一些辅助函数,具体代码参考我的助手函数封装
插件
由于普通的 Umi 应用中,默认不附带任何插件,所以我们需要先安装它
pnpm add -D @umijs/plugins
关于 Umi 插件的详细文档看这里,Umi 的官方插件列表看这里
数据流插件
为了拥有良好的开发体验,以hooks
范式使用和管理全局状态,我们需要启用@umijs/plugin-model
插件
编辑.umirc.ts
或者config/config.ts
文件
export default defineConfig({
// ...
plugins: [
// ...
'@umijs/plugins/dist/model',
],
model: {},
})
示例
数据流插件要求在src
目录下创建一个models
目录,该目录下存放需要全局共享的数据
mkdir src/models
touch src/models/count.ts
import { useCallback, useState } from 'react'
export default () => {
const [counter, setCounter] = useState(0)
const increment = useCallback(() => setCounter((c) => c + 1), [])
const decrement = useCallback(() => setCounter((c) => c - 1), [])
return { counter, increment, decrement }
}
然后编辑src/pages/index.tsx
和src/pages/docs.tsx
import { useModel } from 'umi'
export default function HomePage() {
const { counter, increment, decrement } = useModel('count')
return (
<div>
<button onClick={decrement}>minus</button>
<span className="mx-3">{counter}</span>
<button onClick={increment}>plus</button>
</div>
)
}
import { useModel } from 'umi'
export default function DocsPage() {
const { counter, increment, decrement } = useModel('count')
return (
<div>
<button onClick={decrement}>minus</button>
<span className="mx-3">{counter}</span>
<button onClick={increment}>plus</button>
</div>
)
}
启动项目查看这个计数器例子,可以看到在HomePage
页面中修改了counter
的值后,DocsPage
页面中也会跟着改变
请求插件
编辑.umirc.ts
或config/config.ts
export default defineConfig({
plugins: [
// ...
'@umijs/plugins/dist/request',
],
request: {},
})
新建src/app.tsx
,编写如下请求配置
查看
import type { AxiosRequestConfig, AxiosResponse, RequestConfig } from 'umi'
// 错误处理方案: 错误类型
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3,
REDIRECT = 9,
}
// 与后端约定的响应数据格式
interface ResponseStructure<T = any> {
success: boolean
code: string
data?: T
message?: string
[key: string]: any
}
export const request: RequestConfig = {
errorConfig: {
// 错误抛出
errorThrower: (res: ResponseStructure) => {
const { success, data, errorCode, errorMessage, showType } = res
if (!success) {
const error: any = new Error(errorMessage)
error.name = 'BizError'
error.info = { errorCode, errorMessage, showType, data }
throw error // 抛出自制的错误
}
},
// 错误接收及处理
errorHandler: (error: any, opts) => {
if (opts?.skipErrorHandler) throw error
// 我们的 errorThrower 抛出的错误。
if (error.name === 'BizError') {
const errorInfo: ResponseStructure | undefined = error.info
if (errorInfo) {
const { errorMessage, errorCode } = errorInfo
switch (errorInfo.showType) {
case ErrorShowType.SILENT:
// do nothing
break
case ErrorShowType.WARN_MESSAGE:
// TODO: message
console.warn(errorMessage)
break
case ErrorShowType.ERROR_MESSAGE:
// TODO: message
console.error(errorMessage)
break
case ErrorShowType.NOTIFICATION:
// TODO: notification
console.error({ description: errorMessage, message: errorCode })
break
case ErrorShowType.REDIRECT:
// TODO: redirect
break
default:
// TODO: message
console.error(errorMessage)
}
}
} else if (error.response) {
// Axios 的错误
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
// TODO: message
console.error(`Response status:${error.response.status}`)
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// error.request 在浏览器中是 XMLHttpRequest 的实例
// 而在node.js中是 http.ClientRequest 的实例
// TODO: message
console.error('None response! Please retry.')
} else {
// 发送请求时出了点问题
// TODO: message
console.error('Request error, please retry')
}
},
},
// 请求拦截器
requestInterceptors: [
[
(config: AxiosRequestConfig) => {
// 拦截请求配置,进行个性化处理。
return { ...config }
},
(error) => {
return Promise.reject(error)
},
],
],
// 响应拦截器
responseInterceptors: [
(response: AxiosResponse) => {
// 拦截响应数据,进行个性化处理
const { data } = response
if (!data.success) {
// TODO: message
console.error('请求失败!')
}
return response
},
],
}
Mock
根目录新建mock/index.ts
,示例如下,根据自己的情况添加添加接口
export default {
'POST /api/login': {
code: '200',
message: 'ok',
data: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MjMyODU2LCJzZXNzaW9uIjoiOTRlZTZjOThmMmY4NzgzMWUzNzRmZTBiMzJkYTIwMGMifQ.z5Llnhe4muNsanXQSV-p1DJ-89SADVE-zIkHpM0uoQs',
success: true,
},
}
使用
import { request } from 'umi'
request('/api/login', { method: 'POST' })
UI框架
使用Ant-Design
pnpm add antd @ant-design/icons
编辑.umirc.ts
或config/config.ts
export default defineConfig({
plugins: [
// ...
'@umijs/plugins/dist/antd',
],
antd: {},
})
新建src/global.tsx
,引入样式
import 'antd/dist/reset.css'
布局
先编辑.umirc.ts
或config/config.ts
,启用内置布局插件,并为每个路由新增name
字段,用于给ProLayout
做菜单渲染使用
export default defineConfig({
routes: [
{ path: '/', component: 'index', name: 'Home' },
{ path: '/docs', component: 'docs', name: 'Docs' },
],
plugins: [
// ...
'@umijs/plugins/dist/layout',
],
layout: {
title: 'UmiJS Starter',
},
})
接着编辑src/app.tsx
,添加如下内容
import type { RunTimeLayoutConfig } from 'umi'
// 更多参数见: https://procomponents.ant.design/components/layout#prolayout
export const layout: RunTimeLayoutConfig = () => {
return {
layout: 'mix',
}
}
使用React-Vant
pnpm add react react-dom react-vant @react-vant/icons
移动端适配
pnpm add -D postcss-px-to-viewport-8-plugin
编辑.umirc.ts
或config/config.ts
,增加如下extraPostCSSPlugins
配置项
import path from 'path'
import postcsspxtoviewport8plugin from 'postcss-px-to-viewport-8-plugin'
export default defineConfig({
// ...
extraPostCSSPlugins: [
postcsspxtoviewport8plugin({
viewportWidth: (file: string) => {
return path.resolve(file).includes(path.join('node_modules', 'react-vant')) ? 375 : 750
},
unitPrecision: 6,
landscapeWidth: 1024,
}),
],
})
在Umi中使用Vue
在 Umi 中使用 Vue 的初始化方式和 React 类似,接下来我只会讲不一样的地方
由于 StyleLint 对 Vue 的支持不太友好,所以编码规范插件装 ESLint 和 Prettier 就行
JSX支持
编辑tsconfig.json
{
// ...
"compilerOptions": {
"jsxImportSource": "vue"
}
}
配置TailwindCSS
在 Umi 中使用 Vue 默认是同时支持模板语法和 JSX 语法的,所以修改一下TailwindCSS
的配置
module.exports = {
content: [
'./src/pages/**/*.{jsx,tsx,vue}',
'./src/components/**/*.{jsx,tsx,vue}',
'./src/layouts/**/*.{jsx,tsx,vue}',
],
}
状态管理
由于 Umi 的useModel
只支持 React,所以需要使用Pinia
代替
pnpm add pinia pinia-plugin-persistedstate
接着新建src/app.tsx
,写入如下内容,之后就可以像正常的 Vue 项目一样使用pinia
了
import { createPinia } from 'pinia'
export function onAppCreated({ app }: any) {
app.use(createPinia().use(piniaPluginPersistedstate))
}
持久化
新建src/utils/storage.ts
和src/stores/user.ts
enum StorageSceneKey {
USER = 'storage-user',
}
function getItem<T = any>(key: string): T {
const value: any = localStorage.getItem(key)
return value ? JSON.parse(value) ?? null : null
}
function setItem(key: string, value: any) {
localStorage.setItem(key, JSON.stringify(value))
}
function removeItem(key: string) {
localStorage.removeItem(key)
}
export { StorageSceneKey, getItem, removeItem, setItem }
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { StorageSceneKey } from '../utils'
export const useUserStore = defineStore(
'user',
() => {
const token = ref('')
const isLogged = ref(false)
const setToken = (value: string) => {
token.value = value
isLogged.value = true
}
const removeToken = () => {
token.value = ''
isLogged.value = false
}
return { token, isLogged, setToken, removeToken }
},
{
persist: {
//! 注意这里的key是当前这个Pinia模块进行缓存时的唯一key, 每个需要缓存的Pinia模块都必须分配一个唯一key
key: StorageSceneKey.USER,
},
}
)
Ant-Design-Vue
pnpm add ant-design-vue @ant-design/icons-vue
新建src/global.ts
,引入样式
import 'ant-design-vue/dist/reset.css'