Vue SSR(Vue 2+Koa 2+Webpack 4)構成ガイド

37281 ワード

Vueの公式に言われているように、SSRの構成はVue、webpack、Nodeを熟知しているのに適している.js開発の開発者が読む.まずssr.に移動してください.vuejs.orgは、手動でSSR構成を行う基本的な内容を理解しています.
サービス側レンダリングのアプリケーションを最初から構築するのはかなり複雑です.SSRが必要でWebpackやKoaに詳しくない場合は、NUXTを直接使用してください.js.
本明細書で説明する内容の例は、Vue SSR Koa2 :githubである.com/yi-ge/Vue-S…
本稿の最新版であるVue 2,Webpack 4,Koa 2を例に挙げた.
特に、本明細書では、APIがWEBと同じ項目である場合の構成について説明し、API、SSR Server、Staticは、構成方法を説明するために同じKoaの例を使用しており、すべてのエラーが1つの端末に表示され、デバッグが容易であることを目的としている.
プロジェクトの初期化
git init
yarn init
touch .gitignore
.gitignoreファイルで、一般的なディレクトリを配置します.
.DS_Store
node_modules

#             
/dist/web
/dist/api

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

経験に基づいて、使用する依存項目を事前に追加します.
echo "yarn add cross-env #             
  koa
  koa-body #   ,  
  koa-compress #     
  compressible # https://github.com/jshttp/compressible
  axios #      API    
  es6-promise 
  vue
  vue-router # vue      ,SSR  
  vuex #   ,     ,      Vuex SSR   
  vue-template-compiler
  vue-server-renderer #   
  lru-cache #             
  vuex-router-sync" | sed 's/#[[:space:]].*//g' | tr '
'
' ' | sed 's/[ ][ ]*/ /g' | bash echo "yarn add -D webpack webpack-cli webpack-dev-middleware # webpack-hot-middleware # webpack-merge # Webpack webpack-node-externals # node_modules friendly-errors-webpack-plugin # case-sensitive-paths-webpack-plugin # copy-webpack-plugin # Webpack mini-css-extract-plugin # CSS chalk # console @babel/core # babel-loader @babel/plugin-syntax-dynamic-import # import @babel/plugin-syntax-jsx # JSX babel-plugin-syntax-jsx # , babel-plugin-transform-vue-jsx babel-helper-vue-jsx-merge-props @babel/polyfill @babel/preset-env file-loader json-loader url-loader css-loader vue-loader vue-style-loader vue-html-loader" | sed 's/#[[:space:]].*//g' | tr '
'
' ' | sed 's/[ ][ ]*/ /g' | bash

現在のnpmモジュールのネーミングはますます意味化され,基本的には見名知意である.EslintやStylus、LessなどのCSSプリプロセッシングモジュールについては追加していません.これは本稿の研究の重点ではありません.また、本稿を読んでいる以上、これらの構成はすでに話にならないと信じています.electornに倣ってmainおよびrendererを分離し、srcapiおよびwebディレクトリを作成する.vue-cliに倣って、ルートディレクトリの下にpublicディレクトリを作成し、ルートディレクトリの下の静的リソースファイルを格納します.
|-- public #     
|-- src
    |-- api #     
    |-- web #     

例えばNUXT.jsでは、フロントエンドサーバエージェントAPIがバックエンドレンダリングを行い、私たちの構成ではエージェントを1つ選択するか、このエージェントを減らしてレンダリング結果を直接返すように構成することができます.一般的に、SSRのサーバ側レンダリングは最初のスクリーンのみをレンダリングするので、APIサーバはフロントエンドサーバと同じイントラネットであることが望ましい.
構成package.jsonscripts:
"scripts": {
    "serve": "cross-env NODE_ENV=development node config/server.js",
	"start": "cross-env NODE_ENV=production node config/server.js"
}
yarn serve:開発デバッグの開始yarn start:コンパイルされたプログラムの実行config/app.js一般的な構成をエクスポートします.
module.exports = {
  app: {
    port: 3000, //      
    devHost: 'localhost', //           ,   0.0.0.0,                 , 127.0.0.1 localhost  
    open: true //        
  }
}

SSRの設定
私たちはKoaをデバッグと実際に実行するサーバフレームワークとして、config/server.js:
const path = require('path')
const Koa = req  uire('koa')
const koaCompress = require('koa-compress')
const compressible = require('compressible')
const koaStatic = require('./koa/static')
const SSR = require('./ssr')
const conf = require('./app')

const isProd = process.env.NODE_ENV === 'production'

const app = new Koa()

app.use(koaCompress({ //     
  filter: type => !(/event\-stream/i.test(type)) && compressible(type) // eslint-disable-line
}))

app.use(koaStatic(isProd ? path.resolve(__dirname, '../dist/web') : path.resolve(__dirname, '../public'), {
  maxAge: 30 * 24 * 60 * 60 * 1000
})) //              

// vue ssr  , SSR   API
SSR(app).then(server => {
  server.listen(conf.app.port, '0.0.0.0', () => {
    console.log(`> server is staring...`)
  })
})

上記のファイルは,開発環境であるか否かに応じて,対応する静的リソースディレクトリを構成した.なお、コンパイルされたAPIファイルはdist/apiにあり、フロントエンドファイルはdist/webにあると約束している.koa-staticを参照して静的リソースの処理を実現し、config/koa/static.js:
'use strict'

/**
 * From koa-static
 */

const { resolve } = require('path')
const assert = require('assert')
const send = require('koa-send')

/**
 * Expose `serve()`.
 */

module.exports = serve

/**
 * Serve static files from `root`.
 *
 * @param {String} root
 * @param {Object} [opts]
 * @return {Function}
 * @api public
 */

function serve (root, opts) {
  opts = Object.assign({}, opts)

  assert(root, 'root directory is required to serve files')

  // options
  opts.root = resolve(root)
  if (opts.index !== false) opts.index = opts.index || 'index.html'

  if (!opts.defer) {
    return async function serve (ctx, next) {
      let done = false

      if (ctx.method === 'HEAD' || ctx.method === 'GET') {
        if (ctx.path === '/' || ctx.path === '/index.html') { // exclude index.html file
          await next()
          return
        }
        try {
          done = await send(ctx, ctx.path, opts)
        } catch (err) {
          if (err.status !== 404) {
            throw err
          }
        }
      }

      if (!done) {
        await next()
      }
    }
  }

  return async function serve (ctx, next) {
    await next()

    if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return
    // response is already handled
    if (ctx.body != null || ctx.status !== 404) return // eslint-disable-line

    try {
      await send(ctx, ctx.path, opts)
    } catch (err) {
      if (err.status !== 404) {
        throw err
      }
    }
  }
}

koa-staticは、koa-sendを単純にカプセル化しただけであることがわかる(yarn add koa-send).次はメインイベントSSRに関する構成です.config/ssr.js:
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const LRU = require('lru-cache')
const {
  createBundleRenderer
} = require('vue-server-renderer')
const isProd = process.env.NODE_ENV === 'production'
const setUpDevServer = require('./setup-dev-server')
const HtmlMinifier = require('html-minifier').minify

const pathResolve = file => path.resolve(__dirname, file)

module.exports = app => {
  return new Promise((resolve, reject) => {
    const createRenderer = (bundle, options) => {
      return createBundleRenderer(bundle, Object.assign(options, {
        cache: LRU({
          max: 1000,
          maxAge: 1000 * 60 * 15
        }),
        basedir: pathResolve('../dist/web'),
        runInNewContext: false
      }))
    }

    let renderer = null
    if (isProd) {
      // prod mode
      const template = HtmlMinifier(fs.readFileSync(pathResolve('../public/index.html'), 'utf-8'), {
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: false
      })
      const bundle = require(pathResolve('../dist/web/vue-ssr-server-bundle.json'))
      const clientManifest = require(pathResolve('../dist/web/vue-ssr-client-manifest.json'))
      renderer = createRenderer(bundle, {
        template,
        clientManifest
      })
    } else {
      // dev mode
      setUpDevServer(app, (bundle, options, apiMain, apiOutDir) => {
        try {
          const API = eval(apiMain).default // eslint-disable-line
          const server = API(app)
          renderer = createRenderer(bundle, options)
          resolve(server)
        } catch (e) {
          console.log(chalk.red('
Server error'
), e) } }) } app.use(async (ctx, next) => { if (!renderer) { ctx.type = 'html' ctx.body = 'waiting for compilation... refresh in a moment.' next() return } let status = 200 let html = null const context = { url: ctx.url, title: 'OK' } if (/^\/api/.test(ctx.url)) { // /api , api 。 next() return } try { status = 200 html = await renderer.renderToString(context) } catch (e) { if (e.message === '404') { status = 404 html = '404 | Not Found' } else { status = 500 console.log(chalk.red('
Error: '
), e.message) html = '500 | Internal Server Error' } } ctx.type = 'html' ctx.status = status || ctx.status ctx.body = html next() }) if (isProd) { const API = require('../dist/api/api').default const server = API(app) resolve(server) } }) }

ここでは、生産環境のhtml-minifierファイル(index.html)を圧縮するために、yarn add html-minifierモジュールが新たに追加された.残りの配置と公式に与えられた差は多くなく、これ以上述べない.ただし、Promiseはrequire('http').createServer(app.callback())(詳細はソースコードを参照)を返します.これはkoa 2インスタンスを共有することを目的としている.また、ここでは/apiの先頭の要求をブロックし、要求をAPIサーバに渡して処理する(同じKoa 2インスタンスであるため、ここでは直接next()となる).publicディレクトリには、index.htmlファイルが存在する必要があります.

<html lang="zh-cn">
<head>
  <title>{{ title }}title>
  ...
head>
<body>
  
body>
html>

開発環境では、データを処理するコアはconfig/setup-dev-server.jsファイルにあります.
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const MFS = require('memory-fs')
const webpack = require('webpack')
const chokidar = require('chokidar')
const apiConfig = require('./webpack.api.config')
const serverConfig = require('./webpack.server.config')
const webConfig = require('./webpack.web.config')
const webpackDevMiddleware = require('./koa/dev')
const webpackHotMiddleware = require('./koa/hot')
const readline = require('readline')
const conf = require('./app')
const {
  hasProjectYarn,
  openBrowser
} = require('./lib')

const readFile = (fs, file) => {
  try {
    return fs.readFileSync(path.join(webConfig.output.path, file), 'utf-8')
  } catch (e) {}
}

module.exports = (app, cb) => {
  let apiMain, bundle, template, clientManifest, serverTime, webTime, apiTime
  const apiOutDir = apiConfig.output.path
  let isFrist = true

  const clearConsole = () => {
    if (process.stdout.isTTY) {
      // Fill screen with blank lines. Then move to 0 (beginning of visible part) and clear it
      const blank = '
'
.repeat(process.stdout.rows) console.log(blank) readline.cursorTo(process.stdout, 0, 0) readline.clearScreenDown(process.stdout) } } const update = () => { if (apiMain && bundle && template && clientManifest) { if (isFrist) { const url = 'http://' + conf.app.devHost + ':' + conf.app.port console.log(chalk.bgGreen.black(' DONE ') + ' ' + chalk.green(`Compiled successfully in ${serverTime + webTime + apiTime}ms`)) console.log() console.log(` App running at: ${chalk.cyan(url)}`) console.log() const buildCommand = hasProjectYarn(process.cwd()) ? `yarn build` : `npm run build` console.log(` Note that the development build is not optimized.`) console.log(` To create a production build, run ${chalk.cyan(buildCommand)}.`) console.log() if (conf.app.open) openBrowser(url) isFrist = false } cb(bundle, { template, clientManifest }, apiMain, apiOutDir) } } // server for api apiConfig.entry.app = ['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true', apiConfig.entry.app] apiConfig.plugins.push( new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin() ) const apiCompiler = webpack(apiConfig) const apiMfs = new MFS() apiCompiler.outputFileSystem = apiMfs apiCompiler.watch({}, (err, stats) => { if (err) throw err stats = stats.toJson() if (stats.errors.length) return console.log('api-dev...') apiMfs.readdir(path.join(__dirname, '../dist/api'), function (err, files) { if (err) { return console.error(err) } files.forEach(function (file) { console.info(file) }) }) apiMain = apiMfs.readFileSync(path.join(apiConfig.output.path, 'api.js'), 'utf-8') update() }) apiCompiler.plugin('done', stats => { stats = stats.toJson() stats.errors.forEach(err => console.error(err)) stats.warnings.forEach(err => console.warn(err)) if (stats.errors.length) return apiTime = stats.time // console.log('web-dev') // update() }) // web server for ssr const serverCompiler = webpack(serverConfig) const mfs = new MFS() serverCompiler.outputFileSystem = mfs serverCompiler.watch({}, (err, stats) => { if (err) throw err stats = stats.toJson() if (stats.errors.length) return // console.log('server-dev...') bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) update() }) serverCompiler.plugin('done', stats => { stats = stats.toJson() stats.errors.forEach(err => console.error(err)) stats.warnings.forEach(err => console.warn(err)) if (stats.errors.length) return serverTime = stats.time }) // web webConfig.entry.app = ['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=2000&reload=true', webConfig.entry.app] webConfig.output.filename = '[name].js' webConfig.plugins.push( new webpack.HotModuleReplacementPlugin(), new webpack.NoEmitOnErrorsPlugin() ) const clientCompiler = webpack(webConfig) const devMiddleware = webpackDevMiddleware(clientCompiler, { // publicPath: webConfig.output.publicPath, stats: { // or 'errors-only' colors: true }, reporter: (middlewareOptions, options) => { const { log, state, stats } = options if (state) { const displayStats = (middlewareOptions.stats !== false) if (displayStats) { if (stats.hasErrors()) { log.error(stats.toString(middlewareOptions.stats)) } else if (stats.hasWarnings()) { log.warn(stats.toString(middlewareOptions.stats)) } else { log.info(stats.toString(middlewareOptions.stats)) } } let message = 'Compiled successfully.' if (stats.hasErrors()) { message = 'Failed to compile.' } else if (stats.hasWarnings()) { message = 'Compiled with warnings.' } log.info(message) clearConsole() update() } else { log.info('Compiling...') } }, noInfo: true, serverSideRender: false }) app.use(devMiddleware) const templatePath = path.resolve(__dirname, '../public/index.html') // read template from disk and watch template = fs.readFileSync(templatePath, 'utf-8') chokidar.watch(templatePath).on('change', () => { template = fs.readFileSync(templatePath, 'utf-8') console.log('index.html template updated.') update() }) clientCompiler.plugin('done', stats => { stats = stats.toJson() stats.errors.forEach(err => console.error(err)) stats.warnings.forEach(err => console.warn(err)) if (stats.errors.length) return clientManifest = JSON.parse(readFile( devMiddleware.fileSystem, 'vue-ssr-client-manifest.json' )) webTime = stats.time }) app.use(webpackHotMiddleware(clientCompiler)) }

ページ制限のため、koaおよびlibディレクトリのファイルはサンプルコードを参照します.ここで、libのファイルは、vue-cliから来ており、主にユーザがyarnを使用しているか否かを判断し、ブラウザでURLを開くために使用される.この場合、上記機能の必要に応じて、以下のモジュールを追加する必要があります(オプション).
yarn add memory-fs chokidar readline

yarn add -D opn execa
config/setup-dev-server.jsファイルの内容を読むと、ここで3つのwebpack構成の処理が行われていることがわかります.
Server for API //     `/api`    API  ,     API     

Web server for SSR //        API     ,  SSR

WEB //            


Webpack構成
|-- config
    |-- webpack.api.config.js // Server for API
    |-- webpack.base.config.js //   Webpack  
    |-- webpack.server.config.js // Web server for SSR
    |-- webpack.web.config.js //       

Webpackの構成は通常のVueプロジェクトおよびNodeより優れているためである.jsプロジェクトには大きな違いはありません.もう説明しません.具体的な構成はソースコードを参照してください.
注目すべきは、APIとWEBに別名を指定したことです.
alias: {
  '@': path.join(__dirname, '../src/web'),
  '~': path.join(__dirname, '../src/api'),
  'vue$': 'vue/dist/vue.esm.js'
},

また、webpack.base.config.jsにおいて、コンパイル時にpublicディレクトリの下のファイルをdist/webディレクトリにコピーすると設定されている場合は、index.htmlファイルは含まれない.
スクリプトのコンパイル:
"scripts": {
    ...
    "build": "rimraf dist && npm run build:web && npm run build:server && npm run build:api",
    "build:web": "cross-env NODE_ENV=production webpack --config config/webpack.web.config.js --progress --hide-modules",
    "build:server": "cross-env NODE_ENV=production webpack --config config/webpack.server.config.js --progress --hide-modules",
    "build:api": "cross-env NODE_ENV=production webpack --config config/webpack.api.config.js --progress --hide-modules"
},
yarn buildを実行してコンパイルを行います.コンパイルされたファイルは/distディレクトリに保存されます.正式な環境はAPIとSSRサーバーをできるだけ分離してください.
テストyarn serve(開発)またはyarn start(コンパイル後)コマンドを実行し、http://localhost:3000にアクセスします.
ソースファイルを表示すると、スクリーンレンダリングの結果は次のようになります.
➜  ~ curl -s http://localhost:3000/ | grep Hello
  
"app" data-server-rendered="true"><div>Hello World SSRdiv>div>

,Vue SSR 。

。 :www.wyr.me/post/593