以新手视角,详细介绍各个步骤内容,不深入讲步骤涉及的原理,主要介绍如何操作。
本文示例工程 GitHub:https://github.com/qinshenxue/vue2-vue-router2-webpack2
2017-09-30 升级了 Vue(2.4.4)和 Webpack(3.6.0),github 项目已更新。
2017-11-02 修复了 babel 配置为”presets”: [[“env”, { “modules”: false }]]后,无法使用 import 动态导入功能的问题,在「异步组件(懒加载)」章节已更新解决方法。
新建工程目录 vue2practice,在目录下执行npm init -y来创建一个 package.json,在 package.json 中先添加以下必备模块:
{
"name"
:
"vue2-vue-router2-webpack3"
,
"version"
:
"1.0.0"
,
"devDependencies"
: {
"vue"
:
"^2.4.2"
,
"vue-loader"
:
"^13.0.2"
,
"vue-router"
:
"^2.7.0"
,
"vue-template-compiler"
:
"^2.4.2"
,
"webpack"
:
"^3.4.1"
,
"webpack-dev-server"
:
"^2.6.1"
}
}
|
其中 vue-template-compiler 是 vue-loader 的 peerDependencies,npm3 不会自动安装 peerDependencies,然而 vue-template-compiler 又是必备的,那为什么作者不将其放到 dependencies 中呢?有人在 github 上提过这个问题,我大致翻译一下作者的回答(仅供参考):这样做的原因是因为没有可靠的方式来固定嵌套依赖的关系,怎么理解这句话?首先 vue-template-compiler 和 vue 的版本号是一致的(目前是同步更新),将 vue-template-compiler 指定为 vue-loader 的 dependencies 并不能保证 vue-template-compiler 和 vue 的版本号是相同的,让用户自己指定版本才能保证这一点。查看作者的回答(英文) 。如果两者版本不一致,运行时会出现下图所示的错误提示。
新建目录结构如下,新增的目录及文件先空着,后面的步骤会说明添加什么内容。
vue2pratice
|-- package.json
|-- index.html
// 启动页面
|-- webpack.config.js
// webpack配置文件
|-- src
|-- views
// vue页面组件目录
|-- main.js
// 入口文件
|-- router.js
// vue-router配置
|-- app.vue
// 工程首页组件
|
Webpack 默认读取 webpack.config.js,文件名不能随便改,其中 entry 是必须配置的。
module.exports = {
entry:
'./src/main.js'
,
output: {
path: __dirname +
'/dist'
,
publicPath:
'/static/'
,
filename:
'build.js'
}
}
|
Webpack 2+ 要求output.path必须为绝对路径。关于 Webpack 的各种路径在《详解Webpack2的那些路径》有详细的介绍。
配置 webpack-dev-server,只需在 package.json 添加以下启动命令即可。
"scripts"
: {
"dev"
:
"webpack-dev-server --hot --open"
}
|
webpack-dev-server 2 默认为 inline 模式,热模块替换仍需自己设置。
在 index.html 中添加测试代码,引入打包后的 JS 文件。
Hello, Webpack 3.
<br>
<script src=
"/static/build.js"
></script>
|
在 main.js 中添加测试代码。
// main.js
document.write(
'来自main.js的问候!'
)
|
执行下面的命令来安装模块并启动服务器。
// 安装依赖
npm install
// 运行
npm run dev
|
启动后浏览器会自动打开http://localhost:8080,如果控制台没有报错,页面正确显示 main.js 和 index.html 的内容,改动 main.js 后浏览器不刷新能看到效果,则表示配置没问题。
在 views 目录下新建 index.vue。
<template>
<div>
这是{{page}}页面
</div>
</template>
<script>
export
default
{
data:
function
() {
return
{
page:
'index'
}
}
}
</script>
|
webpack 1 需要特定的 loader 来转换 ES 2015 import/export,webpack 2 起可以开箱即用。但是 ES6 的新语法还是需要 loader 来转换,在没有配置前,先不要用新语法。用了也没报错(比如 let,const等),那是因为你的浏览器已经支持了 ES6 语法(新版本浏览器都已经支持)。
将 vue-router 实例化传入的参数new VueRouter(参数)提取到 router.js 形成路由配置文件。
import index from
'./views/index.vue'
export
default
{
routes: [
{
path:
'/index'
,
component: index
}
]
}
|
从 [email protected],不能用 require 来引入 .vue 文件,因为 .vue 文件最终会被编译成 ES6 module。
首页引入 ouput 配置的 JS,添加 Vue 实例的挂载目标。
<
div
id
=
"app"
></
div
>
<
script
src
=
"/static/build.js"
></
script
>
|
入口JS完成路由配置、初始化 Vue 实例。
import Vue from
'vue'
;
import VueRouter from
'vue-router'
;
import App from
'./app.vue'
;
import routerConfig from
'./router'
;
Vue.use(VueRouter);
var
router =
new
VueRouter(routerConfig)
new
Vue({
el:
'#app'
,
router: router,
render: h => h(App)
});
|
从 Vue 2.2.0 后使用 require(‘vue’) 会报错,应使用 ES6 module(import),具体原因请参考 Vue 更新说明 https://github.com/vuejs/vue/releases,截图如下:
在首页组件 app.vue 中添加路由链接、路由视图组件。
<
template
>
<
div
>
<
div
>
<
router-link
to
=
"/index"
>Home</
router-link
>
</
div
>
<
div
>
<
router-view
></
router-view
>
</
div
>
</
div
>
</
template
>
|
配置 vue 文件对应的 loader。
module: {
rules: [
{
test: /\.vue$/,
use: [
"vue-loader"
]
}
]
}
|
Webpack2 必须在 module.rules 下配置 loader。’-loader’不能省略,必须将 loader 名写全。可以使用 Rule.use 或 Rule.loader 来配置 loader(Rule.loader 是 Rule.use: [ { loader } ] 的简写),建议用 use。
上面完成了新增页面及访问该页面所需的配置,下面来测试下是否能正常访问/index。执行npm run dev,浏览器显示如图界面。
安装 css-loader 后即可在 vue 文件中使用
npm i css-loader -D
|
想要支持import / require引入CSS文件,则需要配置对应的 Rule。
{
test: /\.css$/,
use: [
"vue-style-loader"
,
"css-loader"
]
}
|
<script>
import
"../style/style.css"
</script>
|
以 stylus 为例,安装 stylus 及 stylus-loader。
npm install stylus stylus-loader -D
|
增加 .styl 文件对应的 loader 配置。
{
test: /\.styl$/,
use: [
"vue-style-loader"
,
"css-loader"
,
"stylus-loader"
]
}
|
使用示例:
<style lang=
"stylus"
>
.stylus
.red
color red
</style>
<script>
import
"../css/stylus-example.styl"
</script>
|
使用淘宝镜像:npm set disturl https://npm.taobao.org/dist
也可以单独设置node-sass镜像:npm set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass
安装图片及图标字体依赖的loader。
npm install url-loader file-loader -D
|
增加图片及图标字体的loader配置。
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [{
loader:
"url-loader"
,
options: {
limit: 10000,
name:
'images/[name].[hash:7].[ext]'
// 将图片都放入images文件夹下,[hash:7]防缓存
}
}]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [{
loader:
"url-loader"
,
options: {
limit: 10000,
name:
'fonts/[name].[hash:7].[ext]'
// 将字体放入fonts文件夹下
}
}]
}
|
添加打包命令如下:
"build"
:
"webpack --progress --colors"
|
执行npm run build开始构建,完成后,可以看到工程目录下多了dist目录以及 dist/build.js。
在之前的文章提到过,打开未压缩版的build.js,你会发现ES6的语法没有被转化为ES5,因此需要安装babel 套件来完成语法的转化,否则压缩的时候就会报错。之前广泛使用的转码规则为 babel-preset-es2015,但 Babel 的官网上在9月宣布 ES2015 / ES2016/ ES2017 等等 ES20xx 时代的 presets 通通被废弃(deprecated),取而代之的是 babel-preset-env,并且承诺它将成为“未来不会过时的(future-proof)”解决方案。
npm i babel-loader babel-core babel-preset-env -D
|
增加babel的配置文件.babelrc。
{
"presets"
: [
[
"env"
, {
"modules"
:
false
}]
],
"comments"
:
false
}
|
将 modules 设置为 false,即交由 Webpack 来处理模块化,通过其 TreeShaking 特性将有效减少打包出来的 JS 文件大小,可以自行对比下前后打包出来的文件的大小,效果还是不错的。
comments 即是否保留注释。
接着配置 JS 文件的 loader。
{
test: /\.js$/,
use:
"babel-loader"
,
include: [path.resolve(__dirname,
'src'
)]
}
|
注意:Webpack2建议尽量避免exclude,更倾向于使用include。
压缩 JS 采用webpack.optimize.UglifyJsPlugin,配置如下:
new
webpack.optimize.UglifyJsPlugin()
|
官网称warnings默认为false,你可能会遇到即使没有配置warnings: true,控制台仍显示警告,看下面这段源码就知道了。查看源码
只有当options.compress !== false时 warnings 才会被设置默认值 false,所以一旦配置了 compress 其它选项,那就需同时配置warnings: false。
warnings作用是当插件在压缩过程中移除的无效代码或定义是显示警告信息(display warnings when dropping unreachable code or unused declarations etc.)。
使用extract-text-webpack-plugin插件提取CSS。更改 css 及 less 的 loader 配置如下。
// 安装插件
npm i extract-text-webpack-plugin -D
|
// var ExtractTextPlugin = require("extract-text-webpack-plugin")
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use:
"css-loader"
})
},
{
test: /\.styl$/,
use: ExtractTextPlugin.extract({
use: [
"css-loader"
,
"stylus-loader"
]
})
}
|
上述配置并不能提取 vue 文件中的 style,需要设置 vue-loader 参数才可以。
{
test: /\.vue$/,
use: {
loader:
"vue-loader"
,
options: {
loaders: {
css: ExtractTextPlugin.extract({
use:
'css-loader'
}),
stylus: ExtractTextPlugin.extract({
use: [
"css-loader"
,
"stylus-loader"
]
})
}
}
}
}
|
初始化插件,filename 可以指定 CSS 文件的目录。
new
ExtractTextPlugin({
filename:
"css/style.css"
})
|
安装 postcss-loader 及 postcss 插件。
npm i postcss-loader cssnano -D
|
配置 loader 如下:
// css-loader配置改为
use: [
'css-loader'
,
"postcss-loader"
]
// stylus-loader配置改为
use: [
"css-loader"
,
"postcss-loader"
,
"stylus-loader"
]
|
postcss-loader 要放在 css-loader 和 style-loader 之后,CSS 预处理语言 loader 之前(stylus-loader)。
新增 postcss.config.js 来配置postcss插件,这样就不用给每个 postcss-loader 去配置。更多 postcss-loader 的配置方式请参考 postcss-load-config。
module.exports = {
plugins: [
require(
'cssnano'
)
]
}
|
cssnano 使用了一系列 postcss 插件,包含了常用的 autoprefixer 等,如何传入 autoprefixer 的配置?
require(
'cssnano'
)({
autoprefixer: {
add:
true
,
browsers: [
'> 5%'
]
}
})
|
其中有一个插件 postcss-zindex 使用中发现有些问题。如果想禁用这个插件的话,配置如下:
require(
'cssnano'
)({
zindex: {
disable:
true
}
})
|
附:postcss插件分类搜索网站:http://postcss.parts/
安装 html-webpack-plugin 插件。
npm i html-webpack-plugin -D
|
初始化插件。
// var HtmlWebpackPlugin = require('html-webpack-plugin');
new
HtmlWebpackPlugin({
filename:
'index.html'
,
template:
'index.tpl.html'
})
|
Webpack3 新增的作用域提升。
new
webpack.optimize.ModuleConcatenationPlugin()
|
指定生产环境,以便在压缩时可以让 UglifyJS 自动删除代码块内的警告语句。
new
webpack.DefinePlugin({
'process.env.NODE_ENV'
: JSON.stringify(
'production'
)
})
|
因为这个插件直接做的文本替换,给定的值必须包含字符串本身内的实际引号。通常,有两种方式来达到这个效果,使用 ‘”production”‘, 或者使用 JSON.stringify(‘production’)。
你完全可以在自己的代码中使用process.env.NODE_ENV来区分开发和生产,从而针对不同的环境做一些事情。不用担心这部分代码会被保留,最终会被 UglifyJS 删除。例如:
if
(process.env.NODE_ENV !=
"production"
) {
// 开发环境
}
// webpack.DefinePlugin插件替换后,上述代码会变成
if
(
"production"
!=
"production"
) {
// 开发环境
}
// 输出
if
(
false
) {
// 开发环境
}
// UglifyJS 会删除这段无效代码
|
使用上述插件后再次构建,会发现生成的JS相比原来的体积小了不少。
friendly-errors-webpack-plugin 是一个更友好显示 webpack 错误信息的插件。插件 github 地址:https://github.com/geowarin/friendly-errors-webpack-plugin
一般在开发环境下使用。
var
FriendlyErrorsWebpackPlugin = require(
'friendly-errors-webpack-plugin'
);
var
webpackConfig = {
// ...
plugins: [
new
FriendlyErrorsWebpackPlugin(),
],
// ...
}
|
效果如下图:
显示构建进度插件:webpack.ProgressPlugin
{
// ...
plugins: [
new
webpack.ProgressPlugin(),
],
// ...
}
|
效果如下图:
美化 webpack 编译控制台打印的信息的插件webpack-dashboard
将开发和生产配置文件分离,方便增加各个环境下的个性配置。Webpack2文档中也详细阐述了如何为多环境配置webpack。基本思路如下:
webpack.base.config.js 内容如下:
var
webpack = require(
'webpack'
);
var
path = require(
'path'
);
var
utils = require(
'./utils'
);
function
resolve(relPath) {
return
path.resolve(__dirname, relPath);
}
module.exports = {
entry: { app: resolve(
'../src/main.js'
) },
output: {
filename:
'js/[name].js'
},
module: {
rules: [{
test: /\.js$/,
use:
"babel-loader"
,
include: [resolve(
'../src'
)]
},
{
test: /\.vue$/,
use: {
loader:
"vue-loader"
,
options: utils.vueLoaderOptions()
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader:
"url-loader"
,
options: {
limit: 10000,
name:
'images/[name].[hash:7].[ext]'
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [{
loader:
"url-loader"
,
options: {
limit: 10000,
name:
'fonts/[name].[hash:7].[ext]'
}
}]
}
]
}
}
|
为什么要将vue-loader的options提取出来?其主要是用来配置CSS及CSS预处理语言的loader,开发环境可以不用配置,但是生产环境需要提取CSS、增加postcss-loader等,因此需要提取出来针对不同环境返回相应的options。后面会列出utils.vueLoaderOptions的内容。
为什么没有配置CSS的loader?理由和上面的vue-loader一样。
为什么没有配置path和publicPath?一方面是个性化参数,另外开发和生产可能不相同,因此在webpack.dev.config和webpack.prod.config中分别配置更为合适。
webpack.dev.config.js 内容如下:
var
webpack = require(
'webpack'
);
var
merge = require(
'webpack-merge'
);
var
HtmlWebpackPlugin = require(
'html-webpack-plugin'
);
var
baseWebpackConfig = require(
'./webpack.base.config'
);
var
utils = require(
'./utils'
);
var
config = require(
'./config'
);
Object.keys(baseWebpackConfig.entry).forEach(
function
(name) {
baseWebpackConfig.entry[name] = [
`webpack-dev-server/client?http:
//localhost:${config.dev.port}/`,
"webpack/hot/dev-server"
].concat(baseWebpackConfig.entry[name])
});
module.exports = merge(baseWebpackConfig, {
output: {
path: config.dev.outputPath,
publicPath: config.dev.outputPublicPath
},
module: {
rules: utils.styleLoaders()
},
plugins: [
new
webpack.HotModuleReplacementPlugin(),
new
HtmlWebpackPlugin({
filename:
'index.html'
,
template:
'index.html'
,
inject:
true
})
]
})
|
添加了HtmlWebpackPlugin后,index.html中就不需要在自己去引用打包的JS了,会自动根据打包的JS添加引用,这样更加方便,关于HtmlWebpackPlugin的配置,需要说明两点:
1.template的路径是相对于webpack编译时的上下文目录,说白了就是项目根目录,因此上面可以直接配置index.html,其指向的就是根目录下的index.html;
2.filename则是相对于webpack配置项output.path(打包资源存储路径)。
html-webpack-plugin关于template和filename路径源码如下:
// template
this
.options.template =
this
.getFullTemplatePath(
this
.options.template, compiler.context);
// filename
this
.options.filename = path.relative(compiler.options.output.path, filename);
|
config.js内容如下:
module.exports = {
dev: {
outputPath: path.resolve(__dirname,
'../static'
),
outputPublicPath:
'/'
,
port: 8000
},
prod: {
outputPath: path.resolve(__dirname,
'../static'
),
outputPublicPath:
'/static/'
}
}
|
utils.js内容如下:
var
ExtractTextPlugin = require(
'extract-text-webpack-plugin'
);
var
isProd = process.env.NODE_ENV ===
"production"
;
// 根据项目需求添加CSS预处理语言并安装相应的loader,以stylus-loader为例
var
cssLang = [{
name:
'css'
,
reg: /\.css$/,
loader:
'css-loader'
}, {
name:
'stylus'
,
reg: /\.styl$/,
loader:
"stylus-loader"
}];
function
genLoaders(lang) {
var
loaders = [
'css-loader'
,
'postcss-loader'
];
if
(lang.name !==
'css'
) {
loaders.push(lang.loader);
}
if
(isProd) {
// 生产环境需要提取CSS
loaders = ExtractTextPlugin.extract({
use: loaders
});
}
else
{
// 开发环境需要vue-style-loader将CSS提取到页面头部
loaders.unshift(
'vue-style-loader'
);
}
return
loaders;
}
// 各种CSS的loader
exports.styleLoaders =
function
() {
var
output = [];
cssLang.forEach(lang => {
output.push({
test: lang.reg,
use: genLoaders(lang)
})
})
return
output;
};
// vue-loader的options
exports.vueLoaderOptions =
function
() {
var
options = {
loaders: {}
};
cssLang.forEach(lang => {
options.loaders[lang.name] = genLoaders(lang);
});
return
options;
}
|
接下来就是如何启动webpack-dev-server,vue-cli的webpack模板工程用的express及webpack中间件做开发服务器,其实用webpack-dev-server就能满足需求,当然用express能够做更多的事情,毕竟webpack-dev-server是一个轻量级的express。dev.js内容如下:
var
webpack = require(
'webpack'
);
var
webpackDevServer = require(
'webpack-dev-server'
);
var
devConfig = require(
"./webpack.dev.config"
);
var
config = require(
"./config"
);
var
compiler = webpack(devConfig);
var
server =
new
webpackDevServer(compiler, {
hot:
true
,
noInfo:
true
,
publicPath: config.dev.outputPublicPath,
stats: { colors:
true
}
});
server.listen(config.dev.port,
"0.0.0.0"
);
var
url = `http:
//localhost:${config.dev.port}/`;
// 需先安装 opn 模块 npm i opn -D
var
opn = require(
'opn'
);
// 打包完毕后启动浏览器
server.middleware.waitUntilValid(
function
() {
console.log(`> Listening at ${url}`);
opn(`${url}`);
})
|
生产配置文件(webpack.prod.config.js)内容如下:
// 设定为生产环境
process.env.NODE_ENV =
'production'
;
var
webpack = require(
'webpack'
);
var
merge = require(
'webpack-merge'
);
var
HtmlWebpackPlugin = require(
'html-webpack-plugin'
);
var
ExtractTextPlugin = require(
'extract-text-webpack-plugin'
);
var
baseWebpackConfig = require(
'./webpack.base.config'
);
var
utils = require(
'./utils'
);
var
config = require(
'./config'
);
module.exports = merge(baseWebpackConfig, {
output: {
path: config.prod.outputPath,
publicPath: config.prod.outputPublicPath
},
module: {
rules: utils.styleLoaders()
},
plugins: [
new
webpack.DefinePlugin({
'process.env.NODE_ENV'
:
'"production"'
}),
new
webpack.optimize.UglifyJsPlugin(),
new
ExtractTextPlugin({
filename:
"css/style.css?[contenthash:8]"
}),
new
HtmlWebpackPlugin({
filename:
'index.html'
,
template:
'index.html'
,
inject:
true
})
]
|