登录/注册

Prism.js动态加载所需语言包

用户头像管理员63阅读

前言

Prism是一款非常好用的前端代码高亮插件,很多开发者搭建的文章、博客分享网站中都使用到了prism.js来做代码高亮,但是在官网的下载网站选完了主题和插件后却犯了难:如果选择语言包,如果全选那么体积将近600kb如果选少了害怕以后不够用,还要来补。其次,基本上只有语言包支持Node.js环境,插件基本都是基于DOM实现没有对Node.js环境进行兼容。于是有了一个想法:通过API接口将语言包动态返回,根据前端传来的参数,主题+语言包+插件拼接后返回给前端的script和link标签。

于是我实现了这个功能,并且应用于自己的网站上,网站可以选择主题进行代码高亮,最重要的他会将页面中代码块使用到的高亮语言包进行拼接返回,实现了按需分配。(其实也不是难事,官网download页面也是这样)

最终效果:

后端,JavaScript,讲解,Node.js
实现

需要进行保存的:

  1. components中的prism-core,这个是核心包
  2. components中的其他文件是语言包
  3. themes文件夹下是主题包css
  4. plugins下是插件包
  5. components.json是语言依赖包里面记录了有哪些语言包、依赖关系、别名

下面的查找我也以这五个包名来代替需要返回的文件内容。返回时也是按照核心包(js)+主题(css)+语言包(js)+插件(css、js)进行拼接,下面是我的存放格式,在读取文件时我会以public的绝对路径进行读取,prism下是本文代码高亮相关的。

后端,JavaScript,讲解,Node.js
我们先将GitHub源代码克隆下来,里面有全套的主题包、语言包和插件包到手后最好先写个代码将开发中的未压缩版本删除,只保留min版本。

后端,JavaScript,讲解,Node.js
下面贴出我实现的代码:

整个API接口的完整代码(Express)

import express, { NextFunction, Response, Request } from "express";
import fs from "fs";
import cache from "@/common/middleware/cache";

const app = express();
const router = express.Router();
/** 
 * 定义使用的主题和插件
* !如果使用了工具栏中的按钮,一定在先定义toolbar
*/
const config = {
  themes: "tomorrow",
  plugins: [
    "toolbar",
    "line-highlight",
    "line-numbers",
    "inline-color",
    "copy-to-clipboard",
    "show-language",
  ],
};
/** prism代码高亮的相关配置*/
const components: any = JSON.parse(
  fs.readFileSync("public/prism/components.json").toString()
).languages;

const cors = fs.readFileSync(`public/prism/prism-core.min.js`).toString();
const themes = fs.readFileSync(`public/prism/themes/prism-${config.themes}.min.css`).toString();
const plugins = config.plugins.map(item => {
  return {
    css: fs.existsSync(`public/prism/plugins/${item}/prism-${item}.min.css`)
      ? fs.readFileSync(`public/prism/plugins/${item}/prism-${item}.min.css`).toString()
      : "",
    js: fs.existsSync(`public/prism/plugins/${item}/prism-${item}.min.js`)
      ? `/**插件:${item}**/${fs.readFileSync(`public/prism/plugins/${item}/prism-${item}.min.js`).toString()}\n`
      : "",
  };
});

let language: { [key: string]: string } = {};
fs.readdirSync(`public/prism/language`).forEach(item => {
  language[item.replace(`prism-`, "").replace(`.min.js`, "")] = fs
    .readFileSync(`public/prism/language/${item}`)
    .toString();
});

/**
 * 根据传来的单个类型递归返回全部依赖类型,并且排序
 * @params type {string} 类型
 * @return typeArray {string[]} 所有依赖到的类型
 */
function getAllType(type: string) {
  let typeHub: string[] = [type]; //存储类型(先将原始类型存进来)
  function requireType(_type: string) {
    let languageRequire: string | string[] = components[_type]?.require;
    if (!languageRequire) {
      return; //第一个语言没有依赖就可以直接返回了
    }
    if (typeof languageRequire == "string") {
      typeHub.unshift(languageRequire);
      requireType(languageRequire);
    } else {
      languageRequire.forEach(item => {
        typeHub.unshift(item);
        requireType(item);
      });
    }
  }
  requireType(type);
  return typeHub;
}

router.get("/high-light/:type",cache, async (req: Request, res: Response, next: NextFunction) => {
  if (req.params.type == "css") {
    res.setHeader("Content-Type", "text/css;charset=UTF-8");
    res.write(`${themes}\n${plugins.map(item => item.css).join("\n")}`);
    res.end();
    return;
  }

  let _language = (req.query.languages + "")
    .split(",")
    .map(item => {
      //如果没有类型,将使用别名,转化为正确名称
      if (language[item]) return item;
      let aliasIndex = Object.values(components).findIndex((index: any) => {
        if (!index.alias) return false;
        return typeof index.alias == "string" ? index.alias == item : index.alias.includes(item);
      });
      //既没有别名,又没找到语言的返回false
      return aliasIndex != -1 ? Object.keys(components)[aliasIndex] : false;
    })
    .filter(item => !!item)
    .map(item => getAllType(item as string))
    .flat()
    .map(item => {
      return language[item] ? `/**language:${item}**/\n${language[item]}` : "";
    })
    .join(`\n`);

  res.setHeader("Content-Type", "text/javascript;charset=UTF-8");
  res.write(`
  /**个人博客:blogweb.cn**/
  ${cors}\n${_language}\n${plugins.map(item => item.js).join("\n")}
  `);
  res.end();
});
export default router;

讲解

先将定义一下主题和使用到的插件,然后将这些css和js的包都加载出来,挂到一个对象上。对文件进行读取,在前端带参数请求时可以直接以key:value的形式进行查询、拼接、返回。有的插件只有JS没有CSS,需要进行一下判断,没有CSS就返回一个空字符串,不会影响拼接,并且在合并是无需判断。

后端,JavaScript,讲解,Node.js
在源代码里面有个components.json的文件也是需要保存的,里面记录的各个语言包相互的依赖关系(require属性),以及语言包的别名。我们在编写代码时需要对这种情况进行处理。

后端,JavaScript,讲解,Node.js
我们需要对依赖关系进行处理,同时将依赖包引进,并且依赖包也需要返回给前端。根据components.json内依赖关系进行递归查询,一直到不在有require属性了就返回数组。

后端,JavaScript,讲解,Node.js

到了这里基本的工具函数就写完了,开始编写路由,路由的参数不需要和我的一样(我的不是很标准),我们可以在一个接口内通过参数将CSS和JS都能进行处理,我们判断req.params类型如果是CSS就直接将主题(themes)的CSS和使用到的插件的CSS进行拼接返回即可。

后端,JavaScript,讲解,Node.js

如果是JS就要相对麻烦一些要判断依赖关系,不过我们在之前已经写好工具函数了使用req.query.languages获取前端需要的语言包(参数格式样例:css,typescript,cpp),转为数组后先查找别名,判断之前以key:value形式保存的语言包对象上有没有对应的属性, 没有的话在components.json中进行查找所有语言的require属性,看看是否可以和前端需要的语言匹配,如果找到了是前端返回的语言别名,那就改成标准名称,否则说明Prism.js没有提供对应的语言包,就返回false,然后在后面给过滤掉。

后端,JavaScript,讲解,Node.js
最后这一步就是拼接、合并了,合并后整个语言包的JS为数据格式join一下转为字符串(拼接过程中其实不在需要判断了,直接返回 language[item]就行),上面已经将false过滤掉了。

后端,JavaScript,讲解,Node.js

然后就是最后一步了,将核心包、语言包、插件,按照顺序拼接返回出去。

测试

按照路径先测试css,只有5kb

后端,JavaScript,讲解,Node.js
测试JS路径,以html,typescript为例,只有33kb,因为在语言包的拼接上留了注释,可以ctrl+F搜索一下,只有这两个语言包和相依赖的包,例:ts>js>c-like,算是html一个四个包,前端页面完美运行。

后端,JavaScript,讲解,Node.js

使用

我使用了ORM框架操作数据库,所以直接在文章表中加了个虚拟字段,在服务器端判断文章中的代码高亮使用了什么语言包,可以根据项目实际情况来决定在哪里进行语言判断。同时判断方法需要看HTML结构而定,我之前使用过很多富文本编辑器和Markdown编辑器来写博客,每个编辑器代码块结构又略有不同,所以只能考虑多种情况,麻烦一点。

后端,JavaScript,讲解,Node.js
请求CSS、JS资源包需要在客户端创建link和script标签来加载。

后端,JavaScript,讲解,Node.js

思路

  1. 用户端创建link和script标签携带参数向服务器获取对应的语言包
  2. 读取文件夹,将主题包、插件包中使用的主题或者插件进行读取,将语言包文件读取并保存在对象中
  3. 获取各个语言包的依赖关系,并且保存到数组中
  4. 在遍历语言包数组时对语言包对象上没有的进行别名查询,如果确定是使用了别名,那就改成标准的名字,如果确定是语言包转给你没有成语言,那就返回false,然后过滤掉
  5. 判断type类型进行核心包(js)+主题(css)+语言包(js)+插件(css、js)进行拼接
  6. 返回拼接的CSS和JS字符串
Preview
登录后评论