使用Electron及React开发Markdown笔记软件
前言
本文通过Youtube视频:- Build a Markdown Notes app with Electron, React, Typescript, Tailwind and Jotai - YouTube
整理而来。
是基于Electron、React、TypeScript、Tailwind及Jotai来开发一款基础功能的Markdown笔记软件。
技术栈及依赖
- Electron: 跨平台桌面应用框架
- React: Facebook推出的前端开发框架
- Yarn: 类似npm、pnpm的前端项目包管理工具
- Vite: 类似webpack的前端项目打包工具
- Typescript: 带类型系统的JavaScript
- Tailwind: 原子性CSS框架
- Postcss:CSS 转换的JavaScript库
- Clsx: 动态拼接CSS类名的JavaScript库
- Tailwind-merge:自动合并tailwind css类
- File system | Node.js:Node,js文件操作
- Jotai: 轻量且高性能的React 状态管理库Jotai
- Lodash: Javascript实用工具库
- MDXEditor: 一款Markdown编辑器
- React-icons: React图标库
- Fs-extra: Nodejs异步文件操作库
IDE/编辑器
编辑器这里使用VSCode
VSCode推荐插件
- Auto Import:自动提示和导入依赖
- Auto Rename Tag:自动修改配对Tag
- Tailwind CSS IntelliSense:智能提升Tailwind样式雷
- Prettier:格式化代码
- ESLint:代码格式及出错提示
- vscode-icons:文件图标
项目实践
本文基于macOS进行演示,其他平台请自行切换部分包的安装方法
新建Electorn项目
安装yarn
$ brew install yarn
如果macOS上没有brew可以使用:
/bin/bash -c "$(curl -fsSL https://gitee.com/ineo6/homebrew-install/raw/master/install.sh)"
安装
使用@quick-start/electron
创建electorn项目
$ yarn create @quick-start/electron
选择参考如下
✔ Project name: … note-mark
✔ Select a framework: › react
✔ Add TypeScript? … Yes
✔ Add Electron updater plugin? … No
✔ Enable Electron download mirror proxy? … No
进入项目并安装依赖
cd note-mark
yarn
项目结构
VSCode打开项目
新建(补充)一些目录
# 新建共享(通用)类型及常量目录shared
mkdir src/shared
# 新建后端实用方法目录lib
mkdir src/main/lib
# 新建前端钩子方法目录hooks
mkdir src/renderer/src/hooks
# 新建前端实用方法目录utils
mkdir src/renderer/src/utils
# 新建前端数据目录store及虚拟数据目录mocks
mkdir src/renderer/src/store
mkdir src/renderer/src/store/mocks
项目整体结构说明如下
├──.vscode
├──build // 打包相关资源
├──node_modules // 三方依赖包
├──resources // 资源目录
│ ├── icon.png // 应用图标
├── src // 源码目录
│ ├── main // 主进程目录
│ │ ├── index.ts // 入口文件
│ │ └── lib // 自定义方法目录
│ ├── preload // 预加载脚本目录
│ │ ├── index.d.ts // 在context上下文中声明自定义变量类型
│ │ └── index.ts // 在contextBridge中定义变量及函数调用的ipc方法
│ ├── renderer // 渲染进程目录
│ │ ├── index.html // 索引页面,引入src/main.tsx
│ │ └── src
│ │ ├── App.tsx // 应用根组件,引入components中各种组件
│ │ ├── assets // 静态资源目录
│ │ │ ├── index.css // 全局样式
│ │ ├── components // 组件目录
│ │ ├── env.d.ts // vite环境配置
│ │ ├── hooks // 钩子目录
│ │ ├── main.tsx // 入口文件,引入assets/index.css及App.tsx
│ │ ├── store // 状态管理目录
│ │ └── utils // 实用方法目录
│ └── shared // 全局脚本目录
│ ├── constants.ts // 常量
│ ├── models.ts // 数据类型
│ └── types.ts // 函数类型
├── resources // 资源目录
│ └── icon.png // 应用图标
├── electron-builder.yml // electron打包配置
├── electron.vite.config.ts // vite构建配置
├── package.json // 项目配置文件,包含项目信息、依赖及脚本(命令)等
├── tsconfig.json // typescript编译配置,引入tsconfig.node.json及tsconfig.web.json
├── tsconfig.node.json // typescript配置
├── tsconfig.web.json // typescript配置
└── README.md // 项目说明
preload: 为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本
启动app
运行
yarn dev
示例app初始界面如下:
Electron通信原理
从项目src目录中可以看到main、preload和renderer三个主要目录
Electorn应用分为主进程main及浏览器渲染进程renderer两个进程,类似于后端和前端,其中preload起通信桥梁的作用。
通信示意图如下:
配置项目窗口样式
为了应用美观,我们将窗口改为无标题栏模式,修改如下:
修改src/main/index.ts
...
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
center: true, // 新增
title: 'NoteMark', // 新增
frame: false, // 新增
vibrancy: 'under-window', // 新增
visualEffectState: 'active', // 新增
titleBarStyle: 'hidden', // 新增
trafficLightPosition: { x: 15, y: 10 }, // 新增
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: true,
contextIsolation: true
}
})
...
修改前端index.html
在src/renderer/index.html内容安全策略中增加允许执行不安全脚本,即script-src 'self;'
修改为script-src 'self' 'unsafe-eval';
同时删除不用的title等,修改后index.html内容如下:
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
删除应用根组件App.ts中的多余内容及assets和components中的多余资源和组件,
- 删除src/renderer/src/assets除main.css之外所有内容,并清空main.css文件
- 删除src/renderer/src/comppents/Version.tsx组件
- 修改根应用App.tsx内容,删除多余内容,并只添加一个文本,例如
App.tsx
function App(): JSX.Element {
return (
<>
</>
)
}
export default App
此时应用界面如下
安装TailwindCSS
安装tailwind样式
yarn add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
执行后在项目根目录生成tailwind.config.js和postcss.config.js
修改tailwind.config.js,content中增加'./src/renderer/**/*.{js,ts,jsx,tsx}'
完整内容如下
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/renderer/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {}
},
plugins: [require('@tailwindcss/typography')]
}
安装其他三方包
yarn add -D tailwind-merge
yarn add -D clsx
yarn add -D react-icons
添加主体样式
src/renderer/src/assets/main.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
#root {
@apply h-full;
}
html,
body {
@apply h-full;
@apply select-none;
@apply bg-transparent;
@apply font-mono antialiased text-white;
@apply overflow-hidden;
}
header {
-webkit-app-region: drag;
}
button {
-webkit-app-region: no-drag;
}
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-thumb {
@apply bg-[#555] rounded-md;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
}
创建左右布局
添加React组件
[!tips]
.ts
文件、.d.ts
.tsx
、文件的区别
- ts:TypeScript 脚本文件
- d.ts: 允许在TypeScript中使用现有JavaScript代码的类型定义文件
- tsx:React + 类型检查
React组件基本模板
import { ComponentProps } from 'react'
import { twMerge } from 'tailwind-merge'
export const 组件名 = ({ className, ...props }: ComponentProps<'div'>) => {
return (
<div className={twMerge('flex justify-center', className)} {...props}>
</div>
)
}
创建AppLayout组件
在src/renderer/src/components中创建AppLayout.tsx,参考内容如下
AppLayout.tsx
import { ComponentProps, forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
export const RootLayout = ({ children, className, ...props }: ComponentProps<'main'>) => {
return (
<main className={twMerge('flex flex-row h-screen', className)} {...props}>
{children}
</main>
)
}
export const Sidebar = ({ className, children, ...props }) => {
return (
<aside
className={twMerge('w-[250px] mt-10 h-[100vh+10px] overflow-auto', className)}
{...props}
>
{children}
</aside>
)
}
export const Content = forwardRef<HTMLDivElement, ComponentProps<'div'>>(
({ children, className, ...props }, ref) => (
<div ref={ref} className={twMerge('flex-1 overflow-auto', className)} {...props}>
{children}
</div>
)
)
Content.displayName = 'Content'
此处定义了一个根布局组件RootLayout,一个侧边栏组件Sidebar和一个主体内容组件Content。
根组件App.tsx中导入布局组件
src/renderer/src/App.tsx
import { Content, RootLayout, Sidebar } from "./components/AppLayout"
function App(): JSX.Element {
return (
<>
<RootLayout>
<Sidebar className="p-2">
Sidebar
</Sidebar>
<Content className="border-l bg-zinc-900/50 border-l-white/20">
Content
</Content>
</RootLayout>
</>
)
}
export default App
启动App,显示如下
添加可拖拽头
由于我们隐藏了titleBar,此时窗口没办法拖拽,这里我们添加一个自定义header组件来实现支持拖拽。
新建src/renderer/src/components/DraggableTopBar.tsx
export const DraggableTopBar = () => {
return <header className="absolute inset-0 h-8 bg-transparent" />
}
App.tsx中导入组件
import { Content, RootLayout, Sidebar } from "./components/AppLayout"
import { DraggableTopBar } from "./components/DraggableTopBar"
function App(): JSX.Element {
return (
<>
<DraggableTopBar />
<RootLayout>
<Sidebar className="p-2">
Sidebar
</Sidebar>
<Content className="border-l bg-zinc-900/50 border-l-white/20">
Content
</Content>
</RootLayout>
</>
)
}
export default App
运行App,此时App,点击TopBar区域,可拖拽移动窗口位置。
侧边栏添加按钮
在src/renderer/src/components中新建Button目录
新建通用按钮组件
src/renderer/src/components/Button/ActionBuuton.tsx
import { ComponentProps } from 'react'
import { twMerge } from 'tailwind-merge'
export type ActionButtonProps = ComponentProps<'button'>
export const ActionButton = ({ className, children, ...props }: ActionButtonProps) => {
return (
<button
className={twMerge(
'px-2 py-1, rounded-md border border-zinc-400/50 hover: bg-zink-600/50 transition-all duration-100',
className
)}
{...props}
>
{children}
</button>
)
}
新建笔记按钮
新建src/renderer/src/components/Button/NewNoteBuuton.tsx
import { ActionButton, ActionButtonProps } from './ActionButton'
import { LuFileSignature } from 'react-icons/lu'
export const NewNoteButton = ({ ...props }: ActionButtonProps) => {
const handleCreation = async () => {
console.info("创建笔记")
}
return (
<ActionButton onClick={handleCreation} {...props}>
<LuFileSignature className="w-5 h-5 text-zinc-300" />
</ActionButton>
)
}
删除笔记按钮
新建src/renderer/src/components/Button/DeleteNoteButton.tsx
import { ActionButton, ActionButtonProps } from './ActionButton'
import { FaRegTrashCan } from 'react-icons/fa6'
export const DeleteNoteButton = ({ ...props }: ActionButtonProps) => {
const handelDelete = async () => {
console.info("删除笔记")
}
return (
<ActionButton onClick={handelDelete} {...props}>
<FaRegTrashCan className="w-5 h-5 text-zinc-300" />
</ActionButton>
)
}
添加一个按钮栏,并引入新建笔记及删除笔记按钮
新建src/renderer/src/components/ActionButtonRow.tsx
import { ComponentProps } from 'react'
import { NewNoteButton } from './Button/NewNoteButton'
import { DeleteNoteButton } from './Button/DeleteNoteButton'
export const ActionButtonRow = ({ ...props }: ComponentProps<'div'>) => {
return (
<div {...props}>
<NewNoteButton />
<DeleteNoteButton />
</div>
)
}
根组件App.tsx中导入按钮栏ActionButtonRow
将macOS系统切换到深色模式,启动App显示如下
此处App样式仅适配了深色模式
使用mock数据
添加预览
格式化日期
添加Markdown编辑器
MDXEditor - the Rich Text Markdown Editor React Component
yarn add @mdxeditor/editor
yarn add -D @tailwindcss/typography
修改滚动条
src/renderer/src/assets/index.css添加
// ....
@layer base {
// ...
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-thumb {
@apply bg-[#555] rounded-md;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
}
添加Note Title
yarn add jotai
修改src/renderer/src/components/MarkdownEditor.tsx
// ...
return <MDXEditor
key={selectedNote.title}
markdown={selectedNote.content}
// ...
/>
重置滚动条
实现新增删除笔记
使用文件系统
mkdir ~/MarkNote
cd ~/MarkNote
echo "Hello from Note1" > Note1.md
echo "Hello from Note2" > Note2.md
yarn add fs-extra
yarn add -D lodash
yarn typecheck:web
写入文件
src/shared/types.ts
// ...
export type WriteNote =(title: NoteInfo['title'], content: NoteContent) => Promise<void>
添加Welcome
构建
构建
yarn build
yarn start
构建mac应用
yarn build:mac