Skip to content

构建argular.js应用的最佳实践

jnotnull edited this page Jul 20, 2014 · 24 revisions

Burke Holland had a fantastic post explaining how Angular loads an application and comparing the merits of browserify vs require.js in an Angular app. Burke Holland之前发了一篇文章解释了Angular如何构建应用,并且比较了browserify和require.js在Angular中的优点。

I’ve worked with Angular on quite a few apps at this point, and have seen many different ways to structure them. I’m writing a book on architecting Angular apps right now with the MEAN stack and as such have researched heavily into this specific topic. I think I’ve set on a pretty specific structure I’m very happy with. It’s a simpler approach than what Burke Holland has proposed. 关于这点,其实我已经在好多项目中也涉及到了,我也找到了好多方法构建他们。我正在写一本如何通过MEAN来构建Angular应用的书,因此对这个课题研究的也不叫多。我想我已经找到了一个非常具体的框架。它比Burke Holland提议的简单的多。

I must note that if I was on a project with his structure, I would be content. It’s good. 特别说明的是,如果我在一个使用他的框架的项目中,我也会很满足的。它还是很不错的选择的。

Before we start though, the concept of modules in the world of Angular can be a bit confusing, so let me lay out the current state of affairs. 然而在我们开始之前,Angular中的模块概念还是有点扑朔迷离,让我们花点时间来澄清一下。

What modules are in JavaScript JS中的模块是什么 JavaScript comes with no ability to load modules. A “module” means different things to different people. For this article, let’s use this definition: JS本身没有加载模块的能力。模块的含义就是不同的人做不同的事情。在本文中,我们使用如下定义:

Modules allow code to be compartmentalized to provide logical separation for the developers. In JavaScript, it also prevents the problem of conflicting globals. 模块允许我们按照不同的逻辑把代码切割成不同的部分。在JS中,它还可以阻止global中的变量冲突问题。

People new to JavaScript get a little confused about why we make such a big deal about modules. I want to make one thing clear: Modules are NOT for lazy-loading JavaScript components when needed. Require.js does have this functionality, but that is not the reason it is important. Modules are important to due the language not having support for it, and JavaScript desperately needing it. JS开发新手可能会认为我们对模块有点小题大做。我想澄清一件问题:模块不是为了懒加载时候需要加载的模块。Require.js确实有这个功能,但是这个不是它重要的原因。模块之所以重要是因为语言没有提供这个功能,但是JS确实很需要这个功能。

A module can be different things. It could be Angular, lodash (you’re not still using underscore, are you?), shared code in your organization, some gist you found online, or separating features out inside your codebase. 模块可能由不同的框架组成。它可以是Angular,也可能是lodash,抑或是你项目中别人分享的代码,一些你冲网上找到的礼物,或者中你代码中分离出来的特性。

JavaScript doesn’t support modules, so we’ve traditionally had a few various approaches. (Feel free to skip this next section if you understand JavaScript modules) JS没有模块,因此我们必须通过一些途径去解决没有模块带来的问题(如果你理解了JS模块那你可以忽略掉接下来的章节)

.noConflict() .noConflict() Let me illustrate the problem. Let’s say you want to include jQuery in your project. jQuery will define the global variable ‘$’. If, in your code, you have an existing variable ‘$’ those variables will conflict. For years, we got around this problem with a .noConflict() function. Basically .noConflict() allows you to change the variable name of the library you’re using. 让我们来描述下问题。加入你想在你的项目中使用jQuery。jQuery会定义一个全局变量$。而恰好在你的代码中已经存在了这个变量,那么就会导致冲突。多年来,我们都考.noConfict()方法来解决这个问题。确实.noConfict()允许我们去改变你引用库的名称。

If you had this problem, you would use it like this: 如果你有这个问题,你可以这样使用:

<script> var $ = 'myobject that jquery will conflict with' </script> <script src='//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js'></script> <script> // $ is jQuery since we added that script tag var jq = jQuery.noConflict(); // $ is back to 'myobject that jquery will conflict with' // jq is now jQuery </script>

This has been a common practice in most JavaScript libraries, but it’s not a fantastic solution. It doesn’t provide very good compartmentalizing of code, it forces you to declare things before you use them, and it requires the imported code (either a library or your own code) to actually implement a .noConflict() function. 这已经成为大多数JS中最常见的解决方案,但却不是最优的解决方案。它没有很好的划分你的代码,它强制你在使用之前定义了一些东西,它要求导入的代码都要去实现一个.noConfict()方法。

If that’s confusing, read up on it. It’s important to understand the problem before you continue onto the solutions below. 如果你感觉有点疑惑,那接着向下读。在看下面的解决方案之前,去了解问题是非常重要的。

Nobody was happy with .noConflict(), so they started looking into other ways to solve the problem. We have 4 solutions worth mentioning in this context: 没人会喜欢使用.noConfict(),因此他们寻找了一些其他方法去解决问题。我们在这里提供了四种方案:

Require.js (Implementation of AMD) Browserify (Implementation of CommonJS) Angular dependency injection ES6 modules Require.js (AMD标准实现) Browserify (CommonJS标准实现) Angular dependency injection(Angular的依赖注入) ES6 modules(ES6的模块)

Each one has its pros and cons, and each works quite a bit differently. You can even use 1 or 2 in tandem (Burke used 2). I’ll cover what each does, how they work with Angular, and which one I suggest. 每个方案都有它的优点和缺点,他们每个都有所不同。你甚至一起使用他们。我将讲述他们每一个都提供了什么,如何配合Angular,我建议选择哪一个。

Sample App 样例 Let’s get a little Angular app together so we can talk about it. 让我们开始一个简单的Angular应用。

Here is a simple app that lists users off Github. 这里是获得Github用户李彪的简单实现。

The code is here, but it’s the completed version we will build in this post. Read through for no spoilers! 代码在这里,但是它是一个完整版本了。认真读完别剧透! All the JavaScript could be in this one file: 所有的代码可以在一个文件中: var app = angular.module('app', [])

app.factory('GithubSvc', function ($http) { return { fetchStories: function () { return $http.get('https://api.github.com/users') } } })

app.controller('GithubCtrl', function ($scope, GithubSvc) { GithubSvc.fetchStories().success(function (users) { $scope.users = users }) })

First we declare an ‘app’ object that is our module. We then define a service ‘GithubSvc’ with one function that can serve us users from Github. 首先我们定义了一个app对象,也就是我们的模块。然后我们定义了一个能提供github上用户离别的服务GithubSvc。

After that, we define a controller that uses the service to load that array into $scope. (This is the HTML page that renders it) 后面我们定义了一个控制器用来通过调用service去获得用户数组并加载到$scope中。

Splitting into separate files 把代码切割刀不同文件中 The trouble is that this code is all in one file. Totally unreasonable for a real app. Maybe I’m a curmudgeon, but when I first started looking at Angular and the code samples all showed how to do this, all I wanted to see was a real world solution with proper separation. 这里的代码都在一个文件中,这太糟糕了。这对于一个app来说绝对不可以的。也许我是一个倔老头,但是当我第一次看Angular和它的样例都解释是这样做的,我想说的是合理的去分割代码真的很有必要。

I would like to have this code in a structure like this: 我想我的代码有下面这样的结构: src/module.js src/github/github.svc.js src/github/github.ctrl.js

Note: If this app got large, it might make sense to have a separate ‘github’ module as well. 注:如果应用变得很大,我们还需要分解gitbub模块 The alternate way to do this would be to split things out by functionality rather than part of the codebase: 一个可行的办法就是按照功能去分解。 src/module.js src/services/github.svc.js src/controllers/github.ctrl.js

I don’t have a strong preference either way. Probably very large apps would benefit from the former, and smaller ones the latter. 无论哪种我都能能接受。一个大的应用可能倾向于前者,而小的引用则倾向于后者。

Regardless, without using a module loader like browserify or require.js, we would have to add a script tag for every one of these files. That’s a no go. That could easily grow to hundreds of files. 但是,如果我们不使用browserify或者require.js加载器,那我们必须通过添加script标签去加载每个文件,这可能很快增长到上百个文件。

There are performance reasons why you don’t want to have tons of script tags too. The browser does pipeline them, but it can only do so many at a time. They have overhead, and the latency would be killer to our friends outside of California. 因为有性能原因,我们不能有太多的script标签,浏览器是链式加载的,但是在一个时间点只能加载一个。糟糕的速度可能会让我们的用户流失。

So here is the goal: 因此这是目标:

We need a way to have many Angular files in dev, but they need to be loaded into the browser in bulk (not a script tag for each one). 我们需要在开发的时候拥有多个文件,但是在浏览器运行时候可以按需加载(不是通过script标签去一个一个加载)

This is why people look to module loaders like require.js or browserify. Angular allows you to logically separate out code, but not files. I’m going to show an easier way, but first let’s examine the available module loaders. 这就是为啥我们研究诸如require.js和browserify。Angular允许你按照逻辑去分割代码,而不是按照文件。我将给个更加简单的方法,但是首先让我们比较下当前可用的模块加载器。

Require.js — Too complicated Require.js-太复杂 Require.js was the first major push towards coming up with a consistent way to have modules inside of JavaScript. Require.js allows you to define dependencies inside a JavaScript file that you depend on. It runs inside the browser and is capable of loading modules as needed. Require.js在推动JS模块化上功不可没。它允许你把需要的JS文件定义为依赖,同时可以在浏览器端按需加载。

It accomplishes 2 general tasks, loading of modules and handling the load order. 它完成两项任务:加载模块和控制加载的顺序。

Unfortunately it’s really complicated to setup, requires your code to be written in a specific way, certainly has the steepest learning curve, and can’t deal with circular dependencies well — and that can happen when trying to use a module system on top of Angular. 不幸的是,这要求你的代码必须用一种特殊的方式去写,而这是有陡峭的学习曲线的,我们可能对循环引用控制的不好。而这个就可能发生在Angular的顶层模块系统中。

Burke Holland covered the issues with using require.js with Angular very well, so I encourage you to read that for a clearer reason why you should not use Angular with require.js. Burke Holland列出了在Angular中使用require.js的场景,因此我建议你再认真读一遍,最好能找出一个你不用require.js的理由。

Working with RequireJS and AngularJS was a vacation on Shutter Island. On the surface everything looks very normal. Under that surface is Ben Kingsley and a series of horrific flashbacks. — Burke Holland RequireJS和AngularJS一起使用就像是在禁闭岛上度假。水面上看起来一切非常平静,但是水下确实暗流涌动-Burke Holland

The ability for require.js to load modules on demand is also something that won’t work with Angular (at least, in a reasonable situation). That seems to be something people want, but I’ve certainly never worked on a project that needed it. require.js的按需加载模块的能力是另一个我不想它和Angular一起使用的原因。这个可能是一些人想要的,但是我绝对不会再任何项目中这样做。

I want to emphasize that last point as people get this wrong: Module systems are not so that you only load the code you need. Yes require.js does do that, but it’s not why require.js is useful. Modules are useful to logically separate code for developers to reason about it easier. 我要强调一点人们经常犯的错误:模块系统不仅仅是按照需要加载模块。是的,require.js确实这么做的,但是这不是require.js有用的原因。模块是用于按照逻辑来分解代码。

In any case, it’s a bad solution and I won’t show you how to do it. I bring it up because people often ask me how to integrate require.js with Angular. 不管怎么说,这是一个糟糕的方案,因此我不会告诉你早呢么做。我为什么要说呢,因为总有人问我如何集成require.js和Angular。

Browserify — A much better module loader Browserify-一个更好的模块加载器 Where require.js has the browser load the modules, browserify runs on the server before it runs in the browser. You can’t take a browserify file and run it in a browser, you have to ‘bundle’ it first.

It uses a similar format (and is almost 100% compatible with) the Node.js module loading. It looks like this:

Browserify example It’s a really pretty, easy to read format. You simply declare a variable and ‘require()’ your module into it. Writing code that exports a module is very easy too.

In Node, it’s great. The reason it can’t work in the browser, however, is that it’s synchronous. The browser would have to wait when hitting one of those require sections, then make an http call to load the code in. Synchronous http in a browser is an absolute no-no.

It works in Node since the files are on the local filesystem, so the time it takes to do one of those ‘requires()’ is very fast.

So with browserify, you can take code like this and run it with browserify and it will combine all the files together in a bundle that the browser can use. Once again, Burke’s article covers using browserify with Angular very well.

By the way, if everything I just said about browserify is confusing, don’t worry about it. It’s certainly more confusing than the solution I’m about to propose.

It is a great tool I would jump to use on a non-Angular project. With Angular, however, we can do something simpler.

Angular Dependency Injection — Solves most of our problems Go back and look at our sample app’s app.js. I want to point out a couple of things:

It doesn’t matter what order we create the service or the controller. Angular handles that for us with its built-in Dependency Injection. It also allows us to do things like mocking out the service in a unit test. It’s great, and my number one favorite feature inside Angular.

Having said that, with this method, we do need to declare the module first to use that ‘app’ object. It’s the only place that order of declarations matter in Angular, but it’s important.

What I want to do, is simply concatenate all the files together into one, then require just that JavaScript file in our HTML. Because the app object has to be declared first, we just need to make sure that it’s declared before anything else.

Gulp Concat To do this, I will be using Gulp. Don’t worry about learning a newfangled tool though, I’m going to use it in a very simple way and you can easily port this over to Grunt, Make, or whatever build tool you want (shockingly, even asset pipeline). You just need something that can concat files.

I’ve played around with all the popular build systems and Gulp is far and away my favorite. When it comes to building css and javascript, specifically, it’s bliss.

You might be thinking I’m just replacing one build tool (browserify) with another (gulp), and you would be correct. Gulp, however, is much more general purpose. You can compose this Gulp config with other tools like minification, CoffeeScript precompilation (if you’re into that sort of thing), sourcemaps, rev hash appending, etc. Yes it’s nothing browserify can’t do, but once you learn how to do it with Gulp you can do the same on any other asset (like css). Ultimately it’s much less to learn.

You can use it to process png’s, compile your sass, start a dev node server, or running any code you can write in node. It’s easy to learn, and will provide a consistent interface to your other developers. It provides us a platform to extend on later.

I would much rather just type ‘gulp watch’ and have that properly watch all my static assets in dev mode than have to run ‘watchify’, a separate node server, a separate sass watcher, and whatever else you need to keep your static files up to date.

First I’ll install Gulp and gulp-concat (gotta be in the project and global):

$ npm install --global gulp $ npm install --save-dev gulp gulp-concat By the way, you’ll need a package.json in your app and have Node installed. Here’s a little trick I do to start my Node apps (npm init is too whiny):

$ echo '{}' > package.json Then toss in this gulpfile.js:

gulpfile.js This is a simple task that takes in the JavaScript files in src/ and concatenates them into app.js. Because it expects this array, any file named module.js will be included first. Don’t worry too much about understanding this code, when we get to minification I’ll clear it up.

If you want to play along at home, use these files, then run ‘gulp js’ to build the assets. Donezo.

For more on Gulp, read my article on setting up a full project with it.

Icky Globals We can do better. You know how you create that ‘app’ variable? That’s a global. Probably not a problem to have one ‘app’ global, but it might be a problem when we grow to have more and more modules, they may conflict.

Luckily Angular can solve this for us very easily. The function angular.module() is both a getter and a setter. If you call it with 2 arguments:

angular.module as a setter That’s a setter. You just created a module ‘app’ that has ‘ngRoute’ as a dependency. (I won’t be using ngRoute here, but I wanted to show what it looks like with a dependent module)

Calling that setter will also return the module as an object (that’s what we put into var app). Unfortunately you can only call it once. Disappointingly, getting this stuff wrong throws nasty error messages that can be frustrating to newbies. Stick to the xxx method and all will be good though.

If we call angular.module() with a single argument:

angular.module getter It’s a getter and also returns the module as an object, but we can call it as many times as we want. For this reason, we can rewrite our components from this:

Global module service Into this:

No globals involved The difference is subtle and might seem innocuous to new JavaScript developers. The advanced ones are nodding along now though. To maintain a large JavaScript codebase is to prevent the usage of globals.

To you pedants: I realize that there is still a global ‘angular’ object, but there’s almost certainly no point in avoiding that.

Here we have a pretty well functioning way to build the assets, but there are a few more steps we need to get to the point of a fine-tuned build environment. Namely, it’s a pain to have to run ‘gulp js’ every time we want to rebuild ‘app.js’.

Gulp Watch This is really easy, and I think the code speaks for itself (Lines 10-12):

Gulp with watching This just defines a ‘gulp watch’ task we can call that will fire off the ‘js’ task every time a file matching ‘src/**/*.js’ changes. Blammo.

Minification Alright, let’s talk minification. In Gulp we create streams from files (gulp.src), then pipe them through various tools (minification, concatenation, etc), and finally output them to a gulp.dest pipe. If you know unix pipes, this is the same philosophy.

In other words, we just need to add minification as a pipe. First, install gulp-uglify to minify:

$ npm install -D gulp-uglify

Gulp minification But we have a problem! It has munged the function argument names Angular needs to do dependency injection! Now our app doesn’t work. If you’re not familiar with this problem, read up.

We can either use the ugly array syntax in your code, or we can introduce ng-gulp-annotate.

NPM install:

$ npm install -D gulp-ng-annotate And here’s the new gulpfile:

Gulp minification with ng-annotate I hope you’re starting to see the value in Gulp here. How I can use a conventional format of Gulp plugins to quickly solve each of these build problems I am running into.

Sourcemaps Everyone loves their debugger. The issue with what we’ve built so far is that it’s now this minified hunk of JavaScript. If you want to console.log in chrome, or run a debugger, it won’t be able to show you relevant info.

Here’s a Gulp task that will do just that! (Install gulp-sourcemaps)

Sourcemaps!

Why Concat is Better Concat works better here because it’s simpler. Angular is handling all of the code loading for us, we just need to assist it with the files. So long as we get that module setter before the getters, we have nothing to worry about.

It’s also great because any new files we just add into the directory. No manifest like we would need in browserify. No dependencies like we would need in require.js.

It’s also just generally one less moving part, one less thing to learn.

What we built Here is the final code. It’s an awesome starting point to build out your Angular app.

It’s got structure. It’s got a dev server. It’s got minification. It’s got source maps. It’s got style. (The Vincent Chase kind, not the CSS kind) It doesn’t have globals. It doesn’t have shitloads of <script> tags. It doesn’t have a complex build setup. I tried to make this not about Gulp, but as you can tell: I freaking love the thing. As I mentioned earlier, you could achieve a similar setup with anything that can concat.

If there is interest, I could easily extend this to add testing/css/templates/etc. I already have the code.

Third-party code For third-party code: if it’s something available on a CDN (Google CDN, cdnjs, jsdelivr, etc), use that. If the user has already loaded it from another site, the browser will reuse it. They also have very long cache times.

If it’s something not available on a CDN, I would still probably use a new script tag but load it off the same server as the app code. Bower is good for keeping these sorts of things in check.

If you have a lot of third-party code, you should look into minifying and concatenating them like above, but I would keep it separate from your app code so you don’t have just one huge file.

ES6 Modules — The real solution The next version of JavaScript will solve this problem with built-in modules. They worked hard to ensure that it works well for both fans of CommonJS (browserify) and AMD (require.js). This version is a ways out, and you probably won’t be able to depend on the functionality without a shim of some kind for at least a year, probably a few. When it does come out, however, this post will be a relic explaining things you won’t need to worry about (or at least it’ll be horrifically incorrect).

Angular 2.0 It’s worth mentioning that Angular 2.0 will use ES6 modules, and at that point we’ll be in bliss. It’s nowhere close to release though, so for now, if you want to use Angular, you need a different option. Angular 2.0 will be a dream. It’s going to look a lot more like a series of useful packages than a framework, allowing you to pick and choose functionality, or bake them into an existing framework (like Ember or Backbone).

Angular 2.0 will use a separate library di.js that will handle all of this. It’s way simpler, and it’s only a light layer on top of ES6 modules. We should be able to easily use it in all apps, not just Angular apps. The unfortunate thing for you is that you will need to deal with the crufty state of affairs with JavaScript modules until then.

Man. I love all these great ways JavaScript is improving, but god damn is it a lot to keep learning.

P.S. I have some code samples you can use to asynchronously load an Angular app. Any interest in reading about that?

Clone this wiki locally