diff --git a/docs/.dumi/global.less b/docs/.dumi/global.less new file mode 100644 index 000000000000..a94c13e02d50 --- /dev/null +++ b/docs/.dumi/global.less @@ -0,0 +1,9 @@ +// hide mf related docs +.dumi-default-sidebar-group { + a[href="/docs/guides/mfsu"], + a[href="/en-US/docs/guides/mfsu"], + a[href="/docs/max/mf"], + a[href="/en-US/docs/max/mf"] { + display: none; + } +} diff --git a/docs/.dumi/pages/index.en-US.tsx b/docs/.dumi/pages/index.en-US.tsx new file mode 100644 index 000000000000..701433ce0736 --- /dev/null +++ b/docs/.dumi/pages/index.en-US.tsx @@ -0,0 +1 @@ +export { default } from '.'; diff --git a/docs/.dumirc.ts b/docs/.dumirc.ts index 9a629d704079..6cc7ba938ffd 100644 --- a/docs/.dumirc.ts +++ b/docs/.dumirc.ts @@ -32,6 +32,10 @@ export default defineConfig({ define: { 'process.env.UMI_VERSION': version, }, + locales: [ + { id: 'zh-CN', name: '中文' }, + { id: 'en-US', name: 'EN' }, + ], themeConfig: { name: 'UmiJS', socialLinks: { @@ -39,8 +43,7 @@ export default defineConfig({ }, footer: 'Open-source MIT Licensed | Copyright © 2017-present', nav: { - mode: 'override', - value: [ + 'zh-CN': [ { title: '介绍', link: '/docs/introduce/introduce', @@ -67,155 +70,31 @@ export default defineConfig({ activePath: '/blog', }, ], - }, - sidebar: { - '/docs/guides': [ + 'en-US': [ { - children: [ - { - title: '快速上手', - link: '/docs/guides/getting-started', - }, - { - title: '开发环境', - link: '/docs/guides/prepare', - }, - { - title: '目录结构', - link: '/docs/guides/directory-structure', - }, - { - title: '路由', - link: '/docs/guides/routes', - }, - { - title: '插件', - link: '/docs/guides/use-plugins', - }, - { - title: 'Mock', - link: '/docs/guides/mock', - }, - { - title: '代理', - link: '/docs/guides/proxy', - }, - { - title: '样式', - link: '/docs/guides/styling', - }, - { - title: '路由数据加载', - link: '/docs/guides/client-loader', - }, - { - title: 'TypeScript', - link: '/docs/guides/typescript', - }, - { - title: '环境变量', - link: '/docs/guides/env-variables', - }, - { - title: '脚手架', - link: '/docs/guides/boilerplate', - }, - { - title: '微生成器', - link: '/docs/guides/generator', - }, - { - title: '编码规范', - link: '/docs/guides/lint', - }, - { - title: '调试', - link: '/docs/guides/debug', - }, - { - title: '测试', - link: '/docs/guides/test', - }, - { - title: '开发插件', - link: '/docs/guides/plugins', - }, - { - title: '使用 Vue', - link: '/docs/guides/use-vue', - }, - { - title: 'MPA 模式', - link: '/docs/guides/mpa', - }, - // 暂不开放 - // { - // title: 'MFSU', - // link: 'docs/guides/mfsu', - // }, - ], + title: '介绍', + link: '/en-US/docs/introduce/introduce', + activePath: '/en-US/docs/introduce', + }, + { + title: '指南', + link: '/en-US/docs/guides/getting-started', + activePath: '/en-US/docs/guides', }, - ], - '/docs/max': [ { - children: [ - { - title: 'Umi Max 简介', - link: '/docs/max/introduce', - }, - { - title: '布局与菜单', - link: '/docs/max/layout-menu', - }, - { - title: 'antd', - link: '/docs/max/antd', - }, - { - title: '图表', - link: '/docs/max/charts', - }, - { - title: '数据流', - link: '/docs/max/data-flow', - }, - { - title: '请求', - link: '/docs/max/request', - }, - { - title: '权限', - link: '/docs/max/access', - }, - { - title: '国际化', - link: '/docs/max/i18n', - }, - { - title: '微前端', - link: '/docs/max/micro-frontend', - }, - { - title: 'styled-components', - link: '/docs/max/styled-components', - }, - { - title: 'react-query', - link: '/docs/max/react-query', - }, - { - title: 'valtio', - link: '/docs/max/valtio', - }, - { - title: 'dva', - link: '/docs/max/dva', - }, - { - title: '站点统计', - link: '/docs/max/analytics', - }, - ], + title: 'API', + link: '/en-US/docs/api/api', + activePath: '/en-US/docs/api', + }, + { + title: 'Umi Max', + link: '/en-US/docs/max/introduce', + activePath: '/en-US/docs/max', + }, + { + title: '博客', + link: '/en-US/blog/umi-4-rc', + activePath: '/en-US/blog', }, ], }, diff --git a/docs/docs/blog/code-splitting.en-US.md b/docs/docs/blog/code-splitting.en-US.md new file mode 100644 index 000000000000..5cac55c7c7e3 --- /dev/null +++ b/docs/docs/blog/code-splitting.en-US.md @@ -0,0 +1,50 @@ +--- +toc: content +order: 5 +group: + title: Blog +--- + +# 代码拆分指南 + +Umi 4 默认 按页拆包、按需加载(这近似等同于 Umi 3 中的 `dynamicImport`),通过 [`loading.tsx`](../docs/guides/directory-structure#loadingtsxjsx) 来自定义加载动画。 + +### 使用分包策略 + +Umi 4 内置了不同的代码拆分策略 ( [codeSplitting](../docs/api/config#codesplitting) ) ,通过配置开启: + +```ts +// .umirc.ts +export default { + codeSplitting: { + jsStrategy: 'granularChunks', + }, +}; +``` + +这会按照一定的优化策略进行自动分包,若需手动进行更细致的分包,请参见下文。 + +### 手动拆分 + +当你的产物体积变大时,可进一步手动拆包: + +```ts +import { lazy, Suspense } from 'react' + +// './Page' 该组件将被自动拆出去 +const Page = lazy(() => import('./Page')) + +export default function() { + return ( + loading... + + + ) +} +``` + +通常情况下,我们会手动拆分引用了较大第三方库的组件,实现按需加载。 + +### 分析产物构成 + +通过指定 [ANALYZE](../docs/guides/env-variables#analyze) 环境变量可以分析产物构成,根据分析结果来修改代码和进一步决策。 diff --git a/docs/docs/blog/develop-blog-using-umi.en-US.md b/docs/docs/blog/develop-blog-using-umi.en-US.md new file mode 100644 index 000000000000..43b3d49e9c2b --- /dev/null +++ b/docs/docs/blog/develop-blog-using-umi.en-US.md @@ -0,0 +1,613 @@ +--- +order: 1 +toc: content +group: + title: Blog +--- + +# 使用 Umi 开发一个 Blog + +这篇文章将带领你使用 Umi.js 搭配 [PlanetScale](https://planetscale.com/), [Prisma](https://www.prisma.io/) 和 [Tailwindcss](https://tailwindcss.com/)等服务与技术,开发一个简单的博客网站,并部署到 [Vercel](https://vercel.com) 服务。 + +## 成果展示 + +成果看起来是这样的:你会有一个博客首页展示你的文章 [https://umi-blog-example.vercel.app/](https://umi-blog-example.vercel.app/) + +![博客首页](https://img.alicdn.com/imgextra/i2/O1CN01a9YcgY24tEdndfXsw_!!6000000007448-2-tps-3104-1974.png) + +点击某一篇文章可以看到这篇文章的完整内容:[https://umi-blog-example.vercel.app/posts/5](https://umi-blog-example.vercel.app/posts/5) + +![博客文章页](https://img.alicdn.com/imgextra/i4/O1CN01k84YL21wHCpYx02Yc_!!6000000006282-2-tps-3104-1974.png) + +当然,你还可以在博客中发表新文章:[https://umi-blog-example.vercel.app/posts/create](https://umi-blog-example.vercel.app/posts/create) + +![发表文章页](https://img.alicdn.com/imgextra/i4/O1CN01DZkDt41jvt0BJMZqi_!!6000000004611-2-tps-3104-1974.png) + +前提是你有登入:[https://umi-blog-example.vercel.app/login](https://umi-blog-example.vercel.app/login) + +![博客登入页](https://img.alicdn.com/imgextra/i1/O1CN015ce0oY1uKkfMCa1vq_!!6000000006019-2-tps-3104-1974.png) + +准备好了吗,让我们马上开始吧! + +## 环境准备 + +首先,你必须确保你的本地环境已经准备好进行一个 Umi.js 项目的开发。如果你目前还没有开发过 Umi.js 项目,也没有在本地搭建过开发环境,建议你先阅读 [开发环境](../docs/guides/prepare) 这篇教学。 + +配置完本地环境以后,你已经准备好开始开发 Umi.js 项目了!跟着 [脚手架](../docs/guides/boilerplate) 这篇文档的教学,快速初始化一个 Umi.js 项目吧。 + +### 调整目录结构 + +因为我们的博客网站会使用到 Umi 4 的 API 路由功能,所以我们需要对脚手架自动产生的目录结构进行一些调整。你现在的目录结构应该看起来是这样的: + +```text +. +├── assets +│ └── yay.jpg +├── layouts +│ ├── index.less +│ └── index.tsx +├── node_modules +├── package.json +├── pages +│ ├── docs.tsx +│ └── index.tsx +├── pnpm-lock.yaml +├── tsconfig.json +└── typings.d.ts +``` + +我们需要把 `assets`, `layouts`, `pages` 目录从根目录移动到 `src` 目录下,移动之后他看起来是这样的: + +```text +. +├── src +│ ├── assets +│ │ └── yay.jpg +│ ├── layouts +│ │ ├── index.less +│ │ └── index.tsx +│ └──── pages +│ ├── docs.tsx +│ └── index.tsx +├── node_modules +├── package.json +├── pnpm-lock.yaml +├── tsconfig.json +└── typings.d.ts +``` + +:::info +为什么要这样做呢?这是因为根目录下的 `api` 目录会被我们作为 API 路由生成构建产物的地方,如果我们没有多一层 `src` 目录的话,我们的 API 路由目录就会和构建产物目录冲突啦~ +::: + +### 注册 PlanetScale 服务 + +我们的博客将会把用户和文章的数据保存在 MySQL 数据库中。然而,我们不需要真的自己准备一台服务器来运行数据库,我们可以使用免费的 [PlanetScale](https://planetscale.com/)来一键部署一个开箱即用的数据库! + +首先从 [https://auth.planetscale.com/sign-in](https://auth.planetscale.com/sign-in)登入你的账号,如果你没有注册过,可以选择使用 GitHub 一键登入或是点击 [Sign up for an account](https://auth.planetscale.com/sign-up) 注册一个账号。 + +![登入 PlanetScale](https://img.alicdn.com/imgextra/i4/O1CN01BVVAju1eONxEM9wr5_!!6000000003861-2-tps-2506-1464.png) + +登入之后,在你的 PlanetScale 账号建立一个数据库(如果你是第一次注册,则可以看到他的教学步骤带领你一步步建立一个数据库): + +![建立数据库](https://img.alicdn.com/imgextra/i4/O1CN01St4IQW21lV6f8bpKg_!!6000000007025-2-tps-3104-1974.png) + +建立完成后,点击数据库页面右上角的 **Connect** 按钮: + +![Connect 按钮](https://img.alicdn.com/imgextra/i4/O1CN01Hnyqbo26g4UNnSqoQ_!!6000000007690-2-tps-3104-1974.png) + +你会在弹窗里面看到一个 **Connect With** 的下拉选单,选择 `Prisma`,然后就能获得一串这个格式的字符串: + +```dotenv +DATABASE_URL='mysql://************:************@************.ap-southeast-2.psdb.cloud/umi-blog-example?sslaccept=strict' +``` + +这个字符串就是我们要用来让 Prisma 连接数据库的连线信息,暂时先把他记录起来就可以了 👍 + +### 安装依赖 + +接下来,帮我们的 Umi 项目安装这次教程会用到的依赖: + +```shell +pnpm i -d prisma @types/bcryptjs @types/jsonwebtoken +pnpm i @prisma/client bcryptjs jsonwebtoken +``` + +这两行命令帮我们安装了这些包: + +1. [prisma](https://github.com/prisma/prisma)和 [@prisma/client](https://www.npmjs.com/package/@prisma/client):这两个包让我们可以用 [Prisma](https://www.prisma.io/)来方便地串接数据库,不需要担心任何复杂的 SQL 命令。 +2. [bcryptjs](https://github.com/dcodeIO/bcrypt.js):这个包让我们将用户注册后的密码使用 [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)哈希加密算法来安全的存储在数据库中。 +3. [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken):这个包让我们可以方便地使用用 [JWT(Json Web Token)](https://jwt.io/) 实现用户鉴权。 + +然后将 `package.json` 中 `scripts` 里面的 `build` 脚本从 + +``` +"scripts": { + "dev": "umi dev", + "build": "umi build", + "postinstall": "umi setup", + "start": "npm run dev" +}, +``` + +改成 + +``` +"scripts": { + "dev": "umi dev", + "build": "prisma generate && umi build", + "postinstall": "umi setup", + "start": "npm run dev" +}, +``` + +这可以确保每次开始构建以前都已经生成好 Prisma 客户端。 + +### 安装 Tailwindcss + +使用 Umi 提供的微生成器来在项目中启用 Tailwindcss : + +```shell +npx umi g tailwindcss +``` + +他会帮我们安装 Tailwindcss 所需要的依赖,然后生成需要的文件。 + +### 初始化页面组件 + +接下来,当你使用 `pnpm dev`启动项目后,可能会看到错误讯息并且启动失败了。这是因为我们在配置中声明了一些页面,但并没有帮他建立对应的页面组件! + +我们可以使用 Umi 的微生成器来自动生成这些页面:`login.tsx`, `posts/post.tsx`, `posts/create.tsx`: + +```shell +npx umi g page login posts/post posts/create +``` + +新增后的目录结构是这样的: + +```text +src +├── assets +│ └── yay.jpg +├── layouts +│ ├── index.less +│ └── index.tsx +└── pages + ├── index.less + ├── index.tsx + ├── login.less + ├── login.tsx + └── posts + ├── create.less + ├── create.tsx + ├── post.less + └── post.tsx +``` + +现在再输入一次 `pnpm dev` 就可以看到我们的网站正常启动了。 + +### 配置 umi 项目 + +最后一步,就是要来对 Umi 项目进行配置,完整的配置可以参考 [配置](../docs/api/config) 这篇教学文档,在本次的教学中,只要按照以下配置即可: + +```ts +// .umirc.ts + +export default { + npmClient: 'pnpm', + apiRoute: { + platform: 'vercel', + }, + routes: [ + { path: '/', component: 'index' }, + { path: '/posts/create', component: 'posts/create' }, + { path: '/login', component: 'login' }, + { path: '/posts/:postId', component: 'posts/post' }, + ], + plugins: [require.resolve('@umijs/plugins/dist/tailwindcss')], + tailwindcss: {}, +}; +``` + +其中,`apiRoute` 这个配置项告诉 Umi 我们的项目启用了 **API 路由** 这个功能,而 `platform: 'vercel'` 代表我们要部署到[Vercel](https://vercel.com) 平台,在 `umi build` 的时候会针对这个平台来将 API 路由进行打包。 + +为了顺利部署项目到 Vercel ,你需要在项目根目录下加入一个 `vercel.json` 配置文件: + +```json +{ + "build": { + "env": { + "ENABLE_FILE_SYSTEM_API": "1" + } + }, + "rewrites": [ + { + "source": "/api/:match*", + "destination": "api/:match*" + } + ] +} +``` + +`route` 这个配置项则声明了我们网站的路由架构,可以看到我们的博客网站有这些页面: + +- `/`: 首页 +- `/posts/create`: 建立文章 +- `/login`: 登入 +- `/posts/:postId`: 某篇文章 + +`plugins` 配置项代表我们启用了哪些 Umi 插件,其中 `@umijs/plugins/dist/tailwindcss` 是在 Umi 中使用 Tailwindcss +的插件。下面一项 `tailwindcss: {}` 则是从配置来启用该插件的意思。 + +## API 路由设计 + +我们的整个博客网站由两大部分构成,一半是运行在浏览器内的前端代码,另一半则是运行在 Serverless Function 中的服务端代码。 + +为什么需要分成两边呢?这是因为有些代码我们不能让他在浏览器内运行,比如说用户鉴权、串接数据库等等的功能,这些必须作为一个服务然后以 API 的形式暴露给前端页面调用,这个部分可以透过 Umi 4 的 API 路由功能来实现。 + +> (这里好像放一张图可以比较清楚地解释) + +因为我们已经在 `.umirc.ts` 配置文件中声明了我们要启用 API 路由功能,现在可以直接在 `src` 目录下添加一个 `api`目录,这个目录下以约定式路由的设计来提供 API 路由的开发。 + +作为一个博客的 API 服务,不难想到我们会需要这些接口来供用户调用: + +1. 用户注册: `POST /api/register` +2. 用户登入: `POST /api/login` +3. 发表文章: `POST /api/posts` +4. 查询所有文章: `GET /api/posts` +5. 查询一篇文章: `GET /api/posts/{postId}` + +所以我们可以在 `src/api` 目录下建立这些新文件: + +```text +src +├── api +│ ├── login.ts +│ ├── register.ts +│ └── posts +│ ├── [postId].ts +│ └── index.ts +... +``` + +:::info +你可能注意到了,这里有一个文件叫做 `[postId].ts`,这个写法代表这个路由可以动态匹配不同的值。例如 `/api/posts/1` 和 `/api/posts/2` 两个请求都会交给 `src/api/posts/[postId].ts`处理,但他们的 `req.params` 分别是 `{ postId: 1 }` 和 `{ postId: 2 }`。 +::: + +这里的每个 `.ts` 文件就是一个 **API Handler**,他们默认导出一个函数用来处理发送到该路径的请求,我们可以暂时先这样写: + +```ts +import type { UmiApiRequest, UmiApiResponse } from 'umi'; + +export default async function (req: UmiApiRequest, res: UmiApiResponse) { + res.status(400).json({ error: 'This API is not implemented yet.' }); +} +``` + +然后你可以试着用浏览器或 [Postman](https://www.postman.com) 访问看看这些 API 路由(例如 `http://localhost:8000/api/login` ),就可以看到你刚刚写的响应数据了 🎉 + +![Not implemented yet](https://img.alicdn.com/imgextra/i3/O1CN01IRTlsd1HXmvCRJUt1_!!6000000000768-2-tps-1302-666.png) + +我们等一下再回来实作这些 API 路由的实际功能,因为有一个更重要的事情要先做。 + +## 定义 Schema + +现在必须要先确定一件事情:我们要保存哪些数据、以怎么样的形式保存在数据库,又是以怎么样的形式响应给前端的? + +### 文章数据 + +文章数据(Post)每笔数据就代表了一篇博客里面的文章,我们可以按自己的系统需求来设计他需要保存的内容,例如我们的范例保存了这些数据: + +- `id`: 文章 ID +- `title`: 文章标题 +- `authorId`: 作者的 ID +- `tags`: 文章的标签(以逗号隔开) +- `imageUrl`: 文章封面图片的链接 +- `content`: 文章的内文(markdown 格式) + +### 用户数据 + +用户数据 (User) 每笔数据代表一个在我们博客注册的用户数据,我们可以按照自己的系统需求来设计他需要保存的内容,例如我们的范例保存了这些数据: + +- `id`: 用户 ID +- `name`: 名称 +- `email`: 邮箱 +- `avatarUrl`: 头像链接 +- `passwordHash`: 加密过的密码 + +### 生成配置 + +> 这个章节可以考虑阅读 [Prisma 官方的教学文档](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-typescript-mysql) + +定义好数据格式以后,我们要让 Prisma 帮我们根据 Schema 设计来生成对应的客户端,并且自动的将数据库迁移至为我们设计的格式, + +#### 连线到数据库 + +第一步,我们在根目录建立一个 `.env` 文件,并且在里面加入刚刚在 [注册 PlanetScale 服务](#注册-planetscale-服务) 章节拿到的连线信息。 + +```dotenv +# .env + +DATABASE_URL='mysql://************:************@************.ap-southeast-2.psdb.cloud/umi-blog-example?sslaccept=strict' +``` + +#### 编写 Prisma 配置 + +第二步,在根目录下建立一个 `prisma/schema.prisma` 文件,并把我们设计的 Schema 按照 [Prisma 语法](https://pris.ly/d/prisma-schema) 写进去文件中: + +```prisma +generator client { + provider = "prisma-client-js" + previewFeatures = ["referentialIntegrity"] +} + +datasource db { + provider = "mysql" + referentialIntegrity = "prisma" + url = env("DATABASE_URL") +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String @db.VarChar(255) + content String? + author User @relation(fields: [authorId], references: [id]) + authorId Int + imageUrl String? + tags String + + @@index(authorId) +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + passwordHash String + name String? + posts Post[] + avatarUrl String? +} +``` + +完成后,在命令行输入 + +```shell +npx prisma migrate dev --name init +``` + +他会帮我们将 MySQL 数据库迁移为我们定义的格式。 接下来,在命令行输入 + +```shell +npx prisma generate +``` + +他会帮我们生成一个按照我们的 Schema 设计量身定制的客户端包。 + +--- + +至此,我们已经顺利处理完数据库的部分,接下来只要专注于如何在 API 路由中使用 Prisma 客户端包来获取与更新数据即可。 + +## 实现 API 路由 + +我们现在要回头来实现刚刚建立的那些 `api` 目录下的 `.ts` 文件了,只要我们自己清楚: + +1. API 会如何被调用 (path, request header, request body) +2. 我们应该在 API 内做什么事 +3. 响应什么内容回去 (status, response header, response body) + +那么 API 路由的开发就像写编写一个简单的函数一样。 + +### 用户注册 + +当用户对 `/api/register` 发起 `POST` 请求时,代表他们想要在我们的博客网站注册一个账号。 + +:::info +你可以在 [https://github.com/umijs/umi-blog-example/blob/main/src/api/register.ts](https://github.com/umijs/umi-blog-example/blob/main/src/api/register.ts) 找到这个示范的源代码! +::: + +```ts +// src/api/register.ts + +import type { UmiApiRequest, UmiApiResponse } from 'umi'; +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcryptjs'; +import { signToken } from '@/utils/jwt'; + +export default async function (req: UmiApiRequest, res: UmiApiResponse) { + switch (req.method) { + // 如果对这个路径发起 POST 请求,代表他想要注册一个账号 + case 'POST': + try { + // 建立一个 Prisma 客户端,他可以帮助我们连线到数据库 + const prisma = new PrismaClient(); + + // 在数据库的 User 表中建立一个新的数据 + const user = await prisma.user.create({ + data: { + email: req.body.email, + + // 密码是经过 bcrypt 加密的 + passwordHash: bcrypt.hashSync(req.body.password, 8), + name: req.body.name, + avatarUrl: req.body.avatarUrl, + }, + }); + + // 把建立成功的用户数据(不包含密码)和 JWT 回传给前端 + res + .status(201) + .setCookie('token', await signToken(user.id)) + .json({ ...user, passwordHash: undefined }); + + // 处理完请求以后记得断开数据库链接 + await prisma.$disconnect(); + } catch (e: any) { + // 如果发生未预期的错误,将对应的错误说明的 Prisma 文档发给用户 + res.status(500).json({ + result: false, + message: + typeof e.code === 'string' + ? 'https://www.prisma.io/docs/reference/api-reference/error-reference#' + + e.code.toLowerCase() + : e, + }); + } + break; + default: + // 如果不是 POST 请求,代表他正在用错误的方式访问这个 API + res.status(405).json({ error: 'Method not allowed' }); + } +} +``` + +完成开发后,可以使用 Postman 对这个 API 发起请求,测试功能是否正常运作。 + +### 用户登入 + +当用户对 `/api/login` 发起 `POST` 请求时,代表他们想要登入我们的博客网站并取得一个 JWT 令牌,这可以让他用于建立新文章。 + +:::info +这个部分留给读者练习,你可以在 [https://github.com/umijs/umi-blog-example/blob/main/src/api/login.ts](https://github.com/umijs/umi-blog-example/blob/main/src/api/login.ts) 找到这个示范的源代码! +::: + +### 发表文章 + +当用户对 `/api/posts` 发起 `POST` 请求时,代表他们想要在我们的博客网站发表一篇文章。 + +:::info +这个部分留给读者练习,你可以在 [https://github.com/umijs/umi-blog-example/blob/main/src/api/posts/index.ts](https://github.com/umijs/umi-blog-example/blob/main/src/api/posts/index.ts) 找到这个示范的源代码! +::: + +### 查询所有文章 + +当用户对 `/api/posts` 发起 `GET` 请求时,代表他们想要查询所有文章的数据。 + +:::info +这个部分留给读者练习,你可以在 [https://github.com/umijs/umi-blog-example/blob/main/src/api/posts/index.ts](https://github.com/umijs/umi-blog-example/blob/main/src/api/posts/index.ts) 找到这个示范的源代码! +::: + +### 查询某篇文章 + +当用户对 `/api/posts/{postId}` 发起 `GET` 请求时,代表他们想要查询某篇文章的数据。 + +:::info +这个部分留给读者练习,你可以在 [https://github.com/umijs/umi-blog-example/blob/main/src/api/posts/%5BpostId%5D.ts](https://github.com/umijs/umi-blog-example/blob/main/src/api/posts/%5BpostId%5D.ts) 找到这个示范的源代码! +::: + +## 实作页面组件 + +在这个章节,我们主要要学习如何在页面组件调用 API,来实现获取文章或注册等前后端交互的行为: + +```jsx +// pages/index.tsx + +import React, { useEffect, useState } from 'react'; +import { history } from "umi"; + +export default function HomePage() { + const [posts, setPosts] = useState(); + return ( +
+ {!posts &&

Loading...

} + {posts &&
+ {posts.map(post =>
+
history.push(`/posts/${post.id}`)}> +

{post.title}

+
+
)} +
} +
+ ); +} +``` + +可以看到我们在首页组件维护了一个 `posts` 状态,当 `posts` 是 `undefined` 时,我们认为是数据尚未加载完成。所以我们可以加入一个 `useEffect`让他在组件加载后对 API 路由发起一个请求,去查询目前所有的文章列表: + +```tsx +// pages/index.tsx + +import React, { useEffect, useState } from 'react'; +import { history } from 'umi'; + +export default function HomePage() { + const [posts, setPosts] = useState(); + + async function refresh() { + try { + const res = await fetch('/api/posts'); + if (res.status !== 200) { + console.error(await res.text()); + } + setPosts(await res.json()); + } catch (err) { + console.error(err); + } + } + + useEffect(() => { + refresh(); + }, []); + + return ( +
+ {!posts &&

Loading...

} + {posts && ( +
+ {posts.map((post) => ( +
+
history.push(`/posts/${post.id}`)}> +

{post.title}

+
+
+ ))} +
+ )} +
+ ); +} +``` + +可以看到我们加入了一个 `refresh` 函数,他会帮我们从 API 路由查询目前的文章列表。若你现在访问这个页面,应该可以看到一开始是 `Loading ...` +等一阵子就会有全部文章的标题被渲染出来的效果。 + +![titles](https://img.alicdn.com/imgextra/i1/O1CN01n3CA371n0PlgkdEvQ_!!6000000005027-2-tps-3104-1974.png) + +最后只要帮他加一点样式: + +![titles-with-style](https://img.alicdn.com/imgextra/i1/O1CN01sfpTVd1IGDLK068gY_!!6000000000865-2-tps-3104-1974.png) + +--- + +其他页面留给读者实作,可以加入自己的想法及样式的设计来实现,源代码可参考:[https://github.com/umijs/umi-blog-example/blob/main/src/pages](https://github.com/umijs/umi-blog-example/blob/main/src/pages) + +## 部署 + +最后,将你的项目提交到 git 服务上,然后登入 [Vercel](https://vercel.com): + +![Vercel](https://img.alicdn.com/imgextra/i2/O1CN01X7LqFx1LbEMYzLT3k_!!6000000001317-2-tps-2720-1710.png) + +如果你的项目代码是提交到 GitHub,那么建议你选择 GitHub 登入,这样你就可以在 Vercel 直接导入现有的代码仓库了 👍 + +![Vercel New Project](https://img.alicdn.com/imgextra/i1/O1CN014qIgle23G171pvX0V_!!6000000007227-2-tps-2720-1818.png) + +导入以后,他会自动检测到这个项目是使用 Umi.js 框架搭建的,并且自动化完成相关的配置,因此直接点击 **Deploy** 即可开始部署! + +![Deploy](https://img.alicdn.com/imgextra/i3/O1CN013Ts04x1tqyv4VhzbW_!!6000000005954-2-tps-1468-1064.png)等他部署完成以后,你的博客就正式上线啦 👍 + +--- + +但这个时候你会发现,网站内的 API 路由没有办法正常工作,这是因为他缺少了连线到数据库需要的环境变量。我们需要帮他配置一下: + +![Settings](https://img.alicdn.com/imgextra/i1/O1CN01OYPPB61G61IKR8chz_!!6000000000572-2-tps-2632-1934.png) + +在项目配置页面,下面有可以设置环境变量的地方: + +![Env](https://img.alicdn.com/imgextra/i4/O1CN01nOLflV1I4lRMwsYHk_!!6000000000840-2-tps-2632-1934.png) + +你可以把 `DATABASE_URL`, `JWT_KEY` 等等你用到的环境变量从这里传入。 + +![Redeploy](https://img.alicdn.com/imgextra/i3/O1CN013dlBGs2506aFTigki_!!6000000007463-2-tps-2644-1890.png) + +加入环境变量以后,点击 **Redeploy** 重新部署一次版本,你的博客就能正常运作啦~ diff --git a/docs/docs/blog/legacy-browser.en-US.md b/docs/docs/blog/legacy-browser.en-US.md new file mode 100644 index 000000000000..d3b3d7e09c74 --- /dev/null +++ b/docs/docs/blog/legacy-browser.en-US.md @@ -0,0 +1,127 @@ +--- +toc: content +order: 6 +group: + title: Blog +--- + +# 非现代浏览器兼容 + +## 默认兼容说明 + +Umi 4 默认不支持 IE ,编译兼容目标 `targets` 为 `chrome: 80` ,如需调整,请指定明确的 [targets](../docs/api/config#targets) : + +```ts +// .umirc.ts + +export default { + targets: { chrome: 67 }, +}; +``` + +若想反馈更多关于兼容性的问题,或参与讨论,请前往:[issue / 8656](https://github.com/umijs/umi/issues/8658) + +## 兼容非现代浏览器 + +如果你并不需要兼容至 IE ,只为了提升项目对非现代浏览器的兼容性,可调整兼容目标 [targets](../docs/api/config#targets) 。 + +Umi 4 默认使用现代构建工具,产物生成至 `es6` ,如果你有要打包为 `es5` 产物的考量,请调整配置: + +```ts +// .umirc.ts + +export default { + jsMinifier: 'terser', + cssMinifier: 'cssnano', +}; +``` + +## 兼容旧时代浏览器 ( IE 11 ) + +由于 IE 已经淘汰不再主流,当需要兼容至 IE 时,请阅读以下对策。 + +### 框架自带的 legacy mode + +Umi 4 自带提供一个 `legacy` 配置用于构建降级(使用限制等详见 [legacy](../docs/api/config#legacy) ): + +```ts +// .umirc.ts + +export default { + legacy: {}, +}; +``` + +默认仅在构建时生效,将尝试构建能使 IE 兼容的产物。 + +### legacy mode 的更多自定义 + +`legacy` 开启时,默认会转译全部 `node_modules` ,这在大型项目中,会极大的增加构建时间。 + +若你了解当前项目使用的第三方依赖情况(知道哪些不再提供 `es5` 产物了),可以关闭 `node_modules` 的转换,改为使用 [`extraBabelIncludes`](https://umijs.org/docs/api/config#extrababelincludes) 定点配置那些需要额外纳入转换范围的包。 + +一个例子: + +```ts +// .umirc.ts + +export default { + legacy: { + nodeModulesTransform: false, + }, + extraBabelIncludes: ['some-es6-pkg', /@scope\//], +}; +``` + +### 提高兼容的鲁棒性 + +`legacy` 选项并不能 100% 保证产物 **没有边界情况** 的运行在被淘汰的浏览器内,你可能还需要添加 **前置的** 全量 polyfill 来增强项目的 [鲁棒性](https://baike.baidu.com/item/%E9%B2%81%E6%A3%92%E6%80%A7/832302) 。 + +```ts +// .umirc.ts + +export default { + headScripts: [ + 'http://polyfill.alicdn.com/v3/polyfill.min.js', // or https://polyfill.io/v3/polyfill.min.js + ], + legacy: {}, +}; +``` + +参考的思路有: + +| 方案 | 说明 | +| :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CDN 引入 | 以 cdn 形式引入 **script 形式且前置的** 、目标浏览器环境缺少的 polyfill js 文件,如 [es6-shim](https://github.com/paulmillr/es6-shim) 。 | +| 人工 core-js | 利用 [core-js](https://github.com/zloirock/core-js) 系工具,如通过 [core-js-builder](https://github.com/zloirock/core-js/tree/master/packages/core-js-builder) 构建自己需要的 polyfill 产物,再以 **前置 script 脚本** 形式引入项目。 | +| 动态 polyfill 服务 | 使用根据当前浏览器请求 UA 动态下发所需 polyfill 的服务,比如 [polyfill.io](https://polyfill.io/v3/polyfill.min.js) ,考虑到速度,可使用国内的 [alicdn polyfill.io](http://polyfill.alicdn.com/v3/polyfill.min.js) 服务。另外,你还可以使用 [polyfill-service](https://github.com/Financial-Times/polyfill-service) 自建相同的动态 polyfill 下发服务。 | + +注: + +1. 当你处于内外网隔离开发环境时,可以考虑将全部 polyfill 的 js 内容传入内网,在内网的 CDN 使用,或放入 public 目录等方式使用。 + +2. 使用 script 前置引入的意义在于,在项目 js 资源运行前就准备好一个完整的、被 polyfill 过 api 的环境。 + +### 在开发环境验证 + +推荐的做法是:构建后在本地通过 [`umi preview`](../docs/api/commands#preview) 或 [`serve`](https://www.npmjs.com/package/serve) 、nginx 等启动服务,来验证产物的 IE 11 运行可行性。 + +当你需要在开发环境验证时: + +1. 将 `legacy.buildOnly` 置为 `false` 。 + +2. 由于 react fresh 、hmr 等开发注入的 es6 代码始终在第一位运行,你需要以 script 形式添加一个前置的 polyfill ,提前准备好环境。 + +```ts +// .umirc.ts + +const isProd = process.env.NODE_ENV === 'production'; +export default { + legacy: { + buildOnly: false, + }, + headScripts: isProd ? [] : ['http://polyfill.alicdn.com/v3/polyfill.min.js'], +}; +``` + +注:IE 11 并不能完整支持开发时的热更新,且缓存可能需要人为在控制台进行清除后才能看到最新的页面,请做好准备。 diff --git a/docs/docs/blog/mfsu-faster-than-vite.en-US.md b/docs/docs/blog/mfsu-faster-than-vite.en-US.md new file mode 100644 index 000000000000..19df5e5a07f9 --- /dev/null +++ b/docs/docs/blog/mfsu-faster-than-vite.en-US.md @@ -0,0 +1,41 @@ +--- +order: 3 +toc: content +group: + title: Blog +--- + +# 比 Vite 还快的 MFSU + +

+ 编者按:Change the code, don't Workaround! Webpack + 慢就去改他,优化到位后,Bundle 也可以很快。此方案会在 Umi 4 + 中默认开启,适用于既要 Webpack 功能与生态,又想要 Vite 速度的同学们。 +

+ +Umi 4 中同时支持 webpack 和 vite 两种构建方式,跑通了后,迫不及待对比了 Vite 和 Webpack + MFSU 的效果,结果有点意外。关于什么是 MFSU,我在[《SEE Conf: Umi 4 设计思路文字稿》](https://mp.weixin.qq.com/s?__biz=MjM5NDgyODI4MQ%3D%3D&mid=2247484533&idx=1&sn=9b15a67b88ebc95476fce1798eb49146)中有一段详细介绍。 + +

+ + 两个示例、四种模式、四个维度的对比。 + + 两个示例分别是大型的全量 ant-design-pro 和小型的 libs example;四种模式分别是 webpack、webpack + MFSU、webpack + MFSU with esbuild mode、Vite in umi;四个维度分别是无缓存的冷启动、有缓存的热启动、修改代码后的热更新、页面打开速度。 +

+ +多说几点和统计相关的。上述 webpack 相关模式全部开启物理缓存;Vite 是 Umi 中集成后的 Vite,也有担心是不是 Umi 对于 Vite 的误用,经开发者确认,基本排除误用的可能性,大段时间消耗在预编译依赖上;Ant Design Pro 中包含 less 的使用,这是使用 esbuild 无法加速的部分,这有影响,但对于不同模式应该是公平的;下图数据是本地用 13-inch M1 2022 重启电脑后跑 5 次后平均取值的结果;Vite 的热更速度没统计是因为由于 esm 的特性,改完后要等请求过来后处理完才算结束,无法统计,但肯定是很快的。 + +直接上结果。有兴趣手动验证的同学可到 umijs/umi 仓库的不同 example 目录下执行 npm run dev 验证。 + +![](https://img.alicdn.com/imgextra/i4/O1CN01Gz9AA81szqy3BbRfK_!!6000000005838-2-tps-2150-1084.png) + +

+ 图:全量 ant-design-pro 速度对比图 +

+ +![](https://img.alicdn.com/imgextra/i1/O1CN01HNfH7l23L3SRjJUka_!!6000000007238-2-tps-2058-1078.png) + +

+ 图:libs example 速度对比图 +

+ +可以看到,**在这几个场景下,MFSU with esbuild 数据领先。** 四个模式的页面打开速度差不多,所以对比数据没在图中列出,这也是让我意外的点,原以为 Vite 请求多会让页面打开速度变慢,也有可能项目还不够复杂? diff --git a/docs/docs/blog/mfsu-independent-usage.en-US.md b/docs/docs/blog/mfsu-independent-usage.en-US.md new file mode 100644 index 000000000000..68857e23e3c4 --- /dev/null +++ b/docs/docs/blog/mfsu-independent-usage.en-US.md @@ -0,0 +1,257 @@ +--- +toc: content +order: 4 +group: + title: Blog +--- + +# 独立使用 MFSU + +`MFSU` 支持独立在非 umijs 项目中使用,本文将会介绍如何将 `MFSU` 接入你的 webpack 项目。 + +## 示例项目 + +如何接入 MFSU ?提供以下几个 示例项目 配置供参考: + +Webpack 配置示例:examples/mfsu-independent + +CRA v5 配置示例:cra-v5-with-mfsu-example + +## 安装 + +首先安装 `mfsu` 的依赖: + +```bash + pnpm add -D @umijs/mfsu +``` + +## 配置 MFSU + +配置 MFSU 一共需要简单的四步操作,请确保以下所有行为都只在开发环境生效。 + +### 1. 初始化实例 + +第一步,初始化一个 `MFSU` 实例,这是 `MFSU` 的基础: + +```js +// webpack.config.js + +const { MFSU } = require('@umijs/mfsu'); +const webpack = require('webpack'); + +// [mfsu] 1. init instance +const mfsu = new MFSU({ + implementor: webpack, + buildDepWithESBuild: true, +}); +``` + +### 2. 添加中间件 + +第二步,添加 `MFSU` 的 `devServer` 中间件到 webpack-dev-server 中,他将为你提供 `MFSU` 所需打包后的资源: + +#### webpack 5 + +```js +// webpack.config.js + +module.exports = { + devServer: { + // [mfsu] 2. add mfsu middleware + setupMiddlewares(middlewares, devServer) { + middlewares.unshift(...mfsu.getMiddlewares()); + return middlewares; + }, + }, +}; +``` + +#### webpack 4 + +```js +// webpack.config.js + +module.exports = { + devServer: { + // [mfsu] 2. add mfsu middleware + onBeforeSetupMiddleware(devServer) { + for (const middleware of mfsu.getMiddlewares()) { + devServer.app.use(middleware); + } + }, + }, +}; +``` + +### 3. 配置转换器 + +第三步,你需要配置一种源码转换器,他的作用是用来收集、转换依赖导入路径,替换为 `MFSU` 的模块联邦地址(中间件所提供的)。 + +此处提供两种方案:`babel plugins` 或 `esbuild handler` ,一般情况下选择 `babel plugins` 即可。 + +#### Babel Plugins + +向 `babel-loader` 添加 `MFSU` 的 `babel plugins` 即可。 + +```js +// webpack.config.js + +module.exports = { + module: { + rules: [ + // handle javascript source loader + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + plugins: [ + // [mfsu] 3. add mfsu babel plugins + ...mfsu.getBabelPlugins(), + ], + }, + }, + }, + ], + }, +}; +``` + +#### Esbuild handler + +另一种方案是使用内置提供的 `esbuild-loader` 来处理 `js/ts` 资源,**仅用于开发环境** 。 + +:::info +使用这种方案的好处是:在开发环境获得比 `babel` 更快的编译和启动速度 +::: + +```js +// webpack.config.js + +const { esbuildLoader } = require('@umijs/mfsu'); +const esbuild = require('esbuild'); + +module.exports = { + module: { + rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + use: { + loader: esbuildLoader, + options: { + handler: [ + // [mfsu] 3. add mfsu esbuild loader handlers + ...mfsu.getEsbuildLoaderHandler(), + ], + target: 'esnext', + implementation: esbuild, + }, + }, + }, + ], + }, +}; +``` + +:::warning +什么时候我不应该使用 esbuild 方案?
1. 我有自定义的 `babel plugins` 必须在开发环境使用
2. 我需要显示 `css-in-js` 的开发环境友好类名(一般由 babel plugin 提供支持)
3. 在开发环境多适配一套 `esbuild-loader` 的成本大于配置 `babel plugins` 的成本 +::: + +### 4. 设定 webpack 配置 + +第四步,调用 `MFSU` 提供的方法改变你的 webpack 配置,在这里只有增量行为,你无需担心会影响到你原来的配置内容。 + +如下代码所示,`mfsu.setWebpackConfig` 是一个异步方法,为了调用他你需要将原来的 webpack 配置单独抽为一个对象 `config` 之后,再将调用此方法的返回值导出。 + +```js +// webpack.config.js + +const config = { + // origin webpack config +}; + +const depConfig = { + // webpack config for dependencies +}; + +// [mfsu] 4. inject mfsu webpack config +const getConfig = async () => { + await mfsu.setWebpackConfig({ + config, + depConfig, + }); + return config; +}; + +module.exports = getConfig(); +``` + +到此为止,`MFSU` 完全配置完毕,下面可以开始启动项目使用。 + +## 使用 + +进行完 4 步配置后,启动你的项目,你可以从项目根目录得到 `.mfsu` 文件夹,即 `MFSU` 缓存文件夹,请将其添加到 git 的忽略列表(这些缓存文件你不应该提交他们): + +```bash +# .gitignore + +.mfsu +``` + +符合预期时,你已经可以享受 `MFSU` 带来的好处,包括 `esbuild` 快速的打包和二次热启动的提速。 + +## 其他配置 + +以下是其他你可能会用到的 `MFSU` 实例配置: + +```js +const mfsu = new MFSU({ + cwd: process.cwd(), +}); +``` + +其他 Options: + +| option | default | description | +| :-------------------- | :----------------------- | :--------------------------------------------------------------------- | +| `cwd` | `process.cwd()` | 项目根目录 | +| `getCacheDependency` | `() => {}` | 用返回值来对比,使 MFSU cache 无效的函数 | +| `tmpBase` | `${process.cwd()}/.mfsu` | MFSU 缓存存放目录 | +| `unMatchLibs` | `[]` | 手动排除某些不需要被 MFSU 处理的依赖 | +| `runtimePublicPath` | `undefined` | 同 umijs > [`runtimePublicPath`](../docs/api/config#runtimepublicpath) | +| `implementor` | `undefined` | webpack 实例,需要和项目内使用的唯一实例一致 | +| `buildDepWithESBuild` | `false` | 是否使用 `esbuild` 打包依赖 | +| `onMFSUProgress` | `undefined` | 获取 MFSU 编译进度的回调 | + +## 常见问题 + +#### 如何保证我的 MFSU 配置只在开发环境生效? + +使用环境标识避免所有 `MFSU` 在生产环境构建时的配置侵入: + +```js +const isDev = process.env.NODE_ENV === 'development' + +const mfsu = isDev + ? new MFSU({ + implementor: webpack, + buildDepWithESBuild: true, + }) + : undefined + +// e.g. +{ + test: /\.[jt]sx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + plugins: [ + ...(isDev ? [] : mfsu.getBabelPlugins()) + ] + } + } +} +``` diff --git a/docs/docs/blog/umi-4-rc.en-US.md b/docs/docs/blog/umi-4-rc.en-US.md new file mode 100644 index 000000000000..a7a85d7498f3 --- /dev/null +++ b/docs/docs/blog/umi-4-rc.en-US.md @@ -0,0 +1,150 @@ +--- +toc: content +order: 2 +group: + title: Blog +--- + +# Umi 4 RC 发布 + +大家好,Umi 4 经过几个月的开发,终于要和大家见面了。相比 Umi 2 到 3,3 到 4 的变化是巨大的,开发时间也更长,但我们尽量把对于开发者的影响降低到最小。按捺住激动的心情,在此先和大家分享下都有哪些变化。 + +🎉 新官网和文档
🚀 MFSU V3 & 默认开启
🎭 双构建引擎和 ESMi
🕸 Webpack 5
⛹🏾‍♂️ React Router 6 & 新路由
🐹 最佳实践迭代
🛡️ 依赖预打包
🤺 Umi Max
🐛 Low Import 研发模式
⚠️ 强约束功能集成
🎈 Import All From Umi 迭代
🍀 srcTranspiler 和 depTranspiler
🌼 jsMinifier 和 cssMinifier
🌸 应用元数据
❄️ 微生成器
🧪 贴心小改进
+ +

+ 新官网和文档。 + 下图是新官网的首页,包括重新梳理的文档、信息结构、以及新写的文档插件。目前包含基础的配置、API、升级和快速上手等基础文档,剩余文档还在紧张编写中。有个变化是之前插件的文档集成到 + Umi 官网中,成为 Umi Max 的一部分,之后无需跳转。 +

+ +![](https://img.alicdn.com/imgextra/i1/O1CN014dDq4L1Zc3guRwcse_!!6000000003214-2-tps-1600-941.png) + +

+ MFSU V3 & 默认开启。 + MFSU 更新了他的第三个大版本,如果你有用 Umi 3 内置的 MFSU 并遇到问题,建议重新尝试,这个版本有很多改进,解决基本所有之前可能会遇到的诡异问题,并且编译速度和页面打开速度都更快。昨天我还有写一篇 + [《比 Vite 更快的 MFSU》](https://mp.weixin.qq.com/s?__biz=MjM5NDgyODI4MQ==&mid=2247484624&idx=1&sn=2addfa8cc2511fbea91faf831195788f)。基于此,我们自信地把这个功能在 + Umi 4 中默认开启。还有值得一提的是,MFSU 可脱离 Umi 运行。 +

+ +![](https://img.alicdn.com/imgextra/i2/O1CN01Znj8HD1mCwz72voXv_!!6000000004919-2-tps-1600-807.png) + +

+ 双构建引擎和 ESMi。 + Umi 4 提供 Vite 和 Webpack 两种构建模式供开发者选择,并尽可能保证他们之间功能的一致性,可能有些同学会喜欢 + dev 用 vite,build 用 webpack 这样的组合。同时基于 Vite 模式实现了 ESMi 的 Client + 端,ESMi 依赖服务端,在外网还无法使用。 +

+ +

+ Webpack 5。Umi 4 + 默认使用 webpack 5 并开启物理缓存。 +

+ +

+ + React Router 6 & 新路由。 + + Umi 4 的路由基于 React Router 6 实现,个人非常喜欢这个版本,因为 Remix 的原因,React + Router 6 从设计上考虑了配置式路由的场景,让我得以删除大量 Umi 3 中关于路由渲染的代码。同时基于此,设计了新的路由结构,方便扩展和在未来处理路由的约定式请求。 +

+ +

+ 最佳实践迭代。 + 针对之前 umijs/plugins 仓库中的插件进行了重写、升级,并整合到主仓库。这么做是为了更好的顶层设计,让官方插件之间的风格更一致。 +

+ +

+ ​​依赖预打包。 + 由于服务企业内部,安全和稳定是其中很重要点,加上最近 colors 和 faker.js 闹得社区沸沸扬扬,谁都不希望睡一觉醒来,自己负责的业务挂了,还背个故障。Umi + 4 接着 Umi 3 继续做依赖预打包的事,并且更彻底,不仅是 node 侧的依赖,部分运行时的依赖也会做锁定,比如 + core-js 和 @babel/runtime。 +

+ +![](https://img.alicdn.com/imgextra/i1/O1CN01h44iJg1T09DNuYOlm_!!6000000002319-2-tps-1600-758.png) + +

+ Umi Max。Umi Max + 是内部 Bigfish + 框架的对外版本,解我们自己的问题,同时也给社区另一个集中化框架的选择。 +

+ +

+ + Low Import 研发模式。 + + 这是 Umi 4 的试验性功能之一,目前已开发完成,解的问题是让开发者少些或不写 import + 语句。项目中大量的 import 其实都可以通过工程化的方式自动处理。Umi 4 里通过 lowImport:{' '} + {} 开启,然后就可以无 import 直接用路由相关的 Link、useLocation 等,数据流相关的 + connect、useModel,antd 组件 Button、Calendar 等,以及其他更多。 +

+ +![](https://img.alicdn.com/imgextra/i4/O1CN0142Vcpt25kMZqjmioe_!!6000000007564-2-tps-1600-631.png) + +

+ 强约束功能集成。Umi + 4 提供 API 让强约束和代码校验变得非常容易。API 包括 + api.onCheck、api.onCheckConfig、api.onCheckPkgJSON 和 + api.onCheckCode,顾名思义,非常好理解他们分别是干嘛的,可以分别对依赖类、代码类和配置类的内容做校验和卡点,适用于团队。 +

+ +

+ + Import All From Umi 迭代。 + + 这是两年前 Umi 3 加的功能,最近发现 Remix、prisma、vitekit 等框架和工具都有类似实现。这种方式有好有坏。好处是通过 + umi 将大量依赖管理起来,用户无需手动安装。坏处是更黑盒,同时有点 Hack。Umi 4 不能解其黑盒问题,但解了 + Hack 问题,让实现无副作用,可以和 Vite、MFSU 等方案无缝结合。 +

+ +

+ + srcTranspiler 和 depTranspiler。 + + 提供针对源码编译和依赖编译更多选择。源码编译可选 babel、swc 和 esbuild,目前没有银弹,合适场景做合适的选择。比如 + swc 由于不支持 top level await,和 mfsu 会有些冲突,但他适用于 build,因为有补丁可以兼容到 + es7;比如 esbuild 适用于 dev,因为快。数据方面以 ant-design-pro 项目为例,源码编译用 + esbuild 相比 babel 在 M1 2020 无缓存情况下会快 3s。 +

+ +

+ + jsMinifier 和 cssMinifier。 + + js 压缩和 css 压缩 Umi 4 默认都用的 esbuild,因为快。同时也提供更多选择,js 压缩还支持 + swc、terser 和 uglifyJs,css 压缩还支持 cssnano。 +

+ +

+ 应用元数据。Umi 4 + 有通过 api.appData + 收集各种项目数据,从配置、路由、package.json、tsconfig.json、npmClient + 到数据流、国际化、antd 用了哪个版本、react 和 react-dom + 的版本等,应有尽有,这对于插件开发者会非常实用,也适用于有统计需求的场景。 +

+ +

+ 微生成器。没错,就是 + modern.js 的微生成器,这功能从 modern.js + 里学习了不少,名字就不改了。举个例子,比如 prettier + 功能,可能不是每个项目都需要,就比较适用于微生成器,按需启用、添加配置、安装依赖。 +

+ +

+ 贴心小改进。 + 还有不少贴心小改进,举两个例子。1 是项目中新增 plugin.ts,会默认作为插件添加,方便项目进行一些插件级的扩展;2 + 是调试问题时通常需要修改编译后的代码看看有没有改对,你把 umi.js 下下来存到项目根目录,umi + 会优先使用这份代码。 +

+ +以上是 Umi 4 目前的新功能。 + +除此之外,还有一些计划在正式版发布之前做的事情。包括 api route、umi server and adapter、route loader、稳定的 lint、更多命令、组件研发 father 4、文档工具 dumi 2 等,会在之后的 RC 版本中与大家见面。 + +欢迎大家尝鲜 Umi 4,官方文档有准备 ant-design-pro 从 Umi 3 到 4 的升级文档。同时 RC 阶段,还准备了一个手把手升级的微信交流群,欢迎 Umi 4 的先行者们加入,祝大家升级顺利,也提前祝大家新年快乐 🧨,🐯 年吉祥。 + +

+ +

diff --git a/docs/docs/blog/webpack-5-prod-cache.en-US.md b/docs/docs/blog/webpack-5-prod-cache.en-US.md new file mode 100644 index 000000000000..2ee308d8eb5c --- /dev/null +++ b/docs/docs/blog/webpack-5-prod-cache.en-US.md @@ -0,0 +1,99 @@ +--- +toc: content +order: 7 +group: + title: Blog +--- + +# 物理构建缓存 + +在 `umi build` 构建生产环境产物时,Umi 4 默认没有配置 webpack 5 的物理缓存,这是因为 webpack 的物理缓存失效时机问题,需要依赖用户项目的实际情况,所以没有很好的通用解决方案。 + +所以,当你不明确哪些依赖会让项目物理缓存失效时,很容易产生构建缓存不失效,导致产物是旧的问题,极大影响研发效率。 + +## 缓存场景 + +当你的项目需要构建缓存时,是有原因的,我们粗略把场景分成两类:普通项目、Monorepo 中的项目。 + +### 普通项目 + +构建比较慢,如何复用上次的物理缓存,做到多次构建提速? + +#### 首选解决思路 + +此时首选的优化思路应该是:考虑使用其他更快的现代转译器,比如调整 [`srcTranspiler`](../docs/api/config#srctranspiler) 、[`cssMinifier`](../docs/api/config#cssminifier) 。 + +#### CI 中的问题 + +物理缓存一般存在于 `node_modules/.cache` ,这就意味着如果你在 CI 中构建,构建的基建必须要支持恢复上次的缓存文件,如果构建容器不支持恢复缓存,同样也无法享受好处。 + +#### 选择依据 + +所以,当你: + +1. **多次构建**:确实有多次反复构建的需求。 + +2. **能恢复缓存**:在本地构建,或在 CI 有手段能恢复上次的物理缓存文件。 + +3. **时间长**:项目构建时间比较长、开启其他转译器仍无法提速(或有强诉求无法切换转译器)。 + +满足这些条件后,你才应该考虑开启物理缓存。 + +#### 配置方法 + +```ts +// .umirc.ts + +import { join } from 'path'; +import { defineConfig } from 'umi'; +import { createHash } from 'crypto'; + +export default defineConfig({ + chainWebpack(config, { env }) { + if (env === 'production') { + config.cache({ + type: 'filesystem', + store: 'pack', + // 🟡 假如你的项目在 CI 中构建每次环境变量都不一样,请挑选或者排除 + version: createEnvironmentHash(process.env), + buildDependencies: { + config: [__filename], + tsconfig: [join(__dirname, 'tsconfig.json')], + packagejson: [join(__dirname, 'package.json')], + umirc: [join(__dirname, '.umirc.ts')], + // 🟡 其他可能会影响项目的配置文件路径,其内容变更会使缓存失效 + }, + }); + } + }, +}); + +function createEnvironmentHash(env: Record) { + const hash = createHash('md5'); + hash.update(JSON.stringify(env)); + const result = hash.digest('hex'); + return result; +} +``` + +请格外注意: + +1. 你的项目有哪些文件、依赖会影响项目,配置他们作为依赖,变更时可以使得缓存失效。 + +2. 因为 `process.env` 包括了所有的 nodejs 环境变量,这非常多,如果环境变量在 CI 中每次构建都存在差异,请挑选所需的环境变量,或者排除掉会变化的。 + + ```ts + // 如挑选可能会影响项目内容的环境变量 + createEnvironmentHash({ + NODE_ENV: process.env.NODE_ENV, + // ... + }); + ``` + +### Monorepo 中的项目 + +在 monorepo 中,如何缓存需要前置构建的其他子包,比如构建 `apps/project-umi` 需要先构建好他依赖的子包 `libs/component` ,但是下次 `libs/component` 没有代码改动,如何跳过这部分前置依赖的构建? + +此时推荐你使用 [Turborepo](https://turbo.build/repo) 来做 monorepo 构建方案,具体使用方法请参见 [官方文档](https://turbo.build/repo/docs) 和 [examples](https://github.com/vercel/turbo/tree/main/examples) 。 + +注:如果在 CI 中构建,同样需要容器支持恢复上次的 turbo 缓存,可以通过 [`--cache-dir`](https://turbo.build/repo/docs/reference/command-line-reference#--cache-dir) 选项更改缓存位置。 diff --git a/docs/docs/docs/api/api.en-US.md b/docs/docs/docs/api/api.en-US.md new file mode 100644 index 000000000000..a43c0c3f9548 --- /dev/null +++ b/docs/docs/docs/api/api.en-US.md @@ -0,0 +1,832 @@ +--- +order: 1 +toc: content +--- + +# API + +为方便查找,以下内容通过字母排序。 + +## umi + +### createBrowserHistory + +创建使用浏览器内置 `history` 来跟踪应用的 `BrowserHistory`。推荐在支持 HTML5 `history` 接口的 现代 Web 浏览器中使用。 + +类型定义如下: +```ts +function createBrowserHistory(options?: { window?: Window }) => BrowserHistory; +``` + +使用范例: +```ts +// create a BrowserHistory +import { createBrowserHistory } from 'umi'; +const history = createBrowserHistory(); +// or a iframe BrowserHistory +import { createBrowserHistory } from 'umi'; +const history = createBrowserHistory({ + window: iframe.contentWindow, +}); +``` +### createHashHistory + +`createHashHistory` 返回一个 `HashHistory` 实例。`window` 默认为当前 `document` 的 `defaultView`。 + +`HashHistory` 与 `BrowserHistory` 的主要区别在于,`HashHistory` 将当前位置存储在 URL 的哈希部分中,这意味着它在路由切换时不会发送请求到服务器。如果您将站点托管在您无法完全控制服务器上,或者在只提供同单页面的 Electron 应用程序中,推荐使用 `HashHistory`。 + +使用范例: +```ts +// create a HashHistory +import { createHashHistory } from 'umi'; +const history = createHashHistory(); +``` + +### createMemoryHistory + +`MemoryHistory` 不会在地址栏被操作或读取。它也非常适合测试和其他的渲染环境。 + +```ts +const history = createMemoryHistory(location) +``` + +### createSearchParams + +包装 `new URLSearchParams(init)` 的工具函数,支持使用数组和对象创建 + +```ts +import { createSearchParams } from 'umi'; + + +// 假设路径 http://a.com?foo=1&bar=2 +createSearchParams(location.search); +createSearchParams("foo=1&bar=2"); +createSearchParams("?foo=1&bar=2"); + +// 键值对对象 +createSearchParams({ foo: 'bar', qux: 'qoo'}).toString() +// foo=bar&qux=qoo + +// 键值元组数组 +createSearchParams([["foo", "1"], ["bar", "2"]]).toString() +// foo=1&bar=2 +``` + +[URLSearchParams 文档](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams) + +### generatePath + +使用给定的带参数的 path 和对应的 params 生成实际要访问的路由。 + +```ts +import { generatePath } from 'umi'; + +generatePath("/users/:id", { id: "42" }); // "/users/42" +generatePath("/files/:type/*", { + type: "img", + "*": "cat.jpg", +}); // "/files/img/cat.jpg" +``` + +### Helmet + +即 [react-helmet-async](https://github.com/staylor/react-helmet-async) 提供的 Helmet 组件,用于在页面中动态配置 `head` 中的标签,例如 `title`。 + +> 注意:为了确保 SSR 时 Helmet 仍能正常工作,请务必使用 Umi 提供的 Helmet 而不是单独安装 react-helmet + +```tsx +import { Helmet } from 'umi'; + +export default function Page() { + return ( + + Hello World + + ); +} +``` + +### history + +和 history 相关的操作,用于获取当前路由信息、执行路由跳转、监听路由变更。 + +```ts +// 建议组件或 hooks 里用 useLocation 取 +import { useLocation } from 'umi'; +export default function Page() { + let location = useLocation(); + return ( +
+ { location.pathname } + { location.search } + { location.hash } +
+ ); +} +``` + +如果在 React 组件和 Hooks 之外获取当前路由信息。 + +```ts +// location 对象,包含 pathname、search 和 hash +window.location.pathname; +window.location.search; +window.location.hash; +``` + +命令式路由跳转。 + +```ts +import { history } from 'umi'; + +// 跳转到指定路由 +history.push('/list'); + +// 带参数跳转到指定路由 +history.push('/list?a=b&c=d#anchor', state); +history.push({ + pathname: '/list', + search: '?a=b&c=d', + hash: 'anchor', + }, + { + some: 'state-data', + } +); + +// 跳转当前路径,并刷新 state +history.push({}, state) + +// 跳转到上一个路由 +history.back(); +history.go(-1); +``` + +:::info{title=🚨} +注意:history.push 和 history.replace 需要使用 `state` 需将 `state` 作为这两个 API 的第二个参数传递 +::: + + +路由监听。 + +```ts +import { history } from 'umi'; + +const unlisten = history.listen(({ location, action }) => { + console.log(location.pathname); +}); +unlisten(); +``` + +### Link + +`` 是 React 组件,是带路由跳转功能的 `` 元素。 + +类型定义如下: + +```ts +declare function Link(props: { + prefetch?: boolean; + to: string | Partial<{ pathname: string; search: string; hash: string }>; + replace?: boolean; + state?: any; + reloadDocument?: boolean; +}): React.ReactElement; +``` + +示例: + +```tsx +import { Link } from 'umi'; + +function IndexPage({ user }) { + return {user.name}; +} +``` + +`` 支持相对路径跳转;`` 不做路由跳转,等同于 `` 的跳转行为。 + +若开启了 `prefetch` 则当用户将鼠标放到该组件上方时,Umi 就会自动开始进行跳转路由的组件 js 文件和数据预加载。 + +### matchPath + +`matchPath` 可以将给定的路径以及一个已知的路由格式进行匹配,并且返回匹配结果。 + +类型定义如下: + +```ts +declare function matchPath( + pattern: PathPattern | string, + pathname: string +): PathMatch | null; +interface PathMatch { + params: Params; + pathname: string; + pattern: PathPattern; +} +interface PathPattern { + path: string; + caseSensitive?: boolean; + end?: boolean; +} +``` + +示例: +```ts +import { matchPath } from 'umi'; +const match = matchPath( + { path: "/users/:id" }, + "/users/123", +); +// { +// "params": { "id": "123" }, +// "pathname": "/users/123", +// "pathnameBase": "/users/123", +// "pattern": { "path": "/users/:id" } +// } +``` +### matchRoutes + +`matchRoutes` 可以将给定的路径以及多个可能的路由选择进行匹配,并且返回匹配结果。 + +类型定义如下: + +```ts +declare function matchRoutes( + routes: RouteObject[], + location: Partial | string, + basename?: string +): RouteMatch[] | null; +interface RouteMatch { + params: Params; + pathname: string; + route: RouteObject; +} +``` + +示例: + +```ts +import { matchRoutes } from 'umi'; +const match = matchRoutes( + [ + { + path: "/users/:id", + }, + { + path: "/users/:id/posts/:postId", + }, + ], + "/users/123/posts/456", +); +// [ +// { +// "params": { +// "id": "123", +// "postId": "456" +// }, +// "pathname": "/users/123/posts/456", +// "pathnameBase": "/users/123/posts/456", +// "route": { +// "path": "/users/:id/posts/:postId" +// } +// } +// ] +``` + +### NavLink + +`` 是 `` 的特殊形态,他知道当前是否为路由激活状态。通常在导航菜单、面包屑、Tabs 中会使用,用于显示当前的选中状态。 + +类型定义如下: + +```ts +declare function NavLink(props: LinkProps & { + caseSensitive?: boolean; + children?: React.ReactNode | ((props: { isActive: boolean }) => React.ReactNode); + className?: string | ((props: { isActive: boolean }) => string | undefined); + end?: boolean; + style?: React.CSSProperties | ((props: { isActive: boolean }) => string | React.CSSProperties); +}): React.ReactElement; +``` + +下方示例分别用了 style、className 和 children 来渲染 active 状态。 + +```ts +import { NavLink } from 'umi'; + +function Navs() { + return
    +
  • isActive ? { color: 'red' } : undefined}>Messages
  • +
  • isActive ? 'active' : undefined}>Tasks
  • +
  • {({ isActive }) => Blog}
  • +
; +} +``` + +### Outlet + +`` 用于渲染父路由中渲染子路由。如果父路由被严格匹配,会渲染子路由中的 index 路由(如有)。 + +类型定义如下: + +```ts +interface OutletProps { + context?: unknown; +} +declare function Outlet( + props: OutletProps +): React.ReactElement | null; +``` + +示例: + +```ts +import { Outlet } from 'umi'; + +function Dashboard() { + return ( +
+

Dashboard

+ +
+ ); +} + +function DashboardWithContext() { + return ( +
+

Dashboard

+ +
+ ); +} +``` + +`Outlet` 组件的 `context` 可以使用 API `useOutletContext` 在子组件中获取。 + +### resolvePath + +用于在客户端解析前端路由跳转路径。 + +类型定义如下: + +```ts +declare function resolvePath( + to: Partial | string, + fromPathname?: string +): { + pathname: string; + search: string; + hash: string; +}; +``` + +示例: + +```ts +// 同级相对跳转,返回 { pathname: '/parent/child', search: '', hash: '' } +resolvePath('child', '/parent'); +resolvePath('./child', '/parent'); +resolvePath('', '/parent/child'); +resolvePath('.', '/parent/child'); + +// 祖先层级相对跳转,返回 { pathname: '/parent/sibling', search: '', hash: '' } +resolvePath('../sibling', '/parent/child'); +resolvePath('../../parent/sibling', '/other/child'); + +// 绝对跳转,返回 { pathname: '/target', search: '', hash: '' } +resolvePath('/target', '/parent'); +resolvePath('/target', '/parent/child'); + +// 携带 search 和 hash 跳转,返回 { pathname: '/params', search: '?a=b', hash: '#c' } +resolvePath('/params?a=b#c', '/prev'); +``` + +### terminal + +`terminal` 用于在开发阶段在浏览器向 node 终端输出日志的工具。 + +示例: +```ts +import {terminal} from 'umi'; +// 下面三条命令会在 umi 启动终端上打出用不同颜色代表的日志 +terminal.log('i am log level'); +terminal.warn('i am warn level'); +terminal.error('i am error level'); +``` +注意 `terminal` 只在环境变量 `NODE_ENV` 非 `production` 时生效;在 Umi 的构建产物中对应的日志调用函数不会有任何作用,所以可以不必删除调用 `terminal` 的代码。 + +### useAppData + +`useAppData` 返回全局的应用数据。 + +类型定义如下: + +```ts +declare function useAppData(): { + routes: Record; + routeComponents: Record>; + clientRoutes: ClientRoute[]; + pluginManager: any; + rootElement: string; + basename: string; + clientLoaderData: { [routeKey: string]: any }; + preloadRoute: (to: string) => void; +}; +``` +注意:此处 API 可能还会调整。 + +### useLocation + +`useLocation` 返回当前 location 对象。 + +类型定义如下: + +```ts +declare function useLocation(): { + pathname: string; + search: string; + state: unknown; + key: Key; +}; +``` + +一个场景是在 location change 时做一些 side effect 操作,比如 page view 统计。 + +```ts +import { useLocation } from 'umi'; + +function App() { + const location = useLocation(); + React.useEffect(() => { + ga('send', 'pageview'); + }, [location]); + // ... +} +``` + +### useMatch + +`useMatch` 返回传入 path 的匹配信息;如果匹配失败将返回 `null` + +类型定义如下: + +```ts +declare function useMatch(pattern: { + path: string; + caseSensitive?: boolean; + end?: boolean; +} | string): { + params: Record; + pathname: string; + pattern: { + path: string; + caseSensitive?: boolean; + end?: boolean; + }; +}; +``` + +示例: +```tsx +import { useMatch } from 'umi'; + +// when url = '/events/12' +const match = useMatch('/events/:eventId'); +console.log(match?.pathname, match?.params.eventId); +// '/events/12 12' +``` + +### useNavigate + +`useNavigate` 钩子函数返回一个可以控制跳转的函数;比如可以用在提交完表单后跳转到其他页面。 + +```ts +declare function useNavigate(): NavigateFunction; + +interface NavigateFunction { + ( + to: To, + options?: { replace?: boolean; state?: any } + ): void; + (delta: number): void; +} +``` + +示例: + +* 跳转路径 +```ts +import { useNavigate } from 'umi'; + +let navigate = useNavigate(); +navigate("../success", { replace: true }); +``` + +* 返回上一页 +```ts +import { useNavigate } from 'umi'; + +let navigate = useNavigate(); +navigate(-1); +``` + +### useOutlet + +`useOutlet` 返回当前匹配的子路由元素,`` 内部使用的就是此 hook 。 + +类型定义如下: +```ts +declare function useOutlet(): React.ReactElement | null; +``` + +示例: +```ts +import { useOutlet } from 'umi'; + +const Layout = ()=>{ + const outlet = useOutlet() + + return
+ {outlet} +
+} +``` + +### useOutletContext + +`useOutletContext` 用于返回 `Outlet` 组件上挂载的 `context` 。 + +类型定义如下: +```ts +declare function useOutletContext(): Context; +``` + +示例: +```ts +import { useOutletContext, Outlet } from 'umi'; + +const Layout = () => { + return
+ +
+} + +const SomeRouteComponentUnderLayout = () => { + const layoutContext = useOutletContext(); + + return JSON.stringify(layoutContext) // {"prop":"from Layout"} +} +``` + +### useParams + +`useParams` 钩子函数返回动态路由的匹配参数键值对对象;子路由中会集成父路由的动态参数。 + +类型定义如下: +```ts +declare function useParams< + K extends string = string +>(): Readonly>; +``` + +示例: + +```ts +import { useParams } from 'umi'; + +// 假设有路由配置 user/:uId/repo/:rId +// 当前路径 user/abc/repo/def +const params = useParams() +/* params +{ uId: 'abc', rId: 'def'} +*/ +``` + +### useResolvedPath + +`useResolvedPath` 根据当前路径将目标地址解析出完整的路由信息。 + +类型定义如下: +```ts +declare function useResolvedPath(to: To): Path; +``` + +示例: + +```ts +import { useResolvedPath } from 'umi'; + +const path = useResolvedPath('docs') +/* path +{ pathname: '/a/new/page/docs', search: '', hash: '' } +*/ +``` + +### useRouteData + +`useRouteData` 返回当前匹配路由的数据的钩子函数。 + +类型定义如下: + +```ts +declare function useRouteData(): { + route: Route; +}; +``` +注意:此处 API 可能还会调整。 + +示例: +```ts +import { useRouteData } from 'umi'; + +const route = useRouteData(); +/* route +{ + route: { + path: 'a/page', + id: 'a/page/index', + parentId: '@@/global-layout', + file: 'a/page/index.tsx' + } +} +*/ +``` + +### useRoutes + +`useRoutes` 渲染路由的钩子函数,传入路由配置和可选参数 `location`, 即可得到渲染结果;如果没有匹配的路由,结果为 `null`。 + +类型定义如下: +```ts +declare function useRoutes( + routes: RouteObject[], + location?: Partial | string; +): React.ReactElement | null; +``` + +示例: + +```ts +import * as React from "react"; +import { useRoutes } from "umi"; + +function App() { + let element = useRoutes([ + { + path: "/", + element: , + children: [ + { + path: "messages", + element: , + }, + { path: "tasks", element: }, + ], + }, + { path: "team", element: }, + ]); + + return element; +} +``` + +### useRouteProps + +读取当前路由在路由配置里的 props 属性,你可以用此 hook 来获取路由配置中的额外信息。 + +```ts +// .umirc.ts +routes: [ + { + path: '/', + custom_key: '1', + } +] +``` + +```ts +import { useRouteProps } from 'umi' + +export default function Page() { + const routeProps = useRouteProps() + + // use `routeProps.custom_key` +} +``` + +注:同样适用于约定式路由。 + +### useSelectedRoutes + +用于读取当前路径命中的所有路由信息。比如在 `layout` 布局中可以获取到当前命中的所有子路由信息,同时可以获取到在 `routes` 配置中的参数,这格外有用。 + +实例: + +```tsx +// layouts/index.tsx + +import { useSelectedRoutes } from 'umi' + +export default function Layout() { + const routes = useSelectedRoutes() + const lastRoute = routes.at(-1) + + if (lastRoute?.pathname === '/some/path') { + return
1 :
+ } + + if (lastRoute?.extraProp) { + return
2 :
+ } + + return +} +``` + +### useSearchParams + +`useSearchParams` 用于读取和修改当前 URL 的 query string。类似 React 的 `useState`,其返回包含两个值的数组,当前 URL 的 search 参数和用于更新 search 参数的函数。 + +类型定义如下: + +```ts +declare function useSearchParams(defaultInit?: URLSearchParamsInit): [ + URLSearchParams, + ( + nextInit?: URLSearchParamsInit, + navigateOpts?: : { replace?: boolean; state?: any } + ) => void +]; + +type URLSearchParamsInit = + | string + | ParamKeyValuePair[] + | Record + | URLSearchParams; +``` + +示例: +```ts +import React from 'react'; +import { useSearchParams } from 'umi'; + +function App() { + let [searchParams, setSearchParams] = useSearchParams(); + function handleSubmit(event) { + event.preventDefault(); + setSearchParams(serializeFormQuery(event.target)); + } + return
{/* ... */}
; +} +``` + +### withRouter + +`withRouter` 参考 [react-router faq](https://reactrouter.com/docs/en/v6/getting-started/faq#what-happened-to-withrouter-i-need-it) 实现的版本, 仅实现了部分能力, 请参考类型定义按需使用, 建议迁移到 React Hook API。 + +类型定义如下: + +```ts +export interface RouteComponentProps> { + history: { + back: () => void; + goBack: () => void; + location: ReturnType; + push: (url: string, state?: any) => void; + }; + location: ReturnType; + match: { + params: T; + }; + params: T; + navigate: ReturnType; +} +``` + +示例: +```tsx +import React from 'react'; +import { withRouter } from 'umi'; + +class HelloWorld extends React.Component { + render() { + return ( +
+ Hello World {this.props.location.pathname} +

params: {JSON.stringify(this.props.match.params)}

+ +
+ ); + } +} + +export default withRouter(HelloWorld); +``` diff --git a/docs/docs/docs/api/commands.en-US.md b/docs/docs/docs/api/commands.en-US.md new file mode 100644 index 000000000000..dbfb2d85406c --- /dev/null +++ b/docs/docs/docs/api/commands.en-US.md @@ -0,0 +1,308 @@ +--- +order: 4 +toc: content +--- +# 命令行 + +umi 提供了很多内置的命令行用于启动,构建项目,另外还有一些辅助开发的命令,如生成器等。 + +要获取可用的命令列表,你可以在项目目录中运行 help 命令: + +```bash +umi help +``` + +你应该能看到类似如下的日志: + +```bash +Usage: umi [options] + +Commands: + + build build app for production + config umi config cli + dev dev server for development + help show commands help + lint lint source code using eslint and stylelint + setup setup project + deadcode check dead code + version show umi version + v show umi version + plugin inspect umi plugins + verify-commit verify the commit message, which is usually used with husky. + preview locally preview production build + run run the script commands, support for ts and zx + generate generate code snippets quickly + g generate code snippets quickly + +Run `umi help ` for more information of specific commands. +Visit https://umijs.org/ to learn more about Umi. +``` + +> 为方便查找,以下命令通过字母排序。 + +## build + +构建项目,适用于生产环境的部署。 + +```bash +$ umi build +``` + +## config + +通过命令行快速查看和修改配置。 + +查看配置,可以用 `list` 或 `get`。 + +```bash +$ umi config list + - [key: polyfill] false + - [key: externals] { esbuild: true } + +$ umi config get mfsu + - [key: externals] { esbuild: true } +``` + +修改配置,可以用 `set` 或 `remove`。 + +```bash +$ umi config set polyfill false +set config:polyfill on /private/tmp/sorrycc-wsYpty/.umirc.ts + +$ umi config remove polyfill +remove config:polyfill on /private/tmp/sorrycc-wsYpty/.umirc.ts +``` + +## dev + +启动本地开发服务器,进行项目的开发与调试。 + +```bash +$ umi dev + ╔═════════════════════════════════════════════════════╗ + ║ App listening at: ║ + ║ > Local: https://127.0.0.1:8001 ║ +ready - ║ > Network: https://192.168.1.1:8001 ║ + ║ ║ + ║ Now you can open browser with the above addresses👆 ║ + ╚═════════════════════════════════════════════════════╝ +event - compiled successfully in 1051 ms (416 modules) +``` + +## generate + +用于增量生成文件或启用功能,命令行别名是 `g`。 + +不加任何参数时会给交互式的生成器选择。 + +```bash +$ umi g +# 或 +$ umi generate +? Pick generator type › - Use arrow-keys. Return to submit. +❯ Create Pages -- Create a umi page by page name + Enable Prettier -- Enable Prettier +``` + +也可以指定参数。 + +```bash +# 生成路由文件 +$ umi g page index --typescript --less +``` + +## help + +查看帮助。 + +```bash +$ umi help +Usage: umi [options] + +Commands: + + build build app for production + config umi config cli + dev dev server for development + help show commands help + setup setup project + version show umi version + plugin inspect umi plugins + generate generate code snippets quickly + +Run `umi help ` for more information of specific commands. +Visit https://umijs.org/ to learn more about Umi. +``` + +也可指定命令,查看特定命令的详细帮助。 + +```bash +$ umi help build +Usage: umi build [options] +build app for production. + +Details: + umi build + + # build without compression + COMPRESS=none umi build + + # clean and build + umi build --clean +``` + +## lint + +用于检查及修正代码是否符合规则。 + +```bash +$ umi lint +Usage: umi lint + + 支持只校验 js、ts、tsx、jsx 类型文件: umi lint --eslint-only + + 支持只校验 css、less 等样式文件: umi lint --stylelint-only + + 支持校验 cssinjs 模式校验: umi lint --stylelint-only --cssinjs + + 修正代码: --fix + +``` + +## plugin + +插件相关操作,目前只支持 `list` 子命令。 + +列出所有插件。 + +```bash +$ umi plugin list +- @umijs/core/dist/service/servicePlugin +- @umijs/preset-umi (from preset) +- @umijs/preset-umi/dist/registerMethods (from preset) +- @umijs/preset-umi/dist/features/appData/appData (from preset) +- @umijs/preset-umi/dist/features/check/check (from preset) +- @umijs/preset-umi/dist/features/configPlugins/configPlugins (from preset) +- virtual: config-styles +- virtual: config-scripts +- virtual: config-routes +- virtual: config-plugins +... +``` + +## preview + +`umi preview` 命令会在本地启动一个静态 Web 服务器,将 dist 文件夹运行在 http://127.0.0.1:4172, 用于预览构建后产物, 支持 proxy、mock 等设置。 + +你可以通过 `--port` 参数来配置服务的运行端口。 + +```bash +$ umi preview --port 9527 +``` + +现在 `preview` 命令会将服务器运行在 http://127.0.0.1:9527. + +通过 `--host` 参数来指定 配置服务运行的 hostname。 + +以下用户配置在 `preview` 时也会生效 + +* [https](./config#https) +* [proxy](../guides/proxy) +* [mock](../guides/mock) + +注意 `dist` 目录会随着配置 `outputPath` 的变更而变更。 + +## run + +`umi run` 命令可以让你像 node 运行 js 一样来运行 TypeScript 和 ESM 文件。你可以搭配 [zx](https://github.com/google/zx) 来更好的使用脚本命令。 + +```bash +$ umi run ./script.ts +``` + +## setup + +初始化项目,会做临时文件的生成等操作。通常在 package.json 的 `scripts.postinstall` 里设置。 + +```bash +{ + "scripts": { "postinstall": "umi setup" } +} +``` + +## deadcode + +用于查找 src 目录下未被引用的文件,并在根目录输出文件。 + +```bash +$ umi deadcode +- Preparing... +- begin check deadCode +- write file /examples/umi-run/DeadCodeList-{timeStamp}.txt +- check dead code end, please be careful if you want to remove them +``` + +## mfsu + +`umi mfsu` 命令可以查看 MFSU 依赖信息、重新构建 MFSU 依赖和清除 MFSU 依赖。 + + +```bash title="获取 MFSU 命令帮忙" +$ umi mfsu +``` + +```bash title="获取 MFSU 依赖列表" +$ umi mfsu ls +warning@4.0.3 +regenerator-runtime/runtime.js@0.13.11 +react/jsx-dev-runtime@18.1.0 +react-intl@3.12.1 +react-error-overlay/lib/index.js@6.0.9 +react@18.1.0 +qiankun@2.8.4 +lodash/noop@4.17.21 +lodash/mergeWith@4.17.21 +lodash/concat@4.17.21 +... +``` + +```bash title="重新构建 MFSU 依赖" +$ umi mfsu build +info - Preparing... +info - MFSU eager strategy enabled +warn - Invalidate webpack cache since mfsu cache is missing +info - [MFSU] buildDeps since cacheDependency has changed +... +info - [plugin: @umijs/preset-umi/dist/commands/mfsu/mfsu] [MFSU][eager] build success +``` + +```bash title="清除 MFSU 依赖" +$ # 删除依赖信息列表 +$ umi mfsu remove +$ # 删除依赖信息列表和产物文件 +$ umi mfsu remove --all +``` + +## verifyCommit + +验证 commit message 信息,通常和 [husky](https://github.com/typicode/husky) 搭配使用。 + +比如在 `.husky/commit-msg` 做如下配置, + +```bash +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install umi verify-commit $1 +``` + +## version + +查看 `umi` 版本,等同于 `umi -v`。 + +```bash +$ umi version +4.0.0 +``` + diff --git a/docs/docs/docs/api/config.en-US.md b/docs/docs/docs/api/config.en-US.md new file mode 100644 index 000000000000..5cb7d530f140 --- /dev/null +++ b/docs/docs/docs/api/config.en-US.md @@ -0,0 +1,1517 @@ +--- +order: 2 +toc: content +--- +# 配置 + +对于 umi 中能使用的自定义配置,你可以使用项目根目录的 `.umirc.ts` 文件或者 `config/config.ts`,值得注意的是这两个文件功能一致,仅仅是存在目录不同,2 选 1 ,`.umirc.ts` 文件优先级较高。 + +> 更多目录相关信息介绍,你可以在[目录结构](../guides/directory-structure)了解。 + +umi 的配置文件是一个正常的 node 模块,它在执行 umi [命令行](./commands)的时候使用,并且不包含在浏览器端构建中。 + +> 关于浏览器端构建需要用到的一些配置,还有一些在样式表现上产生作用的一些配置,在 umi 中被统一称为“运行时配置”,你可以在[运行时配置](./runtime-config)看到更多关于它的说明。 + +这里有一个最简单的 umi 配置文件的范例: + +```ts +import { defineConfig } from 'umi'; + +export default defineConfig({ + outputPath: 'dist', +}); +``` + +使用 `defineConfig` 包裹配置是为了在书写配置文件的时候,能得到更好的拼写联想支持。如果你不需要,直接 `export default {}` 也可以。 + +值得关注的是在你使用 umi 的时候,你不需要了解每一个配置的作用。你可以大致的浏览一下以下 umi 支持的所有配置,然后在你需要的时候,再回来查看如何启用和修改你需要的内容。 + +> 为方便查找,以下配置项通过字母排序。 + +## alias + +- 类型:`Record` +- 默认值:`{}` + +配置别名,对 import 语句的 source 做映射。 + +比如: + +```js +{ + alias: { + foo: '/tmp/to/foo', + } +} +``` + +然后代码里 `import 'foo'` 实际上会 `import '/tmp/to/foo'`。 + +有几个 Tip。 + +1、alias 的值最好用绝对路径,尤其是指向依赖时,记得加 `require.resolve`,比如, + +```js +// ⛔ +{ + alias: { + foo: 'foo', + } +} + +// ✅ +{ + alias: { + foo: require.resolve('foo'), + } +} +``` + +2、如果不需要子路径也被映射,记得加 `$` 后缀,比如 + +```js +// import 'foo/bar' 会被映射到 import '/tmp/to/foo/bar' +{ + alias: { + foo: '/tmp/to/foo', + } +} + +// import 'foo/bar' 还是 import 'foo/bar',不会被修改 +{ + alias: { + foo$: '/tmp/to/foo', + } +} +``` + +## autoprefixer + +- 类型:`object` +- 默认值:`{ flexbox: 'no-2009' }` + +用于解析 CSS 并使用来自 Can I Use 的值将供应商前缀添加到 CSS 规则。如自动给 CSS 添加 `-webkit-` 前缀。 + +更多配置,请查阅 [autoprefixer 的配置项](https://github.com/postcss/autoprefixer#options)。 + +## analyze + +- 类型:`object` +- 默认值:`{}` + +通过指定 [`ANALYZE`](../guides/env-variables#analyze) 环境变量分析产物构成时,analyzer 插件的具体配置项,见 [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin) + +使用 Vite 模式时,除了可以自定义 [rollup-plugin-visualizer](https://github.com/btd/rollup-plugin-visualizer) 的配置, `excludeAssets`、`generateStatsFile`、`openAnalyzer`、`reportFilename`、`reportTitle` 这些选项会自动转换适配。 + +## base + +- 类型:`string` +- 默认值:`/` + +要在非根目录下部署 umi 项目时,你可以使用 base 配置。 + +base 配置允许你为应用程序设置路由前缀。比如有路由 `/` 和 `/users`,设置 base 为 `/foo/` 后就可通过 `/foo/` 和 `/foo/users` 访问到之前的路由。 + +> 注意:base 配置必须在构建时设置,并且不能在不重新构建的情况下更改,因为该值内联在客户端包中。 + +## cacheDirectoryPath + +- 类型:`string` +- 默认值:`node_modules/.cache` + +默认情况下 Umi 会将构建中的一些缓存文件存放在 `node_modules/.cache` 目录下,比如 logger 日志,webpack 缓存,mfsu 缓存等。你可以通过使用 `cacheDirectoryPath` 配置来修改 Umi 的缓存文件目录。 + +示例, + +```js +// 更改缓存文件路径到 node_modules/.cache1 文件夹 +cacheDirectoryPath: 'node_modules/.cache1', +``` + +## chainWebpack + +- 类型:`(memo, args) => void` +- 默认值:`null` + +为了扩展 Umi 内置的 webpack 配置,我们提供了用链式编程的方式修改 webpack 配置,基于 webpack-chain,具体 API 可参考 [webpack-api 的文档](https://github.com/mozilla-neutrino/webpack-chain)。 + +如下所示: + +```js +export default { + chainWebpack(memo, args) { + return memo; + }, +}; +``` + +该函数具有两个参数: + +- `memo` 是现有 webpack 配置 +- `args` 包含一些额外信息和辅助对象,目前有 `env` 和 `webpack`。`env` 为当前环境,值为 `development` 或 `production`;`webpack` 为 webpack 对象,可从中获取 webpack 内置插件等 + +用法示例: + +```js +export default { + chainWebpack(memo, { env, webpack }) { + // 设置 alias + memo.resolve.alias.set('foo', '/tmp/to/foo'); + + // 添加额外插件 + memo.plugin('hello').use(Plugin, [...args]); + + // 删除 Umi 内置插件 + memo.plugins.delete('hmr'); + }, +}; +``` + +## clickToComponent + +- 类型: `{ editor?: string }` +- 默认值: `false` + +> 当前仅 React 项目支持。 + +开启后,可通过 `Option+Click/Alt+Click` 点击组件跳转至编辑器源码位置,`Option+Right-click/Alt+Right-click` 可以打开上下文,查看父组件。 + +关于参数。`editor` 为编辑器名称,默认为 'vscode',支持 `vscode` & `vscode-insiders`。 + +配置 clickToComponent 的行为,详见 [click-to-component](https://github.com/ericclemmons/click-to-component)。 + +示例: + +```ts +// .umirc.ts +export default { + clickToComponent: {}, +}; +``` + +## clientLoader + +- 类型: `{}` +- 默认值: `false` + +开启后,可以为每个路由声明一个数据加载函数 `clientLoader`,将页面需要的加载数据程序提取到 `clientLoader` 可以让 Umi +提前在页面组件尚未加载好的时候提前进行数据的加载,避免瀑布流请求的问题,详细介绍请看 [路由数据预加载](../guides/client-loader)。 + +示例: + +```ts +// .umirc.ts +export default { + clientLoader: {}, +}; +``` + +配置开启后,在路由组件中使用: + +```jsx +// pages/.../some_page.tsx + +import { useClientLoaderData } from 'umi'; + +export default function SomePage() { + const { data } = useClientLoaderData(); + return
{data}
; +} + +export async function clientLoader() { + const data = await fetch('/api/data'); + return data; +} +``` + +## codeSplitting + +- 类型:`{ jsStrategy: 'bigVendors' | 'depPerChunk' | 'granularChunks'; jsStrategyOptions: {} }` +- 默认值:`null` + +提供 code splitting 的策略方案。 + +bigVendors 是大 vendors 方案,会将 async chunk 里的 node_modules 下的文件打包到一起,可以避免重复。同时缺点是,1)单文件的尺寸过大,2)毫无缓存效率可言。 + +depPerChunk 和 bigVendors 类似,不同的是把依赖按 package name + version 进行拆分,算是解了 bigVendors 的尺寸和缓存效率问题。但同时带来的潜在问题是,可能导致请求较多。我的理解是,对于非大型项目来说其实还好,因为,1)单个页面的请求不会包含非常多的依赖,2)基于 HTTP/2,几十个请求不算问题。但是,对于大型项目或巨型项目来说,需要考虑更合适的方案。 + +granularChunks 在 bigVendors 和 depPerChunk 之间取了中间值,同时又能在缓存效率上有更好的利用。无特殊场景,建议用 granularChunks 策略。 + +## conventionLayout + +- 类型:`boolean` +- 默认值:`undefined` + +`src/layouts/index.[tsx|vue|jsx|js]` 为约定式布局,默认开启。可通过配置 `conventionLayout: false` 关闭该默认行为。 + +## conventionRoutes + +- 类型:`{ base: string; exclude: RegExp[] }` +- 默认值:`null` + +修改默认的约定式路由规则,仅在使用 umi 约定式路由时有效,约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。 + +使用约定式路由时,约定 `src/pages` 下所有的 `(j|t)sx?` 文件即路由。 + +> 你可以从[约定式路由](../guides/routes#约定式路由)查看更多说明。 + +### base + +`base` 用于设置约定的路由的基础路径,默认从 `src/pages` 读取,如果是文档站点可能会需要将其改成 `./docs`; + +### exclude + +你可以使用 `exclude` 配置过滤一些不需要的文件,比如用于过滤 components、models 等。 + +示例, + +```js +// 不识别 components 和 models 目录下的文件为路由 +conventionRoutes: { + exclude: [/\/components\//, /\/models\//], +} +``` + +## copy + +- 类型:`Array` +- 默认值:`[]` + +配置要复制到输出目录的文件或文件夹。 + +当配置字符串时,默认拷贝到产物目录,如: + +```ts +copy: ['foo.json', 'src/bar.json'] +``` + +会产生如下产物的结构: + +``` ++ dist + - bar.json + - foo.json ++ src + - bar.json +- foo.json +``` + +你也可以通过对象配置具体的拷贝位置,其中相对路径的起点为项目根目录: + +```ts +copy: [ + { from: 'from', to: 'dist/output' }, + { from: 'file.json', to: 'dist' } +] +``` + +此时将产生如下产物结构: + +``` ++ dist + + output + - foo.json + - file.json ++ from + - foo.json +- file.json +``` + +## crossorigin + +- 类型:`{ includes?: string[] }` +- 默认值:`false` + +配置 script 标签的 crossorigin。如果有声明,会为本地 script 加上 crossorigin="anonymous" 的属性。 + +关于参数。`includes` 参数可以为额外的非本地 script 标签加上此属性。 + +比如: + +``` +crossorigin: {} +``` + +然后输出的 HTML 中会有这些变化, + +```diff +- + ++ + +``` + +## cssMinifier + +- 类型:`string` 可选的值:`esbuild`, `cssnano`, `parcelCSS`, `none` +- 默认值:`esbuild` + +配置构建时使用的 CSS 压缩工具; `none` 表示不压缩。 + +示例: + +```js +{ + cssMinifier: 'esbuild' +} +``` + +## cssMinifierOptions + +- 类型:`object` +- 默认值:`{}` + +`cssMinifier` CSS 压缩工具配置选项。 + +示例: + +```js +{ + cssMinifier: 'esbuild', + cssMinifierOptions: { + minifyWhitespace: true, + minifySyntax: true, + }, +} +``` + +对应 CSS 压缩的配置请查看对应的文档。 + +- [esbuild 参考](https://esbuild.github.io/api/#minify) +- [cssnano 参考](https://cssnano.co/docs/config-file/) +- [parcelCSS 参考](https://github.com/parcel-bundler/parcel-css/blob/master/node/index.d.ts) + +## cssPublicPath +- 类型:`string` +- 默认值:`./` + +为 CSS 中的图片、文件等外部资源指定自定义公共路径。作用类似于 `publicPath` 默认值是 `./`。 + +## cssLoader + +- 类型:`object` +- 默认值:`{}` + +配置 css-loader ,详见 [css-loader > options](https://github.com/webpack-contrib/css-loader#options) + +## cssLoaderModules + +- 类型:`object` +- 默认值:`{}` + +配置 css modules 的行为,详见 [css-loader > modules](https://github.com/webpack-contrib/css-loader#modules)。 + +如: + +```ts +cssLoaderModules: { + // 配置驼峰式使用 + exportLocalsConvention: 'camelCase' +} +``` + +## deadCode + +- 类型:`{ patterns?: string[]; exclude?: string[]; failOnHint?: boolean; detectUnusedFiles?: boolean; detectUnusedExport?: boolean; context?: string }` +- 默认值:`false` + +检测未使用的文件和导出,仅在 build 阶段开启。 + +比如: + +``` +deadCode: {} +``` + +然后执行 build,如有发现问题,会打印警告: + +``` +Warning: There are 1 unused files: + 1. /pages/index.module.less + Please be careful if you want to remove them (¬º-°)¬. +``` + +可配置项: + + - `patterns` : 识别代码的范围,如 `['src/pages/**']` + - `exclude` : 排除检测的范围,如 `['src/pages/utils/**']` + - `failOnHint` : 检测失败是否终止进程,默认 `false` 不终止 + - `detectUnusedFiles` : 是否检测未使用的文件,默认 `true` 检测 + - `detectUnusedExport` : 是否检测未使用的导出,默认 `true` 检测 + - `context` : 匹配开始的目录,默认为当前项目根目录 + +## define + +- 类型:`Record` +- 默认值: 如下 + +``` + { + 'process.env.NODE_ENV' : process.env.NODE_ENV, + 'process.env.HMR' : process.env.HMR, + 'process.env.SOCKET_SERVER': process.env.ERROR_OVERLAY' + } +``` + +基于[define-plugin 插件](https://webpack.js.org/plugins/define-plugin/)设置代码中的可用变量。 + +:::warning{title=🚨} +1. 属性值会经过一次 `JSON.stringify` 转换。 +2. key 值的替换是通过语法形式来匹配的,比如配置了 `{'a.b.c': 'abcValue'}` 是无法替换代码中的 `a.b?.c` 的 +::: + +比如, + +``` +define: { FOO: 'bar' } +``` + +然后代码里的 `console.log(hello, FOO)` 会被编译成 `console.log(hello, 'bar')`。 + +当你在 ts 的项目中使用这些变量时,你需要在 typings 文件中声明变量类型,以支持 ts 类型提示,比如: + +如果你的 typings 文件是全局的: + +```ts +// typings.d.ts +declare const FOO: string; +``` + +如果你的 typings 文件是非全局的(包含了 import/export): + +```ts +// typings.d.ts +import './other.d.ts'; + +declare global { + const FOO: string; +} +``` +## devtool + +- 类型:`string` +- 默认值:dev 时默认 `cheap-module-source-map`,build 时候默认无 sourcemap + +设置 sourcemap 生成方式。 + +常见可选值有: + +- `eval`,最快的类型,缺点是不支持低版本浏览器 +- `source-map`,最慢但最全的类型 + +示例, + +```js +// 关闭 dev 阶段的 sourcemap 生成 +devtool: false; + +// 只设置 dev 阶段的 sourcemap +devtool: process.env.NODE_ENV === 'development' ? 'eval' : false; +``` + +## classPropertiesLoose +- 类型:`object` +- 默认值:`{}` + +设置 babel class-properties 启用 loose + +## esbuildMinifyIIFE + +- 类型:`boolean` +- 默认值:`false` + +修复 esbuild 压缩器自动引入的全局变量导致的命名冲突问题。 + +由于 Umi 4 默认使用 esbuild 作为压缩器,该压缩器会自动注入全局变量作为 polyfill ,这可能会引发 异步块全局变量冲突、 qiankun 子应用和主应用全局变量冲突 等问题,通过打开该选项或切换 [`jsMinifier`](#jsminifier-webpack) 压缩器可解决此问题。 + +更多信息详见 [vite#7948](https://github.com/vitejs/vite/pull/7948) 。 + +示例, +```ts +esbuildMinifyIIFE: true +``` + +## externals + +- 类型:`Record | Function` +- 默认值:`{}` + +设置哪些模块不打包,转而通过 ` + +``` + +如果需要额外属性,切换到对象格式,比如, + +```js +headScripts: [ + { src: '/foo.js', defer: true }, + { content: `alert('你好');`, charset: 'utf-8' }, +], +``` + +## helmet + +- 类型:`boolean` +- 默认值:`true` + +配置 `react-helmet-async` 的集成,当设置为 `false` 时,不会集成 `react-helmet-async`,此时无法从框架中 `import { Helmet }` 使用,同时构建产物也会减少[相应的尺寸](https://bundlephobia.com/package/react-helmet-async)。 + +## history + +- 类型:`{ type: 'browser' | 'hash' | 'memory' }` +- 默认值:`{ type: 'browser' }` + +设置路由 history 类型。 + +## historyWithQuery + +- 类型:`‌{}` +- 默认值:`false` + +让 history 带上 query。除了通过 `useNavigate` 进行的跳转场景,此时还需自行处理 query。 + +## https + +- 类型:`{ cert: string; key: string; hosts: string[]; http2?: boolean }` +- 默认值:`{ hosts: ['127.0.0.1', 'localhost'] }` + +开启 dev 的 https 模式,Umi 4 默认使用 [`mkcert`](https://github.com/FiloSottile/mkcert) 快捷创建证书,请确保已经安装。 + +关于参数。 + +- `cert` 和 `key` 分别用于指定 cert 和 key 文件。 +- `hosts` 用于指定要支持 https 访问的 host,默认是 `['127.0.0.1', 'localhost']`。 +- `http2` 用于指定是否使用 HTTP 2.0 协议,默认是 true(使用 HTTP 2.0 在 Chrome 或 Edge 浏览器中中有偶然出现 `ERR_HTTP2_PROTOCOL_ERRO`报错,如有遇到,建议配置为 false)。 + +示例, + +```js +https: { +} +``` + +## icons + +- 类型:`{ autoInstall: {}; alias: Record; include: Array; }` +- 默认值:`false` + +你就可以通过 umi 导出的 Icon 组件快捷地引用 icon 集或者本地的 icon。 + +### icon 集使用 + +在 umi 配置文件设置,开启 icons 功能,并允许自动安装图标库。 + +```ts +icons: { autoInstall: {} }, +``` + +页面使用: + +```ts +import { Icon } from 'umi'; + +``` + +icon 里包含的字符串是 `collect:icon` 的组合,以 `:` 分割。Icon 集推荐在 [Icônes 网站](https://icones.js.org/)上搜索。 + +## 本地 icon 使用 + +在 umi 配置文件设置,开启 icons 功能。 + +```ts +icons: {}, +``` + +本地 svg icon 的使用需要把 svg 保存在 `src/icons` 目录下,然后通过 `local` 这个前缀引用,比如在 `src/icons` 目录下有个 `umi.svg`,然后可以这样引用。 + +```tsx +import { Icon } from 'umi'; + +``` + +### 配置项介绍 + +- `autoInstall` 表示是否自动安装 icon 集;tnpm/cnpm 客户端暂不支持,但可以通过手动按需安装对应 icon 集合包 `@iconify-json/collection-name` 。 参考:[Icon 集合列表](https://github.com/iconify/icon-sets/blob/master/collections.md), collection-name 为列表中的 ***Icon set prefix*** 项。 +- `alias` 用于配置 icon 的别名,比如配置了 `alias:{home:'fa:home'}` 后就可以通过 `icon="home"` 使用 `fa:home` 这个 icon 了。 +- `include` 配置需要强制使用的 icon, 例如 `include: ['fa:home', 'local:icon']`。常见的使用场景:将 icon 字符串定义在一个 map 中,导致无法检测到;在 `mdx` 使用了 `Icon` 组件。 + +### Icon 组件属性 + +- icon,指定 icon +- width,svg 宽度 +- height,svg 高度 +- viewBox,svg viewBox +- style,外部容器样式 +- className,外部容器样式名 +- spin,是否自动旋转 +- rotate,配置旋转角度,支持多种格式,比如 `1`,`"30deg"`、`"25%"` 都可以 +- flip,支持 `vertical`、`horizontal`,或者他们的组合 `vertical,horizontal` + +## ignoreMomentLocale + +- 类型:`boolean` +- 默认值:`true` + +忽略 moment 的 locale 文件,用于减少产物尺寸。 + +注意:此功能默认开。配置 `ignoreMomentLocale: false` 关闭。 + +## inlineLimit + +- 类型:`number` +- 默认值:`10000` (10k) + +配置图片文件是否走 base64 编译的阈值。默认是 10000 字节,少于他会被编译为 base64 编码,否则会生成单独的文件。 + +## jsMinifier (webpack) + +- 类型:`string`,可选值 `esbuild`, `terser`, `swc`, `uglifyJs`, `none` +- 默认值:`esbuild` + +配置构建时压缩 JavaScript 的工具;`none`表示不压缩。 + +示例: + +```ts +{ + jsMinifier: 'esbuild' +} +``` + +## jsMinifierOptions + +- 类型:`object` +- 默认值:`{}` + +`jsMinifier` 的配置项;默认情况下压缩代码会移除代码中的注释,可以通过对应的 `jsMinifier` 选项来保留注释。 + +示例: +```js +{ + jsMinifier: 'esbuild', + jsMinifierOptions: { + minifyWhitespace: true, + minifyIdentifiers: true, + minifySyntax: true, + } +} +``` + +配置项需要和所使用的工具对应,具体参考对应文档: + +- [esbuild 参考](https://esbuild.github.io/api/#minify) +- [terser 参考](https://terser.org/docs/api-reference#minify-options) +- [swc 参考](https://swc.rs/docs/configuration/minification#configuration) +- [uglifyJs 参考](https://lisperator.net/uglifyjs/compress) + +## lessLoader + +- 类型:`object` +- 默认值:`{ modifyVars: userConfig.theme, javascriptEnabled: true }` + +设置 less-loader 的 Options。具体参考参考 [less-loader 的 Options](https://github.com/webpack-contrib/less-loader#lessoptions)。 + +> 默认是用 less@4 版本,如果需要兼容 less@3 请配置使用[less-options-math](https://lesscss.org/usage/#less-options-math)。 + +## legacy + +- 类型:`{ buildOnly?: boolean; nodeModulesTransform?: boolean; checkOutput?: boolean; }` +- 默认值:`false` + +当你需要兼容低版本浏览器时,可能需要该选项,开启后将默认使用 **非现代** 的打包工具做构建,这会显著增加你的构建时间。 + +```ts +legacy: {} +``` + +默认只在构建时生效,通过设定 `buildOnly: false` 关闭该限制。 + +可通过打开 `checkOutput: true` 选项,每次构建结束后将自动运行 [`es-check`](https://github.com/yowainwright/es-check) 检查产物 `.js` 文件的语法是否为 es5 格式。 + +开启此选项后: + + - 不支持自定义 `srcTranspiler` 、`jsMinifier` 、 `cssMinifier` 选项。 + - 将转译全部 `node_modules` 内的源码,`targets` 兼容至 ie 11 ,通过指定 `nodeModulesTransform: false` 来取消对 `node_modules` 的转换,此时你可以通过配置 `extraBabelIncludes` 更精准的转换那些有兼容性问题的包。 + - 因低版本浏览器不支持 Top level await ,当你在使用 `externals` 时,确保你没有在使用异步性质的 [`externalsType`](https://webpack.js.org/configuration/externals/#externalstype) 时又使用了同步导入依赖。 + +## links + +- 类型:`Link[]` +- 默认值:`[]` + +配置额外的 link 标签。 + +示例, + +```js +links: [{ href: '/foo.css', rel: 'preload' }], +``` + +## manifest + +- 类型:`{ fileName: string; basePath: string }` +- 默认值:`null` + +开启 build 时生成额外的 manifest 文件,用于描述产物。 + +关于参数。`fileName` 是生成的文件名,默认是 `asset-manifest.json`;`basePath` 会给所有文件路径加上前缀。 + +注意:只在 build 时生成。 + +## mdx + +- 类型:`{ loader: string; loaderOptions: Object }` +- 默认值:`{}` + +mdx loader 配置 loader 配置路径,[loaderOptions](https://github.com/mdx-js/mdx/blob/v1/packages/mdx/index.js#L12) 配置参数 + +## metas + +- 类型:`Meta[]` +- 默认值:`[]` + +配置额外的 meta 标签。 + +比如, + +```js +metas: [ + { name: 'keywords', content: 'umi, umijs' }, + { name: 'description', content: 'React framework.' }, +], +``` + +会生成以下 HTML, + +```html + + +``` + +## mfsu + +- 类型:`{ esbuild: boolean; mfName: string; cacheDirectory: string; strategy: 'normal' | 'eager'; include?: string[]; chainWebpack: (memo, args) => void; exclude?: Array }` +- 默认值:`{ mfName: 'mf', strategy: 'normal' }` + +配置基于 [Module Federation](https://module-federation.github.io/) 的提速功能。 + +关于参数 + +- `esbuild` 配为 `true` 后会让依赖的预编译走 esbuild,从而让首次启动更快,缺点是二次编译不会有物理缓存,稍慢一些;推荐项目依赖比较稳定的项目使用。 +- `mfName` 是此方案的 remote 库的全局变量,默认是 mf,通常在微前端中为了让主应用和子应用不冲突才会进行配置 +- `cacheDirectory` 可以自定义缓存目录,默认是 `node_modules/.cache/mfsu` +- `chainWebpack` 用链式编程的方式修改 依赖的 webpack 配置,基于 webpack-chain,具体 API 可参考 [webpack-api 的文档](https://github.com/sorrycc/webpack-chain); +- `runtimePublicPath` 会让修改 mf 加载文件的 publicPath 为 `window.publicPath` +- `strategy` 指定 mfsu 编译依赖的时机; `normal` 模式下,采用 babel 编译分析后,构建 Module Federation 远端包;`eager` 模式下采用静态分析的方式,和项目代码同时发起构建。 +- `include` 仅在 `strategy: 'eager' ` 模式下生效, 用于补偿在 eager 模式下,静态分析无法分析到的依赖,例如 `react` 未进入 Module Federation 远端模块可以这样配置 `{ include: [ 'react' ] }` +- `exclude` 手动排除某些不需要被 MFSU 处理的依赖, 字符串或者正则的形式,比如 `vant` 不希望走 MFSU 处理,可以配置 `{ exclude: [ 'vant' ] }` 匹配逻辑为全词匹配,也可以配置 `{ exclude: [ /vant/ ] }` 只要 `import` 路径中匹配该正则的依赖都不走 MFSU 处理 + +示例, + +```js +// 用 esbuild 做依赖预编译 +mfsu: { + esbuild: true, +} + +// 关闭 mfsu 功能 +mfsu: false; +``` + +```js +// webpack 配置修改 +mfsu: { + chainWebpack(memo, args) { + // 添加额外插件 + memo.plugin('hello').use(Plugin, [...args]); + return memo; + } +} +``` + +注意:此功能默认开。配置 `mfsu: false` 关闭。 + +## mock + +- 类型:`{ exclude: string[], include: string[] }` +- 默认值:`{}` + +配置 mock 功能。 + +关于参数。`exclude` 用于排除不需要的 mock 文件;`include` 用于额外添加 mock 目录之外的 mock 文件。 + +示例, + +```js +// 让所有 pages 下的 _mock.ts 文件成为 mock 文件 +mock: { + include: ['src/pages/**/_mock.ts'], +} +``` + +注意:此功能默认开。配置 `mock: false` 关闭。 + +## mountElementId + +- 类型:`string` +- 默认值:`'root'` + +配置 react 组件树渲染到 HTML 中的元素 id。 + +示例, + +```js +mountElementId: 'container' +``` + +## monorepoRedirect + +- 类型:`{ srcDir?: string[], exclude?: RegExp[], peerDeps?: boolean, useRootProject?: boolean }` +- 默认值:`false` + +在 monorepo 中使用 Umi 时,你可能需要引入其他子包的组件、工具方法等,通过开启此选项来重定向这些子包的导入到他们的源码位置(默认为 `src` 文件夹),这也可以解决 `MFSU` 场景改动子包不热更新的问题。 + +这种重定向的好处是:支持热更新,无需预构建其他子包即可进行开发。 + +通过配置 `srcDir` 来调整识别源码文件夹的优先位置,通过 `exclude` 来设定不需要重定向的依赖范围。 + +示例: + +```js +// 默认重定向到子包的 src 文件夹 +monorepoRedirect: {} +// 在子包中寻找,优先定向到 libs 文件夹 +monorepoRedirect: { + srcDir: ['libs', 'src'], +} +// 不重定向 @scope/* 的子包 +monorepoRedirect: { + exclude: [/^@scope\/.+/], +} +``` + +在实际的大型业务 monorepo 中,每个子包的依赖都是从他们的目录开始向上寻找 `node_modules` 并加载的,但在本地开发时,依赖都安装在 `devDependencies` ,和从 npm 上安装表现不一致,所以不可避免会遇到多实例问题。 + +:::info +举个例子,每个子包在本地开发时都需要 `antd` ,在 `devDependencies` 中安装了,也在 `peerDependencies` 中指明了 `antd` ,我们预期该包发布到 npm ,被某个项目安装后, `antd` 是使用的项目本身的依赖,全局唯一,但由于在 monorepo 中,指定在 `devDependencies` 中的依赖必定存在,且子包代码寻找依赖时是从该子包进行的,导致了每个子包都用了自己的 `antd` ,出现了产物中有多份 `antd` 、产物体积增大、消息队列被破坏等情况。 +::: + +为了解决这种问题,我们约定: + +当打开 `peerDeps` 选项时,所有子包指明的 `peerDependencies` 都会被自动添加 `alias` 重定向唯一化,避免多实例的存在: + +```ts +monorepoRedirect: { peerDeps: true } +``` + +经过重定向,依赖全局唯一,便可以在开发时保持和在 npm 上安装包后的体验一致。 + +useRootProject: 当你的项目不在 monorepo 子文件夹里,而在 monorepo 根的话,你可以开启这个选项,以使 monorepoRedirect 生效。 + +## mpa + +- 类型:`object` +- 默认值:`false` + +启用 [mpa 模式](../guides/mpa)。 + +## outputPath + +- 类型:`string` +- 默认值:`dist` + +配置输出路径。 + +注意:不允许设定为 src、public、pages、mock、config、locales、models 等约定式功能相关的目录。 + +## phantomDependency + +- 类型:`{ exclude: string[] }` +- 默认值:`false` + +执行幽灵依赖检测。 + +当使用未在 package.json 中声明的依赖,以及也没有通过 alias 或 externals 进行配置时,会抛错并提醒。 + +![](https://mdn.alipayobjects.com/huamei_ddtbzw/afts/img/A*k5uoQ5TOPooAAAAAAAAAAAAADkCKAQ/original) + +如遇到有需要需做白名单处理,可通过 exclude 配置项实现,exclude 的项是 npm 依赖的包名。 + +```ts +export default { + phantomDependency: { + exclude: ['lodash'] + } +} +``` + +## plugins + +- 类型:`string[]` +- 默认值:`[]` + +配置额外的 Umi 插件。 + +数组项为指向插件的路径,可以是 npm 依赖、相对路径或绝对路径。如果是相对路径,则会从项目根目录开始找。 + +示例, + +```js +plugins: [ + // npm 依赖 + 'umi-plugin-hello', + // 相对路径 + './plugin', + // 绝对路径 + `${__dirname}/plugin.js`, +], +``` + +## polyfill + +- 类型:`{ imports: string[] }` +- 默认值:`{}` + +设置按需引入的 polyfill。默认全量引入。 + +比如只引入 core-js 的 stable 部分, + +```js +polyfill: { + imports: ['core-js/stable'], +} +``` + +如果对于性能有更极致的要求,可以考虑按需引入, + +```js +polyfill: { + imports: ['core-js/features/promise/try', 'core-js/proposals/math-extensions'], +} +``` + +注意:此功能默认开。配置 `polyfill: false` 或设置环境变量 `BABEL_POLYFILL=none` 关闭。 + +## postcssLoader + +- 类型:`object` +- 默认值:`{}` + +设置 [postcss-loader 的配置项](https://github.com/webpack-contrib/postcss-loader#options)。 + +## presets + +- 类型:`string[]` +- 默认值:`[]` + +配置额外的 Umi 插件集。 + +数组项为指向插件集的路径,可以是 npm 依赖、相对路径或绝对路径。如果是相对路径,则会从项目根目录开始找。 + +示例, + +```js +presets: [ + // npm 依赖 + 'umi-preset-hello', + // 相对路径 + './preset', + // 绝对路径 + `${__dirname}/preset.js`, +], +``` + +## proxy + +- 类型:`object` +- 默认值:`{}` + +配置代理功能。 + +比如, + +```js +proxy: { + '/api': { + 'target': 'http://jsonplaceholder.typicode.com/', + 'changeOrigin': true, + 'pathRewrite': { '^/api' : '' }, + } +} +``` + +然后访问 `/api/users` 就能访问到 http://jsonplaceholder.typicode.com/users 的数据。 + +注意:proxy 功能仅在 dev 时有效。 + +## publicPath + +- 类型:`string` +- 默认值:`/` + +配置 webpack 的 publicPath。 + +## reactRouter5Compat + +- 类型:`object` +- 默认值:`false` + +启用 react-router 5 兼容模式。此模式下,路由组件的 props 会包含 location、match、history 和 params 属性,和 react-router 5 的保持一致。 + +但要注意的是, + +1. 此模式下会有额外的 re-render +2. 由于依赖库 history 更新,location 中依旧没有 query 属性 + +## routes + +- 类型:`Route[]` +- 默认值:`[]` + +配置路由。更多信息,请查看 [配置路由](../guides/routes#配置路由) + +## routeLoader + +- 类型:`{ moduleType: 'esm' | 'cjs' }` +- 默认值:`{ moduleType: 'esm' }` + +配置路由加载方式。moduleType 配置为 'cjs' 会用 `require` 的方式加载路由组件。 + +```ts +// moduleType: esm +'index': React.lazy(() => import(/* webpackChunkName: "p__index" */'../../pages/index.tsx')), + +// moduleType: cjs +'index': React.lazy(() => Promise.resolve(require('../../pages/index.tsx'))), +``` + +## run + +- 类型:`{ globals: string[] }` +- 默认值:`null` + +run 命令的全局注入配置。添加`['zx/globals']`,在使用`umi run ./script.ts`的时候,umi会自动注入`import 'zx/globals';`,从而省略掉每个脚本都要写`import 'zx/globals';`。 + +## runtimePublicPath + +- 类型:`object` +- 默认值:`null` + +启用运行时 publicPath,开启后会使用 `window.publicPath` 作为资源动态加载的起始路径。 + +比如, + +```js +runtimePublicPath: {}, +``` + +## scripts + +- 类型:`string[] | Script[]` +- 默认值:`[]` + +配置 `` 中额外的 script 标签。 + +比如, + +```js +scripts: [`alert(1);`, `https://a.com/b.js`], +``` + +会生成 HTML, + +```html + + +``` + +如果需要额外属性,切换到对象格式,比如, + +```js +scripts: [ + { src: '/foo.js', defer: true }, + { content: `alert('你好');`, charset: 'utf-8' }, +], +``` + +## sassLoader + +- 类型:`object` +- 默认值:`{}` + +配置 sass-loader ,详见 [sass-loader > options](https://github.com/webpack-contrib/sass-loader#options) + +## styleLoader + +- 类型:`object` +- 默认值:`false` + +启用 style loader 功能,让 CSS 内联在 JS 中,不输出额外的 CSS 文件。 + +## stylusLoader +- 类型:`object` +- 默认值:`{}` + +配置 stylus-loader ,详见 [stylus-loader > options](https://github.com/webpack-contrib/stylus-loader#options) + +## styles + +- 类型:`string[]` +- 默认值:`[]` + +配置额外的 CSS。 + +配置项支持内联样式和外联样式路径,后者通过是否以 https?:// 开头来判断。 + +插入的样式会前置,优先级低于项目内用户编写样式。 + +比如: + +```js +styles: [`body { color: red; }`, `https://a.com/b.css`], +``` + +会生成以下 HTML, + +```html + + +``` + +## srcTranspiler + +- 类型:`string` 可选的值:`babel`, `swc`, `esbuild` +- 默认值:`babel` + +配置构建时转译 js/ts 的工具。 + +## srcTranspilerOptions + +- 类型:`{ swc?: SwcConfig, esbuild?: EsbuildConfig }` +- 默认值:`undefined` + +如果你使用了 `swc` / `esbuild` 作为 `srcTranspiler` 转译器,你可以通过此选项对转译器做进一步的配置,详见 [SwcConfig](https://swc.rs/docs/configuration/swcrc) 、 [EsbuildConfig](https://esbuild.github.io/api/#transform-api) 配置文档。 + +如给 swc 添加其他的插件: + +```ts +srcTranspilerOptions: { + swc: { + jsc: { + experimental: { + plugins: [ + [ + '@swc/plugin-styled-components', + { + displayName: true, + ssr: true, + }, + ], + ], + }, + }, + }, +} +``` + +## svgr + +- 类型:`object` +- 默认值:`{}` + +svgr 默认开启,支持如下方式使用 React svg 组件: + +```ts +import SmileUrl, { ReactComponent as SvgSmile } from './smile.svg'; +``` + +可配置 svgr 的行为,配置项详见 [@svgr/core > Config](https://github.com/gregberge/svgr/blob/main/packages/core/src/config.ts#L9)。 + +## svgo + +- 类型:`object` +- 默认值:`{}` + +默认使用 svgo 来优化 svg 资源,配置项详见 [svgo](https://github.com/svg/svgo#configuration) 。 + +## targets + +- 类型:`object` +- 默认值:`{ chrome: 80 }` + +配置需要兼容的浏览器最低版本。Umi 会根据这个自定引入 polyfill、配置 autoprefixer 和做语法转换等。 + +示例, + +```js +// 兼容 ie11 +targets: { + ie: 11, +} +``` + +## theme + +- 类型:`object` +- 默认值:`{}` + +配置 less 变量主题。 + +示例: + +```js +theme: { '@primary-color': '#1DA57A' } +``` + +## title + +- 类型:`string` +- 默认值:`null` + +配置全局页面 title,暂时只支持静态的 Title。 + +## verifyCommit + +- 类型:`{ scope: string[]; allowEmoji: boolean }` +- 默认值:`{ scope: ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'workflow', 'build', 'ci', 'chore', 'types', 'wip', 'release', 'dep', 'deps', 'example', 'examples', 'merge', 'revert'] }` + +针对 verify-commit 命令的配置项。 + +关于参数。`scope` 用于配置允许的 scope,不区分大小写,配置后会覆盖默认的;`allowEmoji` 开启后会允许加 EMOJI 前缀,比如 `💥 feat(模块): 添加了个很棒的功能`。 + +``` +verifyCommit: { + scope: ['feat', 'fix'], + allowEmoji: true, +} +``` + +注意:使用 `git revert` 或 `git merge` 命令以及 `changesets` 的发版 merge 格式所产生的 commit message 会默认通过校验。 + +## vite + +- 类型:`object` +- 默认值:`{}` + +开发者的配置会 merge 到 vite 的 [默认配置](https://vitejs.dev/config/)。 + +示例, + +```js +// 更改临时文件路径到 node_modules/.bin/.vite 文件夹 +vite: { + cacheDir: 'node_modules/.bin/.vite', +} +``` + +## writeToDisk + +- 类型:`boolean` +- 默认值:`false` + +开启后会在 dev 模式下额外输出一份文件到 dist 目录,通常用于 chrome 插件、electron 应用、sketch 插件等开发场景。 + diff --git a/docs/docs/docs/api/plugin-api.en-US.md b/docs/docs/docs/api/plugin-api.en-US.md new file mode 100644 index 000000000000..50607538a795 --- /dev/null +++ b/docs/docs/docs/api/plugin-api.en-US.md @@ -0,0 +1,752 @@ +--- +order: 5 +toc: content +--- +# 插件 API + +Umi 的核心就在于它的插件机制。基于 Umi 的插件机制,你可以获得扩展项目的编译时和运行时的能力。以下罗列出我们为你提供的所有的插件API,以帮助你能自由编写插件。 + +在查用 Umi 插件 API 之前,我们建议你先阅读[插件](../guides/plugins)一节,以了解 umi 插件的机制及原理,这将帮助你更好的使用插件 API。 + +> 为方便查找,以下内容通过字母排序。 + +## 核心 API +service 和 PluginAPI 里定义的方法。 + +### applyPlugins +```ts +api.applyPlugins({ key: string, type?: api.ApplyPluginsType, initialValue?: any, args?: any }) +``` +取得 `register()` 注册的 hooks 执行后的数据,这是一个异步函数,因此它返回的将是一个 Promise。这个方法的例子和详解见 [register](#register) api + +### describe +```ts +api.describe({ key?:string, config?: { default , schema, onChange }, enableBy? }) +``` +在插件注册阶段( initPresets or initPlugins stage )执行,用于描述插件或者插件集的 key、配置信息和启用方式等。 + +- `key` 是配置中该插件配置的键名 +- `config.default` 是插件配置的默认值,当用户没有在配置中配置 key 时,默认配置将生效。 +- `config.schema` 用于声明配置的类型,基于 [joi](https://joi.dev/) 。 **如果你希望用户进行配置,这个是必须的** ,否则用户的配置无效 +- `config.onChange` 是 dev 模式下,配置被修改后的处理机制。默认值为 `api.ConfigChangeType.reload`,表示在 dev 模式下,配置项被修改时会重启 dev 进程。 你也可以修改为 `api.ConfigChangeType.regenerateTmpFiles`, 表示只重新生成临时文件。你还可以传入一个方法,来自定义处理机制。 +- `enableBy` 是插件的启用方式,默认是`api.EnableBy.register`,表示注册启用,即插件只要被注册就会被启用。可以更改为 `api.EnableBy.config` ,表示配置启用,只有配置插件的配置项才启用插件。你还可以自定义一个返回布尔值的方法( true 为启用 )来决定其启用时机,这通常用来实现动态生效。 + +e.g. +```ts +api.describe({ + key: 'foo', + config: { + schema(joi){ + return joi.string(); + }, + onChange: api.ConfigChangeType.regenerateTmpFiles, + }, + enableBy: api.EnableBy.config, +}) +``` +这个例子中,插件的 `key` 为 `foo`,因此配置中的键名为 `foo`,配置的类型是字符串,当配置 `foo` 发生变化时,dev 只会重新生成临时文件。该插件只有在用户配置了 `foo` 之后才会启用。 + +### isPluginEnable +```ts +api.isPluginEnable( key:string) +``` +判断插件是否启用,传入的参数是插件的 key + +### register +```ts +api.register({ key: string, fn, before?: string, stage?: number}) +``` +为 `api.applyPlugins` 注册可供其使用的 hook。 + +- `key` 是注册的 hook 的类别名称,可以多次使用 `register` 向同一个 `key` 注册 hook,它们将会依次执行。这个 `key` 也同样是使用 `applyPlugins` 收集 hooks 数据时使用的 `key`。注意: **这里的 key 和 插件的 key 没有任何联系。** +- `fn` 是 hook 的定义,可以是同步的,也可以是异步的(返回一个 Promise 即可) +- `stage` 用于调整执行顺序,默认为 0,设为 -1 或更少会提前执行,设为 1 或更多会后置执行。 +- `before` 同样用于调整执行的顺序,传入的值为注册的 hook 的名称。注意:**`register` 注册的 hook 的名称是所在 Umi 插件的 id。** stage 和 before 的更多用法参考 [tapable](https://github.com/webpack/tapable) + +注意: 相较于 `umi@3`, `umi@4` 去除了 `pluginId` 参数。 + +fn 的写法需要结合即将使用的 applyPlugins 的 type 参数来确定: +- `api.ApplyPluginsType.add` `applyPlugins` 将按照 hook 顺序来将它们的返回值拼接成一个数组。此时 `fn` 需要有返回值,`fn` 将获取 `applyPlugins` 的参数 `args` 来作为自己的参数。`applyPlugins` 的 `initialValue` 必须是一个数组,它的默认值是空数组。当 `key` 以 `'add'` 开头且没有显式地声明 `type` 时,`applyPlugins` 会默认按此类型执行。 +- `api.ApplyPluginsType.modify` `applyPlugins` 将按照 hook 顺序来依次更改 `applyPlugins` 接收的 `initialValue`, 因此此时 **`initialValue` 是必须的** 。此时 `fn` 需要接收一个 `memo` 作为自己的第一个参数,而将会把 `applyPlugins` 的参数 `args` 来作为自己的第二个参数。`memo` 是前面一系列 hook 修改 `initialValue` 后的结果, `fn` 需要返回修改后的`memo` 。当 `key` 以 `'modify'` 开头且没有显式地声明 `type` 时,`applyPlugins` 会默认按此类型执行。 +- `api.ApplyPluginsType.event` `applyPlugins` 将按照 hook 顺序来依次执行。此时不用传入 `initialValue` 。`fn` 不需要有返回值,并且将会把 `applyPlugins` 的参数 `args` 来作为自己的参数。当 `key` 以 `'on'` 开头且没有显式地声明 `type` 时,`applyPlugins` 会默认按此类型执行。 + +e.g.1 add 型 +```ts +api.register({ + key: 'addFoo', + // 同步 + fn: (args) => args +}); + +api.register({ + key: 'addFoo', + // 异步 + fn: async (args) => args * 2 +}) + +api.applyPlugins({ + key: 'addFoo', + // key 是 add 型,不用显式声明为 api.ApplyPluginsType.add + args: 1 +}).then((data)=>{ + console.log(data); // [1,2] +}) +``` +e.g.2 modify 型 +```ts +api.register({ + key: 'foo', + fn: (memo, args) => ({ ...memo, a: args}) +}) +api.register({ + key: 'foo', + fn: (memo) => ({...memo, b: 2}) +}) +api.applyPlugins({ + key: 'foo', + type: api.ApplyPluginsType.modify, + // 必须有 initialValue + initialValue: { + a: 0, + b: 0 + }, + args: 1 +}).then((data) => { + console.log(data); // { a: 1, b: 2 } +}); +``` + +### registerCommand +```ts +api.registerCommand({ + name: string, + description? : string, + options? : string, + details? : string, + fn, + alias? : string | string[] + resolveConfigMode? : 'strict' | 'loose' +}) +``` +注册命令。 +- `alias` 为别名,比如 generate 的别名 g +- `fn` 的参数为 `{ args }`, args 的格式同 [yargs](https://github.com/yargs/yargs) 的解析结果,需要注意的是 `_` 里的 command 本身被去掉了,比如执行`umi generate page foo`,`args._` 为 `['page','foo']` +- `resolveConfigMode` 参数控制执行命令时配置解析的方式,`strict` 模式下强校验 Umi 项目的配置文件内容,如果有非法内容中断命令执行;`loose` 模式下不执行配置文件的校验检查。 + +### registerMethod +```ts +api.registerMethod({ name: string, fn? }) +``` +往 api 上注册一个名为 `'name'` 的方法。 + +- 当传入了 fn 时,执行 fn +- 当没有传入 fn 时,`registerMethod` 会将 `name` 作为 `api.register` 的 `key` 并且将其柯里化后作为 `fn`。这种情况下相当于注册了一个 `register` 的快捷调用方式,便于注册 hook。 + +注意: +- 相较于 `umi@3`, `umi@4` 去除了 exitsError 参数。 +- 通常不建议注册额外的方法,因为它们不会有 ts 提示,直接使用 `api.register()` 是一个更安全的做法。 + +e.g.1 +```ts +api.registerMethod({ + name: foo, + // 有 fn + fn: (args) => { + console.log(args); + } +}) +api.foo('hello, umi!'); // hello, umi! +``` +该例子中,我们往api上注册了一个 foo 方法,该方法会把参数 console 到控制台。 + +e.g.2 +```ts +import api from './api'; + +api.registerMethod({ + name: 'addFoo' + // 没有 fn +}) + +api.addFoo( args => args ); +api.addFoo( args => args * 2 ); + +api.applyPlugins({ + key: 'addFoo', + args: 1 +}).then((data)=>{ + console.log(data); // [ 1, 2 ] +}); +``` +该例子中,我们没有向 `api.registerMethod` 中传入 fn。此时,我们相当于往 api 上注册了一个"注册器":`addFoo`。每次调用该方法都相当于调用了 `register({ key: 'addFoo', fn })`。因此当我们使用 `api.applyPlugins` 的时候(由于我们的方法是 add 型的,可以不用显式声明其 type )就可以获取刚刚注册的 hook 的值。 + +### registerPresets +```ts +api.registerPresets( presets: string[] ) +``` +注册插件集,参数为路径数组。该 api 必须在 initPresets stage 执行,即只可以在 preset 中注册其他 presets + +e.g. +```ts +api.registerPresets([ + './preset', + require.resolve('./preset_foo') +]) +``` + +### registerPlugins +```ts +api.registerPlugins( plugins: string[] ) +``` +注册插件,参数为路径数组。该 api 必须在 initPresets 和 initPlugins stage 执行。 + +e.g. +```ts +api.registerPlugins([ + './plugin', + require.resolve('./plugin_foo') +]) +``` + +注意: 相较于 `umi@3` ,`umi@4` 不再支持在 `registerPresets` 和 `registerPlugins` 中直接传入插件对象了,现在只允许传入插件的路径。 + +### registerGenerator + +注册微生成器用来快捷生成模板代码。 + +示例: + +```ts +import { GeneratorType } from '@umijs/core'; +import { logger } from '@umijs/utils'; +import { join } from 'path'; +import { writeFileSync } from 'fs'; + +api.registerGenerator({ + key: 'editorconfig', + name: 'Create .editorconfig', + description: 'Setup editorconfig config', + type: GeneratorType.generate, + fn: () => { + const configFilePath = join(api.cwd, '.editorconfig') + if (existsSync(configFilePath)) { + logger.info(`The .editorconfig file already exists.`) + return + } + writeFileSync( + configFilePath, + ` +# 🎨 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 +`.trimStart(), + 'utf-8' + ) + logger.info(`Generate .editorconfig file successful.`) + } +}) +``` + +更多示例见 [`已有生成器源码`](https://github.com/umijs/umi/tree/master/packages/preset-umi/src/commands/generators) 。 + +### skipPlugins +```ts +api.skipPlugins( keys: string[]) +``` +声明哪些插件需要被禁用,参数为插件 key 的数组 + +## 扩展方法 +通过`api.registerMethod()` 扩展的方法,它们的作用都是注册一些 hook 以供使用,因此都需要接收一个 fn。这些方法中的大部分都按照 `add-` `modify-` `on-` 的方式命名,它们分别对应了 `api.ApplyPluginsType`的三种方式,不同方式接收的 fn 不太相同,详见 [register](#register) 一节。 + +注意: 下文提到的所有 fn 都可以是同步的或者异步的(返回一个 Promise 即可)。fn 都可以被 + +```ts +{ + fn, + name?: string, + before?: string | string[], + stage: number, +} + +``` +代替。其中各个参数的作用详见 [tapable](https://github.com/webpack/tapable) + +### addBeforeBabelPlugins +增加额外的 Babel 插件。传入的 fn 不需要参数,且需要返回一个 Babel 插件或插件数组。 +```ts +api.addBeforeBabelPlugins(() => { + // 返回一个 Babel 插件(来源于 Babel 官网的例子) + return () => { + visitor: { + Identifier(path) { + const name = path.node.name; + path.node.name = name.split("").reverse().join(""); + } + } + } +}) +``` + +### addBeforeBabelPresets +增加额外的 Babel 插件集。传入的 fn 不需要参数,且需要返回一个 Babel 插件集( presets )或插件集数组。 +```ts +api.addBeforeBabelPresets(() => { + // 返回一个 Babel 插件集 + return () => { + return { + plugins: ["Babel_Plugin_A","Babel_Plugin_B"] + } + } +}) +``` + +### addBeforeMiddlewares +在 webpack-dev-middleware 之前添加中间件。传入的 fn 不需要参数,且需要返回一个 express 中间件或其数组。 +```ts +api.addBeforeMiddlewares(() => { + return (req, res, next) => { + if(false) { + res.end('end'); + } + next(); + } +}) +``` + +### addEntryCode +在入口文件的最后面添加代码(render 后)。传入的 fn 不需要参数,且需要返回一个 string 或者 string 数组。 +```ts +api.addEntryCode(() => `console.log('I am after render!')`); +``` + +### addEntryCodeAhead +在入口文件的最前面添加代码(render 前,import 后)。传入的 fn 不需要参数,且需要返回一个 string 或者 string 数组。 +```ts +api.addEntryCodeAhead(() => `console.log('I am before render!')`) +``` + +### addEntryImports +在入口文件中添加 import 语句 (import 最后面)。传入的 fn 不需要参数,其需要返回一个 `{source: string, specifier?: string}` 或其数组。 +```ts +api.addEntryImports(() => ({ + source: '/modulePath/xxx.js', + specifier: 'moduleName' +})) +``` + +### addEntryImportsAhead +在入口文件中添加 import 语句 (import 最前面)。传入的 fn 不需要参数,其需要返回一个 `{source: string, specifier?: string}` 或其数组。 +```ts +api.addEntryImportsAhead(() => ({ + source: 'anyPackage' +})) +``` + +### addExtraBabelPlugins +添加额外的 Babel 插件。 传入的 fn 不需要参数,且需要返回一个 Babel 插件或插件数组。 + +### addExtraBabelPresets +添加额外的 Babel 插件集。传入的 fn 不需要参数,且需要返回一个 Babel 插件集或其数组。 + +### addHTMLHeadScripts +往 HTML 的 `` 元素里添加 Script。传入的 fn 不需要参数,且需要返回一个 string(想要加入的代码) 或者 `{ async?: boolean, charset?: string, crossOrigin?: string | null, defer?: boolean, src?: string, type?: string, content?: string }` 或者它们的数组。 +```ts +api.addHTMLHeadScripts(() => `console.log('I am in HTML-head')`) +``` + +### addHTMLLinks +往 HTML 里添加 Link 标签。 传入的 fn 不需要参数,返回的对象或其数组接口如下: +```ts +{ + as?: string, crossOrigin: string | null, + disabled?: boolean, + href?: string, + hreflang?: string, + imageSizes?: string, + imageSrcset?: string, + integrity?: string, + media?: string, + referrerPolicy?: string, + rel?: string, + rev?: string, + target?: string, + type?: string +} +``` + +### addHTMLMetas +往 HTML 里添加 Meta 标签。 传入的 fn 不需要参数,返回的对象或其数组接口如下: +```ts +{ + content?: string, + 'http-equiv'?: string, + name?: string, + scheme?: string +} +``` + +### addHTMLScripts +往 HTML 尾部添加 Script。 传入的 fn 不需要参数,返回的对象接口同 [addHTMLHeadScripts](#addHTMLHeadScripts) + +### addHTMLStyles +往 HTML 里添加 Style 标签。 传入的 fn 不需要参数,返回一个 string (style 标签里的代码)或者 `{ type?: string, content?: string }`,或者它们的数组。 + + +### addLayouts +添加全局 layout 组件。 传入的 fn 不需要参数,返回 `{ id?: string, file: string }` + +### addMiddlewares +添加中间件,在 route 中间件之后。 传入的 fn 不需要参数,返回 express 中间件。 + +### addPolyfillImports +添加补丁 import,在整个应用的最前面执行。 传入的 fn 不需要参数,返回 `{ source: string, specifier?:string }` + +### addPrepareBuildPlugins + +### addRuntimePlugin +添加运行时插件,传入的 fn 不需要参数,返回 string ,表示插件的路径。 + +### addRuntimePluginKey +添加运行时插件的 Key, 传入的 fn 不需要参数,返回 string ,表示插件的路径。 + +### addTmpGenerateWatcherPaths +添加监听路径,变更时会重新生成临时文件。传入的 fn 不需要参数,返回 string,表示要监听的路径。 + +### addOnDemandDeps +添加按需安装的依赖,他们会在项目启动时检测是否安装: + +```ts + api.addOnDemandDeps(() => [{ name: '@swc/core', version: '^1.0.0', dev: true }]) +``` + +### chainWebpack +通过 [webpack-chain](https://github.com/neutrinojs/webpack-chain) 的方式修改 webpack 配置。传入一个fn,该 fn 不需要返回值。它将接收两个参数: +- `memo` 对应 webpack-chain 的 config +- `args:{ webpack, env }` `arg.webpack` 是 webpack 实例, `args.env` 代表当前的运行环境。 + +e.g. +```ts +api.chainWebpack(( memo, { webpack, env}) => { + // set alias + memo.resolve.alias.set('a','path/to/a'); + // Delete progess bar plugin + memo.plugins.delete('progess'); +}) +``` + +### modifyAppData (`umi@4` 新增) + +修改 app 元数据。传入的 fn 接收 appData 并且返回它。 +```ts +api.modifyAppData((memo) => { + memo.foo = 'foo'; + return memo; +}) +``` + +### modifyConfig +修改配置,相较于用户的配置,这份是最终传给 Umi 使用的配置。传入的 fn 接收 config 作为第一个参数,并且返回它。另外 fn 可以接收 `{ paths }` 作为第二个参数。`paths` 保存了 Umi 的各个路径。 +```ts +api.modifyConfig((memo, { paths }) => { + memo.alias = { + ...memo.alias, + '@': paths.absSrcPath + } + return memo; +}) +``` + +### modifyDefaultConfig +修改默认配置。传入的 fn 接收 config 并且返回它。 + +### modifyHTML +修改 HTML,基于 cheerio 的 ast。传入的 fn 接收 cheerioAPI 并且返回它。另外 fn 还可以接收`{ path }` 作为它的第二个参数,该参数代表路由的 path +```ts +api.modifyHTML(($, { path }) => { + $('h2').addClass('welcome'); + return $; +}) +``` + +### modifyHTMLFavicon +修改 HTML 的 favicon 路径。 传入的 fn 接收原本的 favicon 路径(string 类型)并且返回它。 + +### modifyPaths +修改 paths,比如 absOutputPath、absTmpPath。传入的 fn 接收 paths 并且返回它。 + +paths 的接口如下: +```ts +paths:{ + cwd?: string; + absSrcPath?: string; + absPagesPath?: string; + absTmpPath?: string; + absNodeModulesPath?: string; + absOutputPath?: string; +} +``` + +### modifyRendererPath +修改 renderer path。传入的 fn 接收原本的 path (string 类型)并且返回它。 + +### modifyServerRendererPath +修改 server renderer path。传入的 fn 接收原本的 path (string 类型)并且返回它。 + +### modifyRoutes +修改路由。 传入的 fn 接收 id-route 的 map 并且返回它。其中 route 的接口如下: +```ts +interface IRoute { + path: string; + file?: string; + id: string; + parentId?: string; + [key: string]: any; +} +``` +e.g. +```ts +api.modifyRoutes((memo) => { + Object.keys(memo).forEach((id) => { + const route = memo[id]; + if(route.path === '/'){ + route.path = '/redirect' + } + }); + return memo; +}) +``` + +### modifyTSConfig + +修改临时目录下的 tsconfig 文件内容。 + +```ts +api.modifyTSConfig((memo) => { + memo.compilerOptions.paths['foo'] = ['bar']; + return memo; +}); +``` + +### modifyViteConfig +修改 vite 最终配置。 传入的 fn 接收 vite 的 Config 对象作为第一个参数并且返回它。另外 fn 还可以接收 `{ env }` 作为第二个参数,可以通过该参数获取当前的环境。 +```ts +api.modifyViteConfig((memo, { env }) => { + if(env === 'development'){ + // do something + } + return memo; +}) +``` +### modifyWebpackConfig +修改 webpack 最终配置。传入的 fn 接收 webpack 的 Config 对象作为第一个参数并且返回它。另外 fn 还可以接收 `{ webpack, env }` 作为第二个参数,其中 webpack 是 webpack 实例,env 代表当前环境。 + +```ts +api.modifyWebpackConfig((memo, { webpack, env }) => { + // do something + + return memo; +}) +``` + +### onBeforeCompiler +generate 之后,webpack / vite compiler 之前。传入的 fn 不接收任何参数。 + +### onBeforeMiddleware +提供在服务器内部执行所有其他中间件之前执行自定义中间件的能力, 这可以用来定义自定义处理程序, 例如: + +```ts +api.onBeforeMiddleware(({ app }) => { + app.get('/some/path', function (req, res) { + res.json({ custom: 'response' }); + }); +}); +``` + +### onBuildComplete +build 完成时。传入的 fn 接收 `{ isFirstCompile: boolean, stats, time: number, err?: Error }` 作为参数。 + +### onBuildHtmlComplete +build 完成且 html 完成构建之后。 + +### onCheck +检查时,在 onStart 之前执行。传入的 fn 不接收任何参数 + +### onCheckCode +检查代码时。传入的 fn 接收的参数接口如下: +```ts +args: { + file: string; + code: string; + isFromTmp: boolean; + imports: { + source: string; + loc: any; + default: string; + namespace: string; + kind: babelImportKind; + specifiers: Record; + }[]; + exports: any[]; + cjsExports: string[]; +} +``` + +### onCheckConfig +检查 config 时。传入的 fn 接收 `{ config, userConfig }`作为参数,它们分别表示实际的配置和用户的配置。 + +### onCheckPkgJSON +检查 package.json 时。传入的 fn 接收 `{origin?, current}` 作为参数。它们的类型都是 package.json 对象 + +### onDevCompileDone +dev 完成时。传入的 fn 接收的参数接口如下: +```ts +args: { + isFirstCompile: boolean; + stats: any; + time: number; +} +``` + +### onGenerateFiles +生成临时文件时,随着文件变化会频繁触发,有缓存。 传入的 fn 接收的参数接口如下: +```ts +args: { + isFirstTime?: boolean; + files?: { + event: string; + path: string; + } | null; +} +``` + + +### onPatchRoute +匹配单个路由,可以修改路由,给路由打补丁 + + +### onPkgJSONChanged +package.json 变更时。传入的 fn 接收 `{origin?, current}` 作为参数。它们的类型都是 package.json 对象 + +### onPrepareBuildSuccess + +### onStart +启动时。传入的 fn 不接收任何参数。 + + +### writeTmpFile +`api.writeTmpFile()`的 type 参数的类型。 + +- content: 写入的文本内容,有内容就不会使用模板。 +- context: 模板上下文。 +- noPluginDir: 是否使用插件名做为目录。 +- path: 写入文件的路径。 +- tpl: 使用模板字符串,没有模板路径会使用它。 +- tplPath: 使用模板文件的路径。 + + +## 属性 +从 api 可以直接访问到的属性,这些属性有一部分来自于 service + +### appData + +### args +命令行参数,这里去除了命令本身。 + +e.g. +- `$ umi dev --foo`, args 为 `{ _:[], foo: true }` +- `$ umi g page index --typescript --less` , args 为 `{ _: [ 'page', 'index''], typescript: true, less: true }` + +### config +最终的配置(取决于你访问的时机,可能是当前收集到的最终配置) + +### cwd +当前路径 + +### env +即 `process.env.NODE_ENV` 可能有 `development`、`production` 和 `test` + +### logger +插件日志对象,包含 `{ log, info, debug, error, warn, profile }`,他们都是方法。其中 `api.logger.profile` 可用于性能耗时记录。 + +```ts +api.logger.profile('barId'); +setTimeout(() => { + api.logger.profile('barId'); +}) +// profile - barId Completed in 6254ms +``` + +### name +当前命令的名称,例如 `$ umi dev `, `name` 就是 `dev` + +### paths +项目相关的路径: +- `absNodeModulesPath`,node_modules 目录绝对路径 +- `absOutputPath`,输出路径,默认是 ./dist +- `absPagesPath`,pages 目录绝对路径 +- `absSrcPath`,src 目录绝对路径,需注意 src 目录是可选的,如果没有 src 目录,absSrcPath 等同于 cwd +- `absTmpPath`,临时目录绝对路径 +- `cwd`,当前路径 + +注意: 注册阶段不能获取到。因此不能在插件里直接获取,要在 hook 里使用。 + +### pkg +当前项目的 `package.json` 对象 + +### pkgPath +当前项目的 `package.json` 的绝对路径。 + +### plugin +当前插件的对象。 +- `type` 插件类型,有 preset 和 plugin 两种 +- `path` 插件路径 +- `id` 插件 id +- `key` 插件 key +- `config` 插件的配置 +- `enableBy` 插件的启用方式 + +注意: 注册阶段使用的 plugin 对象是你 `describe` 之前的对象。 + +### service +Umi 的 `Service` 实例。通常不需要用到,除非你知道为什么。 + +### userConfig +用户的配置,从 `.umirc` 或 `config/config` 中读取的内容,没有经过 defaultConfig 以及插件的任何处理。可以在注册阶段使用。 + +### ApplyPluginsType +`api.applyPlugins()` 的 type 参数的类型。包含 +- add +- modify +- event + +### ConfigChangeType +为 `api.describe()` 提供 `config.onChange` 的类型,目前包含两种: +- restart,重启 dev 进程,是默认值 +- regenerateTmpFiles,重新生成临时文件 + +### EnableBy +插件的启用方式,包含三种: +- register +- config + +### ServiceStage +Umi service 的运行阶段。有如下阶段: +- uninitialized +- init +- initPresets +- initPlugins +- resolveConfig +- collectAppData +- onCheck +- onStart +- runCommand diff --git a/docs/docs/docs/api/runtime-config.en-US.md b/docs/docs/docs/api/runtime-config.en-US.md new file mode 100644 index 000000000000..a2b176b2e61c --- /dev/null +++ b/docs/docs/docs/api/runtime-config.en-US.md @@ -0,0 +1,267 @@ +--- +order: 3 +toc: content +--- +# 运行时配置 + +运行时配置和配置的区别是他跑在浏览器端,基于此,我们可以在这里写函数、tsx、import 浏览器端依赖等等,注意不要引入 node 依赖。 + +## 配置方式 + +约定 `src/app.tsx` 为运行时配置。 + +## TypeScript 提示 + +如果你想在写配置时也有提示,可以通过 umi 的 defineApp 方法定义配置。 + +```js +import { defineApp } from 'umi'; +export default defineApp({ + layout: () => { + return { + title: 'umi', + }; + }, +}); + +// or +import { RuntimeConfig } from 'umi'; +export const layout: RuntimeConfig['layout'] = () => { + return { + title: 'umi', + }; +}; +``` + +## 配置项 + +> 以下配置项按字母排序。 + +### dva + +如果你使用的 dva,那么支持配置 dva 插件的运行时配置,具体参考[插件配置](../max/dva)。 + +比如: + +```ts +export default { + dva: { + immer: true, + extraModels: [], + }, +}; +``` + +#### extraModels + +- Type: string[] +- Default: [] 配置额外到 dva model。 + +#### immer + +- Type: boolean | object +- Default: false 表示是否启用 immer 以方便修改 reducer。 + +注:如需兼容 IE11,需配置 `{ immer: { enableES5: true }}`。 + +### 数据流 + +若你需要定义初始化数据,使用 `getInitialState` 、`useModel` 等 [数据流](../max/data-flow) 相关功能: + +1. 你可以创建自带数据流功能的 `@umijs/max` 项目,详见 [Umi max 简介](../max/introduce) 。 + +2. 或者手动开启数据流功能的插件使用该功能: + + ```bash + pnpm add -D @umijs/plugins + ``` + + ```ts + // .umirc.ts + export default { + plugins: [ + '@umijs/plugins/dist/initial-state', + '@umijs/plugins/dist/model', + ], + initialState: {}, + model: {}, + }; + ``` + +### layout + +修改[内置布局](../max/layout-menu)的配置,比如配置退出登陆、自定义导航暴露的渲染区域等。 + +> 注意:需要开启 [layout](../api/config#layout) 插件,才能使用它的运行时配置。 + +```js +export const layout = { + logout: () => {}, // do something +}; +``` + +更多具体配置参考[插件文档](../max/layout-menu#运行时配置)。 + +### onRouteChange(\{ routes, clientRoutes, location, action, basename, isFirst \}) + +在初始加载和路由切换时做一些事情。 + +比如用于做埋点统计, + +```ts +export function onRouteChange({ + location, + clientRoutes, + routes, + action, + basename, + isFirst, +}) { + bacon(location.pathname); +} +``` + +比如用于设置标题, + +```ts +import { matchRoutes } from 'umi'; + +export function onRouteChange({ clientRoutes, location }) { + const route = matchRoutes(clientRoutes, location.pathname)?.pop()?.route; + if (route) { + document.title = route.title || ''; + } +} +``` + +### patchRoutes(\{ routes \}) + +```ts +export function patchRoutes({ routes, routeComponents }) { + console.log('patchRoutes', routes, routeComponents); +} +``` + +- `routes`: 打平的路由列表。 + +- `routeComponents`: 路由对应的组件映射。 + +注:如需动态更新路由,建议使用 `patchClientRoutes()` ,否则你可能需要同时修改 `routes` 和 `routeComponents`。 + +### patchClientRoutes(\{ routes \}) + +修改被 react-router 渲染前的树状路由表,接收内容同 [useRoutes](https://reactrouter.com/en/main/hooks/use-routes)。 + +比如在最前面添加一个 `/foo` 路由, + +```tsx +import Page from '@/extraRoutes/foo'; + +export function patchClientRoutes({ routes }) { + routes.unshift({ + path: '/foo', + element: , + }); +} +``` + +比如在最前面添加一个重定向路由: + +```tsx +import { Navigate } from 'umi'; + +export const patchClientRoutes = ({ routes }) => { + routes.unshift({ + path: '/', + element: , + }); +}; +``` + +比如添加一个嵌套路由: + +```tsx +import Page from '@/extraRoutes/foo'; + +export const patchClientRoutes = ({ routes }) => { + routes.push({ + path: '/group', + children: [{ + path: '/group/page', + element: , + }], + }); +}; +``` + +比如和 `render` 配置配合使用,请求服务端根据响应动态更新路由, + +```ts +let extraRoutes; + +export function patchClientRoutes({ routes }) { + // 根据 extraRoutes 对 routes 做一些修改 + patch(routes, extraRoutes); +} + +export function render(oldRender) { + fetch('/api/routes') + .then((res) => res.json()) + .then((res) => { + extraRoutes = res.routes; + oldRender(); + }); +} +``` + +注意: + +- 直接修改 routes,不需要返回 + +### qiankun + +Umi 内置了 `qiankun` 插件来提供微前端的能力,具体参考[插件配置](../max/micro-frontend)。 + +### render(oldRender: `Function`) + +覆写 render。 + +比如用于渲染之前做权限校验, + +```bash +export function render(oldRender) { + fetch('/api/auth').then(auth => { + if (auth.isLogin) { oldRender() } + else { + location.href = '/login'; + oldRender() + } + }); +} +``` + +### request + +如果你使用了 `import { request } from 'umi';` 来请求数据,那么你可以通过该配置来自定义中间件、拦截器、错误处理适配等。具体参考 [request](../max/request) 插件配置。 + +### rootContainer(lastRootContainer, args) + +修改交给 react-dom 渲染时的根组件。 + +比如用于在外面包一个 Provider, + +```js +export function rootContainer(container) { + return React.createElement(ThemeProvider, null, container); +} +``` + +args 包含: + +- routes,全量路由配置 +- plugin,运行时插件机制 +- history,history 实例 + +## 更多配置 + +Umi 允许插件注册运行时配置,如果你使用插件,肯定会在插件里找到更多运行时的配置项。 diff --git a/docs/docs/docs/guides/boilerplate.en-US.md b/docs/docs/docs/guides/boilerplate.en-US.md new file mode 100644 index 000000000000..6076835fc1e9 --- /dev/null +++ b/docs/docs/docs/guides/boilerplate.en-US.md @@ -0,0 +1,52 @@ +--- +order: 11 +toc: content +--- +# 脚手架 + +Umi 官方提供了一个脚手架 ,可以轻松快速创建一个项目: + +```bash +# 在当前文件夹下创建项目 +pnpm create umi +# 在当前目录的 my-umi-app 文件夹下创建项目 +pnpm create umi my-umi-app +``` + +这个命令会安装 `create-umi` 脚手架并自动运行,运行后提供了两个可选项: + +1. Pick Npm Client - 选择 Npm 客户端 + +你可以从以下几个选项中选择习惯的 Node 依赖管理工具: + +- [npm](https://www.npmjs.com/) +- [cnpm](https://github.com/cnpm/cnpm) +- [tnpm](https://web.npm.alibaba-inc.com/) +- [yarn](https://yarnpkg.com/) +- [pnpm](https://pnpm.io/) (Umi 官方推荐) + +2. Pick Npm Registry - 选择 Npm 源 + +- [npm](https://www.npmjs.com/) +- [taobao](https://npmmirror.com/) + +选择后会自动生成一个最基本的 Umi 项目,并根据选中的客户端和镜像源安装依赖: + +```text +. +├── package.json +├── pnpm-lock.yaml +├── src +│ ├── assets +│ │ └── yay.jpg +│ ├── layouts +│ │ ├── index.less +│ │ └── index.tsx +│ └── pages +│ ├── docs.tsx +│ └── index.tsx +├── tsconfig.json +└── typings.d.ts +``` + +这样就一键完成 Umi 项目的初始化了。 diff --git a/docs/docs/docs/guides/client-loader.en-US.md b/docs/docs/docs/guides/client-loader.en-US.md new file mode 100644 index 000000000000..d73c379b202d --- /dev/null +++ b/docs/docs/docs/guides/client-loader.en-US.md @@ -0,0 +1,59 @@ +--- +order: 8 +toc: content +--- +# 路由数据加载 + +Umi 提供了开箱即用的数据预加载方案,能够解决在多层嵌套路由下,页面组件和数据依赖的瀑布流请求。Umi +会自动根据当前路由或准备跳转的路由,并行地发起他们的数据请求,因此当路由组件加载完成后,已经有马上可以使用的数据了。 + +## 启用方式 + +配置开启: + +```ts +// .umirc.ts + +export default { + clientLoader: {} +} +``` + +## 使用方式 + +在路由文件中,除了默认导出的页面组件外,再导出一个 `clientLoader` 函数,并且在该函数内完成路由数据加载的逻辑。 + +```tsx +// pages/.../some_page.tsx + +import { useClientLoaderData } from 'umi'; + +export default function SomePage() { + const { data } = useClientLoaderData(); + return
{data}
; +} + +export async function clientLoader() { + const data = await fetch('/api/data'); + return data; +} +``` + +如上代码,在 `clientLoader` 函数返回的数据,可以在组件内调用 `useClientLoaderData` 获取。 + +## 优化效果 + +考虑一个三层嵌套路由的场景: + +1. 我们需要先等第一层路由的组件加载完成,然后第一层路由的组件发起数据请求 +2. 第一层路由的数据请求完成后,开始请求第二层路由的组件,第二层路由的组件加载好以后请求第二层路由需要的数据 +3. 第二层路由的数据请求完成后,开始请求第三层路由的组件,第三层路由的组件加载好以后请求第三层路由需要的数据 +4. 第三层路由的数据请求完成后,整个页面才完成渲染 + +这样的瀑布流请求会严重影响用户的体验,如下图所示: + +![](https://img.alicdn.com/imgextra/i1/O1CN01OcsOL91CPw46Pm7vz_!!6000000000074-1-tps-600-556.gif) + +如果将组件请求数据的程序提取到 `clientLoader` 中,则 Umi 可以并行地请求这些数据: + +![](https://img.alicdn.com/imgextra/i3/O1CN01URnLH81un9EVYGeL9_!!6000000006081-1-tps-600-556.gif) diff --git a/docs/docs/docs/guides/debug.en-US.md b/docs/docs/docs/guides/debug.en-US.md new file mode 100644 index 000000000000..653a0052d394 --- /dev/null +++ b/docs/docs/docs/guides/debug.en-US.md @@ -0,0 +1,77 @@ +--- +order: 14 +toc: content +--- + +# 调试 + +除了使用浏览器的调试工具来完成开发中的调试外,Umi 还推荐以下调试方式来协助项目的调试。 + +## 调试 dev 产物 + +如果你需要在 dev 阶段调试项目的构建产物,以 `umi.js` 举例。先将原来的 `umi.js` 下载到当前项目根目录下。根据调试需要进行编辑后,刷新浏览器,项目使用的 `umi.js` 就替换成了根目录下的 `umi.js` 文件。如果调试完毕需要恢复,直接删除根目录的 `umi.js` 即可。 + +举例: +```bash +# 下载当前项目的 umi.js +$curl http://127.0.0.1:8000/umi.js -O + +# 增加想调试的内容,举例增加 "debug!!!" 弹窗 +$ echo -e '\n;alert("debug!!!");\n' >> umi.js +# 打开浏览器就能看到 alert 弹窗 + +# 退出调试,恢复到正常状态 +$rm umi.js +``` + +以此类推即可调试其他的 JavaScript 文件。 + +## XSwitch + +如果需要在特定的域名环境调试或者验证当前的修改的代码,推荐使用 Chrome 插件 [XSwitch](https://chrome.google.com/webstore/detail/xswitch/idkjhjggpffolpidfkikidcokdkdaogg)。 + + +![xswitch-logo](https://gw.alipayobjects.com/mdn/rms_ffea06/afts/img/A*fp9yRINN6aMAAAAAAAAAAAAAARQnAQ) + + +假设我们想在线上项目地址 `https://www.myproject.com` 上调试本地代码。项目使用 `https://www.myproject.com/umi.hash.js`,为了验证本地的项目,需要将它替换成本地开发环境的 `http://127.0.0.1:000/umi.js` + +首先使用环境变量 `SOCKET_SERVER` 启动本地环境(防止因为连接不上 socket server 导致页面不断刷新)。 +```bash +$SOCKET_SERVER=http://127.0.0.1:8000/ npx umi dev +``` + +然后,在 XSwitch 中配置资源转发规则。 +```json +{ + "proxy": [ + // 数组的第 0 项的资源会被第 1 项目替换 + [ + "https://www.myproject.com/umi.2c8a01df.js", + "http://127.0.0.1:8000/umi.js" + ], + // 使用正则可以方便处理分包情况下 js 资源的加载 + [ + "https://www.myproject.com/(.*\.js)", + "http://127.0.0.1:8000/$1", + ], + // 如果需要验证视觉表现,不要忘记替换 css 资源 + [ + "https://www.myproject.com/umi.ae8b10e0.css", + "http://127.0.0.1:8000/umi.css" + ] + ] +} +``` + +刷新页面,正式域名下的内容就被替换了,这个时候就能方便的指定环境下调试了。 + +如果要退出调试,关闭 XSwitch 插件功能即可。 + +![turn-off-xswitch](https://gw.alipayobjects.com/mdn/rms_ffea06/afts/img/A*qXbNQJvz8-QAAAAAAAAAAAAAARQnAQ) + +:::success{title=💡} +经常使用 XSwitch 的话,可新建一个规则保存。 +::: + +![rule](https://gw.alipayobjects.com/mdn/rms_ffea06/afts/img/A*oWfiT6R0SJkAAAAAAAAAAAAAARQnAQ) diff --git a/docs/docs/docs/guides/directory-structure.en-US.md b/docs/docs/docs/guides/directory-structure.en-US.md new file mode 100644 index 000000000000..6f6d68fc9df1 --- /dev/null +++ b/docs/docs/docs/guides/directory-structure.en-US.md @@ -0,0 +1,305 @@ +--- +order: 2 +toc: content +--- +# 目录结构 + +这里罗列了 Umi 项目中约定(或推荐)的目录结构,在项目开发中,请遵照这个目录结构组织代码。 + +```bash +. +├── config +│ └── config.ts +├── dist +├── mock +│ └── app.ts|tsx +├── src +│   ├── .umi +│   ├── .umi-production +│ ├── layouts +│ │ ├── BasicLayout.tsx +│ │ ├── index.less +│ ├── models +│ │ ├── global.ts +│ │ └── index.ts +│ ├── pages +│ │ ├── index.less +│ │ └── index.tsx +│ ├── utils // 推荐目录 +│ │ └── index.ts +│ ├── services // 推荐目录 +│ │ └── api.ts +│ ├── app.(ts|tsx) +│ ├── global.ts +│ ├── global.(css|less|sass|scss) +│ ├── overrides.(css|less|sass|scss) +│ ├── favicon.(ico|gif|png|jpg|jpeg|svg|avif|webp) +│ └── loading.(tsx|jsx) +├── node_modules +│   └── .cache +│ ├── bundler-webpack +│ ├── mfsu +│ └── mfsu-deps +├── .env +├── plugin.ts +├── .umirc.ts // 与 config/config 文件 2 选一 +├── package.json +├── tsconfig.json +└── typings.d.ts +``` +## 根目录 + +### package.json + +与 Umi 3 不同,Umi 4 不会自动注册 `package.json` 中以 `@umijs/preset-`、`@umijs/plugin-`、`umi-preset-` 和 `umi-plugin-` 开头的插件、预设,若你需要自定义额外的插件、预设,需要手动配置到 [`plugins`](../api/config#plugins) 。 + +### .env + +环境变量,比如: + +```text +PORT=8888 +COMPRESS=none +``` + +### .umirc.ts + +> 与 `config/config.ts` 文件功能相同,2 选 1 。`.umirc.ts` 文件优先级较高 + +配置文件,包含 Umi 所有[非运行时配置](../api/config)(运行时配置一般定义于 [`app.ts`](#apptstsx))。 + +若你需要在不同环境中加载不同配置,这在 Umi 中是根据 [`UMI_ENV`](./env-variables#umi_env) 来实现的,一个不同环境启动的例子: + +```ts +// package.json +{ + "scripts": { + "dev": "umi dev", + "dev:pre": "cross-env UMI_ENV=pre umi dev" + } +} +``` + +### config/config.ts + +> 与 `.umirc.ts` 文件功能相同,2 选 1 。`.umirc.ts` 文件优先级较高 + +与 [`.umirc.ts`](#umircts) 相同,区别是你可以单独在一个 `config` 文件夹下集中管理所有的配置,保持项目根目录整洁。 + +### dist 目录 + +执行 `umi build` 后产物的默认输出文件夹。可通过 [`outputPath`](../api/config#outputpath) 配置修改产物输出文件夹。 + +### mock 目录 + +存放 mock 文件,此目录下所有 `.ts` / `.js` 文件会被 mock 服务加载,从而提供模拟数据,使用方法详见 [Mock](./mock) 。 + +### public 目录 + +存放固定的静态资源,如存放 `public/image.png` ,则开发时可以通过 `/image.png` 访问到,构建后会被拷贝到输出文件夹。 + +注: + +1. 对于 svg 资源,Umi 支持 [svgr](../api/config#svgr) ,可以直接导入作为组件使用: + + ```ts + import SmileUrl, { ReactComponent as SvgSmile } from './smile.svg'; + // + ``` + +2. 对于图片等资源,Umi 支持直接导入获取资源路径: + + ```tsx + import imgUrl from './image.png' + // > + ``` + +### `src` 目录 + +#### .umi 目录 + +:::warning{title=🛎️} +**不要提交 `.umi` 临时文件到 git 仓库,默认已在 `.gitignore` 被忽略。** +::: + +dev 时的临时文件目录,比如入口文件、路由等,都会被临时生成到这里。 + +#### .umi-production 目录 + +:::warning{title=🛎️} +**不要提交 `.umi-production` 临时文件到 git 仓库,默认已在 `.gitignore` 被忽略。** +::: + +build 时的临时文件目录,比如入口文件、路由等,都会被临时生成到这里。 + +#### app.[ts|tsx] + +[运行时配置](../api/runtime-config) 文件,可以在这里扩展运行时的能力,比如修改路由、修改 render 方法等。 + +运行时配置带来的逻辑会在浏览器中运行,因此当有远程配置、动态内容时,这些我们在本地开发时还不确定,不能写死,所以需要在浏览器实际运行项目时动态获取他们。 + +#### layouts/index.tsx + +全局布局,默认会在所有路由下生效,比如有以下路由关系: + +``` +[ + { path: '/', component: '@/pages/index' }, + { path: '/users', component: '@/pages/users' }, +] +``` + +输出为: + +```jsx + + index + users + +``` + +当你需要关闭 layout 时可以使用 `layout: false` ,当你需要更多层 layout 时,可以考虑使用 [`wrappers`](./routes#wrappers) ,仅在配置式路由可用: + +```ts + routes: [ + { path: '/', component: './index', layout: false }, + { + path: '/users', + component: './users', + wrappers: ['@/wrappers/auth'] + } + ] +``` + +#### pages 目录 + +约定式路由默认以 `pages/*` 文件夹的文件层级结构来生成路由表。 + +在配置式路由中,`component` 若写为相对路径,将从该文件夹为起点开始寻找文件: + +```ts + routes: [ + // `./index` === `@/pages/index` + { path: '/', component: './index' } + ] +``` + +##### 基础路由 + +假设 `pages` 目录结构如下: + +``` ++ pages/ + + users/ + - index.tsx + - index.tsx +``` + +那么,会自动生成路由配置如下: + +```ts +[ + { path: '/', component: '@/pages/index.tsx' }, + { path: '/users/', component: '@/pages/users/index.tsx' }, +] +``` + +##### 动态路由 + +约定带 `$` 前缀的目录或文件为动态路由。若 `$` 后不指定参数名,则代表 `*` 通配,比如以下目录结构: + +``` ++ pages/ + + foo/ + - $slug.tsx + + $bar/ + - $.tsx + - index.tsx +``` + +会生成路由配置如下: + +```ts +[ + { path: '/', component: '@/pages/index.tsx' }, + { path: '/foo/:slug', component: '@/pages/foo/$slug.tsx' }, + { path: '/:bar/*', component: '@/pages/$bar/$.tsx' }, +] +``` + +##### pages/404.tsx + +在使用约定式路由时,该文件会自动被注册为全局 404 的 fallback 页面。若你使用配置式路由,需要自行配置兜底路由到路由表最后一个: + +```ts + routes: [ + // other routes ... + { path: '/*', component: '@/pages/404.tsx' } + ] +``` + +#### global.(j|t)sx? + +全局前置脚本文件。 + +Umi 区别于其他前端框架,没有显式的程序主入口(如 `src/index.ts`),所以当你有需要在应用前置、全局运行的逻辑时,优先考虑写入 `global.ts` 。 + +当你需要添加全局 Context 、修改应用运行时,请使用 [`app.tsx`](#apptstsx) 。 + +#### global.(css|less|sass|scss) + +全局样式文件。 + +当你有需要全局使用的样式时,请考虑加入此文件。 + +:::info{title=💡} +需要注意的是,此文件的优先级在第三方组件库的样式之后,所以当你有覆盖第三方库样式的需求时,请使用 [`overrides.css`](#overridescsslesssassscss) 。 +::: + +#### overrides.(css|less|sass|scss) + +高优先级全局样式文件。 + +该文件一般专用于覆盖第三方库样式,其中所有 CSS 选择器都会附加 `body` 前缀以抬高优先级。 + +#### loading.(tsx|jsx) + +全局加载组件。 + +Umi 4 默认 [按页分包](../../blog/code-splitting) ,从而在页面切换时存在加载过程,通过该文件来配置加载动画。 + +### plugin.ts + +项目级 Umi 插件。 + +当你有 Umi 定制需求时,往往会用到 [插件 API](../api/plugin-api) (比如 [修改产物 html](../api/plugin-api#modifyhtml)),此时可创建该文件进行自定义: + +```ts +import type { IApi } from 'umi'; + +export default (api: IApi) => { + api.onDevCompileDone((opts) => { + opts; + // console.log('> onDevCompileDone', opts.isFirstCompile); + }); + api.modifyHTML(($) => { + $; + }); + api.chainWebpack((memo) => { + memo; + }); +}; + +``` + +### favicon + +站点 `favicon` 图标文件。 + +当存在 `src/favicon.(ico|gif|png|jpg|jpeg|svg|avif|webp)` 文件时,将会自动在产物中添加站点 `favicon` : + +```html + +``` + +若使用外部资源等,可以使用 [favicons](../api/config#favicons) 手动配置站点图标,配置值优先于约定。 diff --git a/docs/docs/docs/guides/env-variables.en-US.md b/docs/docs/docs/guides/env-variables.en-US.md new file mode 100644 index 000000000000..821142a2d486 --- /dev/null +++ b/docs/docs/docs/guides/env-variables.en-US.md @@ -0,0 +1,186 @@ +--- +order: 10 +toc: content +--- +# 环境变量 + +Umi 可以通过环境变量来完成一些特殊的配置和功能。 + +## 如何设置环境变量 + +### 执行命令时设置 + +例如需要改变 `umi dev` 开发服务器的端口,可以通过如下命令实现。 + +```bash +# OS X, Linux +$ PORT=3000 umi dev + +# Windows (cmd.exe) +$ set PORT=3000&&umi dev +``` + +如果需要同时在不同的操作系统中使用环境变量,推荐使用工具 [cross-env](https://github.com/kentcdodds/cross-env)。 + +```bash +$ pnpm install cross-env -D +$ cross-env PORT=3000 umi dev +``` + +### 设置在 .env 文件中 + +如果你的环境变量需要在开发者之间共享,推荐你设置在项目根目录的 `.env` 文件中,例如: + +```text +# file .env +PORT=3000 +BABEL_CACHE=none +``` + +然后执行, + +```bash +$ umi dev +``` + +`umi` 会以 3000 端口启动 dev server,并且禁用 babel 的缓存。 + +如果你有部分环境变量的配置在本地要做特殊配置,可以配置在 `.env.local` 文件中去覆盖 `.env` 的配置。比如在之前的 `.env` 的基础上, 你想本地开发覆盖之前 3000 端口, 而使用 4000 端口,可以做如下定义。 + +```text +# file .env.local +PORT=4000 +``` + +`umi` 会以 4000 端口启动 dev server,同时保持禁用 babel 的缓存。 + +此外 `umi` `.env` 文件中还支持变量的方式来配置环境变量。例如: + +``` +# file .env.local +FOO=foo +BAR=bar + +CONCAT=$FOO$BAR # CONCAT=foobar +``` + +注意: + +* 不建议将 `.env.local` 加入版本管理中。 + +## 环境变量列表 + +按字母顺序排列。 + +### APP_ROOT + +指定项目根目录。 + +注意: + +* `APP_ROOT` 不能配在 `.env` 中,只能在命令行里添加 + + +### ANALYZE + +用于分析 bundle 构成,默认关闭。 + +比如: + +```bash +$ ANALYZE=1 umi dev +# 或者 +$ ANALYZE=1 umi build +``` + +可以通过 `ANALYZE_PORT` 环境变量自定义端口或 [`analyze`](../api/config#analyze) 选项自定义配置。 + +### BABEL_POLYFILL + +默认会根据 targets 配置打目标浏览器的全量补丁,设置为 `none` 禁用内置的补丁方案。 + +### COMPRESS + +默认压缩 CSS 和 JS,值为 `none` 时不压缩,`build` 时有效。 + +### DID_YOU_KNOW + +设置为 `none` 会禁用「你知道吗」提示。 + +### ERROR_OVERLAY + +设置为 `none` 会禁用「Error Overlay」,在调试 Error Boundary 时会有用。 + +### FS_LOGGER + +默认会开启保存物理日志,值为 `none` 时不保存,同时针对 webcontainer 场景(比如 stackbliz)暂不保存。 + +### HMR + +默认开启 HMR 功能,值为 `none` 时关闭。 + +### HOST + +默认是 `0.0.0.0`。 + +### PORT + +指定端口号,默认是 `8000`。 + +### SOCKET_SERVER + +指定用于 HMR 的 socket 服务器。比如: + +```bash +$ SOCKET_SERVER=http://localhost:8000/ umi dev +``` + +### SPEED_MEASURE + +分析 Webpack 编译时间,支持 `CONSOLE` 和 `JSON` 两种格式,默认是 `CONSOLE`。 + +```bash +$ SPEED_MEASURE=JSON umi dev +``` + +### UMI_ENV + +当指定 `UMI_ENV` 时,会额外加载指定值的配置文件,优先级为: + + - `config.ts` + + - `config.${UMI_ENV}.ts` + + - `config.${dev | prod | test}.ts` + + - `config.${dev | prod | test}.${UMI_ENV}.ts` + + - `config.local.ts` + +若不指定 `UMI_ENV` ,则只会加载当前环境对应的配置文件,越向下的越具体,优先级更高,高优的配置可以往下移动。 + +注:根据当前环境的不同,`dev`, `prod`, `test` 配置文件会自动加载,不能将 `UMI_ENV` 的值设定成他们。 + +### UMI_PLUGINS + +指定 `umi` 命令执行时额外加载的插件的路径,使用 `,` 隔开。 + +```bash +$ UMI_PLUGINS=./path/to/plugin1,./path/to/plugin2 umi dev +``` + +### UMI_PRESETS + +指定 `umi` 命令执行时额外加载插件集的路径,使用 `,` 隔开。 + +```bash +$ UMI_PRESETS=./path/to/preset1,./path/to/preset2 umi dev +``` + +### WEBPACK_FS_CACHE_DEBUG + +开启 webpack 的物理缓存 debug 日志。 + +```bash +$ WEBPACK_FS_CACHE_DEBUG=1 umi dev +``` diff --git a/docs/docs/docs/guides/generator.en-US.md b/docs/docs/docs/guides/generator.en-US.md new file mode 100644 index 000000000000..b68878e074e2 --- /dev/null +++ b/docs/docs/docs/guides/generator.en-US.md @@ -0,0 +1,350 @@ +--- +order: 12 +toc: content +--- +# 微生成器 + +Umi 中内置了众多微生成器,协助你在开发中快速地完成一些繁琐的工作。 + +## 如何使用 + +下面的命令会列出目前所有可用的生成器,可以通过交互式方式来选择你使用的功能,都有详细的提示。 + +```bash +$ umi generate +# 或者 +$ umi g +``` + +你也可以通过 `umi g ` 的形式来使用对应的生成器。 + +## 生成器列表 + +### 页面生成器 + +快速生成一个新页面,有以下多种使用方式。 + +#### 基本使用 + +交互式输入页面名称和文件生成方式: + +```bash +$umi g page +? What is the name of page? › mypage +? How dou you want page files to be created? › - Use arrow-keys. Return to submit. +❯ mypage/index.{tsx,less} + mypage.{tsx,less} +``` + +直接生成: + +```bash +$umi g page foo +Write: src/pages/foo.tsx +Write: src/pages/foo.less +``` + +以目录方式生成页面,目录下为页面的组件和样式文件: + +```bash +$umi g page bar --dir +Write: src/pages/bar/index.less +Write: src/pages/bar/index.tsx +``` + +嵌套生成页面: + +```bash +$umi g page far/far/away/kingdom +Write: src/pages/far/far/away/kingdom.tsx +Write: src/pages/far/far/away/kingdom.less +``` + +批量生成多个页面: + +```bash +$umi g page page1 page2 a/nested/page3 +Write: src/pages/page1.tsx +Write: src/pages/page1.less +Write: src/pages/page2.tsx +Write: src/pages/page2.less +Write: src/pages/a/nested/page3.tsx +Write: src/pages/a/nested/page3.less +``` + +#### 对页面模板内容进行自定义 + +如果页面生成器使用的默认模板不符合你的需求,你可以对模板内容进行自定义设置。 + +执行 `--eject` 命令: + +```bash +$umi g page --eject +``` + +执行命令后,页面生成器会把它的原始模板写入到项目的 `/templates/page` 目录: + +``` +. +├── package.json +└── templates + └── page + ├── index.less.tpl + └── index.tsx.tpl +``` + +##### 使用模板变量 + +两个模板文件都支持模板语法,你可以像下面这样插入变量: + +```tsx +import React from 'react'; +import './{{{name}}}.less' + +const message = '{{{msg}}}' +const count = {{{count}}} +``` + +可以自定义参数值: + +```bash +$umi g page foo --msg "Hello World" --count 10 +``` +运行命令后,生成的页面内容如下: + +```tsx +import React from 'react'; +import './foo.less' + +const message = 'Hello World' +const count = 10 +``` + +如果你不需要使用模板变量,可以省略 `.tpl` 后缀名,将 `index.tsx.tpl` 简写为 `index.tsx`,`index.less.tpl` 简写为 `index.less`。 + +##### 预设变量 + +在上一小节生成的内容中,我们并没有指定 `name`,但它还是被设置为了一个值。这是因为它属于模板中预设的变量,下面是目前页面模板所有的预设变量: + +|参数|默认值|说明| +|:-:|:-:|:-| +| `name` | - | 当前文件的名称。如果执行 `pnpm umi g page foo`,会生成 `pages/foo.tsx` 和 `pages/foo.less` 两个文件,其中 `name` 的值为 "foo"。 | +| `color` | - | 随机生成一个 RGB 颜色。 | +| `cssExt` | `less` | 样式文件的后缀名。 | + +如果想了解更多模板语法的内容,请查看 [mustache](https://www.npmjs.com/package/mustache)。 + +##### `dir` 模式 + +在不使用 `dir` 模式的情况下,如果你的页面模板文件夹只自定义了一个模板文件,缺失的文件会自动选取默认的模板文件。 + +如果使用 `dir` 模式,它的生成内容会和你的页面自定义模板文件夹保持一致,只有在页面自定义模板文件夹为空时才使用默认模板。如果你的页面自定义模板文件夹内容如下: + +``` +. +├── a.tsx +└── index.tsx.tpl +``` + +生成的目录将是: + +``` +. +├── a.tsx +└── index.tsx +``` + +##### 回退 + +如果还想继续使用默认的模板,可以指定 `--fallback`,此时不再使用用户自定义的模板: + +```bash +$umi g page foo --fallback +``` + +### 组件生成器 + +在 `src/components/` 目录下生成项目需要的组件。和页面生成器一样,组件生成器也有多种生成方式。 + +#### 基本使用 + +交互式生成: +```bash +$umi g component +✔ Please input you component Name … foo +Write: src/components/Foo/index.ts +Write: src/components/Foo/component.tsx +``` + +直接生成: +```bash +$umi g component bar +Write: src/components/Bar/index.ts +Write: src/components/Bar/component.tsx +``` + +嵌套生成: +```bash +$umi g component group/subgroup/baz +Write: src/components/group/subgroup/Baz/index.ts +Write: src/components/group/subgroup/Baz/component.tsx +``` + +批量生成: +```bash +$umi g component apple banana orange +Write: src/components/Apple/index.ts +Write: src/components/Apple/component.tsx +Write: src/components/Banana/index.ts +Write: src/components/Banana/component.tsx +Write: src/components/Orange/index.ts +Write: src/components/Orange/component.tsx +``` + +#### 对组件模板内容进行自定义 + +与[页面生成器](#对页面模板内容进行自定义)相同,组件生成器也支持对模板内容自定义。首先,将原始模板写入到项目的 `/templates/component` 目录: + +```bash +$umi g component --eject +``` + +##### 使用模板变量 + +```bash +$umi g component foo --msg "Hello World" +``` + +自定义组件模板可以省略 `.tpl` 后缀名。你可以将 `index.ts.tpl` 简写为 `index.ts`,`component.tsx.tpl` 简写为 `component.tsx`。 + +组件生成器将生成与你的自定义模板文件夹相一致的内容,你可以根据需要添加更多的自定义模板文件。 + +##### 预设变量 + +|参数|默认值|说明| +|:-:|:-:|:-| +| `compName` | - | 当前组件的名称。如果执行 `pnpm umi g component foo`, `compName` 的值为 `Foo`。 | + +##### 回退 + +```bash +$umi g component foo --fallback +``` + +### RouteAPI 生成器 + +生成 routeAPI 功能的模板文件。 + +交互式生成: +```bash +$umi g api +✔ please input your api name: … starwar/people +Write: api/starwar/people.ts +``` + +直接生成: +```bash +$umi g api films +Write: api/films.ts +``` + +嵌套生成器: +```bash +$umi g api planets/[id] +Write: api/planets/[id].ts +``` + +批量生成: +```bash +$umi g api spaceships vehicles species +Write: api/spaceships.ts +Write: api/vehicles.ts +Write: api/species.ts +``` + +### Mock 生成器 + +生成 [Mock](./mock) 功能的模板文件,mock 的具体实现参考[文档](./mock)。 + +交互式生成: +```bash +$umi g mock +✔ please input your mock file name … auth +Write: mock/auth.ts +``` + +直接生成: +```bash +$umi g mock acl +Write: mock/acl.ts +``` + +嵌套生成: +```bash +$umi g mock users/profile +Write: mock/users/profile.ts +``` + +### Prettier 配置生成器 + +为项目生成 [prettier](https://prettier.io/) 配置,命令执行后,`umi` 会生成推荐的 prettier 配置和安装相应的依赖。 + +```bash +$umi g prettier +info - Write package.json +info - Write .prettierrc +info - Write .prettierignore +``` + +### Jest 配置生成器 + +为项目生成 [jest](https://jestjs.io/) 配置,命令执行后,`umi` 会生成 Jest 配置和安装相应的依赖。根据需要选择是否要使用 [@testing-library/react](https://www.npmjs.com/package/@testing-library/react) 做 UI 测试。 + +```bash +$umi g jest +✔ Will you use @testing-library/react for UI testing?! … yes +info - Write package.json +info - Write jest.config.ts +``` + +### Tailwind CSS 配置生成器 + +为项目开启 [Tailwind CSS](https://tailwindcss.com/) 配置,命令执行后,`umi` 会生成 Tailwind CSS 和安装相应的依赖。 + +```bash +$umi g tailwindcss +info - Write package.json +set config:tailwindcss on /Users/umi/playground/.umirc.ts +set config:plugins on /Users/umi/playground/.umirc.ts +info - Update .umirc.ts +info - Write tailwind.config.js +info - Write tailwind.css +``` + +### DvaJS 配置生成器 + +为项目开启 [Dva](https://dvajs.com/) 配置,命令执行后,`umi` 会生成 Dva 。 + +```bash +$umi g dva +set config:dva on /Users/umi/umi-playground/.umirc.ts +set config:plugins on /Users/umi/umi-playground/.umirc.ts +info - Update config file +info - Write example model +``` + +### Precommit 配置生成器 + +为项目生成 [precommit](https://typicode.github.io/husky) 配置,命令执行后,`umi` 会为我们添加 husky 和 Git commit message 格式校验行为,在每次 Git commit 前会将 Git 暂存区的代码默认格式化。 + +> 注意:如果是初始化出来的 `@umijs/max` 项目,通常不需要该生成器,因为已经配置好 husky 了 + +```bash +$umi g precommit +info - Update package.json for devDependencies +info - Update package.json for scripts +info - Write .lintstagedrc +info - Create .husky +info - Write commit-msg +info - Write pre-commit +``` diff --git a/docs/docs/docs/guides/getting-started.en-US.md b/docs/docs/docs/guides/getting-started.en-US.md new file mode 100644 index 000000000000..0f9bc7f6d30e --- /dev/null +++ b/docs/docs/docs/guides/getting-started.en-US.md @@ -0,0 +1,217 @@ +--- +order: -1 +toc: content +--- +# 快速上手 + +## 环境准备 + +首先得有 node,并确保 node 版本是 14 或以上。(推荐用 [nvm](https://github.com/nvm-sh/nvm) 来管理 node 版本,windows 下推荐用 [nvm-windows](https://github.com/coreybutler/nvm-windows)) + +mac 或 linux 下安装 nvm。 + +```bash +$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash +$ nvm -v +0.39.1 +``` + +安装 node。 + +``` +$ nvm install 16 +$ nvm use 16 +$ node -v +v16.10.0 +``` + +然后需要包管理工具。node 默认包含 npm,但也可以选择其他方案, + +- [pnpm](https://pnpm.io/installation), umi 团队推荐 +- [Yarn](https://yarnpkg.com/getting-started/install) + +安装 pnpm。 + +```bash +curl -fsSL https://get.pnpm.io/install.sh | sh - +$ pnpm -v +7.3.0 +``` + +## 创建项目 + +先找个地方建个空目录。 + +```bash +$ mkdir myapp && cd myapp +``` + +通过官方工具创建项目, + +PNPM + +```bash +$ pnpm dlx create-umi@latest +✔ Install the following package: create-umi? (Y/n) · true +✔ Pick Npm Client › pnpm +✔ Pick Npm Registry › taobao +Write: .gitignore +Write: .npmrc +Write: .umirc.ts +Copy: layouts/index.tsx +Write: package.json +Copy: pages/index.tsx +Copy: pages/users.tsx +Copy: pages/users/foo.tsx +> @ postinstall /private/tmp/sorrycc-vylwuW +> umi setup +info - generate files +``` + +BUN + +```bash +$ bunx create-umi +✔ Pick Umi App Template › Simple App +✔ Pick Npm Client › pnpm +✔ Pick Npm Registry › npm +Write: .gitignore +Write: .npmrc +Write: .umirc.ts +Write: package.json +Copy: src/assets/yay.jpg +Copy: src/layouts/index.less +Write: src/layouts/index.tsx +Copy: src/pages/docs.tsx +Copy: src/pages/index.tsx +Write: tsconfig.json +Copy: typings.d.ts +ready - Git initialized successfully +``` + +NPM + +```bash +$ npx create-umi@latest +Need to install the following packages: + create-umi@latest +Ok to proceed? (y) y +✔ Pick Umi App Template › Simple App +✔ Pick Npm Client › npm +✔ Pick Npm Registry › taobao +Write: .gitignore +Write: .npmrc +Write: .umirc.ts +Write: package.json +Copy: src/assets/yay.jpg +Copy: src/layouts/index.less +Write: src/layouts/index.tsx +Copy: src/pages/docs.tsx +Copy: src/pages/index.tsx +Write: tsconfig.json +Copy: typings.d.ts + +> postinstall +> umi setup +``` + +YARN + +```bash +$ yarn create umi +success Installed "create-umi@4.0.6" with binaries: + - create-umi +✔ Pick Umi App Template › Simple App +✔ Pick Npm Client › yarn +✔ Pick Npm Registry › taobao +Write: .gitignore +Write: .npmrc +Write: .umirc.ts +Write: package.json +Copy: src/assets/yay.jpg +Copy: src/layouts/index.less +Write: src/layouts/index.tsx +Copy: src/pages/docs.tsx +Copy: src/pages/index.tsx +Write: tsconfig.json +Copy: typings.d.ts +yarn install v1.22.18 +success Saved lockfile. +$ umi setup +info - generate files +``` + +注:使用 bun 初始化项目会更快,需要 bun >= `0.4.0` 版本。 + +国内建议选 **pnpm + taobao 源**,速度提升明显。这一步会自动安装依赖,同时安装成功后会自动执行 `umi setup` 做一些文件预处理等工作。 + +### 从模板创建项目 + +```bash + # 从 @umijs/electron-template 创建一个 electron 模板 + pnpm create umi --template electron +``` + +### 参数选项 + +使用 `create-umi` 创建项目时,可用的参数如下: + +| option | description | +| :------------: | :------------------------- | +| `--no-git` | 创建项目,但不初始化 Git | +| `--no-install` | 创建项目,但不自动安装依赖 | + +## 启动项目 + +执行 `pnpm dev` 命令, + +```bash +$ pnpm dev + ╔═════════════════════════════════════════════════════╗ + ║ App listening at: ║ + ║ > Local: https://127.0.0.1:8000 ║ +ready - ║ > Network: https://192.168.1.1:8000 ║ + ║ ║ + ║ Now you can open browser with the above addresses👆 ║ + ╚═════════════════════════════════════════════════════╝ +event - compiled successfully in 1121 ms (388 modules) +event - MFSU compiled successfully in 1308 ms (875 modules) +``` + +在浏览器里打开 [http://localhost:8000/](http://localhost:8000/),能看到以下界面, + +![](https://img.alicdn.com/imgextra/i2/O1CN01ufcj8M1Lpt1yXd8sy_!!6000000001349-2-tps-1372-1298.png) + +## 启用 Prettier(可选) + +如果需要用 prettier 做项目代码的自动格式化,执行 `pnpm umi g`, + +```bash +$ pnpm umi g +✔ Pick generator type › Enable Prettier -- Enable Prettier +info - Write package.json +info - Write .prettierrc +info - Write .prettierignore +info - Install dependencies with pnpm +``` + +## 部署发布 + +执行 `pnpm build` 命令, + +```bash +> umi build +event - compiled successfully in 1179 ms (567 modules) +event - build index.html +``` + +产物默认会生成到 `./dist` 目录下, + +``` +./dist +├── index.html +├── umi.css +└── umi.js +``` + +完成构建后,就可以把 dist 目录部署到服务器上了。 diff --git a/docs/docs/docs/guides/lint.en-US.md b/docs/docs/docs/guides/lint.en-US.md new file mode 100644 index 000000000000..8618e58eb3a2 --- /dev/null +++ b/docs/docs/docs/guides/lint.en-US.md @@ -0,0 +1,142 @@ +--- +order: 13 +toc: content +--- +# 编码规范 + +我们通常会在项目中使用 ESLint、Stylelint 来协助我们把控编码质量,为了实现低成本、高性能、更稳定地接入上述工具,Umi 提供了开箱即用的 Lint 能力,包含以下特性: + +1. **推荐配置**:提供 ESLint 及 Stylelint 推荐配置,可以直接继承使用 +2. **统一的 CLI**:提供 `umi lint` CLI,集成式调用 ESLint 和 Stylelint +3. **规则稳定**:始终确保规则的稳定性,不会出现上游配置更新导致存量项目 lint 失败的情况 + +其中,ESLint 配置具备如下特点: + +1. **仅质量相关**:我们从数百条规则中筛选出数十条与编码质量相关的规则进行白名单开启,回归 Lint 本质,且不会与 Prettier 的规则冲突 +2. **性能优先**:部分 TypeScript 的规则实用性低但项目全量编译的成本却很高,我们对这些规则进行禁用以提升性能 +3. **内置常用插件**:包含 react、react-hooks、@typescript/eslint、jest,满足日常所需 + +另外,Stylelint 配置还内置 CSS-in-JS 支持,可以检测出 JS 文件中的样式表语法错误。听起来很有吸引力?来看看如何接入吧。 + +## 使用方式 +### 安装 + +为了节省安装体积,目前仅在 Umi Max 中内置了 Lint 模块,使用 `max lint` 来执行 lint 过程。**如果你使用的是 Umi,需要先安装 `@umijs/lint`**: + +```bash +$ npm i @umijs/lint -D +# or +$ pnpm add @umijs/lint -D +``` + +然后安装 ESLint 及 Stylelint: + +```bash +$ npm i eslint stylelint -D +# or +$ pnpm add eslint stylelint -D +``` + +### 启用配置 + +在 `.eslintrc.js` 及 `.stylelintrc.js` 里继承 Umi 提供的配置: + +```js +// .eslintrc.js +module.exports = { + // Umi 项目 + extends: require.resolve('umi/eslint'), + + // Umi Max 项目 + extends: require.resolve('@umijs/max/eslint'), +} + +// .stylelintrc.js +module.exports = { + // Umi 项目 + extends: require.resolve('umi/stylelint'), + + // Umi Max 项目 + extends: require.resolve('@umijs/max/stylelint'), +} +``` + +在配置文件创建完毕后,我们其实已经可以通过 `eslint`、`stylelint` 命令来执行 lint 了,但我们仍然推荐使用 `umi lint` 命令,以获得更便捷的体验。 + +### CLI + +`umi lint` 命令的用法如下: + +```bash +$ umi lint [glob] [--fix] [--eslint-only] [--stylelint-only] [--cssinjs] +``` + +参数说明: + +```bash + [glob]: 可选,指定要 lint 的文件,默认为 `{src,test}/**/*.{js,jsx,ts,tsx,css,less}` + --quiet: 可选,禁用 `warn` 规则的报告,仅输出 `error` + --fix: 可选,自动修复 lint 错误 + --eslint-only: 可选,仅执行 ESLint + --stylelint-only: 可选,仅执行 Stylelint + --cssinjs: 可选,为 Stylelint 启用 CSS-in-JS 支持 +``` + +通常来说,直接执行 `umi lint` 应该就能满足大部分情况。 + +## 与 Git 工作流结合 + +我们也推荐使用 [lint-staged](https://github.com/okonet/lint-staged#readme) 和 [Husky](https://typicode.github.io/husky/),将 `umi lint` 与 Git 工作流结合使用,以便在**提交代码时**自动 lint **本次变更**的代码。 + +### lint-staged + +lint-staged 用来驱动 `umi lint` 命令,每次仅将变更的内容交给 `umi lint` 进行检查。 + +安装方式: + +```bash +$ npm i lint-staged -D +#or +$ pnpm add lint-staged -D +``` + +在 `package.json` 中配置 lint-staged: + +```diff +{ ++ "lint-staged": { ++ "*.{js,jsx,ts,tsx,css,less}": [ ++ "umi lint" ++ ] ++ } +} +``` + +此时如果执行 `git add sample.js` 后,再执行 `npx lint-staged`,就能实现仅检查 `sample.js` 本次的变更了。 + +### Husky + +Husky 用来绑定 Git Hooks、在指定时机(例如 `pre-commit`)执行我们想要的命令,安装方式请参考 Husky 文档:https://typicode.github.io/husky/#/?id=automatic-recommended + +初始化完成后,需要手动修改 `.husky/pre-commit` 文件的内容: + +```diff +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +- npm test ++ npx lint-staged +``` + +至此大功告成,每次执行 `git commit` 命令的时候,`umi lint` 就能自动对本次变更的代码进行检查,在确保编码质量的同时也能确保执行效率。 + +## Prettier + +在启用 `umi lint` 的基础上,我们也建议与 [Prettier](https://prettier.io/docs/en/install.html) 一同使用,以确保团队的代码风格是基本一致的。 + +可参考 Prettier 文档将其配置到 lint-staged 中:https://prettier.io/docs/en/install.html#git-hooks + +## 附录 + +1. Umi 内置的 ESLint 规则列表:https://github.com/umijs/umi/blob/master/packages/lint/src/config/eslint/rules/recommended.ts +2. Umi 内置的 Stylelint 配置:https://github.com/umijs/umi/blob/master/packages/lint/src/config/stylelint/index.ts diff --git a/docs/docs/docs/guides/mfsu.en-US.md b/docs/docs/docs/guides/mfsu.en-US.md new file mode 100644 index 000000000000..7c39d8079cb8 --- /dev/null +++ b/docs/docs/docs/guides/mfsu.en-US.md @@ -0,0 +1,154 @@ +--- +order: 19 +toc: content +--- +# MFSU + +## 什么是 MFSU + +MFSU 是一种基于 webpack5 新特性 Module Federation 的打包提速方案。其核心的思路是通过分而治之,将应用源代码的编译和应用依赖的编译分离,将变动较小的应用依赖构建为一个 Module Federation 的 remote 应用,以免去应用热更新时对依赖的编译。 + +开启 MFSU 可以大幅减少热更新所需的时间了;因此我们在 Umi 的项目中默认开启了 MFSU 功能。当然你也可以通过配置 `mfsu: false` 来关闭它。 + +## MFSU 的两种策略 + +MFSU 最关键的一点是如何将应用代码的实际使用的依赖分析出来。根据不同的分析方式 MFSU 有两种工作方式。 + +### normal 策略 (编译时分析) + +采用以下配置启用 + +```ts {2} +mfsu: { + strategy: 'normal', +} +``` + +现代前端项目中的代码都需要经过转译(transpile),才会在生产环境中使用。在转译的过程中转译器(比如:babel) 就会在代码中插入新的依赖。这些插入的依赖在项目代码层面是不可见, 但通过转译的插件才收集到。 + +MFSU 编译时分析的工作方式,先对应用项目源码单独进行编译,编译的同时收集项目的本身依赖和编译引入的依赖。待项目代码编译完成以后,使用收集到的结果继续进行项目依赖部分的代码的构建。 + +从下图中以 React 引用的构建举例可以看出,整个过程是串行的。 + +![normal-process](https://gw.alipayobjects.com/mdn/rms_ffea06/afts/img/A*VRdhQZDag1UAAAAAAAAAAAAAARQnAQ) + +### eager 策略 (扫描方式) + +```ts {2} +mfsu: { + strategy: 'eager', +} +``` + +和编译时分析的方式不同,扫描分析的方式会先读取项目中的所有源代码文件,然后通过静态分析的方式获取项目的依赖。这个过程非常的快,在一个有 17 万行代码,1400 多个文件项目中,分析一次只需要 700ms 左右。如此快速的分析的代价时,收集到的依赖会缺失后面项目代码编译插入的依赖;这部分的依赖最终和项目代码一起编译打包。 + +分析完项目依赖之后,Umi 会拿着这份依赖信息,并行的去进行项目代码的编译和依赖的编译。 + +从下图可以看出,编译部分是并行。 + +![eager-process](https://gw.alipayobjects.com/mdn/rms_ffea06/afts/img/A*XtZ1Spa9hMEAAAAAAAAAAAAAARQnAQ) + + +## 两种构建工具 + +MFSU 支持使用 Webpack 或者 esbuild 构建项目的依赖。默认配置使用 Webpack,和 Webpack 生态很好的兼容。 +Esbuild 通过 `mfsu: { esbuild: true }` 来开启,享受 Esbuild 的高效的构建速度。 + + +## 如何选择 + +**编译时分析**的好处是收集的依赖是完整的,项目代码和依赖代码的构建打包完全分离;再项目代码修改以后只需要构建项目代码部分。缺点也很明显,构建的过程是串行的。 + +**扫描的方式**的优点在于耗时的代码构建都是并行的,对于较大项目的冷启动时间改善非常明显。缺点则是有一部分运行时依赖会和项目代码一起编译。 + +基于优缺点的分析,给出以下建议 + +- 如果不使用 Module Federation 的功能的话,项目依赖变动不频繁,建议先尝试 esbuild 构建 +- 如果在 mono repo 项目中, 推荐使用 "normal" 策略; 推荐开启配置 ["monoreporedirect"](../api/config#monoreporedirect) +- 如果你的项目较大,项目代码基数较大,推荐使用 "eager" 策略 +- 如果项目刚刚启动,会频繁的改动依赖,推荐使用 "eager" 策略 +- 其他类型的项目则随意选择。 + +## 常见问题 + +### 依赖缺失 + +```bash /lodash.capitalize/ +error - [MFSU][eager] build worker failed AssertionError [ERR_ASSERTION]: filePath not found of lodash.capitalize +``` + +检查你的依赖确保,对应的依赖已经安装。( 如例子中的 `lodash.capitalize`) + +### React 多实例问题 + +在浏览器中有如下报错 + +![multi-react-instance](https://gw.alipayobjects.com/mdn/rms_ffea06/afts/img/A*ScIJTZobWE4AAAAAAAAAAAAAARQnAQ) + +根因在某些复杂场景下,React 的代码被打包多份,在运行时产出了多个 React 实例。解法通过 Module Federation 的 `shared` 配置来避免多实例的出现。 +如果有其他依赖出现多实例的问题,可以通过类似的方式解决。 + +```ts {3-5} +mfsu: { + shared: { + react: { + singleton: true, + }, + }, +}, +``` +:::info{title=⚠️} +如果开启了 [MF插件](../max/mf), 需要开启 `shared`,请[参考](../max/mf#和-mfsu-一起使用)。 +::: + +### externals script 兼容问题 + +如果项目依赖 a,a 依赖 b,而项目配置了 b 的 script 类型的 externals 如下。 + +```ts +externals: { + b: ['script https://cdn/b.js', b] +} +``` + +在开启 MFSU 时会报错。 + +```ts +import * as b from 'b'; +console.log(b); +``` + +上述代码的 b 正常应该是 Module 信息,拿到的却是 `Promise`。我理解这是 webpack 的问题,没有处理好 externals script 和 module federation 之间的兼容问题,可能也是因为 externals script 很少有人知道和在使用。 + +解法是不要和 MFSU 混着用,只在 `process.env.NODE_ENV === 'production'` 时开启。 + +```ts +externals: { + ...(process.env.NODE_ENV === 'production' ? {b: ['script https://cdn/b.js', b]} : {}) +} +``` + +### 依赖环问题 + +#### 场景 1 + +在使用 monorepo 时 ,项目依赖了 A 包,A 依赖了 B 包,而 B 包是项目 monorepo 中子包提供。这行就形成项目源码依赖 A ,A 又重新依赖项目源码的情况。 +这种情况建议使用 MFSU 的 exclude 配置。 + +```ts {2-4} +mfsu: { + exclude: [ + 'B' + ] +} +``` + +#### 场景 2 + +项目的某个依赖的不合理的实现,项目中依赖了 Bigfish 插件相关的功能(即:引用了 `.umi` 目录下的内容 );在开启 MFSU 后项目可能不能正常编译;解法和场景 1 类似配置。将这个包配置进 `mfsu.exclude` 字段中。 + + +### worker 兼容问题 + +如果项目代码需要在 Worker 中使用,那么需要将 Worker 需要的依赖添加到 MFSU 的 [`exclude` 配置中](../api/config#mfsu)。 +Worker 相关依赖只能通过这样方式来绕过,因为 Module Federation 是通过 `window` 对象来共享模块的,所以在 worker 中不能使用 Module Federation 中的模块。 diff --git a/docs/docs/docs/guides/mock.en-US.md b/docs/docs/docs/guides/mock.en-US.md new file mode 100644 index 000000000000..3bc7002f676e --- /dev/null +++ b/docs/docs/docs/guides/mock.en-US.md @@ -0,0 +1,159 @@ +--- +order: 5 +toc: content +--- +# Mock + +Umi 提供了开箱即用的 Mock 功能,能够用方便简单的方式来完成 Mock 数据的设置。 + +:::info{title=💡} +什么是 Mock 数据:在前后端约定好 API 接口以后,前端可以使用 Mock 数据来在本地模拟出 API 应该要返回的数据,这样一来前后端开发就可以同时进行,不会因为后端 API +还在开发而导致前端的工作被阻塞。 +::: + +## 目录约定 + +Umi 约定 `/mock` 目录下的所有文件为 [Mock 文件](#mock-文件),例如这样的目录结构: + +```text +. +├── mock + ├── todos.ts + ├── items.ts + └── users.ts +└── src + └── pages + └── index.tsx +``` + +则 `/mock` 目录中的 `todos.ts`, `items.ts` 和 `users.ts` 就会被 Umi 视为 [Mock 文件](#mock-文件) 来处理。 + +## Mock 文件 + +Mock 文件默认导出一个对象,而对象的每个 Key 对应了一个 Mock 接口,值则是这个接口所对应的返回数据,例如这样的 Mock 文件: + +```ts +// ./mock/users.ts + +export default { + + // 返回值可以是数组形式 + 'GET /api/users': [ + { id: 1, name: 'foo' }, + { id: 2, name: 'bar' } + ], + + // 返回值也可以是对象形式 + 'GET /api/users/1': { id: 1, name: 'foo' }, + +} +``` + +就声明了两个 Mock 接口,透过 `GET /api/users` 可以拿到一个带有两个用户数据的数组,透过 `GET /api/users/1` 可以拿到某个用户的模拟数据。 + +### 请求方法 + +当 HTTP 的请求方法是 GET 时,可以省略方法部分,只需要路径即可,例如: + +```ts +// ./mock/users.ts + +export default { + + '/api/users': [ + { id: 1, name: 'foo' }, + { id: 2, name: 'bar' } + ], + + '/api/users/1': { id: 1, name: 'foo' }, + +} +``` + +也可以用不同的请求方法,例如 `POST`,`PUT`,`DELETE`: + +```ts +// ./mock/users.ts + +export default { + + 'POST /api/users': { result: 'true' }, + + 'PUT /api/users/1': { id: 1, name: 'new-foo' }, + +} +``` + +### 自定义函数 + +除了直接静态声明返回值,也可以用函数的方式来声明如何计算返回值,例如: + +```ts +export default { + + 'POST /api/users/create': (req, res) => { + // 添加跨域请求头 + res.setHeader('Access-Control-Allow-Origin', '*'); + res.end('ok'); + } + +} +``` + +关于 `req` 和 `res` 的 API 可参考 [Express@4 官方文档](https://expressjs.com/en/api.html) 来进一步了解。 + +### defineMock + +另外,也可以使用 `defineMock` 类型帮助函数来提供编写 mock 对象的代码提示,如: +```ts +import { defineMock } from "umi"; + +export default defineMock({ + "/api/users": [ + { id: 1, name: "foo" }, + { id: 2, name: "bar" }, + ], + "/api/users/1": { id: 1, name: "foo" }, + "GET /api/users/2": (req, res) => { + res.status(200).json({ id: 2, name: "bar" }); + }, +}); +``` +`defineMock` 仅仅提供类型提示,入参与出参完全一致。 +## 关闭 Mock + +Umi 默认开启 Mock 功能,如果不需要的话可以从配置文件关闭: + +```ts +// .umirc.ts + +export default { + mock: false, +}; +``` + +或是用环境变量的方式关闭: + +```bash +MOCK=none umi dev +``` + +## 引入 Mock.js + +在 Mock 中我们经常使用 [Mock.js](http://mockjs.com/) 来帮我们方便地生成随机的模拟数据。如果你使用了 Umi 的 Mock +功能,建议你搭配这个库来提升模拟数据的真实性: + +```ts +import mockjs from 'mockjs'; + +export default { + // 使用 mockjs 等三方库 + 'GET /api/tags': mockjs.mock({ + 'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }], + }), +}; +``` + +## 其他配置 + +关于 Mock 功能完整的其他配置项,请在文档的 [配置](../api/config#mock) 章节中查看。 diff --git a/docs/docs/docs/guides/mpa.en-US.md b/docs/docs/docs/guides/mpa.en-US.md new file mode 100644 index 000000000000..da85a58ade2a --- /dev/null +++ b/docs/docs/docs/guides/mpa.en-US.md @@ -0,0 +1,158 @@ +--- +order: 18 +toc: content +--- +# MPA 模式 + +Umi 支持传统 MPA 模式,此模式下,会将 `src/pages` 目录下 `*/index.[jt]sx?` 文件作为 webpack entry 进行打包,无路由,无 history,无 umi.js,满足比如 h5 研发、kitchen 插件研发等场景需要。 + +注意:此 MPA 模式和 Umi 3 的 MPA 模式的实现不同,Umi 4 是真 MPA,Umi 3 是 Mock 了路由渲染机制。各有利弊,Umi 4 的 MPA 将不能使用大量插件能力,仅适合当构建工具使用。 + +## 使用 + +mpa 为内置功能,通过配置即可开启。 + +```js +export default { + mpa: { + template: string, + getConfigFromEntryFile: boolean, + layout: string, + entry: object, + }, +} +``` + +MPA 的目录结构是 `src/pages/${dir}/index.tsx` ,每个文件夹 `${dir}` 会生成一个页面,文件夹内的 `index.tsx` 为页面的入口文件,示例见 [examples/mpa](https://github.com/umijs/umi/tree/master/examples/mpa) 。 + +配置项: + + - `template` : 产物 HTML 模板,如 `template/index.html` 将使用项目根目录开始寻找,对应路径的 `index.html` 作为产物 HTML 模板。 + - `getConfigFromEntryFile` : 从每个页面的入口文件(`src/*/index.tsx`)中读取页面独立配置。 + - `layout` : 全局默认 layout 。 + - `entry` : 每个入口文件的配置,如 `{ foo: { title: '...' } }` 可以配置 `src/foo/index.tsx` 页面的 `title` 属性。 + +## 约定的入口文件 + +默认的入口文件是 `src/pages` 目录下的 `*/index.[jt]sx?` 文件。 + +比如: + +``` ++ src/pages + - foo/index.tsx + - bar/index.tsx + - hoo.tsx +``` + +那么,`entry` 结构为: + +```ts +{ + foo: 'src/pages/foo/index.tsx', + bar: 'src/pages/bar/index.tsx' +} +``` + +构建之后,会同时为每个入口文件生成相应的 HTML 文件,此时产物为 `foo.html` 和 `bar.html` 。 + +### 页面级配置 + +### config.json + +约定通过入口文件同层级的 `config.json` 声明配置,比如如下目录结构: + +``` ++ src/pages + + foo + - index.tsx + - config.json +``` + +`foo/config.json` 配置了该页面的独立 `layout` 布局和 `title` 标题: + +```json +{ + "layout": "@/layouts/bar.ts", + "title": "foooooo" +} +``` + +目前默认支持的配置项包括: + +* **template**:模板路径,可参考 [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) 的模板写法,通过 lodash template 语法使用变量。 +* **layout**:页面布局,建议以 `@/` 开头引用 src 目录下的文件。 +* **title**:页面标题,默认是入口文件所在的目录名。 +* **mountElementId**:页面渲染时,挂载到节点的 id,默认是 `root` 。 + +### getConfigFromEntryFile + +Umi 还试验性地支持另一种配置读取方式,通过配置 `mpa: { getConfigFromEntryFile: true }` 开启。 + +此时,你可以不使用 `config.json` ,而是在入口文件中通过 `export const config` 导出该页面的配置。 + +比如: + +```ts +// src/pages/foo/index.tsx +export const config = { + layout: '@/layouts/bar.ts', + title: 'foooooo', +} +``` + +### entry + +在 `.umirc.ts` 中也可以配置每个页面: + +```ts + mpa: { + entry: { + foo: { title: 'foo title' } + } + } +``` + +### 按需启动 + +支持通过设置 `env.MPA_FILTER` 来指定需要启动的页面,以提高构建速度: + +```text +# file .env +# 只会启动 bar、foo 这两个页面 +MPA_FILTER=bar,foo +``` + +## 渲染 + +默认渲染方式为 react,入口文件只需导出 react 组件,即可进行渲染,无需自行写 `ReactDOM.render` 逻辑。 + +```tsx +export default function Page() { + return
Hello
+} +``` + +默认启用 React 18,如果需要 React 17 的渲染方式,请在项目中安装 React 17 的依赖,框架会自动适配 React 版本。 + +```bash +$ pnpm i react@17 react-dom@17 +``` + +## 模板 + +默认模板如下: + +```html + + + + <%= title %> + + +
+ + +``` + +通过 `template` 配置自定义全局 HTML 模板 ,也可以进行页面级配置定义不同页面使用不同的模板,请确保变量至少包含 `<%= title %>` 和 `<%= mountElementId %>`。 diff --git a/docs/docs/docs/guides/plugins.en-US.md b/docs/docs/docs/guides/plugins.en-US.md new file mode 100644 index 000000000000..8b767334e6e8 --- /dev/null +++ b/docs/docs/docs/guides/plugins.en-US.md @@ -0,0 +1,175 @@ +--- +order: 16 +toc: content +--- +# 开发插件 +Umi 的核心就在于它的插件机制。基于 Umi 的插件机制,你可以获得扩展项目的编译时和运行时的能力。你可以利用我们提供的 [插件API](../api/plugin-api) 来自由地编写插件,进而实现修改代码打包配置、修改启动代码、约定目录结构、修改 HTML 等丰富的功能。 + +## 核心概念 +插件的本质就是一个方法,该方法接收了一个参数:api。在插件中,你可以调用 api 提供的方法进行一些 hook 的注册,随后 Umi 会在特定的时机执行这些 hook。 + +比如: +```js +import { IApi } from 'umi'; + +export default (api: IApi) => { + api.describe({ + key: 'changeFavicon', + config: { + schema(joi) { + return joi.string(); + }, + }, + enableBy: api.EnableBy.config + }); + api.modifyConfig((memo)=>{ + memo.favicons = api.userConfig.changeFavicon; + return memo; + }); +}; +``` +这个插件的作用是根据用户配置的 changeFavicon 值来更改配置中的 favicons,(一个很简单且没有实际用途的例子XD)。可以看到插件其实就是一个接收了参数 api 的方法。在这个方法中,我们调用了 `api.modifyConfig` 注册了一个 hook: `(memo)=>{...}`。当你在配置中配置了 `changeFavicon` 之后, Umi 会注册该插件。在 Umi 收集配置的生命周期里,我们在插件里注册的 hook 将被执行,此时配置中的 `favicon` 就会被修改为用户配置中的 `changeFavicon` 。 + +### plugin 和 preset +preset 的作用是预设一些插件,它通常用来注册一批 presets 和 plugins。在 preset 中,上述提到的接受 api 的方法可以有返回值,该返回值是一个包含 plugins 和 presets 属性的对象,其作用就是注册相应的插件或者插件集。 + +比如: +```js +import { IApi } from 'umi'; + +export default (api: IApi) => { + return { + plugins: ['./plugin_foo','./plugin_bar'], + presets: ['./preset_foo'] + } +}; +``` +它们的注册顺序是值得注意的:presets 始终先于 plugins 注册。Umi 维护了两个队列分别用来依次注册 presets 和 plugins,这个例子中的注册的 `preset_foo` 将被置于 presets 队列队首,而 `plugin_foo` 和 `plugin_bar` 将被依次置于 plugins 队列队尾。这里把 preset 放在队首的目的在于保证 presets 之间的顺序和关系是可控的。 + +另外一个值得注意的点是:在 plugin 中,你也可以 return 一些 plugins 或者 presets,但是 Umi 不会对它做任何事情。 + +### 插件的 id 和 key +每个插件都对应一个 id 和 key。 + +id 是插件所在路径的简写,作为插件的唯一标识;而 key 则是用于插件配置的键名。 + +比如插件 `node_modules/@umijs/plugin-foo/index.js` ,通常来说,它的 id 是 `@umijs/plugin-foo` , key 是 `foo`。此时就允许开发者在配置中来配置键名为 `foo` 的项,用来对插件进行配置。 + +## 启用插件 +插件有两种启用方式: 环境变量中启用和配置中启用。(与 `umi@3` 不同,我们不再支持对 `package.json` 中依赖项的插件实现自动启用) + +注意:这里的插件指的是第三方插件,Umi 的内置插件统一在配置中通过对其 key 进行配置来启用。 + +### 环境变量 +还可以通过环境变量 `UMI_PRESETS` 和 `UMI_PLUGINS` 注册额外插件。 +比如: +```shell +$ UMI_PRESETS = foo/preset.js umi dev +``` +注意: 项目里不建议使用,通常用于基于 Umi 的框架二次封装。 + +### 配置 +在配置里通过 `presets` 和 `plugins` 配置插件,比如: +```js +export default { + presets: ['./preset/foo','bar/presets'], + plugins: ['./plugin', require.resolve('plugin_foo')] +} +``` +配置的内容为插件的路径。 + +### 插件的顺序 +Umi 插件的注册遵循一定的顺序: +- 所有的 presets 都先于 plugins 被注册。 +- 内置插件 -> 环境变量中的插件 -> 用户配置中的插件 +- 同时注册(同一个数组里)的插件按顺序依次注册。 +- preset 中注册的 preset 立即执行, 注册的 plugin 最后执行。 + +## 禁用插件 +有两种方式禁用插件 +### 配置 key 为 false +比如: +```js +export default{ + mock: false +} +``` +会禁用 Umi 内置的 mock 插件。 + +### 在插件中禁用其他插件 +可通过 `api.skipPlugins(pluginId[])` 的方式禁用,详见[插件 API](../api/plugin-api)。 + +## 查看插件注册情况 +### 命令行 +```shell +$ umi plugin list +``` + +## 配置插件 +通过配置插件的 key 来配置插件,比如: +```js +export default{ + mock: { exclude: ['./foo'] } +} +``` +这里 mock 就是 Umi 内置插件 mock 的 key。 + +再比如我们安装一个插件 `umi-plugin-bar`, 其 key 默认是 `bar`, 就可以配置: +```js +export default{ + bar: { ... } +} +``` + +### 插件 key 的默认命名规则 +如果插件是一个包的话,key 的默认值将是去除前缀的包名。比如 `@umijs/plugin-foo` 的 key 默认为 `foo`, `@alipay/umi-plugin-bar` 的 key 默认为 `bar`。值得注意的是,该默认规则要求你的包名符合 Umi 插件的命名规范。 + +如果插件不是一个包的话,key 的默认值将是插件的文件名。比如 `./plugins/foo.js` 的 key 默认为 `foo` 。 + +为了避免不必要的麻烦,我们建议你为自己编写的插件显式地声明其 key。 + +## Umi 插件的机制及其生命周期 + +![Umi 插件机制](https://gw.alipayobjects.com/mdn/rms_ffea06/afts/img/A*GKNdTZgPQCIAAAAAAAAAAAAAARQnAQ) + +### 生命周期 + +- init stage: 该阶段 Umi 将加载各类配置信息。包括:加载 `.env` 文件; require `package.json` ;加载用户的配置信息; resolve 所有的插件(内置插件、环境变量、用户配置依次进行)。 +- initPresets stage: 该阶段 Umi 将注册 presets。presets 在注册的时候可以通过 `return { presets, plugins }` 来添加额外的插件。其中 presets 将添加到 presets 队列的队首,而 plugins 将被添加到 plugins 队列的队尾。 +- initPlugins stage: 该阶段 Umi 将注册 plugins。这里的 plugins 包括上个阶段由 presets 添加的额外的 plugins, 一个值得注意的点在于: 尽管 plugins 也可以 `return { presets, plugins }` ,但是 Umi 不会对其进行任何操作。插件的 init 其实就是执行插件的代码(但是插件的代码本质其实只是调用 api 进行各种 hook 的注册,而 hook 的执行并非在此阶段执行,因此这里叫插件的注册)。 +- resolveConfig stage: 该阶段 Umi 将整理各个插件中对于 `config schema` 的定义,然后执行插件的 `modifyConfig` 、`modifyDefaultConfig`、 `modifyPaths` 等 hook,进行配置的收集。 +- collectionAppData stage: 该阶段 Umi 执行 `modifyAppData` hook,来维护 App 的元数据。( `AppData` 是 `umi@4` 新增的 api ) +- onCheck stage: 该阶段 Umi 执行 `onCheck` hook。 +- onStart stage: 该阶段 Umi 执行 `onStart` hook。 +- runCommand stage: 该阶段 Umi 运行当前 cli 要执行的 command,(例如 `umi dev`, 这里就会执行 dev command)Umi 的各种核心功能都在 command 中实现。包括我们的插件调用 api 注册的绝大多数 hook。 + +以上就是 Umi 的插件机制的整体流程。 + +### `register()` 、 `registerMethod()` 以及 `applyPlugins()` + +`register()` 接收一个 key 和 一个 hook,它维护了一个 `key-hook[]` 的 map,每当调用 `register()` 的时候,就会为 key 额外注册一个 hook。 + +`register()` 注册的 hooks 供 applyPlugins 使用。 这些 hook 的执行顺序参照 [tapable](https://github.com/webpack/tapable)。 + +`registerMethod()` 接收一个 key 和 一个 fn,它会在 api 上注册一个方法。如果你没有向 `registerMethod()` 中传入 fn,那么 `registerMethod()` 会在 api 上注册一个“注册器”: 它会将 `register()` 传入 key 并柯里化后的结果作为 fn 注册到 api 上。这样就可以通过调用这个“注册器”,快捷地为 key 注册 hook 了。 + +关于上述 api 的更具体的使用,请参照[插件 API](../api/plugin-api)。 + +### PluginAPI 的原理 +Umi 会为每个插件赋予一个 PluginAPI 对象,这个对象引用了插件本身和 Umi 的 service。 + +Umi 为 PluginAPI 对象的 `get()` 方法进行了 proxy,具体规则如下: +- pluginMethod: 如果 prop 是 Umi 所维护的 `pluginMethods[]` ( `通过 registerMethod()` 注册的方法 )中的方法,则返回这个方法。 +- service props: 如果 prop 是 serviceProps 数组中的属性(这些属性是 Umi 允许插件直接访问的属性),则返回 service 对应的属性。 +- static props: 如果 prop 是参数 staticProps 数组中的属性(这些属性是静态变量,诸如一些类型定义和常量),则将其返回。 +- 否则返回 api 的属性。 + +因此,Umi 提供给插件的 api 绝大多数都是依靠 `registerMethod()` 来实现的,你可以直接使用我们的这些 api 快速地在插件中注册 hook。这也是 Umi 将框架和功能进行解耦的体现: Umi 的 service 只提供插件的管理功能,而 api 都依靠插件来提供。 + +### preset-umi +`umi-core` 提供了一套插件的注册及管理机制。而 Umi 的核心功能都靠 [preset-umi](https://github.com/umijs/umi/tree/master/packages/preset-umi) 来实现。 + +`preset-umi` 其实就是内置的一个插件集,它提供的插件分为三大类: +- registerMethods 这类插件注册了一些上述提到的“注册器”,以供开发者快速地注册 hook,这类方法也占据了 PluginAPI 中的大多数。 +- features 这类插件为 Umi 提供了一些特性,例如 appData、lowImport、mock 等。 +- commands 这类插件注册了各类 command, 提供了 Umi CLI 的各种功能。Umi 能够在终端中正常运行,依靠的就是 command 提供的功能。 diff --git a/docs/docs/docs/guides/prepare.en-US.md b/docs/docs/docs/guides/prepare.en-US.md new file mode 100644 index 000000000000..b6af98c8bd89 --- /dev/null +++ b/docs/docs/docs/guides/prepare.en-US.md @@ -0,0 +1,71 @@ +--- +order: 1 +toc: content +--- + +# 开发环境 + +本文将带领你从零开始在本地搭建一个 Umi.js 项目的开发环境。 + +## Nodejs + +Umi.js 需要使用 [Node.js](https://nodejs.org/zh-cn/) 来进行开发,因此请先确保电脑已经安装了 Node.js 且版本在 14 以上。 + +:::info{title=💡} +如果你是 macOS 用户,建议使用 [nvm](https://github.com/nvm-sh/nvm) 来管理 Node.js 的版本; +Windows 用户建议使用 [nvm-windows](https://github.com/coreybutler/nvm-windows) 。 +::: + +本文将在 macOS 或 Linux 环境下使用 [nvm](https://github.com/nvm-sh/nvm) 安装 [Node.js](https://nodejs.org/zh-cn/) : + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash +nvm -v + +0.39.1 +``` + +安装完成 [nvm](https://github.com/nvm-sh/nvm) 之后,使用以下命令来安装 [Node.js](https://nodejs.org/zh-cn/) : + +```bash +nvm install 16 +nvm use 16 +``` + +安装完成后,使用以下命令来检查是否安装成功并且安装了正确的版本: + +```bash +node -v + +v16.14.0 +``` + +## 依赖管理 + +Node 安装完成后会自带 [npm](https://www.npmjs.com/) 依赖管理工具,但 Umi.js 推荐使用 [pnpm](https://pnpm.io/) 来管理依赖: + +```bash +curl -fsSL https://get.pnpm.io/install.sh | sh - +``` + +安装完成后,可以用以下命令检查是否安装成功: + +```bash +pnpm -v + +7.3.0 +``` + +## IDE + +安装完 [Node.js](https://nodejs.org/zh-cn/) 及 [pnpm](https://pnpm.io/) (或其他依赖管理工具) 后,你需要使用一个自己习惯的 IDE 或文本编辑器来编写代码。如果你还没有习惯的 IDE,可以从下方挑选一个: + +1. [Visual Studio Code](https://code.visualstudio.com/) (推荐) +2. [WebStorm](https://www.jetbrains.com/webstorm/) (推荐) +3. [IntelliJ IDEA](https://www.jetbrains.com/idea/) +4. [Sublime Text](https://www.sublimetext.com/) +5. [Atom](https://atom.io/) + +## 下一步 + +恭喜你!你的本地环境已经准备好开始开发 Umi.js 项目了,马上前往 [脚手架](boilerplate) 学习如何使用 Umi.js 脚手架快速启动一个项目吧!🎉 diff --git a/docs/docs/docs/guides/proxy.en-US.md b/docs/docs/docs/guides/proxy.en-US.md new file mode 100644 index 000000000000..47b6e04c6b29 --- /dev/null +++ b/docs/docs/docs/guides/proxy.en-US.md @@ -0,0 +1,36 @@ +--- +order: 6 +toc: content +--- +# 代理 + +> 代理也称网络代理,是一种特殊的网络服务,允许一个终端(一般为客户端)通过这个服务与另一个终端(一般为服务器)进行非直接的连接。- [维基百科](https://zh.wikipedia.org/wiki/%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8) + +在项目开发(dev)中,所有的网络请求(包括资源请求)都会通过本地的 server 做响应分发,我们通过使用 [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) 中间件,来代理指定的请求到另一个目标服务器上。如请求 `fetch('/api')` 来取到远程 `http://jsonplaceholder.typicode.com/` 的数据。 + +要实现上述的需求我们只需要在配置文件中使用 proxy 配置: + +```ts +export default { + proxy: { + '/api': { + 'target': 'http://jsonplaceholder.typicode.com/', + 'changeOrigin': true, + 'pathRewrite': { '^/api' : '' }, + }, + }, +} +``` + +上述配置表示,将 `/api` 前缀的请求,代理到 `http://jsonplaceholder.typicode.com/`,替换请求地址中的 `/api` 为 `''`,并且将请求来源修改为目标url。如请求 `/api/a`,实际上是请求 `http://jsonplaceholder.typicode.com/a`。 + +一般我们使用这个能力来解决开发中的跨域访问问题。由于浏览器(或者 webview)存在同源策略,之前我们会让服务端配合使用 Cross-Origin Resource Sharing (CORS) 策略来绕过跨域访问问题。现在有了本地的 node 服务,我们就可以使用代理来解决这个问题。 + +> XMLHttpRequest cannot load https://api.example.com. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access. + +原理其实很简单,就是浏览器上有跨域问题,但是服务端没有跨域问题。我们请求同源的本地服务,然后让本地服务去请求非同源的远程服务。需要注意的是,请求代理,代理的是请求的服务,不会直接修改发起的请求 url。它只是将目标服务器返回的数据传递到前端。所以你在浏览器上看到的请求地址还是 `http://localhost:8000/api/a`。 + +值得注意的是 proxy 暂时只能解开发时(dev)的跨域访问问题,可以在部署时使用同源部署。如果在生产上(build)发生跨域问题的话,可以将类似的配置转移到 Nginx 容器上。 + + + diff --git a/docs/docs/docs/guides/routes.en-US.md b/docs/docs/docs/guides/routes.en-US.md new file mode 100644 index 000000000000..ed064d37330d --- /dev/null +++ b/docs/docs/docs/guides/routes.en-US.md @@ -0,0 +1,479 @@ +--- +order: 3 +toc: content +--- + +# 路由 + +在 Umi 应用是[单页应用](https://en.wikipedia.org/wiki/Single-page_application),页面地址的跳转都是在浏览器端完成的,不会重新请求服务端获取 html,html 只在应用初始化时加载一次。所有页面由不同的组件构成,页面的切换其实就是不同组件的切换,你只需要在配置中把不同的路由路径和对应的组件关联上。 + +## 路由类型配置 + +请参考 [history](../api/config#history) 配置。 + +## 配置路由 + +在配置文件中通过 `routes` 进行配置,格式为路由信息的数组。 + +比如: + +```ts +// .umirc.ts +export default { + routes: [ + { path: '/', component: 'index' }, + { path: '/user', component: 'user' }, + ], +} +``` + +Umi 4 默认按页拆包,从而有更快的页面加载速度,由于加载过程是异步的,所以往往你需要编写 [`loading.tsx`](./directory-structure#loadingtsxjsx) 来给项目添加加载样式,提升用户体验。 + +:::info{title=💡} +你可以在 Chrome Devtools > 网络 Tab 中将网络设置成低速,然后切换路由查看加载组件是否生效。 +::: + +### path + +* Type: `string` + +`path` 只支持两种占位符配置,第一种是动态参数 `:id` 的形式,第二种是 `*` 通配符,通配符只能出现路由字符串的最后。 + +✅ 以下是目前***支持***的路由路径配置形式: + +```txt +/groups +/groups/admin +/users/:id +/users/:id/messages +/files/* +/files/:id/* +``` + +❌ 以下是目前***不支持***的路由路径配置形式: +```txt +/users/:id? +/tweets/:id(\d+) +/files/*/cat.jpg +/files-* +``` + +### component + +* Type: `string` + +配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径。如果是相对路径,会从 `src/pages` 开始寻找。 + +如果指向 `src` 目录的文件,可以用 `@`,比如 `component: '@/layouts/basic'`,推荐使用 `@` 组织路由文件位置。 + +### routes + +配置子路由,通常在需要为多个路径增加 layout 组件时使用。 + +比如: + +```js +export default { + routes: [ + { path: '/login', component: 'login' }, + { + path: '/', + component: '@/layouts/index', + routes: [ + { path: '/list', component: 'list' }, + { path: '/admin', component: 'admin' }, + ], + }, + ], +} +``` + +在全局布局 `src/layouts/index` 中,通过 `` 来渲染子路由: + +```tsx +import { Outlet } from 'umi' + +export default function Page() { + return ( +
+ +
+ ) +} +``` + +这样,访问 `/list` 和 `/admin` 就会带上 `src/layouts/index` 这个 layout 组件。 + +### redirect + +* Type: `string` + +配置路由跳转。 + +比如: + +```js +export default { + routes: [ + { path: '/', redirect: '/list' }, + { path: '/list', component: 'list' }, + ], +} +``` + +访问 `/` 会跳转到 `/list`,并由 `src/pages/list` 文件进行渲染。 + +### wrappers + +* Type: `string[]` + +配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 +比如,可以用于路由级别的权限校验: + +```js +export default { + routes: [ + { path: '/user', component: 'user', + wrappers: [ + '@/wrappers/auth', + ], + }, + { path: '/login', component: 'login' }, + ] +} +``` + +然后在 `src/wrappers/auth` 中, + +```jsx +import { Navigate, Outlet } from 'umi' + +export default (props) => { + const { isLogin } = useAuth(); + if (isLogin) { + return ; + } else{ + return ; + } +} +``` + +这样,访问 `/user`,就通过 `auth` 组件做权限校验,如果通过,渲染 `src/pages/user`,否则跳转到 `/login`。 + + +:::info{title=🚨} +`wrappers` 中的每个组件会给当前的路由组件增加一层嵌套路由,如果你希望路由结构不发生变化,推荐使用高阶组件。先在高阶组件中实现 wrapper 中的逻辑,然后使用该高阶组件装饰对应的路由组件。 +::: + +举例: + +```jsx +// src/hocs/withAuth.tsx +import { Navigate } from 'umi' + +const withAuth = (Component) => ()=>{ + const { isLogin } = useAuth(); + if (isLogin) { + return ; + } else{ + return ; + } +} +``` + +```jsx +// src/pages/user.tsx + +const TheOldPage = ()=>{ + // ... +} + +export default withAuth(TheOldPage) +``` + +## 约定式路由 + +除配置式路由外,Umi 也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。 + +**如果没有 routes 配置,Umi 会进入约定式路由模式**,然后分析 `src/pages` 目录拿到路由配置。 + +比如以下文件结构: + +```bash +. + └── pages + ├── index.tsx + └── users.tsx +``` + +会得到以下路由配置, + +```js +[ + { path: '/', component: '@/pages/index' }, + { path: '/users', component: '@/pages/users' }, +] +``` + +> 使用约定式路由时,约定 `src/pages` 下所有的 `(j|t)sx?` 文件即路由。如果你需要修改默认规则,可以使用 [conventionRoutes](../api/config#conventionroutes) 配置。 + +### 动态路由 + +约定,带 `$` 前缀的目录或文件为动态路由。若 `$` 后不指定参数名,则代表 `*` 通配,比如以下目录结构: + +比如: + +* `src/pages/users/$id.tsx` 会成为 `/users/:id` +* `src/pages/users/$id/settings.tsx` 会成为 `/users/:id/settings` + +举个完整的例子,比如以下文件结构, + +``` ++ pages/ + + foo/ + - $slug.tsx + + $bar/ + - $.tsx + - index.tsx +``` + +会生成路由配置如下: + +```javascript +[ + { path: '/', component: '@/pages/index.tsx' }, + { path: '/foo/:slug', component: '@/pages/foo/$slug.tsx' }, + { path: '/:bar/*', component: '@/pages/$bar/$.tsx' }, +]; +``` + +### 全局 layout + +约定 `src/layouts/index.tsx` 为全局路由。返回一个 React 组件,并通过 `` 渲染嵌套路由。 + +如以下目录结构: + +```bash +. +└── src + ├── layouts + │   └── index.tsx + └── pages + ├── index.tsx + └── users.tsx +``` + +会生成如下路由: + +```js +[ + { + path: '/', + component: '@/layouts/index', + routes: [ + { path: '', component: '@/pages/index' }, + { path: 'users', component: '@/pages/users' }, + ], + }, +] +``` + +可以通过 `layout: false` 来细粒度关闭某个路由的 **全局布局** 显示,该选项只在一级生效: + +```ts + routes: [ + { + path: '/', + component: './index', + // 🟢 + layout: false + }, + { + path: '/users', + routes: [ + // 🔴 不生效,此时该路由的 layout 并不是全局布局,而是 `/users` + { layout: false } + ] + } + ] +``` + +一个自定义的全局 `layout` 格式如下: + +```tsx +import { Outlet } from 'umi' + +export default function Layout() { + return +} +``` + +### 不同的全局 layout + +你可能需要针对不同路由输出不同的全局 layout,Umi 不支持这样的配置,但你仍可以在 `src/layouts/index.tsx` 中对 `location.path` 做区分,渲染不同的 layout 。 + +比如想要针对 `/login` 输出简单布局, + +```js +import { useLocation, Outlet } from 'umi'; + +export default function() { + const location = useLocation(); + if (location.pathname === '/login') { + return + } + + // 使用 `useAppData` / `useSelectedRoutes` 可以获得更多路由信息 + // const { clientRoutes } = useAppData() + // const routes = useSelectedRoutes() + + return ( + <> +
+ +