一、如何新建一个alita项目(简单pc或h5)

1、全局安装 alita


yarn global add alita
or
npm i alita -g


2、新建项目目录

在任意地方新建一个空白项目目录

3、执行初始化命令


alita g app

会出现两个选择,是否使用 typescript ,和创建的页面是否是 h5 (后续会增加 cordova 选项)

? 是否使用typescript,默认否? No
? 是否是h5页面,默认pc? No
   create package.json
   create src/pages/index/index.js
   create src/pages/index/index.less
   create src/models/index.js
   create mock/app.js
   create src/services/api.js
✔  success   

我们的业务相关文件都在 src 目录下面,页面相关的文件都在 pages 目录下面。本地静态数据服务文件放在了 mock 文件。

规定只有pages下面的index文件会被识别成路由。

如pages/home/index.js会被识别为/home

pages/list/index.js会被识别为list

而pages/list/a.js不会被识别为路由

4、安装项目需要的包

yarn
or
npm i

等待安装,提示安装成功


[4/4] 📃  Building fresh packages...
success Saved lockfile.
✨  Done in 274.51s.

5、执行项目启动命令


yarn start
or
npm start

会自动打开 http://localhost:8000/

image.png

恭喜你,看到这个页面说明你的 alita 项目初始化成功了,这同时也代表你的开发环境配置成功。


二、业务能力

这部分内容主要讲解在业务开发中会使用到的一些能力,每个能力后面都会附上这个能力的底层实现链接,如果你感兴趣,你可以点击查看。如果你只是非专业前端人员,你可以考虑忽略。

1、如何在页面之间跳转

1.1 首先我们新建一个新页面


alita g pages home

注意:这里是pages,不是page。page创建的是简单页面,pages创建的是带有dva的页面。


? Do you want to use typescript? No
   create src/pages/home/index.js
   create src/pages/home/index.less
   create src/models/home.js
✔  success  

同样可以选择typescript,默认是no,直接敲回车就好了。


1.2.1 通过点击页面链接跳转

打开src/pages/index/index.js文件,在头部引入Link,在html中添加Link链接。 umi声明式跳转 

import { connect } from 'dva';
import React, { Component } from 'react';
+ import { Link } from 'umi';

import styles from './index.less';

@connect(({ index }) => ({ index }))
class Page extends Component {
  state = {};

  render() {
    const {
      index: { name },
    } = this.props;
    return <div className={styles.userCenter}>
+     <Link to="/home">跳转到home页面</Link>
      Hello {name}</div>;
  }
}

export default Page;

点击效果如下

2019-04-23 18.21.58.gif


1.2.2 通过方法跳转

打开src/pages/home/index.js文件,在头部引入 router ,添加按钮点击事件跳转到首页。 umi命令式跳转 


import { connect } from 'dva';
import React, { Component } from 'react';
+ import { Button } from 'antd';
+ import { router } from 'umi'
import styles from './index.less';

@connect(({ home }) => ({ home }))
class Page extends Component {
  state = {};

  render() {
    const {
      home: { name },
    } = this.props;
+    const gotoIndex = () => {
+      router.push('/');
+    }
    return <div className={styles.userCenter}>
+      <Button onClick={gotoIndex}>点击跳到首页</Button>
      Hello {name}</div>;

  }
}

export default Page;


1.2.3 重定向到一个路由

有时候我们需要重定向到一个路由,并且希望无法通过返回,回到上一个页面,比如做登录的时候。我们可以使用replace,用法和push一样。


router.replace('/')


2、数据绑定

打开我们的页面文件 src/pages/list/index.js 

首先值得你关注的是 connect(({ list }) => ({ list })) 这表明了这个页面和model namespace名字为list的数据做了绑定,这样你可以在页面类中使用 this.props.list 取得model的数据。如果你对于它们如何绑定的,感兴趣,那你可以查看 dva 。


然后我们关注一下,数据是如何被取出来,并绑定到页面里面的。


@connect(({ list }) => ({ list }))
class Page extends Component{
  state = {};

  render() {
    const {
      list: { name },
    } = this.props;
    return <div className={styles.userCenter}>Hello {name}</div>;
  }
}

这一部分是简写的用法,如果你不是那么理解。那你可以阅读下面的代码,它们是等价的。


const { list } = this.props;
const { name } = list;
// 如果你也不能理解上面的代码,还有
const list = this.props.list;
const name = list.name;

简写的好处,就是懒的写。然后还有,这里只申明了一个变量name。如果你在下方使用list,将会报错(error  'list' is not defined)。什么时候会使用简写?当你要申明的变量名和对象里面的变量名相同的时候,你就可以简写。

最后是我们的页面代码,也可以说成是html部分。


return <div className={styles.userCenter}>Hello {name}</div>;

首先说明 {} ,表示这里面的是一个变量,变量可以是普通变量,也可以是html流。你也可以在大括号里面写js的逻辑运算符。比如你可以这么写。


return <div className={styles.userCenter}>Hello {name?name:'name为空'}</div>;

image.png

如果你需要选择性的渲染页面,类似 ngIf、v-if、wx:if这样的功能,简单的处理办法就是


 return <div className={styles.userCenter}>Hello {name && (<h1>name不为空</h1>)}</div>;

上面代码表示当name存在时,渲染 <h1>name不为空</h1> 。复杂一些的逻辑判断,建议在render方法内,在return之前编写,把需要渲染的页面,赋值给一个变量,然后在return里面渲染这个变量,这样会让你的代码看起来更干净一点。


render() {
    const {
      list: { name },
    } = this.props;
    // 写在这里,比如
    let tmp = <h1>name不为空</h1>;
    if (!name) {
      tmp = <h1>name为空</h1>
    }
    return <div className={styles.userCenter}>Hello {tmp}</div>;
  }



3、数据存放在哪

首先数据分为两种,一种是需要从服务端获取,或者从其他途径获取,我们简单称之为非页面数据。另一种,是页面内的数据,它只和它自己有关,不和其他人交互。

首先我们再新建一个页面list ,记得使用命令 alita g pages list


alita g pages list
? Do you want to use typescript? No
   create src/pages/list/index.js
   create src/pages/list/index.less
   create src/models/list.js
✔  success   



2.1 非页面数据

我们将数据放在model里面,如果通过`g pages`命令创建的页面,那每一个页面都有它对应的model文件,放在src/models目录下面,和页面名字同名。并且已经通过connect函数绑定到了一起,如果你对这些也不感兴趣,那么,你就记住页面和数据是单向绑定的,简要原则就是「数据驱动页面渲染,事件驱动页面更新」,这是通关口诀,可以背下来。


2.1.1 数据驱动页面

首先我们来看数据绑定

打开 src/models/list.js  文件

首先你应该关注的是 namespace 和 state


  namespace: 'list',

  state: {
    name: ''
  },


可以简单的理解为这个model叫做list,它有一个属性,name来自服务端,它极大可能是一个字符串。我们再给它增加一个属性,如


  namespace: 'list',

  state: {
    name: '',
    some: []
  },

以上增加了一个some,它极大可能是一个数组。

然后因为页面和数据是绑定在一起的,也就是说,在对应的页面中,你可以取到name和some这两个数据用于页面渲染绘制,取不到其他的值。

为了减少干扰项,你可以先注释掉state以下的所有方法。如


import { query } from '@/services/api';

const IndexModel = {
  namespace: 'list',

  state: {
    name: '',
    some: []
  },

  // effects: {
  //   *query({ payload }, { call, put, select }) {
  //     const data = yield call(query, payload);
  //     console.log(data)
  //     yield put({
  //       type: 'save',
  //       payload: { name: data.text },
  //     });
  //   },
  // },
  // subscriptions: {
  //   setup({ dispatch, history }) {
  //     return history.listen(({ pathname }) => {
  //       if (pathname === '/') {
  //         dispatch({
  //           type: 'query'
  //         })
  //       }
  //     });
  //   }
  // },
  // reducers: {
  //   save(state, action) {
  //     return {
  //       ...state,
  //       ...action.payload,
  //     };
  //   },
  // },
};

export default IndexModel;

打开页面查看效果 http://localhost:8000/list 别忘了,我们现在编辑的是list页面。

image.png


简单的修改,list model 的 state的值。 src/models/list.js 


  namespace: 'list',

  state: {
    name: 'Happy',
    some: []
  },

image.png


这里可以记下来,页面上渲染的数据,只和state里面的数据绑定,和其他任何东西没有关系。所以state数据,变化了,页面才会发生变化。


2.1.2 事件驱动页面更新

如果你想深入了解关于事件和监听,可以访问 redux 

首先,先为我们的页面增加一个按钮,并绑定点击事件。


  render() {
    const { list: { name } } = this.props;
    const putChangeData = () => {
      console.log('点击发送更新数据事件')
    }
    return <div className={styles.userCenter}>
      <Button onClick={putChangeData}>点击发送更新数据事件</Button>
      Hello {name}</div>;
  }

这里也有个口诀,「各人自扫门前雪,莫管他人瓦上霜」

一个事件,只要做一个事情就好。比如这里,我只管发送,什么事件发给谁。至于这个事件后续由谁接收,做了什么事情我不关心。

const { list: { name } ,dispatch} = this.props;
    const putChangeData = () => {
      console.log('点击发送更新数据事件')
      dispatch({
        type: 'list/query',
        payload:{},
      });
    }

对于页面,只需要关心发给了谁,这里是namespace为list的query方法,并且附带了参数{},后续这个事件有没有被成功接收,做什么什么事情,我这边不管了。

这些事件都会被model中定义的effects方法接收,验证方法名相同的会触发响应。


  namespace: 'list',

  state: {
    name: 'Happy',
    some: []
  },

  effects: {
    *query({ payload }, { call, put, select }) {
      console.log('有人提醒我做事啦')
    },
  },

这里的payload就可以取到,发出事件传递过来的参数。

有些人会对query前面的 * 感到不熟悉,简单点,你不管它,在effects里面的方法前面都带 * ,其实它这里是表明了这是一个异步函数,里面可以使用 yield 等待其他异步函数执行结果。

第二个参数带了三个方法。

call就是调用一个方法,通常我们用来调用服务端接口。

put的作用和页面中的dispatch一样,也是用来发出事件的。

select方法,就是一个查找,你可以用来查找其他model的数据。

// 比如你想在list的model中取到home的model里面的数据
 const homeData = yield select(state => state.home);

常用于取用户id之类的公共数据。

回到query方法上

import { query } from '@/services/api';
...
effects: {
    *query({ payload }, { call, put, select }) {
      console.log('有人提醒我做事啦');
      console.log('我这里只负责管理,我不做事的啊,叫个小弟去办吧');
      console.log('于是叫来了一个query方法,然后在这里等他处理完');
      const data = yield call(query, payload);
      // 这里会一直等,等到query有响应,可能是错误响应,也可能是正确响应
      console.log(data)
      // 然后我取到这个数据,发起另一个事件save,其实这里响应的是list/save,
      // 因为在同一个namespace,所以前面的namespace list可以省略。
      yield put({
        type: 'save', // list/save
        payload: { name: data.text },
      });
    },
  },

到这里effects的query要做的事情已经处理完成了,接下来的流程和副作用也和它没有关系了。

接下来,我们看save方法。它的逻辑也很简单,先解开旧的state,然后用新的payload里面的数据覆盖它,同名的会被修改掉,其他的被保留原样。

  reducers: {
    save(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
  },

这里修改了,model的state,然后我们最开始已经了解到model的state和页面是绑定的,所以这里变化,导致了页面的变化。

image.png

当然事件有很多种,可以是按钮的点击事件,当然也可以是其他的html事件。也可以是当进入某个页面的时候,触发。这里表示,当路由url是list时,发起同个namespace下的query方法。更多subscriptions,请访问 dva  [subscriptions](https://dvajs.com/api/#subscriptions


subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) => {
        if (pathname === '/list') {
          dispatch({
            type: 'query'
          })
        }
      });
    }
  },

最终我们的 src/models/list.js 


import { query } from '@/services/api';

const ListModel = {
  namespace: 'list',

  state: {
    name: 'Happy',
    some: []
  },

  effects: {
    *query({ payload }, { call, put, select }) {
      console.log('list-effects-query:有人提醒我做事啦');
      console.log('我这里只负责管理,我不做事的啊,叫个小弟去办吧');
      console.log('于是叫来了一个query方法,然后在这里等他处理完');
      const data = yield call(query, payload);
      console.log('这里会一直等,等到query有响应,可能是错误响应,也可能是正确响应');
      console.log(data)
      console.log('然后我取到这个数据,发起另一个事件save,其实这里响应的是list/save');
      yield put({
        type: 'save',
        payload: { name: data.text },
      });
    },
  },
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) => {
        if (pathname === '/list') {
          dispatch({
            type: 'query'
          })
        }
      });
    }
  },
  reducers: {
    save(state, action) {
      console.log('我是list的moedel中的reducers中的save,我修改了list的state,导致页面更新')
      return {
        ...state,
        ...action.payload,
      };
    },
  },
};

export default ListModel;

你一访问list路由就会看到“Hello Alita”,而不用点击按钮。

image.png



2.2 页面数据

其实理解了上面的非页面数据,你就可以很好的理解,页面数据了,有差异的地方时,这些数据保存在页面的state里面,并且通过this.setState方法更新数据。不需要通过事件。


如:我们给list页面增加一个state,pageName,并给它一个初始值为 '页面名称'

class Page extends Component {
  state = {
    pageName: '页面名称'
  };
... 以下省略

然后绑定到页面上,并绑定了点击事件,调用了this.setState,更新了页面的state状态。

import { connect } from 'dva';
import React, { Component } from 'react';
import { Button } from 'antd';
import styles from './index.less';

@connect(({ list }) => ({ list }))
class Page extends Component {
  state = {
    pageName: '页面名称'
  };

  render() {
    const { pageName } = this.state;
    const changePageName = () => {
      this.setState({
        pageName: '修改后的页面名称'
      })
    }
    return <div className={styles.userCenter}>
      <Button onClick={changePageName}>{pageName}</Button>
      </div>;
  }
}
export default Page;

原则上我们尽量把数据保存在model里面,因为这样子事件更加清晰,页面代码阅读起来也比较轻松。还有涉及到代码服用,共享数据等问题的时候,也比较好。但是有些页面中的状态,比如开关,这种。临时状态,可以通过页面数据state内部维护。


4、服务端接口调用

像我们  src/models/list.js 中effects的query方法中,我们使用了 call 调用了 service中一个叫做 query的方法。

import { query } from '@/services/api';
// 注意头部方法的引入
effects: {
    *query({ payload }, { call, put, select }) {
      const data = yield call(query, payload);
      yield put({
        type: 'save',
        payload: { name: data.text },
      });
    },
  },



4.1 文件路径别名,这里的@对应到项目目录的src目录

这样你就不需要在项目中使用相对路径,例如 import a from '../../../../component/a' 

可以直接使用 import a from '@/component/a' 

这么做有两个好处,第一,显而易见的相对路径的层级到底是几层,有时候写起来很难一样看出来。

第二,当你做文件移动的时候,不同的路径层级相对路径也不同,可能发生一种情况,本来一个页面可以用,你新建了一个文件,把它拷贝过去,它就不能用了。

根据别名,我们就可以找到,这个 query的方法,写在 src/services/api 里面。

打开这个文件

import { request } from 'alita';

export async function query() {
  return request('/api/hello');
}

这里是默认了使用get方法请求,如果需要使用post可以这样写。

export async function fakeSubmitForm(params) {
  return request('/api/forms', {
    method: 'POST',
    data: params,
  });
}


4.2 请求工具从alita中引出


import { request } from 'alita'

通过src/app.js文件配置。


export const request = {
  prefix: '/api/v1', // prefix
  suffix: '.json', // suffix
  errorHandler: (error) => {
    // Centralized processing error
    console.log(error);
    
  },
  headers: {
    Some: 'header' // unified headers
  },
  params: {
    Hello: 'world' // the query parameter to be included with each request
  }
};


了解更多   umi-request 


5、代码模版

如果你做的是pc中后台管理页面,那么 ant-design  和 ant-design-pro  是非常好的代码模版库。

首先如果你对react不熟悉,甚至是不懂react,那你可以先从ant-design开始看起。

比如效果图是在页面上增加一个按钮。那你就可以找到antd的button,然后点击“显示代码”

image.png

将里面的代码段复制到页面文件中,当然官网的写法和我们的写法有一点点不同,你需要取你需要的就好。

官网代码段

import { Button } from 'antd';

ReactDOM.render(
  <div>
    <Button type="primary">Primary</Button>
    <Button>Default</Button>
    <Button type="dashed">Dashed</Button>
    <Button type="danger">Danger</Button>
  </div>,
  mountNode
);

我们复制到page里面的是

import { connect } from 'dva';
import React, { Component } from 'react';
import { Button } from 'antd';
import styles from './index.less';

@connect(({ index }) => ({ index }))
class Page extends Component {
  state = {};

  render() {
    return (
      <div>
        <Button type="primary">Primary</Button>
        <Button>Default</Button>
        <Button type="dashed">Dashed</Button>
        <Button type="danger">Danger</Button>
      </div>
    );
  }
}

export default Page;

如果结构和我们相同的,那就直接ctrl c + ctrl v。

当你熟悉了,一个组件怎么通过复制粘贴的方式引用到项目中的时候,那你就可以通过一样的复制粘贴,把ant-design-pro的页面代码片段,添加到自己的项目中了。

组件或者页面需要的数据,通过我们前面提到的「数据驱动页面,事件驱动数据更新」的方式,和服务端对接。

当然h5页面的,可以选择 ant-design-mobile ,使用方式相识。


6、如何通过url传参

需要传参的文件需要命名为 $index 或这 $index$ , $index 表示必须要传参数, $index$ 表示参数可传可不传。

比如新建一个页面 alita g pages detial


alita g pages detial
? Do you want to use typescript? No
   create src/pages/detial/index.js
   create src/pages/detial/index.less
   create src/models/detial.js
✔  success   

重命名 src/pages/detial/index.js  为 src/pages/detial/$index.js 

访问url: http://localhost:8000/detial/502 ,如果访问http://localhost:8000/detial/ 会报路径错误

在页面中可以通过this.props.match.params.index取得上一个页面的传参。

如果这个参数不是必选的,那可以修改文件名为 src/pages/detial/$index$.js 

同样在页面中通过this.props.match.params.index取值,但需要注意的是,这个参数可能为空。

可以访问url: http://localhost:8000/detial/502 ,也可以访问http://localhost:8000/detial/


7、简短的总结

以上关注点,可以通用于任何使用ant-design-pro搭建的,和任何umi搭建的项目中(其实都是umi项目)。

我只是把关注点更聚焦于业务开发中。这也是Alita做的第一件事情。如果你只是作为项目中的其中一个开发人员,并且前端技术不在你的个人计划技术栈内,那么掌握上面的方法,应该足够你应付,类似,“我一个后端开发人员会点js你就让我写前端”,“我做原生开发的啊”,“虽然就改个小bug,可是这个项目怎么跑起来啊”的尴尬