Bootstrap

react+UmiJS+Antd Pro简介

在这里插入图片描述
在这里插入图片描述

一、项目初始化
  • 创建并进入目录
    mkdir umijs
    cd umujs
  • 创建umi项目
    yarn create @umijs/umi-app
    npx @umijs/create-umi-app
  • 安装依赖
    yarn
  • 启动项目
    yarn start
  • 打包,部署发布
    yarn build (构建后项目中会多一个dist项目)
  • 本地验证
    yarn global add serve - - 或 - - npm install http-server -g
    serve ./dist - - 或 - - http-server ./dist
  • 获取镜像源
    yarn config get registry

在这里插入图片描述

二、基本了解
2.1.路由配置

.umirc.ts

// umi的配置项,如果项目复制,可以将配置写在config/config.ts中,并把配置中的路由配置routes.ts拆分出去,但该文件的优先级更高
import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  hash:true,  // 默认为false,用于项目build发布时/dist文件夹下包含hash后缀,每次相关文件改变时,hash后缀就会变,为了减少增量发布和避免浏览器加载缓存
  // base:'/admin/',   // 设置路由前缀,用于部署到非根目录,路由'/'和'/user',设置了base为'/admin/',就可以通过'/admin/'和'/admin/user'来访问之前的路由
  // publicPath:'http://xxx.com/cdn/',   // 打包时webpack会在静态文件路径前面添加的publicPath值,要修改静态文件地址(publicPath:'CDN值' 或者 publicPath:'./相对路径')
                   // 默认是'/'根目录,所以dist/index.html中的静态资源就使用'href="/umi.css"'来获取静态资源
                   // publicPath:'http://xxx.com/cdn/',打包后就需要使用'href="http://xxx.com/cdn/umi.80764e3e.css"'
  // outputPath:'build',  // 打包后文件输出目录,默认是dist
  title:'UmiJS标题',   // 就是项目打开的标题,也可以在路由中设置每一页的标题,title:'首页',路由标题优先级比项目标题优先级高
  // history:{
  //   type:'hash'  // 三种可选项,browser(默认值),hash(会将所有路由都添加一个#),memory()
  //                   // 之前使用http://192.168.220.1:8000/访问,现在http://192.168.220.1:8000/#
  //                   // 之前使用http://192.168.220.1:8000/a,现在http://192.168.220.1:8000/#/a
  // },
  // targets:{
  //   ie:11    // 需要兼容的浏览器最低版本,会自动引入polyfill和做语法转换
  // },
  // proxy: {    // 代理服务器,当浏览器有跨域问题时的配置项
  //   '/api': {   // 要请求的前缀,遇到到/api前缀的请求(如/api/findAllStudent)就会走这个代理服务器
  //     'target': 'http://jsonplaceholder.typicode.com/',   // 将请求的目标地址换成target中的地址
  //     'changeOrigin': true,
  //     'pathRewrite': { '^/api' : '' },
  //   },
  // },
  // theme: {   // 配置主题,实际上是配 less 变量
  //   '@primary-color': '#1DA57A',    // 设置antd的主色调,antd的定制变量,https://ant.design/docs/react/customize-theme-cn
  // },
  routes: [   // 路由配置,path是访问哪个路径,component是访问哪个组件(component:'@/pages/index'使用的是绝对路径,@相当于src,component:'index'是相对路径,会以 src/pages 为基础路径开始解析)
    { path: '/', component: '@/pages/index',title:'首页' },
    { path: '/list', redirect:'/user/one' },   // redirect,访问http://192.168.220.1:8000/list可以重定向到http://192.168.220.1:8000/user/one目录
    // layouts模板路由
    { 
      path:'/user',
      component:'@/layouts/index',
      // exact: true,  // exact: true代表是否严格匹配
      // 路由级别的权限校验
      wrappers:[   // 访问/user页面之前先进入'@/wrappers/auth'判断用户是否登录,如果登录了就转入真实的user组件,如果没有登录就重定向到登录页面
        '@/wrappers/auth',
      ],
      routes:[    // 子路由,多个路由使用同一个component模板
        // 路由传参,":参数名?",加问号时代表传参可传可不传,如果不加问号,就必须传值,否则访问不到页面
        // 访问时使用http://192.168.220.1:8000/user/one/111,在component:"@/pages/index"页面可以使用props.match.params来接收路由传参的所有参数
        // { path: '/user/one/:id?', component: '@/pages/index',title:'组页面'}, 
        { path: '/user/one', component: '@/pages/user2',title:'用户页面2'},  
        { path: '/user/two', component: '@/pages/user',title:'用户页面'},
        { component:'@/pages/404'}  // 当没有以上任何子路由就访问404组件
      ]
    },
    { component:'@/pages/404'}  // 当没有以上任何子路由就访问404组件
  ],
  fastRefresh: {},
});

layouts.index.tsx

import React from 'react';
import { Link, NavLink } from 'umi';
import './index.less';

// jsx可以不用管props的类型
const Index = (props: any) => {
  return (
    <div>
      <h2>Header</h2>
      {/* 
      <Link to={'/user/one'}>用户1</Link>
      <Link to={'/user/two'}>用户2</Link> 
      */}
      {/* 与上面的Link区别是,会给当前点击的对象自动添加一个class="active",可以在index.css中写入.active的样式,点击后样式会显示出来 */}
      <NavLink to={'/user/one'}>用户1</NavLink>
      <NavLink to={'/user/two'}>用户2</NavLink>
      {/* 配置路由父组件,{props.children}可以获取子组件内的节点元素 */}
      {props.children}
      <h2>Footer</h2>
    </div>
  );
};
export default Index;

pages/index.less

.active {
  color: red;
  font-size: 33px;
}

pages/index.tsx

import styles from './index.less';
import { DatePicker } from 'antd';

export default function IndexPage(props: any) {
  // props.match.params可以获取路由上传递的所有参数
  console.log('dnlsbd', props.match.params);
  console.log('dnlsbd', props.match.params.id);
  return (
    <div>
      <h1 className={styles.title}>Page index</h1>
      <DatePicker />
    </div>
  );
}

pages/user2.tsx

// 使用rrc自动生成类组件模板文件
// 使用rsc自动生成函数式组件模板文件(推荐)
import React from 'react';

const User = () => {
  return <div>User Page2</div>;
};
export default User;

pages/user1.tsx

// 使用rrc自动生成类组件模板文件
// 使用rsc自动生成函数式组件模板文件(推荐)
import React from 'react';
import { Button } from 'antd';
// 路由组件中有props这个属性,自定义的其他组件没有props这个属性
const User = (props: any) => {
  return (
    <div>
      <span>User Page1</span>
      <Button
        onClick={() => {
          props.history.push('/');
        }}
      >
        点我回首页
      </Button>
    </div>
  );
};
export default User;

pages/404.tsx

import React from 'react';
const NotFound = () => {
  return (
    <div>
      <h1>404</h1>
    </div>
  );
};
export default NotFound;

wrappers/auth.tsx

// 可以用来判断用户名是否登录
import { Redirect } from 'umi';
export default (props: any) => {
  const isLogin = true;
  if (isLogin) {
    // 如果登录的话就返回子组件内容
    return <div>{props.children}</div>;
  } else {
    // 如果没有登录的话就重定向到登录页面
    return <Redirect to="/login" />;
  }
};
2.2.Html模板

在node_modules/@umijs/core/lib/Html/document.ejs中,里面是将组件挂载到页面上
由于模板代码在node_modules中,一般提交版本代码时是不会提交node_modules的,所以不要在模板上进行修改

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
</head>
<body>

<div id="<%= context.config.mountElementId || 'root' %>"></div>

</body>
</html>

新建 src/pages/document.ejs,umi 约定如果这个文件存在,会作为默认模板

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>自定义模板</title>
  <!-- 引入bootstrap等第三方样式库 -->
  <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <h3>自定义模板</h3>
  <button class="btn btn-success">bootstrap</button>
  <div id="root"></div>
</body>
</html>
2.3.Mock数据
  • Mock数据是前后端分离开发的关键步骤,通过跟服务器预先约定好的接口,模拟请求数据和逻辑
  • 在mock文件夹下写的js或ts文件都会被认为是mock文件
  • 可以引入Mock.js第三方库提升Mock数据能力
mock:false   // 之前请求的api不存在(相当于在浏览器中发起请求了),会返回document.ejs的文档内容
// 提供环境变量Terminal中临时关闭:MOCK=none umi dev
"start:nomock": "MOCK=none umi dev" // 可以在package.json中配置"start:nomock": "MOCK=none umi dev",在terminal中直接执行yarn start:nomock就可以不使用mock启动了

mock/index.ts

// mock默认是打开的,如果想关闭可以在.umirc.json中配置 mock: false,之前请求的api不存在(相当于在浏览器中发起请求了),会返回document.ejs的文档内容
// 以key : value的形式,value支持 Object 和 Array 类型
import mockjs from 'mockjs'
export default {
  // "GET/POST"为请求方式,默认不写是GET方式
  // "/api/index"为请求接口
  'GET /api/index' : {
    id: 1,
    name: 'Tom',
    age: 12
  },
  'GET /api/person' : {
    id: 2,
    name: 'LiLi',
    age: 21
  },
  'GET /api/tags' : mockjs.mock({
    // @city动态生成城市,'value|1-100'生成1-100的value值,type|0-2生成0-2的type值,默认值为1
    'list|100':[{name:'@city','value|1-100':50,'type|0-2':1}]
  }) 
}

pages/index.tsx

import styles from './index.less';
import { DatePicker, Button } from 'antd';
import { useEffect } from 'react';
import { history, request } from 'umi';

export default function IndexPage(props: any) {
  // props.match.params可以获取路由上传递的所有参数
  // console.log('dnlsbd', props.match.params);
  // console.log('dnlsbd', props.match.params.id);
  useEffect(() => {
    setTimeout(() => {
      // 路由跳转:2秒后跳转到/user/one路由中
      // 在组件中不在umi中导入history,也可以在props中直接使用history
      history.push('/user/one');
    }, 2000);
  });
  // 方法一:使用umi提供的request请求数据,接收两个参数,第一个参数是url,第二个参数是options
  const getData = () => {
    request('/api/index').then((res) => {
      console.log(res);
    });
  };
  // 方法二:使用async与await发送异步请求,从服务端获取数据
  const getData2 = async () => {
    const data = await request('/api/person');
    console.log(data);
  };
  // 使用mockjs生成数据
  const getData3 = async () => {
    const data = await request('/api/tags');
    console.log(data);
  };
  return (
    <div>
      <h1 className={styles.title}>Page index</h1>
      <Button onClick={getData3}>点击获取数据</Button>
      <DatePicker />
    </div>
  );
}
2.4.DvaJS
  • umi已经给我们提供plugin-dva插件
  • dva的作用就是让我们更方便的使用redux
  • 约定式的model文件
    src/models下的文件(推荐使用),src/pages/models目录下的文件,src/pages/model.ts文件
    会有校验内容是否是有效的dva model
  • 1.创建ui组件 src/pages/tags.tsx(记得配置路由)
  • 2.创建model src/models/tags.ts
  • 3.使用connect将ui组件和model进行连接

.umirc.ts

{path:'/tags',component:'@/pages/tags'},

pages/tags.tsx

import React from 'react';
import { connect } from 'umi';
import { Button } from 'antd';

const Tags = (props) => {
  const { dispatch } = props;
  const tagslist = props.tags.tagsList.list || [];
  const getTagsList = () => {
    // 使用model获取数据
    dispatch({
      type: 'tags/fetchTagsList', // "models的命名空间/方法名"
      payload: null, // 如果是表单提交就使用这个参数将数据提交给model,model那边的payload参数就可以获取到对应的值
    });
  };
  return (
    <div>
      <h3>Dva的使用</h3>
      <Button onClick={getTagsList}>点击获取TagsList</Button>
      {tagslist.map((item, index) => {
        return <p key={index + item}>{item.name}</p>;
      })}
    </div>
  );
};

// tags会作为props传给Dva组件
export default connect(({ tags }) => ({ tags }))(Tags);
// 报错:mapStateToProps() in Connect(Widget) must return a plain object. Instead received undefined.
// 解决:箭头函数返回的是一个对象,所以{ tags }外面要用()括起来

src/models/tags.ts

import {request} from 'umi'
// 通过request获取mock中的tags数据
const getTags = () => {
  return request('/api/tags')
}
export default{
  // 命名空间,调用model时,通过命名空间调用,不要和其他model同名
  namespace:'tags', 
  // 状态,是通过effects调用后端接口获取的数据,和react中state类似,和redux中保存的state一样
  state:{
    tagsList:[]
  },     
  // 影响,调用外部函数,在外部函数中请求后端接口,获取数据
  effects:{
    // payload:ui组件传过来的参数
    // callback:回调函数
    // put:将数据向下传递给reducers
    // call(外部函数,函数所需参数):访问外部的一些方法,参数一般是payload(ui组件接收的参数)
    // 由于使用request异步请求数据,使用 * 与 yield 配合达到同步请求的效果
    *fetchTagsList({payload,callback},{put,call}){
      // 获取tags数据
      const tagsList = yield call(getTags)
      // 调用put函数将数据传递给reducers
      yield put({
        type: 'setTagsList',    // 类似于 redux 中 action 的 type
        payload: tagsList    // 传数据给reducer
      })
    }
  },   
  // 更新state :使用set方法更新state保存的数据
  reducers:{
    // state:原来的数据
    // action:从effects中yield put获取的值
    setTagsList(state,action){
      console.log("skanbdjad",state)
      console.log("skanbdjad",action)
      return {...state,tagsList:action.payload}
    }
  }  
}
2.5.运行时配置

运行时配置:约定写在src/app.tsx中

import { history } from 'umi';
// 定义额外路由变量
let extraRoutes;

// patchRoutes:动态添加路由
export function patchRoutes({ routes }) {
  // console.log('添加前', routes); // routes是已经在.umirc.ts中注册过的所有路由
  routes.unshift({
    path: '/foo',
    title: 'foo',
    // exact: true,   // 严格查询
    component: require('@/pages/user1').default, // 在.umirc.ts可以直接通过绝对路径获取组件,在app.tsx中需要使用require请求user1文件,再调用user1的default方法返回user1组件
  });
  // console.log('添加后', routes);

  // 遍历后端获得的路由数组
  extraRoutes.map((item) => {
    // 合并服务端返回的路由
    routes.unshift({
      path: item.path,
      component: require(`@/pages${item.component}`).default,
    });
  });
}

// render:覆写render(在patchRoutes之前执行)
export function render(oldRender) {
  // 从后端请求获得路由
  // fetch('/api/routes').then(res=>res.json()).then((res)=>{

  // })
  // 模拟从后端获取的路由数据
  // path是路由,component是要访问的组件
  extraRoutes = [{ path: '/server', component: '/user2' }];
  // 在渲染之前做权限校验
  // 如果登录了就执行patchRoutes中的extraRoutes数组遍历,并将/server路由与原路由进行合并
  // 如果没有登录就使用history.push跳转到登录页面
  const isLogin = true;
  if (!isLogin) {
    history.push('/login');
  }
  // oldRender用于渲染新页面
  oldRender();
}
// onRouteChange:在初始加载和路由切换时做一些事情,在patchRoutes之后执行
export function onRouteChange({ location, routes, action, matchedRoutes }) {
  console.log(location); // 当前所在的路由
  // 路由名称,用于做埋点统计
  console.log(location.pathname);
  // 设置标题
  console.log('jhbad vsd', matchedRoutes);
  console.log('jhbad vsd', matchedRoutes.length);
  // 将有matchedRoutes
  if (matchedRoutes.length) {
    // 如果在.umirc.ts中或该页面新添加的路由中定义的有title就将其设置为文档标题,否则设置为''
    document.title =
      '郑州师范 ' + (matchedRoutes[matchedRoutes.length - 1].route.title || '');
  }
  console.log(routes); // 所有的路由(包括在patchRoutes中动态新增的路由)
  console.log(action); //POP
}
2.6.Umi-UI
  • 安装umi-ui的依赖
    yarn add @umijs/preset-ui -D
  • 在项目package.json中配置
    linux:“start:umi-ui”: “UMI_UI=1 umi dev”
    windows:“start:umi-ui”: “set UMI_UI=1 && umi dev”
  • 运行:yarn start:umi-ui

打开项目后,点击右下角Umi UI的可视化编程根据,解决以下问题
在项目根目录下创建一个.env文件,里面进行以下配置:HOST=0.0.0.0
在这里插入图片描述

通过点击引入相关的Umi-Ui的模板或区块,引入区块中的布局,点击添加到项目中,项目中pages下会出现一个LayoutSide组件
需要进行调整,在src下创建一个layouts文件夹,将LayoutSide组件放到layouts下面,并修改.umirc.ts下的路由配置

import { defineConfig } from 'umi';
export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    {
      name: '注册页',
      path: '/userregister',
      component: './UserRegister',
    },
    {
      name: '模板组件',
      title: '数字郑州',
      path: '/',
      component: '@/layouts/LayoutSide/index',
      routes: [
        {
          name: '空白页面',
          path: '/user',
          component: '@/pages/User/index',
        },
        {
          name: '首页',
          path: '/index',
          component: '@/pages/index',
        },
      ],
    },
  ],
  fastRefresh: {},
  dva: {  // 添加@umijs/preset-react插件中dva的配置
    immer: true,
    hmr: false,
  },
  locale: {   // 添加国际化配置 forMessage
    default: 'zh-CN',
    antd: false,
    title: false,
    baseNavigator: true,
    baseSeparator: '-',
  }
});
三、项目优化
3.1.配置项

将原来的.umirc.ts文件的内容拆分为以下几个文件,在项目根目录先新建一个config文件夹,下面放四个文件

3.1.1.config.ts

相当于.umirc.ts下的配置

// umi的配置项,如果项目复制,可以将配置写在config/config.ts中,并把配置中的路由配置routes.ts拆分出去,但该文件的优先级更高
import { defineConfig } from 'umi';
// 引入路由数组
import routes from './routes'
// 引入其他的默认设置
import defaultSetting from './defaultSetting';
// 引入代理配置
import proxy from './proxy'

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  hash:true,  // 默认为false,用于项目build发布时/dist文件夹下包含hash后缀,每次相关文件改变时,hash后缀就会变,为了减少增量发布和避免浏览器加载缓存
  // title:'UmiJS标题',   // 就是项目打开的标题,也可以在路由中设置每一页的标题,title:'首页',路由标题优先级比项目标题优先级高
  routes,
  title:defaultSetting.title, 
  theme: {   // 配置主题色,实际上是配 less 变量
    '@primary-color': defaultSetting.PrimaryColor,    // 设置antd的主色调,antd的定制变量,https://ant.design/docs/react/customize-theme-cn
  },
  fastRefresh: {},
  targets:{
    ie:11    // 需要兼容的浏览器最低版本,会自动引入polyfill和做语法转换
  },
  proxy:proxy['dev'],  // 代理配置
  locale:{   // 国际化配置
    default:'zh-CN',
    antd:false,
    title:false,
    baseNavigator:true,
    baseSeparator:'-',
  },
  dva:{   // 插件的dva配置
    immer:true,
    hmr:false,
  }
});
3.1.2.defaultSetting.ts

一些默认配置

export default {
  PrimaryColor: '#1DA57A',
  title:'UmiJS标题',
}
3.1.3.proxy.ts

代理配置

// 对不同环境下的代理进行拆分
export default [
  // 运行时的代理
  dev:{
    '/api': {   // 要请求的前缀,遇到到/api前缀的请求(如/api/findAllStudent)就会走这个代理服务器
      'target': 'http://jsonplaceholder.typicode.com/',   // 将请求的目标地址换成target中的地址
      'changeOrigin': true,
      'pathRewrite': { '^/api' : '' },
    },
  },
  // 测试时的代理
  test:{
    '/api': {   // 要请求的前缀,遇到到/api前缀的请求(如/api/findAllStudent)就会走这个代理服务器
      'target': 'http://jsonplaceholder.typicode.com/',   // 将请求的目标地址换成target中的地址
      'changeOrigin': true,
      'pathRewrite': { '^/api' : '' },
    },
  }
]
3.1.4.routes.ts

路由配置

// 暴露出路由数组
export default [
  [   // 路由配置,path是访问哪个路径,component是访问哪个组件(component:'@/pages/index'使用的是绝对路径,@相当于src,component:'index'是相对路径,会以 src/pages 为基础路径开始解析)
    { 
      path: '/', 
      component: '@/layouts/index',  // layouts模板路由
      title:'首页',
      routes:[
        {path:'/',component:'@/pages/index'},
        { 
          path:'/user',    // exact: true,  // exact: true代表是否严格匹配
          wrappers:[   // 路由级别的权限校验,访问/user页面之前先进入'@/wrappers/auth'判断用户是否登录,如果登录了就转入真实的user组件,如果没有登录就重定向到登录页面
            '@/wrappers/auth',
          ],
          routes:[    // 子路由,多个路由使用同一个component模板
            // { path: '/user/one/:id?', component: '@/pages/index',title:'用户页面1'},   // 路由传参,":参数名?",加问号时代表传参可传可不传,如果不加问号,就必须传值,否则访问不到页面,访问时使用http://192.168.220.1:8000/user/one/111,在component:"@/pages/index"页面可以使用props.match.params来接收路由传参的所有参数
            { path: '/user/one', component: '@/pages/user1',title:'用户页面1'},
            { path: '/user/two', component: '@/pages/user2',title:'用户页面2'},
            { component:'@/pages/404'}  // 当没有以上任何子路由就访问404组件
          ]
        },
        {path:'/tags',component:'@/pages/tags'},
        {path:'/login',component:'@/pages/login'},
      ]
    },
    // { path: '/list', redirect:'/user/one' },   // 重定向redirect,访问http://192.168.220.1:8000/list可以重定向到http://192.168.220.1:8000/user/one目录
    { component:'@/pages/404'}  // 当没有以上任何子路由就访问404组件
  ],
]
3.1.5.utils/request.ts

对request进行封装,可以做一些拦截器,方便在services中使用

// 对request进行封装,可以做一些拦截器,方便在services中使用
import request from 'umi-request'
// message用于消息提醒
import {message} from 'antd'
// 请求拦截器
request.interceptors.request.use((url, options) => {
  return {
    url: `${url}`,
    // 可以在这里设置请求头headers
    options: { ...options, interceptors: true, headers: {Hello: 'hello'} },
  };
});
// 响应拦截器
request.interceptors.response.use(response => {
  // 错误状态码的统一处理
  if(response.status > 400){
    const codeMaps = {
      404:'未找到资源。',
      502: '网关错误。',
      503: '服务不可用,服务器暂时过载或维护。',
      504: '网关超时。',
    };
    message.error(codeMaps[response.status]);
  }
  return response;
});
// 将定义的request暴露出去
export default request;
3.1.6.services/tags.ts

在src下创建目录,组件叫什么名字,服务就叫什么名字,将对后端或者models中的请求分离出来

// 将request在src/utils下面进行封装,并设定一些拦截器
import request from "../utils/request"
export const getTags = () => {
  return request('/api/tags')
}
3.2.目录结构

public:一些静态资源,打包时原样复制出去
src/.umi:缓存,每次运行会重新生成,不会进版本库
src/assets:本地静态资源
src/components:公共组件
src/pages/页面组件/components:页面相关的组件
src/services:对后端数据的请求
src/utils:工具类

四、Antd Pro
4.1.创建Antd Pro项目

创建Antd Pro项目(基于umi配置的)

  • yarn create umi

查看yarn的目录

  • yarn global bin

查看全局目录

  • yarn global dir

清缓存

  • yarn cache clean

创建Antd Pro项目报错

  • yarn config set global-folder “D:\Develop\yarnConfig\yarn_global”
    yarn config set cache-folder “D:\Develop\yarnConfig\yarn_cache”

在这里插入图片描述

;