你的網站或許不需要前端構建

蘇洋發表於2019-05-26

本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或重新修改使用,但需要註明來源。 署名 4.0 國際 (CC BY 4.0)

本文作者: 蘇洋

建立時間: 2019年05月27日 統計字數: 8760字 閱讀時間: 18分鐘閱讀 本文連結: soulteary.com/2019/05/27/…


你的網站或許不需要前端構建

自從幾年前 Webpack 替換掉了 GulpGrunt 後,我們可以明顯看到前端專案的工程複雜度越來越高,前端技術迭代速度也越來越快。

大廠也好、培訓班也罷,都針對 Webpack、Babel 、ESLint 前端工程工具三巨頭貢獻出了數不勝數分享和案例。

但是隨之而來的是,前端專案幾乎沒有了往日的“簡單愉快”,想用流行框架寫一個專案,一般得先整一個腳手架,如果你寫的程式沒有“經歷前端構建”,整的你都不好意思和同行打招呼。

這篇文章會以兩個簡單的例子來說明,即使不配置腳手架、使用一些“老傢伙”一樣可以開發出高效能的網站。

額外說明

本篇文章並不完全適用十幾人乃至幾十人以上團隊規模的複雜、需要高密度的協作專案,僅針對中小型專案,諸如簡單的後臺、流程配置、甚至是 Demo。

碎碎唸了這麼多,讓我們正式開始迴歸愉快的前端開發

從一個簡單的“單頁”應用開始

不論是使用 ReactVue 還是使用更有年代感的 jQuery ,做一個簡單的頁面,不外乎分別完成 “頁面結構”、“頁面風格”、“頁面功能” 三個部分的編寫。

我們使用現在比較流行的 Vue 舉個例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>簡單的頁面</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ant-design-vue@1.3.8/dist/antd.min.css">
    <!-- 你也可以選擇儲存在本地使用, 指令碼資源也是一樣 -->
    <!-- <link rel="stylesheet" href="assets/common/antd-v1.3.8.min.css"> -->
    <style>body{color:#2c3e50}#header{height:50px;background:#fff;border-bottom:1px solid #eceef1}#header-nav{float:left;height:50px}#header-search{float:right;width:180px;margin:4px}#header-button{float:right;height:50px;overflow:hidden;line-height:50px}#has-team-news{top:-7px;left:-3px}.logo{width:120px;height:100%;line-height:50px;font-weight:bold;background:rgba(255,255,255,.2);float:left}#left-menu{margin-top:10px}#left-menu-wrap{padding-left:10px;margin-left:10px}#top-switch{margin-top:10px;overflow:hidden}#top-switch-2{float:right;overflow:hidden;width:100px;height:20px;line-height:20px;margin-top:10px}#top-switch-2 a{font-size:12px}#top-switch-2 a.grey{color:gray}#top-divider{margin:10px}#post-container{margin:10px}.slick-slide{text-align:center;height:160px;line-height:160px;background:#364d79;overflow:hidden}.slick-slide h3{color:#fff}#carousel{margin:10px}.demo-loadmore-list{min-height:350px}.post-meta{display:inline-block;font-size:13px;line-height:13px;height:13px;overflow:hidden;font-style:italic;margin-right:4px}.desc{margin:14px 0;font-size:16px}#tag-list .ant-tag{margin-bottom:8px}.item-people{margin:10px 0}#ranking .ant-tabs-top-bar{margin-bottom:0}#car-list,#cars-list{border-top:none}</style>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/ant-design-vue@1.3.8/dist/antd.min.js"></script>
</head>
<body>

    <div id="app">
        <a-layout>
           <a-layout-header id="header">
              <div class="logo">部落格Logo擺放</div>
              <a-menu mode="horizontal" :defaultSelectedKeys="['1']" id="header-nav">
                 <a-menu-item key="1">首頁</a-menu-item>
                 <a-menu-item key="2">
                    團隊
                    <a-badge id="has-team-news" status="success"></a-badge>
                 </a-menu-item>
                 <a-menu-item key="3">標籤</a-menu-item>
              </a-menu>
              <a-button-group id="header-button">
                 <a-button icon="file-text"></a-button>
                 <a-button icon="star"></a-button>
                 <a-button icon="user"></a-button>
              </a-button-group>
              <a-input-search id="header-search" placeholder="你不知道啥?問我鴨" />
           </a-layout-header>
           <a-layout-content>
              <a-row type="flex">
                 <a-col :span="4">
                    <div id="left-menu-wrap">
                       <a-menu id="left-menu" mode="inline" :openKeys="openKeys" @openChange="onMenuOpenChange">
                          <a-sub-menu key="sub0">
                             <span slot="title">
                                <a-icon type="home"></a-icon>
                                <span>推薦</span>
                             </span>
                             <a-menu-item key="1">牛逼的比賽</a-menu-item>
                             <a-menu-item key="2">犀利的觀點</a-menu-item>
                             <a-menu-item key="3">給力的事件</a-menu-item>
                             <a-menu-item key="4">特別的曝光</a-menu-item>
                          </a-sub-menu>
                          <a-sub-menu key="sub1">
                             <span slot="title">
                                <a-icon type="html5"></a-icon>
                                <span>前端</span>
                             </span>
                             <a-menu-item key="1">最佳實踐</a-menu-item>
                             <a-menu-item key="2">基礎知識</a-menu-item>
                             <a-menu-item key="3">多彩樣式</a-menu-item>
                             <a-menu-item key="4">有趣指令碼</a-menu-item>
                          </a-sub-menu>
                          <a-sub-menu key="sub2">
                             <span slot="title">
                                <a-icon type="codepen"></a-icon>
                                <span>後端</span>
                             </span>
                             <a-menu-item key="5">Option 5</a-menu-item>
                             <a-menu-item key="6">Option 6</a-menu-item>
                             <a-sub-menu key="sub3" title="Submenu">
                                <a-menu-item key="7">Option 7</a-menu-item>
                                <a-menu-item key="8">Option 8</a-menu-item>
                             </a-sub-menu>
                          </a-sub-menu>
                          <a-sub-menu key="sub3">
                             <span slot="title">
                                <a-icon type="appstore"></a-icon>
                                <span>運維</span>
                             </span>
                             <a-menu-item key="9">Option 9</a-menu-item>
                             <a-menu-item key="10">Option 10</a-menu-item>
                             <a-menu-item key="11">Option 11</a-menu-item>
                             <a-menu-item key="12">Option 12</a-menu-item>
                          </a-sub-menu>
                          <a-sub-menu key="sub4">
                             <span slot="title">
                                <a-icon type="html5"></a-icon>
                                <span>演算法</span>
                             </span>
                             <a-menu-item key="9">Option 9</a-menu-item>
                             <a-menu-item key="10">Option 10</a-menu-item>
                             <a-menu-item key="11">Option 11</a-menu-item>
                             <a-menu-item key="12">Option 12</a-menu-item>
                          </a-sub-menu>
                          <a-sub-menu key="sub5">
                             <span slot="title">
                                <a-icon type="html5"></a-icon>
                                <span>分類</span>
                             </span>
                             <a-menu-item key="9">Option 9</a-menu-item>
                             <a-menu-item key="10">Option 10</a-menu-item>
                             <a-menu-item key="11">Option 11</a-menu-item>
                             <a-menu-item key="12">Option 12</a-menu-item>
                          </a-sub-menu>
                          <a-sub-menu key="sub6">
                             <span slot="title">
                                <a-icon type="html5"></a-icon>
                                <span>分類</span>
                             </span>
                             <a-menu-item key="9">Option 9</a-menu-item>
                             <a-menu-item key="10">Option 10</a-menu-item>
                             <a-menu-item key="11">Option 11</a-menu-item>
                             <a-menu-item key="12">Option 12</a-menu-item>
                          </a-sub-menu>
                       </a-menu>
                    </div>
                 </a-col>
                 <a-col :span="14">
                    <a-carousel id="carousel" autoplay>
                       <div>
                          <h3>涼風有幸 1</h3>
                       </div>
                       <div>
                          <h3>秋月無邊 2</h3>
                       </div>
                       <div>
                          <h3>啦啦啦啦 3</h3>
                       </div>
                       <div>
                          <h3>置頂精選 4</h3>
                       </div>
                    </a-carousel>
                    <div id="top-switch">
                       <a-dropdown>
                          <a-menu slot="overlay" @click="handleTopMenuClick">
                             <a-menu-item key="1">
                                <a-icon type="user"></a-icon>
                                編輯精選
                             </a-menu-item>
                             <a-menu-item key="2">
                                <a-icon type="user"></a-icon>
                                最新發布
                             </a-menu-item>
                          </a-menu>
                          <a-button style="margin-left: 8px">
                             編輯精選
                             <a-icon type="down" />
                          </a-button>
                       </a-dropdown>
                       <div id="top-switch-2">
                          <a href="#">熱門</a>
                          <a-divider type="vertical"></a-divider>
                          <a href="#" class="grey">最新</a>
                       </div>
                       <a-divider id="top-divider"></a-divider>
                    </div>
                    <div id="post-container">
                       <a-list class="demo-loadmore-list" :pagination="pagination" :loading="loading"
                          itemLayout="horizontal" :dataSource="postDataSource" :locale="{emptyText: '暫無資料'}">
                          <div v-if="showLoadingMore" slot="loadMore"
                             :style="{ textAlign: 'center', marginTop: '12px', height: '32px', lineHeight: '32px' }">
                             <a-spin v-if="loadingMore" />
                             <a-button v-else @click="onLoadMore">loading more</a-button>
                          </div>
                          <a-list-item v-for="(item, index) in postDataSource">
                             <a-card style="width:100%">
                                <h2>這是一篇部落格的標題,可能很長很長很長很長</h2>
                                <div class="post-meta">
                                   <a-icon type="user"></a-icon>
                                   @nickname
                                </div>
                                <div class="post-meta">
                                   <a-icon type="clock-circle"></a-icon>
                                   10分鐘前
                                </div>
                                </a-avatar>
                                <p class="desc">
                                   簡單的內容描述。
                                </p>
                                <div style="float:left">
                                   <a-tag>前端</a-tag>
                                   <a-tag>工程工具</a-tag>
                                   <a-tag>方法論</a-tag>
                                </div>
                                <div class="post-meta" style="float:right">
                                   <span>
                                      <a-icon type="like" style="margin-right: 8px"></a-icon>
                                   </span>
                                   <span>
                                      <a-icon type="star" style="margin-right: 8px"></a-icon>
                                   </span>
                                   <span>
                                      <a-icon type="message" style="margin-right: 8px"></a-icon>
                                   </span>
                                </div>
                             </a-card>
                          </a-list-item>
                       </a-list>
                       <a-pagination :defaultCurrent="6" :total="500" />
                    </div>
                 </a-col>
                 <a-col :span="6">
                    <a-card style="margin: 10px; border-color: #42b983;">
                       <h3 style="color:#42b983">
                          <a-icon type="notification" style="margin-right: 8px"></a-icon>
                          這裡是一個公告標題
                       </h3>
                       <p>這裡有一個描述性詞彙描述性詞彙描述性詞彙描述性詞彙描述性詞彙</p>
                       <a-button style="width:100%;background:#42b983;border-color: #42b983;color: #fff;">開始瀏覽
                       </a-button>
                    </a-card>
                    <a-card id="tag-list" style="margin: 10px;">
                       <h3>
                          <a-icon type="tag" style="margin-right: 8px"></a-icon>
                          熱門標籤
                       </h3>
                       <a-divider></a-divider>
                       <a-tag>前端</a-tag>
                       <a-tag>工程工具</a-tag>
                       <a-tag>方法論</a-tag>
                       <a-tag>工程工具</a-tag>
                       <a-tag>方法論</a-tag>
                       <a-tag>前端</a-tag>
                       <a-tag>方法論</a-tag>
                       <a-tag>工程工具</a-tag>
                       <a-tag>方法論</a-tag>
                       <a-tag>前端</a-tag>
                    </a-card>
                    <a-tabs defaultActiveKey="2" style="margin: 10px;" type="card" id="ranking">
                       <a-tab-pane tab="月度優秀作者" key="1">
                          <a-card id="car-list">
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">1</a-avatar>
                                <a href="#">作者 - 簡單描述</a>
                                <a-badge style="zoom:0.8" count="12"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">2</a-avatar>
                                <a href="#">作者 - 簡單描述</a>
                                <a-badge style="zoom:0.8" count="10"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">1</a-avatar>
                                <a href="#">作者 - 簡單描述</a>
                                <a-badge style="zoom:0.8" count="9"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">4</a-avatar>
                                <a href="#">作者 - 簡單描述</a>
                                <a-badge style="zoom:0.8" count="7"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">5</a-avatar>
                                <a href="#">作者 - 簡單描述</a>
                                <a-badge style="zoom:0.8" count="6"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">6</a-avatar>
                                <a href="#">作者 - 簡單描述</a>
                                <a-badge style="zoom:0.8" count="2"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                          </a-card>
                       </a-tab-pane>
                       <a-tab-pane tab="月度優秀作者" key="2">
                          <a-card id="cars-list">
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">1</a-avatar>
                                <a href="#">作者</a>
                                <a-badge style="zoom:0.8" count="109"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">1</a-avatar>
                                <a href="#">作者</a>
                                <a-badge style="zoom:0.8" count="109"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                             <div class="item-people">
                                <a-avatar style="margin-right:4px">1</a-avatar>
                                <a href="#">作者</a>
                                <a-badge style="zoom:0.8" count="109"
                                   :numberStyle="{backgroundColor: '#8bc34a'} " />
                             </div>
                          </a-card>
                       </a-tab-pane>
                    </a-tabs>
                 </a-col>
              </a-row>
           </a-layout-content>
           <a-layout-footer>
              <div>
                 <a-divider>-EOF-</a-divider>
                 <a-divider type="vertical"></a-divider>
                 <a href="#">投稿</a>
                 <a-divider type="vertical"></a-divider>
                 <a href="#">關於</a>
              </div>
           </a-layout-footer>
        </a-layout>
    </div>

<script>
Vue.use(antd);

const posts = [[],[],[],[],[],[],]

var app = new Vue({
    el: '#app',
    data() {
        return {
            rootSubmenuKeys: ['sub0', 'sub1', 'sub2', 'sub3', 'sub4', 'sub5', 'sub6'],
            openKeys: ['sub0'],

            loading: true,
            loadingMore: false,
            showLoadingMore: true,
            postDataSource: posts,

            pagination: {
                onChange: (page) => {
                    console.log('Change Page', page);
                },
                pageSize: 3,
            },

        }
    },
    mounted() {
        this.getData((res) => {
            this.loading = false
            this.postDataSource = res
        })
    },
    methods: {
        onMenuOpenChange(openKeys) {
            const latestOpenKey = openKeys.find(key => this.openKeys.indexOf(key) === -1)
            if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
                this.openKeys = openKeys
            } else {
                this.openKeys = latestOpenKey ? [latestOpenKey] : []
            }
        },

        handleButtonClick(e) {
            console.log('Button Clicked', e);
        },
        handleTopMenuClick(e) {
            console.log('Top Menu Clicked', e);
        },

        getData(callback) {
            setTimeout(() => {
                callback(posts)
            }, 300)
        },
        onLoadMore() {
            this.loadingMore = true
            this.getData((res) => {
                this.postDataSource = this.postDataSource.concat(res)
                this.loadingMore = false
                this.$nextTick(() => {
                    window.dispatchEvent(new Event('resize'))
                })
            })
        },

    },
})
</script>

</body>
</html>
複製程式碼

將上面的三百來行程式碼儲存為 index.html, 使用瀏覽器直接開啟,不出意外你將看到下面的介面。

一個簡單的單頁 Demo

簡單把玩之後,你一定會說,這個示例頁面沒有什麼複雜互動,而且這不就是官方的推薦用法之一嘛。

是的,但希望你能夠看到,像上面這樣做一個樣子還說的過去的頁面,真的不是必須把構建工具也“摻和”進來,即使你把元件互動的部分填充完畢。

而開發過程,就可以迴歸經典的“邊改邊重新整理”,所見即所得了。

接下來,我們來聊聊如何將上面的程式拆分為模組使用,讓多個頁面之間可以複用模組,當然還是在不使用構建工具的前提下。

拆分功能模組

將單一職責的功能抽象模組化,可以說是工程師們的日常。這樣做除了提高了可維護性、潛在的提升了頁面效能、讓軟體構建更靈活之外、最大的收益便是增加了功能模組的可複用性。

我們日常使用 webpack 的時候,一定有看到過被分割為一堆名為 chunk 檔案的指令碼,或者名稱可能叫做 vendorappcomponent 的檔案。這些便是構建程式幫我們切割的軟體模組了,甚至是上面例子中引入的 *.min.js. 也是如此。

如果我們不使用構建工具進行模組拆分,該怎麼做呢?這裡面常見的坑有哪些呢?

  1. 拆分為多個模組之後,會涉及到額外的網路資源獲取和解析處理。
  2. 拆分為多個模組之後,可能會涉及到額外的模組依賴管理。
  3. 拆分為多個模組之後,會涉及到資料、狀態同步管理。

想要解決前兩個問題,可以通過使用 Require.js 之類的資源載入器,來控制拆分後多出來的資原始檔的載入和對模組進行依賴管理,想了解這個老傢伙的細節,可以瀏覽它的官方網站

而拆分後的模組,想要保持書寫上的簡單明快,這裡選擇使用 Vue 的 Component 語法進行模組儲存,所以需要額外引入一個模組解析器,原理很簡單,通過 XHR 方法將資源獲取後,使用正則將內容分別抽取為“樣式”、“指令碼”、“模版”,然後在合適的時機在瀏覽器環境執行。為了簡化操作,我在 requirejs-vue 的基礎上進行了刪減,有興趣可以圍觀原始碼

至於第三個問題,不論是使用單例共享資料來源、亦或者使用釋出訂閱模式傳遞資料、或者使用觀察者模式都可以,解決的手段還有很多,就不擴充套件了,本文暫且略過,你可以挑一個你覺得順手的使用。

以上面的單頁程式為例,我們先編寫頁面框架。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>將模版分離</title>
    <link rel="stylesheet" href="assets/common/antd-v1.3.8.min.css">

    <script src="assets/common/vue-v2.6.10.min.js"></script>
    <script src="assets/common/moment-v2.24.0.min.js"></script>
    <script src="assets/common/antd-v1.3.8.min.js"></script>

    <script src="assets/common/require-v2.3.6.min.js"></script>

    <script>
        Vue.use(antd);
        requirejs && requirejs.config({
            baseUrl: './assets',
            paths: { 'vue': 'common/require-vue' },
            config: { 'vue': { 'css': 'inject', 'templateVar': '__template__' } }
        });
    </script>
</head>
<body>

    <div id="app">
        <a-layout>
            <a-layout-header id="header"></a-layout-header>

            <a-layout-content>
                <a-row type="flex">
                    <a-col id="navbar" :span="4"></a-col>
                    <a-col id="main" :span="14"></a-col>
                    <a-col id="sidebar" :span="6"></a-col>
                </a-row>
            </a-layout-content>

            <a-layout-footer id="footer"></a-layout-footer>
        </a-layout>
    </div>

    <script>
        requirejs([
            'vue!template/header.html',
            'vue!template/footer.html',
            'vue!template/navbar.html',
            'vue!template/sidebar.html',
            'vue!template/carousel.html',
            'vue!template/feed.html',
        ], function (header, footer, navbar, sidebar, carousel, feed) {

            var appInst = new Vue({ el: '#app' });

            var headerInst = new Vue({ el: '#header' });
            header.$mount();
            headerInst.$el.appendChild(header.$el);

            var footerInst = new Vue({ el: '#footer' });
            footer.$mount();
            footerInst.$el.appendChild(footer.$el);

            var navbarInst = new Vue({ el: '#navbar' });
            navbar.$mount();
            navbarInst.$el.appendChild(navbar.$el);

            var sidebarInst = new Vue({ el: '#sidebar' });
            sidebar.$mount();
            sidebarInst.$el.appendChild(sidebar.$el);

            var mainInst = new Vue({ el: '#main' });
            carousel.$mount();
            mainInst.$el.appendChild(carousel.$el);
            feed.$mount();
            mainInst.$el.appendChild(feed.$el);
        });
    </script>
</body>
</html>
複製程式碼

相比較上一小節三百來行混雜了細節邏輯的程式碼,這個長度只有不到一百行的程式碼是不是邏輯清晰許多呢。

剛剛提到了模組複用,其實也很簡單,比如我們想實現一個“列表頁面”,可以這麼寫:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>複用模組的例子</title>
    <link rel="stylesheet" href="assets/common/antd-v1.3.8.min.css">

    <script src="assets/common/vue-v2.6.10.min.js"></script>
    <script src="assets/common/moment-v2.24.0.min.js"></script>
    <script src="assets/common/antd-v1.3.8.min.js"></script>

    <script src="assets/common/require-v2.3.6.min.js"></script>

    <script>
        Vue.use(antd);
        requirejs && requirejs.config({
            baseUrl: './assets',
            paths: { 'vue': 'common/require-vue' },
            config: { 'vue': { 'css': 'inject', 'templateVar': '__template__' } }
        });
    </script>
</head>
<body>

    <div id="app">
        <a-layout>
            <a-layout-header id="header"></a-layout-header>
            <a-layout-content>
                <a-row type="flex">
                    <a-col :pull="5" :push="5" :span="14" id="main"></a-col>
                </a-row>
            </a-layout-content>
            <a-layout-footer id="footer"></a-layout-footer>
        </a-layout>
    </div>

    <script>
        requirejs([
            'vue!template/header.html',
            'vue!template/footer.html',
            'vue!template/list.html',
        ], function (header, footer, submit) {
            var appInst = new Vue({ el: '#app' });

            var headerInst = new Vue({ el: '#header' });
            header.$mount();
            headerInst.$el.appendChild(header.$el);

            var footerInst = new Vue({ el: '#footer' });
            footer.$mount();
            footerInst.$el.appendChild(footer.$el);

            var mainInst = new Vue({ el: '#main' });
            submit.$mount();
            mainInst.$el.appendChild(submit.$el);

        });
    </script>
</body>
</html>
複製程式碼

聊完頁面框架後,我們接著來看看拆分出的模組怎麼寫,以一個簡單的 header 模組舉例:

<script>
define([], function() {
    return new Vue({
        template: __template__,
        data() {
            return {}
        },
        mounted() {},
        methods: {},
    })
});
</script>

<style>
#header{height:50px;background:#fff;border-bottom:1px solid #eceef1}#header-nav{float:left;height:50px}#header-search{float:right;width:180px;margin:4px}#header-button{float:right;height:50px;overflow:hidden;line-height:50px}#has-team-news{top:-7px;left:-3px}.logo{width:120px;height:100%;line-height:50px;font-weight:bold;background:rgba(255, 255, 255, .2);float:left}
</style>

<template>
    <div>
        <div class="logo">部落格Logo擺放</div>
        <a-menu mode="horizontal" :defaultSelectedKeys="['1']" id="header-nav">
           <a-menu-item key="1">
              首頁
           </a-menu-item>
           <a-menu-item key="2">
              團隊
              <a-badge id="has-team-news" status="success"></a-badge>
           </a-menu-item>
           <a-menu-item key="3">
              標籤
           </a-menu-item>
        </a-menu>
        <a-button-group id="header-button">
           <a-button icon="file-text"></a-button>
           <a-button icon="star"></a-button>
           <a-button icon="user"></a-button>
        </a-button-group>
        <a-input-search id="header-search" placeholder="你不知道啥?問我鴨"></a-input-search>
     </div>
</template>
複製程式碼

可以看到,這完全就是普通的 Vue 元件模版嘛。

其他模組的程式碼可以在這裡找到,拆分方式大同小異,在此就不進行贅述。

和上一小節不同的是,因為我們使用了 XHR 的方式獲取資源,所以使用瀏覽器直接開啟 HTML 頁面的方法來預覽效果,會得到類似下面的報錯而無法得到想要的結果。

Access to XMLHttpRequest at 'file:///Users/soulteary/You-Dont-Need-Webpack/src/assets/template/header.html' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https. 
複製程式碼

這裡解決的方法很簡單,將頁面扔到一個能夠提供 HTTP 服務的程式裡就好了,可以使用 Node(HTTP Server、Express、KOA)等方案、也可以使用 Apache、Nginx、Caddy… 選擇你順手的工具就好啦。

在 GitHub 倉庫中,我提供了一個 docker-compose.yml 編排檔案,如果你本地有安裝 Docker 的話,只需要 Clone 下來專案,接著執行 docker-compose up ,開啟 localhost:10240/split.html 就能看到預覽結果了。

不使用構建工具拆分頁面模組的例子

體驗增強

如果你想獲取和 Webpack 實時重新整理頁面的開發體驗,可以考慮全域性安裝 browsersync 這個工具,除了根據檔案是否修改來重新整理頁面之外,這個老傢伙還能同步不同裝置上,當前除錯頁面的滾動、點選事件等互動操作。介紹這個工具的具體細節,不在本文範疇,有興趣的小夥伴可以訪問它的官方網站: www.browsersync.io/

在本例中,我們將模組拆分為多個 .html 檔案,雖然請求數多了,無法像傳統指令碼、樣式資源一樣享受服務端 combo 的能力。

但是因為使用了 HTML、又沒有經過構建壓縮混淆,配合 CMS 實時更新一些配置,改變頁面功能反而變得更容易進行操作了。畢竟上線後毋需構建發版。(可以瞭解淘寶 TMS 模組化方案)

看起來變多的請求

另外,如果實在對請求數敏感,可以針對模組載入器進行優化,實現類似 lsloader 之類的本地強快取+資源版本管理的功能,減少請求獲取。不過已經 2019 年,這點請求數對於多路複用的 HTTP2,隨處可見的大頻寬完全不是問題。

即使不使用 HTTPS(HTTP2)的方式開啟頁面,進行模組化拆分的頁面首屏體驗也優於未拆分頁面。

未拆分模組的頁面

未進行模組化拆分的頁面重新整理後,會明顯出現頁面白屏抖動。

進行了模組拆分的頁面

而拆分模組的頁面,展示則會“順滑”許多,當然如果你追求極致,還可以新增骨架屏。

如果上面的動圖還不夠清楚的話,可以看兩種情況下的效能測試。

未拆分模組的頁面

未拆分頁面首幀雖然快,但是隨著業務指令碼的複雜,Evaluate Script 的時間也會越來越長,導致 DOM Content Loaded 被無限滯後,在使用者體驗上會帶來卡頓感。

進行了模組拆分的頁面

而進行拆分了頁面模組拆分後,DOM Content Loaded 時機被極大的提前,雖說整體指令碼複雜度不變,但是單一模組複雜度變低,伴隨 DCL 時間提前,模組指令碼的解析完畢時間也提前了。

最後

再次重申,本篇文章不是說我們開發專案不進行腳手架配置、完全不使用 Webpack 等前端優秀工具。

重點是在擁有搭建開發環境的能力後,在適合的場景下,我們應該適當靈活變通,使用更簡單輕快的方案進行開發,騰出配置環境、安裝模組的時間去做更有意思的事情。

本文示例中的介面,參考了 https://love2.io/掘金 的設計,感謝設計師們的辛苦付出。

—EOF


我現在有一個小小的折騰群,裡面聚集了一些喜歡折騰的小夥伴。

在不發廣告的情況下,我們在裡面會一起聊聊軟體、HomeLab、程式設計上的一些問題,也會在群裡不定期的分享一些技術沙龍的資料。

喜歡折騰的小夥伴歡迎掃碼新增好友。(請註明來源和目的,否則不會通過稽核)

關於折騰群入群的那些事

相關文章