从零搭建 vue2 vue-router2 webpack3 工程

2018/7/24

以新手视角,详细介绍各个步骤内容,不深入讲步骤涉及的原理,主要介绍如何操作。

文中示例工程地址: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"
}

验证配置

在 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 安装慢的解决办法:

各种预处理语言的 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
}

接着配置 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 配置

将开发和生产配置文件分离,方便增加各个环境下的个性配置。Webpack 官方文档中也详细阐述了如何为多环境增加配置文件,基本思路如下。

安装 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 的配置,需要说明两点:

  1. template 的路径是相对于 Webpack 编译时的上下文目录,一般就是项目根目录;
  2. 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() 运行时报错,有几种情况:

  1. babel presets 配置为 es2015,则不支持动态导入功能,因此需要安装支持动态导入的 presets(npm install babel-preset-stage-2 -D),或者 babel 插件(npm install babel-plugin-syntax-dynamic-import -D);
  2. 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 版本(限小版本调整),敬请持续关注。

原创文章,持续完善中,转载请注明出处。本文地址: https://www.qinshenxue.com/article/20161118151423.html
Star支持 评论