美文网首页我爱编程
wepack从0开始配置vue环境之四: vuessr渲染

wepack从0开始配置vue环境之四: vuessr渲染

作者: 胖太_91bf | 来源:发表于2018-04-09 19:24 被阅读0次

    github传送门
    webpack之一webpack基础配置
    webpack之二webpack部署优化
    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' // 指定模块导出方式, output.path = ''指定一个新的目录
    5. 添加externals: Object.keys(require('..package.json').dependencies), 声明不要打包某些文件[]; 在node端运行, 不需要在单独打包libs文件到js文件里, 直接通过require()方式, 就直接可以调用node_modules里的模块
    6. 使用extract-text-webpack-plugin的方式去写css-loader
    7. 在webpack.definePlugin()里添加VUE_ENV = server变量
    8. 安装vue-server-renderer到生产环境, 在server配置中引入server-plugin, 在plugins里添加这个插件
    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') // 有了这个插件, 打包输出的会是json文件
    const baseConfig = require('./webpack.config.base')
    
    const isDev = process.env.NODE_ENV === 'development'
    
    let config
    
    config = merge(baseConfig, {
      target: 'node', // 定义打包出来的js的执行环境
      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官方规定
        }),
        new VueServerPlugin()
      ]
    })
    
    
    module.exports = config
    
    • 使用koa实现node server

    1. 安装koa到生产环境, 新建/server/server.js
    2. 编写server.js入口代码:koa可以用try-catch在最外层捕捉到错误
    3. 安装koa-router到生产环境
    4. /server/routers/ssr.js和/server/routers/dev-ssr.js
    5. 编写开发环境逻辑. 安装axios到生产环境, 安装memory-fs到开发环境
    6. 引入webpack.config.server.js配置, 使用webpack()方法执行打包, 实例化一个memoryFs, 将webpack打包输出道内存 outputRileSystem = mfs
    7. 使用watch({}, (err, stats) => {})方法监听每一次webpack打包, 并获取打包的文件
    8. 创建handleSSR中间件, 处理打包出来的bundle
    9. 新建/server/server.template.ejs, 显示bundle的数据, 安装ejs模块
    10. 用fs读取template.ejs里的内容
    11. 使用axios去向webpack-dev-server去请求一个json文件, 从而实现在devServer和nodeServer间建立联系
    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')
    // 在内存里操作文件, 提高效率, 只用在开发环境
    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环境下执行打包命令, 这个serverCompiler可以调用run()和watch()方法
    const serverCompiler = webpack(serverConfig)
    // 实例化一个mfs
    const mfs = new MemoryFs()
    // 指定webpack打包的输出目录在内存里
    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默认生成的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 , 导出一个带ctx, renderer, template参数的方法
    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 () => {
      // 每次都要返回一个新的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)
        // 路由跳转后, 所有的异步操作都完成后执行
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents()
          if (!matchedComponents.length) {
            return reject(new Error('no componet matched'))
          }
          resolve(app)
        })
      })
    }
    
    
    • 开发环境静态资源处理

    1. 修改webpack.config.base.js里的output.public = 'http:127.0.0.1:8001/public/'
    2. 处理favicon.ico, 安装koa-send到生产环境, 在server.js里处理favicon.ico
    3. 使用nodemon自动重启node服务安装到开发环境, 新建/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 同时启动两个服务, 安装在生产环境
    "script": {
      "dev":  "dev": "concurrently \"npm run dev:client\" \"npm run dev:server\"",
    }
    
    • 使用vue-meta处理页面元信息

    1. 安装vue-meta 到生产环境, 在入口文件中引入并, Vue.use()一下
    2. 在组件里添加metaInfo: {} , 在选项里边写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. 在服务器端添加meta信息,更新server-entry.jsvue-meta文档
     context.meta = app.$meta()
    // 服务器端
    const {title} = context.meta.$inject()
    {
      title: title.text()
    }
    
    • 生产环境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()报错, 安装使用uglifyjs-webpack-plugin
    2. 编写ssr.js, 因为生产环境都是把代码打包好的, 所有, 逻辑很简单
    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. 解决静态资源路径问题, 修改webpack.config.client.js的output.publicPath = '/public/', 修改webpack.config.base.js的output.path = '../public'
    2. 新建/server/routers/static.js -> 使用koa-send 配置将public设置成静态目录
    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
    
    • 服务器api请求实现
    1. 创建/server/db/db.js, 编写连接云数据库代码, 并封装代理接口
    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(应用ID + 'UZ' + 应用KEY +'UZ' + 当前时间毫秒数)+ '.' +当前时间毫秒数
        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. 写个koa中间件, 把云数据库接口绑定到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里根据代理接口封装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()
      }
    }
    // 做用户登录验证
    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. 在业务代码中使用axios请求koa接口
      -- 创建/client/model/client-model.js和util.js, 封装接口, 需注意: 服务器返回的错误在axios走的是catch, 对于401报错需要单独处理, catch()里的错误信息, 储存在err.response里拿到status后reject()出去
    // createError()函数封装
    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 => { // 服务器包里(ctx.status)报的错会走axios的catch
          // axios的错误信息放在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, 调用api, 在跳转时为了解耦actions和router使用bus派发事件在入口文件里,监听事件并跳转登录页面
    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写登录业务代码, 所有接口都是这样实现的
    2. 数据请求的时候使用全局loading
      -- 编写loading.vue组件
      -- 在跟组件app.vue中引入loading组件
      -- 在store中声明一个控制loading显示隐藏的字段loading默认false
      -- 在mutations里声明一个startLoading 和 stopLoading
      -- 在actions.js中, 请求数据前用startLoading, 成功或失败的时候, 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
      }
    // 
    
    • 服务器端渲染获取数据

    ? 问题: 客户端调通后, 切换到服务器渲染, 发现请求回来的数据并没有加入到html结构里, 所有的数据都是js渲染的, 爬虫爬不到, 还有首屏渲染数据的复用
    解决问题:
    思考1 :首先考虑ssr时候如何拿到数据1, 数据请求在mounted中, 而在服务器端, 是不会执行到mouted的, 就那不到数据
    浏览器端有同域的概念, 所以写个 '/' 会自动加上域名的端口, 而服务器端没有同域名所有就不能只写个 '/'

    1. 在请求页面声明一个asyncData()方法, 通过getMatchedCompoents()[返回组件实例的数组]获取到asyncData(), 并传参
      asyncData ({route, store}) {
        store.dispatch('fetchAll')
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(123)
          }, 2000)
        })
      },
    
    1. 解决数据请求问题, 可以通过修改axios.create(basnURL), 自己想自己发请求方式实现, 但是没法拿到cookie, 必须通过renderToString()方法比较复杂
    2. 使用服务器端的db方法, 直接向apicloud请求数据, 配置webpack.config.client.js和webpack.config.server.js的resolve.alias, 设置不同环境不同的路径
    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)
        // 路由跳转后, 所有的异步操作都完成后执行
        router.onReady(() => {
          // 可以通过router.getMatchedComponents() 获取到匹配的组件实例
          const matchedComponents = router.getMatchedComponents()
          if (!matchedComponents.length) {
            return reject(new Error('no componet matched'))
          }
          Promise.all(matchedComponents.map(component => {
            // 通过匹配到的实例, 可以调用实例的任何属性和方法
            // 调用asyncData(), 还可以传参数
            if (component.asyncData) {
              return component.asyncData({
                route: router.currentRoute,
                store
              })
            }
          })).then(data => {
            console.log(store.state)
            context.meta = app.$meta()
            resolve(app)
          })
          // 这里的resolve(app) 要等获取玩数据在resolve()
          // context.meta = app.$meta()
          // resolve(app)
        })
      })
    }
    
    • 前后端数据复用和Server端用户认证

    1. 在客户端的client的context添加属性state=store.state,服务端拿到的store数据用, renderToString()完成后, 会把client中的store放到context.renderState()上返回的是一个<script>标签 -> 在window.INITIAL_STATE添加store.state的内容
      2.在client-entry.js将window.INITIAL_STATE里的值给store.state -> store.replaceState()
    if (window.__INITIAL_STATE__) {
      store.replaceState(window.__INITIAL_STATE__)
    }
    
    1. 在获取到数据后, 阻止获取数据的actions,
    // 判断条件有很多
    if(this.content && !this.content.length) {
        this.fetchAll()
    }
    
    1. 用户登录验证
      -- 在asyncData中加入用户的验证
      -- 在server-render.js把session添加到context,方便客户端获取
      -- 在client-entry.js中判断并有的话就赋值给store.user
    • 使用服务器redirect解决, 用户验证跳转出现两个页面的问题

    1. 需要在asyncData()方法中, 未登录时把路由跳转的到/login更新到服务器
    2. 使用context传router到服务器端, 在渲染ejs之前做个重定向
    // 获取html内容
        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内容
          style: context.renderStyles(), // css内容
          script: context.renderScripts(), // js内容
          initalState: context.renderState()
        })
    
    • no-bundle方式(createRenderer())进行ssr

    1. createRenderer()方式不需要打包成json, 把webpack.config.server.js中的new VueServerPlugin()注释掉
    2. 新建dev-ssr-no-bundle.js并修改对应的代码
    const Router = require('koa-router')
    const axios = require('axios')
    const path = require('path')
    const fs = require('fs')
    // 在内存里操作文件, 提高效率, 只用在开发环境
    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环境下执行打包命令, 这个serverCompiler可以调用run()和watch()方法
    const serverCompiler = webpack(serverConfig)
    // 实例化一个mfs
    const mfs = new MemoryFs()
    // 指定webpack打包的输出目录在内存里
    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默认生成的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内容
        const appString = await renderer.renderToString(app, context)
    
    1. 这个时候运行会报错:Failed to resolve async component default: Error: Cannot find module './1.server-entry.js', 造成这个问题的原因是, 异步加载文件时候, 需要从硬盘度文件, 而使用了mfs后, 值吧js文件存到内存里了, 所以找不到这个模块, 解决问题的方法2个, 1, 在routes里, 禁用异步引入, 2. 不用mfs
    2. 不用mfs的配置createRenderer()
    1. 重新配置nodemon, 吧server-build文件夹忽略掉, 防止循环重启
    • 生成环境no-bundle配置和异步模块命名优化

    1. routes异步模块命名, 在webpack.config.client.js中配置webpackNamedChunksPlugin()
    component: () => import(/* webpackChunkName: "app-view" */ '../views/app/App.vue')
    
    • 静态资源上次cdn(七牛云)

    1. 在七牛云创建一个存储空间 -> app.config.js中配置cdn选项 -> 个人面板 -> 密钥管理 -> 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里创建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();
    // 空间对应的机房
    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
    // 终端运行: pm2 start pm2.yml --env production
    

    相关文章

      网友评论

        本文标题:wepack从0开始配置vue环境之四: vuessr渲染

        本文链接:https://www.haomeiwen.com/subject/niabhftx.html