前端模块化方案:前端模块化/插件化异步加载方案探索
Author:zhoulujun Date:
前端模块化序篇
这里建议先复习一下《再唠叨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"属性。
<script type="module" src="./foo.js"></script>
其实这个并没有什么好书的。我想说的是在代码中异步加载模块。实现cmd的效果。比如:
app/es6-file.js:
export class q { export let counter = 3; export function incCounter() { counter++; }
浏览器加载:
<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执行基本的回调:
// "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中,监听完成的事件,然后递归加载依赖:
<script src=“tools/require.js” data-main=“myAppInit.js” ></script>
...或者如下调用指明的函数名称...
<script src=“tools/require.js”></script>
再调用
<script> require([‘myAppInit’, ‘libs/jQuery’], function (myApp, $) { ... </script>
上面两个用法不建议同时使用。虽然Require存在各种特殊情况,但是其灵活性和强大性还是支持它成为浏览器端流行的加载器。
更多参看官网:https://requirejs.org/
Browserify
Browserify允许CommonJS格式模块在前端使用,主要用于在浏览器中使用 npm 包,最终会转换为 commonJS (require) 类似方式,在浏览器使用。
它不只是一个模块加载器,而是模块捆绑器(bundler),是一个完整的代码构建段的工具,提供客户端能加载一堆代码的功能。
首先需要node和npm已经安装,获得包:
npm install -g –save-dev browserify
以CommonaJS格式编写你的模块即可。然后使用下面命令捆绑:
npm install -g –save-dev browserify
它会递归以此发现entry-point中所有依赖包,然后将它们组装在一个单个文件中:
<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都可被加载为一个模块:
<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访问相同的模块
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
Grunt 是老牌的构建工具,特点是配置驱动,你需要做的就是了解各种插件的功能,然后把配置整合到 Gruntfile.js 中
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
Gulp是后起之秀。他们的本质都是通过 JavaScript 语法实现了shell script 命令的一些功能。比如利用jshint插件 实现 JavaScript 代码格式检查这一个功能。早期需要手动在命令行中输入 jshint test.js,而 Grunt 则通过文件 Gruntfile.js 进行配置
Gulp吸取了Grunt的优点,拥有更简便的写法,通过流(Stream)的概念来简化多任务之间的配置和输出,让任务更加简洁和容易上手。
Gulp 特点是代码驱动,写任务就和写普通的 Node.js 代码一样:
// 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
传统的模块化基于单种编程语言,目的是为了解耦和重用,而因为前端本身的特点(需要三种编程语言配合)以及能力限制,所以不能实现跨资源加载也就难以实现组件化。
而 Webpack 打破的这种思维局限,它的 Require anything 的理念在实现模块化的同时也能够很方便实现组件化,借助 Webpack 就可以很轻松的实现这种代码组织结构:
Webpack 的特点:
把一切都视为模块:不管是 CSS、JS、Image 还是 HTML 都可以互相引用,通过定义 entry.js,对所有依赖的文件进行跟踪,将各个模块通过 loader 和 plugins 处理,然后打包在一起。
按需加载:打包过程中 Webpack 通过 Code Splitting 功能将文件分为多个 chunks,还可以将重复的部分单独提取出来作为 commonChunk,从而实现按需加载。
Webpack 也是通过配置来实现管理,与 Grunt 不同的时,它包含的许多自动化的黑盒操作所以配置起来会简单很多(但遇到问题调试起来就很麻烦),一个典型的配置如下:
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
从systemjs的使用学习js模块化 https://segmentfault.com/a/1190000022278429
转载本站文章《前端模块化方案:前端模块化/插件化异步加载方案探索》,
请注明出处:https://www.zhoulujun.cn/html/webfront/engineer/Architecture/8753.html