npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vite-plugin-native

v2.2.2

Published

Supports Node/Electron C/C++ native addons

Downloads

482

Readme

vite-plugin-native

支持在 Node/Electron 中使用 C/C++ native addons. 这是一个基于 Webpack 的 bundle 方案。

感谢 Erick Zhao 提供灵感 :)

NPM version NPM Downloads

English | 简体中文

Install

npm i -D vite-plugin-native

Usage

import native from 'vite-plugin-native'

export default {
  plugins: [
    native({
      // Enable Webpack
      webpack: {},
    })
  ]
}

API

export interface NativeOptions {
  /** @default 'node_natives' */
  assetsDir?: string
  /** By default native modules are automatically detected if this option is not explicitly configure by the user. */
  natives?: string[] | ((natives: string[]) => string[])
  /** Enable and configure webpack. */
  webpack?: {
    config?: (config: Configuration) => Configuration | undefined | Promise<Configuration | undefined>
    'node-loader'?: NodeLoaderOptions,
    '@vercel/webpack-asset-relocator-loader'?: WebpackAssetRelocatorLoader,
  },
}

工作原理

esm 与 cjs

esm 标准导入模块的方式,是使用写在 js 文件顶层作用域 import 语句,并且模块 id 必须是一个固定的字符串常量。这使得所有行为都是可预测的。这对于 bundler 十分的友好,甚至可以非常轻松的实现 tree-shake 功能。其次导出的模块成员必须是具名的,默认为名字为 default。

cjs 标准导入模块的方式,是使用 require 函数导入模块,require 函数与普通的 js 函数没有什么区别,这就决定了 require 函数可以写在任意的作用域且导入模块 id 可以是任何的 js 变量、字符串常量、表达式。这与 import 语句有着本质的区别,也就是说 cjs(require) 的模块规范要比 esm(import) 模块灵活的多,甚至导出模块成员可以是匿名的;这样的行为差异决定了很多时候一个 cjs 模块是无法有效的转换为 esm 模块的,它们的运行行为差异非常大。

esm 与 cjs 之间的相互导入/导出的转换是所有 bundler/transpiler 都会面临的一个问题,如 Vite、Webpack、esbuild、swc、tsc 等等。关于模块转换有个专有名词叫 interop。

我之前在 B 站讲过一个视频 👉🏻 细讲⚡️vite-plugin-commonjs(上)

作用域 与 同步/异步

Vite(Rollup) 对于构建顶层作用域中使用 import 语句导入的模块是没有作用域概念的,代码的 bundle 规则是基于 export 导出成员的名字,它要求模块作者遵循 esm 规范。这样做理论上不会产生什么副作用,因为模块所有导出成员均在顶层作用域且为同步行为,所以这就确定了一切行为都是可预测的。

当然如果你使用 import() 函数导入模块,它更像一个普通的 js 函数,并且将不会有顶层作用域限制。导入语句可以写在任意作用域且模块导入永远为异步行为。Vite(Rollup) 默认是不会将 import() 函数导入的模块合并到 bundle.js 中,如果强行合并到 bundle.js 中可能会导致一些不可预测的副作用,例如用户在被 import() 导入的模块中的顶层作用域立即执行了一些逻辑,这可能会与用户想象中的行为不一致,因为模块会被立即执行!导致执行时机被提前,而不是 bundle.js 由上至下至执行到 import() 函数时候才被加载并执行 👉🏻 output.inlineDynamicImports

require 与 import/import()

我们知道了模块导入的作用域,同步/异步的概念后,这将决定了 Vite(Rollup) 能否能顺利的将 require 与 import/import() 转换成功,也即 interop 问题。

模块互操作(interop)

import 语句与 import() 函数很容易转换成 require 且无任何的副作用。

// Global scope

import foo from './foo'
↓↓↓↓ ✅
const { default: foo } = require('./foo')

import * as foo from './foo'
↓↓↓↓ ✅
const foo = require('./foo')

import { bar, baz } from './foo'
↓↓↓↓ ✅
const { bar, baz } = require('./foo')

function func(name) {
  // Function scope

  const foo = import(`./${name}`)
  ↓↓↓↓ ✅
  const foo = Promise.resolve(require(`./${name}`))
}

require 函数换成 import 语句可能会失败!

// Global scope

const foo = require('./foo') // 需要配合 Object.defineProperty(exports, "__esModule", { value: true });
↓↓↓↓ 🚧
import foo from './foo'

const foo = require(`./${name}`)
↓↓↓↓ ❌
import foo from `./${name}` // 不支持使用变量

function func(name) {
  // Function scope

  const foo = require(`./${name}`)
  ↓↓↓↓ ❌
  const foo = import(`./${name}`) // `require` 是同步 API
}

中心化模块管理

Webpack/esbuild 的 bundle 方案支持使用 require 函数导入模块并且可以无视作用域。他们能作到这点的主要原因都是采用了模块集中管理的策略;在 Webpack 中所有模块都被挂在到一个名为 __webpack_modules__ 的变量上并且使用统一 __webpack_require__ 加载函数加载,在 esbuild 中所有模块均被包裹在一个 __commonJS 模块管理函数中,并返回该模块的加载函数。

Vite 的预处理

Vite 中有个概念是 Pre-Bundling,它有一个很重要的作用就是将 cjs 模块提前构建成一个 bundle.js 然后通过 esm 的形式导出模块,这很好的解决了 interop 的问题。试想一下面有一段包含条件导入模块的代码段,让我们看看预构建是如何处理它的。

// add.js
exports.add = (a, b) => a + b

// minus.js
exports.minus = (a, b) => a - b

// math.js
exports.calc = (a, b, operate) => {
  const calc = operate === '+' // condition require
    ? require('./add').add(a, b)
    : require('./minus').minus(a, b)
  return calc(a, b)
}

Output a bundle.js file with Vite's Pre-Bundling

var __commonJS = (cb, mod = { exports: {} }) => function __require2() {
   const cjs_wrapper = Object.values(cb)[0] // wrapper function
   cjs_wrapper(mod.exports, mod); // inject exports, module
   return mod.exports;
};

// add.js
var require_add = __commonJS({
  "add.js"(exports, module) {
    exports.add = (a, b) => a + b
  }
})

// minus.js
var require_minus = __commonJS({
  "minus.js"(exports, module) {
    exports.minus = (a, b) => a - b
  }
})

// math.js
var require_math = __commonJS({
  "math.js"(exports, module) {
    exports.calc = (a, b, operate) => {
      const calc = operate === '+' // condition require
        ? require_add().add(a, b)
        : require_minus().minus(a, b)
      return calc(a, b)
    }
  }
});

// Finally export a esmodule ✅
export default require_math(); // { calc: Function }

Vite 与 Webpack 产物

假设我们有如下两个文件分别为 add.js 与 index.js:

add.js

export function add(a, b) {
  return a + b
}

index.js

import { add } from './add'

const sum = add(1, 2)

Vite(Rollup) 默认只支持 esm 格式的文件,也就是模块引用需要使用 import 语句。这个设定很好,因为它是 ECMAScript 的模块标准;同样这会带来一些天然的好处比如很容易支持 tree-shake 这是因为 import/export 语句规定所有导入/导出模块必须写在 js 文件顶层作用域,也即它们的行为可预测的。Rollup 的构建产物给人最直观的感觉就是所见即所得,非常符合代码书写的顺序与直觉。

Bundled with Vite

// add.js
function add(a, b) {
  return a + b
}

// index.js
const sum = add(1, 2)

Webpack 的构建产物是以遵循 cjs 规范的方式组织代码,所以有 模块中心(modules)、模块导出挂载点(module.exports)、模块加载函数(require) 等概念。而这些概念在 Rollup 中通通没有;这是两者之间最大的区别,也即两者对模块导入/导出处理的不同。

Bundled with Webpack

var __webpack_modules__ = {
  // index.js
  0: (module, exports, __webpack_require__) => {
    const { add } = __webpack_require__(1)
    const sum = add(1 + 2)
  },
  // add.js
  1: (module, exports, __webpack_require__) => {
    function add(a, b) {
      return a + b
    }
    module.exports = { add };
  },
  ...
}

module.exports = __webpack_require__(0)

Webpack 首先会为所有会为所有模块提供一个 cjs 外壳代码,并且注入 模块挂载点(module, exports),模块引入函数(require)。这与 node.js 的模块加载行为完全一致,可以说 Webpack 十分适合构建 Node/Electron 应用,这个场景天然优于使用 Vite(Rollup) 构建。

重头戏 C/C++ 模块

C/C++ 扩展是为了 Node/Electron 准备的高性能方案。社区对于构建 C/C++ 模块大部分情况下会使用 node-pre-gyp,node-gyp-build 等工具构建成为 .node 文件,然后放到固定的目录结构中,并且提供一个 bindings 工具用于加载 .node 文件,通常它们可能是这样的:

// node_modules/sqlite3/build/Release/node_sqlite3.node
require('bindings')('node_sqlite3.node');
// node_modules/better-sqlite3/build/Release/better_sqlite3.node
require('bindings')('better_sqlite3.node');

C/C++ 构建的 .node 文件本质上是一个 cjs 模块,无论怎样它都不支持使用 import 语句或者 import() 函数加载,所以我们只能使用 require 函数加载它。使用 Webpack 工具构建在兼容性上具有天然的优势,因为它们都基于 cjs 格式。且 Webpack 还具有丰富的插件系统。如果使用 Webpack 将 C/C++ 模块构建成一个 bundle.js 并且以一个 esm 模块格式导出,这既能享受 Webpack 强大成熟的生态系统又能做到兼容 Vite(Rollup)。你可以想到这个方法和上面讲到的 预构建(Pre-Bundling) 思路如出一辙,事实却是如此!

为什么是 Webpack

你可能已经意识到 Vite 的 Pre-Bundling 的行为与 Webpack 的行为趋同,为什么我们不直接使用 Pre-Bundling 构建 C/C++ 模块,而且它也是基于 cjs 格式的 bundle 方案。主要是 Webpack 的生态优势,这能让我们少做很多事情且低风险。

基于 Vite API 方案

未来是否会提供一个基于 Vite(Rollup) 自身 API 的方案?暂时没有明确计划。作者目前就职于一个与该插件毫无关系的商业公司,对于此项目的维护仅仅是出于对 Vite/Electron 社区的热爱!