项目技术栈
- 框架:React
- 状态管理:Dva + React Redux
- React Hook 库:react-use
- UI 组件库:antd 、@share/shareui 、@share/list 、 @share/shareui-form、@share/pro-table
- 网络请求库:@share/network
目录结构
|- bin/ 命令行程序
|- public/ 公共资源
| |+ browserUpgrade/ 提示浏览器升级页面
| |- favicon.ico
|
|- src/
| |+ assets/ 存放资源文件,字体文件、图片
| |+ components/ 存放公共组件
| |+ config/ 存放公共配置文件
| |+ layouts/ 存放布局组件
| |+ models/ 存放 dva 定义文件
| |+ pages/ 存放页面
| |+ services/ 存放接口请求方法定义文件
| |+ styles/ 存放公共样式文件
| |+ utils/ 存放公共工具类
| |- globals.d.ts 全局类型定义文件
|
|- templates/ 存放用于创建文件的模板
| |+ components/ 组件模板
| |+ model/ dva 定义文件模板
| |+ mpa/ 多页应用文件夹模板
| |+ page/ 页面文件模板
|
|- .editorconfig 给编译器识别的配置文件
|- .eslintignore ESLint 忽略文件配置
|- .eslintrc ESLint 配置
|- .gitignore git 忽略文件配置
|- .npmrc npm 环境变量配置
|- application.js 项目构建配置
|- jsconfig.json JS 配置
|- package.json
|- plopfile.js plop 配置
|- README.md
|- webpack.config.js
|- yarn.lock
页面结构
页面文件结构
|- pages/
|+ 单页应用1/
|+ 单页应用2/
|- 单页应用3
|- routes/
| |+ 页面1/
| |+ 页面2/
| |+ 页面3/
|- index.ejs
|- index.js
整个项目采用多个单页应用的形式开发,所有应用都的页面都放在 pages
目录里,pages
下存放着一个个单页应用,每个单页应用之间互不影响,技术栈可以不一样。
单页应用至少包含两个文件:
index.js
单页应用入口文件index.ejs
单页应用 html 文件
如果使用路由,页面组件都放在 routes
目录下。
而页面组件的结果如下:
|- 页面1/
|- Home.js
|- Home.css
|- index.js
|- README.md
除了基本的组件结构,还需加上 README.md
文件,文件内容例如:
## 首页
### 访问地址
http://localhost:4000/social-governance/#/
### 原型地址
http://192.168.0.235:8080/ZZPT/08RPPublish/%E5%B8%82%E5%9F%9F%E7%A4%BE%E4%BC%9A%E6%B2%BB%E7%90%86%E4%B8%AD%E5%8F%B0/%E9%97%A8%E6%88%B7%E7%BD%91%E7%AB%99/index.html#id=tuf5w8&p=%E9%A6%96%E9%A1%B5&g=1
描述一些基本信息:这是什么页面?访问路径是怎样的?原型地址在哪?
结构拆分原则
结构拆分就是将页面文件分散放到多个单页应用里。
不拆分的话会导致整个应用太大,而拆分过细又会导致开发体验和用户交互体验下降,所以需要定个拆分原则。
通常系统都有分主业务和分支业务:
- 主业务页面是系统主要功能,页面直接可能存在频繁交互的。
- 分支业务就是一些相对比较独立的业务,与主要功能很少交互的。
实际项目页面结构:
|- pages/
|+ 89c5e1a8b94841729a8be7e17fdf6e6a-test-login/
|+ 404/
|- index/
| |- routes
| | |- Home/
| | | |- Home.js
| | | |- Home.scss
| | | |- index.js
| | | |- README.md
| | ...
| |
| |- index.ejs
| |- index.js
|
|+ login/
这边拆分为四个单页应用,从上到下依次是:模拟登录、404页、首页、登录页,其中模拟登录名字那么长是防止用户猜到地址而导致非法登录。
主业务放在 index
里,其他都是分支业务。
- 模拟登录用户不会访问的;
- 404 是地址输错才展示,正常不会访问到;
- 登录页只会访问一次,而且与主业务没有数据上的交互,相对比较独立的功能块。
这系统业务比较单一,拆分的比较简单,业务如果比较多元化的话就会拆得比较细。就比如数据开放平台,有个商业选址和开放指数,这块虽然是从主业务跳转过去的,但业务是比较独立的,与主业务没有业务上的交集。
总的来说拆分的原则就是业务相对独立的功能块。如果无法判断是否是独立的话就考虑是否满足以下条件:
- 很少会访问到的页面
- 没有数据上的交互
规范约定
组件目录结构
每个组件都要有单独的文件夹来管理,且文件夹名称与组件同名,每个文件夹下都要有个 index.js
用来映射组件。如下:
- Example
|- Example.js
|- index.js
以上是组件基本格式。
index.js
的内容只是简单的一句:
export { default } from './Example';
如果组件里还拆分出子组件,就在组件所在目录建一个 components
文件夹存放,子组件同样要遵守组件规范。如果子组件是很多组件公用的,就提到公共组件(/src/component
)里面。组件内需要用到样式,就建一个与组件同名的 SCSS 文件,最终的结构如下:
Example/
|- components/
| |- Children1/
| | |- Children1.js
| | |- Children1.scss
| | |- index.js
| |- Children2/
| |- Children2.js
| |- index.js
|- Example.js
|- Example.scss
|- index.js
组件内可能需要抽离出逻辑代码,那就创建一个 hooks.js
文件专门用来放逻辑代码:
Example/
|+ components/
|- hooks.js
|- Example.js
|- Example.scss
|- index.js
如果组件不只是需要抽离 hook 方法,还存在一些非公共的工具方法、数据文件等,那就建一个 logic
文件夹存放:
Example/
|+ components/
|- logic/
| |- config.js
| |- utils.js
| |- hooks.js
| |- data.js
|- Example.js
|- Example.scss
|- index.js
代码规范
以下是基于公司前端开发规范做了一些补充,开发前请检先查下编译器有没有把 eslint
打开。
一个文件里只能有一个组件,且文件名与组件名一致。
文件导入顺序
// scss import styles from './Example.scss'; // react系列 import React, { useState, useEffect } from 'react'; import { connect } from 'dva'; import { withRouter } from 'dva/router'; // 组件,第三方组件 > 公司私库 > 布局组件 > 最外层公共组件 > 模块公共组件 > 子组件 import { Spin } from 'antd'; import { Button } from '@share/shareui'; import BasicLayout from '@/layout/BasicLayout'; import Header from '@/Header'; import Children from './components/Children'; // 网络请求方法 import { findProjectList } from '@/server/safeOpen'; // 配置文件 import { PROJECT_STATUS } from '@/config/status'; import { mapProjectStatus } from '@/config/typeMap'; // 工具类,第三方库 > 公共工具类 > 局部工具类 import _ from 'lodash'; import cx from 'classnames'; import { getUrlSearch } from '@/utils'; // 逻辑代码 import { defaultFormData, useLogic } from './logic'; // 资源文件 import logoImg from '@/assets/images/logo.png'; import bannerImg from './images/banner.png';
组件需先赋值给一个变量,再导出这个变量。
// 坏的 export default () => { return 'hello world'; }
// 好的 const Example = () => { return 'hello world'; } export default Example;
条件状态判断时不允许与数值直接做判断。
// 坏的 if (data.projectStatus === '20') { // ...}
// 好的 import { PROJECT_STATUS } from '@/config/status'; // ... if (data.projectStatus === PROJECT_STATUS.ACCEPTED) { // ... }
暴露给外部使用的句柄以
on*
格式命名,使用句柄以handle*
命名// 组件定义 const MyInput = ({ onUpdate }) => ( <Input onChange={onUpdate} /> ); // 组件使用 const Page = () => { const handleUpdate = () => {}; return ( <MyInput onUpdate={handleUpdate} /> ); }
文件命名规范
类型 | 例子 |
---|---|
camelCase |
projectApply |
snakeCase |
project_apply |
dashCase |
project-apply |
properCase |
ProjectApply |
constantCase |
PROJECT_APPLY |
通常变量和文件命名使用 camelCase
,以下是特殊情况:
- 组件文件和组件样式文件使用
properCase
- CSS全局类名(使用
:global
包起来的)使用dashCase
- 单页应用根目录命名使用
dashCase
开发流程
主要流程:
- 创建单页应用
- 创建页面
- 启动项目
- 开发页面
- 发包
下列详细讲解各个部分包含的东西。
创建单页应用
之前的说法是创建多页应用
创建单页应用和页面使用 plop 创建,模板文件放在 templates
目录下,根据根目录下的配置文件 plopfile.js
来创建。通过执行 yarn run create
来触发程序:
$ yarn run create
? [PLOP] Please choose a generator. (Use arrow keys)
page - 创建页面
component - 创建公共组件
> mpa - 创建多页应用
model - 创建 model
? 请填写文件夹名称:test-app
? 请填写多页模块的标题:测试标题
执行后会在 pages
下创建一个文件夹 test-app
,目录结构如下:
- pages/
|- test-app/
|- routes/ 存放页面组件,初始空目录
|- index.ejs html入口文件
|- index.js js入口文件
index.ejs
文件内容:
<%= require('@/config/htmlEntryTpl.ejs')({
title: '测试标题'
})%>
直接引用了 @/config/htmlEntryTpl.ejs
文件,所有单页应用共用此页面,传递标题参数过去。
htmlEntryTpl.ejs
除了 html 基础内容,还设置了:
- 如果是互联网项目,需要设置
keyword
和description
- 判断浏览器版本信息,如果 IE < 10 则跳转到浏览器升级提示页面
- 页面初始展示加载中样式
- 如果是互联网项目,生产环境下加入百度统计
index.js
文件内容:
import '@/config/global';
import setRouter from '@/config/router';
setRouter(module.id);
@/config/global
每个应用都会引入,所以也算是全局入口文件,在里面做一些样式和字体引入的操作;- 设置路由配置,传入当前模块 id(
module.id
是 webpack 提供的 API),路由自动化会根据模块 id,找到对应的路由配置进行设置。setRouter
做的事情是:- 根据路由配置文件创建路由配置
- 设置首页和 404
- 设置错误边界
- 设置布局组件
- 初始化 dva、引入 model 文件
setRouter
也可传入对象:
setRouter({
moduleId: module.id,
indexPath: '/list', // 首页地址,访问 / 会重定向到 /list 页面
routes: [], // 额外的路由配置
Layout: CustomLayout // 自定义布局组件
})
创建页面
$ yarn run create
? [PLOP] Please choose a generator. (Use arrow keys)
> page - 创建页面
component - 创建公共组件
mpa - 创建多页应用
model - 创建 model
? 请选择所属多页模块: (Use arrow keys)
> index
? 请填写页面组件名称: Demo
? 请填写页面标题: 测试页面
? 请填写页面原型地址: xxx
完成后在 src/pages/index/routes
下会创建一个文件夹 Demo
,目录结构如下:
- Demo/
|- Demo.js
|- Demo.scss
|- index.js
|- README.md
除了一般的组件结构外,还多了一个 README.md 文件:
## 测试页面
### 访问地址
http://localhost:7000/social-governance/#/demo
### 原型地址
xxx
创建页面时输入的标题和原型地址将写在这文件里,同时根据组件名称计算出页面访问地址。
页面组件与一般的组件有些不同,以创建出来的 Demo.js
文件为例:
import styles from './Demo.scss';
import React from 'react';
const Demo = () => {
return (
<div className={styles.demo}>
测试页面
</div>
);
};
Demo.title = '测试页面';
export default Demo;
与一般的组件相比,多了一行 Demo.title = '测试页面'
,这是在设置布局属性,也就是传给布局组件的数据,后面讲到布局组件的时候再细说。
启动项目
使用 yarn
管理项目
# 安装依赖
$ yarn install
# 启动项目
$ yarn start
下列讲解执行 yarn start
具体做了什么事情。
项目的 npm scripts
定义(隐藏了一些无关的)是:
{
"scripts": {
"start": "run-p server router-watch",
"server": "sharekit server",
"router-watch": "node bin/router-watch"
}
}
执行 yarn start
实际上是执行了以下命令行:
$ run-p server router-watch
run-p
是安装 npm-run-all 模块提供的命令行,表示并行运行后面的命令,也就是同时运行 server
和 router-watch
命令。而 server
命令指向 sharekit server
,router-watch
指向 node bin/router-watch
。
sharekit server
这个是脚手架自带的原本用来启动项目的命令,功能不变,就不讲了。node bin/router-watch
用于监听路由变化,是路由自动化的启动命令
总的来说这边是在启动项目的同时,加入路由自动化。
开发页面
这边主要讲解开发过程中要遵守的一些业务开发规范。
表码管理
开发表单页或者做列表查询的时候经常会用到下拉框、复选框、单选框,这时候就需要 options
数据,而大多数 options
数据是从表码获取的。项目提供了一个 hook 方法用来获取表码数据,在 src/utils/dvaHooks.js
文件里的 useBmList
,用法如下:
import { useBmList } from '@/utils/dvaHooks';
const Page = () => {
// 单个
const bmList = useBmList('BM_PROVIDE_DEPT_FULL');
// 多个
const bmList = useBmList([
'BM_PROVIDE_DEPT_FULL',
'OPEN_DATA_FORMAT_CNAME'
]);
// 使用
console.log(bmList.BM_PROVIDE_DEPT_FULL)
return (
// ...
);
}
表码数据存在 dva 里面 ,在同一个单页应用里面共享表码数据。第一次请求会把数据存起来,第二次直接取之前存的数据。需要注意的是,如果是第一次访问,首次渲染 bmList.BM_PROVIDE_DEPT_FULL
返回 undefined
。
常量管理
一般做业务条件判断,大多是对数据状态做判断,例如判断能力资源是否是待审批状态。这个状态数据统一放在 src/config/status.js
文件里:
// 能力资源审核状态
export const ABILITY_STATUS = {
DRAFT: '00', // 草稿
WAIT_AUDIT: '10', // 待审核
SUCCESS: '20', // 审核通过
FAIL: '30', // 审核不通过
DEL: '70', // 已删除
};
有时在列表里展示数据时,需要根据不同的状态展示不同的图标或颜色,这样的映射数据统一放在 src/config/tyoeMap.js
文件里:
// 应用审批状态
export const mapAbilityStatus = {
[ABILITY_STATUS.DRAFT]: { color: '#0bd', text: '草稿' },
[ABILITY_STATUS.WAIT_AUDIT]: { color: '#0bd', text: '待审批' },
[ABILITY_STATUS.SUCCESS]: { color: '#0b8', text: '审批通过' },
[ABILITY_STATUS.FAIL]: { color: '#f65', text: '审批不通过' },
};
网络请求
这个主要讲解接口请求方法的定义规范。
所有的接口请求都需要定义在方法里,而不是直接在页面上调用。而接口请求方法都需要定义在 src/services
文件夹里,根据功能模块拆分存放,例如:
services/
|- apps.js 应用模块
|- catalog.js 数据目录模块
|- common.js 公共模块
...
每个文件都要准守统一的方法定义格式,例如 apps.js
:
import network from '@/utils/network';
/**
* 获取开放应用列表
* YApi地址:http://192.168.0.62:3000/project/218/interface/api/35842
* @returns {Promise}
*/
export function findAppOpenList(data) {
return network.post('/app/openList', data);
}
/**
* 获取应用查询排序列表
* YApi地址:http://192.168.0.62:3000/project/218/interface/api/35866
* @returns {Promise}
*/
export function getAppOrderList() {
return network.get('/appOrder/list');
}
// ...
每个方法需要写 JSDoc,写明接口作用和 YApi 地址就好,想写得更完善点的话可以把入参类型和出参类型写进去。方法内容不需要写多复杂,GET 还是 POST 请求,请求地址是什么,做到这一步就足够,剩下的就是业务的问题。
发包
通常是到 “开发部署平台” 发包。
不过有的项目在开发部署平台上找不到前端模块,目前记得的是开放指数和可视化大屏项目。这种需要经过以下步骤:
- 本地打包,
- 将编译文件复制到对应后端工程的
src/main/resources/static
目录下进行覆盖 - 提交编译代码
- 去开发部署平台发包
路由自动化
在启动项目的同时会开启路由自动化程序,路由自动化会实时的监听 src/pages
文件夹下的文件变更,一旦变更就会重新生成 src/config/mapRoutes.js
文件,而路由配置就是根据这个文件来构建路由。整个开发的过程不需要自己去写路由配置,能根据文件结构实时自动生成对应的路由。
路由自动化程序是使用 NodeJS 开发的,代码放在 bin
目录里面,相关文件结构如下:
bin/
|- model/ 模型对象
| |- Map.js 单页应用信息(以前叫它多页信息)
| |- Spa.js 页面信息(以前叫它单页信息)
| |- Router.js 路由信息
| |- Generator.js 生成器,用于创建文件
|- router-watch.js 启动入口文件
入口文件 router-watch.js
代码如下:
const Router = require('./model/Router');
const PAGE_ROOT = './src/pages'; // 页面根路径
const OUTPUT_PATH = './src/config/mapRoutes.js'; // 配置文件输出路径
const router = new Router(PAGE_ROOT, OUTPUT_PATH);
router.watch();
其实就只是创建了 Router
实例,然后调用了它的 watch
方法。
watch
方法的大致流程如下:
- 打印更新日志
- 获取当前配置
- 读取
src/pages
下的所有文件夹名称 - 根据这些文件夹名称创建 Mpa 实例
- Mpa 实例内部再创建 Spa 实例
- 遍历所有 Mpa 里的所有 Spa 实例,调用
getPageData
- 数据组装
- 读取
- 对比新旧配置,如果有变更才往下走
- 对配置数据做格式转化
- 将数据写入到
src/config/mapRoutes.js
文件里
生成的文件代码如下:
export default {
[require.resolve('../pages/index/index.js')]: [
{
path: '/app',
component: () => import('@/pages/index/routes/App'),
exact: true,
title: '应用超市',
description: 'Application Center'
},
// ...
]
}
require.resolve()
用来获取 module.id
,也就是以 module.id
作为 key ,使用的时候通过 key 值获取对应路由配置。
路由配置除了常规的 path
、component
、exact
,还多了 title
和 description
,这两个属性是通过解析布局属性获取,所以这个页面组件代码应该是这样定义的:
import React from 'react';
const App = () => { /* ... */ }
App.title = '应用超市';
App.description = 'Application Center';
export default App;
这边会将所有页面组件的静态属性(代码位置固定写在以上位置)解析到路由配置上。
注意:这边只接收静态数据,如果传了一个变量进来是无法识别的。
有了这个 src/config/mapRoutes.js
文件后,路由就可以通用同一个组件,只要把 module.id
传进来就能自动配置对应的路由。
注意:这个 module.id
只能在单页应用的入口文件 index.js
传入,不然找不到对应的路由配置。
布局组件
布局组件就是专门用来开发公共布局的组件,例如头部、底部、banner,组件存放在 src/layouts
目录下。
组件的 props
接收来自路由配置传来的布局属性,当前基础布局组件里除了开发公共布局,还做了以下事情:
- 获取用户信息,期间页面处于加载中状态,接口响应回来后才展示页面
- 浏览器滚动条置顶
- 权限判断,如果此页面需要登录,就会判断是否未登录,未登录展示登录提示页
后续需求有调整的话可以基于这个组件再封装一个其他布局组件。