自从几年前 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 这个工具,除了根据文件是否修改来刷新页面之外,这个老家伙还能同步不同设备上,当前调试页面的滚动、点击事件等交互操作。介绍这个工具的具体细节,不在本文范畴,有兴趣的小伙伴可以访问它的官方网站: https://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