Appearance
环境篇-动手搭建我们的简历平台
前言
前章节通过对 Electron 的介绍,想必你对它有了基础的了解,在阅读了需求功能设计与数据存储方案设计之后,也许在你的脑海中已经勾勒出了整个应用的整体结构,先不急!我们先打好基建,一步一步将项目环境搭建起来。
本章节将会将 Electron 环境搭建起来,丢掉 CRA 脚手架,通过一步步的动手实践,并将 TypeScript、ESLint、Prettier 引入,最后搭起我们的 React 项目。
Electron 与 React 是独立的,并不互相影响,不要将其捆绑在一块。你可以通过此小节只搭建 Electron,然后结合自己喜欢的 React 环境(也许你更想用 CRA)
如果你不想做PC端,想做成 Web 端可以跳过 Electron 环节,直接进入第二阶段,⚠️ 请注意,PC 端和 Web 端会在 Webpack 的 target 属性处有所区别。
🧨 如果你阅读完之后,对其中每一环节都已了解,但出于时间成本,想直接开箱即用,你可以通过项目地址的: init-cli,完整配置的分支代码,可以基于此分支直接进行项目开发,代码拉取之后安装一下即可开发。
开发前必读!Electron 与 React 的关系
小伙伴们可能会有些困惑,到底 Electron 与 React 的关系是什么,在没搞清楚的情况下,贸然去开发实践,问题频发。为了在动手搭建之前,让我们来捋清楚,这些讲不清道不明的关系。
抛开 Electron,纯 Web 的开发,那么大家都比较熟悉,最核心的永远是 HTML/CSS/JS
,所谓的 React、Vue、Angular 都是框架,那些 Less、Sass、Stylus 等都是 CSS 的“衍生物”。
在没有这些框架、预处理库出现之前,我们写代码,均是这样的
html
<!DOCTYPE html>
<html lang="en">
<head>
// 👇 加载a.js 和 a.css
<script type="text/javascript" src="./a.js"></script>
<link rel="stylesheet" type="text/css" href="a.css"></link>
</head>
<body>
<div id="root"></div>
</body>
</html>
javascript
// a.js
javascript
// a.css
相比大家对此并不陌生,那么当这些框架出现之后,有什么变化吗?万变不离其宗,如 Vue、React 等框架,经过 Babel 编译、Webpack 打包之后,最终的产物是 JS。既然是 JS,那么我们 HTML 如何加载 JS?你细品细品
再说说 Less、Sass 等,经过 Webpakc 的 Loader 处理之后,最终产物是什么?CSS,既然是 CSS,那么我们 HTML 如何加载 CSS?你再细品细品
所以说,万变不离其宗,下面是来自掘金的 HTML 所加载的文件,再仔细品尝
传统的 Web 开发就是写完代码,Ctrl + S 保存,刷新页面,如果你用 React 框架进行开发,那么你需要每次改动代码之后,进行一次 build 打包,将打包之后的 JS 文件,在 HTML 页面中重新加载,刷新页面,看到最终效果。
强烈建议你搭配第二章-Electron 认识与彩蛋篇-Webpack 认识一起阅读,效果更佳。
一来二去,你怕是会口喷芬芳,于是我们都会在本地起一个 dev-server 服务,具体表现就是:在浏览器中通过 IP + Port
,就能访问到我们的应用页面,比如 http://127.0.0.1:7001
就能本地访问简历平台。
这就是我们的 Web 开发以及在浏览器上如何访问、调试。
想明白之后,我们继续往下看,到底 Electron 是什么玩意,它跟 React 有什么关系?如果你认真阅读了第二章-Electron 认识,应该知道,Electron 是一个让你采用 Web 技术去开发原生应用的框架。
这么说有些抽象,我举个例子
你是一位做中餐的厨师(纯 Web 技术),你想开一个快餐店(做一个原生应用),但你没有钱(自身不具备原生能力)也没人手(自身不具备多端能力),这时候天降正义,有个大老板(Electron 框架)跟你说,你尽管开,其他问题我帮你搞定(所有原生问题、多端问题我都帮你解决),你很高兴,乐呵乐呵就同意了(安装了 Electron),接着大老板拍了拍手,叫了一个管家(主进程)过来,指了指你说,在我们的商场里给这位厨师分配一个档口铺面(创建一个渲染进程,对应一个浏览器窗口),于是你在这个档口铺面(渲染进程)中,利用你精通中餐技术(Web 技术)大展身手,大老板还对你说,我们商场里的公共财产(Electron API、NodeJS 模块),大部分情况下,你都能无条件使用(渲染进程只能用部分 ElectronAPI)。
通过这么一个通俗易懂的例子,想必大家对于 Electron 和 Web 技术已经分清楚关系了。在创建渲染进程时,会对应一个浏览器窗口,这个浏览器窗口,你可以理解为 Chrome 浏览器中的一个 Tab 标签。这里就会涉及到浏览器的进程模型,不再多说。
暂且我们将 Chrome 浏览器的一个 Tab 标签比做一个渲染进程
那么在你点击 “+” 号新增 Tab 时,本质上是新增一个渲染进程。如果将其比作 Electron,等价于在 Electron 中新增了一个渲染进程,并对应了一个浏览器窗口。从展示形式上看,Chrome 浏览器是一个窗口放多个 Tab;而在 Electron 看到的是,多个窗口的对应多个 Tab(一个窗口对应一个 Tab)
有小伙伴问,为什么我不能在 Electron 生成的浏览器窗口中新增 Tab,你不是说 Electron 继承了 Chromium 架构吗?这问题我想你看了上段讲述之后,再自己思考思考
再说回来,在 Chrome 中,我们如何显示 HTML 页面?通过链接 URL 的形式,比如 http/https
,或者 file://
。那么在 Electorn 中,也是如此,每一个渲染进程的入口文件就是一个 HTML 或者 URL 链接,这也就是我们代码中都会这么写
ts
// Electron 主进程
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
devTools: true,
nodeIntegration: true,
},
});
if (isDev()) {
mainWindow.loadURL(`http://127.0.0.1:7001/index.html`);
} else {
mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`);
}
到此关系应该很直白了,小伙伴们应该都能明白,不明白的就多看两遍。最后一点,有小伙伴问,能否在 Chrome 中通过 http://127.0.0.1:7001/index.html
访问应用页面呢?答案是:不一定可以!因为 Electron 对于渲染进程赋予了部分原生能力,比如说你可以调用 Electron API、NodeJS 模块,这些只有在 Electern 生成的浏览器窗口中才能正常访问不报错。如果你跑到 Chrome 浏览器去访问,Chrome 浏览器能识别 Node 中的 fs 是什么吗?path 模块干什么用 Chrome 知道吗?就好比我给你一张广州市 A 店的会员卡,你非跑去 B 店大声嚷囊说打折,人家能愿意吗?
什么时候可以在 Chrome 浏览器访问?就是你的 React 部分代码中,没有 ElectronAPI 相关代码,没有 NodeJS 相关代码,这时候就可以了。
第一阶段:Electron 搭建
官方对于应用搭建有详细的文档说明,下面基于官方文档,讲解一下 Electron 的搭建
1. 安装 Node 环境
在搭建 Electron 应用前,请先确保你的 Node.js 已经安装,接下来在终端输入命令
bash
node -v
npm -v
这两个命令应输出了 Node.js 和 npm 的版本信息。 如果这两个命令都执行成功,那就接着往下走
2. 安装 Electron
我们创建一个新文件夹,名为 visResumeMook
,进入文件夹并安装 Electron
由于这个项目在写小册之前就已经开发,所以如果是最新版本,会出现一些兼容问题,所以这里先限定 ^11.1.1 版本
bash
mkdir visResumeMook
cd visResumeMook
npm install electron@11.1.1
⚠️ 提示:由于网络问题,往往安装 Electron 会很慢,此时可以考虑换个淘宝源
3. 基本框架结构
前面说了,Electron 是基于 Chromium + Node.js 开发的,也就是说 Electron 本质上就是一个 Node.js 应用。这意味着您的 Electron 应用程序的起点将是一个 package.json 文件。
我们创建一个 package.json
文件,并且创建主进程脚本 electron.ts
,该脚本就是应用程序的入口。为了区分主进程模块和渲染进程模块,我以文件夹形式进行区分。
javascript
├── visResumeMook
│ ├── app
│ │ ├── main // 主进程模块
│ │ │ ├── electron.js
│ │ │ └── index.html
│ │ ├── renderer // 渲染进程模块
│ │ └──
│ ├── package.json
│ └──
└──
4. 编写 package.json
编写一下我们的 package.json 配置。我们将应用程序的入口文件配置为主进程脚本
json
{
"name": "visResumeMook",
"version": "0.0.1",
"author": "彭道宽",
"description": "从0到1实现一款轻巧适用的简历平台桌面应用。",
"main": "./app/main/electron.js",
"scripts": {
"start:main": "electron ./app/main/electron.js",
"install:electron": "ELECTRON_MIRROR=https://cdn.npm.taobao.org/dist/electron/ npm install electron"
},
"dependencies": {
"electron": "^11.1.1"
}
}
5. 定义 html
我们编写创建一个 HTML,等会加载此页面
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VisResumeMook</title>
</head>
<body>
<div id="root">简历平台应用搭建起来啦!</div>
</body>
</html>
6. 编写主进程
在主进程脚本中,通过 BrowserWindow
创建浏览器窗口(也就是一个渲染进程),你可以将其看成浏览器的一个 Tab。请注意 BrowserWindow
还有一个配置参数叫做 webPreferences,我们需要将其选项中的 nodeIntegration
设置为 true,这样我们才能在渲染进程中就能使用 node。
javascript
/**
* @desc electron 主入口
*/
const path = require('path');
const { app, BrowserWindow } = require('electron');
function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true, // 注入node模块
},
});
mainWindow.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
7. 启动应用程序
最后我们执行 npm run start:main
,就能看到我们搭建的简历应用啦~
最后我们看看文件结构
🧨 第一阶段的代码对应 electron-init
第二阶段:React 搭建
由于这个项目在写小册之前就已经开发,大版本升级肯定会出现一些兼容问题,为此,以目前的版本为主
接下来我们通过动手实践,一步步来搭建我们的 React 项目,如果你觉得繁琐,也可以通过 CRA 搭建,或者用其他脚手架生成。
1. 安装 React
我们打开终端,此时先安装 React
,并且安装它相应的兄弟姐妹
bash
npm install react@17.0.2
npm install react-router@5.2.0 react-router-dom@5.2.0 react-dom@17.0.2
2. 安装 Babel
接着安装一下 Babel
,它是 JS 编译器,能将 ES6 代码转成 ES5,让我们使用最近的语言特性,而不需要担心兼容性的问题。关于 install
的库,接下来会讲其作用
bash
npm install @babel/polyfill@7.12.1 --save
npm install @babel/core@7.14.3 @babel/cli@7.14.3 --save-dev
npm install @babel/preset-env@7.14.2 @babel/preset-react@7.13.13 @babel/preset-typescript@7.13.0 --save-dev
npm install @babel/plugin-transform-runtime@7.14.3 --save-dev
npm install @babel/plugin-transform-modules-commonjs@7.14.0 --save-dev
安装完成之后,根据 Babel 官网的教程,我们创建 babel.config.js
,配置一下我们常用的插件 plugins 和 预设值 presets
javascript
module.exports = {
presets: [
'@babel/preset-env', // 👉 根据配置的目标浏览器或者运行环境,选择对应的语法包,从而将代码进行转换
'@babel/preset-react', // 👉 react 语法包,让我们可以使用 React ES6 Class Component 的写法,支持JSX、TSX语法格式
'@babel/preset-typescript', // 👉 https://github.com/babel/babel/issues/10570
],
plugins: [
'@babel/plugin-transform-runtime', // 👉 官方提供的插件,作用是减少冗余的代码
[
'@babel/plugin-transform-modules-commonjs', // 👉 将 ECMAScript modules 转成 CommonJS.
{
allowTopLevelThis: true,
loose: true,
lazy: true,
},
],
],
};
3. 安装 Webpack
🌈 如果你对 Webpack 还存在疑问,强烈建议你看一下彩蛋篇-Webpack基础介绍与两大利器
我们安装一下 Webpack
,关于它的传奇
故事和核心灵魂已在前边章节有介绍,忘记了的小伙伴可以回去再看一遍。
bash
npm install webpack@4.44.1 --save-dev
npm install webpack-cli@3.3.12 --save-dev
我们期望监听文件的变化,能够自动刷新网页,做到实时预览,而不是改动一个字母,一个文字都需要重新打包。业界较为成熟的解决方案是通过:webpack-dev-server
插件,OK,我们安装它。
bash
npm install webpack-dev-server@3.11.2 --save-dev
对于主进程和渲染进程来讲,webpack的配置是会存在差异的。比如渲染进程可能需要 less-loader、htmlWebpackPlugins 等“专属”配置,而这些配置对于主进程来讲,是无用的。
存在差异的同时又会有相同点,比如 alias 别名配置等,当我们不采用 webpack-merge 时,会导致每份配置会存在重复的“配置”代码。其次在 dev 和 prod 环境下,配置会存在一些小差别,这时我们代码中会充斥着一些三元运算符来判断环境。最后的结果为每一份配置的可读性相对较差。
为此我们通过 webpack-merge 插件进行处理(👉 评论区有解答)
bash
npm install webpack-merge --save-dev
我们不想每次打包都需要手动修改 HTML 中的文件引用,并且期望采用自己写的 HTML 文件为模版,生成打包之后的入口 HTML,为此我们采用 html-webpack-plugin
插件进行处理。
javascript
npm install html-webpack-plugin@4.3.0 --save-dev
因为每次打包的文件会不同,我们需要先删除之前的 dist 文件,再重新打包,为此我们可以通过 clean-webpack-plugin
进行解决
npm install clean-webpack-plugin --save-dev
由于 Babel 用于编译,Webpack 用于打包输出,两者各司其职,我们通过 babel-loader
打通他们的联系。
bash
npm install babel-loader --save-dev
在上面都安装好相关库之后,接下来到动手环节,首先我们创建一个 webpack
文件夹,专门存放 webpack 相关配置,这里主要分为三个文件:
webpack.base.js
:基础公共配置webpack.main.dev.js
:主进程开发环境配置webpack.render.dev.js
:渲染进程开发环境配置
3.1 webpack.base.js
我们先来创建 webpack.base.js
基础公共配置文件
javascript
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@src': path.join(__dirname, '../', 'app/renderer'),
},
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
],
},
plugins: [new CleanWebpackPlugin()],
};
解读一下这段代码,Webpack 在启动后会从配置的入口模块出发,找到所有依赖的模块,resolve
配置 Webpack 如何寻找模块所对应的文件。我们配置了 extensions,表示在导入语句中没带文件后缀时,Webpack 会自动带上后缀去尝试访问文件是否存在。
我们配置中,配置了 extensions: ['.js', '.jsx', '.ts', '.tsx']
,意味着当遇到 import A from './A'
时,会先寻找 A.js、找不到就去找 A.jsx
,按照规则找,最后还是找不到,就会报错。
alias 代表别名,因为我们经常写 import A from '../../../../../A'
这种导入路径,特别恶心,所以通过配置别名处理。关于 Loader,我们前边小节已介绍,它就是模块打包方案,上述代码即表示:当匹配到 /\.(js|jsx|ts|tsx)$/
文件时,使用 babel-loader
去处理一下。
3.2 webpack.main.dev.js
我们看看主进程的配置,新增 webpack.main.dev.js
文件
javascript
const path = require('path');
const baseConfig = require('./webpack.base.js');
const webpackMerge = require('webpack-merge');
const mainConfig = {
entry: path.resolve(__dirname, '../app/main/electron.js'),
target: 'electron-main',
output: {
filename: 'electron.js',
path: path.resolve(__dirname, '../dist'),
},
devtool: 'inline-source-map',
mode: 'development',
};
module.exports = webpackMerge.merge(baseConfig, mainConfig);
解读一下这段代码,我们定义入口文件为 /app/main/electron.js
,并且定义打包出来的文件目录为 dist,文件名为 electron.js。
需要注意的一点是:由于 JS 的应用场景日益增长,从浏览器到 Node,运行在不同环境下的 JS 代码存在一些差异。target 配置项可以让 Webpack 构建出不同运行环境的代码
关于 target 的可选项,可从官网查阅,这里我们将其配置成 electron-main
,至于主进程的 plugins,我们定义了一些构建变量。最后通过 webpack-merge 合并导出一份完整的配置。
3.3 webpack.render.dev.js
在说配置之前,我们先来创建一个渲染进程对应的代码文件夹。我们在 app
文件夹下新增一个名为 renderer
文件夹。
回顾一下之前 Electron 部分是不是有一个 index.html
文件,我们我们将其移动到 renderer
文件夹下,并修改它
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VisResumeMook</title>
<style>
* {
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>
接着我们在 renderer
下创建一个 React 的 app.jsx
文件
javascript
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route, Switch } from 'react-router-dom';
function App() {
return (
<Router>
<Switch>
<Route path="/">
<div>可视化简历平台</div>
<div>这是 Electron + React </div>
</Route>
</Switch>
</Router>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
我们再来修改一下渲染进程的相关配置,新增 webpack.render.dev.js
文件
javascript
const path = require('path');
const webpackMerge = require('webpack-merge');
const baseConfig = require('./webpack.base.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const devConfig = {
mode: 'development',
entry: {
// 👇 对应渲染进程的 app.jsx 入口文件
index: path.resolve(__dirname, '../app/renderer/app.jsx'),
},
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, '../dist'),
},
target: 'electron-renderer',
devtool: 'inline-source-map',
devServer: {
contentBase: path.join(__dirname, '../dist'),
compress: true,
host: '127.0.0.1', // webpack-dev-server启动时要指定ip,不能直接通过localhost启动,不指定会报错
port: 7001, // 启动端口为 7001 的服务
hot: true,
},
plugins: [
new HtmlWebpackPlugin({
// 👇 以此文件为模版,自动生成 HTML
template: path.resolve(__dirname, '../app/renderer/index.html'),
filename: path.resolve(__dirname, '../dist/index.html'),
chunks: ['index'],
}),
],
};
module.exports = webpackMerge.merge(baseConfig, devConfig);
解读一下这段代码,以 /app/renderer/app.jsx
为入口,并配置了本地开发 devServer,通过 HtmlWebpackPlugin
自动生成一份以 /app/renderer/index.html
为模版的 HTML 文件。注意此时的 target是针对 Electron 渲染进程。最后通过 webpack-merge
合并导出一份完整配置。
5. Electron 与 React 结合起来
对于 Webpack 相关配置已经搭建完毕,我们来看看现在我们的文件目录都有哪些?
接下来我们让 Electron 和 React 结合起来,前面讲到,Electron 可以理解为页面添加了一个壳,由于我们将主进程中的 index.html
移到了渲染进程,所以我们需要修改一下 electron.js
javascript
/**
* @desc electron 主入口
*/
import path from 'path';
import { app, BrowserWindow } from 'electron';
function isDev() {
// 👉 还记得我们配置中通过 webpack.DefinePlugin 定义的构建变量吗
return process.env.NODE_ENV === 'development';
}
function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
devTools: true,
nodeIntegration: true,
},
});
if (isDev()) {
// 👇 看到了吗,在开发环境下,我们加载的是运行在 7001 端口的 React
mainWindow.loadURL(`http://127.0.0.1:7001`);
} else {
mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`);
}
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
🎉 接着进入 package.json
文件中,修改一下启动脚本命令,添加渲染进程的启动方式
json
"scripts": {
"start:main": "webpack --config ./webpack/webpack.main.dev.js && electron ./dist/electron.js",
"start:render": "webpack-dev-server --config ./webpack/webpack.render.dev.js"
},
6. 跑起来
我们开两个终端,一个跑 npm run start:render
,另一个跑 npm run start:main
,看看结果
请注意,这里是同时开两个终端,不是先跑一个,再跑另一个。
👏 不出意外,结果很棒,Electron 和 React 这环境终于打通!
如果出现报错:
Uncaught ReferenceError: require is not defined
,请检查你是否在主进程中添加这行代码,如果添加了,请确保你搭建项目的 Electron 与本应用的版本一致(当前项目的 Electron@^11.1.1)
请自检查一下你的版本是否正确,进入 node_modules,找到 electron,看看 package.json 中的 version 是否是 11.1.1。
javascript
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
// ...
webPreferences: {
// 👇 请确保添加此配置
nodeIntegration: true,
},
});
🧨 第二阶段的代码对应 electron-react-init
第三阶段:引入更多技术点
接下来引入 TypeScript、ESLint、Prettier,让整个项目看起来更加丰富。
1. 安装 TypeScript
关于 TS 的入门学习,我建议小伙伴们去看官方文档,结合项目去上手写 TS,项目中的 TS 不会有很多复杂难以理解的地方,写着写着,你会发现其实 TS 没那么难,如果你想提升 TS,也可以看看 type-challenges 这个库
先安装 TypeScript
npm install typescript --save-dev
装完之后,我们将项目中的 js、jsx 文件都改造成 ts、tsx
由于我们将 renderer/app.jsx
作为入口文件,所以修改后,前往 webpack.render.dev.js
文件修改 entry
,避免项目启动报异常
javascript
entry: {
// 👇 这里改成.tsx
index: path.resolve(__dirname, '../app/renderer/app.tsx'),
}
同时对于主进程 main/electron.js
也需要去 webpack.main.dev.js
修改一下 entry
javascript
entry: {
// 👇 这里改成.ts
entry: path.resolve(__dirname, '../app/main/electron.ts'),
}
接下来我们在 renderer
文件夹下新增一个文件夹取名为 title,在此文件夹下新增 index.tsx 文件,让我们来写一下该组件,并定义组件的 Props
我们会发现,TS 提示错误,原来我们还没安装 React 对应的 TS 包,安装一下
bash
npm install @types/react --save-dev
npm install @types/react-dom --save-dev
npm install @types/react-redux --save-dev
npm install @types/react-router-dom --save-dev
装好之后,我们继续写组件
ts
import React from 'react';
interface IProps {
/**
* @description 标题
*/
text: string;
/**
* @description 样式
*/
styles?: React.CSSProperties;
}
function Title({
text,styles
}: IProps) {
return (
<div style={styles}>{text}</div>
)
}
export default Title;
我们在 app.tsx 下引入此组件,看看是不是会有提示
一切如我们的预期,这表示我们可以很愉快的使用 TS 开发了。
2. 安装 ESLint + Prettier
我们看上面的 <Title />
组件,看着有点膈应,好像不该换行的它换行了,该换行的没换行。我们总不能手动的去按回车、删空格吧?
这时我们使用 Prettier
进行代码格式化,相比于 ESLint 中的代码格式规则,它更加专业。同时我们采用 ESLint
来统一代码风格,提高我们的代码质量。
ESLint 将我们的代码解析成 AST,通过检测 AST 来判断代码是否符合我们设置的规则,往往不同公司团队会自定义一套自己的 ESLint 规范。
我们先来安装一下 Prettier
和 ESLint
bash
npm install eslint@^7.26.0 --save-dev
npm install prettier@^2.3.0 --save-dev
接着安装一些对应的插件信息,具体信息大家可去查询这些库都做了什么工作
bash
npm install eslint-config-alloy@^4.1.0 --save-dev
npm install eslint-config-prettier@^8.3.0 --save-dev
npm install eslint-plugin-prettier@^3.4.0 --save-dev
npm install eslint-plugin-react@^7.23.2 --save-dev
npm install eslint-plugin-react-hooks@^4.2.0 --save-dev
可能有人会问,有 ESLint,是不是也有 TSLint,答案是:有。但我并不推荐。
由于现在 ESLint 的生态比较完善,而 TSLint 首先是不能使用 ESLint 社区的一些成果,其次 TSLint 在生态上也相对较差,所以 TSLint 的作者已经宣布会逐渐放弃 TSLint ,而去支持 typescript-eslint-parser ,同时 Typescript 团队也宣布会将自己开发的 lint 工具从 tslint 迁移到 typescript-eslint-parser
bash
npm install @typescript-eslint/parser@^4.24.0 --save-dev
npm install @typescript-eslint/eslint-plugin@^4.24.0 --save-dev
安装好之后,我们在项目根目录下创建 tsconfig.json
、.prettierrc
、 .eslintrc.js
json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2016" /* 编译结果使用的版本标准: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* 编译结果使用的模块化标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": [
"ESNext",
"DOM"
] /* 在写ts的时候支持的环境,默认是浏览器环境。如需要支持node,安装@type/node */,
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"sourceMap": true,
"strict": true,
"declaration": true,
"removeComments": true /* 编译结果把ts的注释移除掉 */,
"esModuleInterop": true /* es6的模块化和非es6的模块化标准互通 */,
"allowSyntheticDefaultImports": true,
"baseUrl": "./",
"paths": {
"@src/*": ["./app/renderer/*"] // webpack 配置别名,但在TS中会报红找不到,所以tslint也需要配置
},
"moduleResolution": "node"
},
"exclude": ["dist", "node_modules"], // 这里需要排除掉 dist 目录和 node_modules 目录,不进行检查
"include": ["app/**/*.ts", "app/**/*.tsx", "app/**/*.d.ts"]
}
json
// .prettierrc
{
"eslintIntegration": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"endOfLine": "auto"
}
javascript
// .eslintrc.js
module.exports = {
extends: [
'alloy',
'alloy/react',
'alloy/typescript',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
],
globals: {
// 这里填入你的项目需要的全局变量
// 这里值为 false 表示这个全局变量不允许被重新赋值,比如:
__dirname: false,
},
rules: {
'no-undefined': 'warn',
'no-debugger': 'off',
complexity: ['error', { max: 99 }],
// 这里填入你的项目需要的个性化配置,比如:
// @fixable 一个缩进必须用两个空格替代
indent: [
1,
2,
{
SwitchCase: 1,
flatTernaryExpressions: true,
},
],
// @fixable jsx 的 children 缩进必须为两个空格
'react/jsx-indent': [1, 2],
// @fixable jsx 的 props 缩进必须为两个空格
'react/jsx-indent-props': [1, 2],
'react/no-string-refs': 1, // 不要使用ref
'no-template-curly-in-string': 1, // 在string里面不要出现模板符号
'@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/no-duplicate-imports': 'off',
'react/no-unsafe': 'off',
'@typescript-eslint/no-invalid-this': 'off',
'react/jsx-key': 0,
'no-undef': 0,
},
};
这时候我们再去看前边写的代码,会发现一堆报红,我们只需 Ctrl+S
保存一下即可。
⚠️ 提示:如果发现未生效,可以重新打开一下 vscode
3. CSS Modules 问题
大家都知道,CSS 的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。为了解决此情况,CSS Modules 的解决方案就是:使用一个独一无二的 class 的名字,不会与其他选择器重名。所以我们一般会看到,很多类命都是 hash 值 + 组件名
,下面说说如何在 Webpack 中配置 CSS Module
在此项目中,我们采用 less
进行样式相关的编写,安装它
bash
npm install less@3.12.2 --save-dev
我们进入 Webpack 官网 Loader 配置,看看它提供处理样式类型的打包方案,关于这些 Loader 的具体介绍可在官网查阅
javascript
npm install less-loader@6.2.0 --save-dev
npm install postcss-loader@3.0.0 --save-dev
npm install css-loader@3.0.0 --save-dev
// 👇 将我们的样式通过style标签插入到页面head中
npm install style-loader@2.0.0 --save-dev
前面我们说了,Loader 就是模块打包方案,我们去 webpack.render.dev.js
中添加配置
javascript
// webpack.render.dev.js
const devConfig = {
// 👇 追加这段代码,关于Loader与Plugin了解可以去看彩蛋篇
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.less$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]__[hash:base64:5]',
},
},
},
'postcss-loader',
'less-loader',
],
},
],
}
};
这时候我们在 <Title />
组件下编写一个 index.less
文件,看其样式否如我们所愿?
ts
import React from 'react';
import lessStyle from './index.less';
function Title() {
return (
<div style={styles} className={lessStyle.title}>
{text}
</div>
)
}
export default Title;
less
.title {
color: red;
}
有可能会出现下面问题,我们手动安装一下就可以了
npm install autoprefixer@9.0.0 --save-dev
再次运行,看看是否可行?发现还是不行,我们看看报什么错?
解决此问题需要我们在项目根目录下创建 postcss.config.js
,添加一下配置
javascript
module.exports = {
plugins: {
autoprefixer: {
overrideBrowserslist: ['> 0.5%', 'last 5 versions'],
},
},
};
再次运行,看看效果,我们可以看到,类名的格式为[组件名]_[当前类名]_[哈希值取5位]
,从而形成独一无二的 class 名字,不会与其他选择器重名。至此我们完成了样式相关的配置处理。
4. styleName
在 React 中 CSS Modules 会使得我们写代码都要通过 styles 的形式
ts
import React from 'react';
import lessStyle from './index/less';
function Title() {
return <div className={lessStyle.box} />;
}
export default Title;
特别繁琐,所以通过插件 react-css-modules 实现 styleName 的形式,但是每次都需要写成这样
ts
import React from 'react';
import CSSModules from 'react-css-modules';
import lessStyle from './index.less';
class Title extends React.Component {
render() {
return (
<div styleName="box">
<div styleName="cell">test</div>
</div>
);
}
}
export default CSSModules(Title, lessStyle);
也是很蛋疼,所以作者又写了个插件babel-plugin-react-css-modules,这个插件更加好用
为了改造成这种形式,我们进行配置修改,我们先安装插件
javascript
// 👇 不安装会在使用 styleName 时 TS 报错
npm install @types/react-css-modules@4.6.2 --save-dev
// 👇 让我们更好的使用 CSS Module
npm install babel-plugin-react-css-modules@5.2.6 --save-dev
npm install postcss-less@3.1.4 --save-dev
然后在 babel.config.js
文件中添加一下配置
javascript
module.exports = {
plugins: [
...// css-modules
[
'babel-plugin-react-css-modules',
{
exclude: 'node_modules',
webpackHotModuleReloading: true,
generateScopedName: '[name]__[local]__[hash:base64:5]',
autoResolveMultipleImports: true,
filetypes: {
'.less': { syntax: 'postcss-less' },
},
},
],
],
};
最后再看看组件的代码是怎样的
ts
import React from 'react';
import './index.less';
interface IProps {
/**
* @description 标题
*/
text: string;
/**
* @description 样式
*/
styles?: React.CSSProperties;
}
function Title({ text, styles }: IProps) {
return (
<div style={styles} styleName="title">
{text}
</div>
);
}
export default Title;
6. 文件类型报错
当我们在代码中引入一张照片时,打包会发生错误
官方提供了一种专门处理此类型的方案:file-loader
,我们安装一下这个 loader
npm install file-loader --save-dev
修改一下 webpack.base.js
javascript
module.exports = {
// ...
module: {
rules: [
{
test: /\.(jpg|png|jpeg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
},
},
],
},
],
},
plugins: [new CleanWebpackPlugin()],
};
5. 文件部分类型 TS 报红
我们此刻引入一张图片,TS 会报错,说找不到模块,实际上我们的路径是正确的,图片也能正常显示
这时候我们只需要在 app/renderer
目录下,新增一个 global.d.ts
文件即可
ts
// global.d.ts
declare module '*.jpg' {
const jpg: string;
export default jpg;
}
⚠️ 请注意:如果你在根目录下新增 global.d.ts 文件,请确保你的 tsconfig.json 中
include
字段是能匹配到 global.d.ts 文件
关于 global.d.ts 可配置的东西可太多了,一般来说,我们 window.pdk
肯定会被 ts 报红,说 window 上并无此属性,这时候我们又不想改成 (window as any).pdk
,那么我们可以扩展 Window 的类型
ts
// 这里用于扩充window对象上的值
declare interface Window {
pdk: string;
}
总结
本章节主要通过实操,手把手带大家从0到1实现了项目的搭建,这是最重要也是最核心的部分,一层一层、一步一步地讲解,帮助小伙伴们,能够从头到尾把内容连接起来。也期望大家一定要动手实操一遍,感受一下搭建的“快乐”。
🧨 如果你不想动手搭建,可直接访问:init-cli 分支,代码拉取之后安装一下即可开发。
补充说明
1. 关于环境变量
2021.06.10 补充更新,此补充说明是给 10 号前已预览过的小伙伴看的~
早一批的小伙伴应该会记得,我们在 webpack.main.dev.js 中通过 webpack.DefinePlugin 定义了全局变量 p
得先明白一点就是,为什么要配置这个,在前端工程化配置中,一般我们都会有许多环境,如生产环境/开发环境等。在 node 中有全局变量 process 表示当前 node 进程,p
在 package.json 的 scripts 命令中,通过添加 NODE_ENV=development,表示 node 服务启动时,将 NODE_ENV 挂载到 pcross-env
库,抹平掉不同平台之间的差异。
再来说说 webpack.DefinePlugin 中定义的全局变量,它表示 webpack 在编译过程中,所有此变量,都会被替换成我们定义的值。
既然两者都能定义变量,那么采用哪种会比较好呢?由于我们的主进程和渲染进程都是经过 webpack 编译,我个人建议是通过 webpack.DefinePlugin。当然不用自己写环境变量的定义。因为在 webpack 配置中,我们定义了 mode 配置选项,通过官网我们可以看到:
javascript
const mainConfig = {
// ...
mode: 'development',
// 👆 上面定义的 mode 等价于我们在 DefinePlugin 中定义了 process.env.NODE_ENV
};
点击查看 commit 信息,如果看完这里还有疑问,欢迎进群讨论~
2. 关于 dependencies 与 devDependencies
收到小伙伴们的建议,此次进行了更新,在此之前,我们需要搞清楚一下这两者的区别。
- dependencies 是产生环境下的依赖,比如说 Vue、React 等
- devDependencies 是开发环境下的依赖,比如 Webpack、ESLint、Babel 等
可能这么讲小伙伴们还是不怎么能区分,这么说吧。我写了一个 React UI 组件包,其中依赖了 AntDesign,那我需要将 AntDesign 和 React 放在 dependencies 中。
再比如说,我们项目中用到了 Redux,但是 Redux 的 devDependencies 里面有 jest、rxjs 等,在我们安装 Redux 时,我们是不会把 jest、rxjs 拉下来的(也就是 node_modules 里是不会有 jest、rxjs)
一般我们开发过程中,会在项目中安装 webpack、webpack-dev-server、babel、eslint 的等工具库,或者是用于单元测试的 jest 库,这些依赖库都只是在我们项目开发过程中使用,应该写在 devDependencies 里。如果说我们依赖乱写,例如开发依赖放在生产,生产依赖放在开发,这会出现什么问题?
以 rc-redux-model为例,我们可以看到它的依赖非常的少。这是一个 npm 包。当我们执行 npm install rc-redux-model
时,此时会将 dependencies 的依赖都安装,不会安装 devDependencies 里的依赖库。
但如果是通过克隆仓库项目代码,如 git clone https://github.com/SugarTurboS/rc-redux-model
,然后再 npm instal
,这时候会将 dependencies 和 devDependencies 里的依赖库都安装。
小伙伴们可以再私下研究研究,点击查看 commit 信息,如果看完这里还有疑问,欢迎进群讨论~
3. 打包成桌面应用
许多小伙伴们通过本章节将 Electron + React 搭建起来后,还没开始写几行代码,就迫不及待想打包成桌面应用,毕竟搞出自己第一个桌面应用,是一件很骄傲自豪的事情!!!但是又不想等整个小册都完结,才能上手打包构建,为此,阿宽新增一章节 🏆 支线篇-打包生成第一个桌面应用(骄傲自豪)。
此章节为支线任务,与主流程(主线任务)无任何关系。可放心使用,如果在打包期间遇到问题,可加我微信或进群讨论~