前面一个章节提及了路由,因为考虑到整体性的原因,我会单独把 Vue Router放在新的部分,而不在模块化项目这部分里面继续深入了,接下来是状态管理、生产环境部署、服务端渲染以及安全这些和模块化相关的部分,当然状态管理主要是说到 Vuex,这个也是可以展开说的,同样单独成章,就不细聊了。
1 状态管理
由于状态零散地分布在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长,为了解决这个问题,Vue 提供了 vuex甚至集成到时光旅行调试这个翻译真强大……我生出一种无力吐槽的感觉,哈哈……
后面我用历史回滚之类的词吧,因为时光旅行有点太不好描述的别扭了。
如果是 React的开发者,可能会对 Vuex和 Redux是 React生态环境中最流行的 Flux实现:Redux事实上无法感知视图层,所以它能够轻松的通过一些简单绑定和 Vue一起使用;Vuex区别在于它是一个专门为 Vue应用所设计,这使得它能够更好地和 Vue进行整合,同时提供简洁的 API和改善过的开发体验。
1.1 简单状态管理
经常被忽略的是,Vue应用中原始 data对象的实际来源——当访问数据对象时,一个 Vue实例只是简单的代理访问,所以,如果我们有需要被多个实例间共享的状态,可以简单地通过维护一份数据来实现共享:
let sourceOfTruth = {};
let vmA = new Vue({
data: sourceOfTruth
});
let vmB = new Vue({
data: sourceOfTruth
});
现在当 sourceOfTruth发生变更,vmA和 vmB都将自动地更新它们的视图,子组件们的每个实例也会通过 this.$root.$data 去访问。
现在我们有了唯一的数据来源,但是,调试将会变为噩梦:任何时间,我们应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。
为了解决这个问题,我们采用一个简单的 store(仓库)模式:
let store = {
debug: true,
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
if (this.debug) console.log('setMessageAction triggered with', newValue);
this.state.message = newValue;
},
clearMessageAction () {
if (this.debug) console.log('clearMessageAction triggered');
this.state.message = '';
}
};
需要注意,所有 store中 state的变更,都放置在 store自身的 action中去管理,这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发;当错误出现时,我们现在也会有一个 log记录 bug之前发生了什么。
此外,每个实例/组件仍然可以拥有和管理自己的私有状态:
let vmA = new Vue({
data: {
privateState: {},
sharedState: store.state
}
});
let vmB = new Vue({
data: {
privateState: {},
sharedState: store.state
}
});
重要的是,注意我们不应该在 action 中 替换原始的状态对象:组件和 store需要引用同一个共享对象,变更才能够被观察到。
接着我们继续延伸约定,组件不允许直接变更属于 store实例的 state,而应执行 action来分发(dispatch)事件通知 store去改变,我们最终达成了
这样约定的好处是,我们能够记录所有 store中发生的 state变更,同时实现能做到记录变更、保存状态快照、历史回滚的先进调试工具。
1.2 Vuex
Vuex是一个专为 Vue.js 应用程序开发的状态管理模式。
它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化;Vuex也集成到 Vue的官方调试工具 time-travel调试、状态快照导入导出等高级调试功能,如下截图:
什么是“状态管理模式”?
让我们从一个简单的 Vue计数应用开始:
new Vue({
// state data () {
return {
count: 0
}
},
// view template: `
// actions methods: {
increment () {
this.count++;
}
}
});
这个状态自管理应用包含以下几个部分:state:驱动应用的数据源
view:以声明方式将 state 映射到视图
actions:响应在 view 上的用户输入导致的状态变化
以下是一个表示“单向数据流”理念的简单示意:
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:多个视图依赖于同一状态
来自不同视图的行为需要变更同一状态
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力;对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。
以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
这就是 Vuex背后的基本思想,借鉴了 Vuex是专门为Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
如果要交互式地学习 Vuex,可以看这个 Scrimba 上的 Vuex 课程,它将录屏和代码试验场混合在了一起,可以随时暂停并尝试。
什么情况下我应该使用 Vuex?
Vuex可以帮助我们管理共享状态,并附带了更多的概念和框架,这需要对短期和长期效益进行权衡。
如果不打算开发大型单页应用,使用 Vuex可能是繁琐冗余的——如果应用够简单,最好不要使用 Vuex,上文提到的简单的 store 模式就足够所需了。
但是,如果我们需要构建一个中大型单页应用,就需要考虑如何更好地在组件外部管理状态,Vuex将会成为自然而然的选择。
引用 Redux的作者 Dan Abramov 的话说就是:Flux架构就像眼镜:您自会知道什么时候需要它。
具体的使用以及其优点,后面单独成章来深入吧,这里考虑到篇幅,就不展开了。
2 生产环境部署
开发环境下,Vue会提供很多警告来帮我们对付常见的错误与陷阱;而在生产环境下,这些警告语句却没有用,反而会增加应用的体积,此外,有些警告检查还有一些小的运行时开销,这在生产环境模式下是可以避免的。
2.1 不使用构建工具
如果用 Vue完整独立版本,即直接用
2.2 使用构建工具
当使用 webpack或 Browserify类似的构建工具时,Vue源码会根据 process.env.NODE_ENV 决定是否启用生产环境模式,默认情况为开发环境模式。
在 webpack与 Browserify中都有方法来覆盖此变量,以启用 Vue的生产环境模式,同时在构建过程中警告语句也会被压缩工具去除;所有这些在 vue-cli模板中都预先配置好了,但了解一下怎样配置会更好。
(1)webpack
在 webpack 4+ 中,我们可以使用 mode选项:
module.exports = {
mode: 'production'
};
但是在webpack 3及其更低版本中,需要使用
const webpack = require('webpack');
module.exports = {
// ... plugins: [
// ... new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
};
(2)Browserify在运行打包命令时将 NODE_ENV 设置为 production,这等于告诉 vueify避免引入热重载和开发相关的代码
对打包后的文件进行一次全局的 Vue源码中所有用环境变量条件包裹起来的警告语句
例如:
NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js或者在 Gulp中使用
// 使用 envify 的自定义模块来定制环境变量const envify = require('envify/custom');
browserify(browserifyOptions)
.transform(vueify)
.transform(
// 必填项,以处理 node_modules 里的文件 { global: true },
envify({ NODE_ENV: 'production' })
)
.bundle();
// 使用 envify 自定义模块指定环境变量const envify = require('envify/custom');
browserify: {
dist: {
options: {
// 该函数用来调整 grunt-browserify 的默认指令 configure: b => b
.transform('vueify')
.transform(
// 用来处理 `node_modules` 文件 { global: true },
envify({ NODE_ENV: 'production' })
)
.bundle()
}
}
}
2.3 Rollup
const replace = require('@rollup/plugin-replace');
rollup({
// ... plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify( 'production' )
})
]
}).then(...);
2.4 模板预编译
当使用 DOM 内模板或 JavaScript内的字符串模板时,模板会在运行时被编译为渲染函数,通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
如果使用 webpack,并且喜欢分离 JavaScript和模板文件,我们可以使用JavaScript渲染函数。
2.5 提取组件的 CSS
当使用单文件组件时,组件内的 CSS会以
查阅这个构建工具各自的文档来了解更多:
2.6 跟踪运行时错误
如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数(如果已设置),利用这个钩子函数来配合错误跟踪服务是个不错的主意,比如 Vue提供了官方集成。
3 服务端渲染
官方发布了一份完整的构建 Vue 服务端渲染(Server-Side Rendering,SSR)应用的指南,这份指南非常深入,适合已经熟悉 Vue、webpack和Node.js 开发的开发者阅读,可以直接参考 ssr.vuejs.org。
因为这个不是目前我所关注的重点,所以并没有单独开章深入探讨的动力,所以还是看官方指南吧,那个更详细。
3.1 Nuxt.js
从头搭建一个服务端渲染的应用是相当复杂的,不过 Vue有一个优秀的社区项目
Nuxt是一个基于 Vue生态的更高层的框架,为开发服务端渲染的 Vue应用提供了极其便利的开发体验,也可以用它来做为静态站生成器;推荐尝试。
3.2 Quasar Framework SSR + PWA
PWA互通的) SSR应用,让开发者的想法的设计和构建变得轻而易举。
我们可以在服务端挑选执行超过上百款遵循“Material Design 2.0”的组件,并在浏览器端可用;甚至可以管理网站的 标签。
Quasar是一个基于 Node.js 和 webpack的开发环境,它可以通过一套代码完成 SPA(Single Page Application,单页应用)、PWA(Progressive web apps,渐进式 Web应用)、SSR、Electron、Capacitor和 Cordova应用的快速开发。这个部分更像是一个介绍,不够对于的确有需求的开发者来说,有个介绍就足够了,因为细节内容和使用都是必需通过查看官方手册进行学习的,好在 Vue 相关的用户手册中文化都做得很好,不过还是多嘴说一句,学会看英文文档,对于其他工具、框架、库、语言的上手和熟悉,都是非常非常必要的。
4 安全(官方信息,纯搬运)
4.1 报告安全漏洞
当 Vue官方收到一个安全漏洞报告,将给予其最高优先级,并由全职贡献者停下手中的工作处理此事。如发现任何安全漏洞,请邮件给 security@vuejs.org。
虽然发现新安全漏洞是比较罕见的事,我们仍推荐始终使用最新版本的 Vue及其官方的周边库,以确保应用尽可能安全。
4.2 第一原则:永远不要使用不可信任的模板
在使用 Vue 的时候最基本的安全规则是永远不要将不可信任的内容作为模板内容使用。
这样做等价于允许在应用程序中执行任意的 JavaScript——甚至更糟的是如果在服务端渲染的话可能导致服务器被攻破,举个例子:
new Vue({
el: '#app',
template: `
Vue的模板是被编译为 JavaScript的,而其中的表达式会作为渲染流程的一部分执行。尽管该表达式是在一个特定的渲染上下文中进行运算的。
考虑到潜在的全局运行环境的复杂性,作为类似 Vue的框架,想要完全让代码远离潜在的恶意代码执行而不导致性能问题,是不切实际的;最直接的回避这类问题的方式就是确保 Vue模板的内容始终是可信的且完全由开发者掌控。
4.3 Vue 的安全措施
(1)HTML 内容
不论使用模板还是渲染函数,内容都会被自动转义,也就是说对于这份模板:
{{ userProvidedString }}
如果 userProvidedString包含了:
''
则它会被转义成为如下 HTML:
<script>alert("hi")</script>
因此避免了脚本注入;该转义通过诸如 textContent的浏览器原生的 API完成,所以除非浏览器本身存在安全漏洞,否则不会存在安全漏洞。
(2)HTML 属性绑定
同样地,动态 HTML 属性绑定也会自动被转义,也就是说对于这份模板:
hello
如果 userProvidedString包含了:
'" οnclick="alert(\'hi\')'
则它会被转义成为如下 HTML:
" οnclick="alert('hi')
因此避免了通过闭合 title属性而注入新的任意 HTML;该转义通过诸如 setAttribute的浏览器原生的 API完成,所以除非浏览器本身存在安全漏洞,否则不会存在安全漏洞。
4.4 潜在危险
在任何 web 应用中,允许未过滤的用户提供的内容成为 HTML、CSS或 JavaScript都有潜在的危险,因此应当尽可能避免;尽管如此,有些情况下的风险是可接受的。
例如,类似 CodePen和 JSFiddle这样的服务允许用户提供的内容直接被执行,但这是预期行为,且在 iframe中以某种程度被隔离在沙箱中;当一些重要功能不可避免地依赖引入一些安全漏洞,开发者 / 团队需要自行在该功能的重要性和漏洞带来的最坏场景间进行权衡。
(1)注入 HTML
如我们之前看到的,Vue 会自动转义 HTML内容,以避免向应用意外注入可执行的 HTML。
然而,某些情况下我们清楚这些 HTML 是安全的,这时可以显式地渲染 HTML内容:使用模板:
h('div', {
domProps: {
innerHTML: this.userProvidedHtml
}
})使用基于 JSX的渲染函数:
除此之外,允许用户撰写其自己的 Vue模板会带来类似的危险。
(2)注入 URL
在类似这样的 URL 中:
click me
如果没有对该 URL进行“过滤”以防止通过 javascript: 来执行 JavaScript,则会有潜在的安全问题;有一些库如只要是在前端进行 URL过滤,那么就已经有安全问题了:用户提供的 URL永远需要通过后端在入库之前进行过滤,然后这个问题就会在每个客户端连接该 API时被阻止,包括原生移动应用。
还要注意,甚至对于被过滤过的 URL,Vue仍无法保证它们会跳转到安全的目的地。
(3)注入样式
来看这个示例:
:href="sanitizedUrl"
:style="userProvidedStyles"
>
click me
让我们假设 sanitizedUrl已经被过滤过了,所以这已经是一个完全真实的 URL且没有 JavaScript;但通过 userProvidedStyles,恶意用户仍可以提供 CSS来进行“点击诈骗”,例如:将链接的样式设置为一个透明的方框覆盖在“登录”按钮之上,然后再把
我们可以想象到,允许用户为一个
为了确保用户完全远离点击诈骗,我们推荐只允许在一个 iframe沙盒内进行 CSS的完全控制;或让用户通过一个样式绑定来控制,我们推荐使用其对象语法且只允许用户提供特定的可以安全控制的属性值。
例如:
:href="sanitizedUrl"
:style="{color: userProvidedColor,background: userProvidedBackground}"
>
click me
(4)注入 JavaScript
我们坚决不鼓励使用 Vue 渲染
每个 HTML元素都有接受 JavaScript字符串作为其值的 HTML 属性,如 onclick、onfocus和 onmouseenter等等;将用户提供的 JavaScript 绑定到它们任意当中都是一个潜在的安全风险,因此应该避免。请注意,永远不要认为用户提供的 JavaScript是 100% 安全的,除非它是在一个 iframe沙盒里或者应用中只有编写该 JavaScript的用户可以接触到它。
有的时候我们会收到在 Vue模板中可以产生跨站脚本攻击(XSS)的安全漏洞报告,一般情况下,我们不会将这样的案例视为真正的安全漏洞,因为从以下两个可能允许 XSS的场景看,不存在可行的办法来保护开发者:开发者显式地要求 Vue将用户提供的、未经过滤的内容作为 Vue模板进行渲染:这是无法避免的不安全,Vue没有办法知道其源头
开发者向 Vue挂载包含服务端渲染或用户提供的内容的 HTML的整个页面:这实质上和问题 1 是相同的,但是有的时候开发者可能没有意识到;这会使得攻击者提供作为普通 HTML安全但对于 Vue模板不安全的 HTML以导致安全漏洞,最佳实践是永远不要向 Vue挂载可能包含服务端渲染或用户提供的内容。
4.5 最佳实践
通用的规则是只要允许执行未过滤的用户提供的内容 (不论作为 HTML、JavaScript甚至 CSS),就可能令自己处于被攻击的境地;这些建议实际上不论使用 Vue还是别的框架甚至不使用框架,都是成立的。
除了上述关于潜在危险的建议,也可以自行熟悉以下资料:
然后利用学到的知识,对那些包含了第三方组件或通过其它方式影响渲染到 DOM 的内容的依赖的源代码进行重新审查,以发现潜在的危险模式。
4.6 后端协作
HTTP 安全漏洞,诸如伪造跨站请求(CSRF/XSRF)和跨站脚本注入(XSSI),都是后端重点关注的方向,因此并不是 Vue所担心的;尽管如此,和后端团队交流学习如何和他们的 API最好地进行交互,例如在表单提交时提交 CSRF token,永远是更好的选择。
4.7 服务端渲染
使用 SSR时存在额外的安全考量,因此官方的 SSR 文档中概括出了最佳实践以避免安全漏洞,也需要去遵循。