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

前言

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是不可获取的。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理