写在前面
GitHub链接github.com/huggingface…
项目效果展示如下
接下来我们就从零开始手搓这个项目,本项目需要使用的技术包括但不限于——git、react、Web Worker、transformers.js等
有的技术栈没接触过?没关系我会从零开始讲解,哪怕你连react都没接触过。我相信最后各位都会跑通这个项目的。
开干了兄弟们
一、从github拉取这个项目,我们来体验一下
创建一个空的文件夹,我们来拉取这个项目
git clone https://github.com/huggingface/transformers.js.git
我们要打开的是examples/react-translator目录,这级目录便是我们要复刻的项目目录,OK,下面运行一下,开始体验吧
cd transformers.js/examples/react-translator // 进入相应的目录
npm i // 下载项目所需要的全部依赖
npm run dev // 启动项目
终端给我们返回的网址即是我们要访问的网址,我这里是http://localhost:5174/ ,各位Ctrl+鼠标左键就可以进入了
二、这个项目用到了什么技术,这些技术的作用是什么
体验完项目,按常理来说就可以去分析项目的代码了,但是这个项目并没有相应的readme.md文件(一般写项目说明),所有我们这里先去说明这个项目用到了什么技术,这些技术的作用是什么
这里我总结三个项目所使用的技术
-
技术一、React+JS
React 是一个用于构建用户界面的 JavaScript 库,通过组件化和虚拟 DOM 实现高效、灵活的 UI 开发。
-
技术二、Transformers.js
Transformers.js 是一个强大的JavaScript 机器学习库,它允许开发者在浏览器和 Node.js 环境中直接运行各种预训练的机器学习模型。
-
技术三、Web Workers
Web Workers 是浏览器提供的一个多线程解决方案,它允许 JavaScript 代码在后台线程中运行,而不会阻塞主线程的 UI 渲染和用户交互,从而提升 Web 应用的性能和响应速度。
为了方便大家的理解,这里我总结一下这三个技术在项目中的作用——
react是项目的前端框架,负责将我们的代码组件化,并展示前端的UI(展示前端页面),Transformers.js则用来做最核心的翻译工作,Web Workers负责将前端组件展示和Transformers.js做的工作放在不同的线程中,因为Transformers.js做的工作资源消耗较大,防止因为Transformers.js阻塞了组件的展示
三、具体代码分析
1.react组件部分
为了照顾没有学过react的同学,我们暂时把react组件理解为Javascript中的函数,下面我们分别来看项目中的两个组件,我们去分析其代码以及作用
首先是选择框组件
const LANGUAGES = {
"Chinese (Simplified)": "zho_Hans",
......
}
export default function LanguageSelector({ type, onChange, defaultLanguage }) {
return (
<div className='language-selector'>
<label>{type}: </label>
<select onChange={onChange} defaultValue={defaultLanguage}>
{Object.entries(LANGUAGES).map(([key, value]) => {
return <option key={key} value={value}>{key}</option>
})}
</select>
</div>
)
}
为了方便阅读,我将LANGUAGES的内容进行了省略,里面都是诸如"Chinese (Simplified)": "zho_Hans"的键值对,这里将LANGUAGES定义为常量,因为里面的内容是不会改变的(里面的内容其实就是给用户的选项),而且常量用大写字母定义,展现了良好的编程风格
让我们把重点放在函数LanguageSelector()中,这是整个组件的核心
1.export default我们暂且理解为加上这两个单词,我们的这个方法就允许在其他组件中使用
2.LanguageSelector({ type, onChange, defaultLanguage })给组件(我们称之为方法的东西)传了一个对象,对象中有三个参数,这三个参数很好理解
type // 选项框的类型 是源语言还是目标语言?
onChange // 函数类型,当用户选择不同语言时会调用的回调函数。用于更新父组件的内容(调用这个组件的组件)
defaultLanguage // 选项框默认选中的语言
3.return(......),这里面的内容便是我们这个组件放回的内容,这一坨是jsx类语言,就是类似于HTML+JS,很容易理解其中的意思,返回的是一个选项框
4.Object.entries(LANGUAGES).map(([key, value]),Object.entries(LANGUAGES) 将 LANGUAGES 对象转换为键值对数组,而1. .map() 遍历这个数组,为每个语言生成一个 <option> 元素,实际上完成了生成选项框中的选项的任务
其次是进度条组件
export default function Progress({ text, percentage }) {
percentage = percentage ?? 0;
return (
<div className="progress-container">
<div className='progress-bar' style={{ 'width': `${percentage}%` }}>{text} ({`${percentage.toFixed(2)}%`})</div>
</div>
);
}
类似的,这个组件接受了一个对象,其中包含两个参数
text // 谁的进度条
percentage // 进度条的百分比
这里有几个细节
percentage = percentage ?? 0;,这是 ES6 中的空值合并运算符 ?? 的写法。这个运算符的作用是:当左侧的值为 null 或 undefined 时,返回右侧的值,否则返回左侧的值。- ``字符串模板{'width':
${percentage}%}、{${percentage.toFixed(2)}%}即把percentage、percentage.toFixed(2)的值嵌入源代码中
这个组件返回的是一个进度条
2.Web Worker部分
Transformers.js部分是嵌套在Web Worker部分的,所以我们先搞懂Web Worker部分以便于更好理解Transformers.js部分
我们再来重温一下Web Worker的基本概念——
Web Workers 是浏览器提供的一个多线程解决方案,它允许 JavaScript 代码在后台线程中运行,而不会阻塞主线程的 UI 渲染和用户交互,从而提升 Web 应用的性能和响应速度。
我们通过一个最简单的Web Worker案例,去体验一下Web Worker是如何运行的
下面是一个最简单的 Web Worker 示例,包括主脚本和 worker 脚本两部分。
主脚本 (main.js)
首先,在你的 HTML 文件中引入主脚本:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Worker Demo</title>
</head>
<body>
<h1>Web Worker Example</h1>
<script src="main.js"></script>
</body>
</html>
然后,创建 main.js 文件,并添加以下代码:
// 这段代码首先检查当前浏览器是否支持 Web Workers。`Worker` 是创建 Web Worker 的构造函数。
// 如果浏览器不支持 Web Workers,则会显示一条消息给用户。
if (typeof(Worker) !== "undefined") {
//通过 `new Worker("worker.js")` 创建一个新的 Web Worker 实例,
//这里 `"worker.js"` 是指向 Worker 脚本的路径。
//这意味着将要运行的后台脚本位于名为 `worker.js` 的文件中。
var worker = new Worker("worker.js");
// 当接收到消息时触发
worker.onmessage = function(event) {
console.log("从 Worker 接收到的消息: " + event.data);
};
// 向 Worker 发送消息
worker.postMessage("Hello Worker!");
} else {
console.log("抱歉,你的浏览器不支持 Web Workers...");
}
Worker 脚本 (worker.js)
接着,创建一个名为 worker.js 的文件,并添加以下代码:
// 当接收到消息时触发
onmessage = function(event) {
console.log("从主线程接收到的消息: " + event.data);
// 处理完数据后,发送消息回主线程
postMessage("你好,主线程!已收到你的消息: " + event.data);
};
具体的代码解释,我以注释的方式放在了代码之中,值得注意的一点是接收消息是onmessage、发送消息是postMessage这是内置的函数,不要随意修改
上述的代码就完成了worker.js中的内容会在独立的线程中运行,不会影响主线程,而且可以和主线程进行通讯
3.Transformers.js部分
下面是Transformers.js部分的主要代码
import { pipeline } from '@xenova/transformers';
class MyTranslationPipeline {
static task = 'translation';
static model = 'Xenova/nllb-200-distilled-600M';
static instance = null;
static async getInstance(progress_callback = null) {
if (this.instance === null) {
this.instance = pipeline(this.task, this.model, { progress_callback });
}
return this.instance;
}
}
self.addEventListener('message', async (event) => {
let translator = await MyTranslationPipeline.getInstance(x => {
self.postMessage(x);
});
let output = await translator(event.data.text, {
tgt_lang: event.data.tgt_lang,
src_lang: event.data.src_lang,
callback_function: x => {
self.postMessage({
status: 'update',
output: translator.tokenizer.decode(x[0].output_token_ids, { skip_special_tokens: true })
});
}
});
self.postMessage({
status: 'complete',
output: output,
});
});
下面我来解释这段代码中Transformers.js的使用
import { pipeline } from '@xenova/transformers';
从@xenova/transformers库导入pipeline函数,这是加载和使用预训练模型的核心方法
class MyTranslationPipeline {
// 静态属性,指定任务类型为翻译
static task = 'translation';
// 指定使用的预训练模型(Xenova提供的NLLB-200蒸馏版600M参数模型)
static model = 'Xenova/nllb-200-distilled-600M';
// 保存单例实例的引用
static instance = null;
// 静态方法,获取模型实例(单例模式)
static async getInstance(progress_callback = null) {
// 如果实例不存在,则创建新实例
if (this.instance === null) {
// 使用transformers.js的pipeline函数初始化翻译管道
// 传入任务类型、模型名称和进度回调函数
this.instance = pipeline(this.task, this.model, { progress_callback });
}
// 返回实例(可能是新创建的或已存在的)
return this.instance;
}
}
上面这一段代码实现了单例模式,这个意义是巨大的
我们这个类MyTranslationPipeline如果被实例化会加载一个Xenova/nllb-200-distilled-600M模型,这个模型需要的资源是巨大的,而且我们的项目只需要一个这样的模型,所以,我们要保证不论类MyTranslationPipeline实例化多少次,都只会加载一个Xenova/nllb-200-distilled-600M模型
上述代码使用静态变量instance来保存我们加载后的模型,在每次加载前去检查instance是否为空(是否加载过模型),值得注意的是,静态变量的值在类实例化的所有对象中都是通用的值,只有定义为静态变量才能实现单例模式
// 监听主线程发送的消息事件
// `self` 是Worker线程中的全局命名空间对象
// `message` 是Web Worker API定义的事件类型
self.addEventListener('message', async (event) => {
// 获取翻译器实例(单例模式)
// 参数x是模型加载进度对象,通过postMessage转发给主线程,`@xenova/transformers` 库提供的模型加载进度对象
let translator = await MyTranslationPipeline.getInstance(x => {
self.postMessage(x); // 转发模型加载进度
});
// 执行实际翻译任务
let output = await translator(event.data.text, {
// 目标语言代码(如"fra_Latn")
tgt_lang: event.data.tgt_lang,
// 源语言代码(如"eng_Latn")
src_lang: event.data.src_lang,
// 流式输出回调函数
callback_function: x => {
// 将生成的token解码为文本
const decodedText = translator.tokenizer.decode(
x[0].output_token_ids, // 取第一个结果的token序列
{ skip_special_tokens: true } // 跳过特殊标记
);
// 发送增量更新到主线程
self.postMessage({
status: 'update', // 状态标识
output: decodedText // 当前已解码文本
});
}
});
// 发送最终完成消息
self.postMessage({
status: 'complete', // 完成状态标识
output: output, // 最终完整翻译结果
});
});
对于代码的解释我放在了源码的注释中
各位,到这里我们就把辅助的内容讲完了,接下来是最重要的部分,我们所有的功能都会在主组件中进行整合,打起精神
4.整合部分(APP.jsx)
import { useEffect, useRef, useState } from 'react'
import LanguageSelector from './components/LanguageSelector';
import Progress from './components/Progress';
import './App.css'
function App() {
const [ready, setReady] = useState(null);
const [disabled, setDisabled] = useState(false);
const [progressItems, setProgressItems] = useState([]);
const [input, setInput] = useState('I love walking my dog.');
const [sourceLanguage, setSourceLanguage] = useState('eng_Latn');
const [targetLanguage, setTargetLanguage] = useState('fra_Latn');
const [output, setOutput] = useState('');
const worker = useRef(null);
useEffect(() => {
if (!worker.current) {
worker.current = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
}
const onMessageReceived = (e) => {
switch (e.data.status) {
case 'initiate':
setReady(false);
setProgressItems(prev => [...prev, e.data]);
break;
case 'progress':
setProgressItems(
prev => prev.map(item => {
if (item.file === e.data.file) {
return { ...item, progress: e.data.progress }
}
return item;
})
);
break;
case 'done':
setProgressItems(
prev => prev.filter(item => item.file !== e.data.file)
);
break;
case 'ready':
setReady(true);
break;
case 'update':
setOutput(e.data.output);
break;
case 'complete':
setDisabled(false);
break;
}
};
worker.current.addEventListener('message', onMessageReceived);
return () => worker.current.removeEventListener('message', onMessageReceived);
});
const translate = () => {
setDisabled(true);
worker.current.postMessage({
text: input,
src_lang: sourceLanguage,
tgt_lang: targetLanguage,
});
}
return (
<>
<h1>Transformers.js</h1>
<h2>ML-powered multilingual translation in React!</h2>
<div className='container'>
<div className='language-container'>
<LanguageSelector type={"Source"} defaultLanguage={"eng_Latn"} onChange={x => setSourceLanguage(x.target.value)} />
<LanguageSelector type={"Target"} defaultLanguage={"fra_Latn"} onChange={x => setTargetLanguage(x.target.value)} />
</div>
<div className='textbox-container'>
<textarea value={input} rows={3} onChange={e => setInput(e.target.value)}></textarea>
<textarea value={output} rows={3} readOnly></textarea>
</div>
</div>
<button disabled={disabled} onClick={translate}>Translate</button>
<div className='progress-bars-container'>
{ready === false && (
<label>Loading models... (only run once)</label>
)}
{progressItems.map(data => (
<div key={data.file}>
<Progress text={data.file} percentage={data.progress} />
</div>
))}
</div>
</>
)
}
export default App
这部分代码比较长,不过没关系,我们一一来分析
// 应用状态管理
// 模型加载就绪状态:null-未开始 false-加载中 true-完成
const [ready, setReady] = useState(null);
// 翻译按钮禁用状态:防止重复提交
const [disabled, setDisabled] = useState(false);
// 模型文件加载进度列表
const [progressItems, setProgressItems] = useState([]);
// 翻译相关状态
// 输入文本(默认示例)
const [input, setInput] = useState('I love walking my dog.');
// 源语言代码(默认英语)
const [sourceLanguage, setSourceLanguage] = useState('eng_Latn');
// 目标语言代码(默认法语)
const [targetLanguage, setTargetLanguage] = useState('fra_Latn');
// 翻译结果输出
const [output, setOutput] = useState('');
这部分定义了一群响应式变量,具体含义我放在了源代码的注释中
const worker = useRef(null);
useEffect(() => {
if (!worker.current) {
worker.current = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
}
这一段创建了一个worker实例,那么接下来我们要思考,**为什么这样要这样创建呢?**我们完全可以
const worker=useRef(new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});)
这里有一个细节, useEffect()中的函数会在组件挂载后才执行一次,我们创建worker的过程中会加载一个模型,这个过程是很耗时的,如果我们把它放在 useEffect()中,就可以让用户先看到页面,然后再加载模型,这样就不会出现加载模型的过程中用户的页面一片白了
const onMessageReceived = (e) => {
switch (e.data.status) {
case 'initiate':
setReady(false);
setProgressItems(prev => [...prev, e.data]);
break;
case 'progress':
setProgressItems(
prev => prev.map(item => {
if (item.file === e.data.file) {
return { ...item, progress: e.data.progress }
}
return item;
})
);
break;
case 'done':
setProgressItems(
prev => prev.filter(item => item.file !== e.data.file)
);
break;
case 'ready':
setReady(true);
break;
case 'update':
setOutput(e.data.output);
break;
case 'complete':
setDisabled(false);
break;
}
};
这一部分是实现进度条的加载,我们下面简化看一下其结构
switch (e.data.status) {
case 'initiate': // 模型开始加载
case 'progress': // 加载进度更新
case 'done': // 单个文件加载完成
case 'ready': // 全部加载完成
case 'update': // 流式翻译结果
case 'complete': // 翻译最终完成
}
这是我们翻译任务的六个阶段,其中值得注意的是在update阶段
case 'update':
setOutput(e.data.output);
break;
实现了翻译结果逐字输出的效果
worker.current.addEventListener('message', onMessageReceived);
return () => worker.current.removeEventListener('message', onMessageReceived);
这段代码并不难理解,这里不再赘述
return (
<>
<h1>Transformers.js</h1>
<h2>ML-powered multilingual translation in React!</h2>
<div className='container'>
<div className='language-container'>
<LanguageSelector type={"Source"} defaultLanguage={"eng_Latn"} onChange={x => setSourceLanguage(x.target.value)} />
<LanguageSelector type={"Target"} defaultLanguage={"fra_Latn"} onChange={x => setTargetLanguage(x.target.value)} />
</div>
<div className='textbox-container'>
<textarea value={input} rows={3} onChange={e => setInput(e.target.value)}></textarea>
<textarea value={output} rows={3} readOnly></textarea>
</div>
</div>
<button disabled={disabled} onClick={translate}>Translate</button>
<div className='progress-bars-container'>
{ready === false && (
<label>Loading models... (only run once)</label>
)}
{progressItems.map(data => (
<div key={data.file}>
<Progress text={data.file} percentage={data.progress} />
</div>
))}
</div>
</>
)
}
这部分代码将前文定义的变量,方法等融入的HTML中,并调用我们之前定义的组件形成一个完整的界面
至此,核心代码我们已经讲完了,至于index.css、app.css中的内容我们不再讲述,里面是对HTML页面的一些修饰
结语
这个翻译项目巧妙地结合了现代前端技术与机器学习能力,展现了React的高效UI开发与Transformers.js的强大模型推理能力。通过Web Workers将计算密集型任务放在后台线程,不仅保证了页面的流畅交互,还实现了模型加载和翻译过程的实时进度展示,为用户提供了丝滑的体验。
项目的亮点在于模块化设计——将语言选择、进度条、翻译逻辑等拆分为独立组件,并通过Worker线程与主线程通信,实现了高效的资源管理。同时,流式输出翻译结果的设计让用户体验更加自然,而单例模式的运用则避免了重复加载模型的开销。这种技术组合为浏览器端机器学习应用提供了优秀的实践范例。