2017-05-06 31 views
34

Eksperymentowałem z nową wersją native ECMAScript module support, która została ostatnio dodana do przeglądarek. Przyjemnie jest móc importować skrypty bezpośrednio i czysto z JavaScript.Inlineing ECMAScript Modules w HTML

     /example.html      
<script type="module"> 
    import {example} from '/example.js'; 

    example(); 
</script> 
     /example.js     
export function example() { 
    document.body.appendChild(document.createTextNode("hello")); 
}; 

Jednak to tylko pozwala mi na importowanie modułów, które są zdefiniowane przez osobne zewnętrznych plików JavaScript. Zazwyczaj wolę wstawiać niektóre skrypty używane do wstępnego renderowania, więc ich żądania nie blokują reszty strony. Z tradycyjnej biblioteki nieformalnie zbudowane, że może wyglądać następująco:

     /inline-traditional.html      
<body> 
<script> 
    var example = {}; 

    example.example = function() { 
    document.body.appendChild(document.createTextNode("hello")); 
    }; 
</script> 
<script> 
    example.example(); 
</script> 

Jednak naiwnie inline moduły plików oczywiście nie będzie działać, ponieważ spowodowałoby to usunięcie pliku używany do identyfikacji modułu do innych modułów . Prasowanie HTTP/2 może być kanonicznym sposobem radzenia sobie z tą sytuacją, ale nadal nie jest to opcja we wszystkich środowiskach.

Czy można przeprowadzić równoważną transformację za pomocą modułów ECMAScript? Czy jest jakiś sposób, aby <script type="module"> zaimportować moduł wyeksportowany przez inny w tym samym dokumencie?


sobie wyobrazić, to może pracować przez umożliwienie skrypt, aby określić ścieżkę do pliku, i zachowywać się tak, jakby to był już pobrane lub popychany ze ścieżki.

     /inline-name.html      
<script type="module" name="/example.js"> 
    export function example() { 
    document.body.appendChild(document.createTextNode("hello")); 
    }; 
</script> 

<script type="module"> 
    import {example} from '/example.js'; 

    example(); 
</script> 

Albo o zupełnie innym systemem odniesienia, takie jak stosuje się do lokalnych odn SVG:

     /inline-id.html      
<script type="module" id="example"> 
    export function example() { 
    document.body.appendChild(document.createTextNode("hello")); 
    }; 
</script> 
<script type="module"> 
    import {example} from '#example'; 

    example(); 
</script> 

ale żaden z tych hypotheticals faktycznie pracują, a ja Haven” t widział alternatywę, która ma.

+0

Być może jest lepszy sposób, aby to osiągnąć - potencjalnie korzystając z Service Workers? –

+0

Nie sądzę, aby "moduł inline" zgodny z homebrew, nie spełniający specyfikacji, mógł być uważany za dobry początek w przypadku modułów ES. Pakiety Webpack/Rollup są nadal niezbędne w produkcji - * zwłaszcza * jeśli nie chcesz blokować żądań. Tak, pracownik serwisu wygląda jak realne rozwiązanie - ale nadal powinien wysyłać żądania w celu dostarczenia danych ... które mogą być blokowane, przy okazji. – estus

+0

@estus Wyobrażałam sobie, używając pracowników serwisu do wstawiania znaczników '

14

Hacking Wraz naszych własnych import from '#id'

Eksport/import między skryptami inline nie są natywnie obsługiwane, ale to była zabawa ćwiczenie włamać razem implementację dla moich dokumentów.Code-golfed dół do małego bloku, używam go tak:

<script type="module" data-info="https://stackoverflow.com/a/43834063">let l,e,t 
 
='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document, 
 
s,o;for(o of d.querySelectorAll(t+'[type=inline-module]'))l=d.createElement(t),o 
 
.id?l.id=o.id:0,l.type='module',l[x]=o[x].replace(p,(u,a,z)=>(e=d.querySelector(
 
t+z+'[type=module][src]'))?a+`/* ${z} */'${e.src}'`:u),l.src=URL.createObjectURL 
 
(new Blob([l[x]],{type:'application/java'+t})),o.replaceWith(l)//inline</script> 
 

 
<script type="inline-module" id="utils"> 
 
    let n = 1; 
 
    
 
    export const log = message => { 
 
    const output = document.createElement('pre'); 
 
    output.textContent = `[${n++}] ${message}`; 
 
    document.body.appendChild(output); 
 
    }; 
 
</script> 
 

 
<script type="inline-module" id="dogs"> 
 
    import {log} from '#utils'; 
 
    
 
    log("Exporting dog names."); 
 
    
 
    export const names = ["Kayla", "Bentley", "Gilligan"]; 
 
</script> 
 

 
<script type="inline-module"> 
 
    import {log} from '#utils'; 
 
    import {names as dogNames} from '#dogs'; 
 
    
 
    log(`Imported dog names: ${dogNames.join(", ")}.`); 
 
</script>

Zamiast <script type="module">, musimy określić nasze elementy skryptu przy użyciu niestandardowego typu jak <script type="inline-module">. Zapobiega to próbie wykonania przez przeglądarkę własnej zawartości, pozostawiając je do obsługi. Skrypt (pełna wersja poniżej) wyszukuje wszystkie elementy skryptu inline-module w dokumencie i przekształca je w elementy klasycznego modułu skryptów zgodnie z naszym zachowaniem.

Skrypty wbudowane nie mogą być bezpośrednio importowane od siebie, więc musimy nadać skrypty importowalnym adresom URL. Generujemy adres URL blob: dla każdego z nich, zawierający ich kod i ustawiając atrybut src, aby uruchamiał się z tego adresu URL, zamiast uruchamiać wbudowane. Adresy URL blob: działają jak normalne adresy URL z serwera, dzięki czemu można je importować z innych modułów. Za każdym razem, gdy widzimy kolejny inline-module próbujący importować z '#example', gdzie example jest identyfikatorem inline-module, który zmieniliśmy, modyfikujemy ten import, aby zaimportować go z adresu URL blob:. Utrzymuje to jednorazową realizację i deduplikację odwołań, które powinny mieć moduły.

<script type="module" id="dogs" src="blob:https://example.com/9dc17f20-04ab-44cd-906e"> 
    import {log} from /* #utils */ 'blob:https://example.com/88fd6f1e-fdf4-4920-9a3b'; 

    log("Exporting dog names."); 

    export const names = ["Kayla", "Bentley", "Gilligan"]; 
</script> 

Wykonanie elementów skryptowych modułu jest zawsze odroczony aż po dokument jest analizowany, więc nie musimy się martwić o starają się wspierać sposób, że tradycyjne elementy skrypt może zmodyfikować dokument, podczas gdy jest wciąż analizowana .

export {}; 

for (const original of document.querySelectorAll('script[type=inline-module]')) { 
    const replacement = document.createElement('script'); 

    // Preserve the ID so the element can be selected for import. 
    if (original.id) { 
    replacement.id = original.id; 
    } 

    replacement.type = 'module'; 

    const transformedSource = original.textContent.replace(
    // Find anything that looks like an import from '#some-id'. 
    /(from\s+|import\s+)['"](#[\w\-]+)['"]/g, 
    (unmodified, action, selector) => { 
     // If we can find a suitable script with that id... 
     const refEl = document.querySelector('script[type=module][src]' + selector); 
     return refEl ? 
     // ..then update the import to use that script's src URL instead. 
     `${action}/* ${selector} */ '${refEl.src}'` : 
     unmodified; 
    }); 

    // Include the updated code in the src attribute as a blob URL that can be re-imported. 
    replacement.src = URL.createObjectURL(
    new Blob([transformedSource], {type: 'application/javascript'})); 

    // Insert the updated code inline, for debugging (it will be ignored). 
    replacement.textContent = transformedSource; 

    original.replaceWith(replacement); 
} 

Ostrzeżenia: to prosta implementacja nie obsługuje skryptów elementy dodane po początkowym dokument został przeanalizowany ani dopuszczać, elementy skryptowe importować z innych elementów skryptowych, które występują po nich w dokumencie. Jeśli masz w dokumencie zarówno elementy skryptowe module, jak i inline-module, ich względna kolejność wykonania może być niepoprawna. Transformacja kodu źródłowego jest przeprowadzana za pomocą prostego wyrażenia regularnego, które nie obsługuje niektórych przypadków brzegowych, takich jak okresy w identyfikatorach.

+1

Możesz dalej grać w regex w '/ (z | import) \ s + ('|") (# [\ w \ -] +) \ 2/g' – Bergi

4

Jest to możliwe w przypadku pracowników serwisowych.

Ponieważ pracownik serwisowy powinien zostać zainstalowany, zanim będzie mógł przetworzyć stronę, wymaga to oddzielnej strony, aby zainicjować pracownika, aby uniknąć problemu z kurczakiem/jajkiem - lub strona może zostać ponownie załadowana, gdy pracownik jest gotowy.

Here's an example który ma być wykonalne w przeglądarkach obsługujących rodzime ES modułów i async..await (czyli Chrome)

index.html

<html> 
    <head> 
    <script> 
(async() => { 
    try { 
    const swInstalled = await navigator.serviceWorker.getRegistration('./'); 

    await navigator.serviceWorker.register('sw.js', { scope: './' }) 

    if (!swInstalled) { 
     location.reload(); 
    } 
    } catch (err) { 
    console.error('Worker not registered', err); 
    } 
})(); 
    </script> 
    </head> 
    <body> 
    World, 

    <script type="module" data-name="./example.js"> 
     export function example() { 
     document.body.appendChild(document.createTextNode("hello")); 
     }; 
    </script> 

    <script type="module"> 
     import {example} from './example.js'; 

     example(); 
    </script> 
    </body> 
</html> 

sw.js

self.addEventListener('fetch', e => { 
    // parsed pages 
    if (/^https:\/\/run.plnkr.co\/\w+\/$/.test(e.request.url)) { 
    e.respondWith(parseResponse(e.request)); 
    // module files 
    } else if (cachedModules.has(e.request.url)) { 
    const moduleBody = cachedModules.get(e.request.url); 
    const response = new Response(moduleBody, 
     { headers: new Headers({ 'Content-Type' : 'text/javascript' }) } 
    ); 
    e.respondWith(response); 
    } else { 
    e.respondWith(fetch(e.request)); 
    } 
}); 

const cachedModules = new Map(); 

async function parseResponse(request) { 
    const response = await fetch(request); 
    if (!response.body) 
    return response; 

    const html = await response.text(); // HTML response can be modified further 
    const moduleRegex = /<script type="module" data-name="([\w./]+)">([\s\S]*?)<\/script>/; 
    const moduleScripts = html.match(new RegExp(moduleRegex.source, 'g')) 
    .map(moduleScript => moduleScript.match(moduleRegex)); 

    for (const [, moduleName, moduleBody] of moduleScripts) { 
    const moduleUrl = new URL(moduleName, request.url).href; 
    cachedModules.set(moduleUrl, moduleBody); 
    } 
    const parsedResponse = new Response(html, response); 
    return parsedResponse; 
} 

Ciała skryptów są buforowane (nat ive Cache może być również używany) i zwracane dla odpowiednich żądań modułów.

Zmartwienia

  • Podejście jest gorsza aplikacji zbudowanej i pakietowego z łączenie narzędzie jak Webpack lub pakiet pod względem wydajności, elastyczności, solidności i wsparcia przeglądarki - zwłaszcza jeśli blokowanie jednoczesnych żądań są podstawowym problemem .

  • Skrypty śródlinowe zwiększają wykorzystanie przepustowości, w naturalny sposób unika się tego, gdy skrypty są ładowane raz i buforowane przez przeglądarkę.

  • Skrypty śródliniowe nie są modułowe i są sprzeczne z koncepcją modułów ES (chyba że są generowane z rzeczywistych modułów po szablonie po stronie serwera).

  • Inicjalizacja pracownika serwisowego powinna zostać przeprowadzona na oddzielnej stronie, aby uniknąć niepotrzebnych żądań.

  • Rozwiązanie jest ograniczone do jednej strony i nie uwzględnia <base>.

  • Wyrażenie regularne służy tylko do celów demonstracyjnych. W przypadku użycia jak w powyższym przykładzie umożliwia wykonanie dowolnego kodu JS, który jest dostępny na stronie. Należy użyć sprawdzonej biblioteki, takiej jak parse5 (spowoduje to zwiększenie wydajności, a mimo to mogą pojawić się obawy o bezpieczeństwo). Nigdy nie używaj wyrażeń regularnych do parsowania DOM.

+0

Uwielbiam to! Bardzo sprytnie. –

+0

To byłoby nawet bardziej obrzydliwe, więc pewnie go nie polecam, ale jeśli przepisujemy index.html, to daje nam to sposób na synchroniczne wykrywanie, czy pracownik serwisu został załadowany, poprzez dodanie do strony jakiegoś atrybutu, a więc cokolwiek innego z ładowania/uruchamiania niepoprawnie za pierwszym razem, zamiast czekać na wynik async getRegistration –

+0

Tak. 'location.reload()' nie pachnie dobrze, ale demonstruje problem.Zalecam generalnie, aby mieć oddzielne odpowiedzi serwera dla punkty wejściowe '/' i '/? serviceWorkerInstalledOrNotSupported'. – estus

Powiązane problemy