Skip to content
On this page

环境篇-动手搭建我们的简历平台


前言

前章节通过对 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 所加载的文件,再仔细品尝

image.png

传统的 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 标签比做一个渲染进程

image.png

那么在你点击 “+” 号新增 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,就能看到我们搭建的简历应用啦~

image.png

最后我们看看文件结构

image.png

🧨 第一阶段的代码对应 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 相关配置已经搭建完毕,我们来看看现在我们的文件目录都有哪些?

image.png

接下来我们让 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 这环境终于打通!

image.png

如果出现报错: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

image.png

我们会发现,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 下引入此组件,看看是不是会有提示

image.png

image.png

一切如我们的预期,这表示我们可以很愉快的使用 TS 开发了。

2. 安装 ESLint + Prettier

我们看上面的 <Title /> 组件,看着有点膈应,好像不该换行的它换行了,该换行的没换行。我们总不能手动的去按回车、删空格吧?

这时我们使用 Prettier 进行代码格式化,相比于 ESLint 中的代码格式规则,它更加专业。同时我们采用 ESLint 来统一代码风格,提高我们的代码质量。

ESLint 将我们的代码解析成 AST,通过检测 AST 来判断代码是否符合我们设置的规则,往往不同公司团队会自定义一套自己的 ESLint 规范。

我们先来安装一下 PrettierESLint

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;
}

有可能会出现下面问题,我们手动安装一下就可以了

image.png

npm install autoprefixer@9.0.0 --save-dev

再次运行,看看是否可行?发现还是不行,我们看看报什么错?

image.png

解决此问题需要我们在项目根目录下创建 postcss.config.js,添加一下配置

javascript
module.exports = {
  plugins: {
    autoprefixer: {
      overrideBrowserslist: ['> 0.5%', 'last 5 versions'],
    },
  },
};

再次运行,看看效果,我们可以看到,类名的格式为[组件名]_[当前类名]_[哈希值取5位],从而形成独一无二的 class 名字,不会与其他选择器重名。至此我们完成了样式相关的配置处理。

image.png

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 会报错,说找不到模块,实际上我们的路径是正确的,图片也能正常显示

image.png

这时候我们只需要在 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 定义了全局变量 process.env.NODE_ENV。并在 package.json 的脚本命令 start:main 中又声明了 NODE_ENV,目前代码仓库已全量更新。小伙伴们可前往 init-cli 分支 看最新代码。下面来说明一下这两者区别。

得先明白一点就是,为什么要配置这个,在前端工程化配置中,一般我们都会有许多环境,如生产环境/开发环境等。在 node 中有全局变量 process 表示当前 node 进程,process.env 包含着关于系统环境的信息。但是 process.env 中并不存在 NODE_ENV 这个玩意。所以我们通过定义全局变量来区分环境的不同。

在 package.json 的 scripts 命令中,通过添加 NODE_ENV=development,表示 node 服务启动时,将 NODE_ENV 挂载到 process.env 上。在 Linux 和 Mac 上直接这么写是没问题的,但 Widnow 上就会存在问题,这也就是小伙伴们反馈 window 上无法运行的根本原因。如果你一定要在脚本命令中添加一些配置参数,那么建议通过第三方 cross-env 库,抹平掉不同平台之间的差异。

再来说说 webpack.DefinePlugin 中定义的全局变量,它表示 webpack 在编译过程中,所有此变量,都会被替换成我们定义的值。

既然两者都能定义变量,那么采用哪种会比较好呢?由于我们的主进程和渲染进程都是经过 webpack 编译,我个人建议是通过 webpack.DefinePlugin。当然不用自己写环境变量的定义。因为在 webpack 配置中,我们定义了 mode 配置选项,通过官网我们可以看到:

image.png

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)

image.png

一般我们开发过程中,会在项目中安装 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 搭建起来后,还没开始写几行代码,就迫不及待想打包成桌面应用,毕竟搞出自己第一个桌面应用,是一件很骄傲自豪的事情!!!但是又不想等整个小册都完结,才能上手打包构建,为此,阿宽新增一章节 🏆 支线篇-打包生成第一个桌面应用(骄傲自豪)

此章节为支线任务,与主流程(主线任务)无任何关系。可放心使用,如果在打包期间遇到问题,可加我微信或进群讨论~