Skip to content

[toc]

  • 怎么打包组件库
  • 怎么基于组件库打包自己的组件库

前期准备

创建目录

bash
mkdir animal-api
cd animal-api
npm init -y

创建公共库文件index.js

js
import axios from 'axios';
function getCat() {
  return axios.get('https://aws.random.cat/meow').then(res=>{
    return {
      imageSrc: res.data.file,
      text: 'CAT'
    }
  })
}
export default {
  getCat
}

单元测试公共库

安装jest单元测试

bash
yarn add -D jest

创建单元测试脚本文件在test文件夹下index.test.js

js
import animalApi from '../index'
describe('animal-api', () => {
  it('get cat func', () => {
    return animalApi.getCat().then(res => {
      expect(res.imageSrc).not.toBeUndefined();
      expect(res.text).toEqual('CAT');
    })
  })
})

直接运行 npx jest 或者 到package.json 中添加script脚本 "test": "jest" 运行结果: ; jest报错 👆 可以看出报错信息是指测试脚本引入index.js不支持使用esm jest官方文档中有介绍使用babel使运行jest识别esm的方法

bash
yarn add --dev babel-jest @babel/core @babel/preset-env

创建babel.config.js

js
// babel.config.js
module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};

此时运行 npx jest

; 公共库方法报错 此时的报错是index.js的公共库方法报错,缺少axios

bash
yarn add axios

注意axios是生产环境也需要的库,所以不加 -D 再次执行npx jest ; 单元测试脚本输出结果 成功运行单元测试,并且单元测试的结果也是成功

让公共库可以在浏览器环境下运行

浏览器使用直接使用npm第三方库的方式是通过<script src="">来引入的(类似CDN),或者通过js动态创建<script>来引入。

UMD模块化

针对浏览器通过<script>标签引入的方式,第三方库一般会是UMD的模块化方式(同时支持AMD、CJS、全局变量) 如下是jQuery库的UMD模式

js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
  // AMD
  define(['jquery'], factory);
} else if (typeof exports === 'object') {
  // CommonJS
  module.exports = factory(require('jquery'));
} else {
  // 浏览器全局变量(root 即 window)
  root.returnExports = factory(root.jQuery);
}}(this, function ($) {
  // 业务代码
}));

👆 一个自执行函数,把业务代码作为回调函数,传入模块化处理的公共代码中

用babel使公共库以UMD形式输出

使用babel插件@babel/plugin-transform-modules-umd 转译代码输出成新文件(注意这里还不涉及打包)

需要通过在nodejs环境输出文件就需要用到@babel/cli了,之前不需要是因为babel-jest,只需要在运行时转译并运行即可,不需要输出转译后的文件。

所以使用babel plugin 输出转译后文件还需要@babel/cli

  1. 安装babel相关依赖
bash
yarn add -D @babel/cli @babel/plugin-transform-modules-umd

前面已经安装了@babel/core了,这次不用重复安装了

  1. 配置babel插件plugins 在babel.config.js中配置
js
module.exports = {
  presets: [ ... ],
  plugins: ['@babel/plugin-transform-modules-umd']
}
  1. 运行babel指令输出文件

直接运行npx babel index.js --out-dir dist 或者把脚本配置到package.json"build": "babel index.js --out-dir dist"

运行结果: ; babel output

输出转译后文件: ; babel转译后代码

精简后转译后代码:

js
(function (global, callback) {
  if (typeof define === "function" && define.amd) {
    define(["exports", "axios"], callback); // AMD - 根据是否全局有define方法
  } else if (typeof exports !== "undefined") {
    callback(exports, require("axios")); // CJS - 根据是否支持 exports实例
  } else {
    var mod = { exports: {} };
    callback(mod.exports, global.axios); // 调用业务代码callback,需要传递业务代码引用的依赖,👆cjs、amd同理,传入exports和axios
    global.index = mod.exports; // 全局变量
  }
})(typeof globalThis !== "undefined" ? globalThis : (typeof self !== "undefined" ? self : this),
  // 👆 根据环境处理全局变量的this
  function () {
  //  业务代码
});

👆 可以看出UMD输出的代码很像webpack打包后的代码

js
function (_exports, _axios) {
  "use strict";
  Object.defineProperty(_exports, "__esModule", {
    value: true
  });
  _exports.default = void 0;
  _axios = _interopRequireDefault(_axios);
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  function getCat() {
    return _axios.default.get('https://aws.random.cat/meow').then(res => {
      return {
        imageSrc: res.data.file,
        text: 'CAT'
      };
    });
  }
  var _default = {
    getCat
  };
  _exports.default = _default;
});

👆 业务代码中的模块化会被处理为,根据环境决定传入的_exports参数,而上面精简代码可以看出callback部分,amddefine传入参数,cjs直接使用环境支持exports全局变量则传入空对象让代码执行的时候不报错而已(使用依赖全靠各种库的全局变量)

到浏览器测试转译后UMD文件的运行结果

新建html文件:

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>animal-api</title>
</head>
<body>
  <header>测试浏览器环境animal-api</header>
  <div id="imageSrc"></div>
  <div id="text"></div>

  <script src="./dist/index.js"></script>
  <script>
    animalApi.getCat().then(res=>{
      document.querySelector('#imageSrc').textContent = res.imageSrc
      document.querySelector('#text').textContent = res.text
    })
  </script>
</body>
</html>

运行结果: ; 浏览器环境结果 👆 找不到animalApi变量 查看转译后源码,可以看出global.index = mod.exports; // 全局变量,导出的公共库挂载在全局的index下

我们需要指定挂载的变量为我们设置的名字,如下 babel-plugin-transform-modules-umd的exactGlobals配置 ; babel全局变量配置

改写plugin插件配置

js
plugins:[
  ['@babel/plugin-transform-modules-umd',{
    "globals": {
      "index": "animalApi",
    },
    "exactGlobals": true
  }]
]

重新 yarn build 👇 不同的地方

js
if() {
  // ...
} else {
  var mod = {
    exports: {}
  };
  callback(mod.exports, global.axios);
  global.animalApi = mod.exports; // 全局变量挂在到了babel设置的变量下,不再是index
}

刷新浏览器结果: ; 浏览器环境结果2 👆 animalApi找到了,但是getCat方法没找到,再查看源码或者打断点可知global.animalApi = mod.exports的export是个default 因此html处调用改为animalApi.default.getCat().then()

刷新浏览器结果: ; 浏览器环境结果3 👆 能运行到我们的业务代码里面,但是找不到axios 由此可知babel虽然可以帮我们处理模块化转译,但是不会帮我们处理第三方库的模块化。为了处理第三方库,我们需要使用到打包工具webpack

webpack处理第三方依赖

用webpack输出文件的话,为什么不把babel配置到webpack的loader里,而只是用webpack处理依赖这么浪费?

  1. 安装webpakc相关依赖
bash
yarn add -D webpack webpack-cli
  1. 创建webpack配置文件webpack.config.js
js
const path = require('path');
module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname,'lib'),
    filename: 'index.js', // 默认main.js
    library: 'animalApi',
    libraryTarget: 'var' // var this window umd
  }
}

webpack-libaray官方文档

  • 变量:作为一个全局变量,通过 script 标签来访问(libraryTarget:'var')
  • this:通过 this 对象访问(libraryTarget:'this')。
  • window:通过 window 对象访问,在浏览器中(libraryTarget:'window')
  • UMD:在 AMD 或 CommonJS 的 require 之后可访问(libraryTarget:'umd')
  1. 运行webpack

npx webpack 或者配置package.json中的"build": "webpack" 查看webpack编译后文件是一大坨代码

  1. 修改html引入资源路径

由原来引入babel转译后文件修改为webpack编译后文件 <script src="./lib/index.js"></script>

运行结果: ; webpack浏览器运行结果

直接使用webpack编译后的公共库就可以了,也就是说前面自己搞这么多babel是没有用的?因为执行的webpack根本就没有去执行babel相关的东西

不对 babel配置还要给jest用的,运行jest会自动根据babel-jest去使用babel转译代码

是这两个👇 babel相关的东西不需要了 @babel/cli @babel/plugin-transform-modules-umd

运行指令移除依赖:yarn remove @babel/cli @babel/plugin-transform-modules-umd 删除babel.config.js中的plugins配置

想要回看这些babel代码,看代码提交记录即可,这里直接删除,不做保留,所以package.json中的那段babel输出将失效

让公共库在nodejs环境下运行

上面已经实现了浏览器环境运行公共库和jest单元测试运行

而在nodejs环境下运行,和html同理,新建一个nodetest.js文件 调用animalApi的getCat方法 直接运行node会不支持esm,引用umd编译后文件即可正常使用

所以让公共库在nodejs环境下运行并不需要再做什么,只要使用方引用的是webpack编译后的文件即可


按需加载/异步加载/懒加载

区分按需加载和按需打包的概念 运行时(浏览器)、编译时(webpack) import()、tree-shaking es6支持或者script标签实现 、靠babel实现?识别AST中的引入语法import/require转化为按需处理依赖内容

思考🤔:要实现按需加载资源,我们常用的是一种工具方法,通过插入script标签,引入第三方资源,如下:

js
function loadJS(url) {
  const s = document.createElement("script");
  s.type = "text/javascript";
  s.src = url
  document.head.appendChild(s)
}

👇 通过promise实现等到加载完成

js
function loadJS(url) {
  return new Promise((resolve,reject)=>{
    const s = document.createElement("script");
    s.type = "text/javascript";
    s.src = url
    s.onload=()=>{
      resolve()
    }
    s.onerror:()=>{
      reject()
    }
    document.head.appendChild(s)
  })
}

但是这种方式无法获取到按需加载的资源内容和模块化export出来的东西,我们期望可以await出按需加载资源的内容(目前只能await到加载状态而已)

import() 是个function like的语法形式 class中的super() 也是function like语法 function like 并非继承 Function.prototypt ,无法使用import.call()等语法 同理并非继承 Object.prototypt ,无法使用Object构造函数

用function仿写一个import(),polyfills模块加载社区准备了一个importModule函数解决方案 这个解决方案有很多漏洞,仅供参考

js
function miniImport(url) {
  // 返回promise
  return new Promise((resolve,reject)=>{
    // 1. 创建script标签
    const scriptTag = document.createElement('script');
    scriptTag.type = 'module'; // 用esm加载js

    // 2. 生成随机变量名 Number.toString数字到字符串的转换的基数(从2到36) substring第2位往后
    const tempGlobal = '__tempModuleLoadingVariable' + Math.random().toString(32).substring(2) // ...lv2ka6rv2l8
    // 3. js文件用esm 去引按需加载的资源 而不是直接通过script引按需加载的资源,并存入临时全局变量中
    scriptTag.textContent=`
      import * as item from "${url}";
      window.${tempGlobal} = item;
    `
    // 4. 处理引入成功失败的promise返回 并且输出按需加载资源内容,删除script标签和js esm相关的变量
    scriptTag.onload=()=>{
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      scriptTag.remove();
    }
    scriptTag.onerror=()=>{
      reject(new Error('failed to load module script with URL'+url));
      delete window[tempGlobal];
      scriptTag.remove();
    }
    // 5. 把script插入html,即开始执行script的文件内容
    document.documentElement.appendChild(scriptTag)
  })
}

思路是通过一个script标签引入文件内容是esm引入资源(可以拿到资源内容)的js形式 因为import()会加载不同的资源,每次调用都不会有公用的存储空间,所以需要用到window的全局变量来暂存 并且通过随机全局变量名来区分不同资源来暂存 疑问🤔️:script标签的onload事件会等js内容执行完触发?而不是加载到js文件就触发吗?是因为import发生在编译时所以omload事件已经拿到变量了? 需要注意的是该示例,相同资源还是会再加载一次,而es6的import()不会。

另外,Babel为这种语法提供了dynamic-import-webpack插件,你可以安装它,并用它解析import()匹配符。 本质上完全以webpack的require.ensure()来实现按需加载,这样搞没有意义,import()语法出现的目的就是替代webpack的require.ensure(),结果因为兼容问题,又用babel转回webpack的方式

webpack中,早期有一个require.ensure()的语法跟import()一样,是把目标文件单独打包成一个文件,不会打到主包中 import()出来之后,requie.ensure()webpack 特有的,已被 import() 取代。-- webpack官方文档


思考🤔:所以现代工程下的import(),还是会被babel转移成require.ensure()?如果不转的话,不支持的浏览器怎么说? 如何解决import()的兼容问题,如何把需要按需加载的文件剥离(打包)出来?

现代工程不需要用babel插件处理import(),webpack在编译时进行依赖收集会处理按需加载资源,编译成webpack自己的__webpack_require__e 所以受限于兼容问题,很多好用的可以直接在浏览器运行的es6语法,会被webpack编译成自己的实现,到最后看到的效果实际上不是es6的成果 TODO: __webpack_require__e原理

看看深入浅出webpack这本书

组件库搭建阶段,组件库更新怎么让引用方更新组件

注意 vue 的 $attrs 属性透传,中心组件不能用props接收才能透传进去 $attrs 本质就是组件不接收时挂在html标签上的属性