之前写了一篇如何搭建 vue 1.x 工程的文章《vue vue-router webpack》(以下简称为“上文”),在写上文的时候,vue 2还处于beta版本,现在已经正式发布了,在其发布后不久,我就尝试着按上文的步骤来搭建 vue 2.x 的工程,结论是上文的步骤基本上没什么大的问题,只有部分细节需要调整。为了保证整体步骤的完整性,本文依旧从零开始,以一个新手视角,尽可能详尽且不遗漏的阐述每个步骤,同时又为了精简步骤,上文中的一些无关痛痒的话就省略了,对于没有变化的步骤就直接复制上文。
注意:2017-03-14 将 vue 升至 2.2.4,本文会和 vue 最新版本(仅限 2.x.x)保持一致,升级所带来的影响点会记录在升级记录章节下。
初始化工程
新建工程目录vue2practice,在目录下执行npm init来创建一个package.json,在package.json中先添加以下必备模块:
"devDependencies": {
"vue": "^2.1.10",
"vue-loader": "^10.0.2", // vue 组件(*.vue)的webpack模块加载器
"vue-router": "^2.1.3", // vue 路由插件
"webpack": "^1.14.0", // 模块加载器兼打包工具
"webpack-dev-server": "^1.16.2" // 轻量的 node.js express 服务器,用于开发调试
}
新建目录结构如下,新增的目录及文件先空着,下面的步骤会说明添加什么内容。
vue2pratice
|-- package.json
|-- index.html // 启动页面
|-- webpack.config.js // webpack配置文件
|-- src
|-- components // vue UI 组件目录
|-- views // vue页面组件目录
|-- main.js // 入口文件
|-- router.js // vue-router配置
|-- app.vue // 工程首页组件
配置webpack
webpack 默认读取 webpack.config.js,文件名不能随便改,其中 entry 是必须配置的。
module.exports = {
entry: './src/main.js',
output: {
path: './dist',
publicPath: '/dist/',
filename: 'build.js'
}
}
其中output的各配置项作用如下:
- path: './dist' 打包后js、css、image等存放的目录;
- publicPath: '/dist/' 可以不配置,如果不配置则取默认publicPath: '/',在实际项目中,静态资源一般集中放在一个文件夹下,比如static目录,那么这里就应该改成publicPath: '/static/',相应的 index.html 中引用的 JS 也要改成src="/static/build.js",publicPath 可以解释为最终发布的服务器上 build.js 所在的目录,其他静态资源也应当在这个目录下。
- filename: 'build.js' 打包的js文件名,index.html 引用的 JS 要和这里保持一致。
配置 webpack-dev-server,只需在package.json添加以下启动命令即可。
"scripts": {
"dev": "webpack-dev-server --inline --hot --open"
}
各命令参数作用如下:
- --inline 一共两种模式,默认为iframe模式,inline和iframe模式最明显的区别就是访问路径的不同,iframe模式的访问路径是http://localhost:8080/webpack-dev-server/,实际上iframe模式页面嵌入的<iframe>的地址还是http://localhost:8080/,那岂不是可以直接访问,不禁想问为啥不直接访问呢?因为无论是哪种模式,都是为了做到修改代码后能自动刷新,其中iframe模式是在修改代码后,重新加载iframe,而--inline是刷新浏览器,本质上都是重新全部加载一遍
- --hot iframe不需要配置,配置了反而不能正常刷新了,所以只能配合--inline使用,作用是开启热替换,修改代码后,浏览器只会重新加载修改的组件代码,不会全部重新加载
- --open 自动打开浏览器
配置了server,习惯性的测试下上述配置是否成功,确保后续步骤在一个成功的基石上进行,执行npm install,安装完后执行npm run dev,浏览器会自动打开http://localhost:8080/,能访问(可以在index.html添加内容来确认是否启动成功)则说明上面的配置没问题。
Vue
新建页面
在 views 目录下新建about.vue,不像传统的页面,没有html的结构,其实就是一个vue组件,但是承载着页面的功能,后面访问/about 的就是此文件的内容。
<template>
<div>
这是{{page}}页面
</div>
</template>
<script>
module.exports = {
data: function () {
return {
page: 'about'
}
}
}
</script>
配置路由
vue 2.0必须配套使用vue-router 2.0,新版变化还比较大,熟悉1.0的需要再仔细看下2.0文档。新版配置更为简单,不需要先实例化后再调用map来配置(router.map({'path':{component:''}})),改为直接在实例化时传入配置(new VueRouter({ routes: [{path: '', component: ''}]})),因此可以把传入的配置提取到router.js中,方便后续配置,外部调用方式为new VueRouter(require('./router')) 。router.js内容如下:
module.exports = {
routes: [
{
path: '/about',
component: require('./views/about.vue')
}
]
}
首页
首页(index.html)只需引入打包后的 js 文件(src和webpack.config的output 配置一致),#app是整个网站的挂载点,简单点说其实整个网站就是一个 vue 的实例,#app就是实例el属性值。
<body>
<divid="app"></div>
<scriptsrc="dist/build.js"></script>
</body>
接下来就要配置入口js,这个也是和vue 1.0区别比较大的地方,vue-router 1.0通过VueRouter.start(App,'#app')来初始化整个网站,这种方法在vue-router 2.0已经被废弃,因此需要我们来自己来完成new Vue()。main.js内容如下:
const Vue = require('vue');
const VueRouter = require('vue-router');
const App = require('./app.vue');
Vue.use(VueRouter);
const router = new VueRouter(require('./router'))
new Vue({
el: '#app',
router: router,
render: h => h(App)
})
链接路由的不再使用<a v-link="{ path: '/about' }"></a>,改为组件的方式。app.vue内容如下:
<template>
<div>
<div>
<router-linkto="/about">about</router-link>
</div>
<div>
<router-view></router-view>
</div>
</div>
</template>
配置loader
上面添加了 vue 文件,需要在webpack.config.js中添加vue对应的loader,才能将vue文件解析成可执行的代码。
module: {
loaders: [
{
test: /.vue$/, loader: 'vue'
}
]
}
还记得上文提到过,这时候启动会报错,提示一些依赖的loader未安装,果然:
不同于上文的是这次只报了一个错,下面会说明原因。在package.json添加:
{
"vue-template-compiler":"^2.1.10"
}
安装后再次启动就成功了。
上文报错提示缺少很多vue相关的loader,难道新版不需要了?查看node_modules目录,发现vue-loader所依赖的模块都已经安装了(除了vue-template-compiler是自己添加的)。
原来是vue-loader中已经将所依赖的模块放到了其目录下的package.json中的dependencies,因此不需要自己去添加了,同时也发现vue-template-complier出现在peerDependencies下,这也解释了为什么第一次启动会报缺少它的原因。如图所示:
还有在app.vue文件中使用了ES6的语法(比如:const,箭头函数)也不会报缺少babel-loader,这也归功于vue-loader新加的vue-template-es2015-compiler。不过仅仅是部分语法,比如import等语法还是需要安装babel-loader。
支持添加CSS
此时如果在about.vue文件中添加CSS代码会提示错误Cannot find module "!!vue-style-loader!css-loader!......,这是因为CSS所依赖的loader未安装,前面vue-loader已经自动将其依赖的vue-style-loader安装好了,但是vue-style-loader依赖的css-loader并没有自动安装,需要我们自己安装。在package.json中添加:
"css-loader":"^0.26.0"
安装后我们来验证一下,在about.vue中添加如下样式:
<styletype="text/css">
.about {
color: #f00
}
</style>
运行效果如图所示:
只在vue文件中使用嵌入式(<style>)来添加CSS代码,是不用在webpack.config中配置loader的,可以理解为没有增加新的文件类型,背后是vue-loader利用vue-style-loader和css-loader将<style>编译成JS代码,如果想引用CSS文件的话,那意味着增加了新的文件类型(*.css),也就是说需要安装并配置文件对应的loader才能被编译成JS代码。CSS文件对应的loader为css-loader,前面已经安装,只需在webpack.config.js中增加如下配置:
{
test: /.css$/,
loader: 'vue-style!css'
}
JS中引用CSS文件可以像引入JS模块一样调用require,比如在about.vue中添加require('../css/about.css');,那么about.css的样式就会被添加到页面中,自己可以做下测试。
支持添加图片等静态资源
vue模板以及CSS中免不了使用图片,现在如果直接加,又会报找不到模块的错误,静态资源(图片、图标字体、音频、视频、svg文件等)对应的loader为url-loader,loader信息及配置如下:
{
"url-loader":"^0.5.7",
"file-loader":"^0.9.0" // url-loader依赖file-loader
}
{
test: /.(jpe?g|png|gif|svg|mp3)$/,
loader: "url"
}
支持CSS预处理语言
实际项目中大多会采用less、sass、stylus中的一种预处理语言来组织整个项目的CSS,因此需要添加这些预处理语言对应的loader,各个预处理语言的loader信息如下:
{
"less": "^2.3.1", // less-loader依赖less
"less-loader": "^2.2.3",
"node-sass": "^3.4.2", // sass-loader依赖node-sass
"sass-loader": "^4.0.2",
"stylus": "^0.54.5", // stylus-loader依赖stylus
"stylus-loader": "^2.4.0"
}
同CSS,如果只在vue文件中使用,则不需要配置loader,实际项目中一般只会采用一种,下面将3中常用的一起测试一下。在about.vue添加测试代码如下:
<template>
<divclass="about">
<divclass="test-less">测试less</div>
<divclass="test-sass">测试sass</div>
<divclass="test-stylus">测试stylus</div>
</div>
</template>
<stylelang="less">
@color: #00f;
.test-less {
color: @color;
}
</style>
<stylelang="sass">
$color: #0ff;
.test-sass {
color: $color;
}
</style>
<stylelang="stylus">
color = #f00;
.test-stylus {
color: color;
}
</style>
运行效果如下:
如果想通过引用文件的方式来加载样式,就必须配置loader,配置如下:
{
test: /.less$/,
loader: "vue-style!css!less"
},
{
test: /.scss/,
loader: "vue-style!css!sass"
},
{
test: /.styl/,
loader: "vue-style!css!stylus"
}
将about.vue中各种语言<style>节点中的样式放进一个对应文件中,然后在about.vue中引用各个文件如下:
require('../css/about.less');
require('../css/about.scss');
require('../css/about.styl');
打包发布
项目打包发布有多种选择,比如用gulp,grunt,这里就采用webpack来打包,webpack打包只需一个命令,即webpack --progress --colors,后面参数可以不要,参数是为了让编译的输出内容带有进度(--progress)和颜色(--colors)。在package.json添加如下命令:
"scripts": {
"dev": "webpack-dev-server --inline --hot --open",
"build":"webpack --progress --colors"
}
尝试着执行npm run build(我用的是cnpm),如图:
执行完后发现多了一个dist目录,里面只有一个build.js文件,新增的目录及文件是由webpack.config.js的output配置指定的。查看build.js,我们发现会发现几个问题:
- 就加了一个about.vue文件,怎么build.js就233k了,虽然是未压缩版,依旧很吓人,不禁让人怀疑是不是哪里出错了,心想后面加了很多页面岂不是得好几M了。
- js怎么压缩呢?难道还得构建完后再使用别的插件来压缩js?这么折腾人可不好。
- 页面中添加的CSS哪里去了?怎么在build.js里面!我可不想把CSS放到js里面,怎么才能把CSS单独输出到一个文件里呢?
- 页面中引用图片哪里去了?又在build.js里面!怎么做到的,原来是转成DataUrl了,那要是一个几百k的图片,不敢想了……
以上这些问题使用webpack插件就迎刃而解了。
使用webpack插件
什么是webpack插件?这个就类似gulp需要安装所需要的插件一样,webpack不可能做到什么事都会。虽然webpack本身就依赖各种loader,但是有些是还是loader无法完成的,这个时候就需要插件来助一臂之力了。webpack 本身内置了一些常用的插件,还可以通过 npm 安装第三方插件。
下面依次来介绍哪些插件可以解决上面的问题以及插件的使用方法。
如何使用插件
插件的使用一般是在 webpack 的配置信息 plugins 选项中指定。比如:
var webpack = require('webpack');
module.exports = {
plugins: [
new webpack.BannerPlugin('Hello webpack')
]
}
压缩JS
压缩JS用的插件为webpack.optimize.UglifyJsPlugin,uglifyjs应该比较熟悉,使用方法如下:
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
warinings:false是在删除无用代码的时候,不显示警告,其他同类的配置可以查阅github,下面来执行看看效果。
虽然报错了,仍然输出了build.js,只是未压缩,上图错误是指在main.js第9行12列符号错误,也就是说不识别箭头函数,为啥运行可以?其实如果你去看运行的代码,会发现ES6的语法并未转换,只是Chrome支持这些语法,如果用import就会报错了。解决方法是增加babel-loader,将ES6语法进行转换,在package.json如下模块并安装:
{
"babel-loader": "^6.2.8",
"babel-core": "^6.18.2",
"babel-preset-es2015":"6.18.0"
}
接着配置webpack.config.js:
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel'
}
上文在配置babel-loader的时候还加了很多参数,这里为什么没加?经测试这种方式(babel-loader?presets[]=es2015)没有效果,为此我还在github上提了个问题,尤雨溪(vue的作者)回答是query只适用于babel-loader,不适用于vue-loader,应该使用.babelrc。也可以在package.json中配置,两种配置方式如下:
配置好后再执行命令就不会报错了,压缩后的js大小由原来的233k到现在的90k。
提取CSS
提取CSS的插件叫extract-text-webpack-plugin,这个不是webpack自带的,需要自己安装,在package.json中添加:
{
"extract-text-webpack-plugin": "^1.0.1"
}
配置方式如下,原来的style-loader就要去掉了,毕竟我们的目标是将CSS提取出来而不是放在head中。
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
loaders: [{
test: /.css$/,
loader: ExtractTextPlugin.extract("css")
},
{
test: /.less$/,
loader: ExtractTextPlugin.extract("css!less")
},
{
test: /.scss/,
loader: ExtractTextPlugin.extract("css!sass")
},
{
test: /.styl/,
loader: ExtractTextPlugin.extract("css!stylus")
},
]
},
plugins: [
new ExtractTextPlugin("css/style.css")
]
}
在上面的配置中,样式被提取到了style.css文件,在index.html中添加引用后就生效了。启动发现vue文件中<style>里的样式并未纳入到style.css中,而是继续嵌入式在head中,这是由于vue-loader默认用的是vue-style-loader,想要将vue文件中的样式也提取到文件中,需要在webpack.config添加vue的loaders的配置:
module.exports = {
module: {},
plugins: [],
vue: {
loaders: {
css: ExtractTextPlugin.extract("css"),
less: ExtractTextPlugin.extract("css!less"),
scss: ExtractTextPlugin.extract("css!sass"),
stylus: ExtractTextPlugin.extract("css!stylus")
}
}
}
现在已经将所有的样式都提取到了一个文件中,但你会发现CSS并未压缩,看来ExtractTextPlugin的职责并不包含压缩。
PostCSS
既然上面提到CSS并未压缩,那只能去寻找其他的插件来完成这个工作,PostCSS无疑就是最佳的选择之一,当然PostCSS并不是为压缩CSS而生的,它是一个使用JS插件来转换CSS的工具, 这些插件可以支持使用变量,混入,转换未来的CSS语法,内联图片等操作。
首先安装PostCSS,webpack使用postcss-loader而不是postcss,在package.json中添加postcss-loader并安装:
"postcss-loader":"^1.2.0"
我们的首要目标是压缩CSS,既然PostCSS是依赖插件来完成特定目标的,那么我们需要先找到压缩CSS的插件,查询插件的地址为https://github.com/postcss/postcss或http://postcss.parts/,最终发现插件cssnano可以完成我们的目标。配置PostCSS主要包括两点:
- 在webpack.config中注册PostCSS所需插件,vue要单独指定;
- 在CSS先关的loader中添加postcss,包括vue的loaders。
在package.json中添加cssnano并安装:
"cssnano":"^3.8.1"
配置如下:
module.exports = {
module: {
loaders: [
{
test: /.css$/,
loader: ExtractTextPlugin.extract("css!postcss")
},
{
test: /.less$/,
loader: ExtractTextPlugin.extract("css!postcss!less")
},
{
test: /.scss/,
loader: ExtractTextPlugin.extract("css!postcss!sass")
},
{
test: /.styl/,
loader: ExtractTextPlugin.extract("css!postcss!stylus")
}
]
},
vue: {
loaders: {
css: ExtractTextPlugin.extract("css!postcss"),
less: ExtractTextPlugin.extract("css!postcss!less"),
scss: ExtractTextPlugin.extract("css!postcss!sass"),
stylus: ExtractTextPlugin.extract("css!postcss!stylus")
},
postcss: [require("cssnano")]
},
postcss: [require("cssnano")]
}
上面利用cssnano完成了压缩,其实cssnano不止是完成了压缩,还优化了你的CSS代码,比如丢弃重复的样式规则、压缩选择器、合并规则等等,更多特性可以去官网查看。就这一个插件,你可能就感受到了PostCSS的魅力,还有很多插件可以帮助你提升效率,比如autoprefixer可以自动添加浏览器前缀等,只要你想的到,就没有它做不到。
提取图片
如果是很小的图片,转成DataUrl放在JS中还是可以接受的,毕竟可以减少请求,大图肯定不适合这种方式。如果你完全不想这么做,或者只想单独把大图提出来,只需通过增加url-loader的参数即可。
{
test: /.(jpe?g|png|gif|svg)$/,
loader: "url",
query:{
name:'images/[name].[ext]',
limit:10000 // 单位:字节
}
}
- name:'images/[name].[ext]' 将符合test正则的图片都存在images目录下,[name].[ext]是文件名模板,更多占位符请参考file-loader文档
- limit:10000 小于10kb的图片才会被转化成DataUrl,设为0并不意味着所有的图片都不被转换,如果想所有图片都不被转换,建议设为1
静态资源添加版本号
对于前后端分离的站点来讲,前端所有资源都是静态的,因此防止浏览器缓存就非常有必要。防止缓存的方法一般有两种,一种是在文件名中添加文件内容的hash值(build.xxxx.js),另外一种方法是给每个http请求加版本号参数(build.js?xxxx)。下面分别说明下两种方法如何配置。
在上一章节中提到了文件名模板占位符,添加版本号就是利用占位符[hash],而且无论哪种方式都是利用这个占位符做配置。JS、CSS 及 Image 的配置如下:
// hash可配置选项 [<hashType>:hash:<digestType>:<length>] 即 [哈希算法类型:hash:哈希摘要类型:长度]
// JS
filename: 'build.[hash:8].js' // hash
filename: 'build.js?[hash:8]' // query
// CSS
new ExtractTextPlugin("css/style.[hash:8].css") //hash
new ExtractTextPlugin("css/style.css?[hash:8]") //query
// Image
name: 'images/[name][hash:8].[ext]' // hash
name: 'images/[name].[ext]?[hash:8]' // query
这里很容易想到一个问题,就是打包后的CSS和JS文件名改变了,那意味着index.html中对应的引用地址也要跟着改变,但是现在关键是不知道文件名被改成啥了?(PS:虽然构建完成后显示了,如下图所示)即使知道新的文件名,每次都要手动去改index.html中引用地址也很不方便,希望最好能在打包完成后自动修改对应的引用地址,就像图片一样,CSS或页面中引用的图片自动改成了最新的文件名。下面将阐述如何通过生成首页来解决这个问题。
自动生成首页
自动生成首页需要安装webpack插件html-webpack-plugin,模块信息如下:
"html-webpack-plugin":"^2.24.1"
调用插件可以不传配置项,那么index.html会生成到output.path所指定的目录下,生成的页面会引用构建好的CSS和JS文件,但是不会包含<div id="app">。通过配置生成首页的模板(template),可以在模板中添加无法通过配置来完成的内容,还可以设置首页标题(title)、生成的目录及文件名(filename)、favicon图标(favicon)等等,所有配置参见项目github地址。
// 使用默认配置
new HtmlWebpackPlugin()
// 自定义配置
new HtmlWebpackPlugin({
title:'首页标题',
filename:'../index.html',
template:'index.tpl.html',
favicon:'src/images/favicon.ico'
...
})
自动生成的首页只能用于发布,webpack-dev-server并不会访问生成后的首页,而且在我们开发的过程中也不需要配置诸如JS及CSS的压缩、添加版本号等,因为这些配置无疑会带来server启动变慢、占用资源等问题,解决这些问题最直接的办法就是将开发和生产得配置文件分开。
其他插件
除了上述插件外,还有一些打包时需要用到的插件,列举如下。
使用 webpack 的 DefinePlugin 来指定生产环境,以便在压缩时可以让 UglifyJS 自动删除代码块内的警告语句。
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
})
OccurrenceOrderPlugin 可以根据模块调用次数,给模块分配 ids,常被调用的 ids 分配更短的 id,使得 ids 可预测,降低文件大小。既然能减小打包文件体积,当然要用上。
new webpack.optimize.OccurrenceOrderPlugin()
后续
后面可以做的事还有很多,鉴于本文到这里已经很长了,于是决定将后续的配置另起一篇文章(《vue2 vue-router2 webpack(续)》)来讲解。
升级记录
vue 2.2.4
2017-03-14 将 vue 升级至 2.2.4,步骤无影响,所有升级模块信息如下:
"vue": "^2.2.4",
"vue-loader": "^11.1.4",
"vue-router": "^2.3.0",
"vue-template-compiler": "^2.2.4",
"webpack-dev-server": "^1.16.3"
vue 2.1.10
2017-01-21将vue升级至2.1.10,无影响点。所有升级模块信息如下:
"vue": "^2.1.10", // 原2.1.9
"vue-template-compiler": "^2.1.10", // 原2.1.9
"vue-router": "^2.1.3", // 原2.1.1
vue 2.1.9
2017-01-17将vue升级至2.1.9,无影响点。所有升级模块信息如下:
"vue": "^2.1.9", // 原2.1.8
"vue-template-compiler": "^2.1.9" // 原2.1.8
vue 2.1.8
2017-01-10将vue升级至2.1.8,无影响点。所有升级模块信息如下:
"vue": "^2.1.8", // 原2.1.6
"vue-template-compiler": "^2.1.8" // 原2.1.6
vue 2.1.6
2016-12-20 将vue 升级至 2.1.6,无影响点。所有升级模块信息如下:
"vue": "^2.1.6", // 原 2.1.4
"vue-template-compiler": "^2.1.6", // 原 2.1.4
"webpack": "^1.14.0", // 原 1.13.3
vue 2.1.4
2016-12-07 将 vue 升级至 2.1.4,无影响点。所有升级模块信息如下:
"vue-router": "^2.1.1", // 原2.1.0
"vue-template-compiler": "^2.1.4" // 原2.1.3
vue 2.1.3
2016-12-01 将 vue 升级至 2.1.3,其他模块并未升级,升级后需要安装vue-template-compiler项目才能跑起来