@b9g/crank

中文文档
0.7.12发布于 2026/09/05MITTypeScriptESM + CJS零依赖近 30 天下载 3691

Just JavaScript UI 框架.

中文使用文档
文档目录27 节
  1. 开始
  2. 动机
  3. 为什么是克兰克?
  4. 安装
  5. 正在导入 Crank 中 自动 JSX转型.
  6. 导入 JSX 模板标签 .
  7. ECMAScript 模块 CDNs
  8. 主要实例
  9. 一个简单的组件
  10. 国家部分
  11. Async 组件
  12. 装入组件
  13. 通用工具配置
  14. 类型脚本
  15. 宝贝
  16. ESLint 语录
  17. 天体
  18. API 参考
  19. 核心出口
  20. 组件类型
  21. 上下文 API
  22. 特殊道具
  23. 特殊元素标记
  24. Async公用事业(v0.7+)
  25. 生命周期模式
  26. 高级图案
  27. 类型脚本支持
Crank.js Logo

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}你这混蛋

Just Java 脚本 UI 框架

开始

脚手架新项目:

npm create crank

或者立刻试试 在线操场。 。 。 .

其他链接 :

动机

一个感觉像JavaScript的框架.

// State is defined with generator components
function* Timer() {
  // setup goes here
  let seconds = 0;
  const interval = setInterval(() => this.refresh(() => seconds++), 1000);

  for ({} of this) {
    yield <div>Seconds: {seconds}</div>;
  }

  clearInterval(interval); // Cleanup just works
}

renderer.render(<Timer />, document.body);

// Async components just work on client and server
async function UserProfile({userId}) {
  const user = await fetchUser(userId);
  return <div>Hello, {user.name}!</div>;
}

await renderer.render(<UserProfile />, document.body);

为什么是克兰克?

克兰克依靠语言而不是再造语言. 州为地 变量,在生成器范围内。 更新是重新执行:呼叫 refresh() 再次运行循环。 装入状态是 async 函数, 等着呢 设置是循环之前的代码;清理是循环之后的代码. 没有钩子,没有依赖数组,没有编译器,也没有规则 控制流动是生命的循环.

因为组件是简单的函数和发电机,所有你已经 了解JavaScript应用. 关闭持有状态,模块共享逻辑,以及 try编号 :finally 处理撕裂。 如果你能读取 JavaScript, 你可以读取一个 Crank组件.

安装

Crank 软件包在 国家预防机制 结束 页:1 @b9g 组织 (减: 页:1迈克沙文页:1.

npm i @b9g/crank

正在导入 Crank 中 自动 JSX转型.

/** @jsxImportSource @b9g/crank */
import {renderer} from "@b9g/crank/dom";

renderer.render(
  <p>This paragraph element is transpiled with the automatic transform.</p>,
  document.body,
);

导入 JSX 模板标签 .

从版本开始 0.5,克兰克包船a 标记模板 函数 它提供了类似的语法和语义 随着JSX的转变. 这样您就可以在香草中写入 Crank 组件 Java脚本.

import {jsx} from "@b9g/crank/standalone";
import {renderer} from "@b9g/crank/dom";

renderer.render(jsx`
  <p>No transpilation is necessary with the JSX template tag.</p>
`, document.body);

ECMAScript 模块 CDNs

CRANK 也可以在CDN上读取,比如: 日记 (https://cdn.jsdelivr.net/npm/@b9g/crank/)和 esm.sh (英语) (https://esm.sh/@b9g/crank)用于无害环境管理准备环境中.

/** @jsx createElement */
import {createElement} from "https://cdn.jsdelivr.net/npm/@b9g/crank/crank.js";
import {renderer} from "https://cdn.jsdelivr.net/npm/@b9g/crank/dom.js";

renderer.render(
  <div id="hello">
    Running on <a href="https://www.jsdelivr.com">jsDelivr</a>
  </div>,
  document.body,
);

主要实例

一个简单的组件

import {renderer} from "@b9g/crank/dom";

function Greeting({name = "World"}) {
  return (
    <div>Hello {name}</div>
  );
}

renderer.render(<Greeting />, document.body);

国家部分

function *Timer(this: Context) {
  let seconds = 0;
  const interval = setInterval(() => this.refresh(() => seconds++), 1000);
  for ({} of this) {
    yield <div>Seconds: {seconds}</div>;
  }

  clearInterval(interval);
}

Async 组件

import {renderer} from "@b9g/crank/dom";
async function Definition({word}) {
  // API courtesy https://dictionaryapi.dev
  const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`);
  const data = await res.json();
  if (!Array.isArray(data)) {
    return <p>No definition found for {word}</p>;
  }

  const {phonetic, meanings} = data[0];
  const {partOfSpeech, definitions} = meanings[0];
  const {definition} = definitions[0];
  return <>
    <p>{word} <code>{phonetic}</code></p>
    <p><b>{partOfSpeech}.</b>{" "}{definition}</p>
  </>;
}

await renderer.render(<Definition word="framework" />, document.body);

装入组件

import {Fragment} from "@b9g/crank";
import {renderer} from "@b9g/crank/dom";

async function LoadingIndicator() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return (
    <div>
      🐕 Fetching a good boy...
    </div>
  );
}

async function RandomDog({throttle = false}) {
  const res = await fetch("https://dog.ceo/api/breeds/image/random");
  const data = await res.json();
  if (throttle) {
    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  return (
    <div>
      <a href={data.message} target="_blank" style="text-decoration: none; color: inherit;">
        <img
          src={data.message}
          alt="A Random Dog"
          width="300"
        />
        <div>
          Click to view full size
        </div>
      </a>
    </div>
  );
}

async function *RandomDogLoader({throttle}) {
  // for await can be used to race component trees
  for await ({throttle} of this) {
    yield <LoadingIndicator />;
    yield <RandomDog throttle={throttle} />;
  }
}

function *RandomDogApp() {
  let throttle = false;
  this.addEventListener("click", (ev) => {
    if (ev.target.tagName === "BUTTON") {
      this.refresh(() => throttle = !throttle);
    }
  });

  for ({} of this) {
    yield (
      <div>
        <RandomDogLoader throttle={throttle} />
        <div>
          <button>
            Show me another dog!
          </button>
          <div>
            {throttle ? "Slow mode" : "Fast mode"}
          </div>
        </div>
      </div>
    );
  }
}

renderer.render(<RandomDogApp />, document.body);

通用工具配置

下面是用克兰克开始的配置的不完整列表.

类型脚本

TypeScript是被打入的JavaScript的超级集.

以下是您需要设置的 JSX 自动转写配置 .

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@b9g/crank"
  }
}

克兰克用TypeScript来书写,并带有类型. 参见 指导 类型脚本 为了更多 有关克兰克类型的信息.

import type {Context} from "@b9g/crank";
function *Timer(this: Context<typeof Timer>) {
  let seconds = 0;
  const interval = setInterval(() => this.refresh(() => seconds++), 1000);
  for ({} of this) {
    yield <div>Seconds: {seconds}</div>;
  }

  clearInterval(interval);
}

宝贝

Babel是一个受欢迎的开源JavaScript编译器,它允许您用现代语法(包括JSX)来编写代码,并在不支持语法的环境中运行.

这是如何让Babel 转写JSX给克兰克.

自动转换 :

{
  "plugins": [
    "@babel/plugin-syntax-jsx",
    [
      "@babel/plugin-transform-react-jsx",
      {
        "runtime": "automatic",
        "importSource": "@b9g/crank",

        "throwIfNamespace": false,
        "useSpread": true
      }
    ]
  ]
}

ESLint 语录

ESLint是一个流行的开源工具,用于分析和检测JavaScript代码中的问题.

Crank 提供了在软件包名下与 ESLint 合作的配置预设 eslint-plugin-crank。 。 。 .

npm i eslint eslint-plugin-crank

在您的 Eslint 配置中 :

{
  "extends": ["plugin:crank/recommended"]
}

天体

Astro.js是一个现代的静态站点建造者和框架.

Crank 提供了一个 Astro 集成系统 使服务器端渲染和客户端与Astro进行水合.

npm i astro-crank

在你身边 astro.config.mjs。 。 。 .

import {defineConfig} from "astro/config";
import crank from "astro-crank";

// https://astro.build/config
export default defineConfig({
  integrations: [crank()],
});

API 参考

核心出口

import {
  createElement,
  Fragment,
  Copy,
  Portal,
  Raw,
  Text,
  Context
} from "@b9g/crank";

import {renderer} from "@b9g/crank/dom"; // Browser DOM
import {renderer} from "@b9g/crank/html"; // Server-side HTML

import {jsx, html} from "@b9g/crank/standalone"; // Template tag (no build)

import {Suspense, SuspenseList, lazy} from "@b9g/crank/async";

组件类型

职能构成部分 - 无国籍

function Greeting({name = "World"}) {
  return <div>Hello {name}</div>;
}

发电机构成部分 - 严谨的 function*

function* Counter() {
  let count = 0;
  const onclick = () => this.refresh(() => count++);

  for ({} of this) {
    yield <button onclick={onclick}>Count: {count}</button>;
  }
}

Async 组件 - 用途 async 承诺

async function UserProfile({userId}) {
  const user = await fetch(`/api/users/${userId}`).then(r => r.json());
  return <div>Hello, {user.name}!</div>;
}

Async 发电机组件 - 状态+同步

async function* DataLoader({url}) {
  for ({url} of this) {
    const data = await fetch(url).then(r => r.json());
    yield <div>{data.message}</div>;
  }
}

上下文 API

内容如下: this 在组件中(或作为第2个参数).

function Component(props, ctx) {
  console.log(this === ctx); // true
  return props.children;
}

属性

this.props - 当前道具(只读)

this.isExecuting - 该组件是否正在执行

this.isUnmounted - 组件是否已卸载

方法

this.refresh(callback?) - 触发器重新执行

this.refresh();                    // Simple refresh
this.refresh(() => count++);       // With state update (v0.7+)

this.schedule(callback?) - DOM 完成后执行

// el is whatever the component returns Node/Text/HTMLElement/null, an array of dom nodes, etc
this.schedule((el) => {
  console.log("Component rendered", el.innerHTML);
});

this.after(callback?) -DOM直播后执行

// this runs after the DOM nodes have finally entered the DOM
// this is where you put things like autofocus
this.after((el) => {
  console.log(el.isConnected); // true
});

this.cleanup(callback?) - 在卸载时执行

function* Component() {
  const interval = setInterval(() => this.refresh(), 1000);
  this.cleanup(() => clearInterval(interval));

  for ({} of this) {
    yield <div>Tick</div>;
  }
}

this.addEventListener(type, listener, options?) - 倾听事件

this.addEventListener("click", (e) => console.log("Clicked!"));

this.dispatchEvent(event) - 调度活动

this.dispatchEvent(new CustomEvent("mybuttonclick", {
  bubbles: true,
  detail: {id: props.id}
}));

this.provide(key, value) 编号 : this.consume(key) - 上下文 API

// Provider
function* ThemeProvider() {
  this.provide("theme", "dark");
  for ({} of this) {
    yield this.props.children;
  }
}

// Consumer
function ThemedButton() {
  const theme = this.consume("theme");
  return <button class={theme}>Click me</button>;
}

重复

for ({} of this) - 渲染回路(同步)

function* Component() {
  for ({} of this) {
    yield <div>{this.props.message}</div>;
  }
}

for await ({} of this) - Async为赛跑树制作回路

async function* AsyncComponent() {
  for await ({} of this) {
    // Multiple yields race - whichever completes first shows
    yield <Loading />;
    yield <Content />;
  }
}

特殊道具

key - 唯一的调节标识符

<ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>

ref - 访问制成的DOM元素

<audio ref={(el) => (audio = el)} />

// Forward refs through components
function MyInput({ref, ...props}) {
  return <input ref={ref} {...props} />;
}

copy - 预防/控制重新投降

// Boolean: prevent rendering when truthy
<li copy={!el.hasChanged}>{el.value}</li>

// string: copy specific props
<input copy="!value" type="text" />        // Copy all except value
<div copy="class id" />                    // Copy only class and id
<div copy="children" />                    // Copy children

hydrate - SSR水分控制

<div hydrate={false}>                      // Skip hydration
<Portal hydrate={true}>                    // Force hydration
<input hydrate="!value" />                 // Hydrate all except value

class - 字符串或对象(v0.7+)

<button class="btn active" />

<button class={{
  btn: true,
  'btn-active': isActive,
  'btn-disabled': isDisabled
}} />

style - CSS 字符串或对象

<div style="color: red; font-size: 16px" />
<div style={{"font-size": "16px", color: "blue"}} />

innerHTML - Raw HTML 字符串( XSS 风险)

<div innerHTML="<strong>Bold</strong>" />

事件宣传 - 小写事件处理器

<button onclick={handler} />
<input onchange={handler} oninput={handler} />
<form onsubmit={handler} />

主张命名 - 支持方便 HTML 名称

<label class="my-label" for="my-id">Label</label>
// Instead of className and htmlFor

特殊元素标记

<Fragment> - 儿童无包装

import {Fragment} from "@b9g/crank";

<Fragment>
  <div>Child 1</div>
  <div>Child 2</div>
</Fragment>

// Or use: <>...</>
// The Fragment tag is the empty string

<Copy /> - 防止元素重新落地

import {Copy} from "@b9g/crank";

function memo(Component) {
  return function* Wrapped(props) {
    yield <Component {...props} />;
    for (const newProps of this) {
      if (equals(props, newProps)) {
        yield <Copy />;  // Reuse previous render
      } else {
        yield <Component {...newProps} />;
      }
      props = newProps;
    }
  };
}

<Portal> - 进入不同的DOM节点

import {Portal} from "@b9g/crank";

const modalRoot = document.getElementById("modal-root");

function Modal({children}) {
  return (
    <Portal root={modalRoot}>
      <div class="modal">
        {children}
      </div>
    </Portal>
  );
}

<Raw> - 插入原始 HTML 或 DOM 节点

import {Raw} from "@b9g/crank";

function MarkdownViewer({markdown}) {
  const html = marked(markdown);
  return <div><Raw value={html} /></div>;
}

// Or insert DOM node
<Raw value={domNode} />

<Text> - 明确文本节点创建(v0.7+)

import {Text} from "@b9g/crank";

<Text value="Hello world" />

// Access Text nodes in lifecycle
function* Component() {
  this.schedule((node) => {
    if (node instanceof Text) {
      console.log("Text node:", node);
    }
  });
  for ({} of this) {
    yield "Text content";  // Becomes a Text node
  }
}

Async公用事业(v0.7+)

lazy(loader) - 懒惰部件

import {lazy} from "@b9g/crank/async";

const LazyComponent = lazy(() => import("./MyComponent.js"));

<Suspense fallback={<div>Loading...</div>}>
  <LazyComponent />
</Suspense>

Suspense - 申报装载状态

import {Suspense} from "@b9g/crank/async";

<Suspense fallback={<div>Loading...</div>}>
  <AsyncComponent />
</Suspense>

SuspenseList - 协调多个合成组件

import {SuspenseList} from "@b9g/crank/async";

<SuspenseList>
  <Suspense fallback={<div>Loading 1...</div>}>
    <Item1 />
  </Suspense>
  <Suspense fallback={<div>Loading 2...</div>}>
    <Item2 />
  </Suspense>
</SuspenseList>

生命周期模式

挂载 - 代码之前 yield

function* Component() {
  console.log("Mounting...");
  const interval = setInterval(() => this.refresh(), 1000);

  for ({} of this) {
    yield <div>Tick</div>;
  }

  clearInterval(interval);  // Cleanup
}

更新 - 里面的代码使循环

function* Component() {
  for ({} of this) {
    console.log("Updated with:", this.props);
    yield <div>{this.props.message}</div>;
  }
}

清理 - 循环或通过后代码 this.cleanup()

function* Component() {
  const interval = setInterval(() => this.refresh(), 1000);
  this.cleanup(() => clearInterval(interval));

  for ({} of this) {
    yield <div>Tick</div>;
  }
}

高级图案

上级部门

function withLogger(Component) {
  return function* WrappedComponent(props) {
    console.log("Rendering with:", props);
    for ({} of this) {
      yield <Component {...props} />;
    }
  };
}

钩子

function useInterval(ctx, callback, delay) {
  let interval = setInterval(callback, delay);
  ctx.cleanup(() => clearInterval(interval));
  return (newDelay) => {
    delay = newDelay;
    clearInterval(interval);
    interval = setInterval(callback, delay);
  };
}

背景扩展名 (QQ 优于全球扩展)

import {Context} from "@b9g/crank";

Context.prototype.setInterval = function(callback, delay) {
  const interval = setInterval(callback, delay);
  this.cleanup(() => clearInterval(interval));
};

// Use in components
function* Timer() {
  let seconds = 0;
  this.setInterval(() => this.refresh(() => seconds++), 1000);

  for ({} of this) {
    yield <div>Seconds: {seconds}</div>;
  }
}

赛跑组件

async function* DataComponent({url}) {
  for await ({url} of this) {
    // This is the equivalent of calling
    //   renderer.render(<Spinner />, document.body);
    //   renderer.render(<Data />, document.body);
    // but in a component.
    yield <Spinner />;
    yield <Data />;
  }
}

组件竞相制造。 对后退状态有用.


类型脚本支持

import type {Context} from "@b9g/crank";
import {ComponentProps} from "@b9g/crank";  // v0.7+

// Component with typed props
interface Props {
  name: string;
  age?: number;
}

function Greeting({name, age}: Props) {
  return <div>Hello {name}, age {age}</div>;
}

// Generator with typed context
function* Greeting(this: Context<typeof Greeting>, {name}: {name: string}) {
  for ({name} of this) {
    yield <div>Hello {name}</div>;
  }
}

// Extract component props type
function Button({variant}: {variant: "primary" | "secondary"}) {
  return <button class={`btn-${variant}`}>Click</button>;
}

type ButtonProps = ComponentProps<typeof Button>;

综合指南和文件,访问 crant.js.org (英语)

版本与发布记录

@b9g/crank 共发布 37 个版本,首次发布于 2021/10/10;过去 12 个月发布了 10 个版本。

版本发布日期距今
0.7.12latest2026/09/052 个月前
0.7.112026/08/152 个月前
0.7.102026/08/132 个月前
0.7.92026/04/017 个月前
0.7.82026/02/288 个月前
0.7.72026/02/218 个月前

其他发布通道:debug → 0.5.0-debug.0,beta → 0.5.0-beta.7。

关于 @b9g/crank 的常见问题

@b9g/crank 是什么?

Just JavaScript UI 框架.

如何安装 @b9g/crank?

在项目目录运行 npm install @b9g/crank,或使用 pnpm add / yarn add / bun add。

@b9g/crank 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@b9g/crank 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@b9g/crank 可以商用吗?

@b9g/crank 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@b9g/crank 的下载量有多少?

根据 npm 官方下载统计,@b9g/crank 在 2026/10/08 – 2026/11/06 这 30 天内被下载 3,691 次,日均约 123 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@b9g/crank 还在维护吗?

最新版本 0.7.12 发布于 2026/09/05(2 个月前);至今共发布 37 个版本,其中过去 12 个月发布了 10 个;首次发布于 2021/10/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。