Webpack或Browserify&Gulp(哪个更好())

本文概述

  • Gulp快速入门
  • Webpack适用于何处?
  • 为什么开发人员使用Webpack而不是Gulp?
  • 我们的3个任务运行器设置
  • 通用密码
  • Gulp + Browserify设置
  • Gulp + Webpack设置
  • Webpack + NPM脚本设置
  • 总结
随着Web应用程序变得越来越复杂, 使Web应用程序具有可伸缩性变得至关重要。过去编写临时JavaScript和jQuery就足够了, 但是如今构建Web应用程序需要更严格的纪律和正式的软件开发实践, 例如:
  • 单元测试以确保对代码的修改不会破坏现有功能
  • 整理以确保一致的编码样式而没有错误
  • 与开发版本不同的生产版本
该网络还提供了一些独特的开发挑战。例如, 由于网页发出大量异步请求, 因此你的Web应用程序的性能会因必须请求数百个JS和CSS文件而大大降低, 而每个JS和CSS文件都有自己的小开销(标头, 握手等)。通常可以通过将文件捆绑在一起来解决此特定问题, 因此, 你只需要一个捆绑的JS和CSS文件, 而不是数百个单独的文件。
Webpack或Browserify&Gulp(哪个更好())

文章图片
你应该使用哪种捆绑工具:Webpack或Browserify + Gulp?这是选择指南。
鸣叫
使用可编译为原生JS和CSS的语言预处理器(如SASS和JSX)以及诸如Babel之类的JS编译器以从ES6代码中受益, 同时保持ES5兼容性也很常见。
这相当于大量的任务, 这些任务与编写Web应用程序本身的逻辑无关。这是任务运行程序进来的地方。任务运行程序的目的是使所有这些任务自动化, 以便你可以在专注于编写应用程序的同时受益于增强的开发环境。配置任务运行器后, 你要做的就是在终端中调用单个命令。
我将Gulp用作任务运行程序, 因为它对开发人员非常友好, 易学且易于理解。
Gulp快速入门 Gulp的API包含四个功能:
  • gulp.src
  • 吞食
  • gulp.task
  • gulp.watch
Webpack或Browserify&Gulp(哪个更好())

文章图片
例如, 这里是一个示例任务, 它利用了这四个功能中的三个:
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });

执行my-first-task时, 与glob模式/public/js/**/*.js匹配的所有文件都将被压缩, 然后传输到构建文件夹。
它的优点在于.pipe()链接。你将获取一组输入文件, 通过一系列转换将它们通过管道传输, 然后返回输出文件。为了使事情更加方便, 实际的管道转换(例如minify())通常由NPM库完成。因此, 在实践中, 除了重命名管道中的文件外, 你还需要编写自己的转换, 这种情况很少见。
了解Gulp的下一步是了解任务依赖项数组。
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });

【Webpack或Browserify&Gulp(哪个更好())】在这里, my-second-task仅在lint和bundle任务完成后才运行回调函数。这样可以将关注点分离:你可以创建一系列具有单个职责的小型任务, 例如将LESS转换为CSS, 并创建一种主任务, 该主任务只需通过任务依赖项数组调用所有其他任务。
最后, 我们有gulp.watch, 它监视glob文件模式中的更改, 并在检测到更改时运行一系列任务。
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })

在上面的示例中, 对与/public/js/**/*.js匹配的文件的任何更改都将触发lint和reload任务。 gulp.watch的常见用法是触发浏览器中的实时重载, 此功能对开发非常有用, 一旦你体验了它, 就无法生存。
就像那样, 你了解了有关gulp的所有实际知识。
Webpack适用于何处?
Webpack或Browserify&Gulp(哪个更好())

文章图片
使用CommonJS模式时, 捆绑JavaScript文件并不像将它们串联起来那样简单。相反, 你有一个入口点(通常称为index.js或app.js), 在文件顶部带有一系列require或import语句:
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');

ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2';

在app.js中的其余代码之前, 必须先解决依赖关系, 并且这些依赖关系本身可能还要解析其他依赖关系。此外, 你可能需要在应用程序的多个位置使用相同的依赖项, 但是你只想解决一次该依赖项。可以想像, 一旦你的依赖树深入了几层, 绑定JavaScript的过程就会变得相当复杂。这是诸如Browserify和Webpack之类的捆绑器出现的地方。
为什么开发人员使用Webpack而不是Gulp? Webpack是捆绑器, 而Gulp是任务运行器, 因此你希望看到这两种常用的工具。取而代之的是, 使用Webpack代替Gulp的趋势越来越大, 尤其是在React社区中。为什么是这样?
简而言之, Webpack是一种功能强大的工具, 它已经可以执行你将通过任务运行程序执行的绝大多数任务。例如, Webpack已经为捆绑包提供了缩小选项和源映射。此外, Webpack可以通过名为webpack-dev-server的自定义服务器作为中间件运行, 该服务器支持实时重载和热重载(我们将在后面讨论这些功能)。通过使用加载程序, 你还可以将ES6添加到ES5转换中, 以及CSS预处理器和后处理器。实际上, 这只是将单元测试和lint作为Webpack无法独立处理的主要任务。鉴于我们已经将至少六个潜在的gulp任务减少到了两个, 许多开发人员选择直接使用NPM脚本, 因为这避免了向项目中添加Gulp的开销(我们稍后还将讨论) 。
使用Webpack的主要缺点是配置起来非常困难, 如果你试图快速启动并运行项目, 那么这将没有吸引力。
我们的3个任务运行器设置 我将使用三个不同的任务运行器设置来建立一个项目。每个设置将执行以下任务:
  • 设置开发服务器, 以实时重新加载监视的文件更改
  • 根据观察到的文件更改, 以可扩展的方式捆绑我们的JS和CSS文件(包括ES6到ES5的转换, SASS到CSS的转换以及源映射)
  • 作为独立任务或在监视模式下运行单元测试
  • 作为独立任务或在监视模式下运行linting
  • 通过终端中的单个命令提供执行上述所有功能的能力
  • 还有另一个命令来创建带有最小化和其他优化的产品包
我们的三种设置将是:
  • Gulp + Browserify
  • Gulp + Webpack
  • Webpack + NPM脚本
该应用程序将使用React作为前端。最初, 我想使用一种与框架无关的方法, 但是使用React实际上简化了任务运行程序的职责, 因为只需要一个HTML文件, 而且React与CommonJS模式一起很好地工作。
我们将介绍每种设置的优缺点, 以便你可以明智地决定哪种类型的设置最适合你的项目需求。
我已经建立了一个Git存储库, 其中包含三个分支, 每种方法一个(链接)。测试每个设置非常简单:
git checkout < branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)

让我们详细检查每个分支中的代码…
通用密码 资料夹结构
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.js

index.html
一个简单的HTML文件。 React应用程序已加载到< div id =” app” > < / div> 中, 我们仅使用一个捆绑的JS和CSS文件。实际上, 在我们的Webpack开发设置中, 我们甚至不需要bundle.css。
index.js
这充当了我们应用程序的JS入口点。本质上, 我们只是将React Router通过前面提到的id应用程序加载到div中。
routes.js
该文件定义了我们的路线。 URL /, / about和/ contact分别映射到HomePage, AboutPage和ContactPage组件。
index.test.js
这是一系列测试本地JavaScript行为的单元测试。在真正的生产质量应用中, 你需要为每个React组件(至少是操纵状态的组件)编写单元测试, 以测试特定于React的行为。但是, 就本文而言, 仅进行可以在监视模式下运行的功能单元测试就足够了。
组件/App.js
可以将其视为我们所有页面视图的容器。每个页面都包含一个< Header /> 组件以及this.props.children, 它会评估页面视图本身(例如, 如果在浏览器中为/ contact, 则为ex / ContactPage)。
components / home / HomePage.js
这是我们的主视图。我选择使用react-bootstrap, 因为引导程序的网格系统非常适合创建响应页面。正确使用引导程序, 可以大大减少必须为较小视口编写的媒体查询的数量。
其余组件(Header, AboutPage, ContactPage)的结构类似(react-bootstrap标记, 无状态处理)。
现在让我们更多地谈论样式。
CSS样式化方法
我对React组件进行样式设置的首选方法是每个组件都有一个样式表, 其样式范围仅适用于该特定组件。你会注意到, 在我的每个React组件中, 顶级div都有一个与组件名称匹配的类名。因此, 例如, HomePage.js的标记包含以下内容:
< div className="HomePage"> ... < /div>

还有一个关联的HomePage.scss文件, 其结构如下:
@import '../../styles/variables'; .HomePage { // Content here }

为什么这种方法如此有用?它导致高度模块化的CSS, 从而在很大程度上消除了不必要的级联行为的问题。
假设我们有两个React组件Component1和Component2。在这两种情况下, 我们都希望覆盖h2字体大小。
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } }/* Component2.scss */ .Component2 { h2 { font-size: 60px; } }

无论组件是相邻的还是一个组件嵌套在另一个组件中, Component1和Component2的h2字体大小都是独立的。理想情况下, 这意味着组件的样式是完全独立的, 这意味着无论将组件放置在标记中的哪个位置, 该组件的外观都将完全相同。实际上, 这并不总是那么简单, 但无疑是朝正确方向迈出的一大步。
除了每个组件的样式, 我还希望有一个样式文件夹, 其中包含全局样式表global.scss, 以及处理特定职责的SASS局部文件(在这种情况下, 分别是_fonts.scss和_variables.scss用于字体和变量) )。全局样式表允许我们定义整个应用程序的总体外观, 而帮助程序部分可以根据需要由每个组件的样式表导入。
现在已经深入研究了每个分支中的通用代码, 现在让我们将重点转移到第一个任务运行器/捆绑器方法。
Gulp + Browserify设置 gulpfile.js
这产生了一个非常大的gulpfile, 具有22个导入和150行代码。因此, 为了简洁起见, 我将仅详细审查js, css, 服务器, 监视和默认任务。
JS包
// Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...)gulp.task('js', bundle); (...)// Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }

由于多种原因, 这种方法相当难看。一方面, 任务分为三个单独的部分。首先, 创建Browserify捆绑对象b, 传入一些选项并定义一些事件处理程序。然后, 你将拥有Gulp任务本身, 该任务必须传递一个命名函数作为其回调而不是对其进行内联(因为b.on(‘ update’ )使用的回调与此相同)。这几乎没有Gulp任务那么优雅, 你只需传递gulp.src并传递一些更改。
另一个问题是, 这迫使我们采用不同的方法在浏览器中重新加载html, css和js。查看我们的Gulp监视任务:
gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

更改HTML文件后, 将重新运行html任务。
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

如果NODE_ENV不是生产版本, 则最后一个管道调用livereload(), 这会触发浏览器中的刷新。
CSS监视使用相同的逻辑。更改CSS文件后, 将重新运行css任务, 并且css任务中的最后一个管道将触发livereload()并刷新浏览器。
但是, js监视程序根本不会调用js任务。取而代之的是, Browserify的事件处理程序b.on(‘ update’ , bundle)使用完全不同的方法(即热模块替换)来处理重新加载。这种方法的不一致性很令人讨厌, 但不幸的是, 要使构建增量, 就必须这样做。如果我们在包函数的末尾天真地调用livereload(), 这将在任何单个JS文件更改时重新构建整个JS包。这种方法显然无法扩展。你拥有的JS文件越多, 每次重新打包花费的时间就越长。突然, 你500毫秒的重新组合开始需要30秒, 这实际上抑制了敏捷开发。
CSS包
gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

这里的第一个问题是麻烦的供应商CSS包含。每当将新的供应商CSS文件添加到项目时, 我们都必须记住要更改gulpfile, 以将元素添加到gulp.src数组, 而不是将导入添加到实际源代码中的相关位置。
另一个主要问题是每个管道中复杂的逻辑。我必须添加一个名为gulp-cond的NPM库, 以在管道中设置条件逻辑, 并且最终结果不太可读(到处都是三括号!)。
服务器任务
gulp.task('server', () => { nodemon({ script: 'server.js' }); });

此任务非常简单。实际上, 它是命令行调用nodemon server.js的包装, 后者在节点环境中运行server.js。使用nodemon代替node, 以便对文件进行任何更改都会导致其重新启动。默认情况下, nodemon会在任何JS文件更改时重新启动运行过程, 因此, 包含nodemon.json文件以限制其范围很重要:
{ "watch": "server.js" }

让我们回顾一下服务器代码。
server.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();

这将根据节点环境设置服务器的基本目录和端口, 并创建一个express实例。
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));

这将添加connect-livereload中间件(对于我们的实时重新加载设置是必需的)和静态中间件(对于处理我们的静态资产是必需的)。
app.get('/api/sample-route', (req, res) => { res.send({ website: 'srcmini', blogPost: true }); });

这只是一条简单的API路线。如果在浏览器中导航到localhost:3000 / api / sample-route, 你将看到:
{ website: "srcmini", blogPost: true }

在实际的后端中, 你将拥有一个完整的文件夹, 专门用于API路由, 用于建立数据库连接的单独文件, 等等。仅包含此示例路线是为了表明我们可以轻松地在已设置的前端之上构建后端。
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir , '/index.html')); });

这是一条通行的路线, 这意味着无论你在浏览器中键入什么url, 服务器都将返回我们唯一的index.html页面。然后, React Router负责在客户端解析我们的路由。
app.listen(port, () => { open(`http://localhost:${port}`); });

这告诉我们的快速实例监听我们指定的端口, 并在指定URL的新选项卡中打开浏览器。
到目前为止, 我对服务器设置唯一不满意的是:
app.use(require('connect-livereload')({port: 35729}));

鉴于我们已经在gulpfile中使用了gulp-livereload, 这使得必须在两个单独的地方使用livereload。
现在, 最后但并非最不重要的:
默认任务
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });

只需在终端中输入gulp即可运行此任务。奇怪的是需要使用runSequence才能使任务按顺序运行。通常, 一系列任务可以并行执行, 但这并不总是所需的行为。例如, 我们需要在html之前运行clean任务, 以确保在将文件移入目标文件夹之前, 目标文件夹为空。当gulp 4发布时, 它将本地支持gulp.series和gulp.parallel方法, 但是现在我们必须在设置中保留这一小怪异之处。
除此之外, 这实际上还很优雅。我们的应用程序的整个创建和托管是在一个命令中完成的, 了解工作流的任何部分都像检查运行顺序中的单个任务一样简单。此外, 我们可以将整个序列分解为较小的块, 以采用更精细的方法来创建和托管应用。例如, 我们可以设置一个名为validate的单独任务, 该任务运行lint和test任务。或者我们可以有一个运行服务器并进行监视的主机任务。这种编排任务的能力非常强大, 尤其是在你的应用程序扩展且需要更多自动化任务时。
开发与生产构建
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';

使用yargs NPM库, 我们可以向Gulp提供命令行标志。如果在终端中将– prod传递给gulp, 我在这里指示gulpfile将节点环境设置为生产环境。然后, 将我们的PROD变量用作区分gulpfile中开发和生产行为的条件。例如, 我们传递给browserify配置的选项之一是:
plugin: PROD ? [] : [hmr, watchify]

这告诉Browserify在生产模式下不使用任何插件, 而在其他环境中使用hmr和watchify插件。
这个PROD条件非常有用, 因为它使我们不必编写用于生产和开发的单独的gulpfile, 这最终将包含很多代码重复。相反, 我们可以执行诸如gulp – prod在生产中运行默认任务的操作, 或gulp html – prod仅在生产中运行html任务的操作。另一方面, 我们早些时候看到, 用诸如.pipe(cond(!PROD, livereload()))之类的语句乱抛垃圾的Gulp管道并不是最易读的。最后, 你是否要使用布尔变量方法还是设置两个单独的gulpfile是一个优先事项。
现在, 让我们继续使用Gulp作为任务运行器, 而将Browserify替换为Webpack会发生什么。
Gulp + Webpack设置 突然, 我们的gulpfile只有99行, 包含12个导入, 比以前的设置要少很多!如果我们检查默认任务:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });

现在, 我们完整的Web应用程序设置仅需要五个任务, 而无需执行九个任务, 这是一个巨大的改进。
此外, 我们消除了实时重新加载的需求。现在, 我们的监视任务很简单:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

这意味着我们的gulp watcher不会触发任何类型的重新打包行为。另外, 我们不需要将index.html从应用程序转移到dist或进行构建。
让我们专注于减少任务, 我们的html, css, js和fonts任务已全部替换为一个构建任务:
gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });

很简单。依次运行clean和html任务。一旦完成, 请抓住我们的入口点, 将其通过Webpack传递, 传入webpack.config.js文件进行配置, 然后将结果包发送到我们的baseDir(dist或build, 取决于节点env)。
让我们看一下Webpack配置文件:
webpack.config.js
这是一个很大且令人生畏的配置文件, 因此, 让我们解释一下在module.exports对象上设置的一些重要属性。
devtool: PROD ? 'source-map' : 'eval-source-map',

这将设置Webpack将使用的源映射的类型。 Webpack不仅开箱即用地支持源地图, 它实际上还支持各种各样的源地图选项。每个选项都提供了不同的源图详细信息与重建速度(重新绑定更改所花费的时间)之间的平衡。这意味着我们可以使用” 便宜的” sourcemap选项进行开发以实现快速重载, 而在生产中可以使用更昂贵的sourcemap选项。
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]

这是我们的捆绑软件入口点。请注意, 传递了一个数组, 这意味着可能有多个入口点。在这种情况下, 我们有预期的入口点app / index.js以及webpack-hot-middleware入口点, 该入口点用作热模块重新加载设置的一部分。
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },

这是将输出编译的包的位置。最令人困惑的选项是publicPath。它设置将捆绑软件托管在服务器上的基本URL。因此, 例如, 如果你的publicPath是/ public / assets, 则该捆绑包将出现在服务器上的/public/assets/bundle.js下。
devServer: { contentBase: PROD ? './build' : './app' }

告诉服务器项目中哪个文件夹用作服务器的根目录。
如果你对Webpack如何将项目中创建的包映射到服务器上的包感到困惑, 只需记住以下几点:
  • 路径+文件名:项目源代码中捆绑软件的确切位置
  • contentBase(作为根, /)+ publicPath:捆绑软件在服务器上的位置
plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],

这些插件可以通过某种方式增强Webpack的功能。例如, webpack.optimize.UglifyJsPlugin负责缩小。
loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000& name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000& name=fonts/[name].[ext]'} ]

这些是装载机。本质上, 它们对通过require()语句加载的文件进行预处理。它们与Gulp管道有点相似, 因为你可以将装载机链接在一起。
让我们检查一下我们的加载器对象之一:
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}

测试属性告诉Webpack, 如果文件与提供的正则表达式模式(在本例中为/\.scss$/)匹配, 则将应用给定的加载器。 loader属性对应于加载程序执行的操作。在这里, 我们将样式, css, resolve-url和sass加载程序链接在一起, 它们以相反的顺序执行。
我必须承认, 我觉得loader3!loader2!loader1语法不是很优雅。毕竟, 什么时候需要从右到左读取程序中的任何内容?尽管如此, 加载程序还是webpack的一项非常强大的功能。实际上, 我刚刚提到的加载程序允许我们将SASS文件直接导入到我们的JavaScript中!例如, 我们可以在我们的入口点文件中导入供应商和全局样式表:
index.js
import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(< Router history={browserHistory} routes={routes} /> , document.getElementById('app'));

同样, 在Header组件中, 我们可以添加import’ ./Header.scss’ 来导入组件的关联样式表。这也适用于我们所有其他组件。
我认为, 这几乎可以视为JavaScript开发领域的革命性变化。无需担心CSS捆绑, 缩小或源地图, 因为我们的加载程序会为我们处理所有这些工作。甚至热模块重新加载也适用于我们的CSS文件。然后, 能够在同一文件中处理JS和CSS导入, 使开发从概念上讲更加简单:一致性更高, 上下文切换更少, 推理也更容易。
简要概述此功能的工作原理:Webpack将CSS内联到我们的JS包中。实际上, Webpack也可以对图像和字体执行此操作:
{test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000& name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000& name=fonts/[name].[ext]'}

URL加载器指示Webpack如果图像和字体小于100 KB, 则将它们作为数据URL内联, 否则将它们作为单独的文件使用。当然, 我们也可以将截止大小配置为其他值, 例如10 KB。
简而言之, 这就是Webpack的配置。我承认有很多设置, 但是使用它的好处简直是惊人的。尽管Browserify确实具有插件和转换, 但是就增加的功能而言, 它们根本无法与Webpack加载程序进行比较。
Webpack + NPM脚本设置 在此设置中, 我们直接使用npm脚本, 而不是依赖gulpfile来自动执行任务。
package.json
"scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist & & mkdir dist", "clean-build": "rimraf ./build & & mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }

要运行开发和生产版本, 分别输入npm start和npm run start:prod。
这肯定比gulpfile更为紧凑, 因为我们已经将99到150行代码缩减为19个NPM脚本, 如果不包括生产脚本, 则减少了12行(其中大多数只是将节点环境设置为生产的镜像镜像) )。缺点是, 与我们的Gulp任务副本相比, 这些命令有些晦涩难懂, 而表现力却不够。例如, 没有办法(至少据我所知)使单个npm脚本连续运行某些命令, 而并行运行某些命令。一个或另一个。
但是, 这种方法有很大的优势。通过直接从命令行使用NPM库(例如mocha), 你无需为每个库安装等效的Gulp包装器(在本例中为gulp-mocha)。
代替安装NPM
  • 飞丝带
  • 口香糖摩卡
  • gulp-nodemon
  • 等等
我们安装以下软件包:
  • int
  • mocha
  • Nodemon
  • 等等
引用科里·豪斯(Cory House)的帖子, “ 为什么我要对NPM脚本大吃一惊” :
我是Gulp的忠实粉丝。但是在我的上一个项目中, 我最终在gulpfile中包含了数百行, 并包含了大约十二个Gulp插件。我一直在努力使用Gulp集成Webpack, Browsersync, 热重装, Mocha等。为什么?好吧, 有些插件没有针对我的用例的文档。一些插件仅公开了我需要的API的一部分。一个人有一个奇怪的错误, 它只观看少量文件。当输出到命令行时, 另一种颜色被剥离。
他指定了Gulp的三个核心问题:
  1. 对插件作者的依赖
  2. 令人沮丧的调试
  3. 脱节的文件
我倾向于同意所有这些观点。
1.对插件作者的依赖
每当诸如eslint之类的库被更新时, 关联的gulp-eslint库都需要进行相应的更新。如果库维护者不感兴趣, 则该库普尔的原始版本将与本机版本不同步。创建新库时也是如此。如果有人创建了一个库xyz并继续流行, 那么突然间你需要一个相应的gulp-xyz库以在你的gulp任务中使用它。
从某种意义上说, 这种方法无法扩展。理想情况下, 我们希望有一种类似Gulp的方法可以使用本机库。
2.调试失败
尽管gulp-plumber等库可以大大缓解此问题, 但是gulp中的错误报告确实不是很有帮助。如果甚至有一个管道抛出未处理的异常, 你也会获得一个堆栈跟踪, 跟踪一个似乎与源代码中引起该问题的原因完全无关的问题。在某些情况下, 这会使调试成为噩梦。如果错误是神秘的或引起误解的, 那么在Google或Stack Overflow上进行的大量搜索都无法真正为你提供帮助。
3.脱节的文件
通常, 我发现小型gulp库往往具有非常有限的文档。我怀疑这是因为作者通常将图书馆主要供他或她自己使用。此外, 通常必须查看Gulp插件和本机库本身的文档, 这意味着需要进行大量的上下文切换, 并且需要做的阅读量是原来的两倍。
总结 在我看来, Webpack比Browserify更可取, 而NPM脚本比Gulp更可取, 尽管每种选择都有其优点和缺点。 Gulp当然比NPM脚本更具表现力和使用方便, 但是你要为所有添加的抽象付出代价。
并非每种组合都适合你的应用程序, 但是如果你想避免大量的开发依赖项和令人沮丧的调试体验, 那么使用带有NPM脚本的Webpack是可行的。希望本文对你为下一个项目选择合适的工具有用。
有关:
  • 维护控制权:Webpack和React指南, Pt。 1个
  • Gulp揭秘:构建基于流的任务自动化工具

    推荐阅读