从零开始基于vue2 webpack3构建多页应用

2,192 阅读5分钟
序:基于vue2和webpack3进行的多页面应用构建,github地址:github.com/FedWithMori…

一、 项目目录结构

任何一个项目开始构建之前最先要做的就是先确定我们项目的目录结构,包括开发目录和生产目录。
1. 开发目录
.
├── README.md
├── build
│   ├── devtool.js                     // 服务配置
│   ├── entry.js                       // 获取所有的入口路径
│   ├── output.js                      // 输出
│   ├── plugins.js                     // 配置插件
├── package.json
├── webpack.config.dev.js              // 开发环境配置
├── webpack.config.js                  // 生产环境配置
└── src
    ├── assets                         // 静态目录
    │   ├── less                       // 基本样式和基础依赖
    │       ├── mixin.less
    │       ├── reset.less
    │       ├── variable.less
    │   ├── images                     // 图片
    │       ├── home
    │           ├── home.png
    │       ├── index.png
    │       ├── about.png
    │   ├── fonts                      // 字体
    │       ├── a.woff
    ├── components                     // 组件
    │   ├── button.vue
    ├── entry                          // 入口js
    │   ├── home
    │       ├── home.js
    │   ├── index.js
    │   ├── about.js
    ├── page                           // 页面模块
    │   ├── home
    │       ├── home.vue
    │   ├── index.vue
    │   ├── about.vue
2. 生产目录
.
├── dist
    ├── css
    ├── html
    ├── js
    ├── images
    ├── fonts
    ├── vendor

二、 开始构建项目

第一步:新建一个项目目录

在命令面板输入如下命令可创建新的目录:mkdir vue2-webpack3

第二步:初始化项目

2.1 直接在cli里输入命令 cd vue2-webpack3 进入项目
2.2 然后输入命令 npm init,然后依次输入相关的信息后输入yes保存相关的项目信息
2.3 这时候项目里多了一个package.json文件,这个文件里保存了我们项目相关的一些信息,具体情况可以移步 package.json说明文档

第三步:搭建项目结构
3.1 按照开发目录结构创建完所有的目录
3.2 接下来就轮到webpack登场了

大家都知道,webpack的配置文件主要由:entry,output,module,plugins,devtool等几部分构成,为了方便管理(如果全在一个文件内,随着项目的庞大会导致配置页面的内容过多),我单独为除了module以外的几个属性建立了文件

3.3 entry配置

因为我们是多页面应用,所以我们的入口文件肯定是非常多的,为了方便获取所有的入口文件,我们可以利用Node的fs文件系统来获取entry目录下的所有入口文件的路径,代码如下:


const fs = require('fs');
const path = require('path');

const directory = path.resolve(__dirname, '../src/entry') ;
const entryList = {};

(getEntry = (dir) => {

    const entryArr = fs.readdirSync(dir);
    let pathName,
        filePath;

    entryArr.forEach(function(filename) {

        filePath = dir + '/' + filename; 
        if(fs.statSync(filePath).isDirectory()) {

            getEntry(filePath);

        } else {

            pathName = filePath.split('entry/')[1].replace('.js', '');
            entryList[pathName] = filePath;

        }

    })

})(directory)

module.exports = entryList

简单的解析下这段代码,主要是利用到了fs.readdirSync和fs.statSync两个方法。fs.readdirSync方法能够根据你提供路径,获取该路径下的所有文件路径,比如上面代码中我传递的dir(需要注意fs.readdirSync的参数必须是一个绝对路径,相对路径无法获取),fs.readdirSync会返回entry下所有文件的路径,然后我们拿到这些路径以后,再根据fs.statSync来判断这个路径对应的是一个文件还是一个目录,如果是目录,那就再调用一次,直到拿到文件的路径。

3.4 解决了入口文件的问题,接下来继续配置output,代码如下:
const path = require('path');

module.exports = {

    path: path.resolve(__dirname, '../dist'),
    // publicPath: 'http://img.xxx.com',
    filename: 'js/[name].js?ver=[hash:6]'

}

配置很简单,path把所有的文件都输出到dist目录里,filename把所有的js文件都输出到dist/js目录下,同时[name]对应的是入口文件中的pathName,如果有需要,也可以对publicPath进行配置,比如静态单独打包到了一个服务器上,那么我们就需要对静态的路径做统一的处理了。

3.5 配置好入口和出口,继续配置从入口到出口所经历的一些loader,代码如下:

rules: [

    {
        test: /\.less$/,
        exclude: /node_modules/,
        use: ExtractTextPlugin.extract(['css-loader', 'less-loader'])
    },
    {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
            loaders: {
                less: ExtractTextPlugin.extract({
                    use: ['css-loader', 'less-loader'],
                    fallback: 'vue-style-loader'
                })
            }
        }
    },
    {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
    },
    {
        test: /\.(jpg|jpeg|png|gif)$/,
        loaders: 'url-loader',
        options: {
            limit: 10000
        }
    },
    {
        test: /\.(woff|woff2|svg|eot|ttf)$/,
        use: 'file-loader'
    }

]

loader的配置很简单,但是需要注意几个问题。
1)我期望的是能够把vue组件中的样式和less的样式单独抽离到css文件中,所以需要利用extract-text-webpack-plugin这个插件,后面会讲到这个插件的使用
2)我期望能够利用插件自动补齐css样式的前缀,所以引入了postcss-loader转换器和autoprefixer规则,关于自动补齐的配置,我直接新建个postcss.config.js,然后做了如下简单的配置:

module.exports = {
    plugins: {
        'autoprefixer': {}
    }
}

如此配置以后,项目编译less的时候会默认读取该配置文件的内容并根据内容进行后续的处理。
3)我还期望能够在开发的时候用上es6语法,所以我们需要依赖babel来实现转译,webpack中也有babel-loader来做这个事情,我们需要执行下面的命令来安装相关的插件:

npm i babel-loader babel-core babel-preset-env babel-plugin-transform-runtime --save-dev
npm i babel-runtime --save

安装完后,新建一个.babelrc文件用来配置babel,代码如下:

{
    presets: [
        [
            'env',
            {
                'targets': {
                    'browsers': ['last 2 versions', 'ie >= 8']
                }
            }
        ]
    ],
    plugins: ['transform-runtime']
}

简单说下配置
presets的作用主要是告诉babel用哪个语法版本来进行转译(其实presets就是一堆插件的集合),比如说常见的是babel-preset-es2015,配置如下:

{
    presets: ['es2015']
}

按照这个配置babel会将所有的属于es2015正式版本的语法转译为es5的语法,那么一些es2016,es2017之类的语法是无法被转译的,而且现在一些现代浏览器对于es2015+的支持也越来越好,有时候也并不是所有的语法都需要转译,所以babel推出了新的配置,也就是.babelrc的配置,‘env’这个配置可以指定我们期望转译最低兼容的浏览器,比如这里配置的是浏览器最新的两个版本和ie8以上的版本,而且对于es2015+的语法也是同样支持的。
plugins的作用很简单就是引入插件,这里引入了个transform-runtime的插件,它的作用在文章结束时会提到。

3.6 接下来继续配置plugins。

1)因为是多页面项目,所以我们的html页面都需要前端自行创建,为了避免进行这些无用的重复操作,我引入了html-webpack-plugin插件,代码如下:

const entry = require('./entry');
let configPlugins = [];
// 根据入口js数组生成页面
Object.keys(entry).forEach((item) => {

    config = {
        filename: '../dist/html/' + item + '.html',
        template: path.resolve(__dirname, '../src/index.html'),
        chunks: [item]
    }

    configPlugins.push(new HtmlWebpackPlugin(config));

})

首先要将入口路径数组引入进来,因为html-webpack-plugin插件的用法就是实例化一次就会生成一个页面,所以我们对entry的key进行了循环,在每一次的循环中,根据key的信息生成config,然后实例化HtmlWebpackPlugin插件,如此就可以根据entry生成我们想要的html目录和页面

2)上面讲到了extract-text-webpack-plugin插件提取css并生成css文件,配置代码如下:

new ExtractTextPlugin({
    filename: 'css/[name].css'
})

该配置会根据入口的目录结构和文件名称在css中生成对应的结构和css文件,[name]同样取决于pathName

3)生成了css文件后,我还期望能够对通用的css和js进行提取,所以引入了CommonsChunkPlugin,这个插件提供了对chunk中公共的部分进行提取并生成文件的能力,配置如下:

new webpack.optimize.CommonsChunkPlugin({
    name: 'reset',
    filename: 'vendor/common.js',
    minChunks: 3
})

第一个name属性决定了提取出来的公共css文件的名称,这个reset.css文件会生成到css目录下
第二个filename属性决定了公共js的目录和名称,该common.js会生成到vendor目录下
第三个minChunks决定了当有多少个入口文件都含有该模块会对该模块进行抽离,我设置为3,意味着只有当至少3个入口js中都拥有某个相同的部分,才会对该部分进行提取。
ps:在提取的时候要注意个问题,CommonsChunkPlugin只提供了提取公用部分并生成文件的能力,而并没有提供往html页面中自动生成公共文件引入的能力,所有我们需要在模板文件中默认引入公共文件。

4)热更新功能对于任何一个开发人员来说肯定是必不可少的,webpack.HotModuleReplacementPlugin插件提供了这个能力,不过在使用这个插件之前,我们要先安装个webpack-dev-server插件,两个插件结合才可以实现这个热更新的功能。

5)clean-webpack-plugin插件也是必不可少的,它能够在每次编译完成之前先帮助我们删除指定的目录以及目录下所有的文件,这种做法能够帮助我们确保每次生成的代码都是最新的。配置代码如下:

new CleanWebpackPlugin(['dist'], {
    root: path.resolve(__dirname, '../')
})
3.7 关于resolve配置,代码如下:
resolve: {
    extensions: ['.js', '.vue', '.less'],
    alias: {
        less$: path.resolve(__dirname, 'src/assets/less'),
        components$: path.resolve(__dirname, 'src/components')
    }
}

extensions的作用主要是方便我们在引入别的文件时可以省略后缀
alias的作用是设置一些路径的别名,那么在引入别的文件的时候,就可以利用该别名来替代冗长的路径
ps:这里提一下path.resolve,为什么要使用它,是为了保证所有模块引入时地址的统一,毕竟项目的结构是有各种层级的,如果不进行统一,那么不同结构的模块引入时的路径也会不一样。

3.8 配置完插件后,接下来配置DevServer,该配置必须要安装webpack-dev-server插件,配置起来也很简单,代码如下:
var path = require('path');

module.exports = {
  contentBase: path.resolve(__dirname, '../dist'),
  host: 'we.cli',       // 别忘了配置host哦
  port: 8001,           // 端口8001
  inline: true,         // 可以监控js变化
  hot: true,            // 热启动
  compress: true,
  watchContentBase: false
};
3.9 以上都是针对开发环境的配置,我们还需要针对生产环境进行配置,比如对代码进行压缩,新创建个webpack.config.js,然后将webpack.config.dev.js的内容复制一份过来。

1)压缩css
这个比较简单,只需要在每个css-loader后面增加一个minimize参数就行了,如下:

{
    test: /\.less$/,
    exclude: /node_modules/,
    use: ExtractTextPlugin.extract(['css-loader?minimize', 'less-loader'])
}

2) 压缩js
对js的压缩需要依赖webpack.optimize.UglifyJsPlugin插件,我们将plugins.js插件复制一个副本,重命名为plugins.prod.js,然后引入webpack.optimize.UglifyJsPlugin插件,配置如下:

new webpack.optimize.UglifyJsPlugin({
    compress: {
        warnings: false
    }
})

3) 压缩html页面
我们还可以对html页面进行压缩,以达到进一步减少页面体积的效果,而HtmlWebpackPlugin插件本身已经具备这个能力,新增个minify配置即可,配置如下:

Object.keys(entry).forEach((item) => {

    config = {
        filename: '../dist/html/' + item + '.html',
        template: path.resolve(__dirname, '../src/index.html'),
        chunks: [item],
        minify: { 
            // 移除HTML中的注释
            removeComments: true, 
            // 删除空白符与换行符
            collapseWhitespace: true 
        }
    }

    configPlugins.push(new HtmlWebpackPlugin(config));

})

好了,经历了这些步骤以后,项目就构建完了。有兴趣的可以到github上clone代码下来运行看看,地址: github.com/FedWithMori…

三、总结

1. 为什么说presets就是一堆插件的集合呢?

从babel的官网你可以看到babel-preset-es2015其实就是包含了以下这些插件的集合:
transform-es2015-arrow-functions
transform-es2015-block-scoped-functions
transform-es2015-block-scoping
transform-es2015-classes
transform-es2015-computed-properties
transform-es2015-constants
transform-es2015-destructuring
transform-es2015-for-of
transform-es2015-function-name
transform-es2015-literals
transform-es2015-modules-commonjs
transform-es2015-object-super
transform-es2015-parameters
transform-es2015-shorthand-properties
transform-es2015-spread
transform-es2015-sticky-regex
transform-es2015-template-literals
transform-es2015-typeof-symbol
transform-es2015-unicode-regex
transform-regenerator
通过这个我们可以知道,不一定要引入babel-preset-es2015,你也可以针对某个特别的新特性进行单独的转译配置

2.transform-runtime的作用是什么?

答案可以参考下 segmentfault.com/q/101000000… 的高分回答
大概的作用就是在编译的时候默认会使用babel-runtime的工具函数,从而减少编译后的代码量

总的来说,这是一个比较基础的vue+webpack的配置,后续也会进一步的完善,包括对固定第三方依赖的打包缓存,代码检查等
主要参考文献:

webpack文档:doc.webpack-china.org/concepts/
webpack loader:doc.webpack-china.org/loaders/
webpack插件:doc.webpack-china.org/plugins/
Babel的presets和plugins配置解析:excaliburhan.com/post/babel-…
vue-loader:vue-loader.vuejs.org/zh-cn/start…

最后悄悄打个小广告,欢迎对前端兴趣的朋友加入QQ群:474471759