jackieli123723 / jackieli123723.github.io

✅lilidong 个人博客
9 stars 0 forks source link

vue2踩坑干货实践(spa && ssr) #31

Open jackieli123723 opened 6 years ago

jackieli123723 commented 6 years ago

vue日常开发(spa和ssr模式)

1.vue操作需要登录后跳转达到上次的路由

//login.vue 登录函数
      login(){
            this.$store.commit("updateLoading",true);
            let param = {
              username:this.username,
              password:this.password
            }
            this.$http.post(this.serverRoot+"login",param).then((response) => {
                if(response.body.code == 200){
                    storage.save("userInfo",{
                      "username":this.username,
                      "productNum":response.body.data.cartInfo.productNum,
                      "waitPayOrderNum":response.body.data.orderInfo.waitPayOrderNum,
                    });
                    localStore.save("userInfo",{
                      "sessionId":response.body.data.sessionId
                    });
                    storage.save('shopCarNum',Number(response.body.data.cartInfo.productNum))
                    if (storage.fetch("beforeLogin")!='') {
                      this.$router.replace({
                          path: storage.fetch("beforeLogin")
                      })
                      storage.delete("beforeLogin")
                    }else{
                      this.$router.replace({
                          path: '/'
                      })
                    }
                }else{
                    this.show_msg("账号或密码错误");
                }
                this.$store.commit("updateLoading",false);
            }, (response) => {
                this.$store.commit("updateLoading",false);
                this.show_msg("网络错误请重试!")
            })
        },

//main.js
//路由拦截处理来存储beforeLogin理由
//要做到浏览器后退不记录位置写入history要在后退到登录前的路由都要使用replace模式才可以,push的模式会导致回退多次死循环(重点)
//登录成功后replace跳转到searching-用replace否则后退还会回到login路由
router.beforeEach((to, from, next) => {
  let user = storage.fetch('userInfo')
  if (!user && to.path != '/login' && to.matched.some(record => record.meta.requiresAuth)) {
    next({
      path: '/login'
    })
  } else if(to.path == '/login'){
    var fromPath=from.path
    storage.save('beforeLogin',fromPath)
    next()
  }else{
    next()
  }
})

//gameArea.vue
//要做到浏览器后退不记录位置写入history要在后退到登录前的路由都要使用replace模式才可以,push的模式会导致回退多次死循环(用法replace)

<!-- 登录组件 -->
<delconfirm  del-title="请先登录" del-sure-text="去登录" v-on:delItem="toLog"></delconfirm>

    toLog(){
            this.$router.replace({
              path: '/login'
            })
        },
        goback(){
            this.$router.push({
              path:'/'
            });
        }

2.spa模式修改为ssr模式window,sessionStorage,localStorage,document,navigator is not defined对象找不到


//1.fastClick放在 client-entry.js

import { createApp } from './app'

const { app, router, store } = createApp()

//fastClick必须放在这里
//不然报错: navigator is not defined
import FastClick from 'fastclick'
FastClick.attach(document.body)

2.server-entry.js 中来用node模拟来环境配置浏览器环境
import { createApp } from './app'

//node环境配置浏览器环境
//必须放在server-entry.js
const WindowMock = require('window-mock').default;
let window = new WindowMock();
global.window = window;
global.localStorage = window.localStorage;
//window-mock中没有sessionStorage对象注释代码不起作用找不到sessionStorage的getItem等方法
//TypeError: Cannot read property 'getItem' of undefined
//    at sessionStorageFetch (src/storage/store.js:32:35)

// global.sessionStorage = window.sessionStorage;

//所有要解决sessionstorage的问题就需要引入sessionstorage来引入暴露sessionstorage对象
global.sessionStorage = require('sessionstorage');
global.document = window.document;

//node-global环境
//ssr渲染封装

//localStorage.getItem可用(window-mock模块)
//sessionStorage.getItem(sessionstorage模块)

//sessionStorage封装

export function sessionStorageFetch(key) {
  return JSON.parse(sessionStorage.getItem(key)||'[]')
}

export function sessionStorageSave (key,items) {
   sessionStorage.setItem(key,JSON.stringify(items))
}

export function sessionStorageDelete(key) {
 sessionStorage.removeItem(key)
}

//localStorage封装
export function localStorageFetch(key) {
  return JSON.parse(localStorage.getItem(key)||'[]')
}

export function localStorageSave (key,items) {
   localStorage.setItem(key,JSON.stringify(items))
}

export function localStorageDelete(key) {
   localStorage.removeItem(key)
}

//use.vue

import {sessionStorageFetch} from '../../storage/store'

 computed:{
    waitPayOrderNum(){
      return  sessionStorageFetch('userInfo').waitPayOrderNum
    }
  }

3.pm2 启动项目增加日志和压缩

const fs = require('fs')
const path = require('path')
const express = require('express')
const favicon = require('serve-favicon')
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
const app = express()
const port = process.env.PORT || 4001
const log4js = require('log4js');
const compression = require('compression');
const logger = log4js.getLogger('server.js');

// const { JSDOM } = require('jsdom');
// const dom = new JSDOM('<!DOCTYPE html></html>', { url: 'http://localhost:'+port });
// global.window = dom.window;
// global.document = window.document;
// global.navigator = window.navigator;

// const WindowMock = require('window-mock').default;
// let window = new WindowMock();
// global.window = window;
// global.localStorage = window.localStorage;
// global.document = window.document;

let renderer
if (isProd) {
  // In production: create server renderer using server bundle and index HTML
  // template from real fs.
  // The server bundle is generated by vue-ssr-webpack-plugin.
  const bundle = require('./dist/vue-ssr-bundle.json')
  // src/index.template.html is processed by html-webpack-plugin to inject
  // build assets and output as dist/index.html.
  const template = fs.readFileSync(resolve('./dist/index.html'), 'utf-8')
  renderer = createRenderer(bundle, template)
  //Content-Encoding:gzip node压缩静态资源
  //ssr必须放在此处才能生效放在外面无效
 app.use(compression());

 log4js.configure('logconfig/log4j.json');
 app.use(log4js.connectLogger(log4js.getLogger("http"), {
   level: 'trace'
 }));

 // 引入log4j,并且加载配置文件

} else {
  // In development: setup the dev server with watch and hot-reload,
  // and create a new renderer on bundle / index template update.
  require('./build/dev-server')(app, (bundle, template) => {
    renderer = createRenderer(bundle, template)
  })
}

function createRenderer (bundle, template) {
  // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer
  return require('vue-server-renderer').createBundleRenderer(bundle, {
    template,
    cache: require('lru-cache')({
      max: 1000,
      maxAge: 1000 * 60 * 15
    })
  })
}

const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache && isProd ? 60 * 60 * 24 * 30 : 0
})

//自带icon修复
app.use('/static', serve('./static', true))
app.use('/dist', serve('./dist', true))
app.use('/service-worker.js', serve('./dist/service-worker.js'))

app.get('*', (req, res) => {
  if (!renderer) {
    return res.end('waiting for compilation... refresh in a moment.')
  }

  if (isProd){
     logger.info('成功返回了dist打包路由')
  }

  const s = Date.now()

  res.setHeader("Content-Type", "text/html")

  const errorHandler = err => {
    if (err && err.code === 404) {
      res.status(404).end('404 | Page Not Found')
    } else {
      // Render Error Page or Redirect
      res.status(500).end('500 | Internal Server Error')
      console.error(`error during render : ${req.url}`)
      console.error(err)
    }
  }

  renderer.renderToStream({ url: req.url })
    .on('error', errorHandler)
    .on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
    .pipe(res)
})

app.listen(port, () => {
  console.log(`server started at http://localhost:${port}`)
  if (isProd){
     logger.info(`道具商城ssr代码部署成功访问端口http://localhost:${port}`)
  }
})

//本地npm start
//线上
// $ NODE_ENV=production pm2 start server.js --name longyuan-mall-c-ssr --watch
// [PM2] Starting E:\jackieli\longyuanWork\ssr-vue-longyuan-store-front-mobile\server.js in fork_mode (1 instance)
// [PM2] Done.
// ┌─────────────────────┬────┬──────┬────────┬────────┬─────────┬────────┬─────┬───────────┬──────────┐
// │ App name            │ id │ mode │ pid    │ status │ restart │ uptime │ cpu │ mem       │ watching │
// ├─────────────────────┼────┼──────┼────────┼────────┼─────────┼────────┼─────┼───────────┼──────────┤
// │ longyuan-mall-c-ssr │ 3  │ fork │ 575064 │ online │ 0       │ 1s     │ 43% │ 35.7 MB   │ enabled  │
// │ ssr-api             │ 0  │ fork │ 213176 │ online │ 0       │ 44h    │ 0%  │ 13.5 MB   │ disabled │
// └─────────────────────┴────┴──────┴────────┴────────┴─────────┴────────┴─────┴───────────┴──────────┘
//  Use `pm2 show <id|name>` to get more details about an app

4.ssr生命周期

生命周期 SSR期间会执行 beforeCreate created beforeMount mounted 只会在客户端执行 会影响到全局的代码,放在beforeMount、mounted中,因为destroy方法只在客户端会执行。

通用平台API 服务端、浏览器都有自己的一些私有变量,比如window、global,使用代码或第三方包时,使用适配全平台的,比如axios在node和浏览器都能用。

自定义命令 自定义的命令很多都直接操作DOM,这在SSR过程中回出错。 两种解决方法:1、更改代码方式,转化为组件;2、再做一个server-render的版本,只在服务端渲染到时候用。

Source Code Structure

单件中避免状态变量 每次请求,创建一个新的vue实例

jackieli123723 commented 6 years ago

vue2.x.x的常用api

var vm = new Vue({
  // 数据
  data: "声明需要响应式绑定的数据对象",
  props: "接收来自父组件的数据",
  propsData: "创建实例时手动传递props,方便测试props",
  computed: "计算属性",
  methods: "定义可以通过vm对象访问的方法",
  watch: "Vue实例化时会调用$watch()方法遍历watch对象的每个属性",
  // DOM
  el: "将页面上已存在的DOM元素作为Vue实例的挂载目标",
  template: "可以替换挂载元素的字符串模板",
  render: "渲染函数,字符串模板的替代方案",
  renderError: "仅用于开发环境,在render()出现错误时,提供另外的渲染输出",
  // 生命周期钩子
  beforeCreate: "发生在Vue实例初始化之后,data observer和event/watcher事件被配置之前",
  created: "发生在Vue实例初始化以及data observer和event/watcher事件被配置之后",
  beforeMount: "挂载开始之前被调用,此时render()首次被调用",
  mounted: "el被新建的vm.$el替换,并挂载到实例上之后调用",
  beforeUpdate: "数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前",
  updated: "数据更改导致虚拟DOM重新渲染和打补丁之后被调用",
  activated: "keep-alive组件激活时调用",
  deactivated: "keep-alive组件停用时调用",
  beforeDestroy: "实例销毁之前调用,Vue实例依然可用",
  destroyed: "Vue实例销毁后调用,事件监听和子实例全部被移除,释放系统资源",
  // 资源
  directives: "包含Vue实例可用指令的哈希表",
  filters: "包含Vue实例可用过滤器的哈希表",
  components: "包含Vue实例可用组件的哈希表",
  // 组合
  parent: "指定当前实例的父实例,子实例用this.$parent访问父实例,父实例通过$children数组访问子实例",
  mixins: "将属性混入Vue实例对象,并在Vue自身实例对象的属性被调用之前得到执行",
  extends: "用于声明继承另一个组件,从而无需使用Vue.extend,便于扩展单文件组件",
  provide&inject: "2个属性需要一起使用,用来向所有子组件注入依赖,类似于React的Context",
  // 其它
  name: "允许组件递归调用自身,便于调试时显示更加友好的警告信息",
  delimiters: "改变模板字符串的风格,默认为{{}}",
  functional: "让组件无状态(没有data)和无实例(没有this上下文)",
  model: "允许自定义组件使用v-model时定制prop和event",
  inheritAttrs: "默认情况下,父作用域的非props属性绑定会应用在子组件的根元素上。当编写嵌套有其它组件或元素的组件时,可以将该属性设置为false关闭这些默认行为",
  comments: "设为true时会保留并且渲染模板中的HTML注释"
});