本文github仓库地址: https://github.com/Rynxiao/webpack-tutorial ,里面包括了本教程的全部代码。javascript
【若是你以为这篇文章写得不错,麻烦给本仓库一颗星:-D】css
webpack is a module bundler.
webpack takes modules with dependencies and generates static assets representing those modules.html
简单的归纳就是:webpack是一个模块打包工具,处理模块之间的依赖同时生成对应模块的静态资源。前端
图中已经很清楚的反应了几个信息:java
webpack --config webpack.config.js
)npm install -g webpack
npm install webpack // 处理相似以下调用 import webpack from "webpack"; var webpack = require("webpack");
建议安装淘宝的npm镜像,这样下载npm包会快上不少,具体作法:node
// 方式一 npm install xx --registry=https://registry.npm.taobao.org/ // 方式二:安装淘宝提供的npm工具 npm install -g cnpm cnpm install xx // 方式三 // 在用户主目录下,找到.npmrc文件,加上下面这段配置 registry=https://registry.npm.taobao.org/
建立配置文件(webpack.config.js
,执行webpack命令的时候,默认会执行这个文件)react
module.export = { entry : 'app.js', output : { path : 'assets/', filename : '[name].bundle.js' }, module : { loaders : [ // 使用babel-loader解析js或者jsx模块 { test : /\.js|\.jsx$/, loader : 'babel' }, // 使用css-loader解析css模块 { test : /\.css$/, loader : 'style!css' }, // or another way { test : /\.css$/, loader : ['style', 'css'] } ] } };
说明一: webpack.config.js
默认输出一个webpack
的配置文件,与CLI
方式调用相同,只是更加简便
说明二: 执行webpack
命令便可以运行配置,先决条件,全局安装webpack
,项目安装各模块loader
说明三: entry
对应须要打包的入口js
文件,output
对应输出的目录以及文件名,module
中的loaders
对应解析各个模块时须要的加载器jquery
一个简单的例子webpack
basic/app.js
git
require('./app.css'); document.getElementById('container').textContent = 'APP';
basic/app.css
* { margin: 0; padding: 0; } #container { margin: 50px auto; width: 50%; height: 200px; line-height: 200px; border-radius: 5px; box-shadow: 0 0 .5em #000; text-align: center; font-size: 40px; font-weight: bold; }
basic/webpack.config.js
/** * webpack打包配置文件 */ module.exports = { // 若是你有多个入口js,须要打包在一个文件中,那么你能够这么写 // entry : ['./app1.js', './app2.js'] entry : './app.js', output : { path : './assets/', filename : '[name].bundle.js' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] } };
basic/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>basic webpack</title> </head> <body> <div id="container"></div> <script src="./assets/main.bundle.js"></script> </body> </html>
在basic
文件夹执行webpack
,打包信息以下
生成main.bundle.js
文件,chunk
名称为main
,也是webpack
默认生成的chunk
名
4.1.1webpack
的多入口配置
上例的简单配置中,只有一个入口文件,那么若是对应于一个页面须要加载多个打包文件或者多个页面想同时引入对应的打包文件的时候,应该怎么作?
entry : { app1 : './app1.js', app2 : './app2.js' }
在multi-entry
文件夹执行webpack
,打包信息以下
可见生成了两个入口文件,以及各自对应的chunk
名
4.2.1 output.publicPath
output: { path: "/home/proj/cdn/assets/[hash]", publicPath: "http://cdn.example.com/assets/[hash]/" }
引用一段官网的话:
The publicPath specifies the public URL address of the output files when referenced in a browser. For loaders that embed
<script>
or<link>
tags or reference assets like images, publicPath is used as the href or url() to the file when it’s different then their location on disk (as specified by path).
大体意思就是:publicPath
指定了你在浏览器中用什么地址来引用你的静态文件,它会包括你的图片、脚本以及样式加载的地址,通常用于线上发布以及CDN部署的时候使用。
好比有下面一段配置:
var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry : './app.js', output : { path : './assets/', filename : '[name].bundle.js', publicPath : 'http://rynxiao.com/assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new HtmlWebpackPlugin({ filename: './index-release.html', template: path.resolve('index.template'), inject: 'body' }) ] };
其中我将publicPath
设置成了http://rynxiao.com/assets/
,其中设置到了插件的一些东西,这点下面会讲到,总之这个插件的做用是生成了上线发布时候的首页文件,其中script
中引用的路径将会被替换。以下图:
4.2.2 output.chunkFilename
各个文件除了主模块之外,还可能生成许多额外附加的块,好比在模块中采用代码分割就会出现这样的状况。其中chunkFilename
中包含如下的文件生成规则:
[id] 会被对应块的id替换.
[name] 会被对应块的name替换(或者被id替换,若是这个块没有name).
[hash] 会被文件hash替换.
[chunkhash] 会被块文件hash替换.
例如,我在output中以下设置:
output : { path : './assets/', filename : '[name].[hash].bundle.js', chunkFilename: "chunk/[chunkhash].chunk.js" }
同时我修改了一下basic/app.js
中的文件
require('./app.css'); require.ensure('./main.js', function(require) { require('./chunk.js'); }); document.getElementById("container").textContent = "APP";
其中对应的chunk.js
就会生成带有chunkhash
的chunk
文件,以下图:
这在作给文件打版本号的时候特别有用,当时如何进行hash
替换,下面会讲到
4.2.3 output.library
这个配置做为库发布的时候会用到,配置的名字即为库的名字,一般能够搭配libraryTarget
进行使用。例如我给basic/webpack.config.js
加上这样的配置:
output : { // ... library : 'testLibrary' // ... }
那么实际上生成出来的main.bundle.js
中会默认带上如下代码:
var testLibrary = (//....之前的打包生成的代码); // 这样在直接引入这个库的时候,就能够直接使用`testLibrary`这个变量
4.2.4 output.libraryTarget
规定了以哪种方式输出你的库,好比:amd/cmd/或者直接变量,具体包括以下
"var"
- 以直接变量输出(默认library方式) var Library = xxx (default)
"this"
- 经过设置this
的属性输出 this["Library"] = xxx
"commonjs"
- 经过设置exports
的属性输出 exports["Library"] = xxx
"commonjs2"
- 经过设置module.exports
的属性输出 module.exports = xxx
"amd"
- 以amd方式输出
"umd"
- 结合commonjs2/amd/root
例如我以umd
方式输出,如图:
loader
中!
表明的含义
require("!style!css!less!bootstrap/less/bootstrap.less");
// => the file "bootstrap.less" in the folder "less" in the "bootstrap"
// module (that is installed from github to "node_modules") is
// transformed by the "less-loader". The result is transformed by the
// "css-loader" and then by the "style-loader".
// If configuration has some transforms bound to the file, they will not be applied.
表明加载器的流式调用,例如:
{ test : /\.css|\.less$/, loader : 'style!css!less' }
就表明了先使用less加载器来解释less文件,而后使用css加载器来解析less解析后的文件,依次类推
loaders
中的include
与exclude
include
表示必需要包含的文件或者目录,而exclude
的表示须要排除的目录
好比咱们在配置中通常要排除node_modules
目录,就能够这样写
{ test : /\.js$/, loader : 'babel', exclude : nodeModuleDir }
官方建议:优先采用include,而且include最好是文件目录
module.noParse
使用了noParse
的模块将不会被loaders
解析,因此当咱们使用的库若是太大,而且其中不包含require
、define
或者相似的关键字的时候(由于这些模块加载并不会被解析,因此就会报错),咱们就可使用这项配置来提高性能。
例以下面的例子:在basic/
目录中新增no-parse.js
var cheerio = require('cheerio'); module.exports = function() { console.log(cheerio); }
webpack.config.js
中新增以下配置:
module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ], noParse : /no-parse.js/ }
当执行打包后,在浏览器中打开index.html
时,就会报错require is not defined
resolve.alias
为模块设置别名,可以让开发者指定一些模块的引用路径。对一些常常要被import或者require的库,如react,咱们最好能够直接指定它们的位置,这样webpack能够省下很多搜索硬盘的时间。
例如咱们修改basic/app.js
中的相关内容:
var moment = require("moment"); document.getElementById("container").textContent = moment().locale('zh-cn').format('LLLL');
加载一个操做时间的类库,让它显示当前的时间。使用webpack --profile --colors --display-modules
执行配置文件,获得以下结果:
其中会发现,打包总共生成了104个隐藏文件,其中一半的时间都在处理关于moment
类库相关的事情,好比寻找moment
依赖的一些类库等等。
在basic/webpack.config.js
加入以下配置,而后执行配置文件
resolve : { alias : { moment : 'moment/min/moment-with-locales.min.js' } }
有没有发现打包的时间已经被大大缩短,而且也只产生了两个隐藏文件。
配合module.noParse
使用
module.noParse
参看上面的解释
noParse: [/moment-with-locales/]
执行打包后,效果以下:
是否是发现打包的时间进一步缩短了。
配合externals
使用
externals
参看下面的解释
Webpack 是如此的强大,用其打包的脚本能够运行在多种环境下,Web 环境只是其默认的一种,也是最经常使用的一种。考虑到 Web 上有不少的公用 CDN 服务,那么 怎么将 Webpack 和公用的 CDN 结合使用呢?方法是使用 externals 声明一个外部依赖。
externals: { moment: true }
固然了 HTML 代码里须要加上一行
<script src="//apps.bdimg.com/libs/moment/2.8.3/moment-with-locales.min.js"></script>
执行打包后,效果以下:
resolve.extensions
resolve : { extensions: ["", ".webpack.js", ".web.js", ".js", ".less"] }
这项配置的做用是自动加上文件的扩展名,好比你有以下代码:
require('style.less'); var app = require('./app.js');
那么加上这项配置以后,你能够写成:
require('style'); var app = require('./app');
当咱们想在项目中require一些其余的类库或者API,而又不想让这些类库的源码被构建到运行时文件中,这在实际开发中颇有必要。此时咱们就能够经过配置externals参数来解决这个问题:
//webpack.config.js module.exports = { externals: { 'react': 'React' }, //... }
externals对象的key是给require时用的,好比require('react'),对象的value表示的是如何在global(即window)中访问到该对象,这里是window.React。
同理jquery的话就能够这样写:'jquery': 'jQuery',那么require('jquery')便可。
HTML中注意引入顺序便可:
<script src="react.min.js" /> <script src="bundle.js" />
提供了一些方式来使得代码调试更加方便,由于打包以后的代码是合并之后的代码,不利于排错和定位。其中有以下几种方式,参见官网devtool
例如,我在basic/app.js
中增长以下配置:
require('./app.css'); // 新增hello.js,显然在文件夹中是不会存在hello.js文件的,这里会报错 require('./hello.js'); document.getElementById("container").textContent = "APP";
执行文件,以后运行index.html
,报错结果以下:
给出的提示实在main.bundle.js第48行,点进去看其中的报错以下:
从这里你彻底看不出到底你程序的哪一个地方出错了,而且这里的行数还算少,当一个文件出现了上千行的时候,你定位bug
的时间将会更长。
增长devtool
文件配置,以下:
module.exports = { devtool: 'eval-source-map', // .... };
执行文件,以后运行index.html
,报错结果以下:
这里发现直接定位到了app.js
,而且报出了在第二行出错,点击去看其中的报错以下:
发现问题定位一目了然。
5.1.1 Commonjs采用require.ensure
来产生chunk
块
require.ensure(dependencies, callback); //static imports import _ from 'lodash' // dynamic imports require.ensure([], function(require) { let contacts = require('./contacts') })
这一点在output.chunkFileName
中已经作过演示,能够去查看
5.1.2 AMD采用require
来产生chunk
块
require(["module-a", "module-b"], function(a, b) { // ... });
5.1.3 将项目APP代码与公共库文件单独打包
咱们在basic/app.js
中添加以下代码
var $ = require('juqery'), _ = require('underscore'); //.....
而后咱们在配置文件中添加vendor
,以及运用代码分离的插件对生成的vendor
块从新命名
var webpack = require("webpack"); module.exports = { entry: { app: "./app.js", vendor: ["jquery", "underscore", ...], }, output: { filename: "bundle.js" }, plugins: [ new webpack.optimize.CommonsChunkPlugin(/* chunkName= */"vendor", /* filename= */"vendor.bundle.js") ] };
运行配置文件,效果以下:
5.1.4 抽取多入口文件的公共部分
咱们从新创建一个文件夹叫作common
,有以下文件:
// common/app1.js console.log("APP1");
// common/app2.js console.log("APP2");
打包以后生成的app1.bundle.js
、app2.bundle.js
中会存在许多公共代码,咱们能够将它提取出来。
// common/webpack.config.js /** * webpack打包配置文件 * 抽取公共部分js */ var webpack = require('webpack'); module.exports = { entry : { app1 : './app1.js', app2 : './app2.js' }, output : { path : './assets/', filename : '[name].bundle.js' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new webpack.optimize.CommonsChunkPlugin("common.js") ] };
抽取出的公共js为common.js
,如图
查看app1.bundle.js
,发现打包的内容基本是咱们在模块中所写的代码,公共部分已经被提出到common.js
中去了
5.1.5 抽取css文件,打包成css bundle
默认状况下以require('style.css')
状况下导入样式文件,会直接在index.html
的<head>
中生成<style>
标签,属于内联。若是咱们想将这些css文件提取出来,能够按照下面的配置去作。
// extract-css/app1.js require('./app1.css'); document.getElementById("container").textContent = "APP"; // extract-css/app2.js require('./app2.css'); document.getElementById("container").textContent = "APP1 APP2"; // extract-css/app1.css * { margin: 0; padding: 0; } #container { margin: 50px auto; width: 50%; height: 200px; line-height: 200px; border-radius: 5px; box-shadow: 0 0 .5em #000; text-align: center; font-size: 40px; font-weight: bold; } // extract-css/app2.css #container { background-color: #f0f0f0; } // extract-css/webpack.config.js /** * webpack打包配置文件 * 抽取公共样式(没有chunk) */ var webpack = require('webpack'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry : { app1 : './app1.js', app2 : './app1.js' }, output : { path : './assets/', filename : '[name].bundle.js' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : ExtractTextPlugin.extract("style-loader", "css-loader") } ] }, plugins : [ new ExtractTextPlugin("[name].css") ] };
获得的效果以下图:
若是包含chunk文件,而且chunk文件中也由于了样式文件,那么样式文件会嵌入到js中
css合并到一个文件
// ... module.exports = { // ... plugins: [ new ExtractTextPlugin("style.css", { allChunks: true }) ] }
效果如图:
若是包含chunk文件,而且chunk文件中也由于了样式文件,样式文件不会嵌入到js中,而是直接输出到style.css
配合CommonsChunkPlugin一块儿使用
// ... module.exports = { // ... plugins: [ new webpack.optimize.CommonsChunkPlugin("commons", "commons.js"), new ExtractTextPlugin("[name].css") ] }
效果图以下:
线上发布时为了防止浏览器缓存静态资源而改变文件版本,这里提供两种作法:
5.2.1 使用HtmlWebpackPlugin
插件
// version/webpack.config.js /** * webpack打包配置文件 * 文件打版本,线上发布 */ var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry : './app.js', output : { path : './assets/', filename : '[name].[hash].bundle.js', publicPath : 'http://rynxiao.com/assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new HtmlWebpackPlugin({ filename: './index-release.html', template: path.resolve('index.template'), inject: 'body' }) ] };
生成的效果以下:
每次打包以后都会生成文件hash,这样就作到了版本控制
5.2.2 自定义插件给文件添加版本
// version/webpack.config.version.js /** * webpack打包配置文件 * 文件打版本,线上发布,自定义插件方式 */ var path = require('path'); var fs = require('fs'); var cheerio = require('cheerio'); module.exports = { entry : './app.js', output : { path : './assets/', filename : '[name].[hash].bundle.js', publicPath : 'http://rynxiao.com/assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ function() { this.plugin("done", function(stats) { fs.writeFileSync( path.join(__dirname, "stats.json"), JSON.stringify(stats.toJson()) ); fs.readFile('./index.html', function(err, data) { var $ = cheerio.load(data.toString()); $('script[src*=assets]').attr('src','http://rynxiao.com/assets/main.' + stats.hash +'.bundle.js'); fs.writeFile('./index.html', $.html(), function(err) { !err && console.log('Set has success: '+ stats.hash) }) }) }); } ] };
效果如图:
能够达到一样的效果,可是stats暂时只能拿到hash值,由于咱们只能考虑在hash上作版本控制,好比咱们能够建hash目录等等
好比有以下场景:咱们用到 Pen 这个模块, 这个模块对依赖一个 window.jQuery, 可我手头的 jQuery 是 CommonJS 语法的,而 Pen 对象又是生成好了绑在全局的, 但是我又须要经过 require('pen') 获取变量。 最终的写法就是作 Shim 处理直接提供支持:
作法一:
{test: require.resolve('jquery'), loader: 'expose?jQuery'}, // 输出jQuery到全局 {test: require.resolve('pen'), loader: 'exports?window.Pen'} // 将Pen做为一个模块引入
作法二:
new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", "window.jQuery": "jquery" })
This plugin makes a module available as variable in every module.
The module is required only if you use the variable.
Example: Make $ and jQuery available in every module without writing require("jquery").
Loader 是支持链式执行的,如处理 sass 文件的 loader,能够由 sass-loader、css-loader、style-loader 组成,由 compiler 对其由右向左执行,第一个 Loader 将会拿到需处理的原内容,上一个 Loader 处理后的结果回传给下一个接着处理,最后的 Loader 将处理后的结果以 String 或 Buffer 的形式返回给 compiler。当然也是但愿每一个 loader 只作该作的事,纯粹的事,而不但愿一箩筐的功能都集成到一个 Loader 中。
官网给出了两种写法:
// Identity loader module.exports = function(source) { return source; };
// Identity loader with SourceMap support module.exports = function(source, map) { this.callback(null, source, map); };
第一种为基础的写法,采用return
返回, 是由于是同步类的 Loader 且返回的内容惟一。若是你写loader有依赖的话,一样的你也能够在头部进行引用,好比:
// Module dependencies. var fs = require("fs"); module.exports = function(source) { return source; };
而第二种则是但愿多个loader
之间链式调用,将上一个loader
返回的结果传递给下一个loader
。
案例
好比我想开发一个es6-loader,专门用来作以.es6
文件名结尾的文件处理,那么咱们能够这么写
// loader/es6-loader.js // 固然若是我这里不想将这个loader所返回的东西传递给下一个laoder,那么我 // 能够在最后直接返回return source // 这里改变以后,我直接能够扔给babel-loader进行处理 module.exports = function(source, map) { // 接收es6结尾文件,进行source改变 source = "console.log('I changed in loader');" // 打印传递进来的参数 console.log("param", this.query); // ... 咱们还能够作一些其余的逻辑处理 this.callback(null, source, map); }; // loader/loader1.es6 let a = 1; console.log(a); // loader/app.js // 向loader中传递参数 require('./es6-loader?param1=p1!./loader1.es6'); document.getElementById("container").textContent = "APP";
执行webpack打包命令,在控制台会打印出param的值,如图:
在执行完成以后,打开index.html
,在控制台打印出“I changed in loader”,而不是1
进阶
能够去阅读如下这篇文章 如何开发一个 Webpack loader
插件基本的结构
插件是能够实例化的对象,在它的prototype上必须绑定一个apply
方法。这个方法会在插件安装的时候被Webpack compiler
进行调用。
function HelloWorldPlugin(options) { // Setup the plugin instance with options... } HelloWorldPlugin.prototype.apply = function(compiler) { compiler.plugin('done', function() { console.log('Hello World!'); }); }; module.exports = HelloWorldPlugin;
安装一个插件,将其添加到配置中的plugins
数组中。
var HelloWorldPlugin = require('hello-world'); var webpackConfig = { // ... config settings here ... plugins: [ new HelloWorldPlugin({options: true}) ] };
执行效果如图:
这里只做简单的引入,日常通常都不须要本身写插件,若是想进一步了解,能够去看官网例子
// 1.全局安装webpack-dev-server cnpm install -g webpack-dev-server // 2. 设置一个文件启动目录,运行 webpack-dev-server --content-base basic/ // 3. 在浏览器输入localhost:8080
// auto-refresh/app.js document.getElementById("container").textContent = "APP APP HOT "; console.log("OK"); // auto-refresh/server.js var webpack = require('webpack'); var config = require('./webpack.config.js'); var WebpackDevServer = require("webpack-dev-server"); var compiler = webpack(config); new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: true, noInfo: false, historyApiFallback: true }).listen(8080, 'localhost', function (err, result) { if (err) { console.log(err); } console.log('Listening at localhost:3000'); }); // auto-refresh/webpack.config.js /** * webpack打包配置文件 */ var webpack = require('webpack'); module.exports = { entry : [ 'webpack-dev-server/client?http://127.0.0.1:8080', // WebpackDevServer host and port 'webpack/hot/only-dev-server', './app.js' ], output : { path : './assets/', filename : '[name].bundle.js', publicPath : './assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'react-hot!babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }), ] }; // auto-refresh/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>basic webpack</title> </head> <body> <div id="container"></div> <script src="./assets/main.bundle.js"></script> </body> </html> // 运行 node server.js // 浏览器输入:localhost:8080
// 1. 安装babel-core、babel-preset-es2015以及babel-loader // 2. 项目根目录下配置.babelrc文件 { "presets": ["es2015"] } // 3. 将webpack.config.js从新命名为webpack.config.babel.js // 4.运行webpack --config webpack.config.babel.js // 说明node 版本5.0以上,babel-core版本6以上须要如此配置
这是一个 Webpack 支持,但文档里彻底没有提到的特性 (应该立刻就会加上)。只要你把配置文件命名成 webpack.config.[loader].js ,Webpack 就会用相应的 loader 去转换一遍配置文件。因此要使用这个方法,你须要安装 babel-loader 和 babel-core 两个包。记住你不须要完整的 babel 包。
其余办法(未成功)
1.在上述的方案中,其实不须要从新命名就能够直接运行webpack,可是今天试了一直不成功 2.{ test : /\.js|jsx$/, loader : 'babel', query: { //添加两个presents 使用这两种presets处理js或者jsx文件 presets: ['es2015', 'react'] } }