前端模块化方案:前端模块化/插件化异步加载方案探索

2023-05-14 22:40:01 浏览数 (1)

前端模块化序篇

这里建议先复习一下《再唠叨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模块化主流有两种方案:

  1. requirejs/seajs: 是一种在线“编译”模块的方案,相当于在页面上加载一个CommonJS/AMD模块格式解释器。这样浏览器就认识了define, exports,module这些东西,也就实现了模块化。
  2. 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

0 人点赞