模块化开发Q&A

10/12/2021 前端工程化模块化开发

# 一、package-lock.json 有什么作用,如果项目中没有它会怎么样,举例说明

知识点

  • npm
  • package-lock.json 文件
  • 语义化版本

参考答案

(1)package.json 文件中的 dependencies 作用

dependencies字段指定了项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。
它们都指向一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。

{
  "devDependencies": {
    "browserify": "~13.0.0",
    "karma-browserify": "~5.0.1"
  }
}
1
2
3
4
5
6

对应的版本可以加上各种限定,主要有以下几种:

  • 指定版本:比如1.2.2,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。
  • 波浪号(tilde)+指定版本:比如~1.2.2,表示安装1.2.x的最新版本(不低于1.2.2),但是不安装1.3.x,也就是说安装时不改变大版本号和次要版本号。
  • 插入号(caret)+指定版本:比如ˆ1.2.2,表示安装1.x.x的最新版本(不低于1.2.2),但是不安装2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
  • latest:安装最新版本。

原来 package.json 文件只能锁定大版本,也就是版本号的第一位,并不能锁定后面的小版本,你每次 npm install 都是拉取的该大版本下的最新的版本,为了稳定性考虑我们几乎是不敢随意升级依赖包的,这将导致多出来很多工作量,测试/适配等,所以 package-lock.json 文件出来了,当你每次安装一个依赖的时候就锁定在你安装的这个版本。

有了 lock 文件之后,如果需要更新怎么办?两种方式:

  • npm install 包名@具体版本号
  • npm install 包名 默认会升级到最新稳定版

# 二. 阐述 webpack css-loader 的作用 和 原理?

{
  test: /.css$/,
  loader: 'css-loader',
  exclude: /(node_modules|bower_components)/
}
1
2
3
4
5

css-loader 只是帮我们解析了 css 文件里面的 css 代码,默认 webpack 是只解析 js 代码的,所以想要应用样式我们要把解析完的 css 代码拿出来加入到 style 标签中。

下面是 css-loader 的核心实现原理解析。

const postcss = require('postcss');
const Tokenizer = require('css-selector-tokenizer');
const loaderUtils = require('loader-utils');
// 插件,用来提取url
function createPlugin(options) {
  return function(css) {
    const { importItems, urlItems } = options;
    // 捕获导入,如果多个就执行多次
    css.walkAtRules(/^import$/, function(rule) {
      // 拿到每个导入
      const values = Tokenizer.parseValues(rule.params);
      // console.log(JSON.stringify(values));
      // {"type":"values","nodes":[{"type":"value","nodes":[{"type":"string","value":"./base.css","stringType":"'"}]}]}
      // 找到url
      const url = values.nodes[0].nodes[0]; // 第一层的第一个的第一个
      importItems.push(url.value);
    });
    // 遍历规则,拿到图片地址
    css.walkDecls(decl => {
      // 把value 就是 值 7.5px solid red
      // 通过Tokenizer.parseValues,把值变成了树结构
      const values = Tokenizer.parseValues(decl.value);
      values.nodes.forEach(value => {
        value.nodes.forEach(item => {
          /*
            { type: 'url', stringType: "'", url: './bg.jpg', after: ' ' }
            { type: 'item', name: 'center', after: ' ' }
            { type: 'item', name: 'no-repeat' }
          */
          if (item.type === 'url') {
            const url = item.url;
            item.url = `_CSS_URL_${urlItems.length}_`;
            urlItems.push(url); // ['./bg.jpg']
          }
        });
      });
      decl.value = Tokenizer.stringifyValues(values); // 转回字符串
    });
    return css;
  };
}
// css-loader是用来处理,解析@import "base.css"; url('./assets/logo.jpg')
module.exports = function loader(source) {
  const callback = this.async();
  // 开始处理
  const options = {
    importItems: [],
    urlItems: []
  };
  // 插件转化,然后把url路径都转化成require('./bg.jpg'); // ...
  const pipeline = postcss([createPlugin(options)]);
  // 1rem 75px
  pipeline
    //   .process("background: url('./bg.jpg') center no-repeat;")
    .process(source)
    .then(result => {
      // 拿到导入路径,拼接
      const importCss = options.importItems
        .map(imp => {
          // stringifyRequest 可以把绝对路径转化成相对路径
          return `require(${loaderUtils.stringifyRequest(this, imp)})`; // 拼接
        })
        .join('\n'); // 拿到一个个import
      let cssString = JSON.stringify(result.css); // 包裹后就是"xxx" 双引号
      cssString = cssString.replace(/@import\s+?["'][^'"]+?["'];/g, '');
      cssString = cssString.replace(/_CSS_URL_(\d+?)_/g, function(
        matched,
        group1
      ) {
        // 索引拿到,然后拿到这个,替换掉原来的_CSS_URL_0_哪些
        const imgURL = options.urlItems[+group1];
        // console.log('图片路径', imgURL);
        // "background: url('"+require('./bg.jpg')+"') center no-repeat;"
        return `"+require('${imgURL}').default+"`;
      }); // url('_CSS_URL_1_')
      // console.log(JSON.stringify(options));
      // console.log(result.css);
      callback(
        null,
        `
        ${importCss}
        module.exports = ${cssString}
      `
      );
    });
};
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
module.exports = 'body { bgc: red; }'
1

# 三、Tree Shaking 原理

Tree Shaking 的消除原理就是依赖于 ES6 的模块特性。

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable 的
  • 静态加载模块,效率比CommonJS 模块的加载方式高
  • 如果是require,在运行时确定模块,那么将无法去分析模块是否可用,只有在编译时分析,才不会影响运行时的状态

ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。 所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。

# 四、vue-loader 的实现原理是什么?

官方文档的解释:https://github.com/vuejs/vue-loader#how-it-works (opens new window)

vue-loader 是 webpack 的加载器,它使您可以使用称为单文件组件(SFC)的格式创作 Vue 组件。

<template>
  <div class="example">{{ msg }}</div>
</template>
<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>
<style>
.example {
  color: red;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

它是如何工作的?

简单来说就是:将 *.vue 文件变成 *.bundle.js,然后放入浏览器运行。

  1. vue-loader 使用 @vue/component-compiler-utils 将 SFC 源代码解析为 SFC 描述符。然后,它为每种语言块生成一个导入,因此实际返回的模块代码如下所示:
// code returned from the main loader for 'source.vue'
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
script.render = render
export default script

1
2
3
4
5
6
7
8
9
10
11
  1. 我们希望将脚本块中的内容像.js文件一样对待(如果它是<script lang =“ ts”>,我们希望将其视为.ts文件)。其它语言块也一样。因此,我们希望 webpack 将与 .js 匹配的所有已配置模块规则也应用于看起来像 source.vue?vue&type=script。
    这就是 VueLoaderPlugin(src/plugins.ts) 的作用:对于 webpack 配置中的每个模块规则,它都会创建一个针对相应 Vue 语言块请求的修改后的克隆。
    假设我们已经为所有 *.js 文件配置了 babel-loader。该规则将被克隆并应用于 Vue SFC <script> 块。在webpack内部,类似
import script from 'source.vue?vue&type=script'
1

上面的规则将被扩展为:

import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
1

请注意,由于 vue-loader 已应用于 .vue 文件,因此 vue-loader 也已匹配。

同样,如果为 *.scss 文件配置了 style-loader + css-loader + sass-loader:

<style scoped lang="scss">
1

将由 vue-loader 返回为:

import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
1

webpack 会将其扩展为:

import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
1
  1. 在处理扩展请求时,主 vue-loader 将再次被调用。但是,这次加载程序会注意到请求具有查询并且仅针对特定块。因此,它选择(src/select.ts)目标块的内部内容,并将其传递给匹配的目标装载程序。

  2. 对于 <script> 块,差不多就可以了。但是,对于 <template><style> 块,需要执行一些额外的任务:

  • 我们需要使用 Vue template compiler来编译模板;
  • 我们需要在 CSS 加载器之后但在样式加载器之前,在 <style scoped> 块中对 CSS 进行后处理。

从技术上讲,这些是需要注入到扩展的加载程序链中的其他加载程序(src/templateLoader.ts 和 src/stylePostLoader.ts)。如果最终用户必须自己进行配置,那将非常复杂,因此 VueLoaderPlugin 还会注入一个全局的 Pitching Loader(src/pitcher.ts),它会拦截 Vue 的 <template><style> 请求并注入必要的加载器。最终请求如下所示:

// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
1
2
3
4
更新时间: 2022-01-26 22:04