|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: 翻译javascript模块化入门 |
| 4 | +date: 2019-04-13 |
| 5 | +author: JY |
| 6 | +header-img: img/cat1_jsModule.jpg |
| 7 | +catalog: true |
| 8 | +tags: |
| 9 | + - 前端 |
| 10 | + - javascript模块化 |
| 11 | +--- |
| 12 | +# 前言 |
| 13 | +如果你是一个javascript新手,上来就接触到一些术语诸如“module bundlers vs. module loaders,” “Webpack vs. Browserify” and “AMD vs. CommonJS”总是会感到很懵 |
| 14 | +虽然这些属于听着很让人望而却步,但是掌握它们对于javascript开发者来说还是很重要的。 |
| 15 | +这篇文章用简单的文字和代码示例介绍刚刚提到的一些术语,希望对大家有帮助 |
| 16 | +为了简单,本文会分成两部分。1.模块是啥,为啥我们要用到2.模块打包意味着什么,有哪些不同的方式 |
| 17 | + |
| 18 | + |
| 19 | +##part1 |
| 20 | +### 为啥用模块 |
| 21 | +1.易于维护 |
| 22 | +2.命名空间,不污染全局变量 |
| 23 | +3.可重用性 |
| 24 | +### 几个javascript模块的例子 |
| 25 | +模块范式是用来模仿class的概念的(javascript天生不支持class),所以我们能够像Java一样在一个对象里面存变量,公有方法,私有方法。 |
| 26 | +有很多方法可以让我们实现刚才所说的。第一个例子是一个匿名闭包 |
| 27 | +####例子1 |
| 28 | +``` |
| 29 | +(function () { |
| 30 | + // We keep these variables private inside this closure scope |
| 31 | + |
| 32 | + var myGrades = [93, 95, 88, 0, 55, 91]; |
| 33 | + |
| 34 | + var average = function() { |
| 35 | + var total = myGrades.reduce(function(accumulator, item) { |
| 36 | + return accumulator + item}, 0); |
| 37 | + |
| 38 | + return 'Your average grade is ' + total / myGrades.length + '.'; |
| 39 | + } |
| 40 | +
|
| 41 | + var failing = function(){ |
| 42 | + var failingGrades = myGrades.filter(function(item) { |
| 43 | + return item < 70;}); |
| 44 | + |
| 45 | + return 'You failed ' + failingGrades.length + ' times.'; |
| 46 | + } |
| 47 | +
|
| 48 | + console.log(failing()); |
| 49 | +
|
| 50 | +}()); |
| 51 | +``` |
| 52 | +这样一来所有执行过程中产生的变量都在这个匿名的闭包里面了,就不会污染全局变量,这就是刚刚提到的第二点。 |
| 53 | +值得注意的是,在刚刚的例子中最外面的括号是必须有的。因为如果没有括号的话这个函数就属于函数声明(必须有名字),但是加了括号就代表这是一个函数表达式,不需要名字。 |
| 54 | +####例子2 |
| 55 | +我们还可以给模块传递一个全局变量,这样模块会把公有方法放在全局变量里面,Jquery就是用这样的方式 |
| 56 | +``` |
| 57 | +(function (globalVariable) { |
| 58 | +
|
| 59 | + // Keep this variables private inside this closure scope |
| 60 | + var privateFunction = function() { |
| 61 | + console.log('Shhhh, this is private!'); |
| 62 | + } |
| 63 | +
|
| 64 | + // Expose the below methods via the globalVariable interface while |
| 65 | + // hiding the implementation of the method within the |
| 66 | + // function() block |
| 67 | +
|
| 68 | + globalVariable.each = function(collection, iterator) { |
| 69 | + if (Array.isArray(collection)) { |
| 70 | + for (var i = 0; i < collection.length; i++) { |
| 71 | + iterator(collection[i], i, collection); |
| 72 | + } |
| 73 | + } else { |
| 74 | + for (var key in collection) { |
| 75 | + iterator(collection[key], key, collection); |
| 76 | + } |
| 77 | + } |
| 78 | + }; |
| 79 | +
|
| 80 | + globalVariable.filter = function(collection, test) { |
| 81 | + var filtered = []; |
| 82 | + globalVariable.each(collection, function(item) { |
| 83 | + if (test(item)) { |
| 84 | + filtered.push(item); |
| 85 | + } |
| 86 | + }); |
| 87 | + return filtered; |
| 88 | + }; |
| 89 | +
|
| 90 | + globalVariable.map = function(collection, iterator) { |
| 91 | + var mapped = []; |
| 92 | + globalUtils.each(collection, function(value, key, collection) { |
| 93 | + mapped.push(iterator(value)); |
| 94 | + }); |
| 95 | + return mapped; |
| 96 | + }; |
| 97 | +
|
| 98 | + globalVariable.reduce = function(collection, iterator, accumulator) { |
| 99 | + var startingValueMissing = accumulator === undefined; |
| 100 | +
|
| 101 | + globalVariable.each(collection, function(item) { |
| 102 | + if(startingValueMissing) { |
| 103 | + accumulator = item; |
| 104 | + startingValueMissing = false; |
| 105 | + } else { |
| 106 | + accumulator = iterator(accumulator, item); |
| 107 | + } |
| 108 | + }); |
| 109 | +
|
| 110 | + return accumulator; |
| 111 | +
|
| 112 | + }; |
| 113 | +
|
| 114 | + }(globalVariable)); |
| 115 | +``` |
| 116 | +####例子3 |
| 117 | +对象接口,从这个例子里面可以看出myGrade是私有变量,average,failing是共有方法 |
| 118 | +``` |
| 119 | +var myGradesCalculate = (function () { |
| 120 | + |
| 121 | + // Keep this variable private inside this closure scope |
| 122 | + var myGrades = [93, 95, 88, 0, 55, 91]; |
| 123 | +
|
| 124 | + // Expose these functions via an interface while hiding |
| 125 | + // the implementation of the module within the function() block |
| 126 | +
|
| 127 | + return { |
| 128 | + average: function() { |
| 129 | + var total = myGrades.reduce(function(accumulator, item) { |
| 130 | + return accumulator + item; |
| 131 | + }, 0); |
| 132 | + |
| 133 | + return'Your average grade is ' + total / myGrades.length + '.'; |
| 134 | + }, |
| 135 | +
|
| 136 | + failing: function() { |
| 137 | + var failingGrades = myGrades.filter(function(item) { |
| 138 | + return item < 70; |
| 139 | + }); |
| 140 | +
|
| 141 | + return 'You failed ' + failingGrades.length + ' times.'; |
| 142 | + } |
| 143 | + } |
| 144 | +})(); |
| 145 | +
|
| 146 | +myGradesCalculate.failing(); // 'You failed 2 times.' |
| 147 | +myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.' |
| 148 | +``` |
| 149 | +### CommonJS和AMD |
| 150 | +刚刚的例子有一个共同之处:用一个全局的function创建了一个闭包,在闭包里面使用私有的命名空间。 |
| 151 | +但是这些例子中都有一个缺陷。 |
| 152 | +作为一个开发者,你需要知道正确的依赖顺序来加载你的文件。比如,你在项目中用到了Backbone,所以用<script>引用了它的源码。 |
| 153 | +然而,Backbone强依赖Underscore.js。那你就需要把对Underscore的应用加在Backbone前面。一旦依赖变多了,这样的事情会变得很复杂 |
| 154 | +还有一个问题这样仍然有可能导致全局变量冲突,比如例子3中其他模块也有叫myGradesCalculate的. |
| 155 | +那有没有办法啊可以让这些模块不在全局作用域里面呢?答案是肯定的。有两种关于这个的著名的实现CommonJS, AMD |
| 156 | +#### CommonJS |
| 157 | +CommonJs是设计和实现Javascript模块声明API的一个志愿者工作组 |
| 158 | +CommonJs模块其实是一个可重用的一坨javascript,这坨javascript会export一些特定对象。这样就能被其他模块require进来。写过Node.js的朋友应该很熟悉。 |
| 159 | +下面是一个定义CommonJs模块的例子 |
| 160 | +``` |
| 161 | +function myModule() { |
| 162 | + this.hello = function() { |
| 163 | + return 'hello!'; |
| 164 | + } |
| 165 | +
|
| 166 | + this.goodbye = function() { |
| 167 | + return 'goodbye!'; |
| 168 | + } |
| 169 | +} |
| 170 | +
|
| 171 | +module.exports = myModule; |
| 172 | +``` |
| 173 | +其他模块如果下昂要引用的话就像这样 |
| 174 | +``` |
| 175 | +var myModule = require('myModule'); |
| 176 | +
|
| 177 | +var myModuleInstance = new myModule(); |
| 178 | +myModuleInstance.hello(); // 'hello!' |
| 179 | +myModuleInstance.goodbye(); // 'goodbye!' |
| 180 | +``` |
| 181 | +这种方式很好的解决了刚才剔除的缺陷。 |
| 182 | +1.由于用到的模块都需要require()所以依赖关系就变得清晰 |
| 183 | +2.每个模块不需要写全局变量 |
| 184 | +这里需要注意是require()是阻塞的。而javascript又是单线程的,这在server端通常没什么问题因为require的时候只是去读磁盘中的文件然后加载。但是在浏览器中这意味着一个网络请求,唯一的线程会一直等到require的模块文件返回load完毕。 |
| 185 | +### AMD |
| 186 | +接着刚才的问题说。如果在浏览器端能够异步加载模块这样不就好了吗。AMD正式解决这样一个问题的技术,全称是Asynchronous Module Definition。 |
| 187 | +下面给出一个AMD的例子 |
| 188 | +``` |
| 189 | +define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) { |
| 190 | + console.log(myModule.hello()); |
| 191 | +}); |
| 192 | +``` |
| 193 | +define函数的第一个参数是所需要加载的模块。第二个参数是回调函数,刚刚加载好的模块就会作为args传进回调函数。 |
| 194 | +简单来说AMD主要是适用于浏览器。 |
| 195 | +### UMD |
| 196 | +有些项目需要你同时支持AMD和CommonJS功能,那么就可以用UMD(Universal Module Definition) |
| 197 | +UMD支持刚刚说到的两种模块和定义在全局变量的模块。并且即可以在服务端也可以在浏览器端执行。 |
| 198 | +下面是一个UMD的例子: |
| 199 | +``` |
| 200 | +(function (root, factory) { |
| 201 | + if (typeof define === 'function' && define.amd) { |
| 202 | + // AMD |
| 203 | + define(['myModule', 'myOtherModule'], factory); |
| 204 | + } else if (typeof exports === 'object') { |
| 205 | + // CommonJS |
| 206 | + module.exports = factory(require('myModule'), require('myOtherModule')); |
| 207 | + } else { |
| 208 | + // Browser globals (Note: root is window) |
| 209 | + root.returnExports = factory(root.myModule, root.myOtherModule); |
| 210 | + } |
| 211 | +}(this, function (myModule, myOtherModule) { |
| 212 | + // Methods |
| 213 | + function notHelloOrGoodbye(){}; // A private method |
| 214 | + function hello(){}; // A public method because it's returned (see below) |
| 215 | + function goodbye(){}; // A public method because it's returned (see below) |
| 216 | +
|
| 217 | + // Exposed public methods |
| 218 | + return { |
| 219 | + hello: hello, |
| 220 | + goodbye: goodbye |
| 221 | + } |
| 222 | +})); |
| 223 | +``` |
| 224 | +### ES6 |
| 225 | +刚才我们说的所有的打包方式都不是javascript原生支持的。在ES6对语法和语义的定义中,引入了模块的定义。 |
| 226 | +ES6中的模块汲取了CommonJS和AMD的优点,声明式的语法和异步加载,和对循环依赖更好的支持。 |
| 227 | +还有很厉害的一点是ES6模块的引入是对导出内容的实时只读(这句比较难翻,需要看下面代码理解)。在CommonJS中,import只是对export的拷贝。这是CommonJs的例子 |
| 228 | +``` |
| 229 | +// lib/counter.js |
| 230 | +var counter = 1; |
| 231 | +
|
| 232 | +function increment() { |
| 233 | + counter++; |
| 234 | +} |
| 235 | +
|
| 236 | +function decrement() { |
| 237 | + counter--; |
| 238 | +} |
| 239 | +
|
| 240 | +module.exports = { |
| 241 | + counter: counter, |
| 242 | + increment: increment, |
| 243 | + decrement: decrement |
| 244 | +}; |
| 245 | +
|
| 246 | +
|
| 247 | +// src/main.js |
| 248 | +
|
| 249 | +var counter = require('../../lib/counter'); |
| 250 | +
|
| 251 | +counter.increment(); |
| 252 | +console.log(counter.counter); // 1 |
| 253 | +``` |
| 254 | +为啥输出结果不是2呢。因为increment()被调用的时候被改写的counter是在counter.js里面的变量。而在main.js require过来的counter是counter.js里面的变量的拷贝,所以我们无法通过调用increment()改变require过来的count |
| 255 | +ES6很好的解决了这个问题 |
| 256 | +``` |
| 257 | +// lib/counter.js |
| 258 | +export let counter = 1; |
| 259 | +
|
| 260 | +export function increment() { |
| 261 | + counter++; |
| 262 | +} |
| 263 | +
|
| 264 | +export function decrement() { |
| 265 | + counter--; |
| 266 | +} |
| 267 | +
|
| 268 | +
|
| 269 | +// src/main.js |
| 270 | +import * as counter from '../../counter'; |
| 271 | +
|
| 272 | +console.log(counter.counter); // 1 |
| 273 | +counter.increment(); |
| 274 | +console.log(counter.counter); // 2 |
| 275 | +``` |
| 276 | +也就是说ES6的import并不是对export建立拷贝 |
| 277 | + |
| 278 | +##part2 |
| 279 | +### 为啥要打包模块呢 |
| 280 | +当你把你的程序分成多个模块,你通常要把这些模块组织成不同的文件夹和文件。比如在你使用React的时候你有一组你所使用的库的模块 |
| 281 | +那你就需要在你的主html为每一个你所依赖的模块加<script>标签来把这些依赖引入进来。当用户打开你的页面的时候浏览器就会一个一个的单独加载这些<script> |
| 282 | +这样页面加载速度就会很慢。 |
| 283 | + |
| 284 | +为了克服这个问题,我们捆绑或者说把这些文件混合进一个大文件(也可能是多个文件)为了减少http请求数。当我们听到其他开发者谈论构建阶段,构建过程。其实指的就是这些事。 |
| 285 | +还有一个加快打包速度的方法就是减少打包出来的代码量. 减少代码量的办法就是去除源代码中不必要的字符(比如空格,注解,换行符)这样就可以减少整体大小而不改变代码的功能。 |
| 286 | +数据量越小意味着浏览器能够更快的下载,加载这些文件。如果你见过文件里面带个min的后缀比如 “underscore-min.js”,你可能发现了min版本比起完全版小得多(虽然min版的代码不便阅读/调试)。 |
| 287 | +打包工具比如gulp,grunt让混合,压缩变得简单直接,并且保证了刻度的代码仍然可以让开发者看到而浏览器读到的是打包,压缩过的代码。 |
| 288 | + |
| 289 | +### 有哪些不同的打包模块的方式呢 |
| 290 | +只要你使用了一种标准的方式定义模块。就可以方便的混合,压缩你的javascript代码。 |
| 291 | +然而如果你使用了你的浏览器无法解释的非原生的模块系统,比如CommonJS, AMD (甚至是es6的原生模块语法)。你需要一个专门的工具来吧这些模块组织成能浏览器能够解释执行的的代码。这些就是Browserify, RequireJS, Webpack以及一些其他的模块打包工具做的事情。 |
| 292 | +除了打包加载你的模块之外,模块打包工具提供了很多额外的功能比如自动重新编译当你在修改代码和debug的时候。 |
| 293 | + |
| 294 | +### 打包CommonJS |
| 295 | +CommonJs使用同步的方式加载模块,这样虽然很好但是对浏览器来说却是不现实的。比如我们有个main.js应用了一个模块来计算平均数: |
| 296 | +``` |
| 297 | +var myDependency = require(‘myDependency’); |
| 298 | +
|
| 299 | +var myGrades = [93, 95, 88, 0, 91]; |
| 300 | +
|
| 301 | +var myAverageGrade = myDependency.average(myGrades); |
| 302 | +``` |
| 303 | +这个例子里面,我们的代码依赖了myDependency模块。在以下命令里面。浏览器递归的打包了所有依赖的模块,最终生成了bundle.js |
| 304 | +``` |
| 305 | +browserify main.js -o bundle.js |
| 306 | +``` |
| 307 | +Browserify 会先根据当前代码生成一个抽象语法树,这样就可以遍历整个项目依赖图。理清楚了项目的依赖结果之后,就可以按照顺序打包所有的依赖到一个文件里面了。这个时候你就只需要引入单个<script>进你的html代码。 |
| 308 | +接下来你可以用Minify-JS去压缩你的bundle.js。完成! |
| 309 | + |
| 310 | +### 打包AMD |
| 311 | +写不动了,未完待续。。 |
0 commit comments