wepack從0開始配置vue環(huán)境之四: vuessr渲染

github傳送門
webpack之一webpack基礎(chǔ)配置
webpack之二webpack部署優(yōu)化
webpack之三集成vuex和vue-Router

  • 新建webpack配置文件用于配置ssr

  1. 新建/build/webpack.config.server.js:
  2. 新建入口文件/client/server-entry.js
  3. 在配置文件中指定entry的文件為server-entery.js
  4. 在配置文件中指定output.libraryTarget = 'commonjs2' // 指定模塊導(dǎo)出方式, output.path = ''指定一個(gè)新的目錄
  5. 添加externals: Object.keys(require('..package.json').dependencies), 聲明不要打包某些文件[]; 在node端運(yùn)行, 不需要在單獨(dú)打包libs文件到j(luò)s文件里, 直接通過require()方式, 就直接可以調(diào)用node_modules里的模塊
  6. 使用extract-text-webpack-plugin的方式去寫css-loader
  7. 在webpack.definePlugin()里添加VUE_ENV = server變量
  8. 安裝vue-server-renderer到生產(chǎn)環(huán)境, 在server配置中引入server-plugin, 在plugins里添加這個(gè)插件
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const VueServerPlugin = require('vue-server-renderer/server-plugin') // 有了這個(gè)插件, 打包輸出的會是json文件
const baseConfig = require('./webpack.config.base')

const isDev = process.env.NODE_ENV === 'development'

let config

config = merge(baseConfig, {
  target: 'node', // 定義打包出來的js的執(zhí)行環(huán)境
  entry: path.join(__dirname, '../client/server-entry.js'),
  output: {
    libraryTarget: 'commonjs2',
    filename: 'server-entry.js',
    path: path.join(__dirname, '../server-build')
  },
  externals: Object.keys(require('../package.json').dependencies),
  devtool: 'source-map', //ssr用source-map
  module: {
    rules: [{
      test: /\.styl$/,
      use: ExtractTextPlugin.extract({
        fallback: 'vue-style-loader',
        use: [
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: true
            }
          },
          'stylus-loader'
        ]
      })
    }]
  },
  plugins: [
    new ExtractTextPlugin('style.[contenthash:8].css'),
    new webpack.DefinePlugin({
      'process.env.NODE_ENv': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"' // ssr官方規(guī)定
    }),
    new VueServerPlugin()
  ]
})


module.exports = config
  • 使用koa實(shí)現(xiàn)node server

  1. 安裝koa到生產(chǎn)環(huán)境, 新建/server/server.js
  2. 編寫server.js入口代碼:koa可以用try-catch在最外層捕捉到錯(cuò)誤
  3. 安裝koa-router到生產(chǎn)環(huán)境
  4. /server/routers/ssr.js和/server/routers/dev-ssr.js
  5. 編寫開發(fā)環(huán)境邏輯. 安裝axios到生產(chǎn)環(huán)境, 安裝memory-fs到開發(fā)環(huán)境
  6. 引入webpack.config.server.js配置, 使用webpack()方法執(zhí)行打包, 實(shí)例化一個(gè)memoryFs, 將webpack打包輸出道內(nèi)存 outputRileSystem = mfs
  7. 使用watch({}, (err, stats) => {})方法監(jiān)聽每一次webpack打包, 并獲取打包的文件
  8. 創(chuàng)建handleSSR中間件, 處理打包出來的bundle
  9. 新建/server/server.template.ejs, 顯示bundle的數(shù)據(jù), 安裝ejs模塊
  10. 用fs讀取template.ejs里的內(nèi)容
  11. 使用axios去向webpack-dev-server去請求一個(gè)json文件, 從而實(shí)現(xiàn)在devServer和nodeServer間建立聯(lián)系
  12. 修改webpack.config.client.js添加vue-server-renderer, 生成vue-ssr-client-manifest.json
const Router = require('koa-router')
const axios = require('axios')
const path = require('path')
const fs = require('fs')
// 在內(nèi)存里操作文件, 提高效率, 只用在開發(fā)環(huán)境
const MemoryFs = require('memory-fs')
// 直接在nodejs里打包代碼
const webpack = require('webpack')
const VueServerRenderer = require('vue-server-renderer')
// 引入server-render.js
const serverRender = require('./server-render')

// 引入webpack配置文件
const serverConfig = require('../../build/webpack.config.server')
// 在node環(huán)境下執(zhí)行打包命令, 這個(gè)serverCompiler可以調(diào)用run()和watch()方法
const serverCompiler = webpack(serverConfig)
// 實(shí)例化一個(gè)mfs
const mfs = new MemoryFs()
// 指定webpack打包的輸出目錄在內(nèi)存里
serverCompiler.outputFileSystem = mfs

let bundle // 用來記錄webpack每次打包出來的文件

serverCompiler.watch({}, (err, stats) => {
  if (err) throw err
  stats = stats.toJson()
  stats.errors.forEach(err => console.log(err))
  stats.warnings.forEach(warn => console.warn(err))

  const bundlePath = path.join(
    serverConfig.output.path,
    'vue-ssr-server-bundle.json' // vue-server-renderer默認(rèn)生成的json名
  )
  bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'))
  console.log('new bundle completed')
})

const handleSSR = async (ctx) => {
  if (!bundle) {
    ctx.body = "稍定一會"
    return
  }

  const clientManifestRes = await axios.get(
    'http://127.0.0.1:8001/public/vue-ssr-client-manifest.json'
  )

  const clientManifest = clientManifestRes.data

  const template = fs.readFileSync(path.join(__dirname, '../server.template.ejs'), 'utf-8')

  const renderer = VueServerRenderer.createBundleRenderer(bundle, {
    inject: false,
    clientManifest
  })

  await serverRender(ctx, renderer, template)
}

const router = new Router()

router.get('*', handleSSR)

module.exports = router

  • ssr server-entry.js配置

  1. 新建/server/routers/server-render.js , 導(dǎo)出一個(gè)帶ctx, renderer, template參數(shù)的方法
const ejs = require('ejs')
module.exports = async (ctx, renderer, template) => {
  // 聲明我們給前邊的是html文檔
  ctx.headers['Content-Type'] = 'text/html'
  const context = {
    url: ctx.path
  }
  try {
    const appString = await renderer.renderToString(context)
    const html = ejs.render(template, {
      appString,
      style: context.renderStyles(),
      script: context.renderScript()
    })
    ctx.body  = html
  } catch (err) {
    console.log('render err', err)
    throw err
  }
}
  1. 新建/client/create-app.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'

import App from './app.vue'
import createStore from './store/store'
import createRouter from './config/router'

import '@assets/css/reset.styl'

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

export default () => {
  // 每次都要返回一個(gè)新的store和router
  const store = createStore()
  const router = createRouter()

  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })

  return {app, router, store}
}
  1. 新建server-entry.js并配置
import createApp from './create-app'

export default context => {
  return new Promise((resolve, reject) => {
    const {app, router} = createApp()

    console.log(context.url, 'server-entry')
    router.push(context.url)
    // 路由跳轉(zhuǎn)后, 所有的異步操作都完成后執(zhí)行
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject(new Error('no componet matched'))
      }
      resolve(app)
    })
  })
}

  • 開發(fā)環(huán)境靜態(tài)資源處理

  1. 修改webpack.config.base.js里的output.public = 'http:127.0.0.1:8001/public/'
  2. 處理favicon.ico, 安裝koa-send到生產(chǎn)環(huán)境, 在server.js里處理favicon.ico
  3. 使用nodemon自動重啟node服務(wù)安裝到開發(fā)環(huán)境, 新建/nodemon.json并配置
{
  "restartable": "rs",
  "ignore": [
    ".git",
    "node_modules/**/node_modules",
    ".eslint",
    "client",
    "build/webpack.config.client.js",
    "public"
  ],
  "verbose": true,
  "env": {
    "NODE_ENV": "development"
  },
  "ext": "js json ejs"
}
// script里修改為 nodemon server/server.js
  1. 安裝concurrently 同時(shí)啟動兩個(gè)服務(wù), 安裝在生產(chǎn)環(huán)境
"script": {
  "dev":  "dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
}
  • 使用vue-meta處理頁面元信息

  1. 安裝vue-meta 到生產(chǎn)環(huán)境, 在入口文件中引入并, Vue.use()一下
  2. 在組件里添加metaInfo: {} , 在選項(xiàng)里邊寫meta元信息
  3. ssr需要client這邊的入口文件做依稀配合, 新建/client/client-entry.js并配置
import createApp from './create-app'

const {app, router} = createApp()

router.onReady(() => {
  app.$mount('#root')
})
  1. 修改webpack.config.client.js文件的entry
  2. 在服務(wù)器端添加meta信息,更新server-entry.jsvue-meta文檔
 context.meta = app.$meta()
// 服務(wù)器端
const {title} = context.meta.$inject()
{
  title: title.text()
}
  • 生產(chǎn)環(huán)境ssr配置

  1. 在package.json的script添加build:server命令, 添加build命令
"build:client": "cross-env NODE_ENV=prodution webpack --config build/webpack.config.client.js",
"build:server": "cross-env NODE_ENV=prodution webpack --config build/webpack.config.server.js",
"build": "npm run clean && npm run build:client && npm run build:server",
  1. 使用webpack.optimize.UglifyJsPlugin()報(bào)錯(cuò), 安裝使用uglifyjs-webpack-plugin
  2. 編寫ssr.js, 因?yàn)樯a(chǎn)環(huán)境都是把代碼打包好的, 所有, 邏輯很簡單
const Router = require('koa-router')
const path = require('path')
const fs = require('fs')
const VueServerRenderer = require('vue-server-renderer')
const serverRender = require('./server-render')

const clientManifest = require('../../public/vue-ssr-client-manifest.json')

const renderer = VueServerRenderer.createBundleRenderer(
  path.join(__dirname, '../../server-build/vue-ssr-server-bundle.json'),
  {
    inject: false,
    clientManifest
  }
)

const template = fs.readFileSync(path.join(__dirname, '../server.template.ejs'), 'utf-8')

const router = new Router()

router.get('*', async (ctx) => {
  await serverRender(ctx, renderer, template)
})

module.exports = router
  1. 解決靜態(tài)資源路徑問題, 修改webpack.config.client.js的output.publicPath = '/public/', 修改webpack.config.base.js的output.path = '../public'
  2. 新建/server/routers/static.js -> 使用koa-send 配置將public設(shè)置成靜態(tài)目錄
const Router = require('koa-router')
const send = require('koa-send')

const staticRouter = new Router({prefix: '/public'})

staticRouter.get('/*', async (ctx) => {
  await send(ctx, ctx.path)
})

module.exports = staticRouter
  • 服務(wù)器api請求實(shí)現(xiàn)
  1. 創(chuàng)建/server/db/db.js, 編寫連接云數(shù)據(jù)庫代碼, 并封裝代理接口
const axios = require('axios')
const sha1 = require('sha1')

const className = 'test'

const request = axios.create({
  baseURL: `https://d.apicloud.com/mcm/api`
})

const createError = (code, res) => {
  const err = new Error(res.message)
  err.code = code
  return err
}

const handleRequest = ({data, status, ...rest}) => {
  if (status === 200) {
    return data
  } else {
    throw createError(status, rest)
  }
}

module.exports = (appId, appKey) => {
  const getHeaders = () => {
    const now = Date.now()
    // SHA1(應(yīng)用ID + 'UZ' + 應(yīng)用KEY +'UZ' + 當(dāng)前時(shí)間毫秒數(shù))+ '.' +當(dāng)前時(shí)間毫秒數(shù)
    return {
      "X-APICloud-AppId": appId,
      "X-APICloud-AppKey": `${sha1(`${appId}UZ${appKey}UZ${now}`)}.${now}`
    }
  }
  return {
    async getAll () {
      return handleRequest(await request.get(`/${className}`, {
        headers: getHeaders()
      }))
    },
    async addOne (content) {
      return handleRequest(await request.post(
        `/${className}`,
        content,
        {headers: getHeaders()}
      ))
    },
    async getOne (id) {
      return handleRequest(await request.get(
        `/${className}/${id}`,
        {headers: getHeaders()}
      ))
    },
    async update (id, content) {
      return handleRequest(await request.put(
        `/${className}/${id}`,
        content,
        {headers: getHeaders()}
      ))
    },
    async delOne (id) {
      return handleRequest(await request.delete(`/${className}/${id}`, {
        headers: getHeaders()
      }))
    },
    async delAll (ids) {
      const requests = ids.map(id => {
        return {
          method: 'DELETE',
          path: `/${className}/${id}`
        }
      })
      return handleRequest(await request.post(
        '/batch',
        {requests},
        {headers: getHeaders()}
      ))
    }
  }
}
  1. 寫個(gè)koa中間件, 把云數(shù)據(jù)庫接口綁定到ctx上
const createDb = require('./db/db')
const dbConfig = require('../app.config').db
// console.log(dbConfig)
const db = createDb(dbConfig.appId, dbConfig.appKey)

app.use(async (ctx, next) => {
  ctx.db = db
  await next()
})
  1. 在/server/routes/api.js里根據(jù)代理接口封裝koa接口
const Router = require('koa-router')

const apiRouter = new Router({prefix: '/api'})

const validateUser = async (ctx, next) => {
  if (!ctx.session.user) {
    ctx.status = 401
    ctx.body = 'need login'
  } else {
    await next()
  }
}
// 做用戶登錄驗(yàn)證
apiRouter.use(validateUser)

const handleSucc = data => {
  return {
    succ: true,
    data
  }
}

apiRouter
  .post('/add', async (ctx) => {
    const content = ctx.request.body
    const data = await ctx.db.addOne(content)
    console.log(data)
    ctx.body = handleSucc(data)
  })
  .get('/one/:id', async (ctx) => {
    const id = ctx.params.id
    const data = await ctx.db.getOne(id)
    ctx.body = handleSucc(data)
  })
  .get('/all', async (ctx) => {
    const data = await ctx.db.getAll()
    ctx.body = handleSucc(data)
  })
  .put('/update/:id', async (ctx) => {
    const id = ctx.params.id
    const content = ctx.request.body
    console.log(id, content)
    const data = await ctx.db.update(id, content)
    ctx.body = handleSucc(data)
  })
  .delete('/del/:id', async (ctx) => {
    const id = ctx.params.id
    const data = await ctx.db.delOne(id)
    ctx.body = handleSucc(data)
  })
  .post('/delall', async (ctx) => {
    const ids = ctx.request.body.ids
    const data = await ctx.db.delAll(ids)
    ctx.body = handleSucc(data)
  })

module.exports = apiRouter

  1. 封裝登錄接口1. 安裝koa-session -S并配置指定app.keys,
// 配置koa-session
app.keys =['vue ssr kay']
app.use(koaSession({
  key: 'user-session-id',
  maxAge: 2*60*60*1000
}, app))
  1. 在業(yè)務(wù)代碼中使用axios請求koa接口
    -- 創(chuàng)建/client/model/client-model.js和util.js, 封裝接口, 需注意: 服務(wù)器返回的錯(cuò)誤在axios走的是catch, 對于401報(bào)錯(cuò)需要單獨(dú)處理, catch()里的錯(cuò)誤信息, 儲存在err.response里拿到status后reject()出去
// createError()函數(shù)封裝
export const createError = (code, msg) => {
  const err = new Error(msg)
  err.code = code
  return err
}
import axios from 'axios'

import {createError} from './util'

const request = axios.create({
  baseURL: '/'
})


const handleRequest = (request) => {
  return new Promise((resolve, reject) => {
    request.then(res => {
      const data = res.data
      if (!data) {
        return reject(createError(400, 'no data'))
      }
      if (!data.succ) {
        return reject(createError(400, data.message))
      }
      resolve(data.data)
    }).catch(err => { // 服務(wù)器包里(ctx.status)報(bào)的錯(cuò)會走axios的catch
      // axios的錯(cuò)誤信息放在err.response里
      // console.log(err, err.response)
      const errRes = err.response
      if (errRes.status === 401) {
        reject(createError(401, errRes.data))
      }
    })
  })
}

module.exports = {
  getAll () {
    return handleRequest(request.get('/api/all'))
  }
}
  1. 添加actions, 調(diào)用api, 在跳轉(zhuǎn)時(shí)為了解耦actions和router使用bus派發(fā)事件在入口文件里,監(jiān)聽事件并跳轉(zhuǎn)登錄頁面
import model from '../../model/client-model'
import bus from '../../bus/bus'

const handleError = err => {
  if (err.code === 401) {
    console.log(err.message)
    bus.$emit('login')
  }
}

export default {
  updateCountAsync (store, count) {
    setTimeout(() => {
      store.commit('updateCount', count)
    }, 1000)
  },
  fetchAll ({commit}) {
    model.getAll()
      .then(data => {
        commit('allArticles', data)
      })
      .catch(err => {
        // console.log(err.code)
        handleError(err)
      })
  }
}
// 入口文件 client-entry.js
import createApp from './create-app'
import bus from './bus/bus'

const {app, router} = createApp()

bus.$on('login', () => {
  router.replace('/login')
})

router.onReady(() => {
  app.$mount('#root')
})
  1. 在clent-model.js中完善login接口, 然后更改 actions -> mutations -> state -> login.vue寫登錄業(yè)務(wù)代碼, 所有接口都是這樣實(shí)現(xiàn)的
  2. 數(shù)據(jù)請求的時(shí)候使用全局loading
    -- 編寫loading.vue組件
    -- 在跟組件app.vue中引入loading組件
    -- 在store中聲明一個(gè)控制loading顯示隱藏的字段loading默認(rèn)false
    -- 在mutations里聲明一個(gè)startLoading 和 stopLoading
    -- 在actions.js中, 請求數(shù)據(jù)前用startLoading, 成功或失敗的時(shí)候, stopLoading
// app.vue
<template>
  <div class="text">
    <Header></Header>
    <router-link to="/login">login</router-link>
    <router-link to="/app">app</router-link>
    <router-view></router-view>
    <footer-jsx></footer-jsx>
    <div id="loading" v-show="loading">
      <loading/>
    </div>
  </div>
</template>
<script>
import FooterJsx from './layout/footer.jsx'
import Header from './layout/header.vue'
import Loading from './components/loading/loading.vue'
import {
  mapState
} from 'vuex'
export default {
  metaInfo: {
    title: 'kay\'s app'
  },
  computed: {
    ...mapState(['loading'])
  },
  components: {
    FooterJsx,
    Header,
    Loading
  },
  data () {
    return {}
  }
}
</script>
<style lang="stylus" scoped>
#loading
  position: fixed
  top: 0
  left: 0
  right: 0
  bottom: 0
  display: flex
  justify-content: center
  align-items: center
  z-index: 1000
</style>
// 在state中添加
export default {
  count: 0,
  articles: [],
  user: {},
  loading: false
}

// 在mutations添加
 startLoading (state) {
    state.loading = true
  },
  stopLoading (state) {
    state.loading = false
  }
// 
  • 服務(wù)器端渲染獲取數(shù)據(jù)

? 問題: 客戶端調(diào)通后, 切換到服務(wù)器渲染, 發(fā)現(xiàn)請求回來的數(shù)據(jù)并沒有加入到html結(jié)構(gòu)里, 所有的數(shù)據(jù)都是js渲染的, 爬蟲爬不到, 還有首屏渲染數(shù)據(jù)的復(fù)用
解決問題:
思考1 :首先考慮ssr時(shí)候如何拿到數(shù)據(jù)1, 數(shù)據(jù)請求在mounted中, 而在服務(wù)器端, 是不會執(zhí)行到mouted的, 就那不到數(shù)據(jù)
瀏覽器端有同域的概念, 所以寫個(gè) '/' 會自動加上域名的端口, 而服務(wù)器端沒有同域名所有就不能只寫個(gè) '/'

  1. 在請求頁面聲明一個(gè)asyncData()方法, 通過getMatchedCompoents()[返回組件實(shí)例的數(shù)組]獲取到asyncData(), 并傳參
  asyncData ({route, store}) {
    store.dispatch('fetchAll')
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(123)
      }, 2000)
    })
  },
  1. 解決數(shù)據(jù)請求問題, 可以通過修改axios.create(basnURL), 自己想自己發(fā)請求方式實(shí)現(xiàn), 但是沒法拿到cookie, 必須通過renderToString()方法比較復(fù)雜
  2. 使用服務(wù)器端的db方法, 直接向apicloud請求數(shù)據(jù), 配置webpack.config.client.js和webpack.config.server.js的resolve.alias, 設(shè)置不同環(huán)境不同的路徑
  3. 在node端的db.js使用async和await,需要另行配置.babelrc
{
  "presets":[
    "stage-1"
  ],
  "plugins": [
    "transform-vue-jsx",
    "syntax-dynamic-import"
  ],
  "env": {
    "browser": {
      "presets": [
        [
          "env",
          {
            "targets": {
              "browsers": ["last 2 versions", "safari >= 7"]
            }
          }
        ]
      ]
    },
    "node": {
      "presets": [
        "env",
        {
          "targets": {
            "node": "current"
          }
        }
      ]
    }
  }
}

  1. 配置server-entry.js
import createApp from './create-app'

export default context => {
  return new Promise((resolve, reject) => {
    const {app, router, store} = createApp()

    console.log(context.url, 'server-entry')
    router.push(context.url)
    // 路由跳轉(zhuǎn)后, 所有的異步操作都完成后執(zhí)行
    router.onReady(() => {
      // 可以通過router.getMatchedComponents() 獲取到匹配的組件實(shí)例
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject(new Error('no componet matched'))
      }
      Promise.all(matchedComponents.map(component => {
        // 通過匹配到的實(shí)例, 可以調(diào)用實(shí)例的任何屬性和方法
        // 調(diào)用asyncData(), 還可以傳參數(shù)
        if (component.asyncData) {
          return component.asyncData({
            route: router.currentRoute,
            store
          })
        }
      })).then(data => {
        console.log(store.state)
        context.meta = app.$meta()
        resolve(app)
      })
      // 這里的resolve(app) 要等獲取玩數(shù)據(jù)在resolve()
      // context.meta = app.$meta()
      // resolve(app)
    })
  })
}
  • 前后端數(shù)據(jù)復(fù)用和Server端用戶認(rèn)證

  1. 在客戶端的client的context添加屬性state=store.state,服務(wù)端拿到的store數(shù)據(jù)用, renderToString()完成后, 會把client中的store放到context.renderState()上返回的是一個(gè)<script>標(biāo)簽 -> 在window.INITIAL_STATE添加store.state的內(nèi)容
    2.在client-entry.js將window.INITIAL_STATE里的值給store.state -> store.replaceState()
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}
  1. 在獲取到數(shù)據(jù)后, 阻止獲取數(shù)據(jù)的actions,
// 判斷條件有很多
if(this.content && !this.content.length) {
    this.fetchAll()
}
  1. 用戶登錄驗(yàn)證
    -- 在asyncData中加入用戶的驗(yàn)證
    -- 在server-render.js把session添加到context,方便客戶端獲取
    -- 在client-entry.js中判斷并有的話就賦值給store.user
  • 使用服務(wù)器redirect解決, 用戶驗(yàn)證跳轉(zhuǎn)出現(xiàn)兩個(gè)頁面的問題

  1. 需要在asyncData()方法中, 未登錄時(shí)把路由跳轉(zhuǎn)的到/login更新到服務(wù)器
  2. 使用context傳router到服務(wù)器端, 在渲染ejs之前做個(gè)重定向
// 獲取html內(nèi)容
    const appString = await renderer.renderToString(context)

    console.log(ctx.path,'----------',context.router.currentRoute.fullPath)
    if (context.router.currentRoute.fullPath !== ctx.path) {
      return ctx.redirect(context.router.currentRoute.fullPath)
    }
    const {
      title
    } = context.meta.inject()

    const html = ejs.render(template, {
      title: title.text(),
      appString, // html內(nèi)容
      style: context.renderStyles(), // css內(nèi)容
      script: context.renderScripts(), // js內(nèi)容
      initalState: context.renderState()
    })
  • no-bundle方式(createRenderer())進(jìn)行ssr

  1. createRenderer()方式不需要打包成json, 把webpack.config.server.js中的new VueServerPlugin()注釋掉
  2. 新建dev-ssr-no-bundle.js并修改對應(yīng)的代碼
const Router = require('koa-router')
const axios = require('axios')
const path = require('path')
const fs = require('fs')
// 在內(nèi)存里操作文件, 提高效率, 只用在開發(fā)環(huán)境
const MemoryFs = require('memory-fs')
// 直接在nodejs里打包代碼
const webpack = require('webpack')
const VueServerRenderer = require('vue-server-renderer')
const NativeModule = require('module')
const vm = require('vm')
// 引入server-render.js
const serverRender = require('./server-render-no-bundle')

// 引入webpack配置文件
const serverConfig = require('../../build/webpack.config.server')
// 在node環(huán)境下執(zhí)行打包命令, 這個(gè)serverCompiler可以調(diào)用run()和watch()方法
const serverCompiler = webpack(serverConfig)
// 實(shí)例化一個(gè)mfs
const mfs = new MemoryFs()
// 指定webpack打包的輸出目錄在內(nèi)存里
serverCompiler.outputFileSystem = mfs

let bundle // 用來記錄webpack每次打包出來的文件

serverCompiler.watch({}, (err, stats) => {
  if (err) throw err
  stats = stats.toJson()
  stats.errors.forEach(err => console.log(err))
  stats.warnings.forEach(warn => console.warn(err))

  const bundlePath = path.join(
    serverConfig.output.path,
    'server-entry.js' // vue-server-renderer默認(rèn)生成的json名
  )

  try {
    const m = {exports: {}}
    const bundleStr = mfs.readFileSync(bundlePath, 'utf-8')
    const wrapper = NativeModule.wrap(bundleStr)
    const script = new vm.Script(wrapper, {
      filename: 'server-entry.js',
      displayErrors: true
    })
    const result = script.runInThisContext()
    result.call(m.exports, m.exports, require, m)
    bundle = m.exports.default
  } catch(err) {
    console.log('compiler err',err)
  }
  
  console.log('new bundle completed')
})

const handleSSR = async (ctx) => {
  if (!bundle) {
    ctx.body = "稍定一會"
    return
  }

  const clientManifestRes = await axios.get(
    'http://127.0.0.1:8001/public/vue-ssr-client-manifest.json'
  )

  const clientManifest = clientManifestRes.data

  const template = fs.readFileSync(path.join(__dirname, '../server.template.ejs'), 'utf-8')

  const renderer = VueServerRenderer.createRenderer({
    inject: false,
    clientManifest
  })

  await serverRender(ctx, renderer, template, bundle)
}

const router = new Router()

router.get('*', handleSSR)

module.exports = router

  1. 新建server-render-no-bundle.js, 并修改代碼
const app = await bundle(context)

    if (context.router.currentRoute.fullPath !== ctx.path) {
      return ctx.redirect(context.router.currentRoute.fullPath)
    }

    // 獲取html內(nèi)容
    const appString = await renderer.renderToString(app, context)
  1. 這個(gè)時(shí)候運(yùn)行會報(bào)錯(cuò):Failed to resolve async component default: Error: Cannot find module './1.server-entry.js', 造成這個(gè)問題的原因是, 異步加載文件時(shí)候, 需要從硬盤度文件, 而使用了mfs后, 值吧js文件存到內(nèi)存里了, 所以找不到這個(gè)模塊, 解決問題的方法2個(gè), 1, 在routes里, 禁用異步引入, 2. 不用mfs
  2. 不用mfs的配置createRenderer()
  1. 重新配置nodemon, 吧server-build文件夾忽略掉, 防止循環(huán)重啟
  • 生成環(huán)境no-bundle配置和異步模塊命名優(yōu)化

  1. routes異步模塊命名, 在webpack.config.client.js中配置webpackNamedChunksPlugin()
component: () => import(/* webpackChunkName: "app-view" */ '../views/app/App.vue')
  • 靜態(tài)資源上次cdn(七牛云)

  1. 在七牛云創(chuàng)建一個(gè)存儲空間 -> app.config.js中配置cdn選項(xiàng) -> 個(gè)人面板 -> 密鑰管理 -> host - SK - AK
module.exports = {
  db: {
    appId: 'A6077262853551',
    appKey: '209B3F4B-3A58-2F0D-993C-0A9095C051D0'
  },
  cdn: {
    host: 'http://p71tzdhae.bkt.clouddn.com',
    bucket: 'test',
    AK: 'MFzNNdaJ3tA7UsTpk3qY4ZV6NfivIT9WQm0YEfB-',
    SK: 'NAYkaHaiSRKpPRIgYGDiVp903QrgYC67S9uX7xVP'
  }
}
  1. /build里創(chuàng)建upload.js -> 安裝七牛sdk: cnpm i qiniu -S -> 查看api(SDK&工具) -> 編寫上傳圖片代碼
// 1. 引入七牛sdk
const qiniu = require('qiniu')
// 2. 會用到文件操作
const fs = require('fs')
const path = require('path')
const cdnConfig = require('../app.config').cdn
const {ak, sk, bucket} = cdnConfig

const mac = new qiniu.auth.digest.Mac(ak, sk)
var config = new qiniu.conf.Config();
// 空間對應(yīng)的機(jī)房
config.zone = qiniu.zone.Zone_z0;

// 上傳文件的邏輯
const doUpload = (key, file) => {
  var options = {
    // 覆蓋上傳
    scope: bucket + ":" + key
  }
  const formUploader = new qiniu.form_up.FormUploader(config)
  const putExtra = new qiniu.form_up.PutExtra()
  const putPolicy = new qiniu.rs.PutPolicy(options)
  const uploadToken = putPolicy.uploadToken(mac)
  return new Promise((resolve, reject) => {
    formUploader.putFile(uploadToken, key, file, putExtra, (err, body, info) => {
      if(err) {
        return reject(err)
      }
      if (info.statusCode === 200) {
        resolve(body)
        console.log(body)
      } else {
        reject(body)
      }
    })
  })
}

const publicPath = path.join(__dirname, '../public')
// 遞歸上傳所有文件

const uploadAll = (dir, prefix) => {
  const files = fs.readdirSync(dir)
  files.forEach(file => {
    // 1. 拿到完整的路徑
    const filePath = path.join(dir, file)
    const key = prefix ? `${prefix}/${file}` : file

    // 2. 判斷是否是文件夾
    if (fs.lstatSync(filePath).isDirectory()) {
      return uploadAll(filePath, key)
    }

    doUpload(key, filePath)
      .then(res => console.log(res))
      .catch(err => console.log(err))
  })
}

uploadAll(publicPath)
  • 使用pm2部署

  1. 新建/pm.yml文件
apps:
  - script: ./server/server.js
    name: vue-test
    env_production:
      NODE_ENV: production
      HOST: localhost
      PORT: 8008
// 終端運(yùn)行: pm2 start pm2.yml --env production
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末脚线,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡谜悟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門北秽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來葡幸,“玉大人,你說我怎么就攤上這事贺氓∥颠叮” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵辙培,是天一觀的道長蔑水。 經(jīng)常有香客問我,道長扬蕊,這世上最難降的妖魔是什么搀别? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮尾抑,結(jié)果婚禮上歇父,老公的妹妹穿的比我還像新娘。我一直安慰自己再愈,他們只是感情好榜苫,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翎冲,像睡著了一般垂睬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天驹饺,我揣著相機(jī)與錄音钳枕,去河邊找鬼。 笑死逻淌,一個(gè)胖子當(dāng)著我的面吹牛么伯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播卡儒,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼俐巴!你這毒婦竟也來了骨望?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤欣舵,失蹤者是張志新(化名)和其女友劉穎擎鸠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缘圈,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡劣光,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了糟把。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绢涡。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖遣疯,靈堂內(nèi)的尸體忽然破棺而出雄可,到底是詐尸還是另有隱情,我是刑警寧澤缠犀,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布数苫,位于F島的核電站,受9級特大地震影響辨液,放射性物質(zhì)發(fā)生泄漏虐急。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一滔迈、第九天 我趴在偏房一處隱蔽的房頂上張望止吁。 院中可真熱鬧,春花似錦亡鼠、人聲如沸赏殃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仁热。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抗蠢,已是汗流浹背举哟。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迅矛,地道東北人妨猩。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像秽褒,于是被迫代替她去往敵國和親壶硅。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容