Select Page

Ostatnie kilka dni trudno nazwać bardzo owocnymi. Jedynym sukcesem, jeśli można tak to nazwać dodanie modularności.

Moduły? Po co?

Na razie skrypt jest tak krótki, że spokojnie można go utrzymać w jednym pliku. W takim razie po co moduły? Odpowiedź brzmi „Ponieważ chciałem zobaczyć jak to wygląda w praktyce”. W teorii, którą przeczytałem w książce „JavaScript. Programowanie zaawansowane” (Tomasz Jakut, dzięki!) i w różnych miejscach w sieci wszystko jest takie proste. W rzeczywistości okazało się to ciut trudniejsze, ale udało się.

Jak

Rozważałem dwa rozwiązania: Webpack i rollup. Ostatecznie zdecydowałem się na rollupa. Jak zawsze na githubie jest całość, więc zainteresowani mogą sprawdzić czy działa. Na moduły podzieliłem tylko wersję node’ową, ponieważ tak naprawdę nad nią pracuję.
Pierwszą rzeczą było doinstalowanie do projektu rollupa i niezbędnych pluginów.

npm install rollup rollup-plugin-commonjs rollup-plugin-node-resolve --save-dev

Kolejna rzecz do zrobienia to skonfigurowanie rollupa. Odpowiada za to plik rollup.config.js w głównym katalogu projektu (tam gdzie jest plik package.json).

import nodeResolve from 'rollup-plugin-node-resolve';
import convertCJS from 'rollup-plugin-commonjs';

export default {
	entry: './src/scripts/index.js',
	format: 'umd',
	moduleName: 'xmlTraverse',
	plugins: [ convertCJS(), nodeResolve() ],
	dest: './src/scripts/bundle.js'
};

W pliku package.json dodałem również skrypt do uruchamiania rollupa:

"scripts": {
	"rollup": "rollup -c"
},

Teraz npm run rollup powinien szybko wygenerować plik finalny.

Struktura

Podział na moduły jest bardzo prosty i można go przedstawić na następującym schemacie:

index.js
|- Content.js
|   |-helpers.js
|
|- xml-dom-traverse.js
   |- helpers.js

Moduł helpers.js na razie zawiera jedną prostą funkcję – konwertuje dzieci aktywnego węzła na tablicę:

// return node.childNodes
export function children( node ) {
	return Array.from( node.childNodes );
}

Content.js to moduł, w którym zdefiniowana jest klasa pobierająca zawartość zadanego tagu (na razie <office:text>).

'use strict';
import { children } from './helpers';
// Recursive read childNodes and return tagName node when found
function extract( content, tagName ) {
	const childs = children( content );
	return childs.reduce(( prev, current ) => {
		return ( current.nodeName === tagName ) ? current : extract( current, tagName );
	}, '' );
}

class Content {
	constructor( content, tagName ) {
		this.content = extract( content, tagName );
	}
}

export default Content;

Importujemy funkcję children() i rekurencyjne przeszukujemy kolejne elementy drzewa DOM (extract( content, tagName )), aż do znalezienia elementu, który nas interesuje. Korzystamy tutaj z funkcji Array.reduce(), dzięki której możemy spłaszczyć kolejne poziomy przeszukiwania (fragment : extract( current, tagName );).

xml-dom-traverse.js zawiera obiekt ze zdefiniowaną metodą traverse. Jej zadaniem jest przejść przez wszystkie dzieci elementu zwróconego przez klasę Content.

'use strict';
import { children } from './helpers';

// Read nodes starting from highest child
function readFromFirst( node ) {
	return node.reduce(( prev, current ) => {
		if ( current.nodeType === 1 ) {
			return ( prev + readFromFirst( children( current )));
		} else if ( current.nodeType === 3 ) {
			//console.log( current.nodeValue );
			if ( current.parentElement.attributes.length === 0 ) {
				return ( prev + `<${current.parentElement.nodeName}>` + current.nodeValue );
			} else {
				return ( prev + `<${current.parentElement.attributes[ 0 ].nodeValue}>` + current.nodeValue );
			}
		}
	}, '' );
}
//Define XML DOM Traverse module
const XMLDomTraverse = {
	// Read content from node given
	traverse( content ) {
		const contentToRead = children( content );
		console.log( contentToRead.map( element => readFromFirst( children( element ))));
	}
};

export default XMLDomTraverse;

W tym module również importujemy funkcję children() z modułu helpers.js.

Jak widać index.js zbiera razem Content.js i xml-dom-treverse.js. Rollup korzysta z tego pliku i generuje bundle.js, który jest wykorzystywany w głównym pliku naszego skryptu.

'use strict';
export { default as XMLDomTraverse } from './xml-dom-traverse';
export { default as Content } from './Content.js';

Oczywiście, w wyniku wszystkich zmian należało skorygować plik główny naszego skryptu xml.js (w katalogu jest też poprzedni skrypt, w którym wszystko jest razem xml2md.js).

'use strict';
const fs = require( 'fs' ),
	jsdom = require( 'jsdom' ),
	domTraverse = require( './bundle.js' ),
	dom = domTraverse.XMLDomTraverse,
	Content = domTraverse.Content,
	urlOne = '../../TestSamples/OnePara/content-one-para.xml',
	urlTwo = '../../TestSamples/ParaCharStylesAndDefault/content-para-char.xml',
	urlThree = '../../TestSamples/StyledAndUnstyledText/content-styled-unstyled.xml',
	urlFour = '../../TestSamples/ParasAndLists/content-paras-lists.xml';

jsdom.env({
	file: urlFour,
	parsingMode: 'xml',
	done( error, window ) {
		if ( error ) {
			console.log( error );
		}
		const nodeWindow = window,
			response = nodeWindow.document,
			text = new Content( response, 'office:text' );
		dom.traverse( text.content );
	}
});

W definicjach pojawiło się kika nowych elementów:

domTraverse = require( './bundle.js' ),
dom = domTraverse.XMLDomTraverse,
Content = domTraverse.Content,

domTraverse importuje zawartość naszego bundle.js. dom pozwala nam dostać się do zawartości modułu xml-dom-traverse.js, która w pliku index.js jest wyeksportowana jako XMLDomTraverse a Content przechwytuje klasę Content. Upraszczając, tworzymy obiekt domTraverse i do zmiennych dom i Content przypisujemy właściwości tego obiektu (XMLDomTraverse i Content).

Zmienił się też fragment z pobieraniem elementu <office:text> i wyświetlaniem jego zawartości.

const nodeWindow = window,
	response = nodeWindow.document,
	text = new Content( response, 'office:text' );
dom.traverse( text.content );

Zmienna text jest instancją naszej klasy Content (przypisuje zawartość elementu <office:text>). Ponieważ wykorzystujemy klasę, musimy wywołać ją z new. Następnie wywołujemy metodę traverse( node ) obiektu XMLDomTraverse z pliku xml-dom-traverse.js (tutaj jest to zmienna dom, która przechowuje ten obiekt zaimportowany z pliku bundle.js). Warto pamiętać (o czym ja zapomniałem), że konstruktor klasy Content zapisuje nasz element we właściwości content, więc żeby z niej skorzystać musimy ją wywołać. W naszym przypadku, ponieważ instancją jest zmienna text będzie to text.content.