前幾天介紹過用 Tag Helper 製作 ASP.NET Core 網頁自訂元件,我便開心寫起元件,初測 OK,與 Vue.js 整合卻遇到問題,與 Vue 處理內含 <script> <style> HTML 的行為有關,請容我娓娓道來。

我設計出以下實驗重現及觀察。新增 ASP.NET Core MVC 專案,放入 /wwwroot/js/test.js 及 /wwwroot/css/test.css:

let d = document.createElement('div');
d.innerText = 'appended by test.js';
document.querySelector('main').appendChild(d);
.test-css {
    color: red;
}

設計一個 ElementWithJsCssTagHelper,分別用 <link href="..." > 載入 test.css、用 <script src="..." > 載入 test.js,並內嵌 <style> 區塊宣告 .inline-css 樣式、<script> 區塊則在網頁新增一句 appended by inline script block:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace mvc_vue.Models
{
    public class ElementWithCssJsTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "div";
            output.Content.SetHtmlContent(@"
<link href='/css/test.css' rel='stylesheet' />
<style>.inline-css { color: red; }</style>
<script src='/js/test.js'></script>
<div class='test-css'>by test.css</div>
<div class='inline-css'>by inline-css</div>
<script>
let isd = document.createElement('div');
isd.innerText = 'appended by inline script block';
document.querySelector('main').appendChild(isd);
</script>
            ");
        }
        
    }
}

Index.cshtml 長這樣:

@addTagHelper *,mvc-vue
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <element-with-css-js></element-with-css-js>
</div>

測試 .test-css、.inline-css 及 test.js、Script 區塊都正常運作。

大家都知道我是「輕前端」派,寫前端不能沒有 MVVM,但偏好從 MVC 網頁載入 vue.js;喜歡抄起輕機槍就上場殺敵的輕便敏捷,不想動用 npm、webpack、TypeScript 等重裝甲。所以,仿照輕前端作法對 Index.cshtml 稍作修改,引用 vue.global.prod.min.js 建立空的 Vue Model 掛載到主 div 上:

@addTagHelper *,mvc-vue
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center" id="app">
    <element-with-css-js></element-with-css-js>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.33/vue.global.prod.min.js"></script>
<script>
    var vm = Vue.createApp({}).mount('#app');
</script>

結果有點奇怪,.inline-css 沒生效,檢查 HTML,發現 <script src="/js/test.js" >、.inline-css <style> 區塊、<script> 區塊不見了(橘色箭頭處),但 test.js 與 <script> 區塊是有執行的(黃框):

將 vue.global.prod.min.js (線上維運版) 換成 vue.global.min.js (開發測試版,提供額外偵錯訊息)可得到線索,Vue 跳出警示 [Vue warn]: Template compilation error: Tags with side effect (<script> and <style>) are ignored in client component templates.

爬文得知(我沒找到官方文件的相關說明,歡迎補充),Vue 解析 HTML 時會略過 <script>、<style>。兩行 appended by 文字仍出現是因為 <script src="/js/test.js" > 及 <script> 區塊在 Vue 運作前就執行了,但 <style> 區塊被移除造成了 .line-css 樣式失效。

元件自帶所需 JS 跟 CSS 很符合 SoC (關注點分離) 哲學,像 Vue 的 SFC(單一元件檔)便內含 <script> 及 <stylet>,我撰寫 Tag Helper / HtmlHelper / WebForm WebController 也常會遵循相同概念(所以這問題不限 ASP.NET Core、在 MVC 及 WebForm 也可能遇到),而這有違 <script>、<stylet> 應放在 Vue 綁定範圍外的慣例,若要 Vue 正確處理 <script>、<stylet>,則要寫成 Dynamic Component <component :is="'script'">、<component :is="'style'">,所以 ElementWithCssJsTagHelper 需修改如下:

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "div";
            output.Content.SetHtmlContent(@"
<link href='/css/test.css' rel='stylesheet' />
<component :is=""'style'"">.inline-css { color: red; }</component>
<component :is=""'script'"" src='/js/test.js'></component>
<div class='test-css'>by test.css</div>
<div class='inline-css'>by inline-css</div>
<component :is=""'script'"">
let isd = document.createElement('div');
isd.innerText = 'appended by inline script block';
document.querySelector('main').appendChild(isd);
</component>
            ");
        }

測試成功。

I used to include related script and style block in ASP.NET Core tag helper and this causes compilation issue when integrated with Vue.js. This article provide the tips to solve the problem.


Comments

# by Sean

黑暗執行緒大大您好 借題發問一下,像您範例中 <component :is=""'script'"" src='/js/test.js'></component> 請問您是如何處理js 瀏覽器快取的問題? 一旦test.js有修改,但前端User的test.js可能是快取的js檔 像有個myAlert.js 裡面會動態render vue component (用意是 以js指令 做個vue alert box , myAlert(....) 就產生一個Alert 並顯示msg) 此時會在myAlert.js裡面 利用 $.getScript("/js/component/VueAlert.js"); (VueAlert.js就是自行建立的Vue Component) 但麻煩的是一旦VueAlert.js內容有修改,前端頁面不一定有辦法反應,因為client的VueAlert.js被快取了 感謝您。

# by Jeffrey

to Sean, 處理 js/css 前端 Cache 問題,最無敵的方法是加上 ?v=XXXX 之類的參數,每次 js 異動就修 改 XXXX 值,讓 URL 產生差異,瀏覽器即會視為不同檔案重新下載。如果是 ASP.NET Core,有現成的傻瓜解法 https://blog.darkthread.net/blog/asp-append-version/ ,若是寫在前端 $.getScript(),就要自己想辦法,可以每次改版一併更新 getScript 的 Url 加上不同 ?v=...,再不然就是搞個機制偵測檔案異動時自動修改 ?v= 參數,但這要自動化要花點功夫。

Post a comment