Webpack 或 Browserify & Gulp:哪个更好?
已发表: 2022-03-11随着 Web 应用程序变得越来越复杂,使您的 Web 应用程序可扩展变得至关重要。 过去编写临时 JavaScript 和 jQuery 就足够了,而如今构建 Web 应用程序需要更高程度的纪律和正式的软件开发实践,例如:
- 单元测试以确保对代码的修改不会破坏现有功能
- Linting 以确保一致的编码风格没有错误
- 不同于开发版本的生产版本
Web 还提供了一些自己独特的开发挑战。 例如,由于网页发出大量异步请求,您的 Web 应用程序的性能可能会因必须请求数百个 JS 和 CSS 文件而显着降低,每个文件都有自己的微小开销(标头、握手等)。 这个特定问题通常可以通过将文件捆绑在一起来解决,因此您只需要一个捆绑的 JS 和 CSS 文件,而不是数百个单独的文件。
使用编译为原生 JS 和 CSS 的语言预处理器(如 SASS 和 JSX)以及 JS 转译器(如 Babel)在保持 ES5 兼容性的同时从 ES6 代码中受益也是很常见的。
这相当于大量与编写 Web 应用程序本身的逻辑无关的任务。 这就是任务运行程序的用武之地。任务运行程序的目的是自动执行所有这些任务,以便您可以在专注于编写应用程序的同时从增强的开发环境中受益。 配置任务运行程序后,您需要做的就是在终端中调用单个命令。
我将使用 Gulp 作为任务运行器,因为它对开发人员非常友好、易于学习且易于理解。
Gulp 简介
Gulp 的 API 包含四个函数:
-
gulp.src
-
gulp.dest
-
gulp.task
-
gulp.watch
例如,这里是一个使用这四个函数中的三个的示例任务:
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
的文件都会被缩小,然后转移到build
文件夹。
其美妙之处在于.pipe()
链接。 您获取一组输入文件,通过一系列转换对它们进行管道传输,然后返回输出文件。 为了使事情更方便,实际的管道转换,例如minify()
,通常由 NPM 库完成。 因此,在实践中,除了重命名管道中的文件之外,您需要编写自己的转换是非常罕见的。
理解 Gulp 的下一步是理解任务依赖的数组。
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });
这里, 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 适合哪里?
使用 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
中的其余代码之前解决,并且这些依赖项本身可能还有进一步的依赖项需要解决。 此外,您可能在应用程序的多个位置require
相同的依赖项,但您只想解决该依赖项一次。 可以想象,一旦你有一个深几级的依赖树,捆绑你的 JavaScript 的过程就会变得相当复杂。 这就是 Browserify 和 Webpack 等打包工具的用武之地。
为什么开发人员使用 Webpack 而不是 Gulp?
Webpack 是一个打包器,而 Gulp 是一个任务运行器,因此您希望看到这两个工具经常一起使用。 相反,使用 Webpack代替Gulp 的趋势越来越明显,尤其是在 React 社区中。 为什么是这样?
简而言之,Webpack 是一个非常强大的工具,它已经可以执行您通过任务运行器完成的绝大多数任务。 例如,Webpack 已经为你的包提供了缩小和源图的选项。 此外,Webpack 可以通过名为webpack-dev-server
的自定义服务器作为中间件运行,该服务器支持实时重载和热重载(我们稍后会讨论这些功能)。 通过使用加载器,您还可以将 ES6 添加到 ES5 转换,以及 CSS 预处理器和后处理器。 这真的只是让单元测试和 linting 成为 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
索引.html
一个简单的 HTML 文件。 React 应用程序被加载到<div></div>
中,我们只使用一个捆绑的 JS 和 CSS 文件。 事实上,在我们的 Webpack 开发设置中,我们甚至不需要bundle.css
。
index.js
这充当了我们应用程序的 JS 入口点。 本质上,我们只是将 React Router 加载到前面提到的 id app
的div
中。
路由.js
该文件定义了我们的路线。 url /
、 /about
和/contact
分别映射到HomePage
、 AboutPage
和ContactPage
组件。
index.test.js
这是一系列测试原生 JavaScript 行为的单元测试。 在真正的生产质量应用程序中,您将为每个 React 组件(至少是那些操纵状态的组件)编写一个单元测试,以测试特定于 React 的行为。 但是,就本文而言,只需拥有一个可以在监视模式下运行的功能单元测试就足够了。
组件/App.js
这可以被认为是我们所有页面视图的容器。 每个页面都包含一个<Header/>
组件以及this.props.children
,它评估页面视图本身(例如 / ContactPage
如果在浏览器中的/contact
处)。
组件/主页/HomePage.js
这是我们家的景色。 我选择使用react-bootstrap
,因为 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
的styles
文件夹,以及处理特定职责的 SASS 部分(在这种情况下, _fonts.scss
和_variables.scss
分别用于字体和变量)。 全局样式表允许我们定义整个应用程序的一般外观和感觉,而辅助部分可以根据需要由每个组件的样式表导入。
既然已经深入探讨了每个分支中的通用代码,让我们将注意力转移到第一个任务运行器/捆绑器方法上。
Gulp + Browserify 设置
gulpfile.js
这产生了一个惊人的大 gulpfile,包含 22 个导入和 150 行代码。 因此,为简洁起见,我将只详细回顾js
、 css
、 server
、 watch
和default
任务。
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 watch
任务:
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
不是production
,最后一个管道调用livereload()
,这会触发浏览器中的刷新。
CSS watch 使用相同的逻辑。 当一个 CSS 文件发生变化时, css
任务会重新运行, css
任务中的最后一个管道会触发livereload()
并刷新浏览器。
但是, js
手表根本不调用js
任务。 相反,Browserify 的事件处理程序b.on('update', bundle)
使用完全不同的方法(即热模块替换)处理重新加载。 这种方法的不一致很烦人,但不幸的是,为了进行增量构建,这是必要的。 如果我们只是在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" }
让我们回顾一下我们的服务器代码。
服务器.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: 'Toptal', blogPost: true }); });
这只是一个简单的 API 路由。 如果您在浏览器中导航到localhost:3000/api/sample-route
,您将看到:
{ website: "Toptal", 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}`); });
这告诉我们的 express 实例监听我们指定的端口,并在指定 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
任务。 或者我们可以有一个运行server
和watch
的host
任务。 这种编排任务的能力非常强大,尤其是当您的应用程序扩展并需要更多自动化任务时。
开发与生产构建
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
任务。 另一方面,我们之前看到,在 Gulp 管道中乱扔诸如.pipe(cond(!PROD, livereload()))
之类的语句并不是最易读的。 最后,您是想使用布尔变量方法还是设置两个单独的 gulpfile 是一个偏好问题。
现在让我们看看当我们继续使用 Gulp 作为我们的任务运行器但将 Browserify 替换为 Webpack 时会发生什么。
Gulp + Webpack 设置
突然间,我们的 gulpfile 只有 99 行长,有 12 个导入,比我们之前的设置减少了很多! 如果我们检查默认任务:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });
现在我们完整的网络应用程序设置只需要五个任务而不是九个,这是一个巨大的改进。
此外,我们消除了对livereload
的需求。 我们的watch
任务现在很简单:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
这意味着我们的 gulp watcher 不会触发任何类型的重新捆绑行为。 作为额外的奖励,我们不再需要将index.html
从app
传输到dist
或build
。
让我们的注意力回到任务缩减上,我们的html
、 css
、 js
和fonts
任务都被一个build
任务所取代:
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
,取决于节点环境)。
让我们看一下 Webpack 配置文件:
webpack.config.js
这是一个非常大且令人生畏的配置文件,所以让我们解释一下在我们的module.exports
对象上设置的一些重要属性。
devtool: PROD ? 'source-map' : 'eval-source-map',
这设置了 Webpack 将使用的源映射类型。 Webpack 不仅支持开箱即用的源映射,它实际上还支持多种源映射选项。 每个选项都提供了 sourcemap 细节与重建速度(重新捆绑更改所需的时间)之间的不同平衡。 这意味着我们可以在开发中使用“便宜”的 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 如何将项目中创建的包映射到服务器上的包感到困惑,请记住以下内容:
-
path
+filename
:项目源代码中包的确切位置 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'}
test
属性告诉 Webpack 如果文件与提供的正则表达式模式匹配,则应用给定的加载器,在本例中/\.scss$/
。 loader
属性对应于 loader 执行的操作。 在这里,我们将style
、 css
、 resolve-url
和sass
加载器链接在一起,它们以相反的顺序执行。
我必须承认,我没有发现loader3!loader2!loader1
的语法非常优雅。 毕竟,你什么时候需要从右到左阅读程序中的任何内容? 尽管如此,loader 还是 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]'}
如果图像和字体小于 100 KB,URL 加载器会指示 Webpack 将我们的图像和字体作为数据 url 内联,否则将它们作为单独的文件提供。 当然,我们也可以将截止大小配置为不同的值,例如 10 KB。
简而言之,这就是 Webpack 配置。 我承认有相当多的设置,但使用它的好处简直是惊人的。 尽管 Browserify 确实有插件和转换,但它们在附加功能方面根本无法与 Webpack 加载器相比。
Webpack + NPM 脚本设置
在这个设置中,我们直接使用 npm 脚本而不是依赖 gulpfile 来自动化我们的任务。
包.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 脚本串行运行某些命令而并行运行其他命令。 它是一个或另一个。
但是,这种方法有一个巨大的优势。 通过直接从命令行使用诸如mocha
之类的 NPM 库,您无需为每个库安装等效的 Gulp 包装器(在本例中为gulp-mocha
)。
而不是安装 NPM
- gulp-eslint
- 大口喝摩卡
- gulp-nodemon
- 等等
我们安装以下软件包:
- 埃斯林特
- 摩卡
- 节点监视器
- 等等
引用 Cory House 的帖子, Why I Left Gulp and Grunt for NPM Scripts :
我是 Gulp 的忠实粉丝。 但在我的上一个项目中,我的 gulpfile 中有数百行和大约十几个 Gulp 插件。 我正在努力使用 Gulp 集成 Webpack、Browsersync、热重载、Mocha 等等。 为什么? 好吧,一些插件没有足够的文档来满足我的用例。 一些插件只公开了我需要的部分 API。 其中一个有一个奇怪的错误,它只能观看少量文件。 输出到命令行时另一个剥离颜色。
他指出了 Gulp 的三个核心问题:
- 依赖插件作者
- 令人沮丧的调试
- 杂乱无章的文档
我倾向于同意所有这些。
1. 对插件作者的依赖
每当像eslint
这样的库被更新时,相关gulp-eslint
库都需要相应的更新。 如果库维护者失去兴趣,则库的 gulp 版本与本机版本不同步。 创建新库时也是如此。 如果有人创建了一个库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 Under the Hood:构建基于流的任务自动化工具