Webpack 作为前端构建打包神器,已在如今的前端各大项目中必不可少,webpack 提供了一个核心,核心提供了很多开箱即用的功能,同时它可以用loader和plugin来扩展。webpack本身结构精巧,基于tapable的插件架构,扩展性强,众多的loader或者plugin让webpack显得很复杂。
webpack常用配置包括:devtool、entry、 output、module、resolve、plugins、externals等,本文将介绍如何只做一个自己的 loader
每一个 loader 其实都是一个 node 模块,提供一个函数,函数接收资源,进行一定的处理之后输出给下一个 loader。明白了这个基本原理,我们也可以编写一个自己的 loader。
熟悉 webpack 的同学对下面这张图一定不陌生
正如这张图所传达的信息一样,webpack 所做的就是将各种文件打包成浏览器可以识别的文件,比如常用的 babel-loader 可以将 es6 转成 es5,sass-loader、less-loader 负责预编译 sass less 为普通的 css,css-loader 解析 @import 和 url,style-loader 将 css 用 style 标签包裹,打包进 js,此外还有针对文件的 file-loader,raw-loader 等等。不同的 loader 执行不同的任务,对同一类文件配置的多种文件会从下往上以此执行。
项目开发中我们会用到一些 markdown 文件,有一些静态页面比如用户协议页面,我们会在项目中写一个 markdown,再用 js 去读取解析该文件,最后渲染出来。假如我们可以在 webpack 编译的过程中将 md 文件编译为 js 模块,在页面中直接引入,是不是会方便许多呢?
话不多说,开撸
webpack 提供了编写 loader 的文档提供参考
webpack.docschina.org/concepts/#l…官方提供了一个 loader-utils 的库,我们充分利用loader-utils包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项。schema-utils包配合loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。这里有一个简单使用两者的例子:
import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';
const schema = {
type: 'object',
properties: {
test: {
type: 'string'
}
}
};
export default function(source) {
const options = getOptions(this);
validateOptions(schema, options, 'Example Loader');
// 对资源应用一些转换……
return `export default ${ JSON.stringify(source) }`;
}
可以看到这个 loader 要做的就是资源转换,输出一个模版字符串,最终这个模版字符串就是一个普通的 js 模块,可以被 babel 直接处理,仿照这个写法,我们编写我们的 markdown loader。
- 首先提供我们主函数的入口
/** read and parser
* @param {String} sourse
*/
function loader (source) {
this.cacheable && this.cacheable()
const callback = this.async()
const options = Object.assign(defaultOptions, getOptions(this))
let result = null
// dosomething
callback(null, result)
}
2. 一个普通的 markdown 文件,当在 webpack 中解析之后,输入到 loader 中的是一个字符串
## 这是标题 ## ### 这是副标题 ###这里我们借助 commonmark 进行 markdown 文件的解析
/** read and parser
* @param {String} sourse
*/
export function parser (source) {
const fileContent = JSON.parse(JSON.stringify(source))
const reader = new commonmark.Parser()
const writer = new commonmark.HtmlRenderer()
return writer.render(reader.parse(fileContent.trim()))
}
解析之后的内容为对应的富文本
<h1>这是标题</h1><h2>这是副标题</h2>3. 最后我们构造一个普通的 react 组件的模版字符串进行输出
function loader (source) {
this.cacheable && this.cacheable()
const callback = this.async()
const options = Object.assign(defaultOptions, getOptions(this))
let result = null
if (options.use_raw) {
result = `
const raw_content = '${source}'
export default raw_content
`
} else {
const html = parser(source).trim()
result = `
import React from 'react'
const Component = props => (
<div {...props}>${processHtml(html)}</div>
)
export default {
html: '${html.replace(/\n/g, '')}',
Component
}
`
}
callback(null, result)
}
调用 this.async() 方法将模版字符串输出,其实这就是一个 React Component 了,这里输出了一个组件和富文本,方便使用的时候也可以自定义渲染富文本。大家可能注意到了 processHtml 这个方法,这是因为 md 文件中可能有代码块, commonmark 解析代码块时会对 >< 字符进行转义,所以这里需要处理。同时 jsx 中 {} 内的内容会被当作 js 执行,因此以下代码会报错
const MyComponent = () => {
return (
<pre>
<code>
function calc () {
let a = 3
return a
}
</code>
</pre>
)
}
需要将 code 内的内容转为 模版字符串:
const MyComponent = () => {
return (
<pre>
<code>
{`
${function calc () {
let a = 3
return a
}}
`}
</code>
</pre>
)
}
processHtml 主要做这个工作
/** process raw html
* @param {String} html
*/
export function processHtml (html) {
const codeReg = new RegExp('<code class=([\\s\\S]*)>([\\s\\S]*)</code>')
const code = codeReg.exec(html)
let newHtml = html
if (code) {
newHtml = newHtml.replace(code[2], '{`' + code[2] + '`}').replace(/>/g, '>').replace(/</g, '<')
}
return newHtml
}
最终我们的 loader 发包,npm 上可搜索 md-react-loader
npm i md-react-loader在我们项目的 webpack 中进行配置
{
test: /\.(md)$/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets:[
"@babel/preset-env",
"@babel/preset-react"
]
}
},
{
loader: require.resolve('md-react-loader')
}
]
}
注意这个 loader 产出的内容为 es6 语法的 React 组件,还需要 babel 去解析,重新编译之后我们就可以在项目中直接引入 markdown 了
import Md from './MyTest.md'
const MyComponent = () => {
return (
<Md.Component className="my_own_class" />
)
}
export default MyComponent
或者
import Md from './MyTest.md'
const MyComponent = () => (
<div
dangerouslySetInnerHTML={{ __html: Md.html }}
/>
)
export default MyComponent
同时也可以自己添加 className 定义样式,当 md 中含有代码时,这个 loader 提供了高亮的基础 css,同时也需要你安装 prismjs 这个库
import Md from './MyTest.md'
import 'prismjs'
import 'md-react-loader/lib/index.css'
const MyComponent = () => <Md.Component className="my_own_class" />
export default MyComponent
怎么样,一个 webpack loader 是不是非常简单呢?
再打一个广告
github.com/zybingo/md-…祝大家新年快乐~~