xixijiang的主页


  • Home

  • Archives

  • Tags

vue-loader源码解析

Posted on 2019-06-11

前置知识

vue-loader基本知识

vue-loader作用:允许你以一种名为单文件组件(SFC)的格式撰写Vue组件

  1. block(块):vue组件里包含template、script、style、custom blocks这几部分,我们称之为“块”。

  2. 一个vue组件里可以包含多个style块、custom块。

  3. 每个块都可以使用不同的loader来处理,比如:

1
2
3
4
5
6
7
8
9
10
11
<template lang="pug"></template>

<script type="text/vbscript"></script>

<style lang="scss"></style>

<style lang="less"></style>

<docs lang="xxx"></docs>

<foo></foo>

webpack里可以设置相应的loader来处理这些块,比如pug-plain-loader、sass-loader等。

  1. 支持函数式组件
1
2
3
<template functional>
<div>{{ props.foo }}</div>
</template>

webpack loader基本知识

vue-loader与webpack loader密切相关,我们首先看一下webpack loader的执行过程。

每个loader上都可以有一个.pitch方法,loader的处理过程分为两个阶段,pitch阶段和normal执行阶段:

第一步先进行pitch阶段:会先按顺序执行每个loader的pitch方法;

第二步按相反顺序进行normal执行阶段

如果loader的pitch方法有返回值,则直接掉头往相反顺序执行。参考官网例子(pitching loader):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
//...
module: {
rules: [
{
//...
use: [
'a-loader',
'b-loader',
'c-loader'
]
}
]
}
};

上面例子中各个loader的执行顺序如下:

1
2
3
4
5
6
7
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution

如果b-loader返回了内容,则执行顺序如下:

1
2
3
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution

vue-loader在webpack流程中的位置

vue-loader在webpack流程中的位置

图中黄色部分就是vue-loader所涉及的内容,也就是我们这篇文章要分析的。

输入与输出

接下来,我们将通过一个例子,来看vue-loader是怎么工作的(这个例子来自vue-loader/example/)。

1
2
3
4
5
6
7
8
// main.js
import Vue from 'vue'
import Foo from './source.vue'

new Vue({
el: '#app',
render: h => h(Foo)
})
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
// source.vue
<template lang="pug">
div(ok)
h1(:class="$style.red") hello
</template>

<script>
export default {
data () {
return {
msg: 'fesfff'
}
}
}
</script>

<style module>
.red {
color: red;
}
</style>

<foo>
export default comp => {
console.log(comp.options.data())
}
</foo>
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
87
88
// webpack.config.js
const path = require('path')
const VueLoaderPlugin = require('../lib/plugin')

module.exports = {
devtool: 'source-map',
mode: 'development',
entry: path.resolve(__dirname, './main.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/dist/'
},
devServer: {
stats: "minimal",
contentBase: __dirname,
writeToDisk: true,
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
resourceQuery: /blockType=foo/,
loader: 'babel-loader'
},
{
test: /\.pug$/,
oneOf: [
{
resourceQuery: /^\?vue/,
use: ['pug-plain-loader']
},
{
use: ['raw-loader', 'pug-plain-loader']
}
]
},
{
test: /\.css$/,
oneOf: [
{
resourceQuery: /module/,
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[local]_[hash:base64:8]'
}
}
]
},
{
use: [
'vue-style-loader',
'css-loader'
]
}
]
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
data: '$color: red;'
}
}
]
}
]
},
resolveLoader: {
alias: {
'vue-loader': require.resolve('../lib')
}
},
plugins: [
new VueLoaderPlugin()
]
}

对于上述例子,vue-loader的输出结果是:

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// template
// 来自 import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&lang=pug&" 的结果
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{
attrs: {
ok: ""
}
},
[
_c(
"h1",
{
class: _vm.$style.red
},
[_vm._v("hello")
])
])
}

var staticRenderFns = []

render._withStripped = true

// script
// 来自 import script from "./source.vue?vue&type=script&lang=js&" 的结果
var script = {
data () {
return {
msg: 'fesfff'
}
}
}

// style
// 来自 import style0 from "./source.vue?vue&type=style&index=0&module=true&lang=css&" 的结果
.red {
color: red;
}

// 注入style 及 style热加载
var cssModules = {}
var disposed = false

function injectStyles (context) {
if (disposed) return

cssModules["$style"] = (style0.locals || style0)
Object.defineProperty(this, "$style", {
configurable: true,
get: function () {
return cssModules["$style"]
}
})
}

module.hot && module.hot.dispose(function (data) {
disposed = true
})

module.hot && module.hot.accept(["./source.vue?vue&type=style&index=0&module=true&lang=css&"], function () {
var oldLocals = cssModules["$style"]
if (oldLocals) {
var newLocals = require("./source.vue?vue&type=style&index=0&module=true&lang=css&")
if (JSON.stringify(newLocals) !== JSON.stringify(oldLocals)) {
cssModules["$style"] = newLocals
require("/Users/zhangxixi/knowledge collect/vue-loader/node_modules/_vue-hot-reload-api@2.3.3@vue-hot-reload-api/dist/index.js").rerender("27e4e96e")
}
}
})

// normalize component
import normalizer from "!../lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
injectStyles,
null,
null
)

// custom blocks
// 来自 import block0 from "./source.vue?vue&type=custom&index=0&blockType=foo" 的结果
var block0 = comp => {
console.log(comp.options.data())
}

if (typeof block0 === 'function') block0(component)

// hot reload
// script 和 template的热加载
if (module.hot) {
var api = require("/Users/zhangxixi/knowledge collect/vue-loader/node_modules/_vue-hot-reload-api@2.3.3@vue-hot-reload-api/dist/index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!module.hot.data) {
api.createRecord('27e4e96e', component.options)
} else {
api.reload('27e4e96e', component.options)
}
module.hot.accept("./source.vue?vue&type=template&id=27e4e96e&lang=pug&", function () {
api.rerender('27e4e96e', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = "example/source.vue"
export default component.exports

源码结构

首先看一下vue-loader源码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vue-loader/lib/
│
├─── codegen/
│ ├─── customBlock.js/ 生成custom block的request
│ ├─── hotReload.js/ 生成热加载的代码
│ ├─── styleInjection.js/ 生成style的request
│ ├─── utils.js/ 工具函数
├─── loaders/ vue-loader内部定义的loaders
│ ├─── pitcher.js/ pitcher-loader,将所有的单文件组件里的block请求拦截并转成合适的请求
│ ├─── stylePostLoader.js/ style-post-loader, 处理scoped css的loader
│ ├─── templateLoader.js/ template-loader,编译 html 模板字符串,生成 render/staticRenderFns 函数
├─── runtime/
│ ├─── componentNormalizer.js/ 将组件标准化
├─── index.d.ts/
├─── index.js/ vue-loader的核心代码
├─── plugin.js/ vue-loader-plugin的核心代码
├─── select.js/ 根据不同query类型(script、template等)传递相应的content、map给下一个loader

vue-loader-plugin

在webpack开始执行后,会先合并webpack.config里的配置,接着实例化compiler,然后就去挨个执行所有plugin的apply方法。请看webpack这部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack/lib/webpack.js

const Compiler = require("./Compiler")

const webpack = (options, callback) => {
...
options = new WebpackOptionsDefaulter().process(options) // 初始化 webpack 各配置参数
let compiler = new Compiler(options.context) // 初始化 compiler 对象,这里 options.context 为 process.cwd()
compiler.options = options // 往 compiler 添加初始化参数
new NodeEnvironmentPlugin().apply(compiler) // 往 compiler 添加 Node 环境相关方法
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
...
}

从例子中看到,我们的webpack配置了vue-loader-plugin,也就是源码里的vue-loader/lib/plugin.js,这是vue-loader强依赖的,如果不配置vue-loader-plugin,就会抛出错误。根据上面wbepack执行过程,在执行vue-loader核心代码之前,会先经过vue-loader-plugin。那么它到底做了哪些事情?

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
// vue-loader/lib/plugin.js

class VueLoaderPlugin {
apply (compiler) {
// ...

// 事件注册(简化了源代码)
compiler.hooks.compilation.tap(id, compilation => {
let normalModuleLoader = compilation.hooks.normalModuleLoader
normalModuleLoader.tap(id, loaderContext => {
loaderContext[NS] = true
})
})

// ...

const rawRules = compiler.options.module.rules
const { rules } = new RuleSet(rawRules)

// ...
// 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。
// 例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。
const clonedRules = rules
.filter(r => r !== vueRule)
.map(cloneRule)

// ...

// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
// 这个pitcher-loader的作用之一就是给template块添加template-loader,给style块添加style-post-loader,并分别导出一个新的js module request
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}

// replace original rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}

function createMatcher (fakeFile) {/*...*/}

function cloneRule (rule) {/*...*/}

VueLoaderPlugin.NS = NS
module.exports = VueLoaderPlugin

从上面源码可以看出,vue-loader-plugin导出的是一个类,并且只包含了一个apply方法。这个apply方法就是被webpack调用的。

apply方法其实就做了3件事:

  1. 事件监听:在normalModuleLoader钩子执行前调用代码:loaderContext[NS] = true
    (每解析一个module,都会用到normalModuleLoader,由于每解析一个module都会有一个新的loaderContext,vue-loader/lib/index.js会判断loaderContext[NS]的值,为保证经过vue-loader执行时不报错,需要在这里标记loaderContext[NS] = true。)

    说明:loader中的this是一个叫做loaderContext的对象,这是webpack提供的,是loader的上下文对象,里面包含loader可以访问的方法或属性。

  2. 将webpack中配置的rules利用webpack的new RuleSet进行格式化(rules配置),并clone一份rules给.vue文件里的每个block使用。

    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
    rules = [{
    resource: f (),
    use: [{
    loader: "vue-loader",
    options: undefined
    }]
    }, {
    resourceQuery: f (),
    use: [{
    loader: "babel-loader",
    options: undefined
    }]
    }, {
    resourceQuery: f (),
    use: [{
    loader: "babel-loader",
    options: undefined
    }]
    }, {
    resource: ƒ (),
    oneOf: [{
    resourceQuery: ƒ (),
    use: [{
    loader: "pug-plain-loader", options: undefined
    }]
    }, {
    use: [{
    loader: "raw-loader",
    options: undefined
    }, {
    loader: "pug-plain-loader",
    options: undefined
    }]
    }]
    }]
  3. 在rules里加入vue-loader内部提供的rule(暂且称为pitcher-rule),其对应的loader是pitcher-loader,同时将原始的rules替换成pitcher-rule、cloneRules、rules。至于pitcher-rule、pitcher-loader做了什么,我们后面再讲。

放一张图总结下vue-loader-plugin的整体流程:

vue-loader

当webpack加载入口文件main.js时,依赖到了source.vue,webpack内部会匹配source.vue的loaders,发现是vue-loader,然后就会去执行vue-loader(vue-loader/lib/index.js)。接下来,我们分析vue-loader的实现过程。

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// vue-loader/lib/index.js

module.exports = function (source) {
const loaderContext = this

// 会先判断是否加载了vue-loader-plugin,没有则报错
if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) {
// 略
}

// 从loaderContext获取信息
const {
target, // 编译的目标,是从webpack配置中传递过来的,默认是'web',也可以是'node'等
request, // 请求的资源的路径(每个资源都有一个路径)
minimize, // 是否压缩:true/false,现在已废弃
sourceMap, // 是否生成sourceMap: true/false
rootContext, // 当前项目绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader
resourcePath, // 资源文件的绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader/example/source.vue
resourceQuery // 资源的 query 参数,也就是问号及后面的,如 ?vue&type=custom&index=0&blockType=foo
} = loaderContext

// 开始解析SFC,其实就是根据不同的 block 来拆解对应的内容
// parse函数返回的是compiler.parseComponent()的结果
// 如果没有自定义compiler,compiler对应的就是vue-template-compiler。
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext), // 如果loader的options没有配置compiler, 则使用vue-template-compiler
filename,
sourceRoot,
needMap: sourceMap
})

// 如果是语言块,则直接返回
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}

// 接下来分别对不同block的请求进行处理
// template
// 处理template,根据descriptor.template,生成template的js module(生成import语句)
/* 生成的template请求
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
*/
let templateImport = `var render, staticRenderFns`
let templateRequest
if (descriptor.template) {
const src = descriptor.template.src || resourcePath
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``

// 把attrs转成query格式:{lang: pug} => &lang=pug
const attrsQuery = attrsToQuery(descriptor.template.attrs)
// 如果css有scope,那么template就需要加上scoped=true,这是why??
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
const request = templateRequest = stringifyRequest(src + query)
// 这个request会经过pug-plain-loader、template-loader
// 最终template-loader会返回render, staticRenderFns这两个函数
templateImport = `import { render, staticRenderFns } from ${request}`
}

// script
// 处理script,与template类似
/* 生成的script请求:
import script from "./source.vue?vue&type=script&lang=js&"
export * from "./source.vue?vue&type=script&lang=js&"
*/
let scriptImport = `var script = {}`
if (descriptor.script) {
const src = descriptor.script.src || resourcePath
const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
const query = `?vue&type=script${attrsQuery}${inheritQuery}`
const request = stringifyRequest(src + query)
/* script不会再经过其他loader处理,所以从request里import的script就是对应的源码,如
{
data () {
return {
msg: 'fesfff'
}
}
}
*/
scriptImport = (
`import script from ${request}\n` +
`export * from ${request}` // support named exports
)
}

// styles
// 处理styles
/*
genStylesCode做了3件事情:
1. 生成import语句(这一步与template生成import语句类似)
2. 如果需要热加载,添加热加载代码
3.如果需要注入样式,则添加样式注入函数injectStyles
*/
/* 生成的style请求:
import style0 from "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
*/
let stylesCode = ``
if (descriptor.styles.length) {
stylesCode = genStylesCode(
loaderContext,
descriptor.styles, // vue单文件组件支持多个style标签,故descriptor.styles是数组
id,
resourcePath,
stringifyRequest,
needsHotReload,
isServer || isShadow // needs explicit injection?
)
}

// 将由 .vue 提供 render函数/staticRenderFns,js script,style样式,并交由 normalizer 进行统一的格式化,最终导出 component.exports

// 如果stylesCode里含有injectStyles,则表明是需要注入style的,因此可以使用这个正则来判断:/injectStyles/.test(stylesCode)
let code = `
${templateImport}
${scriptImport}
${stylesCode}

/* normalize component */
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
script,
render,
staticRenderFns,
${hasFunctional ? `true` : `false`},
${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
${hasScoped ? JSON.stringify(id) : `null`},
${isServer ? JSON.stringify(hash(request)) : `null`}
${isShadow ? `,true` : ``}
)
`.trim() + `\n`

if (descriptor.customBlocks && descriptor.customBlocks.length) {
code += genCustomBlocksCode(
descriptor.customBlocks,
resourcePath,
resourceQuery,
stringifyRequest
)
}

if (needsHotReload) {
code += `\n` + genHotReloadCode(id, hasFunctional, templateRequest)
}

// Expose filename. This is used by the devtools and Vue runtime warnings.
if (!isProduction) {
// Expose the file's full path in development, so that it can be opened
// from the devtools.
code += `\ncomponent.options.__file = ${JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))}`
} else if (options.exposeFilename) {
// Libraies can opt-in to expose their components' filenames in production builds.
// For security reasons, only expose the file's basename in production.
code += `\ncomponent.options.__file = ${JSON.stringify(filename)}`
}

code += `\nexport default component.exports`

// console.log(code)
return code
}

module.exports.VueLoaderPlugin = plugin

vue-loader整体流程图:

可以看出,整个过程大体可以分为3个阶段。

第一阶段

这一阶段是将.vue文件解析成js代码。

  1. 会先判断是否加载了vue-loader-plugin,没有则报错

    1
    2
    3
    if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) {
    // 略
    }
  2. 从loaderContext中获取到模块的信息,比如request、resourcePath、resourceQuery等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const {
    target, // 编译的目标,是从webpack配置中传递过来的,默认是'web',也可以是'node'等
    request, // 请求的资源的路径(每个资源都有一个路径)
    minimize, // 是否压缩:true/false,现在已废弃
    sourceMap, // 是否生成sourceMap: true/false
    rootContext, // 当前项目绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader
    resourcePath, // 资源文件的绝对路径,对本例子来说是:/Users/zhangxixi/knowledge collect/vue-loader/example/source.vue
    resourceQuery // 资源的 query 参数,也就是问号及后面的,如 ?vue&type=custom&index=0&blockType=foo
    } = loaderContext
  3. 对.vue文件进行parse,其实就是把.vue分成template、script、style、customBlocks这几部分

    1
    2
    3
    4
    5
    6
    7
    const descriptor = parse({
    source,
    compiler: options.compiler || loadTemplateCompiler(loaderContext), // 如果loader的options没有配置compiler, 则使用vue-template-compiler
    filename,
    sourceRoot,
    needMap: sourceMap
    })

    这一阶段最关键的就是parse过程,parse前后对比如下:

    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
    // parse之前 source是:
    '<template lang="pug">\ndiv(ok)\n h1(:class="$style.red") hello\n</template>\n\n<script>\nexport default {\n data () {\n return {\n msg: \'fesfff\'\n }\n }\n}\n</script>\n\n<style scoped>\n.red {\n color: red;\n}\n</style>\n\n<foo>\nexport default comp => {\n console.log(comp.options.data())\n}\n</foo>\n'

    // parse之后 得到的结果
    {
    template:
    { type: 'template',
    content: '\ndiv(ok)\n h1(:class="$style.red") hello\n',
    start: 21,
    attrs: { lang: 'pug' },
    lang: 'pug',
    end: 62
    },
    script:
    { type: 'script',
    content:
    '//\n//\n//\n//\n//\n\nexport default {\n data () {\n return {\n msg: \'fesfff\'\n }\n }\n}\n',
    start: 83,
    attrs: {},
    end: 158,
    map:
    { version: 3,
    sources: [Array],
    names: [],
    mappings: ';;;;;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA',
    file: 'source.vue',
    sourceRoot: 'example',
    sourcesContent: [Array] }
    },
    styles:
    [ { type: 'style',
    content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.red {\n color: red;\n}\n',
    start: 183,
    attrs: [Object],
    scoped: true,
    end: 207,
    map: [Object]
    }
    ],
    customBlocks:
    [ { type: 'foo',
    content:
    '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nexport default comp => {\n console.log(comp.options.data())\n}\n',
    start: 222,
    attrs: {},
    end: 285
    }
    ],
    errors: []
    }
  4. 然后区分.vue请求与block请求(请求source.vue就是.vue请求,source.vue里依赖了template、script等block,那么这些依赖会被解析成.source.vue?vue&type=template这种带query的,我们称之为block请求)。如果是.vue请求,则需要生成js module。否则就执行selectBlock。第一阶段是.vue请求,因此会生成js module:分别生成template、script、style、customBlock的请求路径(这里会在query上添加’vue’,比如./source.vue?vue&type=script&lang=js,这会在第二阶段用到);添加热加载逻辑。

vue-loader第一阶段生成的js代码如下:

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
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
import script from "./source.vue?vue&type=script&lang=js&"
export * from "./source.vue?vue&type=script&lang=js&"
import style0 from "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
import normalizer from "!../lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
"27e4e96e",
null
)
import block0 from "./source.vue?vue&type=custom&index=0&blockType=foo"
if (typeof block0 === 'function') block0(component)
if (module.hot) {
var api = require("/Users/zhangxixi/knowledge collect/vue-loader/node_modules/_vue-hot-reload-api@2.3.3@vue-hot-reload-api/dist/index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!module.hot.data) {
api.createRecord('27e4e96e', component.options)
} else {
api.reload('27e4e96e', component.options)
}
module.hot.accept("./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&", function () {
api.rerender('27e4e96e', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = "example/source.vue"
export default component.exports

vue-loader第一阶段

第二阶段

第一阶段返回的js代码交与webpack继续解析,代码里的这几个import请求就会被接着进行依赖解析,这样就会接着请求所依赖的template、script、style、customBlock。

我们以template的请求为例:
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&",webpack解析出这个module需要的loaders是:pitcher-loader、pug-plain-loader、vue-loader。需要哪些loader是webpack内部根据rules来匹配的,这里的rules是经过vue-loader-plugin处理后的。之所以能解析出pitcher-loader,是因为query里含有vue,此时,是时候回过头来看一下vue-loader-plugin中pitcher-rule和pitcher-loader的代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vue-loader/lib/plugin.js

// ...
// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}
// ...

从上面代码可以看到,pitcher-rule是通过resourceQuery中是否有vue进行匹配的。从第一阶段返回的代码中可以看到,template、script、style、custom-block的请求query中都带有vue。所以这个rule就是匹配这几个block请求的。如果匹配到了,那么pitcher-loader就会加入到这些请求需要的loader数组中。pitcher-loader来自于vue-loader/lib/loaders/pitcher.js。那我们来看一下这个pitcher-loader到底做了什么:

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
// vue-loader/lib/loaders/pitcher.js

module.exports = code => code
module.exports.pitch = function (remainingRequest) {
// ...
const query = qs.parse(this.resourceQuery.slice(1))
let loaders = this.loaders

// if this is a language block request, eslint-loader may get matched
// multiple times
if (query.type) {
// if this is an inline block, since the whole file itself is being linted,
// remove eslint-loader to avoid duplicate linting.
if (/\.vue$/.test(this.resourcePath)) {
loaders = loaders.filter(l => !isESLintLoader(l))
} else {
// This is a src import. Just make sure there's not more than 1 instance
// of eslint present.
loaders = dedupeESLintLoader(loaders)
}
}

// remove self
loaders = loaders.filter(isPitcher)

// ...

// Inject style-post-loader before css-loader for scoped CSS and trimming
if (query.type === `style`) {
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
if (cssLoaderIndex > -1) {
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)

const request = genRequest([
...afterLoaders,
stylePostLoaderPath,
...beforeLoaders
])

return `import mod from ${request}; export default mod; export * from ${request}`
}
}

// for templates: inject the template compiler & optional cache
if (query.type === `template`) {
const path = require('path')
const cacheLoader = cacheDirectory && cacheIdentifier
? [`cache-loader?${JSON.stringify({
// For some reason, webpack fails to generate consistent hash if we
// use absolute paths here, even though the path is only used in a
// comment. For now we have to ensure cacheDirectory is a relative path.
cacheDirectory: (path.isAbsolute(cacheDirectory)
? path.relative(process.cwd(), cacheDirectory)
: cacheDirectory).replace(/\\/g, '/'),
cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'
})}`]
: []

const preLoaders = loaders.filter(isPreLoader)
const postLoaders = loaders.filter(isPostLoader)

const request = genRequest([
...cacheLoader,
...postLoaders,
templateLoaderPath + `??vue-loader-options`,
...preLoaders
])

// the template compiler uses esm exports
return `export * from ${request}`
}

// if a custom block has no other matching loader other than vue-loader itself
// or cache-loader, we should ignore it
if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {
return ``
}

// When the user defines a rule that has only resourceQuery but no test,
// both that rule and the cloned rule will match, resulting in duplicated
// loaders. Therefore it is necessary to perform a dedupe here.
const request = genRequest(loaders)

return `import mod from ${request}; export default mod; export * from ${request}`
}

pitcher-loader做了三件事情,最关键的是第三件事情:

  1. 剔除eslint-loader
  2. 剔除pitcher-loader自身
  3. 根据不同的query进行拦截处理,返回对应的内容,跳过后面的loader执行部分

对于style的处理,先判断是否有css-loader,有的话就生成一个新的request,这个过程会将vue-loader内部的style-post-loader添加进去,然后返回一个js请求。根据pitch的规则,pitcher-loader后面的loader都会被跳过,然后就开始解析这个返回的js请求,它的的内容是:

1
2
import mod from "-!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/index.js!../node_modules/_css-loader@1.0.1@css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&";
export default mod; export * from "-!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/index.js!../node_modules/_css-loader@1.0.1@css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"

对于template的处理类似,也会生成一个新的request,这个过程会将vue-loader内部提供的template-loader加进去,并返回一个js请求:

1
export * from "-!../lib/loaders/templateLoader.js??vue-loader-options!../node_modules/_pug-plain-loader@1.0.0@pug-plain-loader/index.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"

其他block也是类似的。

vue-loader第二阶段

第三阶段

经过第二阶段后,webpack会继续解析每个block对应的js请求。根据这些请求,webpack会匹配到相应的loaders。

对于style,对应的loader是vue-style-loader、css-loader、style-post-loader、vue-loader。执行顺序就是:

vue-style-loader的pitch、css-loader的pitch、style-post-loader的pitch、vue-loader的pitch、vue-loader(分离出style block)、style-post-loader(处理scoped css)、css-loader(处理相关资源的引入路径)、vue-style-loader(动态创建style标签插入css)。

对于template,对应的loader是template-loader、pug-plain-loader、vue-loader,执行顺序是:

template-loader的pitch、pug-plain-loader的pitch、vue-loader的pitch、vue-loader(分离出template block)、pug-plain-loader(将pug模板转化为html字符串)、template-loader(编译 html 模板字符串,生成 render/staticRenderFns 函数并暴露出去)。

其他模块类似。

会发现,在不考虑pitch函数的时候,第三阶段里最先执行的都是vue-loader,此时query是有值的,所以会进入到selecBlock阶段。(这就是vue-loader执行时与第一阶段不同的地方)

1
2
3
4
5
6
7
8
9
10
11
12
13
// vue-loader/lib/index.js

// ...
// 如果是语言块,则直接返回
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
// ...

selectBlock来自select.js,那么我们来看看select.js做了什么:

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
module.exports = function selectBlock (
descriptor,
loaderContext,
query,
appendExtension
) {
// template
if (query.type === `template`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')
}
loaderContext.callback(
null,
descriptor.template.content,
descriptor.template.map
)
return
}

// script
if (query.type === `script`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')
}
loaderContext.callback(
null,
descriptor.script.content,
descriptor.script.map
)
return
}

// styles
if (query.type === `style` && query.index != null) {
const style = descriptor.styles[query.index]
if (appendExtension) {
loaderContext.resourcePath += '.' + (style.lang || 'css')
}
loaderContext.callback(
null,
style.content,
style.map
)
return
}

// custom
if (query.type === 'custom' && query.index != null) {
const block = descriptor.customBlocks[query.index]
loaderContext.callback(
null,
block.content,
block.map
)
return
}
}

select.js其实就是根据不同的query类型,将相应的content和map传递给下一个loader。

最终生成的代码长什么样?

template最终解析代码:

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
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{
attrs: {
ok: ""
}
},
[
_c(
"h1",
{
class: _vm.$style.red
},
[_vm._v("hello")
])
])
}

var staticRenderFns = []

render._withStripped = true

export { render, staticRenderFns }

style最终解析代码:

1
2
3
.red[data-v-27e4e96e] {
color: red;
}

vue-loader第三阶段

一些有意思的代码实现

vue-plugin-loader是如何将rules里配置的规则应用到block里的?

vue-loader-plugin的cloneRules源码:

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
// vue-loader/lib/plugin.js

// ...

function cloneRule (rule) {
const { resource, resourceQuery } = rule
// Assuming `test` and `resourceQuery` tests are executed in series and
// synchronously (which is true based on RuleSet's implementation), we can
// save the current resource being matched from `test` so that we can access
// it in `resourceQuery`. This ensures when we use the normalized rule's
// resource check, include/exclude are matched correctly.
let currentResource
const res = Object.assign({}, rule, {
resource: {
test: resource => {
currentResource = resource
// 始终返回true,是为了能让ruleSet在执行时能够进入resourceQuery的判断规则
// 同时提供currentResource给resourceQuery使用
return true
}
},
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
// 如果query里没有vue,则说明不是.vue的block,不进行匹配
if (parsed.vue == null) {
return false
}
// .vue里给block匹配loader时需要通过lang来匹配。如果没有指定lang,也不进行匹配。
// 会发现,在为block生成request时,都会用到attrsToQuery,而style和script会给attrsToQuery分别传递'css', 'js'作为langFallback, customBlock和template则不需要传递
// 这是因为我们写代码时style和script可以不写lang,不写默认是css、js。这时候vue-loader需要把默认的加上。
// 而customBlock和template没有默认的lang,所以vue-loader不用提供默认的lang。
if (resource && parsed.lang == null) {
return false
}
// 这里需要在原资源路径后拼接一个假的后缀,如source.vue.css,这是为了执行resource时,能够通过资源名后缀匹配到loader
/* 比如,我们配置了一个规则是:
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
}

经过new RuleSet后,会变成:
{
resource: (resource) => {
return /\.css$/.test(resource)
},
use: [
'vue-style-loader',
'css-loader'
]
}

resource是一个函数,此时利用拼接的fakeResourcePath,resource(fakeResourcePath)就可以匹配成功了
*/
const fakeResourcePath = `${currentResource}.${parsed.lang}`
if (resource && !resource(fakeResourcePath)) {
return false
}
if (resourceQuery && !resourceQuery(query)) {
return false
}
return true
}
})

if (rule.oneOf) {
res.oneOf = rule.oneOf.map(cloneRule)
}

return res
}

// ...

// replace original rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]

// ...

尾声

本文只是梳理了vue-loader的整体流程,具体源码细节请参考我写的源码注释

扩展知识

webpack RuleSet源码分析

webview

Posted on 2019-05-31

webapp: 就是我们现在开发的这些网站
nativeapp: 手机自带app

通信机制:

假如在微信中,有个h5页面,需要调用相机,怎么弄呢

方式一:
借助于webview,
webview向h5注册js代码,比如
window.bridge = { photograph: }
那么h5就可以这么调用:
window.bridge.photograph

window.bridge类似addEventListener

方式二:
封装window.prompt方式

方式三:
私协:
http
didi://
我们公司内部实现是:把所有功能通过module注册,fusion.js

h5里写:
didi://photograph&callDataName=func&callbackDataName=callback
photograph是方法,callDataName是从哪里拿参数,callbackDataName回调函数

window.func=function() {return …}必须是个函数,且有返回值
window.callback=function() {}是个函数

人际交往分析

Posted on 2019-05-17
The article has been encrypted, please enter your password to view.
Read more »

初识PM

Posted on 2019-05-17

什么是产品经理?

什么是需求?
什么是产品?

需求来源:
客户:是购买产品的人
用户:是使用产品的人

比如小明买了一瓶水,同时喝了,则他既是客户,也是用户;他买了送给小红,则

收集需求:
听用户说:了解目的
看用户做:用户做了什么(比如接入魔镜系统)
定性分析(为什么点A按钮、不去点B按钮):了解原因
定量分析(有60%点了A按钮、40%点了B按钮):发现现象
(
新产品需要先做什么:
听用户说
定义分析
)

整理需求:
用户需求:用户自以为是的需求,并且经常表达为用户的解决方案(这种解决方案,可能不可行)
->需求分析->
产品需求: 经错需求分析,找到真实的需求,并且表达为产品的解决方案

需求分析:
不要(不可能)满足所有用户的需求
产品在不同阶段应该关注不同的用户需求:初期(拉取、保留用户)、后期(付费用户)
价值+开发量+优先级综合排序需求

满足需求:
提高现实
降低理想
转移需求

比如电梯的例子

说明需求:
给老板看的:
BRD: business requirement document
MRD: market requirement document

给研发、测试看的:
PRD: product requiremnet document
FSD: functional specification document

正则表达式

Posted on 2019-05-15

字符串分割为数组

css技巧

Posted on 2019-05-13

两端对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// html
<div>姓名</div>
<div>手机号码</div>
<div>账号</div>
<div>密码</div>

// css
div {
margin: 10px 0;
width: 100px;
border: 1px solid red;
text-align: justify;
text-align-last: justify;
}

// 是为了处理safari浏览器不识别text-align-last:justify;问题(但是这样有个问题,就是会多出一行,不知道有没有什么办法)
// 如果想处理IE兼容性问题,只能使用空格或者&nbsp;
div:after{
content: '';
display: inline-block;
width: 100%;
}

currentColor — CSS3超高校级好用CSS变量

currentColor:当前的标签所继承的文字颜色

1
2
3
4
5
div {
color: red;
border: 5px solid currentColor;
box-shadow: 0 0 5px solid currentColor;
}

以上css实现的效果是边框和阴影与文字颜色相同,都是red。

有什么应用场景呢?

比如,我们有这样一个html:

1
<a href="##" class="link"><i class="icon icon1"></i>返回</a>

我们希望hover的时候,icon的颜色与hover设置的颜色相同,这时候就可以这样写:

1
2
3
4
5
6
7
8
.icon {
display: inline-block;
width: 16px; height: 20px;
background-image: url(../201307/sprite_icons.png);
background-color: currentColor; /* 该颜色控制图标的颜色 */
}

.link:hover { color: #333; }/* 虽然改变的是文字颜色,但是图标颜色也一起变化了 */

实现文字下面波浪线动画效果

第一步是实现波浪线,有三种办法:

text-decoration: green wavy underline;

text-decoration: green wavy underline;

这种方式有几点问题:

text-decoration

  1. 线的粗细不好调
  2. 字符和装饰线发生重叠的时候,装饰线直接消失了,波浪线变成了一截一截的
  3. 无法预知每个波浪线重复片段的宽度,想要无限运动理论上就不太可行

因此,文字或者图形的波浪线动画效果不能使用text-decoration的波浪线。

纯css实现(利用radial-gradient)

这种方式radial-gradient理解成本高,暂不讨论。

使用SVG波形矢量图作为背景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// css
.underline-wave {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 4'%3E%3Cpath fill='none' stroke='%23333' d='M0 3.5c5 0 5-3 10-3s5 3 10 3 5-3 10-3 5 3 10 3'/%3E%3C/svg%3E") repeat-x 0 100%;
background-size: 20px auto;
}

.underline-wave {
animation: waveMove 1s infinite linear;
}

@keyframes waveMove {
from { background-position: 0 100%; }
to { background-position: -20px 100%; }
}

// html
<a href="javascript:" class="svg-wave">测试测试测试zhanghhhhha</a>

优点是线条边缘平滑,效果细腻,易理解,易上手,易维护。

缺点也很明显,就是波浪线的颜色无法实时跟着文字的颜色发生变化,适用于文字颜色不会多变的场景。

如果我们想要改变波浪线的颜色也很简单,修改background代码中的stroke=’%23333’这部分,’%23’就是就是#,因此,stroke=’%23333’其实就是stroke=’#333’的意思。例如,我们需要改成红色略带橙色,可以stroke=’%23F30’,也可以写完整stroke=’%23FF3300’。

参考:https://www.zhangxinxu.com/wordpress/2019/04/css-wave-wavy-animation/

数据校验

Posted on 2019-05-07

对于控制台系统来说,最常用的就是表单了。而表单是需要校验的,有时候写个正则,搞半天都不对,特此记录下来,方便查找。

校验字符串是否是JSON格式

首先,自然想到的是使用JSON.parse(str),但是单纯的使用JSON.parse(str)不能完全检验一个字符串是JSON格式,有许多例外:

1
2
3
4
5
6
7
JSON.parse('"aaa"'); // aaa
JSON.parse('123'); // 123
JSON.parse('true'); // true
JSON.parse('false'); // false
JSON.parse('["a"]'); // ["a"]
JSON.parse('{}'); // {}
JSON.parse('null'); // null

JS数据类型有字符串、数字、布尔值、数组、对象、Null、Undefined,经过测试,使用JSON.parse能够转换成功的有:数字、字符串、布尔值、数组、空对象、null、json,而其中正确的JSON格式有:数组、空对象、json

所以,得出结论,如果JSON.parse能够转换成功,并且转换后的类型为object,且不等于null,那么这个字符串就是JSON格式的字符串。

所以代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function isJSON(str) {
if (typeof str === 'string') {
try {
var obj = JSON.parse(str);
if(typeof obj === 'object' && obj ){
return true;
} else {
return false;
}

} catch(e) {
console.log('error:'+str+'!!!'+e);
return false;
}
}
console.log('It is not a string!')
}

参考资料:https://www.cnblogs.com/lanleiming/p/7096973.html

数字相关

这里经常犯的错误是,\d{0, 7}经常错写成\d{0-7}

非负整数

1
/^\d+$/

正整数

1
/^[1-9]{1}(\d+)?$/g

整数

1
/^-?\d+$/

限制范围的整数

1
2
// 1-100之间的整数
/^([1-9]{1}\d?|100)$/g

数字

1
/^(\.\d+)|(-?\d+(\.\d*)?)$/

限制小数位数

1
/^-?\d+(\.\d{0,7})?$/

字符串相关

大小写字母、数字、下划线、中划线

1
/^[a-zA-Z0-9_-]+$/g

限制字符串长度

1
2
// 只能输入64个字符
/^.{1,64}$/g

js模块系统详解(二)

Posted on 2019-05-06

各个模块系统的语法

CommonJS的语法是require module.exports

ES6的语法是import export export default

NodeJS的语法是require exports module.exports

require import export module.exports混用

通常我们所做的项目,会遇到require import export module.exports混合使用的情况。

比如,我所做的一个项目使用了node,在node层schema.js里导出了一个属性:

1
2
3
4
5
6
7
8
9
10
11
12
// schema.js
exports.rules = {
name: [{
pattern: /\w+/,
message: '支持英文字母、数字、下划线',
},
{
min: 4,
max: 32,
message: '长度范围必须在4~32字符之间',
}]
};

然后,在前端vue文件里引入:

1
2
// test.vue
import { rules } from '/path/to/schema.js';

对于nodejs模块规范来说,使用exports导出,应该使用require引入。而上面这个例子,是import引入的。那么为什么可以这样做?

这里我看到了一篇文章,非常好,https://juejin.im/post/5a2e5f0851882575d42f5609,推荐给大家。下面的内容,也都是抄的这篇文章的。目的是为了加深自己的记忆与理解。

链接的文章主要从这几个问题入手:

  1. 为什么有的地方使用require去引用一个模块时需要加上default?require('xx').default
  2. 经常在各大UI组件引用的文档上会看到说明import { button } from 'xx-ui'这样会引入所有组件内容,需要添加额外的babel配置,比如babel-plugin-component?
  3. 为什么可以使用es6的import去引用commonjs规范定义的模块,或者反过来也可以?
  4. 我们在浏览一些npm下载下来的UI组件模块时(比如说element-ui的lib文件夹下),看到的都是webpack编译好的js文件,可以使用import或require再去引用。但是我们平时编译好的js是无法再被其他模块import的,这是为什么?
  5. babel在模块化的场景中充当了什么角色?以及webpack?哪个起到了关键作用?
  6. 听说es6还有tree-shaking功能,怎么才能使用这个功能?

webpack在模块化中的作用

webpack本身维护了一套模块系统,这套模块系统兼容了所有前端历史进程下的模块规范,包括amd commonjs es6等。下面只对commonjs es6规范进行说明。

模块化的实现其实就在最后编译的文件内。

先看一下这个demo

1
2
3
4
5
6
7
8
9
10
11
// webpack

const path = require('path');

module.exports = {
entry: './main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
}
};
1
2
3
4
5
// main.js
import a from './a';

export default a;
console.log(a);
1
2
3
// a.js

export default 333;
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
// webpack编译后的js

(function(modules) {


function __webpack_require__(moduleId) {
var module = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}

return __webpack_require__(0);
})({
"./client/a.js":
(function (module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(exports, '__esModule', { value: true });
/* harmony default export */ __webpack_exports__["default"] = (333);
}),
"./client/main.js":
(function (module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(exports, '__esModule', { value: true });
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./client/a.js");
/* harmony default export */ __webpack_exports__["default"] = ('main.js');
console.log('a', _a_js__WEBPACK_IMPORTED_MODULE_0__["default"]);
})
});

上面这段js就是使用webpack编译后的代码(经过精简),其中就包含了webpack的运行时代码,就是关于模块的实现。

其实这段js就是一个自执行函数,这个函数的入参是个对象(注意原文可能由于webpack版本问题,写的是数组。本文用的是webpack4),对象的内容包括了所有依赖的模块。

自执行逻辑相信大家都知道。那么最关键的,也是与require、import有关的,是__webpack_require__这个函数。它是require或者import的替代。而__webpack_exports__就是模块的module.exports的引用。比如,入口模块中调用了__webpack_require__(1), 那么就会得到1这个模块的module.exports。

注意上面的例子,我们都是采用es6的规范,如果把引入的方式改成commonjs呢?

1
2
3
4
5
// main.js
let a = require('./a.js');

export default a;
console.log(a);
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
// webpack编译后的js

(function(modules) {


function __webpack_require__(moduleId) {
var module = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}

return __webpack_require__(0);
})({
"./client/a.js":
(function (module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(exports, '__esModule', { value: true });
/* harmony default export */ __webpack_exports__["default"] = (333);
}),
"./client/main.js":
(function (module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(exports, '__esModule', { value: true });
let a = __webpack_require__(/*! ./a.js */ "./client/a.js");
/* harmony default export */ __webpack_exports__["default"] = ('main.js');
console.log('a', a);
})
});

此时,发现编译后的结果少了把a.js转换的过程,

/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./client/a.js");

变成了

let a = __webpack_require__(/*! ./a.js */ "./client/a.js");

这说明,webpack在模块化时如果发现是es6规范,就会通过webpackrequire将其转成webpack规范。

目前这种编译后的js,将入口模块的输出(即module.exports)进行输出没有任何作用,只会作用于当前作用域。这个js并不能被其他模块继续以require或import的方式引用。

babel的作用

webpack的模块化方案是把es6模块化转换成webpack的模块化,但是其余的es6语法还需要做兼容性处理。babel专门用于处理es6转换es5。当然这也包括es6的模块语法的转换。

其实两者的转换思路差不多,区别在于webpack的原生转换 可以多做一步静态分析,使用tree-shaking技术(下面会讲到)

babel能提前将es6的import等模块关键字转换成commonjs的规范。这样webpack就无需再做处理,直接使用webpack运行时定义的webpack_require处理。

这里就解释了问题5。

babel是如何转换es6模块的?

导出模块

es6的导出模块写法有:

1
2
3
4
5
6
7
export default 123;

export const a = 123;

const b = 3;
const c = 4;
export { b, c };

babel会将这些统统转成commonjs的exports:

1
2
3
4
5
exports.default = 123;
exports.a = 123;
exports.b = 3;
exports.c = 4;
exports.__esModule = true;

babel转换es6的模块输出逻辑非常简单,即将所有输出都赋值给exports,并带上一个__esModule表明这是个由es6转换来的commonjs输出。

同样,对于import,也会转成require。

引入default

对于最常见的

1
import a from './a.js';

es6的本意是引入a.js里的default输出,但是转成commonjs后,var a = require('./a.js')得到的是整个对象,不是es6的本意,所以需要对a进行处理。

我们在导出中提到,default输出会赋值给导出对象的default属性。

1
exports.default = 123;

所以babel加了个help _interopRequireDefault函数。

1
2
3
4
5
6
7
8
9
10
function _interopRequireDefault(obj) {
return obj && obj.__esModule
? obj
: { 'default': obj };
}

var _a = require('assert');
var _a2 = _interopRequireDefault(_a);

var a = _a2['default'];

所以这里最后的a变量就是require的值的default属性。如果原先就是commonjs规范,那么a就是那个模块的导出对象。

引入*通配符

es6使用import * as a from './a.js'的本意是将es6模块的所有命名输出以及default输出打包成一个对象赋值给a变量。

而通过var a = require('./a.js')就能实现上述意图。

所以直接返回这个对象。

1
2
3
if (obj && obj.__esModule) {
return obj;
}

如果本来就是commonjs规范,导出时没有default属性,需要添加一个default属性,并把整个模块对象再次赋值给default属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function _interopRequireWildcard(obj) {
if (obj && obj.__esModule) {
return obj;
}
else {
var newObj = {}; // (A)
if (obj != null) {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key))
newObj[key] = obj[key];
}
}
newObj.default = obj;
return newObj;
}
}

所以import { a } from './a.js'直接换成require('./a.js').a即可。

总结

即使我们使用了es6的模块系统,如果借助babel的转换,es6的模块系统最终还是会转换成commonjs的规范。所以,如果我们是使用babel转换es6模块,混合使用es6的模块和commonjs的规范是没有问题的,因为最终都会转换成commonjs。

这里就解释了问题3。

babel5 & babel6

es6的export default都会被转换成exports.default,即使这个模块只有这一个输出。

我们现在再把main.js改一下:

1
2
3
4
5
6
7
8
9
// main.js

import a from './a.js';
let a2 = require('./a.js');

export default 'main.js';

console.log('a', a);
console.log('a2', a2);

会发现,打印出来的内容是:

1
2
333
Module {default: 333, __esModule: true, Symbol(Symbol.toStringTag): "Module"}

会发现,通过require引入es6模块,得到的是整个对象,这时候需要require('./a.js').default得到想要的结果。

这里就解释了问题1。

在babel5时代,大部分人在用require去引用es6输出的default,只是把default输出看做是一个模块的默认输出,所以babel5对这个逻辑做了hack,如果一个es6模块只有一个default输出,那么在转换成commonjs的时候也一起赋值给module.exports,即整个导出对象被赋值了default所对应的值。这样就不需要加default。

但这样做是不符合es6的定义的,在es6的定义里,default只是个名字,没有任何意义。

webpack编译后的js,如何再被其他模块引用

通过配置output.libraryTarget指定构建完的js的用途。

默认var

如果指定了output.library = 'test',入口模块返回的module.exports暴露给全局var test = returned_module_exports

commonjs

如果library: ‘spon-ui’入口模块返回的module.exports赋值给exports['spon-ui']

commonjs2

入口模块返回的module.exports 赋值给module.exports

所以element-ui的构建方式采用commonjs2,导出的组件的js最后都会赋值给module.exports,供其他模块引用。

这就解释了问题4

模块依赖的优化

按需加载的原理

我们在使用各大 UI 组件库时都会被介绍到为了避免引入全部文件,请使用babel-plugin-component等babel 插件。

1
import { Button, Select } from 'element-ui'

由前文可知,import会先转换为commonjs。

1
2
3
var a = require('element-ui');
var Button = a.Button;
var Select = a.Select;

var a = require('element-ui'); 这个过程就会将所有组件都引入进来了。

所以babel-plugin-component就做了一件事,将import { Button, Select } from 'element-ui'转换成了

1
2
import Button from 'element-ui/lib/button'
import Select from 'element-ui/lib/select'

即使转换成了commonjs规范,也只是引入自己这个组件的js,将引入量减少到最低。

这里解释了问题2

tree-shaking

这个就直接看原文吧。

参考链接https://juejin.im/post/5a2e5f0851882575d42f5609

webpack优化项目打包速度、体积等

Posted on 2019-04-26

计划从这几个点来优化

  1. webpack打包速度
  2. 打包体积

打包速度

版本webpack4

速度对比

第一次 第二次 第三次 第四次 第五次 平均
优化前 36911 35917 35934 35493 36468 36144.6
优化后

encodeURI、encodeComponent、decodeURI、decodeURIComponent

Posted on 2019-03-29 | In JS基础知识

最近项目在处理sso问题,需要重定向。这时候用到了encodeURIComponent,突然意识到对这个api不甚了解。特此学习一番。

encodeURI和encodeURIComponent都是对URI进行编码的方法。

区别:

  1. 输入参数不同:encodeURI输入的参数是一个完整的URI,encodeURIComponent的输入参数URI的组成部分。
  2. 对字符的处理不同,这个见下面的图
  3. 使用场景不同

一张图解释四个函数对不同字符的处理

字符分类

当URI里包含一个没在上面列出的字符或有时不想让给定的保留字符有特殊意义,那么必须编码这个字符。字符被转换成UTF-8编码,首先从UTF-16转换成相应的代码点值的替代。然后返回的字节序列转换为一个字符串,每个字节用一个“%xx”形式的转移序列表示。

使用场景的不同

encodeURI不会对&, +, =编码,而这三个在GET和POST请求中是特殊字符,所以如果一个URI要进行HTTP的GET和POST请求,不适合使用encodeURI进行编码,而encodeURIComponent则可以。

举个例子:

一个URI是:http://0.0.0.0?comment=Thyme &time=again,本意是comment是变量,值是Thyme &time=again, 如果不使用encodeURIComponent(或者使用encodeURI编码),服务器得到的是http://0.0.0.0?comment=Thyme%20&time=again,此时服务器会解析为两个键值对,comment=Thyme和time=again。

而如果使用encodeURIComponent对参数进行编码,encodeURIComponent得到的是%3Fcomment%3DThyme%20%26time%3Dagain

注意事项

  1. 为了避免上面说的那种场景,因此,为了避免服务器收到不可预知的请求,对用户输入的URI部分的内容你都需要用encodeURIComponent进行转义。

  2. encodeURI、encodeURIComponent如果编码一个非高-低位完整的代理字符,将会抛出一个URIError错误。比如:

1
2
3
4
5
6
7
8
// 高低位完整
alert(encodeURIComponent('\uD800\uDFFF'));

// 只有高位,将抛出"URIError: malformed URI sequence"
alert(encodeURIComponent('\uD800'));

// 只有低位,将抛出"URIError: malformed URI sequence"
alert(encodeURIComponent('\uDFFF'));

因此,为了防止报错影响程序运行,可以使用try/catch包裹:

1
2
3
4
5
6
7
function encodeURIFixed() {
try {
encodeURIComponent()
} catch() {

}
}

参考链接

MDN-encodeURIComponent

一张图看懂encodeURI、encodeURIComponent、decodeURI、decodeURIComponent的区别

123…8
xixijiang

xixijiang

切莫停下前进的脚步

74 posts
1 categories
12 tags
© 2019 xixijiang
Powered by Hexo
Theme - NexT.Muse