├── .browserslistrc ├── .huskyrc ├── public ├── favicon.ico ├── index.ssr.html └── index.html ├── src ├── assets │ └── logo.png ├── views │ ├── About.vue │ └── Home.vue ├── entry-client.js ├── store │ └── index.js ├── app.js ├── router │ └── index.js ├── App.vue ├── entry-server.js └── components │ └── HelloWorld.vue ├── babel.config.js ├── .editorconfig ├── .gitignore ├── .eslintrc.js ├── server ├── index.js ├── server.js └── dev.server.js ├── README.zh-cn.md ├── README.md ├── package.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeDebugTest/vue2-ssr-demo/master/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeDebugTest/vue2-ssr-demo/master/src/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /src/entry-client.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | const { app, store } = createApp() 4 | 5 | if (window.__INITIAL_STATE__) { 6 | store.replaceState(window.__INITIAL_STATE__) 7 | } 8 | 9 | app.$mount('#app') 10 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export function createStore () { 7 | return new Vuex.Store({ 8 | state: {}, 9 | mutations: {}, 10 | actions: {} 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /public/index.ssr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue ssr demo 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import { createStore } from './store' 4 | import { createRouter } from './router' 5 | 6 | export function createApp () { 7 | const store = createStore() 8 | const router = createRouter() 9 | 10 | const app = new Vue({ 11 | router, 12 | store, 13 | render: h => h(App) 14 | }) 15 | 16 | return { app, router, store } 17 | }; 18 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue ssr demo 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | const routes = [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue') 11 | }, 12 | { 13 | path: '/about', 14 | name: 'about', 15 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') 16 | } 17 | ] 18 | 19 | export function createRouter () { 20 | return new VueRouter({ 21 | mode: 'history', 22 | base: process.env.BASE_URL, 23 | routes 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const koaStatic = require('koa-static') 3 | const path = require('path') 4 | 5 | const resolve = file => path.resolve(__dirname, file) 6 | const app = new Koa() 7 | 8 | const isDev = process.env.NODE_ENV !== 'production' 9 | const router = isDev ? require('./dev.server') : require('./server') 10 | 11 | app.use(router.routes()).use(router.allowedMethods()) 12 | // 开放目录 13 | app.use(koaStatic(resolve('../dist'))) 14 | 15 | const port = process.env.PORT || 3000 16 | app.listen(port, () => { 17 | console.log(`server started at http://localhost:${port}`) 18 | }) 19 | 20 | module.exports = app 21 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Router = require('koa-router') 4 | const router = new Router() 5 | 6 | const resolve = file => path.resolve(__dirname, file) 7 | 8 | const { createBundleRenderer } = require('vue-server-renderer') 9 | const bundle = require('../dist/vue-ssr-server-bundle.json') 10 | const clientManifest = require('../dist/vue-ssr-client-manifest.json') 11 | 12 | const renderer = createBundleRenderer(bundle, { 13 | runInNewContext: false, 14 | template: fs.readFileSync(resolve('../dist/index.ssr.html'), 'utf-8'), 15 | clientManifest 16 | }) 17 | 18 | function renderToString (context) { 19 | return new Promise((resolve, reject) => { 20 | renderer.renderToString(context, (err, html) => { 21 | if (err) { 22 | return reject(err) 23 | } 24 | resolve(html) 25 | }) 26 | }) 27 | } 28 | 29 | const handleRequest = async (ctx, next) => { 30 | const url = ctx.path 31 | console.log('request path: ', url) 32 | 33 | ctx.res.setHeader('Content-Type', 'text/html') 34 | const context = { url } 35 | // render context data to HTML 36 | const html = await renderToString(context) 37 | ctx.body = html 38 | } 39 | 40 | // static resources (js|css|img|index.html|favicon.ico) need't server render 41 | router.get(/^\/(?!((js|css|img)\/)|(index\.html)|(favicon\.ico))/, handleRequest) 42 | module.exports = router 43 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # hello-world 2 | 3 | ### 动机 4 | 鉴于vue-ssr 官网例子 https://github.com/vuejs/vue-hackernews-2.0/ 5 | 1. too old 6 | 2. too slow,通过webpack 打包,非vue-cli 7 | 3. unused github 上好多通过vue-cli的项目跑不起来 8 | 4. not compatible 不兼容 CSR模式 9 | 10 | 通过此项目你可以快速开始一个更具现代化地 vue-ssr 项目。 11 | 12 | #### 好处 13 | 1. 支持vue-cli4 14 | 2. 向下兼容:支持CSR(客户端渲染)模式 15 | 16 | 17 | ### 安装 18 | ```sh 19 | npm install 20 | ``` 21 | 22 | ### 本地开发 23 | 24 | #### development ssr 模式 25 | ```sh 26 | npm run serve 27 | ``` 28 | npm run serve 会启动两个服务: 29 | 1. npm run dev:client 静态资源服务(默认8090端口),用于供服务端渲染时hot-update 静态资源 30 | 2. npm run dev:server 服务端渲染(3000端口),真正的访问入口,该服务会将静态资源的访问代理到上面的静态资源服务 31 | 32 | 启动后,访问 http://localhost:3000 。dev模式带有hot-reload 的 服务端渲染。 33 | 34 | 其他: 35 | 1. 设置静态服务端口,通过设置环境变量 STATIC_PORT=xxxx 进行控制。 36 | 2. 若访问 http://localhost:8090 则走本地CSR模式,此时浏览器拿到的结果均为不经过SSR产出的结果 37 | 38 | #### development csr 模式 39 | ```sh 40 | npm run dev:client 41 | ``` 42 | 本地开发,但不启用SSR服务端渲染 43 | 44 | #### production ssr 模式 45 | ```sh 46 | npm run build 47 | npm run serve 48 | ``` 49 | 启动后,访问 http://localhost:3000 。线上ssr Server 模拟 50 | 51 | ### 构建 52 | 53 | ```sh 54 | npm run build 55 | ``` 56 | 1. npm run build:client 编译生成ssr 生成客户端渲染所需内容 57 | 2. npm run build:server 编译生成ssr 生成服务端渲染所需内容 58 | 59 | ### 代码检查 60 | 61 | #### 仅检查 62 | 63 | ```sh 64 | npm run lint 65 | ``` 66 | 67 | #### 检查并修复 68 | 69 | ```sh 70 | npm run lint:fix 71 | ``` 72 | 73 | ## [搭建指南](https://github.com/codeDebugTest/blog/issues/4) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hello-world 2 | [中文文档](https://github.com/codeDebugTest/vue2-ssr-demo/blob/master/README.zh-cn.md) 3 | 4 | ## Project setup 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | ``` 11 | npm run serve 12 | ``` 13 | **then visit http://localhost:3000 for ssr development** 14 | 15 | Take care: 16 | *npm run serve* will start two services: 17 | 1. static service running by *npm run dev:client* command. It use to hot-reloads static resources 18 | 2. SSR service running by *npm run dev:server* command. this 19 | 20 | others: 21 | 1. You can set static service port by setting env STATIC_PORT=xxxx on package.json's scripts。 22 | 2. If directly visit http://localhost:8090,then into the CSR mode. The html is not produced by SSR. 23 | 24 | #### CSR mode for development 25 | ```sh 26 | npm run dev:client 27 | ``` 28 | 29 | #### SSR local server for production 30 | ```sh 31 | npm run build 32 | npm run serve 33 | ``` 34 | 35 | ### Compiles and minifies for production 36 | ``` 37 | npm run build 38 | ``` 39 | *npm run build* command will execute two commands as shown below. 40 | 1. **npm run build:client** generate files for client render 41 | 2. **npm run build:server** generate files for server render 42 | 43 | ### Code lints 44 | 45 | #### only lints 46 | 47 | ```sh 48 | npm run lint 49 | ``` 50 | 51 | #### Lints and fixes 52 | ``` 53 | npm run lint:fix 54 | ``` 55 | 56 | ### Customize configuration 57 | See [Configuration Reference](https://cli.vuejs.org/config/). 58 | -------------------------------------------------------------------------------- /src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | const isDev = process.env.NODE_ENV !== 'production' 4 | 5 | // This exported function will be called by `bundleRenderer`. 6 | // This is where we perform data-prefetching to determine the 7 | // state of our application before actually rendering it. 8 | // Since data fetching is async, this function is expected to 9 | // return a Promise that resolves to the app instance. 10 | export default context => { 11 | return new Promise((resolve, reject) => { 12 | const s = isDev && Date.now() 13 | const { app, router, store } = createApp() 14 | 15 | const { url } = context 16 | const { fullPath } = router.resolve(url).route 17 | 18 | if (fullPath !== url) { 19 | return reject(new Error(`can't find the path: ${url}`)) 20 | } 21 | 22 | // set router's location 23 | router.push(url) 24 | 25 | // wait until router has resolved possible async hooks 26 | router.onReady(() => { 27 | const matchedComponents = router.getMatchedComponents() 28 | // no matched routes 29 | if (!matchedComponents.length) { 30 | return reject(new Error(`can't find the matched router for path: ${url}`)) 31 | } 32 | // Call fetchData hooks on components matched by the route. 33 | // A preFetch hook dispatches a store action and returns a Promise, 34 | // which is resolved when the action is complete and store state has been 35 | // updated. 36 | Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ 37 | store, 38 | route: router.currentRoute 39 | }))).then(() => { 40 | isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) 41 | // After all preFetch hooks are resolved, our store is now 42 | // filled with the state needed to render the app. 43 | // Expose the state on the render context, and let the request handler 44 | // inline the state in the HTML response. This allows the client-side 45 | // store to pick-up the server-side state without having to duplicate 46 | // the initial data fetching on the client. 47 | context.state = store.state 48 | resolve(app) 49 | }).catch(reject) 50 | }, reject) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 43 | 44 | 45 | 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-ssr-demo", 3 | "version": "1.0.0", 4 | "description": "a ssr demo project which build by vue-cli", 5 | "scripts": { 6 | "start": "cross-env NODE_ENV=production node ./server/index.js", 7 | "serve": "concurrently \"npm run dev:client\" \"npm run dev:server\"", 8 | "dev:client": "STATIC_PORT=8090 vue-cli-service serve", 9 | "dev:server": "STATIC_PORT=8090 cross-env WEBPACK_TARGET=server node ./server/index.js", 10 | "build": "rm -rf dist && npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json", 11 | "build:client": "NODE_ENV=production WEBPACK_TARGET=client vue-cli-service build", 12 | "build:server": "cross-env NODE_ENV=production WEBPACK_TARGET=server vue-cli-service build", 13 | "lint": "vue-cli-service lint --no-fix; stylelint src/**/*.{vue,css,less}", 14 | "lint:fix": "vue-cli-service lint; stylelint --fix src/**/*.{vue,css,less}" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/codeDebugTest/vue2-ssr-demo.git" 19 | }, 20 | "keywords": [ 21 | "vue", 22 | "vue-ssr", 23 | "vue-cli" 24 | ], 25 | "dependencies": { 26 | "axios": "^0.25.0", 27 | "core-js": "^3.8.3", 28 | "cross-env": "^7.0.3", 29 | "koa": "^2.13.4", 30 | "koa-router": "^9.4.0", 31 | "koa-static": "^5.0.0", 32 | "lodash": "^4.17.21", 33 | "vue": "^2.6.14", 34 | "vue-awesome": "^4.5.0", 35 | "vue-router": "^3.2.0", 36 | "vuex": "^3.4.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/eslint-parser": "^7.17.0", 40 | "@babel/eslint-plugin": "^7.16.5", 41 | "@vue/cli-plugin-babel": "~4.5.0", 42 | "@vue/cli-plugin-eslint": "~4.5.0", 43 | "@vue/cli-plugin-router": "~4.5.0", 44 | "@vue/cli-plugin-vuex": "~4.5.0", 45 | "@vue/cli-service": "~4.5.0", 46 | "@vue/eslint-config-standard": "^5.1.2", 47 | "babel-eslint": "^10.1.0", 48 | "concurrently": "^7.0.0", 49 | "eslint": "^7.32.0", 50 | "eslint-plugin-import": "^2.20.2", 51 | "eslint-plugin-node": "^11.1.0", 52 | "eslint-plugin-promise": "^4.2.1", 53 | "eslint-plugin-standard": "^4.0.0", 54 | "eslint-plugin-vue": "^7.15.1", 55 | "husky": "^4.3.8", 56 | "less": "^3.0.4", 57 | "less-loader": "^5.0.0", 58 | "lint-staged": "^9.5.0", 59 | "memory-fs": "^0.5.0", 60 | "stylelint": "^13.13.1", 61 | "vue-server-renderer": "^2.6.14", 62 | "vue-template-compiler": "^2.6.14", 63 | "webpack-node-externals": "^1.7.2", 64 | "webpackbar": "^5.0.2" 65 | }, 66 | "gitHooks": { 67 | "pre-commit": "lint-staged" 68 | }, 69 | "lint-staged": { 70 | "*.{js,jsx,vue}": [ 71 | "vue-cli-service lint", 72 | "git add" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server/dev.server.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const axios = require('axios') 3 | const MemoryFS = require('memory-fs') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const Router = require('koa-router') 7 | 8 | const webpackConfig = require('@vue/cli-service/webpack.config') 9 | const serverCompiler = webpack(webpackConfig) 10 | const mfs = new MemoryFS() 11 | serverCompiler.outputFileSystem = mfs 12 | 13 | let bundle 14 | serverCompiler.watch({}, (err, stats) => { 15 | if (err) { 16 | throw err 17 | } 18 | 19 | stats = stats.toJson() 20 | stats.errors.forEach(error => console.error(error)) 21 | stats.warnings.forEach(warn => console.warn(warn)) 22 | const bundlePath = path.join( 23 | webpackConfig.output.path, 24 | 'vue-ssr-server-bundle.json' 25 | ) 26 | bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8')) 27 | console.log('new bundle generated') 28 | }) 29 | 30 | const { createBundleRenderer } = require('vue-server-renderer') 31 | function renderToString (context, renderer) { 32 | return new Promise((resolve, reject) => { 33 | renderer.renderToString(context, (err, html) => { 34 | err ? reject(err) : resolve(html) 35 | }) 36 | }) 37 | } 38 | async function handleSsrRequest (ctx) { 39 | console.log('request path: ', ctx.path) 40 | if (!bundle) { 41 | ctx.body = 'you can visit the site until webpack compile completed' 42 | return 43 | } 44 | 45 | const clientManifestResp = await axios.get(`http://localhost:${process.env.STATIC_PORT}/vue-ssr-client-manifest.json`) 46 | const clientManifest = clientManifestResp.data 47 | 48 | const renderer = createBundleRenderer(bundle, { 49 | runInNewContext: false, 50 | template: fs.readFileSync(path.resolve(__dirname, '../public/index.ssr.html'), 'utf-8'), 51 | clientManifest 52 | }) 53 | const html = await renderToString(ctx, renderer) 54 | ctx.body = html 55 | } 56 | 57 | async function handleStaticResourceRequest (ctx) { 58 | console.log('get static resource: ', ctx.path) 59 | let resource 60 | try { 61 | resource = await axios.get( 62 | `http://localhost:${process.env.STATIC_PORT}/${ctx.path}`, 63 | { responseType: 'stream' } 64 | ) 65 | } catch (e) { 66 | console.log('static resources not found', ctx.path) 67 | ctx.status = '404' 68 | return 69 | } 70 | 71 | ctx.body = resource.data 72 | } 73 | 74 | const router = new Router() 75 | // because it can't read dist statice files on development mode, so we can proxy request by statice server. 76 | router.get(/^\/((js|css|img|)\/(.*)|favicon\.ico)$/, handleStaticResourceRequest) 77 | // for hot-update resource request 78 | router.get(/.*\.hot-update\.(json|js)/, handleStaticResourceRequest) 79 | 80 | router.get(/.*/, handleSsrRequest) 81 | module.exports = router 82 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 2 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 3 | const nodeExternals = require('webpack-node-externals') 4 | const WebpackBar = require('webpackbar') 5 | const CssContextLoader = require.resolve('./build-loaders/css-context') 6 | const { merge } = require('lodash') 7 | const path = require('path') 8 | 9 | const resolve = file => path.resolve(__dirname, file) 10 | const target = process.env.WEBPACK_TARGET || 'client' 11 | const isServer = target === 'server' 12 | const isDev = process.env.NODE_ENV !== 'production' 13 | 14 | module.exports = { 15 | lintOnSave: false, 16 | css: { 17 | extract: !isDev, 18 | loaderOptions: { 19 | less: { 20 | lessOptions: { 21 | javascriptEnabled: true 22 | } 23 | } 24 | } 25 | }, 26 | configureWebpack: { 27 | entry: `./src/entry-${target}.js`, 28 | // This allows webpack to handle dynamic imports in a Node-appropriate 29 | // fashion, and also tells `vue-loader` to emit server-oriented code when 30 | // compiling Vue components. 31 | target: isServer ? 'node' : 'web', 32 | // For bundle renderer source map support 33 | devtool: 'source-map', 34 | // This tells the server bundle to use Node-style exports 35 | output: { libraryTarget: isServer ? 'commonjs2' : undefined }, 36 | externals: isServer 37 | ? nodeExternals({ 38 | // do not externalize dependencies that need to be processed by webpack. 39 | // you can add more file types here e.g. raw *.vue files 40 | // you should also whitelist deps that modifies `global` (e.g. polyfills) 41 | whitelist: [/\.css$/] 42 | }) 43 | : undefined, 44 | // Server-side bundle should have one single entry file. Avoid using CommonsChunkPlugin in the server config 45 | optimization: { splitChunks: isServer ? false : undefined }, 46 | plugins: [isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin()] 47 | }, 48 | chainWebpack: config => { 49 | config.module 50 | .rule('vue') 51 | .use('vue-loader') 52 | .tap(options => merge(options, { optimizeSSR: isServer })) 53 | 54 | // add client/server compile process bar 55 | config.plugin('loader') 56 | .use(WebpackBar, [{ name: target, color: isServer ? 'orange' : 'green' }]) 57 | 58 | if (!isDev) { 59 | // copy index.ssr.html for server render 60 | config.plugin('copy').tap(args => { 61 | args[0].push({ 62 | from: resolve('public/index.ssr.html'), 63 | to: resolve('dist/index.ssr.html'), 64 | toType: 'file' 65 | }) 66 | return args 67 | }) 68 | } 69 | 70 | if (isServer) { 71 | // server side unused hot-reload 72 | config.plugins.delete('hmr') 73 | 74 | if (!isDev) { 75 | // css-loader mini-css-extract-plugin(extract-css-loader),will generate browser sentence such as document.getElementsByTagName xxxxx。 76 | // this will result in error (document not defined), running on server side。 77 | // so delete mini-css-extract-plugin and replace with css-context-loader。 78 | const langs = ['css', 'less'] 79 | const types = ['vue-modules', 'vue', 'normal-modules', 'normal'] 80 | 81 | langs.forEach(lang => { 82 | types.forEach(type => { 83 | const rule = config.module.rule(lang).oneOf(type) 84 | rule.uses.delete('extract-css-loader') 85 | rule.use('css-context-loader').loader(CssContextLoader).before('css-loader') 86 | }) 87 | }) 88 | } 89 | } 90 | }, 91 | devServer: { port: isDev && !isServer ? process.env.STATIC_PORT : undefined } 92 | } 93 | --------------------------------------------------------------------------------