2017-05-06 47 views
34

我一直在試驗最近添加到瀏覽器中的新native ECMAScript module support。最後能夠直接從JavaScript中直接導入腳本,這非常令人愉快。在HTML中內聯ECMAScript模塊

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

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

然而,這只是讓我導入由獨立外部 JavaScript文件定義的模塊。我通常更喜歡內聯用於初始渲染的一些腳本,因此他們的請求不會阻止頁面的其餘部分。與傳統的非正式結構庫,這可能是這樣的:

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

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

然而,天真的內嵌模塊文件顯然是行不通的,因爲它會刪除用來識別模塊,其他模塊文件名。 HTTP/2服務器推送可能是處理這種情況的標準方法,但它仍然不是所有環境中的一種選擇。

是否可以使用ECMAScript模塊執行等效轉換? 有沒有辦法讓<script type="module">在另一個文檔中導入另一個導出的模塊?


我想這可以通過允許腳本指定文件路徑的工作,並且表現得好像它已經被下載或從路徑推進。

     /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> 

也許由一個完全不同的參考方案,如用於本地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> 

但這些都不能hypotheticals實際工作中,我的天堂」沒有看到一個替代方案。

+0

也許有更好的方法來實現這一點 - 可能使用服務人員? –

+0

我不認爲符合自制規格的'inline-module'可以被認爲是ES模塊的良好開端。 Webpack/Rollup捆綁包在生產中仍然是不可或缺的 - 尤其*如果您害怕阻止請求。是的,服務人員看起來像一個可行的解決方案 - 但它仍然應該提出要求,以提供數據...可以阻止,順便說一句。 – estus

+0

@estus我想象着使用服務工作人員採用內嵌'

14

黑客加上我們自己的import from '#id'

出口/內嵌腳本之間的進口本身不支持,但它是一個有趣的練習破解一道,爲我的文檔的實現。代碼golfed下來一小塊,我用這樣的:

<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>

相反的<script type="module">,我們需要使用自定義類型像<script type="inline-module">來定義我們的腳本元素。這可以防止瀏覽器嘗試執行其內容本身,讓我們來處理它們。該腳本(以下完整版本)查找文檔中的所有inline-module腳本元素,並將它們轉換爲具有我們想要的行爲的常規腳本模塊元素。

內聯腳本不能直接相互導入,所以我們需要爲腳本提供可導入的URL。我們爲其中的每個人生成一個blob: URL,其中包含其代碼,並將src屬性設置爲從該URL運行,而不是以內聯方式運行。 blob: URL與來自服務器的普通URL相似,因此可以從其他模塊導入它們。每次我們看到後續的inline-module嘗試從'#example'導入時,其中example是我們轉換的inline-module的ID,我們修改該導入以從blob: URL導入。這保持了模塊應該具有的一次性執行和參考重複數據刪除。

<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> 

模塊腳本元素的執行總是推遲,直到解析文檔後,所以我們並不需要擔心試圖支持傳統的腳本元素可以修改文檔的方式,同時它還是被解析。

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); 
} 

警告:這個簡單的實現還不能處理的初始文檔被解析後添加腳本元素,或允許腳本元素從文檔中後,它們出現的其它腳本元素導入。如果在文檔中同時具有moduleinline-module腳本元素,則其相對執行順序可能不正確。源代碼轉換使用原始正則表達式執行,不會處理某些邊緣情況(如ID中的句點)。

+1

你可以進一步高爾夫的正則表達式來'/(from | import)\ s +('|「)(#[\ w \ - ] +)\ 2/g' – Bergi

4

服務人員可以這樣做。

由於服務工作者應當安裝它能夠處理一個頁面之前,這需要有一個單獨的頁面初始化工作人員,以避免雞/蛋的問題 - 或當工人準備一個頁面可以重新加載。

Here's an example其應該是在支持本地ES模塊和async..await(即鉻)瀏覽器的可操作性:

的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; 
} 

腳本正在被緩存(nat也可以使用Cache)並返回相應的模塊請求。

參與討論

  • 的方法是次於構建的應用程序,並在性能,靈活性,堅固性和瀏覽器支持方面捆綁喜歡的WebPack或彙總工具分塊 - 尤其是如果阻塞併發請求是首要關注的問題。

  • 內聯腳本增加了帶寬使用,當腳本加載一次並由瀏覽器緩存時,這當然可以避免。

  • 內聯腳本不是模塊化的,與ES模塊的概念相矛盾(除非它們是由服務器端模板從實模塊生成的)。

  • 服務工作者初始化應該在單獨的頁面上執行以避免不必要的請求。

  • 該解決方案僅限於單個頁面,並且不考慮<base>

  • 正則表達式僅用於演示目的。當像上面例子一樣使用時,它可以執行頁面上提供的任意JS代碼。應使用類似parse5的經過驗證的庫(它會導致性能開銷,但仍可能存在安全問題)。 切勿使用正則表達式來解析DOM

+0

我喜歡它!非常聰明。 –

+0

這將會是偶數更重要的是,所以我可能不會推薦它,但是如果我們重寫index.html,那麼這就給我們一種同步檢測服務工作者是否被加載的方法,通過給頁面添加一些屬性,從而防止第一次加載/運行不當,而不是等待異步getRegistration結果 –

+0

是的''location.reload()'沒有聞起來不錯,但是演示了這個問題通常我會建議有單獨的服務器響應'/'和'/?serviceWorkerInstalledOrNotSupported'入口點。 – estus