使用 Webpack 模块化 Angular 应用程序(1)
- UID
- 1066743
|
使用 Webpack 模块化 Angular 应用程序(1)
过去一年来,我的团队一直在研究一个模块化仪表板,该仪表板使用 Webpack 打包而成,被构建为一个单页 AngularJS 应用程序。但该项目经历了一些转变才变成今天的样子,我们将该应用程序转换成为其当前的模块化形式。
当我们的项目开始时,该仪表板是一个相对较小的应用程序(至少可通过源文件数量进行判断)。我们在直接调用的函数表达式(Immediately Invoked Function Expressions,IIFE)中快速且轻松地编写了 JavaScript,并通过 <script> 标签将此源代码添加到我们的仪表板标记中。构建过程生成了一个被缩小的文件。
“通过模块化源代码,可以创建分离的代码段,这些代码段能更高效地执行,而且在项目增长过程中更容易维护。”
我们许多人都熟悉此模式,而且它非常适用。但我们可以预见一些问题:当脚本需要删除或合并时,或者如果在加载页面时不需要它们,将会发生什么?或者,只在一个完全不同的页面上才需要它时会发生什么?谁还记得这些依赖项是如何互联的?
Angular 框架已包含模块的概念:Angular 模块是 Angular 工件(比如指令、控制器和服务)的集合,规定了如何引导一个应用程序。但不要落入这样的思维陷阱:因为 Angular 应用程序组织为模块,所以认为您不需要实施更加系统的模块化。
为了解决这些问题,我们决定通过更新源代码并使用 模块打包器,对该应用程序执行模块化。通过模块化源代码,可以创建分离的代码段,这些代码段能更高效地执行,而且在项目增长过程中更容易维护。没有模块化,您通常会陷在维护一个 <script> 标签列表上,尝试记住删除不再使用的标签,或者确保您在添加新代码时引用了正确的脚本。借助模块化,您可以获取依赖图(dependency graph)的一部分并在其他地方重用。此外,模块化使惰性加载(lazy-loading)特性成为可能,为用户带来了高度优化的体验。
本教程将回顾我们的模块化经验,引导您完成设置模块化的 Angular 应用程序的过程——无论您是从头开始模块化还是迁移现有应用程序。
挑选模块打包器如果原生浏览器模块(native browser modules)已经存在,那么非常好,您可以消除构建过程中的模块打包步骤;但现在,您需要在构建流中使用一个模块打包器。可用的打包器包括 、 、 和 等。
每个打包器都有其优缺点,但它们执行的任务都是相似的,那就是遍历您的源代码的依赖树,生成运行您的应用程序所需的最小的 JavaScript 文件集。例如,假设您有一个 a.js 文件,它依赖于您在 b.js 和 c.js 中编写的代码。根据您的模块格式(参阅 小节),定义这些文件间的依赖关系。然后,打包器会生成一个同时包含 a.js、b.js 和 c.js 的内容的文件。如果从 a.js 中删除了引入对 b.js 的依赖性的代码,并重新构建该打包模块(bundle),该文件就会仅包含 a.js 和 c.js 的内容。
考虑到 webpack 的富插件生态系统、活跃的社区、模块格式支持的灵活性、性能和生成多个打包模块(bundle)的能力,我们将它设置为我们项目的模块打包器。
挑选模块格式决定模块格式时,我们面临着一个一直存在的争议:选择 还是异步模块定义(Asynchronous Module Definition,AMD)。许多人都很熟悉 AMD,但我们选择了 CommonJS,因为更多的团队成员在使用 Node.js 的过程中积累了它的经验。我们牺牲了 AMD 的异步流,而采用 CommonJS 的简单的同步语法,因为我们知道我们的最终结果是很少的已打包模块。幸运的是,webpack 能开箱即用地理解这两种格式,因此,如果我们使用的任何库使用了 AMD 来打包它们自己,我们的代码会继续正常运行。
配置 webpack选择了打包器和模块格式后,接下来要设置 webpack。我们的构建流程大量使用了 ,所以,在开始合并 webpack 时继续使用 Grunt 对我们而言是合理的。必须安装 webpack——至少应安装在本地;grunt-webpack 插件在构建堆栈中使用 Grunt 时也会带来很大便利。要安装二者,可运行以下命令:
npm install --save-dev grunt-webpack webpack
|
webpack 的配置规定了打包器在开始处理您所提供的模块时的大部分工作。各种配置属性通常包含在 webpack.config.js 文件中。
我们定义了两种配置:开发和分发。它们共享许多通用的属性;区别在于开发配置会打开源代码映射来帮助调试,而分发配置会最小化结果打包模块(bundle)来减小传输给用户的文件大小。
介绍 webpack 的所有配置选项和我们在每个选项中的选择需要大量的篇幅,无法在一个练习中完成。我们只打算介绍出于项目用途而关注的一些选项。
入口点应用程序的入口定义 webpack 将输出哪些打包模块(bundle),webpack 需要哪些打包模块(bundle)来启动其进程。从这个入口点开始,webpack 遍历依赖关系分层结构,构建一个依赖关系图,以便在一个模块需要另一个模块时,必要的模块已加载就位。
您可以定义多个条目,就像我们在项目中所做的一样。主要入口点是我们的应用程序,它定义了仪表板的模块。然后,此模块需要其他所有模块,每个模块需要所有服务、工厂、指令和控制器。另一个入口点是 vendor。此入口点与我们在项目中使用的第三方库隔离。将此入口点分离到它自己的打包模块(bundle)中很有好处,因为第三方库本身不会更改太多,但我们的应用程序会。如果我们将所有文件都打包到一个打包模块(bundle)中,它将包含更改的应用程序代码和未更改的供应商代码,进而增大下载文件大小,因为浏览器缓存未得到正确利用。
惰性加载有时用户不需要一个特性,或者不需要在初步加载页面后看到它。为了提供更好的执行体验,webpack 允许惰性加载(lazy loading)模块,以在需要它们的时候才初始化它们。我们向仪表板的分析方面应用了此能力。数据可视化库非常大,所以从初始下载文件中删除该内容,会使不关心分析的用户能更快地启动。我们依靠 来向用户提供这种体验。在我们原始的图表指令中,以下代码导致应用程序打包模块(bundle)包含 c3 和它的所有依赖项:
link: function () {
var c3 = require('c3');
// Use c3
}
|
我们将该指令更改为:
link: function () {
require.ensure([], function ()
var c3 = require('c3');
// Use c3
}, 'charts');
}
|
现在 webpack 将创建一个新 charts 打包模块(bundle),只有在链接该指令时才会下载该打包模块(bundle)。
ProvidePluginwebpack 是一个将出现的全局变量替换为显示导出的已加载关联模块的插件,它对我们的改进工作不可或缺。因为我们的应用程序的开发周期的绝大部分都没有模块化,它包含对 angular、$、moment 和其他库的许多全局引用,例如:
ProvidePlugin 将前面的代码更改为:
require('moment')().add(2, 'days');
|
使用 ProvidePlugin,我们就不需要查找并替换众多文件中出现的所有这些全局变量。
html-loaderwebpack 的一个优势是,它能够将非 JavaScript 内容转换为 JavaScript 模块,以便可像其他任何 JavaScript 资源一样要求它。可以使用 webpack 加载器完成许多种类的任务;我们利用了 。没有 html-loader,我们就需要采用一个构建步骤来搜索所有 HTML 文件,并将它们注入到 Angular $templateCache 中,以便在指令使用 templateUrl 属性时,可以找到相应的 HTML。
浏览所有指令并将 templateUrl:'/directive/markup.html' 替换为 require('./markup.html') 需要花点时间,但最终结果会变得更容易维护。现在,我们在开发期间就知道是否错误地引用了模板,而不会在构建时才发现引用路径偏移了一个目录级别。
要在您的项目中使用 html-loader,可运行以下命令来安装它:
npm install --save-dev html-loader |
|
|
|
|
|