Vue+Vue-Router+Vuex+SSR项目

共 7128字,需浏览 15分钟

 ·

2022-02-22 01:22

Vue-Vue-Router-Vuex-SSR

  1. Vue+Webpack工程流搭建
  2. Vue+Vue-Router+Vuex项目架构

服务端渲染

现在的前端框架是纯客户端渲染的,(请求🤴网站的时候,返回的html是没有什么内容的),存在问题是没有办法seo, 白屏时间较长。需要等待js加载完成,执行完成之后才会显示内容。

服务端渲染解决这些问题。

webpack升级注意 ⚠️ :1. 版本变化 2. 配置变化 3. 插件变化

vue-loader配置

const isDev = process.env.NODE_ENV === 'development'
// vue-loader.config.js
const docsLoader = require.resolve('./doc-loader')
module.exports = (isDev) => {
 return {
  preserveWhitespacetrue// 去掉后面的空格
  extractCSS: !isDev, // 单独打包到css某文件
  cssModules: {},
  // hotReload: false, // 根据环境变量生成
  loaders: {
   'docs': docsLoader,
  },
  preLoader: {
  },
 }
}
module.exports = (isDev) => {
 return {
  preserveWhitespacetrue,
  extractCSS: !isDev,
  cssModules: {},
 }
}

css module配置

module.exports = (isDev) => {
 return {
  preserveWhitespacetrue,
  extractCSS: !isDev,
  cssModules: {
   localIdentName: isDev ? '[path]-[name]-[hash:base64:5]' : '[hash:base64:5]',
   // localIdentName: '[path]-[name]-[hash:base64:5]',
   camelCasetrue
  }
 }
}

安装使用eslint和editorconfig以及precommit

npm i eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node

创建.eslintrc

{
 "extends""standard"
}
npm i eslint-plugin-html
// package.json

"script": {
 "clean""rimraf dist",
 "lint""eslint --ext .js --ext .jsx --ext .vue client/",
 "build""npm run clean && npm run build:client",
 //自动修复
 "lint-fix""eslint --fix --ext .js --ext .jsx --ext .vue client/"
 "precommit""npm run lint-fix",
}
npm i webpack -D
// webpack 4
npm uninstall webpack webpack-dev-server webpack-merge -D
// webpack 升级
npm i webpack webpack-dev-server webpack-merge webpack-cli -D

npm uninstall babel-loader extract-text-webpack-plugin file-loader html-webpack-plugin -D

webpack.config.base.js

const config = {
 mode: process.env.NODE_ENV, // development || production
 target'web',
}
npm i eslint-loader babel-eslint

Vue的一些点

import Vue from 'vue'

const div = document.createElement('div')
document.body.appendChild(div)

new Vue({
 el: div,
 template'
this is content
'

})

// webpack.config.practice.js
resolve: {
 alias: {
  'vue': path.join(__dirname, '../node_modules/vue/dist/vue.esm.js')
 }
}

index.js

import Vue from 'vue'
import App from './app.vue'

import './assets/styles/global.style'

const root = document.createElement('div')
document.body.appendChild(root)

new Vue({
 rendeer(h) => h(App)
}).$mount(root)

VUE实例

  • Vue实例的创建和作用
  • Vue实例的属性
  • Vue实例的方法
import Vue from 'vue'
const app = new Vue({
 // el: '#root',
 template'{{text}}
',
 data: {
  text0
 }
})

app.$mount('#root')

setInterval(()=>{
 // app.text += 1
 // app.$options.data += 1 不可以使用
 app.$data.text += 1 // 可以使用
},1000)

console.log(app.$data)
console.log(app.$props)
console.log(app.$el)
console.log(app.$options)

console.log(app.$slots)
console.log(app.$scopedSlots)

console.log(app.$ref) // div节点/组件实例

console.log(app.$isServer) // 服务端渲染

app.$options.render = (h) => {
 return h('div', {}, 'new render function')
}  
props: {
 filter: {
  typeString,
  requiredtrue
 },
 todos: {
  typeArray,
  requiredtrue
 }
}

watch

const app = new Vue({
 // el: '#root',
 template'{{text}}
',
 data: {
  text0
 },
 watch: {
  text(newText, oldText) {
   console.log('${newText}:${oldText}')
  }
 }
})
app.$mount('#root')
const unWatch = app.$watch('text', (newText, oldText){
 console.log(`${nextText}:${oldText}`)
})
setTimeout(()=>{
 unWatch() // 取消 2秒后
}, 2000)

事件监听:

app.$on('test', () => {
 console.log('test emited')
})

//触发事件
app.$emit('test')

app.$on('test', (a, b) => {
 console.log('test emited ${a} ${b}')
})

//触发事件
app.$emit('test'12)
$once只监听一次
app.$once('test', (a, b) => {
 console.log('test emited ${a} ${b}')
})

forceUpdate强制组件渲染一次

非响应式的:

// 值在变,但不会导致重新渲染
data: {
 text0,
 obj: {}
}

setInterval(() => {
 app.obj.a = app.text
 app.$forceUpdate()
},1000)

一直在渲染会让你的性能降低

某个对象上的,某个属性名,给他定义一个值:

let i = 0
setInterval(()=>{
 i++
 app.$set(app.obj, 'a', i) // 变化,某个对象上的某个属性值给它一个值
},1000)

vm.$nextTick([callback])

等DOM节点渲染完成。Vue在下一次更新的时候调用callback

将回调延迟到下次DOM更新循环之后执行,在修改数据之后立即使用它,然后等待DOM更新,它跟全局方法Vue.nextTick一样,不同的是回调的this自动绑定到调用它的实例上。

2.1.0新增,如果没有提供回调且支持Promise的环境中,则返回一个Promise.注意polyfill.

vue生命周期

new Vue({
 el'#root',
 template'
{{text}}
'
,
 data: {
  text'jeskson'
 },
 beforeCreate() {
  console.log(this'beforeCreate')
 },
 created() {
  console.log(this'created')
 },
 beforeMount() {
  console.log(this'beforeMount')
 },
 mounted() {
  console.log(this'beforeCreate')
 },
 beforeUpdate() {
  console.log(this'beforeUpdate')
 },
 updated() {
  console.log(this'updated')
 },
 activated() {
  console.log(this'activated')
 },
 deactivated() {
  console.log(this'deactivated')
 },
 beforeDestroy() {
  console.log(this'beforeDestroy')
 },
 destroyed() {
  console.log(this'destroyed')
 },
 render(h) {
  console.log('render function invoked')
  return h('div', {}, this.text)
 },
 renderError(h, err) {
  return h('div', {}, err.stack)
 },
 errorCaptured() {
  // 向上冒泡,并且正式环境可以使用
 }
})
undefined 'beforeCreate'
undefined 'created'
"root"></div> "beforeMount"
0div> "mounted"

template`
 name: {{name}} 
`


computed: {
 name() {
  return `${this.firstName} ${this.lastName}`
 } 
}

for="(item, index) in arr">遍历数组</li>
li>

v-model.number 数字
v-model.trim 去掉空格

 <input type="radio" value="one" v-model="pick"/>
 <input type="radio" value="two" v-model="pick"/>
</div>

定义组件

import Vue from 'vue'

const compoent = {
 template'
This is compoent
'

}

// Vue.component('CompOne', compoent)

new Vue({
 components: {
  CompOne: compoent
 },
 el'#root',
 template''
})

// 报错
const compoent = {
 template'
{{text}}
'
,
 data: {
  text123
 }
}

[vue warn] the "data" option should be a function that returns a per-instance value in component definitions.

props

// 子组件
const compoent = {
 props: {
  activeBoolean,
  propOneString
 },
 template`
  

   
   {{propOne}}
   see me if active
  

 `
,
 data() {
  return {
   text0
  }
 }
}

// 父组件传递
"true" prop-one="text1"></comp-one>

不允许在组件内部修改this.propOne='inner content',

vue warn: avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "propOne"
// 子组件触发props修改

// 子组件
methods: {
 handleChange() {
  this.$emit('change')
 }
}

// 子组件props

props: ['active''propOne'],

props: {
 active: {
  typeBoolean,
  requiredtrue,
  defaulttrue
 }
}

props: {
 active: {
  // type: Boolean,
  // required: true,
  validator(value) {
   return typeof value === 'boolean'
  }
 }
}
const CompVue = Vue.extend(compoent)

new CompVue({
 el'#root',
 propsData: {
  propOne'xxx'
 }
})

🌰

const parent = new Vue({
 name'parent'
})

const componet2 = {
 extends: compoent,
 data () {
  return {
   text1
  }
 },
 mounted () {
  console.log(this.$parent.$options.name) // Root
  this.$parent.text = '12345'
 }
}

new Vue({
 parent: parent,
 name'Root',
 el'#root',
 mounted () {
  console.log(this.$parent.$options.name) // parent
 },
 components: {
  Comp: component2
 },
 data: {
  text2333
 },
 template`
  
{{text}}

  
 `

})

🌰栗子 input

// 子组件
handleInput (e) {
 this.$emit('input', e.target.value)
}

// 父组件
... :value="value" @input="value = arguments[0]"

// 🌰
const component = {
 props: ['value'],
 template`
  

   
  

 `
,
 methods: {
  handleInput (e) {
   this.$emit('input', e.target.value)
  }
 }
}

const component = {
 model:; {
  prop'value1',
  event'change'
 },
 props: ['value1'],
 template`
  

   
  

 `
,
 methods: {
  handleInput (e) {
   this.$emit('change', e.target.value)
  }
 }
}

属性✍️

slot

const component = {
 template`
  
   
    
   

   
    
   

  

 `,
 data () {
  return {
   style: {
    width'200px';
    height: '200px';
    border: '1px solid #aaa'
   }
  }
 }
}


 <span slot="header"> this is header span>
 <span slot="body"> this is body span>
</comp-one>

slot-scope

特殊🌰

template: `
 
  
 
`
,

// 父组件 - 组件内部使用的变量

 <span slot-scope="props"> {{props.value}} {{props.aaa}} span>
</comp-one>

provide inject

provide : {
 yeyethis,
 valuethis.value, // 错误
},

provide() {
 const data = {}

 Object.defineProperty(data, 'value', {
  get() => this.value,
  enumerabletrue
 })

 return {
  yeyethis,
  data
  // value: this.value
 }
}

// 子组件
inject: ['yeye''data']
template'
child component: {{data.value}}
'

new Vue, beforeCreate, created, beforeMount, mounted

没有el,就没有挂载beforeMount, mounted

若使用const app , app.$mount('#root') 就会执行

setInterval(() => {
 app.text = app.text += 1
},1000)

// 主动销毁
app.$destroy()

组件:

activated() {
 // 在组件
 console.log(this'activated')
}
deactivated() {
 // 在组件
 console.log(this'deactivated')
}

变化:

el:

undefined 'beforeCreate'
undefined 'created'
"root"></div> "beforeMount"
0div> "mounted"

dom有关的放在mounted里面,数据可created,mounted

服务端渲染不会执行(因为服务端没有Dom环境,所有更本就没有这些内容),beforeMount,mounted, 会执行create,beforeCreate

生命周期

new Vue()

Init Events(事件已经ok了) & Lifecycle

beforeCreate(不要修改data里的数据)

Init injections & reactivity

created(数据操作)

Has 'el' option ? No when vm.$mount(el) is called

YES Has 'template' option ?

有template属性:

解析render

render(h) {
 console.log('render function invoked')
 return h('div', {}, this.text)
}

Waiting for update signal form WDS...

undefined "beforeCreate"
undefined "created"
 "root"></div> "beforeMount"
render function invoked
 
0div> "mounted"

Yes:有 Compile template into render function

No:没有 Compile el's  outerHTML as template

beforeMount

Create vm.$el and replace 'el' with it

mounted

Mounted(实例创建完成)

beforeDestroy

Teardown watchers, child components and event listeners

destroyed

renderError

打包正式上线不会调用的,开发的时候会使用

renderError (h, err) {
 return h('div', {}, err.stack)
}

正式开发环境,收集线上的错误

errorCaptured() {
 
}

vue里面的data绑定到template

watch监听到某个一数据的变化,指定某个操作,(服务器使用)

Vue的原生指令

Vue的组件 render function

render (createElement) {
 return createElement('comp-one', {
  ref'comp'
 }, [
  createElement('span', {
   ref'span'
  }, this.value)
 ])
}

render (createElement) {
 return createElement('div', {
  stylethis.style,
  on: {
   click() => { this.$emit('click') }
  }
 }, this.$slots.default)
}

Vue-Router && Vuex

import Router from 'vue-router'

import routers from './routes'

exports default () => {
 return new Router({
  routers,
  mode'history',
  // base: '/base/',
  linkActiveClass'active-link'// 子集
  linkExactActiveClass'exact-active-link'// 准确目标
  scrollBehavior (to, from, savedPosition) {
   if (savedPosition) {
    return savedPosition
   } else {
    return { x0y0 }
   }
  },
  // parseQuery (query) {
  // },
  // stringifyQuery (obj) {
  // }
 })
}

transition

"fade">
 <router-view />
</transition>
this.$route
// path: '/app/:id',  to="/app/123"
fullPath'/app/123'
hash""
matched: [{}]
meta: {title:''}
name'app'
params: {id'123'}
path'/app/123'
query: {}
// routes.js
{
 path'/app/:id',
 propstrue,
 component: Todo,
 name'app',
 meta: {
  title'this is app',
  description'asdasd'
 }
}

// todo.vue

props: ['id'],
mounted () {
 console.log(this.id)
}
{
 path'/login',
 components: {
  default: Login,
  a: Todo
 }
}

Vue-router 导航守卫

import createRouter from './config/router'

Vue.use(VueRouter)

const router = createRouter()

router.beforeEach((to, from, next) => {
 // 做登录验证操作
 console.log('before each invoked')
 // next()
 if (to.fullPath === '/app') {
  next('/login'// 如果没有登录的话跳转到登录页面 next({ path: '/login' })
 } else {
  next() // 符合条件
 }
})

router.beforeResolve((to, from, next) => {
 console.log('before resolve invoked')
 next()
})

router.afterEach((to, from) => {
 console.log('after each invoked')
})
// routes.js

{
 path'/app',
 beforeEnter (to, from, next) {
  console.log('app route before enter')
  next() // 只有当点击进入,才会调用
 }
}

// before each invoked
// app route before enter
// before resolve invoked
// after each invoked
// todo.vue
export default {
 beforeRouteEnter (to, from, next) {
  console.log('todo before enter')
  next()
 },
 
 beforeRouteUpdate (to, from, next) {
  console.log('todo update enter')
  next()
 }, 
 
 beforeRouteLeave (to, from, next) {
  console.log('todo leave enter')
  next()
 },

}

离开:

todo leave enter
before each invoked
before resolve invoked
after each invoked

进入:

before each invoked
app route before enter
todo before enter
before resolve invoked
after each invoked

Vuex集成

import Vuex from 'vuex'

const store = new Vuex.Store({
 state: {
  count0
 },
 mutations: {
  updateCount (state, num) {
   state.count = num
  }
 }
})

export default store

服务端渲染

import createRouter from './config/router'
import createStore from './store/store'

Vue.use(VueRouter)
Vue.use(Vuex)

const router = createRouter()
const store = createStore()

Vuex 中 state 和 getters

// store.js
import Vuex from 'vuex'
import defaultState from './state/state'
import mutations from './mutations/mutations'
import getters from './getters/getters'

export default () => {
 return new Vuex.Store({
  state: defaultState,
  mutations,
  getters
 })
}
// state/state.js
export default {
 count0,
 firstName'dada',
 lastName'dada'
}
// mutations/mutations.js
export default {
 updateCount (state, num) {
  state.count = num
 }
}

getters

// getters/getters.js =========== computed
export default {
 fullName(state) {
  return `${state.firstName} ${state.lastName}`
 }
}
// app.vue
computed: {
 count () {
  return this.$store.state.count
 },
 
 fullName () {
  return this.$store.getters.fullName
 }
}

快速使用

import {
 mapState,
 mapGetters
from 'vuex'

computed: {
 // ...mapState(['count']),
 // ...mapState({
 //  counter: 'count'
 // }),
 ...mapState({
  counter(state) => state.count
 }),
 ...mapGetters(['fullName'])
}

Vuex 中 mutation 和 action

// 开发环境 store.js
const isDev = process.env.NODE_ENV === 'development'

export default () => {
 return new Vuex.Store({
  strict: isDev,
  state: defaultState,
  mutations,
  getters
 })
}
// actions/actions.js
// dispatch 触发 actions 的
// 异步
export default {
 updateCountAsync (store, data) {
  setTimeout(() => {
   store.commit('updateCount', data.num)
  }, data.time)
 } 
}
// store.js
import Vuex from 'vuex'
import defaultState from './state/state'
import mutations from './mutations/mutations'
import getters from './getters/getters'
import actions from './actions/actions'

export default () => {
 return new Vuex.Store({
  state: defaultState,
  mutations,
  getters,
  actions
 })
}
import {
 mapState,
 mapGetters,
 mapActions,
 mapMutations
from 'vuex'

mounted () {
 this.updateCountAsync({
  num5,
  time2000
 })
}

// mapActions mapMutations 操作
methods: {
 ...mapActions(['updateCountAsync']),
 ...mapMutations(['updateCount'])
}

Vuex 中的模块

// app.vue
mounted () {
 this['a/updateText']('123')
}

methods: {
 ...mapActions(['updateCountAsync']),
 ...mapMutations(['updateCount''a/updateText'])
}

computed: {
 ...mapState({
  counter(state) => state.count,
  textAstate => state.a.text
 }),
 ...mapGetters(['fullName''a/textPlus'])
 textA () {
  return this.$store.state.a.text
 }
}

// store.js
modules {
 a: {
  namespacedtrue
  state: {
   text1
  },
  mutations: {
   updateText (state, text) {
    console.log('a.state', state)
    state.text = text
   }
  },
  getters: {
   textPlus (state, getters, rootState) {
    return state.text + rootState.count + rootState.b.text
   }
  },
  actions: {
   add ({ state, commit, rootState }) {
    commit('updateText', rootState.count) // 全局找{ root: true }
    // commit('updateCount', rootState.count, { root: true }) // 全局找{ root: true }
   }
  }
 },
 
 b: {
  state: {
   text2
  },
  actions: {
   testAction ({ commit }) {
    commit('a/updateText''test text', { roottrue })
   }
  }
 }
}

store.hotUpdate({})

Vuex 中的 API

// index.js
const router = createRouter()
const store = createStore()

store.registerModule('c', {
 state: {
  text3
 }
})

// 监听这个值的变化
store.watch((state) => state.count + 1, (newCount) => {
 console.log(newCount)
})

// 订阅
store.subscribe((mutation, state) => {
 console.log(mutation.type) // 调用哪个mutation
 console.log(mutation.payload) // mutation 接收的参数 传入的值
})

store.subscribeAction((action, state) => {
 console.log(action.type)
 console.log(action.payload)
})

store.unregisterModule('c')
// store.js
export default () => {
 const store = new Vuex.Store({
  strict: isDev,
  state: defaultState,
  mutations,
  getters,
  actions,
  plugins: [
   (store) => {
    console.log('my plugin invoked')
   }
  ]
 })
}

// my plugin invoked
// before each invoked
// before resolve invoked
// after each invoked

服务端渲染构建流程

访问服务端渲染页面:webpack server compiler -> nodejs server 3333端口

  1. 纯前端渲染:webpack dev server 8000 端口
  2. 访问服务端渲染页面:webpack server compiler server 创建 server bundle -> nodejs server 3333端
npm i vue -D // devDependencies

npm i vue -S // dependencies

npm i vue-server-renderer

npm i koa-router -S

npm i axios -S

server 服务端渲染

const koa = require('koa')
const app = new Koa()
const isDev = process.env.NODE_ENV = 'development'

dev-ssr.js

const Router = require('koa-router')
const axios = require('axios')
const MemoryFS = require('memory-fs')
const webpack = require('webpack')
const VueServerRenderer = require('vue-server-render')

组件开发

notification 通知