Skip to content

搭建一个开箱即用的基于 UmiJS + AntDesign + TailwindCSS + TypeScript 的工程

UI框架以Ant-Design为例

本工程的Github地址

编写此笔记时所使用的UmiJS版本为4.0.87

相关文档

初始化项目

sh
mkdir umi-starter && cd umi-starter
pnpm dlx create-umi@latest

初始化完毕后再安装一个cross-env用来兼容在不同的操作系统中使用环境变量

sh
pnpm add -D cross-env

🎉

这样就创建好一个以UmiJS为脚手架的基础工程了,接下来我们对它做亿点点额外的配置

配置EditorConfig

新建.editorconfig,设置编辑器和 IDE 规范,内容根据自己的喜好或者团队规范

ini
# 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的方案

详细文档看这里

sh
pnpm add -D @umijs/lint eslint stylelint
touch .eslintrc.js
touch .eslintignore
touch .stylelintrc.js
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',
  },
}
ini
.DS_Store
node_modules
dist
.idea
.vscode
.umi
.umi-production
js
module.exports = {
  extends: require.resolve('umi/stylelint'),
  rules: {
    'at-rule-no-unknown': [
      true,
      {
        ignoreAtRules: ['tailwind', 'apply'],
      },
    ],
  },
}
json
{
  "scripts": {
    // ...
    "lint": "umi lint"
  }
}

社区方案

如果你想用其他的社区方案的话,这里推荐使用Nuxt团队的Anthony Fu大佬的eslint-config

sh
pnpm dlx @antfu/eslint-config@latest

编辑eslint.config.mjs

js
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,添加如下内容

json
{
  // ...
  "scripts": {
    // ...
    "lint": "eslint .", 
    "lint:fix": "eslint . --fix"
  }
}

配置Prettier

官方脚手架有快速生成Prettier配置的指令,详细的文档看这里

⚡提示

prettier-plugin-organize-imports这个插件的作用是自动移除没有被使用的import,如果不想要这个功能就在plugins字段中移除

sh
pnpm umi g prettier
json
{
  "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"]
}
txt
node_modules
.umi
.umi-production
.DS_Store
dist
.idea
.vscode

⚡注意

如果你的ESLint配置使用的是上述社区方案,并且又想同时使用prettier的话,需要编辑.vscode/settings.json,把prettier启用。因为 Anthony Fu 大佬的这套eslint-config默认禁用prettier

json
{
  "prettier.enable": true
  // ...
}

安装TailwindCSS

官方脚手架有快速生成TailwindCSS配置的指令,详细的文档看这里

sh
pnpm umi g tailwindcss
js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './src/pages/**/*.{jsx,tsx}',
    './src/components/**/*.{jsx,tsx}',
    './src/layouts/**/*.{jsx,tsx}',
  ],
  corePlugins: {
    preflight: true,
  },
  plugins: [],
}
css
@tailwind base;
@tailwind components;
@tailwind utilities;

助手函数

新建src/utils/utils.ts,封装一些辅助函数,具体代码参考我的助手函数封装

插件

由于普通的 Umi 应用中,默认不附带任何插件,所以我们需要先安装它

sh
pnpm add -D @umijs/plugins

关于 Umi 插件的详细文档看这里,Umi 的官方插件列表看这里

数据流插件

为了拥有良好的开发体验,以hooks范式使用和管理全局状态,我们需要启用@umijs/plugin-model插件

编辑.umirc.ts或者config/config.ts文件

ts
export default defineConfig({
  // ...
  plugins: [
    // ...
    '@umijs/plugins/dist/model', 
  ],
  model: {}, 
})

示例

数据流插件要求在src目录下创建一个models目录,该目录下存放需要全局共享的数据

sh
mkdir src/models
touch src/models/count.ts
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.tsxsrc/pages/docs.tsx

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>
  )
}
tsx
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.tsconfig/config.ts

ts
export default defineConfig({
  plugins: [
    // ...
    '@umijs/plugins/dist/request', 
  ],
  request: {}, 
})

新建src/app.tsx,编写如下请求配置

查看
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,示例如下,根据自己的情况添加添加接口

ts
export default {
  'POST /api/login': {
    code: '200',
    message: 'ok',
    data: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MjMyODU2LCJzZXNzaW9uIjoiOTRlZTZjOThmMmY4NzgzMWUzNzRmZTBiMzJkYTIwMGMifQ.z5Llnhe4muNsanXQSV-p1DJ-89SADVE-zIkHpM0uoQs',
    success: true,
  },
}

使用

tsx
import { request } from 'umi'
request('/api/login', { method: 'POST' })

UI框架

使用Ant-Design

sh
pnpm add antd @ant-design/icons

编辑.umirc.tsconfig/config.ts

ts
export default defineConfig({
  plugins: [
    // ...
    '@umijs/plugins/dist/antd', 
  ],
  antd: {}, 
})

新建src/global.tsx,引入样式

ts
import 'antd/dist/reset.css'

布局

先编辑.umirc.tsconfig/config.ts,启用内置布局插件,并为每个路由新增name字段,用于给ProLayout做菜单渲染使用

ts
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,添加如下内容

tsx
import type { RunTimeLayoutConfig } from 'umi'
// 更多参数见: https://procomponents.ant.design/components/layout#prolayout
export const layout: RunTimeLayoutConfig = () => {
  return {
    layout: 'mix',
  }
}

使用React-Vant

sh
pnpm add react react-dom react-vant @react-vant/icons

移动端适配

sh
pnpm add -D postcss-px-to-viewport-8-plugin

编辑.umirc.tsconfig/config.ts,增加如下extraPostCSSPlugins配置项

ts
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

json
{
  // ...
  "compilerOptions": {
    "jsxImportSource": "vue"
  }
}

配置TailwindCSS

在 Umi 中使用 Vue 默认是同时支持模板语法和 JSX 语法的,所以修改一下TailwindCSS的配置

js
module.exports = {
  content: [
    './src/pages/**/*.{jsx,tsx,vue}', 
    './src/components/**/*.{jsx,tsx,vue}', 
    './src/layouts/**/*.{jsx,tsx,vue}', 
  ],
}

状态管理

由于 Umi 的useModel只支持 React,所以需要使用Pinia代替

sh
pnpm add pinia pinia-plugin-persistedstate

接着新建src/app.tsx,写入如下内容,之后就可以像正常的 Vue 项目一样使用pinia

tsx
import { createPinia } from 'pinia'

export function onAppCreated({ app }: any) {
  app.use(createPinia().use(piniaPluginPersistedstate))
}

持久化

新建src/utils/storage.tssrc/stores/user.ts

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 }
ts
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

sh
pnpm add ant-design-vue @ant-design/icons-vue

新建src/global.ts,引入样式

ts
import 'ant-design-vue/dist/reset.css'

MIT License