[toc]
- 怎么打包组件库
- 怎么基于组件库打包自己的组件库
前期准备
创建目录
mkdir animal-api
cd animal-api
npm init -y
创建公共库文件index.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单元测试
yarn add -D jest
创建单元测试脚本文件在test文件夹下index.test.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"
运行结果: ; 👆 可以看出报错信息是指测试脚本引入index.js不支持使用esm jest官方文档中有介绍使用babel使运行jest识别esm的方法
yarn add --dev babel-jest @babel/core @babel/preset-env
创建babel.config.js
// babel.config.js
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
此时运行 npx jest
; 此时的报错是index.js的公共库方法报错,缺少axios
yarn add axios
注意axios是生产环境也需要的库,所以不加 -D
再次执行npx jest
; 成功运行单元测试,并且单元测试的结果也是成功
让公共库可以在浏览器环境下运行
浏览器使用直接使用npm第三方库的方式是通过
<script src="">
来引入的(类似CDN),或者通过js动态创建<script>
来引入。
UMD模块化
针对浏览器通过<script>标签
引入的方式,第三方库一般会是UMD的模块化方式(同时支持AMD、CJS、全局变量) 如下是jQuery
库的UMD模式
(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
- 安装babel相关依赖
yarn add -D @babel/cli @babel/plugin-transform-modules-umd
前面已经安装了@babel/core
了,这次不用重复安装了
- 配置babel插件plugins 在
babel.config.js
中配置
module.exports = {
presets: [ ... ],
plugins: ['@babel/plugin-transform-modules-umd']
}
- 运行babel指令输出文件
直接运行npx babel index.js --out-dir dist
或者把脚本配置到package.json
中 "build": "babel index.js --out-dir dist"
运行结果: ;
输出转译后文件: ;
精简后转译后代码:
(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
打包后的代码
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
部分,amd
靠define
传入参数,cjs
直接使用环境支持exports
、全局变量
则传入空对象让代码执行的时候不报错而已(使用依赖全靠各种库的全局变量)
到浏览器测试转译后UMD文件的运行结果
新建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配置 ;
改写plugin插件配置
plugins:[
['@babel/plugin-transform-modules-umd',{
"globals": {
"index": "animalApi",
},
"exactGlobals": true
}]
]
重新 yarn build
👇 不同的地方
if() {
// ...
} else {
var mod = {
exports: {}
};
callback(mod.exports, global.axios);
global.animalApi = mod.exports; // 全局变量挂在到了babel设置的变量下,不再是index
}
刷新浏览器结果: ; 👆 animalApi找到了,但是
getCat
方法没找到,再查看源码或者打断点可知global.animalApi = mod.exports
的export是个default
因此html处调用改为animalApi.default.getCat().then()
刷新浏览器结果: ; 👆 能运行到我们的业务代码里面,但是找不到axios 由此可知babel虽然可以帮我们处理模块化转译,但是不会帮我们处理第三方库的模块化。为了处理第三方库,我们需要使用到打包工具
webpack
webpack处理第三方依赖
用webpack输出文件的话,为什么不把babel配置到webpack的loader里,而只是用webpack处理依赖这么浪费?
- 安装webpakc相关依赖
yarn add -D webpack webpack-cli
- 创建webpack配置文件
webpack.config.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
}
}
- 变量:作为一个全局变量,通过 script 标签来访问(libraryTarget:'var')
- this:通过 this 对象访问(libraryTarget:'this')。
- window:通过 window 对象访问,在浏览器中(libraryTarget:'window')
- UMD:在 AMD 或 CommonJS 的 require 之后可访问(libraryTarget:'umd')
- 运行webpack
npx webpack
或者配置package.json
中的"build": "webpack"
查看webpack编译后文件是一大坨代码
- 修改html引入资源路径
由原来引入babel转译后文件修改为webpack编译后文件 <script src="./lib/index.js"></script>
运行结果: ;
直接使用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
标签,引入第三方资源,如下:
function loadJS(url) {
const s = document.createElement("script");
s.type = "text/javascript";
s.src = url
document.head.appendChild(s)
}
👇 通过promise实现等到加载完成
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函数解决方案 这个解决方案有很多漏洞,仅供参考
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这本书
组件库搭建阶段,组件库更新怎么让引用方更新组件