build(docs-infra): convert search-worker.js to TypeScript (#29764)

PR Close #29764
This commit is contained in:
Filipe Silva
2019-04-15 10:15:43 +01:00
committed by Alex Rickabaugh
parent ee603a3b01
commit 3a836c362d
14 changed files with 162 additions and 132 deletions

View File

@ -110,7 +110,7 @@ export class AppComponent implements OnInit {
// Do not initialize the search on browsers that lack web worker support
if ('Worker' in window) {
// Delay initialization by up to 2 seconds
this.searchService.initWorker('app/search/search-worker.js', 2000);
this.searchService.initWorker(2000);
}
this.onResize(window.innerWidth);

View File

@ -1,106 +0,0 @@
'use strict';
/* eslint-env worker */
/* global importScripts, lunr */
var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
// NOTE: This needs to be kept in sync with `ngsw-config.json`.
importScripts('/generated/lunr.min.js');
var index;
var pages /* : SearchInfo */ = {};
// interface SearchInfo {
// [key: string]: PageInfo;
// }
// interface PageInfo {
// path: string;
// type: string,
// titleWords: string;
// keyWords: string;
// }
self.onmessage = handleMessage;
// Create the lunr index - the docs should be an array of objects, each object containing
// the path and search terms for a page
function createIndex(addFn) {
lunr.QueryLexer.termSeparator = lunr.tokenizer.separator = /\s+/;
return lunr(/** @this */function() {
this.ref('path');
this.field('titleWords', {boost: 10});
this.field('headingWords', {boost: 5});
this.field('members', {boost: 4});
this.field('keywords', {boost: 2});
addFn(this);
});
}
// The worker receives a message to load the index and to query the index
function handleMessage(message) {
var type = message.data.type;
var id = message.data.id;
var payload = message.data.payload;
switch(type) {
case 'load-index':
makeRequest(SEARCH_TERMS_URL, function(searchInfo) {
index = createIndex(loadIndex(searchInfo));
self.postMessage({type: type, id: id, payload: true});
});
break;
case 'query-index':
self.postMessage({type: type, id: id, payload: {query: payload, results: queryIndex(payload)}});
break;
default:
self.postMessage({type: type, id: id, payload: {error: 'invalid message type'}})
}
}
// Use XHR to make a request to the server
function makeRequest(url, callback) {
// The JSON file that is loaded should be an array of PageInfo:
var searchDataRequest = new XMLHttpRequest();
searchDataRequest.onload = function() {
callback(JSON.parse(this.responseText));
};
searchDataRequest.open('GET', url);
searchDataRequest.send();
}
// Create the search index from the searchInfo which contains the information about each page to be indexed
function loadIndex(searchInfo /*: SearchInfo */) {
return function(index) {
// Store the pages data to be used in mapping query results back to pages
// Add search terms from each page to the search index
searchInfo.forEach(function(page /*: PageInfo */) {
index.add(page);
pages[page.path] = page;
});
};
}
// Query the index and return the processed results
function queryIndex(query) {
try {
if (query.length) {
var results = index.search(query);
if (results.length === 0) {
// Add a relaxed search in the title for the first word in the query
// E.g. if the search is "ngCont guide" then we search for "ngCont guide titleWords:ngCont*"
var titleQuery = 'titleWords:*' + query.split(' ', 1)[0] + '*';
results = index.search(query + ' ' + titleQuery);
}
// Map the hits into info about each page to be returned as results
return results.map(function(hit) { return pages[hit.ref]; });
}
} catch(e) {
// If the search query cannot be parsed the index throws an error
// Log it and recover
console.log(e);
}
return [];
}

View File

@ -0,0 +1,104 @@
import { WebWorkerMessage } from '../shared/web-worker-message';
import * as lunr from 'lunr';
const SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
let index: lunr.Index;
const pages: SearchInfo = {};
interface SearchInfo {
[key: string]: PageInfo;
}
interface PageInfo {
path: string;
type: string;
titleWords: string;
keyWords: string;
}
addEventListener('message', handleMessage);
// Create the lunr index - the docs should be an array of objects, each object containing
// the path and search terms for a page
function createIndex(loadIndex: IndexLoader): lunr.Index {
// The lunr typings are missing QueryLexer so we have to add them here manually.
const queryLexer = (lunr as any as { QueryLexer: { termSeparator: RegExp } }).QueryLexer;
queryLexer.termSeparator = lunr.tokenizer.separator = /\s+/;
return lunr(/** @this */function () {
this.ref('path');
this.field('titleWords', { boost: 10 });
this.field('headingWords', { boost: 5 });
this.field('members', { boost: 4 });
this.field('keywords', { boost: 2 });
loadIndex(this);
});
}
// The worker receives a message to load the index and to query the index
function handleMessage(message: { data: WebWorkerMessage }): void {
const type = message.data.type;
const id = message.data.id;
const payload = message.data.payload;
switch (type) {
case 'load-index':
makeRequest(SEARCH_TERMS_URL, function (searchInfo: PageInfo[]) {
index = createIndex(loadIndex(searchInfo));
postMessage({ type: type, id: id, payload: true });
});
break;
case 'query-index':
postMessage({ type: type, id: id, payload: { query: payload, results: queryIndex(payload) } });
break;
default:
postMessage({ type: type, id: id, payload: { error: 'invalid message type' } })
}
}
// Use XHR to make a request to the server
function makeRequest(url: string, callback: (response: any) => void): void {
// The JSON file that is loaded should be an array of PageInfo:
const searchDataRequest = new XMLHttpRequest();
searchDataRequest.onload = function () {
callback(JSON.parse(this.responseText));
};
searchDataRequest.open('GET', url);
searchDataRequest.send();
}
// Create the search index from the searchInfo which contains the information about each page to be indexed
function loadIndex(pagesData: PageInfo[]): IndexLoader {
return (indexBuilder: lunr.Builder) => {
// Store the pages data to be used in mapping query results back to pages
// Add search terms from each page to the search index
pagesData.forEach(page => {
indexBuilder.add(page);
pages[page.path] = page;
});
};
}
// Query the index and return the processed results
function queryIndex(query: string): PageInfo[] {
try {
if (query.length) {
let results = index.search(query);
if (results.length === 0) {
// Add a relaxed search in the title for the first word in the query
// E.g. if the search is "ngCont guide" then we search for "ngCont guide titleWords:ngCont*"
const titleQuery = 'titleWords:*' + query.split(' ', 1)[0] + '*';
results = index.search(query + ' ' + titleQuery);
}
// Map the hits into info about each page to be returned as results
return results.map(function (hit) { return pages[hit.ref]; });
}
} catch (e) {
// If the search query cannot be parsed the index throws an error
// Log it and recover
console.log(e);
}
return [];
}
type IndexLoader = (indexBuilder: lunr.Builder) => void;

View File

@ -25,11 +25,11 @@ describe('SearchService', () => {
describe('initWorker', () => {
it('should create the worker and load the index after the specified delay', fakeAsync(() => {
service.initWorker('some/url', 100);
service.initWorker(100);
expect(WebWorkerClient.create).not.toHaveBeenCalled();
expect(mockWorker.sendMessage).not.toHaveBeenCalled();
tick(100);
expect(WebWorkerClient.create).toHaveBeenCalledWith('some/url', jasmine.any(NgZone));
expect(WebWorkerClient.create).toHaveBeenCalledWith(jasmine.any(Worker), jasmine.any(NgZone));
expect(mockWorker.sendMessage).toHaveBeenCalledWith('load-index');
}));
});
@ -37,7 +37,7 @@ describe('SearchService', () => {
describe('search', () => {
beforeEach(() => {
// We must initialize the service before calling connectSearches
service.initWorker('some/url', 1000);
service.initWorker(1000);
// Simulate the index being ready so that searches get sent to the worker
(service as any).ready = of(true);
});

View File

@ -16,10 +16,9 @@ export class SearchService {
* initial rendering of the web page. Triggering a search will override this delay and cause the index to be
* loaded immediately.
*
* @param workerUrl the url of the WebWorker script that runs the searches
* @param initDelay the number of milliseconds to wait before we load the WebWorker and generate the search index
*/
initWorker(workerUrl: string, initDelay: number) {
initWorker(initDelay: number) {
// Wait for the initDelay or the first search
const ready = this.ready = race<any>(
timer(initDelay),
@ -28,7 +27,8 @@ export class SearchService {
.pipe(
concatMap(() => {
// Create the worker and load the index
this.worker = WebWorkerClient.create(workerUrl, this.zone);
const worker = new Worker('./search-worker', { type: 'module' });
this.worker = WebWorkerClient.create(worker, this.zone);
return this.worker.sendMessage<boolean>('load-index');
}),
publishReplay(1),

View File

@ -0,0 +1,5 @@
export interface WebWorkerMessage {
type: string;
payload: any;
id?: number;
}

View File

@ -1,17 +1,12 @@
import {NgZone} from '@angular/core';
import {Observable} from 'rxjs';
export interface WebWorkerMessage {
type: string;
payload: any;
id?: number;
}
import {WebWorkerMessage} from './web-worker-message';
export class WebWorkerClient {
private nextId = 0;
static create(workerUrl: string, zone: NgZone) {
return new WebWorkerClient(new Worker(workerUrl), zone);
static create(worker: Worker, zone: NgZone) {
return new WebWorkerClient(worker, zone);
}
private constructor(private worker: Worker, private zone: NgZone) {

View File

@ -1,19 +1,23 @@
{
"extends": "../tsconfig.json",
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"baseUrl": "",
"types": [],
"lib": [
"es2018",
"dom"
],
"importHelpers": true
},
"exclude": [
"testing/**/*",
"test.ts",
"test-specs.ts",
"**/*.spec.ts"
"**/*.spec.ts",
"**/*-worker.ts"
],
"angularCompilerOptions": {
"disableTypeScriptVersionCheck": true
}
}
}

11
aio/src/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "es2015",
"lib": [
"es2018",
"dom",
"webworker"
],
}
}

View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/worker",
"lib": [
"es2018",
"webworker"
],
"types": [
"lunr"
],
},
"include": ["**/*-worker.ts"]
}