webpack是一个基于node.js编写的资源整合打包器(官方原称:MODULE BUNDLER),通过指定入口文件,他能将该入口文件中引用的所有前端资源都合并打包,并最终输出到你指定的输出目录。w
webpack是一个基于node.js编写的资源整合打包器(官方原称:MODULE BUNDLER),通过指定入口文件,他能将该入口文件中引用的所有前端资源都合并打包,并最终输出到你指定的输出目录。webpack进阶比gulp要复杂得多,本文只以从零开始上手使用为指导。
本文所示代码,可通过git@osc的这个项目获取到:http://git.oschina.net/janpoem/webpack-tutorial。
本文中可能在一些细节上表述得不够清晰,完全按照文章的顺序去阅读和执行,可能会碰到一些问题(也是webpack有太多的细节),所以建议先获取到源代码,先执行个npm install,让他慢慢跑,然后一边看, 一边参考着代码来阅读,能更好的帮助理解文章说到的一些东西。
并且如果有碰到本文中执行不下去,出问题的,欢迎提出严厉的批评和指责!
简单的开始
从零开始
以下部分如果你已经了解node.js和npm,请忽略跳过,直接进入《webpack安装》。
首先,你要确保你的电脑安装了node.js,点击这里找到适合你系统的版本下载并安装。并确保你的系统内能正确执行以下命令:
npm -vnode -v
node.js是什么?
Node.js是一个Javascript运行环境(runtime)。实际上它是对Google V8引擎进行了封装。V8引 擎执行Javascript的速度非常快,性能非常好。Node.js对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境下运行得更好。
npm是什么?
NPM的全称是Node Package Manager ,是一个NodeJS包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准。
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近亲系的es6、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。
最后补一张示例代码的最终的效果图: