1. npm run webpack 和npm run dev的区别
npm run webpack是打包生成一个bundle.js文件,需要我们在引入了这个bundle.js的HTML文件,每一次被打包的js文件有修改了都要刷新html文件才会显示。
npm run dev则是对于devServe的配置,我们在配置中添加了devServe对象,他是专门用来放webpack-dev-serve配置的。webpack-dev-serve可以看成是服务器,他的主要工作就是接受浏览器的请求,然后将资源返回。当服务启动时,会先让Webpack进行模块打包并将资源准备好(即将bundle.js放在了publicPath路径下)。当webpack-dev-serve接收到浏览器的请求时(通过script标签发出),会进行URL地址校验。如果该地址是资源服务地址(webpack-dev-serve配置中的publicPath),就会将最新的打包结果放回给浏览器,这样我们修改的时候就不用每次都刷新页面进行调试了。反之,如果请求地址不是资源服务地址,那么就直接读取硬盘中的源文件并将其返回。
这里有一点需要注意,直接用Webpack开发和webpack-dev-serve开发有很大的区别。前者每次都会生成bundle.js,而webpack-dev-serve只是将打包结果放在内存里,并不会写入实际的bundle.js文件,只是将内存中的打包结果返回给浏览器。
这其实很好理解,本地开发阶段经常会调整目录结构和文件名,如果每次写入实际文件都会产生一些垃圾文件,这会干扰我们进行版本控制。
webpack-dev-serve的另一个便捷的特性就是自动刷新,每次js文件都会自动更新。后续会讲到更先进的模块热替换,我们甚至不需要刷新就能获取更新后的内容
2.src/index.html和src/index.js有什么区别
Webpack打包的是js文件,也在配置文件中写明了index.js是入口文件,即打包出来的bundle.js是在index.js上演变来得。而index.html就是引入了bundle.js的文件,即index.html引入了bundle.js的模板文件
3. 模块
模块之于程序,就如细胞之于生物体。将一个JavaScript文件直接通过script标签插入页面与封装成CommonJS模块的最大不同就是,前者的顶层作用域是全局作用域,在进行变量及函数声明是会污染全局变量;而后者会形成一个属于模块自身的作用域,所有变量和函数只有自己能访问,对外是不可见的
CommonJS
1.导出
导出是模块向外暴露自身的唯一方式。在CommonJS中,通过module.exports可以导出模块中的内容
module.exports = {
name:'calculater
}
CommonJS模块内部会有一个module对象用于存放当前模块信息,可以理解成在每个模块的最开始定义了以下对象
var module = {}
//模块自身逻辑
module.exports = {}
module.export用来指定该模块要对外暴露哪些内容,可以理解成module.exports一个对象,存储进里面的内容都会被暴露出去,同时CommonJS也支持一种简化的导出方式--直接使用exports
export.name = 'calculater'
在实现效果上,这两个没有什么不同,其本质机制是将exports指向module.exports,而module.exports在初始化时是一个空对象。可以简单地理解为,在每个模块首部都默认添加了以下代码:
var module = {
exports:{}
}
var exports = module.exports
因此,exports.add赋值就相当于在module.exports对象上添加了一个属性,然后将module.exports暴露出去。
在理解了导出的原理之后,我们就很容易知道两个误区:
export = {
name:'calculater'
}
由于对exports进行了赋值操作,使其指向了新对象,但我们导出的实际是module.exports对象,此时的赋值操作并没有将name属性存进module.exports对象中,因此name属性不会被导出。
exports.add = function(){}
module.export = {
name:'calculater
}
上面的代码通过export将add属性添加到module.exports中,但是在后续的赋值中将module.exports赋值到了另一个对象,导致add属性丢失,最终导出的只有name
2.导入
在CommonJS中,通过require导入暴露的模块,通过require获得的就是module.exports这个对象。require一个模块时会有两种情况:
- require的模块是第一次被加载。这时会首先执行该模块里的代码,然后获得导出内容。
- require的模块曾被加载过。这时该模块的代码不会被执行,而是直接获得上次执行后的结果
这是因为导出的module对象里有一个属性loaded用于记录该模块是否被加载过。他的默认值是false,当模块第一次被加载和执行后会置为true,后面再次加载时检查到module.loaded为true,则不会再次执行模块代码。
ES6 Module
CommoJS是社区涌现出来的模块标准,但直到2015年6月,由TC39标准委员会正式发布了ES6,从此JavaScript语言才具备了模块这一特性。
ES6 Module会自动采用严格模式,如果原本是CommonJS的模块或者未开启严格模式的代码改写成ES6 Module要注意这点
1.导出
ES6 Module通过export命令来导出,export有两种形式
- 命名导出
- 默认导出
//写法1
export const name = 'calculator'
//写法2
const name = 'calculator'
const add = 'addFunction'
export {name,add}
//可以用as关键字对变量重命名
export {name,add as getSum}
第一种写法是将变量的声明和导出放在同一行;第二种则是先进行变量声明,再用同一个export语句导出。这两种写法是一样的,可以通过as关键字对变量重命名
与命名导出不同,模块的默认导出只能有一个。如:
export default {
name:'calculator'
}
//可以理解为向外导出一个名为default的变量,所以不需要像命名导出一样进行变量声明,直接导出值即可
export default 'this is string'
2.导入
ES6 Module中使用import语法导入模块,针对带命名导出的模块
import {name,add} from './calculator.js'
加载带命名导出的模块时,import后面要跟一对大括号将导入的变量名包裹起来,并且变量名和导出的变量完全一致。并且导入的变量名是只读的,不能对其进行修改。当然,可以通过as关键字对导入的变量重命名
import {name as myName} from './calculator.js'
在导入多个变量时,我们可以采用整体导入的方式
import * as calculator from './calculator'
import * as myModule可以将所有导入的变量作为属性值添加到myModule对象中,从而减少对当前作用域的影响。
针对默认导出来说,import后面直接更变量名,并且这个名字可以自由指定,相当于将默认导出的对象赋值给这个名字。
import LBJ from './calculator'
3.复合写法
在工程中,有时需要将某一个模块导入之后立刻导出,比如专门用来集合所有页面或者组件的入口文件,此时可以采用复合形式的语法
export {name} from './calculator.js'
复合写法目前只支持命名导出的方式暴露,默认导出只能将导入和导出分开写
import calculator from './calculator'
export default calculator
4.CommonJS和ES6 Module的区别
1.动态和静态
CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是动态的,而后者是静态的。在这里,动态的含义是,模块依赖关系的建立是发生在代码运行阶段;而静态则是模块依赖关系的建立发生在代码编译阶段。
在CommonJS中,当模块A加载模块B时,会执行B的代码,并将module.exports对象作为require函数的返回值返回。并且require的模块路径可以动态指定,支持传入一个表达式,我们甚至可以通过if语句判断是否加载某个模块。因此在代码没有被执行前,都没有办法确定明确的依赖关系、
而在ES6 Module中,导入和导出都是声明式的,他不支持导入的路径是一个表达式,导出语句必须放在模块的顶层作用域(比如不能放在if语句中,不是要放在代码最上面)。因此在编译阶段我们就可以确定模块的依赖关系。相比CommonJS来说有以下几个优点:1.死代码检测和排除 2.模块变量类型检查 3.编译器优化
2.值拷贝与动态映射
在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的
在CommonJS中,通过require导入的module.exports对象中的值仅仅是一份值拷贝,即便我们改变了这个值,也不会对导出的模块造成影响。同样导出的模块的值改变了也不会对我们有影响。
在ES6 Module中,导入的变量实际上是对原有值的动态映射,import A from B,动态映射的意思是,当B中的值改变时,A的值也会跟着改变。并且我们不能修改A。
3.循环依赖
一般工程中应该避免循环依赖,但是如果项目复杂到一定规模时,可能会出现A依赖于B,B依赖于C,C依赖于D,D又依赖于A。如何处理循环依赖是开发者必须面对的问题,我们看一下在CommonJS中循环依赖的例子
//foo.js
const bar = require('./bar.js')
console.log('value of bar:',bar);
module.exports = 'this is foo.js'
//bar.js
const foo = require('./foo.js')
console.log('value of foo:',foo);
module.exports = 'this is bar.js'
//index.js
require('./foo.js')
在这里,index.js是执行入口,它加载了foo.js,foo.js和bar.js之间存在循环依赖,理论上我们希望二者都能导入正确的值,并在控制台输出
value of foo:this is foo.js
value of bar:this is bar.js
但实际输出却是
value of foo:{}
value of bar:this is bar.js
原因是执行foo.js时,第一句导入了bar.js,这时候foo.js就不会向下执行了,而是开始执行bar.js。在bar.js中又导入了foo.js,由于我们在一开始就已经导入过foo.js,因此我们直接取他的导出值module.exports,但是foo.js还没有执行完毕,导出值为默认的空对象,因此bar.js执行到打印语句的时候看到value of foo是一个空对象。
将上面的代码改写成ES6 Module同样无法获得正确值,只是和CommonJS默认导出一个空对象不同,这里获取到的是undefined
value of foo:undefined
value of bar:this is bar.js
ES6 Module的导入方式是动态映射,我们可以利用这个特性来解决循环依赖
//index.js
import foo from './foo.js'
foo('index.js')
//foo.js
import bar from './bar.js'
function foo(invoker){
console.log(invoker + 'invokes foo.js');
bar('foo.js')
}
export default foo
//bar.js
import foo from './foo.js'
let invoked = false
function bar(invoker){
if(!invoked){
invoked = true
console.log(invoker + 'invokes bar.js');
foo(bar.js)
}
}
export default bar
执行结果如下
index.js invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js
这样每个模块都获取到了正确的值。分析如下,也解释了CommonJS解决不了循环依赖的问题