前端环境搭建
首先得有node,并确保 node 版本是 10 或以上。(mac 下推荐使用 nvm 来管理 node 版本)
- 安装tyarn (建议使用tyarn),如下:
# 国内源
$ npm i yarn tyarn -g
# 后面文档里的 yarn 换成 tyarn
$ tyarn -v
安装git
详见以下文档:
https://dev.tencent.com/help/doc/git-2/git-start
https://dev.tencent.com/help/doc/faq/bbe781aee786/ssh
补充(在 Windows 上安装):
1.注册https://dev.tencent.com, 设置密码、激活邮箱
2.从http://git-scm.com/download/win地址下载并安装git
3.设置Git用户信息,这样做很重要,因为每一个 Git 的提交都会使用这些信息,并且它会写入到你的每一次提交中,不可更改,代码如下:
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
4.创建公钥,可以参考https://dev.tencent.com/help/doc/faq/bbe781aee786/ssh (在Git Bash命令行运行 ), 将生产的pub文件中的内容拷贝,
然后做出如下操作: 个人设置 => SSH公钥 => 新增公钥,将已经拷贝的公钥内容复制到"公钥内容" 输入框中,并且选择 "永久有效"
5.打开代码预览页面,选择SSH, 可以获取 git@git.dev.tencent.com:wxf407399291/ZT.git 地址
7.在本地安装sourceTree 或者 在vscode ide 中安装git插件
下载并安装
//下载项目代码
git clone -b master git@git.dev.tencent.com:wxf407399291/ZT.git
//安装
tyarn
//启动
npm run zt:dev //对应config/config.dev的环境配置参数
npm run zt:test //对应config/config.test的环境配置参数
//产品编译命令
npm run zt:build //对应config/config.prod的环境配置参数
npm run zt:build:dev //对应config/config.dev的环境配置参数
npm run zt:build:test //对应config/config.test的环境配置参数
//即可预览
http://localhost:800/user/login
构建环境说明
框架内置了三种构建环境,项目可以根据实际环境 分别配置三套运行参数:
config/config.test.js 、config/config.dev.js 、config/config.prod.js
常见配置如下:
export default {
......,
define: {
//业务接口前缀
BASE_API_URL_PREFIX: BASE_API_URL_PREFIX,
//菜单Api地址
_MENU_URL_: `${BASE_API_URL_PREFIX}/queryMenus`,
//登录Api地址
_LOGIN_URL_: `${BASE_API_URL_PREFIX}/login/account`,
//退出Api地址
_LOGOUT_URL_: `${BASE_API_URL_PREFIX}/logout`,
//获取用户信息接口
_GET_CURRENT_USER_: `${BASE_API_URL_PREFIX}/currentUser`,
//获取通告信息接口
_GET_CURRENT_NOTICES_: `${BASE_API_URL_PREFIX}/notices`,
//个人中心路由Url
_ACCOUNT_DETAIL_ROUTERURL_: '/account/center',
//个人设置路由Url
_ACCOUNT_SETTING_ROUTERURL_: '/account/settings',
//查看通知详情Url /notice/detail/:(id)
_NOTICE_DETAIL_ROUTREURL_: '/notice/detail',
//通知列表Url
_NOTICE_LIST_ROUTREURL_: '/notice/list'
},
// Http代理配置
proxy:{
'/test/baidu/': {
target: 'https://www.baidu.com/',
changeOrigin: true,
pathRewrite: { '^/test/baidu/': '' }
},
}
}
开发规范说明
- 目录结构
1.components //存放页面范围的组件
2.locales //存放本模块所有页面的国际化文本
3._mock.js //定义本模块的mock
4.index.jsx // 本模块首页,一个模块会有多个页面,请把首页定义为index.jsx
5.index-option.js // 将路由页面中 配置逻辑代码 全部放到该文件中, 以便于尽量保证index.jsx页面简约、易维护.
6.model.js //定义dva model 配置
7.service.js //定义服务端调用逻辑
8.servicetransforms.js // service中的逻辑中如果包括 数据转换,请把数据转换逻辑代码放到 servicetransforms
9.styles.less //定义本模块所有页面的样式
- components
存放页面范围的模块, 如果一个页面很复杂,需要将各个部分抽象出组件,请将组件放到components中
- locales
静态文本国际化,建议将页面中的静态文本存放在locales中, 例如:
(1)、src/pages/examples/demo/locales/zh-CN.js:
/**
* 模块国际化配置
* 1.将模块的静态资源,全部在locale.js中定义
* 2.key的规范为 pages下的目录结构名称
*
*/
export default {
'examples.demo.name': '测试名称: {name}',
'examples.demo.index1.01': '模拟表格: 作者{name}',
'examples.demo.index1.02': '模拟新增: 作者{name}',
'examples.demo.index1.03': '模拟删除: 作者{name}',
'examples.demo.index1.04': '模拟修改: 作者{name}',
}
(2)、src/pages/examples/demo/index.jsx:
import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
.....
.....
exprot default (props) => {
return (
<div>通过formatMessage的方式: {formatMessage({id: 'examples.demo.index1.01'}, {name: 'xiufu.wang'})}</div>
<div>通过FormattedMessage组件的方式: <FormattedMessage id="examples.demo.index1.01" values={{name: 'xiufu.wang'}}/> </div>
)
}
- _mock.js
-----------------------------------------------样例代码------------------------------------------------------------------------
import mockWork from '@/utils/dev/mock-work'
const assert = require('assert')
let booksList = [{id: '1', name: 'java', price: 12 }, { id: '2', name: 'javascript', price: 104 }]
export default {
// 获取 book 列表
'GET /api/books/list': mockWork(() => booksList, 'data', false),
// 获取 book
'GET /api/book/get': mockWork(req => {
try {
const r = booksList.filter(m => m.id === req.query.id)
assert.ok(req.query.id && r.length > 0, '参数传递失败(id不能为空)')
return r[0]
} catch (error) {
return error.message
}
}, 'data', false),
// 添加 book
'POST /api/book/add': mockWork(req => {
try {
assert.ok(req.body.name && req.body.price, '操作失败, 名称或价格不能为空')
booksList.push({
...req.body,
id: (booksList.length + 1) + '',
})
return booksList[booksList.length - 1]
} catch (error) {
return error.message
}
}, 'data', false),
// 删除 book
'POST /api/book/remove': mockWork(req => {
try {
assert.ok(req.query.id, '参数传递失败(id不能为空)')
booksList = booksList.filter(m => m.id !== req.query.id)
return booksList
} catch (error) {
return error.message
}
}, 'data', false),
};
-----------------------------------------------注意事项------------------------------------------------------------------------
1.建议使用mockWork, 因为mockWork内置实现了服务端的响应规范
2. 例如 /api/books/list 返回结果是
{
"expInfo":"操作成功(mock)",
"isException":false,
"globalId":"mock-100000",
"expTitle":"操作成功(mock)",
"expDetail":"操作成功(mock)",
"isSuccess":true,
// 如果过mockWork第二参数是data1,那么该属性则为 data1, 其中data的值 就是mockWork函数第一个参数的返回对象
"data":[{"name":"dddddddddddddddd","price":1000,"id":"9"},{"name":"ddddddddddddddddd","price":1000,"id":"10"}]}
3. 默认延期3s中
4.mockWork第一个参数是funtion,如果函数返回字符串,则代表异常 那么改字符串就是异常描述
- index.jsx
-----------------------------------------------样例代码------------------------------------------------------------------------
/**
* Routes:
* - ./src/pages/Authorized.jsx
*/
/**
* 首页
*
*/
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Form, Input, Button, Modal } from 'antd';
import DocumentTitle from 'react-document-title';
import classNames from 'classnames';
import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
import PageLayout from '@/components/PageLayout';
import Page from '@/utils/dev/create-page';
import objectProperty from '@/utils/dev/libs/object-property';
import styles from './styles.less';
import useSelector from '@/utils/dev/useSelector';
import usePageHook from '@/utils/dev/usePageHook';
import { saveCurrent, getCurrent } from '@/utils/dev/useCachePage';
import modelDef from './model'
const { confirm } = Modal
const Index = props => {
// 定义 state
const [ count, setCount ] = useState(1)
const nameSpace = modelDef.namespace
const { getFieldDecorator } = props.form
// usePageHook
const { pageLoadingState, initDispatch, dispatch, onDispatchComplete, context, handlers, store, memeOne, effects, options } = usePageHook({
initDipatchCount: 1,
tab(pageTitle) {
return pageTitle + ':' + count
},
// 初始化
onLoad(){
initDispatch({ type: `${nameSpace}/getBooks`, callback: onDispatchComplete })
},
onDestroy() {},
cache: getCurrent,
handlers: {
// 添加
addBook(e){
e.stopPropagation()
e.preventDefault()
props.form.validateFields((err, values) => {
confirm({
title: '确认提示',
content: '确定保存吗?',
onOk:() => {
dispatch({type: `${nameSpace}/add`, payload: {...values, price: 1000}, callback: onDispatchComplete })
setCount(r => r+1)
}
})
})
}
},
// 定义 dispatch 回调
onDispatchComplete(res, isSuccess, action) {
if (isSuccess === true && action.type === `${nameSpace}/add`){
dispatch({type: `${nameSpace}/getBooks`})
}
},
// 定义默认不可变对象
memeOne: {
defaultXXValue: { key: {}, list: [] },
},
})
// 定义useSelector
const booklist = useSelector(state => objectProperty(state, 'exampleDemo.dataList.data', memeOne.emptyArray))
// onLoad
useEffect(() => { effects.onLoad(); return () => {
effects.onDestroy(true)
} }, [])
// onTab
useEffect(() => {
effects.onTab();
}, [...effects.onTab_depts])
// onSyncCache
useEffect(() => {
if (typeof options.cache === 'function') {
const cacheCount = objectProperty(store, 'cache.count', count)
if ( cacheCount ) {
setCount(cacheCount)
}
}
}, [])
// onSaveCache
useEffect(() => { ( typeof options.cache === 'function') && saveCurrent({ count: count }) })
//测试 DocumentTitle
const title = objectProperty(context, 'pageTitle', '-') + ':' + count
// 扩展业务逻辑
useEffect(() => {
// 可以利用 pageLoadingState.current > 0 判断是否存在 初始阶段
if (pageLoadingState.current > 0){
return;
}
}, [count])
return (
<PageLayout loading={pageLoadingState.current} >
<DocumentTitle title={title}>
<>
<div><FormattedMessage id="examples.demo.index1.01" values={{name: 'xiufu.wang'}}/></div>
<hr/>
<div> {JSON.stringify(booklist, null, '')} </div>
<br/><br/>
<div>{formatMessage({id: 'examples.demo.index1.02'}, {name: 'xiufu.wang'})}</div>
<hr/>
<div>
<Form onSubmit={handlers.addBook}>
<Form.Item>
{getFieldDecorator('name', {})(
<Input placeholder="请输入名称" />,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">保存添加</Button>
</Form.Item>
</Form>
</div><br/><br/>
<div><FormattedMessage id="examples.demo.index1.03" values={{name: 'xiufu.wang'}}/></div>
<hr/>
<div> 表格数据{count} </div><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<div><FormattedMessage id="examples.demo.index1.03" values={{name: 'xiufu.wang'}}/></div>
<hr/>
<div> 表格数据 </div>
<hr/>
<div> 测试缓存数据 </div>
<div></div>
</>
</DocumentTitle>
</PageLayout>
)
}
export default Page(['exampleDemo'], [])(Index)
-----------------------------------------------注意事项------------------------------------------------------------------------
1. 如何利用useHook实现类似class模式的声明周期
为了让开发更加简单, 框架提供了usePageHook函数, 开发人员只需要按照usePageHook的配置参数,完成类似class模式的开发, 详细如下:
const { pageLoadingState, initDispatch, dispatch, onDispatchComplete, context, handlers, store, memeOne, effects, options }
= usePageHook({
initDipatchCount: 1,
解释: onLoad方法中initDispatch执行N次, 那么initDipatchCount就设置N, 必须保持一致
tab(pageTitle) {
return pageTitle + ':' + count
},
解释: 因为框架支持tab模式的模块交互,所以这个参数用于配置tab 标题,可以动态生成标题
onLoad(){
initDispatch({ type: `${nameSpace}/getBooks`, callback: onDispatchComplete })
},
解释: 模拟class compomentDidMount函数,所有初始化逻辑代码都在此函数中编写
onDestroy(){
},
解释: 模拟class compomentDidUnMount函数,若需要销毁逻辑代码,可以在此编写
cache: getCurrent,
解释: 在某些场景下,为了提高友好的交互,需要对页面状态进行缓存(基于localStorage实现)。
如果需要缓存,需要cache指定为 getCurrent, 如果不需要缓存请指定false。
handlers:{},
解释: 为了提高性能,我们不得不将函数采用useMemo创建交互阶段的事件处理函数,为了更加便捷,开发人员可以直接在handler中配置,例如
handlers: {
add(){}
}
onDispatchComplete(res, isSuccess, action){
},
解释: 处理所有的dispatch回调
memeOne:{
defaultEmptyUsers: {data: [], modules: []}
}
解释: 该参数用于性能优化, 提供对默认值的创建,保证默认值的不可变性,默认框架提供三个emptyArray, emptyObject, emptyFunction, 对于其他默认数据结构的值
我们可以利用memeOne来定义
使用实例,例如
将以下代码
const booklist = useSelector(state => objectProperty(state, 'exampleDemo.dataList.data', []))
const useInfo = useSelector(state => objectProperty(state, 'exampleDemo.dataList.userData', {data: [], modules: []}))
每一次渲染,booklist都会变
改成
const booklist = useSelector(state => objectProperty(state, 'exampleDemo.dataList.data', memeOne.emptyArray))
const useInfo = useSelector(state => objectProperty(state, 'exampleDemo.dataList.userData', memeOne.defaultEmptyUsers))
无论页面渲染多少次booklist的默认值都不会变
})
2. 为什么存在initDispatch和dispatch ?
框架规范将页面分为两个阶段: 初始化阶段(onLoad)、交互阶段(useEffect), 初始化阶段必须使用initDispatch, 交互阶段必须使用dispatch, 两个方法的调用方式是一样的,例如:
initDispatch({ type: `${nameSpace}/getBooks`, callback: onDispatchComplete })
dispatch({type: `${nameSpace}/add`, payload: {...values, price: 1000}, callback: onDispatchComplete })
3.如何给initDispatch、dispatch指定callback?
必须指定为onDispatchComplete, onDispatchComplete来自于usePageHook函数的返回结果
4. 以下代码是必须的, 由于useEffect只能在组件中使用,无法应用于在usePageHook中,为了实现usePageHook特性,所以需要将以下代码拷贝到文件中
// 存在以下代码,usePageHook中的onload方法才会有效
useEffect(() => { effects.onLoad(); return () => { effects.onDestroy(true)} }, [])
// 如果usePageHook中的tab指定设置了,必须存在下面代码
useEffect(() => {effects.onTab();}, [...effects.onTab_depts])
// 实时缓存页面状态的逻辑的代码请再此useEffect中编写(参考下面代码)
useEffect(() => { ( typeof options.cache === 'function') && saveCurrent({ count: count }) })
// 初始化时同步 缓存中的数据到当前页面状态中, 以下是参考实现代码
useEffect(() => {
if (typeof options.cache === 'function') {
const cacheCount = objectProperty(store, 'xxx.xxx', count)
if ( cacheCount ) {
setCount(cacheCount)
}
}
}, [])
5. 页面标题如何设置?
可以利用 import DocumentTitle from 'react-document-title'; 处理,
详细使用方式请参考 框架实例页面 src/pages/examples/demo/index.jsx
6. 所以的菜单页面文件"最顶部"必须存在以下代码:
/**
* Routes:
* - ./src/pages/Authorized.jsx
*/
7. 除了路由页面,所有的js、jsx文件"最顶部"都必须包括以下代码
/** */
- model.js
为了简化model的开发,框架对此进行了优化, 例如代码:
import modeEffectsReducersBind from '@/utils/dev/model-effects-reducers-bind'
import * as serviceMethods from './service'
/** */
const defModel = modeEffectsReducersBind(serviceMethods, {
getBooks: {
field: 'dataList',
initValue: [],
},
getBook: {
field: 'book',
initValue: {},
},
remove: null,
add: {field: '_add', initValue: 1},
})
export default {
namespace: 'exampleDemo',
state: {
...defModel.state,
},
reducers: {
...defModel.reducers,
},
effects: {
...defModel.effects,
},
}
框架会自动帮你翻译成传统的model定义,如下
{
namespace: 'exampleDemo',
state: {
dataList:[],
book:{},
_add: 1
},
effects: {
getBooks({ callback, payload, ...other }, { call, put }){
try{
const res = yield call(service.getBooks, payload)
callback && callback(res, true, {callback, payload, ...other})
yield put({
type: 'save',
payload: {
dataList: res
}
})
}catch(e){
callback && callback(e, false, {callback, payload, ...other})
}
},
getBook({ callback, payload, ...other }, { call, put }){
try{
const res = yield call(service.getBook, payload)
callback && callback(res, true, {callback, payload, ...other})
yield put({
type: 'save',
payload: {
book: res
}
})
}catch(e){
callback && callback(e, false, {callback, payload, ...other})
}
},
remove({ callback, payload, ...other }, { call, put }){}
add({ callback, payload, ...other }, { call, put }){}
},
reducers(){
save(state, {payload}){
return {
...state,
...payload
}
}
}
}
- service.js
-----------------------------------------------样例代码------------------------------------------------------------------------
import * as http from '@/utils/dev/libs/http';
import { test } from './servicetransforms';
import api from '../../../api.config';
// Api请求路径前缀
const baseUrl = api.basePrefix
export async function getBooks() {
const r = await http.get(`${baseUrl}/books/list`)
return test(r)
}
export async function getBook(params) {
return http.get(`${baseUrl}/book/get`, params)
}
export async function add(params) {
return http.post(`${baseUrl}/book/add`, params)
}
export async function remove(params) {
return http.get(`${baseUrl}/book/remove`, params)
}
-----------------------------------------------注意事项------------------------------------------------------------------------
1.service 负责调用服务接口、数据转换、数据清洗
2.请把数据转换清洗的逻辑代码放到 servicetransforms.js中,例如 servicetransforms.js
考虑以后的模块代码复用性,建议对每一个模块开发mock
如果你需要复用别人开发的模块时,当你将别人代码拷贝到自己的工程目录时,你首先想到的是:“跑起来看看”, 如果别人不提供给你一套完整的mock,你是跑不起来的,所以建议
大家养成编写mock的习惯
………未来会持续更新