08月01, 2015

认识Webpack

Webpack-logo

网上已经有不少Webpack教程入门教程了。 本文记录了我以我的方式方法、思路认识了解Webpack。从官方的Tutorial入手,不断提出问题、解决,一步一步认识Webpack。

从早期的自己写脚本,到现在的各种构建工具,前端工程化已经发展到新的阶段了。

早先在百度地图的时候,地图代码用PHP进行简单粗放的处理。这个阶段算是最原始的自己写脚本处理。后来我用Ruby写了一套集合了开发、动态合并、mock数据、一键build的工具。这算是更进了一步。

现在基于Nodejs的任务管理工具Grunt、Glup都提供了代码合并、压缩、各种JS Transpiler、CSS预处理、各种前端模板的处理。

在Grunt、Gulp中是通过第三方库进行编译的。 在Webpack中也是类似的,只不过是增加了Loader的概念。通过一系列“Loader”完成处理。 处理之后统一输出为JS代码。

初步认识

在深入之前,你需要先照着官方教程实践一下。有个感性的认识。 完成教程的getting started部分后,你可以初步得出以下结论:

  1. Webpack是一个用来打包js工程的工具。官方定义为Module bundler
  2. Webpack命令行参数可以配置到名为webpack.config.js(或webpackfile)的配置文件中
  3. Webpack提供一个可以检测文件变化并编译然后刷新浏览器的webpack-dev-server

那么问题来了: 对于如Grunt、Webpack这种通过配置工作的工具来说,有哪些配置可用,配置的行为、配置的可选值,需要完备的文档才好会用。 幸好Webpack官网提供了详尽的文档

下面是主要配置项的简要说明

context 工程目录,必须是绝对路径
entry 打包生成的bundle。可以是多个
output 生成的文件配置选项
    output.filename 生成的文件名模板,比如 "[name].bundle.js"
    output.path 生成的文件目录,绝对路径
    output.publicPath 线上静态资源目录
    output.chunkFilename 代码块文件名模板
    output.sourceMapFilename source-map文件名模板。默认是[file].map
    output.devtoolModuleFilenameTemplate
    output.devtoolFallbackModuleFilenameTemplate
    output.devtoolLineToLine
    output.hotUpdateChunkFilename
    output.hotUpdateMainFilename
    output.jsonpFunction JSONP异步加载代码块(chunk)时JSONP函数名,默认是webpackJsonp
    output.hotUpdateFunction JSONP异步热更新代码块时JSONP函数名,默认是webpackHotUpdate
    output.pathinfo 是否以注释形式在require中增加模块path信息
    output.library bundle作为库输出,值为库名
    output.libraryTarget 输出库的格式。比如可选amd,umd,commonjs等
    output.umdNamedDefine
    output.sourcePrefix
    output.crossOriginLoading
module
    module.loaders Loader配置
    module.preLoaders, module.postLoaders preLoader和postLoader配置
    module.noParse 不需要loader编译的文件
resolve 模块决议配置
    resolve.alias 模块别名
    resolve.root 模块根目录,绝对路径
    resolve.modulesDirectories 模块目录,工作方式类似node_modules。默认值是["web_modules", "node_modules"]
    resolve.fallback 如果在root和modulesDirectories都找不到,会在这里搜索
    resolve.extensions 用于模块查找的扩展名。
    resolve.packageMains
    resolve.packageAlias
    resolve.unsafeCache
resolveLoader 与resolve类似,不过是给loader模块决议使用的配置
    resolveLoader.moduleTemplates
externals
target 目标环境,代码是用于web还是node还是electron环境等等
bail
profile 每个模块的时间打点信息
cache 是否开启编译缓存以提高性能。watch模式默认开启
debug 设置loaders为debug模式
devtool 用于方便调试的开发工具选项。比如source-map方便调试混淆后的代码
devServer 传给webpack-dev-server的参数
node 传递给node作为polyfills和mocks的参数
amd require.mad和define.amd对应的值。比如{jQuery:true}
loader 提供给loader的额外信息
recordsPath, recordsInputPath, recordsOutputPath
plugins 插件配置

Loaders

我们最关心的是有哪些loader可以用呢? 通过在 https://github.com/webpack 搜索项目名中包含-loader。我找到了这些官方提供的loader:

#裸数据
raw-loader

#脚本代码
coffee-loader
script-loader

#样式相关
css-loader
style-loader
less-loader 

#html相关
html-loader
jade-loader 

#json相关
json-loader
json5-loader

#其他
worker-loader-loader  imports-loader exports-loader source-map-loader  coffee-redux-loader multi-loader react-proxy-loader expose-loader url-loader  node-loader bundle-loader val-loader transform-loader jshint-loader null-loader coverjs-loader

咦,为什么有一个css-loader还有一个style-loader?css-loader是用来加载css文件的 style-loader是用来应用已经加载的css中的样式的。

在配置文件中,配置需要使用的loader。test用来对文件名进行匹配测试,匹配成功的文件会用对应的loader处理。

module: {
    loaders: [
      { test: /\.coffee$/, loader: "coffee-loader" },
      { test: /\.js$/, loader: "jsx-loader" }
    ]
  },

每个loader都有自己独特的配置,需要参考对应文档。 所有loader都可以配置以下项目:

test 用来对文件名进行匹配测试
exclude 被排除的文件名
include 包含的文件名
loader 叹号分割的loaders
loaders loader数组

比如babel的配置就有query、cacheDirectory配置项。

可以想象,loader要做的工作无非就是拿到源码,根据参数配置进行变换,返回变换后的结果。看一下less-loder的源代码:

/**
 * 简化后的伪代码
 */
var less = require("less");
var loaderUtils = require("loader-utils");

module.exports = function(source){
    //解析loader的query string
    var query = loaderUtils.parseQuery(this.query);
    //默认less编译配置
    var config = {
        filename: this.resource,
        compress: !!this.minimize
    };
    //将query中的配置merge到默认配置中
    Object.keys(query).forEach(function(attr){
        config[attr] = query[attr]
    });
    //编译less
    var cb = this.callback;
    less.render(source, config, function(e, result){
        cb(null, result.css, result.map);
    });
};

基本上就是从query读取配置,调用less编译器编译源码。

官网编写loader的教程验证了上述想法。同时也指出了编写loader时要注意的一些问题。

参考:官方给出的已有loader列表

Plugins

有哪些plugin呢? 通过在 https://github.com/webpack 搜索项目名中包含-plugin我找到了这些官方提供的plugin:

extract-text-webpack-plugin
compression-webpack-plugin
i18n-webpack-plugin
component-webpack-plugin

感觉不对啊,那个很多教程中常见的UglifyJsPlugin都没有看到啊!那么只有一个可能,这些plugin都是内置的。在源代码中一定能找到。clone下来webpack的代码。打开lib,满眼都是XXXPlugin。在optimize目录下可以找到UglifyJsPlugin。大致看一下这些代码可以发现,每个Plugin的原型上都有一个apply函数:

/**
 * 从UglifyJsPlugin.js简化而来的伪代码
 */
...
var uglify = require("uglify-js");
...
UglifyJsPlugin.prototype.apply = function(compiler) {
    ...
   compiler.plugin("compilation", function (module) {
        ...
        var input = asset.source();
        var ast = uglify.parse(input);
        //压缩
        if (options.compress !== false) {
            var compress = uglify.Compressor(options.comrpess);
            ast = ast.transform(compress);
        }
        //混淆
        if (options.mangle !== false) {
            ast.mangle_names();
            uglify.mangle_properties(ast);
        }
        //重新从ast生成代码
        var result = uglify.OutputStream();
        ast.print(result);
   });
};

可以想象,webpack会根据配置文件中plugins数组中的配置实例化插件,然后在适当的时候调用其apply函数。 在apply函数中,插件对感兴趣的事件(官方叫做stage)注册处理函数(plugin)。比如UglifyJsPlugin就是在compilation事件触发时,对源代码进行压缩混淆。

通过官网阅读how-to-write-a-plugin可以验证了上面的想法。

既然有compilation事件,那肯定还有其他事件喽。在lib目录下搜索源代码中的compile.plugin调用

$ ack -Q compiler.plugin( | grep plugin|gawk "{print($2)}"|sort|uniq

compiler.plugin("additional-pass",
compiler.plugin("after-compile",
compiler.plugin("after-environment",
compiler.plugin("after-resolvers",
compiler.plugin("compilation",
compiler.plugin("compile",
compiler.plugin("context-module-factory",
compiler.plugin("done",
compiler.plugin("emit",
compiler.plugin("entry-option",
compiler.plugin("environment",
compiler.plugin("invalid",
compiler.plugin("make",
compiler.plugin("normal-module-factory",
compiler.plugin("run",
compiler.plugin("should-emit",
compiler.plugin("this-compilation",
compiler.plugin("watch-run",

一共有18个事件。官方教程中只介绍了done,compilation,emit三个。 用同样的方法,我们还可以查出compilation支持的事件:

$ ack -Q compilation.plugin( | grep plugin|gawk "{print($2,$3)}"|sort|uniq

compilation.plugin("additional-assets", function(callback)
compilation.plugin("additional-chunk-assets", function()
compilation.plugin("after-hash", function()
compilation.plugin("after-optimize-chunk-assets", function(chunks)
compilation.plugin("after-optimize-tree", function(chunks,
compilation.plugin("before-module-ids", function(modules)
compilation.plugin("build-module", function(module)
compilation.plugin("chunk-hash", function(chunk,
compilation.plugin("failed-module", moduleDone);
compilation.plugin("need-additional-pass", function()
compilation.plugin("normal-module-loader", function(context,
compilation.plugin("normal-module-loader", function(loaderContext)
compilation.plugin("normal-module-loader", function(loaderContext,
compilation.plugin("optimize-assets", function(assets,
compilation.plugin("optimize-chunk-assets", function(chunks,
compilation.plugin("optimize-chunk-ids", function(chunks)
compilation.plugin("optimize-chunk-order", function(chunks)
compilation.plugin("optimize-chunks-advanced", function(chunks)
compilation.plugin("optimize-chunks-basic", function(chunks)
compilation.plugin("optimize-module-order", function(modules)
compilation.plugin("optimize-modules-advanced", function(modules)
compilation.plugin("optimize-tree", function(chunks,
compilation.plugin("record", function(compilation,
compilation.plugin("record-chunks", function(chunks,
compilation.plugin("record-modules", function(modules,
compilation.plugin("revive-chunks", function(chunks,
compilation.plugin("revive-modules", function(modules,
compilation.plugin("seal", function()
compilation.plugin("should-generate-chunk-assets", function()
compilation.plugin("should-record", function()
compilation.plugin("succeed-module", moduleDone);
compilation.plugin(["optimize-chunks", "optimize-extracted-chunks"],
compilation.plugin(["optimize-chunks-basic", "optimize-extracted-chunks-basic"],

有了这两个列表,在自己编写插件就可以有的放矢地参考源代码了。

参考:官方给出的已有plugin的列表

Webpack-dev-server

Webpack提供一个小巧的基于express的开发服务器。支持自动刷新、模块热替换。还有代理。具体如何配置在这里

代理(proxy)在开发是还是很有用的。你可以将动态请求映射到后端的开发机,方便联调。

总结

现在照着官方教程你已经可以简单地使用Webpack了。下一步要做的是

  • 了解webpack.config.js中如何配置,有哪些要注意的(比如路径)
  • 实践常用的Loader和Plugin
  • 实践webpack的众多配置项
  • 实践使用webpack-dev-server进行开发

需要时可以更进一步:

  • 学习如何编写Loader和Plugin
  • 阅读已有Loader和Plugin的源码
  • 在源码中了解上面列出的stages的含义

更新:Webpack、Browserify和Gulp三者之间到底是怎样的关系?

下面是我在知乎的回答

Task Runner

Gulp、Grunt和Make(常见于c/cpp)、Ant、Maven、Gradle(Java/Android)、Rake、Thor(Ruby)一样,都是是Task Runner。用来将一些繁琐的task自动化并处理任务的依赖关系。 其中有些是基于配置描述的,描述逻辑比较费劲,比如Ant基于xml。还有些就是代码,比较灵活,个人偏好这种。比如Rake、Thor、Gulp、Gradle。对于Gradle来说也有些蛋疼。因为它本身是Groovy的DSL。如果要深入使用,你还得学一下Groovy语言。其他就好多了Rake、Thor就是写Ruby;Gulp就是JavaScript。相对门槛低很多。

模块化解决方案

Browserify It provides a way to bundle CommonJS modules together, adheres to the Unix philosophy(小工具协作), is in fact a good alternative to Webpack. Webpack takes a more monolithic(整体解决、大而全) approach than Browserify... is relies on configuration.

上面这些工具在功能上有交集:代码的Minify、Concat;资源预处理等;

其实每个工具的官网上都有对工具的设计思想、要解决的问题、与其他工具的对比。自己摘抄下来,做个表格对比一下。高亮出每个工具独特的特性。这样你就知道什么时候需要用哪个工具了。 比如,你的工程模块依赖很简单,不需要把js或各种资源打包,只需要简单的合并、压缩,在页面中引用就好了。那就不需要Browserify、Webpack。Gulp就够用了。

反过来,如果你的工程庞大,页面中使用了很多库(SPA很容易出现这种情况),那就可以选择某种模块化方案。至于是用Browserify还是Webpack就需要根据其他因素来判断了。比如团队已经在使用了某种方案,大家都比较熟悉了。再比如,你喜欢Unix小工具协作的方式,那就Browserify。

充分了解各种工具、方案,选择合适的和自己需要的。没有绝对的好。优点换了场景也会变成缺点。

UPDATE

下面是闲耘™用Makefile管理前端工程任务的例子: https://github.com/hotoo/pinyin/blob/master/Makefile

更多资料:

本文链接:http://aztack.wang/post/about-webpack.html

-- EOF--

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。