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 2,Webpack 4,Koa 2を例に挙げた.
特に、本明細書では、APIがWEBと同じ項目である場合の構成について説明し、API、SSR Server、Staticは、構成方法を説明するために同じKoaの例を使用しており、すべてのエラーが1つの端末に表示され、デバッグが容易であることを目的としている.
プロジェクトの初期化
経験に基づいて、使用する依存項目を事前に追加します.
現在のnpmモジュールのネーミングはますます意味化され,基本的には見名知意である.EslintやStylus、LessなどのCSSプリプロセッシングモジュールについては追加していません.これは本稿の研究の重点ではありません.また、本稿を読んでいる以上、これらの構成はすでに話にならないと信じています.
例えば
構成
SSRの設定
私たちはKoaをデバッグと実際に実行するサーバフレームワークとして、
上記のファイルは,開発環境であるか否かに応じて,対応する静的リソースディレクトリを構成した.なお、コンパイルされたAPIファイルは
ここでは、生産環境の
開発環境では、データを処理するコアは
ページ制限のため、
Webpack構成
Webpackの構成は通常のVueプロジェクトおよびNodeより優れているためである.jsプロジェクトには大きな違いはありません.もう説明しません.具体的な構成はソースコードを参照してください.
注目すべきは、APIとWEBに別名を指定したことです.
また、
スクリプトのコンパイル:
テスト
ソースファイルを表示すると、スクリーンレンダリングの結果は次のようになります.
サービス側レンダリングのアプリケーションを最初から構築するのはかなり複雑です.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を分離し、src
にapi
およびweb
ディレクトリを作成する.vue-cli
に倣って、ルートディレクトリの下にpublic
ディレクトリを作成し、ルートディレクトリの下の静的リソースファイルを格納します.|-- public #
|-- src
|-- api #
|-- web #
例えば
NUXT.js
では、フロントエンドサーバエージェントAPIがバックエンドレンダリングを行い、私たちの構成ではエージェントを1つ選択するか、このエージェントを減らしてレンダリング結果を直接返すように構成することができます.一般的に、SSRのサーバ側レンダリングは最初のスクリーンのみをレンダリングするので、APIサーバはフロントエンドサーバと同じイントラネットであることが望ましい.構成
package.json
のscripts
:"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