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

序:基于vue2和webpack3进行的多页面应用构建,github地址:https://github.com/FedWithMori/vue2-webpack3

一、 项目目录结构

任何一个项目开始构建之前最先要做的就是先确定我们项目的目录结构,包括开发目录和生产目录。
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代码下来运行看看,地址: https://github.com/FedWithMori/vue2-webpack3

三、总结

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的作用是什么?

答案可以参考下 https://segmentfault.com/q/1010000005596587?from=singlemessage&isappinstalled=1 的高分回答
大概的作用就是在编译的时候默认会使用babel-runtime的工具函数,从而减少编译后的代码量

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容