scene reproduction
The author recently added a function to use the ES
module directly on the browser to my project CodeRun . Before using a package, you need to find its online CDN
address and then introduce it, like this:
Now you can do this directly:
So how is this achieved, very simple, using Skypack , the import statement in the picture above will actually end up like this:
import rough from 'https://cdn.skypack.dev/roughjs'
This conversion is achieved through babel
, we can write a babel
plugin, when accessing the import
statement, judge if it is a "naked" import and splicing it Skypack
Address:
// 转换导入语句
const transformJsImport = (jsStr) => {
return window.Babel.transform(jsStr, {
plugins: [
parseJsImportPlugin()
]
}).code
}
// 修改import from语句
const parseJsImportPlugin = () => {
return function (babel) {
let t = babel.types
return {
visitor: {
ImportDeclaration(path) {
// 是裸导入则替换该节点
if (isBareImport(path.node.source.value)) {
path.replaceWith(t.importDeclaration(
path.node.specifiers,
t.stringLiteral(`https://cdn.skypack.dev/${path.node.source.value}`)
))
}
}
}
}
}
}
// 检查是否是裸导入
// 合法的导入格式有:http、./、../、/
const isBareImport = (source) => {
return !(/^https?:\/\//.test(source) || /^(\/|\.\/|\.\.\/)/.test(source));
}
此外, script
标签添加一个type="module"
c1de63979a331cbbcae3f86fd29d7c1f---的属性,因为浏览script
ES
模块,只有Set this property to use module syntax.
Skypack
Skypack
CDN
服务, CDN
服务有点不一样,传统的CDN
文件The fixed access address, which package you want to use, you need to go to the release file of this package to find the file you want.
Most of the early packages provided IIFE
or commonjs
canonical modules, we need to import through link
or script
tags Basically all modern browsers natively support the ES
module, so we can use the module syntax directly on the browser. If you use the traditional CDN
service, then you first need a package that provides the ES
module file, and then we find it from CDN
ES
version of the file address, and then use, if a package does not provide ES
version, then we can not directly import it in the browser as a module, and Skypack
is specially designed for modern browsers, it will automatically do the conversion for us, we just need to tell it the name of the package we want to import, even if this package provides a file with version commonjs
, Skypack
will also return the ES
module, so we can import it directly on the browser as a module.
basic use
Its usage is simple:
https://cdn.skypack.dev/PACKAGE_NAME
Just splice the package name you need to import, for example, we want to import moment
:
import moment from 'https://cdn.skypack.dev/moment';
console.log(moment().format());
If the package name to be imported has a scope, just bring the scope, for example, to import @wanglin1994/markjs
:
import Markjs from "https://cdn.skypack.dev/@wanglin1994/markjs";
new Markjs();
specified version
Skypack
will go to npm
for real-time query according to the package name we provide, and return the latest version of the package, just like we usually execute npm install PACKAGE_NAME
, if You need to import the specified version, then you can also specify the version number, it follows the semver
( Semantic Version
(semantic version)) specification, you can import the specified version like this:
https://cdn.skypack.dev/react@16.13.1 // 匹配 react v16.13.1
https://cdn.skypack.dev/react@16 // 匹配 react 16.x.x 最新版本
https://cdn.skypack.dev/react@16.13 // 匹配 react 16.13.x 最新版本
https://cdn.skypack.dev/react@~16.13.0 // 匹配 react v16.13.x 最新版本
https://cdn.skypack.dev/react@^16.13.0 // 匹配 react v16.x.x 最新版本
Specify the export package or specify the export file
默认情况下, Skypack
会返回包主入口点指定的文件,也就是package.json
的main
module
的file, but sometimes this may not be what we need, take vue@2
as an example:
You can see that the page output is blank, why is this, let us open the vue2.6.14
version of the npm
package, you can first see the dist
which is provided in the directory Lots of files:
According to package.json
it can be seen that its main entrance is:
The files pointed to only contain the runtime, that is, do not contain the compiler, so it does not have the ability to compile templates in the browser, so it ignores the content of {{message}}
, what we want to import should vue.esm.browser.js
or vue.esm.browser.min.js
:
Skypack
also supports Let's import the specified file:
import Vue from 'https://cdn.skypack.dev/vue@2.6.11/dist/vue.esm.browser.js'
Just splicing the path after the package name:
Although it can be loaded into the file we specify in this way, there is a big limitation, that is, if the file to be loaded is not a ES
module, such as a commonjs
Skypack
, then- Skypack
The file will not be converted automatically, it will only be processed when it is used by package name (main entry).
css file
Some packages not only provide the js
file, but also the css
file, which is commonly found in various component libraries, such as element-ui
, for example:
<div id="app">
<div>{{title}}</div>
<el-button type="success">成功按钮</el-button>
<el-button type="primary" icon="el-icon-edit" circle></el-button>
<el-input v-model="input" placeholder="请输入内容"></el-input>
</div>
import Vue from 'vue@2.6.11/dist/vue.esm.browser.js'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
new Vue({
el: '#app',
data() {
return {
title: 'Element UI',
input: ''
}
}
})
We directly import the js
element-ui
css
file in ---9de6dd90a6ed0533763def7d88aa6150------327d4999ac7c5408558c1322e802b92e---, which is normal in our usual development, but the result of running on the browser is as follows :
Obviously, it is impossible to directly import css
in the ES
module, so we need to import --- css
through the traditional style:
@import 'element-ui/lib/theme-chalk/index.css'
fixed url
Although it is convenient to import by package name, because the latest version is returned every time, incompatibility problems are likely to occur. In the actual production environment, a specific version needs to be imported. Skypack
will be automatically generated and fixed The URL
:
In the production environment, we only need to replace it with one of the two lines in the figure URL
.
existing problems
Skypack
looks very good, but the ideal is beautiful, the reality is cruel.
First of all, the first problem is that the service of domestic network access Skypack
is difficult to describe. Anyway, when I use it, I can request it for a while, but it cannot be requested for a while, which is very unstable.
第二个问题就是有些复杂的包可能会失败,比如dayjs
、 vue
、 element-plus
Skypack
Both compilation failed:
Anyway, the author has found that the probability of failure is still very high after using it. You have to keep trying different versions of different files, which is very troublesome.
The third problem I encountered is css
which uses online fonts and cannot be loaded normally:
In view of the above problems, it is better to use it in the actual production environment.
Hands-on implementation of a simple version
Finally let's use nodejs
to implement a super simple version Skypack
.
start a service
Create a new project, create a new index.html
file in the project root directory to test the ES
module, and then use Koa
build a service, install:
npm i koa @koa/router koa-static
const Koa = require("koa");
const Router = require("@koa/router");
const serve = require('koa-static');
// 创建应用
const app = new Koa();
// 静态文件服务
app.use(serve('.'));
// 路由
const router = new Router();
app.use(router.routes()).use(router.allowedMethods())
router.get("/(.*)", (ctx, next) => {
ctx.body = ctx.url;
next();
});
app.listen(3000);
console.log('服务启动成功!');
When we visit /index.html
we can visit demo
page:
Access other paths to get access url
:
Download npm package
Ignoring the scoped package first, we temporarily think that the first segment of the path is the package name to be downloaded, and then we use the npm install
command to download the package (there are other better ways, welcome to leave a message in the comment area~ ):
const { execSync } = require('child_process');
const fs = require("fs");
const path = require("path");
router.get("/(.*)", async (ctx, next) => {
let pkg = ctx.url.slice(1).split('/')[0];// 包名,比如vue@2.6
let [pkgName] = pkg.split('@');// 去除版本号,获取纯包名
if (pkgName) {
try {
// 该包没有安装过
if (!checkIsInstall(pkgName)) {
// 安装包
execSync('npm i ' + pkg);
}
} catch (error) {
ctx.throw(400, error.message);
}
}
next();
});
// 检查某个包是否已安装过,暂不考虑版本问题
const checkIsInstall = (name) => {
let dest = path.join("./node_modules/", name);
try {
fs.accessSync(dest, fs.constants.F_OK);
return true;
} catch (error) {
return false;
}
};
In this way, when we visit /moment
, if this package is not installed, it will be installed, and if it is already installed, it will be skipped directly.
Handling commonjs modules
We can read the package.json
file of the downloaded package, if the following conditions are met, it means the commonjs
module:
1. type
field does not exist or has value commonjs
2. Does not exist module
field
const path = require("path");
const fs = require("fs");
router.get("/(.*)", async (ctx, next) => {
let pkg = ctx.url.slice(1).split("/")[0];
let [pkgName] = pkg.split("@");
if (pkgName) {
try {
if (!checkIsInstall(pkgName)) {
execSync("npm i " + pkg);
}
// 读取package.json
let modulePkg = readPkg(pkgName);
// 判断是否是commonjs模块
let res = isCommonJs(modulePkg);
ctx.body = '是否是commonjs模块:' + res;
} catch (error) {
ctx.throw(400, error.message);
}
}
next();
});
// 读取指定模块的package.json文件
const readPkg = (name) => {
return JSON.parse(fs.readFileSync(path.join('./node_modules/', name, 'package.json'), 'utf8'));
};
// 判断是否是commonjs模块
const isCommonJs = (pkg) => {
return (!pkg.type || pkg.type === 'commonjs') && !pkg.module;
}
commonjs
Module obviously cannot be loaded as ES
module, so it needs to be converted into ES
module first, we can use esbuild for conversion.
code show as below:
npm install esbuild
const { transformSync } = require("esbuild");
router.get("/(.*)", async (ctx, next) => {
let pkg = ctx.url.slice(1).split("/")[0];
let [pkgName] = pkg.split("@");
if (pkgName) {
try {
if (!checkIsInstall(pkgName)) {
execSync("npm i " + pkg);
}
let modulePkg = readPkg(pkgName);
let res = isCommonJs(modulePkg);
// 是commonjs模块
if (res) {
ctx.type = 'text/javascript';
// 转换成es模块
ctx.body = commonjsToEsm(pkgName, modulePkg);
}
} catch (error) {
ctx.throw(400, error.message);
}
}
next();
});
// commonjs模块转换为esm
const commonjsToEsm = (name, pkg) => {
let file = fs.readFileSync(path.join('./node_modules/', name, pkg.main), 'utf8');
return transformSync(file, {
format: 'esm'
}).code;
}
moment
The source code before conversion is as follows:
Converted as follows:
Let's test it in the index.html
file and add the following code:
<div id="app"></div>
<script type="module">
import moment from '/moment';
document.getElementById('app').innerHTML = moment().format('YYYY-MM-DD');
</script>
Handling ES modules
ES
module will be more complicated, because one module may import another module. First, let's support the specified file in the import package. For example, we want to import dayjs/esm/index.js
, when When importing the specified path, we will not perform the commonjs
detection, and the direct default is ES
module:
router.get("/(.*)", async (ctx, next) => {
let urlArr = ctx.url.slice(1).split("/");// 切割路径
let pkg = urlArr[0]; // 包名
let pkgPathArr = urlArr.slice(1); // 包中的路径
let [pkgName] = pkg.split("@"); // 指定了版本号
if (pkgName) {
try {
if (!checkIsInstall(pkgName)) {
execSync("npm i " + pkg);
}
if (pkgPathArr.length <= 0) {
let modulePkg = readPkg(pkgName);
let res = isCommonJs(modulePkg);
if (res) {
ctx.type = "text/javascript";
ctx.body = commonjsToEsm(pkgName, modulePkg);
} else {
// es模块
ctx.type = "text/javascript";
// 默认入口
ctx.body = handleEsm(pkgName, [modulePkg.module || modulePkg.main]);
}
} else {
// es模块
ctx.type = "text/javascript";
// 指定入口
ctx.body = handleEsm(pkgName, pkgPathArr);
}
} catch (error) {
ctx.throw(400, error.message);
}
}
next();
});
We know that when we import the js
file, the file suffix can be omitted, such as import xxx from 'xxx/xxx'
, so we have to check whether it is omitted, and it needs to be added, handleEsm
The function is as follows:
// 处理es模块
const handleEsm = (name, paths) => {
// 如果没有文件扩展名,则默认为`.js`后缀
let last = paths[paths.length - 1];
if (!/\.[^.]+$/.test(last)) {
paths[paths.length - 1] = last + '.js';
}
let file = fs.readFileSync(
path.join("./node_modules/", name, ...paths),
"utf8"
);
return transformSync(file, {
format: "esm",
}).code;
};
dayjs/esm/index.js
This file introduces other files:
Each import
statement browser will issue a corresponding request, let's modify index.html
to test:
<script type="module">
import dayjs from '/dayjs/esm/index.js';
document.getElementById('app').innerHTML = dayjs().format('YYYY-MM-DD HH:mm:ss');
</script>
It can be seen that each import
statement has issued a corresponding request, and the page running results are as follows:
After writing this, you may find that there is no need to judge whether it is the commonjs
module, and it is all handed over to esbuild
for processing. Let's simplify the code:
router.get("/(.*)", async (ctx, next) => {
let urlArr = ctx.url.slice(1).split("/");
let pkg = urlArr[0];
let pkgPathArr = urlArr.slice(1);
let [pkgName] = pkg.split("@");
if (pkgName) {
try {
if (!checkIsInstall(pkgName)) {
execSync("npm i " + pkg);
}
let modulePkg = readPkg(pkgName);
ctx.type = "text/javascript";
ctx.body = handleEsm(pkgName, pkgPathArr.length <= 0 ? [modulePkg.module || modulePkg.main] : pkgPathArr);
} catch (error) {
ctx.throw(400, error.message);
}
}
next();
});
package into a file
Take the entry file of axios
as an example:
The result after compiling with the esbuild
transformSync
method of ---40d1cca92887bc905d0ff8f3db569aa7--- is:
It can be seen that the require
method still exists, and the content of ---3405ca2a49a7287539a9bb7e1bb4846e require
is not packaged, so the es
module cannot be used. We can't use the transformSync
method after packaging it into a file, we need to use buildSync
This method executes the compilation of the file, that is, the input and output are in the form of files.
const { buildSync } = require("esbuild");
// 处理es模块
const handleEsm = (name, paths) => {
const outfile = path.join("./node_modules/", name, "esbuild_output.js");
// 检查是否已经编译过了
if (checkIsExist(outfile)) {
return fs.readFileSync(outfile, "utf8");
}
// 如果没有文件扩展名,则默认为`.js`后缀
let last = paths[paths.length - 1];
if (!/\.[^.]+$/.test(last)) {
paths[paths.length - 1] = last + ".js";
}
// 编译文件
buildSync({
entryPoints: [path.join("./node_modules/", name, ...paths)],// 输入
format: "esm",
bundle: true,
outfile,// 输出
});
return fs.readFileSync(outfile, "utf8");
};
// 检查某个文件是否存在
const checkIsExist = (file) => {
try {
fs.accessSync(file, fs.constants.F_OK);
return true;
} catch (error) {
return false;
}
};
Let's axios
compile the result:
Summarize
本文介绍Skypack
的使用,以及写了一个简单版的ES
模块CDN
服务,如果你用过vitejs
, You'll find that this is one of the things it does, although of course vite
the implementation is much more complicated.
The source code address of demo
.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。