前端模块化
模块化的概念
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化的好处
当引入多个
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
模块化规范
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 如何实现热更新?