luzuoquan's world


  • 首页

  • 归档

  • 标签

React-Router-v4的代码分割

发表于 2019-04-21 |

React-Router v4 的一些新特性

新版 React-Router 采用了 Monorepo 管理方式,里面有若干独立的包。

  1. react-router
  2. react-router-dom
  3. react-router-native
  4. react-router-config

v4 版相比之前的版本有一些需要特别注意的地方。

  1. 部分组件不再是从 ‘react-router’ 中引入,比如 Link 需要从 ‘react-router-dom’ 中引入。
  2. 嵌套路由不在支持,需要从父组件里面再添加子路由,这也贯彻了组件即路由的思想。不过官方还是提供了一个相对友好的工具组件 ‘react-router-config’。
  3. 移除了onEnter 、 onUpdate 、 onLeave 事件,很多习惯了在路由状态层面处理的情况都得在组件的生命周期函数中处理。
  4. 路由匹配规则改动也很大,新的匹配规则遵循 path-to-regexp 规则。

针对这 v4 之前的版本,React-Router 官方给出了一份相对比较详细的升级指南。
Migrating from v2/v3 to v4


官方给出的文档能帮助我们很好的进行升级。但是针对服务端渲染,以及单页面应用服务端渲染后的代码分割,文档并没有给出很好的方案。
同构工程的主要的技术点:

  1. Server 端和 Client 端单独渲染出来的代码是相同的;
  2. Dynamic Imports;
  3. Code Splitting;

在这里我们探讨两种不同的 Code Splitting 的方案。

1. react-asyncmodule

react-asyncmodule (大佬同事开发),是一个专门来做代码分割的解决方案。搭配 Webpack 和 Import 可以很好的解决工程的动态加载和代码分割。

假如现有项目用到的 React-Router 版本是 v4 之前的版本,这绝对是最友好的 Code Splitting 方案。
通常在 v4 版本之前,路由都是这样集中设置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
path: 'login',
getComponent(nextState, callback) {
require.ensure([], require => {
callback(null, require('./view/login/index'))
}, 'login')
}
},
{
path: 'song(/:id)',
getComponent(nextState, callback) {
require.ensure([], require => {
callback(null, require('./view/song/song').default)
}, 'song')
}
},
{
path: 'tiktoksong(/:id)',
getComponent(nextState, callback) {
require.ensure([], require => {
callback(null, require('./view/tiktoksong/tiktoksong'))
}, 'tiktoksong')
}
}

虽然 v4 官方推荐是 Dynamic Routing , 但是对于依然使用 Static Routing 方式集中管理路由的旧项目,v4 提供了 react-router-config 包,让我们依然可以使用集中管理路由的方式。虽然在 router config 的配置上面会和 v3 版的有一些区别,但也还好。从上面的代码中可以看到异步加载解决方案是基于 Webpack 的 require.ensure 来做的。而在 webpack 3+ 以后我们用 import() 来做异步加载。
当使用 import 时, 代码分割的配置就比之前会更复杂。在在这个时候需要引入 react-asyncmodule 方案 ,类似的配置如下:

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
import AsyncModule from 'react-asyncmodule';

const AsyncComponent = AsyncModule({
loading: () => (<Loading />),
error: () => (<div>error</div>)
});
const Home = AsyncComponent(import('./view/index'));

const routesConfig = [{
path: '/:m?',
exact: true,
component: Home,
componentName: 'home',
fetchData: ({ dispatch, queryParams }) => {
const { s } = queryParams;
if (s) {
return (
dispatch(changeTab(2)),
dispatch(setInputValue(s)),
dispatch(setSearchState(false))
);
}
return dispatch(homeRemdRequest(queryParams));
}
}]

对应 webpack 配置文件也比以前稍微复杂。webpack(server端) 的 externals 需要做如下更改:

1
2
3
4
5
6
7
8
9
function getExternals() {
return fs.readdirSync(path.resolve(__dirname, './node_modules'))
.filter(filename => !/\.bin|react-asyncmodule/.test(filename))
.reduce((externals, filename) => {
// eslint-disable-next-line no-param-reassign
externals[filename] = `commonjs ${filename}`;
return externals;
}, {});
}

并且需要将 asyncmodule-import 放到 babel-loader 的 plugins 配置中。这样配置就差不多了,其余的都是正常的同构代码。

2. react-loadable

社区中最火的代码分割的解决方案。如同 react-loadable 文档中引用的一样:

A higher order component for loading components with dynamic imports

react-loadable 的具体用法可以参照其官方文档。但是需要注意的是其是基于动态路由的,也就是在路由设置上不能像前面那种集中配置,当然这也是 v4 版本推荐的路由设置方式。如果是一个新项目,还是推荐的。
在使用 react-loadable 同构的工程还是有一些需要注意的点:

  1. 在 server 端 app 的入口处需要加入 Loadable.preloadAll
  2. react-loadable 自带 Babel 插件 react-loadable/babel 和 Webpack 插件 react-loadable/webpack 需要分别在 babel-loader 的 ‘plugins’ 和 webpack 的 ‘plugins’ 中设置。
  3. 因为 react-loadable/webpack 会生成一份所有包信息的 json 文件。而这份文件在 server 端生成 html 的时候会用到,所以在开发者模式的时候得先执行 webpack 编译,然后再启动 server 。 如果不想这么麻烦,可以使用另外一个 npm 包 npm-run-all 。

最后 react-asyncmodule 和 react-loadable 都是基于 Component 进行代码分割的。两者的区别是:

  1. react-asyncmodule 对 loading 、 error 参数更好的复用;
  2. react-asyncmodule 的总体设置更简单,比如在 node 端不需要 preloadAll 。

这两种代码分割的解决方案都可以很好的帮我们完成同构,最后祝大家’同构’愉快!

你值得关注一下'Fetch'

发表于 2017-06-02 |

前言

到目前为止,我们实现异步获取数据都是通过 AJAX ,或者其他封装成 ‘AJAX’ 类的工具函数(此处不讨论 JSONP 。。。)。而 AJAX 的核心就是 XMLHttpRequest , XMLHttpRequest 的出现,使得 Web 页面开发进入一个新的时代。但是现在针对 XMLHttpRequest 的槽点越来越多。本身的 API 设计不是基于 Promise 的,会导致异步回调深渊(当然, jQuery 已经根据 Promise/A 实现对应的 Promise 方案)。而现在 Fetch 代表着新一代获取资源的技术解决方案(包括跨网络),尽管它目前还是处于实验中。

Fetch 的介绍

尽管 Fetch 的兼容性在 chrome 42和 Firefox 39 以及 Opera 29上面得到了实现,但是对于 IE,哪怕是 IE11 都没有实现。这似乎使其不能在 pc 端上应用。但是社区已经对 新的 Fetch 标准做了很好的兼容。之前在自己的实际项目中用到了 Fetch ,来自于开源项目 whatwg-fetch。

这里有一篇 Fetch 的文章,介绍原生 Fetch。另外社区也有一片类似的文章 This API is so Fetching!

一个通用的 Fetch 的示例:

1
2
3
4
5
fetch(url, options).then(function(response) {
// handle HTTP response
}, function(error) {
// handle network error
})

Fetch 接受两个参数:’url’ , ‘options’。’url’ 是必填项,’options’ 是可选项。看下常规的 GET 、 POST 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// for get
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Token': token
},
credentials: 'same-origin'
}

// for post
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Token': token
},
credentials: 'same-origin',
body: JSON.stringify(formParams)
}

事实上, options的可选项包括

  1. maxRedirects
  2. disableRedirects
  3. credentials
  4. headers
  5. maxResponseLength
  6. method
  7. …(请看API)

Fetch 请求返回的 response 参数是 Response 对象的变量, Response 对象有以下属性

  1. Response.status response的状态码(缺省值:200)
  2. Response.statusText HTTP 状态码消息对应
  3. Response.ok 根据 response 的状态是否在200-299(包括200,299)这个范围内返回一个 Boolean 的值
  4. Response.headers Headers的信息

另外一些常规的方法

  1. text()
  2. json()
  3. blob()
  4. arrayBuffer()
  5. formData()

Fetch 返回的另外一个 function 则是错误信息的回调方法。

最后附上一个完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fetch(`${baseUrl}/process/user/tasks?limit=200`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Token': token
},
credentials: 'same-origin'
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
return response.json();
}
})
.then(json => {
dispatch({
type: types.FETCH_UNHANDLE_BUSINESS_DATA,
data: json.result
})
})

在使用过 polyfill 版的 Fetch 后,可以对比一下与原生的 Fetch 的API的差别。

学习Vue.js源码

发表于 2017-03-02 |

前言

Vue.js是目前MVVM框架中比较流行的一种,在之前的项目中也用过Vue+Vuex。在使用的过程中,也是一边看文档一边进行开发。对Vue的了解也仅限官方文档和一些社区上回答,很大程度上是知其然而不知其所以然。所以在项目告一段落之后,决定从Vue的源码入手,学习一下其内部架构。

Vue源码目录

目标Vue.js版本是2.0.5 /src目录
源码src目录
对应的目录分别为
/compiler、/core、/entries、/platforms、/server、/sfc、/shared

  1. /compiler目录是编译模版;
  2. /core目录是Vue.js的核心(也是后面的重点);
  3. /entries目录是生产打包的入口;
  4. /platforms目录是针对核心模块的’平台’模块,platforms目录下暂时只有web目录(在最新的开发目录里面已经有weex目录了)。web目录下有对应的/compiler、/runtime、/server、/util目录;
  5. /server目录是处理服务端渲染;
  6. /sfc目录处理单文件.vue;
  7. /shared目录提供全局用到的工具函数。
    在刚学习源码时,会对类似
    1
    2
    3
    4
    5
    export function observe (value: any): Observer | void {
    if (!isObject(value)) {
    return
    }
    }

这样函数申明或者变量申明感动疑惑的,可以先了解一下flow。

独立构建&&运行时构建

Vue.js从2.0以后开始出现两个不同的构建版本,详情可以查看官网文档。一开始说到这个,是因为从Vue的build命令里面可以看到有7个build版本,这对学习其源码非常有帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const builds = {
'web-runtime-dev': {
...
},
'web-runtime-cdn-dev': {
...
},
'web-runtime-cdn-prod': {
...
},
'web-standalone-dev': {
...
},
'web-standalone-prod': {
...
},
'web-compiler': {
...
},
'web-server-renderer': {
...
}
}

这里,关注的重点是runtime的版本。

Vue.js结构

通过上面的分析,可以看到Vue.js的组成是由core+对应的‘平台’补充代码构成(独立构建和运行时构建只是platforms下web平台的两种选择)。
Vue.js结构
core目录下面对应的components、global-api、instance、observer、util、vdom模块。

Vue.js的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件

Vue2.0在保持实现‘响应的数据绑定’的同时又引入了’virtual-dom’,那么它是怎么实现的呢?

响应的数据绑定

Vue.js实现数据绑定的关键是Object.defineProperty(obj, prop, descriptor),这也是为什么2.0不支持IE8原因之一,IE8下无法实现defineProperty的腻子脚本。当然实现类似功能的现代语法还有Object.observe(已经废弃)和Proxy。
Vue源码对此实现的逻辑在core/observer目录下。
关注三个类class Observer,class Dep,class Watcher
class Observer在core/observer/index.js中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
...
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

Observer类实例是用来附加到每个被观察的对象(后面称之为响应式对象)上的。普通对象通常是不会变成‘响应式对象’的。经过defineReactive函数的调用,才会将传入的普通对象变成‘响应式对象’。而defineReactive就是利用了Object.defineProperty这个方法。defineReactive源码如下:

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
export function defineReactive () {
...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
...
childOb = observe(newVal)
dep.notify()
}
})
}

Object.defineProperty(obj, prop, descriptor)中的第三个参数就是对象描述符。对象描述符的传入是有要求的,分为赋值描述符合存取描述符。而每次传入的参数只能是其中之一,很显然在defineReactive中传入的就是存取描述符,在传入的存取描述符对象中有get,set方法。set方法会实例化一个Observer,get方法会关联到一个class Dep的实例。但是仔细看get方法,发现只有在Dep.target值为true的时候才会发生关联。所以,接下来分析一下class Dep的源码。

1
2
3
4
5
6
7
8
9
10
11
12
export default class Dep {
...
}
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}

class Dep就是连接class Observer和class Watch类的介质。因为在set方法里面最终会调用dep.notify()方法。class Dep类中的notify方法会使这个dep实例下所有的watch数组更新一次。class Watch类的update方法会调用对应的回调方法,进行对应的更新。同时在前面提到的get方法关联class Dep实例时,是在Dep.target为true的时候才会执行。通过源码可以看到Dep.target的初始值是null,也就是默认是不会执行关联的。源码上对此做了注释,可以看到Dep.target是被赋予全局性质,用来保证同一时刻只有一个Watcher实例在被‘关联’(源码注释的地方是’evaluated’)。而激活Dep.target这个属性的是函数pushTarget。pushTarget函数就在class Watcher中调用了。class Watcher的源码如下:

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
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object = {}
) {
this.vm = vm
vm._watchers.push(this)
// options
...
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
const value = this.getter.call(this.vm, this.vm)
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
}

到这里,class Observer、class Dep和class Watch三个类的关系就清楚了。这也是Vue中实现数据绑定用到的观察者模式的体现。

Virtual-dom

React的大热,是因为其带来了’Virtual-dom’和数据驱动视图的理念(尽管很多人觉得后者更重要)。这里并不想比较‘Virtual-dom’和原生的DOM操作谁快谁慢的问题(事实上在dom结构改动很多的情况下,原生DOM操作比较快。。。),仅仅是理解一下‘Virtual-dom’。
‘Virtual-dom’是一系列的模块集合,用来提供声明式的DOM渲染。来看一个简单的DOM片段

1
2
3
4
5
<div id="parent">
<span class="child">item1</span>
<span class="child">item2</span>
<span class="child">item3</span>
</div>

对DOM片段结构抽象一下:一个根节点div,三个元素子节点span(对应的内部文本节点)。然后用JavaScript对象表示:

1
2
3
4
5
6
7
8
9
10
11
const dom = {
tagName: 'div',
props: {
id: 'parent'
},
children: [
{tagName: 'span', props: {class: 'child'}, children: ["item1"]},
{tagName: 'span', props: {class: 'child'}, children: ["item2"]},
{tagName: 'span', props: {class: 'child'}, children: ["item3"]},
]
}

进而扩展到整个HTML页面的结构。整个HTML页面结构其实可以用一个JavaScript对象表示,通过这个抽象,对dom对象的修改就会影响到HTML页面的结构。所以在改变HTML结构的时候,我们仅仅是修改JavaScript对象。相对以前修改HTML页面结构式通过直接修改DOM元素,现在变成修改对应的JavaScript对象。Vue.js在对DOM的抽象做的更细致,具体代码可以看core/vdom/create-element.js。

在实现了对HTML结构的映射后,接下来就是‘Virtual-dom’的重点,如何比较两个不同HTML结构树的对象–diff算法。diff算法比较的就是两颗’树’的差异。而传统的’树’比较是一个时间复杂度为O(n^3),这个效率明显是不够的。而HTML的结构树的改变不同于传统的’树’。HTML结构树的改变,很少会出现跨越不同层级的改变。基于这个实际上的差异化改变,‘Virtual-dom’的diff算法的时间复杂度是O(n)。有兴趣研究diff算法的,可以看下这里。

在解决了diff算法核心问题后,就要把新旧虚拟树的差异应用到’老HTML结构树’上–’patch’。Vue.js在’patch’的解决方案上参考了开源项目Snabbdom。源码篇幅有点长,也有些复杂,想深入了解的可以配合开源的项目一起分析。

至此,‘Virtual-dom’大致的实现逻辑也清楚了。Vue.js的源码在架构组织上还有很多可以学习的,比如区分’核心模块’和’平台模块’。另外内部实现的’keep-alive’组件也是值得关注的地方,更多了解请到这里。以上只是很’粗浅’的学习,不对的地方希望大家指出来(认真脸.jpg)。

参考资料

Vue官网文档
What is Virtual Dom
The difference between Virtual DOM and DOM

luzuoquan

luzuoquan

这是一个小窝

3 日志
2 标签
© 2019 luzuoquan
由 Hexo 强力驱动
主题 - NexT.Pisces