后台项目应用分享
后台项目应用分享
webpack + react + redux + antd
策略篇
框架选择
兼容性IE9+
- 组件化:React
- 状态管理:React Redux
- 前端路由: React Router
- Ajax请求: Axios
- UI库:Ant Design
- 构建工具:Webpack
组件化开发
组件?组件!
(讨论)
CSS in JS下的样式开发思路
- 样式跟着组件走
button-group.js
button-group.less
- 使用工具方法
@import (reference) "~BaseLess";
.username{
display: inline-block;
max-width: 200px;
.text-overflow() //使用单行溢出隐藏方法
}
(注:在less文件中引用alias
定义或node _modules
下的less文件,需要在路径前加~
)
- 使用
compose
实现样式复用
/* components/Button.css */
.base { /* 所有通用的样式 */ }
.normal {
composes: base;
/* normal 其它样式 */
}
.disabled {
composes: base;
/* disabled 其它样式 */
}
imp
- 重置全局样式
.collapsed {
//anticon类原样输出
:global(.anticon) {
font-size: 16px;
margin-left: 8px;
}
:global(.anticon+span),
:global(.ant-menu-submenu-vertical > .ant-menu-submenu-title:after) {
display: none;
}
}
扩展阅读:CSS Modules 详解及 React 中实践
展示组件 VS 容器组件
我们先来看一下Redux
官方文档中的定义:
展示组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用 Redux | 否 | 是 |
数据来源 | props | 监听 Redux state |
数据修改 | 从props调用回调函数 | 向 Redux 派发 actions |
调用方式 | 手动 | 通常由 React Redux 生成 |
结合官方定义,我们把组件分为三个层次:
- 应用(App): 整个管理系统是一个应用(单页模式一般只有一个应用,多页模式可能有多个应用)
- 容器(Container):可以从路由访问得到的组件叫做容器,类似传统开发模式中的后端页面
- 组件(Component):容器以外的组件都叫组件
Action/Reducer 应该和组件绑定吗
(讨论)
调试Redux
(演示)
构建一个调试工具配置文件devtool.js
/**
* redux调试工具
*/
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
export default createDevTools(
<DockMonitor defaultIsVisible={false}
toggleVisibilityKey="alt-h"
changePositionKey="alt-q">
<LogMonitor />
</DockMonitor>
);
开发环境,在容器root.dev.js
中引入调试工具
import React from 'react';
import DevTools from './devtools';
import Layout from 'components/layout';
import style from './style.less'
export default class Root extends React.Component {
render () {
return (
<div className={style.root}>
<Layout>{this.props.children}</Layout>
<DevTools />
</div>
)
}
}
开发环境,在store.dev.js
中引入调试工具
import DevTools from 'containers/root/devtools'
export default function configureStore(initialState, reducers) {
const store = createStore(
...
compose(
...
//redux调试工具
DevTools.instrument()
)
);
...
return store
}
规范建议
命名规范
- 变量名以驼峰方式,如:
const userInfo = {}
- 类名以大写字母开头,如:
Class UserManager extend from React.Component{
...
}
- 文件(夹)名一律小写,以下划线
_
或中杠线-
作为分隔符 - 文件(夹)名以下划线
_
区分类型,如:common_action
.js,表示Action
- 文件(夹)名以圆点
.
区分环境,如:root.dev
.js,表示开发环境
模块规范
- 同一目录下的模块之间以
相对路径
的方式引用 - 不同目录下的模块之间以
绝对路径
的方式引用 - 常用模块以
alias
的方式引用(推荐alias
以大写字母开头,以区分模块路径引用)
思考:这样设计的好处是什么?
目录划分
build 构建工具及配置
dist 目标目录
src 源码目录
mock mock数据
public 开发环境临时目录
node_modules npm包目录
node_shrinkwrap npm离线包目录
延伸阅读:为什么要有npm离线包?
build:构建工具及配置
build/
lib/ 工具库
*.js
shell/ 部署脚本
*.sh
webpack.config.common.js webpack公共配置
webpack.config.dev.js webpack开发环境配置
webpack.config.prod.js webpack生产环境配置
webpack.dll.config.js webpack.dllPlugin配置
config.js 构建配置
config.js
示例:
const fs = require('fs');
const path = require('path');
//提取多文件共用配置、项目可定制的配置
const pkg = require('../package.json');
const src = path.resolve(__dirname, '../src');
const dist = path.resolve(__dirname, '../dist/resource')
module.exports = {
/*
* 以下配置在项目中通常不需要变动
*/
//package.json
pkg,
//源文件路径,使用绝对路径
src,
//导出路径,使用绝对路径
dist,
//静态资源目录,使用绝对路径
contentBase: path.resolve(__dirname, '../public'),
//导出资源映射表路径,使用绝对路径
manifest: path.resolve(__dirname, '../dist/manifest'),
//资源映射表名称,如下配置将根据当前日期生成对应的资源映射表
manifestFileName: function() {
return `${new Date().getFullYear()}-${new Date().getMonth()+1}-${new Date().getDate()}.json`
},
//dll生成路径
dllPath: {
development: path.resolve(src, 'vendor'),
production: dist
},
//dll资源映射
dllManifest: {
development: path.resolve(src, 'vendor/dll-manifest.json'),
production: path.resolve(dist, 'dll-manifest.json')
},
//js压缩配置
UglifyJsOptions: {
compress: {
//不输出警告
warnings: false
},
//不输出注释
comments: false
},
/*
* 以下配置在项目中通常需要定制
*/
//生产环境中前端资源路径(需要与nginx配置保持一致),可以为域名url
publicPath: '/Public/',
//模块别名,相对于conf.src路径配置
//- 推荐以大写字母开头,以区分非别名
alias: {
// 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-module/path/file"
// "module": "new-module",
// 起别名 "only-module" -> "new-module",但不匹配 "module/path/file" -> "new-module/path/file"
// "only-module$": "new-module",
// 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
// 模块别名相对于当前上下文导入
// "module": "./app/third/module.js"
"Coms": "components/common",
"ActionTypes$": "utils/action_types.js",
"Api$": "utils/api.js",
"Helper$": "utils/helper.js",
"Ajax$": "utils/ajax.js",
"BaseLess$":"utils/baseless/baseless.less"
},
//代理配置
proxy: {
"/": {
target: "http://test-matrix-v2.yileyoo.com",
changeOrigin: true
}
},
//mock配置
mock: {
//mock目录,使用绝对路径
mockPath: path.resolve(__dirname, '../mock'),
//支持设置统一接口后缀,如:.do
apiExt: ''
},
//html模板配置
template: {
all:{
title:'Matrix管理平台'
},
development:{
serverOutput:'<script src="/server/output"></script>'
},
production:{
serverOutput:'<script>window.REDUX_STATE = {!! $jsData !!};</script>'
}
},
theme: path.resolve(src, 'theme/red.less')
}
dist:存放构建结果
dist/
resource
index_xxx.html
app_xxx.js
vendor_xxx.js
app
manifest
2017-x-x.json
src:源码目录
src/
----------------------------------------------------
actions/ Redux Action目录
*_action.js
reducers/ Redux Reducer目录
*_reducer.js
store/ Redux Store目录
index.js
store.dev.js
store.prod.js
----------------------------------------------------
routes/ React路由目录
index.js
----------------------------------------------------
containers/ React容器目录
root/
index.js
root.dev.js
root.prod.js
devtools.js
components/ React组件目录
common/ React公共组件目录(alias:Coms)
page_1/
index.js
*.js
*.less
... React页面组件
page_n/
----------------------------------------------------
vendor/ 第三方库目录
vendor.js
verdor.js.map
dll-manifest.json
static/ 静态资源目录
favicon.ico
utils/ 工具目录
ajax.js ajax工具(alias:Ajax)
api.js api工具(alias:Api)
action_types.js action类型工具(alias:ActionTypes)
helper.js 常用工具方法(alias:Helper)
baseless/*.less 常用less方法(alias:BaseLess)
theme/ 主题目录
*.less
----------------------------------------------------
config/ 配置目录
*_config.js
----------------------------------------------------
mock:存放mock数据的目录
mock/
*.js
*.json
public:开发环境临时目录
public/
index.html
npm package:npm包
node_modules npm包目录
node_shrinkwrap npm离线包目录
other files:根目录下其他文件
.gitignore git忽略配置
.gitlab-ci.yml gitlab-ci配置
npm-shrinkwrap.json npm包版本锁定配置
package.json npm包配置
webpack.config.js webpack配置入口
README.md 说明文档
UI篇
如何引入ant
- 使用 babel-plugin-import(推荐)
// .babelrc or babel-loader option
{
"plugins": [
["import", { libraryName: "antd", style: "css" }] // `style: true` 会加载 less 文件
]
}
然后只需从 antd 引入模块即可,无需单独引入样式。等同于下面手动引入的方式。
// babel-plugin-import 会帮助你加载 JS 和 CSS
import { DatePicker } from 'antd';
如何定制antd
antd
通过less
变量提供了较灵活的主题定制功能。(参考:修改 Ant Design 的样式变量)
需要修改babel-loader
配置:
{
"plugins": [
["import", {
libraryName: "antd",
style: true // 这里需要修改为`style: true`以实现主题配置
}]
]
}
这里我们做了简单的封装:
1)在src/theme
目录建一个主题文件,如:red.less
(参考:antd默认主题文件)
@primary-color: #f00;
2)在build/config.js
文件配置主题文件的路径
{
...
theme: path.resolve(src, 'theme/red.less')
}
后续我们还将推出主题配置监听
、多主题切换
等功能,敬请期待~
动手写一个antd组件
任意在项目中可复用的组件,都可以通过antd组件组合成一个通用(业务)组件。
注意,这里的组件是Component
(见前面的定义)类型。
(演示)
工具篇
devServer什么鬼
- 一个node express应用
- 提供静态资源服务
- 支持文件监听
- 支持热加载
- 支持路由代理
- 支持接口转发
调用方式非常简单,在命令行执行:
webpack-dev-server --env development --port 3000 --hot --inline --progress --open
webpack中的相关配置:
//开发服务器配置
devServer: {
//告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要。
//devServer.publicPath 将用于确定应该从哪里提供 bundle,并且此选项优先。
contentBase: [
conf.contentBase,
path.join(conf.src, 'static'),
path.join(conf.src, 'vendor')
],
//信息显示配置
stats: "normal",
//是否显示全屏遮罩
overlay: true,
//watch配置
watchOptions: {
ignored: /node_modules/,
aggregateTimeout: 300,
poll: 100
},
//联调模式下,使用数据代理
proxy: isDebug ? conf.proxy : {},
//开启浏览器历史
historyApiFallback: true,
//扩展devServer
setup(app) {
//非联调模式下,使用mock数据
!isDebug && makeMock(app, conf.mock)
}
}
不容忽视的HtmlWebpackPlugin
- 自动引用webpack生成的资源,支持过滤
- 默认支持
ejs
模板,可以注入变量,语法亲和 - 社区生态好,一众扩展插件可以满足各种需求(参考)
dllPlugin教程没有教你
教程很多,随便找一篇:怎样令webpack的构建加快十倍、DllPlugin的用法
不实用!!!
get√
到打开方式后,我们来针对实际情况做个总结:
- 区分开发和生产环境
开发环境 | 生产环境 | |
---|---|---|
警告信息 | 是 | 否 |
内容压缩 | 否 | 是 |
文件hash | 否 | 是 |
SourceMap | 是 | 可选 |
- 怎么在页面引用
使用add-asset-html-webpack-plugin插件
{
plugins:[
...
// 在入口页面中引入静态资源
new AddAssetHtmlPlugin({
//通过dllManifest读取dll文件名
filepath: path.resolve(conf.dllPath[NODE_ENV], `${dllManifest.name}.js`)
})
]
}
- 加一个库就必须手写一下
entry
? 不需要!!!
entry: {
//读取package.json中的依赖
vendor: Object.keys(conf.pkg.dependencies)
}
- 完整的
webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
const conf = require('./config');
module.exports = function( /*通过命令行参数--env传入*/ NODE_ENV) {
//是否生产环境
const isProd = NODE_ENV === 'production';
//文件名(不带后缀)
const name = `[name]${isProd?"_[chunkhash:8]":""}`;
//输出文件路径
const filePath = conf.dllPath[NODE_ENV];
//输出manifest路径
const manifest = conf.dllManifest[NODE_ENV];
//sourcemap配置
const devtool = isProd ? '' : 'source-map';
//插件
let plugins = [
new webpack.DllPlugin({
//解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致。
context: __dirname,
//manifest.json文件的输出路径,这个文件会用于后续的业务代码打包
path: manifest,
//dll暴露的对象名,要跟output.library保持一致
name: name
})
];
//生产环境使用压缩版
if (isProd) {
plugins = plugins.concat([
//变量定义
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV)
}),
// js压缩配置
new webpack.optimize.UglifyJsPlugin(conf.UglifyJsOptions)
])
}
return {
entry: {
//读取package.json中的依赖
vendor: Object.keys(conf.pkg.dependencies)
},
output: {
path: filePath,
filename: name + '.js',
//需要与filename保持一致,用于页面引用
library: name
},
devtool,
plugins
}
};
扩展阅读:webpack 构建性能优化策略小结
思考:相比其他方案,dllPlugin的优势在哪里?
SourceMap全方案
- 在
loader
中启用SourceMap
{
loader: 'xxx-loader',
options: {
...
sourceMap: true
}
}
- 在
devtools
中配置SourceMap
类型
开发环境:唯快不破,最大化满足调试需求
{
...
devtool: 'cheap-module-eval-source-map'
}
生产环境:需要考虑安全性和性能
{
...
devtool: 'source-map'
}
扩展阅读:Webpack devtool source map
拆分配置文件
webpack.config.common.js 公共配置
webpack.config.dev.js 开发环境配置
webpack.config.prod.js 生产环境配置
拆分原则:
module
和resolve
在开发和生产环境中的配置差异性相对较小,非常适合抽取到公共配置中entry
、output
和plugins
相对来说开发和生产环境有不同的配置,因此放到dev
和prod
各自配置中devtool
和devServer
等仅出现在开发环境的配置直接放入dev
配置中
协同篇
承载页
纯静态 vs 服务端渲染
- 纯静态:服务端仅提供数据接口,彻底不需要后端维护,缺点是无法做资源回溯和切换
- 服务端渲染:服务端提供数据接口,并将部分数据或状态渲染到页面,缺点是耦合了前后端部署逻辑
对服务端渲染的改进:
- 将后端模板指向前端部署目录的html文件,如:
index.html
- 使用固定的后端模板,将数据或状态以JSON对象的方式输出
- 使用HtmlWebpackPlugin,将后端模板注入到自动生成的页面中
build/config
中的配置:
//html模板配置
template: {
all:{
title:'Matrix管理平台'
},
development:{
serverOutput:'<script src="/server/output"></script>'
},
production:{
serverOutput:'<script>window.REDUX_STATE = {!! $jsData !!};</script>'
}
}
webpack中的配置:
{
plugins:[
...
// 根据模板创建入口页面
new HtmlWebpackPlugin(Object.assign({
template: path.resolve(conf.src, 'index.ejs'),
filename: path.resolve(conf.contentBase, 'index.html')
}, /*全环境模板配置*/conf.template.all, /*当前环境模板配置*/conf.template[NODE_ENV]))
]
}
在index.ejs
模板中引用
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
</head>
<body>
<div id="root" style="height: 100%"></div>
<%= htmlWebpackPlugin.options.serverOutput %>
</body>
</html>
资源灰度 & 回溯
使用 assets-webpack-plugin插件,在webpack中生成资源映射json文件
{
plugins:[
...
//生成资源映射表
new AssetsPlugin({
path: conf.manifest,
filename: conf.manifestFileName(),
processOutput: function (assets) {
//注入dll依赖信息
assets.vendor = {
'js': conf.publicPath + dllManifest.name + '.js'
};
return JSON.stringify(assets);
}
})
]
}
两种方案:
- 同一个html模板,仅切换资源。需提供资源映射表
- 不同html模板,切换模板。需要提供模板映射表
数据接口
文档
——接口定义,告诉我们有哪些接口,接口支持哪些http方法,每个接口字段的含义是什么等
(演示)
mock
——接口没有开发完成,前端根据接口文档模拟的数据
(演示)
proxy
——接口已经开发完成,使用代理的方式实现本地接口联调
webpack中的相关配置:
devServer:{
...
proxy: {
"/api": "http://localhost:3000"
}
}
权限控制
页面权限
前端:
- 在
react-router
中定义所有页面的路由 - 页面初始化时,请求后端接口获取菜单权限
- 仅显示具备权限的菜单
后端:
- 提供获取菜单权限的接口
- 将接受到的路由请求作过滤,具备权限的则转向前端页面
- 将404/500等错误路由转到前端页面
操作权限
前端:
- 请求后端接口获取操作权限
- 根据操作权限控制操作按钮是否显示
后端:
- 提供获取操作权限的接口