【Hybrid开发高级系列】WebPack模块化专题
1 Webpack
1.1 概念简介
1.1.1 WebPack是什么
1、一个打包工具
2、一个模块加载工具
3、各种资源都可以当成模块来处理
4、网站 http://webpack.github.io/
如今,越来越多的JavaScript代码被使用在页面上,我们添加很多的内容在浏览器里。如何去很好的组织这些代码,成为了一个必须要解决的难题。
对于模块的组织,通常有如下几种方法:
1、通过书写在不同文件中,使用script标签进行加载
2、CommonJS进行加载(NodeJS就使用这种方式)
3、AMD进行加载(require.js使用这种方式)
4、ES6模块
思考:为什么只有JS需要被模块化管理,前台的很多预编译内容,不需要管理吗?
基于以上的思考,WebPack项目有如下几个目标:
• 将依赖树拆分,保证按需加载
• 保证初始加载的速度
• 所有静态资源可以被模块化
• 可以整合第三方的库和模块
• 可以构造大系统
从下图可以比较清晰的看出WebPack的功能
这是一个示意图1.1.2 WebPack的特点
1、丰富的插件,方便进行开发工作
2、大量的加载器,包括加载各种静态资源
3、代码分割,提供按需加载的能力
4、发布工具
1.1.3 WebPack的优势
• webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。
• 能被模块化的不仅仅是 JS 了。
• 开发便捷,能替代部分 grunt/gulp 的工作,比如打包、压缩混淆、图片转base64等。
• 扩展性强,插件机制完善,特别是支持 React 热插拔(见 react-hot-loader )的功能让人眼前一亮。
1.1.4 模块化打包工具
webpack是一种模块化的工具,每个资源对于webpack来讲都是一个模块,模块之间的引用,它们之间存在的关系,webpack都可以处理好。
1、兼容多种JS模块规范
2、更好地打包静态资源
3、更好地处理模块间的关系
对应不同的资源,有不同的loader
1、SASS
2、Less
3、React
4、Coffee
5、ES6
6、......
1.1.5 热替换/热加载
页面实时刷新已经不再是前端开发过程所追求的功能,热替换已经是新的潮流,当你修改代码时,你所改动的地方会实时反映到页面中,而这个过程并没有刷新页面。这里,需要注意的一点,webpack对于热替换的机制是不同的处理方式的,在有些情况下是会通过刷新页面来实现热加载。当然也可以通过添加参数--inline来实现热替换。
webpack-dev-server --hot --inline
1.1.6 深入了解webpack的实现原理
webpack打包,最基本的实现方式,是将所有的模块代码放到一个数组里,通过数组ID来引用不同的模块,如下图所示,可以发现入口entry.js的代码是放在数组索引0的位置,其它a.js和b.js的代码分别放在了数组索引1和2的位置,而webpack引用的时候,主要通过__webpack_require_的方法引用不同索引的模块。
/************************************************************************/
/******/
([
/* 0 */
/***/
function(module, exports, __webpack_require__) {
__webpack_require__(1);
__webpack_require__(2);
console.log('Hello,world!');
/***/
},
/* 1 */
/***/
function(module, exports) {
var a = 'a.js';
console.log("I'm a.js");
/***/
},
/* 2 */
/***/
function(module, exports) {
var b = 'b.js';
console.log("I'm b.js");
/***/
}
/******/
]);
1.1.7 与Grunt、Gulp的区别
工作流
1、Grunt/Gulp更多的是一种工作流;
2、提供集成所有服务的一站式平台;
3、改善了开发流程。
模块打包
1、webpack是一种模块化打包工具;
2、能够将css、js、image打包为一个JS文件;
3、更智能的模块打包;
4、更丰富的插件、模块加载器。
webpack是很强大的打包工具,也是强大的模块化打包工具,相比seajs,它也是一种模块化加载的库,也有专门的打包工具,但seajs不能很好的处理模块间的关系,功能上来讲比webpack要少一些。
1.2 WebPack的安装
1、安装命令$ npm install webpack -g
2、进入工程目录生成package.json: $sudo cnpminit # 会自动生成一个package.json文件
3、使用webpack:$ sudo cnpminstall webpack --save-dev #将webpack增加到package.json文件中
4、可以使用不同的版本$ npm install webpack@1.2.x --save-dev
5、如果想要安装开发工具$ npm install webpack-dev-server --save-dev
1.3 WebPack的配置
每个项目下都必须配置有一个 webpack.config.js ,它的作用如同常规的 gulpfile.js/Gruntfile.js ,就是一个配置项,告诉 webpack 它需要做什么。
下面是一个例子
var webpack = require('webpack');
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
module.exports = {
//插件项
plugins:[commonsPlugin],
//页面入口文件配置
entry:{
index : './src/js/page/index.js'
},
//入口文件输出配置
output:{
path: 'dist/js/page',
filename: '[name].js'
},
module:{
//加载器配置
loaders:[
{ test: /\.css$/, loader: 'style-loader!css-loader'},
{test: /\.js$/, loader: 'jsx-loader?harmony'},
{test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
{test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
]
},
//其它解决方案配置
resolve:{
root: 'E:/github/flux-example/src', //绝对路径
extensions: ['', '.js', '.json', '.scss'],
alias:{
AppStore : 'js/stores/AppStores.js',
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}
};
1、plugins 是插件项,这里我们使用了一个 CommonsChunkPlugin的插件,它用于提取多个入口文件的公共脚本部分,然后生成一个 common.js 来方便多页面之间进行复用。
2、entry 是页面入口文件配置,output 是对应输出项配置 (即入口文件最终要生成什么名字的文件、存放到哪里)
3、module.loaders 是最关键的一块配置。它告知 webpack 每一种文件都需要使用什么加载器来处理。 所有加载器需要使用npm来加载
最后是 resolve 配置,配置查找模块的路径和扩展名和别名(方便书写)
1.3.1 Loaders加载器配置
加载器
module: {
loaders: [
{test:/\.scss$/, loaders: ['style', 'css', 'sass']}
]
}
模块加载器对于顺序是有要求的,需按照基本->特殊的顺序使用,否则会有报错。WebPack中的loader不是默认就安装好的,需要使用npm下载安装,相关命令如下:
sudo cnpm install --save-dev html-loader
常用Loader包括css-loader、jsx-loader、style-loader、url-loader、file-loader。
1.4 WebPack使用示例
1.4.1 代码准备
这里有最基本的使用方法,给大家一个感性的认识
1、正确安装了WebPack,方法可以参考上面
2、书写entry.js文件document.write("看看如何让它工作!");
3、书写index.html文件
4、执行命令,生成bundle.js文件$ webpack ./entry.js bundle.js
5、在浏览器中打开index.html文件,可以正常显示出预期
6、增加一个content.js文件module.exports = "现在的内容是来自于content.js文件!";
7、修改entry.js文件document.write(require("./content.js"));
8、执行第四步的命令
1.4.2 进行加载器试验
1、增加style.css文件
body {
background: yellow;
}
2、修改entry.js文件
require("!style!css!./style.css");
document.write(require("./content.js"));
3、执行命令,安装加载器$ npm installcss-loaderstyle-loader # 安装的时候不使用 -g
4、执行webpack命令,运行看效果
5、可以在命令行中使用loader $ webpack ./entry.js bundle.js
--module-bind "css=style!css"
1.4.3 使用配置文件
默认的配置文件为webpack.config.js
• 增加webpack.config.js文件
module.exports = {
entry: "./entry.js",
output:{
path:__dirname,
filename: "bundle.js"
},
module:{
loaders:[
{ test: /\.css$/, loader:"style!css"}
]
}
};
执行程序
$sudo webpack
1.4.4 发布服务器
1、安装服务器
$ npm install webpack-dev-server-g
2、$ webpack-dev-server --progress --colors
服务器可以自动生成和刷新,修改代码保存后自动更新画面http://localhost:8080/webpack-dev-server/bundle
1.5 常用命令
webpack常用命令:
webpack #最基本的启动webpack命令
webpack -w #提供watch方法,实时进行打包更新
webpack -p #对打包后的文件进行压缩
webpack -d #提供SourceMaps,方便调试
webpack --colors #输出结果带彩色,比如:会用红色显示耗时较长的步骤
webpack --profile #输出性能数据,可以看到每一步的耗时
webpack --display-modules #默认情况下node_modules下的模块会被隐藏,加上这个参数可以显示这些被隐藏的模块
2 配置说明
webpack的配置文件是一个node.js的module,用CommonJS风格来书写,形如:
module.exports = {
entry:"./entry",
output: {
path: __dirname +"/dist",
filename:"bundle.js"
}
}
webpack的配置文件并没有固定的命名,也没有固定的路径要求,如果你直接用webpack来执行编译,那么webpack默认读取的将是当前目录下的webpack.config.js
$ pwd
/d/xampp/htdocs/webpack-seed
$ webpack # webpack此时读取的实际上是/d/xampp/htdocs/webpack-seed/webpack.config.js
如果你有其它命名的需要或是你有多份配置文件,可以使用--config参数传入路径:
$ webpack --config./webpackConfig/dev.config.js
另外,在CLI执行webpack指令时可传入的参数(当然除了--config)实际上都可以在配置文件里面直接声明,我强烈建议可以的话尽量都在配置文件里写好,有需要的话写两份配置也好三份也好(反正配置文件间也是可以互相引用的,相同的部分就拆成一个module出来以供读取,最后拼成各种情况下需要的配置就好了)。
2.1 入口文件配置:entry参数
entry可以是字符串(单入口),可以是数组(多入口),但为了后续发展,请务必使用object,因为object中的key在webpack里相当于此入口的name,既可以后续用来拼生成文件的路径,也可以用来作为此入口的唯一标识。我推荐的形式是这样的:
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`),
},
对照我的脚手架项目webpack-seed的文件目录结构,就很清楚了:
├─src # 当前项目的源码
├─pages# 各个页面独有的部分,如入口文件、只有该页面使用到的css、模板文件等
│ ├─alert# 业务模块
│ │ └─index# 具体页面
│ ├─index# 业务模块
│ │ ├─index# 具体页面
│ │ └─login# 具体页面
由于每一个入口文件都相当于entry里的一项,因此这样一项一项地来写实在是有点繁琐,我就稍微写了点代码来拼接这entry:
var pageArr = [
'index/login',
'index/index',
'alert/index',
];
var configEntry = {};
pageArr.forEach((page) => {
configEntry[page] = path.resolve(pagesDir, page +'/page');
});
2.2 输出文件:output参数
output参数告诉webpack以什么方式来生成/输出文件,值得注意的是,与entry不同,output相当于一套规则,所有的入口都必须使用这一套规则,不能针对某一个特定的入口来制定output规则。output参数里有这几个子参数是比较常用的:path、publicPath、filename、chunkFilename,这里先给个webpack-seed中的示例:
output: {
path: buildDir,// var buildDir = path.resolve(__dirname, './build');
publicPath: '../../../../build/',
filename: '[name]/entry.js', // [name]表示entry每一项中的key,用以批量指定生成后文件的名称
chunkFilename: '[id].bundle.js',
},
2.2.1 path
path参数表示生成文件的根目录,需要传入一个绝对路径。path参数和后面的filename参数共同组成入口文件的完整路径。
2.2.2 publicPath
publicPath参数表示的是一个URL路径(指向生成文件的根目录),用于生成css/js/图片/字体文件等资源的路径,以确保网页能正确地加载到这些资源。
publicPath参数跟path参数的区别是:path参数其实是针对本地文件系统的,而publicPath则针对的是浏览器;因此,publicPath既可以是一个相对路径,如示例中的'../../../../build/',也可以是一个绝对路径如http://www.xxxxx.com/。一般来说,我还是更推荐相对路径的写法,这样的话整体迁移起来非常方便。那什么时候用绝对路径呢?其实也很简单,当你的html文件跟其它资源放在不同的域名下的时候,就应该用绝对路径了,这种情况非常多见于后端渲染模板的场景。
2.2.3 filename
filename属性表示的是如何命名生成出来的入口文件,规则有以下三种:
1、[name],指代入口文件的name,也就是上面提到的entry参数的key,因此,我们可以在name里利用/,即可达到控制文件目录结构的效果。
2、[hash],指代本次编译的一个hash版本,值得注意的是,只要是在同一次编译过程中生成的文件,这个[hash]的值就是一样的;在缓存的层面来说,相当于一次全量的替换。
3、[chunkhash],指代的是当前chunk的一个hash版本,也就是说,在同一次编译中,每一个chunk的hash都是不一样的;而在两次编译中,如果某个chunk根本没有发生变化,那么该chunk的hash也就不会发生变化。这在缓存的层面上来说,就是把缓存的粒度精细到具体某个chunk,只要chunk不变,该chunk的浏览器缓存就可以继续使用。
下面来说说如何利用filename参数和path参数来设计入口文件的目录结构,如示例中的path:
buildDir, // var buildDir = path.resolve(__dirname, './build');和filename: '[name]/entry.js',那么对于key为'index/login'的入口文件,生成出来的路径就是build/index/login/entry.js了,怎么样,是不是很简单呢?
2.2.4 chunkFilename
chunkFilename参数与filename参数类似,都是用来定义生成文件的命名方式的,只不过,chunkFilename参数指定的是除入口文件外的chunk(这些chunk通常是由于webpack对代码的优化所形成的,比如因应实际运行的情况来异步加载)的命名。
2.3 各种Loader配置:module参数
webpack的核心实际上也只能针对js进行打包,那webpack一直号称能够打包任何资源是怎么一回事呢?原来,webpack拥有一个类似于插件的机制,名为Loader,通过Loader,webpack能够针对每一种特定的资源做出相应的处理。Loader的种类相当多,有些比较基础的是官方自己开发,而其它则是由webpack社区开源贡献出来的,这里是Loader的List:list of loaders。
而module正是配置什么资源使用哪个Loader的参数(因为就算是同一种资源,也可能有不同的Loader可以使用,当然不同Loader处理的手段不一样,最后结果也自然就不一样了)。module参数有几个子参数,但是最常用的自然还是loaders子参数,这里也仅对loaders子参数进行介绍。
2.3.1 loaders参数
loaders参数又有几个子参数,先给出一个官方示例:
module.loaders: [
{
// "test" is commonly used to match the file extension
test:/\.jsx$/,
// "include" is commonly used to match the directories
include: [
path.resolve(__dirname,"app/src"),
path.resolve(__dirname,"app/test")
],
// "exclude" should be used to exclude exceptions
// try to prefer "include" when possible
// the "loader"
loader:"babel-loader"
}
]
下面一一对这些子参数进行说明:
1、test参数用来指示当前配置项针对哪些资源,该值应是一个条件值(condition)。
2、exclude参数用来剔除掉需要忽略的资源,该值应是一个条件值(condition)。
3、include参数用来表示本loader配置仅针对哪些目录/文件,该值应是一个条件值(condition)。这个参数跟test参数的效果是一样的(官方文档也是这么写的),我也不明白为嘛有俩同样规则的参数,不过我们姑且可以自己来划分这两者的用途:test参数用来指示文件名(包括文件后缀),而include参数则用来指示目录;注意同时使用这两者的时候,实际上是and的关系。
4、loader/loaders参数,用来指示用哪个/哪些loader来处理目标资源,这俩货表达的其实是一个意思,只是写法不一样,我个人推荐用loader写成一行,多个loader间使用!分割,这种形式类似于管道的概念,又或者说是函数式编程。形如loader: 'css?!postcss!less',可以很明显地看出,目标资源先经less-loader处理过后将结果交给postcss-loader作进一步处理,然后最后再交给css-loader。
条件值(condition)可以是一个字符串(某个资源的文件系统绝对路径),可以是一个函数(官方文档里是有这么写,但既没有示例也没有说明,我也是醉了),可以是一个正则表达式(用来匹配资源的路径,最常用,强烈推荐!),最后,还可以是一个数组,数组的元素可以为上述三种类型,元素之间为与关系(既必须同时满足数组里的所有条件)。需要注意的是,loader是可以接受参数的,方式类似于URL参数,形如'css?minimize&-autoprefixer',具体每个loader接受什么参数请参考loader本身的文档(一般也就只能在github里看了)。
2.4 添加额外功能:plugins参数
这plugins参数相当于一个插槽位(类型是数组),你可以先按某个plugin要求的方式初始化好了以后,把初始化后的实例丢到这里来。
2.5 示例代码
诸位看本系列文章,搭配我在Github上的脚手架项目食用更佳哦(笑):
Array-Huang/webpack-seed(https://github.com/Array-Huang/webpack-seed)。
本文提到的所有内容,都可以在示例代码根目录下的webpack.config.js里找到对应的内容。
2.6 生产第三方dll
2.6.1 解决方案的机制和原理
DllPlugin&DllReferencePlugin这一方案,实际上也是属于代码分割的范畴,但与CommonsChunkPlugin不一样的是,它不仅仅是把公用代码提取出来放到一个独立的文件供不同的页面来使用,它更重要的一点是:把公用代码和它的使用者(业务代码)从编译这一步就分离出来,换句话说,我们可以分别来编译公用代码和业务代码了。这有什么好处呢?很简单,业务代码常改,而公用代码不常改,那么,我们在日常修改业务代码的过程中,就可以省出编译公用代码那一部分所耗费的时间了(是不是马上就联想到坑爹的bootstrap了呢)。
整个过程大概是这样的:
1、利用DllPlugin把公用代码打包成一个“Dll文件”(其实本质上还是js,只是套用概念而已);除了Dll文件外,DllPlugin还会生成一个manifest.json文件作为公用代码的索引供DllReferencePlugin使用。
2、在业务代码的webpack配置文件中配置好DllReferencePlugin并进行编译,达到利用DllReferencePlugin让业务代码和Dll文件实现关联的目的。
3、在各个页面中,先加载Dll文件,再加载业务代码文件。
2.6.2 适用范围
Dll文件里只适合放置不常改动的代码,比如说第三方库(谁也不会有事无事就升级一下第三方库吧),尤其是本身就庞大或者依赖众多的库。如果你自己整理了一套成熟的框架,开发项目时只需要在上面添砖加瓦的,那么也可以把这套框架也打包进Dll文件里,甚至可以做到多个项目共用这一份Dll文件。
2.6.3 如何配置哪些代码需要打包进Dll文件?
我们需要专门为Dll文件建一份webpack配置文件,不能与业务代码共用同一份配置:
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const dirVars = require('./webpack-config/base/dir-vars.config.js'); // 与业务代码共用同一份路径的配置表
module.exports = {
output: {
path: dirVars.dllDir,
filename:'[name].js',
library:'[name]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
},
entry: {
/*
指定需要打包的js模块
或是css/less/图片/字体文件等资源,但注意要在module参数配置好相应的loader
*/
dll: [
'jquery', '!!bootstrap-webpack!bootstrapConfig',
'metisMenu/metisMenu.min', 'metisMenu/metisMenu.min.css',
],
},
plugins: [
new webpack.DllPlugin({
path:'manifest.json', // 本Dll文件中各模块的索引,供DllReferencePlugin读取使用
name:'[name]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与参数output.library保持一致
context: dirVars.staticRootDir,// 指定一个路径作为上下文环境,需要与DllReferencePlugin的context参数保持一致,建议统一设置为项目根目录
}),
/* 跟业务代码一样,该兼容的还是得兼容 */
new webpack.ProvidePlugin({
$:'jquery',
jQuery:'jquery',
'window.jQuery': 'jquery',
'window.$': 'jquery',
}),
new ExtractTextPlugin('[name].css'), // 打包css/less的时候会用到ExtractTextPlugin
],
module: require('./webpack-config/module.config.js'), // 沿用业务代码的module配置
resolve:require('./webpack-config/resolve.config.js'), // 沿用业务代码的resolve配置
};
2.6.4 如何编译Dll文件?
编译Dll文件的代码实际上跟编译业务代码是一样的,记得利用--config指定上述专供Dll使用的webpack配置文件就好了:
$ sudo webpack --progress --colors --config ./webpack-dll.config.js
另外,建议可以把该语句写到npm scripts里,好记一点。
2.6.5 如何让业务代码关联Dll文件?
我们需要在供编译业务代码的webpack配置文件里设好DllReferencePlugin的配置项:
new webpack.DllReferencePlugin({
context: dirVars.staticRootDir,// 指定一个路径作为上下文环境,需要与DllPlugin的context参数保持一致,建议统一设置为项目根目录
manifest:require('../../manifest.json'), // 指定manifest.json
name:'dll', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
});
配置好DllReferencePlugin了以后,正常编译业务代码即可。不过要注意,必须要先编译Dll并生成manifest.json后再编译业务代码;而以后每次修改Dll并重新编译后,也要重新编译一下业务代码。
2.6.6 如何在业务代码里使用Dll文件打包的module/资源?
不需要刻意做些什么,该怎么require就怎么require,webpack都会帮你处理好的了。
2.6.7 如何整合Dll?
在每个页面里,都要按这个顺序来加载js文件:Dll文件 => CommonsChunkPlugin生成的公用chunk文件(如果没用CommonsChunkPlugin那就忽略啦) => 页面本身的入口文件。
有两个注意事项:
1、如果你是像我一样利用HtmlWebpackPlugin来生成HTML并自动加载chunk的话,请务必在里手写来加载Dll文件。
为了完全分离源文件和编译后生成的文件,也为了方便在编译前可以清空build目录,不应直接把Dll文件编译生成到build目录里,我建议可以先生成到源文件src目录里,再用file-loader给原封不动搬运过去。
$ webpack --progress --colors --config ./webpack-dll.config.js
2.7 图片打包细则
在实际生产中有以下几种图片的引用方式:
1. HTML文件中img标签的src属性引用或者内嵌样式引用
<img src="photo.jpg" />
<div style="background:url(photo.jpg)"></div>
2. CSS文件中的背景图等设置
.photo {
background: url(photo.jpg);
}
3. JavaScript文件中动态添加或者改变的图片引用
var imgTempl = '';
document.body.innerHTML = imgTempl;
4. ReactJS中图片的引用
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
render() {
return (<img src='photo.jpg' />);
}
}
ReactDom.render(<App />, document.querySelector('#container'));
2.7.1 url-loader
在webpack中引入图片需要依赖url-loader这个加载器。
安装:
npm install url-loader --save-dev
当然你可以将其写入配置中,以后与其他工具模块一起安装。
在webpack.config.js文件中配置如下:
module: {
loaders: [
{
test: /\.(png|jpg)$/,
loader: 'url-loader?limit=8192'
}
]
}
test属性代表可以匹配的图片类型,除了png、jpg之外也可以添加gif等,以竖线隔开即开。
loader后面 limit字段代表图片打包限制,这个限制并不是说超过了就不能打包,而是指当图片大小小于限制时会自动转成base64码引用。上例中大于8192字节的图片正常打包,小于8192字节的图片以base64的方式引用。
url-loader后面除了limit字段,还可以通过name字段来指定图片打包的目录与文件名:
module: {
loaders: [
{
test: /\.(png|jpg)$/,
loader:'url-loader?limit=8192&name=images/[hash:8].[name].[ext]'
}
]
}
上例中的name字段指定了在打包根目录(output.path)下生成名为images的文件夹,并在原图片名前加上8位hash值。
例:工程目录如下
在main.css中引用了同级images文件夹下的bg.jpg图片
background-image: url(./images/bg.jpg);
通过之前的配置,使用$ webpack命令对代码进行打包后生成如下目录
打包目录中,css文件和images文件夹保持了同样的层级,可以不做任务修改即能访问到图片。区别是打包后的图片加了hash值,bundle.css文件里引入的也是有hash值的图片。
background-image: url(images/f593fbb9.bg.jpg);
(上例中,使用了单独打包css的技术,只是为了方便演示)
2.7.2 publicPath
output.publicPath表示资源的发布地址,当配置过该属性后,打包文件中所有通过相对路径引用的资源都会被配置的路径所替换。
output: {
path: 'dist',
publicPath: '/assets/',
filename: 'bundle.js'
}
main.css
background-image: url(./images/bg.jpg);
bundle.css
background-image: url(/assets/images/f593fbb9.bg.jpg);
该属性的好处在于当你配置了图片CDN的地址,本地开发时引用本地的图片资源,上线打包时就将资源全部指向CDN了。
但是要注意,如果没有确定的发布地址不建议配置该属性,否则会让你打包后的资源路径很混乱。
2.7.3 JS中的图片
初用webpack进行项目开发的同学会发现:在js或者react中引用的图片都没有打包进bundle文件夹中。
正确写法应该是通过模块化的方式引用图片路径,这样引用的图片就可以成功打包进bundle文件夹里了
js
var imgUrl = require('./images/bg.jpg'),
imgTempl = '<img src="'+imgUrl+'" />'
document.body.innerHTML = imgTempl;
react
render() {<img src={require('./images/bg.jpg')} />}
2.7.4 HTML中的图片
由于webpack对html的处理不太好,打包HTML文件中的图片资源是相对来说最麻烦的。这里需要引用一个插件——html-withimg-loder
$ npm install html-withimg-loader --save-dev
webpack.config.js添加配置
module: {
loaders: [
{
test: /\.html$/,
loader: 'html-withimg-loader'
}
]
}
在bundle.js中引用html文件
import '../index.html';
这样html文件中的图片就可以被打包进bundle文件夹里了。
2.7.5 html-withimg-loader打包图片时无法处理多层相对路径的问题
对于如下代码:
<img src="./../../image/ziIcon.png" class="icon_login"/>
使用html-withimg-loader打包之后,路径处理出错:
<img src="image/ziIcon.png" class="icon_login">
2.7.6 图片压缩后被打包在build根目录问题
webpack file-loader解析css文件中background-image路径问题。
http://blog.csdn.net/qq_16559905/article/details/54894284
==url-loader配置(1)==
loaders: [
{
test:/\.(png|jpg|jpeg|gif|woff)$/,
loader: 'url?limit=4192&name=[path][name].[ext]'
},
]
limit参数,代表如果小于大约4k则会自动帮你压缩成base64编码的图片,否则拷贝文件到生产目录
==url-loader配置(2)==
test: /\.(png|jpe?g|gif|ico)(\?\S*)?$/,
loader:'file-loader',
query: {
name:'img/[name].[ext]'
}
直接将图片拷贝到生产目录
2.8 CSS样式提取
2.8.1 extract-text-webpack-plugin插件
Extract text from bundle into a file.从bundle中提取出特定的text到一个文件中。使用extract-text-webpack-plugin就可以把css从js中独立抽离出来
安装
$ npm install extract-text-webpack-plugin --save-dev
使用(css为例)
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
loaders: [
{ test: /\.css$/, loader:ExtractTextPlugin.extract("style-loader","css-loader") }
]
},
plugins: [
newExtractTextPlugin("styles.css")
]
}
它将从每一个用到了require("style.css")的entry chunks文件中抽离出css到单独的output文件。
API
newExtractTextPlugin([id:string], filename:string,[options])
1、id:Unique ident for this plugin instance. (For advanded usage only, by default automatic generated)
2、filename:the filename of the result file. May contain [name], [id] and [content hash].
[name] the name of the chunk
[id] the number of the chunk
[content hash] a hash of the content of the extracted file
3、options
allChunks:extract from all additional chunks too (by default it extracts only from the initial chunk(s))
disable:disables the plugin
4、ExtractTextPlugin.extract([notExtractLoader],loader, [options])
根据已有的loader,创建一个提取器(loader的再封装)
1、notExtractLoader (可选)当css没有被抽离时,加载器不应该使用(例如:当allChunks:false时,在一个additional 的chunk中)
2、loader 数组,用来转换css资源的加载器s
3、options
publicPath 重写该加载器(loader)的 publicPath 的设置
多入口文件的extract的使用示例:
let ExtractTextPlugin = require('extract-text-webpack-plugin');
// multiple extract instances
let extractCSS = newExtractTextPlugin('stylesheets/[name].css');
let extractLESS = newExtractTextPlugin('stylesheets/[name].less');
module.exports = {
...
module: {
loaders: [
{test: /\.scss$/i, loader:extractCSS.extract(['css','sass'])},
{test: /\.less$/i, loader:extractLESS.extract(['css','less'])},
...
]
},
plugins: [
extractCSS,
extractLESS
]
};
2.9 HTML处理
2.9.1 HTML-Loader——HTML打包
With this configuration:
{
module:{
rules:[
{ test: /\.jpg$/, use: [ "file-loader"]},
{ test: /\.png$/, use: [ "url-loader?mimetype=image/png"] }
]
},
output:{
publicPath: "http://cdn.example.com/[hash]/"
}
}
<!-- file.html -->
<img src="image.png" data-src="image2x.png" >
require("html-loader!./file.html");
// => '<img src="http://cdn.example.com/49eba9f/a992ca.png" data-src="image2x.png">’
require("html-loader?attrs=img:data-src!./file.html");
// => '<img src="image.png" data-src="data:image/png;base64,...">‘
require("html-loader?attrs=img:src img:data-src!./file.html");
require("html-loader?attrs[]=img:src&attrs[]=img:data-src!./file.html");
// => '<img src="http://cdn.example.com/49eba9f/a992ca.png" data-src="data:image/png;base64,...">'
require("html-loader?-attrs!./file.html");
// => ''<img src="image.jpg" data-src="image2x.png">"
minimized by running webpack --optimize-minimize
'<img src=http://cdn.example.com/49eba9f/a9f92ca.jpg data-src=data:image/png;base64,...>'
or specify the minimize property in the rule's options in your webpack.conf.js
module:{
rules:[{
test: /\.html$/,
use:[ {
loader: 'html-loader',
options:{
minimize: true
}
}],
}]
}
'Root-relative' URLs
For urls that start with a /, the default behavior is to not translate them. If a root query parameter is set, however, it will be prepended to the url and then translated.
With the same configuration as above:
<!-- file.html -->
<img src="/image.jpg">
require("html-loader!./file.html");
// => ''<img src="/image.jpg">"
require("html-loader?root=.!./file.html");
// => ''<img src="http://cdn.example.com/49eba9f/a992ca.jpg">"
Interpolation
You can use interpolate flag to enable interpolation syntax for ES6 template strings, like so:
require("html-loader?interpolate!./file.html");
<img src="${require(`./images/gallery.png`)}">
<div>${require('./components/gallery.html')}</div>
And if you only want to use require in template and any other ${} are not to be translated, you can set interpolate flag to require, like so:
require("html-loader?interpolate=require!./file.ftl");
<#list list as list>
<a href="${list.href!}" >${list.name}</a>
</#list>
<img src="${require(`./images/gallery.png`)}">
<div>${require('./components/gallery.html')}</div>
2.9.2 HtmlWebpackPlugin——动态生成HTML
wzsxyz/html-withimg-loader
https://github.com/wzsxyz/html-withimg-loader
2.9.3 html-withimg-loader——HTML中图片路径处理
如果涉及生成html目录与开发目录不一致时,该控件不能正确生成路径,则推荐将路径通过js来动态绑定。
2.10 集成AngularJS
(Very Good)Webpack + Angular的组件化实践
https://segmentfault.com/a/1190000003915443
2.10.1 主模块引入
Angular自带了Module以及Directive机制,但Angular1.x版本下,我觉得这些机制不太适合做这种多页面网站的组件化,而且也违背了选用jade渲染的初衷。
Angular自己有自己独特的依赖注入以及模块声明方式,看起来似乎和Webpack是水火不容的,但事实上他们完全可以融合。只需要多几行代码:
主文件app.js大概长这样:
var angular = require('angular');
var starkAPP = angular.module('starkAPP', [
]);
module.exports = starkAPP;
注意到我们在这里把starkAPP作为模块的接口暴露出去,然后我们就可以这样写controller:
//someController.js
var starkAPP = require('./app.js');
starkAPP.controller('someController', ['$scope', function($scope) {
//...
}])
2.10.2 生成bundle.js
运行一下 webpack someController.js bundle.js 即生成了一个可以使用的bundle.js。
当然如果你有一堆controller、directive、service,最好用个 main.js 全部声明一下依赖:
//main.js
require('./Controller1');
require('./Controller2');
require('./Controller3');
require('./Service');
require('./Directive');
2.10.3 目录结构设计
这里我只放了浏览器端的文件结构,整个的项目结构可以看 这里
|package.json 存放npm相关的配置
|gulpfile.js gulp的配置文件
|webpack.config.js 存放webpack相关的配置
|build 存放构建完毕的资源文件
|node_modules 不解释了= =
|src 源代码
└── components 组件
├── angular angular组件,比如各种directive、service
├── base 需要全站引入的组件,比如reset.css
└── header 头部组件
├──header.jade
├──header.scss
└──header.js
└── pages 页面定义文件
└── index 首页配置文件
├── index.js
└── index.scss
└── template 提供给node渲染的jade模板
└── index.jade 首页模板
看文件结构绝对是云里雾里的,下面详细说明:
1、首先这是首页的模板 index.jade
html(ng-app="starkAPP")
head
link(rel='stylesheet',href='/static/css/index.css')
script(type='text/javascript',src='/static/js/index.bundle.js')
body(ng-view)
include../components/header/header.jade
注意到我们引入了header的jade,以及两个文件 index.css 和 index.bundle.js
2、 index.css 是啥?
它是 pages/index/ 里面的 index.scss 编译成的:
// pages/index/index.scss
@import '../../components/header/header';
@import '../../components/base/base';
注意到我们在这里引入了header.scss
3、 index.bundle.js 是啥?
它是 pages/index/ 里面的 index.js 经过webpack打包成的东西
// pages/index/index/js
require('../../components/angular/app.js');
require('../../components/header/header.js');
我们在这里引入了angular以及 header.js
总之,pages下面放的就是各个页面的组件依赖文件
比如我的首页index依赖了一个组件header,那么我需要在 index.js 和 index.scss 中声明我依赖了 header.js 以及 header.scss
其实用webpack打包的话,只需要一个定义文件就可以同时打包js和scss,但我还不太确定webpack打包scss这种方法是否成熟。
2.11 module.exports
module.exports用于将模块(文件)作为一个接口(一般是一个函数)暴露给外部。
2.12 集成低版本jQuery1.9.1
在本项目中比较特殊,因为对于第三方类库统一采用了dll单独打包方式,但由于jQuery不支持CDM,所以打包采用extenal的形式打包的。
Webpack-dll.config.js
entry: {
dll: [
'jQuery', 'jQuery-scroll', 'swiper', 'ichart', 'vconsole', 'iscroll'
],
dll_angular :['angular', 'angular-animate', 'angular-route', 'angular-ui-router']
},
externals: {
//require("jQuery ") is external and available
// on the global var jQuery
"jQuery": "'m/js/public/vendor/jquery-1.9.1.min.js'"
},
而在webpack.config.js中需要对jQuery单独引用
//jQuery引用插件
var jQueryProvidePlugin = new webpack.ProvidePlugin({
$: 'jQuery',
jQuery: 'jQuery',
'window.jQuery':'jQuery',
'window.$':'jQuery'
});
module.exports = {
//插件项
plugins: [
jQueryProvidePlugin,
commonsPlugin,
dllReferencePlugin,
dllAngularReferencePlugin
]
}
module: {
//加载器配置
loaders: [
{
// 此loader配置项的目标是NPM中的jquery
test:require.resolve('jQuery'),
// 先把jQuery对象声明成为全局变量`jQuery`,再通过管道进一步又声明成为全局变量`$`
loader: 'expose?$!expose?jQuery'
}
]
}
而业务js文件中则不需要显示require了
//var $ = require('jquery');
2.13 支持ES6
Enable support for ECMAScript 6 (via Babel):
Per project: npm install babel-loader --save-dev
3 开发技巧
3.1 配置技巧Demos
3.1.1 多入口配置
Multiple entry files are allowed. It is useful for a multi-page app.
// main1.js
document.write('<h1>Hello World</h1>);
// main2.js
document.write('<h2>Hello Webpack</h2>');
index.html
<html>
<body>
<script src="bundle1.js"></script>
<script src="bundle2.js"></script>
</body>
</html>
webpack.config.js
module.exports= {
entry: {
bundle1: './main1.js',
bundle2: './main2.js'
},
output: {
filename: '[name].js'
}
};
3.1.2 CSS-loader
Demo04: CSS-loader (source)
Webpack allows you to require CSS in JS file, then preprocessed CSS file with CSS-loader.
main.js
require('./app.css');
app.css
body {
background-color: blue;
}
index.html
<html>
<head>
<script type="text/javascript" src="bundle.js"></script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
webpack.config.js
module.exports= {
entry: './main.js',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{ test: /\.css$/, loader: 'style-loader!css-loader' },
]
}
};
Attention, you have to use two loaders to transform CSS file. First is CSS-loader to read CSS file, and another is Style-loader to insert Style tag into HTML page. Different loaders are linked by exclamation mark(!).
After launching the server, index.html will have internal style sheet.
3.1.3 Imageloader
Demo05: Image loader (source)
Webpack could also require images in JS files.
main.js
var img1 = document.createElement("img");
img1.src = require("./small.png");
document.body.appendChild(img1);
var img2 = document.createElement("img");
img2.src = require("./big.png");
document.body.appendChild(img2);
index.html
webpack.config.js
module.exports= {
entry: './main.js',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' }
]
}
};
url-loader transforms image files. If the image size is smaller than 8192 bytes, it will be transformed into Data URL; otherwise, it will be transformed into normal URL. As you see, question mark(?) is used to pass parameters into loaders.
After launching the server, small.png and big.png will have the following URLs.
<img src="data:image/png;base64,iVBOR...uQmCC">
<img src="4853ca667a2b8b8844eb2693ac1b2578.png">
3.1.4 代码压缩插件UglifyJsPlugin
Demo07: UglifyJs Plugin (source)
Webpack has a plugin system to expand its functions. For example, UglifyJs Plugin will minify output(bundle.js) JS codes.
main.js
var longVariableName = 'Hello';
longVariableName+= ' World';
document.write('<h1>' +longVariableName + '</h1>');
index.html
webpack.config.js
var webpack = require('webpack');
var uglifyJsPlugin =webpack.optimize.UglifyJsPlugin;
module.exports= {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new uglifyJsPlugin({
compress: { warnings: false}
})
]
};
After launching the server, main.js will be minified into following.
var o="Hello";o+="World",document.write("<h1>"+o+"</h1>")
3.1.5 HTMLWebpack Plugin and Open Browser Webpack Plugin
Demo08: HTML Webpack Plugin and Open Browser Webpack Plugin (source)
This demo shows you how to load 3rd-party plugins.
html-webpack-plugin could create index.html for you, and open-browser-webpack-plugin could open a new browser tab when Webpack loads.
main.js
document.write('<h1>Hello World</h1>');
webpack.config.js
var HtmlwebpackPlugin = require('html-webpack-plugin');
var OpenBrowserPlugin = require('open-browser-webpack-plugin');
module.exports= {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new HtmlwebpackPlugin({
title: 'Webpack-demos',
filename: 'index.html'
}),
new OpenBrowserPlugin({
url: 'http://localhost:8080'
})
]
};
Run webpack-dev-server.
$ webpack-dev-server
Now you don't need to write index. html by hand and don't have to open browser by yourself. Webpack did all these things for you.
3.1.6 环境标记
Demo09: Environment flags (source)
You can enable some codes only in development environment with environment flags.
main.js
document.write('<h1>HelloWorld</h1>');
if (__DEV__) {
document.write(new Date());
}
index.html
webpack.config.js
var webpack = require('webpack');
var devFlagPlugin = new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'false'))
});
module.exports= {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [devFlagPlugin]
};
Now pass environment variable into webpack.
# Linux & Mac
$ env DEBUG=true webpack-dev-server
# Windows
$ setDEBUG=true
$ webpack-dev-server
3.1.7 代码分割Codesplitting
Demo10: Code splitting (source)
For big web apps it’s not efficient to put all code into a single file, Webpack allows you to split them into several chunks. Especially if some blocks of code are only required under some circumstances, these chunks could be loaded on demand.
At first, you use require.ensure to define a split point. (official document)
// main.js
require.ensure(['./a'], function(require)
{
var content = require('./a');
document.open();
document.write('<h1>' +content + '</h1>');
document.close();
});
require.ensure tells Webpack that ./a.js should be separated from bundle.js and built into a
single chunk file.
// a.js
module.exports = 'Hello World';
Now Webpack takes care of the dependencies, output files and runtime stuff. You don't have to put any redundancy into your index.html and webpack.config.js.
webpack.config.js
module.exports= {
entry: './main.js',
output: {
filename: 'bundle.js'
}
};
Launch the server.
$ webpack-dev-server
On the surface, you won't feel any differences. However, Webpack actually builds main.js and a.js into different chunks(bundle.js and 1.bundle.js), and loads 1.bundle.js from bundle.js when on demand.
3.1.8 bundle-loader切割代码Codesplitting with bundle-loader
Demo11: Code splitting with bundle-loader (source)
Another way of code splitting is using bundle-loader.
// main.js
// Now a.js is requested, it will be bundled into another file
var load = require('bundle-loader!./a.js');
// To wait until a.js is available (and get the exports)
// you need to async wait for it.
load(function(file)
{
document.open();
document.write('<h1>' +file + '</h1>');
document.close();
});
require('bundle-loader!./a.js') tells Webpack to load a.js from another chunk.
Now Webpack will build main.js into bundle.js, and a.js into 1.bundle.js.
3.1.9 用CommonsChunkPlugin提取通用代码块Common chunk
Demo12: Common chunk (source)
When multi scripts have common chunks, you can extract the common part into a separate file with CommonsChunkPlugin.
// main1.jsx
var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(<h1>HelloWorld</h1>, document.getElementById('a'));
// main2.jsx
var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(<h2>Hello Webpack</h2>, document.getElementById('b'));
index.html
webpack.config.js
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports= {
entry: {
bundle1: './main1.jsx',
bundle2: './main2.jsx'
},
output: {
filename: '[name].js'
},
module: {
loaders:[
{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
},
]
},
plugins: [
new CommonsChunkPlugin('init.js')
]
}
3.1.10 第三方库代码块Vendor chunk
Demo13: Vendor chunk (source)
You can also extract the vendor libraries from a script into a separate file with CommonsChunkPlugin.
main.js
var $ = require('jquery');
$('h1').text('Hello World');
index.html
webpack.config.js
var webpack = require('webpack');
module.exports= {
entry: {
app: './main.js',
vendor: ['jquery'],
},
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin(/* chunkName= */'vendor', /* filename= */'vendor.js')
]
};
If you want a module available as variable in every module, such as making $ and jQuery available in every module without writing require("jquery"). You should use ProvidePlugin (Official
doc).
// main.js
$('h1').text('Hello World');
// webpack.config.js
var webpack = require('webpack');
module.exports= {
entry: {
app: './main.js'
},
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery"
})
]
};
3.1.11 暴露全局变量
Demo14: Exposing global variables (source)
If you want to use some global variables, and don't want to include them in the Webpack bundle, you can enable externals field in webpack.config.js (official
document).
For example, we have a data.js.
var data = 'Hello World';
We can expose data as a global variable.
// webpack.config.js
module.exports= {
entry: './main.jsx',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
},
]
},
externals: {
// require('data') is external and available
// on the global var data
'data': 'data'
}
};
Now, you require data as a module variable in your script. but it actually is a global variable.
// main.jsx
var data = require('data');
var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(<h1>{data}</h1>, document.body);
3.1.12 热更新HMR
Demo15: Hot Module Replacement (source)
Hot Module Replacement(HMR) exchanges, adds, or removes modules while an application is running without a page reload.
You have two ways to enable Hot Module Replacement with the webpack-dev-server.
(1) Specify --hot and --inline on the command line
$ webpack-dev-server --hot --inline
Meaning of the options:
• --hot:
adds the HotModuleReplacementPlugin and switch the server to hot mode.
• --inline:
embed the webpack-dev-server runtime into the bundle.
• --hot --inline:
also adds the webpack/hot/dev-server entry.
(2) Modify webpack.config.js.
• add
new webpack.HotModuleReplacementPlugin() to the plugins field
• add
webpack/hot/dev-server and webpack-dev-server/client?http://localhost:8080 to the entry field
webpack.config.js looks like the following.
var webpack = require('webpack');
var path = require('path');
module.exports= {
entry: [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
'./index.js'
],
output: {
filename: 'bundle.js',
publicPath: '/static/'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
},
include: path.join(__dirname, '.')
}]
}
};
Now launch the dev server.
$ webpack-dev-server
Visiting http://localhost:8080, you should see 'Hello World' in your browser. Don't close the server. Open a new terminal to edit App.js, and modify 'Hello World' into 'Hello Webpack'. Save it, and see what happened in the browser.
App.js
import React, { Component } from 'react';
export default class App extends Component {
render() {
return (<h1>Hello World</h1>);
}
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Appfrom './App';
ReactDOM.render(<App />, document.getElementById('root'));
index.html
3.1.13 将样式抽取出来为独立的文件
将require引入的样式嵌入js文件中,有好处也有坏处。好处是减少了请求数,坏处也很明显,就是当你的样式文件很大时,造成编译的js文件也很大。
我们可以使用插件的方式,将样式抽取成独立的文件。使用的插件就是extract-text-webpack-plugin
基本用法如下:
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
loaders: [
{ test:/\.css$/, loader: ExtractTextPlugin.extract("style-loader","css-loader") }
]
},
plugins: [
newExtractTextPlugin("styles.css")
]
}
根据插件在github上的解释,ExtractTextPlugin.extract可以有三个参数。
第一个参数是可选参数,传入一个loader,当css样式没有被抽取的时候可以使用该loader。
第二个参数则是用于编译解析的css文件loader,很明显这个是必须传入的,就像上述例子的css-loader。
第三个参数是一些额外的备选项,貌似目前只有传入publicPath,用于当前loader的路径。
那什么时候需要传入第一个参数呢,那就得明白什么时候样式不会被抽取出来。
了解过code splittiog的同学便会知道,我们有些代码在加载页面的时候不会被使用时,使用code splitting,可以实现将这部分不会使用的代码分离出去,独立成一个单独的文件,实现按需加载。
那么如果在这些分离出去的代码中如果有使用require引入样式文件,那么使用ExtractTextPlugin这部分样式代码是不会被抽取出来的。
这部分不会抽取出来的代码,可以使用loader做一些处理,这就是ExtractTextPlugin.extract第一个参数的作用。
根据上面的案例,ExtractTextPlugin需要配合plugin使用。
new ExtractTextPlugin([id: string], filename: string,[options])
1、该插件实例的唯一标志,一般是不会传的,其自己会生成。
2、文件名。可以是[name]、[id]、[content hash]
3、[name]:将会和entry中的chunk的名字一致
4、[id]:将会和entry中的chunk的id一致
5、[content hash]:根据内容生成hash值
6、options
7、allchunk: 是否将所有额外的chunk都压缩成一个文件
8、disable:禁止使用插件
这里的参数filename里如何理解呢?上述案例指定了一个固定的名字,因此便会生成一个styles.css文件。
那么像[name]、[id]这些如何理解。这个在你有多个entry的时候,便需要使用这种方式来命名。
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: {
"script": "./src/entry.js",
"bundle": "./src/entry2.js",
},
...
module: {
loaders: [
{ test:/\.css$/, loader: ExtractTextPlugin.extract("style-loader","css-loader") }
]
},
plugins: [
newExtractTextPlugin("[name].css")
]
}
这时候便会生成两个css文件,一个是script.css,另一个便是bundle.css。那些[id]、[content hash]也是一个道理。
只要明白,在你有多个entry是,一定要使用这种方式来命名css文件。
最后还有那个allchunks又是什么呢?很简单,还记得前面提到的code splitting么?将该参数配置为true,那么所有分离文件的样式也会全部压缩到一个文件上。
plugins: [
newExtractTextPlugin("[name].css", {allChunks: true})
]
3.1.14 兼容jQuery
3.1.14.1 推荐法一:ProvidePlugin+ expose-loader
首先来介绍我最为推荐的方法:ProvidePlugin + expose-loader,在我公司的项目,以及我个人的脚手架开源项目webpack-seed里使用的都是这一种方法。
ProvidePlugin的配置是这样的:
var providePlugin = new webpack.ProvidePlugin({
$:'jquery',
jQuery:'jquery',
'window.jQuery': 'jquery',
'window.$': 'jquery',
});
ProvidePlugin的机制是:当webpack加载到某个js模块里,出现了未定义且名称符合(字符串完全匹配)配置中key的变量时,会自动require配置中value所指定的js模块。
如上述例子,当某个老式插件使用了jQuery.fn.extend(object),那么webpack就会自动引入jquery(此处我是用NPM的版本,我也推荐使用NPM的版本)。
另外,使用ProvidePlugin还有个好处,就是,你自己写的代码里,再!也!不!用!require!jQuery!啦!
接下来介绍expose-loader,这个loader的作用是,将指定js模块export的变量声明为全局变量。下面来看下expose-loader的配置:
/*
很明显这是一个loader的配置项,篇幅有限也只能截取相关部分了
看不明白的麻烦去看本系列的另一篇文章《webpack多页应用架构系列(二):webpack配置常用部分有哪些?》:https://segmentfault.com/a/1190000006863968
*/
{
test:require.resolve('jquery'), // 此loader配置项的目标是NPM中的jquery
loader:'expose?$!expose?jQuery', // 先把jQuery对象声明成为全局变量`jQuery`,再通过管道进一步又声明成为全局变量`$`
},
你或许会问,有了ProvidePlugin为嘛还需要expose-loader?问得好,如果你所有的jQuery插件都是用webpack来加载的话,的确用ProvidePlugin就足够了;但理想是丰满的,现实却是骨感的,总有那么些需求是只能用来加载的。
3.1.14.2 法二:externals
externals是webpack配置中的一项,用来将某个全局变量“伪装”成某个js模块的exports,如下面这个配置:
externals: {
'jquery': 'window.jQuery',
},
那么,当某个js模块显式地调用var $ = require('jquery')的时候,就会把window,jQuery返回给它。
与上述ProvidePlugin + expose-loader的方案相反,此方案是先用加载的jQuery满足老式jQuery插件的需要,再通过externals将其转换成符合模块化要求的exports。
我个人并不太看好这种做法,毕竟这就意味着jQuery脱离NPM的管理了,不过某些童鞋有其它的考虑,例如为了加快每次打包的时间而把jQuery这些比较大的第三方库给分离出去(直接调用公共CDN的第三方库?),也算是有一定的价值。
3.1.14.3 法三:imports-loader
这个方案就相当于手动版的ProvidePlugin,以前我用requireJS的时候也是用的类似的手段,所以我一开始从requireJS迁移到webpack的时候用的也是这种方法,后来知道有ProvidePlugin就马上换了哈。
// ./webpack.config.js
module.exports = {
...
module: {
loaders: [
{
test:require.resolve("some-module"),
loader:"imports?$=jquery&jQuery=jquery", // 相当于`var $ = require("jquery");var
jQuery = require("jquery");`
}
]
}
};
3.1.15 html-loader
webpack的一个优势是,它能够将非JavaScript内容转换为JavaScript模块,以便可像其他任何JavaScript资源一样要求它。可以使用webpack加载器完成许多种类的任务;我们利用了html-loader。没有html-loader,我们就需要采用一个构建步骤来搜索所有 HTML 文件,并将它们注入到 Angular $templateCache 中,以便在指令使用templateUrl属性时,可以找到相应的HTML。
浏览所有指令并将templateUrl:'/directive/markup.html'替换为require('./markup.html')需要花点时间,但最终结果会变得更容易维护。现在,我们在开发期间就知道是否错误地引用了模板,而不会在构建时才发现引用路径偏移了一个目录级别。
要在您的项目中使用html-loader,可运行以下命令来安装它:
npm install --save-dev html-loader
3.1.16 ProvidePlugin
webpack ProvidePlugin 是一个将出现的全局变量替换为显示导出的已加载关联模块的插件,它对我们的改进工作不可或缺。因为我们的应用程序的开发周期的绝大部分都没有模块化,它包含对 angular、$、moment 和其他库的许多全局引用,例如:
moment().add(2, 'days');
ProvidePlugin 将前面的代码更改为:
require('moment')().add(2, 'days');
使用 ProvidePlugin,我们就不需要查找并替换众多文件中出现的所有这些全局变量。
3.2 打包技巧
3.2.1 多入口文件的打包
由于项目不适宜整体作为一个SPA,所以各子功能都有一个自己的入口文件,我的源码目录结构如下:
apps目录下放置各个子功能,如question和paper,下面是各自的子页面。components目录放置公共组件,这个后面再说。
由于功能模块是随时会增加的,我不能在webpack的entry中写死这些入口文件,所以用了一个叫做glob的模块,它能够用通配符来取到所有的文件,就像我们用gulp那样。动态获取子功能入口文件的代码如下:
/**
* 动态查找所有入口文件
*/
var files = glob.sync('./public/src/apps/*/index.js');
var newEntries = {};
files.forEach(function(f){
var name = /.*\/(apps\/.*?\/index)\.js/.exec(f)[1];//得到apps/question/index这样的文件名
newEntries[name] = f;
});
config.entry = Object.assign({}, config.entry, newEntries);
webpack打包后的目录是很乱的,如果你入口文件的名字取为question,那么会在dist目录下直接生成一个question.xxxxx.js的文件。但是如果把名字取为apps/question/index这样的,则会生成对应的目录结构。我是比较喜欢构建后的目录也有清晰的结构的,可能是习惯gulp的后遗症吧。这样也便于我们在前端路由中进行统一操作。也是一个小技巧吧,我生成的各入口文件的目录如下:
3.2.2 第三方库的打包
项目中用到了一些第三方库,如vue、vue-router、jquery、boostrap等。这些库我们基本上是不会改动源代码的,并且项目初期就基本确定了,不会再添加。所以把它们打包在一起。当然这个也是要考虑大小不超过500KB的,如果是用到了像ueditor这样的大型工具库,还是要单独打包的。
配置文件的写法是很简单的,在entry中配一个名为vendor的就好,比如:
entry: {
vendor: ['vue', 'vue-router', './public/vendor/jquery/jquery']
},
不管是用npm安装的还是自己放在项目目录中的库都是可以的,只要路径写对就行。
为了把第三方库拆分出来(用标签单独加载),我们还需要用webpack的CommonsChunkPlugin插件来把它提取一下,这样他就不会与业务代码打包到一起了。代码:
new webpack.optimize.CommonsChunkPlugin('vendor');
3.2.3 公共组件的打包
这部分代码的处理我是纠结了好久的,因为webpack的打包思想是以模块的依赖树为标准来进行分析的,如果a模块使用了loading组件,那么loading组件就会被打包进a模块,除非我们在代码中用require.ensure或者AMD式的require加回调,显式声明该组件异步加载,这样loading组件会被单独打包成一个chunk文件。
以上两者都不是我想要的,理由参见文章开头的打包原则,把所有公共组件打包在一起是一个自然合理的选择,但这又与webpack的精神相悖。
一开始我想到了一招曲线救国,就是在components目录下建一个main.js文件,该文件引用所有的组件,这样打包main.js的时候所有组件都会被打包进来,main.js的代码如下:
import loading from './loading.vue';
import topnav from './topnav.vue';
import centernav from'./centernav.vue';
export {loading, topnav, centernav}
有点像sass的main文件的感觉。使用的时候这样写:
let components =require('./components/main');
export default{
components: {
loading: (resolve) =>{
require(['./components/main'],function(components){
resolve(components.loading);
})
}
}
}
缺点是也得写成异步加载的,否则main.js还是会被打包进业务代码。
不过后来我又一想,既然vendor可以,为什么组件不可以用同样的方式处理呢?于是乎找到了最佳方法。 同样先用glob动态找到所有的components,然后写进entry,最后再用CommonsChunkPlugin插件剥离出来。代码如下:
/*动态查找所有components*/
var comps = glob.sync('./public/src/components/*.vue');
var compsEntry = {components: comps};
config.entry = Object.assign({}, config.entry, compsEntry);
要注意CommonsChunkPlugin是不可以new多个的,要剥离多个需要传数组进去,写法如下:
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'components']
})
如此一来,components就和vendor一样可以用标签引入页面了,使用的时候就可以随便引入了,不会再被重复打包进业务代码。如:
import loading from'./components/loading';
import topnav from
'./components/topnav';
3.2.4 把这些文件塞进入口页面
之前说过我们的子功能模块有各自的页面,所以我们需要把这些文件都给引入进这些页面,webpack的HtmlWebpackPlugin可以做这件事情,我们在动态查找入口文件的时候顺便把它做了就行了,代码如下:
/**
*动态查找所有入口文件
*/
var files =glob.sync('./public/src/apps/*/index.js');
var newEntries = {};
files.forEach(function(f){
var name = /.*\/(apps\/.*?\/index)\.js/.exec(f)[1]; //得到apps/question/index这样的文件名
newEntries[name] = f;
var plug = newHtmlWebpackPlugin({
filename: path.resolve(__dirname, '../public/dist/'+ name +'.html'),
chunks: ['vendor', name, 'components'],
template: path.resolve(__dirname, '../public/src/index.html'),
inject: true
});
config.plugins.push(plug);
});
3.2.5 子页面的异步载入
每个功能模块是作为一个SPA应用来处理的,这就意味着我们会根据前端路由来动态加载相应子页面,使用官方的vue-router是很容易实现的,比如我们在question/index.js中可以如下写:
router.map({
'/list': {
component: (resolve) => {
require(['./list.vue'], resolve);
}
},
'/edit': {
component: (resolve) => {
require(['./edit.vue'], resolve);
}
}
});
在webpack的配置文件中就无需再写什么了,它会自动打包出对应的chunk文件,此时我的dist目录就长这样了:
有一点让我疑惑的是,异步加载的chunk文件貌似无法输出文件名称,尽管我在output参数中这么配置:chunkFilename:'[name].[chunkhash].js',[name]那里输出的还是id,可能和webpack处理异步chunk的机制有关吧,猜测的。不过也无所谓的,反正能够正确加载,就是名字难看点。
--------更新于2016.10.11-------
为异步chunk命名的方法我找到了,需要两步。首先output中还是应该这么配置:chunkFilename:'[name].[chunkhash].js'。然后,利用require.ensure的第三个参数,可以为chunk指定名字。上面的代码修改为如下:
router.map({
'/list': {
component: (resolve) => {
// require(['./list.vue'], resolve);
require.ensure([],function(){
resolve(require('./list.vue'));
}, 'list');
}
},
'/edit': {
component: (resolve) => {
//require(['./edit.vue'], resolve);
require.ensure([],function(){
resolve(require('./edit.vue'));
}, 'edit');
}
}
});
这样list和edit这两个组件生成的chunk就有名字了,如下:
我个人还是偏好生成的chunk能带上名字,这样可读性好一些,便于调试和尽快发现错误。
3.3 配置问题
3.3.1 Conflict:Multiple assets emit to the same filename bundle.js
编译时报错:
Conflict: Multiple assets emit to the same filename bundle.js
原因分析:
命令输入错误,在有config文件的情况下,不需要指定生成的目标文件名:
sudo webpack ./entry.js bundle.js
正确命令是:
sudo webpack
3.3.2 退出命令行按Ctrl+Z或者Ctrl+C
3.3.3 ERRORin Entry module not found: Error: Can't resolve 'jsx-loader'
编译报错:
ERROR in Entry module not found: Error: Can't resolve 'jsx-loader'
原因分析:
是因为该工程中jsx-loader没有安装,但是config文件中已经开始使用了。
解决方案:
执行npm命令安装jsx-loader
sudo cnpm install --save-dev jsx-loader
3.3.4 Css样式编写错误导致打包失败Cannotresolve directory '.'
错误:
ERROR in ./~/.0.26.1@css-loader!./m/css/layout_1.css
Module not found: Error: Cannot resolve directory '.' in
/Users/junmac/Documents/DevProjects/临时项目/m_WebPack/m/css
@ ./~/.0.26.1@css-loader!./m/css/layout_1.css6:194865-194878
原因:
是因为css样式中有背景图引用了空链接
例如:
.right_noIcon{
background: url('');
}
background-image: url('');
解决方案:
删除空链接引用;
3.3.5 Can not resolve module 'hidpi-canvas'
错误:
Cannot resolve module 'hidpi-canvas'
3.3.6 打包运行时ui.router报错
原因:
没有显式引用angular-ui-router;
3.3.7 打包后JSONP请求报错
原因:
这是因为AngularJS的打包版本不对,promise.error只在1.2.32版本中才有,但1.6.2版本已经移除了。
解决方案:
卸载1.6.2版本,重新安装1.2.32版本
3.3.8 ele.droploadis not a function
3.3.9 集成Angular时报错:Modulenot found: Error: a dependency to an entry point is not allowed
打包时报错:Module not found: Error: a dependency to an entry point is not allowed
原因分析:
在Angular工程中集成webpack时,依赖关系是从父到子的,因此不需要再父模块中用require引入子模块控制器js代码,也不需要在html中通过标签显式引入。只需要在子模块js前引用父模块js文件即可。
3.3.10 Angular中用require引入子模板时不能用templateUrl键,要用template
在AngularJS的路由配置中,一般情况下是直接使用templateUrl参数来指定模板html文件路径来引入,而基于webpack的整改会用到require函数引入,此引入其实会将html读取成字符串,因此要用template参数,而不是templateUrl,否则加载时就会报加载资源出错问题。
3.3.11 a dependency to an entry point is not allowed
原因:
你的header.js已经作为entry了,就不能在另一个entry里引用它
解决方法:
entry: {
a: "./a", //a requires b
b: ["./b"] // workaround: "./b"can now be in another bundle
}
https://segmentfault.com/q/1010000007590988
3.3.12 子模块单独用js文件编写时的引入顺序问题导致报错Module 'login' is not available
Error: [$injector:nomod] Module 'login' is not available!You either misspelled the module name or forgot to load it. If registering amodule ensure that you specify the dependencies as the second argument.
原因分析:
因为require()函数其实是同步函数,因此对于子模块的引入一定要放在父模块Module的初始化之后,例如:
//var lockCtr = require('../lock/lock.js');
var loginModule = angular.module("login", ['ui.router']);
loginModule.name = "login";
module.exports = loginModule.name;
var lockCtr = require('../lock/lock.js ');
若在module初始化之前做require子模块动作,则必定报错“找不到login module”,因此如果是同步引用(当然也可以用异步加载方式),则必须将其放在loginModule初始化操作之后。
4 参考链接
4.1 webpack入门级教程
webpack入门级教程
http://www.tuicool.com/articles/bA3eym7
WebPack简明学习教程
http://www.jianshu.com/p/b95bbcfc590d
[新姿势]前端革命,革了再革:WebPack
https://segmentfault.com/a/1190000002507327
WebPack:更优秀的模块依赖管理工具,及require.js的缺陷
http://www.kuqin.com/shuoit/20141221/344013.html
webpack解惑:多入口文件打包策略
http://www.cnblogs.com/lvdabao/p/5944420.html
webpack loader列表
http://blog.csdn.net/keliyxyz/article/details/51649429
用Webpack打包你的一切
http://www.tuicool.com/articles/bIRBjmZ
前端webpack
workflow(二)——Webpack基本使用
https://segmentfault.com/a/1190000003985802
LIBRARY AND EXTERNALS
http://webpack.github.io/docs/library-and-externals.html
webpack配置
http://www.cnblogs.com/bergus/p/4820435.html
前端webpackworkflow(二)——Webpack基本使用
https://segmentfault.com/a/1190000003985802
Webpack引入jquery及其插件的几种方法
http://blog.csdn.net/yiifaa/article/details/51916560
4.2 模块化Angular
使用Webpack模块化Angular应用程序
http://www.ibm.com/developerworks/cn/web/wa-modularize-angular-apps-with-webpack-trs/index.html
webpack-demos
https://github.com/ruanyf/webpack-demos
Angular中使用webpack基础篇
http://www.tuicool.com/articles/qQJfEju
(Very Good)Webpack + Angular的组件化实践
https://segmentfault.com/a/1190000003915443
AngularJS进阶(一) 按需加载controllerjs(转帖)
https://my.oschina.net/sourcecoding/blog/304735
angular-component-way-webpack-starter-kit
https://github.com/zombiQWERTY/angular-component-way-webpack-starter-kit
用webpack模块化后,如何使用 jsonp?
https://segmentfault.com/q/1010000004889541
(Good)用ES6和webpack开发angular1.x项目(译)
https://yq.aliyun.com/articles/35701
(Good)Webpack入门——使用Webpack打包Angular项目的一个例子
http://blog.csdn.net/hsany330/article/details/53096632
使用webpack组织Angular1.x
http://www.jianshu.com/p/ca4ba492f868
gulp+webpack+angular1的一点小经验(第二部分webpack包起来的angular1)
http://blog.csdn.net/sisierda/article/details/53573813
Webpack lazy load controller in angular
http://stackoverflow.com/questions/33585021/webpack-lazy-load-controller-in-angular
Angular (SPA) WebPack模块化打包、按需加载解决方案完整实现
http://www.cnblogs.com/teamblog/p/6241189.html
[译]通过Webpack实现AngularJS的延迟加载
https://toutiao.io/posts/46gvgm/preview
webpack异步加载业务模块
http://www.cnblogs.com/rubylouvre/p/4981929.html
4.3 Loaders
Code Splitting
https://webpack.js.org/guides/code-splitting/
Webpack常见问题与解答
http://www.tuicool.com/articles/U3ENRvA
angular2-template-loader: Error: Can't resolve './' in\@angular\compiler\src
How to use webpack import aws-sdk
http://stackoverflow.com/questions/31584310/how-to-use-webpack-import-aws-sdk
vue2 vue-router2 webpack
http://www.qinshenxue.com/article/20161106163608.html
4.4 通用代码抽取
(good)webpack如何分别打包第三方库和自己写的公用业务逻辑库
https://segmentfault.com/q/1010000005975720
Webpack的dll功能
https://segmentfault.com/a/1190000005969643
webpack CommonsChunkPlugin详细教程
https://segmentfault.com/a/1190000006808865
webpack多页应用架构系列(二):webpack配置常用部分有哪些?
https://segmentfault.com/a/1190000006863968
webpack多页应用架构系列(三):怎么打包公共代码才能避免重复?
https://segmentfault.com/a/1190000006871991
webpack多页应用架构系列(四):老式jQuery插件还不能丢,怎么兼容?
https://segmentfault.com/a/1190000006887523
webpack多页应用架构系列(十一):预打包Dll,实现webpack音速编译
https://segmentfault.com/a/1190000007104372
(Good)【多dll构建】怎样令webpack的构建加快十倍、DllPlugin的用法
http://blog.csdn.net/technofiend/article/details/52850596
使用webpack生成html没有插入打包好的js和css
https://segmentfault.com/q/1010000007676619
用webpack的CommonsChunkPlugin提取公共代码的3种方式
http://blog.csdn.net/github_26672553/article/details/52280655
多页面分离资源引用,按需引用JS和css
http://blog.csdn.net/github_26672553/article/details/52280422
nodejs+gulp+webpack+sass基础篇
http://blog.csdn.net/github_26672553/article/details/52280422
webpack CommonsChunkPlugin详细教程
https://segmentfault.com/a/1190000006808865
commonChunkplugin配置项详解
http://blog.csdn.net/liangklfang/article/details/54931523
4.5 图片
(Good)webpack踩坑之路(2)——图片的路径与打包
http://www.cnblogs.com/ghost-xyx/p/5812902.html
(Good)webpack处理html中img的src引入的图片
http://blog.csdn.net/wzs_xyz/article/details/51745898
wzsxyz/html-withimg-loader
https://github.com/wzsxyz/html-withimg-loader
4.6 HTML
(Good)基于webpack的前端工程化开发解决方案探索(1):动态生成HTML
http://www.myexception.cn/web/2045910.html
webpack入门(六)——html-webpack-plugin
http://blog.csdn.net/keliyxyz/article/details/51513114
webpack插件:html-webpack-plugin
http://www.cnblogs.com/haogj/p/5160821.html
webpack怎么打包html
https://zhidao.baidu.com/question/1642789063781673180.html
(Good)webpack-contrib/html-loader
https://github.com/webpack-contrib/html-loader
file-loader引起的html-webpack-plugin坑
http://www.cnblogs.com/wonyun/p/5950722.html
vue.js+webpack为img src赋值的路径问题?
https://segmentfault.com/q/1010000004582219
4.7 CSS
(Good)Webpack常见静态资源处理-模块加载器(Loaders)+ExtractTextPlugin插件
http://www.cnblogs.com/sloong/p/5826818.html
webpack打包css的问题
https://segmentfault.com/q/1010000004585800
webpack如何配置ExtractTextPlugin才能不让入口引入的多个css文件,打包成一个(有些css文件不需要合并)
https://segmentfault.com/q/1010000004231390
webpack less解析打包后的css代码出现重复
https://segmentfault.com/q/1010000004432384?_ea=608121
(Good)webpack file-loader解析css文件中background-image路径问题。
http://blog.csdn.net/qq_16559905/article/details/54894284
(Good)webpack打包后图片路径出错
https://segmentfault.com/q/1010000005660403/a-1020000005660427
4.8 Gulp整合
Webpack webpack+gulp实现自动构建部署
http://www.cnblogs.com/sloong/p/5826859.html
4.9 jQuery集成
webpack 系列 三:webpack如何集成第三方js库
http://www.cnblogs.com/sloong/p/5689135.html
webpack如何全局引入jquery和插件?
https://www.zhihu.com/question/33448231
webpack集成vue,引入jquery打包分包问题(1/2)
http://www.codes51.com/article/detail_304252.html
webpack多页应用架构系列(四):老式jQuery插件还不能丢,怎么兼容?
https://segmentfault.com/a/1190000006887523
webpack如何全局加载第三方插件,类似jQuery?