Webpack最详解
WEB前端开发社区 昨天
什么是模块打包工具
<script>标签加载。<script>标签,可是这么做有问题:你必须保证文件加载的顺序没错,包括知道哪些文件依赖另外一些文件,以及不包含不需要的文件。 多个 <script>标签意味着对服务器发送多次请求,性能会受影响。显然,这些工作量都靠你纯手工完成,计算机一点也帮不上你。
为什么用Webpack?
相对新,因此具有后发优势,能绕过或避免先驱们的缺陷和问题。 上手容易。如果你只想把一堆JavaScript文件打包到一起,并不想要其他花哨的东西,那你甚至都不需要配置文件。 它的插件系统让它能干的事多了去了,因此它也变得非常强大。这样一来,你可能只要用它这一个构建工具就行了。
安装webpack
npm init初始化项目。初始化过程中输入什么信息不重要,除非你打算把它发布到npm上。package.json文件(npm init创建的),就可以在里面声明依赖了下面我们使用npm把webpack安装为一个依赖,命令是npm install webpack -D。(-D的意思是在package.json中把它保存为开发依赖;也可以使用--save-dev。)npm install lodash -S(-S与--save一样)。之后,创建一个目录src,再在里面创建一个文件main.js,内容如下:var map = require('lodash/map');function square(n) {return n*n;}console.log(map([1,2,3,4,5,6], square));
map创建了一个新数组,其中每个元素都是原始数组中对应值的平方。最后,把新数据输出到控制台这个文件都可以通过Node.js运行,试试node src/main.js,结果你会看到输出:[ 1,4,9,16,25,36 ]。使用webpack命令行
src/main.js,输出路径是dist/bundle.js。下面我们就创建一个npm脚本来做这件事(因为没有全局安装webpack,所以不能直接在命令行里运行它)。在package.json中,修改"scripts"如下:… "scripts": { "build": "webpack src/main.js dist/bundle.js", }…npm run build,webpack就帮你干活了。完事以后,花不了多长时间,应该会有一个新的dist/bundle.js文件。这时可以通过Node.js运行它(node dist/bundle.js),也可以在浏览器里通过简单的HTML运行它,可以在控制台看到相同的输出。dist目录及其内容,另外再添加几条执行打包文件的脚本。为此,先得安装del-cli,以便删除文件里不用麻烦跟我们使用不同操作系统的人(我用Windows,不要鄙视我):npm install del-cli -D。然后,把npm脚本改成这样:… "scripts": { "prebuild": "del-cli dist -f", "build": "webpack src/main.js dist/bundle.js", "execute": "node dist/bundle.js", "start": "npm run build -s && npm run execute -s" }…"build"跟以前一样,但现在有了预先清理的"prebuild",它会在每次运行"build"之前自动运行。而且有了"execute",它会调用Node.js执行打包后的脚本。关键是我们可以通过"start"这一个命令完成所有操作(其中的)-s选项是不让npm脚本在控制台输出太多没用的信息)。来,跑一下npm start。应该能在控制台看到webpack的输入,紧跟着平方之后的数组。恭喜!你已经完成了前面提到的仓库中example1分支的所有练习。使用配置文件
webpack.config.js的新文件。这个名字的文件是webpack默认会找的。如果你想给配置文件起个不一样的名字,或者想把它放在别的目录下,可以给webpack传递--config [filename]选项。module.exports = { entry: './src/main.js', output: { path: './dist', filename: 'bundle.js' }};module.exports。似乎并不比在命令行中指定这些选项好多少,但看完这篇文章,你就会觉得还是把选项都放在这里好。package.json文件中传递给webpack的选项都删掉(在build中)。删掉选项的scripts对象如下所示:… "scripts": { "prebuild": "del-cli dist -f", "build": "webpack", "execute": "node dist/bundle.js", "start": "npm run build -s && npm run execute -s" }…npm start,结果应该一样!这就是example2分支的练习了。使用加载器
.js扩展名的文件都要通过ESLint检查,然后再通过Babel将它们由ES2015编译为ES5。如果ESLint遇到警告,那警告信息会输出到控制台,如果遇到了错误,则会阻止webpack继续执行后续操作。main.js文件中的代码改成这样:import { map } from 'lodash';console.log(map([1,2,3,4,5,6], n => n*n));
square函数改写成了箭头函数,(2)使用ES2015的import把'lodash'加载为map。这样其实会把整个Lodash文件都打包到我们的输出文件里,而不像前面使用'lodash/map'一样只导入map。你可以自己把第一行改成import map from 'lodash/map',但我这样做有几个理由:在大型应用中,应该会用到Lodash库中的大部分代码,因此最终可能还是要全部加载它。 如果你使用Backbone.js,很难单独加载你需要的所有功能,因为没办法搞清楚你需要多少 在webpack的下一个主版本中,开发者打算支持所谓的“摇树”(tree-shaking)功能,可用来清除模块中用不着的代码。因此,无论如何结果都一样。 我想用它作为例子来说明我想强调的重点
npm i -D babel-core babel-loader babel-preset-es2015 babel-plugin-transform-runtime babel-polyfill。接着配置webpack来使用它们。首先,需要一个地方添加加载器。好,把webpack.config.js修改成这样:module.exports = { entry: './src/main.js', output: { path: './dist', filename: 'bundle.js' }, module: { rules: [ … ] }};module,其中是rules属性,是一个数组,保存每一个要用到的加载器的配置。我们就在这里添加babel-loader。对每一个加载器,至少要配置两个选项:test和loader。test一般是一个正则表达式,用于测试每个文件的绝对路径。多数情况下,这里的正则表达式都是用于检测文件的扩展名,比如/\.js$/检测文件名是不是以.js结尾。对我们而言,这里使用/\.jsx?$/,既匹配.js,又匹配.jsx,万一你要在应用里使用React呢。然后需要指定loader,也就是指定对通过test检测的文件应用什么加载器。'babel-loader!eslint-loader'。webpack从右向左读取这个字符串,因此会先运行eslint-loader,后运行babel-loader。如果要给某个加载器指定行列,可以使用查询字符串语法。比如,要给Babel设置值为true的fakeoption选项,可以把前面的字符串修改为'babel-loader?fakeoption=true!eslint-loader。此外,也可以用use代替loader,这样就可以传入一个加载器数组,相对更方便阅读和维护。比如,前面的字符串改成数组就是use:['babel-loader?fakeoption=true','eslint-loader']。…rules: [ { test: /\.jsx?$/, loader: 'babel-loader' }]…options对象,也就是一个键-值对。以fakeoption选项为例,就是这样写:…rules: [ { test: /\.jsx?$/, loader: 'babel-loader', options: { fakeoption: true } }]……rules: [ { test: /\.jsx?$/, loader: 'babel-loader', options: { plugins: ['transform-runtime'], presets: ['es2015'] } }]…presets选项,以便把ES2015代码转换成ES5代码。此外还设置了使用我们安装的transform-runtime插件。前面提到过,这个插件并不是必需的,这里只是为了说明怎么使用插件。当然,也可以使用.babelrc文件来设置这些选项,但那样一来我就没法向大家展示怎么在webpack里设置这些选项了。一般来说,我推荐使用.babelrc,但对于当前项目,我们还是使用配置文件。node_modules文件夹中的文件,这样可以加快打包的过程。可以添加一个exclude属性,指定要排除的文件夹。exclude的值应该是一个正则表达式,因此这里设置为/node_modules/。…rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/, options: { plugins: ['transform-runtime'], presets: ['es2015'] } }]…include属性,明确指定只处理src目录中的文件,这里我们就不演示了。配置完了以后,应该就可以运行npm start并得到能在浏览器中运行的ES5代码了。如果你又不想使用transform-runtime插件,想使用腻子脚本了,那就要修改一两个地方。首先,删除包含plugins:['transform-runtime]的那一行代码(如果以后也不会再使用这个插件,可以使用npm删除它)。然后,把webpack配置文件的entry部分修改如下:entry: [ 'babel-polyfill', './src/main.js'],
src/main.js中添加import 'babel-polyfill;,结果与使用前面的配置一样。这里通过webpack配置文件的entry项指定腻子脚本,也是考虑能方便后面的例子,另外也可以展示怎么把多个入口文件打包到一个文件。不管怎样,这就是example3分支的内容。同样,运行npm start可以验证没有问题。使用Handlebars加载器
npm install -D handlebars-loader。不过,要使用它,必须还要安装Handlebars:npm install -D handlebars。这样就做到了加载器与其自身分离,可以自由控制Handlebars的版本。两者可以独立进化。src目录中创建一个新文件numberlist.hbs,包含如下内容:<ul> {{#each numbers as |number i|}} <li>{{number}}</li> {{/each}}</ul>main.js文件应该是这样的:import { map } from 'lodash';import template from './numberlist.hbs';let numbers = map([1,2,3,4,5,6], n => n*n);console.log(template({numbers}));
numberlist.hbs不是JavaScript文件,webpack不知道怎么导入它有一个办法,就是在import语句中告诉webpack使用Handlebars加载器:import { map } from 'lodash';import template from 'handlebars-loader!./numberlist.hbs';let numbers = map([1,2,3,4,5,6], n => n*n);console.log(template({numbers}));
handlebars!。下面来修改配置:…rules: [ {/* babel loader config… */}, { test: /\.hbs$/, loader: 'handlebars-loader' }]…example4分支的练习。运行npm start,就可以看到webpack打包后的输出,结果如下:<ul> <li>1</li> <li>4</li> <li>9</li> <li>16</li> <li>25</li> <li>36</li></ul>
使用插件
npm i -D http-server。接着,修改execute为server,并相应地修改start:…"scripts": { "prebuild": "del-cli dist -f", "build": "webpack", "server": "http-server ./dist", "start": "npm run build -s && npm run server -s"},…npm start会启动一个Web服务器,此时可以打开localhost:8080看到页面。当然,我们需要通过插件创建这个页面,下面就来安装插件。安装插件:npm i -D html-webpack-plugin。webpack.config.js:var HtmlwebpackPlugin = require('html-webpack-plugin');module.exports = {entry: ['babel-polyfill','./src/main.js'],output: {path: './dist',filename: 'bundle.js'},module: {rules: [{test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,options: { plugins: ['transform-runtime'], presets: ['es2015'] }},{ test: /\.hbs$/, loader: 'handlebars-loader' }]},plugins: [new HtmlwebpackPlugin()]};
plugins属性,传入了一个插件的实例。npm start,然后在浏览器中查看URL,只能看到一个空白页。但是如果你看开发者工具,能在控制台中看到输入的HTMLsrc目录下创建一个index.html文件,这就是我们的模板。默认使用EJS模板,当然你可以配置成webpack支持的任何模板语言。我们使用默认的EJS,因为都差不多。这就是模板的内容:<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title><%= htmlWebpackPlugin.options.title %></title></head><body> <h2>This is my Index.html Template</h2> <div id="app-container"></div></body></html>
我们使用了传递给插件的选项来定义页面标题(仅仅因为我们可以这么做)。 没有指定应该在哪里加载脚本。因为插件默认会把脚本添加到 body标签最后。随便放一个带 id属性的div。接下来要用到它。
main.js,把它原来输出到控制台的HTML添加到div里。为此,只要把main.js的最后一行修改成这样:document.getElementById("app-container").innerHTML = template({numbers});。var HtmlwebpackPlugin = require('html-webpack-plugin');module.exports = {entry: ['babel-polyfill','./src/main.js'],output: {path: './dist',filename: 'bundle.js'},module: {rules: [{test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,options: { plugins: ['transform-runtime'], presets: ['es2015'] }},{ test: /\.hbs$/, loader: 'handlebars-loader' }]},plugins: [new HtmlwebpackPlugin({title: 'Intro to webpack',template: 'src/index.html'})]};
template选项指定在哪里找到模板,而title是传入模板的值。好,现在运行npm start,会在浏览器中看到这样的结果:
example5分支的练习结束了。不同的插件有不同的选项和配置,毕竟插件多,能干的事也多,但终归都是通过webpack.config.js文件的plugins数组添加的。说到通过文件名生成和填充HTML,其实有很多其他方式,而如果你在打包文件名之后添加散列值以强制清除缓存,那这些方式都很方便。example6分支,我在里面添加了一个JavaScript压缩插件。但除非你不想用UglifyJS,否则那并非必要的。如果你不喜欢默认的UglifyJS,看看这个分支的代码(只要看webpack.config.js就行了),就知道怎么使用相关插件并配置它了如果你不想修改默认配置,那么只要在运行webpack命令时传入-p参数就可以。这个参数代表“production”,等价于--optimize-minimize和--optimize-occurence-order参数。前者表示压缩JavaScript文件,后者表示优化模块在最终文件中的次序。结果可以让文件更小,运行更快。因为这个分支是先做完的,后来我才研究的-p选项,因此我打算还保留使用UglifyJS,但是再告诉你一个更简单的方式。另一个快捷选项是-d,可以让webpack输出更多调试信息,而且不需要额外配置生成源码地图。更多快捷选项可以参考这里。延迟加载代码
require.ensure(["module-a", "module-b"], function(require) { var a = require("module-a"); var b = require("module-b"); // …});require.ensure确保模块可用(但不执行),接收一个模块名的数组和一个回调。要在回调里使用模块,需要引用作为参数传入的require来请求。require(["module-a", "module-b"], function(a, b) { // …});require,接收一个模块依赖数组和一个回调。回调的参数是每个依赖的引用,顺序也是数组中的顺序。System.import,使用promise而非回调。我觉得这个改进有用,虽然把回调包到promise里要费点事。不过,有一点要注意,由于新规范中引入了import(),System.import已经被废弃。另外,Babel(还有TypeScript)会在你使用它时抛出语法错误。可以使用babel-plugin-dynamic-import-webpack,但这会把它转换成require.ensure,而不是帮Babel把新的import函数当成合法的而不去管它,从而让webpack来处理它。我觉得AMD或require.ensure短时间内不会消失,而System.import也将被支持到第3版本。这个时间已经足够长了,所以还是觉得怎么方便就怎么来吧。import语句,然后把最后一行包在一个setTimeout和一个AMD语法 的require调用中:import { map } from 'lodash';let numbers = map([1,2,3,4,5,6], n => n*n);setTimeout( () => {require(['./numberlist.hbs'], template => {document.getElementById("app-container").innerHTML = template({numbers});})}, 2000);
npm start,后生成另一文件,类似1.bundle.js。打开浏览器,并打开开发者工具的Network,可以看到2秒后,会加载并执行这个新文件朋友,这种延迟加载当然不难实现,但却可以显著减少首次加载的文件大小,让用户体验变得更好。创建第三方文件
CommonsChunkPlugin。因为这个插件已经内置了,所以不需要安装,只要修改webpack.config.js:var HtmlwebpackPlugin = require('html-webpack-plugin');var UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');module.exports = {entry: {vendor: ['babel-polyfill', 'lodash'],main: './src/main.js'},output: {path: './dist',filename: 'bundle.js'},module: {rules: [{test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/,options: { plugins: ['transform-runtime'], presets: ['es2015'] }},{ test: /\.hbs$/, loader: 'handlebars-loader' }]},plugins: [new HtmlwebpackPlugin({title: 'Intro to webpack',template: 'src/index.html'}),new UglifyJsPlugin({beautify: false,mangle: { screw_ie8 : true },compress: { screw_ie8: true, warnings: false },comments: false}),new CommonsChunkPlugin({name: "vendor",filename: "vendor.bundle.js"})]};
entry改成对象字面量,指定多个入口。其中vendor属性指定最终会被打包到第三方文件中的依赖,包括一个腻子脚本和Lodash,同时将主入口文件写在main属性中。接下来把CommonsChunkPlugin的实例添加到plugins对象上,并指定基于“vendor”单独打包,以及保存第三方代码的文件名vendor.bundle.js。bundle.js、1.bundle.js和vendor.bundle.js。运行npm start可以在浏览器中看到你期望的结果。webpack甚至会把它自己处理不同模块加载的主要代码都打包到这个第三方文件中,这是必要的。example8分支的练习结束,我们的教程也结束了这篇文章讲了不少东西,但这只是webpack用途的冰山一角。通过Webpack可以使用CSS modules、cache-busting散列、图片优化,等等就算一本厚厚的书都写不完。我不可能都讲到,否则在我写完这本书的时候,多数内容估计都过时了!所以,还是现在就试试webpack吧,看看能不能帮你提高工作效率。老天保佑,编码愉快!不看的原因确定内容质量低不看此公众号
赞 (0)
