# 生命周期
Vue的生命周期包括两种:
- 组件的生命周期
- 路由的生命周期
不同的业务场景下合理使用对应的钩子函数:
# created
异步请求放在created钩子中,此时this已经指向vue实例,内容也能尽早地到达。
# mounted
如果你想在当前组件中加载第三方库,可以在mounted钩子中动态生成src标签,因为在mounted阶段,组件的el和template已经挂载完成。
# activated
activated钩子,当组件被缓存时触发。
一个常见的场景是:
- 从列表页进入详情页
- 返回列表页
我们希望在第2步的时候停留在上一次的位置,且不重新请求数据。这时可以使用keep-alive来缓存组件。
// keep-alive是vue官方提供的虚拟组件,包在router-view外层就可以了
<keep-alive>
<router-view></router-view>
</keep-alive>
组件被缓存后再次进入不会执行created钩子,页面的状态也会被保留。这样就能实现我们上面的需求。
另一个场景:
- 从列表页进入详情页
- 返回列表页
- 再次进入详情页
在这种场景下,当详情页被缓存后。第3步如果是相同的详情页不会再此请求数据,但是如果是不同的详情页,此时由于created钩子不会执行,那如何重新请求数据呢?你可以这样做:
watch:{
'$route.query.id' (){
// 如果id变了,重新请求数据
}
}
# beforeDestroy
组件被销毁前调用,你可以在这个钩子里清理setTimeout 和 setInterval。
# 路由守卫
合理使用路由守卫,能减少许多不必要的业务逻辑。
# beforeRouteUpdate
beforeRouteUpdate是组件内的,在上一个例子中,我们观察id是否变化来决定是否重新去请求数据,你也可以这样写:
// 当 query 和 params 变化时也会触发这个路由钩子
beforeRouteUpdate(){
// 重新请求数据
}
# router.beforeEach
这是一个全局的路由守卫,在所有的路由执行前触发,一般写在router.js文件中。
我们来看一个业务场景:当用户点击评论按钮时,如果没有登录则跳转到登录页,如果登录则跳转到评论编辑页。
拿到这个需求时你可能已经有思路了:
<btn @click="comment">评论</btn>
comment(){
let isLogin = sessionStorage.getItem("isLogin")
if(!isLogin){
return this.$router.push('login')
}
this.$router.push("comment-edit")
}
如果类似的逻辑不止一处呢,在进入个人中心,在发布文章时都需要进行类似的判断应该如何处理:
- 维护一个需要路由数组,将需要登录才可以进的页面路由放入其中,或者在router的meta中定义一个字段用于判断。
- 当用户没有登录且要跳到需要登录的页面时,跳转到login页面。
let router = new Router()
router.beforeEach((to, from, next) => {
const needLoginRoute = [
'comment',
'person',
....
]
let isLogin = sessionStorage.getItem("isLogin")
let _isToLogin = needLoginRoute.includes(to.path) && isLogin
if(_isToLogin){
next({path : 'login'})
}else{
next()
}
})
# router.afterEach
全局守卫,在路由结束后执行。来看一个使用场景:
- 列表页进入详情页
- 详情页返回列表页
现在我们希望,在组件缓存的情况下返回列表页的时候不重新请求数据但是滑动到顶部。
let router = new Router()
router.afterEach((to, from) => {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
})
# beforeRouteEnter
路由独享守卫,你可以在路由配置上直接定义也可以写在组件内部。来看一个权限验证的场景:用户点击进入超级Vip商城时,如果不是vip且vip等级小于8,则提示没有进入权限。
你的思路可能是:
- 当前页面下请求用户身份信息
- 判断是否是vip且vip等级大于等于8
- 如果是则进入,不是则给出提示
如果这个入口有5个,你是不是得在5个不同的页面重复进行上面的操作。而且,哪天需求或者接口改了,再做修改就非常麻烦了。所以你可以尝试下面的方法:
// 在超级Vip页面做处理,无需修改入口处的逻辑
beforeRouteEnter (to, from, next) {
if(isVip && vipLevel >= 8){
next()
}else{
next(false)
}
}
灵活运用组件和路由钩子,能极大提升项目的可维护性
# 组件通信
# props && emit
props 和 emit 用于父子组件之间通信,也可以使用.sync的语法糖简化书写。
# eventBus
兄弟组件之间事件传递。假设,父组件为A,它下面有两个子组件B,C。一般情况下,B跟C要通信则只能将事件传递至A再由A发送到对应的组件。在组件层级过深的情况下,这种事件传递方式将极其难以维护。
我们可以通过一个新的Vue实例来改造它:
// EventBus.js
import Vue form 'vue'
export const EventBus = new Vue()
// B
import EventBus
sendToC(){
EventBus.$emit("eventBus-b")
}
// C
import EventBus
created(){
EventBus.$on("eventBus-b",handler)
}
# $attrs & $listeners
$attrs 和 $listeners 一般用于封装第三方组件时做属性和事件传递。看一个封装element按钮组件的例子:
// my-btn
<div>
<el-btn v-bind="$attrs"
v-on="$listeners"/>
<slot/>
</el-btn>
</div>
<scipt>
export default {
props : {
myProps : {
type : String
default : ""
}
}
}
</script>
<my-btn myProps="something"
icon="el-icon-search"
round>确定</my-btn>
使用 my-btn
组件时,my-btn
中icon、round这些没有定义的props会通过 $attr
传递给 el-btn
。
# $parent和$children
分别是获得父组件和子组件的实例。
# vuex
vuex不多赘述,值得注意的是,复用性不高的不要使用 vuex,否则会使得项目的状态树过大而难以维护。
# mixins
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
根组件的mixins,可以让其它所有组件都访问到其内部的方法和状态。借此特性你可以将 vuex 中常用的方法到入其中,不用每次都使用 mapAction 、mapMutation 导入方法。
使用场景,假设有一个公共的header,不同的页面组件需要展示不同的title。
// mixins.js
import Vue from 'vue'
Vue.mixin({
methods : {
...mapMutation([
'changeTitle'
])
}
});
// page
created(){
this.changeTitle("page name")
}
除此之外,全局性的filter,工具函数也可以放入其中。但是切记,不要滥用mixin。
# 组件规划
这一章你将掌握,如何摆放组件来形成灵活的,高可维护的视图。
# 使用嵌套路由
假设现在要构建一个移动端应用的骨架。一个基础的骨架至少包含 header 和 bottomAction。并且有些页面可能是不需要bottomAction或者header的。
思路一:
- 创建 header 和 bottomAction
- 在需要的页面引入
这种思路,未免写起来太累。
思路二:
- 创建 header 和 bottomAction
- 在根组件引入,使用vuex管理header和 bottomAction的状态
<template>
<Header/>
<router-view/>
<BottomAction/>
</template>
# 使用动态组件
假设现在有一个注册页面,开始注册到完成一起有4个步骤。这四个步骤中有一个公共的步骤条,以及各自的输入框,如何规划组件?
思路一:
- 创建步骤条公共组件
- 为不同的步骤配置路由且引入步骤条
思路二:
- 创建一个注册组件,包含步骤条和动态组件
- 创建4个子组件
相同的界面,思路二相比思路一无需多次配置路由
<template>
<steps @click="changeChild" />
<component :is="currentChild" />
</template>
<script>
import child from 'child'
import child1 from 'child1'
import child2 from 'child2'
import child3 from 'child3'
export default {
data(){
return {
currentChild : "child"
}
},
methods:{
changeChild(){
// 根据不同的步骤,修改currentChild的值
}
}
}
</script>
# 组件粒度
如果你已经熟练掌握了组件之间通信,嵌套路由和动态组件。那么尽可能地减小组件的粒度,这将带来极高的可维护性。
物极必反,太小的组件粒度同样会使得逻辑难以理解,我们约定:
- 页面组件的代码行数最好不要超过500行
- UI组件的不要超过300行
你只要尽可能地遵守这一约定就可以了。
这一章默认脚手架版本为2.x。
# Vue-cli
# 文件结构
|-- build // 项目构建(webpack)相关代码
| |-- build.js // 生产环境构建代码
| |-- check-version.js // 检查node、npm等版本
| |-- utils.js // 构建工具相关
| |-- vue-loader.conf.js // webpack loader配置
| |-- webpack.base.conf.js // webpack基础配置
| |-- webpack.dev.conf.js // webpack开发环境配置,构建开发本地服务器
| |-- webpack.prod.conf.js // webpack生产环境配置
|-- config // 项目开发环境配置
| |-- dev.env.js // 开发环境变量
| |-- index.js // 项目一些配置变量
| |-- prod.env.js // 生产环境变量
| |-- test.env.js // 测试脚本的配置
|-- src // 源码目录
| |-- components // vue所有组件
| |-- router // vue的路由管理
| |-- App.vue // 页面入口文件
| |-- main.js // 程序入口文件,加载各种公共组件
|-- static // 静态文件,比如一些图片,json数据等
|-- test // 测试文件
| |-- e2e // e2e 测试
| |-- unit // 单元测试
|-- .babelrc // ES6语法编译配置
|-- .editorconfig // 定义代码格式
|-- .eslintignore // eslint检测代码忽略的文件(夹)
|-- .eslintrc.js // 定义eslint的plugins,extends,rules
|-- .gitignore // git上传需要忽略的文件格式
|-- .postcsssrc // postcss配置文件
|-- README.md // 项目说明,markdown文档
|-- index.html // 访问的页面
|-- package.json // 项目基本信息,包依赖信息等
# package.json
package.json文件是项目的配置文件,定义了项目的基本信息以及项目的相关包依赖,npm运行命令等。
在加载依赖时:
- --save 依赖会被记录到 dependencies 中,生产环境打包时会将依赖打包进生成的文件中。
- --save-dev 依赖会被记录到devDependencies中,生产环境不会将对应的依赖打包到生成的文件中。一般用于加载本地开发需要的库。
- 不加 --save 或者 --save-dev 时不会修改package.json,虽然依赖会下载到node_nodules中但是在运行npm install时不会加载该依赖。
# webpack.base.conf.js
基础的 webpack 配置文件主要根据模式定义了入口出口,以及处理 vue, babel等的各种模块,是最为基础的部分。其他模式的配置文件以此为基础通过 webpack-merge 合并。
# webpack.dev.conf.js
开发环境 webpack 配置文件在devServe中的proxyTable可以配置反向代理来实现开发环境的跨域请求。
# webpack.prod.conf.js
生产模式配置文件,相比于开发环境生产环境会压缩,合并,分离第三方库,Gzip,添加hash后缀等优化。
# build.js
项目编译入口
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
// ora,一个可以在终端显示spinner的插件
const ora = require('ora')
// rm,用于删除文件或文件夹的插件
const rm = require('rimraf')
const path = require('path')
// chalk,用于在控制台输出带颜色字体的插件
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start() // 开启loading动画
// 首先将整个dist文件夹以及里面的内容删除,以免遗留旧的没用的文件
// 删除完成后才开始webpack构建打包
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
// 执行webpack构建打包,完成之后在终端输出构建完成的相关信息或者输出报错信息并退出程序
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
# babel
babel6使用.babelrc 做为配置文件,到babel7改为使用 babel.config.js。babel6以上支持preset,无需再做复杂的配置。官方推荐使用babel-preset-env,它是一个能根据运行环境智能加载需要的插件和 polyfill 的 preset。
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}
你可以通过指定更高的浏览器版本来减少插件和 polyfill 的代码量,并且直接使用原生 ES6的新特性。例如,将浏览器设为 Chrome 较高的版本,Promise、Map、Set 等内建类均不会被 polyfill,而同时 class 等新语法也不会被 Babel 转译,转而使用 V8 自带的 ES6 Class。
不兼容低版本浏览器,打包出来的代码会更精简,运行速度也会更快,这是一个值得注意的优化的点。你可以通过修改 browsers
的值来指定代码需要兼容的运行环境。
# .editorconfig
root = true
[*] // 对所有文件应用下面的规则
charset = utf-8 // 编码规则用utf-8
indent_style = space // 缩进用空格
indent_size = 2 // 缩进数量为2个空格
end_of_line = lf // 换行符格式
insert_final_newline = true // 是否在文件的最后插入一个空行
trim_trailing_whitespace = true // 是否删除行尾的空格
我们从结果导向,来学习如何构建webpack配置。
# postcss
postcss 是一个用 JavaScript 工具和插件转换 CSS 代码的工具。你可以利用它做很多的事情,例如:使用 css 未来的特性、为 css 增加浏览器私有前缀,将 px 转换成 rem 等等。
在 webpack4 中使用 postcss 相关插件只需要下载依赖后,在 postcss.config.js
中配置参数就可以了。
# Webpack
# 别名
我们经常使用 ../
这种相对路径去寻找文件,在目录嵌套很深的情况下,看起来特别的不优雅。
module.exports = {
//...
resolve: {
alias: {
@: path.resolve(__dirname, 'src'),
~: path.resolve(__dirname, 'assets/img')
}
}
};
// old
import HelloWorld from '../../components/HelloWorld'
// new
import HelloWorld from '@/components/HelloWorld'
# 跨域处理
跨域一般表现在前端在调用后台接口时,浏览器出现的 403
的错误。这是由于浏览器的安全策略 同源策略
,不同域名、协议 、端口 、发送 异步请求
时浏览器会阻止这样的行为。
在开发环境中,你可以启动一个本地的代理服务器来规避同源策略,脚手架中默认会启动一个devserve 的本地服务器,通过修改 webpack
的配置来实现跨域请求(反向代理)。
devServer: {
proxy: {
"/api": {
target: "http://www.example.org",
changeOrigin: true,
ws: true,
pathRewrite: {
"^/api": ""
}
}
}
}
它的流程是:前端发起异步请求 -> 请求本地的devServe -> devServe请求远程服务器 -> 远程服务器响应请求,返回内容给devServe -> devServe将内容再传给网页
由于服务器之间的通信是没有同源策略限制的,所以我们能利用这种方式处理本地开发是跨域的问题。在实际生产环境你可以这样处理跨域:
- 找后端配置
cors
,cors是一种跨域资源共享策略,允许Web应用访问来自不同源服务器上的指定的资源。 - 使用http容器,常见的是nginx,做反向代理来处理跨域问题,与本地开发解决跨域的思路相同。
- JSONP,此方法只适用于get请求。
- 其它的还有
iframe
,domain
,postMessage
等方式根据实际的场景应用。
# 开发和生产环境的区分
在vue-cli2中,webpack的配置文件是这样的:
- webpack.base.conf.js
- webpack.dev.conf.js
- webpack.prod.conf.js
- webpack.test.conf.js
这分别对应开发、生产和测试环境的配置。其中 webpack.base.conf.js 是一些公共的配置项。我们使用 webpack-merge 把这些公共配置项和环境特定的配置项 merge 起来,成为一个完整的配置项。比如 webpack.dev.conf.js 中:
'use strict'
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const devWebpackConfig = merge(baseWebpackConfig, {
...
})
这三个环境不仅有一部分配置不同,更关键的是,每个配置中用 webpack.DefinePlugin 向代码注入了 NODE_ENV 这个环境变量。
这个变量在不同环境下有不同的值,比如 dev 环境下就是 development。这些环境变量的值是在 config 文件夹下的配置文件中定义的。Webpack 首先从配置文件中读取这个值,然后注入。比如这样:
build/webpack.dev.js
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env.js')
}),
]
config/dev.env.js
module.exports ={
NODE_ENV: '"development"'
}
至于不同环境下环境变量具体的值,比如开发环境是 development,生产环境是 production,其实是大家约定俗成的。
框架、库的作者,或者是我们的业务代码里,都会有一些根据环境做判断,执行不同逻辑的代码,比如这样:
if (process.env.NODE_ENV !== 'production') {
console.warn("error!")
}
# 代码分割
Code Splitting 一般需要做这些事情:
- 为 Vendor 单独打包(Vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存)
- 为 Manifest (Webpack 的 Runtime 代码)单独打包
- 为不同入口的公共业务代码打包(同理,也是为了缓存和加载速度)
- 为异步加载的代码打一个公共的包
Code Splitting 一般是通过配置 CommonsChunkPlugin 来完成的。一个典型的配置如下,分别为 vendor、manifest 和 vendor-async 配置了 CommonsChunkPlugin。
webpack4中使用SplitChunksPlugin来替代这个插件,配置更简洁
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
CommonsChunkPlugin 的特点就是配置比较难懂,大家的配置往往是复制过来的,这些代码基本上成了模板代码(boilerplate)。如果 Code Splitting 的要求简单倒好,如果有比较特殊的要求,比如把不同入口的 vendor 打不同的包,那就很难配置了。
# 缓存策略
缓存策略是这样的:给静态文件一个很长的缓存过期时间,比如一年。然后在给文件名里加上一个 hash,每次构建时,当文件内容改变时,文件名中的 hash 也会改变。
浏览器在根据文件名作为文件的标识,所以当 hash 改变时,浏览器就会重新加载这个文件。 Webpack 的 Output 选项中可以配置文件名的 hash,比如这样:
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
}
缓存策略依赖于文件名的hash值,为了实现增量发布对于不常变动的第三库打包需要用 chunkhash
,而项目本分的代码拆分的chunk则使用 contenthash
。
# 部署
无论前端的工程再复杂,最后打包出来的也只是一个html文件,和一些所需的静态资源。所谓的部署就是将打包出来的文件放到一个http容器中。你可以通过下面的方法部署:
- 将打包出来的dist文件夹放到静态服务器上,修改静态服务器的配置让它可以找到你的index.html。例如 gitpage、对象存储等都支持静态托管。
- 启动nginx,将dist里面的文件放到 nginx 的 html 文件夹里。
# postman-swagger
# P3必备技能
达到P3需要掌握以下技能,根据具体掌握情况分小岗级:
- 熟练使用常见的设计模式,输出高质量代码
- 熟练使用前端的常用类库,框架
- 有独立搭建,开发,优化,部署的能力
- 能读懂开源框架/类库源码
# 熟练使用常见的设计模式,输出高质量代码
常用设计模式包含:
- 单例模式
- 工厂模式
- 策略模式
- 代理模式
- 观察者模式
- 桥接模式
- 责任链模式
对于使用vue,react等工程化项目,需要掌握:
- 组件之间多种通信模式
- 状态管理
- 路由规划
- 利用自动化减少重复的工作
# 熟练使用前端的常用类库,框架
常用的类库包含:
- axios
- lodash
- UI框架
- mocha、 jest
- webpack,gulp,grunt
- babel
- typeScript
- sass,less
- postcss
- jquery
框架包含:
- vue
- react
- angular
- 小程序
- React Native,weex
- Next、Nuxt
- NodeJs
- express、koa、egg
# 有独立搭建,开发,优化,部署的能力
- 工程化项目可以由cli搭建,非工程化项目会使用webpack,gulp等搭建工作流。对于cli搭建的项目,清楚的了解项目中的配置。
- 对于一些常见的业务场景,例如多角色、多权限认证或者复杂的业务场景等能提供清晰的,高可维护的解决方案。
- 优化包含静态资源优化,网络优化,web安全,用户体验优化,性能监控和埋点:
- 静态资源优化,例如使用自动化工具对文件做压缩,合并,混淆,tree Shaknig,以及增量发布等等。其中包含大量工程化实践,不一一列举。
- 网络优化,例如使用cdn加速静态资源访问,多域名的场景下使用dns预解析,http2.0,https等。
- web安全,例如xss,csrf,反爬虫,流量劫持等
- 用户体验优化,例如资源预加载,离线缓存,骨架屏等。增强用户感知,减少资源响应时间,增加页面操作流畅度。
- 性能监控和埋点,能合理使用工具监控项目中存在的问题,例如js报错,响应时间,接口请求时间等。埋点则是通过收集用户产生的数据,指导团队更好地维护产品。
- 部署,前端输出的静态资源在部署上线后,能对页面空白,缓存不刷新等问题给出解决方案。并且对于不同的路由模式,渲染模式知道如何部署到服务器以及修改配置。
# 能读懂开源框架/类库源码
你需要掌握以下能力:
- 如何发布一个nmp包
- 如何编写一个webpack插件
- 如何编写一个命令行工具
- 熟练使用npm scritp