vue.js是一套构建用户界面的渐进式框架。vue采用自底向上增量开发的设计。vue的核心库只关心视图层,非常容易学习,非常容易与其它库和已有项目整合。vue完全有能力驱动采用单文件组件和vue生态系统支持的库开发的复杂单页应用。
vue.js的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件。
总体框架
一个vue-cli的项目结构如下,其中src文件夹是需要掌握的,所以本文也重点讲解其中的文件,至于其他相关文件,了解一下即可。
文件结构细分
1.build——[webpack配置]
build文件主要是webpack的配置,主要启动文件是dev-server.js,当我们输入npm run dev首先启动的就是dev-server.js,它会去检查node及npm版本,加载配置文件,启动服务。
2.config——[vue项目配置]
config文件主要是项目相关配置,我们常用的就是当端口冲突时配置监听端口,打包输出路径及命名等
3.node_modules——[依赖包]
node_modules里面是项目依赖包,其中包括很多基础依赖,自己也可以根据需要安装其他依赖。安装方法为打开cmd,进入项目目录,输入npm install [依赖包名称],回车。
在两种情况下我们会自己去安装依赖:
(1)项目运行缺少该依赖包:例如项目加载外部css会用到的css-loader,路由跳转vue-loader等(安装方法示例:npm install css-loader)
(2)安装插件:如vux(基于WEUI的移动端组件库),vue-swiper(轮播插件
注:有时会安装指定依赖版本,需在依赖包名称后加上版本号信息,如安装11.1.4版本的vue-loader,输入npm install vue-loader@11.1.4
4.src——[项目核心文件]
项目核心文件前面已经进行了简单的说明,接下来重点讲解main.js,App.vue,及router的index.js
4.1 index.html——[主页]
index.html如其他html一样,但一般只定义一个空的根节点,在main.js里面定义的实例将挂载在根节点下,内容都通过vue组件来填充
4.2 App.vue——[根组件]
一个vue页面通常由三部分组成:模板(template)、js(script)、样式(style)
【template】
其中模板只能包含一个父节点,也就是说顶层的div只能有一个(例如上图,父节点为#app的div,其没有兄弟节点)
<router-view></router-view>是子路由视图,后面的路由页面都显示在此处
打一个比喻吧,<router-view>类似于一个插槽,跳转某个路由时,该路由下的页面就插在这个插槽中渲染显示
【script】
vue通常用es6来写,用export default导出,其下面可以包含数据data,生命周期(mounted等),方法(methods)等,具体语法请看vue.js文档,在后面我也会通过例子来说明。
【style】
样式通过style标签<style></style>包裹,默认是影响全局的,如需定义作用域只在该组件下起作用,需在标签上加scoped,<style scoped></style>
如要引入外部css文件,首先需给项目安装css-loader依赖包,打开cmd,进入项目目录,输入npm install css-loader,回车。安装完成后,就可以在style标签下import所需的css文件,例如:
<style>
import './assets/css/public.css'
</style>
这样,我们就可以把style下的样式封装起来,写到css文件夹,再引入到页面使用,整个vue页面也看上去更简洁。
4.3 main.js——[入口文件]
main.js主要是引入vue框架,根组件及路由设置,并且定义vue实例,下图中的
components:{App}就是引入的根组件App.vue
后期还可以引入插件,当然首先得安装插件。
4.4 router——[路由配置]
router文件夹下,有一个index.js,即为路由配置文件
这里定义了路径为'/'的路由,该路由对应的页面是Hello组件,所以当我们在浏览器url访问http://localhost:8080/#/时就渲染的Hello组件
类似的,我们可以设置多个路由,‘/index’,'/list'之类的,当然首先得引入该组件,再为该组件设置路由。
项目加载分析
1、项目启动分析
①入口文件main.js——②入口文件中配置了相关的模板注入信息——③扫描路由。先找到公共路由,再根据注入的模板信息扫描模块中的路由。
2、发送请求获取数据执行分析
①发送请求——②根据请求路由到相关页面——③页面创建后调用钩子函数调用需要用的js方法——④js方法访问API方法——⑤根据API访问后台接口——⑥后台接口返回相关数据——⑦修改数据模型——⑧数据模型双向绑定。
3、路由页面分析
①公共路由与模块路由启动后被扫描。
②根据访问路径查找相关路径对应信息:
a、先找到父路径:path: '/saas-clients',//父路径
b、找到子路径:path: 'index'
c、根据子路径跳转视图地址:component: _import('saas-clients/pages/index'), //跳转的vue视图
d、找到module-saas-clients模块目录下的pages目录下的index.vue文件
e、跳转地址的主目录为saas-clients,而并非module-saas-clients是如何对应的?
根据公共路由中import_development.js、import_production.js2个文件的配置,根据vue项目规则自动加的前缀。
elementui自定义主题(任意颜色)换肤功能
我的项目中使用了scss,elementui官网提供了解决方案如下:
在项目中改变 SCSS 变量
Element 的 theme-chalk 使用 SCSS 编写,如果你的项目也使用了 SCSS,那么可以直接在项目中改变 Element 的样式变量。
1. 新建一个样式文件,例如 element-variables.scss,写入以下内容:
/* 改变主题色变量 */
$--color-primary: teal;
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";
2. 之后,在项目的入口文件中,直接引入以上样式文件即可(无需引入 Element 编译好的 CSS 文件):
import Vue from 'vue'
import Element from 'element-ui'
import './element-variables.scss'
Vue.use(Element)
需要注意的是,覆盖字体路径变量是必需的,将其赋值为 Element 中 icon 图标所在的相对路径即可。
以上方法,我们修改主题色变量 $--color-primary 的值,然后在项目入口文件 main.js 中引入该样式文件,覆盖elementui的css文件,即可实现换肤。
那么怎么做到自定义任意颜色的主题呢?大致思路如下:
- 1.使用 el-color-picker 组件,供用户选择颜色
- 2.监听颜色的变化,创建 style 标签,生成样式内容
- 3.将创建好的样式内容插入 head 中
实际步骤如下:
- 1.在项目中新建一个scss文件,比如就叫 elementui-variables.scss
内容如下:
/* theme color */
$--color-primary: #1890ff;
$--color-success: #13ce66;
$--color-warning: #FFBA00;
$--color-danger: #ff4949;
// $--color-info: #1E1E1E;
$--button-font-weight: 400;
// $--color-text-regular: #1f2d3d;
$--border-color-light: #dfe4ed;
$--border-color-lighter: #e6ebf5;
$--table-border:1px solid #dfe6ec;
/* icon font path, required */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// @import "~element-ui/packages/theme-chalk/src/index";
:export {
theme: $--color-primary;
}
2.考虑到项目全局都有可能使用到颜色这个变量,所以决定用vuex,如图:新建文件 settings.js
settings.js 内容如下,theme 的初始值为 elementui-variables.scss 中定义的theme。
import variables from '@/styles/element-variables.scss'
const settings = {
state: {
theme: variables.theme,
},
mutations: {
CHANGE_SETTING: (state, { key, value }) => {
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
},
actions: {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
}
export default settings
- 3.新建一个组件(一般在components目录下,我将它命名为ThemePicker),用于监听颜色变量 theme 的改变,然后 getHandler 函数生成样式文件,插入head中。在这里我遇到了一个问题,如果使用 document.head.appendChild(styleTag) ,页面就会变成一片空白,然后我就在想有可能是样式的顺序导致一些有用的样式被覆盖了,所以我就讲它改为 document.getElementsByTagName('style')[0].insertBefore(styleTag, null) ; 完美解决问题。
ThemePicker文件内容如下,可以看到默认的颜色是 this.$store.state.settings.theme ,它是在 elementui-variables.scss 中定义的。
<template>
<el-color-picker
v-model="colors.primary"
:predefine="['#409EFF', '#67C23A', '#E6A23C', '#f5222d', '#11a983', '#13c2c2', '#6959CD', '#434f5d', ]"
class="theme-picker"
popper-class="theme-picker-dropdown" title="换肤"
@change="colorChange"
/>
</template>
<script>
import generateColors from "../../utils/color"
import objectAssign from "object-assign"
export default {
name: "App",
data() {
return {
originalStylesheetCount: -1, //记录当前已引入style数量
originalStyle: "", //获取拿到的.css的字符串
colors: {
//颜色选择器默认颜色值,这个值要和element-variables一样
primary: "#001529",
},
primaryColor: "", //提交成功后设置默认颜色
cssUrl: [
"//unpkg.com/element-ui/lib/theme-chalk/index.css",
"./static/css/index.css",
],
}
},
methods: {
colorChange(e) {
if (!e) return
localStorage.setItem("color", e)
this.primaryColor = this.colors.primary
this.colors = objectAssign(
{},
this.colors,
generateColors(this.colors.primary)
)
this.writeNewStyle()
},
writeNewStyle() {
let cssText = this.originalStyle
Object.keys(this.colors).forEach((key) => {
cssText = cssText.replace(
new RegExp("(:|\\s+)" + key, "g"),
"$1" + this.colors[key]
)
})
if (this.originalStylesheetCount === document.styleSheets.length) {
// 如果之前没有插入就插入
const style = document.createElement("style")
style.innerText =
".primaryColor{background-color:" +
this.colors.primary +
"}" +
cssText
document.head.appendChild(style)
} else {
// 如果之前没有插入就修改
document.head.lastChild.innerText =
".primaryColor{background-color:" +
this.colors.primary +
"} " +
cssText
}
},
getIndexStyle(url) {
let that = this
var request = new XMLHttpRequest()
request.open("GET", url)
request.onreadystatechange = function () {
if (
request.readyState === 4 &&
(request.status == 200 || request.status == 304)
) {
// 调用本地的如果拿不到会得到html,html是不行的
if (request.response && !/DOCTYPE/gi.test(request.response)) {
that.originalStyle = that.getStyleTemplate(request.response)
that.writeNewStyle()
} else {
that.originalStyle = ""
}
} else {
that.originalStyle = ""
}
}
request.send(null)
},
getStyleTemplate(data) {
const colorMap = {
"#3a8ee6": "shade-1",
"#409eff": "primary",
"#53a8ff": "light-1",
"#66b1ff": "light-2",
"#79bbff": "light-3",
"#8cc5ff": "light-4",
"#a0cfff": "light-5",
"#b3d8ff": "light-6",
"#c6e2ff": "light-7",
"#d9ecff": "light-8",
"#ecf5ff": "light-9",
}
Object.keys(colorMap).forEach((key) => {
const value = colorMap[key]
data = data.replace(new RegExp(key, "ig"), value)
})
return data
},
},
mounted() {
// 默认从线上官方拉取最新css,2秒钟后做一个检查没有拉到就从本地在拉下
let that = this
// 如果是记住用户的状态就需要,在主题切换的时候记录颜色值,在下次打开的时候从新赋值
this.colors.primary = localStorage.getItem("color") || this.colors.primary //例如
this.getIndexStyle(this.cssUrl[0])
setTimeout(function () {
if (that.originalStyle) {
return
} else {
that.getIndexStyle(that.cssUrl[1])
}
}, 2000)
this.$nextTick(() => {
// 获取页面一共引入了多少个style 文件
this.originalStylesheetCount = document.styleSheets.length
})
},
}
</script>
<style>
.theme-picker {
float: left;
margin-top: 10px;
}
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>
辅助文件:utils/color.js
import color from 'css-color-function'
import formula from './formula.json'
const generateColors = primary => {
let colors = {}
console.log("当前颜色:" , primary)
Object.keys(formula).forEach(key => {
const value = formula[key].replace(/primary/g, primary)
colors[key] = color.convert(value)
})
return colors
}
export default generateColors
页面上有些组件的样式可能是自定义的,不会随 theme 一起变化,可这么实现:
<router-link v-for="tag in Array.from(visitedViews)" ref="tag" :style="isActive(tag)?{
'background-color': theme,
'color': '#ffffff',
'border-color': theme
}:{}"
:to="tag" :key="tag.path" class="tags-view-item">
{{ tag.name }}
<span class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)"/>
</router-link>
computed: {
//...,
theme() {
return this.$store.state.settings.theme
}
},
4.最后在页面上使用 ThemePicker 组件即可:
实际效果录屏:
Vue项目引入阿里图标
引入步骤
1、访问网址https://www.iconfont.cn/
2、选择好后加入购物车
如何批量添加阿里巴巴iconfont图标到购物车
在页面上右键--审查元素,切换到console 控制台 输入如下代码,其实就是触发点击事件,可以一次性选择当前页面所有的图标
var span = document.querySelectorAll('.icon-cover');
for (var i = 0, len = span.length; i < len; i++) {
console.log(span[i].querySelector('span').click());
}
3、进入购物车,添加至项目
4、进入我的项目,下载到本地
5、将解压后的文件放入目标文件夹下
6、在main.js文件内全局引入 阿里云字体图标 css,记得使用 路径要正确,如果此时编译不正确,说明 引用路径存在问题
为避免出现图标方框的情况,请对font进行初始化
在reset.css 或者全局index.css中加入如下代码
// 阿里字体图标设置
.icon, .iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
7、项目中使用字体图标,就可以使用了
(3)Symbol SVG图片格式
①支持多色图标了,不再受单色限制。
②支持像字体那样通过font-size,color来调整样式。
③支持 ie9+
④可利用CSS实现动画。
⑤减少HTTP请求。
⑥矢量,缩放不失真
⑦可以很精细的控制SVG图标的每一部分
使用方法:
第一步:拷贝下载项目,复制inconfont.js到资源文件目录下,并引入inonfont.js文件
引入 ./iconfont.js
第二步:加入通用css代码(引入一次就行):
<style type="text/css">
.icon {
width: 1em; height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
第三步:挑选相应图标并获取类名,应用于页面:
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xxx"></use>
</svg>
vue项目中引入阿里图标SVG
引入过程中出现的问题:页面没有出现图标,原因:在main.js中没有引入iconfont.js
1、阿里demo中的引入方式:
2、我的引入方式(参考别人的项目)
上图中的引入方式,在iconfonts.js中注册了标签
我的方式是在上面的基础上封装了一层,每次使用的时候可以不用写
我的目录结构:
icons/svg中放所有的图标文件
1)index.js中代码功能,注册全局标签,并获取所有的svg文件,index.js中的代码如下:
import Vue from 'vue'
import SvgIcon from '../../components/SvgIcon'// svg component
// register globally
Vue.component('svg-icon', SvgIcon)
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
2)SvgIcon组件中的代码,主要共功能将引入图标的代码封装到组件中,并注册为全局标签使用
<<span style="color: #f92672;">template>
<<span style="color: #f92672;">div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<<span style="color: #f92672;">svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<<span style="color: #f92672;">use :href="iconName" />
</<span style="color: #f92672;">svg>
</<span style="color: #f92672;">template>
<<span style="color: #f92672;">script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '../utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</<span style="color: #f92672;">script>
<<span style="color: #f92672;">style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</<span style="color: #f92672;">style>
3)在main.js中引入即可
import './assets/icons'
import './assets/fonts/iconfont.js'
store的值刷新就被覆盖解决方案
最近在用vue写pc端项目,用vuex来做全局的状态管理, 发现当刷新网页后,保存在vuex实例store里的数据会丢失。
1. 产生原因
2. 解决思路
3. 解决过程
1. 产生原因
其实很简单,因为store里的数据是保存在运行内存中的,当页面刷新时,页面会重新加载vue实例,store里面的数据就会被重新赋值。
2. 解决思路
一种是state里的数据全部是通过请求来触发action或mutation来改变
一种是将state里的数据保存一份到本地存储(localStorage、sessionStorage、cookie)中
很显然,第一种方案基本不可行,除非项目很小或者vuex存储的数据很少。而第二种可以保证刷新页面数据不丢失且易于读取。
3. 解决过程
首先得选择合适的客户端存储
localStorage是永久存储在本地,除非你主动去删除;
sessionStorage是存储到当前页面关闭为止;
cookie则根据你设置的有效时间来存储,但缺点是不能储存大数据且不易读取。
我选择的是sessionStorage,选择的原因vue是单页面应用,操作都是在一个页面跳转路由,另一个原因是sessionStorage可以保证打开页面时sessionStorage的数据为空,而如果是localStorage则会读取上一次打开页面的数据。
然后是怎么用sessionStorage来保存state里的数据。
第一种方案
由于state里的数据是响应式,所以sessionStorage存储也要跟随变化。又由于vuex规定所有state里数据必须通过mutation方法来修改,所以第一种方案就是mutation修改state的同时修改sessionStorage对应存储的属性
第二种方案
第一种方案确实可以解决问题,但这种方法很明显让人觉得怪异,都这样了,那不如直接用sessionStorage来做状态管理。
那怎么才能不用每次修改state时同时也要修改sessionStorage呢?这时我们可以换一个思路,因为我们是只有在刷新页面时才会丢失state里的数据,那有没有办法在点击页面刷新时先将state数据保存到sessionStorage,然后才真正刷新页面?
当然有,beforeunload这个事件在页面刷新时先触发的。那这个事件应该在哪里触发呢?我们总不能每个页面都监听这个事件,所以我选择放在app.vue这个入口组件中,这样就可以保证每次刷新页面都可以触发。
具体的代码如下:在入口App.vue下加入
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
created() {
//在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem("store")) {
this.$store.replaceState(
Object.assign(
{},
this.$store.state,
JSON.parse(sessionStorage.getItem("store"))
)
)
}
//在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener("beforeunload", () => {
sessionStorage.setItem("store", JSON.stringify(this.$store.state))
})
},
}
</script>
<style>
</style>
问题汇总
1、VUE问题
1.1、jsp中使用vue,加载页面显示{}的问题
描述:在jsp中vue的时候,页面加载的时候总会有{{ }}闪烁,看着太烦人了。
解决方案:
①样式中添加
[v-cloak] {
display: none;
}
②vue根元素添加
<body>
... 其它元素 ...
<div id="app" v-cloak></div>
</body>
1.2、动态加载代码 点击事件不管用 解决办法
vue 中拼接代码
vue 中处理
1.3、vue使用el-dialog关闭后重置数据的最佳方法
此方法试用所有需要重置数据的场景
el-dialog打开一次之后,再次打开之前的数据不会销毁,依然存在。
我们需要在关闭后重新初始化数据。
重置表单的方法
this.$refs[formRef].resetFields();
复制代码
有些数据不是表单中的数据,也需要重置。
难道一个个的重新手动赋值吗?当然可以,就是比较麻烦。好在vue帮我们保存了一份原始数据,直接把data复制为原始数据即可
this.$data = this.$options.data();
以上就可以正常运行, 但是如果data中有表单验证相关,会导致控制台出现报错信息,如下面代码中的ruleValidate,排除即可。
data () {
return {
dialogVisible: false,
submitLoading: false,
model: {
id: 0,
carCard: "",
driver: "",
remark: "",
},
ruleValidate: {
carCard: {required: true, message: "不能为空", trigger: "blur"},
},
};
},
重置表单数据,使用的地方特别多,我们封装为全局方法
//重置表单,formRef为表单的ref值,excludeFields为要排除重新初始化值得属性
Vue.prototype.$reset = function (formRef, ...excludeFields) {
this.$refs[formRef].resetFields();
let obj1 = this.$data;
let obj2 = this.$options.data.call(this);
if (!excludeFields || excludeFields.length === 0) {
excludeFields = ["ruleValidate"];
}
for (let attrName in obj1) {
if (excludeFields && excludeFields.includes(attrName)) {
continue;
}
obj1[attrName] = obj2[attrName];
}
};
使用方法
<template>
<el-dialog
v-el-drag-dialog
:close-on-click-modal="false"
:visible.sync="dialogVisible"
:title="model.id === 0 ? '新增车辆' : '编辑车辆'"
class="car-edit"
width="450px"
top="5vh"
@close="$reset('form')">
<el-form ref="form"
:model="model"
:rules="ruleValidate"
class="formFillWidth"
label-width="50px">
<el-form-item label="车牌" prop="carCard">
<el-input v-model="model.carCard" placeholder="请输入"/>
</el-form-item>
<el-form-item label="司机" prop="driver">
<el-input v-model="model.driver" placeholder="请输入"/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="model.remark" placeholder="请输入"/>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button :loading="submitLoading" type="primary" @click="handleSubmit">保存</el-button>
</span>
</el-dialog>
</template>
2、GIT提交冲突解决
3、element使用问题
3.1、el-dialog相互调用是遮罩问题
A页面中调起B(dialog)页面,B(dialog)页面调起C(dialog)页面,会出现遮罩问题,如下图:
解决办法:在C页面中加入append-to-body,如下图: