前端模块化
模块化的概念
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化的好处
当引入多个
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
模块化规范
1. CommonJS
Node 应用由模块组成,采用 CommonJS 模块规范。CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
- 基本语法
- 暴露模块:
module.exports = value
或exports.xxx = value
- 引入模块:
require(xxx)
,如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
- 暴露模块:
2. AMD
AMD规范则是异步加载模块,允许指定回调函数。如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
3. CMD
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
4. ES6模块化
export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
ES6 模块与 CommonJS 模块的差异
它们有两个重大差异:
① CommonJS 模块输出的是一个值的拷贝,赋值过程,ES6 模块输出的是值的引用,解构过程。
② CommonJS(require) 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
webpack的作用是什么?
- 模块打包
- 根据依赖图的依赖关系,将不同模块的文件打包整合在一起,并保证它们之间的引用正确,执行有序。
- 编译兼容
- webpack的
loader
机制可以编译转换.less, .vue, .jsx
等浏览器无法识别的格式文件,提高开发效率
- webpack的
- 能力扩展
- webpack的
Plugin
机制,在实现模块化打包和编译兼容的基础上,通过按需加载来拓展解决loader无法实现的其他事,比如实际的监听等。
- webpack的
webpack的几个关键概念
- Entry:入口文件, 可以是一个或多个入口文件,指示webpack应该使用哪些模块作为构建依赖图的开始
- Output:输出文件,告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件
- Loaders:加载器,本质上是函数,接收资源文件进行转换,并返回新的文件
- Plugins:插件,扩展功能,实现事件监听等
- Mode:模式,通过选择
development
,production
或none
之中的一个,来设置环境
模块化打包运行的流程?
问:webpack
是如何把这些模块合并到一起,并保证正常工作的?
首先需要了解webpack的打包流程:
- 初始化参数
- 读取
webpack
的配置参数;
- 读取
- 开始编译
- 启动
webpack
,创建Compiler
对象并开始解析项目;
- 启动
- 确定入口
- 从入口文件
entry
开始解析,构建其导入文件的依赖图,递归分析形成依赖关系树;
- 从入口文件
- 编译模板
- 对不同文件类型的依赖模块文件使用对应的
Loader
进行编译,最终转为Javascript
文件;
- 对不同文件类型的依赖模块文件使用对应的
- 完成模板编译并输出
- 根据入口文件之间的依赖关系,形成一个个代码块
chunk
,将形成的代码块chunk
输出到文件系统
- 根据入口文件之间的依赖关系,形成一个个代码块
注:整个打包过程中通过plugin
插件监听事件节点,执行插件任务进而达到干预输出结果的目的。
在webpack
源码中,文件的解析与构建主要依赖于compiler
和compilation
两个核心对象实现。
每个模块间的依赖关系,都依赖于 AST
语法树,通过语法树可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。最终Webpack打包出来的bundle
文件是一个IIFE
的执行函数。
webpack如何实现动态加载
在单页应用中,经常使用 webpack 的 动态导入 功能来异步加载模块,从而减少部分文件的体积。我们可以通过webpack 提供的 import()
和 require.ensure
两个 API 来使用该功能。
当文件较多时,用到 Webpack
中的 require.context()
方法,动态加载某个文件夹下的所有JS文件。语法如下:
require.context(directory, useSubdirectories = false, regExp = /^.//);
// 文件夹,是否包含子目录(true/false),正则匹配哪些文件
// 举例:获取stores文件夹下所有js文件,动态导入,不用一个个引入
let requireContext = require.context('./stores', true, /^./.*/index.js$/)
更多可以了解这篇:webpack是如何实现动态导入的
complier 和 compilation 区别
compiler
对象是一个全局单例对象,他负责把控整个webpack
打包的构建流程。complier 对象暴露了 webpack 整一个生命周期相关的钩子,是 webpack
初始化的参数的产物,包含options, entry, plugins
等属性可以简单的理解为webpack
的一个实例。
compilation
对象是每一次构建的上下文对象,是 complier
的实例,是每一次 webpack
构建过程中的生命周期对象。它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler
都会重新生成一个新的compilation
对象,负责此次更新的构建过程。
总结:两个对象都有自己的生命周期钩子。compiler对象
是整个Webpack从启动到关闭的生命周期钩子的对象。compilation 对象
负责的是粒度更小的生命周期钩子,只是代表了一次新的编译。
是否写过Loader?简述一下编写loader的思路?
loader的配置信息
在 webpack
中 loader
是一个函数,对匹配到的内容进行转换,将转换后的结果返回。通过配置可以看出,loader
是支持以数组的形式配置多个的,loader支持链式的调用
,执行顺序是从下到上,从右到左。为了保证loader
能够正常工作,开发需要遵循一些规范,比如返回值必须是标准的JS
代码字符串;遵循"单一职责"(即一个Loader只需要完成一种转换),只关心loader
的输出以及对应的输出。
module.exports = {
module: {
rules: [
{ test: /.css$/, use:['style-loader', 'css-loader'] },
// 从右到左,`css-loader`处理后的文件返回给`style-loader`处理后返回
{ test: /.ts$/, use: 'ts-loader' },
],
},
};
总结编写loader的思路:
loader 的本质为函数,loader
函数中的this
上下文由webpack
提供,因此我们不能将 loader设为一个箭头函数,可以通过this
对象提供的相关属性,获取当前loader
需要的各种信息数据。loader函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。
详细可看参考文档:Webpack原理—编写Loader和Plugin
常见的loader
- 样式类的 loader:
css-loader, style-loader, less-loader
等 - 文件类的 loader:
url-loader, file-loader
等。 - 编译类的 loader:
babel-loader, ts-loader
等 - 校验测试类 loader:
eslint-loader, jslint-loader
等
是否写过Plugin?简述一下编写Plugin的思路?
plugin的配置信息
plugins
是一个类,webpack
为 plugin 提供了很多内置的api,在Webpack运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。需要在原型上定义 apply(compliers)
函数。同时指定要挂载的 webpack
钩子。
最基础的Plugin
的代码是这样的:
class MyPlugin {
// 在构造函数中获取用户给该插件传入的配置
constructor(params){
}
// webpack初始化参数后会调用MyPlugin实例的apply方法,给插件传入complier对象。
apply(complier){
// 绑定钩子事件
// complier.hooks.emit.tapAsync()
compiler.plugin('emit', compilation => {
})
}
}
module.export = MyPlugin // 导出 Plugin
在使用这个Plugin
时,相关配置代码如下:
const MyPlugin = require('./MyPlugin.js');
module.export = {
plugins:[
new MyPlugin(options),
]
}
Webpack启动后,在读取配置的过程中会先执行new MyPlugin(options)
初始化一个MyPlugin
获得其实例。 在初始化compiler
对象后,再调用实例方法myPlugin.apply(compiler)
给插件实例传入compiler
对象。 插件实例在获取到compiler
对象后,就可以通过compiler.plugin(事件名称, 回调函数)
监听到Webpack广播出来的事件。 并且可以通过compiler
对象去操作Webpack。
总结编写Plugin的思路:
- 编写一个
JavaScript
命名函数。 - 在它的原型上定义一个
apply
方法。 - 在应用方法
apply()
中指定挂载的webpack
事件钩子complier.hooks.
。 - 处理
webpack
内部实例的特定数据。 - 功能完成后调用
webpack
提供的回调。
常见的Plugin
html-webpack-plugin
会在打包后自动生成一个html
文件,并且会将打包后的 js 文件引入到html
文件内。optimize-css-assets-webpack-plugin
对 CSS 代码进行压缩。mini-css-extract-plugin
。将写入style
标签内的 css 抽离成一个 用link
导入 生成的 CSS 文件webpack-parallel-uglify-plugin
。开启多进程执行代码压缩,提高打包的速度。clean-webpack-plugin
。每次打包前都将旧生成的文件删除。serviceworker-webpack-plugin
。为网页应用增加离线缓存功能。
Loader和Plugin的区别?
loder
是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,运行在打包文件之前,最终将资源打包到指定文件中plugin
赋予了webpack更多灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决和扩展了loder
无法实现的其他功能,在整个编译周期中都起作用
webpack打包的整个运行时机如下图:
在Webpack 运行的生命周期中会广播出许多事件,plugin
可以监听这些事件,在合适的时机通过Webpack提供的 API
改变输出结果。
对于loader
,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程。
参考文档:loader和Plugin的区别
谈谈webpack与babel
webpack
webpack
是一个模块化打包工具,打包js文件,css文件,图片,html等等,它可以分析整个项目的文件结构,确认文件之间的依赖,比如一个js文件引入了另一个js文件。在这个过程中可以合成js,压缩,最终生成项目文件。
babel
babel
是一个JS编译器,用来转换将最新的JS语法转化成ES5语法,从而能够在大部分浏览器中运行。像ES6的箭头函数就可以做转换。babel执行过程中,分为三步:先解析(parsing)、再转化(TransForm)、最后生成(Generate)es5代码。
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) { return n + 1; });
但babel只转换语法的话,一些最新的api是不转化的,比如Object.assign, Promise等。所以babel还提供了很多插件,也就是babel-pilofill
。安装后,即可支持浏览器运行。babel-pilofill基于core-js和regenerator。但pilofill是引入全部的api支持,如果只用了部分api,可以只引入相应的模块。
更多可以看这篇:深入浅出babel
babel还可以转换JSX语法,对React支持比较好。babel的presents,presents是指plugins的合集。Babel 一般需要配合 Webpack 来编译模块语法。
webpack中配置babel可以看这篇:webpack怎么配置babel?
plugins的执行过程是先出现先运行。
{
"plugins": [
"transform-decorators-legacy", // 先运行
"transform-class-properties" // 再运行
]
}
但presents是先出现后执行,为了更好的兼容性。
{
"presets": [
"es2015", // 最后运行es2015
"react",
"stage-2" // 先运行stage-2
]
}
webpack热更新
模块热替换(HMR - Hot Module Replacement)是 webpack 提供的最有用的功能之一。所谓的热替换是指在不需进行刷新重新加载页面的情况下,进行模块的替换,添加,删除等操作。
如果没有配置 HMR
,那么每次改动时都需要刷新页面,才能看到改动之后的结果,对于调试来说,非常麻烦,而且效率不高,最关键的是,你在界面上修改的数据,会随着刷新页面会丢失。而 Webpack
热更新的机制存在,就算修改了代码,也不会导致刷新,而是保留现有的数据状态,只将模块进行更新替换。也就是说,既保留了现有的数据状态,又能看到代码修改后的变化。
其思路主要有以下几个方面:
- 加载页面时保存应用程序状态
- 只更新改变的内容,节省开发和调试时间
- 修改样式更快,几乎等同于在浏览器中更改样式
一个带有热替换功能的webpack.config.js 文件的配置如下,做了这么几件事
- 引入了webpack库,安装依赖
$ npm install webpack webpack-dev-server --save-dev
- 配置
devServer
,选项中的hot
字段为true
,代表开启热更新devServer: { contentBase: path.resolve(__dirname, 'dist'), hot: true, compress: true // 表示父所有服务启用gzip压缩 },
- 使用
new webpack.HotModuleReplacementPlugin()
plugins: { HotModuleReplacementPlugin: new webpack.HotModuleReplacementPlugin() },
参考文档:Webpack 如何实现热更新?