大型单页面应用的进阶挑战

2015/09/30 · HTML5,
JavaScript ·
单页应用

原文出处: 林子杰(@Zack__lin)   

阅读须知:这里的大型单页面应用(SPA Web
App)是指页面和功能组件在一个某个量级以上,举个栗子,比如
30+个页面100+个组件,同时伴随着大量的数据交互操作和多个页面的数据同步操作。并且这里提到的页面,均属于
hash 页面,而多页面概念的页面,是一个独立的 html
文档。基于这个前提,我们再来讨论,否则我怕我们 Get 不到同一个 G 点上去。

前端发展与现状

大家都知道前端是由HTML、CSS、Js组成的,一开始这样写出来的页面,不能局部加载,复用性比较差,重复工作比较多。微软就推出了ifram标签,就是相当于在网页中嵌套一个网页,切换目录只是切换ifram中的网页,还是直接加载某个完整的html界面。接着ajax的出现,实现了局部刷新,优化了用户体验。后来进入了jQuery时代,jQuery封装了很多原生方法,减少了代码量。现在我们前端进入了前后端分离时代,流行
MV* 框架(MVC、MVP、MVVM),MVVM框架有Angular、Vue、React。

图片 1

MVVM框架

现在我们后台管理系统是基于Vue开发的单页面应用(SPA)。

本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址
www.cnblogs.com/tdws

挑战一:前端组件化

基于我们所说的前提,第一个面对的挑战是组件化。这里还是要强调的是组件化根本目的不是为了复用,很多人根本没想明白这点,总是觉得造的轮子别的业务可以用,说不定以后也可以用。

其实前端发展迭代这么快,交互变化也快,各种适配更新层出不穷。今天造的轮子,过阵子别人造了个高级轮子,大家都会选更高档的轮子,所以现在前端界有一个现象就是为了让别人用自己的轮子,自己使劲不停地造。

在前端工业化生产趋势下,如果要提高生产效率,就必须让组件规范化标准化,达到怎样的程度呢?一辆车除了底盘和车身框架需要自己设计打造之外,其他标准化零件都可以采购组装(专业学得差,有啥谬误请指正)。也就是说,除了
UI
和前端架构需要自己解决之外,其他的组件都是可以奉行拿来主义的,如果打算让车子跑得更稳更安全,可以对组件进行打磨优化完善。

说了这么说,倒不如看看徐飞的文章《2015前端组件化框架之路》 里面写的内容都是经过一定实践得出的想法,所以大部分内容我是赞成而且深有体会的。

Vue简单介绍

1、Vue.js是一个构建数据驱动的web框架
2、Vue.js实现了数据的双向绑定和组件化
3、Vue.js只需要关系数据的变化,无需繁琐的获取和操作dom
比如给一个元素绑定事件并赋值,jQuery的做法是:

<input class="ipt" type="text">
<button class="btn"></button>
<script type="text/javascript">
$.ready(function () {
        $('.ipt').value();
        $('.btn').click(function() {    
        })
 })
</script>

vue的写法是:

<input class="ipt" v-model="value" type="text">
<button class="btn" @click="click"></button>

.vue文件的写法

<template>
    这里写HTML
</template>
<script type="text/ecmascript-6">
    这里写数据和方法
</script>
<style lang="stylus" rel="stylesheet/stylus">
    这里写css
</style>

一.写在前面

项目上线有一段时间了,一个基于webpack+vue+ES6的手机端多页面应用。其实说是多页面应用,实际上在webpack中属于四个app,
 如果真是做纯单页面,那应该有二三十个页面吧。所以我这里的多页面应用,是分为四个SPA。比如微信最下面,有四个导航,微信,通讯录,发现,我。
那么这四个导航,就是我的四个SPA,配置多个入口即可。

在这里就不说太多代码了,项目结构将会放到github上,地址在后面给出,以供参考,上传的大概只是一个目录加上配置情况,其实关键点也就在webpack.config.js了,这里主要配置了entry,loader,plugins,output目录啥的。

在这里先附上package.json和webpack.config.js吧:

 

图片 2图片 3

 1 {
 2   "name": "my-web",
 3   "version": "1.0.0",
 4   "description": "desc",
 5   "main": "index.js",
 6   "scripts": {
 7     "test": "echo "Error: no test specified" && exit 1",
 8     "start": "webpack-dev-server --inline --hot",
 9     "dev1": "webpack-dev-server --open",
10     "dev": "webpack-dev-server --inline --hot",
11     "build": "set NODE_ENV=production&&webpack"
12   },
13   "author": "ws",
14   "license": "ISC",
15   "devDependencies": {
16     "babel-core": "^6.24.1",
17     "babel-loader": "^7.0.0",
18     "babel-plugin-transform-runtime": "^6.23.0",
19     "babel-preset-es2015": "^6.24.1",
20     "babel-runtime": "^6.23.0",
21     "css-loader": "^0.28.4",
22     "extract-text-webpack-plugin": "^2.1.0",
23     "glob": "^7.1.2",
24     "html-webpack-plugin": "^2.28.0",
25     "jquery": "^3.2.1",
26     "node-sass": "^4.5.3",
27     "sass-loader": "^6.0.5",
28     "slideout": "^1.0.1",
29     "style-loader": "^0.18.2",
30     "url-loader": "^0.5.8",
31     "vue": "^2.3.3",
32     "vue-croppa": "^0.1.0",
33     "vue-hot-reload-api": "^2.1.0",
34     "vue-html-loader": "^1.2.4",
35     "vue-ios-alertview": "^1.1.1",
36     "vue-loader": "^12.2.1",
37     "vue-resource": "^1.3.3",
38     "vue-router": "^2.7.0",
39     "vue-style-loader": "^3.0.1",
40     "vue-template-compiler": "^2.3.3",
41     "vue-touch": "^2.0.0-beta.4",
42     "webpack": "^2.6.1",
43     "webpack-dev-server": "^2.4.5"
44   }
45 }

View Code

 

图片 4图片 5

  1 var path = require('path');
  2 var webpack = require('webpack');
  3 // 将样式提取到单独的 css 文件中,而不是打包到 js 文件或使用 style 标签插入在 head 标签中
  4 var ExtractTextPlugin = require('extract-text-webpack-plugin');
  5 // 生成自动引用 js 文件的HTML
  6 var HtmlWebpackPlugin = require('html-webpack-plugin');
  7 var glob = require('glob');
  8 
  9 var entries = getEntry('./source/**/*.js'); // 获得入口 js 文件
 10 var chunks = Object.keys(entries);
 11 console.log('输出chunks', chunks);
 12 module.exports = {
 13     entry: entries,
 14     output: {
 15         path: path.resolve(__dirname, 'public'), // html, css, js 图片等资源文件的输出路径,将所有资源文件放在 Public 目录
 16         publicPath: '/public/',                  // html, css, js 图片等资源文件的 server 上的路径
 17         filename: 'js/[name].js',         // 每个入口 js 文件的生成配置
 18         chunkFilename: 'js/[id].[hash].js'
 19     },
 20     externals: {
 21         jquery: "$",
 22         EXIF: "EXIF",
 23         wx: "wx"
 24     },
 25     resolve: {
 26         extensions: ['.js', '.vue'],
 27         alias: {
 28             'vue': __dirname + '/lib/vue/vue.js',
 29             //'vue-alert': __dirname + '/lib/vue-alert/vue-alert.js'
 30         },
 31     },
 32 
 33     module: {
 34         loaders: [
 35             {
 36                 test: /.css$/,
 37                 // 使用提取 css 文件的插件,能帮我们提取 webpack 中引用的和 vue 组件中使用的样式
 38                 //loader: "style-loader!css-loader",
 39                 loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' })
 40             },
 41             {
 42                 // vue-loader,加载 vue 组件
 43                 test: /.vue$/,
 44                 loader: 'vue-loader',
 45                 options: {
 46                     //解析.vue文件中样式表
 47                     loaders: {
 48                         // Since sass-loader (weirdly) has SCSS as its default parse mode, we map
 49                         // the "scss" and "sass" values for the lang attribute to the right configs here.
 50                         // other preprocessors should work out of the box, no loader config like this necessary.
 51                         //'scss': 'vue-style-loader!css-loader!sass-loader',
 52                         //'css': 'vue-style-loader!css-loader!sass-loader',
 53                         //'js': 'babel-loader',
 54                         //'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
 55                         css: ExtractTextPlugin.extract({ fallback: 'vue-style-loader', use: 'css-loader' }),
 56                         //exclude: [
 57                         //    path.resolve(__dirname, ""),
 58                         //    //path.resolve(__dirname, "app/test")
 59                         //]
 60                         //exclude:'/source/course/course-detail/course-detail.css'
 61                     }
 62                     // other vue-loader options go here
 63                 }
 64             },
 65             {
 66                 test: /.js$/,
 67                 // 使用 es6 开发,这个加载器帮我们处理
 68                 loader: 'babel-loader',
 69                 exclude: /node_modules/
 70             },
 71             {
 72                 test: /.(png|jpg|gif|svg)$/,
 73                 // 图片加载器,较小的图片转成 base64
 74                 loader: 'url-loader',
 75                 query: {
 76                     limit: 10000,
 77                     name: './imgs/[name].[ext]?[hash:7]'
 78                 }
 79             }
 80         ]
 81     },
 82     plugins: [
 83         // 提取公共模块
 84         new webpack.optimize.CommonsChunkPlugin({
 85             name: 'vendors', // 公共模块的名称
 86             chunks: chunks,  // chunks 是需要提取的模块
 87             minChunks: chunks.length
 88         }),
 89         // 配置提取出的样式文件
 90         new ExtractTextPlugin('css/[name].css')
 91     ]
 92 };
 93 
 94 var prod = process.env.NODE_ENV === 'production';
 95 module.exports.plugins = (module.exports.plugins || []);
 96 if (prod) {
 97     module.exports.devtool = 'source-map';
 98     module.exports.plugins = module.exports.plugins.concat([
 99         // 借鉴 vue 官方的生成环境配置
100         new webpack.DefinePlugin({
101             'process.env': {
102                 NODE_ENV: '"production"'
103             }
104         }),
105          new webpack.optimize.UglifyJsPlugin({
106              compress: {
107                  warnings: false
108              }
109          })
110     ]);
111 } else {
112     module.exports.devtool = 'eval-source-map';
113     module.exports.output.publicPath = '/view/';
114 }
115 
116 var pages = getEntry('./source/**/*.html');
117 for (var pathname in pages) {
118     // 配置生成的 html 文件,定义路径等
119     var conf = {
120         filename: prod ? '../views/' + pathname + '.html' : pathname + '.html', // html 文件输出路径
121         template: pages[pathname], // 模板路径
122         inject: true,              // js 插入位置
123         minify: {
124             removeComments: true,
125             collapseWhitespace: false
126         },
127         hash:true
128     };
129     if (pathname in module.exports.entry) {
130         conf.chunks = ['vendors', pathname];
131         //conf.hash = false;
132     }
133     // 需要生成几个 html 文件,就配置几个 HtmlWebpackPlugin 对象
134     module.exports.plugins.push(new HtmlWebpackPlugin(conf));
135 }
136 
137 // 根据项目具体需求,输出正确的 js 和 html 路径
138 function getEntry(globPath) {
139     var entries = {},
140         basename, tmp, pathname;
141 
142     glob.sync(globPath).forEach(function (entry) {
143         basename = path.basename(entry, path.extname(entry));
144         tmp = entry.split('/').splice(-3);
145         pathname = tmp.splice(0, 1) + '/' + basename; // 正确输出 js 和 html 的路径
146         entries[pathname] = entry;
147     });
148     console.log(entries);
149     return entries;
150 }

View Code

 

开发工具使用的VS2017,本来使用WS,但是用习惯VS的我还是受不了,毕竟17还是太强大了嘛。既然是vue项目,那数据请求肯定就是vue-res,
路由就是vue-loader,编译es6大家都是babel。 下面是项目结构预览:

图片 6图片 7图片 8他们分别是图片资源,引用库资源,发布打包后的js和css,src源码和打包后的html

挑战二:路由去中心化

基于我们所说的前提,中心化的路由维护起来很坑爹(如果做一两个页面 DEMO
的就没必要出来现眼了)。MV*
架构就是存在这么个坑爹的问题,需要声明中心化 route(angular 和 react
等都需要先声明页面路由结构),针对不同的路由加载哪些组件模块。一旦页面多起来,甚至假如有人偷懒直接在某个路由写了一些业务耦合的逻辑,这个
route 的可维护性就变得有些糟糕了。而且用户访问的第一个页面,都需要加载
route,即使其他路由的代码跟当前页面无关。

我们再回过头来思考静态页面简单的加载方式。我们只要把 nginx 搭起来,把
html 页面放在对应的静态资源目录下,启动 nginx 服务后,在浏览器地址栏输入
127.0.0.1:8888/index.html
就可以访问到这个页面。再复杂一点,我们把目录整成下面的形式:

/post/201509151800.html /post/201509151905.html /post/201509152001.html
/category/js_base_knowledge.html /category/css_junior_use.html
/category/life_is_beautiful.html

1
2
3
4
5
6
/post/201509151800.html
/post/201509151905.html
/post/201509152001.html
/category/js_base_knowledge.html
/category/css_junior_use.html
/category/life_is_beautiful.html

这种目录结构很熟吧,对 SEO
很友好吧,当然这是后话了,跟我们今天说的不是一回事。这种目录结果,不用我们去给
Web Server 定义一堆路由规则,页面存在即返回,否则返回
404,完全不需要多余的声明逻辑。

基于这种目录结构,我们可以抽象成这样子:

/{page_type}/{page_name}.html

1
/{page_type}/{page_name}.html

其实还可以更简单:

/p/{name}.html

1
/p/{name}.html

从组件化的角度出发,还可以这样子:

/p/{name}/name.js /p/{name}/name.tpl /p/{name}/name.css

1
2
3
/p/{name}/name.js
/p/{name}/name.tpl
/p/{name}/name.css

所以,按照我们简化后的逻辑,我们只需要一个 page.js
这样一个路由加载器,按照我们约定的资源目录结构去加载相应的页面,我们就不需要去干声明路由并且中心化路由这种蠢事了。具体来看代码。咱也懒得去解析了,里面有注释。

项目结构

├── build // 构建相关
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── comm// 打包后生成的目录
│ ├── favicon.ico
│ ├── index.html
│ └── static
├── config // 配置相关
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── package.json //开发生产依赖
├── server //服务及mock数据
│ ├── controller
│ └── mock
├── src //源代码
│ ├── App.vue // 入口页面
│ ├── api // 所有请求
│ ├── assets // 字体等静态资源
│ ├── common // 全局公用方法
│ ├── components // 全局公用组件
│ ├── favicon.ico
│ ├── index.html // html模板
│ ├── main.js // 入口js 加载组件 初始化等
│ ├── pages // 所有页面
│ ├── plugins // 全局工具
│ ├── router // 路由
│ └── store // 全局store管理
└── static // 第三方不打包资源
├── async.js
├── css
├── img
├── jquery-1.8.3.min.js
├── jquery.ztree.js
├── md5.js
├── paopao.js
├── spark-md5.js
├── tinymce
├── upload.js
├── upyun-mu.js
└── vue-style-loader

二.几点重要的收获

1.组件化开发爽啊,
调用者只需要关注输入和输出,代码明朗,容易维护

2.vue-res promise异步风格太优美,太喜欢了。但是有坑,ios8.x,使用内核浏览器运行js,
不支持promise语法,所以需要在入口中,import几个npm下载的node
module:

 npm i whatwg-fetch core-js es6-promise –save-dev

 图片 9

3.记得以前做一个手机端项目,完全没有自动化,各个页面间跳转慢的一比,一点也不流畅,项目结构不容易管理,重复代码特别多。

 近百个页面js版本得不到控制,管理js,css引用困难。微信静态资源缓存如此严重,没有版本控制,每个页面js版本的修改要人命。

4.解决缓存问题,应禁止html缓存,由于使用extract-text-webpack-plugin,可以保证你html入口中只有简单的几行代码,等着自动化帮你引入所需js,所以即使禁止html缓存,也不会影响响应速度,毕竟我们的html文件
     也就1-2k左右.html禁止缓存的原因是防止,js更新后,js
hash版本已改变,但浏览器缓存的html中,依然是请求旧版本js文件,这样一来js版本控制变得没有意义。

  1. js调用手机拍照,
    调用安卓手机拍照不容易呀,所以如果只想在微信上使用网页的话,建议使用
    微信js SDK。

6.
苹果手机和个别安卓手机,使用原生input调用拍照后,图片到页面中会出现旋转问题,所以在微信上
使用js sdk, 在其他浏览器上,就用EXIF.js  手动将其旋转90度
或者180度进行矫正。

7.推荐一款mobile用的不错的弹窗组件
vue-ios-alert.  ios风格的弹窗。地址以及github:  
 http://isay.me/vue-ios-alertview/example/![](https://images2017.cnblogs.com/blog/686162/201709/686162-20170904231146210-1849602734.png)

 

8.手机上的 日期
时分秒选择器,推荐一个有坑的货
 https://github.com/k186 
有坑哦,使用的话,请看closed的第一个issue。另外日期选择还是比较推荐原生。加上时分秒的话原生的可能就不好用。图片 10

9.页面touch切换tag 使用的一个vue-tab
 github找一找,ios8不支持
flex-shrink,要使用-webkit-flex-shrink。

  1. 上拉加载更多
    用的vue-loadmore,侧方滑动菜单 使用了jquery的slideout

11.
如果路由比较多的话,建议路由单独分一个js配置,并且一定要按需加载,否则打包文件太大。如果是用户点击率极高的路由,可以直接require进去。

12.一些js库,就不要通过require了,直接在html引入进去算了,毕竟这些库基本不会更改,也没必要控制版本

13.前端AOP,
 vue-res的拦截器点个赞,我可以在拦截器中,为我每一个请求
都加上authentication
header等信息,像用jq的时候,我不得不手动把ajax包装一层

14.像有些数据的加载,文字方面,最好预先给出加载中这种提示,不能给空白。列表的加载
要多考虑加载中,加载完成和暂无数据的处理。见过很多app和网页都是进入到列表页,首先一个暂无数据的背景图给出来 
       了,结果稍等一下,数据又加载出来了….

15.虽然已经组件化了,但我还建议有一个每个页面公用需要require的js,我一般都叫application.js
 在这里 可以放一些你的常量,枚举,公共方法,helpers,utils,ajax
等配置,并且在这里可以import footer header vue-res vue-alert
等一些每个组件或者页面都需要以来的组件

16.热替换是必须的,比以前用gulp
livereload方便

17.手机端页面调试,推荐vConsole(去github找)。
示例:图片 11图片 12

18.经过babel编译es5的都没问题.。
 我有个单独的小功能,没用es6,直接谷歌调试开发,结果到了ios9.x上
 不支持也不报错,以后避免踩进去吧。下面是代码:

图片 13

19. IOS上计算时间 需要new Date(‘2017/09/09’)的格式,
 而不能使用横杠的格式

挑战三:领域数据中心化

对于单向数据流循环和数据双向绑定谁优谁劣是永远也讨论没结果的问题,要看是什么业务场景什么业务逻辑,如果这个前提没统一好说啥都是白搭。当然,这个挑战的前提是非后台的单页面应用,后台的前端根本就不需要考虑前端内存缓存数据的处理,直接跟接口数据库交互就行了。明确了这个前提,我们接着讨论什么叫领域数据中心化。

前面讨论到两种数据绑定的方式,但是如果频繁跟接口交互:

  • 内存数据销毁了,重新请求数据耗时浪费流量
  • 如果两个接口字段部分不一样但是使用场景一样
  • 多个页面直接有部分的数据相同,但是先来后到导致某些计数字段不一致
  • 多个页面的数据相同,其中某些数据发生用户操作行为导致数据发生变动

因此,我们需要在业务视图逻辑层和数据接口层中间增加一个
store(领域模型),而这个 store 需要有一个统一的 内存缓存 cache,这个
cache 就是中心化的数据缓存。那这个 store 究竟是用来弄啥勒?

图片 14

Store 具有多形态,每个 store
好比某一类物品的仓储(领域,换个词容易理解),如蔬果店 fruit-store,
服装店
clothes-store,蔬果店可以放苹果香蕉黑木耳,服装店可以放背心底裤人字拖。如果品种过于繁多,我们可以把蔬果店精细化运营变成香蕉专卖店,苹果专卖店(!==
appstore),甚至是黑木耳专卖店…( _
_)ノ|,蔬果种类不一样,但是也都是称重按斤卖嘛。

var bannerStore = new fruitStore();

var appleStore = new fruitStore();

有了这些仓储之后,我们可以放心的把数据丢给视图逻辑层大胆去用。想修改数据?直接让
store 去改就行了,其他页面的 DOM
文本内容也得修改吧?那是其他页面的业务逻辑做的事,我们把事件抛出去就好了,他们处不处理那是他们的事,咱别瞎操心(业务隔离)。

那么 store 具体弄啥勒?

图片 15

  • 32
    个赞位置可点赞或者取消,三个页面的赞数需要同步,按钮点赞与取消的状态也要同步。
  • 条目是否已收藏,取消收藏后 Page B 需要删除数据,Page A+C
    需要同步状态,如果在 Page C 又有收藏操作,Page B
    需要相应增减数据,Page A 状态需要同步。
  • 发评论,Page C 需要更新评论列表和评论数,Page A+B
    需要更新评论数。如果 Page B 没有被加载过,这时候 Page B
    拿到的数据应该是最新的,需要同步给 A+C 页面对应的数据进行更新。

所以,store
干的活就是数据状态读写和同步,如果把数据状态的操作放到各个页面自己去处理,页面一旦多了或者复杂起来,就会产生各个页面数据和状态可能不一致,页面之前双向引用(业务耦合严重)。store
还有另一个作用就是数据的输入输出格式化,简单举个栗子:图片 16

  • 任何接口 API 返回的数据,都需要经过 input format
    进行统一格式化,然后再写入
    cache,因为读取的数据已按照我们约定的规范进行的处理,所以我们使用的时候也不需要理会接口是返回怎样的数据类型。
  • 某些组件需要的数据字段格式可能不同,如果把数据处理放在模板进行处理,会导致模板无法更加简洁通用(业务耦合),所以需要
    output format 进行处理。

所以,store
就是扮演着这样的角色——是数据状态读写和同步,以及数据输入输出的格式化处理。

这里来简单讲一下src文件

三.一些缺点

 1.脑子抽风啊,分为四个SPA,
整套项目下来,感觉还是应该做一个SPA。毕竟SPA之间切换,一个SPA切换到另一个SPA
还是加载东西太多,不够流畅。虽然微信浏览器缓存“严重”

2.项目结构划分还是不够合理,但感觉也还能对付用。

3.组件化没有发挥到极致,自己vue组件间通信没搞好,md找子组件,我竟然还有通过遍历的方式。

4.有些组件用的jquery的,搭配的不是很流畅,导致个别操作强行使用dom操作。

5.我有四个环境,开发,测试,demo, 线上。
每次发布到一个环境
 都需要改了配置后,重新打包,很痛苦啊,关于这一点有什么好的办法吗? 

发表评论

电子邮件地址不会被公开。 必填项已用*标注