Require CSS in Electron

Node.js

在实际开发过程中,可能会遇到在 Electron 项目中需要引用组件库的情况。因为组件库往往除了 JavaScript 文件之外,还连带有 CSS 文件,因此一般即便有 lib 文件的输出(也就是 JavaScript 经过了预编译,在 Node.js 环境下可以直接运行),也没法直接使用。这里 CSS 文件一般通过 import 'styles.css'; 这样的语法引入,编译后会变成 require('styles.css')。由于 Node.js 不支持直接 require CSS 文件,因此 lib 在 Electron 下是没法直接运行的。

针对这种情况,常规的做法是通过 Webpack 将组件库打包到最终的产物中,通过 css-loader 和 mini-css-extract-plugin 消化 CSS 产物,最终在 Electron / Web 环境下运行。

然而,这样的做法可能面临几个问题:

  1. 如果 Electron 的不同 Webview 需要组件库的不同部分(不完全重叠),那么实践上最方便的做法只能是分别打包。相当于同一份代码被复制了多份,存在于各自的打包产物中,造成了包体积的浪费;
  2. 上面的做法其实是 Web 的,Electron 环境的优势(包含 Node.js)并没有体现出来。

因为 Node.js 在进行文件载入(require)的时候,提供了扩展能力。因此,只需要做如下的代码增强,在 Electron 项目中也可以直接 require CSS 文件了:

const fs = require('fs');
// 当 Node.js 需要 require 任意后缀是 .css 的文件时,就会执行这个自定义的回调
require.extensions['.css'] = function (module, filename) {
  const css = fs.readFileSync(filename, 'utf8');
  /**
   * 这里让 Electron 支持 require CSS 的思路非常简单,类似于 Webpack 中的 style-loader:
   * 首先创建一个 <style> 标签;
   * 然后将 CSS 文件的内容读取出来;
   * 将 CSS 的内容插入到 <style> 中并最终插入到 <head> 里面;
   * 剩下的渲染工作就交给浏览器了。
   */
  const js = [
    `const css = ${JSON.stringify(css)};`,
    /**
     * 将引用文件的路径作为 id
     * 用于确保同一个 CSS 文件不会因为多次 require 而被重复插入
     */
    `const id = ${JSON.stringify(filename)};`,
    'if (document.head.getElementById(id)) return;',
    'const style = document.createElement("style")',
    'style.id = id;',
    'style.textContent = css;',
    'document.head.appendChild(style);',
  ].join('');
  return module._compile(js, filename);
}

在上述代码中,参考了 style-loader 的思路,实现了一个简单的从 CSS 转化到 JavaScript 代码的操作。剩下的编译工作交给原来 Node.js 的流程去做就可以了(module._compile 部分的代码)。