SunXinFei / sunxinfei.github.io

前后端技术相关笔记,已迁移到 Issues 中
https://github.com/SunXinFei/sunxinfei.github.io/issues
32 stars 3 forks source link

Electron项目总结 #23

Open SunXinFei opened 4 years ago

SunXinFei commented 4 years ago

技术栈 electron + react + sqlite + immutable + redux

npm包功能总结

SunXinFei commented 4 years ago

Electron调试Redux

我们使用electron-devtools-installer 这个插件,

electron.app.on('ready', () => {

  installExtension(installExtension.VUEJS_DEVTOOLS)
    .then(() => {})
    .catch(err => {
      console.log('Unable to install `vue-devtools`: \n', err)
    })
    //添加Redux_DevTools
  installExtension(installExtension.REDUX_DEVTOOLS)
    .then(() => {})
    .catch(err => {
      console.log('Unable to install `redux-devtools`: \n', err)
    })
})

其中store的js文件中我们需要配置添加composeWithDevTools

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import FileManageStore from './test/reducer';
import { combineReducers } from 'redux-immutable';
import thunk from 'redux-thunk';
import Immutable from 'immutable';

let store = createStore(
    combineReducers({ FileManageStore }),
    Immutable.Map({}),
    composeWithDevTools(applyMiddleware(thunk))
);
export default store;

即可见Redux插件变化 image

参考:electron-devtools-installer

SunXinFei commented 4 years ago

将immutable.js引入到项目中的改造

将immutable.js引入到项目中的改造

Redux的本地持久化存储与获取

Redux的本地持久化存储与获取

SunXinFei commented 4 years ago

stat属性

stat {
    dev_t         st_dev;       //文件的设备编号
    ino_t         st_ino;       //节点
    mode_t        st_mode;      //文件的类型和存取的权限
    nlink_t       st_nlink;     //连到该文件的硬连接数目,刚建立的文件值为1
    uid_t         st_uid;       //用户ID
    gid_t         st_gid;       //组ID
    dev_t         st_rdev;      //(设备类型)若此文件为设备文件,则为其设备编号
    off_t         st_size;      //文件字节数(文件大小)
    unsigned long st_blksize;   //块大小(文件系统的I/O 缓冲区大小)
    unsigned long st_blocks;    //块数
    time_t        st_atime;     //最后一次访问时间
    time_t        st_mtime;     //最后一次修改时间
    time_t        st_ctime;     //最后一次改变时间(指属性)
};
SunXinFei commented 4 years ago

双击和单击事件绑定到同一个DOM上冲突

解决方案类似js截流,200ms之内如果双击则cleartimeOut

  let timer = 0;
  let delay = 200;
  let prevent = false;

  doClickAction() {
    console.log(' click');
  }
  doDoubleClickAction() {
    console.log('Double Click')
  }
  handleClick() {
    let me = this;
    timer = setTimeout(function() {
      if (!prevent) {
        me.doClickAction();
      }
      prevent = false;
    }, delay);
  }
  handleDoubleClick(){
    clearTimeout(timer);
    prevent = true;
    this.doDoubleClickAction();
  }
 < button onClick={this.handleClick.bind(this)} 
    onDoubleClick = {this.handleDoubleClick.bind(this)} > click me </button>

参考: onClick works but onDoubleClick is ignored on React component https://stackoverflow.com/questions/25777826/onclick-works-but-ondoubleclick-is-ignored-on-react-component

e.target和e.currentTarget区别

e.target : 触发事件的元素,谁触发就是谁,用来区分事件委托和冒泡 e.currentTarget :绑定事件的元素,不管是不是触发的对象中子元素,依旧是绑定的元素,用来直接获取父元素的一些状态

space-between 实现节点左靠布局

使用空元素填充,来实现 image

chrome浏览器属性-webkit-box-orient 和 css-loader

为了防止css-loader编译这个属性,需要这么写

/* autoprefixer: off */
-webkit-box-orient: vertical;
 /* autoprefixer: on */

babel编译装饰器

babel 6.x npm install babel-plugin-transform-decorators-legacy .babelrc中"plugins"添加"transform-decorators-legacy" 如果是babel 7.x+ npm install @babel/plugin-proposal-decorators --save-dev

{
“plugins”: [
    ["@babel/plugin-proposal-decorators", { “legacy”: true }]
 ]
}

border显示与隐藏切换不影响元素宽高

实现方式有三种:

  1. box-shadow: 0 0 0px 1px rgb(42, 96, 228);
  2. outline,但是圆角只有火狐浏览器支持
  3. 初始border透明,切换颜色
SunXinFei commented 4 years ago

Redux中的action异步操作

问题:我们在实际的组件拆分中,我们经常会遇到相同的一些业务逻辑,比如aaa触发了,都需要发送ajax请求,我们这里是文件路径保存给redux之前都需要判断路径是否存在,而且判断路径是一个异步操作。在这里我们介绍下Redux-thunk。 Redux-thunk是一个Redux是中间件,如下入所示,他位于 Action与 Strore中间,简单的说,他就是对store.dispatch进行了一次升级,他通过改造store.dispatch,可以使得store.dispatch可以接受函数作为参数。 image actions.js代码前后变化:

//旧的代码
// 选择文件夹路径在边栏
export const setFolderPathActiveInSider = (data) => {
    return {
        type: actionType.SETFOLDERPATHACTIVEINSIDER,
        data
    };
}
//新的代码
// 选择文件夹路径在边栏
export const setFolderPathActiveInSider = (data) => {
    return {
        type: actionType.SETFOLDERPATHACTIVEINSIDER,
        data
    };
}

// 选择文件夹路径在边栏
export const setFolderPathActiveInSiderAsync = (data) => {
    return async (dispatch) => {
        const exists = await fsExtra.pathExists(data);
        if (!exists) {
            message.error('路径未找到;该路径已被删除或移动');
            return false;
        }
        dispatch(setFolderPathActiveInSider(data));
        return true;
    }
}

所以在新的代码中,我们利用dispatch来人为的控制Redux中的action的触发,dispatch我们可以写在异步或者回调函数之中都可以,这样我们就可以利用action做一些统一的处理比如ajax请求等等

参考: Redux-thunk https://www.jianshu.com/p/159919acb4d0 redux-thunk - redux的中间件 https://www.jianshu.com/p/b1a039feac26 Redux中间件之redux-thunk使用详解 https://www.jianshu.com/p/a27ab19d3657

SunXinFei commented 4 years ago

项目目录引用与别名

webpack中的别名配置

resolve: {
    alias: {
      '@filemanage': path.join(__dirname, '../src/renderer/views/filemanage2')
    },
    extensions: ['.js', '.jsx', '.vue', '.json', '.css', '.node']
  },

//js中的文件路径以及图片路径

import { setFolderPathActiveInSiderAsync } from '@filemanage/store/action';
const CardImage = ({ type, extension }) => {
  let imgSrc = require(`@filemanage/assets/icon_file/file.png`);

  return (
    <img src={imgSrc} alt="File-icon" />
  )
}

//less中的文件路径

@import "~@filemanage/style/variables.less";
.filemanage-content-option{
  padding: 2px 10px;
  width: 100%;
  height: @filemanageContentOptionHeight;
  border-bottom: 1px solid #e8e8e8;
}
SunXinFei commented 4 years ago

React中的Loading组件

参考: 从 loading 的 9 种写法谈 React 业务开发 https://www.yuque.com/es2049/blog/xel32z React Hooks 深入不浅出 https://www.yuque.com/es2049/blog/durwgr

react中实现类似vue中的scoped

这里使用css-loader的配置,就可以实现scoped的效果,会将className进行hash处理,相当于vue语法中的scoped;如果不想hash处理就是global化,相当于vue中去掉scoped

参考: CSS Modules 用法教程 http://www.ruanyifeng.com/blog/2016/06/css_modules.html

React中Mixin的有害性

参考:Mixins Considered Harmful

SunXinFei commented 4 years ago

使用asar命令行解压asar文件 --用来解读app文件

  1. 运行命令npm install asar -g,全局安装asar;
  2. 从应用程序列表中右击目标app,然后选择显示包内容;
  3. 在Resources目录下,运行命令asar e app.asar app,表示将asar文件解压到app的文件夹中
SunXinFei commented 4 years ago

Electorn中使用better-sqlite3报错

错误信息如下:

 The module '/node_modules/better-sqlite3/build/better_sqlite3.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 57. This version of Node.js requires
NODE_MODULE_VERSION 64. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).

解决方法:

  1. npm install --save-dev electron-rebuild
  2. 使用electron-rebuild进行重新编译 node_modules/.bin/electron-rebuild -f -w better-sqlite3 参考:https://github.com/JoshuaWise/better-sqlite3/issues/126

    sqlite3

    在开发时,会生成.db文件,为了方便我们查看数据,我们使用sqlitebrowser来可视化查看。

SunXinFei commented 4 years ago

Electron中获取文件路径

在Electron中常用到操作用户的本地路径

(electron.app || electron.remote.app).getPath('home'); // 获取用户根目录
(electron.app || electron.remote.app).getPath('userData'); // 用于存储 app 用户数据目录
(electron.app || electron.remote.app).getPath('appData'); // 用于存储 app 数据的目录,升级会被福噶
(electron.app || electron.remote.app).getPath('desktop'); // 桌面目录
(electron.app || electron.remote.app).getAppPath();//当前应用程序所在目录

在 main process 中通过 electron.app 调用,在renderer process中,通过 remote 模块调用。 参考:

https://electronjs.org/docs/api/app https://github.com/whxaxes/blog/issues/8

Node中的路径处理

path.join()该方法使用平台特定的分隔符把全部给定的 path 片段连接到一起,并规范化生成的路径(只是拼接各个path片段) path.resolve()该方法会把一个路径或路径片段的序列解析为一个绝对路径。(除了拼接各个字段还拼接了工作目录的路径) 如果想获取某路径的上一层路径,则使用../进行拼接

__dirname 表示当前执行代码的文件所在的目录的绝对路径
__filename 表示当前执行代码的文件的绝对路径
module.filename ==== __filename 等价
process.cwd() 返回运行当前脚本的工作目录的路径
SunXinFei commented 4 years ago

通过require.context使Reducer模块自动加载

目录结构如下 image

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { combineReducers } from 'redux-immutable';
import thunk from 'redux-thunk';
import Immutable from 'immutable';

const modules = {}
const context = require.context('./reducers', false, /\.js$/) // carrega reducers de todos os componentes

context.keys().forEach((filename) => {
    const key = filename.replace(/(\.\/|\.js)/g, '')
    const ctx = context(filename)

    if (ctx.default) {
        modules[key] = ctx.default
    }
})

let store = createStore(
    combineReducers(modules),
    Immutable.Map({}),
    composeWithDevTools(applyMiddleware(thunk))
);
window.$store = store;

export default store;

image image 以上代码是redux中自动加载reducer,在vuex中同样适用 参考:

https://github.com/agalwood/Motrix/blob/master/src/renderer/store/modules/index.js

SunXinFei commented 4 years ago

Electron平台判断

SunXinFei commented 4 years ago

React 16.3以上版本 parent父组件获取循环loop中children子组件的dom结构

需求背景:文件管理页面常见的框选需要在容器层面给body绑定mouse事件,同时获取文件card的位置来判断是否在框选范围内。

//渲染子组件Card
getCardList(files) {
    let cardList = []
    this.cardObjList = []
    files.map((file, index) => {
      cardList.push(
        <Card getRef={elm => (  this.cardObjList.push(elm.getBoundingClientRect())  )} key={index} index={index} file={file} />
      )
    })
    return cardList
  }
//父组件
render() {
    const { isContentLoading, isShowContextMenu, files } = this.props;
    const { selectEle } = this.state;
    return (
      <div onClick={this.cancelSelectCard}
        onMouseDown={this.down}
        onMouseMove={this.move}
        onMouseUp={this.up}>
        {
          this.getCardList(files)
        }
      </div>
    );
  }
//卡片子组件
render() {
    const { index, file, connectDragSource } = this.props;
return ( 
<div
          ref={elm => {
            connectDragSource(elm)
            if (!elm) return;
            this.props.getRef(elm);
          }} 
          className={isSelectCard ? "filemanage-content-main-card selected" : "filemanage-content-main-card"}
          key={index}
          style={{ width: '110px' }}
          onClick={(e) => { this.handleClick(e) }}
          onDoubleClick={(e) => { this.handleDoubleClick(e, type, path, name, extension) }}>
          <div className="card-prew">
            <CardImage type={type} extension={extension}></CardImage>
          </div>
          <div className="card-filename">
            {name}
          </div>
        </div>
    );
  }

核心代码为子组件中调用父组件的getRef方法并把当前dom传入进去;

ref={elm => {
            connectDragSource(elm)
            if (!elm) return;
            this.props.getRef(elm);
          }}

父组件中列表清空以及getRef的回调push

this.cardObjList = [] //渲染之前列表清空
    files.map((file, index) => {
      cardList.push(
        <Card getRef={elm => (  this.cardObjList.push(elm.getBoundingClientRect())  )} key={index} index={index} file={file} />
)