feWorkflow - 使用 electron, react, redu, immutable 构建桌面 App
15年初创建了适用于目前团队的gulp工作流,旨在以一个gulpfile来操作和执行所有文件结构。随着项目依赖滚雪球式的增长,拉取npm包成了配置中最麻烦而极容易出错的一项。为了解决配置过程中遇到的种种问题,15年底草草实现了一个方案,用nw.js(基于Chromium和node.js的app执行工具)框架来编写了一个简单的桌面应用gulp-ui, 所做的操作是打包gulpfile和所依赖的所有node_modules在一起,然后简单粗暴的在app内部执行gulpfile。
gulp-ui 做出来后再团队中使用了一段时间,以单个项目来执行的方式确实在经常多项目开发的使用环境中多有不便。于是在这个基础上,重写了整个代码结构,开发了现在的版本feWorkflow.
feWorkflow 改用了electron做为底层,使用react, redux, immutable框架做ui开发,仍然基于运行gulpfile的方案,这样可以使每个使用自己团队的gulp工作流快速接入和自由调整。
, document.getElementById('root')));
//html
Document
//实际输出
Hello John
通过React.createClass创建一个react模块,使用render函数返回这个模块中的实际html模板,然后引用ReactDOM的render函数生成到指定的html模块中。调用HelloMessage的方法,则是写成一个xhtml的形式,将name里面的"John"做为一个属性值传到HelloMessage中,通过this.props.name来调用。
当然,这个是未经编译的jsx文件,不能实际输出到html中,如果想要未经编译使用jsx文件,可以在html中引用babel的组件,例如:
Hello React! ReactDOM.render( # Hello, world!, document.getElementById('example') );
自从es6正式发布后,react也改用了babel做为编译工具,也因此许多开发者开始将代码开发风格项es6转变。
于是React.createClass的方法被取代为es6中的扩展类写法:
class HelloWorld extends React.Component {
render() {
return Hello {this.props.name};
}
}
我们可以看到这些语法有了细微的不同:
//ES5的写法
var HelloWorld = React.createClass({
handleClick: function(e) {...},
render: function() {...},
});
//ES6及以上写法
class HelloWorld extends React.Component {
handleClick(e) {...}
render() {...}
}
在feWorkflow中基本都是使用ES6的写法做为开发, 例如最终输出的container模块:
import ListFolder from './list/list';
import Dropzone from './layout/dropzone';
import ContainerEmpty from './container-empty';
import ContainerFt from './layout/container-ft';
import Aside from './layout/aside';
import { connect } from 'react-redux';
const Container = ({ lists }) => (
{lists.size ? : }
);
const mapStateToProps = (states) => ({
lists: states.lists
});
export default connect(mapStateToProps)(Container);
import做为ES6的引入方式,来取代commonJS的require模式,等同于
var ListFoder = require('./list/list');
输出从module.export = Container; 替换成export default Container;,这种写法其实等同于:
// ES5写法
var Container = React.createClass({
render: function() {
...
{this.props.lists.size ? : }
...
},
});
{ lists }的写法编译成ES5的写法等同于:
var Container = function Container(_ref) {
var lists = _ref.lists;
...
}
相当于减少了非常多的赋值操作,极大了减少了开发的工作量。
Webpack
ES6中介绍了一下编译之后的代码,而每个文件里其实也并没有import必须的react模块,其实都是通过Webpack这个工具来执行了编译和打包。在webpack中引入了babel-loader来编译react和es6的代码,并将css通过less-loader, css-loader, style-loader自动编译到html的style标签中,再通过
new webpack.ProvidePlugin({
React: 'react'
}),
的形式,将react组件注册到每个js文件中,不需再重复引用,最后把所有的js模块编译打包输出到 dist/bundle.js,再html中引入即可。
流程图:
,
document.getElementById('root')
);
这样,在container的内部都能接收到store。
我们需要一个操作store的reducer. 当我们的reducer拆分好对应给不同的子组件之后,redux提供了一个combineReducers的方法,把所有的reducers合并起来:
import { combineReducers } from 'redux';
import lists from './list';
import snackbar from './snackbar';
import dropzone from './dropzone';
export default combineReducers({
lists,
snackbar,
dropzone,
});
然后通过createStore的方式链接store与reducer:
import { createStore } from 'redux';
import reducer from '../reducer/reducer';
export default createStore(reducer);
上文介绍redux的时候也说过,state是只读的,只能通过action来操作,同样我们也可以把dispatch映射成为一个props传入Container中。
在子模块中, 则把这个store映射成react的props,再用connect方法,把store和component链接起来:
import { connect } from 'react-redux'; //引入connect方法
import { addList } from '../../action/addList'; //从action中引入addList方法
const AddListBtn = ({ lists, addList }) => (
{
addList('do something here');
return false;
});
}}
;
);
const mapStateToProps = (states) => ({
//从state.lists获取数据存储到lists中,做为属性传递给AddListBtn
lists: states.lists
});
const mapDispatchToProps = (dispatch) => ({
//将addList函数做为属性传递给AddListBtn
addList: (name, location) => dispatch(addList(name, location));
});
//lists, addList做为属性链接到Conta
export default connect(mapStateToProps, mapDispatchToProps)(AddListBtn);
这样,就完成了redux与react的交互,很便捷的从上而下操作数据。
immutable.js
Immutable Data是指一旦被创造后,就不可以被改变的数据。
通过使用Immutable Data,可以让我们更容易的去处理缓存、回退、数据变化检测等问题,简化我们的开发。
所以当对象的内容没有发生变化时,或者有一个新的对象进来时,我们倾向于保持对象引用的不变。这个工作正是我们需要借助Facebook的 Immutable.js来完成的。
不变性意味着数据一旦创建就不能被改变,这使得应用开发更为简单,避免保护性拷贝(defensive copy),并且使得在简单的应用 逻辑中实现变化检查机制等。
var stateV1 = Immutable.fromJS({
users: [
{ name: 'Foo' },
{ name: 'Bar' }
]
});
var stateV2 = stateV1.updateIn(['users', 1], function () {
return Immutable.fromJS({
name: 'Barbar'
});
});
stateV1 === stateV2; // false
stateV1.getIn(['users', 0]) === stateV2.getIn(['users', 0]); // true
stateV1.getIn(['users', 1]) === stateV2.getIn(['users', 1]); // false
feWorkflow项目中使用最多的是List来创建一个数组,Map()来创建一个对象,再通过set的方法来更新数组,例如:
import { List, Map } from 'immutable';
export const syncFolder = List([
Map({
name: 'syncFromFolder',
label: '从目录复制',
location: ''
}),
Map({
name: 'syncToFolder',
label: '复制到目录',
location: ''
})
]);
更新的时候使用setIn方法,传递Map对象的序号,选中location这个属性,通过action传递过来的新值action.location更新值,并返回一个全新的数组。
case 'SET_SYNC_FOLDER':
return state.setIn(['syncFolder', action.index, 'location'], action.location);
数据存储
存:immutable的数据已经不是单纯的json数据格式,当我们要做json格式的数据存储的时候,可以使用toJS()方法抛出js对象,并通过JSON.stringnify()将js数据转换成json字符串,存入localstorage中。
export const saveState = (name = 'state', state = 'state') => {
try {
const data = JSON.stringify(state.toJS());
localStorage.setItem(name, data);
} catch(err) {
console.log('err', err);
}
}
取:读取本地的json格式数据后,当需要加载进页面,首先需要把这段json数据转换会immutable.js数据格式,immutable提供了fromJS()方法,将js对象和数组转换成immtable的Maps和Lists格式。
import { fromJS, Iterable } from 'immutable';
export const loadState = (name = 'setting') => {
try {
const data = localStorage.getItem(name);
if (data === null) { return undefined;}return fromJS(JSON.parse(data), (key, value) => { const isIndexed = Iterable.isIndexed(value); return isIndexed ? value.toList() : value.toMap();});
} catch(err) {
return undefined;
}
};
应用示例
上文介绍了整个feWorkflow的UI技术实现方案,现在来介绍下实际上gulp在这里是如何工作的。
思路
我们知道node中调用child_process的exec可以执行系统命令,gulpfile.js本身会调用离自身最近的node_modules,而gulp提供了API可以通过flag的形式(—cwd)来执行不同的路径。以此为思路,以最简单的方式,在按钮上绑定执行状态(dev或者build,包括flag等),通过exec直接运行gulp file.js.
实现
当按钮点击的时候,判断是否在执行中,如果在执行中则杀掉进程,如果不在执行中则通过exec执行当前按钮状态的命令。然后扭转按钮的状态,等待下一次按钮点击。
命令模式如下:
const ListBtns = ({btns, listId, listLocation, onProcess, cancelBuild, setSnackbar}) => (
{ btns.map((btn, i) => ( { if (btn.get('process')) { kill(btn.get('pid')); } else { let child = exec(`gulp ${btn.get('cmd')} --cwd ${listLocation} ${btn.get('flag')} --gulpfile ${cwd}/gulpfile.js`, { cwd }); child.stderr.on('data', function (data) { let str = data.toString(); console.error('exec error: ' + str); kill(btn.get('pid')); cancelBuild(listId, i, btn.get('name'), child.pid, str, true); dialog.showErrorBox('Oops, 出错了', str); }); child.stdout.on('data', function (data) { console.log(data.toString()) onProcess(listId, i, btn.get('text'), child.pid, data.toString()) }); //关闭 child.stdout.on('close', function () { cancelBuild(listId, i, btn.get('name'), child.pid, '编译结束', false); setSnackbar('编译结束'); console.info('编译结束'); }); } }} /> ))}
);
—cwd把gulp的操作路径指向了我们定义的src路径,—gulpfile则强行使用feWorkflow中封装的gulp file.js。我在js中对路径做了处理,以src做为截断点,拼接命令行,假设拖放了一个位于D:\Code\work\vd\lottery\v3\src下的路径,那么输出的命令格式为:
//执行命令
let child = exec(gulp ${btn.get('cmd')} --cwd ${listLocation} ${btn.get('flag')} --gulpfile ${cwd}/gulpfile.js
)
//编译输出命令:
gulp dev --cwd D:\Code\work\vd\lottery\v3\src --development
同时,通过action扭转了按钮状态:
export function processing(id, index, name, pid, data) {
return {
id,
type: 'PROCESSING',
btns: {
index,
name,
pid,
data,
process: true,
cmd: name
}
};
}
调用dispatch发送给reducer:
const initState = List([]);
export default (state = initState, action) => {
switch (action.type) {
...
case 'PROCESSING':
return state.map(item => {
if (item.get('id') == action.id) {
return item.withMutations(i => {
i
.set('status', action.btns.cmd)
.set('snackbar', action.snackbar)
.setIn(['btns', action.btns.index, 'text'], action.btns.name)
.setIn(['btns', action.btns.index, 'name'], '编译中...')
.setIn(['btns', action.btns.index, 'process'], action.btns.process)
.setIn(['btns', action.btns.index, 'pid'], action.btns.pid);
});
} else { return item; } }); ...
这样,就是整个文件执行的过程。
写在最后
这次的改版做了很多新的尝试,断断续续的花了不少时间,还没有达到最初的设想,也还缺失了一些重要的功能。后续还需要补充不少东西。成品确实还比较简单,代码也许也比较杂乱,所有代码开源在github上,欢迎斧正。
参考资料:
electron docs
babel react-on-es6-plus
webpack
redux
关键字:electron, react.js, Redux, gulp
版权声明
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!