Vue SSR Koa2 Scaffold
This commit is contained in:
7
config/app.js
Normal file
7
config/app.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
app: {
|
||||
port: 3000, // 监听的端口
|
||||
devHost: 'localhost', // 开发环境下打开的地址,监听了0.0.0.0,但是不是所有设备都支持访问这个地址,用127.0.0.1或localhost代替
|
||||
open: true // 是否打开浏览器
|
||||
}
|
||||
}
|
24
config/koa/dev.js
Normal file
24
config/koa/dev.js
Normal file
@ -0,0 +1,24 @@
|
||||
const devMiddleware = require('webpack-dev-middleware')
|
||||
|
||||
module.exports = (compiler, opts) => {
|
||||
const expressMiddleware = devMiddleware(compiler, opts)
|
||||
|
||||
async function middleware (ctx, next) {
|
||||
await expressMiddleware(ctx.req, {
|
||||
end: (content) => {
|
||||
ctx.body = content
|
||||
},
|
||||
setHeader: (name, value) => {
|
||||
ctx.set(name, value)
|
||||
}
|
||||
}, next)
|
||||
}
|
||||
|
||||
middleware.getFilenameFromUrl = expressMiddleware.getFilenameFromUrl
|
||||
middleware.waitUntilValid = expressMiddleware.waitUntilValid
|
||||
middleware.invalidate = expressMiddleware.invalidate
|
||||
middleware.close = expressMiddleware.close
|
||||
middleware.fileSystem = expressMiddleware.fileSystem
|
||||
|
||||
return middleware
|
||||
}
|
31
config/koa/hot.js
Normal file
31
config/koa/hot.js
Normal file
@ -0,0 +1,31 @@
|
||||
// 参考自 https://github.com/ccqgithub/koa-webpack-hot/blob/master/index.js
|
||||
const hotMiddleware = require('webpack-hot-middleware')
|
||||
const PassThrough = require('stream').PassThrough
|
||||
|
||||
module.exports = (compiler, opts = {}) => {
|
||||
opts.path = opts.path || '/__webpack_hmr'
|
||||
|
||||
const middleware = hotMiddleware(compiler, opts)
|
||||
|
||||
return async (ctx, next) => {
|
||||
if (ctx.request.path !== opts.path) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const stream = new PassThrough()
|
||||
ctx.body = stream
|
||||
|
||||
middleware(ctx.req, {
|
||||
write: stream.write.bind(stream),
|
||||
writeHead: (status, headers) => {
|
||||
ctx.status = status
|
||||
Object.keys(headers).forEach(key => {
|
||||
ctx.set(key, headers[key])
|
||||
})
|
||||
},
|
||||
end: () => {
|
||||
stream.end()
|
||||
}
|
||||
}, next)
|
||||
}
|
||||
}
|
74
config/koa/static.js
Normal file
74
config/koa/static.js
Normal file
@ -0,0 +1,74 @@
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
79
config/lib/env.js
Normal file
79
config/lib/env.js
Normal file
@ -0,0 +1,79 @@
|
||||
// From: https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-shared-utils/lib/env.js
|
||||
const { execSync } = require('child_process')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const LRU = require('lru-cache')
|
||||
|
||||
let _hasYarn
|
||||
const _yarnProjects = new LRU({
|
||||
max: 10,
|
||||
maxAge: 1000
|
||||
})
|
||||
let _hasGit
|
||||
const _gitProjects = new LRU({
|
||||
max: 10,
|
||||
maxAge: 1000
|
||||
})
|
||||
|
||||
// env detection
|
||||
exports.hasYarn = () => {
|
||||
if (process.env.VUE_CLI_TEST) {
|
||||
return true
|
||||
}
|
||||
if (_hasYarn != null) {
|
||||
return _hasYarn
|
||||
}
|
||||
try {
|
||||
execSync('yarnpkg --version', { stdio: 'ignore' })
|
||||
return (_hasYarn = true)
|
||||
} catch (e) {
|
||||
return (_hasYarn = false)
|
||||
}
|
||||
}
|
||||
|
||||
exports.hasProjectYarn = (cwd) => {
|
||||
if (_yarnProjects.has(cwd)) {
|
||||
return checkYarn(_yarnProjects.get(cwd))
|
||||
}
|
||||
|
||||
const lockFile = path.join(cwd, 'yarn.lock')
|
||||
const result = fs.existsSync(lockFile)
|
||||
_yarnProjects.set(cwd, result)
|
||||
return checkYarn(result)
|
||||
}
|
||||
|
||||
function checkYarn (result) {
|
||||
if (result && !exports.hasYarn()) throw new Error(`The project seems to require yarn but it's not installed.`)
|
||||
return result
|
||||
}
|
||||
|
||||
exports.hasGit = () => {
|
||||
if (process.env.VUE_CLI_TEST) {
|
||||
return true
|
||||
}
|
||||
if (_hasGit != null) {
|
||||
return _hasGit
|
||||
}
|
||||
try {
|
||||
execSync('git --version', { stdio: 'ignore' })
|
||||
return (_hasGit = true)
|
||||
} catch (e) {
|
||||
return (_hasGit = false)
|
||||
}
|
||||
}
|
||||
|
||||
exports.hasProjectGit = (cwd) => {
|
||||
if (_gitProjects.has(cwd)) {
|
||||
return _gitProjects.get(cwd)
|
||||
}
|
||||
|
||||
let result
|
||||
try {
|
||||
execSync('git status', { stdio: 'ignore', cwd })
|
||||
result = true
|
||||
} catch (e) {
|
||||
result = false
|
||||
}
|
||||
_gitProjects.set(cwd, result)
|
||||
return result
|
||||
}
|
6
config/lib/index.js
Normal file
6
config/lib/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
[
|
||||
'env',
|
||||
'openBrowser'
|
||||
].forEach(m => {
|
||||
Object.assign(exports, require(`./${m}`))
|
||||
})
|
124
config/lib/openBrowser.js
Normal file
124
config/lib/openBrowser.js
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* From: https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-shared-utils/lib/openBrowser.js
|
||||
*
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file at
|
||||
* https://github.com/facebookincubator/create-react-app/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
const opn = require('opn')
|
||||
const execa = require('execa')
|
||||
const chalk = require('chalk')
|
||||
const execSync = require('child_process').execSync
|
||||
|
||||
// https://github.com/sindresorhus/opn#app
|
||||
const OSX_CHROME = 'google chrome'
|
||||
|
||||
const Actions = Object.freeze({
|
||||
NONE: 0,
|
||||
BROWSER: 1,
|
||||
SCRIPT: 2
|
||||
})
|
||||
|
||||
function getBrowserEnv () {
|
||||
// Attempt to honor this environment variable.
|
||||
// It is specific to the operating system.
|
||||
// See https://github.com/sindresorhus/opn#app for documentation.
|
||||
const value = process.env.BROWSER
|
||||
let action
|
||||
if (!value) {
|
||||
// Default.
|
||||
action = Actions.BROWSER
|
||||
} else if (value.toLowerCase().endsWith('.js')) {
|
||||
action = Actions.SCRIPT
|
||||
} else if (value.toLowerCase() === 'none') {
|
||||
action = Actions.NONE
|
||||
} else {
|
||||
action = Actions.BROWSER
|
||||
}
|
||||
return { action, value }
|
||||
}
|
||||
|
||||
function executeNodeScript (scriptPath, url) {
|
||||
const extraArgs = process.argv.slice(2)
|
||||
const child = execa('node', [scriptPath, ...extraArgs, url], {
|
||||
stdio: 'inherit'
|
||||
})
|
||||
child.on('close', code => {
|
||||
if (code !== 0) {
|
||||
console.log()
|
||||
console.log(
|
||||
chalk.red(
|
||||
'The script specified as BROWSER environment variable failed.'
|
||||
)
|
||||
)
|
||||
console.log(chalk.cyan(scriptPath) + ' exited with code ' + code + '.')
|
||||
console.log()
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
function startBrowserProcess (browser, url) {
|
||||
// If we're on OS X, the user hasn't specifically
|
||||
// requested a different browser, we can try opening
|
||||
// Chrome with AppleScript. This lets us reuse an
|
||||
// existing tab when possible instead of creating a new one.
|
||||
const shouldTryOpenChromeWithAppleScript =
|
||||
process.platform === 'darwin' &&
|
||||
(typeof browser !== 'string' || browser === OSX_CHROME)
|
||||
|
||||
if (shouldTryOpenChromeWithAppleScript) {
|
||||
try {
|
||||
// Try our best to reuse existing tab
|
||||
// on OS X Google Chrome with AppleScript
|
||||
execSync('ps cax | grep "Google Chrome"')
|
||||
execSync('osascript openChrome.applescript "' + encodeURI(url) + '"', {
|
||||
cwd: __dirname,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
// Another special case: on OS X, check if BROWSER has been set to "open".
|
||||
// In this case, instead of passing `open` to `opn` (which won't work),
|
||||
// just ignore it (thus ensuring the intended behavior, i.e. opening the system browser):
|
||||
// https://github.com/facebookincubator/create-react-app/pull/1690#issuecomment-283518768
|
||||
if (process.platform === 'darwin' && browser === 'open') {
|
||||
browser = undefined
|
||||
}
|
||||
|
||||
// Fallback to opn
|
||||
// (It will always open new tab)
|
||||
try {
|
||||
var options = { app: browser }
|
||||
opn(url, options).catch(() => {}) // Prevent `unhandledRejection` error.
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the BROWSER evironment variable and decides what to do with it. Returns
|
||||
* true if it opened a browser or ran a node.js script, otherwise false.
|
||||
*/
|
||||
exports.openBrowser = function (url) {
|
||||
const { action, value } = getBrowserEnv()
|
||||
switch (action) {
|
||||
case Actions.NONE:
|
||||
// Special case: BROWSER="none" will prevent opening completely.
|
||||
return false
|
||||
case Actions.SCRIPT:
|
||||
return executeNodeScript(value, url)
|
||||
case Actions.BROWSER:
|
||||
return startBrowserProcess(value, url)
|
||||
default:
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
}
|
83
config/lib/openChrome.applescript
Normal file
83
config/lib/openChrome.applescript
Normal file
@ -0,0 +1,83 @@
|
||||
(*
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
This source code is licensed under the MIT license found in the
|
||||
LICENSE file at
|
||||
https://github.com/facebookincubator/create-react-app/blob/master/LICENSE
|
||||
*)
|
||||
|
||||
property targetTab: null
|
||||
property targetTabIndex: -1
|
||||
property targetWindow: null
|
||||
|
||||
on run argv
|
||||
set theURL to item 1 of argv
|
||||
|
||||
tell application "Chrome"
|
||||
|
||||
if (count every window) = 0 then
|
||||
make new window
|
||||
end if
|
||||
|
||||
-- 1: Looking for tab running debugger
|
||||
-- then, Reload debugging tab if found
|
||||
-- then return
|
||||
set found to my lookupTabWithUrl(theURL)
|
||||
if found then
|
||||
set targetWindow's active tab index to targetTabIndex
|
||||
tell targetTab to reload
|
||||
tell targetWindow to activate
|
||||
set index of targetWindow to 1
|
||||
return
|
||||
end if
|
||||
|
||||
-- 2: Looking for Empty tab
|
||||
-- In case debugging tab was not found
|
||||
-- We try to find an empty tab instead
|
||||
set found to my lookupTabWithUrl("chrome://newtab/")
|
||||
if found then
|
||||
set targetWindow's active tab index to targetTabIndex
|
||||
set URL of targetTab to theURL
|
||||
tell targetWindow to activate
|
||||
return
|
||||
end if
|
||||
|
||||
-- 3: Create new tab
|
||||
-- both debugging and empty tab were not found
|
||||
-- make a new tab with url
|
||||
tell window 1
|
||||
activate
|
||||
make new tab with properties {URL:theURL}
|
||||
end tell
|
||||
end tell
|
||||
end run
|
||||
|
||||
-- Function:
|
||||
-- Lookup tab with given url
|
||||
-- if found, store tab, index, and window in properties
|
||||
-- (properties were declared on top of file)
|
||||
on lookupTabWithUrl(lookupUrl)
|
||||
tell application "Chrome"
|
||||
-- Find a tab with the given url
|
||||
set found to false
|
||||
set theTabIndex to -1
|
||||
repeat with theWindow in every window
|
||||
set theTabIndex to 0
|
||||
repeat with theTab in every tab of theWindow
|
||||
set theTabIndex to theTabIndex + 1
|
||||
if (theTab's URL as string) contains lookupUrl then
|
||||
-- assign tab, tab index, and window to properties
|
||||
set targetTab to theTab
|
||||
set targetTabIndex to theTabIndex
|
||||
set targetWindow to theWindow
|
||||
set found to true
|
||||
exit repeat
|
||||
end if
|
||||
end repeat
|
||||
|
||||
if found then
|
||||
exit repeat
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return found
|
||||
end lookupTabWithUrl
|
26
config/server.js
Normal file
26
config/server.js
Normal file
@ -0,0 +1,26 @@
|
||||
const path = require('path')
|
||||
const Koa = require('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...`)
|
||||
})
|
||||
})
|
197
config/setup-dev-server.js
Normal file
197
config/setup-dev-server.js
Normal file
@ -0,0 +1,197 @@
|
||||
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 = '\n'.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
|
||||
// console.log('web-dev')
|
||||
// update()
|
||||
})
|
||||
|
||||
// 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
|
||||
// console.log('web-dev')
|
||||
// update()
|
||||
})
|
||||
app.use(webpackHotMiddleware(clientCompiler))
|
||||
}
|
96
config/ssr.js
Normal file
96
config/ssr.js
Normal file
@ -0,0 +1,96 @@
|
||||
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
|
||||
})
|
||||
const API = require('../dist/api/api').default
|
||||
const server = API(app)
|
||||
resolve(server)
|
||||
} 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('\nServer 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('\nError: '), e.message)
|
||||
html = '500 | Internal Server Error'
|
||||
}
|
||||
}
|
||||
ctx.type = 'html'
|
||||
ctx.status = status || ctx.status
|
||||
ctx.body = html
|
||||
next()
|
||||
})
|
||||
})
|
||||
}
|
58
config/webpack.api.config.js
Normal file
58
config/webpack.api.config.js
Normal file
@ -0,0 +1,58 @@
|
||||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
|
||||
const { dependencies } = require('../package.json')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
name: 'api',
|
||||
target: 'node',
|
||||
devtool: '#cheap-module-source-map',
|
||||
mode: isProd ? 'production' : 'development',
|
||||
entry: path.join(__dirname, '../src/api/app.js'),
|
||||
output: {
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.resolve(__dirname, '../dist/api'),
|
||||
filename: 'api.js',
|
||||
publicPath: '/'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.join(__dirname, '../src/web'),
|
||||
'~': path.join(__dirname, '../src/api')
|
||||
},
|
||||
extensions: ['.js']
|
||||
},
|
||||
externals: [
|
||||
...Object.keys(dependencies || {})
|
||||
],
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.(js)$/,
|
||||
include: [path.resolve(__dirname, '../src/api')],
|
||||
exclude: /(node_modules|bower_components)/
|
||||
// use: [
|
||||
// {
|
||||
// loader: 'babel-loader',
|
||||
// options: {
|
||||
// presets: ['@babel/preset-env']
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// loader: 'eslint-loader'
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CaseSensitivePathsPlugin(),
|
||||
new FriendlyErrorsPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
'process.env.API_ENV': '"server"'
|
||||
})
|
||||
]
|
||||
}
|
168
config/webpack.base.config.js
Normal file
168
config/webpack.base.config.js
Normal file
@ -0,0 +1,168 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
mode: isProd ? 'production' : 'development',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../dist/web'),
|
||||
publicPath: '/',
|
||||
filename: '[name].[chunkhash:8].js',
|
||||
chunkFilename: '[id].js'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.join(__dirname, '../src/web'),
|
||||
'~': path.join(__dirname, '../src/api'),
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
},
|
||||
extensions: ['.js', '.vue', '.json', '.css']
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.(js|jsx)$/,
|
||||
include: [path.resolve(__dirname, '../src/web')],
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env'],
|
||||
plugins: [
|
||||
'transform-vue-jsx',
|
||||
'@babel/plugin-syntax-jsx',
|
||||
'@babel/plugin-syntax-dynamic-import'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// {
|
||||
// test: /\.(js|jsx|vue)$/,
|
||||
// enforce: 'pre',
|
||||
// exclude: /node_modules/,
|
||||
// use: {
|
||||
// loader: 'eslint-loader'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
test: /\.json$/,
|
||||
use: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /\.pug$/,
|
||||
use: {
|
||||
loader: 'pug-plain-loader'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
// {
|
||||
// test: /\.styl(us)?$/,
|
||||
// use: [
|
||||
// isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
|
||||
// 'css-loader',
|
||||
// 'stylus-loader'
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// test: /\.less$/,
|
||||
// use: [
|
||||
// isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader',
|
||||
// 'css-loader',
|
||||
// 'less-loader'
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: 'vue-html-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
preserveWhitespace: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg|ico)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
name: 'assets/images/[name].[hash:8].[ext]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'assets/images/[name].[hash:8].[ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
name: 'assets/font/[name].[hash:8].[ext]'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'async',
|
||||
minSize: 30000,
|
||||
minChunks: 2,
|
||||
maxAsyncRequests: 5,
|
||||
maxInitialRequests: 3
|
||||
// cacheGroups: {
|
||||
// commons: {
|
||||
// name: 'manifest',
|
||||
// chunks: 'initial',
|
||||
// minChunks: 2
|
||||
// }
|
||||
// }
|
||||
}
|
||||
},
|
||||
performance: {
|
||||
maxEntrypointSize: 400000,
|
||||
hints: isProd ? 'warning' : false
|
||||
},
|
||||
plugins: [
|
||||
new CaseSensitivePathsPlugin(),
|
||||
new CopyWebpackPlugin([{
|
||||
from: path.join(__dirname, '../public'),
|
||||
to: path.join(__dirname, '../dist/web'),
|
||||
ignore: ['.*', 'index.html']
|
||||
}]),
|
||||
new FriendlyErrorsPlugin(),
|
||||
new VueLoaderPlugin(),
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 15
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: isProd ? '[name].[hash].css' : '[name].css',
|
||||
chunkFilename: isProd ? '[id].[hash].css' : '[id].css'
|
||||
})
|
||||
]
|
||||
}
|
29
config/webpack.server.config.js
Normal file
29
config/webpack.server.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
const merge = require('webpack-merge')
|
||||
const nodeExternals = require('webpack-node-externals')
|
||||
const config = require('./webpack.base.config')
|
||||
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
|
||||
const version = ' V ' + require('../package.json').version
|
||||
|
||||
module.exports = merge(config, {
|
||||
name: 'server',
|
||||
target: 'node',
|
||||
devtool: '#cheap-module-source-map',
|
||||
mode: 'production',
|
||||
entry: path.join(__dirname, '../src/web/entry-server.js'),
|
||||
output: {
|
||||
libraryTarget: 'commonjs2'
|
||||
},
|
||||
externals: nodeExternals({
|
||||
whitelist: [/\.vue$/, /\.css$/, /\.styl(us)$/, /\.pug$/]
|
||||
}),
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
'process.env.VUE_ENV': '"server"',
|
||||
'process.env.BM_VERSION': "'" + version + "'"
|
||||
}),
|
||||
new VueSSRServerPlugin()
|
||||
]
|
||||
})
|
26
config/webpack.web.config.js
Normal file
26
config/webpack.web.config.js
Normal file
@ -0,0 +1,26 @@
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const path = require('path')
|
||||
const base = require('./webpack.base.config')
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
|
||||
const version = ' V ' + require('../package.json').version
|
||||
|
||||
console.log(version)
|
||||
|
||||
module.exports = merge(base, {
|
||||
name: 'web',
|
||||
devtool: '#eval-source-map',
|
||||
entry: {
|
||||
app: path.resolve(__dirname, '../src/web/entry-client.js')
|
||||
},
|
||||
mode: isProd ? 'production' : 'development',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
'process.env.VUE_ENV': '"client"',
|
||||
'process.env.BM_VERSION': "'" + version + "'"
|
||||
}),
|
||||
new VueSSRClientPlugin()
|
||||
]
|
||||
})
|
Reference in New Issue
Block a user