你的网站或许不需要前端构建

自从几年前 Webpack 替换掉了 GulpGrunt 后,我们可以明显看到前端项目的工程复杂度越来越高,前端技术迭代速度也越来越快。

大厂也好、培训班也罢,都针对 Webpack、Babel 、ESLint 前端工程工具三巨头贡献出了数不胜数分享和案例。

但是随之而来的是,前端项目几乎没有了往日的“简单愉快”,想用流行框架写一个项目,一般得先整一个脚手架,如果你写的程序没有“经历前端构建”,整的你都不好意思和同行打招呼。

这篇文章会以两个简单的例子来说明,即使不配置脚手架、使用一些“老家伙”一样可以开发出高性能的网站。

额外说明

本篇文章 并不完全适用 十几人乃至几十人以上团队规模的复杂、需要高密度的协作项目,仅针对中小型项目,诸如简单的后台、流程配置、甚至是 Demo。

碎碎念了这么多,让我们正式开始 回归愉快的前端开发

从一个简单的“单页”应用开始

不论是使用 ReactVue 还是使用更有年代感的 jQuery ,做一个简单的页面,不外乎分别完成 “页面结构”、“页面风格”、“页面功能” 三个部分的编写。

我们使用现在比较流行的 Vue 举个例子:



    
    
    
    简单的页面
    
    
    <!--  -->
    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}
    
    


 
    
                                                                   首页                                      团队                                                        标签                                                                                                                                                                                          
                                                                                                                                                推荐                                                           牛逼的比赛                              犀利的观点                              给力的事件                              特别的曝光                                                                                                                                                     前端                                                           最佳实践                              基础知识                              多彩样式                              有趣脚本                                                                                                                                                     后端                                                           Option 5                              Option 6                                                              Option 7                                 Option 8                                                                                                                                                                                  运维                                                           Option 9                              Option 10                              Option 11                              Option 12                                                                                                                                                     算法                                                           Option 9                              Option 10                              Option 11                              Option 12                                                                                                                                                     分类                                                           Option 9                              Option 10                              Option 11                              Option 12                                                                                                                                                     分类                                                           Option 9                              Option 10                              Option 11                              Option 12                                                                       
                                                                             
                          

凉风有幸 1

                      
                      
                          

秋月无边 2

                      
                      
                          

啦啦啦啦 3

                      
                      
                          

置顶精选 4

                      
                                         
                                                                                                                                                编辑精选                                                                                                                            最新发布                                                                                                                 编辑精选                                                                                                      
                          热门                                                      最新                       
                                           
                    
                                                 
                                                          loading more                           
                                                                                        

这是一篇博客的标题,可能很长很长很长很长

                                                                                                                                   

                                   简单的内容描述。                                                                  

                                   前端                                    工程工具                                    方法论                                 
                                                                                                                                                           
                                                                             

                                                     这里是一个公告标题                       

                       这里有一个描述性词汇描述性词汇描述性词汇描述性词汇描述性词汇                        开始浏览                                                                                        

                                                     热门标签                       

                                              前端                        工程工具                        方法论                        工程工具                        方法论                        前端                        方法论                        工程工具                        方法论                        前端                                                                                                                         
                                1                                 作者 - 简单描述                                                              
                            
                                2                                 作者 - 简单描述                                                              
                            
                                1                                 作者 - 简单描述                                                              
                            
                                4                                 作者 - 简单描述                                                              
                            
                                5                                 作者 - 简单描述                                                              
                            
                                6                                 作者 - 简单描述                                                              
                                                                                                                                
                                1                                 作者                                                              
                            
                                1                                 作者                                                              
                            
                                1                                 作者                                                              
                                                                                                                                           
                 -EOF-                                   投稿                                   关于               
                        
  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'))                 })             })         },       }, })  

将上面的三百来行代码保存为 index.html , 使用浏览器直接打开,不出意外你将看到下面的界面。

简单把玩之后,你一定会说,这个示例页面没有什么复杂交互,而且这不就是官方的 推荐用法之一 嘛。

是的,但希望你能够看到,像上面这样做一个样子还说的过去的页面, 真的不是必须把构建工具也“掺和”进来 ,即使你把组件交互的部分填充完毕。

而开发过程,就可以回归经典的“边改边刷新”,所见即所得了。

接下来,我们来聊聊如何将上面的程序拆分为模块使用,让多个页面之间可以复用模块,当然还是在不使用构建工具的前提下。

拆分功能模块

将单一职责的功能抽象模块化,可以说是工程师们的日常。这样做除了提高了可维护性、潜在的提升了页面性能、让软件构建更灵活之外、最大的收益便是增加了功能模块的可复用性。

我们日常使用 webpack 的时候,一定有看到过被分割为一堆名为 chunk 文件的脚本,或者名称可能叫做 vendorappcomponent 的文件。这些便是构建程序帮我们切割的软件模块了,甚至是上面例子中引入的 *.min.js . 也是如此。

如果我们不使用构建工具进行模块拆分,该怎么做呢?这里面常见的坑有哪些呢?

  1. 拆分为多个模块之后,会涉及到额外的网络资源获取和解析处理。
  2. 拆分为多个模块之后,可能会涉及到额外的模块依赖管理。
  3. 拆分为多个模块之后,会涉及到数据、状态同步管理。

想要解决前两个问题,可以通过使用 Require.js 之类的资源加载器,来控制拆分后多出来的资源文件的加载和对模块进行依赖管理,想了解这个老家伙的细节,可以浏览它的 官方网站

而拆分后的模块,想要保持书写上的简单明快,这里选择使用 Vue 的 Component 语法进行模块保存,所以需要额外引入一个 模块解析器 ,原理很简单,通过 XHR 方法将资源获取后,使用正则将内容分别抽取为“样式”、“脚本”、“模版”,然后在合适的时机在浏览器环境执行。为了简化操作,我在 requirejs-vue 的基础上进行了删减,有兴趣可以围观 源码

至于第三个问题,不论是使用单例共享数据源、亦或者使用发布订阅模式传递数据、或者使用观察者模式都可以,解决的手段还有很多,就不扩展了,本文暂且略过,你可以挑一个你觉得顺手的使用。

以上面的单页程序为例,我们先编写页面框架。



    
    
    
    将模版分离
    
 
    
    
    
 
    
 
    
        Vue.use(antd);
        requirejs && requirejs.config({
            baseUrl: './assets',
            paths: { 'vue': 'common/require-vue' },
            config: { 'vue': { 'css': 'inject', 'templateVar': '__template__' } }
        });
    


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

相比较上一小节三百来行混杂了细节逻辑的代码,这个长度只有不到一百行的代码是不是逻辑清晰许多呢。

刚刚提到了模块复用,其实也很简单,比如我们想实现一个“列表页面”,可以这么写:


 

    
    
    
    复用模块的例子
    
 
    
    
    
 
    
 
    
        Vue.use(antd);
        requirejs && requirejs.config({
            baseUrl: './assets',
            paths: { 'vue': 'common/require-vue' },
            config: { 'vue': { 'css': 'inject', 'templateVar': '__template__' } }
        });
    


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

聊完页面框架后,我们接着来看看拆分出的模块怎么写,以一个简单的 header 模块举例:

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

 

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

 

    
                                           首页                                     团队                                                    标签                                                                                     

可以看到,这完全就是普通的 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