前言

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配置文件
1
2
3
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.7.2"
  • 按照上面说的webpack5个核心概念,对其分别进行简单配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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中的脚本命令

    1
    2
    3
    "scripts": {
    "dev": "webpack-dev-server" // 使用webpack开发服务器执行
    }

    当前目录结构如下:

  • 编写app.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = {
    // ...
    // 加载项目的所有loader路径
    resolveLoader: {
    modules: [
    'node_modules', resolve(__dirname, 'loaders')]
    }
    // ...
    }
  • 编写loader文件逻辑
    app.js中用到的tpl函数,传入了一个对象参数option,所以我们完成tpl函数的逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 函数的主要功能是对字符串的操作,我们可以拿到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函数高级用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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函数第一种用法,第二个参数传递要替换后的值,直接传字符串就行了,第二种高级用法,传递一个回调函数,回调里面的参数分别是当前匹配到的字符串,第一分组匹配的内容、第二分组匹配的内容…… 以此类推直到最后一个分组(小括号内的内容),当前匹配到的字符串的索引,原字符串。

  • 抽离工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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函数的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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里面不就可以渲染了吗?没错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 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的工具类提供了相应的方法,利用这个方法,我们实现一下打印日志的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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是不可获取的。