关于前端:手写自己的『模板编译loader』

34次阅读

共计 5152 个字符,预计需要花费 13 分钟才能阅读完成。

前言

webpack 是每个前端简直每天都接触的工具,然而很多人不理解,因为即便你不理解它也不影响你日常开发,你做的只是每天写写业务代码,脚手架一搭,npm run dev回车键一按,完事了,再不然须要配置一些全局变量,按需打包的时候,百度一下,大同小异。
前端工程化时代,webpack 十分有必要入门,至多要看过一遍,晓得一些罕用的配置和执行逻辑,还有一些罕用的插件,比方 html-webpack-plugin, 能写简略的 node 脚本帮忙打包上线等等,这样即便公司只有一个前端,你至多能当一只能使出劲的牛。

初探 loader

webpack 是属于模块化计划,他能让任意类型的文件都能运行在浏览器中,比方 .vue 文件,这个类型的文件只有通过自定义 loader 能力被浏览器解析执行,以后期间还有很多过程,盲猜一下都能猜到这个 loader 就是 vue-loader, 还有 scss,less 要编译成浏览器辨认的 css 必定要通过less-loader 或者sass-loader, 除了以上,列举一下罕用的一些 loader:

  • style-loader 将 css 增加到 DOM 的内联款式标签 style 里
  • css-loader 容许将 css 文件通过 require 的形式引入,并返回 css 代码
  • postcss-loader 用 postcss 来解决 CSS
  • file-loader 散发文件到 output 目录并返回相对路径
  • url-loader 和 file-loader 相似,然而当文件小于设定的 limit 时能够返回一个 Data Url
  • html-minify-loader 压缩 HTML
  • babel-loader 用 babel 来转换 ES6 文件到 ES5

loader 用于对模块的源代码进行转换。loader 能够使你在 import 或 ” 加载 ” 模块时预处理文件。

webpack

从新回过头来了解一下 webpack,先理解一下 webpack5 个外围概念:

Entry

入口 (Entry) 批示 Webpack 以哪个文件作为入口终点剖析构建外部依赖图并进行打包。

Output

输入 (Output) 批示 Webpack 打包后的资源 bundles 输入到哪里去,以及如何命名。

Loader

Loader 让 Webpack 可能去解决那些非 JavaScript 语言的文件,Webpack 自身只能了解 JavaScript。

Plugins

插件 (Plugins) 能够用于执行范畴更广的工作,插件的范畴包含从打包和压缩,始终到从新定义环境中的变量等。

Mode

模式 (Mode) 批示 Webpack 应用相应模式的配置。
分为 development 和 production 两种模式

本人写一个模板编译 loader

  • 创立一个空文件夹,tpl-loader-create-re, tpl 文件是模板文件,很多模板引擎会见到这个类型文件。
  • npm init -y 初始化 npm
  • 增加 webpack4 三大件, 并在根目录新建 webpack.config.js 配置文件
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.7.2"
  • 依照下面说的 webpack5 个外围概念,对其别离进行简略配置:
module.exports = {
  mode: 'development',
  // 入口文件为 src 下的 app.js
  entry: resolve(__dirname, 'src/app.js'),
  // 输入门路
  output: {path: resolve(__dirname, 'build'),
    filename: 'app.js'
  },
  // 抉择一种 source map 格局来加强调试过程。不同的值会显著影响到构建 (build) 和从新构建 (rebuild) 的速度。devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.tpl$/, // 匹配所有.tpl 文件 
        use: [// 应用本人的 loader 或者第三方 loader, 执行程序从下到上(从右到左)
          'babel-loader',  // 须要装置 babel-loader 相干插件 babel-loader,@babel/core html-webpack-plugin
          {
            loader: 'tpl-loader',
            options: {log: true // 自定义配置,是否须要开启日志}
          }
        ]
      }
    ]
  },
  plugins: [
    // 须要 HtmlWebpackPlugin 插件反对,组装 html 文件,将 css 和 js 等文件导入等工作
    new HtmlWebpackPlugin({template: resolve(__dirname, 'index.html')
    })
  ],
  // 开发服务器相干配置
  devServer: {
    // 配置端口为 3333
    port: 3333
  }
}
  • 默认曾经创立了以上配置中用到的文件,批改 package.json 中的脚本命令
"scripts": {"dev": "webpack-dev-server"  // 应用 webpack 开发服务器执行}

当前目录构造如下:

  • 编写 app.js
import infoTpl from './info.tpl'

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

oApp.innerHTML = tpl({
  name: '小聪忙',
  age: 34,
  career: 'web developer',
  hobby: '游览, 画画'
})

要达成的成果:tpl 是咱们本人写的一个模板,我须要把模板导入,而后我只须要传入相干的模板数据就能在页面中显示带数据的模板的 html 页面,
所以要害就是解析咱们的 tpl 文件,咱们须要自定义一个 loader

  • 根目录创立 loader

  • 配置 webpack
module.exports = {
  // ...
  // 加载我的项目的所有 loader 门路
  resolveLoader: {
    modules: ['node_modules', resolve(__dirname, 'loaders')]
  }
  // ...
}
  • 编写 loader 文件逻辑

app.js中用到的 tpl 函数,传入了一个对象参数 option, 所以咱们实现 tpl 函数的逻辑

// 函数的次要性能是对字符串的操作,咱们能够拿到 tpl 文件的内容(字符串 source),将咱们的 option 对应的 value 替换掉模板中的占位处,也就是 `{{}}` 包裹的中央,返回一个新的函数
function tpl(options) {function tplReplace(template, replaceObject) {template.replace(/\{\{(.*?)\}\}/g, (node, key) => {return replaceObject[key];
    })
  }

  return tplReplace(`${source}`, options)
}

// option(replaceObject)
/**
 * {
    name: '小聪忙',
    age: 34,
    career: 'web developer',
    hobby: '游览, 画画'
  }
 */

// template
/*<div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>*/

理解一下 replace 函数高级用法:

let str = '<div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>'
// 匹配 {{}} 模板内容
str.replace(/\{\{(.*?)\}\}/g, (node, key, index, target) => {console.log(node, key, index, target);
    // 后果
    /*{{name}} name 9 <div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>
{{age}} age 25 <div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>
{{career}} career 39 <div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>
{{hobby}} hobby 56 <div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>*/
})

replace函数第一种用法,第二个参数传递要替换后的值,间接传字符串就行了,第二种高级用法,传递一个回调函数,回调外面的参数别离是以后匹配到的字符串,第一分组匹配的内容、第二分组匹配的内容…… 以此类推直到最初一个分组(小括号内的内容),以后匹配到的字符串的索引,原字符串。

  • 抽离工具类
function tplReplace(template, replaceObject) {return template.replace(/\{\{(.*?)\}\}/g, (node, key) => {return replaceObject[key];
  })
}


module.exports = {tplReplace}
  • 回头看 app.js 外面 tpl 其实是个函数,咱们 loader 返回的应该是个字符串,而后交给 babel-loader 解决,它会把字符串转换成 js 代码执行,所以咱们来解决 tpl-loader 函数的逻辑
function tplLoader(source) {
  // 把模板的空格和换行去掉
  source = source.replace(/\s+/g, '');

  return `
    export default (options) => {
      // 上下文中的 tplReplace 是工具类中引入的函数,咱们须要转成字符串交给 babel-loader
      ${tplReplace.toString() }
      return tplReplace('${source}', options)
    }
  `
}

module.exports = tplLoader
  • app.js中打印一下 console.log(tpl(options)) 会给咱们返回什么?

  • 那咱们不是只须要把这个字符串塞到 html body 外面不就能够渲染了吗?没错
// app.js
import tpl from './info.tpl'

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

const options = {
  name: '小聪忙',
  age: 34,
  career: 'web developer',
  hobby: '游览, 画画'
}
// 渲染解决后的模板
oApp.innerHTML = tpl(options)
  • 关上页面看看成果

  • 咱们之前 webpack 配置中的 tpl-loader 中还配置了一个 option 里的 log,这个怎么在 loader 函数外面拿到这个参数?其实 webpack 的工具类提供了相应的办法, 利用这个办法,咱们实现一下打印日志的成果
// tpl-loader/index.js
const {tplReplace} = require('../utils.js')
// webpack 提供的工具类, 能够获取 webpack.config.js 外面配置的 loader 的 options, 比方咱们配置的 log
const {getOptions} = require('loader-utils')

function tplLoader(source) {
  // 把模板的空格和换行去掉
  source = source.replace(/\s+/g, '');
  const {log} = getOptions(this)
  // 如果配置了 true,就打印出以后模板的援用文件
  const _log =  log ? `console.log('compiled the file which is from ${this.resourcePath}')` : ''

  return `
    export default (options) => {${ tplReplace.toString() }
      ${_log}
      return tplReplace('${source}', options)
    }
  `
}

module.exports = tplLoader

  • 这样咱们的 loader 算是根本实现,看看打印的日志

总结

其实看了一些前端框架的源码会发现,前端的底层会有大量的字符串的操作,咱们刚刚的例子其实就是 vue 源码的一部分,也是其原理,操作字符串其实比咱们想的更简单,在实现自定义的一些需要是,本人实现 loader 是不可获取的。

正文完
 0