前端模块化序篇
这里建议先复习一下《再唠叨JS模块化加载之CommonJS、AMD、CMD、ES6》
- AMD: define require
- CMD: exports require
- ES6: export import
之前由于由于ES6本身是原生语言支持实现的模块化,但是现代浏览器大多都还未支持,因此必须使用相应的transpiler工具转换成ES5的AMD,CMD模块,再借助于systemjs/requirejs等模块加载工具才能使用。
前端的模块系统经历了长久的演变,对应的模块化方案也几经变迁。
- JavaScript打包方案从最初简单的文件合并,到AMD 的模块具名化并合并,再到browserify将CommonJS 模块转换成为浏览器端可运行的代码,打包器做的事情越来越复杂,角色也越来越重要,加载器貌似在弱化。
- Javascript中模块加载器从最初小而简单lab.js/curl.js到RequireJS/sea.js、Browserify、Webpack和SystemJS一直在演进发展。
js语言本身并不支持模块化,同时浏览器中js和服务端nodejs中的js运行环境是不同的,如何实现浏览器中js模块化主流有两种方案:
- requirejs/seajs: 是一种在线“编译”模块的方案,相当于在页面上加载一个CommonJS/AMD模块格式解释器。这样浏览器就认识了define, exports,module这些东西,也就实现了模块化。
- browserify/webpack:是一个预编译模块打包的方案,相比于第一种方案,这个方案更加智能。由于是预编译的,不需要在浏览器中加载解释器。你在本地直接写JS,不管是AMD/CMD/ES6风格的模块化,它都能认识,并且编译成浏览器认识的JS。
到了2021,以webkit为内核的众多浏览器 都支持了es6 原生加载。本篇再来梳理一下前端模块方案。
ES6异步加载
浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。
代码语言:javascript复制<script type="module" src="./foo.js"></script>
其实这个并没有什么好书的。我想说的是在代码中异步加载模块。实现cmd的效果。比如:
app/es6-file.js:
代码语言:javascript复制export class q {
export let counter = 3;
export function incCounter() {
counter ;
}
浏览器加载:
代码语言:javascript复制<script>
import { counter, incCounter } from './lib';
// import { counter, incCounter } from 'https://www.zhoulujun.cn/demo/lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
</script>
ES6模块定义名为export,提供一个静态构造函数访问器。
更多的推荐阅读
- ES6模块加载实现,以及异步加载 https://juejin.cn/post/7002007274877091870
- 万岁,浏览器原生支持ES6 export和import模块啦! https://www.zhangxinxu.com/wordpress/2018/08/browser-native-es6-export-import-module/
es5时代模块加载器
比较代表性的就是require.js/sea.js、Browserify
AMD阵营
超快速AMD入门 (Super Quick AMD Primer)
如果您不熟悉AMD的结构,我将为您提供您所听到的最简单的解释。 AMD是您用来异步定义和要求模块的系统。 定义返回一个或零个对象。 define和require的第一个参数通常是一个依赖项数组。 第二个参数是一个函数; define返回结果,require执行基本的回调:
代码语言:javascript复制// "define" a module
define(["namespace/dependencyA", "namespace/dependencyB"], function(depA, depB) {
// Whole bunch of processing
// Return what this module defines
return function() {
// Or an object, or whatever
}
});
// "require" to use modules:
require(["namespace/dependencyC"], function(depC) {
// depC can be used in here only
// Yay for modularity!
有数十种AMD JavaScript加载程序可用,其中最受欢迎的是RequireJS。 还有鲜为人知JavaScript加载程序,例如YepNope,script.js,LAB.js和Dojo的新本机加载程序。我最先接触的就是 curl.js,具体查看 https://github.com/cujojs/curl。
Require.JS
RequireJS 是一个JavaScript 模块加载器,基于AMD 规范实现。
它同时也提供了对模块进行打包与构建的工具r.js,通过将开发时单独的匿名模块具名化并进行合并,实现线上页面资源加载的性能优化。
RequireJS 与r.js 等一起提供的一个模块化构建方案。
Require是出现在2009年,它完全不同于之前的那些懒加载器,它将脚本标签写入到DOM中,监听完成的事件,然后递归加载依赖:
代码语言:javascript复制<script src=“tools/require.js” data-main=“myAppInit.js” ></script>
...或者如下调用指明的函数名称...
代码语言:javascript复制<script src=“tools/require.js”></script>
再调用
代码语言:javascript复制<script>
require([‘myAppInit’, ‘libs/jQuery’], function (myApp, $) { ...
</script>
上面两个用法不建议同时使用。虽然Require存在各种特殊情况,但是其灵活性和强大性还是支持它成为浏览器端流行的加载器。
更多参看官网:https://requirejs.org/
Browserify
https://browserify.org/
Browserify允许CommonJS格式模块在前端使用,主要用于在浏览器中使用 npm 包,最终会转换为 commonJS (require) 类似方式,在浏览器使用。
它不只是一个模块加载器,而是模块捆绑器(bundler),是一个完整的代码构建段的工具,提供客户端能加载一堆代码的功能。
首先需要node和npm已经安装,获得包:
代码语言:javascript复制npm install -g –save-dev browserify
以CommonaJS格式编写你的模块即可。然后使用下面命令捆绑:
代码语言:javascript复制npm install -g –save-dev browserify
它会递归以此发现entry-point中所有依赖包,然后将它们组装在一个单个文件中:
代码语言:javascript复制<script src=”bundle-name.js”></script>
对于前端,你可以最小化合并核心代码,然后让可选模块在之后需要时加载,这样即节约了带宽也不影响模块编程功能实现。
更多请参看官网:https://browserify.org/
Browserify缺点
基于流 Stream,旧时代产物,尽管也能勉强处理 css(CSS bundlers),html(brfs),但是不太友好,且年久失修
browserify必须把源代码打成bundle然后再引用,就决定了他不能直接调试源代码,这对于程序员是很不友好的。虽然我们可以使用 watchify(可以动态把你写的代码立即编译成bundle) 和 --debug 选项(给编译后的代码加上source maps)。但是依然只是近似于直接调试源代码。
SystemJS
https://github.com/systemjs/
Systemjs是一个可配置模块加载器,为浏览器和NodeJs启用动态的Es模板加载器。任何具有标准的URL都可被加载为一个模块:
代码语言:javascript复制<script src="system.js"></script>
<script>
// 加载相对于当前地址的url
SystemJS.import('./local-module.js');
// 加载绝对url的地址
SystemJS.import('https://code.jquery.com/jquery.js');
</script>
可以加载任何类型的模块格式,并由SystemJS自动检测。
SystemJS 诞生于 2015 年,那个时候 ES Module 还未成为标准,在浏览器端只能通过 requirejs、seajs 等方案实现模块加载,随着 npm 在前端界的流行,一个项目中可能存在多种模块规范,所以我认为 SystemJS 最初诞生的目的是为了做一个通用的模块加载器,在浏览器端实现对 CommonJS、AMD、UMD 等各种模块的加载。 SystemJS 是(浏览器尚未正式支持importMap) 原生 ES Module 的替代品,ES Module 被编译成 System.register 格式之后能够跑在旧版本的浏览器当中。
在本地运行时,请确保从本地服务器或启用了本地XHR请求的浏览器运行。如果不是,将会收到一条错误消息。
对于Mac上的Chrome,您可以运行它: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --allow-file-access-from-files &> /dev/null & 在Firefox中,这需要导航到about:config,进入security.fileuri.strict_origin_policy过滤器框并将选项切换为false。
SystemJS加载配置
baseURL
baseURL提供了一种根据一个相对地址装载模块的机制。
这使得能够从许多不同的请求URL访问相同的模块
代码语言:javascript复制SystemJS.config({
// set all requires to "lib" for library code
baseURL: '/lib/',
// set "app" as an exception for our application code
paths: {
'app/*': '/app/*.js'
}
});
// 加载 /modules/jquery.js
SystemJS.import('jquery.js');<br>
更多的参看官方文档:https://github.com/systemjs/systemjs
es5时代模块打包方案
Grunt和Gulp属于任务流工具Tast Runner , 而 webpack属于模块打包工具 Bundler。
grunt
https://gruntjs.com/
Grunt 是老牌的构建工具,特点是配置驱动,你需要做的就是了解各种插件的功能,然后把配置整合到 Gruntfile.js 中
代码语言:javascript复制module.exports = function(grunt) {
grunt.initConfig({
// js格式检查任务
jshint: {
files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
options: {
globals: {
jQuery: true
}
}
},
// 代码压缩打包任务
uglify: {}
watch: {
files: ['<%= jshint.files %>'],
tasks: ['jshint']
}
});
grunt.initConfig({
});
// 导入任务插件
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadnpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
// 注册自定义任务, 如果有多个任务可以添加到数组中
grunt.regusterTask('default', ['jshint'])
};
Grunt 缺点也是配置驱动,当任务非常多的情况下,试图用配置完成所有事简直就是个灾难;再就是它的 I/O 操作也是个弊病,它的每一次任务都需要从磁盘中读取文件,处理完后再写入到磁盘,例如:我想对多个 less 进行预编译、压缩操作,那么 Grunt 的操作就是:
读取 less 文件 -> 编译成 css -> 存储到磁盘 -> 读取 css -> 压缩处理 -> 存储到磁盘
这样一来当资源文件较多,任务较复杂的时候性能就是个问题了。
glup
https://gulpjs.com/
Gulp是后起之秀。他们的本质都是通过 JavaScript 语法实现了shell script 命令的一些功能。比如利用jshint插件 实现 JavaScript 代码格式检查这一个功能。早期需要手动在命令行中输入 jshint test.js,而 Grunt 则通过文件 Gruntfile.js 进行配置
Gulp吸取了Grunt的优点,拥有更简便的写法,通过流(Stream)的概念来简化多任务之间的配置和输出,让任务更加简洁和容易上手。
Gulp 特点是代码驱动,写任务就和写普通的 Node.js 代码一样:
代码语言:javascript复制// gulpfile.js
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var uglify = require('gulp-uglify');
// 代码检查任务 gulp 采取了pipe 方法,用流的方法直接往下传递
gulp.task('lint', function() {
return gulp.src('src/test.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});
// 压缩代码任务
gulp.task('compress', function() {
return gulp.src('src/test.js')
.pipe(uglify())
.pipe(gulp.dest('build'));
});
// 将代码检查和压缩组合,新建一个任务
gulp.task('default', ['lint', 'compress']);
再一个对文件读取是流式操作(Stream),也就是说一次 I/O 可以处理多个任务,还是 less 的例子,Gulp 的流程就是:
读取 less 文件 -> 编译成 css -> 压缩处理 -> 存储到磁盘
在 Grunt 与 Gulp 对比看来还是比较推荐 Gulp!
webpack
https://webpack.js.org/
传统的模块化基于单种编程语言,目的是为了解耦和重用,而因为前端本身的特点(需要三种编程语言配合)以及能力限制,所以不能实现跨资源加载也就难以实现组件化。
而 Webpack 打破的这种思维局限,它的 Require anything 的理念在实现模块化的同时也能够很方便实现组件化,借助 Webpack 就可以很轻松的实现这种代码组织结构:
Webpack 的特点:
- 把一切都视为模块:不管是 CSS、JS、Image 还是 HTML 都可以互相引用,通过定义 entry.js,对所有依赖的文件进行跟踪,将各个模块通过 loader 和 plugins 处理,然后打包在一起。
- 按需加载:打包过程中 Webpack 通过 Code Splitting 功能将文件分为多个 chunks,还可以将重复的部分单独提取出来作为 commonChunk,从而实现按需加载。
Webpack 也是通过配置来实现管理,与 Grunt 不同的时,它包含的许多自动化的黑盒操作所以配置起来会简单很多(但遇到问题调试起来就很麻烦),一个典型的配置如下:
代码语言:javascript复制module.exports = {
//插件项
plugins: [commonsPlugin],
//页面入口文件配置
entry: {
index : './src/js/page/index.js'
},
//入口文件输出配置
output: {
path: 'dist/js/page',
filename: '[name].js'
},
module: {
//加载器配置
loaders: [
{ test: /.css$/, loader: 'style-loader!css-loader' },
{ test: /.js$/, loader: 'jsx-loader?harmony' },
{ test: /.scss$/, loader: 'style!css!sass?sourceMap'},
{ test: /.(png|jpg)$/, loader: 'url-loader?limit=8192'}
]
},
//其它解决方案配置
resolve: {
root: '/Users/Bell/github/flux-example/src', //绝对路径
extensions: ['', '.js', '.json', '.scss'],
alias: {
AppStore : 'js/stores/AppStores.js',
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}
};
参考文章:
SystemJS 探秘 https://zhuanlan.zhihu.com/p/402155045
System.js详解 https://www.cnblogs.com/tangxing/p/7223456.html
Javascript模块加载捆绑器Browserify Webpack和SystemJS用法 https://www.jdon.com/idea/js/javascript-module-loaders.html
browserify 中文文档与使用教程 https://zhuanlan.zhihu.com/p/76604976
curl.js: Incredible AMD Loader https://davidwalsh.name/curljs
用 Browserify 替换 require.js https://blog.csdn.net/nsrainbow/article/details/52736904
前端工程化——构建工具选型:grunt、gulp、webpack https://juejin.cn/post/6844903645700423693
差点被SystemJs惊掉了下巴,解密模块加载黑魔法 https://segmentfault.com/a/1190000039305322
https://www.digitalocean.com/community/tutorials/how-to-dynamically-import-javascript-with-import-maps
从systemjs的使用学习js模块化 https://segmentfault.com/a/1190000022278429
转载本站文章《前端模块化方案:前端模块化/插件化异步加载方案探索》, 请注明出处:https://www.zhoulujun.cn/html/webfront/engineer/Architecture/8753.html