Vue.js SSR Step by Step (1) - 实现简单的client-only vue-webpack 配置

2,667 阅读3分钟

一直都觉得SSR是一个挺麻烦的事情,牵扯的知识范围还挺大的,尤其是用vue-cli 工具,屏蔽了许多配置的细节。
但在使用SSR,不用Nuxt.js 的时候来做SSR,还是挺难上手的,索性好好捋一遍这方面的相关知识,总结成了一个系列的文章。作为 SSR 文档的一个补充,希望对大家有所帮助。

目标

这篇文章的主要目的解读一个简单的 vue-webpack 如何搭建和每一个插件的作用。完成一个 client-only的 vue-webpack 开发环境,具备以下的功能:

  • 处理 vue 单文件组件
  • 编译 ES6
  • 编译 Less 或者 Sass
  • 加载图片
  • 开发服务器
  • 热加载
  • 定义环境变量
  • 能区分生产环境进行压缩

对这相关配置已经非常了解了的同学可以直接关闭了。

添加基本功能

从这一节开始会贴出一步一步实现的代码,尽量还原整个配置的细节。

创建项目文件

$ mkdir simple-webpack && cd simple-webpack
$ npm init

然后按照下面的目录结构新建文件。

.
├── index.html
├── package.json
├── src
│   ├── App.vue
│   ├── app.js
│   └── assets
└── webpack.config.js

安装对应依赖

需要安装 webpack 需要的依赖有:

  • webpack
  • vue-loader
  • babel-core
  • css-loader
  • babel-loader
  • file-loader
  • sass-loader
  • node-sass

依次安装时,把安装依赖保存到package.json,以便下次在不同的环境下使用时,能快速的安装依赖。

添加Vue代码

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>simple - webpack</title>
</head>
<body>
    <div id="app"></div>
  <script src="/dist/build.js"></script>
</body>
</html>

App.vue

<template>
  <div class="demo">
    <h1>Simple-webpack demo</h1>
    <p>这是一个简单的 Vue demo</p>
  </div>
</template>

<script>
</script>

<style>
</style>

app.js

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: h => h(App)
})

添加 webpack.config 配置

先写好我们的入口文件和输出文件的地址和打包后的文件名。

const webpack = require('webpack')
const path = require('path')

module.exports = {
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, './dist/'),
    filename: 'build.js'
  }
}

添加 vue-loader

module.exports = {
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, './dist/'),
    publicPath: '/dist/',
    filename: 'build.js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: ''
      }
    ]
  }
}

此时运行 webpack 打包就可以打包成一个可用的程序了,大家可以自行打包后将文件放在静态服务器中运行。
完成这个基本的打包,用到的webpack loader 和包有:

这个包 npm 官网上都有详细的介绍,这里就不赘述了,大家可以自行去看各个loader 在上面的打包过程中完成什么样工作。

为了让我们编写的代码能在低版本的浏览器中使用,我们添加 babel-loader,在打包的时候将文件中的 ES6 语法转成 .bablerc 中配置的版本。

module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: ''
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.resolve(__dirname, './src')]
      }
    ]
  }

在目录下创建 .babelrc,配置内容如下:

{
  "presets": [
    ["env", { "modules": false }]
  ]
}

然后需要安装一个 babel 插件,babel-preset-env, 关于这个插件的作用具体参见 babel-preset-env: a preset that configures Babel for you

给 App.vue 中添加图片:

<template>
  <div class="demo">
    <h1>Simple-webpack demo</h1>
    <p>这是一个简单的 Vue demo</p>
    <img src="./assets/logo.png" alt="">
  </div>
</template>

因为添加了图片,再运行 webpack 打包的时候,webpack 会报错,因为没有对应的 loader 去加载这些二进制文件。
添加 file-loader:

module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: ''
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.resolve(__dirname, './src')]
      },
      {
        test: /\.(png|jpg|svg|git)$/,
        loader: 'file-loader',
        include: [path.resolve(__dirname, './src/assets')]
      }
    ]
  },

目前我们在单文件组件中,可以使用 css,但是还不能使用 sass,我再添加一个对应的 sass-loader 来处理 sass 文件,因为 css/sass 是 vue-loader 在做代码分割的时候分割出来的文本段,我们只需要在 vue-loader 的 options 中添加对应的配置。

module: {
  rules: [
    {
      test: /\.vue$/,
      loader: 'vue-loader',
      options: {
        'scss': 'vue-style-loader!css-loader!sass-loader',
        'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
      }
    },
    {
      test: /\.js$/,
      loader: 'babel-loader',
      include: [path.resolve(__dirname, './src')]
    },
    {
      test: /\.(png|jpg|svg|git)$/,
      loader: 'file-loader',
      options: {
        name: '[name].[ext]?[hash]'
      }
    }
  ]
},

到目前为止,一个具备打包编译 Vue 项目的 webpack 环境配置已经写好了。接下来我们添加两个比较重要的辅助工具 devServer 和 hot replace。

添加Dev Server 和热更新

添加 Dev Server 的方式的方式有两种:

  • webpack-dev-server
  • webpack-dev-middleware

两种的使用方式和配置可以看这篇官网介绍 开发。这里我们选用一种比较简单的方式,直接使用 webpack-dev-server。
先安装 npm install -S webpack-dev-server ,再修改 package.json 添加 npm script,代码如下:

"scripts": {
  "test": "",
  "dev": "webpack-dev-server --open"
}

open 参数代表服务启动后会自动在浏览器中打开页面。然后再打开 webpack.config.js 文件,添加 devServer 的相关配置。除了以上的配置还能修改 host 和 port,这里我们使用默认就行。添加 hot replace 也是非常简单的,webpack 自带了这一个 plugin,具体使用方法可以看 模块热替换,新增的配置代码如下:

module: {
 //...
},
devServer: {
  historyApiFallback: true,
  hot: true,
  noInfo: false,
  overlay: true
},
plugins: [
  new webpack.HotModuleReplacementPlugin()
]

添加环境变量

process 是 node 中的一个模块,我们可以用它的 env 变量来区分 shell 的环境变量。这样我们就可以通过 npm run devnpm run build来区分我们是开发还是生产构建。
先修改两个 npm script 来区分 shell 环境变量,这里我们借助 cross-env npm 模块实现自定义的环境变量。

"scripts": {
  "test": "",
  "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
  "build": "cross-env NODE_ENV=production webpack"
}
// webpack.config.js
module.exports = {
    //...
}
console.log(process.env.NODE_ENV)
if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}

在 production 环境中添加了两个插件,优化打包的代码和压缩 JS 代码。第一个插件 DefinePlugin 是为前端代码提供与 webpack 一致的环境变量,便于我们在业务代码中区分不同的环境,Vue 框架中也要根据这个环境变量来切分开发环境和生产环境。
我们再稍微整理一下配置,一个简单 client-only 的 webpack 的配置就写好了。

const webpack = require('webpack')
const path = require('path')

module.exports = {
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, './dist/'),
    publicPath: '/dist/',
    filename: 'build.js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          'scss': 'vue-style-loader!css-loader!sass-loader',
          'sass': 'vue-style-loader!css-loader!sass-loader'
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [path.resolve(__dirname, './src')]
      },
      {
        test: /\.(png|jpg|svg|git)$/,
        loader: 'file-loader',
        options: {
          name: '[name].[ext]?[hash]'
        }
      }
    ]
  }
}

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
} else {
  module.exports.devtool = '#eval-source-map'
  module.exports.devServer = {
    historyApiFallback: true,
    hot: true,
    noInfo: false,
    overlay: true
  }
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.HotModuleReplacementPlugin()    
  ])
}

本文只是梳理打造一个简单配置的过程,为后面的 SSR 配置作为基础。webpack 的配置项非常多,而 vue-cli 中提供的 webpack 配置远没有那么简单。更进阶的方式可以阅读参考中文章。

参考