前端模块化的十年征程
外部的模块: 指代引入前端工程的某个外部的包(package),可能由多个JS文件组成,但会通过入口暴露给我们项目调用
内部的模块: 指代我们自己的工程项目中编码的最小单元:即单个的JS文件
外部模块的管理
内部模块的组织
模块源码到目标代码的编译和转换
时间线
生态 诞生时间
Node.js 2009年
NPM 2010年
requireJS(AMD) 2010年
seaJS(CMD) 2011年
broswerify 2011年
webpack 2012年
grunt 2012年
gulp 2013年
react 2013年
vue 2014年
angular 2016年
redux 2015年
vite 2020年
snowpack 2020年
外部模块的管理
Node.js和NPM的发布
NPM时代以前的外部模块使用方式
需要用到jQuery,去 jQuery 官网下载 jQuery库,导入到项目中
需要用到lodash,去lodash官网下载lodash库
需要用到某个BootStrap,去BootStrap官网官网下载BootStrap库,导入到项目中
...
使用上缺乏便利性
难以跟踪各个外部模块的来源
没有统一的版本管理机制
NPM时代以后外部模块的使用方式
NPM是一个远程的JavaScript代码仓库,所有的开发者都可以向这里提交可共享的模块,并给其他开发者下载和使用
NPM还包含一个命令行工具,开发者通过运行npm publish命令把自己写的模块发布到NPM仓库上去,通过运行npm install [模块名],可以将别人的模块下载到自己项目根目录中一个叫node_modules的子目录下
// package.json
{
...
"dependencies": {
"bootstrap": "^4.5.2",
"jquery": "^3.5.1"
}
}
内部模块的组织
模块化第一阶段:原生JS组织阶段
// index.html
随着项目扩大,html文件中会包含大量script标签。
script标签的先后顺序并不能很好地契合模块间的依赖关系。在复杂应用中,模块的依赖关系通常树状或网状的,如a.js依赖于b.js和c.js,b.js依赖于b1.js和b2.js。相对复杂的依赖关系难以用script标签的先后顺序组织。
让代码的逻辑关系难以理解,也不便于维护,容易出现某个脚本加载时依赖的变量尚未加载而导致的错误。
因为对script标签顺序的要求而使用同步加载,但这却容易导致加载时页面卡死的问题
仍然会因为全局变量污染全局环境,导致命名冲突
模块化的第二阶段:在线处理阶段
模块化规范的野蛮生长
AMD && CMD
AMD
// module0.js
define(['Module1', 'Module2'], function(module1, module2) {
var result1 = module1.exec();
var result2 = module2.exec();
return{
result1: result1,
result2: result2
}
});
// 入口文件
require(['math'], function(math) {
math.sqrt(15)
});
define && require的区别
通过AMD规范组织后的JS文件看起来像下面这样
define(function() {
return printSth: function() {
alert("some thing")
}
});
define(['depModule'], function(mod) {
mod.printSth();
});
// amd.js意为某个实现了AMD规范的库
通过依赖数组的方式声明依赖关系,具体依赖加载交给具体的AMD框架处理
避免声明全局变量带来的环境污染和变量冲突问题
正如AMD其名所言(Asynchronous), 模块是异步加载的,防止JS加载阻塞页面渲染
遵循AMD规范实现的模块加载器
CMD
require: 一个方法标识符,调用它可以动态的获取一个依赖模块的输出
exports: 一个对象,用于对其他模块提供输出接口,例如:exports.name = "xxx"
module: 一个对象,存储了当前模块相关的一些属性和方法,其中module.exports属性等同于上面的exports
// CMD
define(function(requie, exports, module) {
//依赖就近书写
var module1 = require('Module1');
var result1 = module1.exec();
module.exports = {
result1: result1,
}
});
// AMD
define(['Module1'], function(module1) {
var result1 = module1.exec();
return{
result1: result1,
}
});
CMD && AMD的区别
AMD推崇依赖前置,即通过依赖数组的方式提前声明当前模块的依赖
CMD推崇依赖就近,在编程需要用到的时候通过调用require方法动态引入
AMD推崇通过返回值的方式对外输出
CMD推崇通过给module.exports赋值的方式对外输出
遵循CMD规范实现的模块加载器
AMD && CMD背后的实现原理
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
var SLASH_RE = /\\\\/g
function parseDependencies(code) {
var ret = []
code.replace(SLASH_RE, "")
.replace(REQUIRE_RE, function(m, m1, m2) {
if(m2) {
ret.push(m2)
}
})
return ret
}
// Parse dependencies according to the module factory code
if(!isArray(deps) && isFunction(factory)) {
deps = parseDependencies(factory.toString())
}
function request(url, callback, charset, crossorigin) {
var node = doc.createElement("script")
addOnload(node, callback, url) // 添加回调,回调函数在 3 中
node.async= true//异步
node.src = url
head.appendChild(node)
}
Module.prototype.onload = function() {
var mod = this
mod.status = STATUS.LOADED
for(var i = 0, len = (mod._entry || []).length; i < len; i++) {
var entry = mod._entry[i]
if(--entry.remain === 0) {
entry.callback()
}
}
delete mod._entry
}
// sea.js的use方法类似于AMD规范中的require方法,用于执行入口函数
Module.use= function(ids, callback, uri) {
var mod = Module.get(uri, isArray(ids) ? ids : [ids])
mod.callback = function() {
var exports = []
var uris = mod.resolve();
// 依次执行加载完毕的依赖模块,并将输出传递给use方法回调
for(var i = 0, len = uris.length; i < len; i++) {
exports[i] = cachedMods[uris[i]].exec()
}
// 执行use方法回调
if(callback) {
callback.apply(global, exports)
}
}
}
sES6的模块化风格
CommonJS && ES6
// ES6
import{ foo } from'./foo'; // 输入
exportconst bar = 1; // 输出
// CommonJS
const foo = require('./foo'); // 输入
module.exports = { 。// 输出
bar:1
}
babel的出现和ES6模块化的推广
在开发的时候,我们追求的是编程的便捷性和可阅读性。
而在生产中,我们追求的是代码对各种浏览器的兼容性。
Babel的工作原理
Parse(解析): 通过词法分析和语法分析,将源代码解析成抽象语法树(AST)
Transform(转换):对解析出来的抽象语法树做中间转换处理
Generate(生成):用经过转换后的抽象语法树生成新的代码
模块化的第三阶段:预处理阶段
在线组织模块的方式会延长前端页面的加载时间,影响用户体验。
加载过程中发出了海量的http请求,降低了页面性能。
broswerify
npm install -g browserify
// main.js
var a = require('./a.js');
var b = require('./b.js');
...
browserify main.js -o bundle.js
webpack
npm install --save-dev webpack
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
...
...
打包工具面临的问题 && 解决方案
虽然允许拆多个包了,但包的总数仍然比较少,比CMD等方案加载的包少很多
Code Splitting有可分为两个方面的作用:
// webpack.config.js
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons', // the commons chunk name
filename: 'commons.js', // the filename of the commons chunk)
minChunks: 3, // Modules must be shared between 3 entries
});
]
};
button.addEventListener('click',function(){
import('./a.js').then(data => {
// use data
})
});
模块化的第四阶段:自动化构建阶段
开发时使用丰富且方便的JS新特性,如用ES6,typescript编程,由自动化构建工具转化成浏览器兼容的ES5格式的JS代码
用Sass,less编写阅读性和扩展性良好的样式代码,由自动化构建工具转化成浏览器兼容的CSS代码
提供开发时SourceMap功能,也即提供生产代码(如ES5)到源代码(typescript)的映射,方便开发调试
提供生产时代码压缩功能,压缩js和css,删除注释,替换变量名(长变短),减少代码加载体积
提供开发热重载功能(Hot Module Reload), 也即在编辑器保存代码的时候自动刷新浏览调试页面。
当然也还包括基本的模块打包功能
其他.....
2012年出现的webpack
2012年出现的grunt
2013年出现的gulp
gulp && webpack
gulp和webpack的区别
gulp是编程式的自动化构建工具
webpack是配置式的自动化构建工具
Gulp
// gulpfile.js
const{ src, dest } = require('gulp');
const less = require('gulp-less');
const minifyCSS = require('gulp-csso');
function css() {
return src('client/templates/*.less')
.pipe(less())
.pipe(minifyCSS())
.pipe(dest('build/css'))
}
Webpack
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.less$/, // 正则匹配less文件
use: [
{ loader: 'style-loader'}, // creates style nodes from JS strings
{ loader: 'css-loader'}, // translates CSS into CommonJS
{ loader: 'less-loader'}, // compiles Less to CSS
],
},
],
},
};
gulp和webpack的共同点
gulp-uglify : 压缩js文件
gulp-less : 编译less
gulp-sass:编译sass
gulp-livereload : 实时自动编译刷新
gulp-load-plugins:打包插件
uglifyjs-webpack-plugin: 压缩js文件
less-loader: 编译less
sass-loader:编译sass
devServer.hot配置为true: 实时自动编译刷新
....
Gulp的没落和webpack的兴起
究其原因
自动构建工具的新趋势:bundleless
主流现代浏览器已经能充分支持ES6了,import和export随心使用
HTTP2.0普及后并发请求的性能问题没有那么突出了