• home > webfront > engineer > Architecture >

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

    Author:zhoulujun Date:

    前端模块化序篇这里建议先复习一下《再唠叨JS模块化加载之CommonJS、AMD、CMD、ES6》AMD: define + requireCMD: exports + requireES


    前端模块化序篇

    这里建议先复习一下《再唠叨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"属性。

    <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,提供一个静态构造函数访问器。

    更多的推荐阅读


    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

    https://browserify.org/

    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

    https://gruntjs.com/

    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

    https://gulpjs.com/

    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

    https://webpack.js.org/

    传统的模块化基于单种编程语言,目的是为了解耦和重用,而因为前端本身的特点(需要三种编程语言配合)以及能力限制,所以不能实现跨资源加载也就难以实现组件化。

    而 Webpack 打破的这种思维局限,它的 Require anything 的理念在实现模块化的同时也能够很方便实现组件化,借助 Webpack 就可以很轻松的实现这种代码组织结构:

    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

    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