vue-cli 3.x
安装
Vue CLI 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。你可以使用 nvm 或 nvm-windows 在同一台电脑中管理多个 Node 版本。
npm install -g @vue/cli
# OR
yarn global add @vue/cli
#查看版本
vue --version
创建项目
vue create hello-world
Please pick a preset #请选择一个预设值
default (babel, eslint) #默认值(babel,eslint)
Manually select features #手动选择要素
Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
#检查项目所需的功能:(按<space>选择,<a>切换全部,<i>反转选择)
插件
移动端的调试插件
npm install vconsole
在现有的项目中安装插件
#安装插件
vue add 插件名称
vue add router
vue add vuex
vue add cube-ui
vue.config.js配置
#lintOnSave:false,
//关闭eslint代码检查
#publicPath
Type: string
Default: '/'
//部署应用包时的基本 URL。用法和 webpack 本身的 output.publicPath 一致,但是 Vue CLI 在一些其他地方也需要用到这个值,所以请始终使用 publicPath 而不要直接修改 webpack 的 output.publicPath
#configureWebpack
Type: Object | Function
//如果这个值是一个对象,则会通过 webpack-merge 合并到最终的配置中。如果这个值是一个函数,则会接收被解析的配置作为参数。该函数及可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本。
#devServer
Type: Object
//所有 webpack-dev-server 的选项都支持。注意:
//有些值像 host、port 和 https 可能会被命令行参数覆写。
//有些值像 publicPath 和 historyApiFallback 不应该被修改,因为它们需要和开发服务器的 publicPath 同步以保障正常的工作。
//模拟数据
module.exports={
configureWebpack:{
devServer:{
before(app) {
// app是express 实例
app.get('/goods', (req, res) => {
res.json([{ id: 1, text: 'abc' },{ id: 2, text: 'abc' }])
})
}
}
}
}
#devServer.proxy
Type: string | Object
//如果你的前端应用和后端 API 服务器没有运行在同一个主机上,你需要在开发环境下将 API 请求代理到 API 服务器。这个问题可以通过 vue.config.js 中的 devServer.proxy 选项来配置。
//devServer.proxy 可以是一个指向开发环境 API 服务器的字符串
module.exports = {
devServer: {
proxy: 'http://localhost:4000'
}
}
module.exports = {
devServer: {
proxy: {
'/api' :{
target: 'http://172.21.51.75:8099',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': '' // 重写路径
}
}
}
}
}
#productionSourceMap
Type: boolean
Default: true
//如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建
第三方插件
#安装sass预处理器
cnpm install sass-loader node-sass vue-style-loader --D
#安装stylus预处理
cnpm install stylus stylus-loader --D
#安装支持pug依赖
cnpm install pug pug-loader pug-filters -D
#安装支持jade依赖
cnpm install jade jade-loader -D
#安装vue-lazyload-----图片懒加载
cnpm install vue-lazyload -S
#安装vue-infinite-scroll -----鼠标滚动加载
cnpm install vue-infinite-scroll -S
#安装vue-currency-filter -----货币过滤器
cnpm install vue-currency-filter -S
#安装node-crypto-----加密插件
cnpm i node-crypto -S
#安装moment ----- 时间格式处理
cnpm install moment -S
使用:
import moment from 'moment'
filters: {
dateFrm:function(el){
//如果是毫秒数
let d = new Date(parseInt(el))
return moment(d).format("YYYY-MM-DD HH:mm:ss")
}
},
{{item.editTime | dateFrm}}
# 安装fastclick ----- 解决移动端点击300秒延迟
cnpm install fastclick -S
import fastclick from 'fastclick'
fastclick.attach(document.body)
# 安装babel-polyfill ----- ES6补丁转译 如promise
cnpm install babel-polyfill -D
import 'babel-polyfill'
# 安装better-scroll ----- 滚动场景需求的插件
npm install @better-scroll/core@next --save
https://better-scroll.github.io/docs/zh-CN/
# 安装create-keyframe-animation ----- 创建动画
cnpm install create-keyframe-animation -S
# 安装 js-base64 ----- base64转码
cnpm install js-base64 -S
# lyric-parser ----- 歌词解析
cnpm install lyric-parser -S
# 安装 good-storage ------ 本地存储sessionStorage和localStorage的封装
cnpm install good-storage -S
#一个用于表单异步校验的库(集成到了elementUI中)
cnpm i async-validator -S
vue-json-viewer
json格式视图展示
npm install vue-json-viewer --save
vue中使用GraphQL
安装
vue add apollo #cli3.0插件安装
#or
npm install --save vue-apollo graphql apollo-boost
#or
yarn add vue-apollo graphql apollo-boost
引入使用
main.js
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import ApolloClient from 'apollo-boost'
Vue.use(VueApollo)
const apolloClient = new ApolloClient({
uri: 'https://api.graphcms.com/simple/v1/awesomeTalksClone'
})
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
new Vue({
el: '#app',
apolloProvider,
render: h => h(App),
})
页面使用
<script>
//导入GraphQL相关api
import gql from 'graphql-tag'
//调用后台接口
const QueryListTag = gql`
query info {
info {
list {
username
content
date
}
link {
lname
lurl
}
weather {
wea
temp
}
}
}
`
export default {
apollo: {
info: {
query: QueryListTag //gql``
}
},
data(){
return {
// info表示服务端返回的数据
info:{
list:[],
link:[],
weather:{}
},
username:'',
content:''
}
},
filters: {
dateFrm:function(el){
let d = new Date(parseInt(el))
return moment(d).format("YYYY-MM-DD HH:mm:ss")
}
},
methods:{
send: function() {
// console.log(this.username, this.content)
// 把表单的数据通过接口提交到服务器
this.$apollo.mutate({
mutation: gql`
mutation createComment($commentInput: CommentInput) {
createComment(commentInput: $commentInput) {
username
content
}
}
`,
//添加表单数据
variables: {
commentInput: {
username: this.username,
content: this.content
}
},
//刷新页面
refetchQueries: [{
query: QueryListTag
}]
})
}
}
}
</script>
常用实例
公共变量
#vue-cli 3.x 写在public里面
(function () {
window.API = {
'CESHI_API_HOST': '172.168.1.1:8080'
}
Object.freeze(window.API)
Object.defineProperty(window, 'API', {
configurable: false,
writable: false
})
})()
#vue-cli 2.x 写在static里面
#都要在index.html里面引用
代理配置
proxyTable: {
"/goods":{
target:'http://localhost:3000'
},
"/goods/**":{
target:'http://localhost:3000'
},
"/users/**":{
target:'http://localhost:3000'
}
},
配置多页面
/**
* 方案一
* 生成的文件为
* -dist
* - img
* - index
* - index.js
* - index.css
* - index.html
* - page1
* - index.js
* - index.css
* - index.html
* - page2
* - index.js
* - index.css
* - index.html
*/
/* const path = require("path");
const glob = require("glob");
const fs = require("fs");
const config = {
entry: "main.js",
html: "index.html",
pattern: ["src/pages/*"]
};
const genPages = () => {
const pages = {};
const pageEntries = config.pattern.map(e => {
const matches = glob.sync(path.resolve(__dirname, e));
return matches.filter(match => fs.existsSync(`${match}/${config.entry}`));
});
Array.prototype.concat.apply([], pageEntries).forEach(dir => {
const filename = dir.split('pages/')[1];
const pathName = 'src' + dir.split('src')[1]
pages[filename] = {
entry: `${pathName}/${config.entry}`,
template: `${pathName}/${config.html}`,
filename: `${filename}/${config.html}`,
};
});
return pages;
};
const pages = genPages();
module.exports = {
productionSourceMap: false,
pages,
chainWebpack: config => {
Object.keys(pages).forEach(entryName => {
config.plugins.delete(`prefetch-${entryName}`);
});
if (process.env.NODE_ENV === "production") {
config.plugin("extract-css").tap(() => [
{
filename: "[name]/css/[name].[contenthash:8].css",
chunkFilename: "[name]/css/[name].[contenthash:8].css"
}
]);
}
},
configureWebpack: config => {
if (process.env.NODE_ENV === "production") {
config.output = {
path: path.join(__dirname, "./dist"),
filename: "[name]/js/[name].[contenthash:8].js",
publicPath: "/",
chunkFilename: "[name]/js/[name].[contenthash:8].js"
};
}
}
}; */
/**
* 方案二
* 生成的文件为
* -dist
* - css
* - index.xxx.css
* - page1.xxx.css
* - page2.xxx.css
* - img
* - index.html
* - page1.html
* - page2.html
*/
let path = require('path')
let glob = require('glob')
//配置pages多页面获取当前文件夹下的html和js
function getEntry(globPath) {
let entries = {},
basename, tmp, pathname;
glob.sync(globPath).forEach(function(entry) {
basename = path.basename(entry, path.extname(entry));
//console.log(entry)
tmp = entry.split('/').splice(-3);
pathname = basename; // 正确输出js和html的路径
// console.log(pathname)
entries[pathname] = {
entry: 'src/' + tmp[0] + '/' + tmp[1] +'/main.js',
template: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[2],
title: tmp[2],
filename: tmp[2]
};
});
return entries;
}
let pages = getEntry('./src/pages/**?/*.html');
//配置end
module.exports = {
publicPath:'',
outputDir: 'dist',
lintOnSave: false, //禁用eslint
productionSourceMap: false,
pages,
devServer: {
index: 'index.html', //默认启动serve 打开index页面
open: process.platform === 'darwin',
host: '',
port: 8080,
https: false,
hotOnly: false,
proxy: null, // 设置代理
before: app => {}
},
// css相关配置
css: {
extract: true, // 是否使用css分离插件 ExtractTextPlugin
sourceMap: false, // 开启 CSS source maps?
loaderOptions: {
less: {
javascriptEnabled: true
}
}, // css预设器配置项
modules: false // 启用 CSS modules for all css / pre-processor files.
},
// 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。
parallel: require('os').cpus().length > 1,
chainWebpack: config => {
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => {
// 修改它的选项...
options.limit = 100
return options
})
Object.keys(pages).forEach(entryName => {
config.plugins.delete(`prefetch-${entryName}`);
});
if(process.env.NODE_ENV === "production") {
config.plugin("extract-css").tap(() => [{
path: path.join(__dirname, "./dist"),
filename: "./css/[name].[contenthash:8].css"
}]);
}
},
configureWebpack: config => {
if(process.env.NODE_ENV === "production") {
config.output = {
path: path.join(__dirname, "./dist"),
publicPath: '',
filename: "./js/[name].[contenthash:8].js"
};
};
}
}
全局配置
#silent 取消 Vue 所有的日志与警告
Vue.config.silent = true
#optionMergeStrategies 自定义合并策略的选项
//合并策略选项分别接收在父实例和子实例上定义的该选项的值作为第一个和第二个参数,Vue 实例上下文被作为第三个参数传入
#devtools 配置是否允许 vue-devtools 检查代码。开发版本默认为 true,生产版本默认为 false。生产版本设为 true 可以启用检查。
// 务必在加载 Vue 之后,立即同步设置以下内容
Vue.config.devtools = true
#errorHandler 指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
// 只在 2.2.0+ 可用
}
#warnHandler 为 Vue 的运行时警告赋予一个自定义处理函数。注意这只会在开发者环境下生效,在生产环境下它会被忽略
Vue.config.warnHandler = function (msg, vm, trace) {
// `trace` 是组件的继承关系追踪
}
#ignoredElements 须使 Vue 忽略在 Vue 之外的自定义元素 (e.g. 使用了 Web Components APIs)。否则,它会假设你忘记注册全局组件或者拼错了组件名称,从而抛出一个关于 Unknown custom element 的警告
Vue.config.ignoredElements = [
'my-custom-web-component',
'another-web-component',
// 用一个 `RegExp` 忽略所有“ion-”开头的元素
// 仅在 2.5+ 支持
/^ion-/
]
#keyCodes 给 v-on 自定义键位别名
Vue.config.keyCodes = {
v: 86,
f1: 112,
// camelCase 不可用
mediaPlayPause: 179,
// 取而代之的是 kebab-case 且用双引号括起来
"media-play-pause": 179,
up: [38, 87]
}
#performance 设置为 true 以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器上
默认 false
#productionTip 设置为 false 以阻止 vue 在启动时生成生产提示
默认 true
全局API
Vue.filter
#注册或获取全局过滤器,main.js
Vue.filter( id, [definition] )
#单个使用
// 注册
Vue.filter('my-filter', function (value) {
// 返回处理后的值
})
// getter,返回已注册的过滤器
var myFilter = Vue.filter('my-filter')
多个filter
filter.js
const levelHander = function(val){
return val + 10
}
export default{
levelHander
}
main.js
import filters from './common/js/filter';
Object.keys(filters).forEach(key=>{
Vue.filter(key,filters[key])
})
index.vue
<span>{{detailData.fatherLevel | levelHander}}</span>
Vue.component
注册或获取全局组件。注册还会自动使用给定的
id设置组件的名称
globalCompontent.js
import upButton from './../../components/button';
function plugin(Vue) {
if (plugin.installed) {
return;
}
Vue.component('upButton',upButton)
}
export default plugin;
main.js
import GLCompontent from './common/js/globalCompontent';
Vue.use(GLCompontent)
app.vue
<up-button></up-button>
Vue.extend
#返回的是一个“扩展实例构造器”。
Vue.nextTick
#在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
Vue.set
#设置对象的属性。如果对象是响应式的,确保属性被创建后也是响应式的,同时触发视图更新。这个方法主要用于避开 Vue 不能检测属性被添加的限制。
Vue.delete
#删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。
Vue.directive
#注册或获取全局指令。
Vue.component
#注册或获取全局组件。注册还会自动使用给定的id设置组件的名称.
Vue.use(plugin)
#使用插件
Vue.mixin( mixin )
#全局注册一个混入,不推荐在应用代码中使用。
Vue.compile( template )
#在 render 函数中编译模板字符串。只在独立构建时有效。
Vue.version
#提供字符串形式的 Vue 安装版本号。
数据
props
props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值
// 简单语法
Vue.component('props-demo-simple', {
props: ['size', 'myMessage']
})
// 对象语法,提供验证
Vue.component('props-demo-advanced', {
props: {
// 检测类型
height: Number,
// 检测类型 + 其他验证
age: {
type: Number,
default: 0,
required: true,
validator: function (value) {
return value >= 0
}
}
}
})
watch
一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用
$watch(),遍历 watch 对象的每一个属性
#普通监听
watch: {
cart(newValue){
localStorage.setItem("cart", JSON.stringify(newValue));
}
}
#深度监听
watch: {
cart: {
deep: true,
handler(newValue) {
localStorage.setItem("cart", JSON.stringify(newValue));
}
}
}
DOM
render
类型:(createElement: () => VNode) => VNode
//字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。
//如果组件是一个函数组件,渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息。
//作用 返回要渲染的函数
render(createElement){
return createElement('table',{attrs:{border:1}})
}
生命周期钩子
beforeCreate //组件实例刚被创建,组件属性计算之前,如data属性等。
created //组件实例创建完成,属性已绑定,但DOM还未生成,$el属性还不存在。
befoteMount //模板编译/挂载之前
mounted //模板编译/挂载之后(不保证组件已在document中)
beforeUpdate //组件更新之前
updated //组件更新之后
activated //for keep-alive,组件被激活时调用
deactivated //for keep-alive,组件被移除时调用
beforeDestroy //组件销毁前调用
destroyed //组件销毁后调用
errorCaptured //当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
使用场景:
beforecreate //举个栗子:可以在这加个loading事件
created //在这结束loading,还做一些初始化,实现函数自执行
mounted //在这发起后端请求,拿回数据,配合路由钩子做一些事情
beforeDestory //你确认删除XX吗?
destoryed //当前组件已被删除,清空相关内容
资源
组合
provide
provide选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持Symbol和Reflect.ownKeys的环境下可工作。
#爷爷组件
<template>
<div class="test">
<son prop="data"></son>
</div>
</template>
<script>
export default {
name: 'Garndfather',
provide: {
return {
form:this //传递实例
}
}
}
inject
#孙子组件
<template>
<div>
{{name}}
</div>
</template>
<script>
export default {
name: 'Grandson',
inject: ["form"]
}
</script>
#执行校验规则
methods: {
validate() {
return new Promise(resolve => {
// 校验规则制定
const descriptor = { [this.prop]: this.form.rules[this.prop] };
// 创建校验器
const validator = new Validator(descriptor);
// 执行校验
validator.validate(
{ [this.prop]: this.form.model[this.prop] },
errors => {
if (errors) {
// 显示错误信息
this.errorMessage = errors[0].message;
resolve(false);
} else {
this.errorMessage = "";
resolve(true);
}
}
);
});
}
}
其他
实例属性
vm.$el
#Vue实例使用的根 DOM 元素,Vue实例管理的真实DOM元素
vm.$parent
#调用父实例
this.$parent.$emit("validate")
vm.$children
#当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源
this.$children
实例数据
实例事件
vm.$emit
#派发事件
method(){
addCart(good){
this.vm.$emit('addCartChild', good);
}
}
vm.$on
#监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
created(){
this.vm.$on("addCart", good => this.addCartChild(good));
this.$on("validate",this.validate)
}
实例生命周期
vm.$mount
//如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。
//如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。
//这个方法返回实例自身,因而可以链式调用其它实例方法
import Vue from 'vue';
export default function(Component, props) {
const instance = new Vue({
render(h) {
return h(Component, {props})
}
}).$mount();
// 将生成dom元素追加至body
document.body.appendChild(instance.$el);
const comp = instance.$children[0];
comp.remove = () => {
// 从body中移除dom
document.body.removeChild(instance.$el);
instance.$destroy();
}
return comp;
}
vm.$destroy
//完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
//触发 beforeDestroy 和 destroyed 的钩子。
指令
修饰符
@click.修饰符
.stop - 调用 event.stopPropagation()。
.prevent - 调用 event.preventDefault()。
.capture - 添加事件侦听器时使用 capture 模式。
.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
.native - 监听组件根元素的原生事件。
.once - 只触发一次回调。
.left - (2.2.0) 只当点击鼠标左键时触发。
.right - (2.2.0) 只当点击鼠标右键时触发。
.middle - (2.2.0) 只当点击鼠标中键时触发。
.passive - (2.3.0) 以 { passive: true } 模式添加侦听器
@keyup.修饰符
.enter
.tab
.delete
.esc
.space
.up
.down
.left
.right
事件
@click-----鼠标点击事件
@keyup.enter.native-----键盘回车事件
@keyup.13-----键盘回车事件
@mouseenter-----鼠标移入事件
@mouseleave-----鼠标移出事件
特性
内置组件
slot(插槽)
name - string,用于命名插槽
<slot></slot>
组件化
组件的使用
#1.导入组件
import 组件名称 from "./../~"
#2.声明组件
export default{
components:{
组件名称
}
}
#3.标签使用
<组件名称></组件名称>
组件通信
声明全局总线
#声明全局总线,在main.js中
Vue.protottype.vm = new Vue();
父子通信
父组件传参给子组件(props)
父组件
<template>
<div>
<cart :title="name"></cart>
</div>
</template>
<script>
export default{
data(){
return {
name:"这个是父组件"
}
}
}
</script>
子组件
<template>
<div>
<div>{{title}}</div>
</div>
</template>
<script>
export default{
props:{
title:{
type:String,
default:""
}
}
}
</script>
父组件调子组件方法
父组件
第一种
<template>
<div>
<cart ref="cart"></cart>
</div>
</template>
<script>
export default{
methods:{
addCart(good){
this.$refs.cart.addCartChild(good)
}
}
}
</script>
第二种
使用vm.$emit 派发事件
子组件
第一种
<template>
<div>
<div>{{title}}</div>
</div>
</template>
<script>
export default{
methods:{
addCartChild(good){}
}
}
</script>
第二种
使用vm.$on监听派发事件
子组件调父组件方法
父组件
第一种
<template>
<div>
<cart @parent="onParent"></cart>
</div>
</template>
<script>
export default{
methods:{
onParent(param){}
}
}
</script>
子组件
第一种
<template>
<div>
<div @click="callParentMethod('abc')"></div>
</div>
</template>
<script>
export default{
methods:{
callParentMethod(param){
this.$emit("parent",param)
}
}
}
</script>
祖孙通信
$attrs 祖转孙
$listeners 孙传祖
兄弟通信
$emit(eventName,v1,v2)
$on(eventName,fun(v1,v2))
全局组件的实现
Vue的实现原理
三种双向数据绑定的方式
发布-订阅者模式(backbone.js)
-
一般通过pub、sub的方式来实现数据和视图的绑定,但是使用起来比较麻烦
-
发布-订阅者模式,也叫观察者模式
-
它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合
-
例子:微信公众号
- 订阅者:只需要订阅微信公众号
- 发布者(公众号):发布新文章的时候,推送给所有订阅者
-
优点:
- 解耦合
- 订阅者不用每次去查看公众号是否有新的文章
- 发布者不用关心谁订阅了它,只要给所有订阅者推送即可
脏值检查(angular.js)
angular.js是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,类似于通过定时器轮询检测数据是否发生了改变。
数据劫持
vue.js则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回掉。
vuejs不兼容IE8以下版本
vue的实现思路
-
实现一个Compile模板解析器,能够对模板中的指令和插值表达式进行解析,并赋予不同的操作
let fragment = document.createDocumentFragment(); //创建了一个存在于内存中的虚拟DOM树 -
实现一个Observer数据监听器,能够对数据对象的所有属性进行监听
Object.defineProperty() <div id="app"> <p>您好,<sapn id="name"></sapn></p> </div> <script> var obj={} Object.defineProperty(obj,"name",{ get(){ return document.querySelector("#name").innerHTML; }, set(nick){ document.querySelector("#name").innerHTML = nick } }) obj.name = "jerry"; </script> -
实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM
-
创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,也就是vue
vue的核心实现源码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>mini-vue</title>
</head>
<body>
<div id="app">
<p>{{msg}}</p>
<!-- 你好 -->
<p v-text="msg"></p>
<p v-html="msg"></p>
<p>{{car.brand}}</p>
<p v-text="car.color"></p>
<input type="text" v-model="msg">
<button v-on:click="clickFn">按钮</button>
</div>
<script src="./src/watcher.js"></script>
<script src="./src/observe.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/vue.js"></script>
<script>
let app = document.querySelector('#app')
const vm = new Vue({
//el:'#app',
el: app,
data:{
msg:'hello vue',
car:{
brand:'大众',
color:'blue'
}
},
methods: {
clickFn(){
console.log(this.$data.msg);
}
}
})
</script>
</body>
</html>
vue.js
//vue实例类
class Vue {
constructor(options = {}) {
//绑定属性
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
//监视data中的数据
new Observer(this.$data)
//把data中所有的数据代理到vm上
this.proxy(this.$data)
//把method中的数据代理到vm上
this.proxy(this.$methods)
if (this.$el) {
//把app模板和Vue实例传给Compile进行解析
new Compile(this.$el,this)
}
}
proxy(data){
Object.keys(data).forEach(key => {
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get(){
return data[key]
},
set(newVal){
if (data[key] === newVal) {
return;
}
data[key] = newVal
}
})
})
}
}
compile.js
/**
* compile对html进行解析
*/
class Compile {
/**
* 构造函数
* @param {app模板} el
* @param {vue实例} vm
*/
constructor(el, vm) {
this.el = typeof el === "string" ? document.querySelector(el) : el
this.vm = vm
//编译模板
if (this.el) {
//第一步:把传进来的真是DOM树放到虚拟DOM树中并返回树节点
// 1.在el中所有的子节点都放入到内存中,用虚拟DOM franment
let franment = this.node2franment(this.el)
//第二步:把虚拟DOM树节点传给编译函数,进行文本节点,元素节点解析
// 2.在内存中编译fragment
this.compile(franment)
// 3.把fragment一次性的添加到页面
this.el.appendChild(franment)
}
}
/* 核心方法 */
//把节点放到虚拟DOM中
node2franment(node) {
let franment = document.createDocumentFragment()
let childNodes = node.childNodes
Array.from(childNodes).forEach(node => {
franment.appendChild(node)
})
return franment;
}
//解析虚拟DOM中的节点
compile(franment) {
let childNodes = franment.childNodes
Array.from(childNodes).forEach(node => {
//编译子节点
if (this.isElementNode(node)) {
// 如果是元素需要解析指令
this.compileElement(node)
}
if (this.isTextNode(node)) {
// 如果是文本节点,需要解析表达式
this.compileText(node)
}
//判断当前节点还有子节点,需要递归的解析
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
//解析html标签
compileElement(node) {
let attribute = node.attributes
//遍历属性,拿到属性名和属性值
Array.from(attribute).forEach(attr => {
//判断属性名是不是指令
if (this.isDirective(attr.name)) {
let type = attr.name.slice(2)
//判断指令的类型
if (this.isEventDirective(type)) {
CompileUtil['eventHandler'](node, this.vm, type, attr.value)
} else {
CompileUtil[type] && CompileUtil[type](node, this.vm, attr.value)
}
}
})
}
//解析文本
compileText(node) {
CompileUtil.mustache(node, this.vm)
}
/* 工具方法 */
//是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
//是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
//判断是否是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
//判断是否是事件指令
isEventDirective(type) {
return type.split(':')[0] === 'on';
}
}
let CompileUtil = {
mustache(node, vm) {
//获取文本
let txt = node.textContent
//用正则表达式匹配双花括号
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
//替换花括号里的内容
node.textContent = txt.replace(reg, this.getVmValue(vm, RegExp.$1))
new Watcher(vm, RegExp.$1, newValue => {
node.textContent = txt.replace(reg, newValue)
})
}
},
//解析v-text
text(node, vm, expr) {
node.textContent = this.getVmValue(vm, expr)
new Watcher(vm, expr, newValue => {
node.textContent = newValue
})
},
//解析v-html
html(node, vm, expr) {
node.innerHTML = this.getVmValue(vm, expr)
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue
})
},
//解析v-model
model(node, vm, expr) {
let _this = this
node.value = this.getVmValue(vm, expr)
node.addEventListener('input', function() {
_this.setVmValue(vm,expr,this.value)
})
new Watcher(vm, expr, newValue => {
node.value = newValue
})
},
//解析事件
eventHandler(node, vm, type, expr) {
let eventType = type.split(":")[1]
let fn = vm.$methods && vm.$methods[expr]
if (eventType && fn) {
node.addEventListener(eventType, vm.$methods[expr].bind(vm))
}
},
//获取vm中的数据
getVmValue(vm, expr) {
let data = vm.$data
expr.split(".").forEach(key => {
data = data[key]
})
return data
},
setVmValue(vm,expr,value){
let data = vm.$data
let arr = expr.split(".")
arr.forEach((key,index)=>{
if (index < arr.length -1 ) {
data = data[key]
}else{
data[key] = value
}
})
}
}
observe.js
/**
* observer用于给data中所有的数据加上getter和setter,
* 方便获取或设置data中数据的时候,实现我们的逻辑
*/
class Observer {
constructor(data){
this.data = data
this.walk(this.data)
}
/* 核心方法 */
//遍历data中所有的数据,都添加上getter和setter
walk(data){
if (!data || typeof data != 'object') {
return;
}
Object.keys(data).forEach(key => {
//给data对象的key添加getter和setter
this.defineReactive(data,key,data[key])
//递归遍历data里的数据
this.walk(data[key])
})
}
//定义响应式数据(数据劫持)
// data中的每一个对象都应该维护一个dep对象
//dep保存了所有的订阅了该数据的订阅者
defineReactive(obj,key,value){
let _this = this
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
// 如果Dep.target中有watcher对象,存储到订阅者数组中
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue){
if (value === newValue) {
return;
}
value = newValue
//如果newValue也是一个对象,也应该对他进行劫持
_this.walk(newValue)
// 发布通知,让所有的订阅者更新内容
dep.notify()
}
})
}
}
watcher.js
/**
* watcher模块负责把compile和observe关联起来
*/
class Watcher {
/**
*
* @param {当前的vue实例} vm
* @param {data中数据的名字} expr
* @param {数据变化的回调} cb
*/
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb
// this表示的就是新创建的watcher对象
// 存储到Dep的target属性上
Dep.target = this
this.oldValue = this.getVmValue(this.vm,this.expr)
//清空Dep
Dep.target = null
}
//对外暴露一个方法,更新页面数据
update(){
// 对比expr 是否发生了变化,如果发生了变化需要调用cb
let oldValue = this.oldValue
let newValue = this.getVmValue(this.vm,this.expr)
if (oldValue != newValue) {
this.cb(newValue,oldValue)
}
}
//获取vm中的数据
getVmValue(vm,expr){
let data = vm.$data
expr.split(".").forEach(key =>{
data = data[key]
})
return data
}
}
/**
* dep类用于管理所有的订阅者和通知这些订阅者
*/
class Dep{
constructor(){
//用于管理订阅者
this.subs = []
}
//添加订阅者
addSub(watcher){
this.subs.push(watcher)
}
//通知
notify(){
// 通知所有的订阅者,调用watcher的update方法
this.subs.forEach(sub => {
sub.update()
})
}
}
vue的工作机制
-
初始化
调用Vue原型上的
_init()进行初始化,会初始化vue的生命周期,props,data,methods,computed,watch,最重要的是利用Object.definedPropty()对data对象里面的属性设置setter和getter函数,也就是来实现响应式和依赖收集 -
挂载组件
调用$mount挂载组件
-
编译
编译三部曲,
parse(解析)、optimize(标记静态节点做优化)、generate(转成字符串) 3.1 parse:利用正则将模板转换成抽象语法树(AST); 3.2 optimize: 标记静态节点,以后update的时候,diff算法可以跳过静态节点 3.3 generate:将抽象语法树(AST)转成字符串,供render去渲染DOM经过以上步骤,就可以得到render funciton
-
响应式
响应式是vue中我认为最核心的部分,利用
Object.definedPropty设置data所返回的对象后,在进行render function被渲染的时候,会对data对象进行数据读取,会触发getter函数,从而把data里面的属性进行依赖收集,依赖收集的目的是将这些属性放到观察者(Watcher)的观察队列中,一旦我们对data里面的属性进行修改时,就会触发setter函数,setter告诉观察者数据变化,需要重新渲染视图,观察者调用update来更新视图 -
虚拟DOM
render funtion 会被转换成虚拟DOM,虚拟DOM实际上就是一个js对象,从顶层DOM层层描述DOM,有tag, children, isStatic, isComment等等许多属性来做DOM描述
-
更新视图
当数据发生变化时候,会经历
setter=>Watcher=>update这些步骤,那么最终是怎么更新视图的呢? 在update的时候,会执行patch,将新旧VNode传进去,通过diff算法算出差异,局部更新视图,做到最优化。

依赖收集与追踪
编译compile

vue-router
$router
this.$router是VueRouter的实例方法,当导航到不同url,可以使用this.$router.push方法,这个方法则会向history里面添加一条记录,当点击浏览器回退按钮或者this.$router.back()就会回退之前的url。
$route
this.$route相当于当前激活的路由对象,包含当前url解析得到的数据,可以从对象里获取一些数据,如name,path等。
模式
#history
#hash
动态路由匹配
嵌套路由
编程式的导航
命名路由
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
命名视图
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
重定向和别名
路由组件传参
#$route
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User }
]
})
#props
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})
路由守卫
全局前置守卫
const router = new VueRouter({ ... })
//每次路由激活之前都会执行回调函数
//to 即将要进入的目标 路由对象
//from 当前导航正要离开的路由
//next 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
router.beforeEach((to, from, next) => {
// ...
})
路由独享的守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内的守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
路由元信息
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
router的实现原理
class VueRouter{
connstructor(Vue,options){
this.$options = options
this.routeMap = {}
this.app = new Vue({
data:{
current:'#/'
}
})
this.init()
this.createRouteMap(this.$options)
this.initComponent(Vue)
}
//初始化hashchange
init(){
window.addEventListener('load',this.onHashChange.bind(this),false)
window.addEventListener('hashchange',this.onHashChange.bind(this),false)
}
createRouteMap(options){
options.routes.forEach(item=>{
this.routeMap[item.path] = item.component
})
}
//注册组件
initComponent(Vue){
Vue.component('router-link',{
props:{
to:String
}
render:function(h){
//h <===> createElement
// <a :href="to"><slot></slot></a>
return h("a",{attrs:{href:this.to}},this.$slots.default)
}
})
const _this = this
Vue.component('router-view',{
render(h){
var component = _this.routeMap[_this.app.current]
return h(component)
}
})
}
//获取当前 hash 串
getHash(){
return window.location.hash.slice(1) || '/'
}
//设置当前路径
onHashChange(){
this.app.current = this.getHash()
}
}
axios
Interceptors(拦截器)
export default function(){
axios.interceptors.request.use(config=>{
const token = localStorage.getItem('token')
if(token){
config.headers.token = 'token'
}
return config;
})
}
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
适合多个组件没有关系的时候使用,可以共享数据
基本使用
mutation-types.js
export const ADD = 'ADD'
export const REDUCE = 'REDUCE'
store.js
import Vue from "vue"
import Vuex from "vuex"
import {ADD,REDUCE} from './mutation-types';
Vue.use(Vuex)
//创建状态管理对象
let store = new Vuex.Store({
#状态
//定义共享变量
state:{
count:0,
todo: [
{ id: 1, text: '睡觉', done: true },
{ id: 2, text: '吃饭', done: false },
{ id: 3, text: '打豆豆', done: true },
{ id: 4, text: '敲代码', done: false }
]
},
#变更状态,同步函数
//修改共享数据
mutations:{
increament:function(state,{num = 0}){
state.count += num
},
[ADD](state) {
state.count++
},
[REDUCE](state) {
state.count--
}
},
#派生状态
//派生属性
getters:{
getTodoById: (state) => {
return (id) => {
return state.todo.find(item => item.id === id)
}
}
level:function(state){
if(state.conut < 5){
return "青铜"
}else if(state.count < 10){
return "白银"
}else{
return "黄金"
}
}
}
#异步操作
//通过调用mutations,间接对状态属性产生影响,而不是直接变更状态
//action可以包含任意异步操作
actions:{
increamentAction:function(context){
context.commit("increament",1)
}
},
}),
export default store
页面调用
methods:{
test:function(){
//提交参数= 提交负荷 提交给mutations
this.$store.commit({
type:"increament",
num:3
})
//调用action中的方法
this.$store.dispatch("increamentAction")
}
}
//html中调用
{{$store.state.count}}
{{$store.getters.level}}
<p>{{getTodoById(2).text}}</p>
<button @click="add">增加按钮</button>
<button @click="reduce">减少按钮</button>
#映射方式
import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
computed:{
...mapState(['count'])
...mapGetters(['level','getTodoById'])
}
methods:{
...mapMutations(["increament"]),
...mapMutations({
add:'ADD',
reduce:'REDUCE'
})
...mapActions(["increamentAction"])
onFun(){
this.increament()
this.increamentAction()
}
}
模块化使用
index.js
import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex)
import moduleA from "./modules/moduleA"
import getters from "./getters"
let store = new Vuex.Store({
getters:getters,
modules:{
counting:moduleA
}
})
export default store
moduleA.js
export default {
//定义共享变量
state:{
count:1
},
//修改共享数据
mutations:{
increament:function(state,step){
state.count += step
}
},
//通过调用mutations,间接对状态属性产生影响
actions:{
increamentAction:function(context){
context.commit("increament",1)
}
}
}
getters.js
export default{
level:function(state){
if(state.counting.conut < 5){
return "青铜"
}else if(state.counting.count < 10){
return "白银"
}else{
return "黄金"
}
}
}
页面调用
{{$store.state.counting.count}}
{{$store.getters.level}}
vuex的实现原理
class KStore{
constructor(options){
this.state = options.state;
this.mutations = options.mutations;
this.actions = options.actions;
//借用vue本身的数据相应式机制
this.vm = new Vue({
data:{
state:this.state
}
})
}
commit(type,payload){
const mutation = this.mutations[type];
mutation(this.state,payload)
}
dispatch(type,payload){
const action = this.actions[type];
const ctx = {
commit:this.commit.bind(this),
state:this.state,
dispatch:this.dispatch.bind(this)
}
return action(ctx,payload)
}
}
vuex实例
index.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations.js'
import actions from './actions.js'
import getters from './getters.js'
Vue.use(Vuex);
const state = {
num:5
}
export default new Vuex.Store({
state,
getters,
actions,
mutations
})
mutations.js
import {GET_HOT_SHOPS} from './mutation-types.js'
[GET_HOT_SHOPS](state,list){
state.hotShops = state.hotShops.concat(list);
},
mutations-types.js
export const GET_HOT_SHOPS = 'GET_HOT_SHOPS'
actions.js
import axios from 'axios'
import * as types from './mutation-types.js'
export default {
getHotShops({commit,state}){
state.busy = true;
commit(types.IS_SHOW_LOADING_TIPS,true);
axios.get('/mock/home/hot_shop.json').then((response)=>{
commit(types.IS_SHOW_LOADING_TIPS,false);
let result = response.data.list.slice(state.num-5,state.num);
if(result.length !== 0){
commit(types.GET_HOT_SHOPS,result);
state.busy = false;
state.num+=5;
}else{
commit(types.IS_SHOW_LOADED_TIPS,true);
}
})
},
}
getters.js
export default{
hotShops: state=>state.hotShops,
}
保存刷新后没有数据
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from "./mutations"
import getters from './getters'
import actions from './actions'
//import state from './state'
Vue.use(Vuex)
export default new Vuex.Store({
state: sessionStorage.getItem('state') ? JSON.parse(sessionStorage.getItem('state')) : {
iscollapse: false,
IsFactory: '',
companys: []
},
mutations,
getters,
actions,
})
APP.vue
<script>
export default {
data() {
return {};
},
mounted() {
window.addEventListener("unload", this.saveState);
},
methods: {
saveState() {
sessionStorage.setItem("state", JSON.stringify(this.$store.state));
},
},
};
Element-UI
单选
<el-form-item label="">
<el-radio-group v-model="form.value">
<el-radio @click.native.prevent="onIsNamesakeCard(item.value)"></el-radio>
</el-radio-group>
</el-form-item>
#单选可以取消函数
onIsNamesakeCard(val){
this.form.value = val == this.form.value ? '' : val
}