关于mobx:bug-solved-This-experimental-syntax-requires-enabling-xxx

另外,这俩问题是我在mobx中应用ES7装璜器语法用到的,用一般的ES6语法是不会碰到这样的问题,嫌麻烦也能够不必装璜器语法,奈何我对这种看上去像Spring正文的语法垂涎已久。 bug1Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.ts(1219)问题出在IDE不意识这个语法 以vscode为例 须要在setting中搜“experimentalDecorators”而后勾选上 bug2Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (22:4)eslint问题出在我的项目自身不意识这个语法(以后这个语法还在试验阶段 babel还没有正式公布),这个解决起来略微有点麻烦,翻了很久stackoverflow和github issue,答复都是支支吾吾 遮遮掩掩,故作此篇,以飨前人。 step1: npm i @babel/core @babel/plugin-proposal-decorators @babel/preset-envstep2:我的项目根目录下创立.babelrc { "presets": ["@babel/preset-env"], "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]}step3:我的项目根目录下创立config-overrides.js const path = require('path')const { override, addDecoratorsLegacy } = require('customize-cra')function resolve(dir) { return path.join(__dirname, dir) }const customize = () => (config, env) => { config.resolve.alias['@'] = resolve('src') if (env === 'production') { config.externals = { 'react': 'React', 'react-dom': 'ReactDOM' } } return config}; module.exports = override(addDecoratorsLegacy(), customize())step4:npm i customize-cra react-app-rewired ...

March 26, 2023 · 1 min · jiezi

关于mobx:mobx数据变了视图没变mobx6的用法

在react中应用react-mobx的状况下,数据曾经被action 扭转了,然而视图层 没有随之概念 如果mobx的版本大于6 "mobx": "^6.3.2","mobx-react": "^7.2.0"切记增加 makeObservable 初始化我的项目 import { observable, action, computed, makeObservable } from "mobx";export class AuthStore { @observable name = 'wangkai000'; @observable sex = '男'; @observable userObj = { name: 'wangkai000', age: 233, token: '12345689' } constructor() { // makeObservable 在mobx6 版本之后 比增加项 makeObservable(this); } @action.bound setName(v) { console.log('触发action'); this.name = v; } @computed get titleName(){ return this.name+'___111'; } }

June 27, 2021 · 1 min · jiezi

关于mobx:reactmobx6使用案例

脚手架 create-react-app 一、装置//npm yarn 随需要,尽量不要混用,混用有些资源可能会呈现掉包 yarn add mobxyarn add mobx-react// 版本号"mobx": "^6.3.2","mobx-react": "^7.2.0",二、配置package.json1.把暗藏的webpack裸露进去,开释之前记得请先提交代码 git commit 一次 yarn run eject2.装置@babel/plugin-proposal-decorators 插件 必须的 yarn add @babel/plugin-proposal-decorators 3.批改增加 package.json配置 (手动) "babel": { "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}] ], "presets": [ "react-app" ]}三、定义mobx的store1.目录机构(mobx反对多个多个状态模块) stores----- auth.js 模块1 ----- test.js 模块2----- index.js 总入口 2.模块 auth.js // auth.js和test.js 截然不同 展现auth.js做案例// @action.bound 和 @action 区别 https://cn.mobx.js.org/refguide/action.htmlimport { observable, action, computed } from "mobx";import { makeObservable} from "mobx";export class AuthStore { // 定义变量 @observable name = 'zhangsan000'; @observable sex = '男'; @observable userObj = { name: 'zhangsan000', age: 233, token: '12345689' } // 初始化 constructor() { // mobx6版本当前 必须得在初始化加makeObservable makeObservable(this); } // 动作(bound 能够主动绑定this 避免this 指向扭转) @action.bound setName(v) { console.log('触发action'); this.name = v; } @action setUserObj(obj) { this.userObj = obj; } // 计算属性 @computed get titleName(){ return this.name+'___111'; } }3.定义导出进口index.js ...

June 27, 2021 · 2 min · jiezi

关于mobx:在-tsconfig-或-jsconfig-中设置-experimentalDecorators-选项以删除此警告

在react中引入mobx,vscode提醒  对润饰器的试验反对性能在未来的版本中可能更改。在 “tsconfig“ 或 “jsconfig“ 中设置 “experimentalDecorators“ 选项以删除此正告 设置一下vscode配置就行。 window零碎: 1.vscode左下角齿轮设置 => 设置2.搜寻 experimentalDecorators3.勾选 Mac零碎: 1.code => 首选项 =>设置 2.搜寻experimentalDecorators 3.勾选扩大配置

June 13, 2021 · 1 min · jiezi

关于mobx:汇总mobx奇淫技巧

简化action属性更新通常状况下,store中会有很多属性,其中有些属性更新会很频繁,每一个属性都须要写一个action函数去更新,当这种须要变更的属性越多时,会导致store中代码量异样的宏大,可读性也会升高。就如上面代码一样: class TestStore { @observable info = {} as IInfo; @observable list = [] as IList; @action setInfo = (value: IInfo) => { this.info = value; } @action setList = (value: IList) => { this.list = value; }}引入typescript中keyof关键字应用,能够将上述action函数简化如下: class TestStore { @observable info = {} as IInfo; @observable list = [] as IList; @action setValue = <T extends keyof TestStore>(key: T, value: this[T]) => { this[key] = value; }}

May 14, 2021 · 1 min · jiezi

关于mobx:Mobx-你懂了吗

Mobx 是什么?Mobx 是一个状态治理库。 Mobx 哲学任何源自利用状态的数据都应该主动地获取。"Anything that can be derived from the application state, should be derived. Automatically." Mobx 准则Mobx 反对单向数据流,即动作更新状态,状态更新视图。 为什么将 React 和 Mobx 联合应用?对于利用开发中常见的问题,React 和 Mobx 都给出了最优和独特的解决方案:React 通过应用虚构 DOM 对 UI 渲染进行了优化;Mobx 通过应用响应式状态图表,将利用状态与 React 组件同步的机制进行了优化,即只有在状态更新的时候才对组件做出扭转。归根结底,React 组件只是状态的富丽展现,而状态的衍生能够由 Mobx 来治理。 Mobx 外围概念1. Observable state(可察看的状态)Mobx 为现有的数据结构提供了可察看的性能,被察看的数据能够是原始值,也能够是援用值。 2. Computed values(计算值)计算值个别通过观察值衍生,当察看值扭转的时候,计算值会自动更新,并在它不再应用时将其优化掉。 衍生:任何源自状态并且不会有进一步相互作用的货色就是衍生。衍生能够有多种形式存在,比方:用户界面、衍生数据 class TodoList { @observable todos = []; @computed get unfinishedTodoCount() { return this.todos.filter(todo => !todo.finished).length; }}3. Reactions(反馈)Reactions 会对 state 变动作出反应,但它不是产生一个值,而是产生一些副作用,比方更新 UI。能够通过应用 Mobx 提供的 when、autorun、reaction 来创立自定义的 Reactions。 ...

February 26, 2021 · 1 min · jiezi

关于mobx:mobxmobxreact

mobxMobx 中创立 store 的常见关键字如下: observable computed action。 observable用来申明可察看的数据computed是申明可察看数据的演变数据,和 observable 具备等同位置action 用来扭转observable数据,然而 action 不是必须的,能够认为其是较好的约定,最好遵循。在 mobx 程序中应用class、装璜器是最佳实际,因而咱们的代码也应用装璜器实现mobx-reactProvider、observer、inject均为是mobx-react提供。 Provider以组件的模式存在,用来包裹最外层组件节点,并且传入 store(通过)context 传递给后辈组件。应用@observer装璜的react组件将转换成一个监听者,当@observable 润饰的数据变动,react组件就会从新渲染。@inject为了使被装璜的组件以 props 的模式获取到 Provider 传递过去的数据。

January 26, 2021 · 1 min · jiezi

关于mobx:mobx该怎么组织和划分store

在平时我的项目开发中,发现前端同学在进行数据管理时存在有如下问题: 进行到开发前期发现有些store体积异样宏大,动辄上千行代码,影响代码的可读性、维护性;很多store之间存在通信的场景,而之前组织的形式不便于store之前的通信;多人合作开发时难免会同时更改同一个store,不可避免的造成代码抵触;鉴于上述问题,在本次事变单导购我的项目中将对store的组织与划分做出如下改良: 思考到每次前端工作都是依据UI模块来拆解、调配的,为了更加符合每个前端开发人员的工作,防止工作穿插。当初,store依据UI模块来划分,每一个UI模块划分出两个store,一个UI store,一个Domain store。UI store是指以后UI的状态,比方:窗口尺寸、以后展现的页面、渲染状态、网络状态等等;Domain store次要蕴含页面所需的各种数据,带有业务性的数据(个别是须要从后端获取的)。 创立一个 RootStore 来实例化所有 stores ,并共享援用,使得所有的childStore都能通过rootStore进行通信,不必再像之前应用回调的模式实现通信。示例: class RootStore { constructor() { this.userStore = new UserStore(this) this.todoStore = new TodoStore(this) }}class UserStore { constructor(rootStore) { this.rootStore = rootStore } getTodos(user) { // 通过根 store 来拜访 todoStore return this.rootStore.todoStore.todos.filter(todo => todo.author === user) }}class TodoStore { @observable todos = [] constructor(rootStore) { this.rootStore = rootStore }}参考:https://cn.mobx.js.org/best/s...

December 16, 2020 · 1 min · jiezi

关于mobx:store变化了而页面取不到值mobx会对什么作出反应

1、没有封装成observer 组件容器组件 import Change from './Change';import Father from './Father';const Main = (props: any) => { return ( <div> <Father></Father> <Change></Change> </div> )}export default Main;Father组件 import { inject } from 'mobx-react';import React from 'react';import Store from '../../store/store';interface IProps { store?: Store;}const Father = (props: IProps) => { const { store } = props; const { message } = store as Store; return <div> <div>title: {message.title}</div> <div>author: {message.author.name}</div> <div>likes: {message.likes[0]}</div> </div>}export default inject('store')(Father);Change组件 ...

December 15, 2020 · 2 min · jiezi

关于mobx:store变化了而页面取不到值new两个store实例

这篇文章讲一讲平时应用mobx时常遇到的一种store变动了而页面取不到值的场景——new了两个store实例 假如当初有一个storeA,外面寄存了一个属性a,在组件A挂载实现时调接口获取数据并赋给属性a。当初有另外一个组件B也须要用到属性a,组件B先再次生成一份storeA并通过Provide注入,而后在外头获取属性a,发现获取不到。 这是因为组件A依赖的storeA实例与组件B依赖的storeA实例并不是同一个援用,每次new都会生成一个新的store实例,store的应用遵循就近准则。 上面举个例子阐明: 全局store import UserInfoStore from "./UserInfoStore";export default { userInfoStore: new UserInfoStore()}注入全局store ReactDOM.render( <HashRouter> <Provider {...stores}> {renderRoutes(routes)} </Provider> </HashRouter>, document.getElementById('root'))Layout组件中获取用户信息寄存到全局的UserInfoStore中 interface IProps extends RouteConfigComponentProps<void> { userInfoStore?: UserInfoStore}const Layout = (props: IProps) => { const {route, userInfoStore} = props const { fetchUserInfo } = userInfoStore as UserInfoStore; useEffect(() => { /** 获取用户信息 */ fetchUserInfo(); }) return ( <div className={styles.layout}> <div className={styles.header}>topbar</div> <div className={styles.menu}>slider</div> <div className={styles.content}> {renderRoutes(route?.routes)} </div> <div className={styles.footer}>footer</div> </div> )}export default inject('userInfoStore')(observer(Layout));当初页面组件Page再次生成新的UserInfoStore并通过Provider注入 ...

December 13, 2020 · 1 min · jiezi

ReactNative分布式热更新系统

热更新是一个非常方便的方案。在应对大量用户和深度定制的时候一定不能使用开源的方案。一般第三方的这种方案,服务器带宽较小,或者不够灵活,不能满足自己的想法。这里推荐自己实现对应的热更新方案。只需要少量代码即可支持。下面推荐一种灵活的热更新方案。包括客户端的改造、接口设计、界面开发,同时是开源的!可以自由改造。体验地址:demo 用户名密码都是:admin 基础数据的准备和实现首先第一点,一个APP如果要支持热更新,需要在打开APP(或者其他进入RN页面之前)就要判断是否需要更新bundle文件。这里就是我们实现热更新的节点。一旦需要热更新就开始下载文件,而判断的接口就是我们这次文章的核心内容。这里简单贴出安卓和ios两端的下载逻辑。 请求之前需要在head中附带上客户端的几个重要信息。客户端版本号version、客户端唯一id:clientid、客户端类型platform、客户端品牌brand。ios下载的例子 -(void)doCheckUpdate{ self.upView.viewButtonStart.hidden = YES; if ([XCUploadManager isFileExist:[XCUploadManager bundlePathUrl].path]) {//沙盒里已经有了下载好的jsbundle,以沙盒文件优先 self.oldSign = [FileHash md5HashOfFileAtPath:[XCUploadManager bundlePathUrl].path]; }else {//真机计算出的包内bundlemd5有变化,可能是压缩了,所以这里写死初始化的md5 // NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"jsbundle"]; // self.oldSign = [FileHash md5HashOfFileAtPath:ipPath]; self.oldSign = projectBundleMd5; } AFHTTPSessionManager *_sharedClient = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://test.com"]]; [self initAFNetClient:_sharedClient]; [_sharedClient GET:@"api/check" parameters:nil progress:nil success:^(NSURLSessionDataTask * __unused task, id JSON) { NSDictionary *dic = [JSON valueForKeyPath:@"data"]; BOOL isNeedLoadBundle = YES; if ([dic isKindOfClass:[NSDictionary class]]) { self.updateSign = [dic stringForKey:@"sign"]; self.downLoadUrl = [dic stringForKey:@"downloadUrl"]; if(self.updateSign.length && self.oldSign.length && (![self.updateSign isEqualToString:self.oldSign])) { //需要更新bundle文件了 self.upView.viewUpdate.hidden = NO; [self updateBundleNow]; isNeedLoadBundle = NO; }else { //不需要更新bundle文件,再处理跳过按钮显示逻辑 [self.upView showSkipButtonOrNot]; } } if (isNeedLoadBundle) { [self loadBundle]; } } failure:^(NSURLSessionDataTask *__unused task, NSError *error) { [self loadBundle]; }];}安卓下载的例子 ...

August 19, 2019 · 2 min · jiezi

ReactTypeScriptMobxAntDesignMobile进行移动端项目搭建

前言:本文基于React+TypeScript+Mobx+AntDesignMobile技术栈,使用Create-React-App脚手架进行一个移动端项目搭建,主要介绍项目搭建过程和相关配置,以及状态管理工具Mobx的使用,最后实现点击按钮数字+1和一个简单的TodoList小功能,希望能对大家有所帮助。GitHub代码地址 项目搭建具体步骤:安装Create-React-App脚手架工具,已经安装的同学请忽略npm install create-react-app -g 创建初始项目 create-react-app my-app --typescript注意: 如果同学你是参考typescript官方文档进行react项目搭建,里面有用create-react-app my-app --scripts-version=react-scripts-ts命令创建的,千万别用,现在已经过时了, webpack版本有问题,后续配置各种想不到的坑 TypeScript中文官网 引入AntDesignMobile,实现组件按需加载 本步骤官网有比较详细的介绍,我就不一一列举配置过程了,建议大家不要eject暴露所有内建配置,后续版本升级维护可能比较麻烦,推荐使用 react-app-rewired 插件即可配置;AntDesignMobile官网安装React路由,状态管理工具mobx,配置sass npm install history @types/history react-router-dom @types/react-router-dom // 安装react路由 npm install mobx-react mobx // 安装mobx npm install node-sass // 安装sass工具,安装之后无需另外配置sass,脚手架工具已经集成了 基本配置完成,运行项目 npm run startReact状态管理工具Mobx介绍:Mobx是一个功能强大,基于面向对象编程方式的一个状态管理工具,上手相对比较容易。就连Redux的作者也曾经向大家推荐过它,对TypeScript支持比较友好,参考官网一张流程图:Mobx中文官网 下面介绍几个Mobx核心概念Observable state(可观察的状态)MobX 为现有的数据结构(如对象,数组和类实例)添加了可观察的功能。 通过使用 @observable 装饰器(ES.Next)来给你的类属性添加注解就可以简单地完成这一切。import { observable } from "mobx";class Todo { id = Math.random(); @observable title = ""; @observable finished = false;}Computed values(计算值)使用 MobX, 你可以定义在相关数据发生变化时自动更新的值。 通过@computed 装饰器或者利用 (extend)Observable 时调用 的getter / setter 函数来进行使用,下面代码中当queue或者refresh发生变化时,MobX会监听数据变化确保,通过Computed触发fooProps方法自动更新。import React from 'react';import {observable, isArrayLike, computed, action, autorun, when, reaction,runInAction} from "mobx";import {observer} from 'mobx-react';// 定义数据Storeclass Store { @observable queue:number = 1; @action refresh = ():void => { this.queue += 1; console.log('this.queue -> ', this.queue); } @computed get fooProps():any { return { queue: this.queue, refresh: this.refresh }; } }Actions(动作)任何应用都有动作。动作是任何用来修改状态的东西,mobx推荐将修改被观测变量的行为放在action中。action只能影响正在运行的函数,而无法影响当前函数调用的异步操作,参考官方文档用法:action(fn)action(name, fn)@action classMethod() {}@action(name) classMethod () {}@action boundClassMethod = (args) => { body }@action(name) boundClassMethod = (args) => { body }@action.bound classMethod() {}action 装饰器/函数遵循 javascript 中标准的绑定规则。 但是,action.bound 可以用来自动地将动作绑定到目标对象。 注意,与 action 不同的是,(@)action.bound 不需要一个name参数,名称将始终基于动作绑定的属性。class Ticker { @observable tick = 0 @action.bound increment() { this.tick++ // 'this' 永远都是正确的 }}const ticker = new Ticker()setInterval(ticker.increment, 1000)利用Mobx作为状态管理,实现两个小功能点击按钮+1import React from 'react';import {observable, isArrayLike, computed, action, autorun, when, reaction,runInAction} from "mobx";import {observer} from 'mobx-react';import {Button} from 'antd-mobile';import './index.scss';// 定义数据Store,用Mobx作为状态管理工具class Store { @observable queue:number = 1; @action refresh = ():void => { this.queue += 1; console.log('this.queue -> ', this.queue); } @computed get fooProps():any { return { queue: this.queue, refresh: this.refresh }; } }// ts组件接收父组件传递过来的数据必须定义接口类型,否则报错interface BarProps{ queue :number}// @observer修饰类,Bar组件接受Foo组建传过来的queue属性@observer class Bar extends React.Component<BarProps>{ render() { const {queue} = this.props return ( <div className="queue">{queue}</div> ) }}interface FooProps { queue: number, refresh():void}// Foo组件接收来自Add组件的store数据@observerclass Foo extends React.Component<FooProps>{ render() { const {queue,refresh} = this.props; return ( <div> <Button type="primary" onClick = {refresh}>Refresh</Button> <Bar queue = {queue} /> </div> ) }}// 初始化store数据,传递给Foo组件const store = new Store();class Add extends React.Component{ render() { return ( <div> <h2 className="add"> hello react-ts-mobx</h2> <Foo queue = {store.queue} refresh = {store.refresh} /> </div> ) }}export default observer(Add)TodoList功能import React from 'react';import {observable, isArrayLike, computed, action, autorun, when, reaction,runInAction} from "mobx";import {observer} from 'mobx-react';import './index.scss';// 定义Todo数据类型class Todo { id:number = new Date().getTime(); title:string = ''; finished:boolean = false; constructor(title:string) { this.title = title; }}// Store数据方法管理class Store { @observable title:string = ''; @observable todos:Todo[] =[]; // 添加Todo的Title @action createTitle (e:any) { console.log('e',e.target.value); this.title = e.target.value; } // 增加Todo数据 @action createTodo = () => { this.todos.unshift(new Todo(this.title)); this.title = ''; } // 删除Todo @action delTodo (id:number) { this.todos.forEach((item,index) => { if (item.id === id) { this.todos.splice(index,1) } }) } // 监听todos数据变化,显示剩余待办数量 @computed get unfinished () { return this.todos.filter(todo => !todo.finished).length; }}interface TodoItemProps { todo:any; store:any;}// 每一条Todo数据组件@observer class TodoItem extends React.Component<TodoItemProps> { render() { const {todo,store} = this.props return ( <div className="item"> <span>{todo.title}</span> <span onClick={()=> store.delTodo(todo.id)}>X</span> </div> ) }}const store = new Store();@observer class TodoList extends React.Component { render() { return ( <div> <h2>TodoList</h2> <header> <input type="text" placeholder="please input" value={store.title} onChange = {e => store.createTitle(e)} /> <button onClick ={store.createTodo}>add</button> </header> <ul> {store.todos.map((todo)=>{ return <li key={todo.id}> <TodoItem todo={todo} store = {store}/> </li> })} </ul> <footer> {store.unfinished} item(s) unfinished </footer> </div> ) }}export default TodoList总结:本人刚接触TypeScript和Mobx不久,总结学习方法:应该先熟悉一些基本概念后,慢慢的做一些小功能,遇到问题认真思考后再查资料或者请教别人,反复看文档,加强对知识的理解;欢迎大佬们留言交流;GitHub代码地址 ...

July 1, 2019 · 3 min · jiezi

七分设计感的纯React项目Mung

多语言版本React版MungReact-Native版MungFlutter版Mung1. Mung:是一个基于React编写,使用豆瓣a开源API开发的一个项目。 2. 功能概述数据保存 :支持断网加载缓存数据。主题换肤 :现在只支持切换主题颜色,本项目没几张图片。查看电影详情 :支持查看电影详情包括评论。一键搜索: 支持标签和语句查找相关的电影。3. 运行结果图 4. 使用到的框架antd-mobile :阿里的UI库,主要用到里面列表显示、Toast提醒。Mobx :实现状态管理。react-loading :加载进度条。react-router-dom :路由管理。react-transition-group :实现动画效果。SCSS: 样式编写方便、清晰。5. 总结这是一个React项目主要,之前写过一个纯React-Native项目Mung没有使用状态管理库和UI库(除渐变库),相比React-Native,现在React水平还是比较一般,尤其是webpack、babel配置等方面,后续有时间还得多看看。里面还有一个ant-design按需加载的问题,使用react-app-rewired配置后和scss发生冲突,有时间再改下。 6. GitHub地址

April 28, 2019 · 1 min · jiezi

使用RxJS管理React应用状态的实践分享

随着前端应用的复杂度越来越高,如何管理应用的数据已经是一个不可回避的问题。当你面对的是业务场景复杂、需求变动频繁、各种应用数据互相关联依赖的大型前端应用时,你会如何去管理应用的状态数据呢?我们认为应用的数据大体上可以分为四类:事件:瞬间产生的数据,数据被消费后立即销毁,不存储。异步:异步获取的数据;类似于事件,是瞬间数据,不存储。状态:随着时间空间变化的数据,始终会存储一个当前值/最新值。常量:固定不变的数据。RxJS天生就适合编写异步和基于事件的程序,那么状态数据用什么去管理呢?还是用RxJS吗? 合不合适呢?我们去调研和学习了前端社区已有的优秀的状态管理解决方案,也从一些大牛分享的关于用RxJS设计数据层的构想和实践中得到了启发:使用RxJS完全可以实现诸如Redux,Mobx等管理状态数据的功能。应用的数据不是只有状态的,还有事件、异步、常量等等。如果整个应用都由observable来表达,则可以借助RxJS基于序列且可响应的的特性,以流的方式自由地拼接和组合各种类型的数据,能够更优雅更高效地抽象出可复用可扩展的业务模型。出于以上两点原因,最终决定基于RxJS来设计一套管理应用的状态的解决方案。原理介绍对于状态的定义,通常认为状态需要满足以下3个条件:是一个具有多个值的集合。能够通过event或者action对值进行转换,从而得到新的值。有“当前值”的概念,对外一般只暴露当前值,即最新值。那么,RxJS适合用来管理状态数据吗?答案是肯定的!首先,因为Observable本身就是多个值的推送集合,所以第一个条件是满足的!其次,我们可以实现一个使用dispatch action模式来推送数据的observable来满足第二个条件!众所周知,RxJS中的observable可以分为两种类型:cold observable: 推送值的生产者(producer)来自observable内部。将会推送几个值以及推送什么样的值已在observable创建时被定义下来,不可改变。producer与观察者(observer) 是一对一的关系,即是单播的。每当有observer订阅时,producer都会把预先定义好的若干个值依次推送给observer。hot observable: 推送值的producer来自observable外部。将会推送几个值、推送什么样的值以及何时推送在创建时都是未知的。producer与observer是一对多的关系,即是多播的。每当有observer订阅时,会将observer注册到观察者列表中,类似于其他库或语言中的addListener的工作方式。当外部的producer被触发或执行时,会将值同时推送给所有的observer;也就是说,所有的observer共享了hot observable推送的值。RxJS提供的BehaviorSubject就是一种特殊的hot observable,它向外暴露了推送数据的接口next函数;并且有“当前值”的概念,它保存了发送给observer的最新值,当有新的观察者订阅时,会立即从BehaviorSubject那接收到“当前值”。那么这说明使用BehaviorSubject来更新状态并保存状态的当前值是可行的,第三个条件也满足了。简单实现请看以下的代码:import { BehaviorSubject } from ‘rxjs’;// 数据推送的生产者class StateMachine { constructor(subject, value) { this.subject = subject; this.value = value; } producer(action) { let oldValue = this.value; let newValue; switch (action.type) { case ‘plus’: newValue = ++oldValue; this.value = newValue; this.subject.next(newValue); break; case ’toDouble’: newValue = oldValue * 2; this.value = newValue; this.subject.next(newValue); break; } }}const value = 1; // 状态的初始值const count$ = new BehaviorSubject(value);const stateMachine = new StateMachine(count$, value);// 派遣actionfunction dispatch(action) { stateMachine.producer(action);}count$.subscribe(val => { console.log(val);});setTimeout(() => { dispatch({ type: “plus” });}, 1000);setTimeout(() => { dispatch({ type: “toDouble” });}, 2000);执行代码控制台会打印出三个值:Console 1 2 4上面的代码简单实现了一个简单管理状态的例子:状态的初始值: 1执行plus之后的状态值: 2执行toDouble之后的状态值: 4实现方法挺简单的,就是使用BehaviorSubject来表达状态的当前值:第一步,通过调用dispatch函数使producer函数执行第二部,producer函数在内部调用了BehaviorSubject的next函数,推送了新数据,BehaviorSubject的当前值更新了,也就是状态更新了。不过写起来略微繁琐,我们对其进行了封装,优化后写法见下文。使用操作符来创建状态数据我们自定义了一个操作符state用来创建一个能够通过dispatch action模式推送新数据的BehaviorSubject,我们称她为stateObservable。const count$ = state({ // 状态的唯一标识名称 name: “count”, // 状态的默认值 defaultValue: 1, // 数据推送的生产者函数 producer(next, value, action) { switch (action.type) { case “plus”: next(value + 1); break; case “toDouble”: next(value * 2); break; } }});更新状态在你想要的任意位置使用函数dispatch派遣action即可更新状态!dispatch(“count”, { type: “plus”})异步数据RxJS的一大优势就在于能够统一同步和异步,使用observable处理数据你不需要关注同步还是异步。下面的例子我们使用操作符from将promise转换为observable。指定observable作为状态的初始值(首次推送数据)const todos$ = state({ name: “todos”, // observable推送的数据将作为状态的初始值 initial: from(getAsyncData()) //… });producer推送observableconst todos$ = state({ name: “todos”, defaultValue: [] // 数据推送的生产者函数 producer(next, value, action) { switch (action.type) { case “getAsyncData”: next( from(getAsyncData()) ); break; } }});执行getAsyncData之后,from(getAsyncData())的推送数据将成为状态的最新值。衍生状态由于状态todos$是一个observable,所以可以很自然地使用RxJS操作符转换得到另一个新的observable。并且这个observable的推送来自todos$;也就是说只要todos$推送新数据,它也会推送;效果类似于Vue的计算属性。// 未完成任务数量const undoneCount$ = todos$.pipe( map(todos => { let _conut = 0; todos.forEach(item => { if (!item.check) ++_conut; }); return _conut; }));React视图渲染我们可能会在组件的生命周期内订阅observable得到数据渲染视图。class Todos extends React.Component { componentWillMount() { todos$.subscribe(data => { this.setState({ todos: data }); }); }}我们可以再优化下,利用高阶组件封装一个装饰器函数@subscription,顾名思义,就是为React组件订阅observable以响应推送数据的变化;它会将observable推送的数据转换为React组件的props。@subscription({ todos: todos$})class TodoList extends React.Component { render() { return ( <div className=“todolist”> <h1 className=“header”>任务列表</h1> {this.props.todos.map((item, n) => { return <TodoItem item={item} key={item.desc} />; })} </div> ); }}总结使用RxJS越久,越令人受益匪浅。因为它基于observable序列提供了较高层次的抽象,并且是观察者模式,可以尽可能地减少各组件各模块之间的耦合度,大大减轻了定位BUG和重构的负担。因为是基于observable序列来编写代码的,所以遇到复杂的业务场景,总能按照一定的顺序使用observable描述出来,代码的可读性很强。并且当需求变动时,我可能只需要调整下observable的顺序,或者加个操作符就行了。再也不必因为一个复杂的业务流程改动了,需要去改好几个地方的代码(而且还容易改出BUG,笑~)。所以,以上基于RxJS的状态管理方案,对我们来说是一个必需品,因为我们项目中大量使用了RxJS,如果状态数据也是observable,对我们抽象可复用可扩展的业务模型是一个非常大的助力。当然了,如果你的项目中没有使用RxJS,也许Redux和Mobx是更合适的选择。这套基于RxJS的状态管理方案,我们已经用于开发公司的商用项目,反馈还不错。所以我们决定把这套方案整理成一个js lib,取名为:Floway,并在github上开源:github源码:https://github.com/shayeLee/floway使用文档:https://shayelee.github.io/floway欢迎大家star,更欢迎大家来共同交流和分享RxJS的使用心得!参考文章:复杂单页应用的数据层设计DaoCloud 基于 RxJS 的前端数据层实践 ...

April 2, 2019 · 2 min · jiezi

Vuex、Flux、Redux、Redux-saga、Dva、MobX

这篇文章试着聊明白这一堆看起来挺复杂的东西。在聊之前,大家要始终记得一句话:一切前端概念,都是纸老虎。不管是Vue,还是 React,都需要管理状态(state),比如组件之间都有共享状态的需要。什么是共享状态?比如一个组件需要使用另一个组件的状态,或者一个组件需要改变另一个组件的状态,都是共享状态。父子组件之间,兄弟组件之间共享状态,往往需要写很多没有必要的代码,比如把状态提升到父组件里,或者给兄弟组件写一个父组件,听听就觉得挺啰嗦。如果不对状态进行有效的管理,状态在什么时候,由于什么原因,如何变化就会不受控制,就很难跟踪和测试了。如果没有经历过这方面的困扰,可以简单理解为会搞得很乱就对了。在软件开发里,有些通用的思想,比如隔离变化,约定优于配置等,隔离变化就是说做好抽象,把一些容易变化的地方找到共性,隔离出来,不要去影响其他的代码。约定优于配置就是很多东西我们不一定要写一大堆的配置,比如我们几个人约定,view 文件夹里只能放视图,不能放过滤器,过滤器必须放到 filter 文件夹里,那这就是一种约定,约定好之后,我们就不用写一大堆配置文件了,我们要找所有的视图,直接从 view 文件夹里找就行。根据这些思想,对于状态管理的解决思路就是:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。根据这个思路,产生了很多的模式和库,我们来挨个聊聊。Store 模式最简单的处理就是把状态存到一个外部变量里面,比如:this.$root.$data,当然也可以是一个全局变量。但是这样有一个问题,就是数据改变后,不会留下变更过的记录,这样不利于调试。所以我们稍微搞得复杂一点,用一个简单的 Store 模式:var store = { state: { message: ‘Hello!’ }, setMessageAction (newValue) { // 发生改变记录点日志啥的 this.state.message = newValue }, clearMessageAction () { this.state.message = ’’ }}store 的 state 来存数据,store 里面有一堆的 action,这些 action 来控制 state 的改变,也就是不直接去对 state 做改变,而是通过 action 来改变,因为都走 action,我们就可以知道到底改变(mutation)是如何被触发的,出现错误,也可以记录记录日志啥的。不过这里没有限制组件里面不能修改 store 里面的 state,万一组件瞎胡修改,不通过 action,那我们也没法跟踪这些修改是怎么发生的。所以就需要规定一下,组件不允许直接修改属于 store 实例的 state,组件必须通过 action 来改变 state,也就是说,组件里面应该执行 action 来分发 (dispatch) 事件通知 store 去改变。这样约定的好处是,我们能够记录所有 store 中发生的 state 改变,同时实现能做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具。这样进化了一下,一个简单的 Flux 架构就实现了。FluxFlux其实是一种思想,就像MVC,MVVM之类的,他给出了一些基本概念,所有的框架都可以根据他的思想来做一些实现。Flux把一个应用分成了4个部分:ViewActionDispatcherStore比如我们搞一个应用,显而易见,这个应用里面会有一堆的 View,这个 View 可以是Vue的,也可以是 React的,啥框架都行,啥技术都行。View 肯定是要展示数据的,所谓的数据,就是 Store,Store 很容易明白,就是存数据的地方。当然我们可以把 Store 都放到一起,也可以分开来放,所以就有一堆的 Store。但是这些 View 都有一个特点,就是 Store 变了得跟着变。View 怎么跟着变呢?一般 Store 一旦发生改变,都会往外面发送一个事件,比如 change,通知所有的订阅者。View 通过订阅也好,监听也好,不同的框架有不同的技术,反正 Store 变了,View 就会变。View 不是光用来看的,一般都会有用户操作,用户点个按钮,改个表单啥的,就需要修改 Store。Flux 要求,View 要想修改 Store,必须经过一套流程,有点像我们刚才 Store 模式里面说的那样。视图先要告诉 Dispatcher,让 Dispatcher dispatch 一个 action,Dispatcher 就像是个中转站,收到 View 发出的 action,然后转发给 Store。比如新建一个用户,View 会发出一个叫 addUser 的 action 通过 Dispatcher 来转发,Dispatcher 会把 addUser 这个 action 发给所有的 store,store 就会触发 addUser 这个 action,来更新数据。数据一更新,那么 View 也就跟着更新了。这个过程有几个需要注意的点:Dispatcher 的作用是接收所有的 Action,然后发给所有的 Store。这里的 Action 可能是 View 触发的,也有可能是其他地方触发的,比如测试用例。转发的话也不是转发给某个 Store,而是所有 Store。Store 的改变只能通过 Action,不能通过其他方式。也就是说 Store 不应该有公开的 Setter,所有 Setter 都应该是私有的,只能有公开的 Getter。具体 Action 的处理逻辑一般放在 Store 里。听听描述看看图,可以发现,Flux的最大特点就是数据都是单向流动的。ReduxFlux 有一些缺点(特点),比如一个应用可以拥有多个 Store,多个Store之间可能有依赖关系;Store 封装了数据还有处理数据的逻辑。所以大家在使用的时候,一般会用 Redux,他和 Flux 思想比较类似,也有差别。StoreRedux 里面只有一个 Store,整个应用的数据都在这个大 Store 里面。Store 的 State 不能直接修改,每次只能返回一个新的 State。Redux 整了一个 createStore 函数来生成 Store。import { createStore } from ‘redux’;const store = createStore(fn);Store 允许使用 store.subscribe 方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。这样不管 View 是用什么实现的,只要把 View 的更新函数 subscribe 一下,就可以实现 State 变化之后,View 自动渲染了。比如在 React 里,把组件的render方法或setState方法订阅进去就行。Action和 Flux 一样,Redux 里面也有 Action,Action 就是 View 发出的通知,告诉 Store State 要改变。Action 必须有一个 type 属性,代表 Action 的名称,其他可以设置一堆属性,作为参数供 State 变更时参考。const action = { type: ‘ADD_TODO’, payload: ‘Learn Redux’};Redux 可以用 Action Creator 批量来生成一些 Action。ReducerRedux 没有 Dispatcher 的概念,Store 里面已经集成了 dispatch 方法。store.dispatch()是 View 发出 Action 的唯一方法。import { createStore } from ‘redux’;const store = createStore(fn);store.dispatch({ type: ‘ADD_TODO’, payload: ‘Learn Redux’});Redux 用一个叫做 Reducer 的纯函数来处理事件。Store 收到 Action 以后,必须给出一个新的 State(就是刚才说的Store 的 State 不能直接修改,每次只能返回一个新的 State),这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。什么是纯函数呢,就是说没有任何的副作用,比如这样一个函数:function getAge(user) { user.age = user.age + 1; return user.age;}这个函数就有副作用,每一次相同的输入,都可能导致不同的输出,而且还会影响输入 user 的值,再比如:let b = 10;function compare(a) { return a >= b;}这个函数也有副作用,就是依赖外部的环境,b 在别处被改变了,返回值对于相同的 a 就有可能不一样。而 Reducer 是一个纯函数,对于相同的输入,永远都只会有相同的输出,不会影响外部的变量,也不会被外部变量影响,不得改写参数。它的作用大概就是这样,根据应用的状态和当前的 action 推导出新的 state:(previousState, action) => newState类比 Flux,Flux 有些像: (state, action) => state为什么叫做 Reducer 呢?reduce 是一个函数式编程的概念,经常和 map 放在一起说,简单来说,map 就是映射,reduce 就是归纳。映射就是把一个列表按照一定规则映射成另一个列表,而 reduce 是把一个列表通过一定规则进行合并,也可以理解为对初始值进行一系列的操作,返回一个新的值。比如 Array 就有一个方法叫 reduce,Array.prototype.reduce(reducer, ?initialValue),把 Array 整吧整吧弄成一个 newValue。const array1 = [1, 2, 3, 4];const reducer = (accumulator, currentValue) => accumulator + currentValue;// 1 + 2 + 3 + 4console.log(array1.reduce(reducer));// expected output: 10// 5 + 1 + 2 + 3 + 4console.log(array1.reduce(reducer, 5));// expected output: 15看起来和 Redux 的 Reducer 是不是好像好像,Redux 的 Reducer 就是 reduce 一个列表(action的列表)和一个 initialValue(初始的 State)到一个新的 value(新的 State)。把上面的概念连起来,举个例子:下面的代码声明了 reducer:const defaultState = 0;const reducer = (state = defaultState, action) => { switch (action.type) { case ‘ADD’: return state + action.payload; default: return state; }};createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。import { createStore } from ‘redux’;const store = createStore(reducer);createStore 内部干了什么事儿呢?通过一个简单的 createStore 的实现,可以了解大概的原理(可以略过不看):const createStore = (reducer) => { let state; let listeners = []; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); }; const subscribe = (listener) => { listeners.push(listener); return () => { listeners = listeners.filter(l => l !== listener); } }; dispatch({}); return { getState, dispatch, subscribe };};Redux 有很多的 Reducer,对于大型应用来说,State 必然十分庞大,导致 Reducer 函数也十分庞大,所以需要做拆分。Redux 里每一个 Reducer 负责维护 State 树里面的一部分数据,多个 Reducer 可以通过 combineReducers 方法合成一个根 Reducer,这个根 Reducer 负责维护整个 State。import { combineReducers } from ‘redux’;// 注意这种简写形式,State 的属性名必须与子 Reducer 同名const chatReducer = combineReducers({ Reducer1, Reducer2, Reducer3})combineReducers 干了什么事儿呢?通过简单的 combineReducers 的实现,可以了解大概的原理(可以略过不看):const combineReducers = reducers => { return (state = {}, action) => { return Object.keys(reducers).reduce( (nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {} ); };};流程再回顾一下刚才的流程图,尝试走一遍 Redux 流程:1、用户通过 View 发出 Action:store.dispatch(action);2、然后 Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。let nextState = xxxReducer(previousState, action);3、State 一旦有变化,Store 就会调用监听函数。store.subscribe(listener);4、listener可以通过 store.getState() 得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。function listerner() { let newState = store.getState(); component.setState(newState); }对比 Flux和 Flux 比较一下:Flux 中 Store 是各自为战的,每个 Store 只对对应的 View 负责,每次更新都只通知对应的View:Redux 中各子 Reducer 都是由根 Reducer 统一管理的,每个子 Reducer 的变化都要经过根 Reducer 的整合:简单来说,Redux有三大原则:单一数据源:Flux 的数据源可以是多个。State 是只读的:Flux 的 State 可以随便改。使用纯函数来执行修改:Flux 执行修改的不一定是纯函数。Redux 和 Flux 一样都是单向数据流。中间件刚才说到的都是比较理想的同步状态。在实际项目中,一般都会有同步和异步操作,所以 Flux、Redux 之类的思想,最终都要落地到同步异步的处理中来。在 Redux 中,同步的表现就是:Action 发出以后,Reducer 立即算出 State。那么异步的表现就是:Action 发出以后,过一段时间再执行 Reducer。那怎么才能 Reducer 在异步操作结束后自动执行呢?Redux 引入了中间件 Middleware 的概念。其实我们重新回顾一下刚才的流程,可以发现每一个步骤都很纯粹,都不太适合加入异步的操作,比如 Reducer,纯函数,肯定不能承担异步操作,那样会被外部IO干扰。Action呢,就是一个纯对象,放不了操作。那想来想去,只能在 View 里发送 Action 的时候,加上一些异步操作了。比如下面的代码,给原来的 dispatch 方法包裹了一层,加上了一些日志打印的功能:let next = store.dispatch;store.dispatch = function dispatchAndLog(action) { console.log(‘dispatching’, action); next(action); console.log(’next state’, store.getState());}既然能加日志打印,当然也能加入异步操作。所以中间件简单来说,就是对 store.dispatch 方法进行一些改造的函数。不展开说了,所以如果想详细了解中间件,可以点这里。Redux 提供了一个 applyMiddleware 方法来应用中间件:const store = createStore( reducer, applyMiddleware(thunk, promise, logger));这个方法主要就是把所有的中间件组成一个数组,依次执行。也就是说,任何被发送到 store 的 action 现在都会经过thunk,promise,logger 这几个中间件了。处理异步对于异步操作来说,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻(可能成功,也可能失败或者超时),这两个时刻都可能会更改应用的 state。一般是这样一个过程:请求开始时,dispatch 一个请求开始 Action,触发 State 更新为“正在请求”状态,View 重新渲染,比如展现个Loading啥的。请求结束后,如果成功,dispatch 一个请求成功 Action,隐藏掉 Loading,把新的数据更新到 State;如果失败,dispatch 一个请求失败 Action,隐藏掉 Loading,给个失败提示。显然,用 Redux 处理异步,可以自己写中间件来处理,当然大多数人会选择一些现成的支持异步处理的中间件。比如 redux-thunk 或 redux-promise 。Redux-thunkthunk 比较简单,没有做太多的封装,把大部分自主权交给了用户:const createFetchDataAction = function(id) { return function(dispatch, getState) { // 开始请求,dispatch 一个 FETCH_DATA_START action dispatch({ type: FETCH_DATA_START, payload: id }) api.fetchData(id) .then(response => { // 请求成功,dispatch 一个 FETCH_DATA_SUCCESS action dispatch({ type: FETCH_DATA_SUCCESS, payload: response }) }) .catch(error => { // 请求失败,dispatch 一个 FETCH_DATA_FAILED action dispatch({ type: FETCH_DATA_FAILED, payload: error }) }) }}//reducerconst reducer = function(oldState, action) { switch(action.type) { case FETCH_DATA_START : // 处理 loading 等 case FETCH_DATA_SUCCESS : // 更新 store 等 case FETCH_DATA_FAILED : // 提示异常 }}缺点就是用户要写的代码有点多,可以看到上面的代码比较啰嗦,一个请求就要搞这么一套东西。Redux-promiseredus-promise 和 redux-thunk 的思想类似,只不过做了一些简化,成功失败手动 dispatch 被封装成自动了:const FETCH_DATA = ‘FETCH_DATA’//action creatorconst getData = function(id) { return { type: FETCH_DATA, payload: api.fetchData(id) // 直接将 promise 作为 payload }}//reducerconst reducer = function(oldState, action) { switch(action.type) { case FETCH_DATA: if (action.status === ‘success’) { // 更新 store 等处理 } else { // 提示异常 } }}刚才的什么 then、catch 之类的被中间件自行处理了,代码简单不少,不过要处理 Loading 啥的,还需要写额外的代码。其实任何时候都是这样:封装少,自由度高,但是代码就会变复杂;封装多,代码变简单了,但是自由度就会变差。redux-thunk 和 redux-promise 刚好就是代表这两个面。redux-thunk 和 redux-promise 的具体使用就不介绍了,这里只聊一下大概的思路。大部分简单的异步业务场景,redux-thunk 或者 redux-promise 都可以满足了。上面说的 Flux 和 Redux,和具体的前端框架没有什么关系,只是思想和约定层面。下面就要和我们常用的 Vue 或 React 结合起来了:VuexVuex 主要用于 Vue,和 Flux,Redux 的思想很类似。Store每一个 Vuex 里面有一个全局的 Store,包含着应用中的状态 State,这个 State 只是需要在组件中共享的数据,不用放所有的 State,没必要。这个 State 是单一的,和 Redux 类似,所以,一个应用仅会包含一个 Store 实例。单一状态树的好处是能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。Vuex通过 store 选项,把 state 注入到了整个应用中,这样子组件能通过 this.&dollar;store 访问到 state 了。const app = new Vue({ el: ‘#app’, // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件 store, components: { Counter }, template: &lt;div class="app"&gt; &lt;counter&gt;&lt;/counter&gt; &lt;/div&gt; })const Counter = { template: &lt;div&gt;{{ count }}&lt;/div&gt;, computed: { count () { return this.$store.state.count } }}State 改变,View 就会跟着改变,这个改变利用的是 Vue 的响应式机制。Mutation显而易见,State 不能直接改,需要通过一个约定的方式,这个方式在 Vuex 里面叫做 mutation,更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 变更状态 state.count++ } }})触发 mutation 事件的方式不是直接调用,比如 increment(state) 是不行的,而要通过 store.commit 方法:store.commit(‘increment’)注意:mutation 都是同步事务。mutation 有些类似 Redux 的 Reducer,但是 Vuex 不要求每次都搞一个新的 State,可以直接修改 State,这块儿又和 Flux 有些类似。具尤大的说法,Redux 强制的 immutability,在保证了每一次状态变化都能追踪的情况下强制的 immutability 带来的收益很有限,为了同构而设计的 API 很繁琐,必须依赖第三方库才能相对高效率地获得状态树的局部状态,这些都是 Redux 不足的地方,所以也被 Vuex 舍掉了。到这里,其实可以感觉到 Flux、Redux、Vuex 三个的思想都差不多,在具体细节上有一些差异,总的来说都是让 View 通过某种方式触发 Store 的事件或方法,Store 的事件或方法对 State 进行修改或返回一个新的 State,State 改变之后,View 发生响应式改变。Action到这里又该处理异步这块儿了。mutation 是必须同步的,这个很好理解,和之前的 reducer 类似,不同步修改的话,会很难调试,不知道改变什么时候发生,也很难确定先后顺序,A、B两个 mutation,调用顺序可能是 A -> B,但是最终改变 State 的结果可能是 B -> A。对比Redux的中间件,Vuex 加入了 Action 这个东西来处理异步,Vuex的想法是把同步和异步拆分开,异步操作想咋搞咋搞,但是不要干扰了同步操作。View 通过 store.dispatch(‘increment’) 来触发某个 Action,Action 里面不管执行多少异步操作,完事之后都通过 store.commit(‘increment’) 来触发 mutation,一个 Action 里面可以触发多个 mutation。所以 Vuex 的Action 类似于一个灵活好用的中间件。Vuex 把同步和异步操作通过 mutation 和 Action 来分开处理,是一种方式。但不代表是唯一的方式,还有很多方式,比如就不用 Action,而是在应用内部调用异步请求,请求完毕直接 commit mutation,当然也可以。Vuex 还引入了 Getter,这个可有可无,只不过是方便计算属性的复用。Vuex 单一状态树并不影响模块化,把 State 拆了,最后组合在一起就行。Vuex 引入了 Module 的概念,每个 Module 有自己的 state、mutation、action、getter,其实就是把一个大的 Store 拆开。总的来看,Vuex 的方式比较清晰,适合 Vue 的思想,在实际开发中也比较方便。对比ReduxRedux:view——>actions——>reducer——>state变化——>view变化(同步异步一样)Vuex:view——>commit——>mutations——>state变化——>view变化(同步操作)view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)React-reduxRedux 和 Flux 类似,只是一种思想或者规范,它和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。但是因为 React 包含函数式的思想,也是单向数据流,和 Redux 很搭,所以一般都用 Redux 来进行状态管理。为了简单处理 Redux 和 React UI 的绑定,一般通过一个叫 react-redux 的库和 React 配合使用,这个是 react 官方出的(如果不用 react-redux,那么手动处理 Redux 和 UI 的绑定,需要写很多重复的代码,很容易出错,而且有很多 UI 渲染逻辑的优化不一定能处理好)。Redux将React组件分为容器型组件和展示型组件,容器型组件一般通过connect函数生成,它订阅了全局状态的变化,通过mapStateToProps函数,可以对全局状态进行过滤,而展示型组件不直接从global state获取数据,其数据来源于父组件。如果一个组件既需要UI呈现,又需要业务逻辑处理,那就得拆,拆成一个容器组件包着一个展示组件。因为 react-redux 只是 redux 和 react 结合的一种实现,除了刚才说的组件拆分,并没有什么新奇的东西,所以只拿一个简单TODO项目的部分代码来举例:入口文件 index.js,把 redux 的相关 store、reducer 通过 Provider 注册到 App 里面,这样子组件就可以拿到 store 了。import React from ‘react’import { render } from ‘react-dom’import { Provider } from ‘react-redux’import { createStore } from ‘redux’import rootReducer from ‘./reducers’import App from ‘./components/App’const store = createStore(rootReducer)render( <Provider store={store}> <App /> </Provider>, document.getElementById(‘root’))actions/index.js,创建 Action:let nextTodoId = 0export const addTodo = text => ({ type: ‘ADD_TODO’, id: nextTodoId++, text})export const setVisibilityFilter = filter => ({ type: ‘SET_VISIBILITY_FILTER’, filter})export const toggleTodo = id => ({ type: ‘TOGGLE_TODO’, id})export const VisibilityFilters = { SHOW_ALL: ‘SHOW_ALL’, SHOW_COMPLETED: ‘SHOW_COMPLETED’, SHOW_ACTIVE: ‘SHOW_ACTIVE’}reducers/todos.js,创建 Reducers:const todos = (state = [], action) => { switch (action.type) { case ‘ADD_TODO’: return [ …state, { id: action.id, text: action.text, completed: false } ] case ‘TOGGLE_TODO’: return state.map(todo => todo.id === action.id ? { …todo, completed: !todo.completed } : todo ) default: return state }}export default todosreducers/index.js,把所有的 Reducers 绑定到一起:import { combineReducers } from ‘redux’import todos from ‘./todos’import visibilityFilter from ‘./visibilityFilter’export default combineReducers({ todos, visibilityFilter, …})containers/VisibleTodoList.js,容器组件,connect 负责连接React组件和Redux Store:import { connect } from ‘react-redux’import { toggleTodo } from ‘../actions’import TodoList from ‘../components/TodoList’const getVisibleTodos = (todos, filter) => { switch (filter) { case ‘SHOW_COMPLETED’: return todos.filter(t => t.completed) case ‘SHOW_ACTIVE’: return todos.filter(t => !t.completed) case ‘SHOW_ALL’: default: return todos }}// mapStateToProps 函数指定如何把当前 Redux store state 映射到展示组件的 props 中const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter)})// mapDispatchToProps 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id))})export default connect( mapStateToProps, mapDispatchToProps)(TodoList)简单来说,react-redux 就是多了个 connect 方法连接容器组件和UI组件,这里的“连接”就是一种映射:mapStateToProps 把容器组件的 state 映射到UI组件的 propsmapDispatchToProps 把UI组件的事件映射到 dispatch 方法Redux-saga刚才介绍了两个Redux 处理异步的中间件 redux-thunk 和 redux-promise,当然 redux 的异步中间件还有很多,他们可以处理大部分场景,这些中间件的思想基本上都是把异步请求部分放在了 action creator 中,理解起来比较简单。redux-saga 采用了另外一种思路,它没有把异步操作放在 action creator 中,也没有去处理 reductor,而是把所有的异步操作看成“线程”,可以通过普通的action去触发它,当操作完成时也会触发action作为输出。saga 的意思本来就是一连串的事件。redux-saga 把异步获取数据这类的操作都叫做副作用(Side Effect),它的目标就是把这些副作用管理好,让他们执行更高效,测试更简单,在处理故障时更容易。在聊 redux-saga 之前,需要熟悉一些预备知识,那就是 ES6 的 Generator。如果从没接触过 Generator 的话,看着下面的代码,给你个1分钟傻瓜式速成,函数加个星号就是 Generator 函数了,Generator 就是个骂街生成器,Generator 函数里可以写一堆 yield 关键字,可以记成“丫的”,Generator 函数执行的时候,啥都不干,就等着调用 next 方法,按照顺序把标记为“丫的”的地方一个一个拎出来骂(遍历执行),骂到最后没有“丫的”标记了,就返回最后的return值,然后标记为 done: true,也就是骂完了(上面只是帮助初学者记忆,别喷~)。function* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ’ending’;}var hw = helloWorldGenerator();hw.next() // 先把 ‘hello’ 拎出来,done: false 代表还没骂完// { value: ‘hello’, done: false } next() 方法有固定的格式,value 是返回值,done 代表是否遍历结束hw.next() // 再把 ‘world’ 拎出来,done: false 代表还没骂完// { value: ‘world’, done: false }hw.next() // 没有 yield 了,就把最后的 return ’ending’ 拎出来,done: true 代表骂完了// { value: ’ending’, done: true }hw.next() // 没有 yield,也没有 return 了,真的骂完了,只能挤出来一个 undefined 了,done: true 代表骂完了// { value: undefined, done: true }这样搞有啥好处呢?我们发现 Generator 函数的很多代码可以被延缓执行,也就是具备了暂停和记忆的功能:遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值,等着下一次调用next方法时,再继续往下执行。用 Generator 来写异步代码,大概长这样:function* gen(){ var url = ‘https://api.github.com/users/github'; var jsonData = yield fetch(url); console.log(jsonData);}var g = gen();var result = g.next(); // 这里的result是 { value: fetch(‘https://api.github.com/users/github'), done: true }// fetch(url) 是一个 Promise,所以需要 then 来执行下一步result.value.then(function(data){ return data.json();}).then(function(data){ // 获取到 json data,然后作为参数调用 next,相当于把 data 传给了 jsonData,然后执行 console.log(jsonData); g.next(data);});再回到 redux-saga 来,可以把 saga 想象成开了一个以最快速度不断地调用 next 方法并尝试获取所有 yield 表达式值的线程。举个例子:// saga.jsimport { take, put } from ‘redux-saga/effects’function* mySaga(){ // 阻塞: take方法就是等待 USER_INTERACTED_WITH_UI_ACTION 这个 action 执行 yield take(USER_INTERACTED_WITH_UI_ACTION); // 阻塞: put方法将同步发起一个 action yield put(SHOW_LOADING_ACTION, {isLoading: true}); // 阻塞: 将等待 FetchFn 结束,等待返回的 Promise const data = yield call(FetchFn, ‘https://my.server.com/getdata'); // 阻塞: 将同步发起 action (使用刚才返回的 Promise.then) yield put(SHOW_DATA_ACTION, {data: data});}这里用了好几个yield,简单理解,也就是每个 yield 都发起了阻塞,saga 会等待执行结果返回,再执行下一指令。也就是相当于take、put、call、put 这几个方法的调用变成了同步的,上面的全部完成返回了,才会执行下面的,类似于 await。用了 saga,我们就可以很细粒度的控制各个副作用每一部的操作,可以把异步操作和同步发起 action 一起,随便的排列组合。saga 还提供 takeEvery、takeLatest 之类的辅助函数,来控制是否允许多个异步请求同时执行,尤其是 takeLatest,方便处理由于网络延迟造成的多次请求数据冲突或混乱的问题。saga 看起来很复杂,主要原因可能是因为大家不熟悉 Generator 的语法,还有需要学习一堆新增的 API 。如果抛开这些记忆的东西,改造一下,再来看一下代码:function mySaga(){ if (action.type === ‘USER_INTERACTED_WITH_UI_ACTION’) { store.dispatch({ type: ‘SHOW_LOADING_ACTION’, isLoading: true}); const data = await Fetch(‘https://my.server.com/getdata'); store.dispatch({ type: ‘SHOW_DATA_ACTION’, data: data}); }}上面的代码就很清晰了吧,全部都是同步的写法,无比顺畅,当然直接这样写是不支持的,所以那些 Generator 语法和API,无非就是做一些适配而已。saga 还能很方便的并行执行异步任务,或者让两个异步任务竞争:// 并行执行,并等待所有的结果,类似 Promise.all 的行为const [users, repos] = yield [ call(fetch, ‘/users’), call(fetch, ‘/repos’)]// 并行执行,哪个先完成返回哪个,剩下的就取消掉了const {posts, timeout} = yield race({ posts: call(fetchApi, ‘/posts’), timeout: call(delay, 1000)})saga 的每一步都可以做一些断言(assert)之类的,所以非常方便测试。而且很容易测试到不同的分支。这里不讨论更多 saga 的细节,大家了解 saga 的思想就行,细节请看文档。对比 Redux-thunk比较一下 redux-thunk 和 redux-saga 的代码:和 redux-thunk 等其他异步中间件对比来说,redux-saga 主要有下面几个特点:异步数据获取的相关业务逻辑放在了单独的 saga.js 中,不再是掺杂在 action.js 或 component.js 中。dispatch 的参数是标准的 action,没有魔法。saga 代码采用类似同步的方式书写,代码变得更易读。代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理。很容易测试,如果是 thunk 的 Promise,测试的话就需要不停的 mock 不同的数据。其实 redux-saga 是用一些学习的复杂度,换来了代码的高可维护性,还是很值得在项目中使用的。DvaDva是什么呢?官方的定义是:dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。简单理解,就是让使用 react-redux 和 redux-saga 编写的代码组织起来更合理,维护起来更方便。之前我们聊了 redux、react-redux、redux-saga 之类的概念,大家肯定觉得头昏脑涨的,什么 action、reducer、saga 之类的,写一个功能要在这些js文件里面不停的切换。dva 做的事情很简单,就是让这些东西可以写到一起,不用分开来写了。比如:app.model({ // namespace - 对应 reducer 在 combine 到 rootReducer 时的 key 值 namespace: ‘products’, // state - 对应 reducer 的 initialState state: { list: [], loading: false, }, // subscription - 在 dom ready 后执行 subscriptions: [ function(dispatch) { dispatch({type: ‘products/query’}); }, ], // effects - 对应 saga,并简化了使用 effects: { [‘products/query’]: function*() { yield call(delay(800)); yield put({ type: ‘products/query/success’, payload: [‘ant-tool’, ‘roof’], }); }, }, // reducers - 就是传统的 reducers reducers: { ‘products/query’ { return { …state, loading: true, }; }, [‘products/query/success’](state, { payload }) { return { …state, loading: false, list: payload }; }, },});以前书写的方式是创建 sagas/products.js, reducers/products.js 和 actions/products.js,然后把 saga、action、reducer 啥的分开来写,来回切换,现在写在一起就方便多了。比如传统的 TODO 应用,用 redux + redux-saga 来表示结构,就是这样:saga 拦截 add 这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer 发一个 addTodoSuccess 的 action, 提示创建成功, 反之则发送 addTodoFail 的 action 即可。如果使用 Dva,那么结构图如下:整个结构变化不大,最主要的就是把 store 及 saga 统一为一个 model 的概念(有点类似 Vuex 的 Module),写在了一个 js 文件里。增加了一个 Subscriptions, 用于收集其他来源的 action,比如快捷键操作。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: ‘minus’ }); }, }, subscriptions: { keyboardWatcher({ dispatch }) { key(’⌘+up, ctrl+up’, () => { dispatch({type:‘add’}) }); }, },});之前我们说过约定优于配置的思想,Dva正式借鉴了这个思想。MobX前面扯了这么多,其实还都是 Flux 体系的,都是单向数据流方案。接下来要说的 MobX,就和他们不太一样了。我们先清空一下大脑,回到初心,什么是初心?就是我们最初要解决的问题是什么?最初我们其实为了解决应用状态管理的问题,不管是 Redux 还是 MobX,把状态管理好是前提。什么叫把状态管理好,简单来说就是:统一维护公共的应用状态,以统一并且可控的方式更新状态,状态更新后,View跟着更新。不管是什么思想,达成这个目标就ok。Flux 体系的状态管理方式,只是一个选项,但并不代表是唯一的选项。MobX 就是另一个选项。MobX背后的哲学很简单:任何源自应用状态的东西都应该自动地获得。译成人话就是状态只要一变,其他用到状态的地方就都跟着自动变。看这篇文章的人,大概率会对面向对象的思想比较熟悉,而对函数式编程的思想略陌生。Flux 或者说 Redux 的思想主要就是函数式编程(FP)的思想,所以学习起来会觉得累一些。而 MobX 更接近于面向对象编程,它把 state 包装成可观察的对象,这个对象会驱动各种改变。什么是可观察?就是 MobX 老大哥在看着 state 呢。state 只要一改变,所有用到它的地方就都跟着改变了。这样整个 View 可以被 state 来驱动。const obj = observable({ a: 1, b: 2})autoRun(() => { console.log(obj.a)})obj.b = 3 // 什么都没有发生obj.a = 2 // observe 函数的回调触发了,控制台输出:2上面的obj,他的 obj.a 属性被使用了,那么只要 obj.a 属性一变,所有使用的地方都会被调用。autoRun 就是这个老大哥,他看着所有依赖 obj.a 的地方,也就是收集所有对 obj.a 的依赖。当 obj.a 改变时,老大哥就会触发所有依赖去更新。MobX 允许有多个 store,而且这些 store 里的 state 可以直接修改,不用像 Redux 那样每次还返回个新的。这个有点像 Vuex,自由度更高,写的代码更少。不过它也会让代码不好维护。MobX 和 Flux、Redux 一样,都是和具体的前端框架无关的,也就是说可以用于 React(mobx-react) 或者 Vue(mobx-vue)。一般来说,用到 React 比较常见,很少用于 Vue,因为 Vuex 本身就类似 MobX,很灵活。如果我们把 MobX 用于 React 或者 Vue,可以看到很多 setState() 和 this.state.xxx = 这样的处理都可以省了。还是和上面一样,只介绍思想。具体 MobX 的使用,可以看这里。对比 Redux我们直观地上两坨实现计数器代码:Redux:import React, { Component } from ‘react’;import { createStore, bindActionCreators,} from ‘redux’;import { Provider, connect } from ‘react-redux’;// ①action typesconst COUNTER_ADD = ‘counter_add’;const COUNTER_DEC = ‘counter_dec’;const initialState = {a: 0};// ②reducersfunction reducers(state = initialState, action) { switch (action.type) { case COUNTER_ADD: return {…state, a: state.a+1}; case COUNTER_DEC: return {…state, a: state.a-1}; default: return state }}// ③action creatorconst incA = () => ({ type: COUNTER_ADD });const decA = () => ({ type: COUNTER_DEC });const Actions = {incA, decA};class Demo extends Component { render() { const { store, actions } = this.props; return ( <div> <p>a = {store.a}</p> <p> <button className=“ui-btn” onClick={actions.incA}>增加 a</button> <button className=“ui-btn” onClick={actions.decA}>减少 a</button> </p> </div> ); }}// ④将state、actions 映射到组件 propsconst mapStateToProps = state => ({store: state});const mapDispatchToProps = dispatch => ({ // ⑤bindActionCreators 简化 dispatch actions: bindActionCreators(Actions, dispatch)})// ⑥connect产生容器组件const Root = connect( mapStateToProps, mapDispatchToProps)(Demo)const store = createStore(reducers)export default class App extends Component { render() { return ( <Provider store={store}> <Root /> </Provider> ) }}MobX:import React, { Component } from ‘react’;import { observable, action } from ‘mobx’;import { Provider, observer, inject } from ‘mobx-react’;// 定义数据结构class Store { // ① 使用 observable decorator @observable a = 0;}// 定义对数据的操作class Actions { constructor({store}) { this.store = store; } // ② 使用 action decorator @action incA = () => { this.store.a++; } @action decA = () => { this.store.a–; }}// ③实例化单一数据源const store = new Store();// ④实例化 actions,并且和 store 进行关联const actions = new Actions({store});// inject 向业务组件注入 store,actions,和 Provider 配合使用// ⑤ 使用 inject decorator 和 observer decorator@inject(‘store’, ‘actions’)@observerclass Demo extends Component { render() { const { store, actions } = this.props; return ( <div> <p>a = {store.a}</p> <p> <button className=“ui-btn” onClick={actions.incA}>增加 a</button> <button className=“ui-btn” onClick={actions.decA}>减少 a</button> </p> </div> ); }}class App extends Component { render() { // ⑥使用Provider 在被 inject 的子组件里,可以通过 props.store props.actions 访问 return ( <Provider store={store} actions={actions}> <Demo /> </Provider> ) }}export default App;比较一下:Redux 数据流流动很自然,可以充分利用时间回溯的特征,增强业务的可预测性;MobX 没有那么自然的数据流动,也没有时间回溯的能力,但是 View 更新很精确,粒度控制很细。Redux 通过引入一些中间件来处理副作用;MobX 没有中间件,副作用的处理比较自由,比如依靠 autorunAsync 之类的方法。Redux 的样板代码更多,看起来就像是我们要做顿饭,需要先买个调料盒装调料,再买个架子放刀叉。。。做一大堆准备工作,然后才开始炒菜;而 MobX 基本没啥多余代码,直接硬来,拿着炊具调料就开干,搞出来为止。但其实 Redux 和 MobX 并没有孰优孰劣,Redux 比 Mobx 更多的样板代码,是因为特定的设计约束。如果项目比较小的话,使用 MobX 会比较灵活,但是大型项目,像 MobX 这样没有约束,没有最佳实践的方式,会造成代码很难维护,各有利弊。一般来说,小项目建议 MobX 就够了,大项目还是用 Redux 比较合适。总结时光荏苒,岁月如梭。每一个框架或者库只能陪你走一段路,最终都会逝去。留在你心中的,不是一条一条的语法规则,而是一个一个的思想,这些思想才是推动进步的源泉。帅哥美女,如果你都看到这里了,那么不点个赞,你的良心过得去么?参考链接https://cn.vuejs.org/v2/guide/state-management.htmlhttps://vuex.vuejs.org/https://cn.redux.js.org/docs/react-redux/http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.htmlhttp://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.htmlhttps://redux-saga-in-chinese.js.orghttps://juejin.im/post/59e6cd68f265da43163c2821https://react-redux.js.org/introduction/why-use-react-reduxhttps://segmentfault.com/a/1190000007248878http://es6.ruanyifeng.com/#docs/generatorhttps://juejin.im/post/5ac1cb9d6fb9a028cf32a046https://zhuanlan.zhihu.com/p/35437092https://github.com/dvajs/dva/issues/1https://cn.mobx.js.orghttps://zhuanlan.zhihu.com/p/25585910http://imweb.io/topic/59f4833db72024f03c7f49b4 ...

December 18, 2018 · 11 min · jiezi

Electron + Antd + Mobx 环境搭建

最近要重构一版桌面管理软件。业务需求与WEB版基本一致,但需要用到断点续传等本地功能。最经济的办法当然是移植到Electron环境中。之前的应用框架主要用到了以下React套餐:ReactReact-Router 路由Mobx 数据管理AntDesign 界面组件经过一天时间的摸索大致找到了门路,构建完成后写了一个脚手架,欢迎下载。下面回顾一下本次环境搭建的过程。安装AntDesign的脚手架关于Antd在CRA框架下的使用,官网有一份很详细的操作说明(包括babel-import等的设置),初学者可以跟着一步一步学习。如果你赶时间,也可以直接用它的脚手架。$ git clone https://github.com/ant-design/create-react-app-antd.git my-project$ cd my-project$ npm install && npm start跑起来以后,会自动弹出一个页面 localhost:3000,网页上显示的就是最基础的包括了antd的demo示例。安装Electron及配置关于electron的介绍,可以看官网文档,本文不涉及。完成antd脚手架安装后,在工程目录下安装electron。$ npm install electron –save-dev安装完成以后,在根目录下需要做一些设置,包括:electron启动脚本npm启动及编译命令render端使用electron模块的设置electron启动脚本在根目录下新建main.js,写入以下内容。(其实与官方例子是一样的)// Modules to control application life and create native browser windowconst {app, BrowserWindow} = require(’electron’)// Keep a global reference of the window object, if you don’t, the window will// be closed automatically when the JavaScript object is garbage collected.let mainWindowfunction createWindow () { // Create the browser window. mainWindow = new BrowserWindow({width: 800, height: 600}) // and load the index.html of the app. mainWindow.loadFile(‘build/index.html’) // Open the DevTools. // mainWindow.webContents.openDevTools() // Emitted when the window is closed. mainWindow.on(‘closed’, function () { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = null })}// This method will be called when Electron has finished// initialization and is ready to create browser windows.// Some APIs can only be used after this event occurs.app.on(‘ready’, createWindow)// Quit when all windows are closed.app.on(‘window-all-closed’, function () { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== ‘darwin’) { app.quit() }})app.on(‘activate’, function () { // On macOS it’s common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (mainWindow === null) { createWindow() }})npm启动及编译命令在工程根目录的package.json中加入以下内容:{ … “main”: “main.js”, “homepage”: “.”, “scripts”: { “electron-dev”: “electron . –env dev”, “electron-build”: “npm run build && electron . –env build”, “start”: “react-app-rewired start”, … }}我们希望在执行npm run electron-dev时,能打开electron应用并展示antd的网页内容,那么需要修改main.js: … // and load the index.html of the app. // mainWindow.loadFile(‘index.html’) // 这个是原内容,修改为以下内容 if (argv && argv[1] === ‘dev’) { // 如果npm run electron-dev,加载本地网页 mainWindow.loadURL(‘http://localhost:3000/’) } else if (argv && argv[1] === ‘build’) { mainWindow.loadURL(url.format({ pathname: path.join(__dirname, ‘./build/index.html’), protocol: ‘file:’, slashes: true })) }…配置到这里后,打开两个终端,分别cd到项目根目录下。$ npm start // 终端1$ npm run electron-dev // 终端2electron界面应该就会展示出来,界面中显示的就是antd的demo内容。render端使用electron模块经过以上设置,界面内容已经可以正常展示并可以实时刷新,但还缺少了一个重要功能:electron模块,用以支持本地化操作。配置的基本思路是,在electron启动过程中在window对象中设置webPreferences的preload属性[文档],将electron模块注册为global对象供renderer侧使用。… // Create the browser window. // mainWindow = new BrowserWindow({ width: 800, height: 600 }) mainWindow = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, fullscreenable: false, webPreferences: { javascript: true, plugins: true, nodeIntegration: false, // 不集成 Nodejs webSecurity: false, preload: path.join(__dirname, ‘./public/renderer.js’) // public目录下的renderer.js } })…设置成功后,在renderer侧即可引入electron的remote模块。// 新建renderer.js,public目录下const process = require(‘process’);process.once(’loaded’, () => { global.electron = require(’electron’)});使用mobx安装mobx及mobx-react$ npm install mobx mobx-react –save安装成功后,在App.js中使用:…import { observer } from ‘mobx-react’;…@observerclass App extends React.Component { …}执行开发命令后,会发现出现了一个编译错误。原因就在于CRA团队并不倾向于支持所有未正式发布的javascript语言特性,目前要使用装饰圈,均需要使用babel等进行转译。这个问题目前有两个方法:使用eject方式或react-app-rewire-mobx模块。使用eject方式将会暴露出所有的webpack配置,开发者如果熟悉可以进行自定义的配置。当然,这样做的话create-react-app的意义就大打折扣了。使用react-app-rewire-mobx则相对方便了许多,只需安装该模块,并在config-overrides.js中加入,即可正常使用。…const rewireMobX = require(‘react-app-rewire-mobx’);module.exports = function override(config, env) { … config = rewireMobX(config, env); return config;}注意按照上面的方式配置以后,仍然会报错。./src/index.js Error: The ‘decorators’ plugin requires a’decoratorsBeforeExport’ option, whose value must be a boolean. If youare migrating from Babylon/Babel 6 or want to use the old decoratorsproposal, you should use the ‘decorators-legacy’ plugin instead of’decorators’.原因在于,新版的babel7.0的描述换了一种方式。应该将上面的配置改为:module.exports = function override(config, env) { … config = injectBabelPlugin(["@babel/plugin-proposal-decorators", { legacy: true }], config); return config;}其实在react-app-rewire-mobx中也仅仅就是就是导入这个修饰符,只不过是旧版的写法。但就因为没有跟随babel7.0更新,所以导致了这个// react-app-rewire-mobx/index.jsconst {injectBabelPlugin} = require(‘react-app-rewired’);function rewireMobX(config, env) { return injectBabelPlugin(’transform-decorators-legacy’, config);}module.exports = rewireMobX; ...

December 13, 2018 · 3 min · jiezi

简洁的 React 状态管理库 - Stamen

说到 React 状态管理,必提的肯定是 Redux 与 MobX,2018 年快过去了,它们依然是最火热的状态管理工具,也有一些基于 Redux 的,如 dva、rematch 等,也有新的,如 mobx-state-tree,这里不对各个解决方案作评价。但还是想吐槽:什么 provider, connections, actions, reducers, effects, dispatch, put, call, payload, @observable, @computed, @observer, @inject…一堆模板代码、各种概念、什么哲学原则… 还有各种多如牛毛的 Api。我只是想早点码完页面下班,早点下班健身、陪妹子…所以,我想要这样的一个状态管理库:轻量 个人做移动端开发比较多简洁 没模板代码, 尽量少的 Api符合直觉 没复杂的概念, 给个 action 改 state 就好清晰 更易写出可维护和可读性好的代码高效 更高的开发效率,这很重要Typescript state 和 action 高度支持智能提示我是个实用主义者,开发效率、代码可维护性和可读性、开发体验大于各种什么范式、各种理论,也不需要装纯,重要的是可以快速处理业务,产生价值,早点下班打王者。有一天,我看到了 mobx 作者的 immer, 我感觉使用 immer, 可以实现一个我理想中的状态管理工具,所以就造了一个轮子,叫 stamen, 他有什么特点呢,Show you the code: stamen。如果有什么核心特点的话,那应该是 “简洁”,这里指的是使用者写代码时简洁,可以专注于业务,而不是自身源代码简洁,把问题留给使用者。CodeSandbox上的例子: Basic | Async用法比较简单:import React from ‘react’;import { render } from ‘react-dom’;import { createStore } from ‘stamen’;const { consume, mutate } = createStore({ count: 1 });const App = () => ( <div> <span>{consume(state => state.count)}</span> <button onClick={() => mutate(state => state.count–)}>-</button> <button onClick={() => mutate(state => state.count++)}>+</button> </div>);render(<App />, document.getElementById(‘root’));只有 state 和 action ,没有其它概念,只有一个 api:const { consume, mutate } = createStore({ count: 1 });Stamen 代码实现只有40行,对于大部分项目来说,这40行代码所包含的功能已然足够。更多用法可以看:Github: https://github.com/forsigner/…文档: http://forsigner.com/stamen-z… ...

October 1, 2018 · 1 min · jiezi

高效的Mobx模式 - (Part 1)

起因很早之前看到的一篇关于mobx的文章,之前记得是有人翻译过的,但是怎么找都找不到,故花了点时间通过自己那半桶水的英文水平,加上Google翻译一下,对于初学者,以及mobx的开发者提供些许帮助。这里针对已经了解mobx,且有过一些简单的开发的同学,其中对文章有一些删减,还有翻译的不对的地方欢迎大家指出。高效的Mobx模式 - (Part 1)MobX提供了一种简单而强大的方法来管理客户端状态。 它使用一种称为(TFRP-Transparent Functional Reactive Programming)的技术,其中如果任何相关值发生变化,它会自动计算派生值。 也就是通过设置跟踪值更改的依赖关系图来完成的。MobX导致思维方式的转变(For the better),并改变您的心理模型围绕管理客户端状态。Part 1 - 构建可观察者当我们使用Mobx时,建立客户端状态模型是第一步, 这是最有可能被客户端上呈现你的域模型的直接体现。实际上客户端状态也就是我们通常说的Store,了解redux的对此都很熟悉,虽然你只有一个Store,但是它是由多个子Store组件的,每一个子Store用来处理应用程序的各种功能。最简单的入门方法是注释Store的属性,这些属性将随着@observable而不断变化。 请注意,我使用的是装饰器语法,但使用简单的ES5函数包装器可以实现相同的功能。import { observable } from ‘mobx’class MyStore { @observable name @observable description @observable author @observable photos = [] }修剪可观察属性通过将对象标记为@observable,您将自动观察其所有嵌套属性。 现在这可能是你想要的东西,但很多时候它更能限制可观察性。 你可以使用一些MobX修饰符来做到这一点:asReference当某些属性永远不会改变值时,这是非常有用的。 请注意,如果您确实更改了引用本身,它将触发更改。let address = new Address();let contact = observable({ person: new Person(), address: asReference(address)});address.city = ‘New York’; // 不会触发通知任何// 将触发通知,因为这是属性引用更改contact.address = new Address();在上面的示例中,address属性将不可观察。 如果您更改地址详细信息,则不会收到通知。 但是,如果您更改地址引用本身,您将收到通知。一个有趣的消息是一个可观察对象的属性,其值具有原型(类实例)将自动使用asReference()注释。 此外,这些属性不会被进一步递归。asFlat这比asReference略宽一些。 asFlat允许属性本身可观察,但不允许其任何子节点。 典型用法适用于您只想观察数组实例而不是其项目的数组。 请注意,对于数组,length属性仍然是可观察的,因为它在数组实例上。 但是,对子属性的任何更改都不会被观察到。首先创建@observable所有内容,然后应用asReference和asFlat修饰符来修剪可观察属性。当你深入实现应用程序的各种功能时,你会发现这种修剪的好处。且当你开始时可能并不明显,这完全很正常。当你识别出不需要深度可观察性的属性时,请确保重新检查你的Store, 它可以对您的应用程序的性能产生积极影响。import {observable} from ‘mobx’;class AlbumStore { @observable name; // 这里不需要观察 @observable createdDate = asReference(new Date()); @observable description; @observable author; // 只观察照片数组,而不是单独的照片 @observable photos = asFlat([]); }扩展可观察属性和修剪可观察属性相反,你可以扩展对象上可观察性的范围/行为,而不是删除可观察性。 这里有三个可以控制它的修饰符:asStructure这会修改将新值分配给observable时完成相等性检查的方式。 默认情况下,仅将引用更改视为更改。 如果您希望基于内部结构进行比较,则可以使用此修饰符。 这主要是针对值类型(也称为结构),只有在它们的值匹配时才相等。如下图:const { asStructure, observable } = require(‘mobx’);let address1 = { zip: 12345, city: ‘New York’};let address2 = { zip: 12345, city: ‘New York’};let contact = { address: observable(address1)};// 将被视为一种变化,因为它是一个新的引用contact.address = address2;// 使用 asStructure() 修饰let contact2 = { address: observable(asStructure(address1)) };// 不会被视为一种变化,因为它具有相同的价值contact.address = address2;asMap默认情况下,将对象标记为可观察对象时,它只能跟踪最初在对象上定义的属性。 如果添加新属性,则不会跟踪这些属性。 使用asMap,您甚至可以使新添加的属性可观察。 在内部,MobX将创建一个类似ES6的Map,它具有与原生Map类似的API。除了使用此修饰符,您还可以通过从常规可观察对象开始来实现类似的效果。 然后,您可以使用extendObservable()API添加更多可观察的属性。 当您想要延迟添加可观察属性时,此API非常有用。computed这是一个如此强大的概念,其重要性无法得到足够的重视。 计算属性不是域的真实属性,而是使用实际属性派生(也称为计算)。 一个典型的例子是person实例的fullName属性。 它派生自firstName和lastName属性。 通过创建简单的计算属性,您可以简化域逻辑。 例如,您可以只创建一个计算的hasLastName属性,而不是检查一个人是否在任何地方都有lastNameclass Person { @observable firstName; @observable lastName; @computed get fullName() { return ${this.firstName}, ${this.lastName}; } @computed get hasLastName() { return !!this.lastName; }}构建可观察树是使用MobX的一个重要方面,这使MobX开始跟踪您的store中有趣且值得改变的部分!Part 2 稍后发布 ...

September 5, 2018 · 1 min · jiezi