├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── build ├── setup-dev-server.js ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.server.conf.js ├── config └── index.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── index.js ├── server ├── index.js └── index.template.html ├── src ├── App.vue ├── app.js ├── assets │ ├── images │ │ └── code.png │ └── styles │ │ └── common.styl ├── components │ ├── A │ │ └── A.vue │ └── B │ │ └── B.vue ├── entry-client.js ├── entry-server.js ├── mixins │ └── title-mixins.js ├── router │ └── index.js └── store │ └── index.js └── static └── favicon.ico /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-syntax-dynamic-import" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": [ 4 | "html" 5 | ], 6 | "parser": "babel-eslint" 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | server-dist/ 4 | server-build 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于webpack4构建的vue应用(SSR/SPA) 2 | 3 | 📦 此项目是通过webpack搭建了vue单页面应用以及vue服务端渲染应用。项目只是作为学习webpack以及SSR,并不足以强大能够用于线上项目。 4 | 5 | > 看完整代码可以查找对应Tag。 6 | 7 | ## webpack-SPA 8 | 9 | [完整代码](https://github.com/lhz960904/webpack-vue-ssr/tree/webpack4-SPA-v1.0) 10 | 11 | #### Usage: 12 | 13 | ```shell 14 | npm install 15 | 16 | npm run dev # 开发环境 17 | 18 | npm run build # 线上构建 19 | ``` 20 | 21 | 22 | 23 | 24 | 25 | ## webpack-SSR 26 | 27 | [完整代码](https://github.com/lhz960904/webpack-vue-ssr/tree/webpack-SSR-v1.0) 28 | 29 | #### Usage: 30 | 31 | ```shell 32 | npm install 33 | 34 | npm run dev # 开发环境 35 | 36 | npm run build # 线上构建 37 | npm run start # 线上运行 38 | ``` 39 | 40 | ```javascript 41 | // movie.vue 42 | export default { 43 | // 更改title 44 | title () { 45 | return 'demo1' 46 | }, 47 | // 异步获取数据 48 | asyncData ({ store, route }) { 49 | // 触发 action 后,例:请求电影、传入id 50 | return store.dispatch('fetchMovie', 54321) 51 | }, 52 | } 53 | 54 | 55 | // store/index.js 56 | return new Vuex.Store({ 57 | state: { 58 | movie: {} 59 | }, 60 | actions: { 61 | fetchMovie ({ commit }, id) { 62 | return new Promise((resolve, reject) => { 63 | // ajax去请求数据 64 | }).then(res => { 65 | commit('setMoive', { res }) 66 | }) 67 | } 68 | }, 69 | mutations: { 70 | setMoive (state, { res }) { 71 | Vue.set(state, 'movie', res) 72 | } 73 | } 74 | }) 75 | ``` 76 | -------------------------------------------------------------------------------- /build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | // memory-fs可以使webpack将文件写入到内存中,而不是写入到磁盘。 4 | const MFS = require('memory-fs') 5 | const webpack = require('webpack') 6 | // 监听文件变化,兼容性更好(比fs.watch、fs.watchFile、fsevents) 7 | const chokidar = require('chokidar') 8 | const clientConfig = require('./webpack.dev.conf') 9 | const serverConfig = require('./webpack.server.conf') 10 | // webpack热加载需要 11 | const webpackDevMiddleware = require('koa-webpack-dev-middleware') 12 | // 配合热加载实现模块热替换 13 | const webpackHotMiddleware = require('koa-webpack-hot-middleware') 14 | 15 | // 读取vue-ssr-webpack-plugin生成的文件 16 | const readFile = (fs, file) => { 17 | try { 18 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 19 | } catch (e) { 20 | console.log('读取文件错误:', e) 21 | } 22 | } 23 | 24 | module.exports = function setupDevServer(app, templatePath, cb) { 25 | let bundle 26 | let template 27 | let clientManifest 28 | 29 | 30 | // 监听改变后更新函数 31 | const update = () => { 32 | if (bundle && clientManifest) { 33 | cb(bundle, { 34 | template, 35 | clientManifest 36 | }) 37 | } 38 | } 39 | 40 | // 监听html模板改变、需手动刷新 41 | template = fs.readFileSync(templatePath, 'utf-8') 42 | chokidar.watch(templatePath).on('change', () => { 43 | template = fs.readFileSync(templatePath, 'utf-8') 44 | update() 45 | }) 46 | 47 | // 修改webpack入口配合模块热替换使用 48 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 49 | 50 | // 编译clinetWebpack 插入Koa中间件 51 | const clientCompiler = webpack(clientConfig) 52 | const devMiddleware = webpackDevMiddleware(clientCompiler, { 53 | publicPath: clientConfig.output.publicPath, 54 | noInfo: true 55 | }) 56 | app.use(devMiddleware) 57 | 58 | clientCompiler.plugin('done', stats => { 59 | stats = stats.toJson() 60 | stats.errors.forEach(err => console.error(err)) 61 | stats.warnings.forEach(err => console.warn(err)) 62 | if (stats.errors.length) return 63 | clientManifest = JSON.parse(readFile( 64 | devMiddleware.fileSystem, 65 | 'vue-ssr-client-manifest.json' 66 | )) 67 | update() 68 | }) 69 | 70 | // 插入Koa中间件(模块热替换) 71 | app.use(webpackHotMiddleware(clientCompiler)) 72 | 73 | const serverCompiler = webpack(serverConfig) 74 | const mfs = new MFS() 75 | serverCompiler.outputFileSystem = mfs 76 | serverCompiler.watch({}, (err, stats) => { 77 | if (err) throw err 78 | stats = stats.toJson() 79 | if (stats.errors.length) return 80 | 81 | // vue-ssr-webpack-plugin 生成的bundle 82 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 83 | update() 84 | }) 85 | 86 | } 87 | 88 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('../config') 3 | const packageConfig = require('../package.json') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | 6 | // 返回文件绝对路径 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.join(assetsSubDirectory, _path) 13 | } 14 | 15 | 16 | // cssloader兼容各种预处理语言(less|sass|scss|stylus) 17 | exports.cssLoaders = function (options) { 18 | options = options || {} 19 | 20 | const cssLoader = { 21 | loader: 'css-loader', 22 | options: { 23 | sourceMap: options.sourceMap 24 | } 25 | } 26 | 27 | const postcssLoader = { 28 | loader: 'postcss-loader', 29 | options: { 30 | sourceMap: options.sourceMap 31 | } 32 | } 33 | 34 | // webpack4.0版本以上采用MiniCssExtractPlugin 而不使用extract-text-webpack-plugin 35 | function generateLoaders(loader, loaderOptions) { 36 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 37 | 38 | if (loader) { 39 | loaders.push({ 40 | loader: loader + '-loader', 41 | options: Object.assign({}, loaderOptions, { 42 | sourceMap: options.sourceMap 43 | }) 44 | }) 45 | } 46 | 47 | if (options.extract) { 48 | return [MiniCssExtractPlugin.loader].concat(loaders) 49 | } else { 50 | return ['vue-style-loader'].concat(loaders) 51 | } 52 | } 53 | 54 | return { 55 | css: generateLoaders(), 56 | postcss: generateLoaders(), 57 | less: generateLoaders('less'), 58 | sass: generateLoaders('sass', { indentedSyntax: true }), 59 | scss: generateLoaders('sass'), 60 | stylus: generateLoaders('stylus'), 61 | styl: generateLoaders('stylus') 62 | } 63 | } 64 | 65 | // Generate loaders for standalone style files (outside of .vue) 66 | exports.styleLoaders = function (options) { 67 | const output = [] 68 | const loaders = exports.cssLoaders(options) 69 | 70 | for (const extension in loaders) { 71 | const loader = loaders[extension] 72 | output.push({ 73 | test: new RegExp('\\.' + extension + '$'), 74 | use: loader 75 | }) 76 | } 77 | 78 | return output 79 | } 80 | 81 | //创建友好错误提示的格式 82 | exports.createNotifierCallback = () => { 83 | const notifier = require('node-notifier') 84 | 85 | return (severity, errors) => { 86 | if (severity !== 'error') return 87 | 88 | const error = errors[0] 89 | const filename = error.file && error.file.split('!').pop() 90 | 91 | notifier.notify({ 92 | title: packageConfig.name, 93 | message: severity + ': ' + error.name, 94 | subtitle: filename || '', 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | // vue-loader v15版本需要引入此插件 5 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 6 | // 服务端渲染用到的插件、默认生成JSON 7 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 8 | 9 | // 用于返回文件相对于根目录的绝对路径 10 | const resolve = dir => path.resolve(__dirname, '..', dir) 11 | 12 | // 创建ESlint相关rules 13 | const createLintingRule = () => ({ 14 | test: /\.(js|vue)$/, 15 | loader: 'eslint-loader', 16 | enforce: 'pre', 17 | include: [resolve('src'), resolve('server')], 18 | options: { 19 | // 更友好、更详细的提示 20 | formatter: require('eslint-friendly-formatter'), 21 | // 只给出警告,开发有很好的体验,emitError为true会阻止浏览器显示内容 22 | emitWarning: !config.dev.showEslintErrorsInOverlay 23 | } 24 | }) 25 | 26 | module.exports = { 27 | entry: resolve('src/entry-client.js'), 28 | output: { 29 | path: config.build.assetsRoot, 30 | filename: '[name].js', 31 | publicPath: process.env.NODE_ENV === 'production' 32 | ? config.build.assetsPublicPath 33 | : config.dev.assetsPublicPath 34 | }, 35 | resolve: { 36 | extensions: ['.js', '.vue'], 37 | alias: { 38 | 'components': resolve('src/components'), 39 | 'assets': resolve('src/assets') 40 | } 41 | }, 42 | module: { 43 | rules: [ 44 | ...(config.dev.useEslint ? [createLintingRule()] : []), 45 | { 46 | // 编译.vue文件, vue-cli2还包含vue-loader.conf.js, 47 | // 但vue-loader15已经将大部分配置改为默认,所以没必要新建个文件 48 | test: /\.vue$/, 49 | loader: 'vue-loader', 50 | options: { 51 | // 配置哪些引入路径按照模块方式查找 52 | transformAssetUrls: { 53 | video: ['src', 'poster'], 54 | source: 'src', 55 | img: 'src', 56 | image: 'xlink:href' 57 | } 58 | } 59 | }, 60 | { 61 | test: /\.js$/, // 利用babel-loader编译js,使用更高的特性,排除npm下载的.vue组件 62 | loader: 'babel-loader', 63 | exclude: file => ( 64 | /node_modules/.test(file) && 65 | !/\.vue\.js/.test(file) 66 | ) 67 | }, 68 | { 69 | test: /\.(png|jpe?g|gif|svg)$/, // 处理图片 70 | use: [ 71 | { 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | { 81 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, // 处理字体 82 | loader: 'url-loader', 83 | options: { 84 | limit: 10000, 85 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 86 | } 87 | } 88 | ] 89 | }, 90 | plugins: [ 91 | new VueLoaderPlugin(), 92 | new VueSSRClientPlugin() 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | const HOST = process.env.HOST 10 | const PORT = process.env.PORT && Number(process.env.PORT) 11 | 12 | const devWebpackConfig = merge(baseWebpackConfig, { 13 | mode: 'development', 14 | entry: { 15 | 'app': path.join(__dirname, '../src/entry-client.js') 16 | }, 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.dev.cssSourceMap, 20 | usePostCSS: true 21 | }) 22 | }, 23 | devtool: config.dev.devtool, // cheap-module-eval-source-map编译更快 24 | plugins: [ 25 | // 热加载必备 26 | new webpack.HotModuleReplacementPlugin(), 27 | // 友好错误提示 28 | new FriendlyErrorsPlugin({ 29 | onErrors: config.dev.notifyOnErrors 30 | ? utils.createNotifierCallback() 31 | : undefined 32 | }) 33 | ] 34 | }) 35 | 36 | module.exports = devWebpackConfig 37 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 8 | 9 | const pordWebpackConfig = merge(baseWebpackConfig, { 10 | mode: 'production', 11 | output: { 12 | path: config.build.assetsRoot, 13 | // chunkhash是根据内容生成的hash, 易于缓存 14 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 15 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 16 | }, 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | // 将css样式单独提取出文件 21 | extract: true, 22 | usePostCSS: true 23 | }) 24 | }, 25 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 26 | plugins: [ 27 | // webpack4.0版本以上采用MiniCssExtractPlugin 而不使用extract-text-webpack-plugin 28 | new MiniCssExtractPlugin({ 29 | filename: utils.assetsPath('css/[name].[contenthash].css'), 30 | chunkFilename: utils.assetsPath('css/[name].[contenthash].css') 31 | }), 32 | // 当vendor模块不再改变时, 根据模块的相对路径生成一个四位数的hash作为模块id 33 | new webpack.HashedModuleIdsPlugin() 34 | ], 35 | // 优化相关, 暂时占个位置 36 | // optimization: { 37 | // splitChunks: { 38 | // chunks: 'all' 39 | // }, 40 | // runtimeChunk: true 41 | // }, 42 | }) 43 | 44 | module.exports = pordWebpackConfig 45 | -------------------------------------------------------------------------------- /build/webpack.server.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const merge = require('webpack-merge') 4 | const nodeExternals = require('webpack-node-externals') 5 | const baseWebpackConfig = require('./webpack.base.conf') 6 | const VueServerPlugin = require('vue-server-renderer/server-plugin') 7 | 8 | module.exports = merge(baseWebpackConfig, { 9 | mode: 'production', 10 | target: 'node', 11 | devtool: 'source-map', 12 | entry: path.join(__dirname, '../src/entry-server.js'), 13 | output: { 14 | libraryTarget: 'commonjs2', 15 | filename: 'server-bundle.js', 16 | }, 17 | externals: nodeExternals({ 18 | whitelist: /\.css$/ 19 | }), 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env.VUE_ENV': '"server"' 23 | }), 24 | // 默认文件名为 `vue-ssr-server-bundle.json` 25 | new VueServerPlugin() 26 | ] 27 | }) 28 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | dev: { 5 | // Paths 6 | assetsSubDirectory: 'static', 7 | assetsPublicPath: '/', 8 | proxyTable: {}, // 可以配置跨域请求 9 | // Dev Server settings 10 | host: '0.0.0.0', // 0.0.0.0可以进行移动端测试 11 | port: 8080, 12 | autoOpenBrowser: false, 13 | errorOverlay: true, 14 | notifyOnErrors: true, 15 | poll: false, // 获取文件变化是否采用轮询 16 | useEslint: true, 17 | showEslintErrorsInOverlay: false, 18 | devtool: 'cheap-module-eval-source-map', 19 | cssSourceMap: true 20 | }, 21 | 22 | build: { 23 | // Template for index.html 24 | index: path.resolve(__dirname, '../dist/index.html'), 25 | // Paths 26 | assetsRoot: path.resolve(__dirname, '../dist'), 27 | assetsSubDirectory: 'static', 28 | assetsPublicPath: '/', 29 | productionSourceMap: false, 30 | devtool: '#source-map', 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |