前端环境搭建

首先得有node,并确保 node 版本是 10 或以上。(mac 下推荐使用 nvm 来管理 node 版本)

  • 安装tyarn (建议使用tyarn),如下:
  1. # 国内源
  2. $ npm i yarn tyarn -g
  3. # 后面文档里的 yarn 换成 tyarn
  4. $ 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的习惯

………未来会持续更新