2021-05-23 20:37:41 +08:00

186 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Vite 整合 Electron 总结
## 前言
- Vite 是面向未来几年的构建工具,很有必要在各个场景下都试试集成进来
- Electron 作为前端标配的桌面开发工具 `@vue/cli` 官方有给出模板
但是 Vite 这块并没提供,毕竟人家定位是和 `webpack` 那样的通用构建工具,甚至连 Vue 都没集成 🖖
那么 Electron 这块集成设计思路按照 vue 的集成风格,应当写一个插件!
## 注意 📢
- 这里假定你简单的知道一些 Vite 的工作原理,这种文章网上有好多的
- 项目整体所有代码在这 [https://github.com/caoxiemeihao/electron-vue-vite](https://github.com/caoxiemeihao/electron-vue-vite) 可以直接 **用于生产** (还要辛苦各位亲点个 start 😘)
## 插件设计分析
1. Vite 在开发期可以简单的理解为用的 `koa` + `esbuild` 组合的形式,试想下如果你写了如下代码
```ts
// src/render/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { ipcRenderer } from 'electron'
console.log('ipcRenderer:', ipcRenderer)
createApp(App)
.mount('#app')
```
那么可能你会收到下面的报错
![import-electron.jpg](./images/import-electron.jpg)
2. 事实上在 `'electron'` 在 Electron 运行环境中是一个 **内置模块** 你可以尝试在控制体中试试下这段代码
> *注意这里先不要引入 Electron 相关的包,保障项目能跑起来*
```js
require.resolve('electron')
"electron" // 将会输出
```
既然 Electron 本就支持全量的 NodeJs API 我们不妨直接在在代码中直接写成
```js
const { ipcRenderer } = require('electron')
```
可以打包票说,这个可以用!(仅限开发期);但是这个会带来两个问题
* 编码风格不统一,人家都在用 `ESModule` 混入 `CommonJs` 确实不好看
* `require('xxxx')` 在打包期间如果不做些处理,并不会被 Rollup 处理 (这里只`.ts`文件,有大神知道怎么对付这种情况的请指点下小弟)
如果你引入的是 `node_modules` 中的包那可就惨了;比如 `require('electron-store')` 这种会原样输出;打包后的程序开起来会找不到 `'electron-store'` 这个模块,铁定报错!
3. 以上两点表现了 `ESModule` 写法在开发期直接运行会报错;如果我们告诉 Vite 这个是内置的模块,不要去处理直岂不是切角柜解决报错了;
思路是把 ESModule 转换成 NodeJs 内置的 CommonJs 形式即可;甚至只要有关 NodeJs API 的包我们都可以转化,毕竟开发期项目根目录是有 `node_modules` 这个 **NodeJs包仓库** 给你用的!
我们可以先去启动好的项目控制台中试试
> *注意这里先不要引入 Electron 相关的包,保障项目能跑起来*
![require-electron.jpg](./images/require-electron.jpg)
**这两个包开发环境下能够正常加载!**
很好!那按照我们的假设,预期的代码行为应该是这样:
```ts
import { ipcRenderer } from 'electron'
import Store from 'electron-store'
// Will generate
const { ipcRenderer } = require("electron")
const Store = require("electron-store")
```
4. 分析至此,我们该动动手写个插件了;让插件去自动化完成 `esm2cjs`
## vitejs-plugin-electron
- Vite 插件上手教程请看官网 [https://vitejs.dev/guide/api-plugin.html](https://vitejs.dev/guide/api-plugin.html) (我个人觉得比 `webpack` 那边的插件要好写)
- 为了支持传参方便日后扩展,我们把它成一个 Function 并返回一个插件
- 代码处理这块,我们需要一个 AST 工具帮忙 - `yarn add acorn`
```ts
import * as acorn from 'acorn'
import { Plugin as VitePlugin } from 'vite'
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue'] // 需要处理的文件后缀
export interface Esm2cjsOptions {
excludes?: string[] // 需要被转换的模块
}
export default function esm2cjs(options?: Esm2cjsOptions): VitePlugin {
const opts: Esm2cjsOptions = {
// 默认我们转换 electron、electron-store 两个模块
excludes: [
'electron',
'electron-store',
],
...options
}
return {
name: 'vitejs-plugin-electron', // 这个 name 就是插件名字
transform(code, id) {
const parsed = path.parse(id) // 解析引入模块的路径id 即引入文件完整路径
if (!extensions.includes(parsed.ext)) return // 只处理需要处理的文件后缀
const node: any = acorn.parse(code, { // 使用 acorn 解析 ESTree
ecmaVersion: 'latest', // 指定按照最新的 es 模块标准解析
sourceType: 'module', // 指定按照模块进行解析
})
let codeRet = code
node.body.reverse().forEach((item) => {
if (item.type !== 'ImportDeclaration') return // 跳过非 import 语句
if (!opts.excludes.includes(item.source.value)) return // 跳过不要转换的模块
/**
* 下面这些 const 声明用来确定 import 的写法
*/
const statr = codeRet.substring(0, item.start)
const end = codeRet.substring(item.end)
const deft = item.specifiers.find(({ type }) => type === 'ImportDefaultSpecifier')
const deftModule = deft ? deft.local.name : ''
const nameAs = item.specifiers.find(({ type }) => type === 'ImportNamespaceSpecifier')
const nameAsModule = nameAs ? nameAs.local.name : ''
const modules = item.
specifiers
.filter((({ type }) => type === 'ImportSpecifier'))
.reduce((acc, cur) => acc.concat(cur.imported.name), [])
/**
* 这里开始根据各种 import 语法做转换
*/
if (nameAsModule) {
// import * as name from
codeRet = `${statr}const ${nameAsModule} = require(${item.source.raw})${end}`
} else if (deftModule && !modules.length) {
// import name from 'mod'
codeRet = `${statr}const ${deftModule} = require(${item.source.raw})${end}`
} else if (deftModule && modules.length) {
// import name, { name2, name3 } from 'mod'
codeRet = `${statr}const ${deftModule} = require(${item.source.raw})
const { ${modules.join(', ')} } = ${deftModule}${end}`
} else {
// import { name1, name2 } from 'mod'
codeRet = `${statr}const { ${modules.join(', ')} } = require(${item.source.raw})${end}`
}
})
return codeRet
},
}
}
```
-`vite.config.ts` 中使用 `vitejs-plugin-electron`
```ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vitejs-plugin-electron'
export default defineConfig((env) => ({
plugins: [
vue(),
electron(),
],
// 其他配置略...
}))
```
- 再次运行下项目
![plugin-electron.jpg](./images/plugin-electron.jpg)
- **It's Worked** 🎉 🎉 🎉
- 细心的童鞋可能发现了我们的代码上面还有一行
```js
const fs = require("fs");
```
看到这个我**有点懵**,我也是开起来项目时候才发现的这,看来关于原生 API 人家 Vite 也是这么干的?可以我没有证据(源码还没看完),不过这确实不是我干的 😂
- 好了,这个插件可以用了;再想想上面的关于 Rollup 的配置其实我们完全可以集成到 `vitejs-plugin-electron` 中的,这样会使 `vite.config.ts` 文件更少、更清晰;具体代码就不演示了,自己拉代码看看吧 🚀
- [https://github.com/caoxiemeihao/vitejs-plugins/tree/main/electron](https://github.com/caoxiemeihao/vitejs-plugins/tree/main/electron)