2018-11-10

Vue 2

v2 官页 v2 API

NPM VUE 脚手架

全局安装 vue-cli

npm view vue-cli //可以查看一下当前全局 vue-cli 的版本 npm install -g @vue/cli //更新vue-cli npm i -g @vue/cli-init //安装init

创建一个基于 webpack 模板的新项目

$ vue init webpack my-project

安装依赖, 走你~

$ cd my-project $ npm run dev

参考 - https://cn.vuejs.org/v2/guide/installation.html#%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7-CLI

App.vueindex.html, div 项目重复id的疑问

通过vue-cli创建vue项目时, 默认的渲染主页是public/index.html文件, 它里有一个id=“app”的div元素; 而在主组件 src/App.vue 的 template 里也有一个id=“app” 的根元素, 为何在渲染时两者不会冲突?

原因如下: 作为入口文件的src/main.js, 创建了应用的vue根实例; 根实例的el选项或者实例方法 $mount() 的参数, 提供了一个已经存在的DOM元素作为挂载Vue实例的挂载目标; 此处挂载元素是index.html中的id为’#app’的div元素;

根实例的render选项, 接收 createElement 方法作为第一个参数用来创建VNode; 而createElement又接收APP组件作为参数; render渲染函数生成的VNode将会替换掉挂载元素,App.vue组件的模板内容将会替换掉index.html中的挂载元素’#app’; 最终渲染出来的页面中的’#app’元素是App.vue组件中的;

注: rende渲染函数是字符串模板template的代替方案; 使用构建工具时, vue.js使用的是不包含编译器的运行时版, 而template一定要通过编译器编译成渲染函数, 所以就用render替代了template;

element-ui

main.js

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
 
Vue.use(ElementUI);
 
new Vue({
  el: '#app',
  render: h => h(App)
});

获取所有已注册的组件 const components = Object.keys(Vue.options.components) console.log(components)

奇葩的语法扫描插件 Eslint

什么都是错误, 怕是对错误有什么误解

./config/index.js

  • 找到 useEslint 禁用 Eslint Loader
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: false,
  • vue-cli 项目: 在 vue.config.js 文件中
module.exports = {
  lintOnSave: false, // 关闭 eslint 检查
}
  • 在项目的根目录下,可以找到一个名为”.eslintrc”或”.eslintrc.js”的配置文件。我们可以在其中找到需要关闭的规则,并设置为”off”来禁用它们。
{
  "rules": {
    "no-console": "off",
    "indent": ["error", 2]
  }
}

npm run dev 运行流程

1. npm run

其实执行了 package.json中的script 配置的脚本

package.json 的定义:

...
"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run e2e",
    "lint": "eslint --ext .js,.vue src test/e2e/specs",
    "build": "node build/build.js"
  }
...

2. webpack.dev.conf.js

导了一堆配置

'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
 
const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,
 
...

3. webpack.base.conf.js

其中在 webpack.base.conf.js 文件中设置项目入口: entry: main.js

webpack.base.conf.js

...
module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
...

静态资源的使用

assets 资源

放在 assets 目录的资源通常是可变, import导入变成一个变量访问, 一些小文件它会编码为base64

import title_img from '@/assets/comptrace/page-title-bg.png'
...
  data() {
    return {
      title_img
    };
  },
...
<div class="page-title" :style="'background-image: url('+title_img+')'">追溯查询</div>

在html svg <img src="@/assets/svgs/img.svg" alt=""> 亦可 在css 可以使用 background-image: url('~@/assets/system/login/bg_pt.svg'); 形式

注意它是相对与打包后的css 相对路径, 在build/utils.jsgenerateLoaders 函数加上publicPath: '../../'

“真正”的静态资源 对比而言, static/中的文件是完全不被 Webpack处理的, 它们被以相同的文件名直接被复制

你务必要使用绝对路径去引用它们, 其实是跟config.js的 build.assetspublicpathbuild.assetssubdirectory 相关的 [见下: 配置 conifg]

static 资源

放在 static 目录的资源, 完全的静态资源不会改变; 适合固定命名的资源, 或者分布式资源 <div class="page-title" :style="'background-image: url(/static/comptrace/page-title-bg.png)'">追溯查询</div>

CSS/JS 资源

js: import mui from '../../../static/mui.min.js'

css: @import "../../common/stylus/mixin";

打包踩坑指南

npm run build

入口是 package.json 定义的命令

"scripts": {
    "dev": "cross-env BABEL_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "build": "cross-env NODE_ENV=production env_config=prod node build/build.js",
    "lint": "eslint --ext .js,.vue src",
    "test": "npm run lint",
    "precommit": "lint-staged",
    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
  },

使用npm run script执行脚本的时候都会创建一个shell, 然后在shell中执行指定的脚本; webpack.prod.conf.js

配置 conifg

conifg/index.js

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),//打包输出目录
 
    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',//把所有的静态资源(包括约定的asset/等)打包到 dist下的 static文件夹下;
    assetsPublicPath: './',//如果代理不是/ 打包时必须配置二级路径(与路由 base配置保持一致) 
    ...
  }

assetsPublicPath

静态资源 js css 的加载路径等.

*您可以根据实际情况自行设置 *如果您打算在子路径下部署网站, 则需要进行设置, *例如GitHub页面; 如果您打算将网站部署到 https://foo.github.io/bar/, 然后, assetsPublicPath 应该设置为/bar/;

in short 简单的如果是代理为二级路径的时候使用 ’/{外部路径}/’ , 否则使用 ’/‘

配置 路由模式

base 路由读取匹配组件的路径与assetsPublicPath 保持一致

mode

-hash

默认是hash模式, 使用url的hash来模拟一个完整的url, 优化当url改变的时候, 页面不会重新加载; (即地址栏以#号结尾)

这会导致window.location.reload() 刷新时vuex 的状态清空,

vue 把#后面当做自己的根目录,处理静态资源, 路由组件;

hash模式下 assetsPublicPathbase 可以使用 ./ 相对路径的形式配置, 刷新也不存在404问题, nginx 也不需要 try_files 配置了,可以纯静态的方式部署!

in short 应该是用来构建单页面应用的 router/index.js

//路由 hash模式
export default new Router({
  //mode: 'history',
  base: '/test/', //如果代理不是/ 打包时必须配置二级路径
  ...
})

-history

去掉#后, 要采用相对路径去引用, 如果图片引用是在js内, 则要采用 require() 方法进行引用;

不怕前进, 不怕后退, 就怕刷新, (如果后端没有准备的话),因为刷新是实实在在地去请求服务器的;

in short 地址栏改变, 刷新后就不是vue路由地址了

router/index.js

//路由 history 模式
export default new Router({
  mode: 'history',
  base: '/test/', //如果代理不是/ 打包时也必须配置二级路径!!!
  ...
})

nginx 代理配置

server {
  listen 80;
  server_name  127.0.0.1;
  root f:/nginx_proxy/;
  location /test/{
    alias f:/nginx_proxy/test2/;
    index index.html index.htm;
    try_files $uri $uri/ /test/index.html; //解决 history 模式 404问题
  }
}

css 引用的问题

extract-text-webpack-plugin 的解决方式

build/utils.jsgenerateLoaders 函数加上publicPath: '../../'

function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader]: [cssLoader]
 
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }
 
    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader',
        publicPath: '../../'//css 引用图片问题
      })
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

mini-css-extract-plugin的解决方式

配置 MiniCSSExtractPlugin.loader 的publicPath即可

  // generate loader string to be used with extract text plugin
  function generateLoaders(loader, loaderOptions) {
    const loaders = []
 
    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
	   loaders.push({//css 引用图片问题
        loader:MiniCssExtractPlugin.loader,
        options: {
          publicPath: '../../'
        }
      })
    } else {
      loaders.push('vue-style-loader')
    }
 
    loaders.push(cssLoader)
 
    if (options.usePostCSS) {
      loaders.push(postcssLoader)
    }
 
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }
 
    return loaders
  }
 
 

详解vue中静态资源的路径问题

elementUI 不显示 ico

MiniCssExtractPlugin

build/webpack.base.conf.js

module:{
  rules: [ 
    ...(config.dev.useEslint ? [createLintingRule()]: []),
    
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: 'url-loader',
      options: {
        limit: 10000,
        publicPath: '../../',
        name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
      }
    }
  ]  
}
 

因为打包后woff, eot, ttf, otf的路径找不到才导致404, 所以将这几个的publicPath设为’../../‘再打包就可以了;

https://www.cnblogs.com/EassieLee/p/11289521.html

响应式原理

深入响应式原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项, Vue 将遍历此对象所有的 property, 并使用 Object.defineProperty 把这些 property 全部转为 getter/setter; Object.defineProperty 是 ES5 中一个无法 shim 的特性, 这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因;

对于已经创建的实例, Vue 不允许动态添加根级别的响应式 property; 但是, 可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property

data = {name}
data = []
 
///对象
this.$set(object, propertyName, value)
 
 
///数组
// Vue.set
this.$set(vm.items, indexOfItem, newValue)
// 数组下标删除
arr.splice(index, 1);
 
 

用法: this.$set( target, propertyName/index, value ) 参数:

{Object | Array} target
{string | number} propertyName/index
{any} value

返回值: 设置的值;

自定义指令

自定义指令 自定义指令

注册

通过 directives 选项注册

export default {
  setup() {
    /*...*/
  },
  directives: {
    // 在模板中启用 v-focus
    focus: {
      /* ... */
    }
  }
}

通过实例

const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ... */
})

以插件的形式注册

const install = function(Vue) {
  Vue.directive('permission', permission)
}
//
Vue.use(install);

生命周期

如果想对DOM做一些底层操作可以使用自定义指令, 一个指令定义对象可以提供如下几个钩子函数 (均为可选): bind: 只调用一次, 指令第一次绑定到元素时调用; 在这里可以进行一次性的初始化设置; inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在, 但不一定已被插入文档中); update: 所在组件的 VNode 更新时调用, 但是可能发生在其子 VNode 更新之前; 指令的值可能发生了改变, 也可能没有; 但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下); componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用; unbind: 只调用一次, 指令与元素解绑时调用;

生命周期回调的参数

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

一个处理用户权限渲染组件的例子

import store from '@/store'
export default {
    inserted(el, binding, vnode) {
        const { value } = binding
        // store 用户权限列表
        const authorities = store.user.info.authorities
        if (value && value instanceof Array && value.length > 0) {
            const permissionRoles = value
            const hasPermission = authoritiesCodes.some(code => {
                return permissionRoles.includes(code)
            })
            if (!hasPermission) {
	            // 无权限, 移除这个节点
                el.parentNode && el.parentNode.removeChild(el)
            }
        } else {
		    //兜底移除
            el.parentNode && el.parentNode.removeChild(el)
        }
    }
}

动态组件(低代码)

用代码创建一个组件

import Vue from 'vue';
import tag from "./tag";
 
let div = document.createElement("DIV")
this.$el.append(div)
 
let tag_constructor = Vue.extend(tag)//获得构造函数, tag是已注册的组件名
new tag_constructor({//创建
  el:div,
  data(){
      return {
          name:"aaaa"
          data:[]
      }
  },
  parent: this
  })
 
///其二
 
//在父的生命周期 mounted 中, 有dom 才能正确挂载显示
  mounted() {
    const CounterFun = Vue.component(deptSelect.name, deptSelect); 
    const instance = new CounterFun({parent: this});
    instance.$mount(this.$el);
  },
/***
 *如需要监听事件可以使用组件实例的 $on 方法
 [参考文档](https://cn.vuejs.org/v2/api/#vm-on)
 
 *如果需要传递 props 数据, new实例是 传入: props: ['msg'] options
 [参考文档](https://cn.vuejs.org/v2/api/#propsData)
 
 */
  

Vue.component 只负责全局注册或查找;

如果想要针对局部注册的组件使用动态创建并添加我们需要使用 Vue.extend 基础Vue构造器创建”子类”达到目的; 其实 Vue.component 方法传入的选项是一个对象时, Vue自动调用 Vue.extend;

VUE 异步组加载

VUE is 特殊特性 VUE 动态组件

</template>
	<div>
    <component :is="$options.components.SpeedSlider" v-bind="needProps" ></component>
	</div>
</template>

:is 属性其实是指向一个渲染条件, 如果没有就不渲染, 可以指向:

  1. 组件数据/
  2. 会返回组件数据的Promise函数对象/
  3. 异步组件工厂函数(2.3.0+ 新增)/
  4. 注册后的组件名
new Vue({
  //必须注册(可以是Promise, 延迟)
  components: {
      'SpeedSlider':()=>import('./speedSlider')
	},
})
speedSlider.js
  ()=>new Promise((resolve,reject) => {  resolve(speedSlider);}),
 
//如果使用变量, 前面一定需要个常量, 告知 webpak 静态编译时它是个什么
//'SpeedSlider':()=>{ require("@/views/" + row.meta_ComponentView.substr(8)) ;}
 
//如果需要 props 也用动态参数的方式
needProps = {"组件需要的prop key", "组件需要的prop val"}
 

异步时 Failed to mount component: template or render function not defined. 错误

require 是 CommonJS 的模块导入方式, 而组件定义时写的export default 是 ES6 方式

因此require 导入的结果其实是一个含 default 属性的对象 所以 vue 中 component 用这个会报错 而恰好 vue 的命名视图组件注册用 components 而合理的用法应该是require(‘xxx.vue’).default 或是用import

它还可以指向, 一个 vue options, 从而实现真正的动态加载!! 参考- https://github.com/vuejs/vue/issues/3270

动态编译 SFC 文件

vue-template-compiler vue远程加载sfc组件思路详解

远程加载SFC 并注册组件 for 低代码

关键是能不能做到从字符中编译成vue组件! 答案是 vue-template-compiler

vue 远程加载sfc组件思路

Vuex

更好的一个解决方案 N_Vue Pinia

Vuex 是什么?

Vuex 是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式; 它采用集中式存储管理应用的所有组件的状态, 并以相应的规则保证状态以一种可预测的方式发生变化; Vuex 也集成到 Vue 的官方调试工具 devtools extension, 提供了诸如零配置的 time-travel 调试, 状态快照导入导出等高级调试功能;

当我们的应用遇到多个组件共享状态时(数据?), 单向数据流的简洁性很容易被破坏:

多个视图依赖于同一状态; 来自不同视图的行为需要变更同一状态; 对于问题一, 传参的方法对于多层嵌套的组件将会非常繁琐, 并且对于兄弟组件间的状态传递无能为力; 对于问题二, 我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝; 以上的这些模式非常脆弱, 通常会导致无法维护的代码;

因此, 我们为什么不把组件的共享状态抽取出来, 以一个全局单例模式管理呢?

Vuex 的状态存储是响应式的; 当 Vue 组件从 store 中读取状态的时候, 若 store 中的状态发生变化, 那么相应的组件也会相应地得到高效更新; 不能直接改变 store 中的状态;改变 store 中的状态的唯一途径就是显式地提交 (commit)mutation; (方便地跟踪每一个状态的变化)

用法

创建

让我们来创建一个 store; 创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation:

import Vue from 'vue'
import Vuex from 'vuex'
 
Vue.use(Vuex);
 
const store = new Vuex.Store({
  state: {
    count: 0,
    todos:[]
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
  mutations: {
    increment (state) {//第一个参数是 state
      state.count++
    }
  },
   getters: {// 相当于 state 的计算属性
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
 
 

mutation 原则(同步)

既然 Vuex 的 store 中的状态是响应式的, 那么当我们变更状态时, 监视状态的 Vue 组件也会自动更新; 这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

  • 最好提前在你的 store 中初始化好所有所需属性;
  • 当需要在对象上添加新属性时, 你应该
  • 使用 Vue.set(obj, ‘newProp’, 123), 或者
  • 以新对象替换老对象; 例如, 利用对象展开运算符我们可以这样写:
  • Mutation 必须是同步函数
修改 commit

现在, 你可以通过 store.state 来获取状态对象, 以及通过 store.commit 方法触发状态变更:

store.commit('increment')

console.log(store.state.count) // -> 1

注入Vue

为了在 Vue 组件中访问 this.$store property, 你需要为 Vue 实例提供创建好的 store; Vuex 提供了一个从根组件向所有子组件, 以 store 选项的方式”注入”该 store 的机制:

new Vue({
  el: '#app',
  store: store,
})

全局注册

这种模式导致组件依赖全局状态单例; 在模块化的构建系统中, 在每个需要使用 state 的组件中需要频繁地导入, 并且在测试组件时需要模拟状态; Vuex 通过 store 选项, 提供了一种机制将状态从根组件”注入”到每一个子组件中(需调用 Vue.use(Vuex))

组件获取多个状态

当一个组件需要获取多个状态的时候, 将这些状态都声明为计算属性会有些重复和冗余; 为了解决这个问题, 我们可以使用 mapState 辅助函数帮助我们生成计算属性, 让你少按几次键:

// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
 
export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,
 
    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',
 
    // 为了能够使用 `this` 获取局部状态, 必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

从组件中修改

从组件的方法提交一个变更:

methods: {
  increment() {
    this.$store.commit('increment')
    console.log(this.$store.state.count)
  }
}
 
 

Action (异步)

Action 类似于 mutation, 不同在于:

  • Action 提交的是 mutation, 而不是直接变更状态;
  • Action 可以包含任意异步操作;

示例

 
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
 

Action 函数接受一个与store 实例具有相同方法和属性的 context 对象(context 对象不是 store 实例本身!), 因此你可以调用context.commit 提交一个 mutation, 或者通过context.state 和 context.getters 来获取 state 和 getters;

修改 dispatch

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

乍一眼看上去感觉多此一举, 我们直接分发 mutation 岂不更方便? 实际上并非如此, 还记得 mutation 必须同步执行这个限制么? Action 就不受约束! 我们可以在 action 内部执行异步操作:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}