目前团队中新的 Web 项目基本都采用了 Vue 或 React ,加上 RN,这些都属于比较重量级的框架,然而对于小型 Web 页面,又显得过大。早期的一些项目则使用了较原始的 HTML 页面构建技术,但业务逻辑基本没法复用。
近半年作过几个小型 Web 页面,在不断学习前端知识的同时,也在重构并摸索小型 Web 项目可能的更好解决方案。本文则对以前的工做进行一次总体描述。css
目标和定位html
单论小型 Web 页面,其相对于 Vue/React 等项目最大不一样是不须要支持 SPA 这种比较重的形式,以 MVP(Minimum Viable Product) 为原则,小页面只要知足需求,作到够用便可。因此在对现有原始 Web 页面进行重构时,会将如下两个方面做为最高优先级:前端
不断提升项目的重用性、可维护性;vue
不断提升前端性能,这里主要是加载性能;ios
对于第一点,组件化代码结构是当前最可行的思路;对于第二点,在作到第一点的前提下,极少的第三方依赖,良好的打包方法,是必需要作到的。nginx
项目结构演化历程es6
本文所描述的小型 Web 页项目结构和打包方法是通过若干次项目重构才获得的。ajax
初版 vuex
初版项目基本上以最原始的 HTML+JS+CSS 为基准。为了让项目代码更好维护,首先考虑到的是有两个点须要作:json
使页面内容具备维护性,须要采用 JS 模板;
因为业务复杂,分工较细,接口繁多,须要将数据接入层 DAL
(Data Access Layer) 单独分离出来;
对于第一个问题,最后选择 Mustache
库。缘由是它语法极简,容易学习,同时该类型语法有广大用户群体,固然一样流行的还有 underscore
/ejs
类型的模板语法。为了保证内容页面的无逻辑性和简单,故 Mustache
的高级版 Handlebars
未被使用。
第二个问题是对公司业务和项目代码有所了解后所下的结论。至关因而对现有代码的重构,主要目的是进行职责分离,将复杂多变的接口隔离出来,让剩下的代码专心解决业务问题。
开始敲代码后,才发现另外一个比较严重的问题:我须要把管理内容模板的代码单独分离出来,使其不会影响主要的业务逻辑,因而想到了 MVP(Model View Presenter) 模式。简单讲,这个就是 MVVM 模式去掉 View-ViewModel 双向数据绑定后的一个弱化版。以下图所示:
在小型 Web 页面中,通常是没有 Model 层的。页面中的 Presenter 部分只负责经过参数控制界面的渲染,并以组件的方式对外公开 View 层事件。按照这个思路,初版项目结构就基本出来了,见下图:
初版项目结构已经足以应付小型 Web 页面的需求了,同时也不会带来较多的复杂性。可是原始 Web 页面天生就不利于模块化开发,同时存在一个根本性问题:
代码解耦会使项目文件结构清晰,职责分离,有利于维护;
打包结果须要将相关代码压缩到单个文件中,便于提升加载性能;
在 Web 页面开发中,这二者造成一个悖论。因此须要引入一个打包机制,将项目代码和打包文件进行解耦。年老的 Gulp 和 Grunt 就不看了,现存项目中用的较多的是 FIS3 和 Webpack 1.x。前者国产,使用起来也很是方便,后者难度高一些,可是跟其余国外开源项目同样,他们总能把一个软件的 50% —— 文档作的很好。(其实还有不少能够吐槽的地方,但看了以后感受相对更踏实)
在看了 Webpack 2.x 的文档以后,基本就肯定使用该打包机制了,它有以下优势让人欲罢不能:
原生支持 import
语法。这样就完全摆脱文件结构很差管理的问题了,面向对象、模块化什么的通通均可以引进来,终于能够舒舒服服写代码了;
支持 Tree Shaking。原本这是 rollup 打包机制独有的特色,如今 Webpack 也有了;
Webpack 的配置文件虽然复杂,但了解以后再配合插件机制,会发现它潜力很大,使用也至关灵活;
在引入 Webpack 2.x 后,不一样功能均以单个文件的形式进行分开,各模块之间接口也变得很是明确,但还有待改进。
加入打包机制后,JavaScript 文件已经解耦的不错了,可是模板还都放在了首页中,样式也都放在了一个文件中,依赖关系混乱,不方便管理。改良它的一个好方法是参考其余优秀项目,好比 Vue 就有一套很好的项目组织结构,直接借鉴就好了。新的项目主要变化以下:
真正将组件分离出来。组件内容采用 Mustache
模板,样式采用 Less
语法,JS 部分则控制组件的渲染逻辑,尽可能不关联业务逻辑,三者合一就至关于一个 .vue 文件了。经过修改 Webpack 配置或使用合适的插件,该方式能够一样支持其余模板和 CSS 语法,好比 ejs
或者 SCSS
;
选择支持多页面入口,而没有采用路由功能。这样能够简化 SPA 中复杂的 URL 结构,同时打包结果也不用附带路由逻辑。这样还有一个好处是后期引入简单版的 SSR 也会很方便,路由就是 nginx 的事儿了;
对于该部分项目结构的详细描述直接看下文。结构图以下:
须要说明的是,图中的 State Store
其实目前是没有的,放在这里主要是为了好看 :)。后期若是把 vuex/MobX/Redux 之类的加进去了,那就完整了,目前由于业务逻辑很简单,状态什么的暴力解决就好了。而 app.js 则处理项目中公共的业务逻辑,让页面入口解脱出来专心处理内容。
项目目录结构以下:
----build # Webpack 配置文件 ... ----src --------assets # 资源文件
--------components ------------GoodsInfo # 商品信息组件 GoodsInfo.mst # 组件模板,采用 Mustache 语法 GoodsInfo.js # 组件渲染和操做逻辑。通常业务无关 GoodsInfo.less # 组件样式
------------RiskPromt ... ------------ShareHeader ... ------------SharePanel ... utils.js # 业务无关,视图层相关的辅助方法集合
--------dal # 数据接入层 index.js # 入口文件。集中管理请求接口和伪数据 getInfoById.js # 接口请求实现 getInfoById.json # 接口返回伪数据,在 index.js 中可生成 mock 方法
--------Main # 默认页面入口 Main.html # 页面模板 Main.js # 页面业务逻辑 Main.less # 页面样式
--------MainBanner # 带有底部 Banner 的页面入口 ... app.js # 抽取多页面共有的业务逻辑,好比分享功能的具体实现 common.js # 应用级的辅助方法集合 common.less package.json README.md
第三方依赖
在 Webpack 2+ 的帮助下,项目选用了以下开源第三方库做为基础依赖:
es6-promise
:采用 Promise 的方式可使代码更清晰更好维护;
axios
:Vue 官方推荐的 vue-resource 替代品;
mustache
:项目所用的模板库
另外还使用了团队维护的 SDK:
@zz/zz-jssdk
:提供 Web 页面和转转 App 客户端的交互接口
@zz/perf
:性能统计工具
因为 axios
官方坚持不集成非标准的 jsonp
请求,对于现存部分只支持 jsonp
请求的接口,还须要引入 jsonp
第三方开源库。
以上是项目文件依赖。开发依赖中,所用的第三方库基本都是 Webpack 相关,包括 Less
文件的解析模块。项目没有引入 babel-polyfill
进行 ES6 语法的开发,由于容易产生没必要要的额外打包代码。
在 Webpack 的语义下,全部的项目文件都是一种资源,供 JavaScript 使用,因此处理任何资源时,只要配置好合适的 loader 便可。该部分则对项目中不一样类型文件的加载和解析规则配置进行了简要描述。这里不会讲解 Webpack 配置细节,相关内容请查看官方文档。
对于通常资源文件的加载,采用 file-loader
便可。对于图片文件,采用推荐的 url-loader
。该加载器有一个选项是,若是图片小于指定值,会将其转化为 DataUri
嵌入到打包文件中,以减小额外 HTTP 请求,项目设置指定值为经常使用的 10K。规则以下:
样式文件
项目中样式文件默认采用 Less
,主要用到该库的两个特性:
能够方便的使用 CSS 变量,典型的好比定义通用像素大小;
层次化的样式描述方式;
Webpack 配置同时保留了 css 文件的加载能力,后期还能够加入对 SCSS
文件支持。规则以下:
同一个项目中,因为 CSS
/LESS
/SCSS
文件之间具备依赖关系,因此强烈推荐采用同一种技术实现。对于单个组件,不大可能像 Vue 同样写个 Webpack Loader 支持 .vue 类型的组件格式。样式文件的加载须要在对应的 .js 文件中显式引入 .less 文件,好比:
项目模板默认采用 Mustache
,在 Webpack 的支持下,模板内容被单独放在一个文件中,并以 .mst
做为自定义后缀,文件内容依然是 HTML 格式,只是根标签为 <template>
。Webpack 中选用 html-loader
对其进行解析,规则以下:
对于 Mustache
模板的自动解析和加载,网上有开源的 mustache-loader
实现,但其关注度实在过低,而 html-loader
足以达到所需功能:
加载 .mst 文件,并压缩内容;
将文件中 img:src
等相对路径属性自动替换为绝对目标地址;
对于其余模板语言一样可使用这种方法,就能够在项目中灵活的使用不一样的模板库了。不过须要注意的是,同一个项目中最好只使用一种模板语言,方便管理,同时不会增长打包文件大小。
将 .mst 模板加载到页面中和 .less 方法差很少。在对应的 .js 文件中显式引入,而后用 extractTemplate
方法提取出模板内容便可:
这种显式引入的方式有一个好处是,能够手动控制不一样的模板和样式。在实际产品需求中,内容和样式改变是很频繁的,而功能逻辑的变化相对要慢一些,这样经过 js 引用不一样版本的模板和样式就会比较灵活。若是能把这一套管理机制抽象出来单独进行配置也是很不错的。
页面文件在 Webpack 中也是以模板的角色存在的,解析方式和模板同样,规则见上文。因为是页面入口文件,在 Webpack 中还须要使用 HtmlWebpackPlugin
插件进行配置。以下配置中,项目存在两个不一样的页面入口,因此须要两个 HtmlWebpackPlugin
实例:
因为用户每次进入 Web 页面都会加载首页,因此首页越小越节省流量。参考 Vue 项目的 index.html
就会发现里面基本只有一个骨架,具体内容都在组件中。但项目配置自己不会对这点进行假设,因此即便在首页中写入全部内容也是可行的。
项目的主要打包配置前文已经介绍差很少了,其余具体配置参看官方文档便可。采用该项目结构的最后打包结果,全部部署文件包括图片加起来没有超过 130K。在浏览器中,由于 gzip 的缘由,全页面加载网络流量不到 70K。
前文已经提到过,把数据请求单独做为一个层主要是为了分离出复杂多变的数据请求接口,还有一个好处是接口 mock 数据也能够在这里统一处理。
一个项目中可能在不少地方都会请求同一个接口。对于单个接口请求,可能有不一样方法,好比用 ajax、fetch、jsonp、axios 甚至 jQuery 库;有的是 GET,有的是 POST;有的还须要带 cookie,其余却不须要;返回数据的格式也许还不是统一的。而 JavaScript 逻辑只关心输入和输出,把这些请求细节都放在另一个地方单独维护,会使主要业务逻辑更加简洁。在项目中使用时,只须要以 Promise
的形式调用方法便可。接口封装的示例代码以下:
先后端协同开发时,须要首先定好接口,给出 mock 数据示例。因此在 DAL
层把 mock 数据封装好,会节省不少工做。在项目中会将 mock 数据直接保存成 .json 格式的文件,而后在 DAL 入口文件中经过 import
导入,再使用一个工厂方法来对外提供 mock
方法,便可使用 mock 数据了。下面是入口文件中相关代码:
有了 DAL
层对各请求接口的聚合,在其余地方使用就比较简单了,直接上代码:
小型 Web 页面的组件和 .vue
文件结构相似,只是分红了三个文件:
样式。内容和使用方式是基本同样的;
模板。后者 Vue
有本身的模板语法,前者则用的 Mustache
,也可支持其余模板。若是 Vue
的模板加载器单独分离出来,那理论上也是能够拿过来使用的;
控制逻辑。JS 逻辑部分则有些不同,Vue
框架有着本身独特完美的双向绑定机制,其接口和生命周期也是围绕它来设计的(这里只针对 .vue
文件进行讨论,类 React
使用方式很大程度是为了方便拉取用户而设定)。小型 Web 页面由于简单,因此重心都放在了组件初始化和渲染上;
组件在小型 Web 页面中定位是很明确的,即只针对页面呈现和交互,因此对外接口的设计也不复杂。若是组件采用的是 MVC 模式,那就很难讨论,由于 Controller 自己就是“老大”,可能有不少行为。Presenter 和 ViewModel 则相对简单,它们的区别只是内在机制不一样,对外是行为是差很少的。这里不考虑大型 Web 页面,小型 Web 页中组件的接口默认就两种:接受纯数据参数(props
);对外公布事件接口。相比于更高级的 Vue,少了一个 Slot
插槽功能。
使用组件的方式很直接,看代码:
组件中一个 init
方法并不能搞定所有需求,由于项目中 init
方法不只包含了组件渲染逻辑,还有事件绑定逻辑。当组件数据内容更新时,还须要抽取出一个 render
或 update
方法单独进行调用来更新界面。这不像 Vue 自带双向数据绑定神器,因此要麻烦点。
使用组件提供的事件也很简单,代码以下:
这里事件句柄的参数采用了 (Object data, Event e)
的形式。其中 data
表示事件来源,它能够是被点击对象的 ViewModel
,或者简单点,直接是被点击对象所表明的的原始数据;e
则是 HTML 的事件参数。
组件内部绑定到具体的模板前文已经示例说明过了。在渲染组件内容时,还须要处理参数内容,并将其渲染到页面指定地方。这里直接上代码:
在构造器中,首先定义 props
参数的格式,并给上默认值。在 init
方法中,则将 data 中的参数赋值给 props
,这里通常是会有数据转化处理逻辑。
最后直接进行组件渲染。能够发现,若是想要使用其余模板引擎,是很容易替换的。若是采用 SSR 服务端渲染组件,那能够各类模板库全放进来,一个工厂方法就能够进行自动化处理。
组件的参数被取名为 props
,彻底是仿造 Vue
/React
。由于它们的功能和定位基本是同样的,并且官方推荐的最佳实践这里也基本都推荐。具体这样作的几点思路以下:
小项目作不到 Vue
/React
的参数验证功能,但显式表示 props
参数有自描述文档的做用,须要哪些参数及其类型一目了然;
构造器中同时给出了 props
默认值,无参数时组件有默认展现形式;
参数只有一个 data
对象。Vue
推荐参数都用基本类型,但内容庞大时,属性繁多,分割成更小组件也不会减小多少使用的复杂性;
props
中的每个属性不能是对象,只能是 Integer
、String
、Boolean
、Array
等基本类型;
将事件的触发封装到组件中也是为了减小业务的复杂性。不少 Web 项目中都是直接操做页面内容,用户交互、内容处理、业务逻辑都耦合在了一块儿,这里组件将用户交互封装起来,同时对外提供事件接口。代码以下:
组件内部保存一个事件回调句柄 clickCallback
,组件初始化时对用户点击事件进行数据绑定,并触发这个回调。
本文简单描述了小型 Web 页面的定位,经过对小型 Web 页面的摸索和演化解释了当前项目结构的设计思路,并对其细节进行了详细描述,重点介绍了数据接入层和组件化开发。
当前的项目并非最终形态,而只是一个 α 版本的雏形,还有不少地方值得改善:
针对首屏时间进行优化,好比支持 SSR;
继续改善打包部署方案,灵活支持多页面部署,达到或接近离线应用的效果;
一些好的 ES6 语法很值得支持,须要找到一个方法在打包层面上渐进式的引入特定语法;
基于 Promise 的语法值得大面积采用,这是代码层面须要考虑的;
Webpack 挺好,但还不够好,但愿插件能更成熟更丰富;
可能还有不少点没考虑到,不过实际需求永远是最高优先级。只要不断的重构和改善,软件就会一直有生命力~