必要的库

首先初始化项目

1
npm init

安装必要的库

1
npm install -S vue vue-router

然后是构建工具,在webpack4中需要单独安装cli

1
npm install -D webpack webpack-cli

项目结构

  • main.js: 是应用的入口
  • src目录:存放代码
  • App.vue:存放根组件
  • views:存放所有顶层组件
  • router.js:存放路由
  • stores:存放vuex的actions/mutations
  • assets:存放图片
  • shared:存放共享组件

接下来需要配置webpack来打包整个应用,webpack通过entry来构建整个依赖图,使用不同的loaders来加载文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ ├── layout
│ ├── main.js
│ ├── router.js
│ ├── shared
│ ├── stores
│ └── views
└── webpack.config.dev.js

Webpack配置

开发配置

编写开发中使用的配置。

  1. 将mode改为development
  2. 入口entry设置为’./‘
  3. 安装vue-loader(导入单文件组件形式的.vue文件)、vue-template-compiler(用于编译vue模版,生成渲染函数)、vue-style-loader(style-loader的改版,style-loader用于将css打包放入\<style>标签中)、css-loader(将url()@import的代码导入)

.vue文件中如果需要import xx from '@/icons',需要设置路径别名,

1
2
3
4
5
resolve: {
alias: {
'@': path.resolve(__dirname, './src') // 定义@为src目录,import xx from '@/icons' 相当于 from 'src/icons'
},
}

一般看到的代码不需要import xx from '@/icons.vue'这是因为在resolve中指定了默认的扩展名.

1
2
3
resolve: {
extensions: ['.js', '.vue'] // 这样就不需要在import from中写尾缀
}

Vue-loader

vue-loader将.vue文件拆分后需要根据不同的代码来调用指定的loader,这个拦截操作是由VueLoaderPlugin实现,比如<script>中的代码,会应用在test: /\.js$/的加载器

1
2
3
4
5
6
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
plugins:[
new VueLoaderPlugin()
]
}

SASS

为支持Sass,需要安装相关加载器sass-loader

1
npm install sass-loader node-sass webpack --save-dev

写入配置,loader执行从下到上执行

1
2
3
4
5
6
7
8
{
test: /\.css|\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
}

SVG导入

因为icon都是svg格式文件,需要使用加载器导入,这里使用svg-sprite-loader,将svg导入为svg雪碧图。

1
npm install svg-sprite-loader -D
1
2
3
4
5
6
7
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
options: {
symbolId: 'icon-[name]' // 指定id,svg use使用id来引用其他svg
}
}

Dev-server

每次修改都需要手动执行webpack --config webpack.config.dev.js很麻瓜,还要webpack能够使用webpack-dev-server。

首先安装一波

1
npm install --save-dev webpack-dev-server

修改原来的command

1
2
3
4
原来的
"dev": "webpack --config webpack.config.dev.js"
新的
"dev": "webpack-dev-server --config webpack.config.dev.js"

JS文件注入

需要把构建好的文件自动注入到index.html中

1
npm install --save-dev html-webpack-plugin

在webpack.config.dev.js中新增

1
2
3
4
5
6
7
8
9
10
11
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins:[
new HtmlWebpackPlugin({
template: 'index.html',
filename: 'index.html',
favicon: 'favicon.ico',
inject: true
})
]
}

HMR

现在就能修改文件后会自动编译刷新,但这样的整页刷新有点捞,使用webpack自带的HotModuleReplacementPlugin来实现热替换。

1
2
3
4
5
6
const webpack = require('webpack')
module.exports = {
plugins:[
new webpack.HotModuleReplacementPlugin(),
]
}

ES6语法

为了支持es6语法,需要安装babel转译器

1
npm install --save-dev @babel/core babel-loader @babel/preset-env

在webpack.config.dev.js新增

1
2
3
4
{
test: /\.js%/,
use: 'babel-loader'
}

添加.babelrc,填入下面的代码

1
2
3
4
5
6
7
8
9
10
{
"presets": [
["@babel/env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}]
]
}

###代码风格

为了规范代码风格,使用eslint

首先是安装

1
npm install --save-dev eslint eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard eslint-config-standard babel-eslint eslint-loader eslint-plugin-vue

在根目录创建.eslintrc.js,填写规范

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'plugin:vue/recommended',
'standard'
],
plugins: [
'vue'
]
}

在package.json中添加新命令"lint": "eslint --ext .js,.vue src"

静态文件处理

将它们拷贝到dist目录

1
npm install --save-dev copy-webpack-plugin
1
2
3
4
5
6
7
8
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins:[
new CopyWebpackPlugin([
{ from: path.join(__dirname, 'src', 'assets'), to: path.join(__dirname, 'dist/static/img'), toType: 'dir'}
])
]
}

测试

Vue.js的官方单元测试是vue-test-utils,教程https://vue-test-utils.vuejs.org/

安装依赖

1
npm install --save-dev jest babel-jest vue-jest jest-serializer-vue @vue/test-utils

Jest: 是单元测试驱动器,vue-jest将单文件组件转换为jest的格式,jest-serializer-vue提供vue组件快照功能。因为测试是跑在node环境,所以新语法需要使用babel-jest,然后提供jest.transform配置。在webpack中使用babel-preset-env,babel会默认关闭es module的转译,因为webpack默认支持,所以需要开启。

向package.json文件添加下面的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"scripts": {
"test": "jest",
},
"jest": {
"moduleFileExtensions": [
"js",
"vue"
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
},
"snapshotSerializers": [
"<rootDir>/node_modules/jest-serializer-vue"
]
}

在使用时遇到

1
2
3
4
5
6
7
8

FAIL src/__tests__/App.spec.js
● Test suite failed to run

Cannot find module 'babel-core'

However, Jest was able to find:
'../App.vue'

因为我的babel太新了,所以vue-jest的require(‘babel-core’)是老babel写法。将vue-jest/compilers/babel-compiler.js中改为require(‘@babel/core’)

提取CSS

将vue单组件文件中的css部分提取到单独的css文件中

1
npm install --save-dev mini-css-extract-plugin

在webpack中添加相关plugin

1
2
3
4
5
6
7
8
9
10
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
test: /\.css|\.scss$/,
use: [
MiniCssExtractPlugin.loader,
// 'vue-style-loader'
'css-loader',
'sass-loader'
]
},

环境分离

将webpack配置分为develop和production两个,首先将原有文件改为webpack.config.comm.js,安装clean-webpack-plugin、webpack-merge

1
npm install --save-dev clean-webpack-plugin webpack-merge

新增clean-webpack-plugin配置到webpack.config.comm.js

1
2
3
plugins: [
new CleanWebpackPlugin(),
]
  • 新建webpack.config.dev.js将devServer相关代码复制进来,使用webpack-merge合并webpack.config.comm.js
  • 新建webpack.config.prod.js将mode改为 ‘production’,使用webpack-merge合并webpack.config.comm.js
  • mini-css-extract-plugin这个插件会导致hmr对于css的改动不会自动刷新,所以将这个插件的配置放到prod中

修改package.json

1
2
3
4
5
6
"scripts": {
"dev": "webpack-dev-server --config webpack.config.dev.js --color",
"build": "webpack --config webpack.config.prod.js --color",
"test": "jest",
"lint": "eslint --ext .js,.vue src"
},

使用npm run build用于打包应用,npm run dev用于开发

美化构建过程

安装相关组件主要是不显示webpack自带的输出,而是改为friendly-errors-webpack来输出。webpackbar美化进度条

npm install friendly-errors-webpack-plugin webpackbar --save-dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: false,
hot: true,
open: 'Chrome',
quiet: true, // 不显示devServer信息
overlay: true // 编译出现错误时将错误显示在页面中
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: ['Application running in http://localhost:8080/']
},
clearConsole: true
})
]

SASS全局变量

因为需要对一些颜色啥的统一起来,所以将它们放到src/styles/index.scss中,然后在组件里直接使用这些变量就行。这个功能需要npm install -S sass-resources-loader

然后在css、scss的加载器前添加它,resources选项就是指明那些全局变量的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rules: [
{
test: /\.css|\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
{
loader: 'sass-resources-loader',
options: {
resources: path.resolve(__dirname, './src/styles/index.scss')
}
}
]
}
]

加载字体

因为用了Element-UI,然后导入样式时import 'element-ui/lib/theme-chalk/index.css'会出现错误:

1
2
3
4
5
6
7
8
9
Failed to compile.
./node_modules/element-ui/lib/theme-chalk/fonts/element-icons.ttf?t=1510834658947
Module parse failed: Unexpected character ‘’ (1:0)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)
@ ./node_modules/css-loader!./node_modules/element-ui/lib/theme-chalk/index.css 7:343-395
@ ./node_modules/element-ui/lib/theme-chalk/index.css
@ ./src/main.js
@ multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./src/main.js

这是因为没有配置字体的loader,这个loader就是url-loader

首先安装npm install -D url-loader,然后配置

1
2
3
4
5
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
loader: 'url-loader',
exclude: path.resolve(__dirname, './src/icons/svg')
}

需要注意的是因为之前配置的图标使用了svg-sprite-loader,在url-loader处理后导致svg-sprite-loader出错,这时候添加exlude字段就行,排除图标所在文件夹。

在svg-sprite-loader指定svg图片路径:

1
2
3
4
5
6
7
8
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
options: {
symbolId: 'icon-[name]', // 将id变为icon-logo这种形式
publicPath: path.resolve(__dirname, './src/icons/svg')
}
},

import()动态加载

这个语法需要babel的插件

npm install @babel/plugin-syntax-dynamic-import -S

在babelrc中添加

1
2
3
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}

开发页面时代理问题

有时候前端使用不同的URL来调用后端API,比如API是知乎提供的,使用前端ajax请求就会遇到跨域问题,我是使用flask需要安装Flask-Cros,webpack的devServer需要配置, http://127.0.0.1:5000就是后端提供的api地址:

1
2
3
4
proxy: {
'/': 'http://127.0.0.1:5000',
changeOrigin: true
}

最终配置

请参考我的github仓库vue-webpack

参考

https://itnext.io/vuejs-and-webpack-4-from-scratch-part-1-94c9c28a534a

https://itnext.io/vue-js-and-webpack-4-from-scratch-part-2-5038cc9deffb

https://itnext.io/vue-js-and-webpack-4-from-scratch-part-3-3f68d2a3c127