一步步讲解如何配置和使用 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
{
test: require('some-module'), // require第三方插件
loader: 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()