写在前面

为了更好的说明,我们模仿Vue.js开发了一个类似的简化版本的前端框架Quick Paper(文档) 来帮助你理解一些细节。因此在开始之前,让我们先大致了解一下此项目的结构,方便后续描述。

温馨提示:我们推荐你在开始之前去Github上把此项目clone下来后,对照着源码进行学习!

目录结构

其实你只需要关注下面四个文件夹:

  1. src:框架源码;
  2. loader:类似vue-loader,用来解析.paper文件的loader;
  3. style-loader:和上面的一样,只不过这个是用来解析样式文件的(包括.css文件和.paper文件中的style标签部分);
  4. loader-plug:一些辅助功能,比如校对webpack的一些配置。

框架源码

接着,我们对源码src部分的目录结构再稍微展开一下(因为我们这里的重点不是源码部分,而是那些loader或plug是如何配置完成一系列解析工作的,因此源码部分就在下面简单的说明就点到为止)。

  • core:框架对象的基础代码

    • global-api:给框架对象挂载的全局方法
    • instance:框架对象

      • index.js:框架对象运行入口
      • init.js:负责对象的初始化相关工作
      • lifecycle.js:负责对象的生命周期管理
      • render.js:对象的渲染启动等方面的任务
    • observe:监听数据改变方法(被框架对象使用)
    • vnode:虚拟DOM相关代码(被框架对象使用)
  • module:为框架对象扩展内置指令,组件等的地方
  • tools:一些工具方法,因为复用性和方便管理,集中写在一起
  • index.js:打包入口文件,也就是这份文件把所有的资源整合成一个完整的框架

所以从上面的代码就可以看出来,文件src/core/instance/index.js是对象本身,从这个文件开始开即可!

如果有什么不清楚的,可以去issue给我们留言。

Loader和执行顺序

对于我们用于学习的项目Quick Paper而言,我们是把代码整合到文件.paper中去,文件结构大致如下:

<template>
    <!-- 页面的元素在这里 -->
</template>
<script>
    // 逻辑控制代码在这里
    export default {

    };
</script>
<style>
    /* 在这里编辑样式代码 */
</style>

你想,我们使用webpack打包项目的时候,他是不可能认识.paper文件的,当然就无法知道如何解析上面这份文件了,而开发一个loader用以解析上面的文件,就是这里要说明的。

loader

在说明loader之前,我们先要看看我们编辑的.paper是如何被我们使用的。因为如何使用就决定了我们需要如何解析。

和vue类似,先假设我们有一个App.paper文件:

import App from './App.paper';
new QuickPaper({
    render:createElement => createElement(App),
    // ...
});

因为render里面只记录了页面内容,可是.paper文件里面可是记录了页面内容+逻辑控制+页面样式的。其余的内容怎么办?

// 导入js [逻辑控制]
import script from './${filename}?QuickPaper&type=script&lang=js&hash=${id}&';

// 导入css [页面样式]
import './${filename}?QuickPaper&type=style&lang=css&hash=${id}&';

script.render=${code};

// [页面内容]
export default script;

可以看出来,页面内容直接默认导出后给render配置项即可,别的内容因为新增了导入语句,会触发对应的loader进行解析,也就是说,这里其实可以分为两步:

  • 第一步:对于未考虑到的内容执行新的导入语句,触发对应的loader解析
  • 第二步:导出render需要的内容

style-loader

比如页面样式部分的导入语句:

import './${filename}?QuickPaper&type=style&lang=css&hash=${id}&';

我们是如何让webpack知道这是一个样式文件,并且是使用css还是scss或别的loader来解析的,这属于插件需要说明的部分。在此之前,我们还需要先说明一下样式loader的工作原理。

为什么样式loader比较特殊?

根据返回值类型,可以把loader分成两种:一种是返回js代码(也就是一个模块的代码,有类似module.export语句)的loader,一个是不能作为最左边loader的其他loader(比如返回一个CSS字符串)。

我们来看看我们webpack里面是如何配置css的loader的:

{
    test: /\.css$/,
    loader: ['quick-paper/style-loader/index.js', 'css-loader', 'postcss-loader']
}

这里的重点是css-loader,他属于第一种,返回js代码的loader,对于我们自定义的'quick-paper/style-loader/index.js'而言,如果让loader按照从右往左的顺序执行,很难拿到真正的css代码。

执行顺序(loader和picth)

在说明如何解决上个问题前,我们需要先说明一下loader的picth和执行顺序。

比如上面配置的三个loader而言,执行顺序分为Pitch阶段和Normal阶段(可以理解为loader本身的行为):

  • Pitch阶段:'quick-paper/style-loader/index.js'->'css-loader'->'postcss-loader'
  • Normal阶段:'postcss-loader'->'css-loader'->'quick-paper/style-loader/index.js'

有一个特点是,在Pitch阶段,如果某个loader有返回值,就会停止后续执行。

温馨提示:停止执行的意思是,在其右边的loader,包括自己都执行完毕了(Pitch阶段和Normal阶段都结束了),返回的值会返回给前一个loader(Normal阶段)!

如何实现?

这里,我们就可以借助给'quick-paper/style-loader/index.js'设置一个有返回值的Pitch来实现。

看看代码结构:

// quick-paper/style-loader/index.js

const loaderApi = () => { };
loaderApi.pitch = function (remainingRequest) {

    // request = ""!!../../node_modules/css-loader/dist/cjs.js!../../node_modules/postcss-loader/src/in...
    let request = loaderUtils.stringifyRequest(this, '!!' + remainingRequest)

    return `

        // 获取真正的css内容
        var content = require(' + request + ');
        // 然后调用方法添加到页面中生效
        require('./addStylesClient.js')(content);
    `;
};
module.exports = loaderApi;

我们在'quick-paper/style-loader/index.js'中定义了Picth方法,在此方法里面,返回了一个js字符串,项目运行的时候会运行这段字符串,这段字符串的意义就是调用样式loader获取真正的css后,运行addStylesClient.js方法使得在页面生效。

温馨提示:关于addStylesClient.js方法请直接查看项目源码,很容易读懂,给样式添加hash值让scope生效,就是这个方法里。

插件的作用和一些技巧

我们这里来解释一下,一个.paper文件拆分以后,如何让对应的loader来进行解析。

插件的执行时机

首先需要理解,什么是插件?

你可以这样理解:如果说loader帮助webpack获得解析更多类型文件,那插件就是一个打杂工,前者有专门的分工,后者是在特殊情况下帮助,而不是针对某个文件。

比如你可以在每次打包前调用一个查看删除上次打包的结果,或者在打包失败的时候重置一些参数,或者是别的一些操作等。

如何实现?

那么,我们这里需要插件干什么?

别忘了我们的需求是(拿css举例子),如果遇到:

import './${filename}?QuickPaper&type=style&lang=css&hash=${id}&';

这样的导入语句,我们工具lang=css发现是一个样式文件,应该交给专门解析css的loader处理,当然,我们可以主动修改webpack的配置:

{
    test: /type=style&lang=css/,
    loader: ['quick-paper/style-loader/index.js', 'css-loader', 'postcss-loader']
}

可是,为了更简单,我们可以通过插件,在每次打包前对loader配置进行修改(当然,也包括js等相关项),如此,便实现了。

03-05 21:27