rthong / blog Goto Github PK
View Code? Open in Web Editor NEW个人博客
个人博客
首先需求是根据state来进行视图渲染:
const appState = {
title: {
text: '第一次title text',
color: 'red'
},
content: {
text: '第一次content text',
color: 'blue'
}
}
// ...
function renderApp(appState) {
console.log('app')
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle(title) {
console.log('title')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent(content) {
console.log('content')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
如果需要在此基础上对视图修改,只需要对appState
进行修改再手动调用渲染就能更新视图上的状态。但是也因此会有很多缺陷,比如说多个其他函数都需要对数据进行操作就会对同一个全局变量进行修改,这样出现问题就会很难定位。
为了解决这个问题,现在统一规定对state的修改必须调用dispatch函数,它接收一个普通的js对象action,其中type表明想要进行的操作
const dispatch = action => {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.payload
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.payload
break
default:
break
}
}
// ...
dispatch({ type: 'UPDATE_TITLE_TEXT', payload: '第二次修改的title text' })
这样,当外部函数想要修改state,都需要调用dispatch传入action,出现问题只需要在switch中进行调试。
通过构建createStore函数专门提供dispatch,并且接受一个stateChange专门对state的操作进行描述
const createStore = (state, stateChange) => {
const dispatch = action => stateChange(state, action)
return { dispatch }
}
// ...
const stateChange = (state, action) => {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
state.title.text = action.payload
break
case 'UPDATE_TITLE_COLOR':
state.title.color = action.payload
break
default:
break
}
}
const store = createStore(appState, stateChange)
这样其他项目只需要通过向createStore传入一个state和一个描述state相关操作的stateChange就也可以使用这样的模式。但有个问题,当dispatch时只是修改了state而并没有渲染数据,所以每次dispatch时都需要手动调用renderApp。我们希望每次dispatch时会自动渲染数据。
我们需要一种方式去“监听”数据的变化,这就用到了观察者模式,通过subscribe将渲染数据的回调保存在listeners数组中,再在dispatch中将listeners数组中的回调依次执行。
const createStore = (state, stateChange) => {
const listeners = [] // 存储渲染回调
const getState = () => state // 外部需要获取最新的state传给renderApp
const dispatch = action => {
stateChange(state, action)
listeners.forEach(fn => fn())
}
const subscribe = fn => {
listeners.push(fn)
}
return { dispatch, subscribe, getState }
}
这样每次dispatch时都会触发渲染回调,只需要在首次的时候手动调用一次渲染。至此,我们完成了一套dispatch更新数据->渲染数据的自动流程,但是还有一些缺陷,比如只更新state部分字段,但是还是会导致全部数据进行渲染,这样就有很严重的性能损失。
为了解决上述问题,就需要对比两次的state的改变,但是之前的变动都是在同一个state上进行修改数值,不能够直接判断。所以我们需要一种方式来判断两次state,为此我们需要在stateChange中返回一份state的拷贝,不变的字段和原state一样而改变的字段就指向一个新的对象。
function renderApp(newState, oldState = {}) {
if (newState === oldState) return
console.log('app')
renderTitle(newState.title, oldState.title)
renderContent(newState.content, oldState.content)
}
function renderTitle(newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return
console.log('title')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}
function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) return
console.log('content')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}
//...
const stateChange = (state, action) => {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return {
...state,
title: {
...state.title,
text: action.payload
}
}
case 'UPDATE_TITLE_COLOR':
return {
...state,
title: {
...state.title,
color: action.payload
}
}
default:
break
}
}
//...
let oldState = getState()
subscribe(() => {
renderApp(getState(), oldState)
oldState = getState()
})
这样将两次的state传入渲染函数就能进行判断状态是否改变从而决定是否重新渲染数据
经过之前几步,现在已经有一个很通用的createStore,使用方式是初始化state和描述状态改变的stateChange,并通过createStore生成的store去调用subscribe订阅渲染数据的回调,然后通过dispatch去返回新的state去触发数据渲染。
进一步优化,其实将state和stateChange放在一起,让stateChange既能够初始化state又能够生成新的state。
const stateChange= (state, action) => {
if (!state) {
return {
title: {
text: '第一次title text',
color: 'red'
},
content: {
text: '第一次content text',
color: 'blue'
}
}
}
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return {
...state,
title: {
...state.title,
text: action.payload
}
}
case 'UPDATE_TITLE_COLOR':
return {
...state,
title: {
...state.title,
color: action.payload
}
}
default:
break
}
}
同时现在需要在createStore中将state进行初始化
const createStore = stateChange=> {
let state = null
const listeners = [] // 存储渲染回调
const getState = () => state // 外部需要获取最新的state传给renderApp
const dispatch = action => {
state = reducer(state, action)
listeners.forEach(fn => fn())
}
const subscribe = fn => {
listeners.push(fn)
}
dispatch({}) //初始化state
return { dispatch, subscribe, getState }
}
最后只需要将stateChange这个名称替换成reducer就实现了一个简单的redux模式。
我们遵循一个发现问题->解决问题这样一个流程实现了redux模式:
useState接收的值只作为初始渲染时的状态,后续的重新渲染的值都是通过setState去设置
除了常规的setState(value)的方式去更新状态以外,setState还可以接收一个函数来更新状态。这种更新状态的方式通常使用在新的 state 需要通过使用先前的 state 计算得出的场景。
在effect 的依赖频繁变化的场景下有时也可以通过函数式更新状态来解决问题,例如一个每秒中自增状态的场景:
但是依赖项设置后会导致每次改变发生时定时器都被重置,这并不是我们想要的,所以这时就能够通过函数式更新状态并且不引用当前state。
一些需要复杂计算的初始状态如果直接将函数运行结果传入useState,会在每次重新渲染时执行所传入的函数,比如:
所以可以向useState中传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用codesandbox
useEffect用来完成副作用。
在class组件中,通常做法是在生命周期中去检查props.A 和 state.B,如果符合某个条件就去触发XXX副作用。而在function组件中,思维模式是我需要处理XXX副作用,它的依赖数据是props.A 和 state.B。从之前的命令式转变到function组件的声明式,开发者不再需要将精力投入到思考不同生命周期判断各种条件再执行副作用这样的事中,而可以将精力聚焦在更高的抽象层次上。
React在每次渲染都有这次对应的state、props、事件处理函数和effect,如果设置了错误的依赖就可能会导致副作用函数中所使用到的值并不是最新的。
举个例子,我们来写一个每秒递增的计数器。在Class组件中,我们的直觉是:“开启一次定时器,清除也是一次”。这里有一个例子说明怎么实现它。当我们理所当然地把它用useEffect的方式翻译,直觉上我们会设置依赖为[],因为“我只想运行一次effect”。
然而,这个例子只会递增一次。因为副作用函数只在第一次渲染时候执行,第一次渲染对应的count是0,所以定时器中永远是setCoun(1)。
依赖项是我们给react的提示,告诉react不必每次渲染都去执行effect,只需要在依赖项变动时才去执行对应的effect。错误的依赖项导致effect中拿到的状态的值可能跟上一次effect执行时一样而不是最新的状态。
类似于这样的问题是很难被想到的,所以需要启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则,此规则会在添加错误依赖时发出警告并给出修复建议。
尽量设置少的依赖项
比如在某些情况下,组件内函数和effect依赖同一个state
因为doSomething
这个函数并没有被使用到多个地方,所以可以将它声明到effect内部去减少依赖项
和上面一种情况类似,但是doSomething
这个函数在多个地方使用
这种情况不方便把一个函数移动到 effect 内部,可以将函数使用useCallback去包裹这个函数
如果函数没有使用到组件内的值,可以将函数声明到组件外部以减少依赖项
比如在依赖当前状态来更新状态的情况下,可以使用函数式更新状态来减少依赖项,就像上面useState中所举的例子一样。
比如在function组件中去使用rxjs数据流时,需要在组件挂载和销毁时监听和取消监听,如果在组件外去定义subject,全局监听的都是同一个observable
这样在其中任一个组件中next值时,都会被所有观察者接收到。
所以这里需要保持每个组件有自己独立的observable,并且它又不需要作为状态去参与渲染,所以这块使用useRef去保存这个observable。
这样就达到了目的。
当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current
属性不会引发组件重新渲染,所以我们可以使用useRef去保存一些不参数渲染的值。
| null
在给.current
直接赋值时,ts会给出错误提示:
那么怎么将 current
属性转为 动态可变
的呢,其实在 useRef
的类型定义中已经给出了答案。
如果需要直接修改useRef
的结果,则在泛型参数的类型中包含| null
就可以了。
useMemo和useCallback一起作为组件渲染优化的选择而出现,但是它们不能作为性能优化的银弹而去在任何情况下去使用。
我们需要知道这两个api本身也有开销。它们 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo/useCallback 可能会影响程序的性能。
所以要想合理使用 useMemo/useCallback,我们需要搞清楚 它们 适用的场景:
useState的替代模式,在状态之间逻辑复杂时使用useReducer可以将what和how分开,只需要在组件中声明式的dispatch对应的行为,所有行为的具体实现都在reducer中维护,让我们的代码可以像用户的行为一样,更加清晰。
这是一个登录demo, 通过useReducer将登录的相关状态抽离出组件内部,防止组件内部多处去维护这些状态,组件内部只需要通过dispatch行为来完成各种交互。
如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer
改写之前的登录demo,将登录button改为子组件,通过useContext
去拿到dispatch
。
如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext
useImperativeHandle
一般与 forwardRef
一起使用。
主要作用与forwarRef
基本相同,都是为了将子组件的一些数据暴露给父组件。区别在于只使用forwarRef
时只能够对ref
进行转发对外暴露dom元素实例
,使用useImperativeHandle
时能够自定义对外暴露的实例值。
// 子组件
const Bar = React.forwardRef<{}, any>((props, ref) => {
const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
// 对外暴露出api
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
blur: () => {
inputRef.current?.blur();
},
changeValue: (val: string) => {
setValue(val);
},
instance: inputRef.current,
}));
return (
<div>
<Input ref={inputRef} value={value}></Input>
</div>
);
});
// 父组件
const Foo = () => {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
console.log('【ref】', ref.current);
}, []);
return <Bar ref={ref}></Bar>;
};
可以看到父组件中打印结果:
确实拿到了子组件中暴露出来的一些数据。
useImperativeHandle
暴露出的api来修改子组件的状态常规父组件控制子组件的行为都是将状态通过props传给子组件然后在父组件中去控制改状态
的形式来实现,但是在很少的情况下这种方式满足不了我们的需求,例如在做一些复杂的通过方法调用而不是组建式调用的ui组件时。这时就可以用到父组件可以通过子组件useImperativeHandle暴露出的api来修改子组件的状态
的方式来达到目的。
比如说上例中在父组件调用的changeValue
方法就能够间接的修改子组件内部的value
状态,从而达到控制子组件的效果。
ref.current.changeValue('这是改变后的值');
不过需要强调的是,这种父组件控制子组件的方式在常规需求中几乎使用不到,应当尽量避免对子组件ref
的过度使用。
redux主要目的就是为了解决多处对同一状态修改带来的问题,而反映到react上就是多个层级不同的组件对同一个状态的操作。首先,需要让子组件有方法去访问到统一个状态,在react中刚好context就是做着个事情的,但是如果要进行状态变更的话就需要修改到context里面的状态,这会提高组件间的耦合性。所以我们可以将context和redux结合起来,既能够通过context获取store又能够通过redux来集中处理state。
首先需要搞清楚如何通过使用context来让子组件获取到其中的状态,
// context.js
export const Context = React.createContext()
// index.js
class Index extends Component {
state = {
themeColor: 'red'
}
render() {
return (
// 在需要用到状态的组件外部包裹Provider
<Context.Provider value={this.state}>
<Header />
<Content changeColor={e => this.setState({ themeColor: e })} />
</Context.Provider>
)
}
}
class Header extends Component {
static contextType = Context
render() {
// 通过this.context进行访问状态
return <h1 style={{ color: this.context.themeColor }}>hong</h1>
}
}
而如果需要直接通过context来修改数据,就需要通过修改顶层的value来重新渲染数据。
至此,我们就了解了context的简单使用模式。
通过在Connext.Provider中value传入store,使得包裹的子组件能够通过context获取到store,也因此可以调用getState获取最新的state,subscribe来监听dispatch对状态的修改从而通过setState来重新渲染页面。
// index.js
const createStore = reducer => {
let state = null
const listeners = [] // 存储渲染回调
const getState = () => state // 外部需要获取最新的state传给renderApp
const dispatch = action => {
state = reducer(state, action)
listeners.forEach(fn => fn())
}
const subscribe = fn => {
listeners.push(fn)
}
dispatch({}) //初始化state
return { dispatch, subscribe, getState }
}
const reducer = (state, action) => {
if (!state) {
return {
themeColor: 'red'
}
}
switch (action.type) {
case 'CHANGE_COLOR':
return {
...state,
themeColor: action.payload
}
default:
return state
}
}
const store = createStore(reducer)
class Index extends Component {
state = {
themeColor: 'red'
}
render() {
return (
<Context.Provider value={store}>
<Header />
<Content changeColor={e => this.setState({ themeColor: e })} />
</Context.Provider>
)
}
}
// ...
// 子组件内
class ThemeSwitch extends Component {
static contextType = Context
state = {
themeColor: ''
}
componentWillMount() {
const store = this.context
this.setState({
themeColor: store.getState().themeColor
})
store.subscribe(() => {
this.setState({
themeColor: store.getState().themeColor
})
})
}
render() {
return (
<div>
<button
style={{ color: this.state.themeColor }}
onClick={() => {
this.context.dispatch({ type: 'CHANGE_COLOR', payload: 'red' })
}}
>
Red
</button>
<button
style={{ color: this.state.themeColor }}
onClick={() => {
this.context.dispatch({ type: 'CHANGE_COLOR', payload: 'blue' })
}}
>
Blue
</button>
</div>
)
}
}
但是这样直接去结合context和redux会使得业务代码和redux相关代码耦合严重,非常不好使用,需要将redux相关和组件解耦。
由于组件大量依赖于context和store,导致其复用性很差,所以需要将redux和context相关从组件中抽离,这就需要使用高阶组件来对原组件进行封装,新组件再和原组件通过props来进行数据传递,保持原组件的pure。
首先是通过connect来进行高阶组件的封装:
// react-redux.js
// 将之前的组件中redux和context相关放入connect,然后将所有state全部以props传给子组件
const Context = React.createContext()
const connect = WrapperComponent => {
class Connect extends React.Component {
static contextType = Context
state = {
allProps: {}
}
componentWillMount() {
const store = this.context
this.setState({
allProps: store.getState()
})
store.subscribe(() => {
this.setState({
allProps: store.getState()
})
})
}
_change(e) {
const { dispatch } = this.context
dispatch({ type: 'CHANGE_COLOR', payload: e })
}
render() {
return <WrapperComponent {...this.state.allProps} change={e => this._change(e)} />
}
}
return Connect
}
// ...
同时在子组件中通过connect(Component)的形式导出这个高阶组件,而原先index.js中Context.Provider由于Context已经移到react-redux.js中,所以也需要对外部导出一个Provider去接收外部传给context的store
// 因为需要和connect共用一个Context所以封装到一起
const Provider = props => {
return <Context.Provider value={props.store}>{props.children}</Context.Provider>
}
至此,这个react-redux已经能用,并且外部组件也只是单纯通过props接收state保持了很好的复用性,context和redux相关也已经和组件实现分离。但是现在的组件从高阶组件接收到的总是所有的state,需要通过一种方式来告诉connect接收哪些数据。
在之前的基础上,我们需要知道每个原组件需要获取哪些state。
我们传入一个名为mapStateToprops
的函数,它接受最新的state作为参数,返回一个对象作为原组件接受的state的props;同样,我们需要在修改state的组件中,告诉connect我们接受一个怎样dispatch的函数,我们传入一个名为mapDispatchToprops
的函数,它接受dispatch作为参数,返回一个对象作为原组件接受的修改state的函数的props:
const mapStateToProps = state => {
return {
themeColor: state.themeColor
}
}
const mapDispatchToProps = dispatch => {
return {
change(color) {
dispatch({ type: 'CHANGE_COLOR', payload: color })
}
}
}
// 暂时这样进行调用
export default connect(
ThemeSwitch,
mapStateToProps,
mapDispatchToProps
)
现在,每个组件都能够只获取到本组件使用到的state和修改state的函数。
const connect = (WrapperComponent, mapStateToProps, mapDispatchToProps) => {
class Connect extends React.Component {
static contextType = Context
state = {
allProps: {}
}
componentWillMount() {
const store = this.context
this._change(store)
store.subscribe(() => {
this._change(store)
})
}
// 需要在初始化和执行dispatch回调时都获取最新的state
_change(store) {
const stateProps = mapStateToProps ? mapStateToProps(store.getState()) : {}
const dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch) : {}
this.setState({
allProps: {
...stateProps,
...dispatchProps
}
})
}
render() {
// 对外部直接传入的props也直接传给原组件
return <WrapperComponent {...this.state.allProps} {}...this.props}/>
}
}
return Connect
}
现在我们的react-redux还存在一些问题,就是当state改变时,Provider包裹的所有子组件都会重新进行渲染,因为mapStateToProps
和maoDispatchToProps
每次返回新的对象,再传给原组件时相当于props发生改变,就会引起重新渲染。现在我们要对它进行优化。
暂时我自己优化的方法是通过在Connect组件的shouldComponentUpdate
方法中通过判断state的改变来达到优化渲染的目的。
通过在每次调用connect时使用一个变量保存当前使用到的state,在下一次渲染时候在shouldComponentUpdate中对比两次使用到的state是否发生变化来决定是否渲染,同时还需要对外部直接传递的props进行判断是否变化。
const connect = (mapStateToProps, mapDispatchToProps) => {
let oldState // 保存当前使用的state
return WrapperComponent => {
class Connect extends React.Component {
static contextType = Context
state = {
allProps: {}
}
componentWillMount() {
const store = this.context
oldState = mapStateToProps(store.getState())
this._change(store)
store.subscribe(() => {
this._change(store)
})
}
shouldComponentUpdate(props) {
// 判断直接传入的props是否更改
if (Object.keys(props).some(key => props[key] !== this.props[key])) {
return true
}
const newState = mapStateToProps(this.context.getState())
// 判断两次使用的state是否更改
const flag = Object.keys(oldState).some(key => oldState[key] !== newState[key])
oldState = newState
return flag
}
_change(store) {
const stateProps = mapStateToProps ? mapStateToProps(store.getState()) : {}
const dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch) : {}
this.setState({
allProps: {
...stateProps,
...dispatchProps
}
})
}
render() {
return <WrapperComponent {...this.state.allProps} {...this.props} />
}
}
return Connect
}
}
这里调用connect和之前不太一样,这里会返回一个函数而不是返回高阶组件,react-redux就是采用这样的方式。其实这里我也不太明白为什么需要这样做。
最近刚接触Flutter,想通过实现一个小项目进行熟悉,于是将之前做过的小程序的天气app改成flutter版。
主要显示布局是以Stack搭建层叠结构,分为最底层的背景层,上一层的天气效果层,最顶层的天气信息和图标。
请求使用的库是flutter中文官网上推荐的Dio,基础用法
Response response =
await dio.get(baseURL, queryParameters: {'cityIds': '$areaid'});
return response.data;
返回的是json。在使用接口数据时想要ide进行字段提示,并且在编译阶段能进行字段检测就需要对json进行序列化并申明interface。flutter中的interface较之ts来说定义比较复杂,如果对象结构复杂,需要去申明多个class。
import 'package:json_annotation/json_annotation.dart';
part 'weather.g.dart';
@JsonSerializable()
class WeatherRes {
final String code;
final String message;
final String redirect;
final List<WeatherInfo> value;
WeatherRes({this.code, this.message, this.redirect, this.value});
factory WeatherRes.fromJson(Map<String, dynamic> json) =>
_$WeatherResFromJson(json);
Map<String, dynamic> toJson() => _$WeatherResToJson(this);
}
var weatherJson = await getWeather(areaid: this.areaid);
// 获取的json通过WeatherRes.fromJson来与WeatherRes对应起来
WeatherRes weather = new WeatherRes.fromJson(weatherJson);
其中包含几个重要的概念: Animation、AnimationController、Tween
// 项目中风车无限转动的动画
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
static final _rotateTween = new Tween<double>(begin: 0.0, end: math.pi * 2);
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) => Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Transform.rotate(
angle: _rotateTween.evaluate(animation), child: child),
child: child),
);
}
class AnimationWidget extends StatefulWidget {
Widget animationChild;
AnimationWidget({this.animationChild});
_AppState createState() => _AppState();
}
class _AppState extends State<AnimationWidget>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 5), vsync: this);
// animation = Tween<double>(begin: 0.0, end: math.pi * 2).animate(controller);
// animation =
animation = new CurvedAnimation(parent: controller, curve: Curves.linear);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.repeat();
}
});
controller.forward();
}
// #enddocregion print-state
@override
Widget build(BuildContext context) => GrowTransition(
child: widget.animationChild,
animation: animation,
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
// #docregion print-state
}
flutter自带的RefreshIndicator widget就能实现下拉刷新,但是文档中说的都是需要RefreshIndicator的child必须是可滚动的widget,我的需求仅仅是下滑手势就进行刷新,并不会有widget被滚动。
这就需要对RefreshIndicator 的key进行标记,再通过this._refreshIndicatorKey.currentState.show()
来手动显示刷新视图并调用onRefresh回调。
new RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: this._refresh,
displacement: 100,
child: ConstrainedBox(
constraints: BoxConstraints.expand(),
child: GestureDetector(
onVerticalDragStart: (DragStartDetails e) {
//打印手指按下的位置(相对于屏幕)
// print("用户手指按下:${e.globalPosition}");
},
//手指滑动时会触发此回调
onVerticalDragUpdate: (DragUpdateDetails e) {
// print('正在滑动');
},
onVerticalDragEnd: (DragEndDetails e) {
// print(e.velocity);
//打印滑动结束时在x、y轴上的速度
//竖直速度大于0表示向下滑动就显示refreshIndicator,并获取数据
if (e.velocity.pixelsPerSecond.dy > 0) {
this._refreshIndicatorKey.currentState.show();
}
},
// ...
flutter中普通widget超出屏幕就会直接报错,我暂时是通过OverflowBox指定maxWidth的值来让子widget超出屏幕
// 多云的云层滚动效果
OverflowBox(
maxWidth: 1200,
child: CloudAnimationWidget(
animationChild:
// 需要两张图片无限滚动的效果
Row(
children: <Widget>[
Container(
width: 600,
height: 200,
child: Image.asset(
'images/cloud.png',
// width: MediaQuery.of(context).size.width,
// fit: BoxFit.cover,
),
),
Container(
width: 600,
height: 200,
child: Image.asset(
'images/cloud.png',
// width: MediaQuery.of(context).size.width,
// fit: BoxFit.cover,
),
),
],
)));
flutter中地位需要引入amap_location,同时还需要通过simple_permissions来获取手机定位权限。
void _checkPersmission() async {
// 获取定位权限
bool hasPermission =
await SimplePermissions.checkPermission(Permission.WhenInUseLocation);
if (!hasPermission) {
PermissionStatus requestPermissionResult =
await SimplePermissions.requestPermission(
Permission.WhenInUseLocation);
if (requestPermissionResult == PermissionStatus.denied) {
Alert(context: context, title: "申请定位权限失败", desc: "Flutter is awesome.")
.show();
return;
}
}
// 直接获取定位信息
AMapLocation loc = await AMapLocationClient.getLocation(true);
print(loc.district);
City res = _getCity(loc.province, loc.city, loc.district);
if (res is City) {
widget.onCityChange(res.areaid);
setState(() {
this.cityName = res.countyname;
});
}
}
目标是生成20个雨滴动画,在每个雨滴动画结束时改变雨滴坐标和动画时间重新生成新的动画再销毁之前的动画。
先是尝试在监听动画回调中去将completed状态的动画重新设置参数赋值给animation和controller,试过之后发现有问题。
之后又继续尝试将一些动画参数作为状态,在completed的时候调用controller.repeat()并setstate来改变动画参数。但是发现,动画只会在第一次结束触发completed状态,之后repeat的动画并不会触发addStatusListener的回调。
于是想了一下让每个雨滴动画结束时调用外部传入的回调,然后在原来的雨滴动画List中替换掉已结束的动画widget,其中每个动画widget需要生成一个唯一的key让flutter来进行更新检测,这样20个雨滴动画就只会更新已结束的动画。
animation.addStatusListener((state) {
if (state == AnimationStatus.completed) {
// 执行外部结束动画回调
widget.onComplete();
}
});
controller.forward();
RainAni _createRainAni(int index) {
return RainAni(
width: widget.width,
height: widget.height,
// 随机生成key来标记每个动画
key: ValueKey(math.Random().nextDouble()),
isInit: this.isInit,
aniMode: widget.aniMode,
// 外部传入的结束动画回调
onComplete: () {
this.isInit = false;
setState(() {
// 替换动画数组中的已结束项
this.rainList[index] = _createRainAni(index);
});
});
}
整个完成下来感觉在体验方面确实十分流畅,当然也与项目简单有关,但是遇到不少坑:
在日常开发中,menu icon主要由三种方式:
使用自定义的svg
在antd pro的文档中描述了前两中方式,但是使用自定义svg的方式并没有在文档中出现。
我们知道antd pro中的菜单是在pro-layout的基础上又封装了一层,将很多配置项注入到pro-layout中。
现在需要将自定义的icon传给pro-layout,但是由于umi的影响,又不能直接在route配置中直接写组件。所以需要在antd-pro的入口文件中,将route中的icon手动转换成pro-layout MenuDataItem
中icon所接收的ReactNode。
接下来就是调用pro-layout中的api去修改菜单数据MenuData
,具体来说有两个api都能够修改,分别是menuDataRender
和postMenuData
。
这两个api据官方文档描述,在不需要国际化的情况下,使用postMenuData
修改数据能显著提升性能
在实际项目开发中,需要基于多种环境去设置不同的环境变量以便于在构建阶段或是运行阶段去使用,例如常见的通过process.env.NODE_ENV
在构建时去判断当前的构建环境是development
还是production
,例如需要在开发环境
、测试环境
和生产环境
去访问不同的接口服务器。
为了模拟真实的项目,使用webpack搭建了一个最小化的项目结构:
├─package.json
├─src
| └index.jsx
├─public
| └index.html
├─config
| └webpack.config.js
Node环境变量就是指process.env
这个属性
它是 Nodejs 应用程序中,process.env
属性,返回包含用户环境的对象,所以它不可以在客户端侧代码中使用,也就不能在浏览器环境上使用。
// process.env(官方示例)
{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'nodejscn',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/nodejscn',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/nodejscn',
LOGNAME: 'nodejscn',
_: '/usr/local/bin/node'
}
使用 cross-env
依赖包,支持跨平台配置环境变量。
// package.json
{
...,
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development AAA=123 webpack serve --config ./config/webpack.config.js",
"build:test": "cross-env NODE_ENV=test webpack --config ./config/webpack.config.js",
"build:pro": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js"
},
...
}
通过在package.json
脚本中设置变量的方式来注入环境变量,同时cross-env
还支持去设置多个环境变量,只需要通过空格区分,例如在dev
脚本中设置的NODE_ENV=development
和AAA=123
。
这样在执行npm start
就能够通过process.env
获取到对应的环境变量。
// webpack.config.js
console.log("【process.env】", process.env.AAA);
但是在index.jsx
中也就是浏览器环境下的文件中打印process.env
就会报错
原因就是前文提到的peocess.env
是Node环境的属性,浏览器环境不能够获取到。让浏览器环境获取到所需变量我们后文再说。
直接通过在script脚本中注入环境变量的方式不利于集中管理环境变量,而且在环境变量较多时这种方式也十分不友好,所以需要一种方式来集中管理这些环境变量。
使用dotenv
依赖包可将环境变量从 .env
文件加载到 process.env
。
dotenv
会默认加载根目录的.env
文件去注入环境变量,通过require('dotenv').config()
即可完成注入。
//webpack.config.js
dotenv.config();
//.env文件
AAA=123
在多环境配置时需要通过规定不同环境对应的.env
文件,例如现在规定.env.test
是测试环境对应的环境变量,.env.production
是生产环境,.env
是开发环境。然后通过dotenv.config({ path: })
去加载对应文件的环境变量。
//webpack.config.js
const PROJECT_PATH = resolve(__dirname, "../");
const dotenvFile = resolve(PROJECT_PATH, `./.env.${process.env.NODE_ENV}`);
// 加载.env*文件 默认加载.env文件
dotenv.config({
path: fs.existsSync(dotenvFile)
? dotenvFile
: resolve(PROJECT_PATH, `./.env`),
});
console.log("【process.env】", process.env.ENV);
这里process.env.NODE_ENV
是为了判断当前的运行环境来去加载对应的.env
文件。
// package.json
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.config.js",
"build:test": "cross-env NODE_ENV=test webpack --config ./config/webpack.config.js",
"build:pro": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js"
}
//.env.production
ENV=pro
//.env
ENV=dev
可以看到不同环境的变量确实已经注入成功。
浏览器环境下也需要根据不同的环境变量来处理一些逻辑,但是它不能获取到process.env
所以不能像注入Node环境变量的方式来实现。浏览器环境变量是基于webpack.DefinePlugin
这个插件在项目构建时引入的,引入之后可以在前端代码中全局获取到对应的变量。
基础使用方式是将所需的变量按键值对的方式传入DefinePlugin
中,需要注意的是变量值需要通过JSON.stringify
进行包裹。
module.exports = {
plugins: [
new DefinePlugin({
aaa: JSON.stringify("!!!!")
})
]
}
执行脚本后可以在index.jsx
中得到对应的结果
//index.jsx
console.log("【app】", aaa);
const App = () => {
return <div>app</div>;
};
浏览器环境中也需要根据项目环境来引入不同的变量,之前在Node
环境中已经获取到不同的环境变量,我们可以建立一个基于此的映射表在项目构建时根据拿到的Node
环境变量来引入对应的浏览器环境变量。
// webpack.config.js
// 浏览器环境注入的变量
const define = {
dev: {
baseURL: "/api/dev",
},
test: {
baseURL: "/api/test",
},
pro: {
baseURL: "/api/pro",
},
};
module.exports = {
new DefinePlugin({
"process.env": Object.keys(define[process.env.ENV]).reduce((env, key) => {
env[key] = JSON.stringify(define[process.env.ENV][key]);
return env;
}, {}),
}),
}
执行npm run build:pro
,在dist目录开启一个服务器,可以在浏览器控制台看到结果
在实际项目中根据不同环境切换接口服务器地址的场景中,就能通过这样的方式来获取到不同环境中的接口地址。
在平时开发中较常使用umi
作为项目框架,umi
通过环境变量UMI_ENV
区分不同环境来指定不同配置。
具体来说是通过脚本中注入的UMI_ENV=xxx
去匹配对应的config.xxx.js
配置文件,然后在define
属性中去配置需要引入浏览器环境的变量。
// webpack.config.js
module.exports = {
...,
"scripts": {
"start": "cross-env UMI_ENV=dev umi dev",
"build:test": "cross-env UMI_ENV=test umi build",
"build:pre": "cross-env UMI_ENV=pre umi build",
"build:pro": "cross-env UMI_ENV=pro umi build",
}
...,
}
//config.dev.js
import { defineConfig } from 'umi';
export default defineConfig({
define: {
'process.env': {
BASE_API: '/api/dev',
},
},
});
useState接收的值只作为初始渲染时的状态,后续的重新渲染的值都是通过setState去设置
除了常规的setState(value)的方式去更新状态以外,setState还可以接收一个函数来更新状态。这种更新状态的方式通常使用在新的 state 需要通过使用先前的 state 计算得出的场景。
在effect 的依赖频繁变化的场景下有时也可以通过函数式更新状态来解决问题,例如一个每秒中自增状态的场景:
但是依赖项设置后会导致每次改变发生时定时器都被重置,这并不是我们想要的,所以这时就能够通过函数式更新状态并且不引用当前state。
一些需要复杂计算的初始状态如果直接将函数运行结果传入useState,会在每次重新渲染时执行所传入的函数,比如:
所以可以向useState中传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用codesandbox
useEffect用来完成副作用。
在class组件中,通常做法是在生命周期中去检查props.A 和 state.B,如果符合某个条件就去触发XXX副作用。而在function组件中,思维模式是我需要处理XXX副作用,它的依赖数据是props.A 和 state.B。从之前的命令式转变到function组件的声明式,开发者不再需要将精力投入到思考不同生命周期判断各种条件再执行副作用这样的事中,而可以将精力聚焦在更高的抽象层次上。
React在每次渲染都有这次对应的state、props、事件处理函数和effect,如果设置了错误的依赖就可能会导致副作用函数中所使用到的值并不是最新的。
举个例子,我们来写一个每秒递增的计数器。在Class组件中,我们的直觉是:“开启一次定时器,清除也是一次”。这里有一个例子说明怎么实现它。当我们理所当然地把它用useEffect的方式翻译,直觉上我们会设置依赖为[],因为“我只想运行一次effect”。
然而,这个例子只会递增一次。因为副作用函数只在第一次渲染时候执行,第一次渲染对应的count是0,所以定时器中永远是setCoun(1)。
依赖项是我们给react的提示,告诉react不必每次渲染都去执行effect,只需要在依赖项变动时才去执行对应的effect。错误的依赖项导致effect中拿到的状态的值可能跟上一次effect执行时一样而不是最新的状态。
类似于这样的问题是很难被想到的,所以需要启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则,此规则会在添加错误依赖时发出警告并给出修复建议。
尽量设置少的依赖项
比如在某些情况下,组件内函数和effect依赖同一个state
因为doSomething
这个函数并没有被使用到多个地方,所以可以将它声明到effect内部去减少依赖项
和上面一种情况类似,但是doSomething
这个函数在多个地方使用
这种情况不方便把一个函数移动到 effect 内部,可以将函数使用useCallback去包裹这个函数
如果函数没有使用到组件内的值,可以将函数声明到组件外部以减少依赖项
比如在依赖当前状态来更新状态的情况下,可以使用函数式更新状态来减少依赖项,就像上面useState中所举的例子一样。
比如在function组件中去使用rxjs数据流时,需要在组件挂载和销毁时监听和取消监听,如果在组件外去定义subject,全局监听的都是同一个observable
这样在其中任一个组件中next值时,都会被所有观察者接收到。
所以这里需要保持每个组件有自己独立的observable,并且它又不需要作为状态去参与渲染,所以这块使用useRef去保存这个observable。
这样就达到了目的。
当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current
属性不会引发组件重新渲染,所以我们可以使用useRef去保存一些不参数渲染的值。
useMemo和useCallback一起作为组件渲染优化的选择而出现,但是它们不能作为性能优化的银弹而去在任何情况下去使用。
我们需要知道这两个api本身也有开销。它们 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo/useCallback 可能会影响程序的性能。
所以要想合理使用 useMemo/useCallback,我们需要搞清楚 它们 适用的场景:
useState的替代模式,在状态之间逻辑复杂时使用useReducer可以将what和how分开,只需要在组件中声明式的dispatch对应的行为,所有行为的具体实现都在reducer中维护,让我们的代码可以像用户的行为一样,更加清晰。
这是一个登录demo, 通过useReducer将登录的相关状态抽离出组件内部,防止组件内部多处去维护这些状态,组件内部只需要通过dispatch行为来完成各种交互。
如果你的页面state比较复杂(state是一个对象或者state非常多散落在各处)请使用userReducer
改写之前的登录demo,将登录button改为子组件,通过useContext
去拿到dispatch
。
如果你的页面组件层级比较深,并且需要子组件触发state的变化,可以考虑useReducer + useContext
平时在开发的过程中,我们可能并不太需要十分了解babel的内容,仅仅知道它能够将新特性的代码转换成能够在旧版本浏览器中运行的代码。但是这一次想要趁着自己搭建脚手架的机会去进一步的了解babel的知识,所以写了这篇文章。以下内容是babel 7.4之后的版本,也就是@babel/polyfill
被废弃需要独立安装core-js
和 regenerator-runtime
模块的版本。
@babel/cli
是babel的命令行工具,主要提供babel
命令。另外还需要安装@babel/core
才能使用babel去编译。
npm install --save-dev @babel/core @babel/cli
将命令配置在 package.json 文件的 scripts 字段中:
// package.json
"scripts": {
"compiler": "babel src --out-dir lib --watch"
}
这样就能够通过npm run compiler
来执行编译,但是babel本身什么都不做,需要添加插件来帮助babel完成工作。
babel所有功能都建立在各种的plugin上,使用方式是安装相应的plugin
再去配置文件中去使用。例如箭头函数转换插件,
安装@babel/plugin-transform-arrow-functions
,然后在.babelrc
配置文件中去指定对应的插件
//.babelrc
{
plugins: ["@babel/plugin-transform-arrow-functions"],
};
然后执行npm run compiler
,可以看到箭头函数已经被编译完成
但是如果我们每个功能都去一个个添加对应的plugin
会很麻烦,多以我们就需要preset
预设去直接添加一组插件。
preset
就是一组插件的集合,最常用的preset
就是@babel/preset-env
。
它的作用是根据目标环境去进行语法转换和导入对应的polyfill
。
需要注意的是,@babel/preset-env
会根据你配置的目标环境,生成插件列表来编译。默认情况下,如果你没有在 Babel 配置文件中(如 .babelrc)设置 targets 或 ignoreBrowserslistConfig,@babel/preset-env
会使用 package.json
的browserslist
配置源。
我们可以模拟生产环境和开发环境的浏览器版本
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];
通过设置不同浏览器环境使用@babel/preset-env
去编译相同代码,可以看到最终的结果也会不同。
module.exports = {
presets: [
[
"@babel/preset-env",
{
// targets: product,
targets: development,
},
],
],
};
babel 只负责对语法进行编译,比如当我们写箭头函数,babel 会帮你把它编译成普通函数。但是对一些新的扩展方法,新的类来说babel就不能转换了。这时就需要去引入polyfill
,polyfill
的中文意思是垫片,所谓垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。
babel v7.4版之后,需要直接安装core-js
和 regenerator-runtime
去替代之前的@babel/polyfill
。croe-js
提供了 ES5、ES6 规范中新定义的各种对象、方法的polyfill,regenerator-runtime
用来实现 ES6/ES7 中 generators、yield、async 及 await 等相关的 polyfill。
首先,我们需要安装他们到生产环境中,因为需要在生产环境中运行其中的polyfill
。
npm install --save core-js regenerator-runtime
在@babel/preset-env
的配置项中把useBuiltIns
设置成usage
,这样会根据目标浏览器环境去引入所需要的polyfill
。需要注意点是,设置useBuiltIns
还需要同时设置corejs
。
//.babelrc
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: product,
useBuiltIns: "usage",
corejs: 3,
},
],
],
};
//index.js
const isHas = [1, 2, 3].includes(2);
const getData = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(100);
}, 1000);
});
const main = async () => {
const res = await getData();
console.log(res);
};
main();
可以看到,编译后的文件中只引入了所用到的polyfill。
useBuiltIns
还可以设置成其他值,比如entry
,这需要在项目入口文件手动引入polyfills,例如@babel/polyfill
或者core-js
//.babelrc
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: product,
useBuiltIns: "entry",
corejs: 3,
},
],
],
};
//index.js
// 入口文件引入core-js
require("core-js");
但是这种方式会引入全量的polyfill。
useBuiltIns
默认值为false
,代表每个文件里不自动添加polyfill,或不将import "@babel/polyfill"
转换为单独的polyfill。
在使用@babel/preset-env
配合useBuiltIns: usage
时,文件中会引入一些辅助方法例如_classCallCheck
,当多处文件都使用到class时同样也会在每个文件中去引入这些辅助方法,这样会增大打包体积并且完全没有必要多次去引入同样的辅助方法。
//index.js
class A {}
//.babelrc.js
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: product,
useBuiltIns: "usage",
corejs: 3,
},
],
],
};
编译结果:
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var A = function A() {
_classCallCheck(this, A);
};
为了解决这个问题就需要使用@babel/plugin-transform-runtime
,使用该插件,所有辅助方法都将引用模块 @babel/runtime
,这样就可以避免编译后的代码中出现重复的辅助方法,有效减少包体积。
@babel/plugin-transform-runtime
需要配合@babel/runtime
来使用,@babel/plugin-transform-runtime
在开发时使用,最终代码需要依赖@babel/runtime
。
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
//index.js
class A {}
//.babelrc.js
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: product,
useBuiltIns: "usage",
corejs: 3,
},
],
],
//使用@babel/plugin-transform-runtime
plugins: [["@babel/plugin-transform-runtime"]],
};
编译结果:
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var A = function A() {
(0, _classCallCheck2.default)(this, A);
};
可以看到这些辅助方法都是从@babel/runtime
中引入。
之前在使用@babel/preset-env
编译promise和includes时会引入core-js
中的全局变量或者在对应的原型链中添加相应的方法,这样都造成了全局环境的污染。虽然这对于应用程序或命令行工具是可以的,但是如果你的代码是要发布供他人使用的库,或者无法完全控制代码运行的环境,则将成为一个问题。
首先,单独使用@babel/plugin-transform-runtime
只能够处理辅助方法,如果想要去引入polyfill
就需要配合@babel/runtime-corejs3
使用。
同样还是在生产环境安装@babel/runtime-corejs3
。
npm install @babel/runtime-corejs3 --save
这里需要在.babelrc
中去除@babel/preset-env
配置中关于polyfill
的部分以免与@babel/runtime-corejs3
重复。
//index.js
const isHas = [1, 2, 3].includes(2);
const getData = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(100);
}, 1000);
});
getData();
//.babelrc.js
module.exports = {
presets: [["@babel/preset-env"]],
plugins: [
[
"@babel/plugin-transform-runtime",
{
corejs: 3,
},
],
],
};
编译结果:
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _setTimeout2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-timeout"));
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
var _context;
var isHas = (0, _includes["default"])(_context = [1, 2, 3]).call(_context, 2);
var getData = function getData() {
return new _promise["default"](function (resolve, reject) {
(0, _setTimeout2["default"])(function () {
resolve(100);
}, 1000);
});
};
getData();
可以看到,使用@babel/plugin-transform-runtime
会用一个临时变量去保存polyfill中的一些值,并不是直接去修改原型链或者新增Promise方法。
@babel/preset-env
配合useBuiltIns: usage
,在开发第三方库时使用@babel/plugin-transform-runtime
在上面介绍@babel/plugin-transform-runtime
的一些使用时可以看到,它不仅能够处理引入多次helper辅助方法的问题,而且在只引入所需polyfill时还不会污染全局环境,那还有必要使用@babel/preset-env
的useBuiltIns
吗?
其实@babel/plugin-transform-runtime
配合@babel/runtime-corejs3
引入polyfill有一个很大的不足就是不能够通过设置目标环境去引入所需要的polyfil。,我们在普通开发时只需要在package.json
中的browserslist
去设置开发环境和生产环境的浏览器版本,然后通过使用@babel/preset-env
的useBuiltIns
就能够根据不同的运行环境去引入适当的polyfill。
但是在开发第三方库时,不能确定代码的运行环境,所以就需要利用@babel/plugin-transform-runtime
来保证引入的polyfill不去污染全局环境。
一般开发: 通过useBuiltIns: usage
去保证引入恰当的polyfill,通过@babel/plugin-transform-runtime
保证辅助函数都是引用@babel/runtime
。
const product = ["ie >= 9"];
const development = ["last 2 Chrome versions"];
module.exports = {
presets: [
[
"@babel/preset-env",
{
//代替browserslist设置浏览器版本
targets: product,
useBuiltIns: "usage",
corejs: 3,
},
],
],
plugins: [["@babel/plugin-transform-runtime"]],
};
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.