Vue 实战 - 04

152 阅读5分钟

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:

  1. 安装less less-loader reset-css
yarn add reset-css
yarn add less less-loader -D
  1. 全局建里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 上

image.png

改变此情况需要透传,利用inheritAttrs: false

// LinkButton.vue
inheritAttrs: false

在Home.vue 上

<link-button linkHref="/redian" target="_blank">热点</link-button>

LinkButton.vue 中没有的声明的props 和 attrs 都会在propsprops 和 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);
    },

问题:网路情况不稳定时,会出现跳屏

解决方案:

  1. 数据结构
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);
  }
}
  1. 取消重复请求 点击2 之后不能够再请求1,直接走error,不会触发之前的事件

SSR

正常情况下,localhost 没有内容,内容都在<div id='app'>image.png

内容是加载了js,js 异步请求数据,拿到数据,填充到中间

--> 问题:首屏加载时间长(先加载html,再加载js,再执行js,再请求,拿到数据再渲染)

--> 1. 如果 数据直接都在 <div id='app'> 返回 html 的时候可以直接渲染,减少白屏时间

--> 2. 不利于爬虫爬信息,SEO 不友好

以上即为做SSR的原因

但是SSR数据过大会造成负担,如果浏览较大,还是会返回给客户端渲染

架构: image.png

如果项目重,代码有客户端入口,以及服务端入口 --> 通过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')
    })