一步步讲解如何配置和使用 webpack。

目录速览

背景

前后因为项目和兴趣捣鼓过几次webpack,由于其配置的复杂和N多的神坑,加之没有总结形成文档,每一次都是重复造轮子,十分耗时+痛苦,webpack的初级配置很简单,网上的教程也是良莠不齐,并不能完全满足自己的开发需求,本人在此旨在完成一篇从入门到可以实际应用的文档,有些原理我也解释不清楚,边学边用吧~

参考链接

首先放出链接,个人认为参考这几篇,使用webpack达到一个中级水平已经很足够了,建议按顺序阅读或者同时补充参考
Webpack傻瓜式指南
让Webpack来帮你打包吧   (源于掘金翻译计划)
Webpack——令人困惑的地方
阮一峰大神的Webpack教程
多页为王:webpack多页应用架构专题系列
Webpack中hash与chunkhash的区别,以及js与css的hash指纹解耦方案(深度好文)

概述

webpack实在是一大利器,有人把打包构建工具的发展史列为如下:grunt->gulp->webpack,还有很多其他的工具,但这样定义和划分是不合适也是不合理的。
先总结如图:
图片

Grunt与Gulp

这两个是自动化构建工具,旨在优化前端工作流程。比如自动刷新页面、combo、压缩css、js、编译less等等。简单来说,就是使用Grunt/Gulp,然后配置你需要的插件,就可以把以前需要手工做的事情让它帮你做了。

Browserify与Webpack

说到 browserify / webpack ,那还要说到 seajs / requirejs 。这四个都是JS模块化的方案。其中seajs / require 是一种类型,browserify / webpack 是另一种类型。

  • seajs / require : 是一种在线”编译” 模块的方案,相当于在页面上加载一个 CMD/AMD 解释器。这样浏览器就认识了 define、exports、module 这些东西。也就实现了模块化。
  • browserify / webpack : 是一个预编译模块的方案,相比于上面 ,这个方案更加智能。没用过browserify,这里以webpack为例。首先,它是预编译的,不需要在浏览器中加载解释器。另外,你在本地直接写JS,不管是 AMD / CMD / ES6 风格的模块化,它都能认识,并且编译成浏览器认识的JS。

一般地,如果工程模块依赖很简单,不需要把js或各种资源打包,只需要简单的合并、压缩,在页面中引用就好了。那就不需要Browserify、Webpack。Gulp就够用了。
反过来,如果你的工程庞大,页面中使用了很多库(SPA很容易出现这种情况),那就可以选择某种模块化方案。至于是用Browserify还是Webpack就需要根据其他因素来判断了。比如团队已经在使用了某种方案,大家都比较熟悉了。再比如,你喜欢Unix小工具协作的方式,那就Browserify。

Webpack的核心原理

Webpack的两个最核心的原理分别是:(1)一切皆模块;(2)按需加载

基本配置

运行webpack命令

1
2
3
4
5
6
7
{
"scripts": {
"server": "webpack-dev-server --host 0.0.0.0 --port 5002 --line --hot --progress --profile --colors --config webpack.config.js",
"dev": "webpack --progress --profile --color --display-modules --display-chunks --config webpack.config.js",
"produce": "webpack --progress --profile --color --display-modules --display-chunks --config webpack.production.config.js"
}
}

package.json文件中的script选项内,添加如上的key-value,分别是”调试”、”测试发布”、”正式发布”的命令。--config可以指定webpack运行的入口配置文件。

有需要的话,可以写上几份配置文件,反正配置文件之间都是可以相互引用,相同部分就拆分出一个module来以供读取,最后拼接成所需要的配置文件即可;

webpack配置文件

webpack配置文件是一个node.js的module,用CommonJS的风格来书写,形如:

1
2
3
4
5
6
7
8
9
10
module.exports = {
context: ROOT_PATH,
entry: {
main: './src/main.js',
},
output: {
path: './dist',
filename: '[name].[hash:8].js',
}
};

webpack的配置文件并没有固定的命名,也没有固定的路径要求,如果你直接用webpack来执行编译,那么webpack默认读取的将是当前目录下的webpack.config.js
如果你有其它命名的需要或是你有多份配置文件,可以使用–config参数传入路径:

1
$ webpack --config ./build/webpack.config.product.js

入口文件配置:context参数

基础目录,绝对路径,用于从配置中解析入口起点(entry point)和 loader
直接配置成项目根目录就好了

1
2
3
4
5
6
7
8
9
module.exports = {
context: ROOT_PATH,
entry: {
// ....
},
output: {
// ....
}
};

入口文件配置:entry参数

Entry配置项告诉Webpack应用的根模块或起始点在哪里,它的值可以是字符串、数组或对象。这看起来可能令人困惑,因为不同类型的值有着不同的目的。

  • 使用任意类型
    倘若你的应用只有一个单一的入口,enter项的值你可以使用任意类型,最终输出的结果都是一样的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    entry: {
    main: path.resolve(SRC_PATH, 'main.js'),
    },
    output: {
    path: DIST_PATH,
    publicPath: '/dist/',
    filename: '[name]-[hash:8].bundle.js',
    },
    // ...
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    entry: path.resolve(SRC_PATH, 'main.js'),
    output: {
    path: DIST_PATH,
    publicPath: '/dist/',
    filename: 'bundle.js',
    },
    // ...
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    entry: [path.resolve(SRC_PATH, 'main.js')],
    output: {
    path: DIST_PATH,
    publicPath: '/dist/',
    filename: 'bundle.js',
    },
    // ...
    }
  • 使用数组类型
    如果想添加多个彼此不互相依赖的文件,可以使用数组格式的值。
    例如,可能在html文件里引用了”googleAnalytics.js”文件,可以告诉Webpack将其加到bundle.js的最后。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    entry: [
    path.resolve(SRC_PATH, 'main.js'),
    path.resolve(SRC_PATH, 'googleAnalytics.js')
    ],
    output: {
    path: DIST_PATH,
    publicPath: '/dist/',
    filename: 'app.bundle.js',
    },
    // ...
    }
  • 使用对象类型
    假设应用是多页面(multi-page app)的,而不是SPA,有多个html文件,此时就可通过对象告诉webpack为每一个html生成bundle文件。以下的配置将会生成两个js文件:main-[hash].bundle.js和profile-[hash].bundle.js分别会在index.html和profile.html中被引用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    entry: {
    'main': path.resolve(SRC_PATH, 'main.js'),
    'profile': path.resolve(SRC_PATH, 'profile.js')
    },
    output: {
    path: DIST_PATH,
    publicPath: '/dist/',
    filename: '[name]-[hash:8].bundle.js',
    },
    // ...
    }
  • 混合类型
    也可以在enter对象里使用数组类型,例如下面的配置将会生成3个文件:vender.js(包含三个文件),index.js和profile.js文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    entry: {
    'vendor': ['jquery', 'analytics.js', 'optimizely.js'],
    'index': './public/src/index.js',
    'profile': './public/src/profile.js',
    },
    output: {
    path: DIST_PATH,
    publicPath: '/dist/',
    filename: '[name]-[hash:8].bundle.js',
    },
    // ...
    }

为了后续发展,请务必使用object,因为object中的key在webpack里相当于此入口的name,既可以后续用来拼生成文件的路径,也可以用来作为此入口的唯一标识

例如,webpack也适用于打包多页应用,利用filename参数和path参数来设计入口文件的目录结构:
文件目录结构:

1
2
3
4
5
6
7
├─src # 当前项目的源码
├─pages # 各个页面独有的部分,如入口文件、只有该页面使用到的css、js、模板文件等
│ ├─alert # 业务模块
│ │ └─index # 具体页面
│ ├─index # 业务模块
│ │ ├─index # 具体页面
│ │ └─login # 具体页面

1
2
3
4
5
6
7
8
9
entry: { // pagesDir是前面准备好的入口文件集合目录的路径
'alert/index': path.resolve(pagesDir, `./alert/index/page`),
'index/login': path.resolve(pagesDir, `./index/login/page`),
'index/index': path.resolve(pagesDir, `./index/index/page`),
},
output: {
path: './dist/',
filename: '[name].js',
}

结果生成出来的文件就会是:

1
2
3
./dist/alert/index.js
./dist/index/login.js
./dist/index/index.js

入口文件配置:output参数

output参数告诉webpack以什么方式来生成/输出文件,值得注意的是,与entry不同,output相当于一套规则,所有的入口都必须使用这一套规则,不能针对某一个特定的入口来制定output规则。output参数里有这几个子参数是比较常用的:pathpublicPathfilenamechunkFilename

1
2
3
4
5
6
7
8
9
10
11
{
entry: {
main: path.resolve(SRC_PATH, 'main.js'),
},
output: {
path: DIST_PATH,
publicPath: '/dist/',
filename: '[name]-[hash:8].bundle.js',
chunkFilename: "[name]-[chunkhash:8].chunk.js"
}
}

path 与 publicPath

path参数表示生成文件的根目录,需要传入一个绝对路径。path参数和后面的filename参数共同组成入口文件的完整路径。

publicPath参数表示的是一个URL路径(指向生成文件的根目录),用于生成css/js/图片/字体文件等资源的路径,以确保网页能正确地加载到这些资源。publicPath被许多Webpack的插件用于在生产模式下更新内嵌到css、html文件里的url值。

publicPath参数跟path参数的区别是:path参数其实是针对本地文件系统的,而publicPath则针对的是浏览器;因此,publicPath既可以是一个相对路径,如示例中的../../../../build/,也可以是一个绝对路径如http://www.xxxxx.com/。一般来说,我还是更推荐相对路径的写法,这样的话整体迁移起来非常方便。那什么时候用绝对路径呢?其实也很简单,当你的html文件跟其它资源放在不同的域名下的时候,就应该用绝对路径了,这种情况非常多见于后端渲染模板的场景。

例如,在localhost(译者注:即本地开发模式)里的css文件中边你可能用“./test.png”这样的url来加载图片,但是在生产模式下“test.png”文件可能会定位到CDN上并且你的Node.js服务器可能是运行在HeroKu上边的。这就意味着在生产环境你必须手动更新所有文件里的url为CDN的路径。
也可以使用Webpack的“publicPath”选项和一些插件来在生产模式下编译输出文件时自动更新这些url。
图片

1
2
3
4
5
6
7
8
// 开发环境:Server和图片都是在localhost(域名)下
.image {
background-image: url('./test.png');
}
// 生产环境:Server部署下HeroKu但是图片在CDN上
.image {
background-image: url('https://someCDN/test.png');
}

“webpack-dev-server 也会默认从 publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。”
– webpack2

filename

filename 表示的是如何命名生成出来的入口文件,规则有以下三种:

  • [name],指代入口文件的name,也就是上面提到的entry参数的key,因此,我们可以在name里利用/,即可达到控制文件目录结构的效果。
  • [hash],指代本次编译的一个hash版本,值得注意的是,只要是在同一次编译过程中生成的文件,这个[hash]的值就是一样的;在缓存的层面来说,相当于一次全量的替换。
  • [chunkhash],指代的是当前chunk的一个hash版本,也就是说,在同一次编译中,每一个chunk的hash都是不一样的;而在两次编译中,如果某个chunk根本没有发生变化,那么该chunk的hash也就不会发生变化。这在缓存的层面上来说,就是把缓存的粒度精细到具体某个chunk,只要chunk不变,该chunk的浏览器缓存就可以继续使用。

chunkFilename

chunkFilename参数与filename参数类似,都是用来定义生成文件的命名方式的,只不过,chunkFilename参数指定的是除入口文件外的chunk(这些chunk通常是由于webpack对代码的优化所形成的,比如因应实际运行的情况来异步加载)的命名。

文件入口配置:devServer配置

“inline”选项会为入口页面添加“热加载”功能,“hot”选项则开启“热替换(Hot Module Reloading)”,即尝试重新加载组件改变的部分(而不是重新加载整个页面)。如果两个参数都传入,当资源改变时,webpack-dev-server将会先尝试HRM(即热替换),如果失败则重新加载整个入口页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
entry: {
main: path.resolve(SRC_PATH, 'main.js'),
},
output: {
path: DIST_PATH,
publicPath: '/dist/',
filename: '[name]-[hash:8].bundle.js',
chunkFilename: "[name]-[chunkhash:8].chunk.js"
},
devServer: {
inline: true,
hot:true
}
}

入口文件配置:各种loader配置

module参数

webpack的核心实际上也只能针对js进行打包,那webpack一直号称能够打包任何资源是怎么一回事呢?原来,webpack拥有一个类似于插件的机制,名为Loader,通过Loader,webpack能够针对每一种特定的资源做出相应的处理。Loader的种类相当多,有些比较基础的是官方自己开发,而其它则是由webpack社区开源贡献出来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
module: {
loaders: [
{
test: /\.json$/,
loader: "json"
},
{
test: /\.vue$/,
loader: "vue"
},
{
test: /\.js$/,
include: SRC_PATH,
loader: "babel",
// 项目根目录下的.babelrc里可以设置
// query: {
// presets: ['es2015'] //该参数是babel的plugin,可以支持最新的es6特性
// }
},
{
test: /\.css$/,
include: SRC_PATH,
loader: "style!css!autoprefixer?browsers=last 5 versions"
},
{
test: /\.less$/,
loader: "style!css!less!autoprefixer?browsers=last 5 versions" //注意loader的处理顺序是从右到左
//loaders: ['style', 'css', 'less'] //也可以有这种写法(推荐)
},
{
test: /\.(scss|sass)$/,
loader: "style!css!sass!autoprefixer?browsers=last 5 versions"
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
loader: "url?limit=20000&name=[name]_[hash:8].[ext]"
},
{
test: /\.html$/,
loader: "html"
}
]
},
// ...

  • test参数
    用来指示当前配置项针对哪些资源,该值应是一个条件值(condition)。
  • include参数
    用来表示本loader配置仅针对哪些目录/文件,该值应是一个条件值(condition)。
  • exclude参数
    用来剔除掉需要忽略的资源或资源目录,该值应是一个条件值(condition)。
  • loader/loaders参数
    用来指示用哪个/哪些loader来处理目标资源,这俩货表达的其实是一个意思,只是写法不一样,我个人推荐用loader写成一行,多个loader间使用!分割,这种形式类似于管道的概念,又或者说是函数式编程。形如loader: ‘css?!postcss!less’,可以很明显地看出,目标资源先经less-loader处理过后将结果交给postcss-loader作进一步处理,然后最后再交给css-loader。

条件值(condition)可以是一个字符串(某个资源的文件系统绝对路径),可以是一个函数(官方文档里是有这么写,但既没有示例也没有说明,我也是醉了),可以是一个正则表达式(用来匹配资源的路径,最常用,强烈推荐!),最后,还可以是一个数组,数组的元素可以为上述三种类型,元素之间为与关系(既必须同时满足数组里的所有条件)。需要注意的是,loader是可以接受参数的,方式类似于URL参数,形如’css?minimize&-autoprefixer’,具体每个loader接受什么参数请参考loader本身的文档(一般也就只能在github里看了)。

webpack2 将module选项的配置更新成如下

  • loader: 'style-loader'use: ['style-loadr']use: [{loader: 'style-loader'}] 的简写方式;
  • options: optionValquery: queryValuse: [{loader: loaderVal, options: optionsVal}] 的简写方式;
    具体例子如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    test: /\.vue$/,
    use: [
    {
    loader: 'vue-loader',
    options: {
    loaders: {
    css: 'style-loader!css-loader!postcss-loader?sourceMap',
    less: 'style-loader!css-loader!postcss-loader?sourceMap!less-loader',
    sass: 'style-loader!css-loader!postcss-loader?sourceMap!sass-loader',
    }
    }
    }
    ]
    },

loader自身可以配置

模块加载器(loader)自身可以根据传入不同的参数进行配置。
在下面的例子中,我们可以配置url-loader来将小于1024字节的图片使用DataUrl替换而大于1024字节的图片使用url,我们可以用如下两种方式通过传入”limit”参数来实现这一目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Option 1 - Use "?" just lick in URLs
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
loader: "url?limit=20000&name=[name]_[hash:8].[ext]"
}
// Option 2 - Use "query" property
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
loader: "url",
query: {
limit: 20000,
name: [name]_[hash:8].[ext]
}
}

入口文件配置:resolve(解析)

这些选项能设置模块如何被解析

resolve.alias

1
2
3
4
5
6
resolve: {
alias: {
// 在给定对象的键后的末尾添加 $,以表示精准匹配
vue$: 'vue/dist/vue.common.js'
}
}

入口文件配置:pulgin(添加额外功能)

这plugins参数相当于一个插槽位(类型是数组),你可以先按某个plugin要求的方式初始化好了以后,把初始化后的实例丢到这里来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var ETP = require('extract-text-webpack-plugin');
// ...
{
module: {
loades: [
{test: /\.css$/, loader:ETP.extract("style-loader","css-loader")}
],
}
plugins: [
//extract-text-webpack-plugin内部使用css-loader和style-loader来收集所有的css到一个地方最终将结果提取结果到一个独立的css文件,并且在html里边引用该css文件。
new ExtractPlugin('style-[hash:8].bundle.css', {allChunks: true}),
new webpack.optimize.CommonsChunkPlugin({
// name: 'vendor', //将依赖合并到主文件
async: true, //不指定块名称的做法,让共同的依赖异步加载
children: true, // 寻找所有子模块的共同依赖
minChunks: 2, // 设置一个依赖被引用超过多少次就提取出来
}),
new HtmlwebpackPlugin({
title: 'webpack',
filename: '../index.html', //默认目录路径为output.path
template: 'index-tpl.html', //默认目录路径为根目录
inject: true,
// cache: true,
}),
new webpack.optimize.UglifyJsPlugin({
mangle: true,
compress: {
warnings: false, // Suppress uglification warnings
},
}),
]
// ...
}

注:加载器(loader)和插件(plugin)
Loader 加载器 处理单独的文件级别并且通常作用于包生成之前或生成的过程中。
Plugin 插件 则是处理包(bundle)或者chunk级别,且通常是bundle生成的最后阶段。一些插件如commonschunkplugin甚至更直接修改bundle的生成方式。

调试代码

使用webpack在开发时经常用devtool来调试代码,其中包含7种模式:

  • eval
    每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL
  • source-map
    生成一个SourceMap文件
  • eval-source-map
    每个module会通过eval()来执行,并且生成一个DataUrl形式的SourceMap.
  • hidden-source-map
    和 source-map 一样,但不会在 bundle 末尾追加注释.
  • inline-source-map
    生成一个 DataUrl 形式的 SourceMap 文件.
  • cheap-source-map
    生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的 sourcemap)
  • cheap-module-source-map
    生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的

webpack 不仅支持这 7 种,而且它们还是可以任意组合上面的eval、inline、hidden关键字,就如文档所说,你可以设置 souremap 选项为 cheap-module-inline-source-map

特点小结

  • cheap模式
    使用 cheap 模式可以大幅提高 souremap 生成的效率。大部分情况我们调试并不关心列信息,而且就算 sourcemap 没有列,有些浏览器引擎(例如 v8) 也会给出列信息
  • eval模式
    使用 eval 方式可大幅提高持续构建效率。官方文档提供的速度对比表格可以看到 eval 模式的编译速度很快
  • module模式
    使用 module 可支持 babel 这种预编译工具(在 webpack 里做为 loader 使用)
  • eval-souce-map模式
    使用 eval-source-map 模式可以减少网络请求。这种模式开启 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那样,浏览器需要发送一个完整请求去获取 sourcemap 文件,这会略微提高点效率。而生产环境中则不宜用 eval,这样会让文件变得极大

sourcemap 模式效率对比图

图片

让项目支持使用ES6进行开发

只介绍如何利用webpack整合Babel来编译ES6的语法,而实际上若要使用ES6(ES2015)的其它属性甚至是ES7(ES2016),其实只需要引入Babel其它的preset/plugin即可,在用法上并无多大变化。

首先,babel-loader,这是webpack整合Babel的关键,我们需要配置好babel-loader来加载那些使用了ES6语法的js文件;另外,那些本来就是ES5语法的文件,其实是不需要用babel-loader来加载的,用了也只会浪费我们编译的时间(但是参考文章中最后也没有给出这方面的解决办法)。

然后,与babel相关的依赖包,其中包括:

  • babel-core
    babel的核心
  • babel-preset-es2015 / babel-preset-es2015-loose
    当只使用ES6语法的时候,使用babel-preset-es2015babel-preset-es2015-loose就可以了,差别是:

    babel-preset-es2015 采用尽可能符合ECMAScript6语义的normal模式
    babel-preset-es2015-loose 采用提供的更简单ES5代码的loose模式
    loose模式的优点和缺点是:
    优点:生成的代码可能更快,对老的引擎有更好的兼容性,代码通常更简洁,更加的“ES5化”。
    缺点:随后从转译的ES6到原生的ES5时可能会遇到问题,是在冒险
    参考文章:Babel 6: loose模式

  • babel-plugin-transform-runtimebabel-runtime,这属于优化项
    两者关系:启用插件 babel-plugin-transform-runtime 后,Babel 就会使用 babel-runtime 下的工具函数
    使用场景:Babel 转译后的代码要实现源代码同样的功能需要借助一些帮助函数
    功能作用:babel 默认会为每一个转换后的文件(在webpack这就是每一个chunk了)都添加一些辅助的方法(仅在需要的情况下),如果用了这个plugin,babel会把这些辅助的方法都集中到一个文件里统一加载统一管理,算是一个减少冗余,增强性能的优化项
    参考文章:babel的polyfill和runtime的区别

    [Runtime transform](http://babeljs.io/docs/plugins/transform-runtime/)
    

打包抽取less/css

webpack的核心只能打包js文件,而js以外的资源都是靠loader进行转换或做出相应的处理的,上面的基本配置是可以打包出一个index.html首页+一个集中打包资源的js文件+若干超过限制可打包大小的图片,css资源也是被集中在js文件里的。然而实际上,我们可以使用plugin插件把css文件资源单独抽取出来。

这样需要使用到一个额外的插件:extract-text-webpack-plugin,首先进行安装:

1
$ yarn add --dev extract-text-webpack-plugin

ExtractTextPlugin的作用是把各个chunk加载的css代码(可能是由less-loader转换过来的)合并成一个css文件并在页面加载的时候以的形式进行加载。

相对于使用style-loader直接把css代码段跟js打包在一起并在页面加载时以inline的形式插入DOM,我还是更喜欢ExtractTextPlugin生成并加载CSS文件的形式;倒不是看不惯inline的css,只是用文件形式来加载的话会快很多,尤其后面介绍用webpack来生成HTML的时候,这会直接生成在里,那么在CSS的加载上就跟传统的前端页面没有差别了,体验非常棒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var plugins = [
// 单独抽取出公共的css文件模块
new ExtractPlugin('[name].[hash:8].css'),
];
// ...
module.exports = {
// ...
module: {
loaders: [
{
test: /\.less$/,
loader: ExtractPlugin.extract('css!less!autoprefixer?browsers=last 5 versions')
},
{
test: /\.css$/,
loader: ExtractPlugin.extract('css!autoprefixer?browsers=last 5 versions')
},
]
}
// ...
};

最后,到底是使用打包抽取样式文件的方法还是使用注入到脚本js文件的方法,看实际需求;

打包图片和字体

使用loader:file-loader 和 url-loader

file-loader

file-loader的主要功能是:把源文件迁移到指定的目录(可以简单理解为从源文件目录迁移到build目录),并返回新文件的路径(简单拼接而成)。

file-loader需要传入name参数,该参数接受以下变量(以下讨论的前提是:源文件src/public-resource/imgs/login-bg.jpg;在根目录内执行webpack命令,也就是当前的上下文环境与src目录同级):

  • [ext]:文件的后缀名,示例为’jpg’。
  • [name]:文件名本身,示例为’login-bg’。
  • [path]:相对于当前执行webpack命令的目录的相对路径(不含文件名本身),示例为’src/public-resource/imgs/‘。这个参数我感觉用处不大,除非你想把迁移后的文件放回源文件的目录或其子目录里。
  • [hash]:源文件内容的hash,用于缓存解决方案。

url-loader

url-loader的主要功能是:将源文件转换成DataUrl(声明文件mimetype的base64编码)。据我所知,在前端范畴里,图片和字体文件的DataUrl都是可以被浏览器所识别的,因此可以把图片和字体都转化成DataUrl收纳在HTML/CSS/JS文件里,以减少HTTP连接数。

  • limit参数,数据类型为整型,表示目标文件的体积大于多少字节就换用file-loader来处理了,不填则永远不会交给file-loader处理。例如require(“url?limit=10000!./file.png”);,表示如果目标文件大于10000字节,就交给file-loader处理了。
  • mimetype参数,前面说了,DataUrl是需要声明文件的mimetype的,因此我们可以通过这个参数来强行设置mimetype,不填写的话则默认从目标文件的后缀名进行判断。例如require(“url?mimetype=image/png!./file.jpg”);,强行把jpg当png使哈。
  • 一切file-loader的参数,这些参数会在启用file-loader时传参给file-loader,比如最重要的name参数。

例子

图片

1
2
3
4
5
6
7
8
9
10
11
12
{
module: {
loaders: [
{
// 如下配置,将小于8192byte的图片转成base64码
test: /\.(png|jpg|jpeg|gif|svg)$/,
loader: "url?limit=8192&name=[name]_[hash:8].[ext]"
},
// ...
]
}
}

由于使用了[hash],因此即便是不同页面引用了相同名字但实际内容不同的图片,也不会造成“覆盖”的情况出现;进一步讲,如果不同页面引用了在不同位置但实际内容相同的图片,这还可以归并成一张图片,方便浏览器缓存呢。

字体文件

1
2
3
4
5
6
7
8
9
10
11
12
{
module: {
loaders: [
{
// 专供iconfont方案使用的,后面会带一串时间戳,需要特别匹配到
test: /\.(woff|woff2|svg|eot|ttf)\??.*$/,
loader: 'file?name=./static/fonts/[name].[ext]'
},
// ...
]
}
}

需要声明的是,由于我使用的是阿里妈妈的iconfont方案,此方案加载字体文件的方式有一点点特殊,所以正则匹配的时候要注意一点,iconfont的CSS是这样的,你们看看就明白了:

1
2
3
4
5
6
7
@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1473142795'); /* IE9*/
src: url('iconfont.eot?t=1473142795#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('iconfont.woff?t=1473142795') format('woff'), /* chrome, firefox */
url('iconfont.ttf?t=1473142795') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('iconfont.svg?t=1473142795#iconfont') format('svg'); /* iOS 4.1- */
}

其他资源

为什么还要转移其它资源,直接引用不行吗?
我之前也是这么做的,直接引用源文件目录src里的资源,比如说webuploader用到的swf文件,比如说用来兼容IE而又不需要打包的js文件。但是后来我发现,这样做的话,就导致部署上线的时候要把build目录和src目录同时放上去了;而且由于build目录和src目录同级,我就只能用build目录和src目录的上一级目录作为网站的根目录了(因为如果把build目录设为网站,用户就读取不到src目录了),反正就是各种的不方便。

解决方案:
新建了一个config文件,例如名为build-file.config.js,内容如下:

1
2
3
4
5
6
7
8
9
10
module.exports = {
js: {
xdomain: require('!file-loader?name=static/js/[name].[ext]!../../../vendor/ie-fix/xdomain.all.js'),
html5shiv: require('!file-loader?name=static/js/[name].[ext]!../../../vendor/ie-fix/html5shiv.min.js'),
respond: require('!file-loader?name=static/js/[name].[ext]!../../../vendor/ie-fix/respond.min.js'),
},
images: {
'login-bg': require('!file-loader?name=static/images/[name].[ext]!../imgs/login-bg.jpg'),
},
};

这个config文件起到两个作用:

  • 每次加载到这个config文件的时候,会执行那些require()语句,对目标文件进行转移(从src目录到build目录);
  • 调用目标文件的代码段,可以从这个config文件取出目标文件转移后的完整路径,例如我在src/public-resource/components/header/html.ejs里是这么用的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title><% if (pageTitle) { %> <%= pageTitle %> - <% } %> XXXX后台</title>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<meta name="renderer" content="webkit" />
<!--[if lt IE 10]>
<script src="<%= BUILD_FILE.js.xdomain %>" slave="<%= SERVER_API_URL %>cors-proxy.html"></script>
<script src="<%= BUILD_FILE.js.html5shiv %>"></script>
<![endif]-->
</head>
<body>
<!--[if lt IE 9]>
<script src="<%= BUILD_FILE.js.respond %>"></script>
<![endif]-->

结束语

至此,一套最基本的调试用代码已经完成,已经可以说是万金油方案了。接下来会慢慢深入到webpack实战当中,记录一些实战过程中重要的东西。

避免重复打包公共代码

不只是用webpack来打包的多页应用还是单页应用(多页应用更甚),都会遇到重复加载公共代码的情况,比如同一份组件代码或者是第三方的加载库在一个项目中只存在一份才是合理的;虽然js文件在浏览器中可以实现缓存加载,但webpack把绝大部份资源都打包成一份,浏览器就没有办法实现公共代码在页面见的缓存。鉴于如此情况,我们就有想法要提取公共代码,为整个项目页面均可使用。

重温在webpack中使用Plugin的方法

大部分Plugin的使用方法都有一个固定的套路:

  1. 利用Plugin的初始方法并传入Plugin预设的参数进行初始化,生成一个实例。
  2. 将此实例插入到webpack配置文件中的plugins参数(数组类型)里即可。

CommonsChunkPlugin(待实操验证)

CommonsChunkPlugin可以找出页面间(单页之间或者多页之间)其中满足条件(被多少个页面引用过)的代码段,判定为公共代码并打包成一个独立的js文件。至此,你只需要在每个页面都加载这个公共代码的js文件,就可以既保持代码的完整性,又不会重复下载公共代码

常用的初始化参数:
name : 给这个包含公共代码的chunk命个名(唯一标识)
filename : 如何命名打包后生产的js文件,也是可以用上[name]、[hash]、[chunkhash]这些变量的啦(具体是什么意思,请看我上一篇文章中关于filename的那一节)
children : true / false,是否寻找所有子模块的共同依赖
minChunks : 公共代码的判断标准:某个js模块被多少个chunk加载了才算是公共代码
chunks : 表示需要在哪些chunk(也可以理解为webpack配置中entry的每一项)里寻找公共代码进行打包。不设置此参数则默认提取范围为所有的chunk

实例分析

初始化一个CommonsChunkPlugin的实例:

1
2
3
4
5
6
7
var webpack = require('webpack');
// ...
var commonsChunkPlugin = new webpack.optimize.CommonsChunkPlugin({
name: 'commons', // 这公共代码的chunk名为'commons'
filename: '[name].bundle.js', // 生成后的文件名,虽说用了[name],但实际上就是'commons.bundle.js'了
minChunks: 4, // 设定要有4个chunk(即4个页面)加载的js模块才会被纳入公共代码。这数目自己考虑吧,我认为3-5比较合适。
});

最终生成文件的路径是根据webpack配置中的ouput.path和上面CommonsChunkPlugin的filename参数来拼的,因此想控制目录结构的,直接在filename参数里动手脚即可,例如:filename: 'commons/[name].bundle.js'

兼容老式jQuery插件

老式jQuery插件的加载机制:查找名为jQuery$的全局对象,通过它们提供的jQuery.fn.extend()jQuery.extend()方法来向jQuery安装插件。

jQuery库是符合AMD和CMD规范的,能很好地被加载和被webpack打包;但webpack作为一个遵从模块化原则的构建工具,自然是要把各模块的上下文环境给分隔开以减少相互间的影响;所以通过webpack加载打包后的jQuery不会作为全局变量,那么不符合AMD/CMD规范的老式jQuery插件自然不会加载成功。

ProvidePlugin

ProvidePlugin是webpack的一个方法,机制是:当webpack加载到某个js模块里,出现了未定义且名称符合(字符串完全匹配)配置中key的变量时,会自动require配置中value所指定的js模块。这样当某个老式插件使用了jQuery.fn.extend(object),那么webpack就会自动引入jquery。

另外,使用ProvidePlugin之后,代码里就不用再次require('jquery')了,因为当它需要被使用到的时候,webpack会自动给模块页面提供该jQuery变量。

思考:

  1. value所指定的js模块,应该指的是通过npm所安装的依赖包
  2. 使用ProvidePlugin来提供jquery变量,依旧是没有把它声明为全局变量,而是当哪个模块需要用到时,webpack就会自动地给该模块引入;

实例配置:

1
2
3
4
5
6
7
8
9
10
plugins: [
// ...
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.$': 'jquery',
'window.jQuery': 'jquery',
}),
// ...
]

expose-loader

该loader的作用:将指定js模块export的变量声明为全局变量。

实例配置:

1
2
3
4
{
test: require.resolve('jquery'), // 此loader配置项的目标是NPM中的jquery
loader: 'expose?$!expose?jQuery', // 先把jQuery对象声明成为全局变量`jQuery`,再通过管道进一步又声明成为全局变量`$`
},

那么有了ProvidePlugin为嘛还需要expose-loader?如果所有的jQuery插件都是用webpack来加载的话,的确用ProvidePlugin就足够了;但理想是丰满的,现实却是骨感的,总有那么些需求是只能用<script>来加载的。

思考:

  1. ProvidePlugin 是用来让webpack给jquery插件动态引入jquery局部变量的;
  2. expose-loader 是用来加载和声明全局变量的;

externals

externals是webpack配置中的一项,是用来将以<script>标签引入的对象作为全局变量,声明到整个工程;这也有很大的实用价值,使用第三方CDN加速资源,能够节省本地服务器空间和传输成本;

实例配置:

1
2
3
4
5
6
7
module.exports = {
// ...
externals: {
jquery: 'window.jQuery',
}
// ...
};

前提:

1
<script src="//cdn.bootcss.com/jquery/3.2.0/jquery.min.js"></script>

imports-loader

手动版的ProvidePlugin,所以现在有了ProvidePlugin就可以不再使用该laoder了

实例配置:

1
2
3
4
5
{
test: require('some-module'), // require第三方插件
loader: imports // imports?$=jquery&jQuery=jquery
// 相当于 var $=require('jquery'); var jQuery=require('jquery');
}

总结

以上的方案其实都属于shimming,并不特别针对jQuery,要举一反三使用。另外,上述方案并不仅用于shimming,比如用上ProvidePlugin来写少几个require,自己多多挖掘,很有乐趣的哈~~

区分开发与生产环境

在做项目过程中一般要区分多种环境:开发环境、测试环境、生产环境,其中必要的当然是开发环境和生产环境;

  • 开发环境:
    开发项目代码的环境,期间会产生很多debug的代码,或者是提前脱离于后端而请求模拟数据的代码,或者是试错用、调试用的代码等等
  • 测试环境:
    测试项目代码的环境,用于测试功能代码或者是新开发的库
  • 生产环境:
    生产项目产品的环境,当开发项目完毕之后,要把产品代码部署到正式环境,正式地以供商用

开发环境与生产环境区分原因如下:

  • 在开发时,不可避免会产生大量debug又或是测试的代码,这些代码不应出现在生产环境中(也即不应提供给用户)。
  • 在把页面部署到服务器时,为了追求极致的技术指标,我们会对代码进行各种各样的优化,比如说混淆、压缩,这些手段往往会彻底破坏代码本身的可读性,不利于我们进行debug等工作。
  • 数据源的差异化,比如说在本地开发时,读取的往往是本地mock出来的数据,而正式上线后读取的自然是API提供的数据了。

下面主要针对两点来介绍如何分离开发环境和生产环境:
一是如何以不同的方式进行编译,也即如何分离开发环境及生产环境的webpack配置文件;
二是在业务代码中如何根据环境的不同而做出不同的处理;

分离开发环境和生产环境的webpack配置文件

如果同时把一份完整的开发环境配置文件和一份完整的生产环境配置文件列在一起进行比较,那么会出现以下三种情况:

  • 开发环境有的配置,生产环境不一定有
    比如说开发时需要生成sourcemap来帮助debug,又或是热更新时使用到的HotModuleReplacementPlugin。
  • 生产环境有的配置,开发环境不一定有
    比如说用来混淆压缩js用的UglifyJsPlugin。
  • 开发环境和生产环境都拥有的配置,但在细节上有所不同
    比如说output.publicPath,又比如说css-loader中的minimize和autoprefixer参数。

更重要的是,为了实现高的可维护性和可扩展性,实际上开发环境和生产环境的配置文件的绝大部分都是一致的,对于这一致的部分来说,我们坚决要消除冗余,否则后续维护起来不仅麻烦,而且还容易出错。

综上所述,即求同存异,提高效率。要实现这样的目标,把webpack的配置文件拆分开来实现成一个个的小模块:(网友多页应用下的分离成果)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├─webpack.dev.config.js # 开发环境的webpack配置文件(无实质内容,仅为组织整理)
├─webpack.config.js # 生产环境的webpack配置文件(无实质内容,仅为组织整理)
├─webpack-config # 存放分拆后的webpack配置文件
├─entry.config.js # webpack配置中的各个大项,这一级目录里的文件都是
├─module.config.js
├─output.config.js
├─plugins.dev.config.js # 俩环境配置中不一致的部分,此文件由开发环境配置文件webpack.dev.config.js来加载
├─plugins.product.config.js # 俩环境配置中不一致的部分,此文件由生产环境配置文件webpack.config.js来加载
├─resolve.config.js

├─base # 主要是存放一些变量
│ ├─dir-vars.config.js
│ ├─page-entries.config.js

├─inherit # 存放生产环境和开发环境相同的部分,以供继承
│ ├─plugins.config.js

└─vendor # 存放webpack兼容第三方库所需的配置文件
├─eslint.config.js
├─postcss.config.js

组织整理最后的配置文件:

1
2
3
4
5
6
7
8
9
10
/* 开发环境webpack配置文件webpack.dev.config.js */
module.exports = {
entry: require('./webpack-config/entry.config.js'),
output: require('./webpack-config/output.config.js'),
module: require('./webpack-config/module.config.js'),
resolve: require('./webpack-config/resolve.config.js'),
plugins: require('./webpack-config/plugins.dev.config.js'),
eslint: require('./webpack-config/vendor/eslint.config.js'),
postcss: require('./webpack-config/vendor/postcss.config.js'),
};

按照这样的形式就可以比较轻松方便地处理开发/生产环境的打包配置文件中的共有和区别部分了

思考:
要是整个工程需要用到这么复杂的打包配置文件目录,要么是coder没有优化项目架构,要么就是这个工程太庞大,还是需要优化项目流程和制定规范

分别调用开发/生产环境的配置文件

1
2
3
4
5
"scripts": {
"build": "node build-script.js && webpack --progress --colors",
"dev": "node build-script.js && webpack --progress --colors --config ./webpack.dev.config.js",
"ser": "webpack-dev-server --host 0.0.0.0 --port 5004 --line --hot --profile --colors --config webpack.dev.config.js",
}

业务代码中区分生产/开发环境

在业务代码中区分环境很简单,只需要一枚全局变量,示意代码如下:

1
2
3
4
5
if(IS_PRODUCTION){
// 生产环境代码
}else{
// 开发环境代码
}

如何实现可动态配置该全局变量是关键,有如下几个方法:
(均是webpack对象的方法)

  • EnvironmentPlugin引入process.env
    这样就可以在业务代码中靠process.env.NODE_ENV来判断,这样就需要在不同环境下都要手动设置一下NODE_ENV的值
  • ProvidePlugin
    使用该插件来控制在不同环境里加载不同的配置文件(业务代码用的)
  • DefinePlugin
    使用该插件来定义全局变量,使之在项目代码中可以使用
    1
    2
    3
    4
    5
    6
    7
    new webpack.DefinePlugin({
    IS_PRODUCTION: JSON.stringify(true),
    VERSION: JSON.stringify("5fa3b9"),
    BROWSER_SUPPORTS_HTML5: true,
    TWO: "1+1",
    "typeof window": JSON.stringify("object")
    })

DefinePlugin
其真正的机制是:DefinePlugin的参数是一个object,那么其中会有一些key-value对。在webpack编译的时候,会把业务代码中没有定义(使用var/const/let来预定义的)而变量名又与key相同的变量(直接读代码的话的确像是全局变量)替换成value。
例如,上面的官方例子,IS_PRODUCTION就会被替换为true;VERSION就会被替换为’5fa3b9’(注意单引号);BROWSER_SUPPORTS_HTML5也是会被替换为true;TWO会被替换为1+1(相当于是一个数学表达式);typeof window就被替换为’object’了。

实例:
编写的代码

1
2
3
4
if (!IS_PRODUCTION)
console.log('Debug info')
if (IS_PRODUCTION)
console.log('Production log')

编译生成的代码

1
2
3
4
if (!true)
console.log('Debug info')
if (true)
console.log('Production log')

使用了UglifyJsPlugin后编译生成的代码

1
console.log('Production log')

需要注意的是,如果你在webpack里整合了ESLint,那么,由于ESLint会检测没有定义的变量(ESLint要求使用全局变量时要用window.xxxxx的写法),因此需要一个global注释声明(/ global IS_PRODUCTION:true \/)IS_PRODUCTION是一个全局变量(当然在本例中并不是)来规避warning。

思考

无论以上3种实现动态配置全局变量的方法有多么便捷,乍看上去都需要在切换工作环境的时候,谨记着去设置IS_PRODUCTION,显然不符合geek风。这样就需要脚本来实现“自动化”了,在打包测试环境代码/热调试代码时,一条脚本命令就可以先设置为开发环境然后再完成目的工作;然后在打包正式环境代码时,另一条脚本命令就可以也是先设置生产环境然后再完成目的工作。

总有刁民想害朕 ESLint为你狙击垃圾代码

ESLint用途:

  • 审查代码是否符合编码规范和统一的代码风格;
  • 审查代码是否存在语法错误;

语法错误好说,编码规范和代码风格如何审查呢?ESLint定义好了一大堆规则作为可配置项;同时,一些大公司会开源出来他们使用的配置(比如说airbnb),你可以在某套现成配置的基础上进行修改,修改成适合你们团队使用的编码规范和代码风格。

更便捷地生成页面文件 webpack-html-plugin大展身手

先讨论两个情况:单页应用和多页应用

  • 单页应用
    单页应用需要一个最基本、最原始的 index.html,要在里面引入js入口文件或者是由extract-text-webpack-plugin抽取出来的css样式文件,但是这个文件需要添加 hash 值已解决迭代更新时的缓存问题,那么最理想的情况就是,webpack 在执行文件打包的时候,自动把生成的资源文件标签插入到 index.html 中;
  • 多页应用
    既然单页应用都有以上需求了,那么相当于多个单页应用组合在一起的多页应用自然也有以上的需求;

综上所述,需要使用到一个webpack的插件来帮助我们自动生成html文件:webpack-html-plugin
使用该插件的作用有如下几点:

  1. 智能地处理资源的动态路径
  2. 方便构建模板布局系统,为多页应用高效地搭建页面框架

基本配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// webpack.base.config.js
// ...
module.exports = {
// ...
output: {
path: DIST_PATH,
publicPath: '/dist/',
filename: '[name].[chunkhash:8].js',
chunkFilename: '[name].[chunkhash:8].js',
},
plugins: [
new HtmlWebpackPlugin({
title: '测试模板',
inject: true, // 当为true或body时,会将script资源插入到body,也可以为head,为false则不插入
hash: true, // 解决资源缓存问题
template: 'index-tpl.html', // 也可以不指定文件模板,让其自动生成默认内容
// template: path.resolve(ROOT_PATH, 'index_tpl.html'), // 等同于上面一行,应该是从根目录开始寻找
filename: 'index.html', // 最终生成的文件名,最后会拼上 output.path 的值
xhtml: true, // 是否把 link 标签渲染为自闭合标签,true为是的
minify: true, // 压缩html代码
// chunks: [], // 允许加载到html文件中的模块文件,不使用时,默认加载全部
}),
new ExtractTextPlugin('[name].[chunkhash:8].css'),
new CommonsChunkPlugin('common.js'),
],
};

处理资源的动态路径

当使用了如上配置,并保证初始化使用 HtmlWebpackPlugin 时的对象属性 inject 不为false时,webpack 会自动把js资源以script标签形式、css资源以link标签形式(前提是使用了如上的 ExtractTextPlugin ),插入到生成的html文档中。

在如上配置中,output.filename 中指定了要生成chunkhash:8值(至于为什么用chunkhash而不用hash,后续有文章解释);output.chunkFilename指定了要生成chunkhash:8;提取出来的css文件也加了chunkhash:8;还有一些图片及其他资源等等。这一系列都加了防止缓存的哈希值,属于动态生成的资源,我们不可能等webpack打包生成出来文件之后,再根据文件名再一个个地去修改再页面中的资源引用地址,这时候,上面所使用到的 html-webpack-plugin 插件就能大展身手了,它会根据webpack打包生成好的资源文件名和地址,逐个去修改完善所在的引用地方。

方便构建模板布局系统

这个应该属于该插件的应用进阶功能了,在多页应用中能够发挥强大的构建模板布局系统的力量。当然在单页应用中也能够灵活地应用,因为使用方法就是那些,万变不离其宗。详细地可能后面自己使用到之后再一项项纪录,现在先简单记录一下实用的技巧。想要知道更多,请移动“多页为王”的gitbook,链接地址在最上方。

html-webpack-plugin 插件允许使用不同类型的模板文件和对应的loader,例如官方给出的有:

  • jade/pug
  • ejs(默认支持)
  • underscore
  • handlebars
  • html-loader

如果不声明使用任何loader,则该webpack插件会默认使用ejs的loader,所以我们在使用ejs文件作为模板时,可以不加载任何loader;而且实际上,我们单使用ejs语法文件作为模板也是很足够的了。

从能够使用那么多模板+loader的情况下,可以推理出,html-webpack-plugin其实并不关心你用的是什么模板引擎,只要你的模板最后export出来的是一份完整的HTML代码(字符串)就可以了。所以这时候当然是要使用js啦!所以接下来我们搞一下事情!搞事情!
给template属性指定一个js文件,然后再该js文件末尾export出一份完整的HTML代码。该js文件可称为“模板接口”,既不是只靠这一份js文件就能形成一份模板,接口之后是一套完整的模板布局体系

安装相关loader

1
$ yarn add --dev ejs-loader

webpack 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.base.config.js
// ...
module.exports = {
// ...
module: {
loaders: [
// ...
{
test: /\.ejs$/,
loader: "ejs-loader", // webpack v2 写法
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: '测试模板',
inject: true, // 当为true或body时,会将script资源插入到body,也可以为head,为false则不插入
hash: true, // 解决资源缓存问题
template: 'renderHtml.js', // !!!!!!!!
filename: 'index.html', // 最终生成的文件名,最后会拼上 output.path 的值
xhtml: true, // 是否把 link 标签渲染为自闭合标签,true为是的
minify: true, // 压缩html代码
// chunks: [], // 允许加载到html文件中的模块文件,不使用时,默认加载全部
}),
new ExtractTextPlugin('[name].[chunkhash:8].css'),
new CommonsChunkPlugin('common.js'),
],
};

模板接口文档

1
2
3
4
5
6
7
8
9
10
11
// renderHtml.js
const content = require('./content.ejs'); // 调取存放本页面实际内容的模板文件
const layout = require('layout'); // 调用管理后台内部所使用的布局方案,我在webpack配置里定义其别名为'layout'
const pageTitle = '消息通知'; // 页面名称

// 给layout传入“页面名称”这一参数(当然有需要的话也可以传入其它参数
// run之后会返回完整的最终生成的HTML字符串
module.exports = layout.init({ pageTitle }).run(content({ pageTitle })); // es6写法

// 其中run函数的最后的功能为 return layout({ renderData }) // var layout = require('./layout.ejs')
// 希望大家看懂了其中的页面嵌套层次

模板页面内容

1
2
3
4
5
6
7
//
<div id="page-wrapper">
<div class="container-fluid" >
<h2 class="page-header"><%= pageTitle %></h2>
<!-- ...... -->
</div>
</div>

从代码里我们可以看出,模板接口的作用实际上就是整理好当前页面独有的内容,然后交与layout作进一步的渲染;
另一方面,模板接口直接把layout最终返回的结果(完整的HTML文档)给export出来,供html-webpack-plugin生成HTML文件使用;

所以可以从中总结出两点技巧:

  1. html-webpack-plugin 插件的配置对象的 template 属性可以为js文件,只要最后export出来的时候html字符串即可;而且使用js文件作为生成页面模板借口非常灵活,能力也很强;
  2. js模板接口文件中可以直接 require ejs文件,并且向内传递数据只需要如下几步:

    1
    2
    3
    4
    5
    6
    var ejsCtn = require('./content.ejs');
    var config = {
    pageTitle: 'xxx',
    // ...
    };
    ejsCtn(config);
  3. js模板接口文件向外输出html字符串也只需要如下(伪代码):

    1
    2
    3
    4
    5
    6
    var ejsCtn = require('./content.ejs');
    var config = {
    pageTitle: 'xxx',
    // ...
    };
    module.exports = ejsCtn(config); // 重点

总结

当多页应用布局比较复杂时,我们就要化繁为简,一步步提取出共性,分离开异性;也即提取出公共模板部分,让每一个页面都可以复用,至于不同的部分,我们就分别定制页面插入到定制好的模板页面中。

首先要养成良好的习惯,上来应该先要有使用模板进行布局的意识,使用模板布局进行系统架构,画出架构图,然后再细化到各个模块部分。

预打包Dll 实现webpack音速编译

这里会重点介绍该功能,因为非常实用!对公司或团队的项目开发和部署效率有着巨大的帮助!!

两个主要的webpack插件

  • DllPlugin
    在打包公用框架或库使用
  • DllReferencePlugin
    在实际项目工程中打包产品代码时使用

其实该功能与 Webpack.optimize.CommonsChunkPlugin 的功能类似,是把公用的部分提取出来进行打包,但是两者也有着本质上的区别,而且应用场景也不一样。

Webpack.optimize.CommonsChunkPlugin 是在一套打包配置文件里引入的,它主要提取各个模块中引入的相同的公用的模块,然后打包成一个新的模块;配置文件中可以指定某些被引入超过设定次数的模块才被提取出来,打包到一个新的模块文件;其中不管是框架还是工具库还是业务逻辑代码,都可以提取打包;这样在打包的逻辑上没有区分是业务逻辑代码还是框架代码,只关心被引入的次数,且这样做能避免打包时重复引入,以减少最终发布包的文件大小;

而预打包Dll的概念和目的就不一样,它首先是想要区分业务逻辑代码还是框架代码;它的出现是想把类似于框架代码这一类在开发中几乎不会进行修改的代码单独使用一份配置文件单独进行打包(比如说bootstrap、jQuery、Vue等等),然后直接在开发环境的配置文件中进行声明和引入,这样可以大大地减少项目文件的编译和打包时间;而且如果把预打包的Dll文件共享出来,还能给团队或公司的其他项目统一使用,极大地保证了各项目的公用代码的统一;另外,如果提前把该Dll文件预先发布到正式环境,同时各个项目在开发过程中,又没有涉及到要修改Dll文件源码的需求,则我们发布上线代码时,也可以大大减少包的大小,也即缩短了产品发布的时间,让大流量的产品在更新时实现秒速更新!

实现步骤:

  1. 利用DllPlugin把公用代码打包成一个“Dll文件”(其实本质上还是js,只是套用概念而已);除了Dll文件外,DllPlugin还会生成一个manifest.json文件作为公用代码的索引供DllReferencePlugin使用;
  2. 在业务代码的webpack配置文件中配置好DllReferencePlugin并进行编译,达到利用DllReferencePlugin让业务代码和Dll文件实现关联的目的;
  3. 在各个页面中,先加载Dll文件,再加载业务代码文件;

适用范围:
Dll文件里只适合放置不常改动的代码,比如说第三方库(谁也不会有事无事就升级一下第三方库吧),尤其是本身就庞大或者依赖众多的库。如果你自己整理了一套成熟的框架,开发项目时只需要在上面添砖加瓦的,那么也可以把这套框架也打包进Dll文件里,甚至可以做到多个项目共用这一份Dll文件

使用DllPlugin插件打包出Dll文件

需要专门为Dll文件建一份webpack配置文件,不能与业务代码共用同一份配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// webpack.dll.config.js
module.exports = {
entry: {
dll: [
path.resolve(SRC_PATH, 'vendor/util.js'),
]
},
output: {
path: DIST_PATH,
publicPath: '/dist/',
filename: '[name]_[chunkhash:8].js',
library: '[name]_[chunkhash:8]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
},
plugins: [
new webpack.DllPlugin({
path: 'manifest.json', // 本Dll文件中各模块的索引,供DllReferencePlugin读取使用
name: '[name]_[chunkhash:8]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与参数output.library保持一致
context: ROOT_PATH, // 指定一个路径作为上下文环境,需要与DllReferencePlugin的context参数保持一致,建议统一设置为项目根目录
}),
/* 跟业务代码一样,该兼容的还是得兼容 */
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
'window.$': 'jquery',
}),
new ExtractTextPlugin('[name].css'), // 打包css/less的时候会用到ExtractTextPlugin
],
// ...
};

编译出Dll文件:

1
$ webpack --progress --profile --color --display-modules --display-chunks --config ./build/webpack.dll.conf.js

也可以将下面的命令写入package.json文件中的scripts字段值中

1
2
3
4
5
6
7
{
//....
"scripts": {
//...
"dll": "webpack --progress --profile --color --display-modules --display-chunks --config ./build/webpack.dll.conf.js "
},
}

使用DllReferencePlugin插件 让业务代码关联Dll文件

需要在供编译业务代码的webpack配置文件里设好DllReferencePlugin的配置项:

1
2
3
4
5
6
7
8
9
10
11
// webpack.dev.config.js
module.exports = {
// ...
plugins: [
new webpack.DllReferencePlugin({
context: ROOT_PATH, // 指定一个路径作为上下文环境,需要与DllPlugin的context参数保持一致,建议统一设置为项目根目录
manifest: path.reslove(ROOT_PATH, 'manifest.json'), // 指定manifest.json
name: (require(path.reslove(ROOT_PATH, 'manifest.json')))['name'], // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
}),
]
};

配置好DllReferencePlugin了以后,正常编译业务代码即可。不过要注意,必须要先编译Dll并生成manifest.json后再编译业务代码,而且以后每次修改Dll并重新编译后,也要重新编译一下业务代码

如何在业务代码里使用Dll文件打包的module资源

最激动人心的就是,不用刻意做些什么,该如何require就如何require,就当作那些打包出去的资源还是在本地工程文件夹中一样

如何整合Dll?

在每个页面,都要按照如下顺序进行加载js文件:

  1. Dll文件
  2. CommonsChunkPlugin生成的公用chunk文件 (如果没用该插件那就忽略跳过)
  3. 页面本身的入口文件

有两个注意事项:

  1. 如果是用HtmlWebpackPlugin插件来生成HTML文件并自动加载chunk文件的话,请务必在<head>里手写<script>来加载Dll文件;
  2. 为了完全分离业务产品代码和Dll产品代码,可以把Dll文件放在一个独有的文件夹,并且可以保持远端服务器部署目录和本地开发即将发布/调试目录保持一致,如果Dll文件长时间不改动,就将其放置在远端服务器部署目录,不必每次发布新的产品代码都重复上传者一部分,本地开发/调试的时候可以请求远端的Dll文件,也可以请求本地的保持和远端部署目录结构一致的目录中的Dll文件。最好的是,做好一份可靠的脚本,自动化地开发/调试/部署!

拒绝复制粘贴 多项目共用基础设施

主要讨论如何在多项目间共用基础设置,又或是某种层次的框架的解决方案。

什么是基础设施

一个完整的网站,不可能只包含一个jQuery,或是某个MVVM框架,其中必定包含了许多解决方案,例如:

  • 如何上传?
  • 如何兼容IE?
  • 如何跨域?
  • 如何使用本地存储?
  • 如何做用户信息反馈?
  • 又或者具体到如何选择日期?等等等等……

这里面必定包含了UI框架、JS框架、各种小工具库,不论是第三方的还是自己团队研发的。而以上所述的种种,就构成了一套完整的解决方案,也称基础设施。基础设施有个重要的特征,那就是与业务逻辑无关,不论是OA还是CMS又或是CRM,只要整体产品形态类似,我们就可以使用同一套基础设施

为什么要共用 基础设施

一个公司或者项目团队里,肯定不止一个代码工程项目,产品形态类似的工程,我们肯定要同步使用或者说共用一套基础设施,比如说,一个地区选择器不可能写两套完全不一样的,又或者是一个日期选择器也不可能写两份,等等之类的。一是维护更新起来工程量很大,一个项目就要维护一份,N个项目就要维护N份,可维护性和可重用性极低。所以,产品形态类似、UI界面相近的工程项目,我们极力主张共用基础设施。

其中基础设施涵盖了:

  • 工具库
  • UI 框架
  • JS 框架
  • 项目框架*
  • 项目架构*

小到前三者,大到后两者,我们都可以称之为基础设施。无论是组件层面、UI层面,还是技术选型框架,又或者是脚手架框架、项目组织架构,都是需要做共性的提取,形成公司或团队的技术积累、资源积累,当有需求使用时,能够做到立即拉出来稍微进行定制就可以使用。

如何实现共用的基础设施

本人跟《多页为王》文章中的思维不一样,也可能是我还没有遇到作者那样的情况和需求,但就本人工作所遇到的问题来说,我更主张大家孜孜不倦地维护和更新各自公司或者团队的代码仓库,包括脚手架、ui组件、ui框架、工具库、模块化打包架构等等之类的。

如果某一个仓库代码进行了更新,那也就的确需要我们一个个项目进行手动修改更新功能或者是修复bug,做到精确和极致。

追求极致 webpack的其他性能优化

上面的文章中提到了很多的插件都为webpack的打包编译进程做了性能优化,下面来介绍几个名不见经传的几个优化插件:

webpack.optimize.OccurrenceOrderPlugin

Assign the module and chunk ids by occurrence count. Ids that are used often get lower (shorter) ids. This make ids predictable, reduces total file size and is recommended.

preferEntry (boolean) give entry chunks higher priority. This make entry chunks smaller but increases the overall size. (recommended)

根据模块调用次数,给模块分配ids,常被调用的ids分配更短的id,使得ids可预测,降低文件大小,该模块推荐使用

使用方式:

1
new webpack.optimize.OccurrencyOrderPlugin()