从零搭建 vue2 vue-router2 webpack3 工程
以新手视角,详细介绍各个步骤内容,不深入讲步骤涉及的原理,主要介绍如何操作。
文中示例工程地址:https://github.com/qinshenxue/vue2-vue-router2-webpack2
##初始化工程
新建工程目录 vue2-vue-router2-webpack3,在目录下执行npm init -y
来创建一个 package.json,在 package.json 中先添加以下必要模块:
{
"name": "vue2-vue-router2-webpack3",
"version": "1.0.0",
"devDependencies": {
"vue": "^2.5.15",
"vue-loader": "^14.2.1",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.15",
"webpack": "^3.11.0",
"webpack-dev-server": "^2.11.1"
}
}
其中 vue-template-compiler 是 vue-loader 的 peerDependencies,npm3 不会自动安装 peerDependencies,然而 vue-template-compiler 又是必备的,那为什么作者不将其放到 dependencies 中呢?有人在 GitHub 上提过这个问题,我大致翻译一下作者的回答(仅供参考,点击查看作者的回答):这样做的原因是因为没有可靠的方式来固定嵌套依赖的关系。怎么理解这句话?vue-template-compiler 是放在 Vue 项目中,因此 vue-template-compiler 和 Vue 的是同步更新的,也就是说两者版本号永远保持一致。如果将 vue-template-compiler 放到 vue-loader 的 dependencies 中,遇到 Vue 升级而 vue-loader 不需要升级的情况,也不得不修改 vue-template-compiler 的版本号来升级 vue-router,显然是不合适的。总之最简单的方式就是让用户自己来指定版本。如果两者版本不一致,运行时会出现下图所示的错误提示。
新建目录结构如下,新增的目录及文件先空着,后面的步骤会说明添加什么内容。
vue2-vue-router2-webpack3
|-- package.json
|-- index.html // 访问首页
|-- webpack.config.js // Webpack 配置文件
|-- src
|-- views // Vue 页面目录
|-- main.js // 入口起点
|-- router.js // vue-router 配置
|-- app.vue // Vue 根组件
##配置 Webpack
Webpack 默认读取 webpack.config.js,文件名不能随便改,其中 entry 是必须配置的,构建时,output.filename
是必需的。
module.exports = {
entry: './src/main.js',
output: {
path: __dirname + '/dist',
publicPath: '/static/',
filename: 'build.js'
}
}
!>output.path
必须为绝对路径。关于 Webpack 的各种路径配置在《详解 Webpack2 的那些路径》有详细的介绍。
webpack-dev-server 不需要配置文件,直接使用其 CLI 提供的命令即可。
"scripts": {
"dev": "webpack-dev-server --hot --open"
}
--hot
:修改代码后,不用自己刷新页面就能看到修改后的结果--open
:启动后自动打开浏览器
##验证配置
在 index.html 中添加测试代码,引入打包后的 JavaScript 文件。
<body>
Hello, Webpack 3.
<br>
<script src="/static/build.js"></script>
</body>
在 main.js 中添加测试代码。
// main.js
document.write('来自 main.js 的问候!')
执行下面的命令来安装依赖模块并启动本地服务器。
# 安装依赖模块
npm install
# 启动本地服务器
npm run dev
启动后浏览器会自动打开http://localhost:8080
,如果控制台没有报错,页面正确显示 main.js 和 index.html 的内容,改动 main.js 后浏览器会自动刷新,则表示配置没问题。
##Vue
###新建页面
在 views 目录下新建 index.vue。
<template>
<div>
这是{{page}}页面
</div>
</template>
<script>
export default {
data: function () {
return {
page: 'index'
}
}
}
</script>
###配置路由
将 vue-router 实例化传入的参数提取到 router.js 作为路由配置文件。
import index from './views/index.vue'
export default {
routes: [
{
path: '/index',
component: index
}
]
}
###修改首页
在 index.html 添加 Vue 根实例的挂载元素。
<body>
<div id="app"></div>
<script src="/static/build.js"></script>
</body>
修改入口
在 main.js 完成路由配置、初始化 Vue 实例。
import Vue from "vue"
import VueRouter from "vue-router"
import App from "./app.vue"
import routerConfig from "./router"
Vue.use(VueRouter)
const router = new VueRouter(routerConfig)
new Vue({
el: "#app",
router: router,
render: h => h(App)
})
在根组件 app.vue 中添加路由链接、路由视图组件。
<template>
<div>
<div>
<router-link to="/index">Home</router-link>
</div>
<div>
<router-view></router-view>
</div>
</div>
</template>
###配置 loader
配置 .vue 文件对应的 loader。
module: {
rules: [
{
test: /\.vue$/,
use: ["vue-loader"]
}
]
}
上面完成了访问一个页面所需要的步骤,接下来可以启动本地服务器(npm run dev
)来测试能否正常访问/index
。
##支持 CSS
直接在 .vue 文件中使用 CSS 会提示Module not found: Error: Can't resolve 'css-loader'
,安装 css-loader 后就可以了。
npm install css-loader -D
想要支持import / require
引入 CSS 文件,则需要配置 .css 文件对应的 loader。
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"]
}
<script>
import "../css/style.css"
</script>
!> vue-style-loader 是 vue-loader 的 dependencies,因此不需要再自己安装,css-loader 是 vue-loader 的 peerDependencies,需要自己安装
##支持 CSS 预处理语言
根据需要安装预处理语言模块及对应的 loader。
# less
npm install less less-loader -D
# sass
npm install node-sass sass-loader -D
# stylus
npm install stylus stylus-loader -D
node-sass 安装慢的解决办法:
- 使用淘宝镜像:
npm set disturl https://npm.taobao.org/dist
- 也可以单独设置 node-sass 镜像:
npm set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass
各种预处理语言的 loader 配置。
// less
{
test: /\.less$/,
use: ["vue-style-loader", "css-loader", "less-loader"]
}
// sass
{
test: /\.s[ac]ss$/,
use: ["vue-style-loader", "css-loader", "sass-loader"]
}
// stylus
{
test: /\.styl$/,
use: ["vue-style-loader", "css-loader", "stylus-loader"]
}
使用示例。
<style lang="less">
body {
color: red;
}
</style>
<style lang="sass">
body
color: green
</style>
<style lang="scss">
body {
color: blue;
}
</style>
<style lang="stylus">
body
color: gray
</style>
<script>
import "../css/style.less"
import "../css/style.scss"
import "../css/style.sass"
import "../css/style.styl"
</script>
##支持图片及图标字体
安装图片及图标字体依赖的 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 文件夹下
}
}]
}
##构建
使用 Webpack CLI 提供的命令构建。点击查看 Webpack 命令参数
"build": "webpack --progress"
执行npm run build
开始构建,完成后,可以看到工程目录下多了 dist 目录,里面包含了打包后的 JavaScript 文件、图片、图标字体文件,但是打包后的 JavaScript 文件没有被压缩,里面还包含了 CSS 代码,语法也没有被转换成 ES5,这些工作就需要使用 Webpack 插件来完成。
##使用 Webpack 插件
ES6 语法转换
安装 babel 套件来完成语法的转化,之前广泛使用的转码规则为 babel-preset-es2015,但 Babel 官网在 9 月宣布 ES2015 / ES2016/ ES2017 等 ES20xx 时代的 presets 通通被废弃,取而代之的是 babel-preset-env,并且承诺它将成为“未来不会过时的”解决方案。
npm install babel-loader babel-core babel-preset-env -D
增加 babel 的配置文件.babelrc
。
{
"presets": [
["env", { "modules": false }]
],
"comments": false
}
modules: false
:禁用 babel 的模块处理,交由 Webpack 来处理模块化,通过其 TreeShaking 特性将有效减少打包出来的 JS 文件大小,可以自行对比下前后打包出来的文件的大小,效果还是不错的comments: false
:不保留注释信息
接着配置 JavaScript 文件的 loader。
var path= require("path")
{
test: /\.js$/,
use: "babel-loader",
include: [path.resolve(__dirname, 'src')]
}
!>Webpack 建议尽量避免 exclude,更倾向于使用 include。
###JavaScript 文件压缩
在 webpack.config.js 中增加压缩插件:
var webpack = require("webpack")
plugins:[
new webpack.optimize.UglifyJsPlugin()
]
###提取CSS
使用 extract-text-webpack-plugin 插件提取 CSS,更改 CSS 及 CSS 预处理语言(以 less 为例)的 loader 配置如下。
# 安装插件
npm install extract-text-webpack-plugin -D
var ExtractTextPlugin = require("extract-text-webpack-plugin")
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: "css-loader"
})
}
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
use: ["css-loader", "less-loader"]
})
}
上述配置并不能提取 .vue 文件中的 CSS 代码,需要增加 vue-loader 配置才行。
{
test: /\.vue$/,
use: {
loader: "vue-loader",
options: {
loaders: {
css: ExtractTextPlugin.extract({
use: 'css-loader'
}),
less: ExtractTextPlugin.extract({
use: ["css-loader", "less-loader"]
})
}
}
}
}
初始化插件,filename 可以指定 CSS 文件的目录。
plugins: [
new ExtractTextPlugin({
filename: "css/style.css"
})
]
CSS 压缩及优化
安装 postcss-loader 及 PostCSS 插件。
npm install postcss-loader cssnano -D
loader 配置调整如下。
// css-loader 配置改为
use: ['css-loader', "postcss-loader"]
// stylus-loader 配置改为
use: ["css-loader", "postcss-loader", "less-loader"]
postcss-loader 要放在 css-loader 后,CSS 预处理语言的 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/
###生成首页
手动引入打包后的 JavaScript 和 CSS 比较麻烦,使用 html-webpack-plugin 插件生成的页面自动引入了打包后的资源。
npm install html-webpack-plugin -D
初始化插件。
var HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.tpl.html'
})
]
index.tpl.html
<html>
<head>
...
</head>
<body>
<div id="app"></div>
</body>
</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 会删除这段无效代码
friendly-errors-webpack-plugin 是一个更友好显示 Webpack 错误信息的插件。插件地址:https://github.com/geowarin/friendly-errors-webpack-plugin
一般在开发环境下使用。
var FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
plugins: [
new FriendlyErrorsWebpackPlugin()
]
有用or有趣的插件推荐
- webpack-bundle-analyzer
- happypack
- webpack-blocks
- electron-webpack-dashboard
- webpack-jarvis
- webpack-dashboard
##分离 Webpack 配置
将开发和生产配置文件分离,方便增加各个环境下的个性配置。Webpack 官方文档中也详细阐述了如何为多环境增加配置文件,基本思路如下。
- 编写一个基本配置文件(webpack.base.config.js)
- 使用 webpack-merge 合并这个基础配置和针对环境的特定的配置(webpack.dev.config.js,webpack.prod.config.js)
安装 webpack-merge。
npm install webpack-merge -D
webpack.base.config.js 内容如下:
var path = require("path")
var webpack = require("webpack")
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: /\.(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]"
}
}
]
}
]
}
}
开发环境一般不配置提取 CSS,而生产环境需要配置,因此上面的基础配置不包含 CSS loader 和 vue-loader。path 和 publicPath 在开发和生产环境下一般不同,因此也不包含在基础配置中。
webpack.dev.config.js 内容如下:
var path = require("path")
var webpack = require("webpack")
var merge = require("webpack-merge")
var HtmlWebpackPlugin = require("html-webpack-plugin")
var baseWebpackConfig = require("./webpack.base.config")
module.exports = merge(baseWebpackConfig, {
output: {
publicPath: "/"
},
module: {
rules: [
{
test: /\.vue$/,
use: ["vue-loader"]
},
{
test: /\.css$/,
use: ["vue-style-loader", "css-loader"]
},
{
test: /\.styl$/,
use: ["vue-style-loader", "css-loader", "stylus-loader"]
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: "index.tpl.html"
})
]
})
关于 html-webpack-plugin 的配置,需要说明两点:
- template 的路径是相对于 Webpack 编译时的上下文目录,一般就是项目根目录;
- filename 则是相对于 Webpack 配置项 output.path(打包资源存储路径)。
生产配置文件(webpack.prod.config.js)内容如下:
var path = require("path")
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")
module.exports = merge(baseWebpackConfig, {
output: {
path: path.resolve(__dirname, "../dist"),
publicPath: "/static/"
},
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: "vue-loader",
options: {
loaders: {
css: ExtractTextPlugin.extract({
use: ["css-loader", "postcss-loader"]
}),
stylus: ExtractTextPlugin.extract({
use: [
"css-loader",
"postcss-loader",
"stylus-loader"
]
})
}
}
}
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ["css-loader", "postcss-loader"]
})
},
{
test: /\.styl$/,
use: ExtractTextPlugin.extract({
use: ["css-loader", "postcss-loader", "stylus-loader"]
})
}
]
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
}),
new webpack.optimize.UglifyJsPlugin(),
new ExtractTextPlugin({
filename: "css/style.css"
}),
new HtmlWebpackPlugin({
filename: "index.html",
template: "index.tpl.html"
})
]
})
对应在 package.json 中添加开发和生产构建的命令如下。
"scripts": {
"dev": "webpack-dev-server --progress --hot --open --config build/webpack.dev.config.js",
"build": "webpack --progress --config build/webpack.prod.config.js",
}
##代码分割
利用插件 webpack.optimize.CommonsChunkPlugin 将来自 node_modules 下的模块提取到 vendor.js。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module, count) {
return module.resource && /\.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0
}
})
vendor.js 包含了webpackBootstrap(webpack模块加载器)的代码,按理说放在 vendor 里面也没啥问题,毕竟后面的模块都需要依赖于此,但是如果你的 chunk 使用了 hash,一旦 app 代码发生了改变,相应的 hash 值会发生变化,webpackBootstrap 的代码也会发生变化(如图),而我们提取 vendor 的初心就是这部分代码改变频率不大,显然这不符合我们的目标,那么应当将 webpackBootstrap 再提取到一个文件中。
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
异步组件(懒加载)
在 Webpack1 时使用require.ensure
做懒加载, Webpack2 中引入了 Code Splitting-Async 的新方法 import(),用于动态引入 ES Module。require.ensure 可以指定 chunkFilename,但是 import 没有很好的途径去指定,webpack3 为了解决这个问题,提出了用魔法注释的方式来指定模块名。
结合 vue-router 可以轻松实现懒加载,配置路由指向异步组件即可实现懒加载,比如:
{
path: '/async',
component: () => import(/* webpackChunkName: "async" */'./views/async.vue')
}
!>如果你发现使用 import() 运行时报错,有几种情况:
- babel presets 配置为 es2015,则不支持动态导入功能,因此需要安装支持动态导入的 presets(
npm install babel-preset-stage-2 -D
),或者 babel 插件(npm install babel-plugin-syntax-dynamic-import -D
); - babel presets 配置为 env,但是 modules 配置为 false,babel 则不会解析 import,可以安装插件 (
npm install babel-plugin-syntax-dynamic-import -D
)解决。
魔法注释虽然指定了块名,但是 Webpack 默认的块名配置为 [id].js,即用的模块的 id 作为块名,因此需要我们手动改下配置。在 webpack.base.config.js 增加output.chunkFilename
。
output: {
filename: 'js/[name].js',
chunkFilename: "js/[name].[chunkhash].js"
}
异步加载效果如下图所示。
!>如果发现魔法注释没有生效,要检查下 .babelrc 的配置项 comments 是否设置成了 false,设为 true 魔法注释才生效,或者直接去掉 comments 配置,其默认为 true。
extract-text-webpack-plugin 默认不会提取异步模块中的 CSS,需要加上配置:
new ExtractTextPlugin({
allChunks: true,
filename: "css/style.css?[contenthash:8]"
})
常见问题
extract-text-webpack-plugin 从 Vue 中提取的 CSS 没有压缩
可以采用 Webpack 插件 optimize-css-assets-webpack-plugin 来解决这个问题,这个插件默认采用 cssnano 来优化优化并压缩 CSS。
如何减少 vendor.js 的体积
如果使用了很多第三方库的话,最终生成的 vendor.js 会非常大,如何合理的减少呢?
一种非常简单的方案就是使用 Webpack externals 配置项,将一些第三方库单独加载。比如:
// webpack.base.config.js
module.exports = {
...
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
'element-ui': 'ELEMENT' // 需要知道使用的库暴露的全局变量名
},
...
}
externals 配置对象{key: value}
的 key 是代码中使用的名称,value 是这个库暴露的全局变量名,因此使用这些库的代码不需要调整。
// 如果 externals 如此配置
externals: {
myVue: 'Vue'
}
// 那么使用的地方就要改成
import vue from 'myVue'
最终打包的代码如下:
模块最终是指向了全局变量。这时候就需要在 index.html 中自行添加第三方库了。比如:
<script src="/static/lib/vue/2.5.15/vue.runtime.min.js"></script>
<script src="/static/lib/vue-router/3.0.1/vue-router.min.js"></script>
<script src="/static/lib/element-ui/2.2.1/index.js"></script>
保持更新
及时更新 Vue 和 Webpack 版本(限小版本调整),敬请持续关注。