webpack从零开始

webpack是一个基于node.js编写的资源整合打包器(官方原称:MODULE BUNDLER),经过指定入口文件,他能将该入口文件中引用的全部前端资源都合并打包,并最终输出到你指定的输出目录。webpack进阶比gulp要复杂得多,本文只以从零开始上手使用为指导。javascript

本文所示代码,可经过git@osc的这个项目获取到:http://git.oschina.net/janpoem/webpack-tutorialphp

本文中可能在一些细节上表述得不够清晰,彻底按照文章的顺序去阅读和执行,可能会碰到一些问题(也是webpack有太多的细节),因此建议先获取到源代码,先执行个npm install,让他慢慢跑,而后一边看, 一边参考着代码来阅读,能更好的帮助理解文章说到的一些东西。css

而且若是有碰到本文中执行不下去,出问题的,欢迎提出严厉的批评和指责!html

简单的开始

从零开始

如下部分若是你已经了解node.js和npm,请忽略跳过,直接进入《webpack安装》。前端

首先,你要确保你的电脑安装了node.js,点击这里找到适合你系统的版本下载并安装。并确保你的系统内能正确执行如下命令:java

npm -v
node -v

node.js是什么?node

Node.js是一个Javascript运行环境(runtime)。实际上它是对Google V8引擎进行了封装。V8引 擎执行Javascript的速度很是快,性能很是好。Node.js对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境下运行得更好。react

npm是什么?webpack

NPM的全称是Node Package Manager ,是一个NodeJS包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准。git

webpack安装

若是你对webpack的基本安装和使用已经十分了解,并已经安装,能够跳过本章节,并直接进入《webpack进阶》章节。

接下来你须要在全局环境内安装webpack和webpack-dev-server,固然后者并非必须的,但仍是强烈推荐你安装。

npm install webpack webpack-dev-server -g

初次使用

随便创建一个测试的目录,好比:webpack-first,用命令行进入,或者webstorm/phpstorm/atom等打开这个目录。

在目录中添加一个文件:webpack.config.js

module.exports = {
	entry: [
		"./main.js"
	],
	output: {
		path: './output',
		filename: 'app.js'
	}
};

在这个文件中,咱们声明了一个入口文件,为当前目录下的main.js,而且输出目录的基础路径为当前目录下的output,输出的文件名为app.js

接着,咱们往下添加test.js和main.js文件,main.js如上所述,为项目的入口文件,test.js为须要引入在main.js中的模块。

test.js

module.exports = [
	'a', 'b', 'c'
];

main.js

// 引入test.js文件,并将其输出的内容(module.exports)赋值到test变量上
var test = require('./test');
// 在浏览器的console中输出test变量的内容
console.log(test);

进入命令行模式(若是是phpstorm/webstorm能够打开他的Terminal工具),输入指令:webpack,会看到他执行的结果:

该命令执行完,webpack会在你的项目内添加一个output的目录,打开这个目录,你会看到根据你的webpack.config.js配置,他生成了一个app.js文件。

这里特别说明一下,若是你仅仅只是须要用webpack来打包最原始的js文件,是不须要在这个项目内安装webpack和webpack-dev-server的,他会使用你全局(就是刚才npm install -g的)安装的版本,这点比gulp好多了。

使用webpack-dev-server

webpack-dev-server,是webpack提供的一个插件,他提供了一个http服务器环境,给你实时预览打包合并的结果。

特别提早说明的是,使用webpack-dev-server指令,你必须指定--content-base指令,--content-base指令,用于指定http服务器的document root目录。

在刚才的项目根目录中,执行如下命令:webpack-dev-server --content-base ./public,咱们以当前目录下的public目录做为http服务器的根目录:

特别说明:在webpack.config.js中指定的output目录和--content-base并无必然的关系,output指定的是你输出的路径。

在项目中添加public目录,并添加一个用于测试的文件:public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>webpack first sample</title>
</head>
<body>
<!--引入output中的app.js文件-->
<script type="text/javascript" src="app.js"></script>
</body>
</html>

访问http://localhost:8080/,并打开你的浏览器调试工具,你将能看到在main.js中输出的信息。

除此以外,你还能够访问:http://localhost:8080/webpack-dev-server/,这里提供了一个基于iframe的方式加载index.html的页面,而且当你每次修改完js文件时,他都会自动刷新这个页面以实时预览。

webpack进阶

上述能够做为一个基本的上手说明,这里要说的,就是一些比较实际——和项目实际关联的内容。

要使用进阶功能,就须要在项目内安装webpack和webpack-dev-server了,若是你还没执行npm init,能够先执行一次npm init,他会须要你回答一些问题,并生成一个与当前项目相关的package.json文件。package.json文件,能有效的管理你的node.js项目的版本,已经依赖库,执行脚本,等信息,尤为当你将项目提交到代码仓库,别人也须要执行你的项目时,经过package.json文件,可以很好的克服不一样的系统环境下,缺失的依赖类库。

npm init执行之后,请执行下面的命令:

npm install webpack webpack-dev-server --save-dev

这个指令的含义是,在当前项目内安装最新版本的webpack和webpack-dev-server。

--save-dev参数,用于告诉npm这两个类库的信息须要写入到package.json中,并做为开发依赖库。npm还有一个--save指令,与--save-dev相似,但他关联的是直接依赖。--save-dev和--save,决定了别人在引用你的项目时,你项目依赖库是否也安装在对方的node_modules目录中,这个详细就不在这里展开了。

执行完这两个命令,打开你的package.json文件,你会看到以下内容:

npm命令须要访问国外的npm库,若是你感受你当前的网络环境出国外不是那么稳定,能够考虑使用cnpm,点击这里看教程,这里就再也不介绍了。

webpack-dev-server命令行参数

在webpack-dev-server包含了一些比较经常使用的参数,能够大大提升咱们的开发效率,这里介绍几个最经常使用的:

--port <端口号>: 指定http服务的端口号

--host <主机名>: 指定http服务的主机名,这在局域网内使用实时调试很是有用。

--compress: 启用gzip压缩

--inline: 将webpack-dev-server的运行时文件合并打包到输出的文件中

--hot: 使用HotModuleReplacementPlugin插件(已经整合在webpack中,无需npm安装),并将http服务器切换到hot模式,其实所谓hot模式,就是当你更改了某个被引用的文件,会hot replace,并从新合并到输出文件中。

通常来讲,--hot --inline会合并使用,这个方式会合并将webpack/hot/dev-server打包到输出文件中。这个webpack/hot/dev-server,实际上就是这个http服务器的hot replace的核心逻辑,而这个东西,其实在你就是你当前项目的node_modules/webpack/hot/dev-server.js文件。打开node_modules/webpack/hot/目录,实际上你会看到这里还有其余的几种模式,好比only-dev-server等。事实上你是能够显式的将这些文件配置在你的webpack.config.js文件中里的,好比:

module.exports = {
	entry: [
		"webpack/hot/only-dev-server",
		"./main.js"
	],
	output: {
		path: './output',
		filename: 'app.js'
	}
};

当你指定了only-dev-server之后,执行:webpack-dev-server --content-base ./public --hot时,注意输出的变化:

而若是我把webpack.config.js中的webpack/hot/only-dev-server注释掉,再执行:webpack-dev-server --content-base ./public --hot --inline

这是官方文档很是隐晦的地方,没有明确说明的地方。

时空门:webpack-dev-server官方说明

而webpack自己比较经常使用的参数有:

--devtool : 调试工具的模式,eval是将你的css和js代码变为eval的方式合并打包。

--config : 指定配置文件

--progress: 在命令行终端输出编译合并的过程信息

--colors: 在命令行终端中显示带颜色的信息

更多更详细的信息,能够看这里,webpack CLI模式的说明

webpack还提供了一个经过你本身书写代码来启动http服务的功能,这个应该说是最高阶的内容了,本文就不讨论了,有兴趣的能够去官方文档看:webpack-dev-server,这篇文档的下半部分,就是彻底由你本身实现一个http服务的简单教程。详细的能够去看webpack node.js API

除此以外,这篇文章是你使用webpack之前,必须阅读的:webpack config配置参数说明,每一个项目有自身的特殊性,本文只是但愿能将一些基本经常使用的东西能作一个涵盖,因此彻底靠本文的内容,确定没法解决每一个项目实际碰到的问题,因此熟读这个配置文档,应该是最起码的要求。

webpack loaders

在实用层面的第二个问题就是各类loaders,这里分两个部分,一个是js部分,包括如js近亲系的es六、jsx、babel tranplier、coffee等,第二部分为CSS以及其近亲,如less、stylus,同时包括其余前端的静态资源的引用问题。

这里是webpack loaders的清单

加载JS部分

基于webpack打包,你能够比以往过去更加放心大胆的使用各类奇葩,前所未见的各类脚本语言——固然这里的前提是你能本身处理好他们的转译问题。而目前babel已经提供了大多数常见的语言、css、html模板的loaders,不过根据我实际经验,webpack主要的优点仍是在于处理js的合并打包。

如下就以es6和jsx为例(使用babel-loader):

npm install babel-loader babel-preset-es2015 babel-preset-react --save-dev

若是你须要使用到babel的一些插件,也须要经过npm来进行安装,关于babel如何使用的问题,能够参考我写的这篇文章:《Babel指南 - 基本环境搭建》。

npm install babel-plugin-transform-class-properties babel-plugin-transform-es2015-block-scoping babel-plugin-transform-es2015-computed-properties --save-dev

在webpack.config.js中,咱们修改以下配置:

module.exports = {
	entry: [
		"./main.js"
	],
	output: {
		path: './output',
		filename: 'app.js'
	},
	module: {
		loaders: [
			{
				test: /\.(es6|jsx)?$/,
				exclude: /(node_modules|bower_components)/,
				loader: 'babel', // 'babel-loader' is also a legal name to reference
				query: {
					presets: ['es2015', 'react'],
					plugins: [
						"transform-es2015-block-scoping",
						"transform-class-properties",
						"transform-es2015-computed-properties"
					]
				}
			}
		]
	}
};

其实也很简单,经过这样,你就能够随意的使用es6和jsx了,好比咱们添加一个例子hello_world.jsx:

注意,要使用到react,你仍是须要先安装:

npm install react react-dom --save-dev

hello_world.jsx代码以下

var React = require('react');

class HelloWorld extends React.Component {

	constructor(props) {
		super(props);
		this.state = {
			url: '',
			value: 'http://tool.oschina.net/'
		};
	}

	getUrl() {
		let url = this.state.value;
		if (/^\/\//.test(url)) {
			url = 'http:' + url;
		}
		else if (!/^https?\:\/\//i.test(url)) {
			url = 'http://' + url;
		}
		return url;
	}

	changeUrl(value) {
		this.setState({value: value});
	}

	goUrl(value, event) {
		if (event && event.preventDefault)
			event.preventDefault();
		this.setState({url: value});
	}

	renderFrame() {
		if (this.state.url) {
			return
		}
	}

	componentDidMount() {
		this.goUrl(this.getUrl());
	}

	render() {
		return <div>
			<h1>Hello world!!</h1>
			<form onSubmit={(e) => this.goUrl(this.getUrl(), e)}>
				<input type="text" value={this.state.value} onChange={(e) => this.changeUrl(e.target.value)}/>
				<button>Hello world</button>
			</form>
			{this.renderFrame()}
		</div>;
	}
}

module.exports = HelloWorld;

接着修改main.js:

var test = require('./test');
var ReactDOM = require('react-dom');
var React = require('react');
var HelloWorld = require('./hello_world.jsx');

var doc = document, body = doc.body;

body.onload = function() {
	var el = doc.createElement('div');
	el.style.opacity = 0;
	el.style.marginTop = '-100px';
	el.style.transitionProperty = 'opacity margin-top';
	el.style.transitionDuration = '800ms';
	el.style.transitionTimingFunction = 'cubic-bezier(0.65,-0.1, 0.24, 1.47)';
	body.appendChild(el);
	ReactDOM.render(React.createElement(HelloWorld), el);
	setTimeout(function() {
		el.style.opacity = 1;
		el.style.marginTop = 0;
	}, 1);
};

访问http://localhost:8080/就会看到:

加载CSS部分

要在webpack中引入css文件,须要简单的作一个梳理。

JS中引入CSS文件

经过css-loader,是能够将一个css文件用require函数,在代码中被引用,而且返回这个css中样式定义的文字内容。固然首先你必须安装css-loader:

npm install css-loader --save-dev

而后咱们就能够在js中引入css

var css = require('css!./style.css')

但这样,只是将css的内容引入,并无加载到页面上,因此咱们须要修改main.js:

var test = require('./test');
var ReactDOM = require('react-dom');
var React = require('react');
var HelloWorld = require('./hello_world.jsx');
var css = require('css!./style.css');

var doc = document, body = doc.body;

body.onload = function() {
	// 在head中把样式加载
	var style = doc.createElement('style');
	style.innerText = css.toString();
	doc.head.appendChild(style);

	var el = doc.createElement('div');
	el.style.opacity = 0;
	el.style.marginTop = '-100px';
	el.style.transitionProperty = 'opacity margin-top';
	el.style.transitionDuration = '800ms';
	el.style.transitionTimingFunction = 'cubic-bezier(0.65,-0.1, 0.24, 1.47)';
	body.appendChild(el);
	ReactDOM.render(React.createElement(HelloWorld), el);
	setTimeout(function() {
		el.style.opacity = 1;
		el.style.marginTop = 0;
	}, 1);
};

这样,咱们会看到http://localhost:8080/已经加载到style.css的样式了。关于css-loader的详细用法,能够去css-loader的github看看。

注意,目前咱们还没去修改webpack.config.js。

style-loader

style-loader:能够将一个已经输出的内容变为一个style的DOM标签输出。安装:

npm install style-loader --save-dev

这时候去掉刚才在body.onload中增长的在head加入输出css的代码,修改main.js以下:

var test = require('./test');
var ReactDOM = require('react-dom');
var React = require('react');
var HelloWorld = require('./hello_world.jsx');
var css = require('style!css!./style.css');

var doc = document, body = doc.body;

body.onload = function() {
	var el = doc.createElement('div');
	el.style.opacity = 0;
	el.style.marginTop = '-100px';
	el.style.transitionProperty = 'opacity margin-top';
	el.style.transitionDuration = '800ms';
	el.style.transitionTimingFunction = 'cubic-bezier(0.65,-0.1, 0.24, 1.47)';
	body.appendChild(el);
	ReactDOM.render(React.createElement(HelloWorld), el);
	setTimeout(function() {
		el.style.opacity = 1;
		el.style.marginTop = 0;
	}, 1);
};

注意:style!css!./style.css。这时候获得的效果,和刚才是同样的。

为何要不厌其烦的将css和style的loader拆开两个章节来介绍呢,由于全部less、stylus实际上都是最终都是基于这个机制在运做的。

加载图片、字体、svg等资源

若是须要在样式中加载图片,那么就须要url-loader,而如字体,则须要使用file-loader,仍是安装:

npm install url-loader file-loader --save-dev

注意,这里就是webpack和gulp最大的区别,webpack中,只要在你的源码中存在被引用的资源,你都须要说明这些资源须要被如何加载。事实上,webpack还有如base64-loader,你也彻底能够将一个图片做为base64-loader来处理。固然这里咱们就以url-loader处理。

使用不一样的loader,将决定了合并打包后的处理方式,若是使用base64-loader,他固然会将图片的内容打包成base64编码合并在js中。而url-loader,则会在输出的目录生成对应的文件(只有本地文件,会输出到output目录下)。

修改webpack.config.js:

module.exports = {
	entry: [
		"./main.js"
	],
	output: {
		path: './output',
		filename: 'app.js'
	},
	module: {
		loaders: [
			{
				test: /\.(es6|jsx)?$/,
				exclude: /(node_modules|bower_components)/,
				loader: 'babel', // 'babel-loader' is also a legal name to reference
				query: {
					presets: ['es2015', 'react'],
					plugins: [
						"transform-es2015-block-scoping",
						"transform-class-properties",
						"transform-es2015-computed-properties"
					]
				}
			},
			{
				test: /\.(png|jpg|jpeg|gif|(woff|woff2)?(\?v=[0-9]\.[0-9]\.[0-9])?)$/,
				loader: 'url-loader?limit=1000'
			},
			{
				test: /\.(ttf|eot|svg)(\?[\s\S]+)?$/,
				loader: 'file'
			}
		]
	}
};

而后重启webpack-dev-server,你就能看到图片、svg等资源已经能正确的加载。

关于合并打包,咱们最后会说到。

加载CSS近亲

这里以stylus为例,其余less、scss等的也都是同理的。

npm install stylus stylus-loader --save-dev

一样的,你只要使用require便可在页面引入styl文件:

require('style!css!stylus!./hello.styl');

若是你以为使用style!css!stylus!的方式加载文件,过于怪异,那么你能够修改webpack.config.js文件,增长两个loader:

{
	test: /\.styl$/,
	loader: "style!css!stylus"
},
{
	test: /\.css$/,
	loader: "style!css"
}

这样,你就可使用顺眼一点的方式来加载:

require('./style.css');
require('./hello.styl');

最终打包

到此,咱们已经完成了所有的编码。接下来就要将内容打包输出了,这时候你只要在项目的根目录下执行一下webpack便可。

这时候咱们会看到,在output目录下,他输出了一个app.js和svg文件。而本例子中的两个css文件,他所有合并到打包到了app.js文件中。

若是你但愿将css文件从js文件中分离出来,须要一个额外的插件:

npm install extract-text-webpack-plugin --save-dev

这个插件能够指定拦截特定加载器输出的文本内容,并最终合并输出到你指定的文件上去,但要特别注意,使用这个插件后,webpack-dev-server自动输出样式调试就会由于这个插件而失效,因此建议构建的时候,使用单独的配置文件,好比添加一个webpack.build.js,使用指令:

webpack --config webpack.build.js

webpack.build.js内容以下:

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
	entry: [
		"./main.js"
	],
	output: {
		path: './output',
		filename: 'app.js'
	},
	module: {
		loaders: [
			{
				test: /\.(es6|jsx)?$/,
				exclude: /(node_modules|bower_components)/,
				loader: 'babel',
				query: {
					presets: ['es2015', 'react'],
					plugins: [
						"transform-es2015-block-scoping",
						"transform-class-properties",
						"transform-es2015-computed-properties"
					]
				}
			},
			{
				test: /\.(png|jpg|jpeg|gif|(woff|woff2)?(\?v=[0-9]\.[0-9]\.[0-9])?)$/,
				loader: 'url-loader?limit=1000'
			},
			{
				test: /\.(ttf|eot|svg)(\?[\s\S]+)?$/,
				loader: 'file'
			},
			{
				test: /\.styl$/,
				// loader: "style!css!stylus"
				loader: ExtractTextPlugin.extract('style', 'css!stylus')
			},
			{
				test: /\.css$/,
				// loader: "style!css"
				loader: ExtractTextPlugin.extract('style', 'css')
			}
		]
	},
	plugins: [
		new ExtractTextPlugin('app.css')
	]
};

此次就能将css文件分离出来了。

webpack还有不少可调节、可优化的配置,可是碍于篇幅,在这里就再也不详细展开了。可能有一些细节的地方,本文没有介绍的很清楚,也请多多见谅。

本文所示代码,可经过git@osc的这个项目获取到:http://git.oschina.net/janpoem/webpack-tutorial

最后补一张示例代码的最终的效果图: