手写一个模版解析Loader

引子

模版编译这个词,想必前端也不陌生。我们日常在编写Vue,React的时候,我们有接触templatetsx,jsx的相关写法。实质上,这些文件游览器是不能识别的,中间都会经过一层编译,转成相应的render函数,再执行后续的操作。

模版文件如何被解析

以下只以vue-clicreate-react-app为例,暂不涉及其他打包工具。(如vite

这里以vue-clicreate-react-app为例,这两个脚手架都是基于webpack的,其中对tsxjsx,template都是有专门的loader去进行解析。

create-reate-app中的babel-loader

image.png

vue-cli中的vue-loader

image.png

我们可以看到,不同框架会有不同的loader去帮忙解析。但在这里,笔者还需多去学习这些对应loader的解析文件的流程,所以这里并不会展开说这些loader具体如何解析这些文件。

在这里,我想引出的是,我们可以尝试写一个loader编译我们的自定义文件

预期成效

在这里,我们可以解析我们自定义文件.xxx来定义我们自己的模板。

笔者决定使用.hug的文件,从而进行定义。以达到下方效果。

<!-- 模版 -->
<template>
  <div>
    <ul>
      <li style="color: red;" > name: {{ name }}</li>
      <li> age: {{ age }}</li>
      <li> role: {{ role }}</li>
    </ul>
  </div>
</template>

<script>
export default () => ({
  name: 'hug',
  age: '18',
  role: 'student'
})
</script>

<!-- 最终效果 -->
<div>
  <ul>
    <li style="color: red;"> name: hug</li>
    <li> age: 18</li>
    <li> role: student</li>
  </ul>
</div>
复制代码

效果

image.png

上方的模版有点类似于vue,但实际上,差别很大,甚至只是模版文件写法长得像,但并没有其他关系。对vue的模版编译感兴趣的话,还请读者自行查阅相关资料。

那我们下面就来动手实现。

编码阶段

项目搭建

我们新起一个项目custom-loader

mkdir custom-loader && cd custom-loader
npm init --y
复制代码

这里需要安装webpack,webpack-cli,webpack-dev-server等,这里直接将package.json配置给出。

{
  "name": "custom-loader",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.16.0",
    "babel-loader": "^8.2.3",
    "html-webpack-plugin": "^5.5.0",
    "webpack-dev-server": "^4.6.0"
  },
  "devDependencies": {
    "webpack": "^5.64.4",
    "webpack-cli": "^4.9.1"
  }
}
复制代码

同时,我们需要去新建webpack的配置文件,在项目的目录下新建webpack.config.js.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: path.resolve(__dirname, 'src/index.js'),  // 入口
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'app.js'
  },
  module: {
    rules: [
      // 这里是匹配我们的自定义文件,给我们的`tpl-loader`解析
      {
        test: /\.hug$/,
        use: [
          'babel-loader',
          {
            loader: 'tpl-loader',
            options: {
              log: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html')
    })
  ],
  devServer: {
    port: 3000
  },
  // 注意,这里要配置loader解析的路径,否则可以找不到你指定的loader
  resolveLoader: {
    modules: [
      'node_modules',
      path.resolve(__dirname, 'loaders')
    ]
  }
}
复制代码

新建src目录,loaders目录,html文件

// src/index.js
console.log('index.js');
复制代码
<!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>Document</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>
复制代码
// loaders/tpl-loader/index.js
function tplLoader(source) {
  return `
    export default {
      log: () => {
        console.log('hello, world');
      }
    }
  `;
}
module.exports = tplLoader;
复制代码

image.png

我们先看看我们的环境搭建成功没有

npm run dev
复制代码

控制台有以下输出,则搭建成功。有报错的话,还请读者自行根据报错去进行解决。

index.js
复制代码

loader编写

上方的时候,我们编写了一个简单的loader, 实质上返回了一个对象,里面有个函数log。(注意,目前该loader还是写死结果的,即无论任何资源经过这个loader,实质上都只会返回带log的对象).

function tplLoader(source) {
  return `
    export default {
      log: () => {
        console.log('hello, world');
      }
    }
  `;
}
复制代码

我们在src下新建模版文件, 并在src/index.js下进行引入

// src/views/info.hug
<template>
  <div>
    <ul>
      <li style="color: red;" > name: {{ name }}</li>
      <li> age: {{ age }}</li>
      <li> role: {{ role }}</li>
    </ul>
  </div>
</template>

<script>
export default () => ({
  name: 'hug',
  age: '18',
  role: 'student'
})
</script>
复制代码
// src/index.js
import Hug from './views/info.hug';

console.log(Hug.log());
复制代码

游览器结果如下,此时我们的loader是生效的。

image.png 接下来我们只需要根据文件的内容,去进行解析,便可以生成不同的结果。 我们给loader传递的第一个参数,实际上就是我们文件的内容。 我们可以打印一下source

function tplLoader(source) {
    // ... code
    console.log(source);
    // ... code
}
复制代码

image.png

剩下的工作,我们只需要对相关内容进行解析即可。

compiler编写

我们新建compiler文件夹,编写compiler功能。

// loaders/tpl-loader/compiler/const.js
const HTML_TEMPLATE_TAG = 'template';
const SCRIPT_TEMPLATE_TAG = 'script';

module.exports = {
  HTML_TEMPLATE_TAG,
  SCRIPT_TEMPLATE_TAG
}
复制代码
// loaders/tpl-loader/compiler/index.js
const { HTML_TEMPLATE_TAG, SCRIPT_TEMPLATE_TAG } = require('./const');

/**
 * 模板中插值替换 <div>{{data}}</div> replaceMap = { data: 1 }
 * 则 <div>{{data}}</div> => <div>1</div>
 * @param {*} template 模板
 * @param {*} replaceMap 数据源
 * @returns 
 */
function tplReplace(template, replaceMap) {
  return template.replace(/\{\{(.*?)\}\}/g, (node, key) => {
    return replaceMap[key.trim()];
  });
}

/**
 * 生成正则表达式
 * @param {*} tag 
 * @returns 
 */
function generateTagReg(tag) {
  return new RegExp(`<${tag}>([\\s\\S]*?)<\/${tag}>`)
}

/**
 * 从模板中根据tag取出内容 例如 getTemplateFromSource(source, 'template') // 取出<template> .... </template> 中的内容
 * @param {*} source 
 * @param {*} templateTag 例如 `template`
 * @returns string
 */
function getTemplateFromSource(source, templateTag) {
  const res = generateTagReg(templateTag).exec(source);
  return res && res.length > 0 ? res[1] : '';
}

/**
 * 拿到模板中的`template`标签的内容
 * @param {*} source 
 * @returns 
 */ 
function getHTMLTemplateFromSource(source) {
  return getTemplateFromSource(source, HTML_TEMPLATE_TAG);
}

/**
 * 拿到模板中的`script`标签的内容
 * @param {*} source 
 * @returns 
 */ 
function getScriptTemplateFromSource(source) {
  return getTemplateFromSource(source, SCRIPT_TEMPLATE_TAG);
}

/**
 * 解析script内容拿到数据
 * @param {*} scriptTemplate 
 * @returns 
 */
function getDataMapFropmScriptTemplate(scriptTemplate) {
  const fnStr = scriptTemplate.replace('export default', '');
  return eval(fnStr)();
}

/**
 * 根据模版,进行编译
 * @param {*} source 
 * @returns string
 */
function tplCompiler(source) {
  const htmlTemplate = getHTMLTemplateFromSource(source);
  const scriptTemplate = getScriptTemplateFromSource(source);

  const dataMap = getDataMapFropmScriptTemplate(scriptTemplate);
  return tplReplace(htmlTemplate, dataMap).replace(/[\n\t]+/g, '');
}

module.exports = {
  tplReplace,
  tplCompiler
}
复制代码

并修改tpl-loader的入口文件

const { tplReplace, tplCompiler } = require('../utils/index.js');
const { getOptions } = require('loader-utils');

function tplLoader(source) {
  const { log } = getOptions(this);

  const _log = log ? `console.log('compiled the file which is from ${this.resourcePath}')` : '';

  const content = tplCompiler(source);

  return `
    export default {
      template: '${content}'
    }
  `;
}
module.exports = tplLoader;
复制代码

上方的compiler实质上是根据自己的规范的定义,通过正则字符串拼接替换等方式生成内容。 这里就不细说compiler的思路,大家其实可以尽情发挥。可以根据自己的规范、规则,编写对应的complier.

最后,我们在更改一下我们的入口文件

import tpl from './views/info.hug';

const oApp = document.querySelector('#app');

oApp.innerHTML = tpl.template;
复制代码

重启启动一下项目。

npm run dev
复制代码

得到结果

image.png

写在最后

上方我们写了一个简易的loader, 从而去实现解析我们的自定义文件。这里并没有去讲述compiler的实现,因为个人只是简单的处理地利用正则去处理这种文件,做的也比较粗糙,其实并没有太大的参考意义。。

但笔者想表达的是,我们可以去进行思考。

  • 我们可以如何去处理我们的自定义文件
  • 一些框架脚手架loader的实现思路。
  • 我们可以发挥想象,去写一些怎么的loader

参考

手写自己的『模板编译loader』

Guess you like

Origin juejin.im/post/7040296876897206308