如果你只是想快速配置 cdn,可以直接看快速配置篇。
Chrome 将于不久后默认开启 限制子资源在私有网络的分发 策略,它会导致非 https 协议无法使用第三方外链,这将导致开发环境无法使用 cdn。这个策略我在测试时对 CSS 样式表无效。
这个安全策略开启后,你可能会在未来遇到这个错误:
我当前是 Chrome 91,可以在 Chrome 浏览器的地址栏中输入 chrome://flags/#block-insecure-private-network-requests 来找到这个策略,在未来的版本它会被默认开启,如果你现在主动启用该策略,那么开发环境使用 cdn 你将遇到一个错误:The request client is not a secure context and the resource is in more-private address space \
local`.`
综上,该博客于 2021-07-08 日做了非常大的改动。本来 Vue 也出 3 了,这篇模块也没还没讨论 Vue3 的 cdn 引入方式。
引入的 vue 文件必须是游览器版本,最少需要 运行时源码,根据你的 Vue 版本不同,选择的文件也不同:
运行时源码和完整版有什么不同?
运行时源码少了编译器,而完整版有,因为 vue-loader 已经编译了template
,所以不需要再次编译。这意味着运行时源码还要小一点,详见vue2 官方文档 或者 vue3 官方文档。
我使用的是 bootcdn 的运行时压缩模块,体积会更小。切记,使用的 cdn 需要和你的 package.json 中依赖包的版本号相同,以免产生版本 bug。
因为开发环境不能安全使用第三方外链,所以第三方 cdn 只能在生产环境中使用。
具体引入方式是在 vue.config.js
中注册 cdn 的模板变量,然后在 public/index.html
中插入。一手准备,一手插入。
使用 vue-cli 构建的项目,可以在 项目/public/index.html 的 head 元素中 插入准备好的 cdn 模板。
我的代码如下:
<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" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>vue-app</title>
<!-- 这里是插入的 CDN 位置,编写下面这行代码即可。 -->
<%= htmlWebpackPlugin.options.cdns %>
</head>
这里使用了 html-webpack-plugin 插件的模板参数,你可以通过此插件准备一些参数插入到这个模板中。
可以看到,我们插入了 <%= htmlWebpackPlugin.options.cdns %>
参数,这就是我们需要准备的 cdn 模板。
来到 项目/vue.config.js
文件,如果没有就创建。
准备分两步:
设置外部依赖:在打包时忽略已经用 cdn 引入的模块。
添加模板参数:准备插入的 cdn 资源元素。
以下代码中,Moment.js 和 Vue3 就是用了 cdn 来引入,并且在打包时忽略这两个模块:
/** @file vue.config.js */
module.exports = {
chainWebpack: (config) => {
// 只在生产环境使用 cdn
if (process.env.NODE_ENV === "production") {
// 忽略 vue 和 moment 这两个模块
config.externals({
vue: "Vue",
moment: "moment",
});
// 修改 HtmlWebpackPlugin 插件参数,植入 cdns 这个模板参数,值为 Vue3 和 Moment.js 的 cdn 链接
config.plugin("html").tap((args) => {
args[0].cdns = `
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.1.2/vue.runtime.global.prod.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js" crossorigin="anonymous"></script>
`;
return args;
});
}
},
};
其中:
config.externals
用于配置 外部扩展,其作用是不打包使用外部引入的扩展,也就是 build 的时候不打包这些模块。它的键名和值是有意义的:
键名:键名为使用外部扩展的模块。 比如 import VueLib123 from "vue"
这句话,模块 from "vue"
是不变的,模块名就是这个就是键名。
值:值就是使用 cdn 后,这个模块在全局上的引用。 比如 Vue 使用 cdn 引入后,全局上使用 Vue
变量来访问,那么外部扩展的值就是 Vue。
config.plugin("html").tap
用于修改 HtmlWebpackPlugin 这个插件的参数,这里插入一个 cdns
参数,所以在 public/index.html
中可以使用 <%= htmlWebpackPlugin.options.cdns %>
来访问这个参数。
- 上述代码使用的 Webpack 配置方式是 链式调用,因为涉及修改插件参数,无法使用 简单配置。
- 我在 script 元素中设置了 crossorigin 属性,取消了用户凭证传递,详见 CORS settings attributes 。
- 如果你想更安全的使用第三方 cdn ,那么推荐你使用 SRI。
注意:源代码只是改了 “项目/public/index.html” 文件和配置了 vue.config.js,没有修改其他代码。此方法并不会在开发环境中使用 cdn,具体原因参考第一节前言。
测试代码的 package.json
依赖为:
{
"dependencies": {
"core-js": "^3.6.5",
"moment": "^2.29.1",
"vue": "^3.0.0"
}
}
其中 moment
和 vue
使用了 cdn 引入。
不使用 cdn 的打包情况(注释掉 vue.config.js 添加的代码就可测试):
warning
webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
File Size Gzipped
dist\js\chunk-vendors.50f67bb1.js 379.14 KiB 110.63 KiB
dist\js\app.d4b48aed.js 6.54 KiB 2.45 KiB
Images and other types of assets omitted.
DONE Build complete. The dist directory is ready to be deployed.
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
Done in 5.03s.
上来直接报一个包太大警告,这项目还什么都没写,就是在 App.vue
里面引入了 Moment.js。
其中 chunk-vendors
为 379.14 KB,包含了 core-js
、moment
和 vue
三个模块。
使用 cdn 的打包情况:
DONE Compiled successfully in 720ms 下午2:52:47
File Size Gzipped
dist\js\chunk-vendors.20dbb2c7.js 24.82 KiB 9.06 KiB
dist\js\app.08fbc8da.js 2.02 KiB 0.99 KiB
Images and other types of assets omitted.
DONE Build complete. The dist directory is ready to be deployed.
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
Done in 3.49s.
此时 chunk-venders 降到 24KB,只剩一些关于 core-js
的依赖了。
小结:
这种打包方式只是阐述基本实现方式,如果你觉得比较麻烦,可以看最后一节快速配置篇。
如果想继续优化打包,建议了解异步路由,异步加载等,谨慎使用 require.context
。
我自己也没那么多时间每次去查怎么配置,所以直接封装了一个函数。
public/index.html
的 head
元素里面添加下面这行代码用来插入 cdn。<%= htmlWebpackPlugin.options.cdns %>
useCDNs.js
在任意位置,我放到了 根路径/webpack/useCDNs.js
中。新建后复制以下代码粘贴进去即可,注释不想要可以删掉。/** @file useCDNs.js */
/** @typedef {string} ModuleName 模块名 */
/** @typedef {string} ModuleRefer 模块在全局的引用 */
/** @typedef {string} ElementTem 元素模板 */
/** @typedef {{mod:ModuleName;refer:ModuleRefer;el:ElementTem}} CDNItem cdn 项目 */
/**
* cdn 使用函数。
*
* 此函数可以在指定开发环境中,指定某些模块作为外部依赖出现,并把准备好的第三方 cdn 模板以 `cdns` 参数通过 HtmlWebpackPlugin 插件插入到 `public/index.html` 文件中。
* 你可以在 `public/index.html` 中使用 ejs 语法 <%= htmlWebpackPlugin.options.cdns %> 来插入准备好的 cdn。
*
* @param {import('webpack-chain')} config webpack-chain 实例
* @param {CDNItem[]} cdns 传入需要使用的 cdn 数组
* @param {string} env 什么环境下使用 cdn ,默认生产环境
*/
module.exports = function useCDNs(config, cdns = [], env = "production") {
if (process.env.NODE_ENV !== env) return;
config.externals(
cdns.reduce((prev, v) => {
prev[v.mod] = v.refer;
return prev;
}, {})
);
config.plugin("html").tap((args) => {
args[0].cdns = cdns.map((v) => v.el).join("");
return args;
});
};
vue.config.js
中使用如下方式调用:/** @file vue.config.js */
const useCDNs = require("./webpack/useCDNs");
module.exports = {
chainWebpack: (config) => {
useCDNs(config, [
{
mod: "vue",
refer: "Vue",
el: `<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.1.2/vue.runtime.global.prod.min.js"></script>`,
},
{
mod: "moment",
refer: "moment",
el: `<script src="https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js" crossorigin="anonymous"></script>`,
},
]);
},
};
其中,mod
和 refer
对应了外部扩展的模块名和引用名,el
为 cdn 元素。
关于 useCDNs
详细使用参考原文件,注释已经写满了。
不用删除,也不能删除。
因为本文方法只是配置了生成环境使用 cdn,开发环境使用的还是 node_moduels
中的本地包,删了开发环境就 GG。
以前我使用的是 bootcdn,但这家前年过年前后崩过一次,后来没怎么使用。
你可以使用 UNPKG,这个是和 npm 库直接关联的。
或者 JSDELIVR,老牌 cdn 有保障,还支持 SRI。
vue-cli3 和 vue-cli4 都兼容这种配置方式。
首先,样式表是没有外部扩展的;其次,文章前言中提到的限制子资源策略对 CSS 并无作用。
如果你想让 CSS 使用 CDN,可以直接在 public/index.html
文件的 head
元素中直接引入即可,开发环境和生产环境都有效。
需要说的是,因为样式表没有外部扩展。如果全搬上文中引入 JS 的方式,就需要删除项目中的 CSS 引入,不然在生产环境中就会存在两份样式表。
比如你用了 ElementUI,在项目中你引入了 import "element-ui/lib/theme-chalk/index.css"
,如果在 public/index.html
中再引入一次,或者在 vue.config.js
中再添加一次,都会造成二次引入。虽然多次引入对渲染结果无影响,但这样做会让浏览器产生额外的样式计算,损耗性能。