You are my JavaScript Queen


  • 首页

  • 标签

  • 归档

微信小程序实现多页面传参通信

发表于 2017-04-10

一 驱动

今年年初做了一款很简单的pm2.5查询的小程序,进入首页,自动定位城市,同时用户可以点击城市区域,进入城市列表,选择任意城市,返回首页,读取选择城市的pm2.5情况。

如图所示:

'首页'

'城市列表'

如果只是在web上,这样的需求非常容易解决。但是小程序的API接口里,从A页面进入B页面是可以带参数的,但是,反过来,从B页面退回A页面,是不允许带参数的。

二 解决方法

我当时选择了把要传的参数(用户选择的城市)存入storage里,也就是使用微信的wx.setStorage方法进行存储,回到A页面的时候先检测storage里的数据。然后就把这个项目收尾了。

之后看到了一篇文章,讲述了如何使用onfire.js这个js库来进行页面传输的方式。我自己动手试了一下,非常好用。

三 onfire.js介绍

onfire.js的github地址在此,只有0.9kb大小,非常轻便。

它可用于:

  • 简单的事件分发;
  • 在 react / vue.js / angular 用于跨组件的轻量级实现;
  • 事件订阅和发布;

应用在小程序里,思路如下:

  1. A页面先订阅一个事件,并定义处理方法。
  2. B页面返回时发送消息。
  3. A页面卸载时,解除订阅。

四 实践测试

在A页面写入一个button,点击button可以跳转到B页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var onfire = require("../utils/onfire.js");
var eventObj = onfire.on('testEvent', function (args) {
//打印从B页面带来的参数
console.log(args)
});
Page({
data: {
},
//跳转事件
gotoNext: function(){
wx.navigateTo({
url: '../test/test',
success: function(res){
}
})
},
onLoad: function(options) {
},
onUnload: function() {
onfire.un('testEvent');
onfire.un(eventObj);
}
})

调用onfire.on方法订阅一个testEvent的消息,args是接收从B页面传来的参数。需要注意一定要在onUnload里取消订阅testEvent的消息,并取消绑定eventObj。

在B页面里写入返回页面的API,并在回调的地方加入消息的分发。

1
2
3
4
5
6
7
8
9
10
11
12
wx.navigateBack({
success: function(res){
//有参数时
onfire.fire('testEvent',{name:'111',age:12});
},
fail: function(res) {
// fail
},
complete: function(res) {
// complete
}
})

操作以后会发现回到A页面的时候,控制台打出了对应的Object为{name:’111’,age:12},可以传数字,字符串,json对象。

有了这个库,我们就可以实现页面之间的随意通信,从而控制ui对应的变化。也便于代码的阅读和后期的维护。

react初体验: 使用react搭建新闻网站

发表于 2017-03-10

近日接触了React,跟着课程用React做了一个新闻展示网站,兼容PC和移动端,项目没有对响应式进行完全的适配,主要是css方面不是重点,重点是体验React的组件化功能和组件的书写方式,以及ant design的试用。
项目地址:react_news_demo。

环境搭建

根据react官网描述,最基本的框架库为react和react-dom,后者主要是用来实现react的虚拟DOM设计方案。
react有自己的官方路由工具——react-router。
关于XHR请求,fetch非常好用。
如果要兼容PC端和移动端,那么请使用react-response。

构建工具为webpack,语法使用ES6,那么久要有相关的解析,React的官网已经进行了相关说明,分别为babel-loader, babel-plugin-react-html-attrs, babel-preset-es2015, babel-preset-react, babelify。

同时还有最重要的组件库,阿里出的ant design.

于是,得出了如下的package.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"name": "react_news_demo",
"version": "1.0.0",
"description": "",
"main": "route.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --hot --inline"
},
"author": "",
"license": "ISC",
"dependencies": {
"antd": "^2.7.3",
"babel-loader": "^6.3.2",
"babel-plugin-react-html-attrs": "^2.0.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.23.0",
"babelify": "^7.3.0",
"css-loader": "^0.25.0",
"fetch": "^1.1.0",
"json-loader": "^0.5.4",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-mixin": "^2.0.2",
"react-responsive": "^1.2.1",
"react-router": "^3.0.2",
"style-loader": "^0.13.1",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.4.1"
},
"devDependencies": {
"babel-plugin-import": "^1.0.1"
}
}

webpack配置

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path = require('path');
module.exports = {
context: path.join(__dirname),
devtool: debug ? "inline-sourcemap" : null,
entry: "./src/js/route.js",
module: {
loaders: [
{
test: /\.js?$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015'],
plugins: ['react-html-attrs'], //添加组件的插件配置
}
},
//下面是使用 ant-design 的配置文件
{ test: /\.css$/, loader: 'style-loader!css-loader' }
]
},
output: {
path: __dirname,
filename: "./src/bundle.js"
},
plugins: debug ? [] : [
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
],
};

根文件的路由配置

可以参考react-router的文档,举例demo为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react'
import ReactDOM from 'react-dom'
import {Router, Route, hashHistory } from 'react-router'
//还要引用相关的组件
export default class Root extends React.Component{
render(){
return(
<div>
<Router history={hashHistory}>
<Route path="/" component={PCIndex}></Route>
<Route path="/details/:uniquekey" component={PCNewsDetails}></Route>
<Route path="/usercenter" component={PCUserCenter}></Route>
</Router>
</div>
)
}
}
ReactDOM.render(
<Root/>,
mountNode
)

组件的写法

首先引入相关的react库,引入需要的ant design组件,之后在构造函数里进行继承和state设置,在生命周期钩子函数中进行相关的函数设定,创造一些方法用于JSX里元素的调用,在render()中设置相关的变量,生成JSX。
demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React from 'react';
import {Row, Col} from 'antd';
import {
Menu,
Icon,
Tabs,
message,
Form,
Input,
Button,
CheckBox,
Modal,
Card,
notification
} from 'antd';
const FormItem = Form.Item;
const SubMenu = Menu.SubMenu;
const TabPane = Tabs.TabPane;
const MenuItemGroup = Menu.ItemGroup;
import {Router, Route, Link, browserHistory} from 'react-router'
class CommonComments extends React.Component {
constructor() {
super();
this.state = {
...
};
};
componentDidMount() {
...
};
handleSubmit(e) {
...
};
addUserCollection() {
...
};
render() {
let {getFieldDecorator} = this.props.form;
const {comments} = this.state;
const commentList = comments.length
?comments.map((comment,index)=>(
<Card key={index} title={comment.UserName} extra={<a href="#">发表于{comment.datetime}</a>}>
<p>{comment.Comments}</p>
</Card>
))
: '没有加载到任何评论';
return (
<div class="comment">
<Row>
<Col span={24}>
{commentList}
<Form onSubmit={this.handleSubmit.bind(this)}>
<FormItem label="您的评论">
<Input type="textarea" placeholder="写评论" {...getFieldDecorator('remark', {initialValue:''})} />
</FormItem>
<Button type="primary" htmlType="submit">提交评论</Button>
&nbsp;&nbsp;
<Button type="primary" htmlType="button" onClick={this.addUserCollection.bind(this)}>收藏该文章</Button>
</Form>
</Col>
</Row>
</div>
);
};
}
export default CommonComments = Form.create({})(CommonComments);

使用fetch进行网络请求

demo:

1
2
3
4
5
6
7
8
var myFetchOptions = {
method: 'GET'
};
fetch(url, myFetchOptions)
.then(response => response.json())
.then(json => {
//成功后的回调函数
});

webpack构筑,Vue框架下的单元测试

发表于 2017-01-13

前言

最近公司鼓励我们开发者进行单元测试,Unit Test一直是很有必要的,尤其对于团队协作和大型的迭代项目开发。但是国内开发者能做到单元测试习惯的并不多。
如果能把测试这一环节努力把控住,就会对我们的项目结果有至关重要的帮助。

测试工具

目前主流的自动化测试工具很多,我选择了Karma。缘分使然,我经常clone的github项目里都有一个叫做karma.conf.js的文件,之前一直不知道是干什么用的,后来研究单元测试的时候才发现是一个自动化测试工具。经过了解,它已经满足了我自动化测试的需求。
Karma的主要工作:

  1. Karma启动一个web服务器,生成包含js源代码和js测试脚本的页面;

  2. 运行浏览器加载页面,并显示测试的结果;

  3. 如果开启检测,则当文件有修改时,执行继续执行以上过程。

安装Karma

全局安装karma:

1
npm i -g karma-cli

打开项目地址,安装karma包:

1
npm i -save-dev karma

之后,和node初始化packa.json一样,使用 karma init命令进行初始化配置即可。
当然也可以直接一路enter,然后在karma.conf.js文件里修改。

这里需要重点说的是,karma init如果在git bash里可能会报错,使用win的cmd终端则可以正常进行

karma配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = function (config) {
config.set({
browsers: ['Chrome'],
frameworks: ['jasmine'],
reporters: ['progress', 'coverage'],
coverageReporter: {
type: 'html',
dir: 'coverage/'
},
files: ['test/index.js'],
preprocessors: {
'test/index.js': ['webpack', 'coverage']
},
singleRun: false
})
}
  • browsers表示观看测试报告的载体,我选择了Chrome浏览器,也有很多同学习惯使用PhantomJS.
  • frameworks是测试框架,我选择了jasmine。
  • files是测试文件。
  • coverageReporter和reporters是测试报告相关的配置。

由于我的项目是用webpack搭建的,所以要重点配置webpack方面的选项

各项说明,详见官网。

使用Webpack

要在webpack的构筑里使用karma,还需要安装一些其他的npm包。

安装karma-webpack:

1
npm i --save-dev karma-webpack

此外,还有一些测试相关的依赖,例如浏览器启动包,覆盖率,jasmine框架等等,这些都需要一一安装。

不多赘言,附上我的package.json上的部分相关配置:

1
2
3
4
5
6
7
8
"devDependencies": {
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.0.2",
"karma-spec-reporter": "0.0.26",
"karma-webpack": "^1.8.0"
}

同时,webpack里需要一些babel的解释器,不过一般情况下,配置webpack环境的时候,这些都已经安装好了。
如果没有安装,可以用以下命令进行:

1
npm i --save-dev babel-loader babel-core babel-preset-es2015

这时,要在karma的配置文件里,增加webpack方面的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
webpack: {
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel!eslint',
// make sure to exclude 3rd party code in node_modules
exclude: /node_modules/
},
{
// edit this for additional asset file types
test: /\.(png|jpg|gif)$/,
loader: 'url',
query: {
// inline files smaller then 10kb as base64 dataURL
limit: 10000,
// fallback to file-loader with this naming scheme
name: '[name].[ext]?[hash]'
}
}
]
}
}

这里确保了vue组件和相关ES6语法可以正常被解析,从而进行karma的测试。

修改的方面主要有:

  1. files只留下test文件。因为webpack会自动把需要的其它文件都打包进来,所以只需要留下入口文件。
  2. preprocessors也修改为test文件,并加入webpack域处理器。
  3. 加入webpack配置选项。可以自己定制配置项,但是不需要entry和output。这里加上babel-loader来编译ES6代码。

接着在bash里命令 karma start就可以看到单元测试的结果了。

会在coverage文件夹里生成一个报告,一看,coverage并不是100%。(前提是我的unit test的expect全部正确)。

造成这样的原因是webpack自身在打包和编译的时候会加入很多自己的代码,从而影响了覆盖率。这就让覆盖率完全没有意义了。有大神已经为我们提供了轮子。

另一个插件

安装istanbul-instrumenter-loader可以避免掉webpack自身代码导致的覆盖率错误。

1
npm i --save-dev istanbul-instrumenter-loader

之后在webpack配置方面增加一个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module: {
postLoaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'istanbul-instrumenter'
}
],
loaders: [
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel!eslint',
// make sure to exclude 3rd party code in node_modules
exclude: /node_modules/
},
{
// edit this for additional asset file types
test: /\.(png|jpg|gif)$/,
loader: 'url',
query: {
// inline files smaller then 10kb as base64 dataURL
limit: 10000,
// fallback to file-loader with this naming scheme
name: '[name].[ext]?[hash]'
}
}
]
}

大功告成以后运行karma start,再看自己的coverage报告,就是100%的覆盖率啦!

Vue组件的覆盖率

但这仅仅是对于js文件可以做到完整准确的覆盖率,对于vue的组件来说,无法达到100%,因为组件化开发,都是html,css,js三个语言写在同一个.vue文件里的,因此覆盖率能有50%左右就很可以了。
对于.vue的文件来说,覆盖率本身并没有太多参考价值,你的代码是否覆盖完全,通过vue的debug模式就可以完全暴露和查阅。主要还是进行用例的测试而非覆盖率测试。

在Vue框架下完成文件上传功能

发表于 2016-11-13

需求描述

近日在工作项目中遇到了一个需求:完成无刷新页面的文件上传功能,上传时要提交文件以外的字段信息,整个项目的框架是Vuejs,无jQuery,也不得使用jQuery的插件。

寻找轮子

由于该需求没有图片文件预览的要求,运行平台也不是移动端,所以相对来说,轮子还是不少的。最终我选择了vue-file-upload。

轮子install

npm

1
npm install --save vue-file-upload

CommonJS

1
var VueFileUpload = require('vue-file-upload');

ES6写法

由于我的项目是用ES6写的,所以重点看这一块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<template>
<vue-file-upload url="http://localhost:8000/vue-file-upload/demo/upload.php" v-bind:files.sync="files" v-bind:events="cbEvents" v-bind:filters="filters" v-bind:request-options="reqopts"></vue-file-upload>
<button type="button" @click="doPost">上传</button>
<table>
<thead>
<tr>
<th>name</th>
<th>size</th>
<th>progress</th>
<th>status</th>
<th>action</th>
</tr>
</thead>
<tbody>
<tr v-for="file in files">
<td v-text="file.name"></td>
<td v-text="file.size"></td>
<td v-text="file.progress"></td>
<td v-text="onStatus(file)"></td>
<td>
<button type="button" value="upload" @click="uploadItem(file)">upload</button>
</td>
</tr>
</tbody>
</table>
<button type="button" @click="uploadAll">上传所有文件</button>
</template>
<script>
import VueFileUpload from '../src/vue-file-upload.vue';
import UploadActions from '../src/config/msg.js';
export default{
data(){
return{
files:[],
//过滤器回调
filters:[
{
name:"imageFilter",
fn(file){
var type = '|' + file.type.slice(file.type.lastIndexOf('/') + 1) + '|';
return '|jpg|png|jpeg|bmp|gif|'.indexOf(type) !== -1;
}
}
],
//事件回调
cbEvents:{
onCompleteUpload:(file,response,status,header)=>{
console.log(file);
console.log("finish upload;")
},
onAddFileSuccess:(file)=>{
console.log(file);
console.log("success add to queue");
}
},
reqopts:{
formData:{
tokens:'tttttttttttttt'
},
responseType:'json',
withCredentials:false
}
}
},
methods:{
doPost(){
this.$broadcast(UploadActions.DOPOST);
},
onStatus(file){
if(file.isSuccess){
return "上传成功";
}else if(file.isError){
return "上传失败";
}else if(file.isUploading){
return "正在上传";
}else{
return "待上传";
}
},
uploadItem(file){
file.upload();
},
uploadAll(){
this.$broadcast(UploadActions.DOPOST);
}
},
components:{
VueFileUpload
}
}
</script>

首先引入vue-file-upload:

1
import VueFileUpload from '../src/vue-file-upload.vue';

然后在data部分里进行文件绑定和其他属性的绑定。

  • filters的部分可以对文件的类型进行过滤
  • cbEvents里进行文件上传后的事件回调
  • reqopts里写入上传文件时除文件以外的XHR传递参数

vue-router钩子函数的生命周期

发表于 2016-10-27

场景描述

近日碰到了一个项目需求,使我不得不去研究vue-router里路由切换的具体过程,在这个过程中发生了什么事情。

场景大概如下:在一个表单页面里,用户可以选择提交,也可以选择不提交而离开。产品经理要求数据不保存在本地浏览器里,在用户提交成功后销毁表单数据;或者用户离开页面(点击取消或者其他tab)时,出现弹窗,提醒用户表单数据还未提交,是否放弃。

在这个场景里,难点在于:1. 如何监听用户离开页面或者关闭页面的事件; 2.如何在用户选择放弃提交以后还能正确指向之前点击的路由目标。

'场景截图'

如上图所示,用户可以点击取消,或者任何侧边dashboard的tab,这就说明,modal弹窗的“取消”按钮上不能增加固定的路由链接,因为无法确定用户会点击哪个路径。只能通过先记录点击路径,然后绑定到取消按钮上。

轮子分析

1.在轮子中解决

首先,内置location对象是可以使用的,但造成的问题是与vue-router分离性太强,同时也无法做到简洁的数据绑定。因此,最佳的方案还是在vue-router本身的功能中解决。
在之前我对于vue-router的了解并不多,一般只是用来做一些基本的路由控制和页面跳转传参,类似如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var Foo = {
template: '<p>This is foo!</p>'
}
var Bar = {
template: '<p>This is bar!</p>'
}
var App = {}
// 创建一个路由器实例
var router = new VueRouter()
// 定义路由规则
router.map({
'/foo': {
component: Foo
},
'/bar': {
component: Bar
}
})
// 路由器会创建一个 App 实例,并且挂载到选择符 #app 匹配的元素上。
router.start(App, '#app')

同时会用到一些路由嵌套,参数传递,大多数情况下,这些功能已经足够覆盖很多项目需求了。

2.vue-router钩子函数生命周期

搞懂了vue路由里钩子函数的运转过程,就可以轻松解决各种跳转需求。
路由的切换过程,本质上是执行一系列路由钩子函数,钩子函数总体上分为两大类:

  • 全局的钩子函数
  • 组件的钩子函数

全局的钩子函数定义在全局的路由对象中,组件的钩子函数则定义在组件的route选项中。

全局钩子函数

全局钩子函数有2个:

  • beforeEach:在路由切换开始时调用
  • afterEach:在每次路由切换成功进入激活阶段时被调用

组件钩子函数

组件的钩子函数一共6个:

  • data:可以设置组件的data
  • activate:激活组件
  • deactivate:禁用组件
  • canActivate:组件是否可以被激活
  • canDeactivate:组件是否可以被禁用
  • canReuse:组件是否可以被重用

切换对象

每个切换钩子函数都会接受一个 transition 对象作为参数。这个切换对象包含以下函数和方法:

  • transition.to
    表示将要切换到的路径的路由对象。
  • transition.from
    代表当前路径的路由对象。
  • transition.next()
    调用此函数处理切换过程的下一步。
  • transition.abort([reason])
    调用此函数来终止或者拒绝此次切换。
  • transition.redirect(path)
    取消当前切换并重定向到另一个路由。

具体的分析过程可以查看官方文档,也可以看这位仁兄的详细教学。

附一张详细教学里的生命周期图片:

'生命周期'

3.实战解决

首先,对我目前的这一个组件绑定一个新的data:completeSubmit。根据这个data的布尔值来判断是否应该跳出询问的弹窗。

1
2
3
4
5
6
7
8
9
10
11
//页面跳转的时候,需要进行询问。
deactivate: function (transition) {
if(!this.completeSubmit){
//未完成提交,出现提示信息
this.confirmModalJump = true;
transition.abort();
}
else{
transition.next();
}
}

出现弹窗的时候,一定要加transition.abort()这行代码,这样router就不会继续前进,地址栏的url也不会进行变化。

那么,如何记录用户即将前往的页面router呢。我们此时先把transition打印出来看一下具体的内容:

transition对象

可以看到,transition里就有关于目的地的记录。那么只要在组件的data里绑定一个新的数据就可以进行记录了。

1
2
3
4
5
6
7
8
9
10
11
12
//页面跳转的时候,需要进行询问。
deactivate: function (transition) {
this.toPage = transition.to.name;
if(!this.completeSubmit){
//未完成提交,出现提示信息
this.confirmModalJump = true;
transition.abort();
}
else{
transition.next();
}
}

通过this.toPage我们就可以拿到目的地了。

modal组件里,通过$dispatch来传递事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
data () {
return {
...
}
},
methods: {
leaveConfirm: function(){
this.$dispatch('confirmLeave', true)
},
cancleBtn: function(){
this.$dispatch('clickDeleteLayer', true)
}
}
}

然后在本组件里进行事件的接收

1
2
3
4
5
6
7
8
9
10
11
events: {
'confirmLeave': function(){
this.confirmModalJump = false;
this.completeSubmit = true;
this.$route.router.go({ name: this.toPage});
window.localStorage.removeItem('addServerInfo');
},
'clickDeleteLayer': function(){
this.confirmModalJump = false;
}
}

在用户确定离开的时候,我们首先把modal隐藏,接着把this.completeSubmit的布尔值进行更改(否则router部分的transition无法进行next()),然后通过this.toPage拿到之前用户的点击地址,在router的对象里进行操作。

关于router的编码式导航,请一定仔细看文档,并区分vue-router的版本号。2.0版本和0.7版本的写法有明显不同。但二者都是支持编码式导航的(另一种方式是常见的html标签式导航)。

最后,清空用户的填写记录,就大功告成啦。

Vuex初体验

发表于 2016-10-18

最近写了一个小项目,涉及到众多vue组件里的数据传输。一直听闻大家说起vuex就是一个状态管理器,可以让组件之间的通信更加的简约和可维护,于是就看了一下vuex方面的东西。

目前只是理解了vuex的一些思路,但是在实践方面还有所欠缺。先把一些简单的demo做一下记录。

做一个只用vueJS的组件通信demo和一个利用了vuex做通信的demo进行对比。

仅仅使用vue

以下是一个简单的小程序,点击按钮加减号,数字就会跟随者进行加减变动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<p>{{count}}
<button @click="inc">+</button>
<button @click="dec">-</button>
</p>
</div>
<script>
new Vue({
el:'#app',
data () {
return {
count: 0
}
},
methods: {
inc () {
this.count++
},
dec () {
this.count--
}
}
})
</script>

整个代码结构非常清晰,代码是代码,数据是数据,这也是我一直以来非常喜欢vue.js的重要原因。代码就是放在methods数组内的两个函数inc、dec,被指令@click关联到button上。而data内返回一个属性count,此属性通过绑定到标签p内。

使用vuex

同样的一个demo,如果用vuex的思想,是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@next"></script>
<div id="app">
<p>{{count}}
<button @click="inc">+</button>
<button @click="dec">-</button>
</p>
</div>
<script>
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
inc: state => state.count++,
dec: state => state.count--
}
})
const app = new Vue({
el: '#app',
computed: {
count () {
return store.state.count
}
},
methods: {
inc () {
store.commit('inc')
},
dec () {
store.commit('dec')
}
}
})
</script>

变化如下:

1. 新的代码添加了对vuex的依赖。
2. methods数组还是这两个方法,这和demo1是一样的;但是方法内的计算逻辑,不再是在函数内进行,而是提交给store对象。这是一个新的对象!
3. count数据也不再是一个data函数返回的对象的属性;而是通过计算字段来返回,并且在计算字段内的代码也不是自己算的,而是转发给store对象。再一次store对象。

换言之,之前在vue实例内做的操作和数据的计算现在都不再自己做了,而是交由对象store来做了。

store对象是Vuex.Store的实例。在store内有分为state对象和mutations对象,其中的state放置状态,mutations则是一个会引发状态改变的所有方法。正如我们看到的,目前的state对象,其中的状态就只有一个count。而mutations有两个成员,它们参数为state,在函数体内对state内的count成员做加1和减1的操作。

功能还是那些功能,现在引入了一个store对象,把数据更新的功能给揽过去,不再需要vue实例自己计算了,代价则是引入了新的概念和层次。

vuex解决了组件之间共享同一状态的麻烦问题。当我们的应用遇到多个组件共享状态时,会需要:

1. 多个组件依赖于同一状态。传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。这需要你去学习下,vue编码中多个组件之间的通讯的做法。

2. 来自不同组件的行为需要变更同一状态。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。

以上的这些模式非常脆弱,通常会导致无法维护的代码。官网说:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态。这里的关键在于集中式存储管理。这意味着本来需要共享状态的更新是需要组件之间通讯的,而现在有了vuex,组件就都和store通讯了。问题就自然解决了。

这就是为什么官网再次会提到Vuex构建大型应用的价值。如果不打算开发大型单页应用,使用 Vuex就会感到很繁琐。项目本身并不复杂,则使用一个store bus来进行运输就可以了。

最后,我在项目里还是弃用了vuex,因为真的本身的传输并没有那么多的层次和路线,只是一次子组件向父组件传输所有数据,然后父组件再分发给其他子组件的过程。

然后,这次学习vuex的过程却是值得的。

1…34
Snapline

Snapline

毕业于香港城市大学
苏州码农
热爱摇滚乐

24 日志
9 标签
GitHub 我的邮箱
Links
  • 张鑫旭
  • 阮一峰
  • 廖雪峰
  • witness
  • 慕课网
  • 掘金前端
  • Vuejs
  • 微信小程序
© 2021 Snapline
特别鸣谢 Hexo
|
主题 — NexT.Pisces v5.1.4