personal blog repository.
sorrycc / blog Goto Github PK
View Code? Open in Web Editor NEW💡
💡
Hi all,
如果。
那么,请试试 dva。
轻量级,基于 react 和 redux,elm 风格的前端框架。
你可以:
貌似没编译过来,这种需要如何解决哈
距离 dva@1 发布已经快整整一年,经过一段时间断断续续的开发,dva@2 终于能和大家见面了。
2.0 最主要的变化是提取了 dva-core,是仅封装了 redux 和 redux-saga 的纯数据流方案。这使得 dva 可以应用在除 react 之外的其他领域,比如 RN、小程序、游戏、vue 等领域;同时也可满足同一领域的多种实现,比如为 react 应用不同的路由方案的 dva-react-router-3 和 dva-no-router。(#530)
本次发布包含 dva-core 和 3 个 react 实现:
此外,还有一些社区基于 dva-core 的实现:
为了方便在视图层 dispatch action 并处理回调,比如 #175,我们在 dispatch 里针对 effect 类型的 action 做了返回 Promise 的特殊处理。
例如:
dispatch({ type: 'count/addAsync' })
.then(() => {
console.log('done');
});
react-router@4 的路由是组件式的,手动处理组件的按需加载并结合 model 和 app 有点麻烦,所有封装了 dva/dynamic
util 方法。
const Users = dynamic({
app,
models: () => [
import('./models/users'),
],
component: () => import('./routes/Users'),
});
// render
<Route exact path="/users" component={Users} />
注:之前手动加 namespace 的会收到一个 warning。
{
namespace: 'count',
effects: {
*a(action, { take }) {
// Before
yield take('count/b');
// After
yield take('b');
}
}
}
/@@start
和 /@@end
的 action,可利用此约定实现 put 的同步执行例如:
yield put({ type: 'addDelay', payload: { amount: 2 } });
yield take('addDelay/@@end');
const count = yield select(state => state.count);
yield put({ type: 'addDelay', payload: { amount: count, delay: 0 } });
参考用例 dva/effects-test.js at d49e3567eaadf06d12c701e670b2ba3bbe043553 · dvajs/dva · GitHub 。
路由基于 react-router@4 实现,写法上会有不同。
// model.js
export default {
namespace: 'count',
reducers: {
a() {},
},
effects: {
*a() {},
}
}
// 只会执行 effects.a 不会执行 reducers.a
dispatch({ type: 'count/a' });
dva/mobile
之前的 dva/mobile
其实是无路由版的 dva,所以可以用 dva-no-router 代替。
这是 history 库的变动,有和 query 相关的请用 query-string 处理一遍。
[email protected] 用户如何升级?
原则上推荐升级到基于 react-router@4 的 dva@2,react-router@4 路由的去中心化带来的好处绝对值得一学,比如布局嵌套、Inclusive 路由、路由抽象等等。可以参考 user-dashboard 升级到 dva@2 的 commit 。
但肯定会有出于成本考虑,既想用 dva@2,又想继续用老版本的路由方案的,我们为大家准备了 dva-react-router-3。大家可以参考 dva-example-react-router-3 进行升级。而为了减少代码修改量,比如把所有的 import xx from 'dva'
改成 import xx from 'dva-react-router-3'
,大家可以通过 babel-plugin-module-resolver 进行构建时替换:
["module-resolver", {
"alias": {
"dva": "dva-react-router-3"
}
}]
(完)
mobx可以在不同页面传值吗?具体应该怎么操作呢?
更新:我们基于此最佳实践做了一个封装方案:dva,可以简化使用 redux 和 redux-saga 时很多繁杂的操作。
前端变化虽快,但其实一直都围绕这几个概念在转:
在 redux 的生态圈内,每个环节有多种方案,比如 Data 可以是 immutable
或者 plain object
,在你选了 immutable
之后,用 immutable.js 还是 seamless-immutable,以及是否用 redux-immutable 来辅助数据修改,都需要选择。
本文总结目前 react + redux 的最佳实践,解释原因,并提供可选方案。
心急的朋友可以直接看代码:https://github.com/sorrycc/github-stars
routing
react-router + react-router-redux: 前者是业界标准,后者可以同步 route 信息到 state,这样你可以在 view 根据 route 信息调整展现,以及通过 action 来修改 route 。
无
为 redux 提供数据源,修改容易。
plain object
: 配合 combineReducer 已经可以满足需求。
同时在组织 Store 的时候,层次不要太深,尽量保持在 2 - 3 层。如果层次深,可以考虑用 updeep 来辅助修改数据。
immutable.js: 通过自定义的 api 来操作数据,需要额外的学习成本。不熟悉 immutable.js 的可以先尝试用 seamless-immutable,JavaScript 原生接口,无学习门槛。
另外,不推荐用 redux-immutable 以及 redux-immutablejs,一是没啥必要,具体看他们的实现就知道了,都比较简单;更重要的是他们都改写了 combineReducer
,会带来潜在的一些兼容问题。
数据的过滤和筛选。
reselect: store 的 select 方案,用于提取数据的筛选逻辑,让 Component 保持简单。选 reselct 看重的是 可组合特性
和 缓存机制
。
无
合理的 CSS 方案,考虑团队协作。
css-modules: 配合 webpack 的 css-loader 进行打包,会为所有的 class name 和 animation name 加 local scope,避免潜在冲突。
直接看代码:
Header.jsx
import style from './Header.less';
export default () => <div className={style.normal} />;
Header.less
.normal { color: red; }
编译后,文件中的 style.normal
和 .normal
在会被重命名为类似 Header__normal___VI1de
。
bem, rscss ,这两个都是基于约定的方案。但基于约定会带来额外的学习成本和不遍,比如 rscss 要求所有的 Component 都是两个词的连接,比如 Header
就必须换成类似 HeaderBox
这样。
radium,inline css 方案,没研究。
统一处理业务逻辑,尤其是异步的处理。
redux-saga: 用于管理 action,处理异步逻辑。可测试、可 mock、声明式的指令。
redux-loop: 适用于相对简单点的场景,可以组合异步和同步的 action 。但他有个问题是改写了 combineReducer
,会导致一些意想不到的兼容问题,比如我在特定场景下用不了 redux-devtool 。
redux-thunk, redux-promise 等: 相对原始的异步方案,适用于更简单的场景。在 action 需要组合、取消等操作时,会不好处理。
在 saga 之前,你可能会在 action creator 里处理业务逻辑,虽然能跑通,但是难以测试。比如:
// action creator with thunking
function createRequest () {
return (dispatch, getState) => {
dispatch({ type: 'REQUEST_STUFF' });
someApiCall(function(response) {
// some processing
dispatch({ type: 'RECEIVE_STUFF' });
});
};
}
然后组件里可能这样:
function onHandlePress () {
this.props.dispatch({ type: 'SHOW_WAITING_MODAL' });
this.props.dispatch(createRequest());
}
这样通过 redux state 和 reducer 把所有的事情串联到起来。
但问题是:
Code is everywhere.
通过 saga,你只需要触发一个 action 。
function onHandlePress () {
// createRequest 触发 action `BEGIN_REQUEST`
this.props.dispatch(createRequest());
}
然后所有后续的操作都通过 saga 来管理。
function *hello() {
// 等待 action `BEGIN_REQUEST`
yield take('BEGIN_REQUEST');
// dispatch action `SHOW_WAITING_MODAL`
yield put({ type: 'SHOW_WAITING_MODAL' });
// 发布异步请求
const response = yield call(myApiFunctionThatWrapsFetch);
// dispatch action `PRELOAD_IMAGES`, 附上 response 信息
yield put({ type: 'PRELOAD_IMAGES', response.images });
// dispatch action `HIDE_WAITING_MODAL`
yield put({ type: 'HIDE_WAITING_MODAL' });
}
可以看出,调整之后的代码有几个优点:
异步请求。
isomorphic-fetch: 便于在同构应用中使用,另外同时要写 node 和 web 的同学可以用一个库,学一套 api 。
然后通过 async
+ await
组织代码。
示例代码:
import fetch from 'isomorphic-fetch';
export async function fetchUser(uid) {
return await fetch(`/users/${uid}`).then(res => res.json());
};
(完)
在 webpack2.2 即将发布 之际,我们来看下 webpack2 有哪些新特性。至于是否升级,大家心里应该有自己的打算吧。
感觉 webpack2 最大的改进是 ES6 modules 和 Tree Shaking,其他都是配置方面的。
用的时候注意要把 babel-preset-es2015 的 modules 关掉:
{
"presets": [
["es2015", { "modules": false }]
]
}
这一特性通过 babel 插件的方式已可实现,比如 babel-plugin-import, babel-plugin-lodash ,所以现在开来已不觉得惊艳了。另外,他需要 npm 包的额外支持,打出一份 ES6 Module 的文件。
输出多 chunk 时需提供 promise-polyfill 。
提供异步的 webpack 配置方式。
劣势挺明显,社区、文档、性能,每样都是痛点。
插件和 loader 社区需要一段时间的适配。比如 ericclemmons/npm-install-webpack-plugin 目前还不支持 webpack2 。
官方文档 看起来还不错,不过大部分人遇到 webpack 配置问题,通常会去 stackoverflow 等问答社区搜,而这些社区在 webpack2 还很少有积累,估计很难找到答案。
把 roadhog 尝试 升级到 webpack2,并以 dva-example-user-dashboard 为例,比较了下 webpack1 和 webpack2 的性能,如下:
webpack1 | webpack2 | |
---|---|---|
roadhog server | 9s | 14s |
roadhog build | 18s | 19s |
roadhog build --debug (不压缩) | 9s | 14s |
慢了不是一点点。。
暂不升级,等 webpack2 社区完善以及性能提升吧。
(完)
走到第六步了 都根据作者的做了发动 但是没有报错 数 据也没有显示出来 ,首页还是那个小丑,为什么啊
OS: win64
nodeversion: v7.7.2
npm version: 5.0.3
dva-cli version: 0.7.8
错误为:
dva g route users
D:\node\node_modules\dva-cli\bin\dva-generate
create routeComponent src/routes/Users.js, src/routes/Users.cs
s
TypeError: root.findRouters(...).getRouterInfo is not a function
at transform (D:\node\node_modules\dva-cli\node_modules\dva-ast\l
ib\transform.js:42:32)
at exports.default (D:\node\node_modules\dva-cli\node_modules\dva
-ast\lib\api\index.js:46:36)
at D:\node\node_modules\dva-cli\lib\generate.js:92:27
at generate (D:\node\node_modules\dva-cli\lib\generate.js:108:11)
at Object. (D:\node\node_modules\dva-cli\bin\dva-gener
ate:11:27)
at Module._compile (module.js:571:32)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:488:32)
at tryModuleLoad (module.js:447:12)
at Function.Module._load (module.js:439:3)
先来看一个典型的 mobx + react 例子。(在 jsfiddle 里打开)
import { observable } from 'mobx';
import { observer } from 'react-mobx';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
const appState = observable({
count: 0,
});
appState.increment = function() {
this.count ++;
};
appState.decrement = function() {
this.count --;
};
@observer
class Count extends Component {
render() {
return (<div>
Counter: { appState.count } <br />
<button onClick={this.handleInc}> + </button>
<button onClick={this.handleDec}> - </button>
</div>);
}
handleInc() {
appState.increment();
}
handleDec() {
appState.decrement();
}
}
ReactDOM.render(<Count />, document.getElementById('root'));
这个例子里,先通过 mobx 定义了 appState,Count 的 render 执行时里引用 appState 的数据。然后如果用户点击 + 或 - 按钮,会触发 appState 的修改,appState 的修改会自动触发 Counter 的更新。
而要理解 mobx 的原理,我们需要一个更底层的例子。
import { observable, autorun } from 'mobx';
const counter = observable(0);
autorun(() => {
console.log('autorun', counter.get());
});
counter.set(1);
运行结果是:
autorun 0
autorun 1
大家可能会好奇,为什么 counter.set()
之后,autorun
会自动执行? 要达到这个目的,通过 counter
需要知道 autorun
是依赖他的。那么这个依赖关系是在什么时候以及如何生成的呢?
先看代码,这里涉及了 mobx 的 observable 和 autorun 接口。与此相关的有 Observable 和 Derivation 两个类。Observable 是数据源,Derivation 是推导。
类定义如下:
Observable
- observing: [Derivation]
- get()
- set()
Derivation
- observer: [Observable]
然后,autorun
执行的步骤是这样的:
到这里,Observable 和 Derivation 的依赖关联就建立起来了。
那么 counter.set()
执行之后是如何触发 autorun
自动执行? 在有了上面这一层依赖关系之后,这个就很好理解了。counter.set()
执行时会从自己的 observing 属性里取依赖他的 Derivation,并触发他们的重新执行。
再看一个例子。
import { observable, autorun } from 'mobx';
const counter = observable(0);
const foo = observable(0);
const bar = observable(0);
autorun(() => {
if (counter.get() === 0) {
console.log('foo', foo.get());
} else {
console.log('bar', bar.get());
}
});
bar.set(10); // 不触发 autorun
counter.set(1); // 触发 autorun
foo.set(100); // 不触发 autorun
bar.set(100); // 触发 autorun
执行结果:
foo 0
bar 10
bar 100
autorun 先是依赖 counter 和 foo,然后 counter 设为 1 之后,就不依赖 foo,而是依赖 counter 和 bar 了。所以之后修改 foo 并不会触发 autorun 。
那么 mobx 是如何在运行时计算依赖的呢?
实际上前面的 autorun
的执行步骤是做了简化的,真实的是这样:
(+)
(+)
相比之前的,增加了 diff 的逻辑,以达到每次执行的时候动态更新依赖关系表的目的。
大家在看前面的例子里可能会有个疑问,为啥第一个例子里可以通过 appState.counter
来设置,而后面的例子里需要用 counter.get
和 counter.set
来取值和设值?
这和数据类型有关,mobx 支持的类型有 primitives, arrays, classes 和 objects 。primitives (原始类型) 只能通过 set 和 get 方法取值和设值。而 Object 则可以利用 Object.defineProperty
方法自定义 getter 和 setter 。
Object.defineProperty(adm.target, propName, {
get: function() { return observable.get(); },
set: ...
});
详见源码。
ComputedValue 同时实现了 Observable 和 Derivation 的接口,即可以监听 Observable,也可以被 Derivation 监听。
Reaction 本质上是 Derivation,但他不能再被其他 Derivation 监听。
autorun 是 Reaction 的简单封装。
其他的 TFRP 类库,比如 Tracker 和 Knockout ,数据更新后的执行都是异步的,需要等到下一个 event loop 。(可以想象成 setTimeout)
而 Mobx 的执行是同步的,这样做有两个好处:
由于 mobx 的更新是同步的,所以每 set 一个值,就会触发 reaction 的更新。所以为了批量更新,就引入了 transation 。
transaction(() => {
user.firstName = "foo";
user.lastName = "bar";
});
在一些情况下,等所有的修改执行完再执行所有的 deviration 会更合适。注意 transaction 只是推迟了 deviration 的执行,本身还是同步的。
action 是 transation 是简单封装,支持通过 decorator 的方式调用。并且是 untrack
的,这样可以在 Derivation 里调用他。
第一次 render 时:
有数据修改时:
shouldComponentUpdate:
componentWillReact:
第一眼看 mobx 觉得非常简单,概念也少。这对于简单项目可能够了,但在项目复杂之后就需要用到一些高级的功能,从而需要接触很多的概念,比如 Observable, ComputedValue, Derivation, Action, Transation, Autorun, Reaction, Modifier 等等。其实一点都不比 redux 简单。。
个人很喜欢 mobx 这个库,里面包含很多非常巧妙的实现和优化。所以试着想把原理给讲明白,但写完之后发现还是有些晦涩。
已更新 2019 版,详见 https://github.com/sorrycc/awesome-tools 。
经常被人问到我在用啥,所以记录下,以下是目前在用的软件和硬件。
由于这些经常会变,所以这篇文章也会做持续更新。
我的编辑器是 Intellij Idea,内置功能很强大了,主要是省心,不必费精力去找插件等。然后也会辅助用 VSCode 去做一些临时文件的快速编辑。
Run command...
为 /usr/local/bin/idea \1
(图),这样 Command + 点击文件路径,就会在 Intellij Idea 里打开用 homebrew 安装。
ps一下。
@sorrycc 执行到第四步报错,前面都能正确访问...api/users .
E:\iSearch6\user-dashboard>dva g route users
E:\nodejs\node_global\node_modules\dva-cli\bin\dva-generate
create routeComponent src/routes/Users.js, src/routes/Users.css
TypeError: root.findRouters(...).getRouterInfo is not a function
at transform (E:\nodejs\node_global\node_modules\dva-cli\node_modules\dva-as
t\lib\transform.js:42:32)
at exports.default (E:\nodejs\node_global\node_modules\dva-cli\node_modules
dva-ast\lib\api\index.js:46:36)
at E:\nodejs\node_global\node_modules\dva-cli\lib\generate.js:92:27
at generate (E:\nodejs\node_global\node_modules\dva-cli\lib\generate.js:108:
11)
at Object. (E:\nodejs\node_global\node_modules\dva-cli\bin\dva-ge
nerate:11:27)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
以下内容已整合到脚手架:https://github.com/sorrycc/dva-boilerplate-electron
近期,我们在内部做了一个类似 IDE 性质的应用,基于 electron。过程中趟过不少坑,也有了些心得,记录如下。
包含:
数据通讯方案决定整体的架构方案。
翻翻 Electron 文档,应该不难发现,Electron 有两个进程,分别为 main 和 renderer,而两者之间是通过 ipc 进行通讯。main 端有 ipcMain,renderer 端有 ipcRenderer,分别用于通讯。
一个简单的读取文件的例子:
main 端
ipcMain.on('readFile', (event, { filePath }) => {
content content = fs.readFileSync(filePath, 'utf-8');
event.sender.send('readFileSuccess', { content });
});
renderer 端
ipcRenderer.on('readFileSuccess', (event, { content }) => {
console.log(`content: ${content}`);
});
ipcRender.send('readFile', {
filePath: '/path/to/file',
});
我们刚开始也是这么做,但过了几星期发现太绕,于是重构成通过 remote 方式。 remote 是一种简化的通讯方案,内部也是 ipc,所以运行起来和前面的方案并无差别,但使用上简化很多。比如,上面的例子可简化如下:
main 端
无
renderer 端
const content = remote.require('fs').readFileSync('/path/to/file');
架构方案有多种,选择适合自己的。
在架构方案的选择上纠结过很久,不过这很大程度是和前面的通讯方案有关的。
传统 ipc 方案,main 端用 ipcMain, renderer 端用 ipcRenderer。
main 端和 renderer 端分别部署一个 dva(不了解 dva 的可以理解为 redux),封装 ipc 基于 action 通讯。main 端的 action 如果包含 toRenderer
会自动走到 renderer 端的,反之 renderer 端的 action 如果包含 toMain
则自动走到 main 端。
上述两个方案的缺点是:
我们的最终方案是:
global.services
,这样在 renderer 里才能通过 remote.getGlobal('services')
调用到定完整体架构之后,就要确定目录结构了,以及如何做构建和打包等等。我们在这也是绕了好大一圈,因为 electron 官网没有推荐这个,后面慢慢翻文档才发现这种组织方式的好处。
先说结论,我们采用的是 Two-Package 的目录结构,并且基于 webpack 打包 main 和 renderer 。
Two-Package Structure 是 pack 工具 electron-builder 给的约定,也是目前业界用的较多的方案。
+ dist // pack 完后的输出,.dmg, .exe, .zip, .app 等文件
+ build // background.png, icon.icns, icon.ico
+ app // 用于 pack 给用户的目录
+ dist // src 目录打包完放这里
+ assets // 字体、图片等资源文件
+ pages // 存放页面
- package.json // 生产依赖,存 dependencies
+ src // 源码
+ main // main
+ renderer // renderer
+ shared // main 和 renderer 公用文件
- package.json // 开发依赖,存 devDependencies
最大的好处是可以很好地分离开发依赖和生成环境依赖。开发依赖存 package.json
,生产依赖存 app/package.json
,这样在 pack 后交付给用户时就不会包含 webpack, mocha 等等的开发依赖了。
那么怎么区别依赖类型呢? 比如:
这没有标准答案,和源码打包策略有关,即 src
目录的源码是如何到 app/dist
下的。
首先打包我们是用的 webpack + babel,分别把 src/main
和 src/renderer
下的文件打包为 app/dist/main.js
和 app/dist/renderer.js
。打包 renderer 可以理解,打包 main 可能有人会有疑问。我们打包 main 是为了编码风格的一致。
我们需要 externals 掉一些不能或不应该被打包到一起的依赖。
electron
这样,renderer 端所有的依赖都是开发依赖,main 端的所有依赖都是生产依赖。
所以,在这种打包机制下,前面的问题就有了答案:
main
externals(context, request, callback) {
callback(null, request.charAt(0) === '.' ? false : `require("${request}")`);
},
renderer
externals(context, request, callback) {
let isExternal = false;
const load = [
'electron',
];
if (load.includes(request)) {
isExternal = `require("${request}")`;
}
callback(null, isExternal);
},
翻下 electron 开源应用的源码,我们会发现有些是用 electron-packager,有些是用 electron-builder 。这两个是什么关系?我们应该用哪个呢?
答案是用 electron-builder。 electron-builder 是基于 electron-packager 实现的,并在此基础上做了 Two-Package.json Structure 的约定,以及自动更新等等功能。
由于我们用了 pty.js,包含 C++ 的原生实现。所以在 papck 前需先用 electron-rebuild 做 rebuild。
{
"build": "NODE_ENV=production webpack",
"rebuild": "electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/ -m ./app/node_modules",
"pack": "npm run build && npm run rebuild && build"
}
electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/
。(完)
如果使用atool-build + dora,如何将fetch请求转发到远程服务器,方便在前后端分离的情况下,进行接口对接。
说一说基于 dva 实现 dva-hackernews 的过程。
基本思路是按照 service -> model -> component 的顺序来实现的,好处是可以用真实数据,不用额外写 mock 方法。
通过 dva-cli 生成项目初始文件,然后 npm start
启动。
hackernews 数据接口来自 firebase,所以可以直接用 firebase 这个 package 。firebase 基于 websocket 连接实现,除了初次请求慢些,后面的数据加载很快。相比 http 来说,省去不少请求。
为了方便在 effects 里调用,service 方法需要返回 promise 。watchList
除外,这个不在 effects 里调,而是在 subscriptions 里,用于实时更新列表数据。
写 model 层是脑力劳动,而写 component 层是体力劳动。
先设计数据结构,为了让 reducer 里写得比较容易,所以选择扁平化的方式。即把 item 拎出来,以 id 为 key 统一存放,然后其他地方即可引用 id 。
{
list: {
top: [123, 456],
new: [123, 456],
},
itemsById: {
123: { title: 'foo' },
456: { title: 'bar' },
789: { title: 'wow' },
},
}
这样更新 item 就比较简单,反之如果要更新 list.top['123'] 的数据,想想都麻烦。(没用 immutable.js)
然后是完成处理 action 的部分,reducers 和 effects,分别负责 state 更新和异步逻辑。
state 更新的部分写在 reducers 里,没什么特别的,灵活掌握 array 和 object 的各种方法就可以了,注意 array 到 object 的转换可以用 reduce 简化。
saveItems(state, { payload: itemsArr }) {
const items = itemsArr.reduce((memo, item) => {
memo[item.id] = item;
return memo;
}, {});
return { ...state, itemsById: { ...state.itemsById, ...items }};
},
异步逻辑部分,写在 effects 里。通过 generator 组织,所以基本上都是一层缩进下来就完了。
*fetchList({ payload }) {
const { type, page } = payload;
yield put({ type: 'app/showLoading' });
const ids = yield call(fetchIdsByType, type);
const itemsPerPage = yield select(state => state.item.itemsPerPage);
const items = yield call(
fetchItems,
ids.slice(itemsPerPage * (page - 1), itemsPerPage * page)
);
yield put({ type: 'saveList', payload: { ids, type } });
yield put({ type: 'saveItems', payload: items });
yield put({ type: 'app/hideLoading' });
},
为了实时性,切换页面不管 item 是否有缓存,都会重新请求一遍。
评论数据是递归获取的,因为不知道有几层。还好是 websocket,如果换成 http 的实现应该会很慢。虽然是比较快,但在评论页面也能明显感觉到是一层层更新出来的。
定义完所有 action 的处理,接下来要看如何调用他们。基本上就两个地方,subscriptions 和 component 。
subscription 意为订阅,用于数据源的订阅。
而初始数据加载实际上是订阅了 history 的变更,待满足 url 匹配时,触发 action 加载远程数据。这些逻辑不放 route component 还有好处是可以更好地配合 hmr,同时让 route component 保持 stateless component 的写法。
由于 react-router 的限制,这里需使用 path-to-regexp 库来解决 url 匹配的问题。
history.listen(({ pathname }, { params }) => {
if (pathToRegexp(`/item/:itemId`).test(pathname)) {
dispatch({
type: 'item/fetchComments',
payload: params.itemId,
});
}
});
当用户进入 item 页面时,通过 action item/fetchComments
获取评论数据。
同上,实时更新也写在 subscriptions 里,等于是订阅了 list 的数据源。有更新时,保存新的 id,然后重新加载本页数据。
watchList(type, ids => {
dispatch({
type: 'saveList',
payload: {
type, ids
},
});
dispatch({
type: 'fetchList',
payload: {
type,
page,
},
});
});
由于我们的数据是扁平化的,不能直接交由 component 渲染,需要一层 selector 。比如我想要 top 下第 1 页的列表。
export function listSelector(state, ownProps) {
const page = parseInt(ownProps.params.page || 1, 10);
const { itemsPerPage, activeType, lists, itemsById } = state.item;
const ids = lists[activeType].slice(itemsPerPage * (page - 1), itemsPerPage * page);
const items = ids.reduce((memo, id) => {
if (itemsById[id]) memo.push(itemsById[id]);
return memo;
}, []);
const maxPage = Math.ceil(lists[activeType].length / itemsPerPage);
return {
items,
page,
maxPage,
activeType,
};
}
写完 model 层,感到一阵轻松,剩下的基本不费脑了。
动画没有用上 react-motion,而是基于 ReactCSSTransitionGroup 实现,方法和 vue 以及 angular 都类似。动效可以上 nganimate 找一个喜欢的样式过来用。
<ReactCSSTransitionGroup
transitionName="item"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
>
{
items.map(item => <Item key={item.id} item={item} />)
}
</ReactCSSTransitionGroup>
以上是实现 hackernews 一些经验。先写什么并不重要,主要是要有分层的概念,可以先写 model,也可以先写 component 。dva 借鉴 elm 的概念整合了 reducers, effects 和 subscriptions 到 model,让分层更清晰,并让各种觉得的代码有所归属。希望大家能动手实践一把,会发现相比现有 redux 方法的优势。
如果我先按关键字查询,得出列表再点击下一页,这样会将所有数据重新加载一遍,我看了你的代码也没有处理这块。
你好 我这边碰到的问题是:项目里面的文件很多,router.js里面的路由大概有30多个 然后运行roadhog build 就不动了 有啥解决办法吗》?
随着 IE8 逐渐退出舞台,很多高级的 CSS 特性都已被浏览器原生支持,再不学下就要过时了。
:empty
区分空元素兼容性:不支持 IE8
假如我们有以上列表:
<div class="item">a</div>
<div class="item">b</div>
<div class="item"></div>
我们希望可以对空元素和非空元素区别处理,那么有两种方案。
用 :empty
选择空元素:
.item:empty {
display: none;
}
或者用 :not(:empty)
选择非空元素:
.item:not(:empty) {
border: 1px solid #ccc;
/* ... */
}
:*-Of-Type
选择元素兼容性:不支持 IE8
举例说明。
给第一个 p 段落加粗:
p:first-of-type {
font-weight: bold;
}
给最后一个 img 加边框:
img:last-of-type {
border: 10px solid #ccc;
}
给无相连的 blockquote 加样式:
blockquote:only-of-type {
border-left: 5px solid #ccc;
padding-left: 2em;
}
让奇数列的 p 段落显示红色:
p:nth-of-type(even) {
color: red;
}
此外,:nth-of-type
还可以有其他类型的参数:
/* 偶数个 */
:nth-of-type(even)
/* only 第三个 */
:nth-of-type(3)
/* 每第三个 */
:nth-of-type(3n)
/* 每第四加三个,即 3, 7, 11, ... */
:nth-of-type(4n+3)
calc
做流式布局兼容性:不支持 IE8
左中右的流式布局:
nav {
position: fixed;
left: 0;
top: 0;
width: 5rem;
height: 100%;
}
aside {
position: fixed;
right: 0;
top: 0;
width: 20rem;
height: 100%;
}
main {
margin-left: 5rem;
width: calc(100% - 25rem);
}
vw
和 vh
做全屏滚动效果兼容性:不支持 IE8
vw
和 vh
是相对于 viewport 而言的,所以不会随内容和布局的变化而变。
section {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
}
section:nth-of-type(1) {
background-image: url('https://unsplash.it/1024/683?image=1068');
}
section:nth-of-type(2) {
background-image: url('https://unsplash.it/1024/683?image=1073');
}
section:nth-of-type(3) {
background-image: url('https://unsplash.it/1024/683?image=1047');
}
section:nth-of-type(4) {
background-image: url('https://unsplash.it/1024/683?image=1032');
}
body {
margin: 0;
}
p {
color: #fff;
font-size: 100px;
font-family: monospace;
}
unset
做 CSS Reset兼容性:不支持 IE
body {
color: red;
}
button {
color: white;
border: 1px solid #ccc;
}
/* 取消 section 中 button 的 color 设置 */
section button {
color: unset;
}
column
做响应式的列布局兼容性:不支持 IE9
nav {
column-count: 4;
column-width: 150px;
column-gap: 3rem;
column-rule: 1px dashed #ccc;
column-fill: auto;
}
h2 {
column-span: all;
}
(完)
import React from 'react';
import { Table, Icon } from 'antd';
import { routerRedux } from 'dva/router';
const CompanyList = ({total, current, loading, dataSource, }) => {
const columns = [];
const pagination = {
total: total,
current: current,
pageSize: 10,
onChange: pageChangeHandler,
};
function pageChangeHandler(page,size) {
dispatch({
type: 'company/query',
payload: { page: page },
});
}
return (<Table columns={columns} loading={loading} dataSource={dataSource} pagination={pagination} scroll={{ x: 1500, y: 700 }} />);
}
export default CompanyList
我在ui组件里面发起一个action提示我dispath不是一个函数。有人遇到过吗
你好,到第三步时,浏览器输出 Uncaught SyntaxError: Unexpected token <index.js 1,这个咋解决?
大家用 redux 这么久,有没有被那么多概念和约定烦到?
比如:
如果有,那么可以尝试下 mobx 。
mobx 只做一件事,解决 state 到 view 的数据更新问题。
mobx 是一个库 (library),不是一个框架 (framework)。他不限制如何组织代码,在哪里保存 state 、如何处理事件,怎么发异步请求等等。我们可以回归到 Vanilla JavaScript,可以和任意类库组合使用。
mobx 引入了几个概念,Observable state
, Derivations
和 Reactions
。
可以拿 Excel 表格做个比喻,Observable state
是单元格,Derivations
是计算公式,单元格的修改会触发公司的重新计算,并返回值,而最终公式的计算结果需要显示在屏幕上(比如通过图表的方式),这是 Reactions
。
下面通过代码理解下这些概念,以 mobx 和 react 的组合使用为例:(Open Demo on jsfiddle)
import { observable, computed } from 'mobx';
import { observer } from 'mobx-react';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
////////////////////
// Store
class TodoStore {
@observable todos = [];
@computed get completedTodosCount() {
return this.todos.filter(todo => todo.completed === true).length;
}
addTodo(task) {
this.todos.push({ task, completed: false });
}
}
////////////////////
// Components
@observer
class TodoList extends Component {
render() {
const { todoStore } = this.props;
return (
<div>
{ todoStore.todos.map((todo, index) => <Todo todo={todo} key={index} />) }
Progress: { todoStore.completedTodosCount }
</div>
);
}
}
@observer
class Todo extends Component {
render() {
const { todo } = this.props;
return (
<li onDoubleClick={this.onRename}>
<input
type="checkbox"
checked={ todo.completed }
onChange={ this.onToggleCompleted }
/>
{ todo.task }
</li>
);
}
onToggleCompleted = () => {
const todo = this.props.todo;
todo.completed = !todo.completed;
}
onRename = () => {
const todo = this.props.todo;
todo.task = prompt('Task name', todo.task) || "";
}
}
////////////////////
// Init
const todoStore = new TodoStore();
todoStore.addTodo('foo');
todoStore.addTodo('bar');
ReactDOM.render(
<TodoList todoStore={todoStore} />,
document.getElementById('mount')
);
这里通过 @observable
定义 Observable state
,通过 @computed
定义 Derivations
,通过 @observer
封装了 React Component 的 render 方法,这是 Reactions
。
mobx 官网 罗列了不少区分与 flux 框架的优点,这里摘录一些比较打动我的。
简单
没有 connect,没有 cursor,没有 Immutable Data ... 总之感觉会少很多代码。同时概念也更少。
可以用 class 来组织和修改数据
对于组织复杂的领域模型比较适用。
可以用 JavaScript 引用来组织和修改数据
比如,可以直接
todo.completed = true;而不需要
return todos.map((todo) => { if (todo.id === action.payload.id) { return {...todo, {completed: true}} else { return todo; } });同时也不需要引入额外的 immutable.js。
性能相比 redux 有优势
mobx 会建立虚拟推导图 (virtual derivation graph),保证最少的推导依赖。dan_abramov 亲自操刀为 todoMVC 做了极致的优化才和 mobx 打成平手。链接
参考之前 redux + redux-saga 的方案,这里的一些点可能会成为你我不用他的原因。
浏览器兼容性,不支持 IE8
由于用了 reactive arrays, objects with reactive properties (getters) 这些 ES5 特性,而且这些特性不能通过 es5-shim 解决。兼容列表可参考:http://kangax.github.io/compat-table/es5/
缺少最佳实践
这部分不在 mobx 的范围之内,需要自己探索一套最佳实践。比如如何触发 action,如何组织 store,如何组织业务逻辑,如何发异步请求,如何在 React Component 之间传递数据等等。
热替换 (Hot Module Replacement)
用过 HMR,就不愿再回到手动刷页面的时代。mobx 支持 [react-transform] 的热替换方式,但是否支持 webpack 原生热替换情况下对 store 进行替换,还有待探索。
mobx 简单高效,在用吐了 redux 之后,对 mobx 简直爱不释手。除了不支持 IE8 这个硬伤,其他缺点都还是可以接受的。我会在后面的小项目中应用它,并尝试探索一套最佳实践。
当我访问http://localhost:8000/api/users 时候会出现Uncaught SyntaxError: Unexpected token < index.js:1,
然后这个页面不会返回JSON数据。
PS:我单独访问http://jsonplaceholder.typicode.com/users是可以访问的
// 1. Initialize
const app = dva({
history: browserHistory,
});
<Router history={history}>
<Route path="/" component={IndexPage} />
<Route path="/test" component={Test} />
</Router>
库地址:https://github.com/sorrycc/roadhog
简单来说,roadhog 是可配置的 react-create-app。
roadhog 是一个 cli 工具,提供 server
和 build
两个命令,分别用于本地调试和构建。命令行体验和 create-react-app 一致,配置略有不同,比如默认开启 css modules,然后还提供了 JSON 格式的配置方式。
http://ow.blizzard.cn/heroes/roadhog
做 roadhog 有多方原因:
首先,create-react-app 体验实在太好了,细节做地很到位,比如启动成功后会自动打开浏览器窗口这个操作,会检查当前是否已经有打开当前 URL 的 Tab,有的话就刷新那个 Tab 。但可惜他并不支持配置,比如我们要用 less 和 css-modules,就不能使用了。相信会有不少人有同样的想法。
另外,我们目前是基于 atool-build 和 dora 的工具套件。dora 有插件机制,atool-build 的配置和 webpack 一样,基于编程。这两种扩展方式都太灵活,灵活是优点,但导致我们做功能升级时需要考虑太多的事情,并且无法保证兼容。
那么 roadhog 的配置方式和之前的有何不同呢?
我们做 cli 工具有一段时间了,从 spm2, spm3, atool-build + dora 到现在的 roadhog。(目前 roadhog 并非 atool 的升级版,两者场景不同, atool 扩展性更好) 配置方式从 JSON 到编程,最终又回归到 JSON 。
roadhog 为啥用 JSON 格式的配置?
做 atool 的时候我们是用编程的配置方式,优点是灵活,可随意改变工具内置的 webpack 配置。
但缺点也很明显:
1. 配置麻烦
比如要删除内置的 CommonChunkPlugin,不加注释基本没人能看懂了。
// Don't extract common.js and common.css
webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) {
return !(plugin instanceof webpack.optimize.CommonsChunkPlugin);
});
更多详见:https://github.com/dvajs/dva-cli/blob/1a4cb33/boilerplates/app/webpack.config.js
2. 工具升级困难
举一个实际的例子。
atool-build 内部配置有一段为:
{
test: /\.css$/,
loader: 'css!postcss'
}
后面由于一些原因,我们改成了:
{
test: /\.css$/,
loader: `${require.resolve('css')}!${require.resolve('postcss')}`
}
但立马导致一些用户出错,原因是他的配置里有判断 loader 内容是否为 css!postcss
,这就让工具的升级寸步难行。
基于上面的原因,roadhog 的配置以 JSON 格式呈现。
下面是目前支持的全部配置项,他们在 roadhog#配置 中有详细解释:
{
"entry": "src/index.js",
"disableCSSModules": false,
"less": false,
"publicPath": "/",
"extraBabelPlugins": [],
"autoprefixer": null,
"proxy": null,
"env": null,
}
以及未来可能支持的配置项:https://github.com/sorrycc/roadhog/issues?q=is%3Aissue+is%3Aopen+label%3Aconfig 。
安装 roadhog:
$ npm i roadhog -g
新建项目目录:
$ mkdir myapp && cd myapp
创建 package.json
,内容为:
{}
创建 src/index.js
,内容为:
import './index.html';
document.write('Hello, roadhog!');
创建 src/index.html
,内容为:
<script src="index.js"></script>
启动:
$ roadhog server
正常的话,会自动帮你打开浏览器,你会看到 Hello, roadhog!
。
(完)
http://localhost:8000/#/api/users?_k=jdmjn2
报错:browser.js:49 Warning: [react-router] Location "/api/users" did not match any routes
Uncaught SyntaxError: Unexpected token <
先要明白 mobx 和 redux 的定位是不同的。redux 管理的是 (STORE -> VIEW -> ACTION) 的整个闭环,而 mobx 只关心 STORE -> VIEW 的部分。
但作为两个目前最火的 React 应用框架库,人们习惯于把他们比较到一起。下面我们也来看下 mobx 和 redux 相比的优缺点。(据说每个列 3 点会让人更容易记住。。)
基于运行时的数据订阅
mobx 的数据依赖始终保持了最小,而且还是基于运行时。而如果用 redux,可能一不小心就多订阅或者少订阅了数据。所以为了达到高性能,我们需要借助 PureRenderMixin 以及 reselect 对 selector 做缓存。所以,如果。
OverSubscription 的例子:
- 非实时计算
view() { if (count === 0) { return a; } else { return b; } }基于 redux 的方案,我们必须同时监听 count, a 和 b 。在 counte === 0 的时候,b 如果修改了,也会触发 view 。而这个时候的 b 其实是无意义的。
- 粗粒度 subscription
view() { todos[0].title }基于 redux,我们通常会订阅 todos,这样 todos 的新增、删除都会触发 view 。其实这里真正需要监听的是 todos 第一个元素的 title 属性是否有修改。
通过 OOP 的方式组织领域模型 (domain model)
OOP 的方式在某些场景下会比较方便,尤其是容易抽取 domain model 的时候。进而由于 mobx 支持引用的方式引用数据,所以可以非常容易得形成模型图 (model graph ),这样可以更好地理解我们的应用。
修改数据方便自然
mobx 是基于原生的 JavaScript 对象、数组和 Class 实现的。所以修改数据不需要额外语法成本,也不需要始终返回一个新的数据,而是直接操作数据。
缺最佳实践和社区
mobx 比较新,遇到的问题可能社区都没有遇到过。并且,mobx 并没有很好的扩展/插件机制。
随意修改 store
我们都知道 redux 里唯一可以改数据的地方是 reducer,这样可以保证应用的安全稳定;而 mobx 可以随意修改数据,触发更新,给人一种不安全的感觉。
最新的 mobx 2.2 加入了
action
的支持。并且在开启strict mode
之后,就只有action
可以对数据进行修改,限制数据的修改入口。可以解决这个问题。
逻辑层的限制
如果更新逻辑不能很好地封装在 domain class 里,用 redux 会更合适。另外,mobx 缺类 redux-saga 的库,业务逻辑的整合不知道放哪合适。
MobX 是一个 TFRP 编程范式的实现实现。
那什么是 TFRP?
要知道 TFRP,就得先了解 FRP 。先看 FRP 的定义:
The essence of functional reactive programming is to specify the dynamic behavior of a value completely at the time of declaration.
-- Heinrich Apfelmus
所以,FRP 的本质是,在声明一个值的时候,同时指定他的动态行为。这个值可能是事件,也可能是数据。
然后 FRP 有两个重要的分支:
基于 Event Stream 的 FRP 擅长于管理 Stream,可进行 Joining
, splitting
, merging
, mapping
, sampling
等等。在需要处理多个 Event Stream 的时候非常有用,但对于简单场景来说,就过于复杂了。比如 RxJS 和 BaconJS 就属于此类。
Transparent FRP 是在背后去实现 Reactive Programming 。和 Event Stream 的 FRP 一样,TFRP 会在需要的时候更新 View,不同的是 TFRP 不需要你定义如何 (How) 以及何时 (When) 更新。这一类型的框架有 Meter(Tracker),knockoutJS 和 EmberJS 。
那么已经有这么多实现了,为什么还要有 Mobx ?
Mobx 和其他实现有些不同。
更多关于 FRP 的资料:
Tracker 文档:
可能问题并不贴合文章,也显得很是伸手党。但是这个问题我也思考和google了很久,一直没有找到答案。
所以在这里冒昧的问下。
是用django接受url访问,返回模版html,内嵌antd组件吗?
还是说完全由dva接管前段,所有数据都通过api接口从后端django获取?
对于这个问题,我一直没有理清思路,望作者指点一二,或者给我一些链接,参考书籍。
谢谢~
我的主要用途是把他作为子路由,给 PS4 用。
低配路由器即可,我选了 极路由 S1 ,¥89,支持 UDP 转发。
参考 极路由1S(HC5661A)刷Padavan固件教程 | 约翰提托博客 完成刷固件操作,然后可以通过 http://192.168.123.1/ 进行管理操作了。
Mac 下通过 ssh 和 scp 进行连接和传文件,比如:
$ ssh [email protected] -p 1022
推荐上 https://www.kislens.com/aff.php?aff=59 买年付 60 块钱的,多线,试过 youtube 不卡。
(完)
dva主要用reactdom,能插入到react native里面用吗?
import()
,用于按需加载 es 模块。https://twitter.com/addyosmani/status/912556308649304064在 ~/.bashrc
或者 ~/.bash_profile
里配:
PATH=$PATH:./node_modules/.bin
<script nomodules ...>
<script type="modules" ...>
<script type=module>
引入。隐隐感觉这功能会带来调试和打包工具的革新。https://www.chromestatus.com/feature/5365692190687232,https://paulirish.github.io/es-modules-todomvc/.babrlrc.js
配置、移除 babel 自身的 babel-runtime 依赖以减少安装尺寸、Deprecate ES20xx presets,推荐用 babel-preset-env 等等。https://babeljs.io/blog/2017/09/12/planning-for-7.0fs.copyFile
、console.group()
、正式支持 ES Module 等。https://nodejs.org/en/blog/release/v8.5.0/Failed to start the server, since you have enabled dllPlugin, but have not run
roadhog buildDllbefore
roadhog server`.
请问这个是什么错误
这几天收到比较多关于 roadhog 的疑问,为啥用 roadhog,啥时不用 roadhog,怎么从 atool-build + dora 切换到 roadhog 等等。解释如下:
roadhog 是约束型配置,基于 JSON 格式,给出有限的配置方式;atool-build + dora 是扩展型,表现为插件和编程 webpack.config.js 的方式。
标了
暂
的后续可能会支持,看需求吧。
roadhog 的劣势:
roadhog 的优势:
既然 roadhog 功能没 atool-build + dora 强大,那为啥要切换呢?
"disableCSSModules": true
以下情况不推荐换 roadhog 。
删除 atool-build
和 dora
相关依赖,加上 roadhog
依赖。
$ npm install roadhog --save-dev
修改 scripts 部分,让 start
和 build
走 roadhog:
"start": "roadhog server"
"build": "roadhog build"
可参看这个 Commit 或 dva-example-user-dashboard 。
.roadhogrc
如果是用 dva + antd 的组合,babel 插件部分通常这么配:
"extraBabelPlugins": [
"transform-runtime",
["import", { "libraryName": "antd", "style": "css" }]
],
"env": {
"development": {
"extraBabelPlugins": [
"dva-hmr"
]
}
}
然后把 webpack.config.js 中的配置参考 roadhog#配置 迁移到 .roadhogrc
中。
webpack.config.js
(完)
本文仅包含 MacOX 经验,Windows 待实践后更新。
electron 官方的 auto update 文档并不完善,多番 google 后,找到一种使用简单、跨平台、无特殊服务器要求的方案。
效果图:
出于安全考虑,自动更新必须搭配证书和 https 服务器使用。证书的生成可以用苹果开发者证书或者 StartSSL 等生成证书,而只有苹果开发者证书可以过掉 GateKeeper 。
打包时通过环境变量 CSC_LINK
和 CSC_KEY_PASSWORD
指定证书,另外在 Mac 下可不手动指定,electron-builder 会自动寻找合适的证书。
详见: Code Signing · electron-userland/electron-builder Wiki · GitHub
update.json
经测试,
update.json
不一定要 https 服务器。
格式参考以下例子:
先安装依赖,在 ./app
目录执行:
$ npm install electron-simple-updater --save
在 app/package.json
中配置 update.json
的地址:
"updater": {
"url": "https://raw.githubusercontent.com/sorrycc/test-release/master/update.json"
},
main 端配置 updater :
import updater from 'electron-simple-updater';
updater.init({
checkUpdateOnStart: false,
autoDownload: false,
logger: log,
});
renderer 端绑定时间并启动更新检测:
import { remote } from 'electron';
const updater = remote.require('electron-simple-updater');
updater.on('update-available', (meta) => {
console.log('[updater] update avaiable', meta.version);
updater.downloadUpdate();
});
updater.on('update-downloading', () => {});
updater.on('update-downloaded', () => {
if (window.confirm('Restart and install updates?')) {
updater.quitAndInstall();
}
});
updater.on('error', (err) => {});
updater.checkForUpdates();
基于 electron-builder 。
mac 下要有 release.json
和 {ProductName}-{version}-mac.zip
,需部署到 https 服务器。
release.json 格式如下:
{
"url": "https://github.com/sorrycc/test-release/releases/download/1.2.0/ReleaseTracker-0.2.0-mac.zip",
"name": "",
"notes": "",
"pub_date": "2017-1-20T14:18:48.988Z"
}
参考 Release 1.2.0 · sorrycc/test-release · GitHub 。
参考前面的例子更新 update.json
。
(完)
我有个这样的场景:
list = [
{
infoList:[
{
name:'xx'
}
]
}
];
循环list的时候,infoList里面的item动态增加了checked属性变成:
list = [
{
infoList:[
{
name:'xx',
checked: true/false
}
]
}
];
这个属性我是通过:
extendObservable(commodityItem,{
checked: false
});
这种方式让其变成obserable的。现在我想在ui层面上事件修改,如:
<select checked={item.checked} onChanage={(checked)=>{ item.checked = checked; }} />
不起作用额,想请教下应该怎么做?刚开始用mobx。感谢。
本文仅适用于 dva@1,dva@2 的文档请移步 #62
本文仅适用于 dva@1,dva@2 的文档请移步 #62
本文仅适用于 dva@1,dva@2 的文档请移步 #62
本文会一步步引导大家如何创建一个 CURD 应用,包含查询、编辑、删除、创建,以及分页处理,数据 mock,自动处理 loading 状态等,基于 react, dva 和 antd 。
最终效果:
先安装 dva-cli,并确保版本是 0.7.x。
$ npm i [email protected] -g
$ dva -v
0.7.0
然后创建应用:
$ dva new user-dashboard
$ cd user-dashboard
babel-plugin-import 用于按需引入 antd 的 JavaScript 和 CSS,这样打包出来的文件不至于太大。
$ npm i antd --save
$ npm i babel-plugin-import --save-dev
修改 .roadhogrc
,在 "extraBabelPlugins"
里加上:
["import", { "libraryName": "antd", "style": "css" }]
修改 .roadhogrc
,加上 "proxy"
配置:
"proxy": {
"/api": {
"target": "http://jsonplaceholder.typicode.com/",
"changeOrigin": true,
"pathRewrite": { "^/api" : "" }
}
},
然后启动应用:(这个命令一直开着,后面不需要重启)
$ npm start
浏览器会自动开启,并打开 http://localhost:8000 。
访问 http://localhost:8000/api/users ,就能访问到 http://jsonplaceholder.typicode.com/users 的数据。(由于 typicode.com 服务的稳定性,偶尔可能会失败。不过没关系,正好便于我们之后对于出错的处理)
用 dva-cli 生成路由:
$ dva g route users
然后访问 http://localhost:8000/#/users 。
用 dva-cli 生成 Model :
$ dva g model users
修改 src/models/users.js
:
import * as usersService from '../services/users';
export default {
namespace: 'users',
state: {
list: [],
total: null,
},
reducers: {
save(state, { payload: { data: list, total } }) {
return { ...state, list, total };
},
},
effects: {
*fetch({ payload: { page } }, { call, put }) {
const { data, headers } = yield call(usersService.fetch, { page });
yield put({ type: 'save', payload: { data, total: headers['x-total-count'] } });
},
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname, query }) => {
if (pathname === '/users') {
dispatch({ type: 'fetch', payload: query });
}
});
},
},
};
新增 src/services/users.js
:
import request from '../utils/request';
export function fetch({ page = 1 }) {
return request(`/api/users?_page=${page}&_limit=5`);
}
由于我们需要从 response headers 中获取 total users 数量,所以需要改造下 src/utils/request.js
:
import fetch from 'dva/fetch';
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
/**
* Requests a URL, returning a promise.
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
* @return {object} An object containing either "data" or "err"
*/
export default async function request(url, options) {
const response = await fetch(url, options);
checkStatus(response);
const data = await response.json();
const ret = {
data,
headers: {},
};
if (response.headers.get('x-total-count')) {
ret.headers['x-total-count'] = response.headers.get('x-total-count');
}
return ret;
}
切换到浏览器(会自动刷新),应该没任何变化,因为数据虽然好了,但并没有视图与之关联。但是打开 Redux 开发者工具,应该可以看到 users/fetch
和 users/save
的 action 以及相关的 state 。
用 dva-cli 生成 component:
$ dva g component Users/Users
然后修改生成出来的 src/components/Users/Users.js
和 src/components/Users/Users.css
,并在 src/routes/Users.js
中引用他。具体参考这个 Commit。
需留意两件事:
src/constants.js
改完后,切换到浏览器,应该能看到带分页的用户列表。
添加 layout 布局,使得我们可以在首页和用户列表页之间来回切换。
src/components/MainLayout/MainLayout.js
和 CSS 文件src/routes
文件夹下的文件中引用这个布局参考这个 Commit。
注意:
dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。
先安装 dva-loading :
$ npm i dva-loading --save
修改 src/index.js
加载插件,在合适的地方加入下面两句:
+ import createLoading from 'dva-loading';
+ app.use(createLoading());
然后在 src/components/Users/Users.js
里绑定 loading 数据:
+ loading: state.loading.models.users,
具体参考这个 Commit 。
切换到浏览器,你的用户列表有 loading 了没?
只改一个文件 src/components/Users/Users.js
就好。
处理分页有两个思路:
我们用的是思路 2 的方式,好处是用户可以直接访问到 page 2 或其他页面。
参考这个 Commit 。
经过前面的 9 步,应用的整体脉络已经清晰,相信大家已经对整体流程也有了一定了解。
后面的功能调整基本都可以按照以下三步进行:
我们现在开始增加用户删除功能。
src/services/users.js
:export function remove(id) {
return request(`/api/users/${id}`, {
method: 'DELETE',
});
}
src/models/users.js
:*remove({ payload: id }, { call, put, select }) {
yield call(usersService.remove, id);
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},
src/components/Users/Users.js
,替换 deleteHandler
内容:dispatch({
type: 'users/remove',
payload: id,
});
切换到浏览器,删除功能应该已经生效。
处理用户编辑和前面的一样,遵循三步走:
先是 service,修改 src/services/users.js
:
export function patch(id, values) {
return request(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(values),
});
}
再是 model,修改 src/models/users.js
:
*patch({ payload: { id, values } }, { call, put, select }) {
yield call(usersService.patch, id, values);
const page = yield select(state => state.users.page);
yield put({ type: 'fetch', payload: { page } });
},
最后是 component,详见 Commit。
需要注意的一点是,我们在这里如何处理 Modal 的 visible 状态,有几种选择:
另外,怎么存也是个问题,可以:
此教程选的方案是 2-2,即存 component state,并且 visible 按 user 存。另外为了使用的简便,封装了一个 UserModal
的组件。
完成后,切换到浏览器,应该就能对用户进行编辑了。
相比用户编辑,用户创建更简单些,因为可以共用 UserModal
组件。和 Step 11 比较类似,就不累述了,详见 Commit 。
到这里,我们已经完成了一个完整的 CURD 应用。但仅仅是完成,并不完善,比如:
请期待下一篇。
(完)
css3
对 Roof 不感兴趣的同学可以直接从 Redux 段落读起。
下文说说我理解的支付宝前端应用架构发展史,从 roof 到 redux,再到 dva。
Roof 应该是从 0.4 开始在项目里大范围推广的。
Roof 0.4 接触不多,时间久了已经没有太多印象了,记忆中很多概念是从 baobab 里来的,通过 cursor 订阅数据,并基于此设计了很多针对复杂场景的解决方案。
这种方式灵活且强大,现在想想如果这条路一走到底,或许比现在要好一些。但由于概念比较多,当时大家都比较难理解 cursor 这类的概念。并且 redux 越来越流行。。
然后有了 Roof 0.5,提供 createRootContainer 和 createContainer,实现类似 react-redux 里 Provider 和 connect 的功能,并隐藏了 cursor 的概念。
// 定义 state
createRootContainer({
user: { name: 'chris', age: 30 }
})(App);
// 绑定 state
createContainer({
myUser: 'user',
})(UserInfo);
这在一定程度上迎合了 redux 用户的习惯。但 redux 用户却并不满足,就算不能用 redux,也希望能在 roof 上使用上更多 redux 相关的特性。
还有个在这一阶段讨论较多的另一个问题是没有最佳实践,大家针对同一个问题通常有不同的解法。最典型的是异步请求的处理,有些人直接写从 Component 生命周期里,有些好一点的提取成 service/api,但还是在 Component 里调,还有些提取成 Controller 。
这是 library 相对于 framework 的略势,Roof 本质上是一个 library,要求他去解决所有开发中能想到的问题其实是不公平的。那么如何做的? 目前看起来有两种方案,1) boilerplate 2) framework 。这在之后会继续探讨。
在经历了几个 bugfix 版本之后,Roof 0.5.5 却是个有新 feature 的更新。感觉从这个版本起已经不是原作者的本意了,而是对于用户的妥协。
这个版本引入了一个新的概念:action
。
这也是从 redux (或者说 flux) 里而来的,所有用户操作都可以被理解成是一个 action,这样在 Component 里就不用直接调 Controller 或者 api/service 里的接口了,一定程度上做了解耦。
createActionContainer({
myUser: 'user',
}, {
// 绑定 actions
userActions,
})(UserInfo);
这让 Roof 越来越像 redux,但由于没有引入 dispatch
,在实际项目中遇到了不少坑。比较典型的是 action 之间的互相调用。
function actionA() {
actionB();
}
function actionB() {}
还有 action 里更新数据之前必须重新从 state 里拉最新的进行更新之类的问题,记得当时还写过 issue 来记录踩过的坑。这是想引入 redux,但却只引入一半的结果。
然后是 Roof 0.5.6@beta,这个版本的内核已经换成了 redux,引入 reducer
和 dispatch
来解决上个版本遇到的问题。所以本质上他等同于 react-redux,看下 import
语句应该就能明白。
import { createStore, combineReducers } from 'redux';
import { createDispatchContainer, createRootContainer } from 'roof';
大家可能注意到这个版本有个 @beta
,这也是目前 Roof 的最终版本。因为大家意识到既然已经这样了,为啥不用 redux 呢?
然后就有不少项目开始用 redux,但是 redux 是一个 library,要在团队中使用,就需要有最佳实践。那么最佳实践是什么呢?
Redux 本身是一个很轻的库,解决 component -> action -> reducer -> state 的单向数据流转问题。
按我理解,他有两个非常突出的特点是:
可预测性是由于他大量使用 pure function 和 plain object 等概念(reducer 和 action creator 是 pure function,state 和 action 是 plain object),并且 state 是 immutable 的。这对于项目的稳定性会是非常好的保证。
可扩展性则让我们可以通过 middleware 定制 action 的处理,通过 reducer enhancer 扩展 reducer 等等。从而有了丰富的社区扩展和支持,比如异步处理、Form、router 同步、redu/undo、性能问题(selector)、工具支持。
但是那么多的社区扩展,我们应该如何选才能组成我们的最佳实践? 以异步处理为例。(这也是我觉得最重要的一个问题)
用地比较多的通用解决方案有这些:
redux-thunk 是支持函数形式的 action,这样在 action 里就可以 dispatch 其他的 action 了。这是最简单应该也是用地最广的方案吧,对于简单项目应该是够的。
redux-promise 和上面的类似,支持 promise 形式的 action,这样 action 里就可以通过看似同步的方式来组织代码。
但 thunk 和 promise 都有的问题是,他们改变了 action 的含义,使得 action 变得不那么纯粹了。
然后出现的 redux-saga 让我眼前一亮,具体不多说了,可以看他的文档。总之给我的感觉是优雅而强大,通过他可以把所有的业务逻辑都放到 saga 里,这样可以让 reducer, action 和 component 都很纯粹,干他们原本需要干的事情。
所以在异步处理这一环节,我们选择了 redux-saga。
最终通过一系列的选择,我们形成了基于 redux 的最佳实践。
但就像之前所有的 Roof 版本一样,每个时代的应用架构都有自己的问题。Redux 这套虽然已经比较不错,但仍避免不了在项目中暴露自己的问题。
文件切换问题
redux 的项目通常要分 reducer, action, saga, component 等等,我们需要在这些文件之间来回切换。并且这些文件通常是分目录存放的:
+ src
+ sagas
- user.js
+ reducers
- user.js
+ actions
- user.js
所以通常我们需要在这三个 user.js 中来回切换。(真实项目中通常还有 services/user.js
等) 不知大家是否有感觉,这样的频繁切换很容易打断编码思路?
saga 创建麻烦
我们在 saga 里监听一个 action 通常需要这样写:
function *userCreate() {
try {
// Your logic here
} catch(e) {}
}
function *userCreateWatcher() {
takeEvery('user/create', userCreate);
}
function *rootSaga() {
yield fork(userCreateWatcher);
}
对于 redux-saga 来说,这样设计可以让实现更灵活,但对于我们的项目而言,大部分场景只需要用到 takeEvery 和 takeLatest 就足够,每个 action 的监听都需要这么写就显得非常冗余。
entry 创建麻烦
可以看下这个 redux entry 的例子,除了 redux store 的创建,中间件的配置,路由的初始化,Provider 的 store 的绑定,saga 的初始化,还要处理 reducer, component, saga 的 HMR 。这就是真实的项目应用 redux 的例子,看起来比较复杂。
基于上面的这些问题,我们封装了 dva 。dva 是基于 redux 最佳实践 实现的 framework,api 参考了 choo,概念来自于 elm 。详见 dva 简介。
并且除了上面这些问题,dva 还能解决 domain model 组织和团队协作的问题。
来看个简单的例子:(这个例子没有异步逻辑,所以并没有包含 effects 和 subscriptions 的使用,感兴趣的可以看 Popular Products 的 Demo)
import React from 'react';
import dva, { connect } from 'dva';
import { Route } from 'dva/router';
// 1. Initialize
const app = dva();
// 2. Model
app.model({
namespace: 'count',
state: 0,
reducers: {
['count/add' ](count) { return count + 1 },
['count/minus'](count) { return count - 1 },
},
});
// 3. View
const App = connect(({ count }) => ({
count
}))(function(props) {
return (
<div>
<h2>{ props.count }</h2>
<button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
<button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
</div>
);
});
// 4. Router
app.router(
<Route path="/" component={App} />
);
// 5. Start
app.start(document.getElementById('root'));
5 步 4 个接口完成单页应用的编码,不需要配 middleware,不需要初始化 saga runner,不需要 fork, watch saga,不需要创建 store,不需要写 createStore,然后和 Provider 绑定,等等。但却能拥有 redux + redux-saga + ... 的所有功能。
更多 dva 的详解,后面会逐步补充。
从 Roof 到 Redux 再到 dva 一路走来,每个方案都有自己的优点和缺陷,后一个总是为了解决前一个方案的问题而生,感觉上是在逐步变好的过程中,这让我觉得踏实。
另外,感叹坚持走自己的路是件很困难的事情,尤其是积累了一定用户量之后。在害怕失去用户和保留本心之间需要有个权衡和坚守。
本文已迁移至 https://github.com/dvajs/dva-docs/blob/master/v1/zh-cn/getting-started.md
我们将基于 dva 完成一个简单 app,并熟悉他的所有概念。
最终效果:
这是一个测试鼠标点击速度的 App,记录 1 秒内用户能最多点几次。顶部的 Highest Record 纪录最高速度;中间的是当前速度,给予即时反馈,让用户更有参与感;下方是供点击的按钮。
看到这个需求,我们可能会想:
在代码组织部分,可能会想:
以及:
我们可以带着这些问题来看这篇文章,但不必担心有多复杂,因为全部 JavaScript 代码只有 70 多行。
你应该会更希望关注逻辑本身,而不是手动敲入一行行代码来构建初始的项目结构,以及配置开发环境。
那么,首先需要安装的是 dva-cli 。dva-cli 是 dva 的命令行工具,包含 init、new、generate 等功能,目前最重要的功能是可以快速生成项目以及你所需要的代码片段。
$ npm install -g dva-cli
安装完成后,可以通过 dva -v
查看版本,以及 dva -h
查看帮助信息。
安装完 dva-cli 后,我们用他来创建一个新应用,取名 myApp
。
$ dva new myApp --demo
注意:--demo
用于创建简单的 demo 级项目,正常项目初始化不加要这个参数。
然后进入项目目录,并启动。
$ cd myApp
$ npm start
几秒之后,会看到这样的输出:
proxy: listened on 8989
livereload: listening on 35729
📦 173/173 build modules
webpack: bundle build is now finished.
(如需关闭 server,请按 Ctrl-C.)
在浏览器里打开 http://localhost:8989/ ,正常情况下,你会看到一个 "Hello Dva" 页面。
接到需求之后推荐的做法不是立刻编码,而是先以上帝模式做整体设计。
这个需求里,我们定义 model 如下:
app.model({
namespace: 'count',
state: {
record : 0,
current: 0,
},
});
namespace 是 model state 在全局 state 所用的 key,state 是默认数据。然后 state 里的 record 表示 highest record
,current
表示当前速度。
完成 Model 之后,我们来编写 Component 。推荐尽量通过 stateless functions 的方式组织 Component,在 dva 的架构里我们基本上不需要用到 state 。
import styles from './index.less';
const CountApp = ({count, dispatch}) => {
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: {count.record}</div>
<div className={styles.current}>{count.current}</div>
<div className={styles.button}>
<button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>
</div>
</div>
);
};
注意:
import styles from './index.less';
,再通过 styles.xxx
的方式声明 css classname 是基于 css-modules 的方式,后面的样式部分会用上count
和 dispatch
,count
对应 model 上的 state,在后面 connect 的时候绑定,dispatch
用于分发 actiondispatch({type: 'count/add'})
表示分发了一个 {type: 'count/add'}
的 action,至于什么是 action,详见:[email protected]更新 state 是通过 reducers 处理的,详见 [email protected]。
reducer 是唯一可以更新 state 的地方,这个唯一性让我们的 App 更具可预测性,所有的数据修改都有据可查。reducer 是 pure function,他接收参数 state 和 action,返回新的 state,通过语句表达即 (state, action) => newState
。
这个需求里,我们需要定义两个 reducer,count/add
和 count/minus
,分别用于计数的增和减。值得注意的是 count/add
时 record 的逻辑,他只在有更高的记录时才会被记录。
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
+ reducers: {
+ add(state) {
+ const newCurrent = state.current + 1;
+ return { ...state,
+ record: newCurrent > state.record ? newCurrent : state.record,
+ current: newCurrent,
+ };
+ },
+ minus(state) {
+ return { ...state, current: state.current - 1};
+ },
+ },
});
注意:
{ ...state }
里的 ...
是对象扩展运算符,类似 Object.extend
,详见:对象的扩展运算符add(state) {}
等同于 add: function(state) {}
还记得之前的 Component 里用到的 count 和 dispatch 吗? 会不会有疑问他们来自哪里?
在定义了 Model 和 Component 之后,我们需要把他们连接起来。这样 Component 里就能使用 Model 里定义的数据,而 Model 中也能接收到 Component 里 dispatch 的 action 。
这个需求里只要用到 count
。
function mapStateToProps(state) {
return { count: state.count };
}
const HomePage = connect(mapStateToProps)(CountApp);
这里的 connect 来自 react-redux。
接收到 url 之后决定渲染哪些 Component,这是由路由决定的。
这个需求只有一个页面,路由的部分不需要修改。
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
注意:
history
默认是 hashHistory 并且带有 _k
参数,可以换成 browserHistory,也可以通过配置去掉 _k
参数。现在刷新浏览器,如果一切正常,应该能看到下面的效果:
默认是通过 css modules 的方式来定义样式,这和普通的样式写法并没有太大区别,由于之前已经在 Component 里 hook 了 className,这里只需要在 index.less
里填入以下内容:
.normal {
width: 200px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
box-shadow: 0 0 20px #ccc;
}
.record {
border-bottom: 1px solid #ccc;
padding-bottom: 8px;
color: #ccc;
}
.current {
text-align: center;
font-size: 40px;
padding: 40px 0;
}
.button {
text-align: center;
button {
width: 100px;
height: 40px;
background: #aaa;
color: #fff;
}
}
效果如下:
在此之前,我们所有的操作处理都是同步的,用户点击 + 按钮,数值加 1。
现在我们要开始处理异步任务,dva 通过对 model 增加 effects 属性来处理 side effect(异步任务),这是基于 redux-saga 实现的,语法为 generator。(但是,这里不需要我们理解 generator,知道用法就可以了)
在这个需求里,当用户点 + 按钮,数值加 1 之后,会额外触发一个 side effect,即延迟 1 秒之后数值 1 。
app.model({
namespace: 'count',
+ effects: {
+ *add(action, { call, put }) {
+ yield call(delay, 1000);
+ yield put({ type: 'minus' });
+ },
+ },
...
+function delay(timeout){
+ return new Promise(resolve => {
+ setTimeout(resolve, timeout);
+ });
+}
注意:
*add() {}
等同于 add: function*(){}
takeEvery
),还可以选择 takeLatest
,或者完全自定义 take
规则刷新浏览器,正常的话,就应该已经实现了最开始需求图里的所有要求。
在实现了鼠标测速之后,怎么实现键盘测速呢?
在 dva 里有个叫 subscriontions 的概念,他来自于 elm。
Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
dva 中的 subscriptions 是和 model 绑定的。
+import key from 'keymaster';
...
app.model({
namespace: 'count',
+ subscriptions: {
+ keyboardWatcher({ dispatch }) {
+ key('⌘+up, ctrl+up', () => { dispatch({type:'count/add'}) });
+ },
+ },
});
这里我们不需要手动安装 keymaster 依赖,在我们敲入 import key from 'keymaster';
并保存的时候,dva-cli 会为我们安装 keymaster
依赖并保存到 package.json
中。输出如下:
use npm: tnpm
Installing `keymaster`...
[keymaster@*] installed at node_modules/.npminstall/keymaster/1.6.2/keymaster (1 packages, use 745ms, speed 24.06kB/s, json 2.98kB, tarball 15.08kB)
All packages installed (1 packages installed from npm registry, use 755ms, speed 23.93kB/s, json 1(2.98kB), tarball 15.08kB)
📦 2/2 build modules
webpack: bundle build is now finished.
index.js
import dva, { connect } from 'dva';
import { Router, Route } from 'dva/router';
import React from 'react';
import styles from './index.less';
import key from 'keymaster';
const app = dva();
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'count/minus' });
},
},
subscriptions: {
keyboardWatcher(dispatch) {
key('⌘+up, ctrl+up', () => { dispatch({type:'count/add'}) });
},
},
});
const CountApp = ({count, dispatch}) => {
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: {count.record}</div>
<div className={styles.current}>{count.current}</div>
<div className={styles.button}>
<button onClick={() => { dispatch({type: 'count/add'}); }}>+</button>
</div>
</div>
);
};
function mapStateToProps(state) {
return { count: state.count };
}
const HomePage = connect(mapStateToProps)(CountApp);
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
app.start('#root');
// ---------
// Helpers
function delay(timeout){
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
}
我们已在开发环境下进行了验证,现在需要部署给用户使用。敲入以下命令:
$ npm run build
输出:
> @ build /private/tmp/dva-quickstart
> atool-build
Child
Time: 6891ms
Asset Size Chunks Chunk Names
common.js 1.18 kB 0 [emitted] common
index.js 281 kB 1, 0 [emitted] index
index.css 353 bytes 1, 0 [emitted] index
该命令成功执行后,编译产物就在 dist 目录下。
通过完成这个简单的例子,大家前面的问题是否都已经有了答案? 以及是否熟悉了 dva 包含的概念:model, router, reducers, effects, subscriptions ?
还有其他问题?可以关注 dva repo 了解更多细节。
(完)
在columns中需也需要用·{}·来包含第二个变量
render: (text, { id }) => ( <span className = { styles.operation } > <a href = "" > Edit </a> <Popconfirm title = "Confirm to delete?" onConfirm = { deleteHandler.bind(null, {id})} > <a href = "" > Delete </a> </Popconfirm> </span> ),
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.