Appearance
模块化开发Q&A
一、package-lock.json 有什么作用,如果项目中没有它会怎么样,举例说明
知识点
- npm
- package-lock.json 文件
- 语义化版本
参考答案
(1)package.json 文件中的 dependencies 作用
dependencies字段指定了项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。 它们都指向一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。
js
{
"devDependencies": {
"browserify": "~13.0.0",
"karma-browserify": "~5.0.1"
}
}
对应的版本可以加上各种限定,主要有以下几种:
- 指定版本:比如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 的作用 和 原理?
js
{
test: /.css$/,
loader: 'css-loader',
exclude: /(node_modules|bower_components)/
}
css-loader 只是帮我们解析了 css 文件里面的 css 代码,默认 webpack 是只解析 js 代码的,所以想要应用样式我们要把解析完的 css 代码拿出来加入到 style 标签中。
下面是 css-loader 的核心实现原理解析。
js
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}
`
);
});
};
js
module.exports = 'body { bgc: red; }'
三、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
vue-loader 是 webpack 的加载器,它使您可以使用称为单文件组件(SFC)的格式创作 Vue 组件。
html
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
它是如何工作的?
简单来说就是:将
*.vue
文件变成*.bundle.js
,然后放入浏览器运行。
- vue-loader 使用 @vue/component-compiler-utils 将 SFC 源代码解析为 SFC 描述符。然后,它为每种语言块生成一个导入,因此实际返回的模块代码如下所示:
js
// 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
- 我们希望将脚本块中的内容像
.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内部,类似
js
import script from 'source.vue?vue&type=script'
上面的规则将被扩展为:
js
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
请注意,由于 vue-loader 已应用于 .vue 文件,因此 vue-loader 也已匹配。
同样,如果为 *.scss 文件配置了 style-loader + css-loader + sass-loader:
html
<style scoped lang="scss">
将由 vue-loader 返回为:
js
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
webpack 会将其扩展为:
js
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
在处理扩展请求时,主 vue-loader 将再次被调用。但是,这次加载程序会注意到请求具有查询并且仅针对特定块。因此,它选择(src/select.ts)目标块的内部内容,并将其传递给匹配的目标装载程序。
对于
<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>
请求并注入必要的加载器。最终请求如下所示:
js
// <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'