图片来源:
学习 vue 过程中的笔记,未完更新中 ... 完整【示例代码】请去我的 GitHub 仓库 查看
更新于 2017.12.24
首发于夏味的博客:1. 环境配置
注意本笔记使用的版本为当时的最新稳定版
- Vue 2.x
- webpack 2
- node 8.9.0
- npm 5.6.0
1.1 使用到的技术文档
1.2 需要安装的相关依赖,未来不一定正确,以官方文档为准
首先需要安装 node, 然后使用命令 npm install 依赖名称
来安装
- babel-core
- babel-loader
- babel-preset-env
- babel-preset-stage-2 (使用
import()
时才需要) - css-loader
- html-webpack-plugin
- style-loader
- vue
- vue-loader
- vue-template-compiler
- webpack
- webpack-dev-server
- vue-router
- axios
- vuex(选用)
1.3 webpack 配置项简介
项目根目录下,创建 webpack.config.js
配置文件
const path = require('path'); //node 内置模块const HtmlWebpackPlugin = require('html-webpack-plugin'); // 用于在 html 页面里自动插入资源引用的标签const webpack = require('webpack'); //使用 webpack 内置的插件时,需要引入 webpackmodule.exports = { entry: { index: './src/index.js' // 入口文件 // bbb: './src/bbb.js' // 可以多个入口文件 }, output: { path: path.resolve('./dist'), // 输出路径,必须是绝对路径.path.resolve是nodeJS模块方法,把相对路径转为绝对路径 // 或者使用语法__dirname +'/dist' 或 path.join(__dirname,'dist') // __dirname 表示当前模块的目录的绝对路径(并非全局变量,等价于path.dirname(__filename)) // path.join用于处理连接路径时,统一不同系统路径符\和/问题。 // publicPath: '/assets/', // 发布路径,填写此项后,打包后文件路径不再是相对路径,而是基于服务器根目录的路径, filename: 'js/[name].js', // [name] 表示块的名称,输出文件名,可以包含路径 chunkFilename: 'js/[name].js' //webpack 分割后的文件,[id] 表示块的编号。[name] 表示块的名称,没有名称时会自动使用编号 }, resolve: { alias: { vue$: 'vue/dist/vue.esm.js' // 默认是运行时构建,这里使用了template,必须用运行+编译时构建 } }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' }, //使用 babel 对 js 转译 { test: /\.vue$/, exclude: /node_modules/, loader: 'babel-loader!vue-loader' }, // 先使用 vue-loader对 vue 文件转译 { test: /\.css$/, loader: 'style-loader!css-loader' } ] }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', // 输出文件名,可以包含路径 template: 'src/index.html', // 模板文件位置 inject: 'body', //插入位置,也可以写 head hash: true, // 在文件名后面加 hash 值,默认false chunks: ['index'] // 表示插入的块,对应 webpack 入口文件中的 index,不写会插入所有的入口文件 }), new webpack.HotModuleReplacementPlugin() //如果 devServer 的配置项里 hot:true ,则需要配置此项 ], externals: { vue: 'Vue' //打包时排除 vue,vue内容不会被写入js。 //注意左边的 vue 是模块名,右边的 Vue 是不使用构建工具时的标准变量名,必须是Vue,与import的变量名无关 }, devServer: { contentBase: './dist/', //表示静态资源(非webpack编译产生)文件的目录位置, //这个目录的资源会被放到同样当成服务器根目录去 //遇到同名文件,webpack编译后产生的文件优先级更高 compress: true, //是否压缩 port: 9000, //端口号 host: '0.0.0.0', //默认是localhost,如果想被外部访问,这样设置 historyApiFallback: true, //当使用 history 模式路由时,设置为true,404页面会被重定向到主页, hot: true // 热替换,可以在不刷新页面的情况下更新修改后数据,也可以配置在package.json 的 scripts 里,加 --hot参数 }};复制代码
项目根目录下,创建 .babelrc
配置文件
babel-preset-env
相当于 es2015 ,es2016 ,es2017 及最新版本
{ "presets": ["env"]}复制代码
1.4 运行
package.json 文件里添加配置
{ // ...其他参数 "scripts": { "build": "webpack", "dev": "webpack-dev-server" }}复制代码
然后使用 npm run dev
来启动 server 使用 npm run build
来打包输出
这里的 build 是自己起的。写为 "build": "webpack -p"
, 打包后时压缩代码
1.5 webpack-dev-server 热替换
热替换指,在不刷新页面的状态下,把修改后的结果更新到页面上
有两种配置方式
- webpack CLI 方式:
package.json 文件里添加配置
{ "scripts": { "dev": "webpack-dev-server --hot" }}复制代码
- webpack 配置方式:
webpack.config.js 文件里添加配置
const webpack = require('webpack');module.exports = { // 其他配置... plugins: [ // 其他配置... new webpack.HotModuleReplacementPlugin() ], devServer: { // 其他配置... hot: true }};复制代码
package.json 文件里依然是 "dev": "webpack-dev-server"
2. vue 语法
2.1 基本用法
{ { name }}复制代码
import Vue from 'vue';let param = { el: '#app', data: { name: 'hello vue' } //注意这里的 data 也可以使用这种语法 //data() { // return {name:'hello vue'} //}};new Vue(param);复制代码
2.2 基本的组件
复制代码
import Vue from 'vue';// 这是一个组件let meName = { template: '{ {name}}', // 组件的模板,渲染后会替换掉data() { return { name: 'xiawei' }; // 组件中 data 必须是函数,数据 return 出去,不可以写为 data:{name:'xiawei'} }};new Vue({ el: '.container', components: { 'my-name': meName }});复制代码
2.3 vue 文件形式的组件
为了方便,可以使用 vue 文件 来封装组件,可以认为一个 vue 文件是一个组件,子组件继续使用其他 vue 文件 引入。
index.js
import Vue from 'vue';import myname from './components/myname.vue';new Vue({ el: '.container', components: { 'my-name': myname }});复制代码
myname.vue
{ {name}}复制代码
template 里,必须用一个 div 或者一个其他标签,包裹住所有的 html 标签
默认 lang="css" 可以省略,需要使用 sass 时,可以写 lang="scss" 等
scoped 是 vue 提供的属性 表示这里的样式只能在本组件内生效
2.4 组件通讯
2.4.1 使用 props 给组件传参
复制代码
{ {value}}复制代码
2.4.2 访问其他组件,获取参数
可以通过 $parent
访问父组件,$children
访问子组件
user-login 有三个子组件,部分代码如下
复制代码User Login
这时在 user-submit 组件
复制代码
要区分子组件是第几个,并不方便,可以使用 ref
来解决这个问题
相关代码修改为以下即可
复制代码
//获取 user-name 组件 data 中 username 的值this.$parent.$refs.uname.username;复制代码
2.4.3 父子组件自定义事件通讯
父组件 user-login.vue 里,给子组件 user-name 设置自定义事件 updateUserName
这个事件是绑定在 user-name 组件上的,在 组件对象 .$listeners
里可以查看到,可以用 组件对象 .$emit
来触发
$emit 触发时,参数 1 是事件名,后几个参数可以传给事件对象(类似 jQuery 的trigger 方法)
复制代码
子组件 user-name.vue,当输入框内容改变,触发 change
事件
然后执行了 $emit
来触发 updateUserName
事件,this.username
作为参数传给了updateUserName
事件
复制代码
2.5 v-if,路由原理
v-if 主要用于渲染模板,下面代码
当变量 isadmin
为 true 时,只显示 Admin Login
反之,只显示User Login
注意,程序依据 isadmin == true
的结果来判断
Admin Login
User Login
复制代码
在 index.js 添加下面代码
当浏览器路径 hash 部分(#
号及其后面的部分)变化时,会触发 hashchange
事件
判断 hash 的值,各种值走自己的业务逻辑,就可以切换页面、改变数据,这就是路由原理
window.onhashchange = function() { if (window.location.hash === '#admin') { myvue.$children[0].$data.isadmin = true; } else { myvue.$children[0].$data.isadmin = false; }};复制代码
相关需要掌握的还有 v-for
,参见
2.6 计算属性 computed
计算属性和 data 里的普通属性调用时相同的,但定义时不同
计算属性使用函数定义,return 的值,就是计算属性的值
当计算属性内的其他变量的值发生变化时,函数就会执行,运算得到新的值
所以计算属性的值是依赖其他变量的,它没有初始值,不可以在 data 里声明
下面的例子,通过计算属性比对输入的值来筛选 fav.class2
返回通过筛选条件的新数组,当 return true
时符合条件被选入。
返回符合条件的字符串序号,如果找不到时,会返回数字 -1
,可以用来匹配字符串类似的方法,还有
复制代码
type 1 type 2 { { fav.class1 }} { { fav.class2 }}
2.6.1 计算属性配合过滤方法
vue 2.x 的,与 vue 1.x 语法不同,并不适合和 v-for
配合使用,计算属性配合过滤方法来实现。
上节的例子,更复杂一点,数组的情况 ( 和上面重复的部分没写出来,完整代码请查看github)
getFavs
决定展示第几条数据,filterClass2
负责对展示出来的数据筛选
{ { fav.class1 }} { { code }} 复制代码
2.7 路由
2.7.1 路由的基本使用
首先 npm 安装依赖官方的路由插件 vue-router
index.html
复制代码
index.js 文件
import Vue from 'vue';import VueRouter from 'vue-router'; // 引入插件Vue.use(VueRouter); // 使用插件import pagenav from './components/page-nav.vue';import newslist from './components/news-list.vue';import userlogin from './components/user-login.vue';const routerConfig = new VueRouter({ routes: [ { path: '/news', component: newslist }, { path: '/login', component: userlogin } ]});// 全局注册公共组件(也可以像原先注册子组件的方式来做)Vue.component('page-nav', pagenav);let myvue = new Vue({ el: '.container', router: routerConfig // 路由中引入过子组件了,所以此处不需要再引入子组件 // components: { // 'page-nav': pagenav, // 'user-login': userlogin // }});复制代码
page-nav.vue 的部分代码
推荐使用 router-link
语法作为切换按钮,它默认会渲染成 a
标签也可以使用 a 标签来做
当某个 router-link
被点击选中时,vue 会给它的 html 标签添加上 class router-link-active
可以通过给 .router-link-active
写 css 样式, 来给选中的 router-link
添加样式
复制代码
2.7.2 axios 的基本使用
引入
import axios from 'axios';复制代码
如需全局引入,可以再加上下面这句,组件内调用时使用 this.$axios
即可
Vue.prototype.$axios = axios;复制代码
get 请求
axios .get('http://localhost:8000/test.php', { params: { ID: 12345 } }) .then(response => { alert(response.data); });复制代码
post 请求参数 axios 默认转为 json 格式
axios .post('http://localhost:8000/test.php', { name: 'xiawei', age: 20 }) .then(response => { alert(response.data); });复制代码
键值对方式(php 用 $_POST
可以取到值)
axios.post('http://localhost:8000/test.php', 'name=xiawei&age=20');复制代码
也可以使用 node 内置模块来转换格式
import querystring from 'querystring';axios.post( 'http://localhost:8000/test.php', querystring.stringifyname({ name: 'xiawei', age: 20 }));复制代码
这部分的 php 代码,是放置在项目根目录的 test.php 文件
Mac 内置了 php,直接启动 php 内置服务:到项目根目录下,Terminal 里执行下面命令即可
windows 下载 php 后,把 php 目录添加到系统环境变量 PATH 里后,同样执行下面命令
php -S 0.0.0.0:8000复制代码
2.7.3 动态加载新闻详细页
在新闻列表页,点击标题跳转到新闻详细页,动态加载新闻内容
index.js 部分代码
import axios from 'axios';Vue.prototype.$axios = axios;const routerConfig = new VueRouter({ routes: [ { path: '/', component: newslist },// 设置首页 { path: '/news', component: newslist, name: 'newslist' },// 可以给路由设置别名 name { path: '/news/:newsid', component: newsdetail, name: 'newsdetail' },// 如果需要参数,使用冒号的来做占位符 { path: '/login', component: userlogin, name: 'userlogin' } ]});复制代码
new-list.vue 部分代码
复制代码
{ {news.title}} { {news.pubtime}}{
{news.desc}}
news-detail.vue 部分代码
复制代码{ { newstTitle }} { { newsDate }}
{
{ newsContent }}
通过全局变量 $route 来访问路由里的各种数据
例如 $route.params.newsid
可以获得路由占位符 :newsid
处的新闻编号值 101
2.8 异步加载和 webpack 代码分割
当项目比较大的时候,可以使用异步加载组件的方式来按需加载,而不是一次性加载全部组件。
还可以配合 webpack 代码分割功能,把打包后的 js,分割成多个 js 文件,做到按需引用。
之前的引入组件的方式是
import userlogin from './components/user-login.vue';复制代码
使用 vue 异步加载的方式引入
var userlogin = function(resolve) { resolve(require('./components/user-login.vue'));};复制代码
使用 ES2015 语法,并且简化参数名,可以写为
const userlogin = r => { r(require('./components/user-login.vue'));};复制代码
结合 webpack 代码分割功能后
const userlogin = r => { require.ensure([], () => { r(require('./components/user-login.vue')); });};复制代码
如果需要把某几个组件打包为一组,给它们的 require.ensure()
(、)添加最后一个参数(例如'aaa'
),且值相同
require.ensure([], () => { r(require('./components/user-login.vue')); },'aaa');复制代码
也可以使用 webpack + ES2015 语法来进行代码分割
import()
() 是 ES2015 草案的语法,所以使用时需要 babel 转译
babel 配置里需要添加草案语法的转译 presets stage-2
,npm 安装依赖 babel-preset-stage-2
.babel
文件,注意配置的数组里,presets 解析的顺序是从右到左的,先执行 stage-2
{ "presets": ["env", "stage-2"]}复制代码
const userlogin = () => import('./components/user-login.vue');// 也就是 function() { return import('./components/user-login.vue')};复制代码
把某几个文件打包为一组时,使用这个语法
const userlogin = () => import(/* webpackChunkName: "aaa" */'./components/user-login.vue');复制代码
最后分割后的文件名,可以在 webpack 配置里 output 配置项里添加 chunkFilename
配置项来控制
output: { filename: 'js/[name].js', // [name] 表示块的名称,输出文件名,可以包含路径 chunkFilename: 'js/[name].js' //webpack 分割后的文件,[id] 表示块的编号。[name] 表示块的名称,没有名称时会自动使用编号},复制代码
2.9 开发插件
有时现有的插件并不能满足自己的业务需求,这时需要自己开发插件
2.9.1 自定义指令
在 src 文件夹下新建一个 js 文件,比如命名为 plugin.js
export default { install(Vue) { // 添加实例方法 Vue.prototype.$name = 'xiawei';// 可以在组件内使用 this.$name 取到值 'xiawei' // 这里添加时方法来检测用户名是否合法,6~20位合法,否则显示提示 Vue.prototype.checkUserName = value => { if (value == '') return true; return /\w{6,20}/.test(value); }; // 可以在组件内使用 this.checkUserName(’‘’) // 添加全局自定义指令 v-uname Vue.directive('uname', { bind() { console.log('begin'); }, update(el, binding, vnode) { vnode.context[binding.expression] = !/\w{6,20}/.test(el.value); } }); }};复制代码
directive
() 里的生命周期里的三个参数:
- el 参数表示指令所绑定的元素,可以用来直接操作 dom
- binding 参数表示绑定对象,binding.expression 取到传入的表达式,binding.value 可以取到表达式的值 这里的表达式也可以是函数名,取到的值是函数体,binding.oldValue
- vnode 参数表示 Vue 编译生成的虚拟节点
关于里,添加全局方法或属性 Vue.myGlobalMethod
和添加实例方法和属性 Vue.prototype.$myMethod
二者区别
全局方法或属性使用 Vue.名称
来调用,而实例方法和属性使用 (实例化后的 Vue 对象).名称
来调用,也就是组件内的常见 this.名称
来调用,即使看起来名称一样的Vue.aaa
和Vue.prototype.aaa
也是两个不同的变量
具体可以参见这篇文章:
index.js 内加载插件
import plugin from './plugin.js';Vue.use(plugin);复制代码
user-name.vue 添加 v-uname
和 label 元素
复制代码
2.9.2 手动挂载子组件
上面只是控制变量,并不是很方便,可以通过插件动态插入移除提示框
export default { install(Vue) { // 创建变量,定义初始值 Vue.errorLabel = null; Vue.hasErrorLabel = false; // 这个全局变量来标记是否插入了 label,给初始值时必须放在 update 外面 // 添加全局自定义指令 v-uname Vue.directive('uname', { bind(el) { let error = Vue.extend({ template: '' }); Vue.errorLabel = (new error()).$mount().$el; // $mount() 方法不填参数时,表示把 vm 实例对象变成一个可以挂载的状态,这时就可以访问到 $el 获取到元素了 }, update(el, binding, vnode) { // 这里每次 update 是从组建原始的状态 update 的,所以不会重复插入多个 if (/\w{6,20}/.test(el.value)) { if (Vue.hasErrorLabel) { el.parentNode.removeChild(Vue.errorLabel); Vue.hasErrorLabel = !Vue.hasErrorLabel; } } else { if (!Vue.hasErrorLabel) { el.parentNode.appendChild(Vue.errorLabel); Vue.hasErrorLabel = !Vue.hasErrorLabel; } } } }); }};复制代码
user-name.vue 组件里,这时不需要写 label 元素,只需要写入 v-uname
即可
复制代码
2.9.3 插件里包含子组件
上一小节的代码,当有多个 input 元素时,就会出现其他元素显示不正常的情况,原因是多个标签共用了同一个 Vue.hasErrorLabel
所以当插件不仅仅处理数据时,还需要独立的处理 dom 元素时,使用子组件的方式更加合理,它们是互相独立的
export default { install(Vue) { Vue.component('p-username', { template: ``, // 这里使用了 ES2015 的模板字符串语法 data() { return { textValue: '' }; }, computed: { showErrorLabel() { return !(/\w{6,20}/.test(this.textValue) || this.textValue == ''); } } }); }};复制代码
其中,为了方便 template 里使用了 ES2015 的模板字符串语法()
user-name.vue 文件(不需要写 input 元素)
复制代码
2.10 全局状态管理 vuex
应遵循以下规则
- 应用级的状态集中放在 store 中
- 计算属性使用 getters
- 改变状态的方式是提交 mutations,这是个同步的事务
- 异步逻辑应该封装在 action 中
也即是与组件的概念相对应的 store -> data getters -> computed mutations/actions -> methods
2.10.1 vuex 基本使用
npm 安装依赖 vuex
index.js
import Vuex from 'vuex';Vue.use(Vuex);const vuex_store = new Vuex.Store({ state: { user_name: '' }, mutations: { showUserName(state) { alert(state.user_name); } }});复制代码
赋值:user-name.vue 组件中使用
复制代码
this.$store.state.user_name = this.username;复制代码
触发:user-submit.vue 组件中使用
this.$store.commit('showUserName');复制代码
即可完成简单的输入用户名,点提交按钮后 alert 出用户名
2.10.2 vuex 计算属性
vuex 里的计算属性使用的是 getters
,用法和 组件里的计算属性 computed
类似,只是被触发的时机不同
从数据里展示没有删除的新闻展示
index.js
const vuex_store = new Vuex.Store({ state: { user_name: '', newslist: [] }, mutations: { showUserName(state) { alert(state.user_name); } }, getters: { getNews(state) { return state.newslist.filter(news => !news.isdeleted); } }});复制代码
news-list.vue
复制代码
export default { created() { if (this.$store.state.newslist.length == 0) { this.$axios.get('http://localhost:8000/news.php').then(response => { this.$store.state.newslist = response.data; }); } }};复制代码
2.10.3 actions
mutations 是同步执行的,里面不能放异步执行的东西 actions 里放异步执行的,异步执行完后,去手动触发 mutations
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context,param) { // 异步业务 param -> param2 context.commit('increment',param2); } }})复制代码
组件内触发
this.$store.dispatch('increment',param);复制代码
2.10.4 把业务按模块分类
之前写的 index.js 是这样
const vuex_store = new Vuex.Store({ state: { user_name: '', newslist: [] }, mutations: { showUserName(state) { alert(state.user_name); } }, getters: { getNews(state) { return state.newslist.filter(news => !news.isdeleted); } }});复制代码
按模块分离后
index.js
import news_module from './store/news.js';import users_module from './store/users.js';const vuex_store = new Vuex.Store({ modules: { news: news_module, users: users_module }});复制代码
news.js
export default { state: { newslist: [] }, getters: { getNews(state) { return state.newslist.filter(news => !news.isdeleted); } }}复制代码
users.js
export default { state: { user_name: '' }, mutations: { showUserName(state) { alert(state.user_name); } }}复制代码
分离后,注意相关模块里的 this.$store.state
按业务模块名分别改为 this.$store.state.news
、this.$store.state.users
注意不同业务模块里,getters 里函数重名了会报错, mutations 里函数重名了会两边都执行
推荐课程: