这篇文章主要是项目中用到的开发框架功能点上的一个总结,包括基本的操做流程和一些心得体会。javascript
15年初建立了适用于目前团队的gulp工做流,旨在以一个gulpfile来操做和执行全部文件结构。随着项目依赖滚雪球式的增加,拉取npm包成了配置中最麻烦而极容易出错的一项。为了解决配置过程当中遇到的种种问题,15年末草草实现了一个方案,用nw.js(基于Chromium和node.js的app执行工具)框架来编写了一个简单的桌面应用gulp-ui, 所作的操做是打包gulpfile和所依赖的全部node_modules在一块儿,而后简单粗暴的在app内部执行gulpfile。css
gulp-ui 作出来后再团队中使用了一段时间,以单个项目来执行的方式确实在常常多项目开发的使用环境中多有不便。因而在这个基础上,重写了整个代码结构,开发了如今的版本feWorkflow.html
feWorkflow 改用了electron作为底层,使用react, redux, immutable框架作ui开发,仍然基于运行gulpfile的方案,这样可使每一个使用本身团队的gulp工做流快速接入和自由调整。前端
功能:一键式开发/压缩java
less实时监听编译cssnode
css前缀自动补全react
格式化html,并自动替换src源码路径为tc_idc发布路径linux
压缩图片(png|jpg|gif|svg)webpack
压缩或格式化js,并自动替换src源码路径为tc_idc发布路径git
同步刷新浏览器browserSync
与 NW.js 类似,Electron 提供了一个能经过 JavaScript 和 HTML 建立桌面应用的平台,同时集成 Node 来授予网页访问底层系统的权限。
使用nw.js时遇到了不少问题,设置和api比较繁琐,因而改版过程用再开发便利性上的考虑转用了electron。
electron应用布署很是简单,存放应用程序的文件夹须要叫作 app
而且须要放在 Electron 的 资源文件夹下(在 macOS 中是指 Electron.app/Contents/Resources/
,在 Linux 和 Windows 中是指 resources/
) 就像这样:
macOS
electron/Electron.app/Contents/Resources/app/ ├── package.json ├── main.js └── index.html
在 Windows 和 Linux 中
electron/resources/app ├── package.json ├── main.js └── index.html
而后运行 Electron.app
(或者 Linux 中的 electron
,Windows 中的 electron.exe
), 接着 Electron 就会以你的应用程序的方式启动。
package.json
主要用来指定app的名称,版本,入口文件,依赖文件等。
{ "name" : "your-app", "version" : "0.1.0", "main" : "main.js" }
main.js
应该用于建立窗口和处理系统事件,官方也是推荐使用es6
来开发,典型的例子以下:
const electron = require('electron'); //引入app模块 const {app} = electron; // 引入窗口视图 const {BrowserWindow} = electron; //设置一个变量 let mainWindow; function createWindow() { //实例化一个新的窗口 mainWindow = new BrowserWindow({width: 800, height: 600}); //加载electron主页面 mainWindow.loadURL(`file://${__dirname}/index.html`); //打开chrome开发者工具 mainWindow.webContents.openDevTools(); //监听窗口关闭状态 mainWindow.on('closed', () => { mainWindow = null; }); } //当app初始化完毕,开始建立一个新窗口 app.on('ready', createWindow); //监听app窗口关闭状态 app.on('window-all-closed', () => { //mac osx中只有执行command+Q才会退出app,不然保持活动状态 if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { //mac osx中再dock图标点击时从新建立一个窗口 if (mainWindow === null) { createWindow(); } });
index.html
则用来输出你的html:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Hello World!</title> </head> <body> <h1>Hello World!</h1> We are using node <script>document.write(process.versions.node)</script>, Chrome <script>document.write(process.versions.chrome)</script>, and Electron <script>document.write(process.versions.electron)</script>. </body> </html>
electron官方提供了一个快速开始的模板:
# Clone the Quick Start repository $ git clone https://github.com/electron/electron-quick-start # Go into the repository $ cd electron-quick-start # Install the dependencies and run $ npm install && npm start
更多入门介绍能够查看这里Electron快速入门.
由于项目中用到了react
以及redux
,为了方便开发,将chrome
的这两项插件引入到项目中。以redux
为例:
安装redux-devtools-extension的chrome扩展
;
地址栏输入chrome://extensions/
打开扩展程序面板,找到redux-devtools-extension
的ID, 这串ID是相似于lmhkpmbekcpmknklioeibfkpmmfibljd
的字符串;
找到系统chrome存储扩展的目录:
windows地址: %LOCALAPPDATA%GoogleChromeUser DataDefaultExtensions;
linux可能在:
~/.config/google-chrome/Default/Extensions/ ~/.config/google-chrome-beta/Default/Extensions/ ~/.config/google-chrome-canary/Default/Extensions/ ~/.config/chromium/Default/Extensions/
macOS目录地址:~/Library/Application Support/Google/Chrome/Default/Extensions
;
调用BrowserWindow.addDevToolsExtension
的API, 把这串地址传递进去,对于redux-devtools-extensions,大概会是~/Library/Application Support/Google/Chrome/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/2.2.1.1_0/'
。在electron表现为:
BrowserWindow.addDevToolsExtension('~/Library/Application Support/Google/Chrome/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/2.2.1.1_0/');
这样,在electron的控制台就能够看到对应的devtools标签了。
若是用的是react-devtools在这一步已经可使用,须要注意的是对于redux-devtools-extensions还有一个步骤,store
在createStore
时须要增长一行判断window.devToolsExtension && window.devToolsExtension()
:
const store = createStore(reducer, window.devToolsExtension && window.devToolsExtension());
React作为一个用来构建UI的JS库开辟了一个至关另类的途径,实现了前端界面的高效率高性能开发。React的虚拟DOM不只带来了简单的UI开发逻辑,同时也带来了组件化开发的思想。
ES6作为js的新规范带来了许多新的变化,从代码的编写上也带来了许多的便利性。
一个简单的react
模块示例:
//jsx var HelloMessage = React.createClass({ render: function() { return <div>Hello {this.props.name}</div>; } }); ReactDOM.render(<HelloMessage name="John" />, document.getElementById('root')));
//html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="root"></div> </body> </html>
//实际输出 <div id="root">Hello John</div>
经过React.createClass
建立一个react
模块,使用render
函数返回这个模块中的实际html模板,而后引用ReactDOM
的render
函数生成到指定的html模块中。调用HelloMessage
的方法,则是写成一个xhtml
的形式<HelloMessage name="John" />
,将name
里面的"John"作为一个属性值传到HelloMessage
中,经过this.props.name
来调用。
固然,这个是未经编译的jsx
文件,不能实际输出到html中,若是想要未经编译使用jsx
文件,能够在html
中引用babel
的组件,例如:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello React!</title> <script src="build/react.js"></script> <script src="build/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script> </head> <body> <div id="example"></div> <script type="text/babel"> ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('example') ); </script> </body> </html>
自从es6
正式发布后,react
也改用了babel
作为编译工具,也所以许多开发者开始将代码开发风格项es6
转变。
因而React.createClass
的方法被取代为es6中的扩展类写法:
class HelloWorld extends React.Component { render() { return <div>Hello {this.props.name}</div>; } }
咱们能够看到这些语法有了细微的不一样:
//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 }) => ( <div className="container"> <div className="container-bd"> {lists.size ? <ListFolder /> : <ContainerEmpty />} <Dropzone /> </div> <ContainerFt /> <Aside /> </div> ); 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 ? <ListFolder /> : <ContainerEmpty />} ... }, });
{ lists }
的写法编译成ES5的写法等同于:
var Container = function Container(_ref) { var lists = _ref.lists; ... }
至关于减小了很是多的赋值操做,极大了减小了开发的工做量。
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中引入便可。
流程图:
webpack部分设置:
var path = require('path'); var webpack = require('webpack'); module.exports = { devtool: 'source-map', entry: [ './src/index' ], output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js', publicPath: '/dist/' }, target: 'atom', module: { loaders: [ { test: /\.js$/, include: path.join(__dirname, 'src'), loader: require.resolve('babel-loader'), ... }, ... ] }, ... };
webpack须要设置入口文件entry
,在此是引入了源码文件夹src中的index.js
,和一个或多个出口文件output
,输出devtoolsource-map
使得源代码可见,而非编译后的代码,而后制定所须要的loader
来作模块的编译。
与electron
相关的一个比较重要的点是,必须指定target: atom
,不然会出现没法resolve electron modules的报错提示。
更多介绍能够参考Webpack 入门指迷
feWorkflow项目中选用了react-transform-hmr作为模板,已经写好了基础的webpack
文件,支持react
热加载,再也不须要常常去刷新electron,不过该做者已经中止维护这个项目,而是恢复维护react-hot-reload
,如今从新开发React Hot Loader 3, 有兴趣能够去了解一下。
Redux是针对JavaScript apps的一个可预见的state容器。它能够帮助咱们写一个行为保持一致性的应用,能够运行再不一样的环境中(client,server,和原生),并不是常容易测试。
Redux 能够用这三个基本原则来描述:
1. 单一数据源
整个应用的 state 被储存在一个 object tree 中,而且这个 object tree 只存在于惟一一个 store 中。
let store = createStore(counter) //建立一个redux store来保存你的app中全部state //当state更新时,可使用 subscribe()来绑定监听更新UI,一般状况下不会直接使用这个方法,而是会用view层绑定库(相似react-redux等)。 store.subscribe(() => console.log(store.getState()) //抛出全部数据 )
2. State是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
全部的修改都被集中化处理,且严格按照一个接一个的顺序执行. 而执行的方法是调用dispatch
。
store.dispatch({ type: 'COMPLETE_TODO', index: 1 });
3. 使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你须要编写 reducers
。
Reducer
只是一些纯函数,它接收先前的 state
和 action
,并返回新的 state
。
function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } }
redux流程图:
redux在react中应用还须要加载react-redux
模块,由于store
为单一state结构头,咱们仅须要在入口处调用react-redux的Provider
方法抛出store
render( <Provider store={store}> <Container /> </Provider>, 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 }) => ( <FloatingActionButton onClick={(event) => { 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 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}) => ( <div className="btn-group btn-group__right"> { btns.map((btn, i) => ( <RaisedButton key={i} className="btn" style={style} label={btn.get('name')} labelPosition="after" primary={btn.get('process')} secondary={btn.get('fail')} pid={btn.get('pid')} onClick={() => { 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('编译结束'); }); } }} /> )) } </div> );
—cwd
把gulp的操做路径指向了咱们定义的src路径,—gulpfile
则强行使用feWorkflow中封装的gulp file.js。我在js中对路径作了处理,以src
作为截断点,拼接命令行,假设拖放了一个位于D:Codeworkvdlotteryv3src
下的路径,那么输出的命令格式为:
//执行命令 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上,欢迎斧正。
参考资料: