В этом посте продемонстрировано, как с лёгкостью использовать WebAssembly внутри приложения, написанного на Angular. Иногда в приложении на Angular требуется выполнить задачу, которая в JavaScript завершается не слишком быстро. Конечно, можно переписать алгоритм на другом языке, например, AssemblyScript и Rust — и код станет эффективнее. Затем можно скомпилировать получившиеся фрагменты кода в файле WASM и потоком передать двоичные данные в приложение, чтобы можно было вызывать из него функции WASM. Бывает и так, что разработчику не удаётся найти в реестре NPM опенсорсные библиотеки, нужные для решения задачи. В таком случае можно написать пакет не на JS, а на каком-нибудь другом языке, затем скомпилировать этот пакет в WASM и опубликовать код WASM в реестре NPM. Angular-разработчики устанавливают новый пакет как зависимость и выполняют WASM-функции внутри приложения.
В следующем демонстрационном примере я напишу на AssemblyScript несколько функций для работы с простыми числами, а затем опубликую файл индекса в формате WASM. Затем скопирую файл WASM в приложение Angular, потоком отправлю двоичные данные через WebAssembly API и, наконец, стану вызывать эти функции, чтобы с их помощью выполнять различные действия над простыми числами.
WebAssembly – это составное слово, которое делится на 2 части: Web (Веб) и Assembly (Ассемблер). Когда пишешь на высокоуровневых языках, например, на AssemblyScript и Rust, получается код, который компилируется в ассемблер при помощи специальных инструментов. После этого разработчик может нативно выполнять ассемблерный код прямо в браузере, то есть, в вебе.
С этим демо-примером связаны 2 github-репозитория. В первом используется язык AssemblyScript, на котором пишется TypeScript-подобный код, компилируемый в Wasm. Во втором репозитории лежит простое приложение на Angular, в котором при помощи функций Wasm исследуются некоторые любопытные свойства простых чисел.
В индексе репозитория с AssemblyScript содержится 3 функции для работы с простыми числами:
AssemblyScript долбавляет в package.json скрипты, генерирующие, соответственно, debug.wasm и release.wasm.
Я скопировала release.wasm в каталог assets приложения Angular, написала загрузчик WebAssembly, чтобы организовать потоковую передачу двоичного файла и вернуть экземпляр WebAssembly. Главный компонент связывает экземпляр с компонентами, где он выступает в качестве ввода. Эти компоненты используют экземпляр для выполнения Wasm и вспомогательных функций для получения результатов операций над простыми числами.
AssemblyScript — это TypeScript-подобный язык, на котором можно писать код, в дальнейшем компилируемый в WebAssembly.
Начинаем новый проект
Устанавливаем зависимость
Выполняем команду для добавления скриптов в package.json и файлов скаффолда
Наши собственные скрипты для генерации файлов debug.wasm и release.wasm
Реализуем алгоритм для работы над простыми числами на AssemblyScript
primeNumberLog — это внешняя функция, которая логирует простые числа в findFirstNPrimes. У этой функции нет тела, и именно приложение Angular отвечает за то, чтобы предоставить для неё детали реализации.
После выполнения скрипта npm run asbuild в каталоге builds/ будут содержаться файлы debug.wasm и release.wasm. Та часть работы, которая касается WebAssembly, уже готова, и далее мы будем работать только с приложением Angular.
В WebAssembly не переносятся такие высокоуровневые типы данных, как, например, массивы и булевы значения. Поэтому я установила загрузчик assemblyscript и с помощью содержащихся в нём вспомогательных функций привожу к корректным типам значения, возвращаемые функциями Wasm.
Установка зависимости
Сборка загрузчика WebAssembly
Далее, методом проб и ошибок я смогла импортировать в приложение Angular функции Wasm, организовав потоковую передачу release.wasm при помощи загрузчика assemblyscript.
Я инкапсулировала загрузчик WebAssembly в загрузочный сервис, тем самым позволив всем компонентам Angular многократно использовать функционал потоковой передачи данных. Если в браузере поддерживается функция instantiateStreaming, то возвращается экземпляр WebAssembly. Если функция instantiateStreaming не поддерживается, то будет вызвана резервная функция. Резервная функция преобразует ответ в массив ArrayBuffer и собирает экземпляр WebAssembly.
DEFAULT_IMPORTS также предоставляет реализацию primeNumberLog. primeNumberLog объявляется в файле index.ts репозитория AssemblyScript. Следовательно, ключом объекта служит его позиция в индексе без файлового расширения.
Связываем экземпляр WebAssembly с компонентами Angular
В AppComponent я потоком передала release.wasm, чтобы собрать экземпляр WebAssembly. Затем я связала этот экземпляр с выводом из Angular Components.
Применение WebAssembly к Angular Components
IsPrimeComponent вызывает функцию isPrime, определяющую, является ли данное целое число простым. isPrime возвращает 1, если перед нами простое число, в противном случае возвращает 0. Следовательно, оператор === сравнивает целочисленные значения, чтобы в результате вернуть булево.
FindFirstNPrimesComponent вызывает функцию findFirstNPrimes, чтобы получить первые N простых чисел. Функция findFirstNPrimes не может перенести целочисленный массив, поэтому я пользуюсь вспомогательной функцией __getArray, имеющейся в загрузчике, и с её помощью преобразую целочисленное значение в корректный целочисленный массив.
OptimizedSieveComponent вызывает функцию optimizedSieve, чтобы получить все простые числа, которые меньше to obtain N. Функция optimizedSieve также не может перенести целочисленный массив, и я пользуюсь вспомогательной функцией __getArray, с её помощью преобразую целочисленное значение в корректный целочисленный массив.
Готовый пример показан на следующей странице:
railsstudent.github.io
Вот и всё. Надеюсь, вам понравился этот пост, и он вдохновит вас на изучение Angular и других технологий.
Ресурсы:
В следующем демонстрационном примере я напишу на AssemblyScript несколько функций для работы с простыми числами, а затем опубликую файл индекса в формате WASM. Затем скопирую файл WASM в приложение Angular, потоком отправлю двоичные данные через WebAssembly API и, наконец, стану вызывать эти функции, чтобы с их помощью выполнять различные действия над простыми числами.
Что такое WebAssembly?
WebAssembly – это составное слово, которое делится на 2 части: Web (Веб) и Assembly (Ассемблер). Когда пишешь на высокоуровневых языках, например, на AssemblyScript и Rust, получается код, который компилируется в ассемблер при помощи специальных инструментов. После этого разработчик может нативно выполнять ассемблерный код прямо в браузере, то есть, в вебе.
Как этот демо-пример может использоваться на практике
С этим демо-примером связаны 2 github-репозитория. В первом используется язык AssemblyScript, на котором пишется TypeScript-подобный код, компилируемый в Wasm. Во втором репозитории лежит простое приложение на Angular, в котором при помощи функций Wasm исследуются некоторые любопытные свойства простых чисел.
В индексе репозитория с AssemblyScript содержится 3 функции для работы с простыми числами:
- isPrime – Определить, является ли данное целое число простым
- findFirstNPrimes – Найти первые N простых чисел, где N – целое число
- optimizedSieve – Найти все простые числа менее N, где N — целое число
AssemblyScript долбавляет в package.json скрипты, генерирующие, соответственно, debug.wasm и release.wasm.
Я скопировала release.wasm в каталог assets приложения Angular, написала загрузчик WebAssembly, чтобы организовать потоковую передачу двоичного файла и вернуть экземпляр WebAssembly. Главный компонент связывает экземпляр с компонентами, где он выступает в качестве ввода. Эти компоненты используют экземпляр для выполнения Wasm и вспомогательных функций для получения результатов операций над простыми числами.
Пишем WebAssembly на AssemblyScript
AssemblyScript — это TypeScript-подобный язык, на котором можно писать код, в дальнейшем компилируемый в WebAssembly.
Начинаем новый проект
npm init
Устанавливаем зависимость
npm install --save-dev assemblyscript
Выполняем команду для добавления скриптов в package.json и файлов скаффолда
npx asinit .
Наши собственные скрипты для генерации файлов debug.wasm и release.wasm
"scripts": {
"asbuild:debug": "asc assembly/index.ts --target debug --exportRuntime",
"asbuild:release": "asc assembly/index.ts --target release --exportRuntime",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"start": "npx serve ."
}
Реализуем алгоритм для работы над простыми числами на AssemblyScript
// assembly/index.ts
// Запись в файле, в этой записи указан модуль WebAssembly
// импорт модуля
declare function primeNumberLog(primeNumber: i32): void;
export function isPrime(n: i32): bool {
if (n <= 1) {
return false;
} else if (n === 2 || n === 3) {
return true;
} else if (n % 2 === 0 || n % 3 === 0) {
return false;
}
for (let i = 5; i <= Math.sqrt(n); i = i + 6) {
if (n % i === 0 || n % (i + 2) === 0) {
return false;
}
}
return true;
}
export function findFirstNPrimes(n: i32): Array<i32> {
let primes = new Array<i32>(n);
for (let i = 0; i < n; i++) {
primes[i] = 0;
}
primes[0] = 2;
primeNumberLog(primes[0]);
let num = 3;
let index = 0;
while(index < n - 1) {
let isPrime = true;
for (let i = 0; i <= index; i++) {
if (num % primes[i] === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primeNumberLog(num);
primes[index + 1] = num;
index = index + 1;
}
num = num + 2;
}
return primes;
}
const MAX_SIZE = 1000001;
export function optimizedSieve(n: i32): Array<i32> {
const isPrime = new Array<bool>(MAX_SIZE);
isPrime.fill(true, 0, MAX_SIZE);
const primes = new Array<i32>();
const smallestPrimeFactors = new Array<i32>(MAX_SIZE);
smallestPrimeFactors.fill(1, 0, MAX_SIZE);
isPrime[0] = false;
isPrime[1] = false;
for (let i = 2; i < n; i++) {
if (isPrime[i]) {
primes.push(i);
smallestPrimeFactors[i] = i;
}
for (let j = 0; j < primes.length && i * primes[j] < n && primes[j] <= smallestPrimeFactors[i]; j++) {
const nonPrime = i * primes[j];
isPrime[nonPrime] = false;
smallestPrimeFactors[nonPrime] = primes[j];
}
}
const results = new Array<i32>();
for (let i = 0; i < primes.length && primes[i] <= n; i++) {
results.push(primes[i]);
}
return results;
}
primeNumberLog — это внешняя функция, которая логирует простые числа в findFirstNPrimes. У этой функции нет тела, и именно приложение Angular отвечает за то, чтобы предоставить для неё детали реализации.
После выполнения скрипта npm run asbuild в каталоге builds/ будут содержаться файлы debug.wasm и release.wasm. Та часть работы, которая касается WebAssembly, уже готова, и далее мы будем работать только с приложением Angular.
Комбинируем сильные стороны WebAssembly и Angular
В WebAssembly не переносятся такие высокоуровневые типы данных, как, например, массивы и булевы значения. Поэтому я установила загрузчик assemblyscript и с помощью содержащихся в нём вспомогательных функций привожу к корректным типам значения, возвращаемые функциями Wasm.
Установка зависимости
npm i @assemblyscript/loader
Сборка загрузчика WebAssembly
Далее, методом проб и ошибок я смогла импортировать в приложение Angular функции Wasm, организовав потоковую передачу release.wasm при помощи загрузчика assemblyscript.
src
├── assets
│ └── release.wasm
├── favicon.ico
├── index.html
├── main.ts
└── styles.scss
Я инкапсулировала загрузчик WebAssembly в загрузочный сервис, тем самым позволив всем компонентам Angular многократно использовать функционал потоковой передачи данных. Если в браузере поддерживается функция instantiateStreaming, то возвращается экземпляр WebAssembly. Если функция instantiateStreaming не поддерживается, то будет вызвана резервная функция. Резервная функция преобразует ответ в массив ArrayBuffer и собирает экземпляр WebAssembly.
DEFAULT_IMPORTS также предоставляет реализацию primeNumberLog. primeNumberLog объявляется в файле index.ts репозитория AssemblyScript. Следовательно, ключом объекта служит его позиция в индексе без файлового расширения.
// web-assembly-loader.service.ts
import { Injectable } from '@angular/core';
import loader, { Imports } from '@assemblyscript/loader';
const DEFAULT_IMPORTS: Imports = {
env: {
abort: function() {
throw new Error('Abort called from wasm file');
},
},
index: {
primeNumberLog: function(primeNumber: number) {
console.log(`primeNumberLog: ${primeNumber}`);
}
}
}
@Injectable({
providedIn: 'root'
})
export class WebAssemblyLoaderService {
async streamWasm(wasm: string, imports = DEFAULT_IMPORTS): Promise<any> {
if (!loader.instantiateStreaming) {
return this.wasmFallback(wasm, imports);
}
const instance = await loader.instantiateStreaming(fetch(wasm), imports);
return instance?.exports;
}
async wasmFallback(wasm: string, imports: Imports) {
console.log('using fallback');
const response = await fetch(wasm);
const bytes = await response?.arrayBuffer();
const { instance } = await loader.instantiate(bytes, imports);
return instance?.exports;
}
}
Связываем экземпляр WebAssembly с компонентами Angular
В AppComponent я потоком передала release.wasm, чтобы собрать экземпляр WebAssembly. Затем я связала этот экземпляр с выводом из Angular Components.
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{
provide: APP_BASE_HREF,
useFactory: () => inject(PlatformLocation).getBaseHrefFromDOM(),
}
]
};
// full-asset-path.ts
export const getFullAssetPath = (assetName: string) => {
const baseHref = inject(APP_BASE_HREF);
const isEndWithSlash = baseHref.endsWith('/');
return `${baseHref}${isEndWithSlash ? '' : '/'}assets/${assetName}`;
}
// app.component.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule, IsPrimeComponent, FindFirstNPrimesComponent, OptimizedSieveComponent],
template: `
<div class="container outer" style="margin: 0.5rem;">
<h2>Angular + WebAssembly Demo</h2>
<app-is-prime [instance]="instance" />
<app-find-first-nprimes [instance]="instance" />
<app-optimized-sieve [instance]="instance" />
</div>
`,
})
export class AppComponent implements OnInit {
instance!: any;
releaseWasm = getFullAssetPath('release.wasm');
wasmLoader = inject(WebAssemblyLoaderService);
async ngOnInit(): Promise<void> {
this.instance = await this.wasmLoader.streamWasm(this.releaseWasm);
console.log(this.instance);
}
}
Применение WebAssembly к Angular Components
IsPrimeComponent вызывает функцию isPrime, определяющую, является ли данное целое число простым. isPrime возвращает 1, если перед нами простое число, в противном случае возвращает 0. Следовательно, оператор === сравнивает целочисленные значения, чтобы в результате вернуть булево.
// is-prime.component.ts
@Component({
selector: 'app-is-prime',
standalone: true,
imports: [FormsModule],
template: `
<form>
<label for="primeNumber">
<span>Input an positive integer: </span>
<input id="primeNumber" name="primeNumber" type="number"
[ngModel]="primeNumber()" (ngModelChange)="primeNumber.set($event)" />
</label>
</form>
<p class="bottom-margin">isPrime({{ primeNumber() }}): {{ isPrimeNumber() }}</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IsPrimeComponent {
@Input({ required: true })
instance!: any;
primeNumber = signal(0);
isPrimeNumber = computed(() => {
const value = this.primeNumber();
return this.instance ? this.instance.isPrime(value) === 1 : false
});
}
FindFirstNPrimesComponent вызывает функцию findFirstNPrimes, чтобы получить первые N простых чисел. Функция findFirstNPrimes не может перенести целочисленный массив, поэтому я пользуюсь вспомогательной функцией __getArray, имеющейся в загрузчике, и с её помощью преобразую целочисленное значение в корректный целочисленный массив.
// find-first-nprimes.component.ts
@Component({
selector: 'app-find-first-nprimes',
standalone: true,
imports: [FormsModule],
template: `
<form>
<label for="firstNPrimeNumbers">
<span>Find first N prime numbers: </span>
<input id="firstNPrimeNumbers" name="firstNPrimeNumbers" type="number"
[ngModel]="firstN()" (ngModelChange)="firstN.set($event)" />
</label>
</form>
<p class="bottom-margin">First {{ firstN() }} prime numbers:</p>
<div class="container first-n-prime-numbers bottom-margin">
@for(primeNumber of firstNPrimeNumbers(); track primeNumber) {
<span style="padding: 0.25rem;">{{ primeNumber }}</span>
}
<div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FindFirstNPrimesComponent {
@Input({ required: true })
instance!: any;
firstN = signal(0);
firstNPrimeNumbers = computed(() => {
const value = this.firstN();
if (this.instance) {
const { findFirstNPrimes, __getArray: getArray } = this.instance;
return getArray(findFirstNPrimes(value));
}
return [];
});
}
OptimizedSieveComponent вызывает функцию optimizedSieve, чтобы получить все простые числа, которые меньше to obtain N. Функция optimizedSieve также не может перенести целочисленный массив, и я пользуюсь вспомогательной функцией __getArray, с её помощью преобразую целочисленное значение в корректный целочисленный массив.
// optimized-sieve.component.ts
@Component({
selector: 'app-optimized-sieve',
standalone: true,
imports: [FormsModule],
template: `
<form>
<label for="primeNumber">
<span>Input an positive integer: </span>
<input id="primeNumber" name="primeNumber" type="number"
[ngModel]="lessThanNumber()" (ngModelChange)="lessThanNumber.set($event)" />
</label>
</form>
<p class="bottom-margin">Prime numbers less than {{ lessThanNumber() }}</p>
<div class="container prime-numbers-less-than-n bottom-margin">
@for(primeNumber of primeNumbers(); track primeNumber) {
<span style="padding: 0.25rem;">{{ primeNumber }}</span>
}
<div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedSieveComponent {
@Input({ required: true })
instance!: any;
lessThanNumber = signal(0);
primeNumbers = computed(() => {
const value = this.lessThanNumber();
if (this.instance) {
const { optimizedSieve, __getArray: getArray } = this.instance;
return getArray(optimizedSieve(value));
}
return [];
});
}
Готовый пример показан на следующей странице:
railsstudent.github.io
Вот и всё. Надеюсь, вам понравился этот пост, и он вдохновит вас на изучение Angular и других технологий.
Ресурсы:
- Репозиторий с WebAssembly: github.com/railsstudent/prime-number-wasm
- Репозиторий с демо-примером на Angular + WebAssembly: github.com/railsstudent/ng-webassembly-demo
- Интерактивное демо: railsstudent.github.io/ng-webassembly-demo
- AssemblyScript: angular.dev/guide/templates/control-flow#if-block-conditionals
- WebAssembly API: developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming_static