开篇
周末在家撸了...一个【随机提问器】,很简单的一个小工具,需求就是从一堆问题里面随机抽选几道题目进行QA
大佬们看到这里,估计代码在脑海里都已经写完了吧。
别急,需求还没说完呢!
我有一个自己的前端面试题库,将题按照分类进行了划分,然后将题目通过 markdown 文本的形式进行记录的。 所以我需要将这些题目和答案从 markdown 中剥离出来,可以怎么做呢?
- 发扬【复制粘贴工程师】优良的品质,将问题答案复制粘贴到一个 js对象 里面。
- 竟然想不出第二点了(大佬们不妨帮忙头脑风暴一下)。
我这么懒的人,差点就采用了第一种方法,但是想到每一次在这个题库里面新增题目,我就要对应的【复制粘贴】一下到 js对象 里我就觉得自己愧对这份前端工作。在这个时候我的脑海里突然出现了公司的两条企业精神——【科学】和【创新】,是的,这让我燃起了斗志,决定通过代码的方式去攻克这个问题,我的思路大致如下:
- 读取 markdown 文件,获取文本字符串
- 利用
unified包以及相关解析器将字符串转换成 AST - 将 AST 简化成我需要的 QA 的对象
难点已经攻克,后面的思路无非就是创建随机数,....不多赘述了。
看完本篇文章你能学到什么呢?
- webpack的实践运用场景以及一些新特性
- 认识并运用
unified工具包以及相关解析器 - 如何读取项目中的文件内容
- 强化对 AST 的认识
- 你也能自己写出这样一个小工具
- ...
(大佬请无视本段)在平时的生活中,我相信大家脑子里可能出现过各种各样可以提效的小工具,一开始想的都很美好,具体往深处思考的时候可能就会发现一些问题自己现有技术暂时不知道怎么做,应为大部分前端开发人员的工作就是写业务需求,所以知识面就锁定在几个前端框架的应用层面,所以在此想给一些朋友一点建议:
【希望大家不要将全部精力都集中在框架上,跳出框架,去看看 “外面的世界”,比如自己去写一个脚手架,自己去尝试一些小工具,去 Github 上看看别人都做了哪些有趣的事情等等。千万不能眼高手低,一个行动的矮子永远比一个只 “思想” 的巨人要走的远。】只有实际去做了,你才会发现你有没有收获。
旋转老脸推荐一下往期文章🙃:
正文
初始化项目
上面我们提到会用到 unified 包,这些包都是模块化编程的产物,早期的前端基本上通过 CDN 将 js 文件直接挂载到 HTML 中,加载完成后会将我们需要的对象挂载到 window 上,所以我们可以在全局作用域访直接访问到。
既然是模块化的产物,我们也需要通过模块化的方式将他们引进来。这样就离不开模块打包工具了,作者选用的是 webpack。
首先,我们在项目的根路径下初始化项目 npm init。
安装 webpack
npm install webpack webpack-cli --save-dev
配置webpack
webpack 入门还是很简单的,开发的几个基本配置(也是常规必备配置)牢记下面五个点:
- 配置模块打包入口
- 配置模块打包出口
- 设置模板
HTML,利用插件将打包后的文件自动引入到模板中 - 遇到不同的模块配置不同的
loader解析 - 配置
dev-server,并启用热更新。
这是个基础模板,然后基本上就是根据不同的需求在这个基础配置上修修补补。
首先在项目根目录下创建 webpack.config.js 配置文件,我们先来配置入口和出口:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './main.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
}
}
然后在 dist 文件夹下面创建一个模板 HTML 文件,安装 html-webpack-plugin 和 webpack-dev-server:
npm install html-webpack-plugin webpack-dev-server --save-dev
完善配置:
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './main.js',
output: {
filename: '[name].main.js',
path: path.resolve(__dirname, 'dist')
},
devServer: {
static: false,
compress: true,
port: 9000,
},
plugins: [
new HtmlWebpackPlugin({
template: './dist/index.html'
})
]
}
从 webpack-dev-server v4.0.0 开始,热模块替换是默认开启的,否则你需要在 devServer 中设置 hot 为 true。
然后我们修改一下 package.json 中的 script 设置启动命令:
// package.json
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server",
"build": "webpack"
},
这样我们的基本的配置就完成了,可以在 main.js 中写一句代码测试一下。
读取markdown文件内容
通常有两种办法:
- 使用直接将文件作为字符串导入进来。
- 使用
fetch请求对应的文件路径。
关于静态资源处理在 webpack5.0 版本之前借助 loader 对文件资源进行处理,webpack5.0版本做了更新,不再需要 loader了,我们一个个来看下:
老方法(webpack4.x)
通常有两种方式 :
file-loader将文件发送到指定路径raw-loader将文件作为字符串导入
file-loader
安装:
npm install file-loader --save-dev
完善一下配置:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
loader: require.resolve('file-loader'),
exclude: [/\.(js)$/, /\.html$/, /\.json$/],
options: {
name: 'static/[name].[ext]',
},
}
],
},
}
webpack 遇到此类文件的时候,会通过解析他们的路径,并将文件发送到 static/ 路径下。
我们通过 fetch 去请求资源文件需要先把资源文件引入进来:
import path from './books/demo.md'
// 或者
// 通过 `require` 引入进来的 ./books/demo.md 是一个 `Module` 对象:
const path = require('./books/demo.md').default
function fetchFiles(){
fetch(path).then(res=>{
console.log(res);
})
}
raw-loader
这种方式就更简单了,直接将文件以字符串的方式导入进来:
import demoText from './books/demo.md'
console.log(demoText);
如图:
再来看看新方式读取文本内容。
新版本方法
其实读取文件的方式没有变化,只不过webpack 5.0提供了新的模块类型替换loader,我们不再需要借助loader,只需要配置上做一些修改。
asset/resource发送一个单独的文件并导出 URL。之前通过使用file-loader实现。asset/source导出资源的源代码。之前通过使用raw-loader实现。
做如下配置:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.md$/,
type: 'asset/resource' // 或者 asset/source
}
],
},
}
同时,如果我们对资源文件有路径要求我们也可以加配置:
// webpack.config.js
module.exports = {
...
ouput:{
...
path: path.resolve(__dirname, 'dist'),
assetModuleFilename:'books/[hash][ext][query]' // 静态资源路径
},
}
文本转AST
【AST】即抽象语法树,通过对象这种数据类型抽象的描述你的内容:
例如:“公交车上有5个人”
{
transportType: "公交车", // 交通工具类型
peopleNumber: 5 // 人数
}
【AST】的运用场景还是非常多的,比如:babel,ESLint,webpack,跨平台框架等等。还有很多伙伴对这个东西了解又陌生,了解他是什么,陌生在难以理解它真正的作用。不妨思考一下,我们怎么将 ES6 的代码转换成ES5 的呢?我们怎么将钢转换成各式各样的钢制品的呢?
我们不是巴啦啦小魔仙,没有魔法直接将钢变成钢制品的。我们先得把钢熔炼了,然后再制作成各种产品。
javascript也一样,它能做的就是操作各种类型的数据。先把 ES6 的代码转换成 AST,然后去对这个 AST 做一些操作,转换成 ES5 再将它写成 js 文件。
回到主题,我们将markddown的文本字符串转换成 AST,先来认识几个工具包:
unified:一个将文本处理为抽象语法树的接口,支持处理remark(markdown)、retext(自然语言)和rehype(html)。传送门🚪remark-parse: 一个用于unified包的解析器,将markdown转换成 AST 语法树。remark-gfm: 一个用于扩展 Github 上对markdown新增的一些语法的解析插件。
安装:
npm install unified remark-parese remark-gfm --save-dev
运用:
import { unified } from "unified";
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm'
/**
* 将文本字符串转换成AST
* @param { string } text 需要处理为AST的文本字符
*/
function handleTextToAST(text){
const processor = unified().use(remarkParse).use([remarkGfm]);
const tree = processor.runSync(processor.parse(text));
console.log('tree',tree);
}
转换之树的结构大致如下:
// AST 结构
{
type:'root', // 类型
position:{}, // 位置
children:[ // 子节点
{
type:'heading', // 标题
position:{},
depth:1,
children:[
{
type:'text',
value:'React Hooks介绍一下',
position:{}
}
]
},
{
type:'paragraph', // 段落
position:{},
depth:1,
children:[
{
type:'text',
value:'xxxxxxxxxx',
position:{}
}
]
}
]
}
将AST处理成QA对象
我想要的数据结构如下:
{
q: 'React Hooks介绍一下',
a: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
我准备遍历 AST ,将 type 为 heading 的节点认定为题目问题,type 为 paragraph 的节点认定为上一个为 heading 问题的答案。当然还有一些其他类型的 type 类型需要考虑,比如 list,listItem 等,这个简化流程还是比较简单的,上代码:
/**
* 将AST->简化为题目Object
* @param {object} ast
* @returns
*/
function handleASTToJSON(ast){
const questions = [] // 问题列表
let question = {} // 单个问题
if(ast&&ast.children){
ast.children.forEach(element => {
if(element.type === 'heading'){
// 将上一个问题进栈
questions.push(question)
// 重置问题
question = {};
getTextInTree(element,question,'q')
}else{
getTextInTree(element,question,'a')
}
});
questions.push(question)
}
return questions;
}
我们所需要的题目和答案在树中嵌套的比较深,所以需要递归获取,来实现一下 getTextInTree。
const TRAVERSE_TYPES = ['heading','paragraph','link','list','listItem','strong']
/**
*
* @param { object } obj ast源对象
* @param { object } question 题目对象
* @param { string } qaType 'q' || 'a'
* @returns
*/
function getTextInTree(obj,question,qaType){
if(!obj) return;
if(TRAVERSE_TYPES.includes(obj.type)){
obj.children.forEach(item=>{
getTextInTree(item,question,qaType)
})
}else{
question[qaType] = (question[qaType] || '') + obj.value + "<br /><br />";
}
}
处理完之后的数据结构差不多这个样子:
[
{
q: "节流<br /><br />",
a: "xxxxxxxxx"
},
{
q: "函数柯里化<br /><br />",
a: "xxxxxxxxxxx"
},
{
q: "0.1+0.2 ! == 0.3<br /><br />",
a: "xxxxxxx"
}
]
剩下的事情就很简单了。
创建随机数,读取题目即可。
写到最后:
1. 为什么要做这个小工具呢?
周末的时候突然收到了一条面试邀请,就在这个时候我突然意识到自己前几个月复习的面试知识又给忘了,灵魂拷问之【为什么学过的知识就留不在脑子里呢?】,答案就是反复记忆,这是有科学依据的。当然,不可否认的是将知识在你脑海中形成系统有助于你记忆。除此之外,有些知识虽然更你现在脑海中的知识体系也没有丝毫挂钩,但你依然对它倒背如流,比如唐诗【静夜思】。所以,反复记忆是一个高效的事情,如果能合理的利用这个工具对自己进行 QA ,能节省你下次准备面试的时间。
2. 为什么要用 markdown 的形式呢?
这样的话我可以在坐地铁或者其他时候在github中像电子书一样直接阅读浏览它,总之已经这样了,你也可以用你想要的形式。
好啦,其他没什么了,下课👻!
朋友们,学而不思则罔,思而不学则殆,加油。
希望能给大家带来些许收获。
如果不对的地方欢迎大家留言指正,谢谢。