webpack折腾记(三)

webpack在提供便利的同时会带来一些新的问题,将所有的工具代码打包到不同的页面文件中,会导致页面脚本体积太大,且不利于缓存。一种解决思路是将很常用的模块(以及整个项目的依赖库文件)放在CDN,通过导入externals来使用外部模块,对于那些只在几个页面内公用的模块,可以将他们进行打包。 然而,webpack只是打包工具,对于externals的外部模块,不提供对应的模块加载器功能,而手动在页面上导入大量的外部脚本不是一件很合理的事儿。 这篇文章是关于如何处理webpack与cdn资源之间尝试(写完感觉又钻进了某个死胡同)。

<!--more-->

这里可以把模块分成三类:

  • CDN模块,通过AMD外部引入模块
  • 纯粹的本地模块,不依赖任何CDN模块,可以使用ES6模块或CommonJS规范定义
  • 依赖于CDN模块的本地模块,需要遵循AMD规范

现在在项目中,采用的是手动将CDN模块以script标签引入,然后在externals声明外部模块。 如何科学地管理第三方模块呢?下面是一点思考。

参考:

1. 方案一:requirejs与AMD模块

  • 使用AMD加载第三方CDN文件
  • 使用webpack打包本地模块

纯粹的本地模块

// math.js
var add = function(a, b){
    return a + b;
}
export default {
    add
}

依赖于CDN模块的本地模块

// red.js
// 由于jquery文件是AMD形式加入的,而打包的代码是按脚本插入先后顺序执行的,
// 因此这里会出现 "$ is not a function"的错误

import $ from "jquery";

var redify = function(el){
    $(el).css("color", "red");
}

export default {
    redify
}

无法通过下面配置解决

externals: {
    jquery: 'jQuery'
}

只能遵循AMD规范

// 引入配置文件
import "../require.config"

define("blue", ['jquery', 'layer'], function($){
    var blueify = function(el){
        $(el).css("color", "blue");
    }

    var msg = function(el){
        $(el).on("click", function(){
            layer.confirm($(this).text());
        })
    }

    return {
        blueify,
        msg
    }    
})

除了CDN模块,后面两种类型的模块都需要进行打包,这可以通过webpack简单实现

1.1. 使用方式

建立一个require.config.js的配置文件,用于管理所有的外部文件

var require = window.require;

// 外部CDN文件
require.config({
    baseUrl: '/',
    paths: {
        jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery',
        layer: 'https://cdn.bootcss.com/layer/3.0.3/layer',
        axios: 'http://doufuweb.la/assets/js/lib/axios.min'
    },
    shim: {
        layer: {
            deps: ["jquery"],
            exports: "layer"
        }
    }
})

export default require;

每个页面都对应一个入口文件,这个入口文件是通过webpack输出的

<script src="./src/require.js" data-main='/dist/index'></script>

在对应的入口文件源文件中,引入本地模块

import require from "./require.config"

import math from "./src/math.js"
import "./src/blue"

require([],function(){
    var sum = math.add(1, 2);
    console.log(sum);
})

require(["blue"], function(blue){
    blue.blueify("#test");
    blue.msg("#test");
})

webpack.config.js中,为每个页面配置对应的输入和输出

var path = require("path")

module.exports = {
    entry: {
        // 多个页面导入即可
        index: path.resolve(__dirname, "./index.js")
    },
    output: {   
        filename: "[name].js",
        path: path.resolve(__dirname, './dist/')
    },
    externals: {
        jquery: 'jQuery'
    }
}

1.2. 评估

优点:

  • 不需要将所有文件都打包,统一管理CDN文件,维护成本更低
  • 通过AMD,不需要管理外部文件的相互依赖
  • 通过webpack进行打包本地模块,不需要使用r.js

缺点:

  • 依赖于CDN模块的本地模块,只能遵循AMD规范定义,即使用define,因此需要准确区分模块的id
  • 现有的Commonjs迁移到AMD模块,工作量比较大,手动迁移容易出问题

1.3. 疑问

为什么webpack没有提供类似的功能,即在加载externals的时候,如果没有找到全局变量,则通过AMD加载对应的文件~

webpack内置了requiredefine的方法,但是不能指定url,这样就无法实现上面的功能。正在重新翻文档,希望一切顺利。

突然发现作者*sokra *在这个issue回复的~

webpack doesn't take care of the module loading. You need to use another library for the loading part

然后找到了方案二。

2. 方案二:SystemJS脚本加载器

通过SystemJSexternals

2.1. 思路

回到webpack,之所以我们需要在页面中手动引入脚本,是因为externals需要获取对应模块的全局变量,然后才能在其他模块中使用。其实现类似于

module.exports = window.jQuery;

换个思路,只要在执行webpack的加载对应,内存中存在对应的全局变量即可,这并不需要我们手动去在页面上添加链接,也就是说,我们只需要一个脚本加载器,可以在对应的依赖脚本加载完毕之后再执行我们的本地模块即可。之前把事情想得太复杂了。

然后发现了SystemJS, 这是一款用来加载脚本的插件,对应的浏览器版本提供了与requirejsload类似的异步加载功能。

没错,就是你了。有大佬给出了脚本加载器的简单实现

2.2. 使用方式

对着文档,跟requirejs的模块加载方式比较类似,

同样先定义一个配置文件,参考配置参数文档

import SystemJS from "systemjs"

SystemJS.config({
    map: {
        // jquery: "//code.jquery.com/jquery.js",
        jquery: "//imgdh.doufu.la/jquery/jquery-3.2.1.min.js"
    }
})

export default SystemJS

然后在对应的页面文件上按需引入

import SystemJS from "./systemjs.config"

// 测试加载时间
console.time("sc");
SystemJS.import('jquery').then($=>{
    console.timeEnd("sc")
    console.log("this is index");

    // 这里使用对应的逻辑
    console.log($);
})

SystemJS再将CDN脚本加载到全局环境之后,会自动移除对应的脚本节点,所以在页面上无法看见对应的script标签。

2.3. 评估

优点:

  • 由于只是在执行externals之前控制脚本加载,对具体的模块基本没有影响,因此迁移成本低
  • 每个页面文件手动SystemJS.import依赖文件,按需加载,方便维护

缺点:

  • ES6的importexport只能出现在top level即最上层,因此本地的模块如果依赖CDN模块,需要使用CommonJS的方式定义和使用
  • 需要通过维护SystemJS.configexternals两个地方的模块声明

总体来说要比方案一的迁移成本低得多。

3. 最后

最后决定采用方案二,这样,貌似解决了困扰我很久的一个问题。但是,在每个模块中都需要手动去处理Systemjs,感觉也不是很合理,由于目前并没有在项目中尝试,因此也无法确定是否正确~容我三思。

整个项目的测试代码放在github上了。