1-2 前置准备css
开发环境:html
Node.js(v8.2+)前端
NPM(v5.2+)react
Visual Studio Code(VS Code)webpack
VS Code经常使用插件:git
Prettier-Code formatter 格式化代码github
Reactjs code snippets 快速生成 react 经常使用模块化代码web
Auto Rename Tag 对相关标签重命名时,对应标签相应改变算法
主要依赖库版本(需高于如下版本):数据库
React: ^16.4.1
Redux: ^4.0.0
React Redux: ^5.0.7
React Router: ^4.3.1
2-1 建立项目结构
React项目脚手架: create-react-app
零配置建立 React 应用 (不须要配置babel和webpac等)
构建: JS、CSS、图片 (资源打包构建)
开发效率: 自动刷新、代理转发、单元测色等
create-react-app 的使用
新建项目:npx create-react-app [项目名] (npm >= 5.2)
在命令面板安装 code 就能够在终端中使用 code 能够在vs code 中快速打开项目
package.json
{ "name": "dz-app", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.10.1", "react-dom": "^16.10.1", "react-scripts": "3.1.2" //其余的相关依赖都封装到了react-script中,webpack。babel等 }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", //测试 "eject": "react-scripts eject" //将本来相关配置的封装弹射出来,将wepack.cofig在项目中显现出来 },
使用 Mock 数据
方式一: 代理到 mock 服务器 (经过开启一台mock服务器,将mock数据放到该服务器上,将前端请求转发到这个服务器上)
npm install -g serve 安装服务
在package.json 中配置
“”proxy“: {
"/api": {
"target": "http://localhost:5000"
}
}
方式二:直接将 mock 数据放到项目 public 文件夹 (public 中新建 mock 文件夹 > data.json 文件) 经过localhost:3000/mock/data.json 就能访问
缘由是:public 文件夹的静态资源是不会被构建的,打包后直接放到项目中使用的
3-1 组件划分
组件划分原则
解耦:下降单一模块/组件的复杂度
复用:保证组件一致性,提高开发效率
组件颗粒度须要避免过大或太小
3-2 编写静态组件
开发过程解耦:静态页面和动态交互
组件开发顺序:自上而下 or 自下而上
App -> TodoList -> Todo -> AddTodo -> Footer
3-3 如何设计 State
什么是 State?
表明 UI 的完整且最小状态集合
如何判断一个变量是不是 State
是否经过父组件props 传入? 经过父组件传入的不是
是否不会随着时间、交互操做变化? 不会随时间、交互操做的不是
是否能够经过其余 state 或 props 计算获得? 可经过其余 state 或 props 计算获得的不是,有冗余,不符合最小状态集合
3-4 分析 State 保存位置
State 的双层含义
表明应用UI 的全部状态的集合
状态集合中的每一部分(待办事项列表、新增输入框文本、筛选条件)
肯定依赖 state 的每个组件
若是某个 state 被多个组件依赖,寻找共同的父组件(状态上移)
3-5 添加交互行为
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
4-1 Redux 基本思想
4-2 设计应用 Sate
集中管理,全局惟一
不可变性
定义方式同React State
4-3 定义Action
描述如何修改状态
JSON对象,type属性必需
发送: store.dispatch
4-4 action的处理器: reducer
4-5 reducer 拆分
便于扩展和维护
合并API: combineReducers
4-6 建立store
4-7 集成react-redux(容器型组件拆分)
向根组件注入Store -> Provider组件
链接React 组件和Redux状态层 -> connect
获取React 组件所需的 State和 Actions -> map api
4-8 集成react-redux(容器型编写)
4-9 集成react-redux 回顾
5-1 项目结构组件方式
Redux 项目结构组织方式
按照类型
按照功能模块
Ducks(鸭子)
按照类型 ( 指的是一个文件在项目中充当的角色类型,即这个文件是一个组件、容器、reducer、action,会放在不一样的文件下,redux官网示例采用的结构 )
缺点:开发一个功能的时候须要频繁更换目录修改不一样的文件,当项目结构逐渐的变大就会变的不方便
actions/ components/
--action1.js --component1.js
--action2.js --component2.js
reducers/ containers/
--reducers.js --container1.js
--reducer2.js --container2.js
按照功能模块 ( 一个功能模块对应一个文件夹 )
优势: 方便开发,易于功能扩展
缺点: redux 会将整个应用的状态做为一个Store来管理,也就是做为全局的state来管理,不一样的功能的模块之间共享这个全局的state的部分状态,可能不一样功能模块之间出现耦合
feature1/ feature2/
-- components/ -- components/
-- Comtainer.js -- Container.js
-- actions.js -- actions.js
-- reducer.js -- reducer.js
Ducks
最初来源:https://github.com/erikras/ducks-modular-redux
提倡将相关的reducer、 action types、 action 写到一个文件里、本质上以应用的状态做为模块的划分依据,不是以界面功能做为依据,这样管理相同的依赖都在同一文件中,无论哪一个容器组件须要使用,只需引用便可
reducers、action types、actions 组合到一个文件中,做为独立模块
划分模块依据: 应用状态State,而不是界面功能
ducks
src
components/ (放置整个应用级别的通用组件,通用的loading,错误弹框等)
containers/ (按照页面的功能模块划分,每一个功能模块对应一个文件夹,功能模块下又会有component文件夹,这里的component夹 存放的是这个功能模块的专用组件,他们的复用性相对较弱,只在这个功能模块下使用,而index.js 是feature文件夹 对外暴露的接口,是容器型组件,这种项目结构组织方式,将视图层和状态层完全解耦,甚至可将前端开发人员 分为两部分 视图层 和 redux状态层的开发,这种状况下feature1 和 feature2 每一个功能模块 只须要知道redux 中的哪些模块,而后引用这些模块就能够了)
feature1/
compinents/ (这里的component夹 存放的是这个功能模块的专用组件,他们的复用性相对较弱,只在这个功能模块下使用)
index.js/ (而index.js 是feature文件夹 对外暴露的接口,是容器型组件)
feature2/
redux/n
index.js
module1.js (对应redux 项目结构组织当中的一个模块, 包含了所使用的actions、reducer、action creator,因此这种项目结构组织方式会根据应用状态来划分)
module2.js
index.js
还需对模块进行调整
// Action Creators 会将action 定义到一个命名空间当中
export const actions = {
loadWidget: function loadWidget() {
retrun { type: LOAD }
}
createWidget:
updateWidget: updateWidget
}
5-2 State 设计原则
设计redux state的时候 常见的两种错误
以API 为设计State 的依据
以页面UI 为设计的State 的依据
eg:
//获取博客列表: /posts [ { "id": 1, "title": "Blog Title", "create_time": "2017-01-10T23:07:43.248Z", "author": { "id": 81, "name": "Mr Shelby" } } ... ] // 获取博客详情: /posts/{id} { "id": 1, "title": "Blog Title", "create_time": "2017-01-10T23:07:43.248Z", "author": { "id": 81, "name": "Mr Shelby" }, "content": "Some really short blog content. " } // 获取博客的评论: /posts/{id}/comments [ { "id": 41, "author": "Jack", "create_time": "2017-01-11T23_07:43.248Z", "content": "Good article!" } ... ]
以api 为设计的state
有不少数据重复(title、create_time),有些数据类型为数组,遍历不方便, 由于api 是基于服务器端的逻辑进行设计的,而不是基于前端应用的状态进行设计的
{ “posts”: [ { "id": 1, "title": "Blog Title", "create_time": "207-01-10T23:07:43.248Z", "author": { "id": 81, "name": "Mr Shelby" } }, ... ], "currentPost": { "id": 1, "title": "Blog Title", "create_time": "2017-01-10T23:07:43.248Z", "author': { "id": 81, "name": "Mr Shelby" }, "content": "Some really short blog content. " }, "currentComments": [ { "id": 1, "author": "Jack", "create_time": "2017-01-11T23:07:43.284Z", "content": "Good article!" }, ... ] }
以页面UI 为设计的State 的依据、
会形成存储的浪费,也会存在数据不一致的风险
{ "all": [ { "id": 1, "text": "todo 1", "completed": false }, { "id": 2, "text": "todo 2", "completed" true } ], "uncompleted": [ { "id": 1, "text": "todo 1", "completed": false } ], "completed": [ { "id": 2, "text": "todo 2", "completed": false } ] }
像设计数据库同样设计State
设计数据库基本原则
数据按照领域分类,存储在不一样的表中,不一样的表中存储的列数据不能重复
表中每一列的数据都依赖于这张表的主键
表中除了主键之外的其余列,互相之间不能有直接依赖关系
设计State原则
数据按照领域把整个应用的状态按照领域 分红若干子State, 子State之间不能保存重复的数据
表中State 以键值对的结构存储数据,以记录的 key/ID 做为记录的索引,记录中的其余字段都依赖于索引
State 中不能保存能够经过已有数据计算而来的数据,即State 中的字段不互相依赖
{ "posts": { "1": { "id": 1, "title": "Blog Title", "content": "Some really short blog content.", "created_at": "2016-01-11T23:07:43.248Z", "author": 81, "comments": [ 352 ] },
"3": {
} ... }, "postIds": [1, ..., 3], //存了每一个博客的Id值 "comments": { "352": { "id": 352, "content": "Good article!", "author": 41 }, ... }, "authors": { "41": { "id": 41, "name": "Jack" }, "81": { "id": 81, "name": "Mr Shelby" }, ... } }
补充
State 应该尽可能扁平化(避免嵌套层级过深)
UI State: 具备松散性
5-3 selector 函数
选择器函数,做用是从Redux State 中读取部分数据,给Container Components 来使用
5-4 深刻理解前端状态管理思想
5-5 Middleware
示例: redux-thunk
加强store dispatch的能力
5-6 Store Enhancer
加强redux store的功能
createStore(reducer,[preloadedState], [enhancer])
5-7 经常使用集成库 Immutable.js
简化操做和提升效率
5-9 经常使用集成库 Reselect
6-1 客户端路由和服务端路由
MPA 和 SPA
MPA(Multiple Page App):多页面应用 每当url 发送变化的时候都会返回新的html,打开network 查看请求html信息是变化的
优势:不一样的url,返回不一样的html,利于搜索引擎爬虫爬取,有利于SEO
SPA(Single Page App):单页面应用 和多页面应用相反
优势:url发生变化的时候,公用的页面组件不会从新渲染,因此不会像多页面应用从白屏在到页面显示,用户体验更好, 性能更好
缺点:对SEO不友好
服务端路由
多页面应用更多依赖于服务端的功能
客户端路由
SPA 依赖于客户端路由
6-2 Router 相关库
<BrowserRouter>
HTML5 history API ( pushState, repalceState等 )
须要Web服务器额外配置
<HashRouter>
使用url 的 hash 部分做为路由信息
主要为了兼容老版本浏览器
6-4 路由匹配
6-5 路由渲染组件的方式
<Route component>
<Route render>
<Route children>
7-1 前端架构是什么?
软件架构是什么?
传统架构
抽象(未来源于真实业务场景中的需求功能进行抽象,用设计模式和算法去描述)
解耦(模块化)
组合(将解耦后的各个模块按照模块之间的接口规范结合在一块儿)
由抽象、解耦、组合这三个层次构成通常意义上的软件架构
-----------------------------------------------------------------------------------------------
由另外一层次上看什么是架构
架构(整个软件最高层次的抽象) => 框架 设计模式(在选取不一样的框架和设计模式实现架构) => 代码(实现框架和设计模式)
------------------------------------------------------------------------------------------------
前端架构的特殊性
前端不是一个独立的子系统,又横跨整个系统
分散性:前端工程化
页面的抽象、解耦、组合
和传统架构不一样,前端结构主要解决的两个问题: 前端工程化 和 页面的抽象、解耦、组合
前端工程化(具体要让咱们的前端项目可控。高效)
可控:脚手架、开发规范等
高效:框架、组件库、Mock平台、构建部署工具等
前端抽象问题
页面UI抽象:组件
统用逻辑抽象:领域实体、网络请求、异常处理等
-----------------------------------------------------------------------------
7-2 案例分析
功能路径
理解和梳理业务需求是设计软件架构的第一步,一般咱们会根据需求文档和界面原型图来梳理业务需求
展现:首页 -> 详情页
搜索:搜索页 -> 结果页
购买:登陆 -> 下单 -> 个人订单 -> 注销
--------------------------------------------------------------------------------
7-3 前端架构之工程化的准备1
技术选型和项目脚手架
技术选型考虑的三要素:
业务知足程度
技术栈的成熟度(使用人数、周边生态、产库维护等)
团队的熟悉度
技术选型:
UI层:React
路由:React Router
状态管理:Redux
脚手架
Create React App (npx create-react-app my-app)
7-4 前端架构之工程化准备2
基本规范
目录结构
构建体系
Mock数据
目录结构
public
mock
products
likes.json
src
components(展现型组件)
Header
style.css
index.js
containers(容器型组件)
App
style.css
index.js
Home
components
style.css
index.js
redux
modules
middleware
utils
images
index.html
package.json
删除多余部分: 将App.js 、App.css 移到 container > App
logo.svg serviceWorker.js 删除
---------------------------
7-5 前端架构抽象1:
状态模块定义
商品、店铺、订单、评论(领域实体)
各页面UI 状态(各页面具体的UI 状态,eg:多选框是否选中状态,输入框信息怎样,loading弹出框是否弹出)
前端基础状态:登陆态、全局异常信息 (特殊UI 状态,各个页面共享)
Redux 模块分层 (分两层:领域实体 和 状态)
容器组件 -> 页面状态 通用状态 -> 领域状态
redux
modules
entities
comments.js
index.js(聚合 领域状态)
orders.js
products.js
shops.js
home.js
detail.js
app.js
index.js (聚合全部 领域状态 和 UI 状态)
store.js
orders.js、comments.js、products.js、shops.js
const reducer = (state = {}, action) => { return state } export default reducer
index.js
import { combineReducers } from "redux" import products from "./products" import shops from "./shops" import orders from "./orders" import comments from "./comments" // 合并领域状态 const rootReducer = combineReducers({ products, shops, orders, comments })
index.js (聚合全部 领域状态 和 UI 状态)
import { combineReducers } from "redux" import entities from "./entities" import home from "./home" import detail from "./detail" import app from "./app" //合并成根reducer const rootReducer = combineReducers({ entities, home, detail, app })
store.js
import { createStore, applyMiddleware } from "redux" import thunk from "redux-thunk" import rootReducer from "./modules" if ( process.env.NODE_ENV !== "production" && window.__REDUX_DEVTOOLS_EXTENSION__ ) { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; store = createStore(rootReducer, applyMiddleware(thunk) ) } else { let store = createStore(rootReducer, applyMiddleware(thunk) ) } export default store
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import { Provider } from 'react-redux' import App from './containers/App'; import store from './redux/store' ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'));
7-6 前端架构之抽象2:网络请求层封装(redux-thunk)
utils
request.js
url.js
抽象2: 网络请求层
常规使用方式
request.js
const headers = new Headers({ "Accept": "application/json", "Content-Type": "application/json" }) function get(url) { return fetch(url, { method: "GET", headers: headers }).then(response => { handleResponse(url, response) }).catch(err => { console.error(`Request failed. Url = ${url}. Message=${err}`) return Promise.reject({error: {message: "Request failed."}}) }) } function post(url, data) { return fetch(url, { method: "POST", headers: headers, body: data }).then(response => { handleResponse(url, response) }).catch(err => { console.error(`Request failed. Url = ${url}. Message=${err}`) return Promise.reject({error: {message: "Request failed."}}) }) } function handleResponse(url, esponse) { if(response.status === 200) { return response.json() } else { console.error(`Request failed. Url = ${url}`) return Promise.reject({error: {message: "Request failed due to server error"}}) } } export { get, post }
url.js
export default { getProductList: (rowIndex, pageSize) => `/mock/products/likes.json?rowIndex=${rowIndex}&pageSize=${pageSize}`, }
home.js
import { get } from "../../utils/request" import url from "../../utils/url" export const types = { FETCH_LIKES_REQUEST: "HOME/FETCH_LIKES_REQUEST", //获取猜你喜欢请求 FETCH_LIKES_SUCCESS: "HOME/FETCH_LIKES_REQUEST", //获取猜你喜欢请求成功 FETCH_LIKES_FAILURE: "HOME/FETCH_LIKES_REQUEST", //获取猜你喜欢请求失败 } export const actions = { loadLikes: () => { return (dispath, getState) => { dispath( fetchLikesRequest() ) return get( url.getProductList(0, 10) ).then( data => { dispath(fetchLikesSuccess(data)) }, error => { dispath(fetchLikesFailure(error)) } ) } } } const fetchLikesRequest = () => ({ type: types.FETCH_LIKES_REQUEST }) const fetchLikesSuccess = (data) => ({ type: types.FETCH_LIKES_SUCCESS, data }) const fetchLikesFailure = (error) => ({ type: types.FETCH_LIKES_FAILURE, error }) const reducer = (state = {}, action ) => { switch (action.type) { case types.FETCH_LIKES_REQUEST: //todo case types.FETCH_LIKES_SUCCESS: //todo case types.FETCH_LIKES_FAILURE: //todo default: return state } return state } export default reducer
7-8 前端架构之抽象2:网络请求层封装(redux中间件)
网络请求层
常规使用方式
使用redux 中间件封装
使用redux 中间件封装
redux
middleware
api.js
modules
entities
products.js
products.js
export const schema = { name: 'products', id: 'id' } const reducer = (state = {}, action ) => { return state } export default reducer
api.js
import { type } from "os" // 通过中间处理的action 所具备的标识 export const FETCH_DATA = 'FETCH DATA' export default store => next => action => { const callAPI = action[FETCH_DATA] if(typeof callAPI === 'undefined') { return next(action) } const { endpoint, schema, types } = callAPI if (typeof endpoint !== 'string') { throw new Error('endponit必须为字符串类型的URL') } if(!schema) { throw new Error('必须指定领域实体的schema') } if(!Array.isArray(types) && types.length !== 3) { throw new Error('须要指定一个包含了3个action type 的数组') } if(!type.every(type => typeof === 'string')) { throw new Error('action type必须为字符串类型') } const [requestType, successType, failureType] = types next({type: requestType}) return FETCH_DATA(endpoint, schema).then( response => next({ type: successType, response }), error => next({ type: failureType, error: error.message || '获取数据失败' }) ) }
7-9 前端架构之抽象2:网络请求层封装(redux中间件)
api.js
//import { type } from "os" import { get } from "../../utils/request" import { normalize } from "path" // 通过中间处理的action 所具备的标识 export const FETCH_DATA = 'FETCH DATA' export default store => next => action => { const callAPI = action[FETCH_DATA] if(typeof callAPI === 'undefined') { return next(action) } const { endpoint, schema, types } = callAPI if (typeof endpoint !== 'string') { throw new Error('endponit必须为字符串类型的URL') } if(!schema) { throw new Error('必须指定领域实体的schema') } if(!Array.isArray(types) && types.length !== 3) { throw new Error('须要指定一个包含了3个action type 的数组') } if(!type.every(type => typeof === 'string')) { throw new Error('action type必须为字符串类型') } const actionWith = data => { const finalAction = {...action, ...data} delete finalAction[FETCH_DATA] return finalAction } const [requestType, successType, failureType] = types next(actionWith({type: requestType})) return FETCH_DATA(endpoint, schema).then( response => next(actionWith({ type: successType, response })), error => next(actionWith({ type: failureType, error: error.message || '获取数据失败' })) ) } //执行网络请求 const fetchData = (endpoint, schema) => { return get(endpoint).then(data => { return normalizeData(data, schema) }) } //根据schema, 将获取的数据扁平化处理 const normalizeData = (data, schema) => { const {id, name} = schema let kvObj = {} let ids = [] if (Array.isArray(data)) { data.forEach(item => { kvObj[item[id]] = item ids.push(item[id]) }) } else { kvObj[data[id]] = data ids.push(data[id]) } return { [name]: kvObj, ids } }
home.js
import { get } from "../../utils/request" import url from "../../utils/url" import { FETCH_DATA } from "../middleware/api" import { schema } from "./entities/products" export const types = { FETCH_LIKES_REQUEST: "HOME/FETCH_LIKES_REQUEST", //获取猜你喜欢请求 FETCH_LIKES_SUCCESS: "HOME/FETCH_LIKES_REQUEST", //获取猜你喜欢请求成功 FETCH_LIKES_FAILURE: "HOME/FETCH_LIKES_REQUEST", //获取猜你喜欢请求失败 } export const actions = { loadLikes: () => { return (dispatch, getState) => { const endpoint = url.getProductList(0, 10) return dispatch(fetchLikes(endpoint)) } }, } const fetchLikes = (endpoint) => ({ [FETCH_DATA]: { types: [ type.FETCH_LIKES_REQUEST, type.FETCH_LIKES_SUCCESS, type.FETCH_LIKES_FAILURE ], endpoint, schema }, }) const reducer = (state = {}, action ) => { switch (action.type) { case types.FETCH_LIKES_REQUEST: //todo case types.FETCH_LIKES_SUCCESS: //todo case types.FETCH_LIKES_FAILURE: //todo default: return state } return state } export default reducer
products.js
export const schema = { name: 'products', id: 'id' } const reducer = (state = {}, action ) => { if(action.response && action.response.products) { return {...state, ...action.response.products} } return state } export default reducer
store.js
import { createStore, applyMiddleware } from "redux" import thunk from "redux-thunk" import api from "./middleware/api" import rootReducer from "./modules" let store if ( process.env.NODE_ENV !== "production" && window.__REDUX_DEVTOOLS_EXTENSION__ ) { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; store = createStore(rootReducer, applyMiddleware(thunk, api) ) } else { store = createStore(rootReducer, applyMiddleware(thunk, api) ) } export default store
7-10 前端架构之抽象3:通用错误处理
错误信息组件
错误状态
src
components
ErrorToast
style.css
index.js
index.js
import React, {Component} from 'react' import "./style.css" class ErrorToast extends Component { render() { const { msg } = this.props return ( <div className="errorToast"> <div className="errorToast_text"> {msg} </div> </div> ) } componentDidMount() { this.timer = setTimeout( () => { this.props.clearError() }, 3000 ) } componentWillUnmount() { if (this.timer) { clearTimeout(this.timer) } } } export default ErrorToast
app.js
const initialState = { error: null } export const types = { CLEAR_ERROR: "APP/CLEAR_ERROR" } //action creators export const actions = { clearError: () => ({ type: types.CLEAR_ERROR }) } const reducer = (state = initialState, action ) => { const { type, error } = action if (type === types.CLEAR_ERROR) { return {...state, error: null} } else if (error) { return {...state, error: error} } return state } export default reducer
App
index.js
index.js
import React from 'react'; import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import ErrorToast from "../../components/ErrorToast" import { actions as appActions, getError } from '../../redux/modules/app'; import './style.css'; function App() { render () { const {error, appActions: {clearError} } = this.props return ( <div className="App"> { error ? <ErrorToast msg={error} clearError={ clearError } /> : null } </div> ) } } const mapStateToProps = () => { return { error: getError(state) } } const mapDispatchToprops = (dispatch) => { return { error: getError(state) } } export default connect(mapStateToProps, mapDispatchToprops)(App);
8-1 页面分析和组件划分
首页开发