Vue 实战
vue cli4 介绍
安装
npm install -g @vue/cli
# OR
yarn global add @vue/cli
常用命令
查看版本
vue --version
更新
vue update -g @vue/cli
# OR
yarn global upgrade @vue/cli@4
其他
* vue --help
* vue --version
* vue inspect // 看到webpack 配置
* vue inspect > output.js
* vue inspect --mode production > output.prod.js // 可以方便输出在一个文件中
* vue create
* vue ui
创建项目
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i
> to invert selection)
❯◉ Lint on save // 保存时校验
◯ Lint and fix on commit // fix 可以修复 空格或者;
项目分析
首页 + 详情页: 左侧内容不同
header.vue 通过插槽 插入不同内容
主页布局: 左侧导航 + 行文列表(宽度自适应) + 链接内容
功能: 左侧导航吸顶功能 中间长列表加载 链接侧有搜索功能
详情页 多一个头部内容 有相关推荐 router方面
App.vue
<router-view/> 功能:乘放在router/index.js中声明的组件
组件默认会在#app的样式中居中
布局问题
a 标签 margin-top 不生效
开发者工具中 <div data-v-5f2c...> 代表作用域样式
引入less reset-css:
- 安装less less-loader reset-css
yarn add reset-css
yarn add less less-loader -D
- 全局建里styles/index.less
// styles/index.less
@import 'reset-css/less/reset'
// main.js
import './styles/index.less'
注意less 与 less-loader版本适配问题,否则this.getOptions not a function
首页布局:中间自适应 左边向左浮动,接下来右边向右浮动,中间自适应形成BFC,不会向两边扩展
<div class="layout-body">
<div class="layout-body-left">left</div>
<div class="layout-body-right">rigth</div>
<div class="layout-body-middle">middle</div>
</div>
.layout-body {
&-left {
float: left;
width: 200px;
height: 600px;
margin-right: 10px;
background-color: red;
}
&-right {
float: right;
width: 100px;
height: 300px;
margin-left: 10px;
background-color: yellow;
}
&-middle {
overflow: auto;
height: 700px;
background-color: blue;
}
}
浮动布局要清除浮动:否则父元素不会按照最高的自元素撑开 原理:在父类内的最后加上一个伪元素,使父元素的最大高度违子元素高度
吸顶功能 css 实现
position: sticky;
top: 0;
左侧内容
给LinkButton 中的 a 传入target:_blank 属性:直接写在 home.vue 会传在 button 的 div 上
改变此情况需要透传,利用inheritAttrs: false
// LinkButton.vue
inheritAttrs: false
在Home.vue 上
<link-button linkHref="/redian" target="_blank">热点</link-button>
LinkButton.vue 中没有的声明的props 和 attrs 都会在attrs,因此直接绑定,但不包含 class 和 style
<a :href="linkHref" v-bind="$attrs"><slot></slot></a>
<a rel="noopener"> 从哪个页面点到当前页面
<a rel="noereferer"> 不要加referer
<a rel="nofollow"> 爬虫不要再往下爬
中间数据
NewsList.vue 直接插入中间 slot
发送请求获取这部分数据
新建 http 文件夹,使用 axios 获取数据
// src/http/index.js
import axios from 'axios'
const io = axios.create({ // 生成实例
baseURL: '/api', //所有请求都会加一个前缀
timeout: 30 * 1000 // 超时
})
// 请求/响应中间件的处理
io.interceptors.response.use(res => res.data, err => {
console.error(err)
})
export default io
news API
// src/http/news.js
import http from './index'
export const getNewsList = () => {
http.get('/getNewList')
}
NewsList 组件中
import * as newsApi from '@/http/news'
export default {
mounted() {
this.getNewsList()
},
methods: {
getNewsList () {
newsApi.getNewsList()
}
}
}
一些服务端代码
// server/index.js
const path = require('path');
const Koa = require('koa');
const KoaStatic = require('koa-static');
const KoaMount = require('koa-mount');
const ApiRequest = require('./api');
// const { isProd } = require('../utils/env');
// const router = isProd ? require('./router.prod') : require('./router.dev');
const app = new Koa();
const port = process.env.PORT || 3000;
// ajax
app.use(KoaMount('/api', ApiRequest));
// 静态资源
app.use(KoaMount('/assets', KoaStatic(path.resolve(__dirname, '../dist/assets'))));
// 页面路由
app.use(router.routes());
app.listen(port, () => {
console.log(`server started at port: ${port}`);
});
api.js
const xss = require('xss');
module.exports = async function (ctx) {
console.log(ctx.path, process.env.VUE_ENV);
if (ctx.path === '/getNewsList') {
ctx.body = await new Promise(((resolve) => {
setTimeout(() => {
resolve(require('./mock/news-list1.json'));
}, 2000);
}));
return;
}
if (ctx.path.startsWith('/getNewsDetail')) {
if (ctx.query.newsId === '6921941954200617479') {
const data = require('./mock/news-detail-6921941954200617479.json');
data.data = xss(data.data);
ctx.body = data;
} else {
ctx.body = require('./mock/news-detail-a6928669811266601483.json');
}
if (ctx.query.newsId == 3333) {
throw new Error('sssss');
}
return;
}
ctx.body = {
error: 1,
data: {},
};
};
以及mock 数据
启动服务器:
node ./server/index.js
本地开发拿到 3000 端口的数据:使用 devServer 的配置
// ./vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
};
异步请求
// NewsList.vue
data () {
return {
newsList: []
}
},
methods: {
async getNewsList() {
const res = await newsApi.getNewsList();
console.log(res);
if (res.message === 'success') {
this.newsList = res.data;
}
},
},
优化: 由于每个 item 内部信息不需要都响应,因为也不知道点还是不点,因此冻结res.data 内部的数据,提高运行效率
this.newsList = Object.freeze(res.data);
}
左侧页前端路由问题
<div>
<button @click="handleClick('1')">按钮1</button>
<button @click="handleClick('2')">按钮2</button>
<span>文案 {{ message }}</span>
</div>
data() {
return {
message: '',
};
},
methods: {
handleClick(val) {
this.message = val;
},
},
切换标签的时候,会有异步请求,按钮1返回时间长,按钮2返回快,先点击按钮1,再按按钮2,最终从2变成1 模拟一个异步
handleClick(val, timeout = 100) {
setTimeout(() => {
this.message = val;
}, timeout);
},
问题:网路情况不稳定时,会出现跳屏
解决方案:
- 数据结构
data () {
msgObj: {
1: '1',
2: '2',
},
currentKey: '',
msg: ''
},
methods: {
changeMsg(value, timeout = 100) {
this.currentKey = value // 同步改currentKey
setTimeout(() => {
// 取值取obj当中key的值
this.message = this.msgObj[this.currentKey];
}, timeout);
}
}
- 取消重复请求 点击2 之后不能够再请求1,直接走error,不会触发之前的事件
SSR
正常情况下,localhost 没有内容,内容都在<div id='app'> 中
内容是加载了js,js 异步请求数据,拿到数据,填充到中间
--> 问题:首屏加载时间长(先加载html,再加载js,再执行js,再请求,拿到数据再渲染)
--> 1. 如果 数据直接都在 <div id='app'> 返回 html 的时候可以直接渲染,减少白屏时间
--> 2. 不利于爬虫爬信息,SEO 不友好
以上即为做SSR的原因
但是SSR数据过大会造成负担,如果浏览较大,还是会返回给客户端渲染
架构:
如果项目重,代码有客户端入口,以及服务端入口 --> 通过webpack 打包之后有server bundle(运行在服务端,直接连同数据渲染到HTML中) 以及 client bundle
服务端渲染到HTML 之后,还要通过水合(hydrate)客户端 bundle,因为服务端只能显示页面,无法处理事件及定时器等
问题
-
如何区分客户端打包还是服务端打包
- cross-env 传入环境变量,区分server client
"build:server": "cross-env VUE_ENV=server vue-cli-service build --no-clean", // nodemon.json 设置环境变量, pm2也可以设置 "env": { "VUE_ENV": "server", }, process.env.VUE_ENV === 'server' -
添加两个webpack入口,
- server-entry
- client-entry 仅仅挂载$mount('#app')
import crateVueApp from './app' const {app, store} = crateVueApp() if(window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } app.$mount('#app') -
webpack添加entry配置
-
server-entry
- 输出一个函数,返回promise
data 作为函数,为了不同的组件不共享数据。工厂模式每次返回新的实例
-
热更新
- 改用代码获取webpack配置及启动webpack服务
// 1. 获取webpack配置文件 const webpackConfig = require('@vue/cli-service/webpack.config') // 2. 编译webpack配置文件 const serverComplier = webpack(webpackConfig)- 将webpack打包的结果输出到内存中
// 3. 设置webpack打包到内存中 const mfs = new MemoryFS() serverComplier.outputFileSystem = mfs- 监听webpack打包的结果,更新bundle
// 4.监听文件的修改,获取最新的vue-ssr-server-bundle.json let bundle serverComplier.watch({}, (err, stats) => { if(err) { throw(err) } stats = stats.toJson() stats.errors.forEach(error => console.error(error)) stats.warnings.forEach(warning => console.warn(warning)) const bundlePath = path.join(webpackConfig.output.path, 'vue-ssr-server-bundle.json') bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8')) console.log('new bundle created') })