My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

路由

发表于 2018-12-04

为什么需要路由

1.单页应用需要进行页面切换
2.通过URL可以定位到页面
3.更有语义的组织资源

基本原理

在组件容器里根据URL决定显示什么样的组件

REACT router特性(和后端路由对比)

1.声明式路由定义

通过react组件标签进行声明,可以放在任何地方,不需要具体的路由表进行声明

2.动态路由

传统路由一旦配置了它就是一个配置文件,成为一个静态文件
而react router的路由是页面在render的时候才会被解析的,有相应路由的标记标签就是有相应的配置,没有标签就没有相应的配置

三种路由实现方式

  1. url路径:通过改变URl更改视图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import React from "react";
    import { BroswerRouter as Router, Route, Link } from "react-router-dom";

    const Home = () => <h1>Home</h1>;
    const Hello = () => <h1>Hello</h1>;
    const About = () => <h1>About Us</h1>;

    export default class RouterSample extends React.PureComponent {
    render() {
    return (
    <Router>
    <div>
    <ul id="menu">
    <li>
    <Link to="/home">Home</Link>
    </li>
    <li>
    <Link to="/hello">Hello</Link>
    </li>
    <li>
    <Link to="/about">About</Link>
    </li>
    </ul>

    <div id="page-container">
    <Route path="/home" component={Home} />
    <Route path="/hello" component={Hello} />
    <Route path="/about" component={About} />
    </div>
    </div>
    </Router>
    );
    }
    }
  2. hash路由:使用Router 的HashRouter组件 进行路由容器包裹,切换路由时,
    具体的路径会被放在URl的#后面,通过改变hash更变视图《兼容低版本浏览器》

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import React from "react";
    import { HashRouter as Router, Route, Link } from "react-router-dom";

    const Home = () => <h1>Home</h1>;
    const Hello = () => <h1>Hello</h1>;
    const About = () => <h1>About Us</h1>;

    export default class RouterSample extends React.PureComponent {
    render() {
    return (
    <Router>
    <div>
    <ul id="menu">
    <li>
    <Link to="/home">Home</Link>
    </li>
    <li>
    <Link to="/hello">Hello</Link>
    </li>
    <li>
    <Link to="/about">About</Link>
    </li>
    </ul>

    <div id="page-container">
    <Route path="/home" component={Home} />
    <Route path="/hello" component={Hello} />
    <Route path="/about" component={About} />
    </div>
    </div>
    </Router>
    );
    }
    }
  3. 存路由:使用react-router 的MemoryRouter组件进行路由容器包裹,路由信息放在内存中管理,URL不变的情况下,即可进行视图切换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    import React from "react";
    import { HashRouter as Router, Route, Link } from "react-router-dom";
    import { MemoryRouter } from "react-router";

    const Home = () => <h1>Home</h1>;
    const Hello = () => <h1>Hello</h1>;
    const About = () => <h1>About Us</h1>;

    export default class RouterSample extends React.PureComponent {
    render() {
    return (
    <MemoryRouter>
    <div>
    <ul id="menu">
    <li>
    <Link to="/home">Home</Link>
    </li>
    <li>
    <Link to="/hello">Hello</Link>
    </li>
    <li>
    <Link to="/about">About</Link>
    </li>
    </ul>

    <div id="page-container">
    <Route path="/home" component={Home} />
    <Route path="/hello" component={Hello} />
    <Route path="/about" component={About} />
    </div>
    </div>
    </MemoryRouter>
    );
    }
    }

基于路由配置进行资源组织 好处

1.实现业务逻辑的松耦合
2.易于扩展,重构和维护
3.路由层面实现Lazy Load

REACT Router API

1.:普通链接,会触发浏览器刷新
类似a标签,但是不会触发浏览器的刷新,点击时router会接管导航,对切换进行处理不会传递到浏览器,让其进行真正的页面切换
to属性代表链接到的URl的地址
2.:类似 Link但是会添加当前选中状态
可以添加activeClassName属性,当当前链接符合to属性值时,显示相应的样式 还有其他属性,详情请看react-router文档

1
<NavLink to="/faq" activeClassName='selected'>FAQs</NavLink>

3.:满足条件时提示用户是否离开当前页面
切换页面时,跟用户提供一个确认的操作

1
2
3
4
5
import { Prompt } from "react-router";
<Prompt
when={formIsHalfFilledOut}
message='Are you sure you want to leave'
/>

4.重定向当前页面,例如登录判断

1
2
3
4
5
6
7
8
import { Redirect,Route } from "react-router";
<Route exact path='/' render={()=>(
loggedIn?(
<Redirect to='/dashboard' />
) : (
<PublicHomePage/>
)
)}/>

5.:路由配置的核心标记,路径匹配时显示对应组件
path:路由
component:相应要显示的组件
exact:是否精准配置path
多个route path都符合当前路由时,那相应组件都会进行显示

1
<Route exact path="/" component={Home} />

6.:只显示第一个匹配的路由
找到一个匹配的路径就只显示这个路径相应的组件,其他组件不显示

1
2
3
4
5
6
7
import { Switch,Route } from "react-router";
<Switch>
<Route path="/home" component={Home} />
<Route path="/hello" component={Hello} />
<Route path="/about" component={About} />
<Route component={Nomatch} />
</Switch>

参数传递

通过URL传递参数 :
获取参数:this.props.match.params
页面状态尽量通过URl参数定义,方便页面间跳转时数据传递,否则需要将数据转化成组件内部state进行渲染,过程复杂

套嵌路由

1.每个React组件都可以时路由容器
2.React Router的声明式语法可以方便的定义嵌套路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import React from "react";
import {
BrowserRouter as Router,
Route,
Link
} from "react-router-dom";

const Category = ({ match }) => (
<h1>Sub Category {match.params.subId}</h1>
);

const SubCategory = ({ match }) => (
<div>
<h1>Category {match.params.id}</h1>

<ul id="menu">
<li>
<Link to={`/category/${match.params.id}/sub/1`}>
Sub Category 1
</Link>
</li>
<li>
<Link to={`/category/${match.params.id}/sub/2`}>
Sub Category 2
</Link>
</li>
<li>
<Link to={`/category/${match.params.id}/sub/3`}>
Sub Category 3
</Link>
</li>
</ul>
<div id="page-container-2">
<Route
path="/category/:id/sub/:subId"
component={Category}
/>
</div>
</div>
);

export default class NestedRoute extends React.PureComponent {
render() {
return (
<Router>
<div>
<ul id="menu">
<li>
<Link to="/category/1">Category 1</Link>
</li>
<li>
<Link to="/category/2">Category 2</Link>
</li>
<li>
<Link to="/category/3">Category 3</Link>
</li>
</ul>

<div id="page-container">
<Route
path="/category/:id"
component={SubCategory}
/>
</div>
</div>
</Router>
);
}
}

My Little World

构建

发表于 2018-12-04

UI库

ant design 适合企业级应用,复杂交付,密集数据展示
material UI:时尚,花哨,具有的美观性,更适合开发面向消费者的应用(google版原始react实现)
sematic UI:把UI当做一种language来描述(jquery版原始react实现)

选择UI库的考虑因素

  1. 组件库是否齐全(齐全的话可以减少很多工作量,不用自己实现)
  2. 样式风格是否符合业务需求(企业级应用一般要求简洁明了,对于密集性数据展示比较合理;对于移动端面向消费者一般要求好看,button,字体比较大)
  3. API 设计是否便捷和灵活(使用起来是否方便,从而会影响开发效率)
  4. 技术支持是否完善(技术文档是否齐全;github上提issue时,能不能得到快速解答)
  5. 开发是否活跃(是否有稳定团队在进行快速迭代和维护,在遇到BUG或者需要新功能时可以得到满足)

使用next.js 构建react 同构应用

同构应用:浏览器第一次向服务器请求页面时,服务器返回解析好的页面,不需要浏览器执行js来进行渲染(这样可以加快页面首次打开的速度),之后页面的操作/切换,像单页面一样,不需要浏览器刷新,均由前端完成,包括UI渲染,页面路由切换等,不需要再向服务端请求页面

next.js的几个规则
1.页面就是pages目录下的一个组件
所有页面放在pages文件夹下,一个页面即一个组件,文件名和文件路径对应路由路径
2.static目录映射静态文件(图片)
3.page具有特殊静态方法 getInitialProps
next.js提供给react组件初始化props的方法

命令行装包:

1
npm install --save react react-dom next

在页面中使用其它 react 组件
1.页面也是标准的node模块,可使用其它 react组件
2.页面会针对性打包,仅包含其引入的组件
,不会加载其他额外的资源

所有组件放在components文件夹下,在pages文件夹下的页面js中使用时,直接用路径import进去

使用Link实现同构路由

1.使用 next/link 定义链接

1
2
3
4
5
import Link from 'next/link'
export default ()=>
<div>
<Link href='/about'><a>here</a></Link>
</div>

2.点击链接时页面不会刷新
如果使用a标签会产生刷新

3.使用 prefatch预加载目标资源
如果不添加该属性,则链接对应的组件,在切换的时候再加载相应页面打包的内容,有该属性,则next.js 打包时就会获取所有链接对应的组件,会预加载所有的js内容,从而提高页面切换速度,但是并不会预加载需要从服务器端API请求的数据

4.使用 replace 属性替换 url
替换当前页在路由中的history,无法后退回当前页

动态加载页面

1
2
3
4
5
6
import dynamic from 'next/dynamic'
const DynamicComponet=dynamic(import('../components/hello'))
export default ()=>
<div>
<DynamicComponet/>
</div>

页面在加载时,除了加载整个页面的main.js,还会加载动态加载的组件的js包,当这个动态组件被render的时候才会被加载

测试

react 让前端单元测试变得容易
1.react 应用很少需要访问浏览器 API
2.虚拟 Dom 可以在 NodeJs 环境运行和测试 (不需要浏览器环境,在内存中render虚拟DOM即可完成测试)
3.Redux隔离了状态管理,纯数据层单元测试

单元测试涉及的工具
1.Jest: Facebook 开源的 JS 单元测试框架 (零配置,可以直接开始测试)
2.JS dom浏览器环境的 NodeJS 模拟 (一个在nodejs中可以模拟浏览器的library)
3.Enzyme:React组件渲染和测试 (在nodejs中渲染虚拟DOM进行测试,通过进行不同程度的render(shallow Rendering,Full Rendering,Static Rendering,进行不同程度的测试,比如利用shallow Rendering浅渲染测试渲染结果是否符合预期的DOM结构)
4.Nock: 模拟 HTTP请求 (模拟得到请求返回数据)
5.Sinon: 函数模拟和调用跟踪 (例如测试组件UI时,跟踪响应函数有没有被调用,调用几次,而不是去测试响应函数是否符合预期,测试响应函数是否符合预期属于响应函数测试范畴。集成在JEST中)
6.istanbul:单元测试覆盖率(通过对已有代码的修改和埋点去判断相关代码有没有被执行到)

开发调试工具

eslint
1.使用 .eslintrc 进行规则的配置
2.使用 airbnb 的 JavaScript 代码风格

检查语法风格,拼写错误,会报错,保证代码的一致性

Prettier
vscode 的一款插件,根据一定规则做代码格式化 .eslintrc
1.代码格式化的神器
2.保证更容易写出风格一致的代码
不会报错,会直接根据规则将代码格式化

React DevTool chrome插件
定位组件,可以以组件树的形式查看DOM结构,
勾选highlight updates可看到需要更新的组件

Redux DevTool chrome插件
可查看action和action引起的变化
time machine 功能,可以回溯action,方便观察loading过程,进而对loading进行优化
可自动生成测试代码

理想架构

易于开发(但可能会不容易扩展和维护)
1.开发工具是否完善 (采用的技术栈是否有相应的开发工具来支持)
2.生态圈是否繁荣 (是否有类似项目遇到过相同问题可以借鉴)
3.社区是否活跃(提问有人回答)

易于扩展
1.增加新功能是否容易
2.新功能是否会显著增加系统复杂度(系统放大之后会难以扩展和维护,考虑引进新功能时,如何架构能保证减少系统复杂度的提高)

易于维护
1.代码是否容易理解 (基于一定的最佳实践规范去编写,保证多人开发有一定的规则)
2.文档是否健全(通过注释,架构图,具体文档描述项目中的一些关键点,方便后续工作人员查看,也方便日后回头开发能快速理解关键点,来维护已有项目)

易于测试
1.功能的分层是否清晰 (UI,数据层之间依赖少)
2.副作用少 (模块高内聚,对外部依赖少)
3.尽量使用纯函数(输入决定输出,确定)

易于构建
1.使用通用技术和架构 (保证现有项目不需要定制化,就可以打包部署)
2.构建工具的选择(webpack ,roolup,使用常用技术栈,减少额外配置)

大型前端应用需要拆分复杂度原因
当项目增加更多功能的时候,项目复杂度并不会指数级的上升,始终保持在一个可控的范围之内

拆分复杂度技术架构

从功能上进行区分,将业务逻辑拆成高内聚松耦合的模块,每个模块负责一个功能,拥有自己的componet,action和reducer,这样即使当一个功能需要删除时,只要删除相应的模块即可,也保证其他模块不受影响;路由不再由一个文件统一定义,每个feature自己定义一个路由,然后由route的loader把每个模块的路由文件加载进根结点的routing文件

技术上的component,路由,reducer是按照功能的方式组织在模块上

如何组织 component,action 和reducer
文件夹结构
• 按 feature 组织源文件
• 组件和样式文件同一级
• React 单独文件夹
• 单元测试保持同样目录结构放在 tests 文件夹

组件和样式组织
一个功能的组件js和样式文件放在一个模块中,再由一个index.js引入各个模块的组件js文件,一个style文件引入各个模块的样式文件,进行统一管理输出,打包时就只是打包index.js和style文件
注意:
如果组件中引入样式文件,那么当这个组件被重复使用时,样式文件会被多次引入,打包时就会多次进行打包,造成不必要的代码冗余
使用上述加载方式,样式文件独立加载,js文件只需要加载它所需要的资源,就不会发生多次加载

action 和 reducer组织
一个模块的,action 和 reducer 一一对应放在同一文件,再由一个actions.js引入所有action,一个reducers.js引入所有reducer,这两个文件只是充当loader的角色
好处:action 和 reducer 非常小,不会被无限的去扩展,更容易理解

CONSTANTS.js中命名的常量一功能名开头,避免与其他模块常量命名冲突

再由rootreducer加载所有模块下的reducer,进行combineReducers挂载,实现全局使用

优点:一个模块内部是高内聚的,各个模块之间是松耦合的

主要思想:使用 root loader 加载 feature的各个资源

路由配置
每个模块(页面级)路由通过export JSON格式数据 ,自己配,再loader到根路由,由根路由统一处理解析json格式为react Router的声明式形式

每个模块都有都有自己的专属路由配置
顶层路由使用JSON配置更易维护和理解

My Little World

开发思想

发表于 2018-12-04

源码
git仓库

传统框架存在问题

1.传统 UI操作关注太多细节
DOM API 繁多,需要手动调用,且要经过多步骤进行更新
react 解决办法:始终整体‘刷新’页面 ,无需关心细节,(不管谁是哪部分新增或更改的,根据现在状态整体刷新页面)

2.应用程序状态分散在各处f难以追踪和维护
传统数据模型存在问题:传统 MVC结构复杂,难以扩展和维护
react 解决办法:Flux 单向数据流

创建组件
1.创建静态 UI
2.考虑组件的状态组成 :是由state内部维护还是用外部传进来的props维护
3.考虑组件的交互方式:内部进行了操作,如何告知/把状态暴露到父组件

受控组件VS非受控组件

受控组件: 表单元素状态由使用者维护
组件发生变化,利用onchange将状态给到到父组件(使用者),父组件更新状态,再通过props传递状态给组件,组件重新渲染

非受控组件:表单元素状态 DOM自身维护

创建原则:单一职责原则

1.每个组件只做一件事
2.如果组件变得复杂,那么应该拆分成小组件
拆分成小组件好处:
a.降低复杂度;
b.提高性能,如果一个组件足够大之后,任何一种状态变化,都会引起整个组件刷新,
拆分成小组件后,未涉及到的小组件,没有状态变化,就不会刷新

数据状态管理:DRY(Don’t Repeat Yourself)原则

1.能计算得到的状态就不要单独存储 :
2.组件尽量无状态,所需数据通过 props 获取

JSX

本质:不是模板语言,而是一种语法糖(相当于使用React.createElement创建组件)
优点:
1.声明式创建界面的直观(可以使用自定义和原生标签直接进行创建,方便构建UI)
2.代码动态创建界面的灵活 (节点的创建,属性更改,消失完全可以向写JS一样进行操作)
3.无需学习新的模板语言(JS+JSX特性)
约定:自定义组件以大写字母开头
1.react 认为小写的 tag是原生 dom 节点,如 div
2.大写字母开头为自定义组件
3.JSX 标记可以直接使用属性语法,例如<meau.Item />,使用属性语法,可以不以大写字母开头
JSX防注入攻击
React DOM 在渲染之前默认会 过滤 所有传入的值。它可以确保你的应用不会被注入攻击。
所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(跨站脚本) 攻击

生命周期方法

constructor
js标准类的构造函数
1.用于初始化内部状态,很少使用(初始化工作一般会在其他声明周期内使用)
2.唯一可以直接修改state的地方(可以这么写this.state.xxx = ‘abc’)

getDerivedStateFromProps
react 16.3新引入的API
从属性props初始化内部状态state
1.当 state 需要从 props 初始化时使用
2.尽量不要使用:维护两者状态一致性会增加复杂度
如果state需要从props获得,一般使用props可以通过动态计算得到,如果用state进行单独存储,就有必要时刻维护二者的一致性
维护一致性则增加了复杂度,容易出BUG
3.每次 render都会调用
该方代替原来的componentwillreceiveProps方法
4.典型场景:表单控件获取默认值
初始值来自props,用户输入后为用户输入的值

componentDidMount
1.UI渲染完成后调用
2.只执行一次
3.典型场景:获取外部资源(只需要获取一次资源)

componentWillUnmount
1.组件移除时被调用
2.典型场景:资源释放

getSnapshotBeforeUpdate
1.在页面 render之前调用,state 已更新
2.典型场景:获取 render之前的 DOM 状态

componentDidUpdate
捕获每一次更新
1.每次 UI 更新时被调用
2.典型场景:页面需要根据 props 变化重新获取数据

shouldComponentUpdate
1.组件决定 Virtual DOM是否要重绘
2.一般可以由 PureComponent 自动实现
3.典型场景:细节性能优化

在react 16.3中去掉的方法有
componentWillMount
componentWillReceive
componentWillUpdate

状态更新可能是异步的

1
2
3
4
5
6
7
8
//此代码可能无法更新计数器
this.setState({
counter: this.state.counter + this.props.increment,
});
//更改为以下写法即可
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));

1.算法复杂度O(n):通过广度优先分层比较/遍历实现线性算法

2.虚拟 DOM如何计算 diff
虚拟 DOM的两个假设
a.组件的 DOM结构是相对稳定的
b.类型相同的兄弟节点可以被唯一标识
根结点比较
属性变化及顺序:利用标识调整位置
节点类型发生变化:删除原有结点,构建新类型结点
节点跨层移动:删除原有结点,在应有(新)位置构建相同类型新结点

3.key属性的作用:提高性能

组件复用设计模式

1.高阶组件

传入原始组件,结合生命周期创建一些额外的功能,然后附加到原始组件上,返回增加功能后的原始组件
额外的功能一般都是一些通用的功能,避免在原始组建上增加额外功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from "react";

export default function withTimer(WrappedComponent) {
return class extends React.Component {
state = { time: new Date() };
componentDidMount() {
this.timerID = setInterval(() => this.tick(), 1000);
}

componentWillUnmount() {
clearInterval(this.timerID);
}

tick() {
this.setState({
time: new Date()
});
}
render() {
return <WrappedComponent time={this.state.time} {...this.props} />;
}
};
}

2.函数子组件

将子组件以函数返回值形式作为children属性传入原始组件,在原始组件中,在需要显示原始组件的地方执行函数,
或者在合适的生命周期中执行函数拿到多个子组件,再决定在什么位置显示不同的子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//写法一
<Mouse children={mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>
//写法二
<Mouse>
{mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}
</Mouse>

//在原始组件应用子组件
class Mouse extends React.Component{
render(){
<div>
{this.props.children({x:50,y:100})}
</div>
}
}

context API

应用场景:全局性的一些字段,如theme的切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React from "react";

const enStrings = {
submit: "Submit",
cancel: "Cancel"
};

const cnStrings = {
submit: "提交",
cancel: "取消"
};
//传入原始数据/默认值,创建context
const LocaleContext = React.createContext(enStrings);

class LocaleProvider extends React.Component {
state = { locale: cnStrings };
toggleLocale = () => {
const locale =
this.state.locale === enStrings
? cnStrings
: enStrings;
this.setState({ locale });
};
render() {
return (
// context的 Provider属性是一个组件,包裹需要用到context数据子组件的组件,其value值为相应在子组件中需要用到的数据
<LocaleContext.Provider value={this.state.locale}>
<button onClick={this.toggleLocale}>
切换语言
</button>
{this.props.children}
</LocaleContext.Provider>
);
}
}

class LocaledButtons extends React.Component {
render() {
return (
//context的 Consumer属性也是一个组件,包裹需要使用context数据的子组件,
<LocaleContext.Consumer>
{locale => (//local 即Context.Provider 的value值
<div>
<button>{locale.cancel}</button>
&nbsp;<button>{locale.submit}</button>
</div>
)}
</LocaleContext.Consumer>
);
}
}

export default () => (
<div>
<LocaleProvider>
<div>
<br />
<LocaledButtons />//使用context数据的组件必须为Provider组件的子组件,
</div>
</LocaleProvider>
<LocaledButtons />//如果子组件在Context.Provider形成组件的外面使用,则只会应用默认数据,不会随Value属性发生变化
</div>
);

为什么需要脚手架

react:进行UI开发
redux:进行状态的管理
react/router:进行路由的管理
babel:把最新的js特性翻译成兼容浏览器的旧的JS语法
webpack:进行打包
eslint:语法检查
因而需要引入以上几个技术栈的几十个个package,不但要了解,还要知道如何让进行相应的配置
在开启一个项目时,配置过程做完一次基本/只要实现一次,就基本可以复用,用来再开启新项目
所以为方便开发,用脚手架实现配置过程

脚手架有:
Create React App 基础(webpack,)
Rekit 开源项目
Codesandbox 在线开发工具
dva 蚂蚁金服的一套脚手架工具

为什么需要打包

1.编译ES6语法特性,编译JSX(保证浏览器可以兼容执行)
2.整合资源,例如图片,Less/Sass,svg
3.优化代码体积(对变量名进行缩短,去除注释,去除空格,去除不必要的变量等等)

webpack
将项目中所有资源进行整合,将每种资源(js,png,sass…)使用插件的方式载入对应的Loader,
loader能够决定自己支持什么样的语法,loader的输出是可以执行的js语言,可以兼容多平台多版本浏览器
可以部署的语言

打包注意事项
1.设置nodejs环境为production
开发环境可能需要检查输入的参数的类型,语法,拼写等问题,但生产环境不需要

2.禁用开发时专用代码,比如logger
打包时可以根据环境判断决定是否执行一定的代码,所以开发用到的代码在打包时不应被打到生产环境,要做区分
3.设置应用根路径
告诉应用到哪里获取地址

My Little World

命令模式

发表于 2018-11-08

应用情景

有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。
此时希望用一种松耦合的方式设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

主要思想

将命令接收者的具体执行函数存入 命令堆栈,命令发起者需要执行/撤销动作时,对堆栈进行处理
命令接收者可以以闭包形式被执行,也可以作为对象属性被执行到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//闭包形式
var setCommand = function(btn,fn){
btn.onclick = function(){
fn()
}
}
var MenuBar = {
refresh:function(){
console.log('refresh menu')
}
}
var RefreshMenuBarCommand = function(reciver){
return function(){reciver.refresh()}
}
var refreshMenuBarCommand = RefreshMenuBarCommand(Menubar)
setCommand(btn1,refreshMenuBarCommand)

//宏命令,组装一个可以一次性触发多个命令的命令;属性形式,统一使用execute触发
var MacroCommand = function(){
return {
commandsList:[],
add:function(command){
this.commandsList.push(command);
},
execute:function(){
for(var i=0,cammand;command = this.commandsList[i++];){
command.execute();
}
},
undo:function(){
for(var i=0,cammand;command = this.commandsList[i++];){
command.undo();
}
},

}
}
var closeDoorCom ={
execute:function(){
console.log('close door')
},
undo:function(){console.log('open door')}
}
var openTvCom ={
execute:function(){
console.log('open TV')
},
undo:function(){console.log('close Tv')}
}
var macroCommand = MacroCommand()
macroCommand.add(closeDoorCom)
macroCommand.add(openTvCom)
macroCommand.execute()
macroCommand.undo()

与策略区别

策略模式指向问题域更小,所有策略对象目标一致,他们只是达到这个目的的不同手段,内部实现针对‘算法’
命令对象解决的目标更具发散性,还可以完成撤销,排队等功能

My Little World

订阅发布模式

发表于 2018-11-08

主要思想

通过逻辑处理函数/对象event,使事件订阅和发布过程忽略事件的具体处理过程
订阅时只需要给到event要订阅的事件A,和响应函数,
发布时给到event事件A被触发和相关参数,即可执行订阅时的响应函数
event 充当中介者的角色,管理事件响应的存储和执行

1.简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let event = {
caches:{},
on:function(name,fn){
if(!this.caches[name]){
this.caches[name] = []
}
this.caches[name].push(fn)
},
emit:function(name,...args){
if(!this.caches[name]){return}
for(let i=0;i<this.caches[name].length;i++){
let fn=this.caches[name][i]
fn(args)
}
},
remove:function(name,fn){
if(this.caches[name].length<1){return}
for(let i=0;i<this.caches[name].length;i++){
let fn1=this.caches[name][i]
if(fn === fn1){
this.caches[name].splice(i,1)
}
}
}
}
let fn1 = (val)=>{console.log(val,1)}
let fn2 = (val)=>{console.log(val,2)}
event.on('aaa',fn1)
event.on('aaa',fn2)
event.emit('aaa','fire!')

2.全局事件冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
var Event = (function(){ 
var global = this,
Event,_default = 'default';
Event = function(){
var _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {},
_create,
find,
each = function( ary, fn ){
var ret;
for ( var i = 0, l = ary.length; i < l; i++ ){
var n = ary[i];
ret = fn.call( n, i, n);
}
return ret;
};

_listen = function( key, fn, cache ){
if ( !cache[ key ] ){
cache[ key ] = [];
}
cache[key].push( fn );
};

_remove = function( key, cache ,fn){
if ( cache[ key ] ){
if( fn ){
for( var i = cache[ key ].length; i >= 0; i-- ){
if( cache[ key ][i] === fn ){
cache[ key ].splice( i, 1 );
}
}
}else{
cache[ key ] = [];
}
}
};

_trigger = function(){
var cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
ret,
stack = cache[ key ];

if ( !stack || !stack.length ){
return;
}

return each( stack, function(){
return this.apply( _self, args );
});
};

_create = function( namespace ){
var namespace = namespace || _default;
var cache = {},
offlineStack = [], // 离线事件
ret = {
listen: function( key, fn, last ){
_listen( key, fn, cache );
if ( offlineStack === null ){
return;
}
if ( last === 'last' ){
offlineStack.length && offlineStack.pop()();
}else{
each( offlineStack, function(){
this();
});
}

offlineStack = null;
},

one: function( key, fn, last ){
_remove( key, cache );
this.listen( key, fn ,last );
},

remove: function( key, fn ){
_remove( key, cache ,fn);
},
trigger: function(){
var fn,
args,
_self = this;

_unshift.call( arguments, cache );
args = arguments;
fn = function(){
return _trigger.apply( _self, args );
};

if ( offlineStack ){
return offlineStack.push( fn );
}
return fn();
}
};
return namespace ?( namespaceCache[ namespace ] ? namespaceCache[ namespace ] : namespaceCache[ namespace ] = ret ): ret;
};

return {
create: _create,
one: function( key,fn, last ){
var event = this.create( );
event.one( key,fn,last );
},
remove: function( key,fn ){
var event = this.create( );
event.remove( key,fn );
},
listen: function( key, fn, last ){
var event = this.create( );
event.listen( key, fn, last );
},
trigger: function(){
var event = this.create( );
event.trigger.apply( this, arguments );
}
};
}();

return Event;

})();

My Little World

AOP(切面)编程/装饰者模式

发表于 2018-11-07

定义

在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责

主要思想

在每次要执行具体函数B时,先执行A,在每次执行完B之后都执行C,
以此类推,有必要的话,在C执行完再执行D,从而形成执行链

具体实现

先用一个temp函数包住AB,再用一个final函数包住temp和C,这样,在执行final函数时,会按顺序A-B-C执行

1.使用普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let before=(fn,beforeFn)=>{
return (...args)=>{
beforeFn(args)
return fn(args)
}
}
let after = (fn,afterFn)=>{
return (...args)=>{
let ret = fn(args)
afterFn(args)
return ret
}
}

let a = ()=>{console.log(2)}
a = before(a,()=>{console.log(1)})
a = after(a,()=>{console.log(3)})
a() //1,2,3

2.改造Function对象,可以实现链式添加职责

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.before = function(beforeFn){
return ()=>{
beforeFn(arguments);
return this(arguments);
}
}
Function.prototype.after = function(afterFn){
return ()=>{
let ret = this(arguments);
afterFn(arguments);
return ret;
}
}
var func = function (){
console.log(2);
return 15}
func = func.before(()=>{console.log(1)}).after(()=>{console.log(3)})
func() //1,2,3 返回15

应用

1.数据统计上报
将点击响应事件包含的上报动作隔离出来,以after的顺序,装饰到点击事件响应函数上

2.动态改变函数参数
组装ajax请求参数时,在请求函数前装饰before函数,进行不同参数参加处理

3.插件式表单验证
在submit函数前装饰before函数进行不同条件验证

区分代理模式

代理模式的意图是当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者,
本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情
通常只有一层代理-本体的引用

AOP的作用用于对对象加入行为,一开始不能确定对象的全部功能时,
经常会形成一条装饰链

My Little World

迭代器模式

发表于 2018-11-06

定义

提供一种方法顺序访问一个聚合对象种的各个元素,而又不需要暴露该对象的内部表示
把迭代过程从业务逻辑中分离出来,不用关心对象的内部构造,可以按顺序访问其中的每个元素

用途/场景

查找可用组件,进行数据处理

主要思想

内部迭代器
函数内部定义好迭代规则,完全接手整个迭代过程,外部只需要一次初始调用

外部迭代器
需要调用进行下一轮迭代的函数,可以手工控制迭代的过程或者顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//使用内部迭代器实现
var iteratorInner=(arr,fn)=>{
for(let i=0;i<arr.length;i++){
fn(arr,i)
}
return arr
}
console.log(iteratorInner([1,2,3],(arr,i)=>{arr[i]++})) //[2,3,4]

//使用外部迭代器
let iteratorOuter = (arr,fn)=>{
let count = 0;
let nextFn =()=>{
if(count <arr.length){
fn(arr,count)
}
return {
result:arr,
count:count === arr.length ? count:++count,,
done:count === arr.length
}
}
return {
next:nextFn,
isdone:count === arr.length
}
}
var it = iteratorOuter([1,2,3],(arr,i)=>{
arr[i]++
})
console.log(it.next())
//count: 1
//done: false
//result: (3) [2, 3, 4]
console.log(it.next())
//count: 2
//done: false
//result: (3) [2, 3, 4]
console.log(it.next())
//count: 3
//done: true
//result: (3) [2, 3, 4]
My Little World

适配器模式

发表于 2018-11-06

定义
解决俩个软件实体间接口不兼容的问题,使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体,可以一起工作

用途/场景
1.调用第三方(地图/支付)接口时,不同平台提供的相同功能的接口函数名不同,或者调用时需要进行的操作过程不同,
将这些过程封装/分类,通过适配器决定如何使用

2.处理数据,将不符合使用要求的数据结构转换成预期的样子

主要思想
将处理过程封装在适配器函数中,直接在适配器函数中进行处理,或者让适配器返回处理后的可适用结果
调用适配器传入需要处理的数据/条件,适配器进行处理或返回处理结果

注意事项
那些繁琐的处理代码始终是存在的,只不过在写业务的时候看不到它罢了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
1.转换数据结构
//将如下数据结构
var guangdongCity = [{
name: 'shenzhen',
id: 11,
}, {
name: 'guangzhou',
id: 12,
}];
//转换成如下数据结构
var guangdongCity = { shenzhen: 11, guangzhou: 12};

var adapter = (data)=>{
let temp = {};
for(let i=0;i<data.length;i++){
temp[data[i].name] = data[i].id
}
return temp
}
console.log(adapter(guangdongCity))

2.SDK适配例子1
var googleMap = {
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var baiduMap = {
display: function(){
console.log( '开始渲染百度地图' );
}
};
var baiduMapAdapter = {
show: function(){
return baiduMap.display();
}
};
var renderMap = function( map ){
if ( map.show instanceof Function ){
map.show();
}
};
renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMapAdapter ); // 输出:开始渲染百度地图
3.SDK适配例子2
function pay ({
price,
goodsId
}) {
return new Promise((resolve, reject) => {
const config = {}

switch (platform) {
case 'wechat':
// 微信的处理逻辑
config.price = price
config.goodsId = goodsId
config.appId = 'XXX'
config.secretKey = 'XXX'
wx.pay(config).then((err, data) => {
if (err) return reject(err)

resolve(data)
})
break
case 'QQ':
// QQ的处理逻辑
config.price = price * 100
config.gid = goodsId
config.appId = 'XXX'
config.secretKey = 'XXX'
config.success = resolve
config.error = reject
qq.pay(config)
break
case 'alipay':
// 支付宝的处理逻辑
config.payment = price
config.id = goodsId
config.token = 'XXX'
alipay.pay(config, resolve, reject)
break
}
})
}
// run anywhere
await pay({
price: 10,
goodsId: 1
})

对于SDK提供方,仅仅需要知道自己所需要的一些参数,然后按照自己的方式进行数据返回。

对于SDK调用方,仅仅需要我们约定好的通用的参数,以及按照约定的方式进行监听回调处理。

整合多个第三方SDK的任务交由适配器来做,然后将适配器的代码压缩,混淆,放在一个看不见的角落里去,这样的代码逻辑就会变得很清晰了
参考资料:适配器在JavaScript中的体现

My Little World

代理模式

发表于 2018-11-06

分类

保护代理
代理B可以帮助A过滤掉一些请求,可以直接在代理B中拒绝掉
用于控制不同权限的对象对目标对象的访问

虚拟代理
把一些开销很大的对象,延迟到真正需要它的时候才去创建

单一职责原则

就一个类(通常也包括对象和函数)而言,应该仅有一个引起它变化的原因。

主要思想

A想调用C,要经过B,B在中间做C的代理,用于判断什么时候可以执行C
比如多次调用缓存起来再一起执行C;传入相同参数只计算一次,将结果保存,以后传入相同参数直接返回结果,不再计算

用途/实现

1.虚拟代理合并 HTTP请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求, 后一次性发送给服务器。
var synchronousFile = function( id ){
console.log( '开始同步文件,id 为: ' + id );
};

var proxySynchronousFile = (function(){
var cache = [], // 保存一段时间内需要同步的 ID
timer; // 定时器
return function( id ){
cache.push( id ); //每次进来收集
if ( timer ){
// 保证不会覆盖已经启动的定时器
return;
}
timer = setTimeout(function(){
synchronousFile( cache.join( ',' ) ); // 2 秒后向本体发送需要同步的 ID 集合 到时间统一处理 clearTimeout( timer ); // 清空定时器
timer = null;
cache.length = 0; // 清空 ID 集合
}, 2000 );
}
})();
var checkbox = document.getElementsByTagName( 'input' );
for ( var i = 0, c; c = checkbox[ i++ ]; ){
c.onclick = function(){
if ( this.checked === true ){
proxySynchronousFile( this.id );
}
}
};

2.缓存代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var mult = function(){     
console.log( '开始计算乘积' );
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
// 创建缓存代理的工厂
var createProxyFactory = function( fn ){
var cache = {};
return function(){
var args = Array.prototype.join.call( arguments, ',' );
if ( args in cache ){
return cache[ args ];
}
return cache[ args ] = fn.apply( this, arguments );
}
};
var proxyMult = createProxyFactory( mult ),
proxyMult( 1, 2, 3, 4 ); // 输出:24
proxyMult( 1, 2, 3, 4 ); // 输出:24

扩展:用于ajax异步请求数据,下次请求相同数据时,是否可以取消请求,直接用之前缓存的数据,适用固定数据

My Little World

策略模式

发表于 2018-11-06

主要思想

定义一系列的算法,把它们一个个封装起来,通过逻辑函数使他们可以相互替换
避免if-else多分支判断

具体实现

定义一些系列算法(具体处理过程),把他们各自封装成策略类,算法被封装在策略类内部方法里。在客户对Context(如何调用算法的处理函数)发起请求时,Context总是把请求委托给这些策略对象中间的某一个进行计算

注意点

编写代理函数时,注意如何调用,让算法得到执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let strategies = { //算法集合
"S": function( salary ){
return salary * 4;
},
"A": function( salary ){
return salary * 3;
},
"B": function( salary ){
return salary * 2;
}
};
var calculateBonus = function( level, salary ){ //Context,委托函数,负责调用哪个算法
return strategies[ level ]( salary );
};

console.log( calculateBonus( 'S', 20000 ) );//80000
console.log( calculateBonus( 'A', 10000 ) );//30000

变形:
var S = function( salary ){ return salary * 4; };
var A = function( salary ){ return salary * 3; };
var B = function( salary ){ return salary * 2; };
var calculateBonus = function( func, salary ){ return func( salary ); };
calculateBonus( S, 10000 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//具体策略方法
var strategies = {
isNonEmpty: function( value, errorMsg ){
if ( value === '' ){
return errorMsg;
}
},
minLength: function( value, length, errorMsg ){
if ( value.length < length ){
return errorMsg;
}
},
isMobile: function( value, errorMsg ){
if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
return errorMsg;
}
}
};
//执行代理 Context
var Validator = function(){
this.cache = [];
};

Validator.prototype.add = function( dom, rules ){
var self = this;
for ( var i = 0, rule; rule = rules[ i++ ]; ){
(function( rule ){
var strategyAry = rule.strategy.split( ':' );
var errorMsg = rule.errorMsg;
self.cache.push(function(){
var strategy = strategyAry.shift();
strategyAry.unshift( dom.value );
strategyAry.push( errorMsg ); //strategyAry,传递到具体算法函数参数,这里进行处理
return strategies[ strategy ].apply( dom, strategyAry );
});
})( rule )
}
};
Validator.prototype.start = function(){
for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
var errorMsg = validatorFunc();
if ( errorMsg ){
return errorMsg;
}
}
};
//具体应用
var registerForm = document.getElementById( 'registerForm' );
var validataFunc = function(){
var validator = new Validator();
validator.add( registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 10 位'
}]);
validator.add( registerForm.password, [{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于 6 位'
}]);
validator.add( registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '手机号码格式不正确'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function(){
var errorMsg = validataFunc();
if ( errorMsg ){
alert ( errorMsg );
return false;
}
}
1…111213…25
YooHannah

YooHannah

246 日志
1 分类
21 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces