引子
模版编译
这个词,想必前端也不陌生。我们日常在编写Vue
,React
的时候,我们有接触template
和tsx
,jsx
的相关写法。实质上,这些文件游览器是不能识别的,中间都会经过一层编译,转成相应的render
函数,再执行后续的操作。
模版文件如何被解析
以下只以
vue-cli
和create-react-app
为例,暂不涉及其他打包工具。(如vite
)
这里以vue-cli
和create-react-app
为例,这两个脚手架都是基于webpack
的,其中对tsx
,jsx
,template
都是有专门的loader
去进行解析。
create-reate-app
中的babel-loader
vue-cli
中的vue-loader
我们可以看到,不同框架会有不同的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>
复制代码
效果:
上方的模版有点类似于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;
复制代码
我们先看看我们的环境搭建成功没有
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
是生效的。
接下来我们只需要根据文件的内容,去进行解析,便可以生成不同的结果。 我们给loader
传递的第一个参数,实际上就是我们文件的内容。 我们可以打印一下source
function tplLoader(source) {
// ... code
console.log(source);
// ... code
}
复制代码
剩下的工作,我们只需要对相关内容进行解析即可。
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
复制代码
得到结果
写在最后
上方我们写了一个简易的loader
, 从而去实现解析我们的自定义文件。这里并没有去讲述compiler
的实现,因为个人只是简单的处理地利用正则去处理这种文件,做的也比较粗糙,其实并没有太大的参考意义。。
但笔者想表达的是,我们可以去进行思考。
- 我们可以如何去处理我们的
自定义文件
。 - 一些框架脚手架
loader
的实现思路。 - 我们可以发挥想象,去写一些怎么的
loader
。