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。
MVVM框架
现在我们后台管理系统是基于Vue开发的单页面应用(SPA)。
本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws
本文首发于TalkingCoder,一个有逼格的程序员社区。转载请注明出处和作者。
基于我们所说的前提,第一个面对的挑战是组件化。这里还是要强调的是组件化根本目的不是为了复用,很多人根本没想明白这点,总是觉得造的轮子别的业务可以用,说不定以后也可以用。
其实前端发展迭代这么快,交互变化也快,各种适配更新层出不穷。今天造的轮子,过阵子别人造了个高级轮子,大家都会选更高档的轮子,所以现在前端界有一个现象就是为了让别人用自己的轮子,自己使劲不停地造。
在前端工业化生产趋势下,如果要提高生产效率,就必须让组件规范化标准化,达到怎样的程度呢?一辆车除了底盘和车身框架需要自己设计打造之外,其他标准化零件都可以采购组装(专业学得差,有啥谬误请指正)。也就是说,除了 UI 和前端架构需要自己解决之外,其他的组件都是可以奉行拿来主义的,如果打算让车子跑得更稳更安全,可以对组件进行打磨优化完善。
说了这么说,倒不如看看徐飞的文章《2015前端组件化框架之路》 里面写的内容都是经过一定实践得出的想法,所以大部分内容我是赞成而且深有体会的。
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吧:
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
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。 下面是项目结构预览:
他们分别是图片资源,引用库资源,发布打包后的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
3.记得以前做一个手机端项目,完全没有自动化,各个页面间跳转慢的一比,一点也不流畅,项目结构不容易管理,重复代码特别多。
近百个页面js版本得不到控制,管理js,css引用困难。微信静态资源缓存如此严重,没有版本控制,每个页面js版本的修改要人命。
4.解决缓存问题,应禁止html缓存,由于使用extract-text-webpack-plugin,可以保证你html入口中只有简单的几行代码,等着自动化帮你引入所需js,所以即使禁止html缓存,也不会影响响应速度,毕竟我们的html文件 也就1-2k左右.html禁止缓存的原因是防止,js更新后,js hash版本已改变,但浏览器缓存的html中,依然是请求旧版本js文件,这样一来js版本控制变得没有意义。
6. 苹果手机和个别安卓手机,使用原生input调用拍照后,图片到页面中会出现旋转问题,所以在微信上 使用js sdk, 在其他浏览器上,就用EXIF.js 手动将其旋转90度 或者180度进行矫正。
7.推荐一款mobile用的不错的弹窗组件 vue-ios-alert. ios风格的弹窗。地址以及github: http://isay.me/vue-ios-alertview/example/
8.手机上的 日期
时分秒选择器,推荐一个有坑的货
https://github.com/k186
有坑哦,使用的话,请看closed的第一个issue。另外日期选择还是比较推荐原生。加上时分秒的话原生的可能就不好用。
9.页面touch切换tag 使用的一个vue-tab github找一找,ios8不支持 flex-shrink,要使用-webkit-flex-shrink。
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找)。
示例:
18.经过babel编译es5的都没问题.。 我有个单独的小功能,没用es6,直接谷歌调试开发,结果到了ios9.x上 不支持也不报错,以后避免踩进去吧。下面是代码:
19. IOS上计算时间 需要new Date('2017/09/09')的格式, 而不能使用横杠的格式
本文为系列文章,总共分四节,建议按顺序阅读:
对于单向数据流循环和数据双向绑定谁优谁劣是永远也讨论没结果的问题,要看是什么业务场景什么业务逻辑,如果这个前提没统一好说啥都是白搭。当然,这个挑战的前提是非后台的单页面应用,后台的前端根本就不需要考虑前端内存缓存数据的处理,直接跟接口数据库交互就行了。明确了这个前提,我们接着讨论什么叫领域数据中心化。
前面讨论到两种数据绑定的方式,但是如果频繁跟接口交互:
- 内存数据销毁了,重新请求数据耗时浪费流量
- 如果两个接口字段部分不一样但是使用场景一样
- 多个页面直接有部分的数据相同,但是先来后到导致某些计数字段不一致
- 多个页面的数据相同,其中某些数据发生用户操作行为导致数据发生变动
因此,我们需要在业务视图逻辑层和数据接口层中间增加一个 store(领域模型),而这个 store 需要有一个统一的 内存缓存 cache,这个 cache 就是中心化的数据缓存。那这个 store 究竟是用来弄啥勒?
Store 具有多形态,每个 store 好比某一类物品的仓储(领域,换个词容易理解),如蔬果店 fruit-store, 服装店 clothes-store,蔬果店可以放苹果香蕉黑木耳,服装店可以放背心底裤人字拖。如果品种过于繁多,我们可以把蔬果店精细化运营变成香蕉专卖店,苹果专卖店(!== appstore),甚至是黑木耳专卖店…( _ _)ノ|,蔬果种类不一样,但是也都是称重按斤卖嘛。
var bannerStore = new fruitStore();
var appleStore = new fruitStore();
有了这些仓储之后,我们可以放心的把数据丢给视图逻辑层大胆去用。想修改数据?直接让 store 去改就行了,其他页面的 DOM 文本内容也得修改吧?那是其他页面的业务逻辑做的事,我们把事件抛出去就好了,他们处不处理那是他们的事,咱别瞎操心(业务隔离)。
那么 store 具体弄啥勒?
- 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
还有另一个作用就是数据的输入输出格式化,简单举个栗子:
- 任何接口 API 返回的数据,都需要经过 input format 进行统一格式化,然后再写入 cache,因为读取的数据已按照我们约定的规范进行的处理,所以我们使用的时候也不需要理会接口是返回怎样的数据类型。
- 某些组件需要的数据字段格式可能不同,如果把数据处理放在模板进行处理,会导致模板无法更加简洁通用(业务耦合),所以需要 output format 进行处理。
所以,store 就是扮演着这样的角色——是数据状态读写和同步,以及数据输入输出的格式化处理。
1.脑子抽风啊,分为四个SPA, 整套项目下来,感觉还是应该做一个SPA。毕竟SPA之间切换,一个SPA切换到另一个SPA 还是加载东西太多,不够流畅。虽然微信浏览器缓存“严重”
2.项目结构划分还是不够合理,但感觉也还能对付用。
3.组件化没有发挥到极致,自己vue组件间通信没搞好,md找子组件,我竟然还有通过遍历的方式。
4.有些组件用的jquery的,搭配的不是很流畅,导致个别操作强行使用dom操作。
5.我有四个环境,开发,测试,demo, 线上。 每次发布到一个环境 都需要改了配置后,重新打包,很痛苦啊,关于这一点有什么好的办法吗?
《Vue Webpack使用规范》
现在 Hybrid App 架构应用很火啊 _ (:3」∠)_,不搞一下都不好意思说自己是做 H5的。这里所说的 Hybrid App 可不是那种内置打包的 html 源码那种,而是直接去服务端请求 html 文档那种,可能会使用离线缓存。有的人以为如果要使用 Hybrid 架构,就不能使用 SPA 的方式,其实 Hybrid 架构更应该使用 SPA。
遇到的几个问题,我简单列举一下:
客户端通过 url 传参
如果通过 http get 请求的 query 参数进行传参,会导致命中不到 html 文档缓存,所以通过 SPA 的 hash query 传参,可以规避这个问题。
与其他 html 页面进行跳转
这种场景下,进入新页面和返回旧页面导致 webview 会重新加载本地的 html 文档缓存,视觉体验很不爽,即使页面使用了离线缓存,而 SPA 可以规避这个问题。
使用了离线缓存的页面需要支持代码多版本化
由于采用了非覆盖性资源发布方式,所以需要仍然保留旧的代码一段时间,以防止用户使用旧的 html 文档访问某些按需加载功能或清除了本地缓存数据而拿不到旧版本代码。
js 和 css 资源 离线化
由于离线缓存的资源需要先在 manifest 文件声明,你也不可能总是手动去维护需要引用的 js 和 css 资源,并且那些按需加载的功能也会因此失去按需加载的意义。所以需要将 js 和 css 缓存到 localstorage,直接省去这一步维护操作。至于用户清除 localstorage,参考第三点解决方案。
图标资源离线化
将图标文件进行 base64 编码后存入 css 文件,方便离线使用。
咱们公司后台项目目前大概有十几个api模块。随着业务的迭代,模块会越来越多。所以这里根据业务模块来划分pages,并且将pages 和 api 两个模块一一对应,方便维护,如下图
aip和pages.png
这样不管项目怎么累加,api和pages比较好维护。
这个项目产品将继续开发,不过下一阶段还有个项目,我将使用一个SPA完成,关于vue有什么好的各种mobile组件,希望dalao不吝推荐。
如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下加【推荐】按钮。
如果,您希望更容易地发现我的新博客,不妨点击下方红色【关注】的。
因为,我的分享热情也离不开您的肯定支持。
感谢您的阅读,我将持续输出分享,我是蜗牛, 保持学习,谨记谦虚。不端不装,有趣有梦。
《Vue Webpack开发可复用的单页面富应用教程(配置篇)》
@前端农民工 在 别处 已经说得很清楚了,直接传送门过去看吧,这里不罗嗦了。
1 赞 2 收藏 评论
这里的components放置的都是全局公用的一些组件,如上传组件,富文本等等。
components.png
《Vue Webpack开发可复用的单页面富应用教程(组件篇)》
vex要根据需求去使用,咱们后台项目来说,虽然业务模块比较多,还有权限,但业务之间的耦合度是很低的,所以根本没有必要使用vuex来存储data,每个页面里存放自己的data就行。当然有些数据还是需要用vuex来统一管理的,如登录,用户信息,还是用vuex管理方便。
《Vue Webpack开发可复用的单页面富应用教程(技巧篇)》
路由这个概念最先是在后台出现的,浏览器发出请求,服务器根据请求,解析url路径,根据服务器的路由配置,返回相应 html 字串。我们前端路由的实现本质上就是检测 url 的变化,截获 url 地址,然后解析来匹配路由规则,每次 hash 值的变化,会触发 hashchange 这个事件,通过替换 DOM 的方式来实现页面的切换,还有通过HTML5的两个api,pushState 和 replaceState实现记住路由。
在上一节中,我们介绍了在项目https://github.com/icarusion/vue-vueRouter-webpack中关于webpack的一些基础配置,包括开发环境和生产环境,在本节中,我们重点介绍使用Vue.js和vue-router,通过组件化的方式来开发单页面富应用的相关内容。读者可以clone或下载这个项目,结合具体代码来看本文。
<router-view> 是用来渲染路径匹配到的组件。<router-view> 还可以内嵌<router-view>,实现路由嵌套。
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
基础知识扫盲
今天我只是给大家简单的说了一下后台管理的结构和vue的简单知识,大家要是有兴趣可以去了解一下,也可以随时交流~
本段主要介绍一些前端的基础概念,老司机可以直接跳过。
单页面富应用(SPA)和前端路由
单页面富应用(即Single Page Web Application,以下简称SPA)应该是最近几年火起来的,尤其是在Angular框架诞生后,很多SPA的网站以及基于Electron或Ionic的桌面App和移动App层出不穷,比如Teambition。
SPA的核心即是前端路由。何为路由呢?说的通俗点就是网址,比如www.talkingcoder.com/article/list;专业点就是每次GET或者POST等请求,在服务端有一个专门的正则配置列表,然后匹配到具体的一条路径后,分发到不同的Controller,然后进行各种操作后,最终将html或数据返回给前端,这就完成了一次IO。当然,目前绝大多数的网站都是这种后端路由,也就是多页面的,这样的好处有很多,比如页面可以在服务端渲染好直接返回给浏览器,不用等待前端加载任何js和css就可以直接显示网页内容,再比如对SEO的友好等。那SPA的缺点也是很明显的,就是模板是由后端来维护或改写。前端开发者需要安装整套的后端服务,必要还得学习像PHP或Java这些非前端语言来改写html结构,所以html和数据、逻辑混为一谈,维护起来即臃肿也麻烦。然后就有了前后端分离的开发模式,后端只提供API来返回数据,前端通过Ajax获取到数据后,再用一定的方式渲染到页面里,这么做的优点就是前后端做的事情分的很清楚,后端专注在数据上,前端专注在交互和可视化上,从此前后搭配,干活不累,如果今后再开发移动App,那就正好能使用一套API了,当然缺点也很明显,就是首屏渲染需要时间来加载css和js。这种开发模式被很多公司认同,也出现了很多前端技术栈,比如以jQuery artTemplate Seajs(requirejs) gulp为主的开发模式所谓是万金油了。在Node.js出现后,这种现象有了改善,就是所谓的大前端,得益于Node.js和JavaScript的语言特性,html模板可以完全由前端来控制,同步或异步渲染完全由前端自由决定,并且由前端维护一套模板,这就是为什么在服务端使用artTemplate、React以及即将推出的Vue2.0原因了。那说了这么多,到底怎样算是SPA呢,其实就是在前后端分离的基础上,加一层前端路由。
前端路由,即由前端来维护一个路由规则。实现有两种,一种是利用url的hash,就是常说的锚点(#),JS通过hashChange事件来监听url的改变,IE7及以下需要用轮询;另一种就是HTML5的History模式,它使url看起来像普通网站那样,以"/"分割,没有#,但页面并没有跳转,不过使用这种模式需要服务端支持,服务端在接收到所有的请求后,都指向同一个html文件,不然会出现404。所以,SPA只有一个html,整个网站所有的内容都在这一个html里,通过js来处理。
前端路由的优点有很多,比如页面持久性,像大部分音乐网站,你都可以在播放歌曲的同时,跳转到别的页面而音乐没有中断,再比如前后端彻底分离。前端路由的框架,通用的有Director,更多还是结合具体框架来用,比如Angular的ngRouter,React的ReactRouter,以及我们后面用到的Vue的vue-router。这也带来了新的开发模式:MVC和MVVM。如今前端也可以MVC了,这也是为什么那么多搞Java的钟爱于Angular。
开发一个前端路由,主要考虑到页面的可插拔、页面的生命周期、内存管理等。
编写可复用的代码、模块化、组件
编写可复用的代码是对编程质量的一个体现。写一个通用工具函数、维护一个对象,这些都可以说是可复用的,不过我们这里讨论的,主要是利用CommonJS规范来进行模块化开发。那代码复用和模块化有什么关系呢,其实模块化的一个原因就是可以使代码复用,你开发的模块可以提供给其他人用,一个模块可以是小到一个配置文件,也可以大到一个日历组件。把一个页面拆分成不同的模块,然后来组装,这样既能提高开发效率,又方便维护。那组件又是什么呢?如果说模块化是一种开发模式,那组件就是这种模式的具体实现。比如一个Button按钮、一个输入框,或者一个上传控件都可以封装为一个组件,在使用的时候,可能只用写一行,就能实现文件上传功能,甚至可以支持拖拽上传、大小和格式限制等。那一个组件具体怎么开发呢,这就是本文后面重点讨论的内容了。
Vue的路由和它的组件化
在项目https://github.com/icarusion/vue-vueRouter-webpack中,我们使用的技术栈是vue.js vue-router webpack,其中webpack的作用已经在上篇文章中详细介绍了。在说vue-router之前,我们先聊聊Vue的组件。
组件的构造
Vue的组件可以说是Vue中最神奇也是最难懂的部分了,这部分懂了,vue也就懂了。vue组件的特点是可插拔、独立作用域、观察者模式、完整的生命周期。我们来看一个组件的基本构成:
Vue.component('child', { props: ['msg'], template:'{{ msg }}', data:function(){return{ title:'TalkingCoder'} }, methods: {// ...}, ready:function(){ }, beforeDestroy:function(){ }, events: {// ...}});
一个组件基本跟一个vue实例是类似的,也有自己的methods和data,只不过data是通过一个function来返回了一个对象,具体原因可以查看vue的文档。
props是从父级通过html特性传递来的数据,它可以是字符串、数字、布尔、数组、对象,默认是单向的,也可以设置为双向绑定的。props里的参数可以直接通过像this.msg这种方式调用,这与data的里的数据是一样的。
template是这个组件使用的html片段,可以直接是字符串,也可以像'#child'这样标识一个dom节点。
ready和beforeDestroy是两个常用的生命周期,ready是在组件准备好时的一个回调,一般在这里我们可以使用获取数据、实例化第三方组件、绑定事件等,beforeDestroy正好相反,是在组件即将被销毁时触发回调,在这里我们销毁自定义的实例、解绑自定义事件、定时器等。
如何使用组件
组件一般是由它的父级来显示调用的,比如上面的child组件,我们就可以在父级中使用:
newVue({ data: { msg1:'Hello,TalkingCoder', msg2:'你好,TalkingCoder'}})
上例使用了两次child组件,使用props传递了一个参数msg,并且第二个组件的参数是双向绑定的,在双向绑定后,无论修改父级还是子元素的msg,双方的数据和view都会响应到,而单向绑定后,子组件修改是不会影响到父级的。
在渲染完,的内容就会替换为组件template内的字符串了,虽然使用的是同一个child组件,但是两次使用的作用域是独立的,这也是为什么在组件内data要使用function来返回一个对象的原因。
父子组件间的通信
在Vue.js中,父子之间的通信主要通过事件来完成,这种就是我们熟悉的观察者模式(或叫订阅-发布模式),很多框架也都使用了这种设计模式,比如Angular。父组件通过Vue内置的$broadcast()向下广播事件和传递数据,子组件通过$dispatch()向上派发事件和传递数据,双方都可以在events对象内接收自定义事件,并且处理各自的业务逻辑。
父组件使用了多个相同子组件,如何区分呢?比如我们上面的demo使用了两次child组件,但是如何来区分这两个呢,也就是说如果给child广播事件,如何给其中指定的一个广播呢,因为广播后,它俩都会接收到事件的。我们可以使用v-ref来标识组件:
newVue({ data: { msg1:'Hello,TalkingCoder', msg2:'你好,TalkingCoder'}, methods: { sendData:function(){this.$refs.child1.$emit('set-data', {});this.$refs.child2.$emit('set-data', {}); } }})
通过$refs就可以给指定的组件触发事件了,事实上,通过$refs是可以获取到子组件的整个实例的。
子组件派发事件,而父组件仍然使用了多个相同子组件,如何区分是哪个组件派发的呢?还是上面的demo,比如我们的child组件$dispatch了一个自定义事件,可以这样来区分:
newVue({ data: { msg1:'Hello,TalkingCoder', msg2:'你好,TalkingCoder'}, methods: { sendData:function(){this.$refs.child1.$emit('set-data', {});this.$refs.child2.$emit('set-data', {}); }, handler1:function(){// ...}, handler2:function(){// ...} }})
像绑定DOM2事件一样,使用@xxx或v-bind:xxx来绑定自定义事件,来执行不同的方法。
内容分发slot
有时候我们编写一个可复用的组件时,比如下面的一个confirm确认框:
标题、关闭按钮是统一的,但是中间正文的内容(包括样式)是想自定义的,这时候就会用到Vue组件的slot来分发内容。比如子组件的template的内容为:
确定取消
父组件这样调用子组件:
欢迎来到TalkingCoder
最终渲染完的内容为:
欢迎来到TalkingCoder
确定取消
编写可复用组件
这里引用一段来自vue.js文档的内容:
在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。
Vue.js 组件 API 来自三部分——prop,事件和 slot:
prop允许外部环境传递数据给组件;
事件允许组件触发外部环境的 action;
slot允许外部环境插入内容到组件的视图结构内。
使用v-bind和v-on的简写语法,模板的缩进清楚且简洁:
Hello!
路由、组件和组件化
上文说了那么多,现在终于到重点了。在上一篇文章中,我们简单的提到了组件化,这也是将Vue使用到极致的必经之路。我们先看一下src/main.js文件。
Vue有点像Express的用法,也有中间件的概念,比如我们用到的vue-router,还有vuex,它们都是vue的中间件,当然我们自己也可以开发基于vue的中间件。
importVuefrom'vue';importVueRouterfrom'vue-router';importAppfrom'components/app.vue';importEnvfrom'./config/env';Vue.use(VueRouter);// 开启debug模式Vue.config.debug =true;// 路由配置varrouter =newVueRouter({ history: Env !='production'});router.map({'/index': { name:'index', component:function(resolve){require(['./routers/index.vue'], resolve); } }});router.beforeEach(function(){window.scrollTo(0,0);});router.afterEach(function(transition){});router.redirect({'*':"/index"});router.start(App,'#app');
以上代码就是main.js的内容,这也是我们项目跑起来后第一个执行的js文件。在导入了Vue和VueRouter模块后,使用Vue.use(VueRouter)安装路由模块。路由可以做一些全局配置,具体可以查看文档,这里只说一个就是history,上文已经介绍了关于HTML5的History,它用history.pushState()和history.replaceState()来管理历史记录,服务器需要正确的配置,否则可能会404。开启后地址栏会像一般网站那样使用“/”来分割,比“#”要优雅很多,可以看到我们通过环境模块env.js默认给开发环境开启了History模式路由,生产环境没有开启,为的是可以让大家来体验到这两者的差异性,使用者可以自己来修改配置。
导入的app.vue模块就是我们的入口组件了,上篇文章已经介绍过,我们通过webpack生成的index.html里,body内只有一个挂载节点
,当我们通过执行router.start(App, '#app')后,app.vue组件就会挂载到#app内了,所以app.vue组件也是我们工程起来后,第一个被调用的组件,可以在它里面完成一些全局性的操作,比如获取登录信息啊,统计日活啊等等。
在app.vue内,有一个的自定义组件,它就是整个网站的路由挂载节点了,切换路由时,它的内容会动态的切换,其实是在动态的切换不同的组件,得益于webpack,路由间的切换可以是异步按需加载。
router.map()就是设置路由匹配规则,比如访问127.0.0.1:8080/index,就会匹配到"/index",然后通过component,在回调里使用require()异步加载组件,这个过程是可以改为同步的,不过应该没有人会这么做。vue-router支持匹配带参数的路由,比如'/user/:id'可以匹配到'/user/123',或者'/user/*any/bar'可以匹配到'/user/a/b/bar',:id是参数,*any是全匹配,不过vue-router支持的路由规则还是比较弱的,一般后端框架,比如Python的Tornado或者Node.js的Express是支持正则的。
vue的路由只是动态的调用组件,根本上还是MVVM,而Angular的路由是MVC的,在ng的controller里,可以使用templateURL来使用一个html片段,而vue的组件是不支持这种模式的,必须把html字符串写(或编译)在template里,因为在Vue的设计里,一个组件(.vue文件)是应该把它的样式、html和js紧耦合的,这正是组件化的魅力所在。
嵌套路由。vue-router是支持嵌套路由的,在app.vue里的是我们的根路由挂载,如果需要,可以在某个具体的路由组件里面再使用一个来分发二级路由。具体使用方法可查看文档。
路径跳转。vue-router使用v-link指令来跳转,它会隐式的在DOM上绑定点击事件:
首页首页
如果是在js里跳转,可以这样:
module.exports = { data:function(){return{ } }, methods: { go:function(){console.log(this.$route);console.log(this.$router);this.$router.go('/index'); } }}
使用vue内置的$router方法也可以跳转,如果感兴趣,可以试试上面$route和$router打印出什么内容,通过$route是可以得到当前路由的一些状态信息的,比如路径和参数。
vue-router还有一些钩子函数,通俗讲就是在发生一次路由时某个状态的一些回调。我们的项目main.js中使用了:
router.beforeEach(function(){window.scrollTo(0,0);});router.afterEach(function(transition){console.log(transition);});
beforeEach()是在路由切换开始时调用,这里我们将页面返回了顶端。
afterEach()是在路由成功切换到激活状态时调用,可以打印出transition看看里面都有什么。一般在这里可以做像自动导航、自动面包屑的一些全局工作。
router.redirect()很简单,就是重定向了,找不到路由时可以跳转到指定的路由。
小结
跟vue相关的组件化内容大概就是这么多了,说到底,vue的路由也是个组件,与普通组件并没有任何差异化,只是概念的不同。vue还有一些知识,比如自定义指令,自定义过滤器,这些原理也很类似,使用也很简单,大家可以参考项目中的demo,结合文档来学习使用。在下一篇中,将介绍一些开发中沉淀的技巧或使用经验。
本文由星彩网app下载发布于前端技术,转载请注明出处:大型单页面应用的进阶挑战,page应用实战总结和