npm run start
隐藏在 package.json 里的秘密
随便哪个 dva 的项目,只要敲入 npm start 就可以运行启动。之前敲了无数次我都没有在意,直到我准备研究源码的时候才意识到:在敲下这行命令的时候,到底发生了什么呢?
答案要去 package.json 里去寻找。
有位技术大牛曾经告诉过我:看源码之前,先去看 package.son 。看看项目的入口文件,翻翻它用了哪些依赖,对项目便有了大致的概念。
package.json 里是这么写的:
1 | "scripts": { |
翻翻依赖,"roadhog": "^0.5.2"
。
既然能在 devDependencies 找到,那么肯定也能在 npm 上找到。原来是个和 webpack 相似的库,而且作者看着有点眼熟…
如果说 dva 是亲女儿,那 roadhog 就是亲哥哥了,起的是 webpack 自动打包和热更替的作用。
在 roadhog 的默认配置里有这么一条信息:
1 | { |
后转了一圈,启动的入口回到了 src/index.js
。
src/index.js
在 src/index.js
里,dva 一共做了这么几件事:
从 ‘dva’ 依赖中引入 dva :
import dva from 'dva'
;通过函数生成一个 app 对象:
const app = dva()
;加载插件:
app.use({})
;注入 model:
app.model(require('./models/example'))
;添加路由:
app.router(require('./routes/indexAnother'))
;启动:app.start(‘#root’);
在这 6 步当中,dva 完成了 使用 React 解决 view 层
、redux 管理 model
、saga 解决异步
的主要功能。事实上在我查阅资料以及回忆用过的脚手架时,发现目前端框架之所以被称为“框架”也就是解决了这些事情。前端工程师至今所做的事情都是在*分离动态的 data 和静态的 view *,只不过侧重点和实现方式也不同。
至今为止出了这么多框架,但是前端 MVX 的思想一直都没有改变。
dva
寻找 “dva”
既然 dva 是来自于 dva
,那么 dva 是什么这个问题自然要去 dva 的源码中寻找了。
剧透:dva 是个函数,返回一了个 app 的对象。
剧透2:目前 dva 的源码核心部分包含两部分,
dva
和dva-core
。前者用高阶组件 React-redux 实现了 view 层,后者是用 redux-saga 解决了 model 层。
老规矩,还是先翻 package.json 。
引用依赖很好的说明了 dva 的功能:统一 view 层。
1 | // dva 使用的依赖如下: |
不过 script 没有给太多有用的信息,因为 ruban build
中的 ruban
显然是个私人库(虽然在 tnpm 上可以查到但是也是私人库)。但根据惯例,应该是 dva 包下的 index.js
文件提供了对外调用:
1 | Object.defineProperty(exports, "__esModule", { |
显然这个 exports.default
就是我们要找的 dva,但是源码中没有 ./lib
文件夹。当然直接看也应该看不懂,因为一般都是使用 babel 的命令 babel src -d libs
进行编译后生成的,所以直接去看 src/index.js
文件。
src/index.js
src/index.js
在此 :
在这里,dva 做了三件比较重要的事情:
- 使用 call 给 dva-core 实例化的 app(这个时候还只有数据层) 的 start 方法增加了一些新功能(或者说,通过代理模式给 model 层增加了 view 层)。
- 使用 react-redux 完成了 react 到 redux 的连接。
- 添加了 redux 的中间件 react-redux-router,强化了 history 对象的功能。
使用 call 方法实现代理模式
dva 中实现代理模式的方式如下:
1. 新建 function ,函数内实例化一个 app 对象。
2. 新建变量指向该对象希望代理的方法, oldStart = app.start
。
3. 新建同名方法 start,在其中使用 call,指定 oldStart 的调用者为 app。
4. 令 app.start = start,完成对 app 对象的 start 方法的代理。
上代码:
1 | export default function(opts = {}) { |
// 使用 querySelector 获得 dom
if (isString(container)) {
container = document.querySelector(container);
invariant(
container,
[app.start] container ${container} not found
,
);
}
// 其他代码
// 实例化 store
oldAppStart.call(app);
const store = app._store;
// export _getProvider for HMR
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// If has container, render; else, return react component
// 如果有真实的 dom 对象就把 react 拍进去
if (container) {
render(container, store, app, app._router);
// 热加载在这里
app._plugin.apply(‘onHmr’)(render.bind(null, container, store, app));
} else {
// 否则就生成一个 react ,供外界调用
return getProvider(store, this, this._router);
}
// 使用高阶组件包裹组件
function getProvider(store, app, router) {
return extraProps => (
{ router({ app, history: app._history, …extraProps }) }
);
}
// 真正的 react 在这里
function render(container, store, app, router) {
const ReactDOM = require(‘react-dom’); // eslint-disable-line
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
1 |
|
JavaScript 并不存在 class 这个东西,即便是 es6 引入了以后经过 babale 编译也会转换成函数。因此直接使用无状态组件,省去了将 class 实例化再调用 render 函数的过程,有效的加快了渲染速度。
即便是 class 组件,React.createElement 最终调用的也是 render 函数。不过这个目前只是我的推论,没有代码证据的证明。
1 |
|
import connectAdvanced from ‘../components/connectAdvanced’
export function createConnect({
connectHOC = connectAdvanced,
…. 其他初始值
} = {}) {
return function connect( { // 0 号 connnect
mapStateToProps,
mapDispatchToProps,
… 其他初始值
} = {}
) {
….其他逻辑
return connectHOC(selectorFactory, {// 1号 connect
…. 默认参数
selectorFactory 也是个默认参数
})
}
}
export default createConnect() // 这是 connect 的本体,导出时即生成 connect 0
1 | ``` |
结论:对于 connect()(MyComponent)
- connect 调用时生成 0 号 connect
- connect() 0 号 connect 调用,返回 1 号 connect 的调用
connectHOC()
,生成 2 号 connect(也是个函数) 。 - connect()(MyComponent) 等价于 connect2(MyComponent),返回值是一个新的组件
redux 与 router
redux 是状态管理的库,router 是(唯一)控制页面跳转的库。两者都很美好,但是不美好的是两者无法协同工作。换句话说,当路由变化以后,store 无法感知到。
于是便有了 react-router-redux
。
react-router-redux
是 redux 的一个中间件(中间件:JavaScript 代理模式的另一种实践 针对 dispatch 实现了方法的代理,在 dispatch action 的时候增加或者修改) ,主要作用是:
加强了React Router库中history这个实例,以允许将history中接受到的变化反应到stae中去。
从代码上讲,主要是监听了 history 的变化:
history.listen(location => analyticsService.track(location.pathname))
dva 在此基础上又进行了一层代理,把代理后的对象当作初始值传递给了 dva-core,方便其在 model 的
subscriptions 中监听 router 变化。
看看 index.js
里 router 的实现:
1.在 createOpts 中初始化了添加 react-router-redux 中间件的方法和其 reducer ,方便 dva-core 在创建 store 的时候直接调用。
- 使用 patchHistory 函数代理 history.linsten,增加了一个回调函数的做参数(也就是订阅)。
subscriptions 的东西可以放在 dva-core 里再说,
1 | import createHashHistory from 'history/createHashHistory'; |
剧透:redux 中创建 store 的方法为:
1 | // combineReducers 接收的参数是对象 |
视图与数据
src/index.js
主要实现了 dva 的 view 层,同时传递了一些初始化数据到 dva-core 所实现的 model 层。当然,还提供了一些 dva 中常用的方法函数:
dynamic
动态加载(2.0 以后官方提供 1.x 自己手动实现吧)fetch
请求方法(其实 dva 只是做了一把搬运工)saga
(数据层处理异步的方法)。
这么看 dva 真的是很薄的一层封装。
而 dva-core 主要解决了 model 的问题,包括 state 管理、数据的异步加载、订阅-发布模式的实现,可以作为数据层在别处使用(看 2.0 更新也确实是作者的意图)。使用的状体啊管理库还是 redux,异步加载的解决方案是 saga。当然,一切也都写在 index.js 和 package.json 里。
如果我没被懒癌打倒的话,会继续写完的^_^y